/* ============================================================================
   Sites — section presentation
   ============================================================================

   WHY THIS FILE EXISTS

   The Sites section partials (app/views/sites/shared/*, 63 of them) were forked
   from the marketing site's partials, and their STYLES were left behind in
   marketing.css. That file is loaded by exactly three layouts —
   `marketing`, `marketing_preview` and `signup` — and a Sites page renders under
   `application`. So every bespoke presentation rule the Sites renderer emitted
   resolved to nothing.

   Measured 2026-08-27 on a live authenticated page fetch (business 1): 27
   stylesheets served, 1.39 MB of CSS, and ZERO matches for `.site-mode-*`,
   `.site-lead`, `.site-page-content`, `.site-section`, `.hero`, `.hero-carousel`,
   `.gallery-item`, `.customer-marquee-*` or `.feature-block-img`. That silently
   killed all five visual modes, the hero treatment and the image gallery — all of
   which shipped and were tested.

   The logo marquee is measured DEAD in Sites and deliberately not ported: its
   only consumer is `_customer_grid`, one of 16 partials sitting in sites/shared
   that no SitePage::SECTION_SCHEMAS type can reach (marketing leftovers of the
   original fork). Porting its CSS would have served code nothing renders. `app/views/sites/shared/
   _hero.html.erb` records the same finding in its own comment and defers the
   fix as "a shared-layout change and out of this file's scope".

   WHY A FORK RATHER THAN LOADING marketing.css

   Two independent reasons, either sufficient:

   1. marketing.css cannot be loaded into the app. It carries `:root`, `body`,
      `html`, `h1,h2,h3`, `.btn`, `.navbar` and `.container` rules — 7 blocks
      that would restyle every button and heading in the product and clobber the
      per-tenant theme block the layout injects at <style id="custom-theme">.
   2. The two sets must DIVERGE, not converge. The partials are already forked
      (`marketing/shared/*` vs `sites/shared/*`, nothing outside Sites renders
      `sites/shared`), and Sites pages belong to a tenant, so they follow the
      TENANT's brand while marketing follows MangoApps'. Sharing a stylesheet
      would make every future tenant-branding change a marketing-site risk.

   Nothing was removed from marketing.css — the marketing site is untouched.

   EVERYTHING HERE IS SCOPED UNDER .site-page-content

   Not hygiene — required. `.hero` is a generic class name and 13 views outside
   Sites already use it (EPMS manager dashboard, four Training views, Comms Hub
   blocks, the compensation PDF). An unscoped `.hero` rule would restyle all of
   them. `.site-page-content` is the wrapper emitted by
   apps/sites/pages/show.html.erb and _preview_sections.html.erb, so the scope
   is exactly "inside a rendered Sites page" and cannot leak.

   HOW IT IS LOADED

   Per-view via `content_for :stylesheets` (the layout yields it in <head>), not
   globally — the app layout is already ~50% of every warm request and this
   belongs only on the surfaces that render sections. Every such surface must
   declare it; see the note on SitesHelper#render_site_section.

   WHAT DELIBERATELY DIFFERS FROM THE MARKETING ORIGINAL

   `.hero`'s background. Marketing's is a fixed MangoApps-green gradient with
   white text. Copying that would paint every tenant's hero in OUR brand — the
   same "vendor-branded defaults on tenant intranet pages" defect the 2026-08-22
   /ux-audit fixed elsewhere in this app. And `color: white` over the RAW tenant
   `--brand-primary` (which may be light) is unreadable — which is why, from
   2026-08-27 to 2026-09-04, this was a LIGHT brand-tinted band with the page's
   own text colour and `btn-outline-primary` CTAs. The "Hero band" block below
   records how that was resolved: the band is the brand MIXED toward black,
   never the brand itself, so white text and `btn-outline-light` CTAs are safe
   for any tenant.

   Propshaft memoizes digests at boot: after editing this file, restart the
   container or dev keeps serving the old copy.
   ========================================================================== */


/* ── Hero band ─────────────────────────────────────────────────────────────
   SATURATED, WHITE-TEXT (2026-09-04). This band was a 9% brand tint with the
   page's own text colour — the header comment above still records why: white
   over an arbitrary tenant brand is a contrast gamble, so the dark treatment
   waited "until a brand-aware foreground pair is decided". The pair is now
   decided the way the site shell and the Training heroes decide it: never
   paint the brand itself, paint the brand MIXED 55–82% TOWARD BLACK, which is
   dark enough for white text whatever the brand (a pale yellow brand mixes to
   a deep olive, not to yellow). One technique, three surfaces — the gallery
   band, the site hero, and this section — so a site's pages and its landing
   agree on what "the brand" looks like.

   Two `background` declarations on purpose: the first is the fallback any
   engine understands, the second wins wherever color-mix() is supported
   (Chrome 111+, Safari 16.2+, Firefox 113+). --brand-primary comes from the
   tenant theme block; the literal is the Bootstrap primary, used only when a
   tenant has no brand set. An author-supplied `bg_style` on the section
   (inline style) still wins over both, as it always did.

   SQUARE CORNERS (Sites tester round 7, 2026-10-07 — Melanie: "The Hero
   Banner should use square corners rather than rounded corners to better
   align with the intended design"). The band was the one section with
   rounded corners: every other block that paints a ground
   (`.site-section-bg`) is square, so a hero met the band above or below it
   with a page-coloured notch at each corner. Four rules used to square some
   of those corners again — under a band that followed, edge to edge on the
   reader, beside a side menu — and each missed a case (a band ABOVE the
   hero kept the notch). The band is square everywhere now, on the published
   page and the editor canvas alike, and those rules are gone. The phone twin
   draws a hero as every other phone section, so it is unchanged. */
.site-page-content .hero {
    background: #1f2d3d;
    background: var(--sec-bg, linear-gradient(120deg, color-mix(in srgb, var(--brand-primary, #0d6efd) 55%, #000000), color-mix(in srgb, var(--brand-primary, #0d6efd) 80%, #000000)));
    color: var(--sec-fg, #ffffff);
}

/* Everything the partial writes on the band goes white — including headings.
   `!important` on the headings is not optional: the tenant theme block
   (layouts/_tenant_theme) paints `h1…h6 { color: … !important }` inline, and
   without it the hero's headline rendered in the theme's dark heading colour
   on the dark band (measured: rgb(26,58,82) on #1a3a52-ish, invisible). Links
   that are not buttons underline so they stay findable without a colour of
   their own. The `.badge.bg-light` chip and the `btn-warning` primary CTA
   already carry their own fills. */
.site-page-content .hero h1,
.site-page-content .hero h2,
.site-page-content .hero h3 {
    color: var(--sec-fg, #ffffff) !important;
}

.site-page-content .hero p,
.site-page-content .hero .lead {
    color: var(--sec-fg, #ffffff);
}

.site-page-content .hero a:not(.btn) {
    color: var(--sec-fg, #ffffff);
    text-decoration: underline;
}

/* ── Section background tokens ─────────────────────────────────────────────
   The whole per-section background feature, in five rules. An author picks a
   TOKEN ("Brand", "Soft grey"); Sites::SectionPalette turns it into a
   background/foreground pair; SitesHelper#wrap_site_section_background puts that
   pair on the section wrapper as two custom properties. Custom properties
   INHERIT, which is the trick that keeps this small: `.hero` above reads
   `var(--sec-bg, <its old gradient>)` and needed no new selector of its own, and
   the other 63 section partials need no edit at all.

   Every value --sec-bg can hold comes from that frozen token list, never from a
   request — PageSectionsController validates the key against the palette before
   it is stored, so nothing author-supplied reaches this declaration.

   --on-dark is only added for the tokens that are dark bands. The `!important`
   on the headings is not optional and not new: the tenant theme block
   (layouts/_tenant_theme) paints `h1…h6 { color: … !important }` inline, and
   without it a headline on a dark band renders in the theme's dark heading
   colour and disappears — the same defect measured on the hero in 2026-09-04.
   `.text-muted` needs its own line for the same reason: Bootstrap 5.3 sets it
   with `!important` too. It is NOT dimmed (Sites QA round 5, 2026-09-24): it
   used to drop to 85% opacity, but a dark token's ink is measured to clear AA
   by a hair (4.6:1, BusinessTheme.brand_band_stop), so any dimming falls under
   it — 3.9:1 on an Accent band — and a muted paragraph inside a muted wrapper
   compounded to 72% (4.2:1 even on Brand solid, Feature Rows' description). */
.site-section-bg {
    background: var(--sec-bg);
}

/* A HERO PAINTS ITS OWN BAND (Sites tester round 4, 2026-10-03). `.hero`
   draws `var(--sec-bg)` itself ("Hero band" above), so the wrapper painting
   the same field behind it showed a sliver under the band — on every hero with a background chosen,
   and, once an untouched block draws its type's default ground the way the
   panel shows it, on every hero. The wrapper still carries the variables and
   the ink classes; only its own fill steps aside. */
.site-section-bg:has(> .hero) {
    background: none;
}

.site-section-bg--on-dark {
    color: var(--sec-fg, #ffffff);
}

.site-section-bg--on-dark h1,
.site-section-bg--on-dark h2,
.site-section-bg--on-dark h3,
.site-section-bg--on-dark h4 {
    color: var(--sec-fg, #ffffff) !important;
}

.site-section-bg--on-dark p,
.site-section-bg--on-dark .lead,
.site-section-bg--on-dark small,
.site-section-bg--on-dark .text-muted {
    color: var(--sec-fg, #ffffff) !important;
}

/* Not `.card` / `.list-group-item`: an anchor that IS a card or a list row
   paints its own ground (below). At 0,2,1 this beat the card rule's 0,2,0, so
   Quick Links' white cards drew their titles in the band's white (1:1). */
.site-section-bg--on-dark a:not(.btn, .card, .list-group-item) {
    color: var(--sec-fg, #ffffff);
    text-decoration: underline;
}

/* A SURFACE WITH ITS OWN BACKGROUND OWNS THE INK INSIDE IT. The rules above
   paint every paragraph and heading in the band's foreground, which is right
   for text sitting ON the band and wrong for text sitting on something drawn
   over it. It did not matter while only hero-shaped sections could be dark;
   now that any section can, a resource grid on a dark band is a row of WHITE
   cards whose body copy was being forced white — measured on /pages/231:
   `p` computed rgb(255,255,255) inside `.card.bg-white`.

   Same reasoning as the measure opt-outs for `table` and `.alert` further up:
   a container that paints its own ground decides what is legible on it. The
   `!important` matches the rules it is undoing, and `inherit` rather than a
   literal so a card with its own text colour keeps it.

   The list of surfaces is the census's (Sites QA round 5, 2026-09-24): a list
   row's HEADING was still forced white (Key Documents, Upcoming Events and
   Latest Updates drew white titles on white rows, 1:1), and a light utility
   fill was not a surface at all (On This Page, a Custom HTML `bg-light` box
   and Meet the Team's placeholder portraits, 1.1:1). A row that is itself
   muted (`list-group-item text-muted`) keeps the page's muted grey. */
.site-section-bg--on-dark :is(.card, .alert, .list-group-item, .accordion-item, .bg-white, .bg-light, .bg-body, .bg-body-secondary, .bg-body-tertiary) {
    color: var(--bs-body-color, #212529);
}

.site-section-bg--on-dark :is(.card, .alert, .list-group-item, .accordion-item, .bg-white, .bg-light, .bg-body, .bg-body-secondary, .bg-body-tertiary) :is(h1, h2, h3, h4, p, .lead, small, .text-muted) {
    color: inherit !important;
}

.site-section-bg--on-dark :is(.card, .alert, .list-group-item, .accordion-item, .bg-white, .bg-light, .bg-body, .bg-body-secondary, .bg-body-tertiary):not(.site-item--on-dark).text-muted {
    color: var(--bs-secondary-color, #6c757d) !important;
}

.site-section-bg--on-dark :is(.card, .alert, .list-group-item, .accordion-item, .bg-white, .bg-light, .bg-body, .bg-body-secondary, .bg-body-tertiary) a:not(.btn) {
    color: inherit;
}

/* ── TABLE OF CONTENTS LINKS (owner decision R3, 2026-09-30; Sites QA, Justina) ──
   "On this page" is a list of links and reads as one on every ground. The rule
   above hands a link on a light surface inside a dark band the surface's body
   ink — right for a stray link in a card's prose, wrong for a jump list, whose
   entries went body-black with no hover inside a Brand-solid band while the same
   list was brand-coloured on a plain page. So:
     - the inline list sits on its own light card (`site-toc-card`, `.bg-light`),
       and its links take the BRAND LINK COLOUR on every section background —
       `--bs-link-color`, which the tenant theme (BusinessTheme
       #link_and_validation_tokens → #readable_fg) already deepens until it
       clears 4.5:1 on the platform's light surfaces, `.bg-light`'s #f5f7fb
       among them, so a pale brand (a yellow) reaches the card as a darker shade
       of itself, never as a failing one. In the dark theme the card is dark and
       the variable is the dark theme's link colour. `!important` because the
       rules it overrides differ by ground and theme; the class is this list's
       alone.
     - the sidebar list sits on the section's own ground, so it takes that
       ground's link ink from the rules every link there already follows (the
       page's link colour, `--sec-brand-ink` on a light token, the band's ink on
       a dark one); it only loses the grey it used to force.
   Both: no underline at rest, underlined under the pointer (and darker on the
   card — a sidebar link on a dark band is already the band's white, so it only
   gains the underline: darkening white on a dark ground would cost contrast),
   and a ring plus the underline for the keyboard. */
.site-page-content a.site-toc-link {
    text-decoration: none;
}

.site-page-content .site-toc-card a.site-toc-link {
    color: var(--bs-link-color, #0d6efd) !important;
}

.site-page-content .site-toc-card a.site-toc-link:hover {
    color: var(--bs-link-hover-color, var(--bs-link-color, #0a58ca)) !important;
}

.site-page-content a.site-toc-link:is(:hover, :focus-visible) {
    text-decoration: underline;
    text-underline-offset: 0.2em;
}

.site-page-content a.site-toc-link:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 1px;
    border-radius: 2px;
}

/* The ring sat on the list's own "1." marker: the number is the <ol>'s, drawn
   right against the link box. A hair of space keeps the ring off it. */
.site-page-content ol > li > a.site-toc-link {
    margin-inline-start: 0.2rem;
}

/* ── TABLE OF CONTENTS: COLUMNS AND THE BAR (Sites QA round 3, 2026-10-01,
   owner decision "Columns + optional bar") ──
   COLUMNS. The card's numbered list flows into two columns past six entries
   and three past twelve (the partial picks the count). CSS columns, so the
   list reads top to bottom down each column and the <ol>'s numbering is the
   DOM's. The count is a MAXIMUM: each column keeps at least the width in the
   shorthand, so a narrow block (the editor canvas beside the panel, a small
   window) drops to fewer columns by itself — no breakpoint, which would
   measure the window rather than the block. The gap leaves room for the next
   column's "12." marker, which hangs to the left of its column. */
.site-page-content .site-toc-cols {
    column-gap: 2.5rem;
}

.site-page-content .site-toc-cols-2 {
    columns: 2 16rem;
}

.site-page-content .site-toc-cols-3 {
    columns: 3 13rem;
}

.site-page-content .site-toc-cols > li {
    break-inside: avoid;
}

/* THE BAR. One line of pill links on the section's own ground: no numbers, no
   card. The links take the ground's ink from the rules every link there
   follows (as the sidebar list does, above), and the pill's edge and hover
   fill are mixed from that same ink — so a pill reads on a white page, a
   tinted band and a Brand-solid band alike, in either theme. No underline
   under the pointer (the fill says it); the keyboard keeps the ring and the
   underline above. */
.site-page-content .site-toc-bar-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 1rem;
    row-gap: 0.5rem;
}

.site-page-content .site-toc-bar-heading {
    flex: 0 0 auto;
}

/* The strip scrolls sideways when it does not fit. Its scrollbar is hidden —
   the edge with links behind it fades instead (underline_tabs_scroller_controller
   sets `is-overflowing` and the two fade widths, which track the scroll left in
   each direction) — and its padding keeps each pill's focus ring inside the
   scroll box, which clips anything drawn outside it. */
.site-page-content .site-toc-bar-list {
    display: flex;
    flex: 1 1 18rem;
    min-width: 0;
    gap: 0.5rem;
    padding: 4px;
    margin: -4px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
}

.site-page-content .site-toc-bar-list::-webkit-scrollbar {
    display: none;
}

.site-page-content .site-toc-bar-list.is-overflowing {
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--tabs-fade-start, 0px), #000 calc(100% - var(--tabs-fade-end, 0px)), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, #000 var(--tabs-fade-start, 0px), #000 calc(100% - var(--tabs-fade-end, 0px)), transparent 100%);
}

.site-page-content .site-toc-bar-list > li {
    flex: 0 0 auto;
}

.site-page-content a.site-toc-pill {
    display: inline-flex;
    align-items: center;
    min-height: 2.25rem;
    padding: 0.375rem 0.875rem;
    border: 1px solid color-mix(in srgb, currentColor 40%, transparent);
    border-radius: 999px;
    font-size: 0.9375rem;
    line-height: 1.25;
    white-space: nowrap;
}

.site-page-content a.site-toc-pill:hover {
    text-decoration: none;
    background-color: color-mix(in srgb, currentColor 10%, transparent);
    border-color: currentColor;
}

/* ── TABLE OF CONTENTS: THE LIST FOLLOWS ALIGNMENT (Sites tester round 4,
   2026-10-02: "add option to center align") ──
   The block takes Alignment now (SitePage::SECTION_LAYOUT_PRESET_KEYS), as a
   heading-only preset — `site-align-heading-*` on the wrapper — because the
   whole-block classes re-set every line of text, and a numbered list centred
   line by line reads as a ragged column of links. What moves is the list as
   one piece:
     - the CARD shrinks to its entries (columns included: the count is a
       maximum, each column keeps its minimum width) and moves as one box, its
       label and its numbered list still reading down its left edge;
     - the BAR's links move along their line, the label with them. The list
       stops growing to fill the row, so the label and the links are one group
       the row places; a strip too long for the row still wraps under its label
       and scrolls sideways from its first link — `safe` keeps an overflowing
       row at the start, never cut off on both sides;
     - a held "Plain list" (retired, still drawn) sets its lines with it.
   Untouched (Left) every style is exactly what it was. */
.site-page-content :is(.site-align-heading-center, .site-align-heading-end) .site-toc-card {
    width: fit-content;
    max-width: 100%;
}
.site-page-content .site-align-heading-center .site-toc-card { margin-inline: auto; }
.site-page-content .site-align-heading-end .site-toc-card { margin-inline: auto 0; }
.site-page-content :is(.site-align-heading-center, .site-align-heading-end) .site-toc-bar-list {
    flex: 0 1 auto;
}
.site-page-content .site-align-heading-center .site-toc-bar-row {
    justify-content: center;
    justify-content: safe center;
}
.site-page-content .site-align-heading-end .site-toc-bar-row {
    justify-content: flex-end;
    justify-content: safe flex-end;
}
.site-page-content .site-align-heading-center .site-toc-plain { text-align: center; }
.site-page-content .site-align-heading-end .site-toc-plain { text-align: end; }

/* ── EVERY MARK ON A DARK GROUND FOLLOWS THE INK (Sites QA round 5, 2026-09-24) ──
   "When the background of a block changes, the copy adjusts its colour — the
   bullet points (check marks) should follow the same behaviour" (R03, Feature
   Rows on Brand solid: white text, green checks on a green band, gone).

   The rules above hand the band's ink to headings, paragraphs, muted text and
   links, and stop there. Everything else that carries a colour of its own kept
   it, and on a dark band a colour chosen for a white page is the one thing
   guaranteed not to read: a census of every block on every dark token (page
   "QA5 Contrast Census", site 467) measured check marks at 1.1–2.9:1, brand
   icons and brand outline buttons at 1.3:1 (the brand on the brand mixed toward
   black), and `text-warning-emphasis` alert icons at 1.1:1.

   So on a dark ground a mark or a word painted with a COLOUR UTILITY
   (`text-success`, `text-primary`, `text-*-emphasis` …) takes the band's ink,
   exactly as the text beside it does — the phone twin has done this for icons
   since 2026-09-23 (mobile_tailwind.css, "Icons on a dark token take its ink
   too"). The palette's dark tokens are measured so that ink clears AA (4.6:1,
   BusinessTheme.brand_band_stop), so a mark in the ink clears the 3:1 a
   graphic needs on every tenant's brand, which no fixed semantic green can.
   A glyph with no colour utility is left to inherit: on the band that is
   already the ink, and inside a chip styled by its own stylesheet (Stats
   Bar's change chip) it is the chip's ink, which a blanket `i` rule would
   have painted white on the chip's pale fill.

   NOT inside something that paints its own ground — a card, an alert, a list
   row, a light utility fill (`bg-*`), a badge, a button, a tab, a picker, an
   element with an inline background. Those keep the colours they were
   designed with, because their ground is not the band. The one exception is a
   dark ITEM (SitesHelper#site_item_surface, `.site-item--on-dark`): it paints
   its own ground and IS a dark band, so it is the context, not an exemption.
   The exemption is scoped INSIDE the dark section (`.site-section-bg--on-dark
   … *`), so a card or fill above the section on the page cannot switch it
   off. */
.site-section-bg--on-dark :is(.text-primary, .text-secondary, .text-success, .text-info, .text-warning, .text-danger, .text-dark, .text-black, .text-body, .text-body-secondary, .text-body-emphasis, .text-primary-emphasis, .text-secondary-emphasis, .text-success-emphasis, .text-info-emphasis, .text-warning-emphasis, .text-danger-emphasis, .text-dark-emphasis, .text-white-50, .text-black-50):not(.btn, .badge, .nav-link, [class^="bg-"], [class*=" bg-"]):not(.site-section-bg--on-dark :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .btn, .badge, .nav-link, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.site-item--on-dark) *) {
    color: var(--sec-fg, #ffffff) !important;
}

/* An OUTLINE button on the band draws its label and border in the ink; its
   brand label measured 1.3:1 on a brand band, and even `btn-outline-light`'s
   off-white label is under AA on an Accent band (4.35:1). `!important` because the tenant
   theme (layouts/_tenant_theme) sets `--bs-btn-color … !important` on every
   `.btn-outline-primary` / `.btn-primary`, which is also why the item-level
   button rules below never took effect until they carried it too (measured
   2026-09-24: `--bs-btn-color` stayed #b84e1f inside a brand card). */
.site-section-bg--on-dark :is(.btn-outline-primary, .btn-outline-secondary, .btn-outline-success, .btn-outline-info, .btn-outline-warning, .btn-outline-danger, .btn-outline-dark, .btn-outline-light):not(.site-section-bg--on-dark :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.site-item--on-dark) *) {
    --bs-btn-color: var(--sec-fg, #ffffff) !important; --bs-btn-border-color: var(--sec-fg, #ffffff) !important;
    --bs-btn-hover-bg: var(--sec-fg, #ffffff) !important; --bs-btn-hover-border-color: var(--sec-fg, #ffffff) !important; --bs-btn-hover-color: #212529 !important;
    --bs-btn-active-bg: var(--sec-fg, #ffffff) !important; --bs-btn-active-border-color: var(--sec-fg, #ffffff) !important; --bs-btn-active-color: #212529 !important;
}

/* A BRAND button on the band is the brand on the brand mixed toward black: its
   label reads, its edge does not (1.3:1 — a word floating on the band, not a
   button). It takes the ink as its fill and the page's dark ink as its label,
   the treatment a dark card's button (below) and a dark band's selected tab
   (Tabbed Content, further down) already get.

   The fill, border and label are set as PROPERTIES, not only through
   Bootstrap's variables: the tenant theme paints `.btn-primary` with
   `background-color … !important` and `color … !important` directly, so the
   variables alone never reached the button (measured 2026-09-24: a brand card's
   "Learn more" stayed #b84e1f with a white label). Hover darkens the ink a
   step so the button still answers the pointer. */
.site-section-bg--on-dark .btn-primary:not(.site-section-bg--on-dark :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.site-item--on-dark) *),
.site-page-content .site-item--on-dark .btn-primary {
    background-color: var(--sec-fg, #ffffff) !important;
    border-color: var(--sec-fg, #ffffff) !important;
    color: #212529 !important;
}
.site-section-bg--on-dark .btn-primary:is(:hover, :focus-visible, :active):not(.site-section-bg--on-dark :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.site-item--on-dark) *),
.site-page-content .site-item--on-dark .btn-primary:is(:hover, :focus-visible, :active) {
    background-color: color-mix(in srgb, var(--sec-fg, #ffffff) 85%, #000000) !important;
    border-color: color-mix(in srgb, var(--sec-fg, #ffffff) 85%, #000000) !important;
}

/* A TABLE on the band is transparent (AdminKit resolves `--bs-table-bg` to
   transparent), so its cells sit on the band — in the theme's dark table ink:
   Office Hours and Plan Comparison measured 1.2–2.0:1. Its cells take the ink;
   the header row keeps the fill and ink SitesHelper#site_table_header_style
   paints on it, and a contextual row (`.table-primary`) sets its own. */
.site-section-bg--on-dark .table:not(.site-section-bg--on-dark :is(.card, .alert, .list-group-item, [class^="bg-"], [class*=" bg-"]):not(.site-item--on-dark) *) {
    --bs-table-color: var(--sec-fg, #ffffff);
    --bs-table-striped-color: var(--sec-fg, #ffffff);
    --bs-table-hover-color: var(--sec-fg, #ffffff);
    --bs-table-active-color: var(--sec-fg, #ffffff);
    --bs-border-color: color-mix(in srgb, var(--sec-fg, #ffffff) 30%, transparent);
}

/* An ALERT keeps a light ground of its own. The tenant theme paints alerts as a
   15% wash (`.alert-danger { background-color: rgba(…, .15) !important }`) with
   black text — a wash of the BAND on a dark band: announcements and the status
   banner measured 1.5–3.9:1. `--bs-alert-bg` is the opaque tint the same theme
   defines, so the alert looks exactly as it does on a white page. */
.site-section-bg--on-dark .alert:not(.site-item--on-dark) {
    background-color: var(--bs-alert-bg, var(--bs-body-bg, #ffffff)) !important;
}

/* A dimmed line under a heading (`opacity-75` / `-90` on the hero's, the CTA
   banner's and the callout's support copy) is full ink on a dark ground: the
   ink is measured at the AA floor, so 75% of it is under the floor on every
   brand — 2.9:1 on an Accent band. The hero and the band sections also paint a
   dark default band when no token is chosen (`site_default_band_style`), so
   they are named directly. Text only — a decorative glyph keeps its opacity. */
.site-page-content :is(.site-section-bg--on-dark, .hero:not(.site-section-bg:not(.site-section-bg--on-dark) *), section[style*="background"]:not(.site-section-bg:not(.site-section-bg--on-dark) *)) :is(div, p, span):is(.opacity-50, .opacity-75, .opacity-90) {
    opacity: 1 !important;
}

/* THE PICTURE'S BOX IS THE PICTURE (Sites QA, 2026-09-30 — "rounded in the
   editor, square on the published page"). custom.css makes every `.row >
   [class*="col-"]` a column flexbox, so the hero's picture is a flex item and
   was STRETCHED to the column's width; a tall picture then hit the 520px cap
   and `object-fit: contain` drew it narrower than its own box, centred in
   transparent side strips. The box's rounded corners fell in those strips and
   the picture's own corners were square — measured on a 1000×1400 picture:
   box 463×520 around a 371×520 picture on the page, 547×520 on the full-width
   reader. The editor's narrower column left strips smaller than the radius,
   so the corners half-showed there. `align-self: center` sizes the box to the
   picture (371×520 on every surface) and keeps it centred, so the radius and
   the shadow land on the picture wherever it is drawn. A picture smaller than
   the column is drawn at its own size, as the phone twin draws it, rather than
   blown up.

   ONE CLASS FOR IT, `site-contained-img` (Sites tester round 4, 2026-10-02, R8:
   "CTA Banner image has square corners"). Feature Rows' picture took the same
   fix. A picture drawn whole inside a capped box in a column carries the
   class; its own class keeps only its height cap (`.hero-image`,
   `.feature-block-img`). The CTA Banner's picture is drawn to fill its column
   now (below). */
.site-page-content .site-contained-img {
    object-fit: contain;
    align-self: center;
}

.site-page-content .hero-image {
    max-height: 520px;
    filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.15));
}

/* THE PICTURE BESIDE THE WORDS IS AS TALL AS THE WORDS (Sites tester round 6,
   2026-10-06 — Justina, CTA Banner: "have image match height of however much
   copy is in the block … the image is big in file size, so it's odd that it is
   appearing so small"). The section's one picture beside its text column — CTA
   Banner, Solution Overview, Security Badges, Data Insights — was drawn whole
   at its own size, capped (350px tall on the banner), so a portrait photo was
   a narrow strip and a small file a stamp beside a tall column of words.

   Beside the words (the block's two-column width, the `lg` tier of "BLOCK-WIDTH
   BREAKPOINTS"), the picture fills its column at the height of the words,
   cropped to fit (`cover`) with its corners kept. It is taken out of the row's
   flow so the WORDS set the row's height; the column keeps a minimum so a
   one-line banner still shows a picture, and the words sit in the middle of
   that height. Stacked (a narrow block, the phone), it is the column's width
   at a fixed 4:3, the shape the phone twins give it.

   Not the Hero's pictures or Feature Rows': those are shown whole on purpose —
   a product screenshot or an illustration a crop would cut — and the hero's
   rotating pictures share one frame sized to the tallest ("Hero image
   carousel" below). Not Heading + Image: its picture sits under the words, not
   beside them. */
.site-page-content .site-split-picture {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
    object-fit: cover;
}
@container site-cq (min-width: 720px) {
    /* `!important`: the row's `align-items-center` utility is. */
    .site-page-content .site-split-row { align-items: stretch !important; }
    .site-page-content .site-split-row > :not(.site-split-picture-col) { justify-content: center; }
    .site-page-content .site-split-row > .site-split-picture-col {
        position: relative;
        min-height: 15rem;
    }
    /* Inset by the column's own gutter padding, so the picture lines up with
       the column's content edge as the words beside it do. */
    .site-page-content .site-split-row > .site-split-picture-col > .site-split-picture {
        position: absolute;
        inset: 0 calc(var(--bs-gutter-x) * 0.5);
        width: calc(100% - var(--bs-gutter-x));
        height: 100%;
        aspect-ratio: auto;
    }
}

/* ── Hero image carousel — crossfade rotation ─────────────────────────────
   ONE STEADY FRAME, SIZED TO THE TALLEST PICTURE (Sites QA round 4,
   2026-09-23). Every slide sits in the SAME grid cell, so the cell is as tall
   as the tallest picture whichever one is showing, and each picture is centred
   in it at its own shape — never cropped, never stretched: img-fluid and
   .hero-image cap it at the column's width and 520px with both dimensions
   auto, which keeps its aspect. Only .active is visible.

   This replaced an absolute stack that left only the .active slide in flow, so
   the band re-measured itself on every rotation and everything below the hero
   jumped with it — 50 to 207px on the QA bench (/pages/2924: a landscape
   photo, a portrait and a UI screenshot). A slide that has not loaded yet adds
   nothing to the cell, so the frame can grow while the pictures arrive, never
   as they turn. The visible slide sits on top so a click or long-press lands
   on the picture the reader is looking at. */
.site-page-content .hero-carousel {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    place-items: center;
    min-height: 280px;
}

.site-page-content .hero-carousel-img {
    grid-area: 1 / 1;
    opacity: 0;
    transition: opacity 0.8s ease-in-out;
}

.site-page-content .hero-carousel-img.active {
    z-index: 1;
    opacity: 1;
}

/* Under reduced motion the hero's carousel starts PAUSED (modules/site_carousel)
   and turns only when the reader picks a dot or presses play, so the frame is
   sized to the one picture on show rather than reserving room for pictures
   that may never appear. They stay aria-hidden in the markup either way. */
@media (prefers-reduced-motion: reduce) {
    .site-page-content .hero-carousel-img:not(.active) {
        display: none;
    }
}

/* CAROUSEL CONTROLS (sites/shared/_carousel_controls — the Hero Banner's
   images and Testimonials, one partial and one controller module; Sites tester
   round 4, R7). A pause toggle and a dot per slide, in the block's own ink
   (`currentColor`), so they read on every ground the author picks — a band's
   white, a tint's dark ink. Each control is a 24px target (WCAG 2.5.8); the
   dot drawn inside it is 10px, a ring, filled for the slide on show
   (`aria-current`, which the controller moves). The focus ring is the ink too. */
.site-page-content .site-carousel-controls {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 0.25rem;
    margin-top: 0.75rem;
}
/* The controls follow the block's Alignment (/builder-audit 2026-10-03):
   under Left or Right a Testimonials slide moves as one piece — the quote, the
   byline, the button — and the row of dots under it stayed in the middle. It
   sits in the quote's own column, so it starts or ends where the quote does.
   The Hero Banner offers no Alignment, so its row stays centred. */
.site-page-content .site-align-start .site-carousel-controls { justify-content: flex-start; }
.site-page-content .site-align-end .site-carousel-controls { justify-content: flex-end; }
.site-page-content .site-carousel-controls > button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font-size: 0.75rem;
    line-height: 1;
    cursor: pointer;
}
.site-page-content .site-carousel-controls > button:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 1px;
}
.site-page-content .site-carousel-dot::before {
    content: "";
    box-sizing: border-box;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 2px solid currentColor;
    background: transparent;
    transition: background-color 0.2s ease;
}
.site-page-content .site-carousel-dot[aria-current="true"]::before {
    background: currentColor;
}
@media (prefers-reduced-motion: reduce) {
    .site-page-content .site-carousel-dot::before { transition: none; }
}

/* ── Feature block images ────────────────────────────────────────────────── */
/* `site-contained-img` for the hero picture's reason (".hero-image" above): the
   picture is a flex item of its column, and stretched to the column's width a
   tall one was drawn inside transparent side strips with square corners. */
.site-page-content .feature-block-img {
    max-height: 450px;
    width: auto;
    max-width: 100%;
}

/* ── Tabbed content / image gallery ──────────────────────────────────────
   THE ROWS ARE WASHES OF THE GROUND'S OWN INK (Sites QA round 5, 2026-09-24,
   swept with R18). An unselected label was `var(--bs-secondary)`, which the
   app maps to the tenant's SECONDARY BRAND colour, not Bootstrap's grey:
   measured on /pages/2048, orange #f7931e on the #fff8f0 page, 2.18:1 — and
   on a dark band the chosen row was a light fill whose description the
   on-dark rules force white, while the others were grey on the band. The
   label now takes the section's ink (`--sec-fg`, or the page's body colour
   with no background) and the rows mark hover and choice with a wash of it,
   the chosen row also bold and aria-current — the same cues as before, on
   every ground: light, a dark band, the dark theme. The partial's own style
   block carries the old values for a page without this file; these win
   inside .site-page-content by specificity. */
.site-page-content .gallery-preview {
    transition: opacity 0.2s ease;
}

.site-page-content .gallery-item {
    --gallery-ink: var(--sec-fg, var(--bs-body-color, #212529));
    transition: background-color 0.15s ease;
    cursor: pointer;
}

.site-page-content .gallery-item:hover {
    background-color: color-mix(in srgb, var(--gallery-ink) 6%, transparent);
}

.site-page-content .gallery-item-active {
    background-color: color-mix(in srgb, var(--gallery-ink) 12%, transparent) !important;
}

.site-page-content .gallery-item-active span {
    color: var(--gallery-ink) !important;
    font-weight: 600 !important;
}

.site-page-content .gallery-item span {
    color: var(--gallery-ink);
}

/* ── Tabbed Content: the tab strip on any ground ─────────────────────────
   (Sites QA round 5, 2026-09-24 — R18, R20, R21. Everything from here to the
   matching "end" line belongs to sites/shared/_tabbed_content.)

   R18. THE STRIP KEEPS THE BACKGROUND PANEL'S PROMISE — "Text colour changes
   automatically for readability". Bootstrap paints an unselected pill in the
   link colour and the selected one in the raw brand, which on a Brand band is
   brand on brand-mixed-toward-black: measured on /pages/10508 (Brand solid),
   unselected titles 1.29:1 and the selected pill 1.29:1 against the band.

   No new colours are chosen here. The strip takes the pair the section
   already carries — `--sec-fg` / `--sec-bg` from Sites::SectionPalette via
   SitesHelper#wrap_site_section_background, or the page's own ink and ground
   when the section has no background — and uses it three ways:
     - an unselected title is the section's INK (what its body text is);
     - the selected pill is FILLED: on a light ground with the Brand solid
       colour the palette deepened until white on it clears AA
       (`--site-tab-brand`, set by the partial), so the pill stays the brand
       and is at least that far from a white ground; on a dark band with the
       band's ink, carrying the page's dark ink — the treatment a brand
       button already gets on a dark card (`.site-item--on-dark .btn-primary`);
     - hover is a wash of the ink and the focus ring is the ink, so both read
       wherever the titles do (Bootstrap's ring is the brand at 25% — nothing
       on a brand band).
   Where the brand fill was never measured against the ground — the app's
   dark theme, and an author's custom light colour (`site-tabs--ink`, decided
   by the partial) — the selected pill takes the ink and the ground swaps
   into its text: the section's own measured pair, reversed. */
.site-page-content .site-tabs {
    --site-tab-ink: var(--sec-fg, var(--bs-body-color, #212529));
    --site-tab-fill: var(--site-tab-brand, var(--site-tab-ink));
    --site-tab-on-fill: #ffffff;
}

:root[data-theme="dark"] .site-page-content .site-tabs,
.site-page-content .site-tabs.site-tabs--ink {
    --site-tab-fill: var(--site-tab-ink);
    --site-tab-on-fill: var(--sec-bg, var(--bs-body-bg, #ffffff));
}

/* `:root` only lifts this to the dark-theme rule's weight, so on a dark band
   it wins in both themes by coming later. */
:root .site-page-content .site-section-bg--on-dark .site-tabs {
    --site-tab-fill: var(--sec-fg, #ffffff);
    --site-tab-on-fill: #212529;
}

.site-page-content .site-tabs .nav-link {
    color: var(--site-tab-ink);
}

.site-page-content .site-tabs .nav-link:hover:not(.active) {
    color: var(--site-tab-ink);
    background-color: color-mix(in srgb, var(--site-tab-ink) 12%, transparent);
}

.site-page-content .site-tabs .nav-link.active {
    color: var(--site-tab-on-fill);
    background-color: var(--site-tab-fill);
    font-weight: 600;
}

.site-page-content .site-tabs .nav-link:focus-visible {
    outline: 2px solid var(--site-tab-ink);
    outline-offset: 2px;
    box-shadow: none;
}

/* The pills sit together wherever the strip is justified. Each tab is an
   <li>, and the Centred and Right presets give every li an auto inline
   margin (their measure-box rules, "CENTRING THE MEASURE BOX"), which in a
   flex row spreads the items: measured before, four pills strung ~150px
   apart across the whole 860px strip. */
.site-page-content .site-tabs .nav-item {
    margin-inline: 0;
    max-width: none;
}

/* R21. A TAB'S TEXT RUNS THE WIDTH OF ITS PANEL ("too much blank space on the
   right"). The reading measure stopped it at 634px of an 860px panel with
   nothing beside it; a column only narrows for a picture, and the partial
   already does that per item. Lifted the way `text_width: full` lifts it for
   Rich Text (see "THE AUTHOR LIFTED THE MEASURE"); 0,3,0 beats the measure's
   0,1,1. */
.site-page-content .site-tab-body :is(p, li, blockquote, .lead, h3) {
    max-width: none;
}

/* WORDS BESIDE A PICTURE READ FROM THEIR START EDGE (Sites tester round 9,
   2026-10-10 — Melanie: "when an image is added, align the title and
   description to the left; when no image is added, keep the copy centred").
   A Centred or Right preset moves the heading, the strip and a tab's text that
   stands alone; a column of words BESIDE a picture keeps its start edge, the
   one that meets the picture's side of the row — as a CTA Banner's and a
   Solution Overview's words beside their picture are never centred (round 4,
   `split_media`). Only while the picture is beside them: below
   the block's lg tier the picture stacks above and the words follow the preset
   again, as on the phone, where they always stack.
   Tabbed Content alone: a tab with a picture and one without share one preset
   here, so the preset cannot be a choice about the words beside a picture.
   Feature Rows and Platform Pillars, whose every row has one, keep following
   it (round 4, R12). 0,3,1 beats the presets' `.site-align-* .text-start`
   (0,3,0) and their measure-box margins (0,2,1). */
@container site-cq (min-width: 720px) {
  .site-page-content .site-cq .site-beside-picture,
  .site-page-content .site-cq .site-beside-picture :is(p, li, h3, h4, .text-start) {
      text-align: start !important;
      margin-inline: 0;
  }
}

/* R20. SEVERAL LINES ARE A CHECKLIST — list spacing (the li's own mb-2) and
   Feature Rows' check mark. Left-set, a line that wraps hangs clear of its
   mark (the li is a flex row). Under a Centred or Right preset each line is
   set with its mark in the text flow, as Feature Rows sets it, so the mark
   stays beside the words it ticks: in a flex row the mark stayed at the
   panel's edge while a wrapped line centred away from it. The body follows
   the heading and the strip, as round 4 made a Centred tab body do. */
.site-page-content :is(.site-align-center, .site-align-end) .site-tab-list li { display: block !important; }
.site-page-content :is(.site-align-center, .site-align-end) .site-tab-list li > .fa-check-circle { margin-block: 0 !important; margin-inline: 0 .5rem !important; }
.site-page-content .site-tab-list li:last-child { margin-bottom: 0 !important; }

/* THE REST OF THE BLOCK ON A DARK BAND — swept with R18, because a Brand
   background reaches everything in the panel, not only the strip.
     - The check mark is `text-success` (an !important utility), a green that
       is gone on a dark green band; on a dark band it takes the band's ink,
       like the text beside it.
     - (The picture gallery's rows are washes of the ink on every ground —
       see "Tabbed content / image gallery" above.)
     - A solution card paints its own light fill (`bg-light`), so — like
       `.card` in the on-dark block above — it owns the ink inside it. */
.site-page-content .site-section-bg--on-dark .site-tab-body .fa-check-circle.text-success {
    color: var(--sec-fg, #ffffff) !important;
}

.site-page-content .site-section-bg--on-dark .site-tab-body .bg-light {
    color: var(--bs-body-color, #212529);
}

.site-page-content .site-section-bg--on-dark .site-tab-body .bg-light :is(h4, p, small, .text-muted) {
    color: inherit !important;
    opacity: 1;
}
/* ── end Tabbed Content ─────────────────────────────────────────────────── */

/* ── Lead paragraph ──────────────────────────────────────────────────────
   marketing.css hard-codes #6c757d here; this reads the tenant theme's own
   muted token and falls back to the same value. */
.site-page-content .site-lead {
    font-size: 1.15rem;
    line-height: 1.6;
    color: var(--text-muted, #6c757d);
    font-weight: 400;
}

/* ── Visual modes ────────────────────────────────────────────────────────
   The page's density, applied by cascade from the .site-mode-* class that
   show.html.erb puts on the .site-page-content wrapper. Both selectors are
   needed: `.site-section` for partials that set it, and `> div > section` for
   the wrapper div that show.html.erb emits per section (which carries the
   anchor id and data-section-index).

   These five rules ARE the visual-modes feature. Until this file was served,
   switching mode changed nothing a reader could see.

   WHY `!important` ON THE PADDING (and only there)

   Serving this file was necessary but not sufficient: every section partial
   carries a Bootstrap `py-5`/`py-4` spacing utility, and Bootstrap generates its
   spacing utilities AS `!important`
   (`.py-5{padding-bottom:3rem!important;padding-top:3rem!important}`). So no
   amount of selector specificity can win — measured in a headed browser after
   the stylesheet was already being served, all five modes still computed an
   identical 52.8px top and bottom. `executive`'s max-width applied, because
   nothing sets max-width as a utility; the padding did not.

   This is safe rather than a specificity war, because the utility is a PARTIAL
   DEFAULT and not an author's choice: `spacing` appears zero times in
   SitePage::SECTION_SCHEMAS, is not editable anywhere in the section editor, and
   is set by `spacing ||= 'py-5'` in exactly 5 of the 63 partials. Visual mode is
   explicit user intent; the utility is a fallback. Intent wins.

   The cleaner long-term shape is a spacing custom property — partials would
   apply `padding-block: var(--site-section-pad)` and each mode would set the
   variable, leaving genuine per-section overrides free to win. That is a change
   to the partials rather than to this stylesheet, so it is not made here. */
/* `> div > .site-section-wrap > section` as well: a section with a chosen
   background (or a heading size / alignment) is wrapped in one more div by
   SitesHelper#wrap_site_section_background, and the direct-child selector
   stopped matching it — so exactly the sections an author had styled fell out
   of the mode's rhythm and got the partial's own padding instead. One of the
   "all the different padding sizes" reported on 2026-09-21. */
.site-mode-utility .site-section,
.site-mode-utility > div > section,
.site-mode-utility > div > .site-section-wrap > section {
    padding-top: 1.5rem !important;
    padding-bottom: 1.5rem !important;
}

.site-mode-balanced .site-section,
.site-mode-balanced > div > section,
.site-mode-balanced > div > .site-section-wrap > section {
    padding-top: 3rem !important;
    padding-bottom: 3rem !important;
}

.site-mode-editorial .site-section,
.site-mode-editorial > div > section,
.site-mode-editorial > div > .site-section-wrap > section {
    padding-top: 4rem !important;
    padding-bottom: 4rem !important;
}

.site-mode-executive .site-section,
.site-mode-executive > div > section,
.site-mode-executive > div > .site-section-wrap > section {
    padding-top: 4rem !important;
    padding-bottom: 4rem !important;
}

/* EXECUTIVE'S NARROW COLUMN IS THE WORDS', NOT THE BAND'S (Console #7362,
   2026-10-08). The 800px cap sat on the SECTION, so whatever a section paints
   itself stopped at 800px — a Hero became an 800px box and a Single Image or
   Heading + Image set to Full width was 800px wide — while a chosen
   background, painted by the wrapper outside the section, ran the whole
   column: on one page, bands of two widths. The cap is on the content now:
   every `.container` in the page, at any depth (the dispatcher's own band
   wraps some blocks in a second section, SitesHelper#ensure_section_band), so
   the words keep the same 800px measure they had and every band — a hero's,
   a background's, a Full width picture — spans the column alike. A census of
   all 52 block types in executive mode found one block with words outside a
   container: the Stats Bar's heading, which it now draws in one. */
.site-mode-executive .container {
    max-width: 800px;
}

.site-mode-campaign .site-section,
.site-mode-campaign > div > section,
.site-mode-campaign > div > .site-section-wrap > section {
    padding-top: 3rem !important;
    padding-bottom: 3rem !important;
}

/* ── A SECTION THAT MEETS ITS NEIGHBOURS: `site-section-flush` ─────────────
   2026-10-07, Sites tester round 7 (Melanie, the Single Image block): "no
   additional padding or spacing above or below the image, so it can sit
   seamlessly within the page layout". The five mode rules above pad EVERY
   section with !important, so a block that asks for no padding is the one
   deliberate exception: its section carries `site-section-flush`
   (sites/shared/_single_image) and these selectors out-rank each mode
   selector shape — the bare section, a section inside the per-section
   wrapper, and one inside the background / preset wrapper too — by one
   class, so the order of the rules is not what decides it. Nothing else pads
   a section: the per-section wrappers (`[data-section-index]`, the editor's
   `.site-section-preview`, `.site-section-wrap`) carry no margin or padding,
   so the picture meets the block above and the block below on the published
   page, the editor canvas and the gallery alike. The phone twin is
   mobile_tailwind.css `m-site-flush`. */
.site-page-content section.site-section-flush,
.site-page-content > div > section.site-section-flush,
.site-page-content > div > .site-section-wrap > section.site-section-flush {
    padding-top: 0 !important;
    padding-bottom: 0 !important;
}

/* A CARD OR ROW ON A DARK TOKEN (SitesHelper#site_item_surface): the section
   on-dark rules carry its headings, muted text, links and marks; these carry
   the brand button, which would otherwise sit brand-on-brand-mixed-black.
   Scoped to the ITEM class, never the section class: a white card on a dark
   section keeps its brand button.

   `!important` on the variables (Sites QA round 5, 2026-09-24): the tenant
   theme sets `--bs-btn-color … !important` on `.btn-primary` and
   `.btn-outline-primary`, so without it these never applied — measured on a
   brand card: the outline button's label stayed the raw brand, 1.3:1. */
.site-page-content .site-item--on-dark .text-primary { color: var(--sec-fg, #ffffff) !important; }
.site-page-content .site-item--on-dark .btn-primary {
    --bs-btn-bg: var(--sec-fg, #ffffff) !important; --bs-btn-border-color: var(--sec-fg, #ffffff) !important; --bs-btn-color: #212529 !important;
    --bs-btn-hover-bg: var(--sec-fg, #ffffff) !important; --bs-btn-hover-border-color: var(--sec-fg, #ffffff) !important; --bs-btn-hover-color: #212529 !important;
    --bs-btn-active-bg: var(--sec-fg, #ffffff) !important; --bs-btn-active-border-color: var(--sec-fg, #ffffff) !important; --bs-btn-active-color: #212529 !important;
}
.site-page-content .site-item--on-dark .btn-outline-primary {
    --bs-btn-color: var(--sec-fg, #ffffff) !important; --bs-btn-border-color: var(--sec-fg, #ffffff) !important;
    --bs-btn-hover-bg: var(--sec-fg, #ffffff) !important; --bs-btn-hover-border-color: var(--sec-fg, #ffffff) !important; --bs-btn-hover-color: #212529 !important;
    --bs-btn-active-bg: var(--sec-fg, #ffffff) !important; --bs-btn-active-border-color: var(--sec-fg, #ffffff) !important; --bs-btn-active-color: #212529 !important;
}

/* ── One type scale for a section: heading → subheading → body ─────────
   Every section partial opens with an <h2>, and a census on 2026-09-21 found
   them in twelve different dress: display-5, display-6, h3, h4, bare
   fw-bold. Two sections side by side on one page read as two different
   products ("too many different font sizes", reported twice). They all carry
   `site-section-heading` now and take their size from here; the hero keeps
   its own display size, because it is the page's banner and is meant to be
   bigger than everything under it.

   The author's per-section presets ride as classes on the section's wrapper
   (SitesHelper#wrap_site_section_background): `site-heading-sm` /
   `site-heading-lg` step the heading, `site-align-start` /
   `site-align-center` set the text alignment — with `!important`, because
   Bootstrap's own `.text-center` / `.text-start` utilities are `!important`
   and a partial's default must lose to the author's choice. */
.site-page-content h2.site-section-heading {
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.2;
    margin-bottom: 1rem;
}
.site-page-content .site-heading-sm h2.site-section-heading { font-size: 1.5rem; }
.site-page-content .site-heading-lg h2.site-section-heading { font-size: 2.75rem; }

/* THE SUBHEADING STEP — and why it is a class here rather than `fs-5` in each
   partial (2026-09-22, QA report R1 "the heading / subheading / body type
   scale is inconsistent and in places inverted").

   `fs-5` DOES NOT MEAN "one step up" IN THIS APP. AdminKit resets Bootstrap's
   heading scale to application-chrome sizes, Bootstrap derives `.fs-5` from
   `$h5-font-size`, and the `.site-page-content` block below scales section
   PROSE up to 1.0625rem for reading. The two cross over, so measured on a
   live page (/apps/sites/list/467/pages/2035, 2026-09-22):

       h2.site-section-heading  35.2px / 700
       .fs-5                    15.4px / 400   <- the "subheading"
       .site-page-content base  18.7px / 400   <- the body under it

   The subheading rendered SMALLER than the body it introduces. That is the
   same crossover this file already fixed once, for `h5`/`.h6` — see the
   "h5 AND h6 ARE SMALLER THAN THE BODY IN THIS APP" block below, written
   2026-08-27 — and `.fs-*` is its un-swept twin. It is not fixed by
   redefining `.fs-5` inside `.site-page-content`: 8 of its 30 uses in
   sites/shared are sizing an ICON (`<i class="fas … fs-5">` in
   _comparison_table ×4, _feature_cards_grid, _pricing_cards,
   _use_cases_list), which a prose size would blow up.

   So the ROLE gets a name. One declaration for the size AND the gap under it,
   because the same report called the rhythm uneven and the gap was being
   chosen per partial (`mb-4` in four of them, nothing in _section_header).

   The step is `--type-page` (22px, PAGE_COMPOSITION_STANDARDS §5A, declared
   on :root in custom.css) with a literal fallback that lands on the same
   22px against this app's 110% root. The heading and the body around it are
   NOT on the §5A scale and are deliberately left alone: they are the Sites
   reading scale this file sets on purpose (see "TENANT BRAND & CONTENT TYPE
   SCALE" below), so the stack reads 35.2 → 22 → 18.7.

   The heading-size presets do not step this: with the body at 18.7px there
   is no honest step below 22px to move to, and `site-heading-sm` (26.4px)
   over 22 over 18.7 is still monotonic.

   WHY `!important` ON THE SIZE, and only there. Below 576px
   `mobile_optimizations.css` normalises the desktop layout with two blanket
   `!important` rules — `div:where(:not([style*="font-size"])) { font-size:
   1rem }` and the same shape for `.text-muted` at `0.925rem` — which no
   selector can out-specify. Without this the inversion came straight back at
   a narrow window: measured at 375px, a `text-muted` subheading fell to
   16.28px under a 17.6px body. Same reasoning as the visual-mode padding
   further up: the blanket rule is a DEFAULT for app chrome that happens to
   be div-shaped, this is an explicit type step, and intent wins. Measured
   after, at 575 / 768 / 1400: 28.16 → 22 → 17.6, 35.2 → 22 → 18.7,
   35.2 → 22 → 18.7, with no horizontal overflow at any of them. */
.site-page-content .site-section-sub {
    font-size: var(--type-page, 1.25rem) !important;
    font-weight: 400;
    line-height: 1.45;
    margin-bottom: 1.5rem;
}

.site-page-content .site-align-center,
.site-page-content .site-align-center .text-start { text-align: center !important; }
.site-page-content .site-align-start,
.site-page-content .site-align-start .text-center { text-align: start !important; }
/* RIGHT (2026-09-22, QA batch 3: "options should be Left, Right, Centred").
   Same shape as the two above: it re-aligns the text, including blocks a
   partial marked centred or start. */
.site-page-content .site-align-end,
.site-page-content .site-align-end .text-center,
.site-page-content .site-align-end .text-start { text-align: end !important; }
/* HEADING ONLY (2026-09-24, Sites QA round 5: "Employee Spotlight / Employee
   Directory: text alignment should only apply to the heading"). On the blocks
   in SitePage::SECTION_ALIGN_HEADING_ONLY_TYPES every card pairs a picture, an
   icon or a marker with its text, so the preset moves the heading, its
   eyebrow and its subheading — measure boxes included — and nothing else.
   Higher specificity than the measure-box rules below, so no order games. */
.site-page-content .site-align-heading-start :is(h2.site-section-heading, .site-section-sub, .site-section-sub > p, .site-section-eyebrow) {
    text-align: start !important;
    margin-inline: 0;
}
.site-page-content .site-align-heading-center :is(h2.site-section-heading, .site-section-sub, .site-section-sub > p, .site-section-eyebrow) {
    text-align: center !important;
    margin-inline: auto;
}
.site-page-content .site-align-heading-end :is(h2.site-section-heading, .site-section-sub, .site-section-sub > p, .site-section-eyebrow) {
    text-align: end !important;
    margin-inline: auto 0;
}
/* ONE BUTTON ROW, AND IT FOLLOWS THE TEXT (Sites tester round 4, 2026-10-02,
   R12: "the button should move to the right"). A section's button sits in a
   flex row — that row is what keeps the button its own width — and text-align
   cannot reach a flex row's items, so at Right the words went right and the
   button stayed put. Round 5 answered it with a guess, `.d-flex.justify-
   content-center:has(> .btn)`, which reached only the rows that happened to be
   centred: a left-set row (Solution Overview beside its picture, Feature Rows,
   Platform Pillars) never moved at all. Every section-level button row now
   carries `site-cta-row` and these rules are the one place it is justified:
     - untouched it sits at the start; `site-cta-row--center` where the
       partial's column is centred (a CTA Banner or Solution Overview with no
       picture) — never a `justify-content-*` utility on the row;
     - Left / Centred / Right justify it start / centre / end — the WHOLE-block
       classes only. Where Alignment moves just the heading (a CTA Banner
       beside its picture), the button stays with the text it ends.
   `!important` for the reason the text presets carry it. The CTA Banner's row
   of ticked points is a `site-card-row` ("THE CARD ROW FOLLOWS THE HEADING"). */
.site-page-content .site-cta-row { justify-content: flex-start; }
.site-page-content .site-cta-row.site-cta-row--center { justify-content: center; }
.site-page-content .site-align-start .site-cta-row { justify-content: flex-start !important; }
.site-page-content .site-align-center .site-cta-row { justify-content: center !important; }
.site-page-content .site-align-end .site-cta-row { justify-content: flex-end !important; }
/* The measure boxes follow the preset too — the full twins of the
   `.text-center` / `.text-start` rules sit further down, after those rules,
   so they win on equal specificity (review finding 2026-09-21: a `p`-only
   rule here lost to `.text-start p` and left a centred 54ch box with
   start-aligned text, ragged against its own heading). */

/* ── Reduced motion ─────────────────────────────────────────────────────
   marketing.css has no prefers-reduced-motion handling at all; added here
   rather than ported. The marquee is an infinite scroll and the carousel an
   auto-advancing crossfade, which are exactly what this query exists for.
   The hero carousel does not advance at all for this visitor (its controller
   refuses to start the timer), so the transition line is belt and braces;
   the hero block above also sizes its frame to the one picture shown. */
@media (prefers-reduced-motion: reduce) {
    .site-page-content .gallery-item,
    .site-page-content .gallery-preview,
    .site-page-content .hero-carousel-img {
        transition: none;
    }
}


/* ============================================================================
   TENANT BRAND & CONTENT TYPE SCALE
   ============================================================================

   What was ALREADY working, so it is not redone here: `bootstrap_overrides.css`
   maps `--bs-primary: var(--brand-primary)`, so every `text-primary`,
   `btn-primary` and `bg-primary` in the section partials already renders in the
   tenant's brand colour. The accent was never the gap, and an earlier draft of
   this review said otherwise — corrected after reading the override file.

   What was NOT working: the tenant's FONT choice. `--font-heading` and
   `--font-body` are injected into every page by the layout's <style
   id="custom-theme"> block, and a grep of every stylesheet in the app found ZERO
   consumers of either. The setting existed and changed nothing, product-wide.
   Sites is the first consumer. Scoped to .site-page-content rather than made
   global, because consuming it app-wide is a shared-layout change touching every
   app and needs its own review.

   And the type SCALE. `--font-size-base` is 14px — correct for dense application
   chrome and 2-4px under what a reading surface wants. Prose set at application
   density reads as a settings screen no matter how well it is styled, so content
   here steps up to ~17px with looser leading. Of everything in this file this is
   the change that most makes a page read as a page rather than a panel.
   ========================================================================== */

.site-page-content {
    font-family: var(--font-body, inherit);
    font-size: 1.0625rem;   /* ~17px against the app's 14px base */
    line-height: 1.65;
    /* A word longer than its box breaks instead of running off the block — a
       long URL, a product code, a compound word (/builder-audit 2026-10-02: a
       40-letter word ran 381px past a hero and off every phone heading; the
       tester's "text continues off the page" on Mini Icon Grid). */
    overflow-wrap: break-word;
}
/* A badge, pill or button wraps a long label inside itself. Bootstrap's .badge
   never wraps, and the Journey Map owner pill also carries .text-nowrap, so a
   long team name ran out of the pill and past the block — Platform Pillars'
   badge 624px, Resource Hub's category chips 890px, the owner pill out of its
   step (/builder-audit 2026-10-02). Short labels are unchanged.

   `break-word`, NOT `anywhere` (Sites tester round 4, R33: the Ask AI widget's
   "Ask" button drew "As / k"). `anywhere` also lowers a box's min-content width
   to a single letter, so in a flex row a button beside a growing field shrank
   to one letter's width. `break-word` breaks a word only where it would run out
   of its box, and `max-width: 100%` still caps the box at its row, so a
   40-letter label still breaks inside its pill while a short label keeps its
   words whole. A control marked `.text-nowrap` (a submit button beside its
   field, a "View" link) keeps its label on one line — it asked for that. */
.site-page-content :is(.badge, .rounded-pill, .btn, .site-step-owner):not(.text-nowrap) {
    white-space: normal !important;
    max-width: 100%;
    overflow-wrap: break-word;
}

/* THE READING GUTTER — the same argument as the font size above, applied to
   the other app-chrome value a section inherits (2026-09-22, QA report R4
   "a manually built page has hero padding and alignment a generated page
   does not").

   This app sets `--bs-gutter-x: 0.825rem`, so every `.container` a section
   partial emits carries 6.6px of horizontal padding — right for a dense
   application panel, and far too tight for a page whose sections paint
   full-width COLOURED BANDS. Measured on /apps/sites/list/467/pages/1617:
   the hero band ran 326…1358 and its headline started at 332, six pixels
   from its own edge. What made it look like a page-to-page difference is
   that the gap only SHOWS in the left-aligned branch: a hero with an image
   renders the split layout, one without renders the centred `col-lg-10`,
   and the centred text is nowhere near the edge. Both branches had
   byte-identical padding — the variant, not the padding, is what differed.

   Why the gutter and not padding on `.hero`: the page must keep ONE left
   edge (PAGE_COMPOSITION_STANDARDS §5C). Insetting only the band would put
   its text at 356 while every other section stayed at 332. Moving the
   gutter moves them together — measured after: hero headline 352 and the
   next section's heading 352 under the app shell, 158 and 158 on the
   reader, at 375px the band sits 20…355 with text at 46 and
   `scrollWidth == innerWidth`.

   1.5rem is Bootstrap's own stock container gutter, which is what this
   value was before the app tightened it for chrome. Scoped to
   `.site-page-content`, so no other surface's `.container` moves. */
.site-page-content .container {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
}

.site-page-content h1,
.site-page-content h2,
.site-page-content h3,
.site-page-content h4,
.site-page-content h5,
.site-page-content h6 {
    font-family: var(--font-heading, inherit);
    color: var(--heading-color, inherit);
    line-height: 1.22;
    text-wrap: balance;
}

/* A heading an author writes INSIDE a Rich Text body (the panel's Quill editor
   makes it an h3 under the section's own h2, owner decision R1 2026-09-30)
   inherited the app theme's weight 400 and read lighter than the bold words
   beside it; the phone twin draws it semibold. Same weight here, with room
   above so it opens its paragraph rather than trailing the one before. */
.site-page-content .rich-text-content :is(h2, h3, h4) {
    font-weight: 600;
    margin-top: 1.25rem;
    margin-bottom: 0.5rem;
}

.site-page-content .rich-text-content > :is(h2, h3, h4):first-child {
    margin-top: 0;
}

/* Inherit the content scale rather than Bootstrap's app-tuned sizes. `.small`
   and `.text-muted` are deliberately left alone — partials use them for genuine
   de-emphasis (captions, meta lines), which should stay smaller than body copy.

   `:not(.small)` IS WHAT MAKES THAT SENTENCE TRUE (2026-09-22, QA batch 3). The
   rule was `.site-page-content p` — specificity 0,1,1, which beats Bootstrap's
   `.small` at 0,1,0 — so every `<p class="small">` in Sites rendered at the full
   18.7px body size. Measured: Resource Hub's "small" descriptions came out LARGER
   than their 16px titles (reported), and the same inversion sat under Mini Icon
   Grid, Feature Cards, Quick Links and Team Members captions. */
.site-page-content p:not(.small),
.site-page-content li:not(.small) {
    font-size: inherit;
}

.site-page-content .lead {
    font-size: 1.2rem;
    line-height: 1.55;
}

/* h5 AND h6 ARE SMALLER THAN THE BODY IN THIS APP, which inverts every heading
   that uses them inside site content — the heading renders lighter AND smaller
   than the paragraph it heads, so it reads as a caption under bigger text.

   Not a Sites bug on its own: AdminKit's theme sets `.h5, h5 { font-size:
   0.875rem }` and every heading to `font-weight: 400`, which is coherent at the
   app's 14px chrome scale. Sites is the surface that scales prose UP to
   1.0625rem for reading, so the two cross over and only here. Measured on a
   status banner: heading 15.4px/400 against body 18.7px/400.

   Thirty `.h5`/`.h6` headings across 21 of the 49 section partials are in this
   position, which is why this is one rule here rather than thirty edits there.
   h1–h4 are left alone — they are still larger than the body, so their
   hierarchy holds. */
.site-page-content h5,
.site-page-content .h5 {
    font-size: 1.125rem;
    font-weight: 600;
}

.site-page-content h6,
.site-page-content .h6 {
    font-size: 1.0625rem;
    font-weight: 600;
}

/* A TITLE DRAWN IN A COLOUR SOMEONE CHOSE (Category Cards). A colour as type
   needs 4.5:1 on its card, or 3:1 when the type is large — bold from 18.66px.
   This title is drawn at the 3:1 shade (SitesPaletteHelper::Mark#ink_style
   `large:`), which is what lets a company's orange be its own orange and not
   a brown, so its size and weight are HELD here: never under 19px, always
   bold, whatever the root size or a heading rule above would make it. */
.site-page-content .site-mark-title {
    font-size: max(1.125rem, 19px);
    font-weight: 700 !important;
}


/* ============================================================================
   READING MEASURE
   ============================================================================

   Measured on the dev tenant 2026-08-27: a page's text ran the full 1292px of
   the content column, which at this file's ~17px scale is well over 100
   characters per line against a comfortable 65-75. Content filled whatever
   container it was handed, identically in-app, in reader mode and through the
   public share door. Long measure is the kind of defect that reads as "this
   feels tiring" rather than "this is broken", which is why it survived every
   pass that judged the page by screenshot.

   CONSTRAIN THE TEXT, NOT THE SECTION. That is what makes one rule safe across
   all 49 section types without classifying any of them: `max-width` on a
   paragraph is INERT wherever the paragraph is already narrower than the
   measure — every card, column and grid cell in the palette — and bites only
   where prose spans a wide band, which is exactly the defect. Grids keep their
   full width and lose nothing. No per-type class and no wrapper hook needed.

   `margin-inline: auto` is applied ONLY inside an explicitly centred block.
   Centring unconditionally would pull a heading to the middle of a LEFT-aligned
   two-column feature row (whose column is ~600px, close enough to the measure
   to bite), so the auto margin is scoped to `.text-center`, where a left-hung
   box would otherwise be visibly wrong. A left-aligned wide band simply gets a
   left-aligned measure column, which is a normal document setting.

   Table cells opt out: a data table's own layout owns its widths, and a cell
   wide enough to reach the measure belongs in a scrolling container instead.

   WHY 54ch AND NOT 65-75ch. The `ch` unit is the width of "0", which in this
   font is markedly wider than the average character — so a `ch` value does NOT
   equal a character count. Calibrated against the longest published page on the
   dev tenant (4,961 characters, 18.7px): unconstrained it set 92 characters per
   line; 68ch changed NOTHING because 803px exceeded that page's own 799px
   container; 62ch gave 77; 52ch gave 68. 54ch ships, and measures 634px / 77
   characters on that page — a little above the 65-75 ideal and far below the
   92 it replaced. Finer tuning is not meaningful by this method: characters per
   line is derived from a rounded line COUNT, so 52ch and 56ch both read as 77
   on shorter paragraphs. Set in `ch` rather than px deliberately — it tracks a
   tenant's own `--font-body`, where a fixed pixel measure would drift per font. */
.site-page-content p,
.site-page-content li,
.site-page-content blockquote,
.site-page-content .lead,
.site-page-content h1,
.site-page-content h2,
.site-page-content h3 {
    max-width: 54ch;
}

/* A WIKI CALLOUT FILLS ITS COLUMN (Console #3988 — the block census's
   width:rows-fill-list, the last of its 36). `.wiki-content blockquote` is a
   filled callout box (custom.css), not a quotation, so the 54ch cap above drew
   it as a short coloured box inside an article whose headings ran the full
   column: 42 callouts at 634px in an 860px column on the census page. The
   measure is for lines of prose; a box is sized by its column, and the
   paragraphs inside it keep their own measure. */
.site-page-content .wiki-content blockquote {
    max-width: none;
    /* The BOX takes the column; its WORDS keep the measure. A callout's text
       is usually written straight into the blockquote, with no paragraph of
       its own to carry the 54ch cap — so lifting the cap alone ran its lines
       the full 860px while the paragraphs around it stopped at 634. Padding
       resolves against the column, so this leaves exactly 54ch of text.
       `--callout-start` is the box's own start padding, which an indented
       callout changes (below). */
    --callout-start: 0.95rem;
    padding-inline-end: max(0.95rem, calc(100% - 54ch - var(--callout-start)));
}

/* A CALLOUT THE AUTHOR ALIGNED OR INDENTED (/code-review 2026-10-03). A wiki
   body keeps the editor's block classes, and the end padding above assumed
   text that starts at the box's start edge: a centred callout's words centred
   132px left of the box's centre (measured, 860px column), a right-aligned
   one stopped 282px short of the right edge, and an indented one lost its
   indent from the measure (406px of text at the third step). Centred words
   share the spare room on both sides; right-aligned ones take it at the
   left; an indented box counts its indent (custom.css,
   `.ql-editor-preview .ql-indent-N`: 3em a step). Justified text is already
   right — it fills the measure.
   `text-align: right` is PHYSICAL, so its spare room is too: with logical
   padding a right-aligned callout stopped 282px short again for a reader
   whose language runs right to left. The indent is the opposite case — the
   editor's rule is a physical `padding-left`, which is the END side in such a
   language, where the end padding above replaced it and the lines overran
   the measure by the indent; here the indent is the box's start padding. */
.site-page-content .wiki-content blockquote.ql-align-center {
    padding-inline: max(0.95rem, calc((100% - 54ch) / 2));
}
.site-page-content .wiki-content blockquote.ql-align-right {
    padding-left: max(0.95rem, calc(100% - 54ch - 0.95rem));
    padding-right: 0.95rem;
}
.site-page-content .wiki-content blockquote[class*="ql-indent-"]:not(.ql-align-center):not(.ql-align-right) {
    padding-inline-start: var(--callout-start);
}
.site-page-content .wiki-content blockquote.ql-indent-1 { --callout-start: 3em; }
.site-page-content .wiki-content blockquote.ql-indent-2 { --callout-start: 6em; }
.site-page-content .wiki-content blockquote.ql-indent-3 { --callout-start: 9em; }
.site-page-content .wiki-content blockquote.ql-indent-4 { --callout-start: 12em; }
.site-page-content .wiki-content blockquote.ql-indent-5 { --callout-start: 15em; }
.site-page-content .wiki-content blockquote.ql-indent-6 { --callout-start: 18em; }
.site-page-content .wiki-content blockquote.ql-indent-7 { --callout-start: 21em; }
.site-page-content .wiki-content blockquote.ql-indent-8 { --callout-start: 24em; }

/* CENTRING THE MEASURE BOX — three shapes, because the partials use all three.
   This was `.text-center > p`, a direct-child selector, and it matched only one
   of them. Measured on one page: four of six section headings sat 125–140px
   left of the container they head, while the rest of the section was centred.

     1. prose inside a centred block            (.text-center p)
     2. prose that IS the centred element       (p.text-center) — an element is
        not its own child, so the old rule could never match this one, and it is
        what _journey_map and the resource grid both use on their heading
     3. prose wrapped in a styling div inside a centred block — also a
        descendant, so rule 1 now covers it

   The reason it was a direct-child selector is real and is preserved BELOW: a
   descendant match alone would also centre the measure box of the lists
   _feature_cards_grid deliberately marks `text-start` inside a centred card.
   Re-aligning explicitly is what that reset is for, and it comes after so it
   wins on equal specificity wherever both match. */
.site-page-content .text-center p,
.site-page-content .text-center li,
.site-page-content .text-center blockquote,
.site-page-content .text-center .lead,
.site-page-content .text-center h1,
.site-page-content .text-center h2,
.site-page-content .text-center h3,
.site-page-content p.text-center,
.site-page-content li.text-center,
.site-page-content blockquote.text-center,
.site-page-content .lead.text-center,
.site-page-content h1.text-center,
.site-page-content h2.text-center,
.site-page-content h3.text-center {
    margin-inline: auto;
}

/* An author who re-aligned a block inside a centred one meant it. Last in
   source order, so on equal specificity it wins over the centring above. */
.site-page-content .text-start p,
.site-page-content .text-start li,
.site-page-content .text-start blockquote,
.site-page-content .text-start .lead,
.site-page-content .text-start h1,
.site-page-content .text-start h2,
.site-page-content .text-start h3,
.site-page-content .text-end p,
.site-page-content .text-end li,
.site-page-content .text-end blockquote,
.site-page-content .text-end .lead,
.site-page-content .text-end h1,
.site-page-content .text-end h2,
.site-page-content .text-end h3 {
    margin-inline: 0;
}

/* THE AUTHOR'S ALIGNMENT PRESET, last of all, so it wins over both blocks
   above on equal specificity: every measure box in a centred section is
   centred, every one in a start-aligned section is flush — including the
   blocks a partial had marked the other way, since the preset re-aligned
   their text (see the preset rules near the type scale). */
.site-page-content .site-align-center p,
.site-page-content .site-align-center li,
.site-page-content .site-align-center blockquote,
.site-page-content .site-align-center .lead,
.site-page-content .site-align-center h1,
.site-page-content .site-align-center h2,
.site-page-content .site-align-center h3 {
    margin-inline: auto;
}
.site-page-content .site-align-start p,
.site-page-content .site-align-start li,
.site-page-content .site-align-start blockquote,
.site-page-content .site-align-start .lead,
.site-page-content .site-align-start h1,
.site-page-content .site-align-start h2,
.site-page-content .site-align-start h3 {
    margin-inline: 0;
}
/* A right-aligned section hangs its measure boxes off the RIGHT edge, so the
   ragged edge is on the left where the reader's eye starts each line, and the
   54ch box never floats in the middle of a right-set block. */
.site-page-content .site-align-end p,
.site-page-content .site-align-end li,
.site-page-content .site-align-end blockquote,
.site-page-content .site-align-end .lead,
.site-page-content .site-align-end h1,
.site-page-content .site-align-end h2,
.site-page-content .site-align-end h3 {
    margin-inline: auto 0;
}

/* A LIST FOLLOWS ITS TEXT'S ALIGNMENT (Sites tester round 6, 2026-10-06 —
   Justina: a numbered list in a centred Rich Text drew its lines in the middle
   and its numbers at the far left). A marker sits OUTSIDE its line by default,
   hung off the start edge of the list's box, so once the lines are centred or
   set right the numbers and bullets stay behind. Inside, the marker is the
   first thing on its line and moves with it, and the start padding that made
   room for the hung markers goes. A left-set list is unchanged.

   Keyed on the alignment the blocks share: the author's preset on the section
   (`site-align-center` / `-end`, which re-aligns everything in the block), and
   a block whose own default is centred or right (`.text-center` / `.text-end`)
   unless the preset set it left or a `.text-start` inside re-aligned the list.
   Authored lists only — the rich text the panel's editor, the AI or markdown
   writes carries no class; a list with one is a partial's own layout (a
   checklist, a tab strip, the steps) with its own markers or none. */
.site-page-content :is(.site-align-center, .site-align-end) :is(ul, ol):not([class]),
.site-page-content :is(.text-center, .text-end):not(.site-align-start, .site-align-start *) :is(ul, ol):not([class], .text-start *) {
    list-style-position: inside;
    padding-inline-start: 0;
}
/* A line the editor centred or set right on its own (Quill's `ql-align-*`
   classes, kept by the page's allowlist): its marker moves with it, and a list
   whose every line is centred or right drops the hung markers' padding too. */
.site-page-content li:is(.ql-align-center, .ql-align-right) {
    list-style-position: inside;
}
.site-page-content :is(ul, ol):not([class]):has(> li):not(:has(> li:not(.ql-align-center, .ql-align-right))) {
    padding-inline-start: 0;
}

.site-page-content table p,
.site-page-content table li,
.site-page-content td,
.site-page-content th {
    max-width: none;
}
/* A LIST-GROUP ROW IS A ROW, NOT A LINE OF PROSE (2026-09-26, Sites QA round
   6: "File Downloads: stretch the file container to the full width"). The
   measure's `li` rule caught every `li.list-group-item`, so File Downloads,
   Document Links, Policy Links and the News feed drew their bordered lists at
   54ch — ~570px inside a ~780px block — and a long description pushed the
   type pill to the list's edge rather than the block's. */
.site-page-content li.list-group-item {
    max-width: none;
}

/* NEWS ROWS (Sites tester round 2026-10-06, row 5: "The post previews do not
   look good; needs more space between the border and text"). The News
   section's posts are a padded list now (sites/shared/_news_feed_posts says
   why), and each row is ONE target: its title's `stretched-link` covers the
   row and `list-group-item-action` lights it on hover. A keyboard user tabs to
   the title, not the row, so the row lights the same way while anything in it
   has focus. Acknowledge sits above the stretched link — its own target, never
   a click on the post. The picture keeps one 4:3 frame whatever was uploaded. */
.site-page-content .site-news-list > .list-group-item:focus-within {
    background-color: var(--bs-list-group-action-hover-bg);
}
.site-page-content .site-news-list .site-news-act {
    position: relative;
    z-index: 2;
}
.site-page-content .site-news-list .site-news-thumb {
    width: 6rem;
    aspect-ratio: 4 / 3;
    object-fit: cover;
}

/* THE AUTHOR LIFTED THE MEASURE (2026-09-22, `text_width: full` — the owner's
   answer to the Rich Text report "does not span across the whole block").
   Last in this family so it beats every measure rule above, including the
   alignment presets' margin twins, on specificity (:is takes its most specific
   argument, .lead, so this is 0,3,0 against their 0,2,1). */
.site-page-content .site-width-full :is(p, li, blockquote, .lead, h1, h2, h3) {
    max-width: none;
}

/* A BOXED BANNER IS CHROME, NOT PROSE. The measure exists for text running the
   full width of a page; inside an alert the box already sets the line length,
   and capping it again stopped the text two-thirds of the way across a panel
   whose own background kept going — which reads as a rendering fault, not as a
   comfortable measure. Same reasoning as the table opt-out above: a container
   that owns its own width owns the line length inside it. */
.site-page-content .alert p,
.site-page-content .alert li,
.site-page-content .alert h1,
.site-page-content .alert h2,
.site-page-content .alert h3 {
    max-width: none;
}

/* A DISCLOSURE ROW OWNS ITS OWN LINE LENGTH — question AND answer. `<summary>`
   is a full-width click target whose heading and chevron are laid out as a
   flex row, so capping the heading at the measure never shortened a line of
   text — it pinned the flex item and parked the chevron wherever the cap
   landed (measured on the FAQ at /pages/232: a 1217px row whose question
   stopped at 671px, chevron floating at x=1005). The answer sits directly
   under that full-width question inside the same bordered row, and capping IT
   stopped the text at 634px of an 887px row whose question ran the whole
   width — which reads as a rendering fault, exactly as it did inside the
   alert above, not as a comfortable measure. Same rule as the table and the
   alert: a container that owns its own width owns the line length inside it.
   `details` covers the summary too, so this is one opt-out, not two. */
.site-page-content details p,
.site-page-content details li,
.site-page-content details .lead,
.site-page-content details h1,
.site-page-content details h2,
.site-page-content details h3 {
    max-width: none;
}

/* THE "IN SHORT" CARD OWNS ITS LINE LENGTH TOO (2026-10-01, Sites QA round 3).
   The page's AI summary (shared/_in_short) is a card drawn inside this scope,
   so the measure caught its summary and its "Written by AI" line and stopped
   them about two-thirds of the way across a card whose white kept going — the
   alert's failure again. Same opt-out as the alert and the disclosure row. */
.site-page-content [data-in-short] p,
.site-page-content [data-in-short] h1,
.site-page-content [data-in-short] h2,
.site-page-content [data-in-short] h3 {
    max-width: none;
}


/* ============================================================================
   BLOCK-WIDTH BREAKPOINTS — columns follow the block, not the window
   ============================================================================
   Every Sites block that lays out columns — text beside a picture (hero, CTA
   banner, feature rows, solution overview, data insights, security badges,
   mini icon grid, tabbed content, two-column cards, pricing, platform
   pillars) and every card grid (category cards, resource hub, quick links,
   team, contact cards, button grid, feature cards, galleries …) — used
   Bootstrap's md/lg/xl classes, which read the WINDOW. Inside the app shell
   the icon rail and the Sites sidebar take ~370px of that window, and the
   Sites reader has its own page rail, so at a 1024px window a block had
   ~603px of content and still split in half (~290px text columns, one-word
   headings broken over two lines) or laid four cards across (~140px each).
   The editor canvas, narrower than the window it sits in, had the same
   fault (measured 2026-09-22, QA batch 3).

   The owner's decision (2026-09-22/23): container queries, for the two-column
   blocks and then for the card grids, so one rule is right in the app shell,
   the reader and the editor canvas alike.

   Markup: put `site-cq` on the element that directly wraps the row — a
   `.container` that holds only rows, or a plain wrapper div (never the row
   itself: a container query styles descendants, not the container). Then
   write the Bootstrap class with `cq-` before the tier: col-md-6 col-lg-4
   → col-cq-md-6 col-cq-lg-4, and g-cq-lg-5, order-cq-lg-1,
   flex-cq-lg-row-reverse, text-cq-md-end, mb-cq-md-0, p-cq-lg-5. Below a
   tier's threshold its classes do nothing, so the element keeps its base
   classes (col-6, col-sm-6, or full width).
   `col-sm-*` STAYS A WINDOW CLASS, on purpose (three blocks use it:
   use_cases_list, data_insights' tiles, mini_icon_grid_block's "Three").
   576px is narrower than any desktop layout's window, so on a desktop page
   it is on; the narrowest box it lands in is the icon grid's card beside
   the text (measured 348px of content at a 1280 window, three 122px tiles —
   the layout its "Three" setting was verified at), and below 576px the
   window and the block agree anyway. A block-width sm tier would need a
   threshold under that card's width to keep it, which makes it a rule that
   never switches off on a desktop page. SitesHelper#site_block_columns
   passes col-sm-* through for the same reason. A nested `site-cq` answers for
   its own rows — a pillar's picture splits on the CARD's width. An author-
   or template-supplied `columns` string goes through
   SitesHelper#site_block_columns, which does this translation for it.

   THE TIERS, in px of the container's content — PIXELS, not rem: this app's
   root is 17.6px (custom.css sets `html { font-size: 110% }`), so 45rem
   would be 792px (the first cut used rem, and a 752px pillar card stayed
   stacked). Each threshold sits between the content widths a Sites
   `.container` gives at the two windows either side of the old breakpoint
   (540/720/960/1140px wide, less 2 × 26.4px of padding — see
   `.site-page-content .container` above), so a page with nothing beside it
   lays out exactly as it did:
     md  560px  (sm .container ~487 → md ~667)
     lg  720px  (md ~667 → lg ~907); a 50/50 split with the widest gutter
                 (3rem, 52.8px here) never gives a column under ~334px
     xl  960px  (lg ~907 → xl ~1087)
   Literals, here — a container query cannot read a custom property. Later
   tiers come later in the file, so col-cq-md-6 col-cq-lg-4 is a third once
   both apply.

   `container-type: inline-size` also makes the element a containing block
   for fixed-position descendants and a stacking context. None of these
   blocks draws a modal, a dropdown or a fixed element inside its columns
   (checked 2026-09-23) — keep it that way, or put that element outside the
   `site-cq` wrapper. The site navbar keeps Bootstrap's own collapse. */
.site-cq { container: site-cq / inline-size; }

@container site-cq (min-width: 560px) {
  .site-cq .col-cq-md-1  { flex: 0 0 auto; width: 8.33333333%; }
  .site-cq .col-cq-md-2  { flex: 0 0 auto; width: 16.66666667%; }
  .site-cq .col-cq-md-3  { flex: 0 0 auto; width: 25%; }
  .site-cq .col-cq-md-4  { flex: 0 0 auto; width: 33.33333333%; }
  .site-cq .col-cq-md-5  { flex: 0 0 auto; width: 41.66666667%; }
  .site-cq .col-cq-md-6  { flex: 0 0 auto; width: 50%; }
  .site-cq .col-cq-md-7  { flex: 0 0 auto; width: 58.33333333%; }
  .site-cq .col-cq-md-8  { flex: 0 0 auto; width: 66.66666667%; }
  .site-cq .col-cq-md-9  { flex: 0 0 auto; width: 75%; }
  .site-cq .col-cq-md-10 { flex: 0 0 auto; width: 83.33333333%; }
  .site-cq .col-cq-md-11 { flex: 0 0 auto; width: 91.66666667%; }
  .site-cq .col-cq-md-12 { flex: 0 0 auto; width: 100%; }
  .site-cq .col-cq-md-auto { flex: 0 0 auto; width: auto; }
  /* !important like the Bootstrap utilities they stand in for, so they beat
     a base `text-center` / `mb-4` on the same element. */
  .site-cq .text-cq-md-end { text-align: right !important; }
  .site-cq .mb-cq-md-0 { margin-bottom: 0 !important; }
}

@container site-cq (min-width: 720px) {
  .site-cq .col-cq-lg-1  { flex: 0 0 auto; width: 8.33333333%; }
  .site-cq .col-cq-lg-2  { flex: 0 0 auto; width: 16.66666667%; }
  .site-cq .col-cq-lg-3  { flex: 0 0 auto; width: 25%; }
  .site-cq .col-cq-lg-4  { flex: 0 0 auto; width: 33.33333333%; }
  .site-cq .col-cq-lg-5  { flex: 0 0 auto; width: 41.66666667%; }
  .site-cq .col-cq-lg-6  { flex: 0 0 auto; width: 50%; }
  .site-cq .col-cq-lg-7  { flex: 0 0 auto; width: 58.33333333%; }
  .site-cq .col-cq-lg-8  { flex: 0 0 auto; width: 66.66666667%; }
  .site-cq .col-cq-lg-9  { flex: 0 0 auto; width: 75%; }
  .site-cq .col-cq-lg-10 { flex: 0 0 auto; width: 83.33333333%; }
  .site-cq .col-cq-lg-11 { flex: 0 0 auto; width: 91.66666667%; }
  .site-cq .col-cq-lg-12 { flex: 0 0 auto; width: 100%; }
  .site-cq .col-cq-lg-auto { flex: 0 0 auto; width: auto; }
  .site-cq .g-cq-lg-5 { --bs-gutter-x: 3rem; --bs-gutter-y: 3rem; }
  .site-cq .order-cq-lg-1 { order: 1 !important; }
  .site-cq .order-cq-lg-2 { order: 2 !important; }
  .site-cq .flex-cq-lg-row-reverse { flex-direction: row-reverse !important; }
  .site-cq .text-cq-lg-end { text-align: right !important; }
  .site-cq .mb-cq-lg-0 { margin-bottom: 0 !important; }
  .site-cq .mt-cq-lg-0 { margin-top: 0 !important; }
  .site-cq .p-cq-lg-5 { padding: 3rem !important; }
}

@container site-cq (min-width: 960px) {
  .site-cq .col-cq-xl-1  { flex: 0 0 auto; width: 8.33333333%; }
  .site-cq .col-cq-xl-2  { flex: 0 0 auto; width: 16.66666667%; }
  .site-cq .col-cq-xl-3  { flex: 0 0 auto; width: 25%; }
  .site-cq .col-cq-xl-4  { flex: 0 0 auto; width: 33.33333333%; }
  .site-cq .col-cq-xl-5  { flex: 0 0 auto; width: 41.66666667%; }
  .site-cq .col-cq-xl-6  { flex: 0 0 auto; width: 50%; }
  .site-cq .col-cq-xl-7  { flex: 0 0 auto; width: 58.33333333%; }
  .site-cq .col-cq-xl-8  { flex: 0 0 auto; width: 66.66666667%; }
  .site-cq .col-cq-xl-9  { flex: 0 0 auto; width: 75%; }
  .site-cq .col-cq-xl-10 { flex: 0 0 auto; width: 83.33333333%; }
  .site-cq .col-cq-xl-11 { flex: 0 0 auto; width: 91.66666667%; }
  .site-cq .col-cq-xl-12 { flex: 0 0 auto; width: 100%; }
  .site-cq .col-cq-xl-auto { flex: 0 0 auto; width: auto; }
}

/* ============================================================================
   READER SHELL — the rail, and nothing else
   ============================================================================

   layouts/site_reader puts the 64px app rail back for a LOGGED-IN reader while
   leaving the 220px submenu panel out. Two rules are needed because the rail's
   own geometry in custom.css is written for the app layout, which has a 56px
   fixed navbar above it: `position: sticky; top: 56px; height: calc(100vh -
   56px)`. This layout has no navbar, so without the override the rail would
   start 56px down and run 56px short.

   `align-items: stretch` (the flex default) is what makes the rail's sticky
   box full height; `min-width: 0` on the main column is the standard flex
   guard that stops wide content — a table, a code block — from pushing the
   column wider than the viewport instead of scrolling inside itself. */
.site-reader-shell {
    display: flex;
    align-items: stretch;
}

.site-reader-main {
    flex: 1;
    min-width: 0;
}

/* The full-view reader carries the rail but not the 220px panel the Apps and
   Admin zones open (the layout keeps it out on purpose), and the rail's click
   handler silently gives up without `#sidebar-wrapper` — two icons that did
   nothing (ux-audit sites 2026-09-23, U6-03). Keyed on the body class, so it
   stays right while Turbo keeps the permanent rail between pages. */
body.site-reader-layout .sidebar-rail-icon[data-zone="apps"],
body.site-reader-layout .sidebar-rail-icon[data-zone="admin"] {
  display: none;
}

body.site-reader-layout .sidebar-rail {
    top: 0;
    height: 100vh;
}


/* ============================================================================
   SITES GALLERY CARDS
   ============================================================================

   The index is not a `.site-page-content` surface, so these are the one set of
   rules in this file scoped outside it. They exist because the gallery's cards
   ran ragged: descriptions were unclamped, so each card in a row resolved to a
   different height and the row read as unfinished regardless of what was in it.

   `-webkit-line-clamp` is the only cross-browser line clamp today and is
   prefixed in every engine that supports it, Firefox and Safari included —
   the prefix is the standard spelling here, not a Chrome-only hack. */
/* The shared cover band owns photo cropping and the failed-image fallback.
   This compact directory variant keeps the site's familiar icon above it.
   Phone rows use the same partial; their sizing lives in mobile_tailwind.css. */
.site-card-visual {
    position: relative;
    flex-shrink: 0;
    border-top-left-radius: inherit;
    border-top-right-radius: inherit;
}

.site-card-visual > .cover-band--generated {
    opacity: .18;
}

.site-card-visual__icon {
    position: absolute;
    left: 16px;
    bottom: 16px;
}

.site-card-chips {
    position: absolute;
    top: 12px;
    right: 12px;
}

.site-card-desc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* The design system's secondary line (`--type-small`, 13px), one step
       above the meta line's caption below it — see `.site-card-meta`. */
    font-size: var(--type-small, 13px);
}

/* Pins the meta line to the bottom of a flex card body so every card in a row
   ends on the same baseline whatever the description length. Pairs with
   `d-flex flex-column` on the card body in the index view.

   THE META LINE IS A CAPTION (2026-10-09, Sites tester round 8 — Melanie:
   "Reduce the font size of the secondary information on site cards, such as
   page counts and 'Updated' timestamps, so it has less visual emphasis than
   the site name and description"). Both lines drew at `.small` — 80% of the
   tenant's body size, 12px on the default 15px — so the facts and the
   description were one size. 12px is the scale's floor (`--type-caption`,
   custom.css: meta lines, "THE FLOOR"), so the facts stay on it — the size of
   the Dashboard line the report pointed to as the model — and the
   description takes the step above, the secondary line (`--type-small`,
   13px). The tile reads name → description → facts. A page row on a site's
   page uses the same line (apps/sites/shared/_card_meta) and the same step. */
.site-card-meta {
    margin-top: auto;
    font-size: var(--type-caption, 12px);
}


/* ============================================================================
   FULL-BLEED SECTIONS
   ============================================================================

   SECTION_SCHEMAS declares `layout: "full"` on `hero` and `section_divider`, and
   nothing read it: `render_site_section` builds locals from the section's own
   keys and never consulted the schema's layout, so the declaration was inert
   metadata and every section rendered contained. 35 of the 63 partials hard-code
   `<div class="container">` internally, which is what actually pins the width.

   Rather than edit those 35 partials, the WRAPPER carries the intent —
   SitesHelper#site_section_full_bleed? puts `.site-section-full` on the
   per-section div in show.html.erb and _preview_sections.html.erb. The inner
   container is NOT released (2026-09-21: it was, and a band's text ran to
   the edge); only the band's background bleeds, in the reader rule below.

   `!important` on max-width for the same reason as the mode padding: Bootstrap
   sets `.container` max-width in a stack of media queries, and this has to beat
   it at every breakpoint.

   Page content sits inside a `.container-fluid`, so releasing the inner container
   makes the section span the full content column. On READER surfaces only, the
   negative margins below then claim the gutter too, which is what lets a hero
   actually touch both edges — see the scoping note there for why the app shell
   deliberately stops at the column. */
/* THE BAND BLEEDS; ITS CONTENT DOES NOT (2026-09-21). This rule used to
   release the inner container's max-width, so a full-bleed hero's headline
   and picture ran to the very edge of the band — 12px from the viewport on
   a reader page — while the sections under it sat in a centred container.
   "Add padding to the left and right" and "all the different padding sizes"
   were both this. A band's BACKGROUND touching both edges is the intent; its
   text lining up with every other section's text is what makes the page
   read as one page. The container keeps its normal width, centred, and the
   reader-surface bleed below still carries the coloured field to the edges. */

/* The rail's width, as far as the bleed identity is concerned. Declared ONLY
   where the rail actually is; every other context falls back through the
   `var(--site-rail-w, 0px)` default at the use site below.

   The first version of this also reset the variable to 0 on `.site-page-content`
   "for safety". That element is a DESCENDANT of body, so its declaration won
   the cascade on exactly the pages the variable exists for, and the correction
   silently never applied — the hero measured 32px under the rail and 32px past
   the right edge. A custom property needs its fallback at the `var()` call, not
   a reset on an inner element. */
body.site-reader-railed {
    --site-rail-w: 64px;
}

@media (max-width: 767.98px) {
    body.site-reader-railed {
        --site-rail-w: 0px;
    }
}

/* THE BLEED IS READER-SURFACES ONLY (`body.site-reader-layout` = the published
   reader view and the public share door). It is NOT applied under the app
   shell, and the scoping is the whole point of this selector.

   WHY: `50% - 50vw` reaches the viewport edge only when the content column is
   CENTRED IN THE VIEWPORT. Under layouts/application it is not — the column
   starts at the rail (64px) plus the panel (0 or 220px) plus the `.content` /
   `.container-fluid` gutters, and NOTHING in the app shell declares
   `--site-rail-w`, so the correction below fell through to its `0px` default
   and the identity ran unchecked. Measured on
   /apps/sites/list/:id/pages/:id at 1512px: the main column is 64…1512 and the
   hero rendered 32…1544 — 32px UNDER the sidebar rail on the left, 32px PAST
   the viewport on the right, and `document.scrollWidth` 1564 vs `innerWidth`
   1512, i.e. a real horizontal scrollbar on every Sites page with a hero.
   The editor preview pane (_preview_sections) is the same shell and the same
   bug. Reported from the dev tenant 2026-08-28.

   The `--site-rail-w` correction cannot be generalised to the app shell: the
   offset there is not a constant (the 220px app submenu is user-toggleable,
   and the editor renders sections inside a preview pane of arbitrary width),
   so any value hard-coded here is wrong in some state. A section that stays in
   its column is right in every state — under app chrome a band running under
   the sidebar was never the intent anyway. `.site-section-full` still releases
   the inner `.container` max-width above, so a hero spans the full content
   column; it just stops there.

   Reader surfaces keep the true edge-to-edge bleed unchanged — that layout has
   the rail and nothing else, `--site-rail-w` is declared on it, and the page IS
   the destination. */
body.site-reader-layout .site-page-content .site-section-full {
    /* The standard full-bleed identity: -50vw + 50% reaches the viewport edge
       from ANY parent padding, where the first version's gutter-var guess
       (`-var(--bs-gutter-x)/2`) overshot the real padding by ~5px and produced
       a horizontal scrollbar at every width — caught by the /verify sweep's
       `scrollWidth <= innerWidth` assertion, invisible to the eye.

       `+ rail/2` corrects for the identity's one assumption: that the content
       column is CENTRED IN THE VIEWPORT. With the reader rail present it is
       not — the column starts 64px in, so its centre sits 32px right of the
       viewport centre and an uncorrected bleed would run 32px under the rail.
       Adding half the rail width moves both edges back by exactly that much,
       which lands the section on the main column's edges instead of the
       viewport's. */
    margin-left: calc(50% - 50vw + (var(--site-rail-w, 0px) / 2));
    margin-right: calc(50% - 50vw + (var(--site-rail-w, 0px) / 2));
}

/* CONTAIN THE ROW-GUTTER OVERHANG AT ITS SOURCE (2026-08-28).
   Bootstrap's contract is that a row's negative margin equals its container's
   horizontal padding, so the two cancel and column CONTENT lands on the
   container's content box. A row that raises its own gutter above the
   container's breaks that half of the deal: this app sets `--bs-gutter-x` to
   0.825rem (6.6px of container padding) while the hero partial's row carries
   `g-4 g-lg-5` (3rem, so -26.4px margins), and releasing `max-width` above does
   not change either number. The row's BORDER BOX therefore escapes the container
   by 19.8px on each side.

   Nothing paints there — the columns' own 26.4px padding pulls their content
   back to exactly the container's content box (measured: heading and image land
   at 70.6…1505.4 inside a 64…1512 section) — so this is a transparent box, not
   a visible crop. What it is, is real overflow: on the reader surface the row
   reached x=1531.8 against a 1512px viewport, `document.scrollWidth` 1532 vs
   `clientWidth` 1512, and the ONLY reason no horizontal scrollbar appeared is
   the body-level belt below. That made a body rule load-bearing for a box two
   levels down, and left the same 19.8px escaping into the sidebar gutter on
   every other surface, where it survives today purely because the section
   happens to be inset far enough from the viewport edge.

   `clip` (not `hidden`) creates no scroll container and leaves overflow-y
   visible. Applied to the SECTION rather than the container so it holds for
   `section_divider` too — hero and section_divider are the only two types
   SECTION_SCHEMAS declares `layout: "full"`, and neither contains a dropdown,
   popover or sticky child that would need to escape.

   SCOPED TO THE READER SURFACES, and that scoping is measured, not cautious.
   `.hero-image` carries `filter: drop-shadow(0 8px 24px …)`, whose 24px tail
   reaches FURTHER than the 19.8px row overhang — so there is no
   `overflow-clip-margin` that contains the row while sparing the shadow, and
   clipping the app-shell section costs the shadow's outer tail (pixel-diffed:
   max channel delta 14/255 across the ~42px beyond the section's right edge).
   On a reader surface that cost is exactly zero, because the section's right
   edge IS the viewport edge, where `body.site-reader-layout { overflow-x: clip }`
   below already cuts at the same x — this only moves the cut to the box that
   causes it, so the body rule stops being load-bearing for a descendant two
   levels down. Verified: full-page pixel diff of the reader before and after is
   BYTE-IDENTICAL (0 of 4,082,400 bytes differ) while its `scrollWidth` drops
   1532 → 1512, and the app shell computes `overflow-x: visible` both before and
   after, as a `body.site-reader-layout` selector must.

   The app shell keeps the 19.8px transparent overhang. It reaches no viewport
   edge there (the section is inset by the sidebar on the left and the fluid
   gutter on the right — checked at 1512/1280/1024/820px with the app submenu
   both open and closed, `scrollWidth == clientWidth` in all eight), so it costs
   nothing today, and the shadow is worth more than defending a hypothetical. A
   surface that renders `.site-section-full` closer to a viewport edge should
   join this selector rather than reintroduce a body-level belt. */
body.site-reader-layout .site-page-content .site-section-full {
    overflow-x: clip;
}

/* Belt for the 50vw identity's one flaw: with a vertical scrollbar present,
   100vw exceeds the writable width by the scrollbar's width, so a full-bleed
   section can overhang by ~15px on scrollbar-reserving platforms. `clip` (not
   `hidden`) removes the sideways scroll without creating a new scroll
   container; body overflow propagates to the viewport, which is the point. */
body.site-reader-layout {
    overflow-x: clip;
}

/* BESIDE A SIDE MENU THE BAND STAYS IN ITS COLUMN (2026-10-06, ISS-20261006-662).
   The bleed identity above assumes the content column is centred in the
   viewport less the app rail. A site with a SIDE menu (layout "vertical") puts
   the column in a `.d-flex` beside a ~240px `.site-nav-sidebar`, so the column
   is off-centre by half the menu and the band ran under the menu on the left
   and past the viewport on the right — measured in full view at 2048px: column
   311…2026, hero 177…2161, `scrollWidth` 2161. The menu's width is the
   author's content, not a constant, so no `--site-rail-w`-style correction can
   be right; the band keeps to its column as it does under the app shell.
   Reaches pages/show and the tablet
   preview frame (_preview_content in layouts/site_preview_frame) alike —
   `:has()` keys on the wrapper both build, as custom.css's side-menu rules do.
   From 768px only: below that the menu stacks above a full-width column
   (custom.css) and the edge-to-edge bleed is right again. A browser without
   `:has()` drops these rules and keeps the bleed. */
@media (min-width: 768px) {
    body.site-reader-layout .d-flex:has(> .site-nav-sidebar) .site-page-content .site-section-full {
        margin-left: 0;
        margin-right: 0;
    }
}

/* ── Flex children must be allowed to shrink ───────────────────────────────
   A flex item's `min-width` defaults to `auto`, which means it cannot shrink
   below the intrinsic width of its content — so one long unbreakable run inside
   a `.d-flex` row pushes the row, and then the document, wider than the
   viewport. Measured at 375px on a seeded Runbooks page: a status banner's body
   column sat at 395px and scrolled the page sideways by 49px.

   Scoped to page CONTENT and to div children only: `min-width: 0` merely
   PERMITS shrinking, but icons and buttons that rely on their intrinsic width
   are left alone. */
.site-page-content .d-flex > div {
    min-width: 0;
}
/* …and every child of a flex COLUMN is at most as wide as the column. A
   centred child is sized to its content, and its content's minimum is its
   longest word, so one long word in a Journey Map step title made the h3 379px
   inside a 225px column and ran 53px into the next step (/builder-audit
   2026-10-02, finding 6013) — overflow-wrap never got a narrower line to break
   it on. Capped at the column, the word breaks. Nothing that fits is changed:
   an icon or a button narrower than its column keeps its own width. A `.row`
   is left out: its negative gutters are meant to reach past the column.
   Every grid column is a flex column too — custom.css sets
   `.row > [class*="col-"] { display: flex; flex-direction: column }` app-wide —
   so a heading straight inside a column had the same minimum: the Hero's long
   headline measured 1193px in a 736px column, 381px past the block. */
.site-page-content :is(.d-flex.flex-column, .row > [class*="col-"]) > :not(.row) {
    min-width: 0;
    max-width: 100%;
}

/* ── Section heading alignment follows the page's presentation ─────────────
   18 of the 44 section partials centre their heading and 26 left-align, with no
   rule deciding which — so one page read as several documents stitched
   together: "Overview" and "Quick links" left, "…at a glance" and "Around the
   team" centred, at different sizes.

   Rather than restyle 18 partials (a redesign, and a risky sweep), alignment
   becomes a property of the presentation the page already declares. The
   workmanlike modes left-align everything; the editorial ones keep centring,
   because that is what they are for. One rule, and the visual_mode control now
   decides something a reader can see.

   Set on the HEADING itself rather than on a wrapper. The first attempt used
   `.text-center:has(> h2)`, which only matched where the h2 is a DIRECT child —
   and the wrapper structure varies across the palette, so a balanced page still
   came back with two different alignments. Overriding the inherited value on
   the h2 catches every case regardless of how deeply it is wrapped, and leaves
   the centring of everything else in those wrappers (captions, button rows)
   exactly as it was. */
.site-mode-utility .site-page-content h2,
.site-mode-balanced .site-page-content h2 {
    text-align: left;
}


/* ============================================================================
   Asset library — moved out of per-view <style> blocks
   (design-pass wave B, R2B-1, 2026-09-23)
   ============================================================================

   Each rule here was a <style> block inside one Sites asset view. Scoped under
   a class that view puts on its own root, so nothing reaches another screen —
   `#drop-zone` alone is NOT unique: Mango GTM's content/assets/new uses the
   same id, and several other upload views use `drop-zone` as a class.

   `.sites-assets-new` — apps/sites/assets/new.html.erb (Upload assets). */

/* The drop zone's keyboard focus ring. #file-input is `visually-hidden`, not
   `d-none`, so the multi-file queue stays reachable by keyboard; the ring is
   drawn on the zone that CONTAINS the focused input, via :focus-within. */
.sites-assets-new #drop-zone:focus-within {
    outline: 2px solid var(--bs-primary, #0d6efd);
    outline-offset: 2px;
}

/* ============================================================================
   Library pages — rules moved out of per-view <style> blocks
   (R2B-2, design-system convergence wave B, 2026-09-23)
   ============================================================================
   Two views carried their own <style> block: the AI examples gallery and
   Saved sections. Each rule below is the old rule verbatim, scoped under a
   class ONLY that view's modals carry, so no name here can reach another
   view. Both views already declare this sheet (content_for :stylesheets), and
   the preview fragments they fetch are inserted with innerHTML into those
   modals — the same document, not an iframe — so the rules still reach them.
   Neither block had an @media or dark-mode rule; none is dropped. */

/* ── AI examples: the mock browser chrome (apps/sites/examples/index) ────────
   Drawn by the three preview fragments, examples/_preview_{section,page,site}.
   Scoped under .sites-examples-preview, the class on the three preview
   modals. The fragments' inline style attributes (dot colours, ghost-line
   widths) still win over these, as they did over the <style> block. */
.sites-examples-preview .ep-scale-wrapper {
    transform: scale(0.88);
    transform-origin: top center;
    margin-bottom: -6%;
}
.sites-examples-preview .ep-frame {
    background: #f0f0f0;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 16px rgba(0,0,0,0.10), 0 0 0 1px rgba(0,0,0,0.06);
}
.sites-examples-preview .ep-chrome {
    background: #e0e0e0;
    padding: 8px 12px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    border-bottom: 1px solid #d0d0d0;
}
.sites-examples-preview .ep-dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
}
.sites-examples-preview .ep-url-bar {
    flex: 1;
    background: #fff;
    border-radius: 6px;
    padding: 5px 12px;
    font-size: 11px;
    color: #6c757d;
    text-align: center;
    max-width: 520px;
    margin: 0 auto;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    border: 1px solid #d5d5d5;
}
.sites-examples-preview .ep-site-header {
    background: #fff;
    padding: 10px 28px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    border-bottom: 1px solid #e9ecef;
}
.sites-examples-preview .ep-logo-placeholder {
    width: 24px;
    height: 24px;
    border-radius: 5px;
    background: linear-gradient(135deg, #0d6efd 0%, #6610f2 100%);
}
.sites-examples-preview .ep-nav-link {
    font-size: 11px;
    color: #9ca3af;
    cursor: default;
}
.sites-examples-preview .ep-nav-active {
    color: #0d6efd;
    font-weight: 600;
}
/* The page under the example's sections is the page ground (R14, pre-merge
   review): it was white while a block on "Page background" painted the theme's
   page colour, the editor's old mismatch in a third place. */
.sites-examples-preview .ep-page-body {
    background: var(--site-page-ground);
    min-height: 200px;
}

/* Ghost hero placeholder */
.sites-examples-preview .ep-ghost-hero {
    text-align: center;
    padding: 28px 48px 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    border: 1.5px dashed #d1d5db;
    border-radius: 6px;
    margin: 16px 28px;
    background: #fafbfc;
}
.sites-examples-preview .ep-ghost-btn {
    width: 80px;
    height: 22px;
    border: 1.5px dashed #d1d5db;
    border-radius: 12px;
}

/* Ghost content below */
.sites-examples-preview .ep-ghost-below {
    padding: 20px 28px 24px;
}
.sites-examples-preview .ep-ghost-line {
    height: 10px;
    background: #e9ecef;
    border-radius: 3px;
    margin-bottom: 4px;
}
.sites-examples-preview .ep-ghost-card {
    flex: 1;
    padding: 14px;
    background: transparent;
    border: 1.5px dashed #d1d5db;
    border-radius: 6px;
}

/* Active section highlight */
.sites-examples-preview .ep-active-section {
    position: relative;
    border-left: 3px solid #0d6efd;
    box-shadow: inset 0 0 0 1px rgba(13, 110, 253, 0.10);
    background: rgba(13, 110, 253, 0.012);
}
.sites-examples-preview .ep-section-label {
    position: absolute;
    top: 8px;
    left: 8px;
    background: #0d6efd;
    color: #fff;
    font-size: 10px;
    font-weight: 600;
    padding: 2px 8px;
    border-radius: 3px;
    z-index: 2;
    letter-spacing: 0.02em;
    pointer-events: none;
}
.sites-examples-preview .ep-section-render {
    padding: 0;
    overflow: hidden;
    /* The site example's pages draw their sections straight onto the grey
       browser frame (examples/_preview_site); each sits on the page ground. */
    background: var(--site-page-ground);
}
.sites-examples-preview .ep-section-render > section,
.sites-examples-preview .ep-section-render > div {
    margin: 0;
}

/* Site preview sidebar */
.sites-examples-preview .ep-site-sidebar {
    width: 200px;
    min-width: 200px;
    background: #f8f9fa;
    border-right: 1px solid #e9ecef;
    overflow-y: auto;
}
.sites-examples-preview .ep-sidebar-link {
    display: block;
    /* Neutralize the native <button> chrome so the tab renders exactly as the
       old anchor did (full-width, left-aligned, no border/background). */
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    padding: 6px 12px;
    font-size: 11px;
    color: #495057;
    text-decoration: none;
    border-radius: 4px;
    margin-bottom: 2px;
    cursor: pointer;
}
.sites-examples-preview .ep-sidebar-link:hover {
    background: #e9ecef;
    color: #212529;
}
.sites-examples-preview .ep-sidebar-link.active {
    background: #0d6efd;
    color: #fff;
}

/* Full page/site section separators */
.sites-examples-preview .page-preview-content .ep-section-render,
.sites-examples-preview .site-page-panel .ep-section-render {
    border-bottom: 1px solid #f0f0f0;
}
.sites-examples-preview .page-preview-content .ep-section-render:last-child,
.sites-examples-preview .site-page-panel .ep-section-render:last-child {
    border-bottom: none;
}

/* ── Saved sections: preview frame + insert picker (section_templates/index) ─
   .st-preview-meta / .st-preview-surface are drawn by the preview fragment
   (section_templates/_preview); .st-insert-option is a row of the "Insert
   into page…" picker. Scoped under .sites-section-templates, the class on
   the page's two modals. */
.sites-section-templates .st-preview-meta {
    padding: 10px 16px;
    border-bottom: 1px solid #e9ecef;
    background: #f8f9fa;
}
.sites-section-templates .st-preview-surface {
    /* The page ground (R14): a saved section shows on what its page shows it on. */
    background: var(--site-page-ground);
    overflow-x: auto;
}
.sites-section-templates .st-preview-surface > section,
.sites-section-templates .st-preview-surface > div {
    margin: 0;
}
.sites-section-templates .st-insert-option.active {
    background-color: var(--bs-primary, #0d6efd);
    color: var(--bs-on-primary, #fff);
}
.sites-section-templates .st-insert-option.active .text-muted {
    color: color-mix(in srgb, var(--bs-on-primary, #fff) 75%, transparent) !important;
}

/* ============================================================================
   Page editor + page reader — rules moved out of per-view <style> blocks
   (R2B-4, design-system convergence wave B, 2026-09-23)
   ============================================================================
   Two views carried their own <style>: the full-screen page editor
   (apps/sites/pages/edit) and the reader's print door (apps/sites/pages/show).
   Both already declare this sheet (content_for :stylesheets). Every rule below
   is the old rule verbatim, with its old comment, scoped so it cannot reach
   another screen:

   - Editor: under .sites-editor-workspace, the editor's own root (the fixed,
     full-screen region). Several names here are NOT unique on their own —
     Inspections' template builder uses .section-drag-handle, and an unscoped
     .site-section-preview:hover would outline sections on every Sites page
     that loads this sheet. The canvas is a plain div in the same document (the
     editor has no iframe; the srcdoc frames are the Preview page's), so every
     rule still reaches it, and the modals the editor mounts on document.body
     use none of these names. The root rule and the body scroll lock keyed on
     the root's existence are self-scoped. The @keyframes gained a
     sites-editor- prefix, because a keyframes name is global.
   - Reader print: under body.site-reader-layout, the reading layout's own body
     class. */

/* ── Page editor (apps/sites/pages/edit) ─────────────────────────────────── */

/* The workspace layer. 1040 is chosen, not arbitrary: above the platform
   navbar (1030) and the AdminKit sidebar (5), below a Bootstrap offcanvas
   (1045) and modal (1055) — which is exactly what lets the publish drawer
   and every nested dialog open over the editor with no override anywhere.
   Everything else about the shell (fixed, inset 0, flex column) is Bootstrap
   utilities on the element. */
.sites-editor-workspace { z-index: 1040; background: #f0f2f5; }
/* The document behind the workspace is still scrollable and its scrollbar is
   still drawn — the Bootstrap modal used to hide both via .modal-open. This
   is that one effect, kept.
   KEYED ON THE WORKSPACE EXISTING, not on a class this script adds. A body
   class is the one piece of editor state that lives OUTSIDE this view's
   markup, so it does not leave when the DOM does — it can only be removed by
   code that runs on the way out. TurboLifecycle's cleanup is reliable (it is
   stamped by render generation, not filtered by pathname — see
   _runAllCleanups), but "reliable" is not the same as "unconditional": a hard
   navigation, a crash mid-teardown or a future refactor that drops the
   unregister all leave the lock on with nothing left to lift it, and the
   symptom is an app that cannot scroll until you reload. A CSS rule cannot
   outlive its element, so there is nothing to lift. Browsers without `:has()`
   (Firefox < 121) merely keep a scrollbar. */
body:has(.sites-editor-workspace) { overflow: hidden; }

/* THE PAGE GROUND, DEFINED ONCE (Sites tester round 4, R14: "a new block's
   background is white, and turns grey as soon as you edit it"). A block with
   no background shows whatever is behind the page, and the panel's first
   swatch — "Page background", what it selects for such a block and what the
   first auto-save stores (Sites::SectionPalette "none") — painted the theme's
   page colour. The editor canvas was white, so the two differed there and
   nowhere else, and the block changed colour under the first keystroke.
   One value now: the tenant theme's page background, the one the reading
   layout paints its body with (layouts/site_reader). The editor canvas, the
   "none" token and the reading layouts all read it. */
:root {
    --site-page-ground: var(--page-bg, var(--bs-body-bg, #f5f7fb));
}
/* The same ground as a class, for a preview box that draws a page's sections
   (pages/new's starter preview). */
.site-page-ground {
    background: var(--site-page-ground);
}

/* Full-screen editor layout.

   The canvas is two elements: this one positions the AI dock and does not
   scroll, and -scroll inside it is the only scrolling region. Putting the
   overflow on the positioning context would carry the dock away with the
   page it floats over.

   The field around the page is the page ground a step darker (it was a fixed
   #f0f2f5 around a white page): the page is now the page ground itself, and a
   field derived from it keeps the page reading as a page on any theme. */
.sites-editor-workspace .site-editor-preview {
    flex: 1;
    min-width: 0;
    position: relative;
    overflow: hidden;
    background: color-mix(in srgb, var(--site-page-ground) 93%, #000000);
}
.sites-editor-workspace .site-editor-preview-scroll {
    height: 100%;
    overflow-y: auto;
    padding: 1.5rem;
}
.sites-editor-workspace .site-editor-preview #site-page-preview {
    max-width: 900px;
    margin: 0 auto;
    border-radius: 0.5rem;
    box-shadow: 0 1px 8px rgba(0,0,0,0.08);
    min-height: 100%;
}
/* The canvas is the page the reader sees: the page ground (above). */
#site-page-preview {
    background: var(--site-page-ground);
}
/* The Pages panel (apps/sites/pages/_pages_panel, one tree 2026-09-30): the
   editor's left column — the site's menu as a tree, then the pages not in it.
   Closed, a 2.75rem rail with "Pages"; open, 17.5rem beside the canvas from xl
   up, and floating over the canvas's left edge below xl so a narrow editor
   keeps its page. */
.sites-editor-workspace .sites-editor-body { position: relative; }
.sites-editor-workspace .sites-pages-panel {
    flex: 0 0 2.75rem;
    width: 2.75rem;
    min-width: 0;
    border-right: 1px solid var(--bs-border-color);
    background: var(--bs-body-bg);
    overflow: hidden;
}
.sites-editor-workspace[data-pages-panel="open"] .sites-pages-panel__rail { display: none; }
.sites-pages-panel__rail {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.75rem 0;
    color: var(--bs-secondary-color);
    text-decoration: none;
}
.sites-pages-panel__rail:hover, .sites-pages-panel__rail:focus-visible { color: var(--bs-link-color, var(--bs-primary)); }
.sites-pages-panel__rail-label { writing-mode: vertical-rl; transform: rotate(180deg); font-size: var(--type-small, 13px); font-weight: 600; }
.sites-editor-workspace .sites-pages-panel__content[hidden] { display: none; }
@media (min-width: 1200px) {
    .sites-editor-workspace[data-pages-panel="open"] .sites-pages-panel { flex-basis: 17.5rem; width: 17.5rem; }
}
@media (max-width: 1199.98px) {
    .sites-editor-workspace .sites-pages-panel__content {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        width: 17.5rem;
        z-index: 30;
        background: var(--bs-body-bg);
        border-right: 1px solid var(--bs-border-color);
        box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15);
    }
}
.sites-pages-panel__body { overflow-y: auto; min-height: 0; }
/* ON THE TYPE SCALE (§5A, 2026-10-02). The rows were 0.875rem = 15.4px under
   this app's 110% root: larger than the tenant's body text (14px), than their
   own group headings (14.3px), and as large as the panel's title — a 51-page
   list read as the loudest thing on the editor. Now title 16/600 › group
   heading 13/600 › row 13 (the scale's dense-row step) › count and hint 12.
   The bare `.small` texts were 80% of a 14px body = 11.2px, under the floor. */
.sites-pages-panel__head h2 { font-size: var(--type-title, 16px); font-weight: 600; }
.sites-pages-panel__inner .small { font-size: var(--type-caption, 12px); }
.sites-pages-panel__summary {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.375rem 0.5rem;
    font-size: var(--type-small, 13px);
    font-weight: 600;
    color: var(--bs-secondary-color);
}
.sites-pages-panel__summary::-webkit-details-marker { display: none; }
.sites-pages-panel__chevron { font-size: 0.6875rem; }
.sites-pages-panel__list { min-height: 0.5rem; }
.sites-pages-panel__row {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.1875rem 0.25rem 0.1875rem 0;
    border-radius: 0.375rem;
    font-size: var(--type-small, 13px);
    min-width: 0;
}
.sites-pages-panel__row:hover { background: var(--bs-tertiary-bg); }
.sites-pages-panel__row.is-current { background: var(--bs-primary-bg-subtle); }
.sites-pages-panel__row.is-current .sites-pages-panel__link { color: var(--bs-primary-text-emphasis); font-weight: 600; }
.sites-pages-panel__grip {
    flex: 0 0 auto;
    width: 1.25rem;
    text-align: center;
    color: var(--bs-secondary-color);
    cursor: grab;
    opacity: 0.55;
}
.sites-pages-panel__row:hover .sites-pages-panel__grip { opacity: 1; }
.sites-pages-panel__link {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--bs-body-color);
    text-decoration: none;
    white-space: nowrap;
}
.sites-pages-panel__link:hover { text-decoration: underline; }
.sites-pages-panel__actions { flex: 0 0 auto; }
.sites-pages-panel__actions .btn { padding: 0 0.375rem; border-color: transparent; }
.sites-pages-panel__row:hover .sites-pages-panel__actions .btn,
.sites-pages-panel__actions .btn:focus-visible,
.sites-pages-panel__actions .btn[aria-expanded="true"] { border-color: var(--bs-border-color); }
.sites-pages-panel__children { padding-left: 1.25rem; min-height: 0; }
/* ONE STRIP OPENS, AND ONLY WHEN ASKED FOR (Sites QA round 3, 2026-10-01).
   Every dropdown's sub-item list used to open a strip during ANY drag, and
   crossing one on the way down the list nested the row by accident. Now
   only the list whose parent the author holds over, or pushes the row
   right into, opens (sites_pages_panel_controller#_evaluateNest) — boxed,
   indented, and saying where the row will go while it is empty. */
.sites-pages-panel__children.is-drop-target {
    min-height: 1.75rem;
    margin: 0.125rem 0 0.25rem 1.25rem;
    padding-left: 0;
    border: 1px dashed var(--bs-primary);
    border-radius: 0.375rem;
}
.sites-pages-panel__children.is-drop-target:not(:has(> li))::before {
    content: attr(aria-label);
    display: block;
    padding: 0.25rem 0.5rem;
    font-size: 0.75rem;
    color: var(--bs-primary-text-emphasis);
}
.sites-pages-panel__row.is-drop-parent { box-shadow: inset 0 0 0 1px var(--bs-primary-border-subtle); }
/* THE DRAGGED ROW'S PLACEHOLDER IS NOT THE CURRENT PAGE. It was the current
   page's own tint at 40% opacity, so a row a failed drop left behind read as
   a faded "you are here" (and the opacity carried into its ⋮ menu). A dashed
   slot at full opacity, its name in the secondary ink. */
.sites-pages-panel__ghost > .sites-pages-panel__row {
    background: var(--bs-body-bg);
    outline: 2px dashed var(--bs-primary-border-subtle);
    outline-offset: -2px;
}
.sites-pages-panel__ghost > .sites-pages-panel__row .sites-pages-panel__link { color: var(--bs-secondary-color); }
/* Saving: rows take no pointer and dim their TEXT only — opacity on the whole
   body also faded any ⋮ menu open inside it. */
.sites-pages-panel__inner[aria-busy="true"] .sites-pages-panel__body { pointer-events: none; }
.sites-pages-panel__inner[aria-busy="true"] .sites-pages-panel__link,
.sites-pages-panel__inner[aria-busy="true"] .sites-pages-panel__grip { opacity: 0.6; }
/* How to put a page under another by drag, said while dragging — in the foot,
   so the rows under the pointer do not move. */
.sites-pages-panel__drag-hint { display: none; }
.sites-pages-panel--dragging .sites-pages-panel__drag-hint { display: block; }
.sites-pages-panel--dragging .sites-pages-panel__foot-link { display: none; }

/* THE STUCK NAVBAR SITS ABOVE THE SECTION TOOLBARS. custom.css pins the site
   navbar at z-index 3 and the toolbars below are at 10, so as a section's top
   edge scrolled under the stuck bar its blue toolbar was drawn straight
   across the navbar's links — the "floating" look. Same selector as the
   sticky rule it overrides, plus this ancestor, so it wins on specificity
   and applies only inside the editor's canvas; the reader keeps its own
   stacking. A toolbar under the bar is unreachable for the few pixels it is
   covered, which is how every sticky header behaves. */
.sites-editor-workspace .site-editor-preview .site-navbar.site-navbar-sticky.navbar {
    z-index: 20 !important;
}

/* The dock is narrower than the Ask AI sidebar these defaults were written
   for, so the transcript is tightened to match. Everything else about it —
   placement, the open/closed shapes — lives with the component in
   custom.css under .mgx-ai-dock. */
.sites-editor-workspace #sites-ai .chat-messages-container { padding: 0.75rem; }
.sites-editor-workspace #sites-ai .chat-messages { min-height: 100px; gap: 0.75rem; }
.sites-editor-workspace #sites-ai .chat-input-container { padding: 0.625rem 0.75rem; }
.sites-editor-workspace #sites-ai .message { max-width: 92%; margin-bottom: 0; font-size: 0.85rem; }
.sites-editor-workspace #sites-ai .message-content { padding: 10px 14px; border-radius: 16px; }
.sites-editor-workspace #sites-ai .chat-welcome { padding: 1.5rem 1rem; }

/* Section hover + toolbar */
.sites-editor-workspace .site-section-preview {
    position: relative;
    transition: outline 0.2s ease;
}
/* INSET, NOT OUTSET. Both outlines used to sit 2px OUTSIDE the section's box,
   which puts their top line inside whatever is above (the stuck navbar, for
   the first section) and their bottom line inside the next section — and the
   next section's own background paints over it. The selected hero showed as
   a box with no top and a clipped bottom. Drawn 2px inside the box, the
   outline lives in pixels only this section owns, and nothing adjacent can
   cover it at any scroll position. */
.sites-editor-workspace .site-section-preview:hover {
    outline: 2px solid rgba(13, 110, 253, 0.3);
    outline-offset: -2px;
    border-radius: 4px;
}
/* THE SECTION THE PANEL IS EDITING STAYS LIT. Hover was the only outline, so
   the one section guaranteed NOT to be marked was the one being edited — the
   mouse is over on the panel, not the canvas. Solid rather than the hover
   tint, and after it in source order so it wins where both apply. */
.sites-editor-workspace .site-section-preview.is-editing {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
    border-radius: 4px;
}
/* Its toolbar stays up too — that is the strip carrying the section's NAME,
   which is how you confirm the panel and the canvas are talking about the
   same thing. */
.sites-editor-workspace .site-section-preview.is-editing .section-toolbar {
    opacity: 1;
    pointer-events: auto;
}
.sites-editor-workspace .section-toolbar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 10;
    opacity: 0;
    transition: opacity 0.2s ease;
    pointer-events: none;
}
.sites-editor-workspace .site-section-preview:hover .section-toolbar {
    opacity: 1;
    pointer-events: auto;
}
/* Keyboard equivalent of the hover reveal above. Without this the toolbar
   stays pointer-events:none, so .focus() on an edit/duplicate/save-template/
   delete/move button never lands and the per-section controls are literally
   unreachable without a mouse. */
.sites-editor-workspace .site-section-preview:focus-within .section-toolbar {
    opacity: 1;
    pointer-events: auto;
}
.sites-editor-workspace .section-toolbar-inner {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0.5rem;
    background: rgba(13, 110, 253, 0.9);
    color: white;
    font-size: 0.75rem;
    border-radius: 0 0 0.375rem 0.375rem;
    max-width: fit-content;
    margin: 0 auto;
}
.sites-editor-workspace .section-toolbar-type {
    text-transform: capitalize;
    font-weight: 500;
    opacity: 0.9;
}
.sites-editor-workspace .section-toolbar-actions {
    display: flex;
    gap: 0.25rem;
    margin-left: 0.5rem;
}
.sites-editor-workspace .section-toolbar-btn {
    background: none;
    border: none;
    color: white;
    padding: 0.15rem 0.35rem;
    border-radius: 0.25rem;
    cursor: pointer;
    font-size: 0.7rem;
    opacity: 0.85;
}
/* The pencil while its section is in the panel: a pressed key, not a plain
   icon, so the toggle reads as one. */
.sites-editor-workspace .section-toolbar-btn[aria-pressed="true"] {
    background: rgba(255, 255, 255, 0.28);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7);
    border-radius: 0.25rem;
}
.sites-editor-workspace .section-toolbar-btn:hover {
    background: rgba(255,255,255,0.2);
    opacity: 1;
}
.sites-editor-workspace .section-toolbar-btn-danger:hover {
    background: rgba(220, 53, 69, 0.8);
}
.sites-editor-workspace .section-drag-handle {
    cursor: grab;
    padding: 0.15rem 0.25rem;
    opacity: 0.7;
}
.sites-editor-workspace .section-drag-handle:hover { opacity: 1; }

/* Drag states */
.sites-editor-workspace .site-section-ghost {
    opacity: 0.4;
    outline: 2px dashed #0d6efd;
    outline-offset: 2px;
}
.sites-editor-workspace .site-section-chosen {
    outline: 2px solid #0d6efd;
    outline-offset: 2px;
}

/* Highlight animation (when AI updates a section) */
.sites-editor-workspace .site-section-preview.highlighted {
    outline: 2px solid #0d6efd;
    outline-offset: 4px;
    border-radius: 4px;
    animation: sites-editor-section-flash 2s ease-out;
}
@keyframes sites-editor-section-flash {
    0% { background-color: rgba(13, 110, 253, 0.1); }
    100% { background-color: transparent; }
}

/* Responsive: stack on small screens. The dock needs no rule here — it
   floats over the canvas at every width and goes full-bleed on its own
   (see the media query on .mgx-ai-dock). */
@media (max-width: 767.98px) {
    .sites-editor-workspace .sites-editor-body { flex-direction: column; }
    .sites-editor-workspace .site-editor-preview-scroll { padding: 0.5rem; }
}

/* ── Page reader: print / save as PDF (apps/sites/pages/show) ─────────────
   Hide the reader bar, the app rail and the toasts on paper; the page body
   prints as the reader sees it. This was a <style media="print"> that
   pages/show wrote into <head> only on its ?print=1 door ("Print or save as
   PDF" in the page menu, which opens the reading layout and calls
   window.print()). Keyed on the reading layout's body class instead, so the
   browser's own Print on a reader page now gets the same clean paper; the
   print=1 script that opens the dialog stays in the view. */
@media print {
    body.site-reader-layout .site-reader-bar,
    body.site-reader-layout #sidebar-rail,
    body.site-reader-layout .sidebar-rail,
    body.site-reader-layout .toast-container,
    body.site-reader-layout .js-ask-page-form { display: none !important; }
    body.site-reader-layout { background: #fff !important; }
}

/* ============================================================================
   Dark mode (/design-pass sites --dark, 2026-09-23)
   ============================================================================
   Until this block the sheet carried no dark rule. A site's CONTENT follows the
   viewer's theme — its sections paint on the dark page in the reader, the editor
   and the previews — but the frames drawn around that content were written for a
   white page, and one section control was written for AdminKit's LIGHT palette.
   Tokens only; the platform dark block defines --bs-body-bg / --bs-card-bg. */

/* 1. The editor's canvas field was a literal #f0f2f5: a pale slab filling the
      editor around a page whose sections render dark. The workspace takes the
      page background the reader shows the same page on; the field around the
      canvas — which is that page ground itself since R14 — a step darker, so
      the page still reads as a page. */
:root[data-theme="dark"] .sites-editor-workspace { background: var(--bs-body-bg); }
:root[data-theme="dark"] .sites-editor-workspace .site-editor-preview { background: color-mix(in srgb, var(--site-page-ground) 72%, #000000); }

/* 2. The AI examples' mock browser and the saved-section preview. Their page body
      and site chrome were literal whites while the sections inside render with the
      dark theme's light text — the example site preview drew "Department news" and
      its three announcement cards white on white, and the site's own name in the
      mock sidebar near-invisible. The frame goes dark with the content it frames. */
:root[data-theme="dark"] .sites-examples-preview .ep-frame {
    background: var(--bs-body-bg);
    box-shadow: 0 2px 16px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--bs-border-color);
}
:root[data-theme="dark"] .sites-examples-preview .ep-chrome {
    background: color-mix(in srgb, var(--bs-card-bg, #333) 85%, var(--bs-emphasis-color, #fff));
    border-bottom-color: var(--bs-border-color);
}
:root[data-theme="dark"] .sites-examples-preview .ep-url-bar {
    background: var(--bs-body-bg);
    border-color: var(--bs-border-color);
    color: var(--text-muted, #b0b4b9);
}
:root[data-theme="dark"] .sites-examples-preview .ep-site-header,
:root[data-theme="dark"] .sites-examples-preview .ep-site-sidebar,
:root[data-theme="dark"] .sites-section-templates .st-preview-meta {
    background: var(--bs-card-bg, #333);
    border-color: var(--bs-border-color);
}
:root[data-theme="dark"] .sites-examples-preview .ep-page-body,
:root[data-theme="dark"] .sites-section-templates .st-preview-surface { background: var(--site-page-ground); }
:root[data-theme="dark"] .sites-examples-preview .ep-sidebar-link { color: var(--bs-body-color); }
:root[data-theme="dark"] .sites-examples-preview .ep-sidebar-link:hover {
    background: color-mix(in srgb, var(--bs-card-bg, #333) 85%, var(--bs-emphasis-color, #fff));
    color: var(--bs-emphasis-color);
}
:root[data-theme="dark"] .sites-examples-preview .ep-ghost-hero { background: transparent; }
:root[data-theme="dark"] .sites-examples-preview .ep-ghost-hero,
:root[data-theme="dark"] .sites-examples-preview .ep-ghost-btn,
:root[data-theme="dark"] .sites-examples-preview .ep-ghost-card { border-color: var(--bs-border-color); }
:root[data-theme="dark"] .sites-examples-preview .ep-ghost-line { background: var(--bs-border-color); }
:root[data-theme="dark"] .sites-examples-preview .page-preview-content .ep-section-render,
:root[data-theme="dark"] .sites-examples-preview .site-page-panel .ep-section-render { border-bottom-color: var(--bs-border-color); }

/* 3. A section's secondary button on a dark band. _hero, _feature_cards_grid and
      friends pick `btn-outline-light` for exactly that case, and AdminKit dark
      redefines "light" as #2d3846 — so the hero's "Meet the team" rendered navy on
      the navy band (1.29:1) in the reader, the editor and the preview. On a band the
      section already judged dark, light means light. */
:root[data-theme="dark"] .site-page-content .btn-outline-light {
    --bs-btn-color: #f8f9fa;
    --bs-btn-border-color: #f8f9fa;
    --bs-btn-hover-color: #212529;
    --bs-btn-hover-bg: #f8f9fa;
    --bs-btn-hover-border-color: #f8f9fa;
    --bs-btn-active-color: #212529;
    --bs-btn-active-bg: #f8f9fa;
    --bs-btn-active-border-color: #f8f9fa;
    --bs-btn-disabled-color: #f8f9fa;
    --bs-btn-disabled-border-color: #f8f9fa;
}

/* 4. The stats bar's delta — RETIRED 2026-09-24 (Sites QA round 5). This rule
      gave the delta the band's own ink on a dark band, because no green or red
      can be promised on an arbitrary brand; it left a 0.7em arrow as the only
      difference between Up and Down ("nothing appears to change"). The change is
      now a chip that paints its own ground in both themes — "THE STATS BAR"
      block directly below — and _stats_bar no longer emits the data-bs-theme
      hook this rule keyed on. */

/* ============================================================================
   THE STATS BAR (sites/shared/_stats_bar) — 2026-09-24, Sites QA round 5
   ============================================================================
   Five parts per stat, drawn in the order the panel asks for them: value,
   change (with its trend), label, benchmark, description.

   THE CHANGE IS ITS OWN LINE (R14). It sat inside the display-6 value line, so
   a change that wrapped took the 44px number's 52.8px line height: measured on
   /pages/10508 at 1280, "this is the change" broke into two 18px lines 53px
   apart. Now its own element with its own 1.35 line height — measured 19px
   between wrapped lines at 1024.

   THE TREND IS A CHIP (R17). Its direction draws the arrow (up / down / flat)
   and its verdict the chip — good, bad or neutral; "down_good" and "up_bad"
   exist because a falling number is often the good news. Each chip is
   Bootstrap 5.3's own subtle triple (the -bg-subtle fill, the -border-subtle
   edge, the -text-emphasis ink — what .alert-success is made of), written as
   literals so neither theme's variables can move them. The chip paints its
   own ground, so its contrast does not depend on the section's: measured ink
   and arrow on fill 10.35:1 good, 10.22:1 bad, 10.51:1 neutral, on brand,
   brand solid, accent, neutral dark, white, soft grey, both tints and custom
   green / red / yellow bands, in both themes. On a dark band the pale chip
   stands off it at 3.5:1 or better; on a light one its edge is faint and the
   ink carries it.

   BENCHMARK AND DESCRIPTION LOOK DIFFERENT (R15) because they are different:
   the benchmark is a figure to compare with — scale glyph, medium weight,
   tabular figures; the description is a line of context — regular, italic.
   Both at the band's full ink: the 0.75 opacity they carried took contrast
   they cannot spare on a band measured only to clear AA for its full ink.
   The label leads the two lines under it by size and weight (see "LABEL
   LEADS, BENCHMARK FOLLOWS" below). Sizes stay on the reading scale: label
   .9375rem, chip, benchmark and description .8125rem — nothing under 12px.

   WHY `!important` ON THE SIZES, and only there — the same reason as
   `.site-section-sub` above: below 576px mobile_optimizations.css sets every
   div and span to 1rem !important, which flattened the value, the label and
   both small lines to one size (the value measured 17.6px at 375, under its
   own label's line). The value gets its display-6 size back there too.

   THE ROWS LINE UP ACROSS STATS. A stat with no change used to put its label
   a chip's height above its neighbours'. Each stat is a subgrid of five rows,
   so labels, benchmarks and descriptions share a line whatever each stat
   carries. Two stats to a row by default, the odd last one centred across
   both; from 560px of BLOCK width (`site-cq` on the partial's container —
   see "BLOCK-WIDTH BREAKPOINTS") they share rows ("MANY STATS WRAP" below).
   A browser without subgrid still stacks each stat correctly, unaligned. */
.site-page-content .site-stats {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
}
.site-page-content .site-stats > .site-stat {
    grid-row: span 5;
    display: grid;
    grid-template-rows: subgrid;
    /* minmax(0, 1fr), not the implicit `auto` column: an auto column is as wide
       as the longest word in the stat, so a long label pushed the value and the
       label 115px past the stat (finding 5990). */
    grid-template-columns: minmax(0, 1fr);
}
.site-page-content .site-stats > .site-stat:nth-child(n+3) { padding-top: 1.5rem; }
.site-page-content .site-stats > .site-stat:last-child:nth-child(odd) { grid-column: 1 / -1; }
.site-page-content .site-stat-value     { grid-row: 1; }
.site-page-content .site-stat-change    { grid-row: 2; margin-top: .5rem; }
.site-page-content .site-stat-label     { grid-row: 3; }
.site-page-content .site-stat-benchmark { grid-row: 4; }
.site-page-content .site-stat-desc      { grid-row: 5; }

/* MANY STATS WRAP (finding 5990). From 560px the stats share a row while each
   column keeps 8rem — the width a display-6 "150+" needs — and at most six (or
   the partial's balanced --site-stats-cols past six) sit in one row, so nine
   are five and four. It was every stat in one row whatever the count, and at
   nine the values ran past their columns. Each band of five subgrid rows is
   spaced by its stats' bottom padding, taken back under the last band. */
@container site-cq (min-width: 560px) {
    .site-page-content .site-stats {
        grid-template-columns: repeat(auto-fit, minmax(max(8rem, calc(100% / var(--site-stats-cols, 6))), 1fr));
        margin-bottom: -1.5rem;
    }
    .site-page-content .site-stats > .site-stat { padding-bottom: 1.5rem; }
    .site-page-content .site-stats > .site-stat:nth-child(n+3) { padding-top: 0; }
    .site-page-content .site-stats > .site-stat:last-child:nth-child(odd) { grid-column: auto; }
}
/* …and once the block is wide enough for every column the partial asked for
   (4, 5 or 6 × 8rem, never under the 560px where the two-up rules stop), the
   columns are exactly that many, on a grid of half columns, so the short last
   row can be centred: its first stat starts one half-column in per stat it is
   short (`site-stat--shift-N`, N ≤ 5). Narrower, the auto-fit rows above take
   over and the last row starts at the left. `:nth-child(n)` lifts these over
   the two-up rule that sizes an odd last stat (`:last-child:nth-child(odd)`),
   which otherwise left the ninth stat one half-column wide. */
@container site-cq (min-width: 35rem) {
    .site-page-content .site-stats.site-stats--cols-4 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
    .site-page-content .site-stats.site-stats--cols-4 > .site-stat:nth-child(n) { grid-column: span 2; }
}
@container site-cq (min-width: 40rem) {
    .site-page-content .site-stats.site-stats--cols-5 { grid-template-columns: repeat(10, minmax(0, 1fr)); }
    .site-page-content .site-stats.site-stats--cols-5 > .site-stat:nth-child(n) { grid-column: span 2; }
}
@container site-cq (min-width: 48rem) {
    .site-page-content .site-stats.site-stats--cols-6 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
    .site-page-content .site-stats.site-stats--cols-6 > .site-stat:nth-child(n) { grid-column: span 2; }
}
@container site-cq (min-width: 35rem) {
    .site-page-content .site-stats.site-stats--cols-4 > .site-stat--shift-1:nth-child(n) { grid-column: 2 / span 2; }
    .site-page-content .site-stats.site-stats--cols-4 > .site-stat--shift-2:nth-child(n) { grid-column: 3 / span 2; }
    .site-page-content .site-stats.site-stats--cols-4 > .site-stat--shift-3:nth-child(n) { grid-column: 4 / span 2; }
}
@container site-cq (min-width: 40rem) {
    .site-page-content .site-stats.site-stats--cols-5 > .site-stat--shift-1:nth-child(n) { grid-column: 2 / span 2; }
    .site-page-content .site-stats.site-stats--cols-5 > .site-stat--shift-2:nth-child(n) { grid-column: 3 / span 2; }
    .site-page-content .site-stats.site-stats--cols-5 > .site-stat--shift-3:nth-child(n) { grid-column: 4 / span 2; }
    .site-page-content .site-stats.site-stats--cols-5 > .site-stat--shift-4:nth-child(n) { grid-column: 5 / span 2; }
}
@container site-cq (min-width: 48rem) {
    .site-page-content .site-stats.site-stats--cols-6 > .site-stat--shift-1:nth-child(n) { grid-column: 2 / span 2; }
    .site-page-content .site-stats.site-stats--cols-6 > .site-stat--shift-2:nth-child(n) { grid-column: 3 / span 2; }
    .site-page-content .site-stats.site-stats--cols-6 > .site-stat--shift-3:nth-child(n) { grid-column: 4 / span 2; }
    .site-page-content .site-stats.site-stats--cols-6 > .site-stat--shift-4:nth-child(n) { grid-column: 5 / span 2; }
    .site-page-content .site-stats.site-stats--cols-6 > .site-stat--shift-5:nth-child(n) { grid-column: 6 / span 2; }
}

.site-page-content .site-stats .site-stat-chip {
    display: inline-block;
    max-width: 100%;
    padding: .2em .75em;
    border: 1px solid;
    border-radius: 1em;
    font-size: .8125rem !important;
    font-weight: 600;
    line-height: 1.35;
    overflow-wrap: anywhere;
}
.site-page-content .site-stats .site-stat-chip i { font-size: .85em; margin-inline-end: .4em; }
.site-page-content .site-stats .site-stat-chip--good    { color: #0a3622; background-color: #d1e7dd; border-color: #a3cfbb; }
.site-page-content .site-stats .site-stat-chip--bad     { color: #58151c; background-color: #f8d7da; border-color: #f1aeb5; }
.site-page-content .site-stats .site-stat-chip--neutral { color: #2b2f32; background-color: #e2e3e5; border-color: #c4c8cb; }

/* LABEL LEADS, BENCHMARK FOLLOWS (2026-09-29, Sites QA: "the label and the
   benchmark look the same"). They were .85rem / 600 over .8125rem / 500 in the
   same ink — a 0.6px and one-weight step nobody can see. Colour cannot carry
   the difference: on a dark band the ink is measured to clear AA with nothing
   to spare, so a paler benchmark would fall under it. Size and weight do: the
   label is the stat's name at .9375rem / 600, the benchmark a regular-weight
   .8125rem line with its glyph, and a little space holds them apart. Nothing
   under 12px; the same on every ground, because neither touches colour. */
.site-page-content .site-stats .site-stat-label {
    margin-top: .5rem;
    font-size: .9375rem !important;
    font-weight: 600;
    line-height: 1.35;
}
.site-page-content .site-stats .site-stat-benchmark {
    margin-top: .375rem;
    font-size: .8125rem !important;
    font-weight: 400;
    line-height: 1.45;
    letter-spacing: .01em;
    font-variant-numeric: tabular-nums;
}
.site-page-content .site-stats .site-stat-benchmark i { font-size: .9em; margin-inline-end: .4em; }
.site-page-content .site-stats .site-stat-desc {
    margin-top: .25rem;
    font-size: .8125rem !important;
    font-style: italic;
    line-height: 1.45;
}
@media (max-width: 575.98px) {
    .site-page-content .site-stats .site-stat-value { font-size: calc(1.375rem + 1.5vw) !important; }
}

/* ============================================================================
   KEY METRICS (sites/shared/_data_insights) — 2026-09-26, Sites QA round 6
   ============================================================================
   "Adjust the font sizes to create stronger visual hierarchy … so the key
   metrics stand out more clearly." Measured on /pages/14240 at 1280: the value
   (an `h4`) 19.3px, its icon 19.3px, the label 18.7px — three lines of nearly
   one size, so a metric read as copy, and a wrapped label ran into the metric
   under it (the row gap is the partial's `gy-5`).

   THE VALUE takes the Stats Bar's display size — Bootstrap's display-6,
   calc(1.375rem + 1.5vw) up to 2.5rem (44px) — so the two metric blocks read
   as one family on a page. Written out rather than as the `display-6` class
   for the reason the Stats Bar gives: below 576px mobile_optimizations.css
   sets every h3 to 1.4rem `!important`, which only an `!important` size here
   gets past.

   THE ICON is a fixed share of the value (.7em), so it scales with the number
   it marks instead of being set on its own; it keeps no colour of its own (the
   value's ink — see the partial). .7em of a 44px value lands on the digits'
   cap height when it sits on the baseline.

   THE LABEL steps down to 15px, §5A's body step — the Stats Bar label's size
   and the caption the icon blocks settled on (_mini_icon_grid_block,
   _connected_callout, _journey_map) — at a caption's 1.45, where the page's
   1.65 reading leading spread a wrapped label into 31px lines. `text-muted`
   keeps it the page's muted ink on a light ground and the band's full ink on a
   dark one (sites.css, "Section background tokens"). */
.site-page-content .site-metric-value {
    font-size: calc(1.375rem + 1.5vw) !important;
    font-weight: 700;
    line-height: 1.1;
    margin-bottom: .5rem;
}
@media (min-width: 1200px) {
    .site-page-content .site-metric-value { font-size: 2.5rem !important; }
}
.site-page-content .site-metric-value > i {
    font-size: .7em;
    margin-inline-end: .3em;
}
.site-page-content .site-metric-label {
    font-size: 15px !important;
    line-height: 1.45;
}
/* ── end KEY METRICS ─────────────────────────────────────────────────────── */

/* THE PUBLIC SHARE PAGE'S "LAST UPDATED" LINE, WITH THE EXECUTIVE COLUMN
   (public/site_links/page, 2026-09-23 r2). The colophon is a `.container px-4`
   so its text starts where every section's text starts — the same container
   plus the reading gutter (`.site-page-content .container` above). The
   executive presentation narrows every section's content to an 800px centred
   column (`.site-mode-executive .container`), which would leave the line out at
   the wide container's edge; it follows the page's mode here. Adjacent-sibling
   because the colophon is the element right after the page's content wrapper,
   and only there: with no sections there is no mode to follow. */
.site-page-content.site-mode-executive + .site-public-colophon {
    max-width: 800px;
}
/* …and the page's TITLE above the content, when it is printed: the same
   container, so the same column. It comes before the content wrapper, where a
   sibling rule cannot reach it, so the page says its mode on the wrapper
   itself (public/site_links/page). */
.site-public-title--narrow {
    max-width: 800px;
}

/* ── The page foot's cards (2026-09-25) ─────────────────────────────────
   Ask AI, Content review and Comments as one row of cards under the page,
   each opening its panel (apps/sites/pages/_foot_cards). A card here IS a
   <button>, so the browser's button box is reset to the card's own, and the
   whole card answers the pointer and the keyboard: a hover that lifts the
   border to the brand, and the focus ring a button would have. */
.site-foot-card {
    padding: 0;
    color: inherit;
    font: inherit;
    background-color: var(--bs-card-bg, #fff);
    transition: border-color .15s ease, box-shadow .15s ease;
}
.site-foot-card:hover {
    border-color: var(--bs-primary);
    box-shadow: 0 .125rem .5rem rgba(0, 0, 0, .08);
}
.site-foot-card:focus-visible {
    outline: 0;
    border-color: var(--bs-primary);
    box-shadow: 0 0 0 .25rem rgba(var(--bs-primary-rgb, 13, 110, 253), .25);
}
@media (prefers-reduced-motion: reduce) {
    .site-foot-card { transition: none; }
}
/* ── end page foot's cards ── */

/* ── qa6b: THE SECTION INTRO — body text under a section heading ─────────
   2026-09-26, Sites QA round 6: "Heading + Image: can we replace subheading
   section to be Body instead so the font is smaller", and the same report for
   Team Grid. What an author writes under those headings is a paragraph — the
   reported one ran eight lines at the 22px `.site-section-sub` step. This is
   the same slot at the page's BODY size: it sets no size of its own, so it
   reads 18.7px like every paragraph on the page, with the subheading's gap
   under it (the stack is 35.2 heading → 18.7 body).

   WHY `font-size: inherit !important`: below 576px mobile_optimizations.css
   sets every `.text-muted` to 0.925rem !important, which would put this
   paragraph at 16.28px beside 17.6px body copy — the narrow-window inversion
   `.site-section-sub` guards against for the same reason. Inheriting from the
   container keeps it on the body size at every width.

   HEADING ALIGNMENT: on a block whose alignment preset moves only the heading
   (SitePage::SECTION_ALIGN_HEADING_ONLY_TYPES — Team Grid is one), the text
   under the heading belongs to the heading and moves with it, exactly as the
   `.site-align-heading-*` rules move `.site-section-sub`. A block whose preset
   moves everything (Heading + Image) is re-aligned by the general rules. */
.site-page-content .site-section-intro {
    font-size: inherit !important;
    margin-bottom: 1.5rem;
}
.site-page-content .site-align-heading-start :is(.site-section-intro, .site-section-intro :is(p, li)) {
    text-align: start !important;
    margin-inline: 0;
}
.site-page-content .site-align-heading-center :is(.site-section-intro, .site-section-intro :is(p, li)) {
    text-align: center !important;
    margin-inline: auto;
}
.site-page-content .site-align-heading-end :is(.site-section-intro, .site-section-intro :is(p, li)) {
    text-align: end !important;
    margin-inline: auto 0;
}
/* ── end qa6b section intro ── */

/* ── Data Table rows (Sites QA round 6, 2026-09-26) ─────────────────────────
   "The table content is difficult to read when the background color is
   changed because the rows use alternating fill colors." AdminKit paints the
   stripe a fixed #f8f9fa, and on a dark token the rows carry the band's white
   ink (the on-dark `.table` rule near the top of this file) — white on
   near-white, 1.05:1 on Brand solid, Neutral dark and a dark custom colour
   alike. On a light token it was a grey-blue bar that belonged to no palette
   (on the site's cream page it read as a foreign stripe).

   The stripe and the hover are now a SHADE of whatever the ground is: a
   translucent black over it. On a dark ground that only raises the white
   ink's contrast (Brand solid's ink is measured to 4.6:1, so a lighter stripe
   would have fallen under AA); on the palette's light grounds the dark ink
   has 10:1 or more to spare. The one ground where darkening is not safe is a
   CUSTOM light colour, which BusinessTheme.on_color may have inked at the
   4.5:1 floor: there _table adds `--lighten` and the stripe is a translucent
   white, which moves away from dark ink instead.

   The box-shadow is restated because AdminKit's `.table-striped … { box-shadow:
   none }` and `.table-hover … { box-shadow: inset … rgba(0,0,0,.075) }` paint
   past the variables; three classes out-rank both, in any load order. */
.table.table-striped.site-data-table > tbody > tr > * {
    box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg, transparent)));
}
.table.site-data-table {
    --bs-table-striped-bg: rgb(0 0 0 / 0.045);
    --bs-table-hover-bg: rgb(0 0 0 / 0.08);
    --bs-table-striped-color: var(--bs-table-color);
    --bs-table-hover-color: var(--bs-table-color);
}
.site-section-bg--on-dark .table.site-data-table {
    --bs-table-striped-bg: rgb(0 0 0 / 0.18);
    --bs-table-hover-bg: rgb(0 0 0 / 0.3);
}
/* On a light ground the rows take the SECTION's ink, which for a custom colour
   is the one BusinessTheme.on_color measured (#212529 or black) — the page's
   body colour it used to inherit read 3.7:1 on a mid grey. On the palette's
   light tokens --sec-fg IS the body colour, so nothing changes there. */
.site-section-bg:not(.site-section-bg--on-dark) .table.site-data-table {
    --bs-table-color: var(--sec-fg, var(--bs-body-color));
}
.table.site-data-table.site-data-table--lighten {
    --bs-table-striped-bg: rgb(255 255 255 / 0.35);
    --bs-table-hover-bg: rgb(255 255 255 / 0.5);
}
/* ── end Data Table rows ─────────────────────────────────────────────────── */

/* ── Testimonials on White / Soft grey in the dark theme (Sites QA round 6,
   /pre-merge 2026-09-26) ──────────────────────────────────────────────────
   _testimonials paints its highlight and button in the brand moved just far
   enough to read on the ground (Sites::SectionPalette#brand_readable_on) —
   measured against White and Soft grey as the LIGHT colours they are in the
   light theme, so the brand is deepened toward black. Those two tokens are the
   theme's own page and secondary grounds, and the dark theme turns them
   near-black: measured on page 14426, the highlight read 3.2:1 on White and
   1.4:1 on Soft grey, and the button's fill all but vanished into the ground.

   In the dark theme both marks take the ground's ink instead — `--sec-fg`,
   which on these two tokens is the theme's body colour (11.6:1 and 5.5:1 there)
   — and the button's label the page's dark ink on that light fill, exactly as
   a dark token's marks do ("EVERY MARK ON A DARK GROUND FOLLOWS THE INK",
   above). `!important` because the section and the button set these variables
   inline. The light theme never matches `[data-theme="dark"]`, so nothing
   changes there; the tints and a custom colour stay light in the dark theme and
   carry no `--theme-ground` class. */
:root[data-theme="dark"] .testimonial-carousel--theme-ground {
    --testi-accent: var(--sec-fg, var(--bs-body-color)) !important;
}
:root[data-theme="dark"] .testimonial-carousel--theme-ground .testimonial-cta {
    --bs-btn-color: #212529 !important;
    --bs-btn-hover-color: #212529 !important;
    --bs-btn-active-color: #212529 !important;
    --bs-btn-focus-shadow-rgb: 255, 255, 255 !important;
}
/* ── end Testimonials in the dark theme ── */

/* ── TESTIMONIALS: ONE CENTRED COLUMN (owner decision, Sites QA round 3,
   2026-10-01) ─────────────────────────────────────────────────────────────
   The slide is the highlight, the quote, who said it and the button beneath
   the name, all centred (_testimonials: `text-center` on the column, the
   byline and button a centred flex column). The author's Left and Right move
   the whole slide as one piece — the text through the `.site-align-*` rules,
   and the byline and button through the column's alignment here, which the
   text rules cannot reach (`align-items-center` is an !important utility).
   The heading follows the same rules; the dots stay centred under every
   choice. Beside a photo the name, title and company start together at the
   photo's edge, as on Leader Quote's featured quote, whatever the column does. */
.site-page-content .site-align-start .testimonial-byline { align-items: flex-start !important; }
.site-page-content .site-align-end .testimonial-byline { align-items: flex-end !important; }
.site-page-content .testimonial-byline__text--beside-photo { text-align: start; }

/* ── A LIGHT GROUND THE THEME DID NOT PAINT (#3891, /pre-merge round 6,
   2026-09-26; the same fault as #3331, #3338 and #3849) ──────────────────────
   In the app's DARK theme every block on a tint — Brand tint, Accent tint,
   Light — and on a custom light colour drew its heading and text in the dark
   theme's light ink on a ground that is still light: 1.0–1.2:1, measured on
   every block of the census pages (a copy of each block on each ground), and on
   every table header (the default header is a tint). Those grounds are the
   brand or the author's hex mixed toward #ffffff, so they stay light in both
   themes; White and Soft grey are the theme's own grounds and go dark with it.

   The fix is the twin of `.site-section-bg--on-dark` above: on such a ground,
   the text takes the ground's measured ink (`--sec-fg` — #212529 on a tint in
   the dark theme via --site-light-ground-ink, BusinessTheme.on_color's pick on a
   custom colour), and so do links (underlined, so they are still findable
   without a colour of their own), marks painted with a colour utility, outline
   and dark buttons, and table cells. A custom colour gets it in BOTH themes:
   its ink was measured for it and the page's was not — a mid grey (#888888)
   drew its muted copy at 1.55:1 and its body text at 3.64:1 in the light theme.
   A tint in the light theme is left exactly as it was: the page's inks were
   measured on it (10–12:1) and keep the tenant's heading and link colours.

   WHICH GROUNDS. SitesHelper writes the pair inline — `--sec-bg` / `--sec-fg`
   on the section wrapper, `background` / `--sec-fg` on a card or row with a
   background of its own (#site_item_surface) — so the ground is read from that
   style: a token whose value names a `var(--bs-…)` (White and Soft grey) follows
   the theme; a dark token carries `.site-section-bg--on-dark`; a custom colour's
   value is a bare hex. Sites::SectionPalette keeps `var(--bs-` out of every
   other light token's ink on purpose (LIGHT_GROUND_INK), and its spec pins it.

   WHAT KEEPS ITS OWN INK — the same list as the on-dark rules, for the same
   reason: something that paints its own ground (a card, a list row, a button,
   a solid badge, a `bg-*` fill, an element with an inline background)
   decides what reads on it. Two differences, both measured. An ALERT is a 15%
   wash of whatever it sits on, so on these grounds its text takes the ink (its
   heading measured 1.04:1 in the dark theme, 3.25:1 on a mid grey in the light
   one). And in the dark theme a SUBTLE fill (`bg-*-subtle`, and the soft badges
   design_system.css makes of `badge bg-{tone}`) is a translucent wash as well,
   where the light theme's is an opaque pastel with its own ink (1.3–1.5:1
   before). A card or row that is ITSELF such a light ground is the ground for
   what is inside it. ── */
:root { --site-light-ground-ink: var(--bs-body-color, #212529); }
:root[data-theme="dark"] { --site-light-ground-ink: #212529; }

:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]),
.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) {
    color: var(--sec-fg);
}

/* A surface inside the ground paints its own, so it takes back the page's ink
   — in the dark theme a card, a list row or a `bg-light` panel is dark, and the
   rule above would otherwise hand its text the ground's dark ink by
   inheritance (a Custom HTML `bg-light` box measured 1.09:1). Not a button or a
   badge: they set their own colour. */

:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"]):not(.bg-transparent, [class*="-subtle"], .btn, .badge, [style*="--sec-fg:"]) {
    color: var(--bs-body-color);
}

/* Headings, text and muted text. `!important` for the reasons the on-dark rules
   give: the tenant theme paints `h1…h6` with it (in the dark theme,
   `[data-theme="dark"] h1…h6`), and Bootstrap sets `.text-muted` with it. In
   the dark theme a subtle fill and an alert are washes of the light ground, so
   their text is the ground's too. */

:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, p, .lead, small, .small, .text-muted, li, dt, dd, figcaption, blockquote, cite, .alert, .alert-heading, [class*="-subtle"]):not(:is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]))):not(:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"])) *),
.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, p, .lead, small, .small, .text-muted, li, dt, dd, figcaption, blockquote, cite, .alert, .alert-heading):not(:is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]))):not(.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"])) *) {
    color: var(--sec-fg) !important;
}

/* The soft badges (design_system.css, "Soft status badges") are a translucent
   wash in the dark theme — of a light ground, a light chip. */

:root[data-theme="dark"] body.mgx-restyle .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) .badge:is(.bg-primary, .bg-secondary, .bg-success, .bg-danger, .bg-warning, .bg-info):not(.rounded-pill, .position-absolute):not(:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"])) *),
:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) .badge[class*="-subtle"]:not(:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"])) *) {
    color: var(--sec-fg) !important;
}

/* Marks painted with a colour utility, and links (underlined, so they stay
   findable without a colour of their own). */

:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.text-primary, .text-secondary, .text-success, .text-info, .text-warning, .text-danger, .text-light, .text-white, .text-dark, .text-black, .text-body, .text-body-secondary, .text-body-tertiary, .text-body-emphasis, .text-primary-emphasis, .text-secondary-emphasis, .text-success-emphasis, .text-info-emphasis, .text-warning-emphasis, .text-danger-emphasis, .text-light-emphasis, .text-dark-emphasis, .text-white-50, .text-black-50, a):not(.btn, .card, .list-group-item, .nav-link, .badge, .dropdown-item):not([class^="bg-"]:not([class*="-subtle"], .bg-transparent), [class*=" bg-"]:not([class*="-subtle"], .bg-transparent)):not(:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"])) *),
.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(.text-primary, .text-secondary, .text-success, .text-info, .text-warning, .text-danger, .text-light, .text-white, .text-dark, .text-black, .text-body, .text-body-secondary, .text-body-tertiary, .text-body-emphasis, .text-primary-emphasis, .text-secondary-emphasis, .text-success-emphasis, .text-info-emphasis, .text-warning-emphasis, .text-danger-emphasis, .text-light-emphasis, .text-dark-emphasis, .text-white-50, .text-black-50, a):not(.btn, .card, .list-group-item, .nav-link, .badge, .dropdown-item, [class^="bg-"], [class*=" bg-"]):not(.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"])) *) {
    color: var(--sec-fg) !important;
}

:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) a:not(.btn, .card, .list-group-item, .nav-link, .badge, .dropdown-item):not(:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"])) *),
.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) a:not(.btn, .card, .list-group-item, .nav-link, .badge, .dropdown-item):not(.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"])) *) {
    text-decoration: underline;
}

/* A dimmed line (`opacity-75` on a hero's or a callout's support copy) is full
   ink: a custom colour's ink can sit at the AA floor (#888888 measured 4.49:1
   dimmed), the same reason the on-dark grounds undo it. */

:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(div, p, span):is(.opacity-50, .opacity-75, .opacity-90):not(:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"])) *),
.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(div, p, span):is(.opacity-50, .opacity-75, .opacity-90):not(.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"])) *) {
    opacity: 1 !important;
}

/* Outline buttons draw their label and edge in the ink and fill with it on
   hover under white (the ink on these grounds is always #212529 or black). A
   dark button's fill in the dark theme is AdminKit's light grey under a white
   label (1.2:1); on a light ground it is the ink. `!important` because the
   tenant theme sets the outline-primary variables with it. */

:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.btn-outline-primary, .btn-outline-secondary, .btn-outline-success, .btn-outline-info, .btn-outline-warning, .btn-outline-danger, .btn-outline-dark, .btn-outline-light, .btn-link):not(:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"])) *),
.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(.btn-outline-primary, .btn-outline-secondary, .btn-outline-success, .btn-outline-info, .btn-outline-warning, .btn-outline-danger, .btn-outline-dark, .btn-outline-light, .btn-link):not(.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"])) *) {
    --bs-btn-color: var(--sec-fg) !important; --bs-btn-border-color: var(--sec-fg) !important;
    --bs-btn-hover-bg: var(--sec-fg) !important; --bs-btn-hover-border-color: var(--sec-fg) !important; --bs-btn-hover-color: #ffffff !important;
    --bs-btn-active-bg: var(--sec-fg) !important; --bs-btn-active-border-color: var(--sec-fg) !important; --bs-btn-active-color: #ffffff !important;
}

:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) .btn-dark:not(:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"])) *) {
    background-color: var(--sec-fg) !important;
    border-color: var(--sec-fg) !important;
    color: #ffffff !important;
}

/* A table sits on the ground (AdminKit's table background is transparent): its
   cells take the ink. The header keeps the fill and ink SitesHelper paints on
   it (#site_table_header_style), and a contextual row (`.table-light`) its own. */

:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) .table:not(:root[data-theme="dark"] .site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [class*="-subtle"], [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"])) *),
.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) .table:not(.site-page-content [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"]) :is(.card, .list-group-item, .accordion-item, .dropdown-menu, .btn, .nav-link, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-fg:"]:not(.site-section-bg--on-dark):is([style*="--sec-bg: #"], [style*="background: #"])) *) {
    --bs-table-color: var(--sec-fg);
    --bs-table-striped-color: var(--sec-fg);
    --bs-table-hover-color: var(--sec-fg);
    --bs-table-active-color: var(--sec-fg);
}

/* Testimonials: in the dark theme the brand mark is the one measured against
   the brand the dark theme paints (Sites::SectionPalette
   #brand_readable_in_dark_theme; _testimonials sets it only where it differs). */
:root[data-theme="dark"] .testimonial-carousel--dark-accent {
    --testi-accent: var(--testi-accent-dark) !important;
}
/* ── end A LIGHT GROUND THE THEME DID NOT PAINT ── */

/* ── A3 (Sites item 3, 2026-09-26): SMALL TEXT AT CAPTION LEADING ───────────
   The block census (scripts/sites_block_census, measure T) found small text —
   `.small`, `<small>` and every data-table cell (AdminKit's `.table` is
   `--type-small`, 13px) — spaced at the page's 1.65 READING leading
   (`.site-page-content` above) in 16 blocks: a 15px description wrapped onto
   25px lines, a 13px cell onto 21px ones, where a caption reads at ~1.45.
   Round 6 fixed Image Gallery and File Downloads one partial at a time
   (`d-block lh-base`); this is the rule under every block, so the next one
   written gets it too.

   1. THE LEADING. Anything marked small — and a table, whose cells inherit —
      takes caption leading. Running copy is untouched: body text is
      `p:not(.small)` / `li:not(.small)` and keeps the 1.65 reading leading.
      `:where()` ALL THE WAY DOWN, so this has no specificity at all: it only
      replaces the INHERITED reading leading, and any rule that sets an
      element's own line-height still wins — `.badge` (1), `.btn`, the stats
      label and metric label, a `lh-*` utility.

   2. AN INLINE <small> ON A LINE OF ITS OWN IS LAID OUT AS A BLOCK.
      Line-height on an inline box cannot tighten its lines: they sit on the
      PARENT's strut, 18.7px × 1.65, so a job title or a video description
      under its heading wrapped at 31px — 2.06× its size — whatever it said
      (the round-6 File Downloads report, in Video Gallery and Org Chart). A
      <small> that starts its own line (after a heading, paragraph or div) and
      ends it (the last child, or a block, `d-block` or visually-hidden
      element next) is a caption line, so it becomes one. A <small> inside a
      sentence, after an inline box (`d-inline-*`), or in a row of meta smalls
      stays inline — which is why Calendar's time · location, once such a row,
      is one `div.small` now (sites/shared/_calendar_events).

   The phone twins are /m/ (mobile_tailwind.css), which this file never
   reaches; their text-sm lines already sit at 1.43. */
:where(.site-page-content) :where(small, .small, .table) {
    line-height: 1.45;
}
:where(.site-page-content :is(h1, h2, h3, h4, h5, h6, p, div, ul, ol, dl, blockquote, figure, table, .d-block):not([class*="d-inline"])
       + small:is(:last-child, :has(+ :is(h1, h2, h3, h4, h5, h6, p, div, ul, ol, dl, blockquote, figure, table, hr, .d-block, .visually-hidden)))) {
    display: block;
}
/* ── end A3 small text ── */

/* ── A2: THE APP'S DARK THEME ON EVERY SECTION GROUND (Console #3924, #3926;
   Sites item 3, 2026-09-26) ─────────────────────────────────────────────────
   A block census of every block on every ground in the dark theme (page
   "sites-block-census", scripts/sites_block_census) confirmed 107 contrast
   failures. They came from eight causes, each fixed once below for every block
   that has it. The light theme never matches `[data-theme="dark"]`; the one
   rule here that applies in both themes (4, outline buttons) says why. */

/* 1. SOFT GREY IS A STEP, NOT A SLAB. The token is the theme's own
      `--bs-secondary-bg`, which the dark theme resolves to AdminKit's gray-200
      (#4e5863): a 2.4:1 jump from the page (#262524), where the light theme's
      Soft grey sits 1.2:1 from white — and light enough that the theme's own
      inks fail on it: muted text 3.82:1 (40 blocks), brand links 3.42:1, the
      outline pills 3.73:1, a brand button's edge 1.5:1. In the dark theme it is
      the same step the light theme takes — 40% of that grey over the page —
      measured 1.3:1 from the page, with muted text at 5.9:1 and brand ink at
      5.3:1. Everything that paints the token follows: the section (`--sec-bg`),
      a card or row given the token (`background`), a table header.
      AdminKit's grey is named BY VALUE: since 2026-09-27 the dark theme
      resolves `--bs-secondary-bg` itself to a ground measured one step off the
      CARD (BusinessTheme#secondary_ground, #41464b for the census tenant) —
      right for a chat bubble or a disabled field on a card, while a Soft grey
      band sits on the PAGE and takes this step from it; 40% of the measured
      ground over the page would be 1.1:1, no band at all. The light theme's
      twin lives in BusinessTheme#light_secondary_ground_css: a tenant whose
      inks need a paler light ground gets one, and `.site-page-content` gets
      AdminKit's #e9ecef back there for the same reason. */
:root[data-theme="dark"] {
    --site-soft-ground-dark: color-mix(in srgb, #4e5863 40%, var(--bs-body-bg, #262524));
}
:root[data-theme="dark"] .site-page-content [style*="--sec-bg: var(--bs-secondary-bg"] {
    --sec-bg: var(--site-soft-ground-dark) !important;
}
:root[data-theme="dark"] .site-page-content [style*="background: var(--bs-secondary-bg"] {
    background: var(--site-soft-ground-dark) !important;
    --bs-table-bg: var(--site-soft-ground-dark) !important;
}

/* 2. A HEADING ON A DARK BAND IS THE BAND'S INK, whatever size class it
      carries. The on-dark rules name h1–h4; the tenant's dark block paints
      `[data-theme="dark"] .h1….h6` at a higher weight, so a step or card title
      written `<h3 class="h5">` stayed the theme's heading grey (#eaeaec) on the
      band — 3.85–4.28:1 on Accent (Journey Map, Team Grid, Numbered Steps,
      Timeline, FAQ). Not inside a surface of its own: the on-dark card rule
      owns those. */
:root[data-theme="dark"] .site-page-content .site-section-bg--on-dark :is(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6):not(.site-section-bg--on-dark :is(.card, .alert, .list-group-item, .accordion-item, .bg-white, .bg-light, .bg-body, .bg-body-secondary, .bg-body-tertiary):not(.site-item--on-dark) *) {
    color: var(--sec-fg, #ffffff) !important;
}

/* 3. A WASH IS LAID OVER THE PAGE, NOT OVER WHATEVER IS BEHIND IT. In the dark
      theme an alert is an 18% wash of its tone and a soft badge
      (design_system.css "Soft status badges", `badge bg-{tone}` and
      `bg-{tone}-subtle`) is too — and the inks on them were measured against
      that wash over the dark PAGE. Over a brand band the wash is a mid-tone:
      Announcements' and Status Banner's copy measured 2.7–3.5:1 on Brand solid
      and Accent, their priority badges 2.1–2.6:1, Numbered Steps' "Day 1/2"
      pills and Feature Blocks' / Mini Icon Grid's badges 2.6–3.1:1; and a
      badge inside an alert is a wash over a wash (4.1:1 even on the page).
      So each is painted over the page's own ground, exactly as it looks on
      the page. Not on a ground that stays light (a tint, a custom light
      colour): the light-ground block above gives their washes its ink on
      purpose. Wiki Article's callout (`.wiki-content blockquote`, the success
      wash custom.css gives the wiki's own quotes) is the one exception: it
      states its own ink, the theme's body colour ("A WIKI CALLOUT OWNS ITS
      INK", custom.css), so it is laid over the page on EVERY ground — its
      links read 2.8:1 over Soft grey, and on a tint its light ink sat on the
      wash of a light ground at 1.03:1. */
:root[data-theme="dark"] .site-page-content .alert:not(.site-item--on-dark, [style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) *) {
    background-color: var(--bs-body-bg) !important;
    background-image: linear-gradient(var(--bs-alert-bg, transparent), var(--bs-alert-bg, transparent)) !important;
}
:root[data-theme="dark"] .site-page-content .wiki-content blockquote {
    background: linear-gradient(var(--bs-success-bg-subtle), var(--bs-success-bg-subtle)), var(--bs-body-bg);
}
:root[data-theme="dark"] body.mgx-restyle .site-page-content .badge:is(.bg-primary:not(.rounded-pill, .position-absolute), .bg-primary-subtle) { --site-chip-wash: var(--bs-primary-bg-subtle); }
:root[data-theme="dark"] body.mgx-restyle .site-page-content .badge:is(.bg-secondary:not(.rounded-pill, .position-absolute), .bg-secondary-subtle) { --site-chip-wash: var(--bs-secondary-bg-subtle); }
:root[data-theme="dark"] body.mgx-restyle .site-page-content .badge:is(.bg-success:not(.rounded-pill, .position-absolute), .bg-success-subtle) { --site-chip-wash: var(--bs-success-bg-subtle); }
:root[data-theme="dark"] body.mgx-restyle .site-page-content .badge:is(.bg-danger:not(.rounded-pill, .position-absolute), .bg-danger-subtle) { --site-chip-wash: var(--bs-danger-bg-subtle); }
:root[data-theme="dark"] body.mgx-restyle .site-page-content .badge:is(.bg-warning:not(.rounded-pill, .position-absolute), .bg-warning-subtle) { --site-chip-wash: var(--bs-warning-bg-subtle); }
:root[data-theme="dark"] body.mgx-restyle .site-page-content .badge:is(.bg-info:not(.rounded-pill, .position-absolute), .bg-info-subtle) { --site-chip-wash: var(--bs-info-bg-subtle); }
/* `dark` is the seventh tone the panel offers a badge (Feature Rows'
   `badge_color`, SitePage::SECTION_THEME_COLORS), and the one the design
   system has no soft badge for: AdminKit's dark palette fills
   `bg-dark-subtle` with an OPAQUE mid grey (#7a828a) under its own
   `text-dark-emphasis` (#bdc0c5), 2.14:1 on the page, and under a light
   ground's ink 3.96:1 (/pre-merge round 7, 2026-09-26). It becomes the same
   18% wash as the others, of the dark theme's `--bs-dark` (#e9eaec). */
:root[data-theme="dark"] body.mgx-restyle .site-page-content .badge.bg-dark-subtle { --site-chip-wash: rgba(233, 234, 236, 0.18); background-color: var(--site-chip-wash) !important; }
:root[data-theme="dark"] body.mgx-restyle .site-page-content .badge:is(.bg-primary, .bg-secondary, .bg-success, .bg-danger, .bg-warning, .bg-info):not(.rounded-pill, .position-absolute):not([style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) *),
:root[data-theme="dark"] body.mgx-restyle .site-page-content .badge:is(.bg-primary-subtle, .bg-secondary-subtle, .bg-success-subtle, .bg-danger-subtle, .bg-warning-subtle, .bg-info-subtle, .bg-dark-subtle):not([style*="--sec-fg:"]:not(.site-section-bg--on-dark, [style*="var(--bs-"]) *) {
    background: linear-gradient(var(--site-chip-wash, transparent), var(--site-chip-wash, transparent)), var(--bs-body-bg) !important;
}

/* 4. AN OUTLINE BUTTON'S EDGE FOLLOWS ITS OWN VARIABLE — in both themes. The
      tenant theme paints `.btn-outline-primary` with a literal
      `border-color: … !important` (layouts/_tenant_theme, light and dark
      blocks alike), so every Sites rule that moves an outline button onto a
      ground's ink by setting `--bs-btn-border-color` — the on-dark rule, the
      light-ground rule above — moved the label and never the edge: Button
      Grid's buttons kept the brand edge on a band (1.0–1.3:1 in the light
      theme, 2.9–3.0:1 in the dark) and the dark theme's pale brand edge on a
      tint (1.6–2.0:1). The property now reads the variable, whoever set it;
      where nothing did, the tenant sets both to the same colour, so nothing
      changes. `:root` lifts it over the tenant's `[data-theme="dark"] …`. */
:root .site-page-content :is(.btn-outline-primary, .btn-outline-secondary, .btn-outline-success, .btn-outline-info, .btn-outline-warning, .btn-outline-danger, .btn-outline-dark, .btn-outline-light) {
    border-color: var(--bs-btn-border-color) !important;
}
:root .site-page-content :is(.btn-outline-primary, .btn-outline-secondary, .btn-outline-success, .btn-outline-info, .btn-outline-warning, .btn-outline-danger, .btn-outline-dark, .btn-outline-light):is(:hover, :focus-visible) {
    border-color: var(--bs-btn-hover-border-color) !important;
}
:root .site-page-content :is(.btn-outline-primary, .btn-outline-secondary, .btn-outline-success, .btn-outline-info, .btn-outline-warning, .btn-outline-danger, .btn-outline-dark, .btn-outline-light):is(:active, .active, .show) {
    border-color: var(--bs-btn-active-border-color) !important;
}
/* …AND SO DOES ITS LABEL ONCE IT FILLS (/pre-merge round 7, 2026-09-26 — a
   hover state the block census cannot see). AdminKit paints every hovered
   outline button's label a literal white (light.css / dark.css,
   `.btn-outline-*:hover:not(:disabled):not(.disabled) { color: #fff }`),
   over the label each writer measured for the fill: the tenant theme's
   on-colour, the band and light-ground rules above, the dark theme's rules
   below. Measured under the pointer: Resource Hub's filter pills 1.94:1 in
   the dark theme (white on the lifted accent, where the theme's own label
   reads 7.9:1), a dark card's "Open" 1.47:1 and a hero's secondary button on
   Soft grey 1.2:1. The label reads its variable, as the edge does — and so
   does the fill, because the label is measured for the fill written beside
   it: the tenant theme also paints `.btn-outline-primary:hover` a literal
   brand fill (!important), which left a dark card's outline button, whose
   rule above hands it a white fill and a dark label, with the dark label on
   the brand (3.24:1). */
:root .site-page-content :is(.btn-outline-primary, .btn-outline-secondary, .btn-outline-success, .btn-outline-info, .btn-outline-warning, .btn-outline-danger, .btn-outline-dark, .btn-outline-light):is(:hover, :focus-visible) {
    background-color: var(--bs-btn-hover-bg) !important;
    color: var(--bs-btn-hover-color) !important;
}
:root .site-page-content :is(.btn-outline-primary, .btn-outline-secondary, .btn-outline-success, .btn-outline-info, .btn-outline-warning, .btn-outline-danger, .btn-outline-dark, .btn-outline-light):is(:active, .active, .show) {
    background-color: var(--bs-btn-active-bg) !important;
    color: var(--bs-btn-active-color) !important;
}

/* 5. A BRAND BUTTON ON THE THEME'S OWN DARK SURFACES GETS AN EDGE. The dark
      theme's brand fill is measured for its white label (#a86040, 4.8:1), which
      leaves it too dark to stand off a dark surface: 2.65:1 on a card (#333,
      Embedded Widget), 3.0:1 on Platform Pillars' panel, 1.5:1 on Soft grey,
      and Resource Hub's pressed "All" pill (#864d33) 2.3:1 on the page. No
      fill can clear both 4.5:1 under white and 3:1 against #333, so the
      button keeps its fill and label and takes a 1px edge of the same colour
      lifted 40% toward white (5.9:1 on a card). Not on a dark band or a dark
      item (their rules above repaint the button in the band's ink) and not on
      a ground that stays light (the fill already stands off it); a card on
      any of those is the theme's own dark surface again. */
:root[data-theme="dark"] .site-page-content .btn-primary:not(.site-section-bg--on-dark *, [style*="--sec-fg:"]:not([style*="var(--bs-"]) *, [style*="background"]:not([style*="var(--bs-"]) *),
:root[data-theme="dark"] .site-page-content :is(.card, .list-group-item, .accordion-item):not(.site-item--on-dark, [style*="background"]:not([style*="var(--bs-"])) .btn-primary {
    border-color: color-mix(in srgb, var(--bs-btn-bg, var(--bs-primary)) 60%, #ffffff) !important;
}

/* 6. (retired 2026-09-27) A solid fill under a hard-coded `text-white` now takes
      the theme's measured label on every surface: color_text_fix.css's dark
      `.text-white` rule no longer outranks `.bg-{tone}`'s --bs-on-{tone}
      (Console #3998). The six rules that did it here for Sites content were
      removed once a CSSOM check showed they changed no computed colour on the
      census pages. */

/* 7. `btn-dark` AND `btn-light` KEEP THEIR MEANING. AdminKit's dark palette
      inverts both — `btn-dark` becomes a light grey (#e9eaec) under its old
      white label (1.2:1: the CTA Banner's button on White and Soft grey), and
      `btn-light` a dark slate (#2d3846) that vanishes into a dark band (edge
      1.2–2.1:1, the CTA Banner's button on every dark band). The CTA Banner
      picks `btn-dark` for a light-looking ground and `btn-light` for a dark
      band (_cta_banner), so in the dark theme `btn-dark` keeps AdminKit's fill
      with AdminKit's own dark label, and `btn-light` is light again, as the
      outline-light rule (3, in "Dark mode" above) already does. The
      light-ground rule above still repaints `btn-dark` in a tint's ink. */
/* `btn-dark`'s label and `btn-outline-dark`'s filled label are the
   platform's now (design_system.css blocks 23 and 24, Console #3997 and
   #4034): both read AdminKit's own dark label (#222e3c) in every app. The
   two rules that did it here were removed on 2026-09-27 once a CSSOM check
   showed they changed no computed colour, resting or hovered. `btn-light`
   stays: making it light again is this block's own call for dark bands. */
:root[data-theme="dark"] .site-page-content .btn-light {
    --bs-btn-color: #212529;
    --bs-btn-bg: #f8f9fa;
    --bs-btn-border-color: #f8f9fa;
    --bs-btn-hover-color: #212529;
    --bs-btn-hover-bg: #e2e6ea;
    --bs-btn-hover-border-color: #e2e6ea;
    --bs-btn-active-color: #212529;
    --bs-btn-active-bg: #dae0e5;
    --bs-btn-active-border-color: #dae0e5;
    color: var(--bs-btn-color) !important;
}

/* 8. A FIELD AND A TIMELINE DOT ON A GROUND OF ITS OWN. In the dark theme a
      form field is a dark fill with a mid-grey edge that only stands off the
      page: Employee Directory's search box measured 1.8–2.3:1 on the dark
      bands and 1.9:1 on Soft grey. On those grounds the edge is the ground's
      ink at 60%. The Timeline's dot is the brand ringed in `--card-bg`,
      which is white in the light theme (a halo on every ground) and #333 in
      the dark one (1.7–2.1:1 on a band, 2.4:1 on Soft grey); its ring is the
      ground's ink, the halo the light theme draws. */
:root[data-theme="dark"] .site-page-content :is(.site-section-bg--on-dark, [style*="--sec-bg: var(--bs-secondary-bg"], [style*="background: var(--bs-secondary-bg"]) :is(.form-control, .form-select):not(.site-section-bg--on-dark :is(.card, .list-group-item, .dropdown-menu, .modal-content):not(.site-item--on-dark) *) {
    border-color: color-mix(in srgb, var(--sec-fg, var(--bs-body-color)) 60%, transparent) !important;
}
:root[data-theme="dark"] .site-page-content .site-timeline-dot {
    border-color: var(--sec-fg, var(--bs-body-color)) !important;
}
/* The Timeline's line (sites/shared/_timeline): its muted ink, painted as a
   fill. A class, not an inline `background:` — every rule that hands a
   ground's ink to `.text-muted` skips an element whose style attribute names
   a background, so the inline fill kept the dark theme's pale grey on a pale
   ground (1.5–1.9:1 on the tints and on pale custom colours; Sites tester
   round 7 pre-merge, 2026-10-07). */
.site-page-content .site-timeline-spine {
    background: currentColor;
}
/* 9. A BRAND BAND IS MEASURED AGAINST THE BRAND THE DARK THEME PAINTS. The
      band's stops were measured so white just clears AA on the tenant's stored
      colour; the dark theme lifts `--brand-*` toward white, so a band whose
      stop was walked (a pale brand) mixed lighter there: white copy on the
      census Accent band measured 4.20–4.49:1 in eleven blocks. SitesHelper
      writes the band measured against the dark theme's colour beside the
      light one when they differ (Sites::SectionPalette#dark_theme_bg):
      `--sec-bg-dark` on a section wrapper, `--item-bg-dark` on whatever
      paints a band inline (a card or row, a table header, the default band of
      a CTA banner, stats bar or callout). The light theme never reads them. */
:root[data-theme="dark"] .site-page-content [style*="--sec-bg-dark:"] {
    --sec-bg: var(--sec-bg-dark) !important;
}
:root[data-theme="dark"] .site-page-content [style*="--item-bg-dark:"] {
    background: var(--item-bg-dark) !important;
    --bs-table-bg: var(--item-bg-dark) !important;
}
/* The editor's swatches sit outside site-page-content, but promise the same
   paint. The helper supplies the same measured dark background as the page. */
:root[data-theme="dark"] .site-ground-swatch {
    background: var(--site-swatch-dark) !important;
}
/* 10. (retired 2026-09-27) `bg-dark` and `bg-black` stay dark on every surface
      now, with a white label (color_text_fix.css "Dark background", Console
      #4035); the two rules that did it here for Sites content were removed
      once a CSSOM check showed they changed no computed colour. */
/* ── end A2 dark theme ── */

/* ── A1: THE BRAND ON THE SECTION'S OWN GROUND (Sites item 3, 2026-09-26;
   Console #3925 and the block census's light-theme worklist) ────────────────
   The tenant's brand as TEXT is measured for the page, not for the ground a
   section paints. On the census bench (brand #b84e1f, secondary #f7931e) its
   links, timeline dates, "Open in Wikis" and outline-button labels read
   4.17:1 on the Brand tint and 4.27:1 on Soft grey, and the secondary brand's
   outline pills (Resource Hub's filters) 4.09–4.46:1 on the tints. `--sec-fg`
   solved this for ordinary text; this is the same idea for the brand-coloured
   marks. SitesHelper writes three more inline properties on every section
   wrapper, and on every card or row with a background of its own
   (Sites::SectionPalette#brand_ink_style):
     --sec-brand-ink     the brand deepened by the least that reads on this
                         ground (per tenant, over var(--brand-primary))
     --sec-accent-ink    the same for the secondary brand
     --sec-on-brand-ink  a label on either (an outline button's hover fill)
   and every block reads the one value — no partial names a colour.

   LIGHT THEME, LIGHT GROUNDS ONLY. The values are measured on the light
   theme's brand; in the dark theme a tint's marks take its ink ("A LIGHT
   GROUND THE THEME DID NOT PAINT", above) and White / Soft grey go dark with
   the page. On a dark ground the ink IS `--sec-fg`, which the on-dark rules
   above already hand the marks. The ground that matches is the nearest one
   that wrote the ink and is not dark; a dark card inside a light section
   writes its own pair, so what is inside it reads white on it, as the on-dark
   item rules say.

   WHAT KEEPS ITS OWN COLOUR: anything inside an element that paints its own
   ground (a card, an alert, a list row, a button, a badge, a `bg-*` fill, an
   inline background) and did not write its own ink — the brand on a white card
   is the tenant's, measured for white. Counted from the ground that wrote the
   ink down: the editor and the previews draw the page inside cards of their own. A link takes the ink at the base (no
   !important), so a colour utility on it (`text-dark`, `link-secondary`) still
   wins; `.text-primary` needs !important because the tenant theme paints it
   with one. ── */
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) :is(.text-primary, .link-primary):not([style*="--sec-brand-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-brand-ink:"]) *) {
    color: var(--sec-brand-ink) !important;
}

/* THE SECONDARY BRAND AS TEXT, on the same ground. `.text-secondary` is not a
   grey here: the tenant theme paints it in the secondary brand colour, with
   !important, measured for a card. The muted rule
   below listed it for a day and turned a secondary-brand icon or phrase on
   any light section ground into the muted grey (a plain Feature Cards icon,
   a `text-secondary` span in authored text — code review 2026-10-03). It takes
   the accent ink the wrapper already writes for this ground, as
   `.btn-outline-secondary` does; `.text-body-secondary` is the muted one. */
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) .text-secondary:not([style*="--sec-brand-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-brand-ink:"]) *) {
    color: var(--sec-accent-ink) !important;
}

/* THE MUTED INK ON THE SAME GROUND (Console #4313). The tenant theme measures
   its muted ink against the card, the page and the modal; a section's tint is
   none of those, and a tenant's own muted ink read 4.19–4.22:1 on the Brand
   and Accent tints. `--sec-muted-ink` is that same theme ink deepened by
   the least that reads on this ground (Sites::SectionPalette#muted_readable_on
   — the ink itself where it already clears). Same reach as the brand rule
   above: light grounds, the light theme, and nothing inside an element that
   paints its own ground. `!important` because the tenant theme paints
   `.text-muted` with one. */
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-muted-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) :is(.text-muted, .text-body-secondary, .form-text):not([style*="--sec-muted-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-muted-ink:"]) *) {
    color: var(--sec-muted-ink) !important;
}

:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) a:not(.btn, .card, .list-group-item, .nav-link, .dropdown-item, .badge):not([style*="--sec-brand-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-brand-ink:"]) *) {
    color: var(--sec-brand-ink);
}
/* Hover moves the ink toward the ground's own ink (`--sec-fg`), as the tenant's
   link hover darkens: toward `--sec-on-brand-ink` — white on a light ground —
   "Search the full directory" and "Open Calendar" on Soft grey went from
   4.64:1 at rest to 3.19:1 under the pointer (/pre-merge round 7,
   2026-09-26), the one link on the page that paled on hover. */
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) a:not(.btn, .card, .list-group-item, .nav-link, .dropdown-item, .badge):hover:not([style*="--sec-brand-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-brand-ink:"]) *) {
    color: color-mix(in srgb, var(--sec-brand-ink) 80%, var(--sec-fg, #212529));
}

/* Marks drawn as an edge: `.border-primary` (a rule, a ring, an accent bar).
   Not a card's own edge (/pre-merge round 7, 2026-09-26): the design system
   draws every `.card.border-primary` in the card border (design_system /
   custom.css "body.mgx-restyle .card.border-primary"), and Org Chart's root
   card went brand-edged on White, Soft grey and the tints only — the page
   with no background kept the card border. */
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) .border-primary:not(.card):not([style*="--sec-brand-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-brand-ink:"]) *) {
    border-color: var(--sec-brand-ink) !important;
}

/* Outline buttons: the label and the edge are the ink, and hover fills with
   it under its label. The real `border-color` / hover properties as well as
   Bootstrap's variables, because the tenant theme sets both on
   `.btn-outline-primary` with !important (BusinessTheme#to_css_variables). */
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) .btn-outline-primary:not([style*="--sec-brand-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-brand-ink:"]) *) {
    --bs-btn-color: var(--sec-brand-ink) !important; --bs-btn-border-color: var(--sec-brand-ink) !important;
    --bs-btn-hover-bg: var(--sec-brand-ink) !important; --bs-btn-hover-border-color: var(--sec-brand-ink) !important; --bs-btn-hover-color: var(--sec-on-brand-ink) !important;
    --bs-btn-active-bg: var(--sec-brand-ink) !important; --bs-btn-active-border-color: var(--sec-brand-ink) !important; --bs-btn-active-color: var(--sec-on-brand-ink) !important;
    border-color: var(--sec-brand-ink) !important;
}
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) .btn-outline-primary:is(:hover, :focus, :active):not([style*="--sec-brand-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-brand-ink:"]) *) {
    background-color: var(--sec-brand-ink) !important;
    border-color: var(--sec-brand-ink) !important;
    color: var(--sec-on-brand-ink) !important;
}
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) .btn-outline-secondary:not([style*="--sec-brand-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-brand-ink:"]) *) {
    --bs-btn-color: var(--sec-accent-ink) !important; --bs-btn-border-color: var(--sec-accent-ink) !important;
    --bs-btn-hover-bg: var(--sec-accent-ink) !important; --bs-btn-hover-border-color: var(--sec-accent-ink) !important; --bs-btn-hover-color: var(--sec-on-brand-ink) !important;
    --bs-btn-active-bg: var(--sec-accent-ink) !important; --bs-btn-active-border-color: var(--sec-accent-ink) !important; --bs-btn-active-color: var(--sec-on-brand-ink) !important;
}

/* A form field's edge on a light ground (Employee Directory's filter box:
   2.47–2.98:1 on the tints, Soft grey and a custom yellow). The field is white
   and its border a mid grey measured for white; on a tinted ground neither
   reaches 3:1. The border becomes the ground's own ink at 62% — a shade of
   whatever the section carries, #6f7274 on the page's #212529 — which clears
   3:1 on every light token. Focus keeps its own ring and colour. */
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) :is(.form-control, .form-select, .input-group-text):not(:focus):not([style*="--sec-brand-ink:"] :is(.card, .alert, .list-group-item, .accordion-item, .dropdown-menu, .modal, .btn, .badge, .table-primary, .table-secondary, .table-success, .table-danger, .table-warning, .table-info, .table-light, .table-dark, .table-active, [class^="bg-"], [class*=" bg-"], [style*="background"]):not(.bg-transparent, [style*="--sec-brand-ink:"]) *) {
    border-color: color-mix(in srgb, var(--sec-fg) 62%, #ffffff);
}
/* ── end A1 brand on the section's own ground ── */

/* ── Cb (Sites item 3, 2026-09-26): THE DATA TABLE'S CAPTION · NUMBERED STEPS'
   LABEL COLUMN ──────────────────────────────────────────────────────────────
   1. THE DATA TABLE'S CAPTION (the owner's call: Subheading and Caption both
      draw). The author's Caption is the table's own <caption> (_table), set
      `caption-top`: where the note drew before, and where the panel lists it.
      It is the table's own text size, not `.small` — inside a `.table` that is
      the app's 12px caption step (mgx-restyle) — muted like the note it was,
      and it starts where the rows start. A caption box is as wide as its
      table, so on a table wider than its block (which scrolls inside
      `.table-responsive`) its lines ran past the visible edge: the scroll box
      is a size container and the caption keeps to its visible width.

   2. NUMBERED STEPS' LABELS SHARE ONE COLUMN (Console #3995). Each step was a
      flex row whose chip was as wide as its own label, so a title started
      wherever its chip ended — "1" and "Day 12" put two titles of one list
      30px apart. The steps are one grid; the first track of each column is
      `max-content`, the widest chip in it (a long label is still capped by the
      pill's own 9rem and wraps, never cut), and each step lays its chip and
      its words on that grid as a SUBGRID, so every title in a column starts at
      the same x. One column, or two from 560px of BLOCK width, which is what
      the `col-cq-md-6` it replaces drew. The chip keeps its own width at the
      start of the track. A browser without subgrid gives each step its own
      two tracks: side by side as before, unaligned.
      `--stacked` (any label over 6 characters, the partial decides): a column
      as wide as a long pill would squeeze every step's words to what it
      leaves, so each chip sits above its title, every title starts at the
      column's edge, and the pill drops its 9rem cap to read on fewer lines.
      The cap is set here, not inline on the chip: custom.css releases a
      `.rounded-circle`'s 40px floor for a circle whose style attribute says
      `width:`, which `max-width:` does, and a one-digit step drew as an oval. */
.site-page-content .site-data-table-scroll {
    container-type: inline-size;
}
.site-page-content .table.site-data-table > caption.site-data-table-caption {
    max-width: 100cqi;
    padding: 0 0 0.5rem;
    text-align: start;
}

/* A TABLE'S SCROLL BOX (Sites QA round 3, 2026-10-01: a 1px vertical scroll
   bar; Sites tester round 4, 2026-10-02: the bottom border missing). Both were
   one pixel: the hidden caption's `margin: -1px` made the table box shorter
   than its rows. Round 3 hid the scroll bar here with `overflow-y: hidden`,
   which clipped that pixel — the bottom line. The caption's margin is fixed at
   its source now, for every table on the platform (custom.css, "A HIDDEN
   CAPTION TAKES NO SPACE AWAY FROM ITS TABLE"), so the box needs no rule. */

.site-page-content .site-steps {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: 1rem;
}
@container site-cq (min-width: 560px) {
    .site-page-content .site-steps {
        grid-template-columns: repeat(2, max-content minmax(0, 1fr));
    }
}
.site-page-content .site-steps > li {
    display: grid;
    grid-column: span 2;
    grid-template-columns: max-content minmax(0, 1fr);
    grid-template-columns: subgrid;
    column-gap: 1rem;
    align-items: start;
}
.site-page-content .site-steps > li > .site-steps__chip {
    justify-self: start;
    max-width: 9rem;
}
.site-page-content .site-steps > li > :last-child {
    min-width: 0;
}
.site-page-content .site-steps.site-steps--stacked {
    grid-template-columns: minmax(0, 1fr);
}
@container site-cq (min-width: 560px) {
    .site-page-content .site-steps.site-steps--stacked {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}
.site-page-content .site-steps.site-steps--stacked > li {
    grid-column: auto;
    grid-template-columns: minmax(0, 1fr);
    row-gap: 0.5rem;
}
.site-page-content .site-steps.site-steps--stacked > li > .site-steps__chip {
    max-width: 100%;
}
/* A long label's pill: `rounded-pill`'s shape without the class, which the
   design system keeps solid as a counter (design_system.css "Soft status
   badges") while the other step chips are soft. */
.site-page-content .site-steps__chip--pill {
    border-radius: var(--bs-border-radius-pill, 50rem) !important;
}
/* ── end Cb ── */

/* ── F (/pre-merge of Sites item 3, 2026-09-27): THE HERO'S BADGE PILL ───────
   Console #4037. The hero's category pill is `badge bg-light
   text-primary-emphasis`, and the platform repaints `.badge.bg-light` (the
   secondary brand's 20% tint under the MGX restyle, #f8f9fa without it), so
   the label — the brand as the theme paints it — was never measured against
   the chip under it: #b84e1f on the peach chip read 4.12:1 on page 1617, under
   AA for 13px semibold text. SitesHelper#site_hero_badge_style writes the pair
   on the pill: the palette's Brand tint, and the brand measured for that tint
   (SectionPalette#brand_readable_on), per tenant and per site brand. Light
   theme only: the dark theme's own chip (`.bg-light` there is a shade of the
   card) carries the lifted emphasis ink at 5.6:1. The phone twin's
   `bg-primary/10 text-primary` measured 6.4:1 in both schemes. */
:root:not([data-theme="dark"]) .site-page-content .hero .badge.site-hero-badge[style*="--hero-badge-ink:"] {
    background-color: var(--hero-badge-bg) !important;
    color: var(--hero-badge-ink) !important;
}
/* ── end F hero badge ── */

/* ── q1f (/pre-merge of #4044, 2026-09-27): A WITHHELD SECTION'S ANCHOR ─────
   Console #4096. A section that draws nothing gets no wrapper on the reader
   (SitesHelper#site_reader_section_frames): its zero-height one sat between a
   hero and the band after it, so the rule that squares the hero's corners over
   a band (`:has(+ [data-section-index] > .site-section-bg)`) read the empty one
   and the hero kept its rounded corners at the seam (that rule went with the
   hero's rounded corners, 2026-10-07). Its anchor rides at the
   top of the next section as this span — where a jump to it landed before —
   held clear of the sticky header as every section wrapper is (custom.css,
   `.site-page-content [data-section-index]`). */
.site-page-content .site-section-anchor {
    display: block;
    scroll-margin-top: 70px;
}
/* ── end q1f ── */

/* ── Sites QA 2026-09-29 (lane B): alignment rows, automatic accents, quotes ──
   The owner's decisions on the tester's report: D1 (a part-filled row follows
   the heading), D2 (the three quote blocks read as one family) and D6 (an
   accent picks a colour that reads on the section's background — never a new
   colour setting). ── */

/* D1: THE CARD ROW FOLLOWS THE HEADING. The contract for every card-grid block
   (SitePage::SECTION_CARD_ROW_TYPES names them; the hint under "Alignment"
   says so):
     - put `site-card-row` on the flex row that holds the cards (a Bootstrap
       `.row`, or any flex row of items) and DROP any `justify-content-*`
       utility on it — this rule is the one place the row is justified;
     - add `site-card-row--start` when the block's heading sits LEFT by default
       (SitePage::SECTION_NATURAL_TEXT_ALIGN "start": Team Members, Employee
       Directory), so an untouched block keeps its heading and its row together.
   With no alignment chosen the row is centred under a centred heading. The
   author's Left / Centred / Right — the whole-block classes and the
   heading-only ones alike, because a short row is layout, not the text inside
   a card — justify it start / centre / end. A FULL row has no free space, so it
   looks the same under every choice; only the orphans move. `!important` on the
   preset lines for the reason the text presets carry it: a partial's own
   `justify-content-*` utility is !important and must lose to the author. */
.site-page-content .site-card-row { justify-content: center; }
.site-page-content .site-card-row.site-card-row--start { justify-content: flex-start; }
.site-page-content :is(.site-align-start, .site-align-heading-start) .site-card-row { justify-content: flex-start !important; }
.site-page-content :is(.site-align-center, .site-align-heading-center) .site-card-row { justify-content: center !important; }
.site-page-content :is(.site-align-end, .site-align-heading-end) .site-card-row { justify-content: flex-end !important; }

/* A CARD IS NEVER TOO NARROW FOR ITS WORDS (Sites tester round 6, 2026-10-06:
   three ticked cards in the editor canvas drew "Departmen t", "Onboardin g",
   "documentatio n"). The block-width tiers ("BLOCK-WIDTH BREAKPOINTS") put four
   cards across from 720px of block, which is ~180px a card — and a card that
   sets an icon beside its words keeps ~90px of that for a heading, where an
   ordinary word does not fit and `overflow-wrap` broke it wherever it ran out.

   A card in a card row keeps at least 240px: four across only from 960px of
   block (the `xl` tier), three from 720px, two from 480px — a row drops a
   column BEFORE a card gets narrower, on the block's own width, so the editor
   canvas (narrower than its window) is right too. The row stays full width
   and each card the same width; only how many share a row changes. The
   thresholds sit on the tiers' own, so a row only ever moves one tier down.

   A row the card COUNT chose four across for (SitesHelper#site_card_grid_columns
   gives 4, 7 and 8 cards `col-md-6 col-lg-3`) drops to its own two-across
   width instead of three: that helper's rule is "the width whose last row is
   fuller wins", and two across fills better than three for every count it
   gave four — four cards are 2 + 2, never 3 and an orphan (second reading of
   this round). People and button rows (`col-6`: Team Members, Org Chart,
   Team Grid, Button Grid) are left as they are — a centred name or a
   button's short label fits their tiles, and they are two to a row at any
   narrower width already.

   `site-card-grid` is the same floor for a grid of cards that is not a
   `site-card-row` — Category Cards and Quick Links, laid out by the same
   count rule, whose part-filled rows keep their own alignment (they are not
   card rows the Alignment setting moves). */
@container site-cq (min-width: 720px) and (max-width: 959.98px) {
    .site-page-content :is(.site-card-row, .site-card-grid) > :is(.col-cq-lg-3, .col-cq-md-3:not([class*="col-cq-lg-"])):not(.col-6, .col-cq-md-6) { width: 33.33333333%; }
    .site-page-content :is(.site-card-row, .site-card-grid) > .col-cq-md-6.col-cq-lg-3:not(.col-6) { width: 50%; }
}
@container site-cq (min-width: 560px) and (max-width: 719.98px) {
    .site-page-content :is(.site-card-row, .site-card-grid) > :is(.col-cq-md-3, .col-cq-md-4):not(.col-6) { width: 50%; }
}
@container site-cq (max-width: 479.98px) {
    .site-page-content :is(.site-card-row, .site-card-grid) > .col-sm-6 { width: 100%; }
}
/* …and a word too long for its card breaks with a hyphen where the page's
   language has hyphenation (the <html lang> every layout sets), never bare —
   `overflow-wrap` on `.site-page-content` still catches a word no dictionary
   can split (a product code). Hyphenation is the browser's at a line's end,
   not only for a word that fits nowhere, so it is kept to the CARDS of a card
   row — the columns a third of a row or narrower at some width, where a word
   can run out of room — and to words of ten letters or more with four kept on
   each side, so a short word is never split to fill out a line. A row's
   full-width or half-width prose (an FAQ's answers, a leader's quote), a
   flow of steps and a row of tags keep their words whole, and so do the
   people rows above. */
.site-page-content :is(.site-card-row, .site-card-grid) > :is(.col-cq-md-3, .col-cq-md-4, .col-cq-lg-3, .col-cq-lg-4, .col-cq-xl-3, .col-cq-xl-4, .col-sm-6):not(.col-6) {
    hyphens: auto;
    hyphenate-limit-chars: 10 4 4;
}
/* An address is typed as it is read, so a hyphen inside one (a contact card's
   email or phone number) would be copied into it. */
.site-page-content :is(.site-card-row, .site-card-grid) :is(a[href^="mailto:"], a[href^="tel:"]) {
    hyphens: manual;
}

/* D6: A MEDIA FRAME STANDS OFF ITS GROUND. `site-media-frame` goes on the box
   that frames a video or a picture edge to edge (Video Embed's 16:9 frame, an
   uploaded video). A black player on a dark band — Neutral dark, or a deep
   brand — had no edge at all; a white poster on a white page had none either.
   The hairline is chosen from the ground, never set by the author: the page's
   own translucent border on a light ground (it follows the theme), the band's
   ink at 60% on a dark one (the ink is the colour measured to read there).
   An outline, not a border or a shadow: it takes no layout space, follows the
   frame's radius, and leaves any `shadow-sm` the partial draws alone. */
.site-page-content .site-media-frame {
    outline: 1px solid var(--bs-border-color-translucent, rgba(0, 0, 0, .175));
    outline-offset: 0;
}
.site-page-content :is(.site-section-bg--on-dark, .site-item--on-dark) .site-media-frame {
    outline-color: color-mix(in srgb, var(--sec-fg, #ffffff) 60%, transparent);
}

/* Use Cases' cards carry no stripe (owner decision, Sites QA round 3,
   2026-10-01). The 3px green left edge lost to the design system's hairline
   card border (design_system.css §11, `.card.border-0`) on a light ground,
   where it vanished, and on a dark one drew as a white inset edge beside a
   green one. The tick inside the card is the accent; the card is the same
   hairline card as every other card block on every ground. */

/* ── A PERSON'S NAME IS A LINK (Sites QA round 3, 2026-10-01) ──────────────
   Team Members, Employee Spotlight, Team Grid, Employee Directory and Org
   Chart open a colleague's profile from their name (a stretched link — the
   whole card is the target), but the name was drawn `text-reset
   text-decoration-none`: the heading's ink and no underline, so nothing said
   it could be clicked. The name keeps the ink of the text around it — the
   card's body ink, or the band's ink on a dark ground, so it reads wherever
   the block is put — and carries the phone twin's mark, a dotted underline.
   Under the pointer (anywhere on the card: the stretched link covers it) the
   underline turns solid and heavier; from the keyboard it does the same and
   the name gets a ring in its own colour. The colour itself never moves:
   darkening a dark band's white ink would cost contrast, and the underline
   says "link" on every ground without relying on colour (WCAG 1.4.1).
   `!important` on the colour for the reason `.text-reset` had it: the link
   rules this replaces differ by ground and some are !important. */
.site-page-content a.site-person-link {
    color: inherit !important;
    text-decoration: underline dotted;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.2em;
}
.site-page-content a.site-person-link:is(:hover, :focus-visible) {
    text-decoration-style: solid;
    text-decoration-thickness: 2px;
}
.site-page-content a.site-person-link:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
    border-radius: 2px;
}

/* D6 + R4: JOURNEY MAP STATUS MARKERS — ONE INK, THE STATE IN THE SHAPE.
   The circles were success green (complete), brand (in progress) and grey
   (upcoming): two greens on a brand-green tenant, and the state told by colour
   alone with the word only visually-hidden (WCAG 1.4.1; Sites tester round 4,
   R4). Every step is now drawn in ONE ink — the brand; on a light tinted
   ground the brand measured for it (`--sec-brand-ink`, light theme only, "THE
   BRAND ON THE SECTION'S OWN GROUND" above); on a dark ground the band's ink —
   and the state is the SHAPE, written by SitesHelper#site_step_marker, with the
   word printed beside the owner:
     --complete   a solid disc with a check
     --active     a solid disc with a halo
     --upcoming   an outline ring, the step's icon or number inside (also a
                  step with no state, or the author's own word)
   The ring's inside is see-through, so it sits on whatever ground the author
   picked — which is why the vertical spine runs BETWEEN the markers
   (`.site-step-spine`).

   ONE INK THE THEME MEASURED (pre-merge review, round 4). The ring, the number
   or icon inside it and the solid disc are all the theme's link ink
   (`--bs-link-color`: the brand clamped to AA as text on the page, the card
   and the shell, in each theme). The raw brand read 4.47:1 as the ring's
   number on this tenant's page and 3.36:1 in the dark theme, and a pale brand
   drew a white check at 1.6–2:1 on a disc that all but vanished into the page
   (1.3:1). Because that ink clears AA against the light page, white on it
   clears AA too; in the dark theme it is a light ink, so the glyph on the disc
   is the page's dark ink (#212529, 5:1 or better on it). Measured on #0077be,
   #2e63b3, #1b8751, #ffd54f and #7fdbff: ring 4.8–7.8:1, glyph 4.6:1 and up.
   On a light tinted ground both are the brand measured for that ground and
   the label measured for it. */
.site-page-content .site-step-marker {
    --site-step-ink: var(--bs-link-color, var(--bs-primary));
    --site-step-on-ink: #ffffff;
    background-color: transparent;
    color: var(--site-step-ink);
    box-shadow: inset 0 0 0 2px var(--site-step-ink);
}
:root[data-theme="dark"] .site-page-content .site-step-marker {
    --site-step-on-ink: #212529;
}
:root:not([data-theme="dark"]) .site-page-content [style*="--sec-brand-ink:"]:not(.site-section-bg--on-dark, .site-item--on-dark) .site-step-marker {
    --site-step-ink: var(--sec-brand-ink);
    --site-step-on-ink: var(--sec-on-brand-ink, #ffffff);
}
.site-page-content :is(.site-step-marker--complete, .site-step-marker--active) {
    background-color: var(--site-step-ink);
    color: var(--site-step-on-ink);
    box-shadow: none;
}
.site-page-content .site-step-marker--active {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--site-step-ink) 35%, transparent);
}
/* The vertical layout's line from one marker to the next, in the muted ink at
   a third: `.text-muted` on the element is what the dark-band rules carry onto
   the band's ink, so the line follows every ground. */
.site-page-content .site-step-spine {
    background: currentColor;
    opacity: 0.35;
}
/* On a dark ground: the band's ink, the same three shapes (complete and in
   progress are ink discs with a dark glyph). */
.site-page-content :is(.site-section-bg--on-dark, .site-item--on-dark) .site-step-marker {
    background-color: transparent !important;
    color: var(--sec-fg, #ffffff) !important;
    box-shadow: inset 0 0 0 2px var(--sec-fg, #ffffff);
}
.site-page-content :is(.site-section-bg--on-dark, .site-item--on-dark) :is(.site-step-marker--complete, .site-step-marker--active) {
    background-color: var(--sec-fg, #ffffff) !important;
    color: #212529 !important;
}
.site-page-content :is(.site-section-bg--on-dark, .site-item--on-dark) .site-step-marker--active {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--sec-fg, #ffffff) 40%, transparent);
}

/* JOURNEY MAP FLOW — the horizontal layout's rows (Sites tester round 4, headed
   verify: "4 steps wrap 3 + 1 — the 4th alone on a second row, no arrow from
   step 3, the sequence reads as broken"). A step was its copy's width up to
   260px and the row broke on that width, so four ordinary steps wrapped at a
   1280 window beside the Sites sidebar. Now each step is an EQUAL SHARE of its
   row — `100% / --flow-cols` — and its words wrap inside it; how many share a
   row follows the block's width, each a balanced count the partial works out
   (SitesHelper#site_balanced_columns → --flow-cols-3/-4/-5):

     under 480px   stacked: one step under another, the arrow turned down
     from 480px    at most three a row
     from 640px    at most four
     from 800px    at most five (four steps are one row at the normal content
                   width; six are three and three, eight four and four)

   Each threshold is the width at which every step still gets 160px (about
   125px for its words beside its arrow), so no tier is narrower than another.

   On a wrapped row the last step's arrow is hidden by site_flow_row_controller,
   and every step keeps its arrow's room (the last step carries an invisible
   one), so the steps of a row are the same width. The arrow's 0.7rem top is
   the circle's midline: circle centre 1.25rem less half the glyph's 1.1rem. */
.site-page-content .site-flow {
    display: flex;
    flex-direction: column;
    align-items: center;
}
.site-page-content .site-flow > .site-flow__step {
    flex-direction: column;
    align-items: center;
    width: 100%;
    max-width: 22rem;
}
.site-page-content .site-flow__body {
    flex: 0 0 auto;
    width: 100%;
    min-width: 0;
}
.site-page-content .site-flow__connector {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    padding: 0.75rem 0;
    transform: rotate(90deg);
}
.site-page-content .site-flow__connector--end { display: none; }
@container site-cq (min-width: 480px) {
    .site-page-content .site-flow {
        --flow-cols: var(--flow-cols-3, 3);
        flex-direction: row;
        flex-wrap: wrap;
        align-items: stretch;
        row-gap: 3rem;
        column-gap: 0;
    }
    .site-page-content .site-flow > .site-flow__step {
        flex-direction: row;
        align-items: stretch;
        flex: 0 0 calc(100% / var(--flow-cols));
        width: auto;
        min-width: 0;
        max-width: calc(100% / var(--flow-cols));
    }
    .site-page-content .site-flow__body { flex: 1 1 auto; width: auto; }
    .site-page-content .site-flow__connector {
        align-self: flex-start;
        padding: 0.7rem 0.5rem 0;
        transform: none;
    }
    .site-page-content .site-flow__connector--end { display: flex; }
}
@container site-cq (min-width: 640px) {
    .site-page-content .site-flow { --flow-cols: var(--flow-cols-4, 4); }
}
@container site-cq (min-width: 800px) {
    .site-page-content .site-flow { --flow-cols: var(--flow-cols-5, 5); }
}

/* JOURNEY MAP OWNER PILL (Sites QA 2026-09-29: "the owner text sits low and
   cramped"). A `.badge` is line-height 1 with a tight em padding, and at 12px
   in this font the words rode on the bottom edge of the pill. A pill of its own
   line height, centred as a flex box, with room either side. It follows its own
   step's description — the horizontal flow used to push it to the bottom of
   the row with `mt-auto`, so on a row of uneven descriptions a pill sat a line
   or two below the words it belongs to. */
.site-page-content .site-step-owner {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: .3rem .7rem;
    font-size: .75rem !important;
    font-weight: 600;
    line-height: 1.3;
    max-width: 100%;
}
/* The pill's words in their own box (`__text`, both layouts). A flex item
   never shrinks below its longest word, so with the words as the flex box's
   bare text a single word longer than the pill — a 40-letter team name — ran
   out of it and over the next step (pre-merge review, Sites tester round 4: a
   252px word in a 225px pill). `min-width: 0` lets the box take the pill's
   width, and `break-word` breaks only that overflowing word; the pill itself
   still keeps its longest word whole wherever there is room. */
.site-page-content .site-step-owner__text {
    min-width: 0;
    max-width: 100%;
    overflow-wrap: break-word;
}

/* ONE 4:3 FRAME (`site-frame-4x3`). IMAGE GALLERY: ONE FRAME AT EVERY WIDTH
   (Sites QA 2026-09-29: "the canvas crop and the published crop differ"). The
   picture was 200px tall under a column-wide width, so its shape — and what
   `object-fit: cover` cut away — changed with the column: the editor's
   narrower canvas cropped the sides less than the reader's page. A fixed 4:3
   frame crops the same at every width, and the panel's hint on the image field
   says so (SitePage::SECTION_ENTRY_HINTS).
   PLATFORM PILLARS TAKE THE SAME FRAME (Sites tester round 7, 2026-10-07:
   "ensure all images are displayed at the same size and dimensions"): each
   pillar's picture kept its own proportions, so two pillars side by side had
   pictures of different heights and their headings started on different
   lines. The phone twins draw the same shape (`aspect-[4/3] object-cover`). */
.site-page-content .site-frame-4x3 {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
    object-fit: cover;
}

/* A TICKED POINT'S TICK SITS ON ITS FIRST LINE (Sites QA 2026-09-29, CTA
   Banner). The row was `align-items-center`, so a point that wrapped to two
   lines put the tick halfway down beside them, and in the centred banner the
   second line centred itself under the tick. The tick and the words now share
   the first line's baseline, and the words are one box that wraps to its own
   left edge. */
.site-page-content .site-check-item {
    display: flex;
    align-items: baseline;
    gap: .5rem;
}
.site-page-content .site-check-item > i { flex-shrink: 0; }
.site-page-content .site-check-item > .site-check-text {
    min-width: 0;
    text-align: start;
}

/* STATUS BANNER IS OPAQUE ON EVERY GROUND (Sites QA 2026-09-29: "the yellow
   banner looks see-through on the light backgrounds"). The tenant theme paints
   every `.alert-*` as a 15% wash of its status colour (BusinessTheme,
   `rgba(…, .15) !important`), so the banner took on whatever the section
   painted under it — a warning over a Brand tint read as a muddy peach — and
   only a dark band had an opaque answer (above). The status banner is a
   standing notice, so it keeps the look it has on a white page on every
   ground: the same 15% of the tenant's own status colour, mixed over white
   instead of over the section. Scoped to this block; the theme is untouched.
   The app's dark theme keeps its own answer ("A WASH IS LAID OVER THE PAGE,
   NOT OVER WHATEVER IS BEHIND IT", above), which already paints over an opaque
   ground and outranks this rule where it applies. */
.site-page-content .site-status-banner.alert {
    --site-status-tone: var(--bs-info, #0dcaf0);
    background-color: color-mix(in srgb, var(--site-status-tone) 15%, #ffffff) !important;
    background-image: none !important;
}
.site-page-content .site-status-banner.alert-success { --site-status-tone: var(--bs-success, #198754); }
.site-page-content .site-status-banner.alert-warning { --site-status-tone: var(--bs-warning, #ffc107); }
.site-page-content .site-status-banner.alert-danger  { --site-status-tone: var(--bs-danger, #dc3545); }
/* A BODY OF SEVERAL PARAGRAPHS STAYS SEVERAL PARAGRAPHS. custom.css makes every
   `.alert p` inline, !important (one-line flash and form alerts), so a Status
   Banner's notices — one paragraph each, typed or composed from its older
   `items` — ran together on one line ("…until 3 PM.VPN — …", Sites tester
   round 6), and so did an Announcement's body. Scoped to the authored body of
   those two blocks (`.site-alert-body`); other alerts keep the inline rule. */
.site-page-content .site-alert-body p { display: block !important; margin: 0 0 0.25rem; }
.site-page-content .site-alert-body p:last-child { margin-bottom: 0; }
/* ── end Sites QA 2026-09-29 lane B ── */

/* ── A SPOTLIGHT PERSON FROM THE DIRECTORY (2026-09-30, Sites QA — Justina) ──
   An Employee Spotlight item can name a person from the directory
   (apps/sites/pages/_inspector_entry_person). Once someone is chosen the card
   draws THEIR name, job title, department and photo, so the row's typed
   fields for those (.site-person-field) fold away rather than sit there
   looking editable while the page ignores them. Keyed on Tom Select's own
   `has-items` state, so it follows the picker live with no script of ours.
   A person who is no longer active (`--gone`) keeps them open: readers see
   the typed details then. The inputs stay in the form, so the typed values
   are kept on save — they are the fallback if the person leaves. Browsers
   without `:has()` simply keep the fields showing. */
.sites-editor-workspace [data-inspector-row] .card-body:has(.site-person-pick:not(.site-person-pick--gone) .ts-wrapper.has-items) .site-person-field {
  display: none;
}
/* ── end spotlight person ── */
