/* TPL.gr — shared stylesheet. Tokens per TPL-Design-Prompt-v3-SHARP / Option C design system. */
/* Self-hosted fonts (2026-07-15) — replaces the render-blocking, pre-consent Google Fonts
   <link> that used to sit in every page head (ePrivacy + perf). These are inline @font-face
   rules (NOT @import, which serialized and delayed first paint); latin/latin-ext/greek subsets,
   font-display:swap, unicode-range keeps the greek subset off the critical path for EN pages.
   woff2 files live in ../fonts/. Regenerate via scratchpad/fetch-fonts.mjs. */
@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;src:url(../fonts/inter-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;src:url(../fonts/inter-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;src:url(../fonts/inter-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Inter';font-style:normal;font-weight:500;font-display:swap;src:url(../fonts/inter-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
@font-face{font-family:'Inter';font-style:normal;font-weight:500;font-display:swap;src:url(../fonts/inter-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Inter';font-style:normal;font-weight:500;font-display:swap;src:url(../fonts/inter-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Inter';font-style:normal;font-weight:600;font-display:swap;src:url(../fonts/inter-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
@font-face{font-family:'Inter';font-style:normal;font-weight:600;font-display:swap;src:url(../fonts/inter-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Inter';font-style:normal;font-weight:600;font-display:swap;src:url(../fonts/inter-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:500;font-display:swap;src:url(../fonts/jetbrains-mono-500-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:500;font-display:swap;src:url(../fonts/jetbrains-mono-500-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:500;font-display:swap;src:url(../fonts/jetbrains-mono-500-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Sofia Sans';font-style:normal;font-weight:300;font-display:swap;src:url(../fonts/sofia-sans-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
@font-face{font-family:'Sofia Sans';font-style:normal;font-weight:300;font-display:swap;src:url(../fonts/sofia-sans-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Sofia Sans';font-style:normal;font-weight:300;font-display:swap;src:url(../fonts/sofia-sans-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Sofia Sans';font-style:normal;font-weight:700;font-display:swap;src:url(../fonts/sofia-sans-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
@font-face{font-family:'Sofia Sans';font-style:normal;font-weight:700;font-display:swap;src:url(../fonts/sofia-sans-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Sofia Sans';font-style:normal;font-weight:700;font-display:swap;src:url(../fonts/sofia-sans-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Sofia Sans';font-style:normal;font-weight:800;font-display:swap;src:url(../fonts/sofia-sans-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
@font-face{font-family:'Sofia Sans';font-style:normal;font-weight:800;font-display:swap;src:url(../fonts/sofia-sans-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Sofia Sans';font-style:normal;font-weight:800;font-display:swap;src:url(../fonts/sofia-sans-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}

/* ————— Breakpoint contract — scroll mechanics (2026-07-29) ———————————————
   ONE system, three bands, shared by every scroll-critical gate on the site:
       mobile  ≤720px      tablet  721–1024px      desktop  ≥1025px
   Written as `(max-width:720px)`, `(min-width:721px)`, `(min-width:1025px)`,
   and `(min-width:721px) and (max-width:1024px)` for tablet-only.
   Already implemented by: tpl.js's `breakpoint()` helper (hero video + poster
   source picker), `.hero-photo.hero-multi` (720/1024/1025 aspect ratios), the
   `.hiw` scrubbed panels and the `.fitx`/`.fit-curtain` scene. tpl.js's
   breakpoint() and .hero-multi's CSS must stay in LOCKSTEP — the JS picks the
   asset the CSS reserves the box for, so changing one without the other ships a
   1024-wide crop into a 430-wide frame.
   RULE: a gate that switches a scroll MECHANIC on or off — a pinned scene, a
   scroll scrub, a sticky column, a footer curtain, a scroll-driven layout swap —
   reads from this system. A component may keep a different number only with a
   comment beside it naming what specifically breaks at the contract boundary;
   an undocumented magic number is a defect, not a preference.
   Out of scope: purely cosmetic breakpoints (520/560/640/860/960/1100 and the
   remaining stage-height/grid tweaks) — they tune type and columns, they do not
   decide whether a mechanic runs. */
:root{
  color-scheme:light;
  /* paper ramp */
  --paper-0:#FFFFFF; --paper-50:#FBFAF7; --paper-100:#F2F1ED; --paper-200:#EBE9E3;
  --paper-300:#E8E6E0; --hairline:#DEDCD5; --paper-500:#C5C3BB;
  /* theme-INVARIANT primitives — deliberately NOT redefined in [data-theme="dark"] below, so they
     never flip. For surfaces that must stay fixed-white in every theme (logo plates / popovers:
     full-colour partner wordmarks with near-black lettering vanish on a dark card). Retrofit onto
     the older hard-coded #fff plates (.ft-row img, .qr-plate) next time either is touched. */
  --paper-fixed:#FFFFFF; --hairline-fixed:rgba(0,0,0,.10); --shadow-popover-fixed:0 8px 24px rgba(0,0,0,.16);
  /* the nav mega-panel is a much larger surface than a popover, so it carries a wider,
     softer shadow — a big sheet should read as thicker than a small chip */
  --shadow-panel:0 28px 48px -20px rgba(0,0,0,.16);
  --ink:#181818; --ink-soft:#4A4A4A; --ink-muted:#6B6C6C; --inverse:#0B0B0C;
  /* --band is the ground under a .band-inverse tonal break. It starts life equal to --inverse, so
     light theme renders exactly as before, but unlike --inverse it DOES flip: see the dark-theme
     block for the measurement that made it its own token (2026-08-20, ADR-010). */
  --band:#0B0B0C;
  /* red ramp — ≤5% of any viewport */
  /* --red-wash (a 6% red rgba) was deleted 2026-09-01, OWNER DIRECTION: no pale-red surface
     anywhere on the site. Its only two consumers were the .cs-more--tint / .mfsec--tint section
     grounds, now paper steps (:9767, :9958). Do not re-add it — a tint is a hue, and the ground
     ladder is built from tones. */
  --red:#AB0000; --red-hover:#B50000; --red-pressed:#7A0000; --red-ink:#AB0000;
  /* text-safe red on the fixed --inverse band (7.05:1) — deliberately NOT theme-swapped,
     since --inverse never flips; the hero eyebrow established the value */
  --red-ink-inverse:#ef7676;
  /* region accents — regions only (branches cards, region switcher); never brand chrome.
     *-ink = text-safe on the light paper ramp (≥4.5:1); base = fills/dots/rules only.
     --india is fill-only in light mode (2.2:1 on paper) — --india-ink is its text partner. */
  --green:#1F5C3A; --americas:#114B98; --india:#F8993A;
  --green-ink:#1F5C3A; --americas-ink:#114B98; --india-ink:#8F4A06;
  /* semantic feedback — deliberately OFF the brand red, so an error never reads as chrome
     and success has a colour at all (form states, live-status kickers) */
  --success:#1F5C3A; --error:#C3341E;
  --font-display:'Sofia Sans',sans-serif; --font-body:'Inter',sans-serif;
  --font-mono:'JetBrains Mono',monospace;
  --ease:cubic-bezier(0.2,0.8,0.2,1);
  /* Two stronger curves for grabbable surfaces, where --ease is too soft to read
     as physical. --ease-drawer is the iOS sheet curve (Ionic); --ease-out has the
     punch the built-in easings lack. Both are ease-OUT shaped on purpose: never
     ease-in on entry, it withholds movement at the moment the eye is on it.
     Used by the mobile menu sheet (.ms-*); adopt for the next drawer that needs
     to be draggable rather than adding a third curve beside them. */
  --ease-drawer:cubic-bezier(.32,.72,0,1);
  --ease-out:cubic-bezier(.23,1,.32,1);
  /* in-out curve for a box that resizes in place: slow ends, fast middle, so the eye reads
     one continuous morph rather than a start and a stop. Used by the dropdown host (.mm)
     height. Distinct from --ease-out, which decelerates into rest for things that arrive. */
  --ease-morph:cubic-bezier(.77,0,.175,1);
  /* overshoot curve for a small element that springs INTO place from nothing: the storefront
     row's hover arrow (.srow .ddgo) is the only user. Named rather than left inline for the
     same reason as --ease-drawer: the next thing that needs a spring adopts this instead of
     adding a fourth curve beside it. Never for anything that resizes or moves the layout. */
  --ease-spring:cubic-bezier(.34,1.56,.64,1);
  --max:1280px; --radius-card:16px;
  /* --radius-btn — the action shape. Both .btn (page-body CTAs) and .abar-cta (the chrome
     CTA) read this token, so the two can never drift out of step. Chips, tabs and tags stay
     pills on purpose: a pill reads as a filter, this rectangle reads as an action. */
  --radius-btn:8px;
  /* --logo-h — ONE height for the TPL wordmark in both places it is printed large: the main
     menu bar (.abar-logo img) and the footer sign-off (.ft-logo). They were 34px and 56px
     respectively until 2026-08-21; the owner asked for them to match, so the size is shared
     rather than restated. NOTE the pairing only exists above 820px: below it the mobile footer
     accordion swaps the wordmark for the square tpl-mark-white.svg and .ft-logo--mark pins it
     to 52x52, so the step below is effectively the MENU BAR's alone. */
  --logo-h:56px;
  /* the shared page gutter. --max caps the reading measure (.wrap, .dd-inner); the chrome
     bars (.nbar, .abar) are full-bleed and use only this gutter, so the logo and the pitch
     CTA sit on the same edge line as each other at every width. */
  --bar-gutter:clamp(20px,4vw,48px);
  /* ————— Chrome offset — ONE source of truth for "clear the fixed chrome" —————————
     --abar-h     the sticky .abar's own height (.abar-wrap reads this token, so the bar and
                  everything that clears it can never drift apart).
     --chrome-top TOTAL fixed chrome above the content. Anything that must sit clear of it is
                  `calc(var(--chrome-top) + its own gap)` — the anchor-jump scroll-margin
                  (+16px), .hlp-lead (+38px), investors' .ib-lead (+24px). The gaps stay
                  component-local (they are each component's chosen breathing room, not
                  chrome); only the CHROME part is shared.
     Normally --chrome-top is just the abar: the notification bar scrolls away in flow, and
     html.nbar-fix parks it off-screen with body{padding-top} holding its place, so it
     contributes no FIXED height. The single state where it does is html.nbar-fix.nbar-in,
     which re-pins it above the abar — the override sits beside those rules (search
     `.nbar-in{--chrome-top`). State-derived in CSS on purpose: tpl.js used to publish
     --chrome-top from nbar.offsetHeight unconditionally, which over-reserved one nbar
     (~39px) on every ordinary downward anchor jump, when the nbar is not even on screen.
     Resolves with JS off (nothing here needs a measurement). --nbar-h is declared here too,
     at its real measured height, so the nbar-fix/nbar-in rules below (and tpl.js, which
     overwrites this with an actual getBoundingClientRect() the instant it runs) have one
     correct fallback instead of three separately-typed guesses. */
  --abar-h:72px; --chrome-top:var(--abar-h); --nbar-h:39px;
}
/* the wordmark's one responsive step. 820px is inherited from the footer's own long-standing
   breakpoint for this mark, kept so the token has a single story; in practice only the menu bar
   reads it here, per the note on --logo-h. The bar is already in burger mode well above this
   (the desktop nav goes at 1180px), so the logo sits alone with 24px of vertical clearance. */
@media(max-width:820px){:root{--logo-h:48px}}
/* dark theme — lighter tonal variants of the same paper/ink ramps (never inversions).
   The topbar switch sets html[data-theme="dark"] and persists it (tpl.js).
   Two-tier red: fills keep the brand #AB0000 (--red); red TEXT uses --red-ink,
   brightened here to pass 4.5:1 on the dark paper.

   CARD SURFACES ARE var(--paper-0), IN BOTH THEMES (2026-08-24). Before this, five near-blacks
   described one surface in dark: #191919 (.mf-card, .wwd-card, .audience-tiles a), #17171B
   (.rv-card/.rv-arrow), rgba(20,20,23,.5) (.gtee--noise .gtee-card), #17171A (.sfc) and
   #141417 (a gtee icon interior). They drifted because the LIGHT rule was a literal
   (background:#fff), and a literal light surface forces a second literal down here to undo it.
   A card whose light surface is var(--paper-0) needs no dark rule at all: the token flips to
   #1A1A1C on its own. So give a new card background:var(--paper-0) and write NO dark override.
   Two surfaces stay explicit on purpose: .wwd-card, whose light surface is a translucent
   color-mix that must not carry into dark, and .gtee--noise .gtee-card, which is translucent so
   the noise band reads through it. Fixed-dark bands (.band-inverse, .sfc) keep their literals
   under the theme-invariant rule documented at ~236 and ~2207. */
html[data-theme="dark"]{
  --paper-0:#1A1A1C; --paper-50:#161618; --paper-100:#101012; --paper-200:#0B0B0C;
  --paper-300:#232326; --hairline:#2E2E33; --paper-500:#55554F;
  --ink:#F2F1ED; --ink-soft:#C9C8C2; --ink-muted:#989791;
  --red-ink:#E05252;
  /* ===== the dark-theme tonal break (2026-08-20, ADR-010) =====
     Every .band-inverse used the fixed --inverse (#0B0B0C) in both themes, so in dark theme the
     band sat on a body ground of --paper-100 #101012. MEASURE THE GROUND YOU ACTUALLY HAVE: the
     first pass at this reasoned against --paper-0 (#1A1A1C) and got the size of the problem
     wrong in both directions. Read off the rendered page, body is #101012 (L* 4.7) and the old
     band was L* 3.0, a step of ΔL* 1.7. That is genuinely too quiet, and it is a third of the
     ΔL* 4.9 this site already accepts as a visible step in light theme (--paper-100 on white).
     WCAG contrast ratio is no use here at all: it reads 1.04:1 and would read about that for any
     pair of near-blacks, because the ratio compresses to nothing down there while perception
     does not. L* is the metric.
     #020203 takes the step to ΔL* 4.2, which is 2.5x the old one and just under the light-theme
     step, and it is most of the 4.7 that exists between #101012 and pure black.
     WHY DARKER RATHER THAN LIGHTER, which is the usual dark-system idiom: lighter has far more
     headroom (#232326 would give ΔL* 9.1) but it inverts what the band CONTAINS. .band-inverse
     .card is #17171A (L* 7.9), so on any band lighter than that the raised card becomes an inset
     one, and the card would read as raised in light theme and sunken in dark. Matching it would
     mean re-toning .card, .mf-card, .kpi, .step-card, .li-chip and their borders, and changing
     what band-inverse IS. Darker keeps every one of those relationships and improves the card's
     own elevation (ΔL* 4.8 → 5.9 above the band).
     Not pure black despite the extra 0.5: large #000 fields under light text halate, and this
     leaves a floor for anything that ever has to sit under a band.
     THE EDGES ARE ASYMMETRIC, which is the other half of why the fill step matters. Measured on
     the rendered page, not assumed: a band carries no border of its own (0px), its TOP edge is
     drawn by the preceding section's border-bottom (--hairline #2E2E33, L* 19), and its BOTTOM
     edge has nothing at all, because .aud sets border-bottom:0 deliberately so the band flows
     into what follows. So the lower boundary is carried by the fill difference alone, and at
     ΔL* 1.7 there was effectively nothing holding it. Left as it is on purpose: adding a hairline
     there would overrule a standing design choice to fix a problem the fill step already fixes.
     No text literal changed, and all of them improve on the darker ground: #EBE9E3 17.4:1,
     #C5C3BB 12.0:1, --red-ink-inverse 7.6:1. */
  --band:#020203;
  --green-ink:#57B786; --americas-ink:#7FA8E8; --india-ink:#F8993A;
  --success:#57B786; --error:#F0705C;
  color-scheme:dark;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
/* The document scrollbar wears the brand red (owner call, 2026-09-01): thumb on --red-ink,
   the per-theme red — #AB0000 on paper, #E05252 on the dark ground, #900000 under
   a11y-contrast — so one rule stays legible everywhere. scrollbar-color covers
   Chromium 121+ and Firefox — and once it is set, Chromium ignores the ::-webkit-* block
   below, which therefore styles Safari alone: a 12px rail whose 3px transparent border +
   content-box clip renders a slim 6px pill. The Safari hover leans the red toward the
   theme's ink via color-mix, which intensifies in the right direction on both grounds. */
html{scrollbar-color:var(--red-ink) transparent}
::-webkit-scrollbar{width:12px;height:12px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background-color:var(--red-ink);border-radius:8px;border:3px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background-color:color-mix(in srgb,var(--red-ink) 82%,var(--ink))}
/* anchor jumps (#leadership, #pitch, ...) land clear of whatever chrome is actually fixed:
   --chrome-top (abar, +nbar only while re-pinned) + 16px of breathing room = the 88px this
   rule shipped with at rest. Do NOT re-add a flat +88px on top of --chrome-top — that was
   the double count that made every jump land ~88px low once --chrome-top carried the abar.
   KNOWN LIMITATION (2026-07-29): the browser samples --chrome-top once, when it computes the
   jump target, not continuously. An upward same-page jump that crosses upAcc>140 mid-scroll
   (tpl.js's scroll handler) can add nbar-in AFTER the target offset was already computed for
   the un-pinned 72px chrome, landing the heading ~39px under the re-pinned nbar+abar. Fixing
   this needs suppressing upAcc accumulation for a short window after an anchor click /
   hashchange, not a CSS change — not yet done. */
section[id],[id]:target{scroll-margin-top:calc(var(--chrome-top) + 16px)}
body{font-family:var(--font-body);font-size:16px;line-height:1.6;color:var(--ink);background:var(--paper-100)}
/* height:auto is REQUIRED, not cosmetic, now that every raster <img> carries width/height
   attributes for CLS (audit N9, 2026-07-30). The height attribute is a presentational hint with
   a real used value: without this, any image narrowed by max-width:100% would keep its intrinsic
   pixel height and stretch. Components that set a real height (.ft-row img, .hx-photo,
   .step-photo img, …) are all at higher specificity and still win. */
img{max-width:100%;height:auto;display:block}
/* ————— assets that ship with a baked-in plate —————
   Eight marks in the register are not artwork on a transparent ground: they are a WHITE
   RECTANGLE with the logo sitting on it, measured 2026-08-31 by rasterising all 150 logo assets
   and reading the frame of each box (91-100% near-white on the outer 4px ring): Halti, BRBL and
   the six /amazon/ service lockups. Every component that draws a logo assumes transparency
   around the ink, so that plate turns into a visible box the moment the surface under it is not
   the same white — and it is never the same white for long: .bw-face:hover swaps the tile to
   --paper-200 (#EBE9E3 in light), dark theme puts every surface near-black, and any tinted band
   does it at rest. Blending, not a filter, is what deletes a ground: multiply keeps the dark ink
   and lets any lighter surface through, screen keeps the light ink and lets any darker surface
   through, which is why the dark rule is screen — by then the component's invert has turned the
   plate near-black and the ink light.
   Keyed on the ASSET, deliberately, not on a component: the plate travels with the file, so the
   fix has to travel with it too, or the next row that shows Halti grows the box back. Nothing
   here is a substitute for a transparent asset. When one lands, its line leaves this block. */
img[src*="brands/halti"],
img[src*="brands/brbl"],
img[src*="/mq/amazon-"]{mix-blend-mode:multiply}
html[data-theme="dark"] img[src*="brands/halti"],
html[data-theme="dark"] img[src*="brands/brbl"],
html[data-theme="dark"] img[src*="/mq/amazon-"]{mix-blend-mode:screen}
a{color:inherit}

/* type */
h1,h2,h3{font-family:var(--font-display);font-weight:800;letter-spacing:-0.02em;line-height:1.05}
h1{font-size:clamp(38px,5.5vw,72px)}
h2{font-size:clamp(30px,3.5vw,48px);line-height:1.15;letter-spacing:-0.014em}
h3{font-size:20px;font-weight:700;line-height:1.3;letter-spacing:-0.004em}
.two-tone span:not(.word){color:var(--ink-soft)}
/* mid-page h2s resolve to a red second tone — statically here, so pages without GSAP
   match the filled end-state of the scroll reveal (index/d2c/b2b2c load GSAP, the other
   17 pages don't; before this rule they silently kept the gray fallback forever).
   h1 heroes keep the muted tone (the reveal deliberately skips them). */
h2.two-tone span:not(.word){color:var(--red-ink)}
/* opt-in modifier for a hero h1 that wants the mid-page treatment instead of the muted
   second tone: clause 1 stays --ink, clause 2 goes red. Used on contact.html. Type+class
   on both compounds so it beats :151 wherever it's applied. */
h1.two-tone--red span:not(.word){color:var(--red-ink)}
/* Second tone on the fixed-dark bands. Was rgba(255,255,255,.66) while the scrub
   skipped these surfaces; now that it fills them too, the static end-state has to
   be the same red the fill lands on (same match-the-end-state rule as :152). */
.band-inverse h2.two-tone span:not(.word){color:var(--red-ink-inverse)}
h2.two-tone{margin-bottom:12px}
/* Scroll-scrubbed heading colour-fill reveal (GSAP). JS splits each mid-page
   two-tone heading into per-word spans that fill gray→colour left→right on scroll:
   .w-ink (sentence 1) → dark ink, then .w-red (sentence 2) → primary red.
   inline-block isolates each word; JS only adds .word when motion is allowed, so
   no-JS / reduced-motion headings render untouched at their natural two-tone. */
.two-tone .word{display:inline-block}
/* Fill palette, declared ON the heading rather than read off :root, so the scrub
   (tpl.js, `colours()`) resolves whatever the heading's own surface needs. Defaults
   are the light-surface tokens every mid-page heading filled toward before. */
.two-tone{--fill-gray:var(--ink-muted);--fill-ink:var(--ink);--fill-red:var(--red-ink)}
/* The fixed-dark bands do not flip with the theme, so the paper tokens are the wrong
   measure there — --ink is near-black in light theme and would fill to nothing. Same
   theme-invariant-literal rule as .band-inverse .tile-icon (~1705); the red is the
   inverse token those bands already use for .biglink:hover and .fitx-head .eyebrow. */
.band-inverse .two-tone,.fitx-band .two-tone,.ainx-sec .two-tone{
  --fill-gray:rgba(255,255,255,.34);--fill-ink:#fff;--fill-red:var(--red-ink-inverse)}
/* Hero heading — common rule across every page's #main hero (index .hero,
   the plain subpage heroes, and the models .hero/.hero-photo pages; excludes
   contact's .ct-statement and 404's sr-only h1). The heading is two
   <br>-separated clauses meant to read as two lines; text-wrap:balance evens
   out any clause too long for one line at the big display size instead of
   leaving it ragged. On phones the whole hero steps down to a smaller,
   balanced size site-wide (allow wrap — a different, consistent mobile style). */
#main h1.two-tone{text-wrap:balance}
@media (max-width:640px){
  #main h1.two-tone{font-size:clamp(28px,8vw,34px);line-height:1.12}
}
.eyebrow{font-family:var(--font-mono);font-weight:500;font-size:12px;color:var(--red-ink);letter-spacing:0.12em;text-transform:uppercase;display:block;margin-bottom:16px}
/* Breadcrumbs are NOT defined here. An in-hero variant briefly lived at this line on 2026-08-06
   (the eyebrow itself made navigable) and was reverted the same day: the .crumb-bar row further
   down this file already covers 47 pages against that variant's 23, publishes --crumb-h so the
   chrome offsets resolve, and handles the mobile overflow and the ai-neural palette. Two .crumb
   blocks in one stylesheet also silently collided — the later one won on equal specificity, so
   the in-hero version rendered in the crumb-bar's mono/uppercase/muted treatment rather than its
   own. The single definition lives at .crumb / .crumb-bar (~3570). Do not re-add one here. */
.kicker{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--ink-muted)}
.lede{font-size:18px;color:var(--ink-soft);max-width:65ch}
.num{font-family:var(--font-mono);font-weight:500}

/* layout */
.wrap{max-width:var(--max);margin:0 auto;padding:0 clamp(20px,4vw,48px)}
section{padding:96px 0;border-bottom:1px solid var(--hairline)}
/* KPI band reads as a thin ledger strip, not a full section; tighten it and the run into the router below */
section:has(.kpi-band){padding:48px 0}
section:has(.kpi-band)+section{padding-top:64px}
/* D2C: the "How it works" scrub scene's last panel is meant to end flush against
   whatever comes next, so its own bottom padding is zeroed. "The comparison" section
   right after keeps its normal 96px top padding rather than sitting flush on the panel. */
#how{padding-bottom:0}
@media(max-width:720px){section{padding:48px 0}}
.grid-60-40{display:grid;grid-template-columns:3fr 2fr;gap:48px;align-items:start}
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:24px}
.grid-5{display:grid;grid-template-columns:repeat(5,1fr);gap:20px}
.grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:24px}
@media(max-width:1100px){.grid-5{grid-template-columns:repeat(3,1fr)}}
@media(max-width:960px){.grid-60-40,.grid-3,.grid-4,.grid-5{grid-template-columns:1fr 1fr}}
/* .grid-5 (people, team.html only) keeps the 960px 2-up below 640px too — collapsing it to
   1 column stacked 17 full-width portrait cards, roughly a screen each. Compact card sizing
   for that width lives with the team styles (~2120). */
@media(max-width:640px){.grid-60-40,.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}}

.lang-menu{position:relative}
.lang-btn{display:inline-flex;align-items:center;gap:8px;background:none;border:0;color:#fff;cursor:pointer;font:inherit;min-height:24px;padding:2px 4px;position:relative}
.lang-btn::before{content:'';position:absolute;inset:-10px -4px} /* 44px hit area, no visual change */
.lang-btn .chev{width:12px;height:12px;transition:transform 200ms var(--ease)}
.lang-btn[aria-expanded="true"] .chev{transform:rotate(180deg)}
/* the panel used to appear with no motion at all while .lang-btn .chev animated at 200ms right
   beside it. @starting-style + allow-discrete gives it the same origin-aware entry as
   .nbar-region-panel without any JS; transform-origin is its trigger, top-right. */
.lang-list{position:absolute;right:0;top:calc(100% + 10px);min-width:170px;background:var(--paper-0);border:1px solid var(--hairline);border-radius:12px;box-shadow:0 12px 32px rgba(0,0,0,0.18);padding:6px;z-index:70;font-size:14px;
  transform-origin:top right;opacity:1;scale:1;
  transition:opacity 150ms var(--ease-out),scale 150ms var(--ease-out),display 150ms allow-discrete}
@starting-style{.lang-list{opacity:0;scale:.97}}
.lang-list[hidden]{display:none;opacity:0;scale:.97}
@media(prefers-reduced-motion:reduce){.lang-list{transition:opacity 150ms var(--ease),display 150ms allow-discrete;scale:1}
  @starting-style{.lang-list{scale:1}}}
html.a11y-no-motion .lang-list{transition:none;scale:1}
.lang-list button{display:flex;align-items:center;gap:10px;width:100%;background:none;border:0;font:inherit;color:var(--ink);padding:9px 10px;border-radius:8px;cursor:pointer;text-align:left;min-height:36px}
.lang-list button:hover{background:var(--paper-100)}
.lang-list button[aria-checked="true"]{color:var(--red-ink);font-weight:600} /* weight = non-color cue */
.theme-switch{display:inline-flex;align-items:center;gap:8px;background:none;border:0;cursor:pointer;font:inherit;padding:2px;min-height:24px;position:relative}
.theme-switch::before{content:'';position:absolute;inset:-10px -4px} /* 44px hit area, no visual change */
.theme-switch .ico{width:13px;height:13px;color:rgba(255,255,255,0.45);transition:color 200ms var(--ease)}
.theme-switch[aria-checked="false"] .sun{color:#fff}
.theme-switch[aria-checked="true"] .moon{color:#fff}
/* dark theme: the switch sets html[data-theme="dark"]; the palette pass lands after site sign-off */
/* ===== Notification bar (.nbar) — announcement left, language + theme right ===== */
/* option-A topbar port: translucent glass bar + hairline; solid --inverse fallback.
   position/z-index above the sticky nav (.abar z-50) so the region dropdown — trapped
   in .nbar's backdrop-filter stacking context — renders over the nav instead of under it. */
/* flat light bar on the page tokens (matches the paper nav; auto-adapts in dark mode) — no glassmorphism */
.nbar{position:relative;z-index:60;background:var(--paper-100);color:var(--ink-soft);border-bottom:1px solid var(--hairline)}
.nbar-wrap{max-width:none;padding:0 var(--bar-gutter);min-height:38px;display:flex;align-items:center;justify-content:space-between;gap:12px}
.nbar-msg{display:inline-flex;align-items:center;gap:9px;min-width:0;color:inherit;text-decoration:none;font-family:var(--font-mono);font-size:11.5px;font-weight:500;letter-spacing:.04em;text-transform:uppercase;min-height:38px;transition:color 160ms var(--ease)}
.nbar-txt{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.nbar-msg:hover{color:var(--ink)}
.nbar-tag{font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:#fff;background:var(--red);border:0;border-radius:9999px;padding:3px 9px;flex:none}
.nbar-arr{width:14px;height:14px;flex:none;color:var(--ink-muted);transition:transform 200ms var(--ease),color 200ms var(--ease)}
@media(hover:hover) and (pointer:fine){.nbar-msg:hover .nbar-arr{transform:translateX(4px);color:var(--red-ink)}}
.nbar-ctl{display:flex;align-items:center;gap:2px;flex:none}
/* ————— B2B & Partner login (.nbar-login) — the one door into the B2B platform,
   which lives in a separate repo (spec: docs/superpowers/specs/2026-08-17-b2b-partner-login-
   front-door-design.md). Utility treatment on purpose: .abar-cta below is the site's ONE
   conversion action and a second solid pill beside it would split the eye. Hence outline,
   body font at 12.5px, and --ink rather than --red until hover.
   FIRST in .nbar-ctl so tab order reads login → language → theme, which is decreasing order
   of what a returning B2B user came for.
   Height: --nbar-h is 39px so a 44px visual control cannot fit. Same answer as the
   .lang-btn/.theme-switch pair two rules up — 26px visual, ::before overlay to 44px.
   The nbar parks off-screen on scroll-down (html.nbar-fix), so this is present on arrival
   and on scroll-up, not permanently. Accepted: logging in is a utility action taken on
   arrival, not a goal to pursue down the page. */
.nbar-login{display:inline-flex;align-items:center;gap:6px;position:relative;min-height:26px;padding:4px 9px;margin-right:6px;border:1px solid var(--hairline);border-radius:7px;color:var(--ink);text-decoration:none;font-family:var(--font-body);font-size:12.5px;font-weight:600;letter-spacing:.01em;white-space:nowrap;transition:color .15s var(--ease),border-color .15s var(--ease),background .15s var(--ease)}
.nbar-login::before{content:'';position:absolute;inset:-9px -4px} /* 44px hit area, no visual change */
.nbar-login .lock{width:13px;height:13px;flex:none}
@media(hover:hover) and (pointer:fine){.nbar-login:hover{color:var(--red-ink);border-color:var(--red-ink);background:var(--paper-200)}}
.nbar-login:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
/* 640px is where .nbar-tag and .nbar-msg-x already drop (see the .nbar-msg media rule below),
   so the bar thins out at ONE width rather than four. The button never disappears: unlike
   .abar-cta it has no burger-sheet fallback, so "Login" alone is the floor. */
@media(max-width:640px){.nbar-login{padding:4px 8px;margin-right:4px}.nbar-login .cta-long{display:none}}
html.a11y-no-motion .nbar-login{transition:none}
@media(prefers-reduced-motion:reduce){.nbar-login{transition:none}}
/* right cluster — pill triggers on the dark bar */
.nbar .lang-btn{color:var(--ink-soft);font-family:var(--font-body);font-size:12.5px;font-weight:500;gap:5px;padding:6px 8px;border-radius:7px;min-height:0;background:none;border:0;transition:background .15s var(--ease),color .15s var(--ease)}
.nbar .lang-btn:hover{color:var(--ink);background:var(--paper-200)}
.nbar .theme-switch{position:relative;width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;border-radius:7px;background:none;border:0;transition:background .15s var(--ease)}
.nbar .theme-switch::before{content:'';position:absolute;inset:-6px -4px} /* keeps a 44px hit target on the slim icon */
.nbar .theme-switch:hover{background:var(--paper-200)}
.nbar .theme-switch .ico{width:16px;height:16px;color:var(--ink-soft)}
/* region switcher — pill trigger on the dark bar, light dropdown panel (JS-injected) */
.nbar-region{position:relative;flex:none;border-bottom:0}
.nbar-region>summary::after,.nbar-region[open]>summary::after{content:none} /* kill the global FAQ-accordion +/− marker (both states) — keep only the caret */
.nbar-region>summary{position:relative;list-style:none;display:inline-flex;align-items:center;gap:7px;padding:6px 8px;min-height:0;border-radius:7px;background:none;border:0;font-family:var(--font-body);font-size:12.5px;font-weight:500;color:var(--ink-soft);cursor:pointer;user-select:none;white-space:nowrap;transition:background .15s var(--ease),color .15s var(--ease)}
.nbar-region>summary::before{content:'';position:absolute;inset:-6px -2px} /* 44px hit target */
.nbar-region>summary::-webkit-details-marker{display:none}
.nbar-region>summary::marker{content:""}
.nbar-region>summary:hover{color:var(--ink);background:var(--paper-200)}
.nbar-swatch{width:8px;height:8px;border-radius:50%;background:var(--red);flex:none}
.nbar-region-caret{width:12px;height:12px;flex:none;color:var(--ink-muted);transition:transform .2s var(--ease)}
.nbar-region[open]>summary .nbar-region-caret{transform:rotate(180deg)}
/* entry via @starting-style, the same recipe as .lang-list/.a11y-panel -- the starting style
   fires on open with no JS and no keyframes; transform-origin is its trigger, top-right.
   PHANTOM PANEL (fixed 2026-08-23): this rule used to set `display:flex` unconditionally and
   lean on the <details> element to hide its own subtree while closed, on the reasoning recorded
   here that "the details [open] toggle renders the subtree". It does not, for THIS panel.
   Chromium hides closed details content with `::details-content{content-visibility:hidden}`,
   and content-visibility is containment: an out-of-flow box escapes it when its containing
   block sits OUTSIDE the contained subtree. `.nbar-region` is `position:relative`, so this
   absolutely-positioned panel resolves against the <details> itself — an ancestor of
   ::details-content — and so was laid out, painted and hit-tested at full opacity with the
   menu closed: a 300x270 phantom card hanging under the ribbon over page content, which is
   what "TPL AMERICAS LLC" hover reports were actually showing (the region logos and names in
   this panel, NOT a duplicated header). Verified over CDP with the details closed: computed
   display flex, opacity 1, visibility visible, content-visibility visible, top 45px.
   The answer is the one .a11y-panel already uses (~line 3774): own the display state
   explicitly instead of trusting an ancestor to do it, and transition it allow-discrete so the
   @starting-style entry still runs. Never restore an unconditional `display` here. */
.nbar-region-panel{position:absolute;top:calc(100% + 10px);right:0;min-width:300px;background:var(--paper-0);border:1px solid var(--hairline);border-radius:12px;box-shadow:0 16px 40px rgba(0,0,0,.22);padding:8px;display:none;flex-direction:column;gap:2px;z-index:80;
  transform-origin:top right;opacity:1;scale:1;
  transition:opacity 150ms var(--ease-out),scale 150ms var(--ease-out),display 150ms allow-discrete}
.nbar-region[open] .nbar-region-panel{display:flex}
@starting-style{.nbar-region[open] .nbar-region-panel{opacity:0;scale:.97}}
.nbar-region-item{display:grid;grid-template-columns:auto 60px 1fr auto;align-items:center;gap:12px;padding:10px 12px;border-radius:8px;text-decoration:none;color:var(--ink);transition:background .15s var(--ease)}
a.nbar-region-item:hover{background:var(--paper-100)}
.nbar-region-item[aria-disabled="true"]{opacity:.6;cursor:default}
.nbar-region-logo{display:block;width:60px;height:20px;object-fit:contain;object-position:left center}
.nbar-region-txt{display:flex;flex-direction:column;gap:2px;min-width:0}
.nbar-region-name{font-size:14px;font-weight:600;line-height:1.2;color:var(--ink)}
.nbar-region-meta{font-size:12px;color:var(--ink-muted)}
.nbar-region-loc{font-size:11px;color:var(--ink-muted)}
.nbar-region-tag{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;padding:3px 8px;border-radius:9999px;background:var(--paper-100);color:var(--ink-muted);white-space:nowrap}
.nbar-region-item.is-active{background:var(--paper-100)}
.nbar-region-item.is-active .nbar-region-tag{background:var(--red);color:#fff}
@media(prefers-reduced-motion:reduce){.nbar-region-panel{transition:opacity 150ms var(--ease),display 150ms allow-discrete;scale:1}
  @starting-style{.nbar-region[open] .nbar-region-panel{scale:1}}}
html.a11y-no-motion .nbar-region-panel{transition:none;scale:1}
/* Phone: the panel becomes a near-full-width sheet, so it has to be positioned against the
   full-bleed .nbar (already position:relative) instead of the 43px summary it hangs off.
   With right:0 still resolving against .nbar-region, the widened panel grew LEFTWARD from
   that control's right edge and ran out of the viewport — left:-74px at 390px, and negative
   overflow never scrolls, so the region dots and half of every logo were unreachable.
   Taking .nbar-region out of the positioning chain hands the panel to .nbar, where
   left/right land on the bar's own gutter (aligned with the message and the theme switch).
   Percentages in `top` now resolve against the 38px bar, not the 26px control. */
@media(max-width:640px){.nbar-region-label{display:none}.nbar-region{position:static}.nbar-region-panel{min-width:0;left:var(--bar-gutter);right:var(--bar-gutter);width:auto}}
.nbar .theme-switch:hover .ico{color:var(--ink)}
/* Icon = the MODE, not the resolved theme: in System the button must read "following
   your device", not "dark", or the next click looks like it will do nothing. No-JS
   visitors get no data-theme-mode and fall through to the sun. */
.nbar .theme-switch .moon,.nbar .theme-switch .auto{display:none}
html[data-theme-mode="system"] .nbar .theme-switch .sun{display:none}
html[data-theme-mode="system"] .nbar .theme-switch .auto{display:inline-flex}
html[data-theme-mode="dark"] .nbar .theme-switch .sun{display:none}
html[data-theme-mode="dark"] .nbar .theme-switch .moon{display:inline-flex}
.nbar a:focus-visible,.nbar button:focus-visible{outline-color:var(--ink)}
html.a11y-no-motion .nbar-msg,html.a11y-no-motion .nbar-arr{transition:none}
@media(prefers-reduced-motion:reduce){.nbar-msg,.nbar-arr{transition:none}}
@media(max-width:640px){.nbar-msg{font-size:12.5px}.nbar-tag{display:none}.nbar-msg-x{display:none}}

/* ===== Anthropic-style single bar (.abar) — replaces .topbar + nav.main ===== */
.abar{position:sticky;top:0;z-index:50;background:var(--paper-100);border-bottom:1px solid var(--hairline);transition:box-shadow 240ms var(--ease),background 240ms var(--ease),top 280ms var(--ease)}
/* scrolled = frosted glass, same recipe as the floating cards over the boxes image */
.abar.scrolled{background:rgba(242,241,237,0.78);background:color-mix(in srgb,var(--paper-100) 78%,transparent);-webkit-backdrop-filter:blur(12px) saturate(1.3);backdrop-filter:blur(12px) saturate(1.3);box-shadow:0 8px 24px rgba(0,0,0,0.06)}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.abar.scrolled{background:var(--paper-100)}}
/* notification bar returns on a few ticks of upward scroll (html classes set by tpl.js).
   The parked state is a NEGATIVE top, not transform:translateY(-100%), and that is load-bearing:
     · engaging nbar-fix changes top from `auto` (static) to a length, and `auto` is not
       interpolable, so NO transition runs — the bar is simply already parked. With the old
       transform the before-change value was `none`, so the 280ms transition DID run: at the
       toggle moment the bar (already scrolled out of view) snapped back to viewport 0 over the
       .abar and slid out again. Measured 2026-07-29 over CDP at y=200: one CSSTransition
       (transform, 280ms), .nbar top sampled 0 → -39px across 18 frames. Same on release
       (length → auto also does not interpolate), so the hand-back into flow is instant too.
     · nbar-in ⇄ nbar-fix is length → length, so the slide still animates — and now on the SAME
       property, duration and easing as `.abar`'s own `top 280ms var(--ease)` below, so the two
       bars move as one coordinated block instead of two desynchronised animations.
     · the park offset and the reserved space now read the same --nbar-h token, so they cannot
       disagree and let the bar peek. tpl.js's release thresholds are derived from that same
       measurement — see the comment on `release` in the scroll handler.
   Deliberate exception to the site's "animate transform/opacity only" rule, for the reason
   above; `.abar` already animates `top` on the same 280ms window, so this adds no new class of
   work — one out-of-flow bar each, no content reflow (body's padding-top does not transition). */
html.nbar-fix body{padding-top:var(--nbar-h)}
html.nbar-fix .nbar{position:fixed;top:calc(-1 * var(--nbar-h));left:0;right:0;z-index:60;transition:top 280ms var(--ease)}
html.nbar-in .nbar{top:0}
html.nbar-in .abar{top:var(--nbar-h)}
/* ...and that is the ONE state where the fixed chrome is taller than the abar, so it is the
   one state that redefines --chrome-top (see the token block at the top of this file). Every
   consumer — anchor scroll-margin, .hlp-lead, investors' .ib-lead — follows from here. */
html.nbar-fix.nbar-in{--chrome-top:calc(var(--abar-h) + var(--nbar-h))}
html.a11y-no-motion .nbar,html.a11y-no-motion .abar{transition:none}
@media(prefers-reduced-motion:reduce){html.nbar-fix .nbar,.abar{transition:none}}
/* GHOST HEADER — why the frost is dropped while the abar is off its resting top.
   `.abar.scrolled` is 78 % translucent over a `backdrop-filter:blur()`. Chromium samples that
   backdrop from a snapshot of the page taken at the element's own paint offset, and it does NOT
   re-take it when the element is moved by an animated `top`. So in the re-pinned state (and
   across both 280ms slides into and out of it) the blurred bar shows a copy of ITSELF sampled
   at the old offset — a second, washed-out header sitting --nbar-h above the real one, visible
   through the translucent 78 %. It surfaces on any repaint inside the chrome, which is why
   hovering the ribbon message (its arrow nudges 4px, its colour transitions) makes it appear.
   Reported on the top ribbon 2026-08-23; same class of artefact as .menu-lock's forced-opaque
   rule at ~line 845, and the same answer: a moving bar is a SOLID bar. Keyed on state, not on
   JS-measured geometry, so it also holds with JS off (where neither class is ever set).
   .nbar-move is set by tpl.js for the length of the slide in EITHER direction — the CSS state
   alone covers only the re-pinned half, and the slide back out is blurred and moving too.
   2026-08-28: tpl.js now arms that same window on the nbar-fix toggle as well, not just on
   nbar-in. Engaging or releasing nbar-fix swaps the nbar out of flow for `body{padding-top}`,
   so the whole document jumps one nbar in a single frame under the frost — the same stale
   snapshot, from a state nothing was guarding. Because the artefact is composited it outlived
   the jump and only surfaced later, on the next repaint inside the chrome (an owner report of
   a clipped second logo and nav appearing on ribbon hover). The frost is therefore live in
   exactly one state now: scrolled down, parked, and not moving. */
html.nbar-in .abar.scrolled,html.nbar-move .abar.scrolled{
  background:var(--paper-100);-webkit-backdrop-filter:none;backdrop-filter:none}
/* Full-bleed bar on three tracks: logo on the left edge, links centred, pitch CTA on the
   right edge. The two 1fr tracks are what centres the nav — they stay equal to each other
   whatever the logo and the CTA happen to measure, so the links read as centred on the page
   rather than centred in the space left over between them (which is what a flex
   `margin-inline:auto` would give, and it drifts left as the CTA grows). Links, logo and CTA
   all compress fluidly so the desktop bar survives down to the burger breakpoint.

   Why the link type ramps 15px -> 13px instead of staying at 15px: centring costs horizontal
   room, because both 1fr tracks must be as wide as the WIDER edge item (the 160-174px pitch
   CTA, not the ~102px logo), so the nav gets `available - 2x CTA` rather than
   `available - logo - CTA`. Measured at 1181px (the burger handoff) with the type pinned at
   15px, the CTA overlapped the last link by 24px. The ramp buys back ~76px of label width and
   leaves 22px of clearance at 1181 and 80px at 1440. Do not flatten it to a fixed 15px without
   also moving the burger breakpoint up to ~1280 — which would cost the desktop nav on
   1280x800 laptops. Verified over CDP at 2560/1920/1600/1440/1366/1280/1200/1181. */
.abar-wrap{max-width:none;padding:0 var(--bar-gutter);height:var(--abar-h);display:grid;
  grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:stretch;
  column-gap:clamp(12px,1.6vw,28px)}
.abar-logo{grid-column:1;justify-self:start;display:flex;align-items:center}
.abar-logo img{height:var(--logo-h);width:auto;display:block;aspect-ratio:811.82/270.13}
.abar-nav{grid-column:2;display:flex;align-items:stretch;justify-content:center;height:100%;gap:clamp(0px,.18vw,2px)}
.abar-nav a{position:relative;display:inline-flex;align-items:center;height:100%;padding:0 clamp(8px,.98vw,14px);white-space:nowrap;font-family:var(--font-body);font-size:clamp(13px,1.05vw,15px);font-weight:500;color:var(--ink-soft);text-decoration:none;border-radius:8px;transition:color 160ms var(--ease),background 160ms var(--ease)}
.abar-nav a:hover{color:var(--red-ink)}
/* press feedback: the panel items already had an :active state, the triggers did not, so
   clicking a top-level item felt dead. Same wash the open trigger uses. */
.abar-nav a{transition:color 140ms var(--ease),background 100ms var(--ease)}
.abar-nav a:active{background:var(--paper-200);border-radius:8px}
.abar-nav a[aria-current]{color:var(--ink)}
/* current-page indicator — a small dot under the label, macOS-dock style, instead of a hover/active fill */
.abar-nav a[aria-current]::after{content:'';position:absolute;left:50%;bottom:10px;transform:translateX(-50%);width:5px;height:5px;border-radius:50%;background:var(--red)}
.abar-right{grid-column:3;justify-self:end;display:flex;align-items:center;gap:6px}

/* gap, not a markup space: the pill's label can carry a second flex item (an icon, or a span the
   narrow breakpoints drop). Flex blockifies such a span and trims its leading whitespace, which
   rendered "Pitchyour brand" when the label was still split; the space stays in the HTML because
   it is what makes the accessible name correct, and gap restores the visual word space. */
.abar-cta{display:inline-flex;align-items:center;gap:.26em;height:44px;padding:0 clamp(13px,1.5vw,22px);border-radius:var(--radius-btn);white-space:nowrap;background:var(--red);color:#fff;font-family:var(--font-body);font-weight:600;font-size:clamp(14px,1.05vw,15px);text-decoration:none;transition:background 150ms var(--ease),transform 100ms var(--ease)}
.abar-cta:hover{background:var(--red-hover)}

/* the shared language + theme controls, restyled for the light bar */
.abar .lang-btn{color:var(--ink-soft);font-family:var(--font-body);font-size:14px;font-weight:500;gap:6px;padding:8px 10px;border-radius:8px;min-height:44px}
.abar .lang-btn:hover{color:var(--ink);background:var(--paper-200)}
/* theme toggle — compact icon-only button; icon reflects the active theme (pre-paint via data-theme) */
.abar .theme-switch{width:44px;height:44px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:none;border:0;border-radius:8px;cursor:pointer}
.abar .theme-switch:hover{background:var(--paper-200)}
.abar .theme-switch .ico{width:18px;height:18px;color:var(--ink-soft)}
.abar .theme-switch:hover .ico{color:var(--ink)}
/* Icon = the MODE, not the resolved theme: in System the button must read "following
   your device", not "dark", or the next click looks like it will do nothing. No-JS
   visitors get no data-theme-mode and fall through to the sun. */
.abar .theme-switch .moon,.abar .theme-switch .auto{display:none}
html[data-theme-mode="system"] .abar .theme-switch .sun{display:none}
html[data-theme-mode="system"] .abar .theme-switch .auto{display:inline-flex}
html[data-theme-mode="dark"] .abar .theme-switch .sun{display:none}
html[data-theme-mode="dark"] .abar .theme-switch .moon{display:inline-flex}

/* nav dropdowns (.dd) — full-width band attached flush under the bar (no floating card),
   grouped sections with mono kickers, stroke icons with real looping micro-motion on hover. */
.abar-nav .dd{display:inline-flex;align-items:stretch}
.abar-nav .dd>a .chev{width:11px;height:11px;margin-left:6px;transition:transform 180ms var(--ease)}
/* ===== RED HAS ONE JOB IN THIS COMPONENT: "you are here" =====================================
   It used to carry six meanings at once — panel seam, open-menu wash, section dot, current page,
   featured row and every hover — so on an open panel four reds fired simultaneously and none of
   them read as information. Red now marks location only: the section dot on the trigger, and the
   current row inside the panel. Everything else moved to surface, weight and neutral value:
     open trigger  → neutral raise      hover      → background shift + arrow slide
     panel seam    → hairline           flagship   → raised surface + weight
   ============================================================================================ */
/* open trigger reads as a lit tab flowing into the panel below it — flat bottom corners meet the
   panel's top edge flush, so the pill + seam read as one connected shape (now in neutral) */
.abar-nav .dd.open>a{color:var(--ink);background:var(--paper-200);border-radius:8px 8px 0 0}
.abar-nav .dd.open>a .chev{transform:rotate(180deg)}
/* trigger shows the same dot when the open page lives inside this dropdown (e.g. any /models/ subpage) */
.abar-nav .dd:has(a[aria-current])>a{color:var(--ink)}
.abar-nav .dd:has(a[aria-current])>a::after{content:'';position:absolute;left:50%;bottom:10px;transform:translateX(-50%);width:5px;height:5px;border-radius:50%;background:var(--red)}
.abar-nav .dd>a:hover,.abar-nav .dd.open>a:hover,.abar-nav .dd:has(a[aria-current])>a:hover{color:var(--ink)}
/* Open/close runs on transitions, not keyframes: sweeping the pointer across the nav
   retargets the in-flight panel instead of restarting it from zero. visibility (not
   display) keeps the closed panel un-tabbable while still being transitionable, and is
   delayed on close so the fade can finish. Exit is faster than enter. */
.dd-panel{position:absolute;left:0;right:0;top:100%;background:var(--paper-100);border-top:1px solid var(--hairline);box-shadow:var(--shadow-panel);z-index:90;opacity:0;transform:translateY(-7px);visibility:hidden;pointer-events:none;transition:opacity 120ms cubic-bezier(.23,1,.32,1),transform 120ms cubic-bezier(.23,1,.32,1),visibility 0s linear 120ms}
.dd.open .dd-panel{opacity:1;transform:none;visibility:visible;pointer-events:auto;transition:opacity 190ms cubic-bezier(.23,1,.32,1),transform 190ms cubic-bezier(.23,1,.32,1),visibility 0s}
/* Shared morphing panel host (spec 2026-08-20): tpl.js adopts the four .dd-panels
   into one .mm at boot; the host owns position, entry motion, shadow and the
   height morph; adopted panels keep their inner layout and crossfade with blur.
   Open/close rides --ease-out (cubic-bezier(.23,1,.32,1)) — the curve the per-dd
   panels have always used and the one the spec pins; --ease stays on the crossfade
   and the stagger, which are new motion with no production value to preserve. */
.mm{position:absolute;left:0;right:0;top:100%;z-index:90;background:var(--paper-100);border-top:1px solid var(--hairline);box-shadow:var(--shadow-panel);
  overflow:hidden;height:0;opacity:0;transform:translateY(-7px);visibility:hidden;pointer-events:none;
  transition:opacity 120ms var(--ease-out),transform 120ms var(--ease-out),visibility 0s linear 120ms,height 260ms var(--ease-morph)}
.mm.open{opacity:1;transform:none;visibility:visible;pointer-events:auto;
  transition:opacity 190ms var(--ease-out),transform 190ms var(--ease-out),visibility 0s,height 260ms var(--ease-morph)}
/* An adopted panel is visibility:hidden until it is the .on one, exactly as it was inside its
   own .dd, and for the same reason: the host's visibility:hidden does NOT hide it. visibility
   inherits but is re-declarable, so a `visible` child of a `hidden` parent shows — declaring
   visible here left all four panels' 31 links in the tab order and the AX tree on every page.
   The hidden flip is delayed 140ms on the way out so the crossfade can finish, and undelayed
   on the way in so the incoming panel is reachable immediately. */
.mm .dd-panel{position:absolute;left:0;right:0;top:0;border-top:0;box-shadow:none;
  opacity:0;filter:blur(2px);transform:none;visibility:hidden;pointer-events:none;
  transition:opacity 140ms var(--ease),filter 140ms var(--ease),visibility 0s linear 140ms}
.mm .dd-panel.on{opacity:1;filter:none;visibility:visible;pointer-events:auto;
  transition:opacity 140ms var(--ease) 50ms,filter 140ms var(--ease) 50ms,visibility 0s}
/* stagger: first open only, per-column ladder, no markup hooks needed */
.mm.fresh .dd-panel.on :is(.dd-thesis,.dd-kicker,.dd-items>a,.dd-more){opacity:0;transform:translateY(5px);animation:dd-rise 240ms var(--ease) forwards}
.mm.fresh .dd-panel.on .dd-kicker{animation-delay:24ms}
.mm.fresh .dd-panel.on .dd-items>a:nth-of-type(1){animation-delay:48ms}
.mm.fresh .dd-panel.on .dd-items>a:nth-of-type(2){animation-delay:72ms}
.mm.fresh .dd-panel.on .dd-items>a:nth-of-type(3){animation-delay:96ms}
.mm.fresh .dd-panel.on .dd-items>a:nth-of-type(4){animation-delay:120ms}
.mm.fresh .dd-panel.on .dd-items>a:nth-of-type(5){animation-delay:144ms}
.mm.fresh .dd-panel.on .dd-items>a:nth-of-type(6){animation-delay:168ms}
.mm.fresh .dd-panel.on .dd-items>a:nth-of-type(7){animation-delay:192ms}
.mm.fresh .dd-panel.on .dd-items>a:nth-of-type(n+8){animation-delay:216ms}
.mm.fresh .dd-panel.on .dd-more{animation-delay:240ms}
@keyframes dd-rise{to{opacity:1;transform:none}}
/* Once the host has moved inside .abar-nav (tab order, see place() in tpl.js) the 31 panel
   links match `.abar-nav a` too. `.dd-panel a` redeclares nearly all of it, but two things do
   leak: position:relative from line 432, and the red 5px section dot from
   `.abar-nav a[aria-current]::after` - which panel rows genuinely match, since the current
   page's row carries aria-current. Only `.dd-panel a::after{display:none}` was hiding it.
   Reset both here rather than leaning on that. */
.abar-nav .mm a{position:static}
.abar-nav .mm a::after{content:none}
/* The whole nav goes to the burger below 1180 (`.abar-wrap>.abar-nav{display:none}`), and the
   adopted panels are no longer inside it to inherit that, so the host has to go on its own. */
@media(max-width:1180px){.mm{display:none}}
/* "Pause animations" (html.a11y-no-motion) is a live site setting, not a media query, so every
   motion rule above needs its class-based twin — same pattern as the .dd-panel block below.
   Open and closed are declared separately on purpose in BOTH no-motion paths: the closed state
   keeps a visibility delay so the fade can finish, the open state must not have one. Under
   either path the blur goes too, so the swap is strictly a crossfade. */
html.a11y-no-motion .mm{transform:none;transition:opacity 150ms var(--ease-out),visibility 0s linear 150ms,height 0s}
html.a11y-no-motion .mm.open{transform:none;transition:opacity 150ms var(--ease-out),visibility 0s,height 0s}
html.a11y-no-motion .mm .dd-panel{filter:none;transition:opacity 140ms var(--ease),visibility 0s linear 140ms}
html.a11y-no-motion .mm .dd-panel.on{filter:none;transition:opacity 140ms var(--ease) 50ms,visibility 0s}
html.a11y-no-motion .mm.fresh .dd-panel.on :is(.dd-thesis,.dd-kicker,.dd-items>a,.dd-more){animation:none;opacity:1;transform:none}
@media(prefers-reduced-motion:reduce){
  .mm{transform:none;transition:opacity 150ms var(--ease-out),visibility 0s linear 150ms,height 0s}
  .mm.open{transform:none;transition:opacity 150ms var(--ease-out),visibility 0s,height 0s}
  .mm .dd-panel{filter:none;transition:opacity 140ms var(--ease),visibility 0s linear 140ms}
  .mm .dd-panel.on{filter:none;transition:opacity 140ms var(--ease) 50ms,visibility 0s}
  .mm.fresh .dd-panel.on :is(.dd-thesis,.dd-kicker,.dd-items>a,.dd-more){animation:none;opacity:1;transform:none}
}
.dd-inner{max-width:var(--max);margin:0 auto;padding:34px clamp(20px,4vw,48px) 42px}
/* Panel shell (2026-07-27, after the DOSS reference): a thesis rail, then full content columns
   divided by vertical rules, each closed by its own foot link, with one global bar underneath.
   The rules are back — the earlier version's rule failed not because rules are wrong but because
   the column beside it was EMPTY. They only work when every column carries its weight, which is
   why the second models column lists the channels rather than a single link.
   Desktop-only territory: the whole nav swaps to the burger below 1180px. */
/* justify-content:center because the columns are fixed-width and never filled the wrap: the models
   panel laid out 288 + 52 + 660 = 1000px inside a 1280px wrap and let all 280px of the remainder
   fall on the right, so the block sat with a 60px left margin against a 220px right one. Centring
   splits it evenly. align-items:start so the short thesis rail is no longer a stretched empty box
   the full height of the item list — it has nothing to fill that height with, and pretending
   otherwise is what made the panel read as unfinished rather than as spacious. */
.dd-grid{display:grid;align-items:start;justify-content:center}
/* The cap belongs on the TRACK, not on .dd-col. With `1fr` the track absorbed every spare pixel
   and the element's max-width:660 just left a hole on its right, so the block sat 60px from the
   left edge and 220px from the right. minmax(0,660px) leaves that slack as real free space for
   justify-content:center to split evenly. */
.dd-panel--models .dd-grid{grid-template-columns:clamp(230px,20vw,300px) minmax(0,660px)}
/* Two tracks since 2026-08-19 (owner brief, spec section C): the storefronts we run beside the
   marketplaces we sell on.
   The tracks are UNEQUAL, revised 2026-08-19 after "we need to make a better arrangement for the
   link". Equal 1fr tracks held 5 chips against 2, so the storefront column ran 326px while the
   marketplace column ran 132px, and the "View all marketplaces" link was left hanging under a
   194px hole. Weighting the tracks 1.6:1 lets the storefront chips sit 2-across in 3 rows
   instead of 1-across in 5, which brings the two columns to 3 rows against 2 and puts the two
   more-links a single row apart instead of half a panel. */
.dd-panel--channels .dd-grid{grid-template-columns:clamp(230px,20vw,300px) minmax(0,1.6fr) minmax(0,1fr)}
/* Stretching only these two columns - the thesis rail keeps align-items:start, since a stretched
   empty rail is what that rule exists to prevent - ends both column rules together and, because
   .dd-more already carries margin-top:auto, lands both "View all" links on one line. With the
   unequal tracks above the columns now differ by one chip row rather than three, so this aligns
   a small remainder instead of papering over a hole. */
.dd-panel--channels .dd-col{align-self:stretch}
/* Single-content panels: cap the column so a row's arrow stays beside its text instead of
   being flung to the far edge of a 900px column. The channels panel is exempt: since it split
   into two tracks its own columns are already capped by the grid. */
.dd-panel--models .dd-col,.dd-panel--why .dd-col{max-width:660px}
.dd-panel--about .dd-grid{grid-template-columns:clamp(230px,20vw,300px) minmax(0,1fr) minmax(0,1fr)}
.dd-panel--why .dd-grid{grid-template-columns:clamp(230px,20vw,300px) minmax(0,660px)}
.dd-rail{padding:0 24px 8px 12px}
.dd-col{display:flex;flex-direction:column;padding:0 0 0 52px;border-left:1px solid var(--hairline)}
/* A column may carry more than one labelled group: the About panel groups "Answers & careers"
   and "Credentials" in its second column (2026-08-19), because three groups is the honest
   shape of that panel and a third grid track would squeeze every row's description. Only a
   kicker FOLLOWING a list needs the separation — the first one in a column keeps its flush top.
   Hairline rather than space alone, matching how .tm-team separates the team groups. */
.dd-col .dd-items + .dd-kicker{padding-top:22px;margin-top:22px;border-top:1px solid var(--hairline)}
/* column separation belongs to the grid, not to a sibling margin: as a margin it was taken
   out of the second column only, so `1fr 1fr` rendered 422px vs 370px and the narrower
   column wrapped descriptions the wider one fitted on one line. */
.dd-grid{column-gap:52px}
.dd-kicker{display:block;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);padding:0 12px 20px}
/* the rail states what the section IS, so the menu opens on a claim rather than a cold list */
/* text-wrap:balance because the thesis is two hard-broken sentences in a narrow rail, and
   the first one wrapped to leave "own." alone on its own line at 1181px, the narrowest
   width the desktop nav renders at. Balance evens the two halves instead. It applies per
   segment, so the <br> between the sentences is unaffected. */
.dd-thesis{font-family:var(--font-display);font-size:clamp(19px,1.5vw,23px);font-weight:600;line-height:1.22;letter-spacing:-.015em;color:var(--ink);padding:0 12px;text-wrap:balance}
/* rows: [icon] [CODE] [name] [→] with the description on a second line under the name */
.dd-items{display:block}
.dd-panel a.ddrow{
  display:grid;grid-template-columns:22px minmax(0,1fr) auto;
  grid-template-areas:"i name go" ". desc desc";
  gap:5px 12px;align-items:center;
  padding:12px 12px;border-radius:8px;font-size:inherit;font-weight:400;
}
.dd-col--coded a.ddrow{grid-template-columns:22px auto minmax(0,1fr) auto;grid-template-areas:"i code name go" ". desc desc desc"}
.dd-panel .ddt{display:contents}
.dd-panel .ddrow .ddi{grid-area:i;align-self:start;margin-top:3px;display:block}
.ddc{grid-area:code}
.ddn{grid-area:name}
.ddd{grid-area:desc}
.ddgo{grid-area:go}
/* foot link per column — margin-top:auto pins them to one baseline across columns of any length */
.dd-panel a.dd-more{display:flex;align-items:center;justify-content:space-between;gap:16px;
  margin-top:auto;padding:24px 12px 2px;border-top:1px solid var(--hairline);
  font-family:var(--font-body);font-size:14px;font-weight:600;color:var(--ink)}
.dd-more .ddgo{grid-area:auto;opacity:.72}
/* Specificity, not preference: the generic `.dd-panel a:hover` (0,2,1) outranked the old
   `.dd-more:hover` (0,2,0), so this link's intended "no fill" never applied — hover painted a
   49px slab that swallowed the divider rule above it (the 24px top padding sits inside the
   background box). Qualified to (0,3,1) so the intent wins. :active needs it for the same reason. */
.dd-panel a.dd-more:hover,.dd-panel a.dd-more:active{background:none;color:var(--ink)}
.dd-panel a.dd-more:hover .ddgo{transform:translateX(3px);opacity:1;color:var(--red-ink)}
/* channel chips: labels only — a channel name needs no gloss */
.dd-chips{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:3px 20px}
/* Sales channels panel rows (spec 2026-08-20): storefront rows lead, marketplace rows follow.
   Both stack name over line; .srow adds weight and the hover arrow. */
.dd-panel a.srow{display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-areas:"name go" "desc go";align-items:center;gap:1px 14px;padding:10px 12px}
.srow .ddn{grid-area:name;font-size:16.5px;font-weight:700}
.srow .ddd{grid-area:desc;font-size:12.5px;line-height:1.45}
.srow .ddgo{grid-area:go;align-self:center;opacity:0;transform:translateX(-7px);color:var(--red-ink);
  transition:transform 260ms var(--ease-spring),opacity 140ms var(--ease)}
@media(hover:hover) and (pointer:fine){.dd-panel a.srow:hover .ddgo{opacity:1;transform:none}}
html.a11y-no-motion .srow .ddgo{transform:none;transition:opacity 140ms var(--ease)}
@media(prefers-reduced-motion:reduce){.srow .ddgo{transform:none;transition:opacity 140ms var(--ease)}}
.dd-panel a.qrow{display:block;padding:10px 12px}
.qrow .ddn{display:block;font-size:15.5px}
.qrow .ddd{display:block;font-size:12.5px;line-height:1.45}
/* type scale: mono code, display-face name, body description — three roles, three faces */
.ddc{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.12em;color:var(--ink-muted);white-space:nowrap;transition:color 140ms var(--ease)}
.ddn{font-family:var(--font-display);font-size:17px;font-weight:600;line-height:1.25;letter-spacing:-.01em;color:var(--ink);transition:color 140ms var(--ease)}
.ddd{font-family:var(--font-body);font-size:13.5px;font-weight:400;line-height:1.5;color:var(--ink-soft)}
/* decorative — the whole row is a labelled link — but .5 rendered it at ~1.9:1, which reads as
   a rendering fault rather than restraint. .72 keeps it subordinate and still legible. */
.ddgo{width:16px;height:16px;flex:none;color:var(--ink-muted);opacity:.72;transition:transform 160ms var(--ease),opacity 160ms var(--ease),color 160ms var(--ease)}
/* hover is a value shift, not a hue shift — the background already moved, so the code and arrow
   only need to step up in weight of colour, not change identity */
.dd-panel a.ddrow:hover .ddc{color:var(--ink-soft)}
.dd-panel a.ddrow:hover .ddgo{transform:translateX(3px);opacity:1;color:var(--ink)}
/* you-are-here (aria-current) and the hover arrows — the red in the panel */
.dd-panel a.ddrow[aria-current]{box-shadow:inset 2px 0 0 var(--red-ink)}
.dd-panel a.ddrow[aria-current] .ddc,.dd-panel a.ddrow[aria-current] .ddn{color:var(--red-ink)}
.dd-panel a{display:flex;gap:12px;align-items:flex-start;height:auto;padding:10px 12px;border-radius:10px;color:var(--ink);font-size:14.5px;font-weight:600;text-decoration:none;white-space:normal;transition:background 140ms var(--ease),transform 100ms var(--ease)}
.dd-panel a::after{display:none}
.ddi{width:20px;height:20px;flex:none;margin-top:2px;color:var(--ink-muted);overflow:visible;transition:color 140ms var(--ease)}
.ddi .mv,.ddi .mv-a,.ddi .mv-b{transform-box:fill-box;transform-origin:center}
.ddt{display:block;min-width:0}
.dd-panel a small{display:block;color:var(--ink-muted);font-size:12.5px;font-weight:400;margin-top:2px;line-height:1.45}
.dd-panel a:hover{background:var(--paper-200);color:var(--ink)}
.dd-panel a:active{background:var(--paper-300);transform:scale(var(--press))}
.dd-panel a:is(:hover,:focus-visible) .ddi{color:var(--red-ink,#ab0000)}
.dd-panel a[aria-current]{color:var(--red-ink,#ab0000)}
.dd-panel a[aria-current] .ddi{color:var(--red-ink,#ab0000)}
/* Nav-dropdown icon hover-wiggles (ddm-* keyframes) removed 2026-07-22 — motion trim.
   Icons still recolour on hover (.dd-panel a:hover .ddi rule below); they no longer move.
   The AI card and its iso parallax went with the 2026-07-27 ledger rebuild — the go-arrow nudge is the only
   motion left in this panel, and it is directional feedback, not decoration.
   AMENDED 2026-08-21, owner call: the ddm-* family returns, as INFINITE loops this time, and the
   hover recolour is now brand red (with :focus-visible parity — the rows are links, keyboard gets
   what the mouse gets). Only one row is ever hovered, so exactly one icon loops at a time. Every
   keyframe below starts AND ends at transform:none so the loop engages without a jump; transforms
   and opacity only. The two no-motion guards further down still kill all of it. */
@keyframes ddm-spin{to{transform:rotate(360deg)}}
@keyframes ddm-pull-a{0%,100%{transform:none}50%{transform:translateX(1.4px)}}
@keyframes ddm-pull-b{0%,100%{transform:none}50%{transform:translateX(-1.4px)}}
@keyframes ddm-drive{0%,100%{transform:none}25%{transform:translateX(-.8px)}75%{transform:translateX(.8px)}}
@keyframes ddm-scan{0%,100%{transform:none}30%,45%{transform:translateX(1.6px)}70%,85%{transform:translateX(-1.6px)}}
@keyframes ddm-slide{0%,100%{transform:none}25%{transform:translateX(-2px)}75%{transform:translateX(2px)}}
@keyframes ddm-bars{0%,100%{transform:none}50%{transform:scaleY(.6)}}
@keyframes ddm-twinkle{0%,100%{transform:none;opacity:1}50%{transform:scale(.7);opacity:.6}}
@keyframes ddm-join{0%,100%{transform:none;opacity:1}50%{transform:translateX(-1.2px);opacity:.55}}
@keyframes ddm-bob{0%,100%{transform:none}50%{transform:translateY(-1.4px)}}
@keyframes ddm-sway{0%,100%{transform:none}25%{transform:rotate(-3deg)}75%{transform:rotate(3deg)}}
/* two per-icon origin overrides: the clock hands must turn about the DIAL centre (12,12 in the
   24px viewBox), not their own fill-box, or the tick wobbles; the chart bars grow from their
   baseline, not their middle. Both sit after the .ddi .mv fill-box default, so they win the tie. */
.ddi--hist .mv{transform-box:view-box;transform-origin:12px 12px}
.ddi--proof .mv{transform-origin:center bottom}
/* each glyph loops the way it depicts: 11 primitives across 16 icons. ddi--gear has no .mv part —
   it is a symmetric ring of spokes, so the whole 20px svg spins about its own centre. */
.dd-panel a:is(:hover,:focus-visible) .ddi--gear{animation:ddm-spin 2.6s linear infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--hist .mv{animation:ddm-spin 4.8s steps(12,end) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--d2c .mv{animation:ddm-pull-a 1.6s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--xd .mv-a{animation:ddm-pull-a 1.7s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--xd .mv-b{animation:ddm-pull-b 1.7s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--link .mv-a{animation:ddm-pull-a 1.9s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--link .mv-b{animation:ddm-pull-b 1.9s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--truck .mv{animation:ddm-drive 1.8s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--eye .mv{animation:ddm-scan 2.8s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--belt .mv{animation:ddm-slide 2.2s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--proof .mv{animation:ddm-bars 1.6s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--ai .mv{animation:ddm-twinkle 1.5s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--team .mv{animation:ddm-join 1.8s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--pin .mv{animation:ddm-bob 1.5s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--faq .mv{animation:ddm-bob 1.6s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--case .mv{animation:ddm-sway 2.4s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--bag .mv{animation:ddm-sway 2.4s var(--ease) infinite}
.dd-panel a:is(:hover,:focus-visible) .ddi--medal .mv{animation:ddm-sway 2.2s var(--ease) infinite}
html.a11y-no-motion .abar-nav .dd>a .chev{transition:none}
/* reduced motion keeps the opacity fade (it aids comprehension) and drops the slide */
html.a11y-no-motion .dd-panel,html.a11y-no-motion .dd.open .dd-panel{transform:none}
html.a11y-no-motion .dd-panel a:is(:hover,:focus-visible) .ddi,html.a11y-no-motion .dd-panel a:is(:hover,:focus-visible) .ddi .mv,html.a11y-no-motion .dd-panel a:is(:hover,:focus-visible) .ddi .mv-a,html.a11y-no-motion .dd-panel a:is(:hover,:focus-visible) .ddi .mv-b{animation:none}
html.a11y-no-motion .dd-panel a:is(.ddrow,.dd-more):hover .ddgo{transform:none}
@media(prefers-reduced-motion:reduce){.dd-panel,.dd.open .dd-panel{transform:none}.abar-nav .dd>a .chev{transition:none}.dd-panel a:is(:hover,:focus-visible) .ddi,.dd-panel a:is(:hover,:focus-visible) .ddi .mv,.dd-panel a:is(:hover,:focus-visible) .ddi .mv-a,.dd-panel a:is(:hover,:focus-visible) .ddi .mv-b{animation:none}.dd-panel a:is(.ddrow,.dd-more):hover .ddgo{transform:none}}

/* mobile: links + CTA collapse into a hamburger; lang + theme stay in the bar */
.abar-burger{display:none;width:44px;height:44px;border:0;background:none;color:var(--ink);cursor:pointer;align-items:center;justify-content:center;border-radius:10px;margin-left:2px;transition:transform 120ms var(--ease-out)}
.abar-burger svg{width:24px;height:24px}
.abar-burger:hover{background:var(--paper-200)}
.abar-burger:active{transform:scale(.94)}
/* burger → ✕. transform-box:view-box so all three bars share the icon's centre as
   their origin: each translates onto (12,12) and then rotates about it. Driven by
   aria-expanded, so the state lives in one place and the icon cannot desync from
   the sheet. The three <g> wrappers are the only markup this component needs.
   ORDER IS LOAD-BEARING — rotate() must come BEFORE translateY(). A CSS transform
   list composes right-to-left, so `translateY(5px) rotate(45deg)` rotates the bar
   about (12,12) while it is still 5px above that point, swinging it out to the
   right, and only then translates. The two arms then cross low and left of centre
   instead of at their midpoints: measured b1 at (15.5,13.5) and b3 at (15.5,10.5)
   against a (12,12) target, i.e. the whole mark sat 3.6px right of centre and the
   X did not close. `rotate() translateY()` translates each bar onto the centre
   first, then spins it about itself. Both arms land dead on (12,12). */
.abar-burger svg g{transform-box:view-box;transform-origin:12px 12px;
  transition:transform 260ms var(--ease-out),opacity 140ms var(--ease)}
.abar-burger[aria-expanded="true"] svg .b1{transform:rotate(45deg) translateY(5px)}
.abar-burger[aria-expanded="true"] svg .b2{opacity:0;transform:scaleX(.25)}
.abar-burger[aria-expanded="true"] svg .b3{transform:rotate(-45deg) translateY(-5px)}
@media(max-width:1180px){
  .abar-wrap>.abar-nav{display:none}
  .abar-burger{display:flex}
}
/* The CTA used to go at 1180 with the nav, which took the site's ONE conversion action out of
   the chrome on every tablet and phone, on 48 of the 49 pages: below that width it survived only
   in the burger sheet's foot, a tap and a scroll away (UX review 2026-08-06, finding 9).
   The nav has to go at 1180 because seven items plus four panels genuinely do not fit. The CTA is
   one 44px pill and does — logo + pill + burger measures 286px at the tightest, against a 320px
   floor. It shrinks rather than disappearing: the pill loses height and padding at 1180 and again
   at 900, and keeps its whole label all the way down — "Let's talk", "Talk to us" and "Get help"
   each fit. (Until 2026-08-20 the label was the longer "Pitch your brand", split so that 900 could
   drop it to the verb alone.) Under 720 the sheet foot takes over, where the hero's own CTAs are
   also in reach. */
@media(max-width:1180px) and (min-width:721px){
  .abar-right>.abar-cta{display:inline-flex;height:40px;padding:0 16px;font-size:14px}
}
@media(max-width:900px) and (min-width:721px){
  .abar-right>.abar-cta{padding:0 14px}
}
@media(max-width:720px){
  .abar-right>.abar-cta{display:none}
}

/* ——— .abar-login: the persistent B2B door (2026-08-25, owner call) ————————————————
   The nbar's login is the documented "one door into the B2B platform" (~335), but the nbar
   auto-hides on scroll-down, so past the first viewport the site had NO reachable login — the
   sticky .abar carried none, and ~354 already admitted the login "has no burger-sheet fallback".
   This is that fallback, twice over: a quiet chip in .abar-right on desktop (subordinate to
   .abar-cta — one primary per bar), and a .ms-login row the JS-built sheet foot adds on phones
   (tpl.js sheet module), which is why this chip can hide in the burger band exactly like
   .abar-cta. Same lock + label language as .nbar-login so the door reads as ONE thing.

   GATED ON THE NBAR (owner call 2026-08-25, reversing the note that used to sit here): at
   scroll 0 both bars are on screen and the chip used to duplicate .nbar-login for that beat.
   It now reveals only while the notification bar is actually gone. The nbar is hidden in
   exactly one state — html.nbar-fix:not(.nbar-in): .nbar-fix parks it at top:-nbarH (~454)
   and .nbar-in brings it back to top:0 (~455) — so that is the whole condition.

   The earlier rejection said the classes "churn per scroll tick" and the chip would flicker.
   That is true of a JS observer on <html>'s class (see the reveal module's note ~3688) but not
   of a CSS gate: setIn() early-returns when the class is already right, so nbar-in toggles only
   on a real direction change, and the fade is 280ms — the same duration as the nbar's own top
   slide — so the two move together instead of fighting.
   Nothing here changes layout: opacity + a 5px slide only, never display/width/padding, so
   .abar-cta does not shift when the chip comes and goes. The reserved space costs nothing
   visually because .abar-right is end-justified, so the gap sits left of the CTA where the bar
   is already empty. visibility:hidden (not just opacity:0) keeps the hidden chip out of the tab
   order, so at scroll 0 there is one login in the tab order, not two.
   history.html is body.hx-page, which opts out of the nbar scroll module entirely (tpl.js
   `isHx`), so nbar-fix never lands there — it falls back to .abar.scrolled, which is set on the
   bar itself at y>8 and is monotonic. */
.abar-login{display:inline-flex;align-items:center;gap:7px;position:relative;height:44px;padding:0 clamp(13px,1.5vw,22px);border:1px solid var(--hairline);border-radius:var(--radius-btn);color:var(--ink);text-decoration:none;font-family:var(--font-body);font-size:clamp(14px,1.05vw,15px);font-weight:600;letter-spacing:.01em;white-space:nowrap;opacity:0;visibility:hidden;transform:translateX(5px);pointer-events:none;transition:color .15s var(--ease),border-color .15s var(--ease),background .15s var(--ease),opacity 280ms var(--ease),transform 280ms var(--ease),visibility 0s linear 280ms}
.abar-login .lock{width:15px;height:15px;flex:none}
@media(hover:hover) and (pointer:fine){.abar-login:hover{color:var(--red-ink);border-color:var(--red-ink);background:var(--paper-200)}}
.abar-login:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
html.a11y-no-motion .abar-login{transition:none}
@media(prefers-reduced-motion:reduce){.abar-login{transition:none}}
/* the reveal: nbar parked off-screen (normal pages), or simply scrolled (history.html, which
   opts out of the nbar module). transition-delay:0s cancels the hide path's visibility delay so
   the chip becomes focusable the instant it starts fading in. */
html.nbar-fix:not(.nbar-in) .abar-login,
body.hx-page .abar.scrolled .abar-login{opacity:1;visibility:visible;transform:none;pointer-events:auto;transition-delay:0s}
/* same box as .abar-cta at every step (owner call 2026-08-25: "same size as Talk to us") — the
   outline-vs-fill contrast alone carries the hierarchy; height/padding/type mirror ~514 and ~832-836 */
@media(max-width:1180px) and (min-width:721px){.abar-login{height:40px;padding:0 16px;font-size:14px}}
@media(max-width:900px) and (min-width:721px){.abar-login{padding:0 14px}}
@media(max-width:720px){.abar-login{display:none}}

/* ===== Mobile menu — draggable bottom sheet with one level of drill-down
   (.ms-*, "menu sheet"; replaced the full-screen .abar-panel on 2026-07-30, ADR-002).

   What the full-screen panel got wrong, and what this does instead:

   1. It covered the bar it was launched from (inset:0, z-index:200), which put
      language and theme out of reach for as long as the menu was open. This sheet
      starts its scrim at --chrome-top, so the bar stays lit AND interactive, and
      the burger stays on screen to close what it opened.
   2. It faded in on opacity alone, so it had no origin and no path — and could
      not be dismissed the way it arrived. This translates up from the bottom
      edge and goes back to it, with drag-to-dismiss on the same axis.
   3. Its entrance stagger ran to ~690ms (.24s delay + .45s duration) on a control
      opened on every page. Nothing staggers now: only the sheet's own Y and the
      scrim animate. This was a deliberate removal, not an oversight.
   4. Its accordions expanded by display:none→block inside the scroller, which
      snapped the height and pushed rows around mid-read; eleven channels became
      eleven stacked rows. Groups now drill sideways into their own screen, and the
      sheet keeps ONE size across every level — the 409px level 1 needs — so its top
      edge, the grab handle and the back button never move. Sub-levels taller than
      that scroll inside it, and the two scroll-edge affordances say so.
   5. Its 01..07 ledger indices encoded nothing — nav order is not a sequence.
      Each group now carries its real count instead (5 / 2 / 11 / 8), which tells
      you the size of what is behind the row before you open it. Counted from the
      DOM in tpl.js, never hardcoded.

   JS builds it from .abar-nav (tpl.js), same as the panel before it, so the IA
   has exactly one source of truth. ===== */
.ms-scrim{position:fixed;inset:0;z-index:190;
  background:rgba(11,11,12,.44);opacity:0;visibility:hidden;
  -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);
  transition:opacity 200ms var(--ease),visibility 200ms var(--ease)}
.ms-scrim.is-on{opacity:1;visibility:visible;transition-duration:240ms}
.ms-scrim[hidden]{display:none}
/* The scrim covers the whole viewport and the chrome is LIFTED above it, rather
   than the scrim starting at the bar's bottom edge. Deliberate: the bar's
   on-screen bottom depends on scroll position and on whether the .nbar is still
   in flow — at scroll 0 it is, which puts the bar's bottom at 112px, not 72px.
   --chrome-top cannot express that (it measures FIXED chrome and excludes the
   nbar by design, see its declaration), and top:var(--chrome-top) covered the
   lower 40px of the bar and swallowed clicks on the burger itself.
   195 sits between the scrim's 190 and the sheet's 200.
   The bar is also forced opaque while open: .abar.scrolled is translucent, and a
   light translucent surface stacked over a dimmed page loses both its legibility
   and its colour. */
html.menu-lock .nbar,html.menu-lock .abar{z-index:195}
html.menu-lock .abar{background:var(--paper-100);-webkit-backdrop-filter:none;backdrop-filter:none}

/* Asymmetric timing carried by state, not by JS: the base rule is the CLOSING
   transition and .is-on carries the OPENING one. Slow enough to read arriving,
   snappy leaving. */
.ms-sheet{--ms-gut:clamp(18px,5vw,26px);--ms-row:54px;
  position:fixed;left:0;right:0;bottom:0;z-index:200;
  display:flex;flex-direction:column;
  /* The ~82% cap lives HERE, not in JS. svh is the SMALL viewport, so it cannot move
     when mobile browser chrome collapses — window.innerHeight can, which let the
     sheet open at one height at the top of a page and another further down. The
     head+foot subtraction is structural rather than arithmetic: both are flex:none,
     so .ms-stage is the only child that can give way. Nothing in this component
     reads window.innerHeight or measures the chrome any more. */
  max-height:82vh;   /* pre-svh fallback — resolves against the LARGE viewport */
  max-height:82svh;
  background:var(--paper-0);border-top:1px solid var(--hairline);
  border-radius:18px 18px 0 0;
  box-shadow:0 -26px 46px -18px rgba(0,0,0,.24);
  transform:translateY(100%);visibility:hidden;
  transition:transform 240ms var(--ease-drawer),visibility 240ms var(--ease-drawer)}
.ms-sheet.is-on{transform:translateY(0);visibility:visible;
  transition:transform 340ms var(--ease-drawer),visibility 340ms var(--ease-drawer)}
.ms-sheet[hidden]{display:none}

/* head: grab handle + label / back. touch-action:none so a vertical drag that
   starts here is always the sheet's and never claimed by the level's scroller. */
.ms-head{flex:none;position:relative;padding:0 var(--ms-gut) 6px;touch-action:none;cursor:grab;
  transition:box-shadow 200ms var(--ease)}
.ms-head:active{cursor:grabbing}
.ms-grab{display:block;width:38px;height:4px;margin:8px auto 2px;border-radius:2px;
  background:var(--paper-500);opacity:.65}
.ms-headrow{display:flex;align-items:center;min-height:40px}
/* text-only: an eyebrow never carries a glyph */
.ms-eyebrow{font-family:var(--font-mono);font-weight:500;font-size:10.5px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-muted)}
.ms-back{display:inline-flex;align-items:center;gap:7px;min-height:40px;margin-left:-6px;padding:0 8px 0 6px;
  background:none;border:0;border-radius:8px;cursor:pointer;color:var(--ink-soft);
  font-family:var(--font-mono);font-weight:500;font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
  transition:background 240ms var(--ease),color 160ms var(--ease)}
.ms-back:active{background:var(--paper-200);color:var(--ink);transition-duration:0ms}
.ms-back:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.ms-back svg{width:15px;height:15px;flex:none}
.ms-head [hidden]{display:none}
/* Scroll edge effect rather than a permanent divider: the hairline exists only
   while there is content scrolled up underneath it. */
.ms-sheet.is-scrolled .ms-head{box-shadow:0 1px 0 var(--hairline),0 6px 12px -8px rgba(0,0,0,.18)}

/* stage: the levels, absolutely stacked so they can cross over each other, at ONE
   height for the whole time the sheet is open — the height LEVEL 1 needs (409px;
   measured, see sizeStage in tpl.js). Sub-levels scroll inside it, they never
   resize it.
   Height is a layout property AND the sheet is pinned bottom:0, so animating it
   moved the sheet's TOP edge — the grab handle and the back button the user is
   reaching for — by up to 485px on every drill, across the axis the content is
   travelling on. That read as instability, not responsiveness. Nothing
   layout-triggering animates in this component now; only transform and opacity.
   flex:0 1 auto (not flex:none) is what lets .ms-sheet's max-height bite — head and
   foot stay flex:none, so the stage is the only child that gives way.
   min-height:160px is the old Math.max(220,…) floor, lowered on purpose: a 220px
   floor makes the sheet 359px, which exceeds 82svh below ~438px of viewport height,
   and an unshrinkable child inside a max-height'd box overflows its own top edge —
   the head would paint outside the rounded border. Landscape phones sit in that
   band. 160px pushes the threshold below 365px, which nothing real reaches.
   The 56svh fallback covers only the frames between sheet.hidden=false and
   sizeStage(), and degrades to a usable stage if a measurement ever returns 0. */
.ms-stage{position:relative;overflow:hidden;flex:0 1 auto;min-height:160px;
  height:var(--ms-stage-h,56svh)}
/* bottom scroll edge — present only while there IS more below, so a half-cut
   last row reads as "keep going" instead of as a hard crop */
.ms-stage::after{content:'';position:absolute;left:0;right:0;bottom:0;height:36px;pointer-events:none;
  background:linear-gradient(to top,var(--paper-0),transparent);
  opacity:0;transition:opacity 200ms var(--ease)}
.ms-sheet.has-more .ms-stage::after{opacity:1}
.ms-level{position:absolute;left:0;right:0;top:0;max-height:100%;
  overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;
  padding:2px 0 10px;
  opacity:0;visibility:hidden;transform:translateX(18%);
  transition:opacity 220ms var(--ease-out),transform 280ms var(--ease-out),visibility 280ms}
.ms-level:focus{outline:none}
.ms-level.is-on{opacity:1;visibility:visible;transform:none}
/* a level drilled PAST parks to the left, so going back returns along the same
   path it left by */
.ms-level.is-back{transform:translateX(-14%)}

/* level 1 — two blocks, one hairline between them: rows that open a section
   (count + chevron) above rows that leave the menu (neither). The affordance
   carries the difference, so it needs no labelling. */
.ms-block{display:flex;flex-direction:column}
.ms-block--leaf{margin-top:9px;padding-top:9px;border-top:1px solid var(--hairline)}
.ms-row{display:flex;align-items:center;gap:14px;width:100%;min-height:var(--ms-row);
  padding:0 var(--ms-gut);margin:0;background:none;border:0;text-align:left;cursor:pointer;
  font-family:var(--font-display);font-weight:700;font-size:20px;letter-spacing:-.01em;line-height:1.1;
  color:var(--ink);text-decoration:none;
  transition:background 260ms var(--ease)}
/* instant on press, gentle on release: the press is the user acting, the release
   is the system relaxing */
.ms-row:active{background:var(--paper-200);transition-duration:0ms}
.ms-row:focus-visible{outline:2px solid var(--ink);outline-offset:-3px;border-radius:6px}
.ms-lbl{flex:1;min-width:0}
.ms-n{font-family:var(--font-mono);font-weight:500;font-size:11.5px;letter-spacing:.08em;
  color:var(--ink-muted);font-variant-numeric:tabular-nums}
.ms-arw{width:15px;height:15px;flex:none;color:var(--paper-500)}
.ms-row[aria-current]{color:var(--red-ink)}
.ms-row[aria-current] .ms-n{color:var(--red-ink)}
/* current page marker: the same red dot the desktop bar uses */
.ms-row--leaf[aria-current]::after{content:'';width:5px;height:5px;border-radius:50%;
  background:var(--red);flex:none}

/* level 2 — title plus the desktop panel's own thesis line, so drilling in reads
   as arriving somewhere rather than unfolding a list */
.ms-l2hd{padding:2px var(--ms-gut) 12px}
.ms-l2t{margin:0;font-family:var(--font-display);font-weight:800;font-size:25px;
  letter-spacing:-.015em;line-height:1.05;color:var(--ink)}
.ms-l2s{margin:7px 0 0;font-family:var(--font-body);font-weight:400;font-size:13px;line-height:1.45;
  color:var(--ink-muted)}
.ms-sub{display:block;font-family:var(--font-mono);font-weight:500;font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-muted);padding:14px var(--ms-gut) 6px}

.ms-level .ddrow{display:flex;align-items:flex-start;gap:12px;padding:10px var(--ms-gut);
  text-decoration:none;color:var(--ink);transition:background 260ms var(--ease)}
.ms-level .ddrow:active{background:var(--paper-200);transition-duration:0ms}
.ms-level .ddrow:focus-visible{outline:2px solid var(--ink);outline-offset:-3px}
.ms-level .ddi{width:20px;height:20px;flex:none;margin-top:2px;color:var(--ink-muted);display:block}
/* the code carries real meaning — "Distributor" alone is not "B2B2C · Distributor" */
.ms-level .ddc{display:block;flex:none;min-width:42px;margin-top:3px;
  font-family:var(--font-mono);font-weight:500;font-size:10px;letter-spacing:.1em;color:var(--ink-muted)}
.ms-level .ddt{display:flex;flex-direction:column;gap:2px;min-width:0}
.ms-level .ddn{font-family:var(--font-body);font-weight:600;font-size:15.5px;line-height:1.25;
  letter-spacing:0;color:inherit}
.ms-level .ddd{font-family:var(--font-body);font-weight:400;font-size:12.5px;line-height:1.4;
  color:var(--ink-muted)}
.ms-level .ddgo{display:none}
.ms-level .ddrow[aria-current]{box-shadow:inset 2px 0 0 var(--red-ink)}
.ms-level .ddrow[aria-current] .ddn,.ms-level .ddrow[aria-current] .ddc{color:var(--red-ink)}

/* The storefront/marketplace rows are cloned OUT of .dd-panel into the sheet (see the
   builder in tpl.js), so every `.dd-panel a.srow` rule stops matching and the row would
   render as one run-on inline paragraph with a ~20px target. These give it the same box the
   .ddrow above gets. No .ddt wrapper here - .srow puts .ddn/.ddd directly in the anchor and
   lays them out with grid-areas, which the sheet does not inherit - so they are blocked
   explicitly; that also restores the space Chrome puts in the accessible name between the
   two spans. min-height is the 44px house touch floor, not WCAG's 24px absolute. */
.ms-level a.srow,.ms-level a.qrow{display:flex;flex-direction:column;gap:2px;
  min-height:44px;justify-content:center;padding:9px var(--ms-gut);
  text-decoration:none;color:var(--ink);transition:background 260ms var(--ease)}
.ms-level a.srow .ddn,.ms-level a.qrow .ddn,
.ms-level a.srow .ddd,.ms-level a.qrow .ddd{display:block}
.ms-level a.srow:active,.ms-level a.qrow:active{background:var(--paper-200);transition-duration:0ms}
.ms-level a.srow:focus-visible,.ms-level a.qrow:focus-visible{outline:2px solid var(--ink);outline-offset:-3px}
.ms-level a.srow[aria-current],.ms-level a.qrow[aria-current]{box-shadow:inset 2px 0 0 var(--red-ink)}
.ms-level a.srow[aria-current] .ddn,.ms-level a.qrow[aria-current] .ddn{color:var(--red-ink)}

/* the section's own page, pinned last as the summary destination */
.ms-more{display:flex;align-items:center;gap:10px;margin:12px var(--ms-gut) 0;min-height:48px;
  padding:0 14px;border:1px solid var(--hairline);border-radius:10px;
  font-family:var(--font-body);font-weight:600;font-size:14.5px;color:var(--ink);text-decoration:none;
  transition:background 260ms var(--ease)}
.ms-more:active{background:var(--paper-200);transition-duration:0ms}
.ms-more:focus-visible{outline:2px solid var(--ink);outline-offset:1px}
.ms-more span{flex:1}
.ms-more svg{width:16px;height:16px;flex:none;color:var(--ink-muted)}
/* same you-are-here treatment the cloned rows above get, for the pages whose mark lands here */
.ms-more[aria-current]{box-shadow:inset 2px 0 0 var(--red-ink)}
.ms-more[aria-current] span{color:var(--red-ink)}

/* foot — one CTA, constant across levels so it never jumps */
/* The foot's divider is a scroll edge too, in the same grammar as .ms-head above:
   a hairline exists only while content actually runs under it. With a constant-height
   stage the shortest level ("Proof of concept", 2 rows plus a summary row since
   2026-08-17, when it replaced the 2-row "Who we serve" and REQ-003 gave it the
   .dd-more every other panel had) leaves less paper than the ~177px measured then, and a
   permanent rule above that reads as a box that failed to fill; without it the screen
   reads as head / title / rows / paper / CTA. The border BOX is kept and only its
   colour changes, so the foot's height never shifts by 1px between states — a shift
   there would move the whole sheet. Colour only, deliberately no shadow: .has-more
   already drives the stage's 36px bottom fade, and an upward shadow here would stack
   two affordances in the same band. */
.ms-foot{flex:none;padding:12px var(--ms-gut) calc(14px + env(safe-area-inset-bottom,0px));
  border-top:1px solid transparent;transition:border-color 200ms var(--ease)}
.ms-sheet.has-more .ms-foot{border-top-color:var(--hairline)}
.ms-cta{display:flex;align-items:center;justify-content:center;min-height:52px;border-radius:10px;
  background:var(--red);color:#fff;font-family:var(--font-body);font-weight:600;font-size:16px;
  text-decoration:none;transition:background 150ms var(--ease),transform 120ms var(--ease-out)}
.ms-cta:active{background:var(--red-pressed);transform:scale(.975)}
.ms-cta:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
/* the sheet-foot login — the burger-band fallback ~354 said didn't exist. Built by tpl.js from
   the page's own .nbar-login, so subdirectory pages keep their ../ depth and login.html (which
   carries no chip to clone) gets no self-link. Quiet on purpose: .ms-cta stays the foot's one
   primary. */
.ms-login{display:flex;align-items:center;justify-content:center;gap:8px;min-height:48px;margin-bottom:10px;border:1px solid var(--hairline);border-radius:10px;color:var(--ink);text-decoration:none;font-weight:600;font-size:15px}
.ms-login .lock{width:15px;height:15px;flex:none}
.ms-login:active{background:var(--paper-200)}
.ms-login:focus-visible{outline:2px solid var(--ink);outline-offset:2px}

/* hover only where a real pointer exists — a tap must not leave a stuck state */
@media(hover:hover) and (pointer:fine){
  .ms-row:hover{background:var(--paper-200)}
  .ms-back:hover{background:var(--paper-200);color:var(--ink)}
  .ms-level .ddrow:hover{background:var(--paper-200)}
  .ms-level .ddrow:hover .ddi{color:var(--red-ink)}
  .ms-more:hover{background:var(--paper-200)}
  .ms-cta:hover{background:var(--red-hover)}
}

/* Above phone width the burger band still runs to 1180px, so this sheet also
   serves tablets and small laptops. Full-bleed there stranded the label at one
   edge and the count at the other (measured 875px apart at 1160px) and stretched
   the CTA to the full width. Capping and centring keeps the row proportions
   identical to the phone layout at every size in the band.
   Centred with margin-inline:auto against left:0/right:0 — NOT translateX,
   because the drag handler owns `transform` and would clobber it. */
@media(min-width:721px){
  .ms-sheet{width:480px;max-width:calc(100vw - 40px);margin-inline:auto;
    border-left:1px solid var(--hairline);border-right:1px solid var(--hairline)}
}
/* above the burger breakpoint there is no mobile menu at all */
@media(min-width:1181px){.ms-sheet,.ms-scrim{display:none}}

html.menu-lock{overflow:hidden}

/* Reduced motion — gentler, not absent: the sheet cross-fades in place and levels
   cross-fade without travel. The sheet's size never changes, so there is no height
   left to snap. Dragging still works, because direct manipulation is feedback
   rather than decoration. */
@media(prefers-reduced-motion:reduce){
  .ms-sheet,.ms-sheet.is-on{transform:none;transition:opacity 180ms var(--ease),visibility 180ms}
  .ms-sheet{opacity:0}
  .ms-sheet.is-on{opacity:1}
  .ms-level,.ms-level.is-back{transform:none}
  .ms-level{transition:opacity 160ms var(--ease),visibility 160ms}
  .abar-burger svg g{transition:opacity 140ms var(--ease)}
  .abar-burger:active,.ms-cta:active{transform:none}
}
html.a11y-no-motion .ms-sheet,html.a11y-no-motion .ms-sheet.is-on{transform:none;transition:opacity 180ms var(--ease),visibility 180ms}
html.a11y-no-motion .ms-sheet{opacity:0}
html.a11y-no-motion .ms-sheet.is-on{opacity:1}
html.a11y-no-motion .ms-level,html.a11y-no-motion .ms-level.is-back{transform:none}
html.a11y-no-motion .ms-level{transition:opacity 160ms var(--ease),visibility 160ms}
html.a11y-no-motion .abar-burger svg g{transition:opacity 140ms var(--ease)}
html.a11y-no-motion .abar-burger:active,html.a11y-no-motion .ms-cta:active{transform:none}

/* buttons */
.btn{display:inline-block;font-family:var(--font-body);font-weight:600;font-size:15px;text-decoration:none;padding:13px 26px;border-radius:var(--radius-btn);transition:background 150ms var(--ease),transform 100ms var(--ease);min-height:44px}
.btn-primary{background:var(--red);color:#fff}
.btn-primary:hover{background:var(--red-hover)}
.btn-primary:active{background:var(--red-pressed)}
.btn-ghost{border:1px solid var(--ink);color:var(--ink)}
.btn-ghost:hover{background:var(--ink);color:var(--paper-100)}
/* press "give" — Apple §1 direct response; guarded off under reduced motion (colour feedback remains) */
.btn:active{transform:scale(.97)}
.abar-cta:active{transform:scale(.97)}
@media(prefers-reduced-motion:reduce){.btn:active,.abar-cta:active{transform:none}}
html.a11y-no-motion .btn:active,html.a11y-no-motion .abar-cta:active{transform:none}
/* Press feedback for the icon-button family. .btn/.abar-cta above already had it, but the 44x44
   controls users tap most often acknowledged nothing. No transition is declared on purpose: these
   elements carry their own transition shorthands (.to-top animates `bottom`, .a11y-toggle a focus
   halo) and re-declaring it here would clobber them. An instant scale on press and release is the
   same technique already used on .ms-level .ddrow. The scale rides a token so the two reduced-motion
   escapes stay one line each instead of repeating the selector list three times. */
:root{--press:.95}
@media(prefers-reduced-motion:reduce){:root{--press:1}}
html.a11y-no-motion{--press:1}
/* .gnode is deliberately NOT in this list: the force graph writes each node’s position
   into transform, and a bare transform here would replace it and drop every node to 0,0.
   The graph presses via --ns inside its own transform instead (see .gnode:active). */
.rv-arrow:active,.rv-play:active,.cs-arrow:active,.hx-nav-btn:active,.tm-arrow:active,
.tpl-modal-x:active,.pd-close:active,.to-top:active,
.a11y-toggle:active,.share-btn:active,.share-copy:active,.espa-x:active,.map-chip:active,
.mx-chip:active,.ft-acc-btn:active,.lang-btn:active,.theme-switch:active,
.rs-chip:active,.ct-copy:active,.blog-tab:active{transform:scale(var(--press))}

/* hero — full-bleed background video with a scrim; content sits on top (light text).
   The poster (set on the <video>) shows until the clip loads. */
.hero{position:relative;overflow:hidden;isolation:isolate;min-height:clamp(480px,66vh,720px);display:flex;align-items:center}
/* .hero is a flex container, so a <picture> wrapper around .hero-bg becomes a flex ITEM while
   the absolutely-positioned img inside it does not — which nudged the hero content down ~1px on
   every <picture> hero. display:contents removes the wrapper's box so .hero-bg is the flex child
   again, exactly as a bare <img> was. Source selection is unaffected (the picture algorithm is
   DOM-based, not layout-based). Verified by pixel diff: restores the pre-<picture> rendering
   byte-for-byte. Added 2026-07-30 with the audit-H2 mobile hero crops, and it also silently
   corrects the five heroes that already shipped a <picture> (d2c, investors, values, faq, team). */
.hero picture{display:contents}
.hero .hero-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0;background:#0b0b0c}
.hero .hero-scrim{position:absolute;inset:0;z-index:1;background:linear-gradient(90deg,rgba(8,8,10,0.88) 0%,rgba(8,8,10,0.64) 42%,rgba(8,8,10,0.30) 72%,rgba(8,8,10,0.12) 100%)}
/* subtle static film grain — ties the hero into the site's grain language (Discipline) and the
   tactile footer. Applied like the footer art — a brand-red-tinted noise SVG anchored
   center bottom/cover, dissolving at the hero's bottom edge into the light section below.
   Vertically-flipped copy (gradient reversed in the file, not via transform, so it composits
   cleanly). Above the video+scrim, below content (z-index 1 < content 2) — text contrast
   untouched. Static — no reduced-motion concern. */
.hero::before{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;background:url("https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noise-overlay-flipped.svg") center bottom/cover no-repeat}
/* dark mode: swap the grain overlay's gradient half from light #EDEDED to dark #101012 so the
   hero's bottom edge dissolves into the dark section below instead of flashing a light streak.
   Dark-flipped twin of the unflipped master in _source/retired/images/ (same red-tinted noise,
   fade reversed to the bottom to match the light flipped file's dissolve-into-next-section
   behaviour). */
html[data-theme="dark"] .hero::before{background-image:url("https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noise-overlay-dark-flipped.svg")}
.hero .hero-content{position:relative;z-index:2;width:100%}
/* Background-video pause control (SC 2.2.2 Pause, Stop, Hide) — RE-ADDED 2026-08-28.
   The markup and the tpl.js wiring for .hero-pause both survived the 2026-08-11 removal (that
   call was about the HOME hero's slide rotation, which had no fallback control — see the note
   beside .hero-slides); what went with it was this styling. So the button that scangauge.html
   and intercars.html unhide on load was rendering as a 4x19px static box at the hero's top-left,
   underneath .hero-scrim — present in the a11y tree, unclickable in the page. Verified with
   elementFromPoint over its own centre: .hero-scrim, on both pages.
   z-index 3 puts it above the scrim and the grain (both 1) and above .hero-content (2), so it
   stays hittable wherever the copy runs long. 44px is WCAG 2.5.8 AA's floor.
   [hidden] guard is load-bearing: display:flex would otherwise beat the attribute and show the
   control before the video exists — the same trap documented at .hs-nav. */
.hero-pause{position:absolute;right:20px;bottom:20px;z-index:3;display:flex;align-items:center;
  justify-content:center;width:44px;height:44px;padding:0;border-radius:50%;cursor:pointer;
  color:#fff;background:rgba(8,8,10,.55);border:1px solid rgba(255,255,255,.55);
  -webkit-appearance:none;appearance:none;transition:background .18s ease,scale .18s ease}
.hero-pause[hidden]{display:none}
.hero-pause:hover{background:rgba(8,8,10,.78);scale:1.06}
.hero-pause:focus-visible{outline:2px solid #fff;outline-offset:3px}
.hero-pause svg{width:18px;height:18px}
/* one button, two glyphs: aria-pressed is the state, and the icons follow it rather than a
   second class tpl.js would have to keep in sync */
.hero-pause .ico.play{display:none}
.hero-pause[aria-pressed="true"] .ico.pause{display:none}
.hero-pause[aria-pressed="true"] .ico.play{display:block}
@media(max-width:640px){.hero-pause{right:14px;bottom:14px}}
@media(prefers-reduced-motion:reduce){.hero-pause{transition:none}.hero-pause:hover{scale:1}}
html.a11y-no-motion .hero-pause{transition:none}
html.a11y-no-motion .hero-pause:hover{scale:1}
/* light text over the dark video */
.hero .eyebrow{color:var(--red-ink-inverse)}
.hero h1.two-tone{color:#fff}
.hero h1.two-tone span:not(.word){color:rgba(255,255,255,0.66)}
/* --red on a hero: .hero's own second-tone rule outranks :233, so the modifier needs a
   hero-scoped twin. Over the dark scrim the red has to be the inverse token (#ef7676,
   7.6:1) — the #AB0000 of :233 is unreadable on it. Used on about.html. */
.hero h1.two-tone--red span:not(.word){color:var(--red-ink-inverse)}
.hero .lede{color:rgba(255,255,255,0.88)}
.hero .btn-ghost{border-color:rgba(255,255,255,0.55);color:#fff}
/* both sides theme-invariant: --ink flips to #F2F1ED in dark, so pairing it with a hard #fff
   background rendered white text on a white button on every hero. Same pattern as .ainx-sec. */
.hero .btn-ghost:hover{background:#F2F1ED;color:#181818}
/* Photo hero (e.g. D2C): stronger scrim + capped text column so left-aligned text stays on a dark-enough wash for AA over a bright photo; white focus ring that holds against the variable photo bg */
.hero-photo .hero-scrim{background:linear-gradient(90deg,rgba(8,8,10,.94) 0%,rgba(8,8,10,.84) 44%,rgba(8,8,10,.6) 74%,rgba(8,8,10,.34) 100%)}
/* --soft: for a photo hero whose subject spans the full frame, where the standard scrim hides the
   thing the page is about (the team group shot: faces edge to edge, so the .94/.84 left end
   buries half the people). In use by values.html since 2026-07-30.
   RETUNED 2026-07-30, and the shape changed, not just the strength. The stops were .86/.72/.46/.20
   — eased back from .hero-photo and annotated as holding "the headline and lede above AA on that
   photo". They did not. Measured by compositing the real photo with this exact gradient in a
   canvas and sampling every glyph pixel (Range.getClientRects, not element boxes), the lede came
   out at 1.94:1 at 390px and 4.06:1 at 1000px, and the h1 at 2.82-2.90:1 at 1000/1440 — three
   real failures. Reverting to the standard .hero-photo stops would NOT have fixed it either
   (lede 2.78:1 at 390, 3.43:1 at 768): a left-heavy wash is simply the wrong shape here, because
   at phone width the text runs edge to edge, so the light right end sits directly under it.
   Solving for the minimum alpha each glyph pixel needs gives an envelope that is nearly FLAT at
   ~.60 with a ~.83 spike at the far left (the 12px eyebrow, which needs 4.5:1 at small size).
   Hence a gentle fade onto a high floor rather than a fade toward transparent. The left end is
   unchanged at .86, so the leftmost people are no darker than before; only the middle and right,
   where every failure was, come up. Worst measured glyph contrast now, at 390/768/1000/1440:
   h1 5.14/4.89/4.45/4.49, lede 5.16/5.47/6.80/8.87, eyebrow 5.06/4.89/4.84/5.06 — all AA, with
   the h1 judged against 3:1 as large text and everything else against 4.5:1. */
.hero-photo--soft .hero-scrim{background:linear-gradient(90deg,rgba(8,8,10,.86) 0%,rgba(8,8,10,.78) 44%,rgba(8,8,10,.66) 74%,rgba(8,8,10,.60) 100%)}
/* .hero-flat: dark ground for a .hero with no photo behind it. --inverse is fixed near-black in
   both themes, so the hero's white text literals stay valid when the theme flips.
   The shared .hero::before grain carries a dissolve-to-light gradient tuned to sit over photo or
   video detail; on flat near-black there is nothing to break it up and it blooms into a pink haze
   across the lower half. Damped well past .hero-photo's .35, which keeps the soft grain seam into
   the light KPI band below without the smear. */
.hero-flat{background:var(--inverse)}
.hero-flat::before{opacity:.14}
.hero-photo h1.two-tone{max-width:960px}
.hero-photo .lede{max-width:620px}
/* D2C trial: the shared grain overlay (tuned for the dark video-poster hero) reads as heavy
   reddish mud over a bright, detailed real photo — cut it well back here first; roll to
   .hero globally only if this reads right (2026-07-24). */
.hero-photo::before{opacity:.35}
/* Photo-hero height: derive it from each breakpoint's art-directed crop instead of the
   generic vh guess above. .hero's clamp(480px,66vh,720px) assumes ~66vh comfortably clears
   480px — true on a tall plain browser window, but real phone chrome eats enough of the
   visual viewport that 66vh routinely lands AT the 480px floor. At that floor a ~390px-wide
   phone gives a 390/480≈0.81 box against the 430x620≈0.69 portrait crop — ~15% cropped off
   top+bottom, invisible in a headless --window-size test (no browser chrome to shrink vh)
   but very visible on a real device. Matches the same 720/1024 breakpoints tpl.js uses to
   pick the video/poster source, so the box always matches the file actually loaded.
   Scoped to .hero-multi (D2C, B2B2C, cross-docking — each ships a distinct portrait/tablet/
   desktop crop) and NOT bare .hero-photo: a page with a single landscape asset for every size
   must keep the plain vh-based height above, because locking it to a 430x620 portrait ratio
   would crop a 1920x720 source far worse than the bug this fixes. Cross-docking was that case
   until 2026-07-29, when its portrait + tablet crops were baked and it joined the set. */
@media(max-width:720px){.hero-photo.hero-multi{aspect-ratio:430/620;min-height:0}}
@media(min-width:721px) and (max-width:1024px){.hero-photo.hero-multi{aspect-ratio:1024/720;min-height:0}}
/* Pin the inline size so aspect-ratio can only ever drive the BLOCK size. Without a definite
   width the ratio resolved the other way round, from the inherited .hero min-height
   (clamp(480px,66vh,720px)): at a 900px-tall viewport the hero computed 594 x 1920/720 = 1584px
   wide inside a 1425px parent, so d2c and b2b2c scrolled ~159px sideways at >=1025px. Fixing it
   by dropping min-height instead (matching the two rules above) is what it looks like you should
   do, and it is wrong: height then comes purely from the ratio, which is only 379px at 1025px,
   and .hero{overflow:hidden} silently clips the h1 and lede. Width definite + the floor kept
   gives 1425x594 at 1440 and 1905x714 at 1920 — ratio when there is room, floor when there
   isn't, and never a horizontal scrollbar. */
.hero-photo.hero-multi{width:100%}
/* HERO SLIDES — homepage-only background rotation behind the fixed message (spec:
   docs/superpowers/specs/2026-08-03-home-hero-slider-design.md). Slides are stacked
   full-bleed layers at the .hero-bg plane; only opacity ever animates. The scrim is
   scoped: bare-.hero stops (.88/.64/.30/.12) measured below AA over this same team
   photo on values.html (2026-07-30); starting stops here are the .hero-photo set —
   Task-4 glyph-run measurement (2026-08-03) owns the final values: 63-point sweep
   (1440/768/390 x 3 slides x sampled frames x eyebrow/h1/lede) found only the 100%
   stop under-alpha'd — worst offender 390px slide1(photo) .lede at x=330, 3.48:1 vs
   4.5:1 floor (768px slide1 .lede also failed, 4.43:1 at x=599); envelope-derived fix
   raises the 100% stop .34->.55, all other stops already dominated the measured need
   and are unchanged. Re-run: node SCRATCH/hs/probe-scrim.mjs, all 63 lines PASS.
   SLIDE PICKER REMOVED 2026-08-06 (UX review, finding 5). Two revisions lived here — the
   2026-08-03 hairline indicator bars, then the 2026-08-06 thumbnail filmstrip (glass dock,
   progress rail, three thumbs, wrapping chevrons) — and the review retired the whole idea
   rather than the styling: the hero's eyebrow, h1, lede and CTAs are FIXED, so any per-slide
   control promised a message change that never came. Gone with the markup: .hs-nav, .hs-rail,
   .hs-rail-fill, .hs-strip, .hs-thumbs, .hs-thumb, .hs-arrow. The scrim envelope above and the
   cross-fade below are untouched. .hero-pause itself was removed later (2026-08-11, owner's
   call) — it was this rotation's only SC 2.2.2 mechanism and nothing replaced it; see the JS
   comment above `[data-hero-slider]`'s IIFE for the full note.
   The dock was also the hero's entry in the sanctioned-glass family list (see .rs-formpanel);
   that entry is struck at the same time — nothing on this hero is glass any more.
   Superseded specs: 2026-08-03-home-hero-slider-design.md (bars),
   2026-08-06-home-hero-filmstrip-nav-design.md (filmstrip).
   `--hs-fill` / `.hs-run` RECREATED 2026-08-11, deliberately NOT restoring what they drove.
   Same names, same contract (duration in ms via `--hs-fill`, `.hs-run` starts the fill,
   remove-reflow-add restarts it) — continuity with the history above, not a fresh invention.
   What's different: `.hs-seg` is a plain `aria-hidden` div, never a button, so it carries no
   click-to-jump and no per-slide label. That is the entire point — a timer readout, not a
   picker, so it cannot repeat the "implies you can choose a message" failure this comment is
   about. See .hs-progress below. */
.hero-slides .hs-slide{position:absolute;inset:0;z-index:0;opacity:0;transition:opacity 900ms var(--ease-out);pointer-events:none}
.hero-slides .hs-slide.is-on{opacity:1}
/* Tuned to THIS photograph (2026-08-10). The home hero measured a median 3.82:1 on the faded
   second headline line against real composited pixels, where every other hero measures ~8.5:1 —
   the most-visited page was the least legible of the set. The cause is local: "distribution
   networks." runs out to ~55% of the canvas, and on this backdrop that band holds a bright blue
   wall and light shirts, where the other heroes hold dark ground. The extra stops deepen the wash
   only across 40-58%, then rejoin the original curve by 74% so the group photo (the entire point
   of this hero) keeps its faces. Scoped to .hero-slides, so no other hero is touched. */
.hero-slides .hero-scrim{background:linear-gradient(90deg,rgba(8,8,10,.94) 0%,rgba(8,8,10,.91) 40%,rgba(8,8,10,.82) 58%,rgba(8,8,10,.62) 74%,rgba(8,8,10,.55) 100%)}
@media (prefers-reduced-motion: reduce){.hero-slides .hs-slide{transition-duration:0s}}
html.a11y-no-motion .hero-slides .hs-slide{transition-duration:0s}
/* RESTING SLIDE FOR MOTION-OFF VISITORS (2026-08-24). The photo moved from slot 1 to slot 3
   that day, so the slide carrying `is-on` in the markup is now the cross-docking video - and
   the slider module in tpl.js returns early under reduced motion, before it touches a single
   class. Left alone, that hands motion-off visitors a still video poster where they used to
   get the team group shot, which is also the only slide with a real 3-breakpoint <picture>
   behind it. These two rules put them back on the photo. Both are (0,3,0), the same weight as
   `.hs-slide.is-on` above, and they sit later in the file, so source order decides and the
   :not() is what actually switches the other slots off - a plain `.hs-slide{opacity:0}` would
   be (0,2,0) and quietly lose to .is-on. KEYED TO .hs-rest, NOT :last-child: this shipped as
   :last-child for one draft and matched NOTHING, because the slides are not the last children
   of .hero-slides - .hero-scrim, .hero-content and .hs-progress all follow them - so every
   slide went to opacity 0 and motion-off visitors got a bare scrim over empty space. Caught in
   the browser, not in review. A class also survives reordering the way :nth-child(3) would
   not. A no-JS visitor still lands on slot 1's poster: there is no `no-js` class on this site
   for CSS to test, and that gap is called out in index.html beside the slides. */
@media (prefers-reduced-motion: reduce){
  .hero-slides .hs-slide:not(.hs-rest){opacity:0}
  .hero-slides .hs-slide.hs-rest{opacity:1}
}
html.a11y-no-motion .hero-slides .hs-slide:not(.hs-rest){opacity:0}
html.a11y-no-motion .hero-slides .hs-slide.hs-rest{opacity:1}
@media(min-width:1025px){.hero-photo.hero-multi{aspect-ratio:1920/720}}
.hero a:focus-visible,.hero button:focus-visible{outline-color:#fff;box-shadow:0 0 0 2px rgba(0,0,0,.45)}

/* Progress bar (2026-08-11, revised same day) — bottom edge now, not top; white fill at
   full opacity, not --red-ink-inverse; and clickable. Wide, not full-bleed: see the
   .a11y/.to-top clearance comment below for why the insets aren't a flat 20px.
   [hidden] guard: same trap .hero-pause used to document — the unconditional display:flex
   below otherwise beats the hidden attribute.
   MEASURED, not guessed (2026-08-06's own lesson on this hero): the FIRST version (top edge,
   --red-ink fill, no backdrop) measured 2.2-2.6:1 on the cross-docking slide's brighter band —
   under the WCAG 1.4.11 floor of 3:1. The dark plate below (rgba(11,11,12,.55), the same
   token .hero-pause/.hs-arrow already used for this exact problem) is what actually fixed
   it and stays load-bearing here: a plain white fill has plenty of contrast on its own once
   it has a guaranteed-dark surface to sit on, which is exactly what still cannot be assumed
   from the raw video underneath. Re-verify at this new position before trusting the old
   measurement — vertical position doesn't change the scrim (it's a horizontal-only gradient)
   but the actual video/photo CONTENT at a different y-coordinate is not the same pixels. */
.hs-progress{position:absolute;z-index:2;bottom:24px;left:80px;right:80px;display:flex;gap:8px;
  padding:9px 10px;border-radius:8px;background:rgba(11,11,12,.55);box-sizing:border-box}
.hs-progress[hidden]{display:none}
/* FOUND BY MEASUREMENT, NOT GUESSED (2026-08-11): at mobile heights the hero fills the whole
   first screen (min-height clamps to the viewport), so its bottom edge coincides with the
   viewport's own bottom edge — exactly where the fixed .espa-bar also anchors. Measured a
   360x700 viewport with the ESPA co-funding bar showing: bar rect y 664-685, espa-bar rect y
   644-700 — the bar was rendering completely UNDER a 56px+ opaque banner, invisible to any
   real visitor for the entire session that banner is up. Same fix already used sitewide for
   this exact problem (.a11y, .to-top, .hx-nav all do this): lift by var(--espa-h) while
   html.espa-on is set. Applied unconditionally, not just in the mobile query below — a short
   desktop browser window (~800px tall) clamps the hero to a similar height and hits the same
   collision at full desktop width, per the .hx-nav comment (~tpl.css:5221) documenting the
   identical risk for that component. */
html.espa-on .hs-progress{bottom:calc(24px + var(--espa-h))}
/* SECOND collision, surfaced by the espa fix above (2026-08-11, measured not guessed): lifting
   the bar to clear .espa-bar moves it into the SAME band .a11y and button.to-top occupy in
   their two bottom corners (both also lift by --espa-h — see :3114). Measured full overlap on
   both sides at 360x700 before this fix. Left/right insets below are THE SITE'S OWN corner
   geometry, not invented margins — .a11y-toggle/.to-top are 52px wide at a 20px inset (:2928,
   :3057), so their outer edge sits at 72px; +8px clearance (matching the bar's own inter-segment
   gap) = 80px. This is why the base rule above isn't a flat 20px like .hero-pause's old corner
   offset was — this bar is much wider than a single corner control and actually reaches them. */

/* border/padding/font reset: a real <button> now (2026-08-11), for the click-to-jump below —
   browser default button chrome would otherwise show through. cursor:pointer replaces the
   pointer-events:none the ambient-only version had; these are real controls now. */
/* NO overflow:hidden here. It was set to clip .hs-seg-fill's rounded corners, which the fill
   does not need — it is inset:0 with its own matching border-radius:2px, and scaleX only ever
   runs 0 -> 1, so it can never spill. What overflow:hidden DID do was clip this rule's own
   ::before, collapsing the hit target below back to the 3px bar: measured 2px of reach above
   the bar and 1px below, so a click 3px high of centre landed on .hs-progress instead of the
   button. None of the three components the comment below points at clips its hit host. */
.hs-seg{flex:1;height:3px;border:0;padding:0;margin:0;font:inherit;cursor:pointer;
  border-radius:2px;background:rgba(255,255,255,.40);position:relative}
/* Hit target: the segment is 3px tall, nowhere near WCAG 2.5.8's 24x24 — same ::before
   trick .lang-btn/.theme-switch/.rv-dots use to reach it without changing the visual size.
   No horizontal expansion needed: flex:1 already makes each segment far wider than 24px. */
.hs-seg::before{content:'';position:absolute;inset:-14px 0}
.hs-seg-fill{position:absolute;inset:0;background:#fff;border-radius:2px;
  transform:scaleX(0);transform-origin:left;pointer-events:none}
.hs-seg.is-done .hs-seg-fill{transform:scaleX(1)}
.hs-seg.hs-run .hs-seg-fill{animation:hs-fill var(--hs-fill,7000ms) linear forwards}
/* Freezes the fill exactly where it is rather than resetting to 0 — covers the automatic
   pauses (tab-hidden, scrolled-away) and the manual one. REWORKED 2026-08-12: a click used
   to set userStopped for good, which parked the hero on that slide with no way forward and
   no way back (there is no next/prev here). Now only the CURRENT segment toggles the stop;
   clicking a different one jumps and keeps rotating. See choose() in tpl.js. */
.hs-progress.is-paused .hs-seg-fill{animation-play-state:paused}
@keyframes hs-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}
@media(max-width:720px){
  .hs-progress{bottom:14px;gap:6px}
  html.espa-on .hs-progress{bottom:calc(14px + var(--espa-h))}
}
/* Separate breakpoint on purpose — 640px, not this block's 720px. .a11y/.to-top shrink to
   44px wide at a 12px inset exactly at max-width:640px (:3068, an H6 audit fix, not a round
   number), so their outer edge moves from 72px to 56px right there. 641-720px still has the
   large 72px-reach corner buttons, so the base rule's 80px insets must hold through that band
   — only at <=640px does clearance shrink too: 56px + 8px margin = 64px. */
@media(max-width:640px){
  .hs-progress{left:64px;right:64px}
}
@media(prefers-reduced-motion:reduce){.hs-progress{display:none}.hs-seg-fill{animation:none!important}}
html.a11y-no-motion .hs-progress{display:none}
html.a11y-no-motion .hs-seg-fill{animation:none!important}

/* Art hero — a plain ink-on-paper hero with theme-paired artwork behind it. The scrim is mixed
   from --paper-100, which flips with the theme, so ONE gradient serves light and dark and only
   the image swaps (via --hero-art). Art is anchored right: every pair supplied so far keeps its
   subject centre-right and its left third near-empty, which is where the text sits. */
/* The four wash stops are variables so a page whose art is high-contrast can hold more paper
   under the text column without redeclaring the whole background shorthand (which would mean
   repeating the art layer here and again in the ≤900px block). Defaults are the original
   values, so --faq renders exactly as before. */
.art-hero{position:relative;isolation:isolate;overflow:hidden;
  --wash-a:95%;--wash-b:84%;--wash-c:42%;--wash-d:18%}
.art-hero::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:
    linear-gradient(90deg,
      color-mix(in srgb,var(--paper-100) var(--wash-a),transparent) 0%,
      color-mix(in srgb,var(--paper-100) var(--wash-b),transparent) 44%,
      color-mix(in srgb,var(--paper-100) var(--wash-c),transparent) 74%,
      color-mix(in srgb,var(--paper-100) var(--wash-d),transparent) 100%),
    var(--hero-art) var(--hero-art-pos,right center)/var(--hero-art-size,cover) no-repeat}
/* ≤900px the text spans the full width, so the wash goes top-to-bottom instead of left-to-right */
@media(max-width:900px){
  .art-hero::before{background:
    linear-gradient(to bottom,
      color-mix(in srgb,var(--paper-100) 94%,transparent) 0%,
      color-mix(in srgb,var(--paper-100) 86%,transparent) 62%,
      color-mix(in srgb,var(--paper-100) 55%,transparent) 100%),
    var(--hero-art) var(--hero-art-pos,right center)/var(--hero-art-size,cover) no-repeat}
}
.art-hero--faq{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/own-the-risk-light.webp)}
html[data-theme="dark"] .art-hero--faq{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/own-the-risk-dark.webp)}

/* Online sales channels — one theme-paired pair per channel, baked from the masters in
   _source/channel-masters/ by tools/bake-channel-heroes.sh. Same ink-on-paper
   treatment as --faq and for the same reason: light and dark twins are supplied, so forcing
   the .hero-photo dark scrim would throw the light one away.
   Mobile is a separate recomposition, not the same file scaled. The masters are 2.67:1, so
   cover at phone widths would show only the right end of the container ("gle" of Google); the
   -mobile files reframe the whole container into a 430x620 portrait.
   That portrait swap is scoped to ≤720 -- the same breakpoint d2c/b2b2c and tpl.js use -- and
   NOT to the shared ≤900 art-hero breakpoint. At 820 the portrait art is narrower than the
   viewport, which exposes its padded rectangle as a visible panel: the top and bottom edges
   are feathered into paper but the left and right are not, and they cannot be without fading
   the container, which runs nearly the full width of the frame. So 721-900 keeps the desktop
   side-by-side layout instead, where the landscape master still frames correctly. */
/* Shared channel-hero behaviour; each page carries .art-hero--chan plus its own modifier.
   Two departures from --faq, both because this art is high-contrast product photography
   rather than ghosted background texture:
   - Wash nudged up only slightly from the default. Measured at 1440 the h1's second clause
     ends at x=1148 (79.7% across) -- it genuinely overlaps the container, which starts at
     x=680, and no wash tuning separates them. Washing hard enough to clear the overlap
     (tried 89/62/26) erased the light-mode container: it is pale-on-pale, so paper laid over
     it kills it, while the dark twin survives anything because it is light-on-near-black.
     Legibility was never the constraint -- ink on the pale container measures well past AA --
     so these stops stay close to the default and let the art read.
   - Lede capped to the same 620px .hero-photo uses; uncapped it ran to x=866, into the art. */
.art-hero--chan{--wash-a:96%;--wash-b:86%;--wash-c:48%;--wash-d:20%}
.art-hero--chan .lede{max-width:620px}

/* ===== Pinned hero: the hero sticks and the page rides up over it =====
   Same family as D2C's .fit-curtain and the history footer curtain, but driven by sticky rather
   than a negative margin, so it needs no hero-height maths and survives a hero of any height.

   WHO GETS IT — opt-in by hero class, so a page added later inherits the behaviour by using the
   house hero patterns and nobody has to remember a flag:
     · #main.hero      — every photo/video hero (index, customers, partners, investors, team,
                         values, and the four co-operation model pages: d2c, b2b2c,
                         cross-docking, oaas)
     · #main.art-hero  — the theme-paired art heroes (faq, channels/amazon, channels/google)
     · body.chan-page #main — the type-only channel heroes, which carry no hero class. As of
       2026-07-29 that is just the hub (channels/index.html) — every one of the 11 individual
       channel pages (Amazon, eBay, Google, Meta, eMAG, Zalando, ABOUT YOU, BestPrice, Skroutz,
       Walmart, Allegro) now carries .art-hero--<slug> and a baked theme pair. The hub itself has
       no single channel to art for and is expected to stay type-only.
   WHO OPTS OUT, and why:
     · .hx-page — history's GSAP route scene plus its footer curtain: a competing pin, since
       history's own scene claims the same top-of-viewport sticky slot the hero would use
     · .ai-neural — dark page, canvas hero, and four of its bands are transparent, so the paper
       default below would band a near-black page in the wrong grey
   .fit-curtain (the 5 model pages' mid-page "pinned Fit scene + curtain lift") is DELIBERATELY
   NOT an exclusion. It was one until 2026-07-29: the earlier assumption was that any page
   running its own pinned scene would fight this one. Re-examined, that does not hold — the
   hero's sticky range is bounded by its own (short) section, `.fitx-stick`'s sticky range is
   bounded by its own `.fitx-band` further down the page, and neither reads the other's layout
   (position:sticky never changes document flow/height, so `.hiw`'s hand-rolled scroll handler,
   which measures `#hiw-scene`'s getBoundingClientRect().top, is unaffected by the hero above it
   also being sticky). The two pins occupy disjoint vertical ranges and never compete. Verified
   2026-07-29 on all four (CDP: hero correctly hides behind the paper-100 default the instant the
   next section scrolls over it, `.hiw`/`.steps-expand` and the `.fitx-band`/`.fit-curtain`
   hand-off both still fire at their normal scroll depths, zero new horizontal overflow).
   A new page with its own curtain therefore does NOT need to exclude itself by adopting
   .fit-curtain — only .hx-page and .ai-neural style conflicts warrant an opt-out.

   HOW, three parts, all needed:
     1. the hero sticks at the top and drops to the back of the stack (z-index 0, under the
        .abar's 50, so the frosted header still passes over it);
     2. later bands are positioned so they paint ABOVE the hero — later siblings win at equal
        z-index, but only once they are positioned at all. Deliberately NO z-index here: setting
        one would outrank .fitx-band's 0 and .fit-curtain's 2 on the pages that use them. Limited
        to section/footer too, because the sibling list also holds the fixed chrome (.crsr cursor,
        .ms-sheet mobile menu, .a11y widget) and forcing position:relative on those breaks them.
     3. most bands are transparent and inherit the page paper, which would let the pinned hero
        show straight through them, so they are defaulted to paper. Wrapped in :where() to keep
        that at ZERO specificity: a band defining its own background (.wwd and .aud with artwork,
        .band-inverse, .fitx-band, footer) always wins, and only genuinely bare bands take the
        default. background-color longhand, never the shorthand, for the same reason.
   The leading edge carries an upward shadow so the band reads as a sheet sliding over the hero
   rather than the hero simply vanishing — on a type-only hero both layers are paper, so without
   it there is no visible seam at all.
   Desktop only, off under either reduced-motion signal, where it degrades to the normal stacked
   flow. Verified over CDP: the hero's viewport top holds at 0 while the page scrolls.

   TWO SEPARATE 820/821 FACTS LIVE HERE. They are about the SAME gate but they are not the same
   statement, so do not read them as one, and do not read either as being about the history page.

   (a) THIS gate's own off-switch — behaviour. #main:is(.hero,.art-hero) stops pinning and
       scrolls away in normal flow at ≤820px (the inverse of the min-width:821px below) and under
       either reduced-motion signal. CDP-verified at 820 and under reduce.

   (b) THIS gate's divergence from the contract — BREAKPOINT NOTE (2026-07-29, scroll-parity
       Task 1). The 821 in (a) is NOT the 721px of the breakpoint contract at the top of this
       file, and the line above used to claim this gate was "gated exactly like .fit-curtain",
       which has always been min-width:721px, so the two never matched.
       The 821 is left UNCHANGED on purpose: it is the one scroll gate that applies to nearly
       every page on the site (any #main.hero / #main.art-hero), it was CDP-verified at 821 the
       same day, and the scroll-parity brief did not list it. No technical reason for 821 over
       721 is known — do not invent one. Moving it needs its own pass over every hero page, at
       721 and 820, checking that a hero taller than the viewport does not get covered before it
       can be read.

   Neither number concerns the history page's .hx gates, which ARE on the contract at 720/721:
   body.hx-page is excluded from this selector outright.

   2026-07-30 (audit C2): every page now wraps hero..last-section in <main id="page-main">, so
   the sticky hero's containing block is <main> rather than <body> and the hero unsticks at the
   footer's top edge instead of staying pinned behind it. Visually inert — the footer is opaque
   and already carries position:relative unconditionally (see `footer{...}` ~1665), so it always
   painted over the pinned hero anyway. The `~ :is(section,footer)` / `+ :is(section,footer)`
   clauses below keep `footer` for the record; it no longer matches (footer is outside <main>)
   and does not need to. Verified by before/after pixel diff on index/customers/amazon/d2c. */
@media(min-width:821px) and (prefers-reduced-motion:no-preference){
  html:not(.a11y-no-motion) body:not(.hx-page):not(.ai-neural) #main:is(.hero,.art-hero),
  html:not(.a11y-no-motion) body.chan-page:not(:has(.fit-curtain)) #main{position:sticky;top:0;z-index:0}

  html:not(.a11y-no-motion) body:not(.hx-page):not(.ai-neural) #main:is(.hero,.art-hero) ~ :is(section,footer),
  html:not(.a11y-no-motion) body.chan-page:not(:has(.fit-curtain)) #main ~ :is(section,footer){position:relative}

  :where(html:not(.a11y-no-motion) body:not(.hx-page):not(.ai-neural) #main:is(.hero,.art-hero) ~ section),
  :where(html:not(.a11y-no-motion) body.chan-page:not(:has(.fit-curtain)) #main ~ section){background-color:var(--paper-100)}

  html:not(.a11y-no-motion) body:not(.hx-page):not(.ai-neural) #main:is(.hero,.art-hero) + :is(section,footer),
  html:not(.a11y-no-motion) body.chan-page:not(:has(.fit-curtain)) #main + :is(section,footer){box-shadow:0 -20px 44px -26px rgba(0,0,0,.22)}
}

.art-hero--amazon{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-amazon-hero-light.webp)}
html[data-theme="dark"] .art-hero--amazon{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-amazon-hero-dark.webp)}
.art-hero--google{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-google-hero-light.webp)}
html[data-theme="dark"] .art-hero--google{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-google-hero-dark.webp)}
.art-hero--meta{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-meta-hero-light.webp)}
html[data-theme="dark"] .art-hero--meta{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-meta-hero-dark.webp)}
.art-hero--ebay{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-ebay-hero-light.webp)}
html[data-theme="dark"] .art-hero--ebay{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-ebay-hero-dark.webp)}
.art-hero--emag{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-emag-hero-light.webp)}
html[data-theme="dark"] .art-hero--emag{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-emag-hero-dark.webp)}
.art-hero--zalando{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-zalando-hero-light.webp)}
html[data-theme="dark"] .art-hero--zalando{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-zalando-hero-dark.webp)}
.art-hero--aboutyou{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-aboutyou-hero-light.webp)}
html[data-theme="dark"] .art-hero--aboutyou{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-aboutyou-hero-dark.webp)}
.art-hero--bestprice{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-bestprice-hero-light.webp)}
html[data-theme="dark"] .art-hero--bestprice{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-bestprice-hero-dark.webp)}
.art-hero--skroutz{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-skroutz-hero-light.webp)}
html[data-theme="dark"] .art-hero--skroutz{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-skroutz-hero-dark.webp)}
/* Walmart's truck spans nearly the full 1920px frame (not a right-confined product shot like
   every other channel), and its blue/white "Walmart+" livery is saturated rather than pale, so
   the shared .art-hero--chan wash (tuned for pale-on-pale product renders) lets the logo ghost
   through directly behind the h1/lede. Raised wash-a/b/c (0-74% span, where the text column
   sits) well above the --chan default; wash-d left close to default so the cab at the right
   edge, past the text, still reads with normal art visibility. */
.art-hero--walmart{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-walmart-hero-light.webp);
  --wash-a:99%;--wash-b:95%;--wash-c:70%;--wash-d:22%}
html[data-theme="dark"] .art-hero--walmart{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-walmart-hero-dark.webp)}
.art-hero--allegro{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-allegro-hero-light.webp)}
html[data-theme="dark"] .art-hero--allegro{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-allegro-hero-dark.webp)}
.art-hero--shopflix{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-shopflix-hero-light.webp)}
html[data-theme="dark"] .art-hero--shopflix{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-shopflix-hero-dark.webp)}
/* 721-900: the shared rule flips the wash to top-to-bottom here, on the assumption the text
   spans the full width. For these heroes it does not until ~720 -- at 820 the h1 still sets on
   two lines and the lede fills ~590 of 820 -- so restore the desktop left-to-right wash and the
   landscape art. Cover at 820 lands on source x≈885-1920, which holds the whole container. */
@media(min-width:721px) and (max-width:900px){
  .art-hero--chan::before{background:
    linear-gradient(90deg,
      color-mix(in srgb,var(--paper-100) var(--wash-a),transparent) 0%,
      color-mix(in srgb,var(--paper-100) var(--wash-b),transparent) 44%,
      color-mix(in srgb,var(--paper-100) var(--wash-c),transparent) 74%,
      color-mix(in srgb,var(--paper-100) var(--wash-d),transparent) 100%),
    var(--hero-art) right center/cover no-repeat}
}
@media(max-width:720px){
  /* Full-bleed photo-behind-text, matching .hero/.hero-multi's own mechanism (see d2c/b2b2c):
     a FIXED, content-independent aspect-ratio box (not text-driven height, and not the interim
     "reserved band below the copy" this replaced -- see git history + docs/superpowers/specs/
     2026-07-29-amazon-mobile-hero-fullbleed-design.md for why that was tried first and dropped)
     with the art as a true `cover` background genuinely BEHIND the whole text block, held
     legible by one continuous scrim rather than by dodging the art. Piloted on Amazon alone,
     verified, then rolled to all 6 2026-07-29.
     430/700 clears every channel's copy except Google/Zalando, whose lede+eyebrow run longer
     (~630px tall at 375px wide vs ~535-572px for the other four) -- those two get a taller
     ratio below so their last button doesn't clip against the box edge. Needs mobile art baked
     to the SAME aspect ratio as whichever rule applies (bake-channel-heroes.sh's PADH/IY) --
     matching the canvas to the box outright avoids `cover` needing to crop at all, same trick
     .hero-multi's own mobile crop already relies on. */
  .art-hero--chan{width:100%;aspect-ratio:430/700;min-height:0;padding-bottom:0}
  .art-hero--chan::before{background:
    linear-gradient(to bottom,
      color-mix(in srgb,var(--paper-100) 90%,transparent) 0%,
      color-mix(in srgb,var(--paper-100) 80%,transparent) 55%,
      color-mix(in srgb,var(--paper-100) 58%,transparent) 100%),
    var(--hero-art) center/cover no-repeat}
  .art-hero--google,.art-hero--zalando{aspect-ratio:430/770}
  /* Same Walmart-only wash bump as the desktop rule above (tpl.css:830) -- this ≤720px stop
     set is a flat shared rule, not the --wash-* custom properties, so it needs its own override
     rather than inheriting one. Raised across all 3 stops: the CTA row sits low in the box,
     right where the shared rule is least washed (100% stop, 58%), and that is exactly where
     the truck's blue livery was colliding with the button pill. */
  .art-hero--walmart::before{background:
    linear-gradient(to bottom,
      color-mix(in srgb,var(--paper-100) 92%,transparent) 0%,
      color-mix(in srgb,var(--paper-100) 88%,transparent) 55%,
      color-mix(in srgb,var(--paper-100) 78%,transparent) 100%),
    var(--hero-art) center/cover no-repeat}
  .art-hero--amazon{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-amazon-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--amazon{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-amazon-hero-dark-mobile.webp)}
  .art-hero--google{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-google-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--google{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-google-hero-dark-mobile.webp)}
  .art-hero--meta{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-meta-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--meta{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-meta-hero-dark-mobile.webp)}
  .art-hero--ebay{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-ebay-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--ebay{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-ebay-hero-dark-mobile.webp)}
  .art-hero--emag{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-emag-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--emag{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-emag-hero-dark-mobile.webp)}
  .art-hero--zalando{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-zalando-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--zalando{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-zalando-hero-dark-mobile.webp)}
  .art-hero--aboutyou{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-aboutyou-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--aboutyou{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-aboutyou-hero-dark-mobile.webp)}
  .art-hero--bestprice{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-bestprice-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--bestprice{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-bestprice-hero-dark-mobile.webp)}
  .art-hero--skroutz{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-skroutz-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--skroutz{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-skroutz-hero-dark-mobile.webp)}
  .art-hero--walmart{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-walmart-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--walmart{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-walmart-hero-dark-mobile.webp)}
  .art-hero--allegro{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-allegro-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--allegro{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-allegro-hero-dark-mobile.webp)}
  .art-hero--shopflix{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-shopflix-hero-light-mobile.webp)}
  html[data-theme="dark"] .art-hero--shopflix{--hero-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-shopflix-hero-dark-mobile.webp)}
}

/* focus visibility (WCAG 2.4.7 / 1.4.11) — explicit rings, light and dark contexts */
a:focus-visible,button:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.band-inverse a:focus-visible,.band-inverse button:focus-visible,footer a:focus-visible,footer button:focus-visible{outline-color:var(--paper-100)}

/* cards & bands */
.card{background:var(--paper-50);border:1px solid var(--hairline);border-radius:var(--radius-card);padding:32px}
.card h3{margin-bottom:10px}
.card p{color:var(--ink-soft);font-size:15px}
/* CTA band bg — abstract photo faded into the paper color via a radial blend, sits behind .wrap */
.cta-band{position:relative}
/* the ribbon photo reads busiest right where the text sits (center of the radial fade), so the
   text column gets its own frosted glass panel — same recipe as the scrolled nav bar — to hold
   contrast without boxing off the atmosphere at the section's edges */
.cta-band>.wrap{position:relative;z-index:1;background:rgba(242,241,237,.72);background:color-mix(in srgb,var(--paper-100) 72%,transparent);-webkit-backdrop-filter:blur(18px) saturate(1.15);backdrop-filter:blur(18px) saturate(1.15);border-radius:var(--radius-card);padding:clamp(32px,5vw,56px) clamp(20px,4vw,48px)}
html[data-theme="dark"] .cta-band>.wrap{background:rgba(20,20,22,.6);background:color-mix(in srgb,var(--paper-100) 60%,transparent)}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.cta-band>.wrap{background:var(--paper-100)}}
.cta-bg{position:absolute;inset:0;z-index:0;overflow:hidden;pointer-events:none}
.cta-bg::before{content:"";position:absolute;inset:-6%;background:url(https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/d2c-cta-bg.webp) center/cover no-repeat;opacity:.8}
.cta-bg::after{content:"";position:absolute;inset:0;background:radial-gradient(ellipse 62% 60% at 50% 42%,transparent 0%,var(--paper-100) 88%)}
@media(max-width:820px){.cta-bg::after{background:radial-gradient(ellipse 95% 30% at 50% 16%,transparent 0%,var(--paper-100) 85%)}}
/* CTA ledger — full-width "what happens next" row, no boxes/cards (model-page closers) */
.cta-ledger{list-style:none;display:grid;grid-template-columns:repeat(3,1fr);gap:0;margin:0;padding:26px 0;border-top:1px solid var(--hairline);border-bottom:1px solid var(--hairline)}
.cta-ledger li{padding:2px 32px}
.cta-ledger li:first-child{padding-left:0}
.cta-ledger li:not(:first-child){border-left:1px solid var(--hairline)}
.cta-ledger .num{display:block;font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.08em;color:var(--red-ink);margin-bottom:10px}
.cta-ledger p{margin:0;color:var(--ink-soft);font-size:15px;line-height:1.5}
@media(max-width:820px){.cta-ledger{grid-template-columns:1fr}.cta-ledger li{padding:20px 0;border-left:none!important}.cta-ledger li:first-child{padding-top:0}.cta-ledger li:not(:first-child){border-left:none;border-top:1px solid var(--hairline)}}
.cta-actions{display:flex;gap:16px;flex-wrap:wrap;align-items:center;margin:0}
/* partner logo strip — between hero and KPI band; transparent SVGs on paper, grayscale, marquee */
.brands{padding:48px 0}
/* ── dot-tpl-box ─────────────────────────────────────────────────────────────────────────
   Reusable background field of TPL boxes on a honeycomb lattice, driven by
   assets/js/dot-tpl-box.js (load it per page; it is decorative and not in the shared bundle).
   The moving cousin of #recognition's masked box-tile.svg, at the same 4%-ink resting weight.

     <section class="dtb-host">
       <div class="dot-tpl-box" data-dot-tpl-box aria-hidden="true"><canvas></canvas></div>
       <div class="wrap"> ...content... </div>
     </section>

   .dtb-host is load-bearing, not decoration: overflow:hidden is the promise that the field —
   which is drawn larger than its host and cropped by it — can never change a section's
   height. Positioned children inside the host need z-index:1 to sit above the canvas; the
   .wrap rule below covers the common case. The script's header comment documents the
   data-dtb-* tuning attributes and the two ways to break the pattern. */
.dtb-host{position:relative;isolation:isolate;overflow:hidden}
.dtb-host>.wrap,.dtb-host>section{position:relative;z-index:1}
/* .brands has no .wrap — its kicker and marquee are direct children — so a brands strip
   hosting the field needs them lifted the same way (channels/index.html case studies). */
.dtb-host>.brands-kick,.dtb-host>.brand-marquee{position:relative;z-index:1}
/* Two masks composited: --dtb-fade-x fades the left/right edges, --dtb-fade-b lands the
   bottom edge softly (the home page needs that, because the field ends on the seam with the
   dark .aud band). Both are tokens so a host can retune them without restating the mask —
   e.g. `style="--dtb-fade-b:100%"` for a section that should carry the field to its foot. */
.dot-tpl-box{position:absolute;inset:0;z-index:0;pointer-events:none;
  --dtb-fade-x:10%;--dtb-fade-b:78%;
  -webkit-mask:linear-gradient(90deg,transparent,#000 var(--dtb-fade-x),#000 calc(100% - var(--dtb-fade-x)),transparent),
               linear-gradient(to bottom,#000 var(--dtb-fade-b),transparent);
  -webkit-mask-composite:source-in;
  mask:linear-gradient(90deg,transparent,#000 var(--dtb-fade-x),#000 calc(100% - var(--dtb-fade-x)),transparent),
       linear-gradient(to bottom,#000 var(--dtb-fade-b),transparent);
  mask-composite:intersect}
.dot-tpl-box canvas{display:block;width:100%;height:100%}
/* forced-contrast mode flattens every paper token to #fff — a 4% ink field on white is noise
   there, not texture, so every host drops back to a plain surface, as #recognition does. */
html.a11y-contrast .dot-tpl-box{display:none}
/* Phones get no pointer brush, so the field is pure texture there — a touch fainter, and the
   side fades pull in so it never crowds narrow content. */
@media (max-width:640px){.dot-tpl-box{opacity:.7;--dtb-fade-x:18%}}

/* .chfield — the home page's host, wrapping the channel strip alone (owner call 2026-09-01:
   the KPI band moved back out to bare paper, no texture; it had lived inside since the field
   was one continuous run behind both).
   Everything structural comes from .dtb-host; the rule below is the one thing that is
   specific to sitting directly under the home hero.

   At >=821px that hero is position:sticky and the bands over it are lifted by
   `#main:is(.hero,.art-hero) ~ :is(section,footer){position:relative}` plus the :where()
   --paper-100 backdrop beneath it, in tpl-critical.css (~1338). Those are SIBLING selectors
   on `section`: .chfield is a div, and the bands inside it are no longer siblings of #main,
   so wrapping them dropped both declarations and let the pinned hero show straight through
   the strip. .chfield takes over the role .brands used to play as the hero's next sibling —
   same guards, same declarations, including the +section edge shadow, which lands on the same
   element it always did. Keep in step with the original if it moves.
   ANY future .dtb-host placed directly after a sticky hero needs this same treatment. */
@media(min-width:821px) and (prefers-reduced-motion:no-preference){
  html:not(.a11y-no-motion) body:not(.hx-page):not(.ai-neural) #main:is(.hero,.art-hero) ~ .chfield{
    position:relative;box-shadow:0 -20px 44px -26px rgba(0,0,0,.22)}
  :where(html:not(.a11y-no-motion) body:not(.hx-page):not(.ai-neural) #main:is(.hero,.art-hero) ~ .chfield){
    background-color:var(--paper-100)}
}
/* .csfield -- the case-study strip's host, same job as .chfield and for the same reason.
   It is NOT next to the hero, but it is still downstream of it, and the two declarations that
   lift every band over the pinned hero at :1597 are SIBLING selectors on `section`. Wrapping
   the strip in a div made it a child instead, so it lost both and the hero showed straight
   through the field. Restated here for .csfield: position and the --paper-100 backdrop only.
   NO box-shadow -- that one is the hero seam's edge and belongs to the first band alone.
   ANY future .dtb-host that wraps a section on a sticky-hero page needs this same pair. */
@media(min-width:821px) and (prefers-reduced-motion:no-preference){
  html:not(.a11y-no-motion) body:not(.hx-page):not(.ai-neural) #main:is(.hero,.art-hero) ~ .csfield{position:relative}
  :where(html:not(.a11y-no-motion) body:not(.hx-page):not(.ai-neural) #main:is(.hero,.art-hero) ~ .csfield){
    background-color:var(--paper-100)}
}

/* Same bug, second sticky-hero selector. The block at :1633 pins #main on TWO conditions --
   `#main:is(.hero,.art-hero)` on most pages, and `body.chan-page #main` whatever the hero's
   class -- but the .chfield/.csfield restatements above only ever covered the first. A
   .chfield on a .chan-page page (couriers/index.html, whose #main is .cr-hero) therefore got
   neither the lift nor the backdrop, and the pinned hero read straight through the channel
   strip on scroll. Position and backdrop only, as with .csfield: these hosts sit downstream
   of the hero, not against it, so the seam shadow is not theirs to draw.
   ANY future .dtb-host wrapper on a .chan-page page needs this pair too. */
@media(min-width:821px) and (prefers-reduced-motion:no-preference){
  html:not(.a11y-no-motion) body.chan-page:not(:has(.fit-curtain)) #main ~ :is(.chfield,.csfield){position:relative}
  :where(html:not(.a11y-no-motion) body.chan-page:not(:has(.fit-curtain)) #main ~ :is(.chfield,.csfield)){
    background-color:var(--paper-100)}
}
.brands-kick{text-align:center;font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);margin:0 0 26px}
.brand-marquee{position:relative;overflow:hidden}
.brand-marquee::before,.brand-marquee::after{content:"";position:absolute;top:0;bottom:0;width:11%;z-index:2;pointer-events:none}
.brand-marquee::before{left:0;background:linear-gradient(90deg,var(--paper-100) 25%,transparent)}
.brand-marquee::after{right:0;background:linear-gradient(270deg,var(--paper-100) 25%,transparent)}
.brand-track{display:flex;width:max-content;animation:brand-scroll 44s linear infinite}
.brand-marquee:hover .brand-track{animation-play-state:paused}
.brand-set{display:flex;align-items:center;gap:clamp(30px,3.5vw,56px);padding-right:clamp(30px,3.5vw,56px)}
.brand-set a{flex:none;display:block;line-height:0;border-radius:4px} /* logo → channel-page links keep the img as the only visible box */
/* The kicker is a real link to the section's hub. The color/decoration reset lives here, not
   inline on the anchor, because an inline color beats every stylesheet rule including :hover —
   which is exactly how the old hover sat dead in the markup for weeks. Hover follows the house
   link grammar (.nbar-msg, .dd-more): text to red ink, arrow steps 4px toward the destination. */
.brands-kick a{color:inherit;text-decoration:none;transition:color .18s ease}
.brands-kick .bk-arr{display:inline-block;transition:transform .18s ease}
.brands-kick a:focus-visible{color:var(--red-ink)}
@media(hover:hover) and (pointer:fine){
  .brands-kick a:hover{color:var(--red-ink)}
  .brands-kick a:hover .bk-arr{transform:translateX(4px)}
}
/* A BOX, not a height. Pinning height alone and letting width run free is the worst case for a
   PLATE mark: inter-cars-v2.svg is 3.13:1, so at the row's 52px it opened to 163px and became the
   widest and heaviest object on the strip. max-width caps it at the row's own dominant width and
   object-fit:contain letterboxes the artwork inside the 125x52 cell, which is the same two-sided
   box the .bw-cell logo wall uses (owner call 2026-08-31: "the brands wall default is more
   correct"). Measured, it takes Inter Cars from 163x52 to 125x40.
   The explicit HEIGHT must stay. Switching to height:auto+max-height collapses every viewBox-only
   SVG in the row to 0x0 (dna, rtm, scott, elmon, coffee-island, vatopedi, inter-cars all measured
   0 wide) because they carry no intrinsic size for auto to resolve against - the same trap the
   .bw-cell comment records. Only five marks sitewide are wider than the cap and move at all:
   scangauge 217, boulekos 167, inter-cars 163, avgerinos 163, vatopedi 148. */
.brand-track img{height:calc(52px * var(--os,1));max-width:calc(125px * var(--os,1));width:auto;object-fit:contain;filter:grayscale(1);opacity:.55;transition:filter .25s var(--ease),opacity .25s var(--ease)}
.brand-track img:hover{filter:none;opacity:1}
/* dark mode: logos are dark-ink on transparent → render as dim white silhouettes */
html[data-theme="dark"] .brand-track img{filter:brightness(0) invert(1);opacity:.6}
html[data-theme="dark"] .brand-track img:hover{filter:brightness(0) invert(1);opacity:1}
/* ABOUT YOU is the one mark in the strip drawn as white plates with knockout lettering rather than
   dark-ink-on-transparent. brightness(0) forces every pixel black and invert(1) then forces every
   pixel white, so the whole logo collapsed into two solid rectangles in dark theme (verified by
   render, 2026-08-10 — it was the only one of the eleven that broke). A plain invert flips the
   plates to near-black, which is lost against the dark band, and the lettering to white: the same
   silhouette every other mark gets. */
html[data-theme="dark"] .brand-track .bmc--aboutyou img{filter:invert(1)}
html[data-theme="dark"] .brand-track .bmc--aboutyou img:hover{filter:invert(1);opacity:1}
/* Inter Cars is the one mark in these rows that is a PLATE rather than dark ink on transparent:
   brands/inter-cars-v2.svg (2026-08-31) is a red block and a navy block with KNOCKOUT white
   lettering, on a genuinely transparent ground. It replaced case-studies/intercars-logo.png, an
   opaque red raster that needed a four-filter key to stop it reading as a red box among white
   silhouettes, plus a clip-path to hide a stray white divider bar the crop had left behind.
   None of that applies to a real vector with an alpha channel, so on paper the mark now simply
   takes each row's own grayscale treatment and sits with its neighbours (verified by render,
   2026-08-31: identical computed filter and opacity to the cells either side of it).
   Dark is the one place it still needs an exception. These rows run brightness(0) invert(1),
   which collapses a knockout plate into a single solid white bar with no wordmark left. Invert
   to a light plate with dark lettering instead, which is exactly how BILSTEIN already reads in
   the logo wall two cells over. */
/* opacity must DERIVE from --lw, not hardcode the old default: this rule sits after the density
   block and a literal .6 here silently cancelled the .36 band the plate was given on 2026-08-31
   (measured: the mark still computed .6 in dark while its three peers computed .41). */
html[data-theme="dark"] .brand-track img[src*="inter-cars"]{
  filter:invert(1) grayscale(1) brightness(1.6);opacity:calc(var(--lw,.55) + .05)}
html[data-theme="dark"] .brand-set .bmc:hover img[src*="inter-cars"],
html[data-theme="dark"] .brand-set .bmc:focus-visible img[src*="inter-cars"]{
  filter:invert(1) grayscale(1) brightness(1.6);opacity:1}

/* ————— optical normalisation of the logo walls —————
   Measured 2026-08-10 by rendering every mark on white with the wall's own grayscale/opacity
   neutralised, then summing its ink (1 - luminance per pixel) over the shared canvas. All three
   walls came back with a ~4x spread between lightest and heaviest: Funky Buddha 25.1% against
   Avgerinos 6.1%, About You 19.2% against eBay 4.8%, NVIDIA 17.7% against AWS 4.6%. That is why
   the rows read as a jumble rather than a set — the BOX sizes were already normalised by the
   shared 240x100 canvas, the DENSITY never was, so a heavy mark shouts and a light one disappears.
   --lw corrects for it, bucketed by measured ink so a new logo only has to be dropped into the
   right band instead of hand-tuned:
       ink < 8%    -> .76        ink 13-18%  -> .43
       ink 8-13%   -> .55        ink >= 18%  -> .36   (.55 is also the unlisted default)
   Both themes derive from the one token. Hover still goes to full colour at opacity 1; this
   balances the resting wall only. */
.brand-track img{opacity:var(--lw,.55)}
html[data-theme="dark"] .brand-track img{opacity:calc(var(--lw,.55) + .05)}
.brand-track img[src*="ebay"],.brand-track img[src*="google"],.brand-track img[src*="skroutz"],
.brand-track img[src*="allegro"],.brand-track img[src*="aws"],.brand-track img[src*="microsoft"],
.brand-track img[src*="switch-back"],.brand-track img[src*="avgerinos"],
.brand-track img[src*="shop-and-trade"]{--lw:.76}
.brand-track img[src*="amazon"],.brand-track img[src*="openai"],.brand-track img[src*="nvidia"],
.brand-track img[src*="desigual"],.brand-track img[src*="elmon"],.brand-track img[src*="coffee-island"]{--lw:.43}
/* The customer strip was never bucketed - only elmon and coffee-island of its twelve marks had a
   band. Re-measured 2026-08-31 with the method above, calibrated against the two values this
   comment already records (eBay came back 4.9 against 4.8, About You 19.4 against 19.2), so these
   sit on the same scale: Passadena 57.2%, BSB 46.2%, Inter Cars 46.0%, Boulekos 32.0% all clear
   the >=18% band, and Shop & Trade at 5.7% belongs in .76 with the other faint marks. Those five
   are exactly the ones that read as shouting or vanishing next to their neighbours. Note density
   is size-independent, so the max-width cap above does not move any of these numbers - the cap
   fixes AREA, the band fixes DENSITY, and a plate mark needs both. */
.brand-track img[src*="aboutyou"],.brand-track img[src*="blackyak"],.brand-track img[src*="funky-buddha"],
.brand-track img[src*="passadena"],.brand-track img[src*="bsb"],.brand-track img[src*="inter-cars"],
.brand-track img[src*="boulekos"]{--lw:.36}

/* ————— --ox / --oy — optical centring of the marks —————
   The same problem --lw solves for density, one axis over. Every host here centres a mark's
   BOX, and almost no logo has its ink centred inside its own artboard, so a box-centred row
   reads as marks nudged at random: Shop & Trade sat 9px low in a 52px row because its type
   lives in the bottom half of the file under a swoosh that fills the top right, and RTM and
   SCOTT lean 9-11px in opposite directions. USER DIRECTION 2026-09-01: "θέλω visual να είναι
   όλα στο κέντρο".
   Method, so a new mark can be added without taste entering it: rasterise the source file at
   800px wide, take the ALPHA-weighted centroid of the ink (alpha, not darkness — ScanGauge's
   "Gauge" is white and a darkness measure puts its centre 35px out), and read it as a
   fraction of the mark's own box where .5 means already centred. --ox/--oy are the whole
   correction, (.5 - c) written as a percentage, so one pair holds at every size the mark is
   drawn at and in every host it appears in. Anything under 1% is left off as noise.
       file             cx    cy        file             cx    cy
       dna            .536  .470        scott          .426  .496
       vatopedi       .447  .569        inter-cars     .460  .485
       scangauge      .486  .505        coffee-island  .507  .530
       elmon          .467  .489        bsb            .485  .518
       rtm            .585  .496        passadena      .520  .433
       shop-and-trade .582  .676        boulekos       .552  .469
   It rides on `translate`, NOT on `transform`, and that is the whole reason it is one line
   instead of five: three rules already write transform on these images (the .post hover
   scale, and the .cs-more--bare pair that sets transform:none to cancel it), and a nudge
   folded into transform has to be restated inside every one of them or it silently
   disappears. `translate` is a separate property, composes with whatever transform is in
   play, and nothing else on this site sets it.
   The tokens are declared on the bare image so the value travels with the FILE; only the two
   rules below spend them, so a host that has not opted in is unaffected.

   --os is the third of the set and answers a different complaint: not where a mark sits but
   how loud it is. USER DIRECTION 2026-09-01, "το BSB και το Passadena είναι λίγο μεγάλα, να
   μην κλέβουν την παράσταση". The row gives every mark the same 125x52 cell, but a compact
   file fills it while a wide wordmark on a padded artboard hits the width cap first and its
   type lands at half the height. Measured as INK AREA in the row's own cell (coverage x cell,
   the same rasterisation as above), the strip runs 700-1550 for ten of the twelve, and then
   BSB 2614 and Passadena 2802 — both a clean 2x the median, both filling the full 52px of ink
   height against 21-28px of type either side of them. --os scales the cell so their ink area
   lands at 1550, the top of the cluster rather than its middle: they stay the largest marks
   in the row, which is honest, they just stop being twice everything else.
       bsb .77 -> 84x40, area 1550    passadena .74 -> 70x38, area 1535
   Inter Cars measures larger still (4591) and is deliberately NOT scaled: it is a knockout
   PLATE, its area is the block and not the lettering, and --lw already holds it at .36 with
   the other plate marks. Scaling it would shrink the wordmark inside a box that is already
   the row's agreed width cap.
   --os multiplies the cell in BOTH sizing rules, the 52/125 desktop cell and the 34/82 phone
   one, so the ratio survives the breakpoint. The --ox/--oy percentages resolve against the
   element's own box, so they follow the scale for free and need no restating. */
.brand-track img,
.post-media--logo img{translate:var(--ox,0) var(--oy,0)}
img[src*="/dna.svg"]{--ox:-3.6%;--oy:3%}
img[src*="vatopedi"]{--ox:5.3%;--oy:-6.9%}
img[src*="scangauge"]{--ox:1.4%}
img[src*="elmon"]{--ox:3.3%;--oy:1.1%}
img[src*="rtm-logo"]{--ox:-8.5%}
img[src*="shop-and-trade-logo"]{--ox:-8.2%;--oy:-17.6%}
img[src*="/scott.svg"]{--ox:7.4%}
img[src*="inter-cars"]{--ox:4%;--oy:1.5%}
img[src*="coffee-island"]{--oy:-3%}
img[src*="/bsb.svg"]{--ox:1.5%;--oy:-1.8%;--os:.77}
img[src*="passadena"]{--ox:-2%;--oy:6.7%;--os:.74}
img[src*="boulekos"]{--ox:-5.2%;--oy:3.1%}

/* Only marks that LEAD SOMEWHERE light up. Since 2026-08-24 five of the seven marks in the
   technology strip are links (channel pages for Google and Meta, stack pages for Anthropic,
   NVIDIA and AWS); Microsoft and OpenAI have no page behind them and stay bare. The generic
   .brand-track img:hover above recolours every mark regardless, which on a mixed strip is a
   promise the two bare ones cannot keep — so cancel it for direct-child images, which are
   exactly the unlinked ones (the linked marks sit inside an anchor). Scoped to .brands--tech
   so the all-unlinked customer and partner marquees on other pages keep their hover. */
.brands--tech .brand-set>img:hover{filter:grayscale(1);opacity:var(--lw,.55)}
html[data-theme="dark"] .brands--tech .brand-set>img:hover{filter:brightness(0) invert(1);opacity:calc(var(--lw,.55) + .05)}
/* Hit area. These anchors are bare boxes around the mark (no chip padding like .bmc carries),
   so the tap target IS the logo box: 52px tall on desktop, which clears the 44px minimum, but
   the same rule drops the mark to 34px at 560px and below and takes the target under it. Pad the
   small breakpoint back over the line, with the numbers the .bmc chips already use. */
@media(max-width:560px){.brands--tech .brand-set a{padding:8px 9px}}
/* Same arithmetic for the maker wall on partners.html, whose marks became links to
   brands.html on 2026-08-28 (.bml). Inert on customers.html, which has no anchors. */
@media(max-width:560px){.brands--makers .brand-set a{padding:8px 9px}}
@keyframes brand-scroll{to{transform:translateX(-50%)}}
/* ── marquee loop: a set must also be WIDER THAN THE VIEWPORT, not just equal to its twin ──
   translateX(-50%) over two identical sets shifts the track by exactly one set width, so set 2
   lands where set 1 began. That is necessary but NOT sufficient: the track only covers the window
   for the whole cycle if ONE set is at least as wide as the visible container. Once the shift
   approaches a set width, the track's right edge crosses into view and an empty strip grows before
   the animation snaps back.
   Home's channel strip never hits this at desktop sizes — 11 logos give a 2438-2561px set against a
   <=1920px window. Home's technology strip did: 7 logos give only 1187-1266px, so it broke by a
   measured 93px at 1280, 214px at 1440 and 655px at 1920 (owner report 2026-08-04, "it doesnt
   repeat proper").
   Phones looked fine for a plain geometric reason, NOT because the marquee is switched off there:
   the `.brand-set+.brand-set{display:none}` collapse below is scoped to prefers-reduced-motion and
   a11y-no-motion only, with NO width breakpoint, so a phone with motion enabled runs the marquee
   normally. At 390px one set is ~1084px, already wider than the window, so the loop was covered.
   FIX: four sets and a -25% shift, so the covering length is 3 sets (~3798px) instead of 1.
   Set count and shift must always agree: N sets need -(100/N)%, and gap-free needs (N-1) x set
   width >= viewport.
   Additive on purpose — a NEW keyframe rather than parameterising the existing one, so the channel
   and .brands--makers strips are provably untouched.
   Repeating logos INSIDE one set would have been wrong: both motion fallbacks below collapse to
   `.brand-set+.brand-set{display:none}`, so the first set is what phones and reduced-motion users
   actually see, and it must stay one clean pass of the unique logos. */
@keyframes brand-scroll-4{to{transform:translateX(-25%)}}
.brands--tech .brand-track{animation-name:brand-scroll-4}
html.a11y-no-motion .brand-marquee,html.a11y-no-motion .brand-track{overflow:visible;width:auto}
html.a11y-no-motion .brand-marquee::before,html.a11y-no-motion .brand-marquee::after{display:none}
html.a11y-no-motion .brand-track{animation:none;justify-content:center}
html.a11y-no-motion .brand-set{flex-wrap:wrap;justify-content:center;row-gap:24px;padding-right:0}
html.a11y-no-motion .brand-set+.brand-set{display:none}
@media(prefers-reduced-motion:reduce){
  .brand-marquee{overflow:visible}
  .brand-marquee::before,.brand-marquee::after{display:none}
  .brand-track{animation:none;width:auto;justify-content:center}
  .brand-set{flex-wrap:wrap;justify-content:center;row-gap:24px;padding-right:0}
  .brand-set+.brand-set{display:none}
}
/* home channel chips (.bmc) — each marquee logo sits in a borderless hit-area card that links to
   that channel's page. 2026-07-30, USER DIRECTION: the static 1px --hairline stroke was removed so
   the strip reads as a bare row of logos. 2026-08-03, USER DIRECTION: the red conic-sweep hover
   outline is gone too — it read as a selection box, and no other logo marquee carries an outline;
   hover is now the same grayscale→color reveal as every other strip (triggered from the whole
   padded hit area rather than the img alone), and keyboard focus falls through to the global
   a:focus-visible ink outline. Do not reinstate the sweep here.
   The aria-hidden duplicate set carries identical <span> chips so the translateX(-50%)
   loop stays seamless. .brands--makers (customers/partners) has no .bmc.
   Selector is .brand-set .bmc so it outweighs the bare-link .brand-set a rule above.

   2026-07-30, USER DIRECTION: the per-channel hero art was REMOVED from these chips. It used to
   sit behind each logo as a background-image washed to 85% paper, thinning to 55% on hover. The
   marquee reads as a quiet row of logos and the art was fighting the wordmarks at chip size;
   the channel photography belongs on channels/index.html, where each channel gets a real card
   with room for it. The `--chip-art` custom property and its 22 per-channel definitions are gone
   with it — do not reinstate them here without re-checking legibility of the white-artwork
   logos (walmart.svg is pure #ffffff, so any light art behind it erases the mark). */
.brand-set .bmc{position:relative;flex:none;display:flex;align-items:center;justify-content:center;padding:16px 26px;border-radius:10px;line-height:0}
/* On phones the wrapped strip fell to two logos per row, so eleven channel marks ate a whole
   viewport of scroll directly below the hero. Smaller optical size, tighter gaps and less chip
   padding fit three per row and give roughly half that screen back. Placed AFTER the .bmc rule
   above deliberately: it carries the same (0,2,0) specificity, so source order decides. */
@media(max-width:560px){
  /* the cap scales with the row: 125 x 34/52 keeps the same box proportion the desktop row has,
     otherwise the plate marks slip back under it and run wide again on a phone */
  .brand-track img{height:calc(34px * var(--os,1));max-width:calc(82px * var(--os,1))}
  .brand-set{gap:12px}
  .brand-set .bmc{padding:8px 9px}
}
@media(hover:hover) and (pointer:fine){
  .brand-set .bmc:hover img{filter:none;opacity:1}
  html[data-theme="dark"] .brand-set .bmc:hover img{filter:brightness(0) invert(1);opacity:1}
}
.brand-set .bmc:focus-visible img{filter:none;opacity:1}
html[data-theme="dark"] .brand-set .bmc:focus-visible img{filter:brightness(0) invert(1);opacity:1}
/* ————— Daily toolset (.tls-*) — home #toolset only —————
   A STATIC row, not a marquee. The homepage already carries two scrolling logo strips (channel
   chips above, technology partners below); a third would read as wallpaper rather than as a claim.
   HOUSE TREATMENT, on theme tokens throughout: bare marks straight on the paper surface, no plate
   and no stroke, grayscale at .55 → full colour on hover, and in dark theme the same
   brightness(0) invert(1) silhouette the marquee uses. This is the direction recorded for the .bmc
   channel chips at :1233 ("the static 1px --hairline stroke was removed so the strip reads as a
   bare row of logos") and the .brand-track rules at :1215 — a logo row on this site is bare
   marks on paper, not carded artwork.
   The first build plated each mark on a fixed-white card, which is how the reference screenshot
   this section came from handles it. It solved the dark-theme problem (several of these marks are
   near-black) but imported a surface language the site does not use anywhere except the footer
   compliance row. The invert filter solves the same problem the way the rest of the site already
   does. 2026-08-04, USER DIRECTION: use the house style.
   NO red conic sweep. These tiles are inert — no link, no focus target — and :1233 is explicit
   that a logo surface hovers with the grayscale→colour reveal, not the card sweep.
   All eleven sit on ONE row at desktop, left-aligned with the heading above. A 4+4 block capped at
   736px was tried first and read as a centred island floating under left-aligned type. */
.tls-grid{list-style:none;margin:clamp(26px,3.4vw,40px) 0 0;padding:0;
  display:grid;grid-template-columns:repeat(11,minmax(0,1fr));gap:clamp(10px,1.2vw,18px)}
/* 11 columns (2026-08-20, was 9): the row stays ONE line at desktop because the line is the
   object. The gap tightened with it — at the old clamp(12px,1.7vw,24px) an 11-column track left
   ~82px per cell, which clipped the widest mark (entersoft, 2.4:1) via max-width and shrank it
   optically below its neighbours. */
/* No fill, no border, no radius — the padding is a hover hit area only, same as .bmc. */
.tls-tile{display:flex;flex-direction:column;align-items:center;gap:12px;padding:10px 4px}
/* Fixed row height, not an aspect-ratio: it keeps the eleven marks on one optical baseline and
   stops the row jittering as images decode, without growing when the column does. */
.tls-mark{display:grid;place-items:center;width:100%;height:clamp(34px,3.2vw,44px)}
.tls-mark img{height:clamp(26px,2.6vw,36px);width:auto;max-width:100%;object-fit:contain;
  filter:grayscale(1);opacity:.55;transition:filter .25s var(--ease),opacity .25s var(--ease)}
/* dark mode: the marks are colour/near-black artwork on transparent, so they render as dim white
   silhouettes — identical to .brand-track img at :1218. Brand colour is deliberately given up in
   dark theme; legibility of the mark comes first, and this is the call the site already made. */
html[data-theme="dark"] .tls-mark img{filter:brightness(0) invert(1);opacity:.6}
.tls-name{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.1em;
  text-transform:uppercase;color:var(--ink-muted);text-align:center;line-height:1.3;
  transition:color .25s var(--ease)}
/* (A .tls-stand rule lived here while three marks had no artwork — a lettered stand-in drawn from
   data-letter via generated content, deliberately never a text node, because tools/build-md.py
   walks the DOM for the .md mirrors and llms.txt and a real text node published as
   "- E Entersoft ERP" in the machine-readable copy of the page. All eight tiles carry artwork as
   of 2026-08-04, so the rule is gone rather than shipping dead to every visitor. The recipe is in
   REQ-004 if a ninth tool ever arrives without a logo.) */
@media(hover:hover) and (pointer:fine){
  .tls-tile:hover .tls-mark img{filter:none;opacity:1}
  html[data-theme="dark"] .tls-tile:hover .tls-mark img{filter:brightness(0) invert(1);opacity:1}
  .tls-tile:hover .tls-name{color:var(--ink)}
}
/* The other half of that hover gate. .55 was tuned as a PRE-hover state — dimmed, one hover from
   full colour. On anything without hover it is the FINAL state, so a permanently dimmed mark is
   half a design. Raised to .78 there: still monochrome, but reading as a deliberately quiet mark
   rather than one waiting to be activated.
   Deliberately NOT switched to full colour on touch, 2026-08-04, and not scoped to a width either:
     · Family — the channel strip and the partner marquee are both grayscale and share this exact
       no-hover situation. A phone visitor scrolls past all three; only one of them going colour
       breaks the set.
     · Adobe's #FA0C00 is nearly TPL's brand red. On hover it appears alone and deliberately; in
       permanent colour it would sit next to our own red accents, which is what the red-as-sole-
       accent rule exists to prevent.
     · Eight brand palettes at once is a lot of chroma for the quietest section on the page.
   hover:none rather than a max-width, because touch tablets and laptops never get the reveal
   either — a width breakpoint would leave the same hole at 800px on an iPad. */
@media(hover:none){
  .tls-mark img{opacity:.78}
  html[data-theme="dark"] .tls-mark img{opacity:.8}
}
/* 8 divides as 8, 4+4 or 2+2+2+2 — never 3, which would leave a ragged 3+3+2, so the grid steps
   8 -> 4 -> 2. Below 1180 a tile stops clearing ~120px, which is where "VS CODE" starts wrapping
   under the mark; below 640 two columns is all that fits at a legible mark width. */
/* 3-up below desktop. Eleven tiles is prime, so the last row would leave one dead cell —
   the final tile spans the remainder instead, which matters most on mobile where the cells are
   compartments of a hairline matrix and an empty one reads as a mistake. */
@media(max-width:1180px){
  .tls-grid{grid-template-columns:repeat(3,minmax(0,1fr));max-width:660px}
  .tls-tile:last-child{grid-column:span 2}
}
/* MOBILE: the same eleven marks, but as a hairline matrix instead of a loose scatter.
   A bare row works at desktop because the row itself is the object — eleven marks on one line read
   as deliberate. Broken up on a phone that reading disappears and the groups look accidental,
   with the marks floating in a 360px block adrift in the wrap.
   This is the .abn construction (:1444) verbatim — gap:1px over a --hairline background paints the
   rules, cells sit on --paper-0, one outer hairline and --radius-card close it — so the matrix is
   one object with eleven compartments rather than eleven small cards, with the last tile
   double-width so the grid closes flush (see the :last-child rule above). Both tokens flip with the theme,
   so it needs no dark-mode rule of its own.
   The marks also go UP to 30px here: a 24px mark in a ~180px cell was the main reason the mobile
   layout read as unfinished. 2026-08-04, USER DIRECTION: keep the mobile grid, make it stylish. */
@media(max-width:640px){
  .tls-grid{grid-template-columns:repeat(3,minmax(0,1fr));max-width:none;
    gap:1px;background:var(--hairline);border:1px solid var(--hairline);
    border-radius:var(--radius-card);overflow:hidden}
  .tls-tile{background:var(--paper-0);padding:20px 8px 16px;gap:12px}
  .tls-mark{height:40px}
  .tls-mark img{height:30px}
}

/* the gateway lede sits directly on the branch-card grid; 40px is the gap the section was
   tuned to on 2026-09-03 (was an inline style on the page, moved here unchanged) */
#global-gateway .lede{margin:0 0 40px}
/* ————— Global gateway carrier row (.gg-carriers) — home #global-gateway only —————
   The carrier row IS .tls-grid above: same bare marks on paper, same grayscale → colour hover and
   dark-theme invert, no fill and no rules of its own. 2026-08-24 user direction, after a matrix
   version was tried and dropped: "dont use container make -them like the tools we use". So the
   only things overridden here are the track, the per-mark heights, and the link.
   Track: no width cap at any step, so the row shares its column with the three branch cards and
   the label above it. That alignment is the other half of the same direction - the row used to
   cap at 640px and stop 144px short of the cards (measured 1160.5 against 1304.5 at 1440), and
   capping at the base 660px below 1180 put it 153px short again at 900. The column counts
   themselves are the TRACK LADDER below, rebuilt 2026-08-31 so seven carriers close flush at
   every width instead of orphaning two of them on a second line.
   Phones keep the hairline matrix, because that is exactly what the toolset row does at ≤640 (the
   2026-08-04 "keep the mobile grid, make it stylish" direction). It comes from the base .tls-grid
   rules, which is why every override here that would fight it is scoped to min-width:641px. */
/* ===== TRACK LADDER — mobile-first, and every step closes flush.
   2026-08-31 USER DIRECTION: "make the delivery partners not break into 2 lines - I want a better
   grid system - always mobile first then desktop." The row was 5-up above 960, which put seven
   carriers on 5+2: a full line and then a two-mark orphan, the thing being complained about.
   Seven is prime, so no single column count divides it and any count picked for cell width alone
   leaves a ragged tail. The ladder therefore only uses counts that close, stated in ASCENDING
   width order (mobile first), each with the tail it needs:
     base    2-up, tile 7 spans the row   ->  2+2+2+2   small phones, ~140px of cell content
     >=480   3-up, tile 7 spans the row   ->  3+3+3
     >=641   4-up, no span                ->  4+3       bare row, so a plain tail is a row ending
     >=960   7-up                         ->  ONE LINE  the row is the object again
   960 is the step for the single line, and the binding measure is no longer DHL — normalised on
   optical area below, it is 81px wide at the clamp floor — but the longest NAME. A wrapped kicker
   is the same two-line fault one level down, and the longest is "BOX NOW · 2026" at 101px with the
   tracking below. Measured: a 7-up cell holds 108px of content at 960 and 101px at 900, where BOX
   NOW breaks in two. So 960 is the floor, one carrier's name away from it. */
.gg-carriers{margin-top:clamp(14px,1.6vw,20px);grid-template-columns:repeat(2,minmax(0,1fr))}
/* Full-width tail, not the base .tls-grid's span 2 (that closes ELEVEN tiles in 3 columns, not
   seven): at 2-up and at 3-up alike, spanning the whole row is what leaves no empty compartment
   in the drawn matrix. */
.gg-carriers .tls-tile:last-child{grid-column:1/-1}
@media(min-width:480px){.gg-carriers{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(min-width:641px){
  .gg-carriers{grid-template-columns:repeat(4,minmax(0,1fr));max-width:none}
  /* The span closes the phone matrix, where an empty compartment reads as a mistake because the
     rules are drawn. Above 640 nothing is drawn, so a spanning last cell would only centre BOX NOW
     under a left-aligned row and lose the first line's rhythm. Left in a plain cell with the
     fourth simply unused, 4+3 reads as a row ending. */
  .gg-carriers .tls-tile:last-child{grid-column:auto}
}
@media(min-width:960px){.gg-carriers{grid-template-columns:repeat(7,minmax(0,1fr))}}
/* Seven cells is the tightest this row has ever been, so the tracking eases from .1em to .06em in
   the 7-up band — it buys the ~8px that keeps the longest name on one line, without shrinking the
   type below the site's 11px mono floor. Deliberately NOT white-space:nowrap: if a future carrier
   arrives with a longer name it should wrap and be seen, not overflow its cell silently. */
@media(min-width:960px){.gg-carriers .tls-name{letter-spacing:.06em}}
/* PER-MARK HEIGHTS, not the row's single clamp — these five are the widest ratio spread on the
   site (7.07:1 and 3.68:1 and 3.72:1 wordmarks against a 0.84:1 and a 1.36:1 plate) and one
   shared height makes the row read as a mistake: at the .tls-mark clamp of 36px the DHL wordmark
   measured 167px wide in a 175px cell while the UPS shield sat at 30px beside it. Normalised on
   CAP HEIGHT instead, which is the ink height of the wordmarks: DHL, FedEx and ACS all match at
   22px (156 / 81 / 82px wide), and the two plates take the 34px a shield conventionally gets
   against a cap-height wordmark. Each SVG names its own class in its file header. */
.gg-carriers .gg-word img{height:clamp(16px,1.6vw,22px)}
.gg-carriers .gg-shield img{height:clamp(26px,2.6vw,34px)}
/* .gg-xw — DHL, and the one mark cap height cannot serve (2026-08-24 user direction: "fix the dhl
   logo size"). Its lockup is the word between two sets of flanking speed lines, 7.07:1 where the
   next widest wordmark is 3.72:1, so at the shared 22px cap it rendered 155px wide and read as
   the biggest thing in the row by a distance: measured on sqrt(w*h), the usual way to compare
   marks of different aspect, DHL was 139% of FedEx and ACS while BOX NOW sat at 94% and UPS 74%.
   So this one mark is normalised on optical AREA instead: 42.2 / sqrt(7.07) = 15.9px of height,
   which lands it at 113px wide and ~101% of the anchor. The trade is deliberate and visible up
   close — DHL's letters now sit a little shorter than FedEx's and ACS's — but the alternative is
   a row with one mark 39% larger than its neighbours, which is what reads as a mistake at a
   glance. Same 0.72 factor across the whole clamp so it holds at every width.
   Phones get a flat 13px rather than the scaled clamp: the matrix cell leaves 94px of content
   width there, and anything taller than 13.3px makes the lockup wider than that, at which point
   max-width silently scales the mark down instead of this rule setting it. */
.gg-carriers .gg-xw img{height:clamp(11.5px,1.15vw,16px)}
/* BOX NOW is the one planned lane, so its tile says so rather than passing as a live carrier (the
   hub card says "Planned 2026"). It rides inside .tls-name so it inherits the mono kicker and the
   hover colour shift, one step quieter than the carrier name it qualifies. A real text node, not
   generated content, because tools/build-md.py walks the DOM for the .md mirrors and llms.txt and
   the qualifier has to survive into the machine-readable copy of the page. */
.gg-soon{opacity:.62;letter-spacing:.06em}
/* ===== The whole tile is the link (2026-08-24 user direction: "the logos should be clicable too")
   The tile stops being the flex column and the LINK inside it becomes one, carrying the mark, the
   name and all of the padding — which is the point: the target went from ~30x13px of name text to
   the whole tile. .tls-tile's padding was only ever a hover hit area, so moving it onto the anchor
   changes nothing visually and makes that area clickable. display:block on the tile lets
   height:100% on the link fill the grid row, so the targets in a row are equal, not just the
   cells. Padding matches .tls-tile's own 10px 4px above 640 and the matrix cell's 20/8/16 below,
   so both layouts keep the spacing they had before the anchor existed. */
.gg-carriers .tls-tile{display:block;padding:0}
.gg-carriers .gg-link{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:12px;height:100%;box-sizing:border-box;padding:10px 4px;border-radius:10px;
  text-decoration:none;color:inherit}
@media(max-width:640px){.gg-carriers .gg-link{padding:20px 8px 16px}}
/* The name was an underlined link and is now plain text inside the tile link, so the underline
   comes back on hover as the affordance — gated with the hover query for the same reason the
   grayscale reveal is, and paired with :focus-visible so the keyboard gets it too. The ring is
   inset so it reads as a box around the tile rather than bleeding into the gap between tiles (and
   on phones, over the drawn matrix rule); it follows the radius set above. */
.gg-carriers .gg-link:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}
.gg-carriers .gg-link:focus-visible .tls-name{text-decoration:underline;text-underline-offset:3px}
/* :focus-visible gets the SAME reveal hover does — the site's parity convention (see the
   .dd-panel rows). Deliberately outside the hover:hover gate the grayscale rules sit behind: a
   keyboard exists on touch devices too, and gating parity on a pointer query would leave the
   focused tile as the only one that never lights up. */
.gg-carriers .gg-link:focus-visible .tls-mark img{filter:none;opacity:1}
html[data-theme="dark"] .gg-carriers .gg-link:focus-visible .tls-mark img{filter:brightness(0) invert(1);opacity:1}
/* BOX NOW took `filter:none` in all seven of these states until 2026-09-01, so it sat in full
   brand green while every mark beside it was a dim grey silhouette — the one tile in the row
   that did not behave like a tile. USER DIRECTION: "το BOX NOW είναι σε color από το default,
   δεν λειτουργεί όπως τα άλλα."
   The exemption was written against an older artwork and the note it carried had stopped being
   true. It said grayscale left a grey rectangle and brightness(0) invert(1) a solid white one,
   "a box with no logo left in it" — which is what happens when the letterforms are PAINTED.
   They are not, any more: rasterised at 4x, partners/carriers/boxnow.svg measures 63.3% opaque
   green (#44D62C) and 36.7% fully transparent, with no white anywhere in the file. The letters
   are real holes, so both of the row's filters keep them — grayscale gives a grey plate with
   the paper reading through the letters, brightness(0) invert(1) a light plate with the dark
   band reading through them. Verified by render in both themes.
   So the mark takes the row's own treatment now and there is no rule here to carry it. It is
   still the only PLATE in the row, and a plate reads heavier than a silhouette at the same
   opacity — if that ever needs answering, answer it with weight (the .36 density band the
   marquee gives its plate marks), never by exempting it back into colour. */
.gg-carriers .gg-link:focus-visible .tls-name{color:var(--ink)}
@media(hover:hover) and (pointer:fine){
  .gg-carriers .gg-link:hover .tls-name{text-decoration:underline;text-underline-offset:3px}
}
@media(max-width:640px){.gg-carriers{max-width:none}
  .gg-carriers .gg-word img{height:20px}
  .gg-carriers .gg-xw img{height:13px}
  .gg-carriers .gg-shield img{height:30px}}

/* ————— Channel hub card art (.ca) — channels/index.html only —————
   The 11 hub cards were type-only while every individual channel page already carried its own
   hero art, and the cards had ~120px of dead space between body copy and the CTA. This puts each
   channel's photography at the TOP of its card as a band, so the art shows at full strength
   instead of washed behind a wordmark (which is what was removed from the home marquee).
   Bleeds to the card edges by cancelling .mf-card's padding, with a hairline underneath so the
   art reads as a deliberate panel rather than a bleed accident.
   Source: channel-<slug>-card-{light,dark}.webp — a 640x240 pure downscale of the existing
   1920x720 hero pair (same 2.67:1, so no crop decisions and no subject loss), 3-8KB each.
   Theme-paired via --ca-art, the same mechanism .art-hero--<slug> uses. */
.ca{display:block;margin:calc(-1 * var(--ca-pad)) calc(-1 * var(--ca-pad)) 22px;
  aspect-ratio:640/240;border-radius:var(--radius-card) var(--radius-card) 0 0;
  border-bottom:1px solid var(--hairline);
  background:var(--ca-art) center/cover no-repeat,var(--paper-200)}
/* .mf-card's own padding, mirrored so the band can cancel it and bleed edge to edge */
.mf-grid .mf-card{--ca-pad:28px}
@media(max-width:640px){.mf-grid .mf-card{--ca-pad:22px}}
.ca--amazon{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-amazon-card-light.webp)}
html[data-theme="dark"] .ca--amazon{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-amazon-card-dark.webp)}
.ca--ebay{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-ebay-card-light.webp)}
html[data-theme="dark"] .ca--ebay{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-ebay-card-dark.webp)}
.ca--google{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-google-card-light.webp)}
html[data-theme="dark"] .ca--google{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-google-card-dark.webp)}
.ca--meta{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-meta-card-light.webp)}
html[data-theme="dark"] .ca--meta{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-meta-card-dark.webp)}
.ca--emag{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-emag-card-light.webp)}
html[data-theme="dark"] .ca--emag{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-emag-card-dark.webp)}
.ca--skroutz{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-skroutz-card-light.webp)}
html[data-theme="dark"] .ca--skroutz{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-skroutz-card-dark.webp)}
.ca--bestprice{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-bestprice-card-light.webp)}
html[data-theme="dark"] .ca--bestprice{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-bestprice-card-dark.webp)}
.ca--zalando{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-zalando-card-light.webp)}
html[data-theme="dark"] .ca--zalando{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-zalando-card-dark.webp)}
.ca--aboutyou{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-aboutyou-card-light.webp)}
html[data-theme="dark"] .ca--aboutyou{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-aboutyou-card-dark.webp)}
.ca--walmart{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-walmart-card-light.webp)}
html[data-theme="dark"] .ca--walmart{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-walmart-card-dark.webp)}
.ca--allegro{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-allegro-card-light.webp)}
html[data-theme="dark"] .ca--allegro{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-allegro-card-dark.webp)}
.ca--shopflix{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-shopflix-card-light.webp)}
html[data-theme="dark"] .ca--shopflix{--ca-art:url(https://img.tpl.one/websites/tplgr/v2/assets/images/heroes/channel-shopflix-card-dark.webp)}

/* ===== AI-native editorial pass (ai.html) — X-inspired: asymmetric grid, big stats, and a
   "wired-into-the-floor" partner schematic instead of a logo marquee. Every value derives
   from the TPL token system (paper/ink/red, Sofia display, JetBrains mono). ===== */

/* AI-native hero — head + interconnected operations graph
   NOTE: the graph stage is a deliberate always-dark, frosted-glass surface — a
   page-scoped exception to the SHARP no-glassmorphism rule (cf. models/ai-transformation
   body.ai-neural). Stage colours are theme-invariant literals on purpose: the panel is
   dark in both light and dark theme, so paper/ink tokens (which flip) must not be used. */
.ainx-top{display:flex;align-items:flex-end;justify-content:space-between;gap:clamp(20px,4vw,60px);flex-wrap:wrap}
.ainx-head{max-width:none}
.ainx-lede{margin:0;max-width:440px;flex:1 1 320px}
.ainx-cta{display:flex;gap:16px;flex-wrap:wrap;margin:clamp(22px,3vw,32px) 0 0}
@media(max-width:820px){.ainx-top{align-items:flex-start}.ainx-lede{max-width:none}}

/* AI-native hero graph — Obsidian-style force graph. Canvas paints the edges; the nodes
   are real DOM elements so focus, tab order and links stay native. Positions are written
   by tpl.js as a transform; the press-scale rides in --ns INSIDE that transform, so it
   can never replace the position the way a bare transform rule would.
   Spec: docs/superpowers/specs/2026-08-20-ai-native-obsidian-graph-design.md */
.ainx-graph{position:relative;margin-top:clamp(26px,4vw,44px);border-radius:var(--radius-card);
  background:#0B0B0C;aspect-ratio:1180/620;overflow:hidden;
  box-shadow:0 1px 0 rgba(255,255,255,.05) inset,0 24px 60px -30px rgba(0,0,0,.5)}
.ainx-graph .ainx-wires{position:absolute;inset:0;display:block}
.ainx-graph .ainx-nodes{position:absolute;inset:0}
/* HUD */
.ainx-hud{position:absolute;top:14px;left:16px;z-index:5;pointer-events:none;display:flex;flex-direction:column;gap:2px;
  font-family:var(--font-mono);font-size:10.5px;letter-spacing:.12em;line-height:1.7;color:#8A8A86}
.ainx-hud b{color:#F2F1ED;font-weight:500}
.ainx-hud-live b{color:#E23A32}

/* ---- nodes: a coloured dot, sized by link count, positioned by JS ---- */
.gnode{position:absolute;left:0;top:0;background:none;border:0;padding:0;margin:0;
  cursor:pointer;text-decoration:none;line-height:0;
  /* the dot is 3-14px, far under any pointer's aim; --hit pads each one out to the
     WCAG 2.5.8 24px floor without changing what is drawn */
  touch-action:none;-webkit-tap-highlight-color:transparent}
.gnode::before{content:'';position:absolute;left:50%;top:50%;width:calc(var(--r)*2 + var(--hit)*2);
  height:calc(var(--r)*2 + var(--hit)*2);transform:translate(-50%,-50%);border-radius:50%}
.gnode .dot{display:block;width:calc(var(--r)*2);height:calc(var(--r)*2);border-radius:50%;
  background:var(--nc);transition:background .18s var(--ease),box-shadow .18s var(--ease)}
.gnode.gn-hub .dot{box-shadow:0 0 0 1px rgba(226,58,50,.55),0 0 26px rgba(226,58,50,.45)}
.gnode.gn-eng .dot{box-shadow:0 0 0 1px rgba(255,255,255,.18)}

/* ---- labels: 11 permanent (hub + engines + categories), the rest on hover/focus ---- */
.gnode .lb{position:absolute;left:50%;top:calc(50% + var(--r) + 7px);transform:translateX(-50%);
  font-family:var(--font-mono);font-size:9.5px;letter-spacing:.11em;line-height:1.3;white-space:nowrap;
  color:#8A8A86;pointer-events:none;opacity:0;transition:opacity .18s var(--ease),color .18s var(--ease);
  text-shadow:0 0 5px #0B0B0C,0 0 5px #0B0B0C,0 0 9px #0B0B0C}
.gnode.gn-lbl .lb{opacity:1;color:#B4B3AE}
.gnode.gn-hub .lb{font-size:10.5px;font-weight:700;letter-spacing:.13em;color:#F2F1ED;top:calc(50% + var(--r) + 9px)}
.gnode.gn-eng .lb{font-size:10px;font-weight:600;color:#E7E6E2}
.gnode:hover .lb,.gnode:focus-visible .lb,.gnode.is-open .lb{opacity:1;color:#fff}
/* a node close to a wall anchors its label inward instead of centring it, or a long name
   on an edge node runs off the stage (JS sets gn-l/gn-r from the settled position) */
.gnode.gn-l .lb{left:calc(50% - var(--r));transform:none}
.gnode.gn-r .lb{left:auto;right:calc(50% - var(--r));transform:none}

/* ---- frosted info card. Only ever one is visible, so the blur cost is bounded ---- */
.gnode .ginfo{position:absolute;left:50%;top:calc(50% + var(--r) + 10px);transform:translateX(-50%) translateY(-4px);
  width:214px;padding:10px 12px;border-radius:10px;background:rgba(18,18,20,.72);
  backdrop-filter:blur(16px) saturate(1.3);-webkit-backdrop-filter:blur(16px) saturate(1.3);
  border:1px solid rgba(226,58,50,.55);color:#E7E6E2;font-family:var(--font-body);
  font-size:12px;line-height:1.42;text-align:left;white-space:normal;
  opacity:0;pointer-events:none;transition:opacity .18s var(--ease),transform .18s var(--ease);
  z-index:8;box-shadow:0 8px 30px rgba(0,0,0,.5)}
.gnode .ginfo b{display:block;font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;color:#E05252;margin-bottom:4px}
.gnode:hover .ginfo,.gnode:focus-visible .ginfo,.gnode.is-open .ginfo{opacity:1;transform:translateX(-50%) translateY(0)}
/* keep the card on the stage near the edges — JS sets gn-up / gn-l / gn-r from position */
.gnode.gn-up .ginfo{top:auto;bottom:calc(50% + var(--r) + 10px)}
.gnode.gn-l .ginfo{left:calc(50% - var(--r));transform:translateX(0) translateY(-4px)}
.gnode.gn-l:hover .ginfo,.gnode.gn-l:focus-visible .ginfo,.gnode.gn-l.is-open .ginfo{transform:translateX(0) translateY(0)}
.gnode.gn-r .ginfo{left:auto;right:calc(50% - var(--r));transform:translateX(0) translateY(-4px)}
.gnode.gn-r:hover .ginfo,.gnode.gn-r:focus-visible .ginfo,.gnode.gn-r.is-open .ginfo{transform:translateX(0) translateY(0)}

/* ---- highlight: the hovered node and its neighbourhood stay lit, the rest recedes ---- */
.gnode{z-index:2}
.gnode.gn-cat,.gnode.gn-eng{z-index:3}
.gnode.gn-hub{z-index:4}
.gnode:hover,.gnode:focus-visible,.gnode.is-open{z-index:9}
.ainx-graph.is-cold .gnode{opacity:.22;transition:opacity .2s var(--ease)}
.ainx-graph.is-cold .gnode.is-hot{opacity:1}
.ainx-graph.is-cold .gnode.is-hot .dot{box-shadow:0 0 0 1px rgba(226,58,50,.5),0 0 14px rgba(226,58,50,.35)}
.ainx-graph.is-cold .gnode.is-hot .lb{opacity:1;color:#E7E6E2}
.gnode:hover .dot,.gnode:focus-visible .dot,.gnode.is-open .dot{background:#E23A32;box-shadow:0 0 0 1px rgba(226,58,50,.6),0 0 20px rgba(226,58,50,.55)}
.gnode:focus-visible{outline:none}
.gnode:focus-visible .dot{outline:2px solid #fff;outline-offset:3px}
/* the shared :active press-scale is a bare transform and would replace the position
   written by JS; --ns rides inside that same transform instead. */
.gnode:active{--ns:var(--press,.97)}

/* ---- fallback / no-JS + screen-reader equivalent ---- */
/* DEAD SINCE 2026-08-28, WITH EVERYTHING ABOVE IT. ai.html's hero graph was replaced by the
   company-map star chart (.atl-stage--ainx in atlas.css), so nothing on the site renders
   .ainx-graph, .gnode, .ainx-hud or .ainx-fallback any more, and tpl.js's Graph now returns
   at its own `if (!stage) return`. Kept, not deleted, so the old graph can come back without
   an archaeology trip. The list those rules used to hide is still on the page — as content
   rather than as a fallback: see .ainx-register below. */
.ainx-fallback{position:absolute;inset:0;overflow:auto;padding:clamp(22px,4vw,40px);color:#E7E6E2;font-family:var(--font-body)}
.ainx-fallback .ainx-fb-hub{margin:0 0 16px;font-size:15px;line-height:1.5}
.ainx-fallback b{color:#fff}
.ainx-fallback ul{list-style:none;margin:0;padding:0;display:grid;gap:12px}
.ainx-fallback li{font-size:13.5px;line-height:1.5;padding-left:14px;border-left:2px solid rgba(226,58,50,.6)}
.ainx-graph.is-live .ainx-fallback{display:none}

/* ---- .ainx-register: the star chart's text twin, in flow under the plate ----
   This is the list .ainx-fallback used to hold, promoted from a hidden fallback to the
   section's content (2026-08-28). It had to be promoted: the plate that replaced the DOM
   graph is a canvas, so it carries no text a crawler or a screen reader can reach and no tab
   stops — exactly the gap company-map.html has and cannot close. The ~15 links in here are
   also ai.html's only route to the brand, storefront and channel pages. Own block rather
   than a modifier on .ainx-fallback: that name is a lie now, and those rules are dead. */
.ainx-register{margin:clamp(24px,3vw,36px) 0 0;color:#E7E6E2;font-family:var(--font-body)}
.ainx-register .ainx-fb-hub{margin:0 0 18px;font-size:15px;line-height:1.55;max-width:76ch}
.ainx-register b{color:#fff}
/* two or three columns as the measure allows: eight bullets in one column ran the hero
   another 300px down the page, and every one of them is a self-contained line. */
.ainx-register ul{list-style:none;margin:0;padding:0;display:grid;gap:12px 28px;
  grid-template-columns:repeat(auto-fit,minmax(290px,1fr))}
.ainx-register li{font-size:13.5px;line-height:1.5;padding-left:14px;border-left:2px solid rgba(226,58,50,.6)}
/* links keep the inherited colour and the browser's underline, as they did inside the
   fallback — the underline is what carries them, so WCAG 1.4.1 does not depend on hue. */
/* tablet: a squarer stage so the blob keeps breathing room. Cosmetic only — it does not
   change the model, so it stays off the breakpoint contract deliberately. */
@media(max-width:900px){.ainx-graph{aspect-ratio:1/1}}
/* mobile (<=720px, breakpoint contract): portrait stage and the thinned ~48-node set.
   Must match tpl.js's `thin`/`mq` media queries, which drop every node without m:1 in
   the register at the same width; this block must also stay AFTER the 900px rule to win
   the aspect-ratio at <=720. */
@media(max-width:720px){
  .ainx-graph{aspect-ratio:320/560}
  .ainx-hud{display:none}
  .gnode .lb{font-size:9px}
  .gnode .ginfo{width:180px;font-size:11.5px}
}
@media(prefers-reduced-motion:reduce){.gnode .dot,.gnode .lb,.gnode .ginfo,.ainx-graph.is-cold .gnode{transition:none}}
html.a11y-no-motion .gnode .dot,html.a11y-no-motion .gnode .lb,html.a11y-no-motion .gnode .ginfo,
html.a11y-no-motion .ainx-graph.is-cold .gnode{transition:none}

/* big-stat band — TPL's real numbers in the X "business impact" treatment (mirrors .kpi-band) */
.abn{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--hairline);border:1px solid var(--hairline);border-radius:var(--radius-card);overflow:hidden}
.abn-cell{background:var(--paper-0);padding:clamp(22px,2.6vw,36px)}
.abn-num{display:block;font-family:var(--font-display);font-weight:800;letter-spacing:-.02em;line-height:.98;font-size:clamp(38px,4.6vw,62px);color:var(--ink)}
.abn-num i{font-style:normal;color:var(--red-ink)}
.abn-lbl{display:block;margin-top:12px;font-family:var(--font-mono);font-size:12px;letter-spacing:.03em;text-transform:uppercase;color:var(--ink-muted);line-height:1.45}
@media(max-width:720px){.abn{grid-template-columns:1fr 1fr}}

/* stack bento — the 26→100 dot-grid card is the hero tile (tall, left); two stack right */
.stack-bento{display:grid;grid-template-columns:1.12fr 1fr;grid-auto-rows:1fr;gap:20px}
.stack-bento>.reveal{display:flex}
.stack-bento>.reveal>.card{width:100%}
.stack-bento>.reveal:first-child{grid-row:1/3}
@media(max-width:820px){.stack-bento{grid-template-columns:1fr}.stack-bento>.reveal:first-child{grid-row:auto}}

/* ---- "wired into the floor" partner schematic — named partners as nodes on hairline rails
   converging into the operation hub; small red pulses travel the rails inward ---- */
.wire-svg{display:block;width:100%;height:auto;overflow:visible}
.wire-rail{fill:none;stroke:var(--hairline);stroke-width:1.5}
.wire-arrow{fill:none;stroke:var(--red);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.wire-node-dot{fill:var(--paper-0);stroke:var(--red);stroke-width:2}
.wire-name{font-family:var(--font-display);font-weight:800;letter-spacing:-.01em;font-size:30px;fill:var(--ink)}
.wire-hub{fill:var(--paper-0);stroke:var(--hairline);stroke-width:1.5}
.wire-hub-edge{fill:var(--red)}
.wire-hub-t{font-family:var(--font-display);font-weight:800;letter-spacing:-.01em;font-size:26px;fill:var(--ink)}
.wire-hub-s{font-family:var(--font-mono);font-size:13px;letter-spacing:.02em;fill:var(--ink-muted)}
.wire-pulse{fill:var(--red);transform-box:view-box;animation:wire-flow 2.6s var(--ease) infinite}
@keyframes wire-flow{0%{transform:translateX(0);opacity:0}12%{opacity:1}82%{opacity:1}100%{transform:translateX(var(--travel,0));opacity:0}}
@media(prefers-reduced-motion:reduce){.wire-pulse{display:none}}
html.a11y-no-motion .wire-pulse{display:none}

/* registries — a quiet grounding rail beneath the schematic */
.wire-cap{max-width:840px;margin:clamp(22px,2.8vw,32px) 0 0;color:var(--ink-muted);font-size:15.5px;line-height:1.65}
.wire-cap strong{color:var(--ink);font-weight:600}
.wire-reg{margin:22px 0 0;padding-top:20px;border-top:1px solid var(--hairline);font-family:var(--font-mono);font-size:13px;letter-spacing:.02em;color:var(--ink-muted);display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 14px}
.wire-reg b{color:var(--red-ink);font-weight:600;text-transform:uppercase;letter-spacing:.1em;font-size:11.5px;margin-right:4px}

/* mobile: swap the horizontal schematic for a vertical wired stack */
.wire-stack{display:none}
@media(max-width:720px){
  .wire-svg{display:none}
  .wire-stack{display:flex;flex-direction:column;position:relative;margin:8px 0 0;padding-left:26px}
  .wire-stack::before{content:"";position:absolute;left:5px;top:16px;bottom:70px;width:1.5px;background:var(--hairline)}
  .wsrow{position:relative;padding:11px 0}
  .wsrow::before{content:"";position:absolute;left:-21px;top:16px;width:11px;height:11px;border-radius:50%;background:var(--paper-0);border:2px solid var(--red);box-sizing:border-box}
  .wsrow b{display:block;font-family:var(--font-display);font-weight:800;font-size:23px;letter-spacing:-.01em;color:var(--ink)}
  .wsrow span{font-family:var(--font-mono);font-size:12px;color:var(--ink-muted)}
  .wshub{margin-top:14px;padding:18px 20px;border:1.5px solid var(--hairline);border-left:3px solid var(--red);border-radius:var(--radius-card);background:var(--paper-0)}
  .wshub b{display:block;font-family:var(--font-display);font-weight:800;font-size:23px;color:var(--ink)}
  .wshub span{font-family:var(--font-mono);font-size:12px;color:var(--ink-muted)}
}
.kpi-band{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1px;background:var(--hairline)}
.kpi{background:var(--paper-100);padding:28px 24px}
.kpi .num{font-size:32px;color:var(--ink);display:block}
.kpi small{color:var(--ink-muted);font-size:13px}
/* below one-row widths, auto-fit orphans the last KPI of an odd-count band beside empty
   hairline cells; pin explicit columns instead and let a lone last item span the full row.
   Phones: 2-up. Tablets: 4-up, EXCEPT the counts that do not divide into it -- a 6-KPI band
   drops to 3-up so it pairs 3×2 rather than 4+2, and a 3-KPI band (every case study, plus
   brands.html) drops to 3-up so its single row is full.
   The lone-last-item span is what fills an odd tail, but it is wrong for a band whose column
   count already equals its item count: applied to the 3-up case it pushed the third KPI onto
   a row of its own and left a two-column hole of bare hairline where the paper should be.
   Hence the :not() -- the span rule is for tails, not for exact fits. */
@media(max-width:720px){
  .kpi-band{grid-template-columns:1fr 1fr}
  .kpi:last-child:nth-child(odd){grid-column:1/-1}
}
@media(min-width:721px) and (max-width:959px){
  .kpi-band{grid-template-columns:repeat(4,1fr)}
  .kpi-band:has(.kpi:first-child:nth-last-child(3)),
  .kpi-band:has(.kpi:first-child:nth-last-child(6)){grid-template-columns:repeat(3,1fr)}
  .kpi-band:not(:has(.kpi:first-child:nth-last-child(3))) .kpi:last-child:nth-child(odd){grid-column:1/-1}
}
/* the inverse band is theme-invariant, so its TEXT colours must be too — the paper-*
   tokens flip near-black in dark theme and would vanish on the fixed dark ground
   (this is why stray inline rgba-whites kept appearing in band markup; fixed here). */
.band-inverse{background:var(--band);color:#EBE9E3}
.band-inverse h2{color:#fff}
.band-inverse .eyebrow{color:#EBE9E3} /* --red on --inverse is 2.56:1, below AA */
.band-inverse .lede{color:#C5C3BB}
.band-inverse .biglink:hover{color:var(--red-ink-inverse)}
.band-inverse .card{background:#17171A;border-color:#2a2a2e}
/* .kicker's --ink-muted (#6B6C6C) measured 3.40:1 on this card (audit H1, 2026-07-30). Fixed
   literal, not a paper token, for the same reason .band-inverse .lede above uses one: paper
   tokens flip near-black in dark theme and would vanish on this theme-invariant surface.
   #9A9994 = 6.27:1, and stays quieter than the #C5C3BB body copy so hierarchy holds. */
.band-inverse .card .kicker{color:#9A9994}
.band-inverse .card h3{color:#fff} /* raw white by convention */
.band-inverse .card p{color:#C5C3BB}
/* .bcat is not .card, so the band's card palette above does not reach it. Without these the
   register card keeps background:var(--paper-0), which is white in light theme (fine, if loud)
   but #1A1A1C in dark — a near-black card on a #020203 band, i.e. invisible. Same values as
   .band-inverse .card so the two read as one family rather than two dark greys. */
.band-inverse .bcat{background:#17171A;border-color:#2a2a2e}
.band-inverse .bcat-h{border-bottom-color:#2a2a2e}
.band-inverse .bcat-h h3{color:#fff}
.band-inverse .bcat-n{color:#9A9994}
.band-inverse .bcat-note{color:#C5C3BB}
.band-inverse .bcat-list li{border-color:#2a2a2e;color:#EBE9E3}
.band-inverse .tile-icon{color:#fff} /* currentColor SVGs — --ink is near-black in light theme, invisible on the fixed-dark band */
/* .mf-card on an inverse band (contact's Promise) — same numbered/hover-reveal system as
   home's Discipline section, re-skinned dark; --red-ink-inverse is the theme-invariant
   red already established for text on this fixed-dark band (see the hero convention) */
.band-inverse .mf-card{background:#17171A;border-color:#2a2a2e}
@media(hover:hover) and (pointer:fine){.band-inverse .mf-card:hover{border-color:rgba(255,255,255,.4)}}
.band-inverse .mf-card:focus-within{border-color:rgba(255,255,255,.4)}
.band-inverse .mf-title{color:#fff}
.band-inverse .mf-body{color:#C5C3BB}
/* .4 composited to 3.80:1 on --inverse; .mf-idx carries real content (01/02/03) so it needs AA.
   .mf-more is aria-hidden but tracks it so the two muted marks on a card stay matched. */
.band-inverse .mf-idx{color:rgba(255,255,255,.52)}
.band-inverse .mf-ico{color:#fff}
.band-inverse .mf-ico path{stroke:#9a9a9a}
@media(hover:hover) and (pointer:fine){.band-inverse .mf-card:hover .mf-ico path{stroke:var(--red-ink-inverse)}}
.band-inverse .mf-card:focus-within .mf-ico path{stroke:var(--red-ink-inverse)}
.band-inverse .mf-more{color:rgba(255,255,255,.52)}
@media(hover:hover) and (pointer:fine){.band-inverse .mf-card:hover .mf-more{color:var(--red-ink-inverse)}}
.band-inverse .mf-card:focus-within .mf-more{color:var(--red-ink-inverse)}
/* red band — brand-primary ground; layered on .band-inverse, which keeps the white
   person-card treatment and focus outlines.
   SANCTIONED red-surface exception (2026-07-29). The 5%-red rule governs ACCENTS — a red fill
   this size would breach it read literally, so it is recorded here as deliberate rather than
   left to be re-flagged: one section, one page (values.html's company-name moment). Its sibling
   exception is the sitewide red footer (see `footer{background:var(--red)}` below), the
   brand bookend at the foot of all 35 pages. Everything else stays red-as-accent-only. */
.band-red{background:var(--red)}

/* recognition — award ledger: year | award | logo rows; the red line and full-colour
   logo surface on hover (decorative only — every fact is in the row text) */
.aw-tally{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--red-ink,#ab0000);margin:0 0 40px}
.aw-list{list-style:none;margin:0;padding:0;border-top:1px solid var(--hairline)}
.aw-row{position:relative;display:grid;grid-template-columns:96px minmax(0,1fr) auto;align-items:center;gap:clamp(16px,3vw,40px);padding:26px clamp(12px,2vw,24px) 26px clamp(14px,2vw,26px);border-bottom:1px solid var(--hairline);transition:background 240ms var(--ease)}
.aw-row::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--red);transform:scaleY(0);transform-origin:bottom;transition:transform 260ms var(--ease)}
.aw-year{font-family:var(--font-mono);font-size:13px;font-weight:500;letter-spacing:.08em;color:var(--ink-muted)}
.aw-name{font-family:var(--font-display);font-weight:800;font-size:clamp(22px,2.6vw,34px);letter-spacing:-.015em;line-height:1.1;margin:0;color:var(--ink)}
.aw-what{margin:6px 0 0;font-size:15px;color:var(--ink-soft)}
.aw-win{font-style:normal;font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--red-ink,#ab0000)}
/* .55 washed the award marks out to the point of illegibility on the light ledger rows, which
   defeats the purpose: these are third-party proof, not texture. .82 keeps them subordinate to the
   award name while staying readable. */
.aw-logo{height:52px;width:132px;object-fit:contain;justify-self:end;opacity:.82;filter:grayscale(1);transition:opacity 240ms var(--ease),filter 240ms var(--ease)}
@media(hover:hover) and (pointer:fine){
  .aw-row:hover{background:var(--paper-200)}
  .aw-row:hover::before{transform:scaleY(1);transform-origin:top}
  .aw-row:hover .aw-logo{opacity:1;filter:none}
}
.aw-member{display:flex;align-items:center;gap:clamp(24px,4vw,48px);flex-wrap:wrap;padding-top:40px}
.aw-member-label{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}
.aw-member img{height:40px;width:auto;opacity:.75}
@media(max-width:720px){
  .aw-row{grid-template-columns:minmax(0,1fr) auto;row-gap:4px;padding-left:0;padding-right:0}
  .aw-year{grid-row:1;grid-column:1}
  .aw-main{grid-row:2;grid-column:1}
  .aw-logo{grid-row:1 / 3;grid-column:2;height:42px;width:96px}
  .aw-row::before{display:none}
  .aw-member img{height:32px}
}
html.a11y-no-motion .aw-row,html.a11y-no-motion .aw-row::before,html.a11y-no-motion .aw-logo{transition:none}
@media(prefers-reduced-motion:reduce){.aw-row,.aw-row::before,.aw-logo{transition:none}}

/* Awards hover FX — on every awards ledger (section.aw-fx: home, customers, investors). Mirrors
   the cross-docking .flow-rows hover: a full-bleed white band + soft drop shadow lifts behind the
   row, the award name goes red, and a red /shapes mask blooms — here it sits LEFT, behind the
   YEAR (not the logos), with a different shape per row. */
.aw-fx{overflow-x:clip}
/* repurpose the row ::before (was the left red bar) into the full-bleed white band */
.aw-fx .aw-row::before{left:50%;top:0;bottom:0;width:100vw;margin-left:-50vw;transform:none;
  background:var(--paper-0);box-shadow:0 20px 44px -24px rgba(0,0,0,.30);opacity:0;
  transition:opacity .3s var(--ease);z-index:0}
.aw-fx .aw-year,.aw-fx .aw-main,.aw-fx .aw-logo{position:relative;z-index:1}
/* red shape bloom, anchored behind the year on the left */
/* justify-self:start shrinks the year cell to the text so the shape centres on the TYPE, not the 96px track */
.aw-fx .aw-year{justify-self:start}
/* varied shape per row (from /assets/shapes) so the bloom isn't the same badge every time */
.aw-fx .aw-row:nth-child(1) .aw-year{--shape:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTk3IiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDE5NyAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xOTYuNzcxIDc5LjM2NjFWNjUuMDk5OUwxOTYuNzQ5IDY1LjEwODhMMTk2Ljc0IDM1LjkxNkMxOTYuNzQgMzIuMTg4NyAxOTYuNzQgMjcuMDg3NyAxODkuNzczIDIzLjA5MDFMMTg5LjcyIDIzLjA1OTFMMTQ5LjIxMyAxLjYzOTc5QzE0Ny4yODkgMC41NjcyNzkgMTQ1LjEwNCAwIDE0Mi45MDIgMEMxNDAuNjk5IDAgMTM4LjQ1NiAwLjU4NTAwOCAxMzYuNTExIDEuNjg0MTFMMTI1LjY3IDcuODI2N0wxMjUuNTY4IDcuNzczNTJMOTguMDU5OCAyMi41NTM4TDk4LjAwMjIgMjIuNTg0OUM5Ny44ODI1IDIyLjY1NTggOTcuNzgwNiAyMi43NDg4IDk3LjY2MDkgMjIuODE5OEw1Ny41OTY3IDEuNjM5NzlDNTUuNjczMyAwLjU2NzI3OSA1My40ODg0IDAgNTEuMjg1NyAwQzQ5LjA4MzEgMCA0Ni44NDA1IDAuNTg1MDA4IDQ0Ljg5NSAxLjY4NDExTDM0LjA1NDYgNy44MjY3TDMzLjk1MjYgNy43NzM1Mkw2LjQ0Mzk2IDIyLjU1MzhMNi4zODYzNCAyMi41ODQ5QzQuNDE4NTggMjMuNzM3MiAyLjc4MzIyIDI1LjUyMzIgMS42NDg2NiAyNy43NDhDMC41NjcyODEgMjkuODY2NCAwIDMyLjI4NjIgMCAzNC43NTA0TDAuMDM1NDU1OSA4MC44OTUxQzAuMDM1NDU1OSA4NC4yNDU2IDEuODEyNjQgODcuNDA1NiA0LjY4MDA3IDg5LjE0MjhMNC43NTA5OCA4OS4xODcyTDE4LjkyNDEgOTYuODk4NlY5Ni44Mjc3TDIzLjk1ODcgOTkuNDU1OEw2LjQ0Mzk2IDEwOC44NjVMNi4zODYzNCAxMDguODk2QzQuNDE4NTggMTEwLjA0OCAyLjc4MzIyIDExMS44MzQgMS42NDg2NiAxMTQuMDU5QzAuNTY3MjgxIDExNi4xNzcgMCAxMTguNTk3IDAgMTIxLjA2MUwwLjAzNTQ1NTkgMTY3LjIwNkMwLjAzNTQ1NTkgMTcwLjU1NiAxLjgxMjY0IDE3My43MTYgNC42ODAwNyAxNzUuNDU0TDQuNzUwOTggMTc1LjQ5OEwxOC45MjQxIDE4My4yMVYxODMuMTM5TDQ3LjYwMjggMTk4LjExNEM1MC44MjkyIDE5OS43OTggNTQuNjgwNSAxOTkuNzc2IDU3Ljg4OTIgMTk4LjA1Mkw5OC4xMDQxIDE3Ni40NDJMMTEwLjU0IDE4My4yMDVWMTgzLjEzNEwxMzkuMjE5IDE5OC4xMUMxNDIuNDQ1IDE5OS43OTQgMTQ2LjI5NiAxOTkuNzcxIDE0OS41MDUgMTk4LjA0N0wxOTAuNDA3IDE3Ni4wN0MxOTQuMzk2IDE3My45NTEgMTk2Ljc4IDE3MC4wNjUgMTk2Ljc4IDE2NS42NzNWMTUxLjQwNkwxOTYuNzU4IDE1MS40MTVMMTk2Ljc0OSAxMjIuMjIyQzE5Ni43NDkgMTE4LjQ5NSAxOTYuNzQ5IDExMy4zOTQgMTg5Ljc4MiAxMDkuMzk3TDE4OS43MjkgMTA5LjM2NkwxNzEuNjc4IDk5LjgyMzdMMTkwLjQwNyA4OS43NTg5QzE5NC4zOTYgODcuNjQwNCAxOTYuNzggODMuNzUzNyAxOTYuNzggNzkuMzYxN0wxOTYuNzcxIDc5LjM2NjFaIiBmaWxsPSIjNzI4ODk5Ii8+Cjwvc3ZnPgo=)}
.aw-fx .aw-row:nth-child(2) .aw-year{--shape:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjE5NyIgdmlld0JveD0iMCAwIDE4NCAxOTciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xODMuMDE5IDExMy4zMjNMMTgzLjAwMSA2Mi41MTE3QzE4My4wMDEgNTYuMDI3OCAxODIuOTk3IDQ3LjE0NjMgMTcwLjg3NiA0MC4xODgzTDE3MC43ODIgNDAuMTM1MUwxMDAuMjY3IDIuODU0MTNDOTYuOTE2NCAwLjk4ODMwOSA5My4xMTgyIDAgODkuMjgwMiAwQzg1LjQ0MjIgMCA4MS41NDIyIDEuMDE0OSA3OC4xNTYyIDIuOTMzOUw1OS4yODUzIDEzLjYyMzZMNTkuMTAzNSAxMy41MjYxTDExLjIxNzEgMzkuMjU3NkwxMS4xMTk2IDM5LjMxNTJDNy42OTgxNyA0MS4zMjI5IDQuODQ0MDQgNDQuNDI5NiAyLjg3MTg2IDQ4LjI5ODZDMC45OTI3MzkgNTEuOTg2IDAgNTYuMTk2MiAwIDYwLjQ4NjNMMC4wNjY0NzU1IDE0MC44MUMwLjA2NjQ3NTUgMTQ2LjYzOCAzLjE2NDM2IDE1Mi4xMzggOC4xNTAyMyAxNTUuMTY1TDguMjc0MzIgMTU1LjI0TDMyLjk0NjYgMTY4LjY2VjE2OC41NEw4Mi44NjczIDE5NC42MDhDODguNDgyNSAxOTcuNTQyIDk1LjE4NzkgMTk3LjUwMiAxMDAuNzY4IDE5NC41MDJMMTcxLjk2NiAxNTYuMjQ2QzE3OC45MTEgMTUyLjU1OSAxODMuMDU0IDE0NS43OTEgMTgzLjA1NCAxMzguMTQ2VjExMy4zMUwxODMuMDE5IDExMy4zMjhWMTEzLjMyM1oiIGZpbGw9IiM3Mjg4OTkiLz4KPC9zdmc+Cg==)}
.aw-fx .aw-row:nth-child(3) .aw-year{--shape:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTk3IiBoZWlnaHQ9IjIwNSIgdmlld0JveD0iMCAwIDE5NyAyMDUiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xOTYuNzggNTAuMTg2NlY0MS4xNjMzTDE5Ni43NjcgNDEuMTcyMlYyMi43MDlDMTk2Ljc1OCAyMC4zNTEyIDE5Ni43NTggMTcuMTI0OCAxOTIuMzU3IDE0LjU5ODZMMTkyLjMyNiAxNC41ODA5TDE2Ni43MDUgMS4wMzcwNkMxNjUuNDg2IDAuMzU4OTgyIDE2NC4xMDggMCAxNjIuNzEyIDBDMTYxLjMxNiAwIDE1OS45MDIgMC4zNjc4NDcgMTU4LjY3IDEuMDYzNjVMMTUxLjgxNCA0Ljk0NTk4TDE1MS43NDggNC45MTA1MkwxMzQuMzQ4IDE0LjI1NzRMMTM0LjMxMyAxNC4yNzk1QzEzMy4wNjcgMTUuMDA2NCAxMzIuMDMgMTYuMTM2NSAxMzEuMzE3IDE3LjU0NThDMTMxLjA2OCAxOC4wMjg5IDEzMC44NjUgMTguNTM4NiAxMzAuNzA1IDE5LjA2NkMxMzAuMjA0IDE3LjUzNyAxMjkuMTEgMTUuOTY4MSAxMjYuNzMgMTQuNTk4NkwxMjYuNjk5IDE0LjU4MDlMMTAxLjA3OCAxLjAzNzA2Qzk5Ljg1OTEgMC4zNTg5ODIgOTguNDgwOCAwIDk3LjA4NDggMEM5NS42ODg3IDAgOTQuMjc1IDAuMzY3ODQ3IDkzLjA0MjkgMS4wNjM2NUw4Ni4xODY4IDQuOTQ1OThMODYuMTIwMyA0LjkxMDUyTDY4LjcyMDcgMTQuMjU3NEw2OC42ODUzIDE0LjI3OTVDNjcuNDM5OSAxNS4wMDY0IDY2LjQwMjkgMTYuMTM2NSA2NS42ODkzIDE3LjU0NThDNjUuNjM2MSAxNy42NDc4IDY1LjU5MTggMTcuNzU0MSA2NS41NDMxIDE3Ljg1NjFDNjQuOTA5MyAxNi43MjU5IDYzLjg1NDUgMTUuNjA0NyA2Mi4wOTUxIDE0LjU5ODZMNjIuMDU5NiAxNC41ODA5TDM2LjQzODkgMS4wMzcwNkMzNS4yMjAyIDAuMzU4OTgyIDMzLjg0MTggMCAzMi40NDU4IDBDMzEuMDQ5OCAwIDI5LjYzNiAwLjM2Nzg0NyAyOC40MDM5IDEuMDYzNjVMMjEuNTQ3OCA0Ljk0NTk4TDIxLjQ4MTMgNC45MTA1Mkw0LjA4MTc2IDE0LjI1NzRMNC4wNDYzMSAxNC4yNzk1QzIuODAwOTUgMTUuMDA2NCAxLjc2Mzg5IDE2LjEzNjUgMS4wNTAzNiAxNy41NDU4QzAuMzY3ODQ2IDE4Ljg4NDIgMC4wMDQ0MzExNCAyMC40MTc3IDAuMDA0NDMxMTQgMjEuOTczM0wwLjAyNjU5MDIgNTEuMTU3MkMwLjAyNjU5MDIgNTMuMjc1NiAxLjE1MjI5IDU1LjI3NDQgMi45NjQ5MyA1Ni4zNzM1TDMuMDA5MjUgNTYuNDAwMUwxMS45NzQ5IDYxLjI3NTJWNjEuMjMwOUwyNC40NjQgNjcuNzUwMkwyMS41NDc4IDY5LjQwMzNMMjEuNDgxMyA2OS4zNjc4TDQuMDgxNzYgNzguNzE0Nkw0LjA0NjMxIDc4LjczNjhDMi44MDA5NSA3OS40NjM2IDEuNzYzODkgODAuNTkzOCAxLjA1MDM2IDgyLjAwMzFDMC4zNjc4NDYgODMuMzQxNSAwLjAwNDQzMTE0IDg0Ljg3NSAwLjAwNDQzMTE0IDg2LjQzMDVMMC4wMjY1OTAyIDExNS42MTRDMC4wMjY1OTAyIDExNy43MzMgMS4xNTIyOSAxMTkuNzMyIDIuOTY0OTMgMTIwLjgzMUwzLjAwOTI1IDEyMC44NTdMMTEuOTc0OSAxMjUuNzMyVjEyNS42ODhMMjcuODk0MyAxMzQuMDAyTDIxLjU0MzQgMTM3LjYwMUwyMS40NzY5IDEzNy41NjZMNC4wNzczMyAxNDYuOTEyTDQuMDQxODcgMTQ2LjkzNUMyLjc5NjUyIDE0Ny42NjEgMS43NTk0NiAxNDguNzkyIDEuMDQ1OTIgMTUwLjIwMUMwLjM2MzQxMyAxNTEuNTM5IDAgMTUzLjA3MyAwIDE1NC42MjhMMC4wMjIxNTkxIDE4My44MTJDMC4wMjIxNTkxIDE4NS45MzEgMS4xNDc4NiAxODcuOTI5IDIuOTYwNDkgMTg5LjAyOUwzLjAwNDgxIDE4OS4wNTVMMTEuOTcwNSAxOTMuOTNWMTkzLjg4NkwzMC4xMTAyIDIwMy4zNTdDMzIuMTQ4OSAyMDQuNDIxIDM0LjU4NjQgMjA0LjQwNyAzNi42MTYyIDIwMy4zMTdMNjIuNDg1MSAxODkuNDE5QzYzLjcyNiAxODguNzU4IDY0LjcxODcgMTg3LjgyOCA2NS40MTAxIDE4Ni43MjRDNjUuOTE5OCAxODcuNjU5IDY2LjY2ODggMTg4LjQ2MSA2Ny41OTk1IDE4OS4wMjlMNjcuNjQzOCAxODkuMDU1TDc2LjYwOTUgMTkzLjkzVjE5My44ODZMOTQuNzQ5MiAyMDMuMzU3Qzk2Ljc4NzggMjA0LjQyMSA5OS4yMjU0IDIwNC40MDcgMTAxLjI1NSAyMDMuMzE3TDEyNy4xMjQgMTg5LjQxOUMxMjguNzQyIDE4OC41NTkgMTI5LjkzOCAxODcuMjM4IDEzMC41OSAxODUuNjY5QzEzMS4wMzcgMTg3LjA1MiAxMzEuOTY0IDE4OC4yNTcgMTMzLjIzMSAxODkuMDI5TDEzMy4yNzYgMTg5LjA1NUwxNDIuMjQxIDE5My45M1YxOTMuODg2TDE2MC4zODEgMjAzLjM1N0MxNjIuNDIgMjA0LjQyMSAxNjQuODU3IDIwNC40MDcgMTY2Ljg4NyAyMDMuMzE3TDE5Mi43NTYgMTg5LjQxOUMxOTUuMjc4IDE4OC4wOCAxOTYuNzg0IDE4NS42MiAxOTYuNzg0IDE4Mi44NDJWMTczLjgxOEwxOTYuNzcxIDE3My44MjdWMTU1LjM2NEMxOTYuNzYyIDE1My4wMDYgMTk2Ljc2MiAxNDkuNzggMTkyLjM2MSAxNDcuMjU0TDE5Mi4zMyAxNDcuMjM2TDE2OC4xNDYgMTM0LjQ1TDE5Mi43NiAxMjEuMjI1QzE5NS4yODIgMTE5Ljg4NyAxOTYuNzg5IDExNy40MjcgMTk2Ljc4OSAxMTQuNjQ4VjEwNS42MjVMMTk2Ljc3NSAxMDUuNjM0Vjg3LjE3MDdDMTk2Ljc2NyA4NC44MTI5IDE5Ni43NjcgODEuNTg2NSAxOTIuMzY2IDc5LjA2MDNMMTkyLjMzNSA3OS4wNDI2TDE3MS42NTYgNjguMTA5MUwxOTIuNzY1IDU2Ljc2OEMxOTUuMjg2IDU1LjQyOTUgMTk2Ljc5MyA1Mi45Njk4IDE5Ni43OTMgNTAuMTkxMUwxOTYuNzggNTAuMTg2NlpNMTI3LjEyIDU2Ljc2MzVDMTI4LjczNyA1NS45MDM3IDEyOS45MzQgNTQuNTgzIDEzMC41ODUgNTMuMDE0MkMxMzEuMDMzIDU0LjM5NjkgMTMxLjk1OSA1NS42MDI0IDEzMy4yMjcgNTYuMzczNUwxMzMuMjcxIDU2LjQwMDFMMTQyLjIzNyA2MS4yNzUyVjYxLjIzMDlMMTU0LjcyNiA2Ny43NTAyTDE1MS44MSA2OS40MDMzTDE1MS43NDMgNjkuMzY3OEwxMzQuMzQ0IDc4LjcxNDZMMTM0LjMwOCA3OC43MzY4QzEzMy4wNjMgNzkuNDYzNiAxMzIuMDI2IDgwLjU5MzggMTMxLjMxMiA4Mi4wMDMxQzEzMS4wNjQgODIuNDg2MiAxMzAuODYgODIuOTk1OCAxMzAuNzAxIDgzLjUyMzJDMTMwLjIgODEuOTk0MiAxMjkuMTA1IDgwLjQyNTMgMTI2LjcyNSA3OS4wNTU5TDEyNi42OTQgNzkuMDM4MkwxMDYuMDE1IDY4LjEwNDdMMTI3LjEyNCA1Ni43NjM1SDEyNy4xMlpNNjIuNDg1MSA1Ni43NjM1QzYzLjcyNiA1Ni4xMDMyIDY0LjcxODcgNTUuMTcyNSA2NS40MTAxIDU0LjA2ODlDNjUuOTE5OCA1NS4wMDQxIDY2LjY2ODggNTUuODA2MiA2Ny41OTk1IDU2LjM3MzVMNjcuNjQzOCA1Ni40MDAxTDc2LjYwOTUgNjEuMjc1MlY2MS4yMzA5TDg5LjA5ODUgNjcuNzUwMkw4Ni4xODIzIDY5LjQwMzNMODYuMTE1OSA2OS4zNjc4TDY4LjcxNjMgNzguNzE0Nkw2OC42ODA5IDc4LjczNjhDNjcuNDM1NSA3OS40NjM2IDY2LjM5ODQgODAuNTkzOCA2NS42ODQ5IDgyLjAwMzFDNjUuNjMxNyA4Mi4xMDUgNjUuNTg3NCA4Mi4yMTE0IDY1LjUzODYgODIuMzEzM0M2NC45MDQ5IDgxLjE4MzIgNjMuODUwMSA4MC4wNjE5IDYyLjA5MDYgNzkuMDU1OUw2Mi4wNTUyIDc5LjAzODJMNDEuMzc2IDY4LjEwNDdMNjIuNDg1MSA1Ni43NjM1Wk04Ni4xMTU5IDEzNy41NjZMNjguNzE2MyAxNDYuOTEyTDY4LjY4MDkgMTQ2LjkzNUM2Ny40MzU1IDE0Ny42NjEgNjYuMzk4NCAxNDguNzkyIDY1LjY4NDkgMTUwLjIwMUM2NS42MzE3IDE1MC4zMDMgNjUuNTg3NCAxNTAuNDA5IDY1LjUzODYgMTUwLjUxMUM2NC45MDQ5IDE0OS4zODEgNjMuODUwMSAxNDguMjYgNjIuMDkwNiAxNDcuMjU0TDYyLjA1NTIgMTQ3LjIzNkwzNy44NzA0IDEzNC40NUw2Mi40ODUxIDEyMS4yMjVDNjMuNzI2IDEyMC41NjUgNjQuNzE4NyAxMTkuNjM0IDY1LjQxMDEgMTE4LjUzMUM2NS45MTk4IDExOS40NjYgNjYuNjY4OCAxMjAuMjY4IDY3LjU5OTUgMTIwLjgzNUw2Ny42NDM4IDEyMC44NjJMNzYuNjA5NSAxMjUuNzM3VjEyNS42OTNMOTIuNTI4OCAxMzQuMDA3TDg2LjE3NzkgMTM3LjYwNUw4Ni4xMTE0IDEzNy41N0w4Ni4xMTU5IDEzNy41NjZaTTE1MS43NDMgMTM3LjU2NkwxMzQuMzQ0IDE0Ni45MTJMMTM0LjMwOCAxNDYuOTM1QzEzMy4wNjMgMTQ3LjY2MSAxMzIuMDI2IDE0OC43OTIgMTMxLjMxMiAxNTAuMjAxQzEzMS4wNjQgMTUwLjY4NCAxMzAuODYgMTUxLjE5NCAxMzAuNzAxIDE1MS43MjFDMTMwLjIgMTUwLjE5MiAxMjkuMTA1IDE0OC42MjMgMTI2LjcyNSAxNDcuMjU0TDEyNi42OTQgMTQ3LjIzNkwxMDIuNTA5IDEzNC40NUwxMjcuMTI0IDEyMS4yMjVDMTI4Ljc0MiAxMjAuMzY1IDEyOS45MzggMTE5LjA0NSAxMzAuNTkgMTE3LjQ3NkMxMzEuMDM3IDExOC44NTkgMTMxLjk2NCAxMjAuMDY0IDEzMy4yMzEgMTIwLjgzNUwxMzMuMjc2IDEyMC44NjJMMTQyLjI0MSAxMjUuNzM3VjEyNS42OTNMMTU4LjE2MSAxMzQuMDA3TDE1MS44MSAxMzcuNjA1TDE1MS43NDMgMTM3LjU3VjEzNy41NjZaIiBmaWxsPSIjNzI4ODk5Ii8+Cjwvc3ZnPgo=)}
.aw-fx .aw-row:nth-child(4) .aw-year{--shape:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTk3IiBoZWlnaHQ9IjE5NyIgdmlld0JveD0iMCAwIDE5NyAxOTciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xOTYuNzYyIDEwNC40NzNMMTk2Ljc1MyA4My43ODAzQzE5Ni43NTMgODEuMTM4OSAxOTYuNzUzIDc3LjUyMjUgMTkxLjgxNiA3NC42OTA1TDE5MS43ODEgNzQuNjY4M0wxNzYuNTQ0IDY2LjYxMTJMMTgxLjgyMiA0OS4wNzQyTDE4MS44MzYgNDkuMDI5OUMxODIuMjQzIDQ3LjQ2NTQgMTgyLjE3MiA0NS43NTAzIDE4MS42MjMgNDQuMDY2MkMxODEuMTA0IDQyLjQ2MTggMTgwLjE3NCA0MC45NjM5IDE3OC45NDIgMzkuNzI3NEwxNTUuNzk0IDE2LjYxOTVDMTU0LjExNCAxNC45Mzk5IDE1MS42NDEgMTQuMjQ4NSAxNDkuMzMyIDE0LjgxNThMMTQ5LjI3NSAxNC44MjkxTDEzOC4zMDYgMTguMDY4OEwxMzguMzQxIDE4LjEwNDJMMTMwLjk4NCAyMC40MTMyTDEyMi40NzEgNC41NjkyN0wxMjIuNDQ4IDQuNTI5MzhDMTIxLjYzMyAzLjEzNzc3IDEyMC4zNjUgMS45NzIxOSAxMTguNzg4IDEuMTcwMDJDMTE3LjI4NSAwLjQwMzMwMSAxMTUuNTcgMCAxMTMuODI0IDBMODEuMTE2NyAwLjAyNjU5MDJDNzguNzQxMiAwLjAyNjU5MDIgNzYuNTAzMSAxLjI4NTI0IDc1LjI3MTEgMy4zMTk0OEw3NS4yNCAzLjM2ODIzTDY5Ljc3NTUgMTMuNDE1M0g2OS44MjQzTDY2LjI5MjEgMjAuMTgyOEw0OS4xNDA3IDE1LjAyNDFMNDkuMDk2NCAxNS4wMTA4QzQ3LjUzMTkgMTQuNjAzIDQ1LjgxNjggMTQuNjc0IDQ0LjEzMjcgMTUuMjIzNUM0Mi41MjgzIDE1Ljc0MiA0MS4wMzA0IDE2LjY3MjcgMzkuNzk4MyAxNy45MDQ4TDE2LjY5MDUgNDEuMDUyNUMxNS4wMTA4IDQyLjczMjIgMTQuMzE5NCA0NS4yMDUyIDE0Ljg4MjMgNDcuNTE0MkwxNC44OTU2IDQ3LjU3MThMMTguMTM1MyA1OC41NDA3TDE4LjE3MDcgNTguNTA1M0wyMC40NTMxIDY1Ljc3OEw0LjU2OTI3IDc0LjMwOTNMNC41MjkzOCA3NC4zMzE1QzMuMTMzMzQgNzUuMTQ3IDEuOTcyMTkgNzYuNDE0NSAxLjE3MDAyIDc3Ljk5MjJDMC40MDMzMDEgNzkuNDk0NiAwIDgxLjIwOTggMCA4Mi45NTU5TDAuMDI2NTkxOSAxMTUuNjYzQzAuMDI2NTkxOSAxMTguMDM5IDEuMjg5NjggMTIwLjI3NyAzLjMxOTQ4IDEyMS41MDlMMy4zNjgyMyAxMjEuNTRMMTMuNDE1MyAxMjcuMDA0VjEyNi45NTZMMjAuNjQ4MSAxMzAuNzMyTDE1LjQxODUgMTQ4LjEwOUMxNC40NzkgMTUxLjE3MSAxNS4yMzY4IDE1NC4zMTQgMTcuNDM1IDE1Ni41MTJMMjQuNTg4MSAxNjMuNjY1VjE2My42NTJMMzkuMjMxIDE3OC4yNzdDNDEuMDk2OCAxODAuMTQzIDQzLjY1ODUgMTgyLjcgNDkuMTQ5NiAxODEuMjE1TDQ5LjE4OTQgMTgxLjIwNkw2Ni4wODgyIDE3NS45OTRMNzQuODMyMyAxOTIuMjY0Qzc2LjMzNDcgMTk1LjA5MSA3OS4wOTEzIDE5Ni43OCA4Mi4yMDI1IDE5Ni43OEg5Mi4zMTYxTDkyLjMwNzIgMTk2Ljc2N0wxMTMgMTk2Ljc1OEMxMTUuNjQxIDE5Ni43NTggMTE5LjI1NyAxOTYuNzU4IDEyMi4wODkgMTkxLjgyMUwxMjIuMTEyIDE5MS43ODVMMTMwLjQxMyAxNzYuMDgzTDE0OC4xNzUgMTgxLjQyOEMxNTEuMjM4IDE4Mi4zNjcgMTU0LjM4IDE4MS42MSAxNTYuNTc4IDE3OS40MTFMMTYzLjczMSAxNzIuMjU4SDE2My43MTRMMTc4LjMzOSAxNTcuNjE1QzE4MC4yMDUgMTU1Ljc0NSAxODIuNzYyIDE1My4xODggMTgxLjI3MyAxNDcuNjk3TDE4MS4yNjQgMTQ3LjY1N0wxNzYuMDI1IDEzMC42NzRMMTkyLjI1OSAxMjEuOTUyQzE5NS4wODcgMTIwLjQ1IDE5Ni43NzYgMTE3LjY5MyAxOTYuNzc2IDExNC41ODJWMTA0LjQ2OEwxOTYuNzYyIDEwNC40NzdWMTA0LjQ3M1oiIGZpbGw9IiM3Mjg4OTkiLz4KPC9zdmc+Cg==)}
.aw-fx .aw-year::before{content:"";position:absolute;left:50%;top:50%;width:96px;height:96px;
  transform:translate(-50%,-50%) scale(.8);background:var(--red);opacity:0;z-index:-1;pointer-events:none;
  -webkit-mask-image:var(--shape);mask-image:var(--shape);
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;
  -webkit-mask-size:contain;mask-size:contain;
  transition:opacity .4s var(--ease),transform .45s var(--ease)}
@media(hover:hover) and (pointer:fine){
  .aw-fx .aw-row:hover{background:transparent}
  .aw-fx .aw-row:hover::before{opacity:1;transform:none}
  .aw-fx .aw-row:hover .aw-year::before{opacity:1;transform:translate(-50%,-50%) scale(1)}
  .aw-fx .aw-row:hover .aw-year{color:#fff}
  .aw-fx .aw-row:hover .aw-name{color:var(--red-ink)}
  .aw-fx .aw-row:hover .aw-logo{opacity:1;filter:none}
}
@media(prefers-reduced-motion:reduce){.aw-fx .aw-year::before{transition:none}}
html.a11y-no-motion .aw-fx .aw-year::before{transition:none}
/* #recognition's own background — a quieter cousin of the home bento band (.wwd), which washes
   --paper-100 over backgrounds/bg-3d-shapes.webp: a PHOTOGRAPH of white rounded boxes. Here the
   same "field of TPL boxes behind the content" idea is drawn instead of shot. shapes/box-tile.svg
   is a 520px-square tile (drawn at 440px here) holding three of the four brand shapes (shape-5, -2, -1 — the same set the
   .aw-year bloom on this section cycles through) scaled, rotated and spaced apart, so tiling it
   scatters boxes across the band instead of gridding them. Drawn, not photographic, keeps it
   deliberately NOT identical to .wwd: no wash gradient, no blend mode, one flat --paper-100 base,
   and the boxes are ink at 4% so the ledger hairlines and the full-bleed white hover band stay
   the loudest things here. The tile is a MASK, not an image, so --ink carries it and one rule
   covers both themes (dark takes a touch more alpha — light ink on near-black reads fainter at
   the same value). Three boxes per tile was the sparse setting: filling the tile turns the
   negative space into the pattern and the boxes read as confetti. */
/* :not(.ib-sec) keeps this to the HOME ledger — investors.html reuses id="recognition" for its
   own ledger inside the .ib-sec band rhythm, and an id-level background would outrank it there. */
#recognition:not(.ib-sec){position:relative;background:var(--paper-100)}
#recognition:not(.ib-sec)>.wrap{position:relative;z-index:1}
#recognition:not(.ib-sec)::before{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:var(--ink);opacity:.04;
  -webkit-mask-image:url(../shapes/box-tile.svg);mask-image:url(../shapes/box-tile.svg);
  -webkit-mask-repeat:repeat;mask-repeat:repeat;
  -webkit-mask-size:440px 440px;mask-size:440px 440px;
  -webkit-mask-position:-40px -60px;mask-position:-40px -60px}
html[data-theme="dark"] #recognition:not(.ib-sec)::before{opacity:.06}
/* forced-contrast mode flattens every paper token to #fff — a 4% ink field on white is noise
   there, not texture, so the band drops back to a plain surface. */
html.a11y-contrast #recognition:not(.ib-sec)::before{display:none}
@media(max-width:720px){
  /* the tile shrinks with the band so a phone still sees more than one box */
  #recognition:not(.ib-sec)::before{-webkit-mask-size:340px 340px;mask-size:340px 340px}
}
/* The case-studies hub hero wore this same masked field as .boxfield from 2026-08-29 until
   2026-09-01, when the owner asked for the pointer-reactive dot-tpl-box canvas there instead —
   the honeycomb of TPL marks that scatters and reddens under the cursor. Everything structural
   comes from .dtb-host (position, isolation, the overflow clip, .wrap above the canvas); the one
   thing the hero still owns is the --paper-100 ground the masked field used to bring with it,
   which is also the complete no-JS / forced-contrast state. */
.cs-hub-hero{background:var(--paper-100)}

/* ≤720px the row sheds its side padding, so the 96px bloom centered on the flush-left year
   pokes past the viewport edge and into the rows above/below (clipped mid-shape by the
   section's overflow-x:clip). Narrow hover-capable viewports get the plain ledger hover
   instead: row tint, no bloom — and the year keeps its ink (the hover-white year would
   vanish on paper without the red shape behind it). */
@media(max-width:720px){
  .aw-fx .aw-year::before{display:none}
  .aw-fx .aw-row:hover{background:var(--paper-200)}
  .aw-fx .aw-row:hover .aw-year{color:var(--ink-muted)}
}
/* the ledger's hairlines run FULL-BLEED, matching the full-bleed white hover band: the wrap-bound
   list/row borders are swapped for 100vw pseudo-element rules (the section's overflow-x:clip
   catches the spill, same as the band). ::after paints after the ::before band in tree order,
   so the lines stay visible while a row is hovered. */
.aw-fx .aw-list{border-top:0;position:relative}
.aw-fx .aw-row{border-bottom:0}
.aw-fx .aw-list::before,.aw-fx .aw-row::after{content:"";position:absolute;left:50%;width:100vw;margin-left:-50vw;height:1px;background:var(--hairline)}
.aw-fx .aw-list::before{top:0}
.aw-fx .aw-row::after{bottom:0}

/* contact — paper split: type CTA left, working form right (composes an email; no backend) */
.ct-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,440px);gap:clamp(36px,6vw,88px);align-items:start}
.ct-mail{display:flex;align-items:center;column-gap:clamp(14px,2vw,26px);row-gap:12px;flex-wrap:wrap;margin:clamp(24px,3vw,40px) 0 0}
.ct-link{display:inline-flex;align-items:center;gap:14px;min-height:44px;text-decoration:none;color:var(--ink)}
.ct-addr{position:relative;font-family:var(--font-display);font-weight:800;font-size:clamp(26px,3vw,42px);letter-spacing:-.02em;line-height:1.1;overflow-wrap:anywhere}
.ct-addr::after{content:"";position:absolute;left:0;right:0;bottom:-4px;height:3px;background:var(--red);transform:scaleX(0);transform-origin:right center;transition:transform 300ms var(--ease)}
.ct-link:hover .ct-addr::after,.ct-link:focus-visible .ct-addr::after{transform:scaleX(1);transform-origin:left center}
.ct-arrow{width:26px;height:auto;flex:none;color:var(--ink-muted);transition:transform 300ms var(--ease),color 300ms var(--ease)}
.ct-link:hover .ct-arrow,.ct-link:focus-visible .ct-arrow{transform:translateX(8px);color:var(--red-ink,#ab0000)}
.ct-copy{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft);background:none;border:1px solid var(--hairline);border-radius:2px;padding:12px 18px;min-height:44px;cursor:pointer;transition:color 240ms var(--ease),border-color 240ms var(--ease)}
.ct-copy:hover,.ct-copy:focus-visible{color:var(--ink);border-color:var(--ink)}
.ct-copy.is-copied{color:var(--red-ink,#ab0000);border-color:currentColor}
.ct-meta{margin:clamp(28px,4vw,44px) 0 0;padding-top:20px;border-top:1px solid var(--hairline);font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.06em;line-height:1.9;color:var(--ink-muted)}
.ct-form{padding:clamp(26px,3vw,36px)}
.ct-field{display:block;margin-bottom:20px}
.ct-field span{display:block;font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:6px}
.ct-field input,.ct-field textarea{width:100%;font:inherit;font-size:15px;line-height:1.5;color:var(--ink);background:var(--paper-0);border:1px solid var(--hairline);border-radius:8px;padding:13px 16px;min-height:44px;transition:border-color 200ms var(--ease),background 200ms var(--ease)}
.ct-field textarea{min-height:110px;resize:vertical}
.ct-field input:focus-visible,.ct-field textarea:focus-visible{outline:2px solid var(--ink);outline-offset:2px;border-color:var(--ink)}
.ct-field input:user-invalid,.ct-field textarea:user-invalid{border-color:var(--error)}
.ct-form .btn{display:block;width:100%;border:0;cursor:pointer;text-align:center;margin-top:26px}
.ct-form-note{margin:14px 0 0;font-size:13px;color:var(--ink-muted);text-align:center}
.ct-form-note.ct-ok{color:var(--ink);font-weight:500}

/* ---- slide-to-verify: the anti-spam gate — a thin rail and one red handle -------------
   Fourth and, per the owner, final skin (2026-08-28, option S3 of the comparison set): a 2px
   rail across the field box, a red fill line behind the handle, and a 30px red disc in a white
   ring as the handle itself. No pattern, no scene, no glow. The disc and its ring are drawn as
   background layers on the full 44x42 thumb box, so the drawn handle can be 30px while the hit
   target and the drag-guard geometry stay 44px — nothing in tpl.js changes.
   THE GATE IS NO LONGER CONTACT-ONLY: the same control now also sits in the tpl.js mail modals
   (pitch / introduce / customer-care), wired through the shared TPL.slideGate. These class
   rules are the single source of its look everywhere; keep them id-free.
   --webkit and --moz need separate thumb/track rules; keep them in step by hand. */
.ct-captcha{margin-top:22px}
.ct-slide{position:relative;height:60px}
/* iOS Safari reserves roughly the leftmost 20px of the viewport for its interactive
   back-swipe, and the thumb's resting box began at exactly x=20 on every phone width
   measured (320/360/390/430). A grab on the thumb's left half could navigate back instead
   of dragging. Inset the control clear of that gutter; costs 16px of travel at 320. */
@media(max-width:560px){.ct-slide{margin-inline:8px}}
.ct-slide-input{-webkit-appearance:none;appearance:none;width:100%;margin:0;
  background:transparent;cursor:grab;touch-action:pan-y;
  --th:44px;--v:0;
  --fill:calc(var(--th)/2 + var(--v) * (100% - var(--th)) / 100)}
/* The field chrome comes from `.ct-field input` (0-1-1), so this needs 0-2-1 to retune it.
   One bordered box only: the rail and the handle live inside its padding. */
.ct-field input.ct-slide-input{height:60px;padding:8px;border-radius:12px;
  background:var(--paper-0);border:1px solid var(--hairline)}
.ct-slide-input:active{cursor:grabbing}
.ct-slide-input:focus-visible{outline:2px solid var(--ink);outline-offset:3px;border-radius:14px}
.ct-slide-input::-webkit-slider-runnable-track{height:42px;border-radius:0;border:0;
  background:
    linear-gradient(var(--red),var(--red)) 0 center/var(--fill) 2px no-repeat,
    linear-gradient(var(--paper-200),var(--paper-200)) 0 center/100% 2px no-repeat}
.ct-slide-input::-webkit-slider-thumb{-webkit-appearance:none;
  width:var(--th);height:42px;margin-top:0;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h13M13 6l6 6-6 6'/%3E%3C/svg%3E") center/16px no-repeat,
    radial-gradient(circle,transparent 0 14.5px,var(--paper-0) 15px 19.5px,var(--hairline) 19.5px 20.5px,transparent 21px),
    radial-gradient(circle,var(--red) 0 14.5px,transparent 15px);
  transition:transform 120ms var(--ease-drawer)}
.ct-slide-input:active::-webkit-slider-thumb{transform:scale(.97)}
/* At rest --fill still measures --th/2, which would draw a stub of red line with nothing in
   progress. Zero the fill only. tpl.js flags the state because CSS cannot branch on a custom
   property's own value. */
.ct-slide-input.is-zero{--fill:0px}
.ct-slide-input.is-zero::-moz-range-progress{background:transparent}
.ct-slide-input::-moz-range-track{height:42px;border-radius:0;border:0;
  background:
    linear-gradient(var(--red),var(--red)) 0 center/var(--fill) 2px no-repeat,
    linear-gradient(var(--paper-200),var(--paper-200)) 0 center/100% 2px no-repeat}
/* everything is drawn on the track, so the progress box stays out of the way entirely */
.ct-slide-input::-moz-range-progress{height:42px;background:transparent}
.ct-slide-input::-moz-range-thumb{width:var(--th);height:42px;border:0;border-radius:0;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h13M13 6l6 6-6 6'/%3E%3C/svg%3E") center/16px no-repeat,
    radial-gradient(circle,transparent 0 14.5px,var(--paper-0) 15px 19.5px,var(--hairline) 19.5px 20.5px,transparent 21px),
    radial-gradient(circle,var(--red) 0 14.5px,transparent 15px);
  cursor:grab}
/* verified: rail and disc flip to --success, the arrow becomes a check */
.ct-captcha.is-ok .ct-slide-input::-webkit-slider-runnable-track{
  background:linear-gradient(var(--success),var(--success)) 0 center/100% 2px no-repeat}
.ct-captcha.is-ok .ct-slide-input::-webkit-slider-thumb{
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l4.5 4.5L19 7'/%3E%3C/svg%3E") center/16px no-repeat,radial-gradient(circle,transparent 0 14.5px,var(--paper-0) 15px 19.5px,var(--hairline) 19.5px 20.5px,transparent 21px),radial-gradient(circle,var(--success) 0 14.5px,transparent 15px)}
.ct-captcha.is-ok .ct-slide-input::-moz-range-track{
  background:linear-gradient(var(--success),var(--success)) 0 center/100% 2px no-repeat}
.ct-captcha.is-ok .ct-slide-input::-moz-range-progress{background:transparent}
.ct-captcha.is-ok .ct-slide-input::-moz-range-thumb{
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l4.5 4.5L19 7'/%3E%3C/svg%3E") center/16px no-repeat,radial-gradient(circle,transparent 0 14.5px,var(--paper-0) 15px 19.5px,var(--hairline) 19.5px 20.5px,transparent 21px),radial-gradient(circle,var(--success) 0 14.5px,transparent 15px)}
/* the status line reads as instrumentation, matching the registry codes in .rs-record on the
   contact page, rather than as a sentence of encouragement */
.ct-slide-msg{margin:12px 0 0;font-family:var(--font-mono);font-size:12px;font-weight:500;
  line-height:1.4;letter-spacing:0.07em;text-transform:uppercase;color:var(--ink-muted);text-align:center}
.ct-captcha.is-ok .ct-slide-msg{color:var(--success);font-weight:600}
.ct-captcha.is-shake .ct-slide{animation:ct-shake 340ms var(--ease)}
@keyframes ct-shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-6px)}40%{transform:translateX(5px)}60%{transform:translateX(-4px)}80%{transform:translateX(3px)}}
html.a11y-no-motion .ct-slide-input::-webkit-slider-runnable-track,html.a11y-no-motion .ct-slide-input::-webkit-slider-thumb{transition:none}
html.a11y-no-motion .ct-captcha.is-shake .ct-slide{animation:none}
@media(prefers-reduced-motion:reduce){
  .ct-slide-input::-webkit-slider-runnable-track,.ct-slide-input::-webkit-slider-thumb{transition:none}
  .ct-captcha.is-shake .ct-slide{animation:none}
}
/* forced-colors (Windows High Contrast) strips every gradient this skin is drawn with — the
   rail, the fill line and the disc all vanish, leaving a floating white arrow that HC light
   themes render invisible. Hand the control back to the UA there: the native range is drawn
   in system colors, stays operable, and the status line carries the verified state as text. */
@media(forced-colors:active){
  .ct-slide-input{-webkit-appearance:auto;appearance:auto}
  .ct-slide-input::-webkit-slider-runnable-track{background:none;height:auto}
  .ct-slide-input::-webkit-slider-thumb{-webkit-appearance:auto;background:none;width:auto;height:auto}
  .ct-slide-input::-moz-range-track{background:none;height:auto}
  .ct-slide-input::-moz-range-progress{background:none}
  .ct-slide-input::-moz-range-thumb{background:none;width:auto;height:auto}
}

/* Send stays hidden until the slide-to-verify gate reports .is-ok — the button existing at
   all is the "you may submit now" signal, on top of the slide's own checkmark/message, so a
   bot (or an impatient visitor) never even sees a control to click before verifying. Keyed
   off .is-ok, which slideRender() already toggles, rather than a second JS write. Same
   since scripting-off visitors already lose this button entirely (see the
   @media(scripting:none) rule below), hiding it by default here adds no new no-JS gap.

   The reveal is a keyframe rather than a transition + @starting-style: display:none → block
   is a discrete jump, and driving the fade off an animation on the newly-shown element is
   the one recipe that runs on the FIRST reveal in every engine, with no transition-behavior
   support to depend on. `both` holds the end frame so the button stays at opacity 1. */
.ct-form .btn[type="submit"],.mm-form .btn[type="submit"]{display:none}
.ct-captcha.is-ok ~ .btn[type="submit"]{display:block;animation:ct-send-in 420ms var(--ease) both}
@keyframes ct-send-in{from{opacity:0;transform:translateY(10px) scale(.98)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.ct-captcha.is-ok ~ .btn[type="submit"]{animation:none}}
html.a11y-no-motion .ct-captcha.is-ok ~ .btn[type="submit"]{animation:none}

/* per the code's own comment, this is the ONLY feedback a visitor gets that the mailto
   hand-off actually happened -- a hard [hidden] cut undersold the one reassurance moment
   the form has. No fade-OUT needed (it never re-hides once shown), so [hidden] + starting-
   style is enough -- same recipe as .a11y-panel/.lang-list, without needing a class swap. */
[data-ct-status]{opacity:1;transition:opacity 200ms var(--ease)}
[data-ct-status][hidden]{display:none;opacity:0}
@starting-style{[data-ct-status]:not([hidden]){opacity:0}}
@media(prefers-reduced-motion:reduce){[data-ct-status]{transition:none}}
html.a11y-no-motion [data-ct-status]{transition:none}
/* recovery button, injected next to the status line after a mailto hand-off */
.ct-copy-msg{margin:12px auto 0;display:block}
.ct-copy-msg[hidden]{display:none}
@media(max-width:920px){.ct-split{grid-template-columns:1fr;gap:44px}}
html.a11y-no-motion .ct-addr::after,html.a11y-no-motion .ct-arrow,html.a11y-no-motion .ct-copy,html.a11y-no-motion .ct-field input,html.a11y-no-motion .ct-field textarea,html.a11y-no-motion .ct-field select{transition:none}
@media(prefers-reduced-motion:reduce){.ct-addr::after,.ct-arrow,.ct-copy,.ct-field input,.ct-field textarea,.ct-field select{transition:none}}
/* non-color invalid cue (3.3.1): text under the field, injected by tpl.js on blur */
/* freshly inserted each time it appears (tpl.js removes the node entirely once valid, not
   just hides it) -- an error under a just-blurred field with no entrance read as a snap
   rather than guidance. @starting-style covers a brand-new node's first paint directly,
   no [hidden] dance needed since there's nothing to toggle, only insert/remove. */
.ct-err{margin:6px 0 0;font-size:12.5px;line-height:1.4;color:var(--error);
  opacity:1;transform:translateY(0);transition:opacity 180ms var(--ease),transform 180ms var(--ease)}
@starting-style{.ct-err{opacity:0;transform:translateY(-2px)}}
@media(prefers-reduced-motion:reduce){.ct-err{transition:none}}
html.a11y-no-motion .ct-err{transition:none}
/* contact — type-led "Statement" hero. The three-line promise is the hero; everything
   else is quiet around it. The phone handset is a small masthead emblem (top-left), NOT a
   photo to fill space. Generous top space, tight leading on the statement. */
.ct-hero{padding:clamp(40px,5vw,72px) 0 clamp(48px,6vw,80px)}
.ct-emblem{display:block;width:auto;height:clamp(84px,9vw,120px);margin:0 0 clamp(20px,3vw,32px) -4px}
.ct-statement{font-family:var(--font-display);font-weight:800;letter-spacing:-.03em;line-height:1.02;font-size:clamp(38px,6.4vw,82px);color:var(--ink);margin:0;max-width:16ch}
/* the one red moment: a signature-style underline under the speed payoff. It's a drawn
   rule, not text-decoration, so it sits just below the baseline at a deliberate weight. */
.ct-underline{position:relative;white-space:nowrap}
.ct-underline::after{content:"";position:absolute;left:0;right:0;bottom:.02em;height:.09em;background:var(--red);border-radius:2px}
.ct-hero-lede{margin:clamp(24px,3vw,34px) 0 clamp(28px,3.5vw,40px);max-width:52ch}
.ct-hero-actions{display:flex;gap:16px;flex-wrap:wrap;align-items:center;margin:0}
/* desk routing — restrained inline row (not chips): a mono lead-in, then underline-on-hover
   text links separated by thin rules. Keeps the pre-addressed mailto routing. */
.ct-desks{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px 20px;margin-top:clamp(32px,4vw,48px);padding-top:clamp(24px,3vw,30px);border-top:1px solid var(--hairline)}
.ct-desks-lbl{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted)}
.ct-desks-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px 0}
.ct-desk{position:relative;font-family:var(--font-body);font-size:15px;font-weight:600;color:var(--ink-soft);text-decoration:none;padding:6px 18px;transition:color 150ms var(--ease)}
.ct-desk+.ct-desk{border-left:1px solid var(--hairline)}
.ct-desk::after{content:"";position:absolute;left:18px;right:18px;bottom:2px;height:1.5px;background:var(--red);transform:scaleX(0);transform-origin:left center;transition:transform 200ms var(--ease)}
.ct-desk:hover,.ct-desk:focus-visible{color:var(--ink)}
.ct-desk:hover::after,.ct-desk:focus-visible::after{transform:scaleX(1)}
@media(max-width:560px){.ct-desk{padding:6px 12px}.ct-desk:first-child{padding-left:0}.ct-desk::after{left:12px;right:12px}}
html.a11y-no-motion .ct-desk,html.a11y-no-motion .ct-desk::after{transition:none}
@media(prefers-reduced-motion:reduce){.ct-desk,.ct-desk::after{transition:none}}
/* honeypot — visually hidden, never focusable by a real (tabindex=-1) visitor; bots that
   fill every field trip it, and tpl.js silently drops the submit when it's non-empty */
.ct-hp{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
/* find-us location cards — hover/focus flips to the address + directions on the back.
   Reduced-motion / a11y-no-motion drop the 3D transform entirely and stack both faces
   in normal flow (today's plain-card content, unchanged). */
.ct-loc-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.ct-loc-card{perspective:1400px}
.ct-loc-flip{position:relative;min-height:232px;transition:transform 600ms var(--ease);transform-style:preserve-3d}
.ct-loc-card:hover .ct-loc-flip,.ct-loc-card:focus-within .ct-loc-flip{transform:rotateY(180deg)}
.ct-loc-face{position:absolute;inset:0;backface-visibility:hidden;-webkit-backface-visibility:hidden;background:var(--paper-50);border:1px solid var(--hairline);border-radius:var(--radius-card);padding:28px}
.ct-loc-back{transform:rotateY(180deg);display:flex;flex-direction:column;justify-content:center}
@media(max-width:820px){.ct-loc-grid{grid-template-columns:1fr}}
@media(prefers-reduced-motion:reduce){
  .ct-loc-flip{transition:none;transform:none!important;min-height:0}
  .ct-loc-face{position:static;backface-visibility:visible}
  .ct-loc-back{transform:none;margin-top:14px}
}
html.a11y-no-motion .ct-loc-flip{transition:none;transform:none!important;min-height:0}
html.a11y-no-motion .ct-loc-face{position:static;backface-visibility:visible}
html.a11y-no-motion .ct-loc-back{transform:none;margin-top:14px}
/* ===== Contact — reference-adapted layout (2026-07-21) =====
   A big display headline broken by a tilted photo stack (crossfade slideshow), then a
   dark form panel with chip-selector questions, then our map + on-the-record + FAQ.
   Own tokens only; NO red-wash background anywhere on this page. */
.rs-hero{padding:clamp(40px,5vw,72px) 0 clamp(32px,4vw,56px)}
.rs-h1{max-width:14ch;margin:0}
.rs-hero .lede{margin:clamp(20px,3vw,30px) 0 0;max-width:56ch}

/* Hero — kept plain: the standard .two-tone h1 every other subpage uses, just centered. */
.rs-center{max-width:760px;margin:0 auto;text-align:center}
.rs-hero-wrap{text-align:center}
.rs-hero .ct-desks{justify-content:center}
.rs-hero .lede{max-width:56ch;margin-left:auto;margin-right:auto}

/* #write's own background — backgrounds/bg-grain.webp, already a vertical/portrait asset, so
   it's used straight (no rotation) via a full-bleed cover layer. vmax sizing guarantees full
   coverage regardless of the section's own (content-driven, auto) height. THE RULE for this
   class of bg image (a full-bleed grain/texture photo, not a tiled overlay like .fig-img's
   noise) — read var(--paper-50) as the base, then blend the image in at 25% opacity so it
   softens/warms into the site's own paper tone instead of sitting as a stark, separate photo.
   Same "blend to soften" family as .fig-img's noise overlay (mix-blend-mode:overlay below) and
   .cta-bg's radial fade-into-paper — apply this same treatment to any future full-bleed
   grain/texture section background. */
#write{position:relative;isolation:isolate;overflow:hidden;background:var(--paper-50)}
#write::before{content:"";position:absolute;inset:0;background:url(https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/bg-grain.webp) center/cover no-repeat;mix-blend-mode:multiply;opacity:.25;z-index:-1}
/* multiply crushes toward black against dark theme's dark paper — soft-light lightens instead */
html[data-theme="dark"] #write::before{mix-blend-mode:soft-light}

/* the form panel — frosted glass over that texture. NOT the only glass on the site: this is
   one member of the sanctioned frosted-surface family (each added deliberately, per page):
   .wwd-card (home bento), .cta-band>.wrap (model pages), .gtee--noise .gtee-card
   (cross-docking/oaas), investors #people .card.person (page-scoped),
   .abar.scrolled (nav chrome), .ms-scrim (menu scrim, chrome),
   .ainx-stack .card (ai.html stack band, over the cube-monogram art), the ai.html hero
   graph stage (frosted nodes + info card, always-dark), plus the body.ai-neural page.
   STRUCK 2026-08-06: .hs-nav (home hero slider dock) was added to this list earlier the same
   day and removed with the slide picker it housed (UX review, finding 5) — the home hero
   carries no glass surface now.
   STRUCK 2026-08-21: .step-img::after (oaas step-card hover mask). The frost existed to hold
   back a photograph that was never real per-step photography, and at 120px behind it none of
   the four resolved into a subject. It became a flat ink tint of the same brand shape the same
   day and then went entirely, with the card row it sat in, when that section was rebuilt as
   .bnd. Either way: no imagery behind it, so by this list's own rule no glass.
   The unifying rule: GLASS REQUIRES IMAGERY OR CONTENT BEHIND IT TO BLUR — photo, noise,
   art, or overlaid page; never a flat paper/inverse band. The SHARP no-glass ban applies
   to anything OUTSIDE this list. Everything inside the panel — labels, chips, fields —
   uses the SITE'S OWN ink/paper tokens, not a bespoke palette. */
.rs-formpanel{background:rgba(255,255,255,.6);-webkit-backdrop-filter:blur(22px) saturate(1.5);backdrop-filter:blur(22px) saturate(1.5);border:1px solid rgba(255,255,255,.7);border-radius:var(--radius-card);padding:clamp(28px,4vw,48px)}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.rs-formpanel{background:var(--paper-0)}}
/* phones: the panel was triple-inset (wrap gutter + panel pad + .ct-form pad ≈ 74px/side),
   squeezing the fields. Bleed the panel edge-to-edge (cancel the wrap gutter, negative
   margin mirrors .wrap's padding clamp), drop the card chrome at the sides, and collapse
   the inner form padding so the fields run the full viewport width minus one 20px inset.
   Desktop is untouched. */
@media(max-width:720px){
  .rs-formpanel{margin-inline:calc(-1*clamp(20px,4vw,48px));border-inline:0;border-radius:0;padding:28px 20px}
  .rs-formpanel .ct-form{padding:0}
}
.rs-fgroup{margin-bottom:clamp(28px,3.2vw,40px)}
.rs-fgroup:last-of-type{margin-bottom:0}
.rs-fkicker{display:block;font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink);padding-bottom:14px;margin-bottom:24px;border-bottom:1px solid var(--hairline)}
.rs-flabel{display:block;font-family:var(--font-body);font-size:14.5px;font-weight:600;color:var(--ink);margin-bottom:12px}
.rs-flabel em{font-style:normal;font-weight:400;color:var(--ink-muted)}

/* where-you-sell — the chip checkboxes serve every width. The old ≤720px native
   <select multiple> mirror rendered as a stuck-open listbox and iOS Safari ignored taps on
   it entirely (2026-09-03), so phones keep the chips, gridded two per row: six short labels
   make three even rows, and each whole cell is the 44px touch target. */
.rs-chip-row{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:clamp(24px,3vw,34px)}
@media(max-width:720px){
  .ct-choice .rs-chip-row{display:grid;grid-template-columns:1fr 1fr}
  .ct-choice .rs-chip span{width:100%;justify-content:center;padding:0 10px;text-align:center}
}
.rs-chip{position:relative;display:inline-flex}
.rs-chip input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
.rs-chip span{display:inline-flex;align-items:center;min-height:44px;padding:0 18px;border-radius:9999px;border:1px solid var(--hairline);background:var(--paper-0);color:var(--ink-soft);font-family:var(--font-body);font-size:14px;font-weight:600;transition:background 150ms var(--ease),border-color 150ms var(--ease),color 150ms var(--ease)}
.rs-chip input:hover+span{border-color:var(--ink)}
.rs-chip input:checked+span{background:var(--ink);border-color:var(--ink);color:var(--paper-0)}   /* selected = ink fill, NOT red */
.rs-chip input:focus-visible+span{outline:2px solid var(--ink);outline-offset:3px}
html.a11y-no-motion .rs-chip span{transition:none}
@media(prefers-reduced-motion:reduce){.rs-chip span{transition:none}}

.rs-field{display:block;margin-bottom:20px}
.rs-field-row{display:grid;grid-template-columns:1fr 1fr;gap:0 20px}
/* phone: a fixed-width dial-code select beside the national-number input, one field visually */
.ct-phone-row{display:flex;gap:10px}
.ct-phone-row select{flex:0 0 152px;width:152px}
.ct-phone-row input{flex:1 1 auto;min-width:0}
@media(max-width:480px){.ct-phone-row{flex-wrap:wrap}.ct-phone-row select{flex:1 1 100%;width:100%}}
.ct-cc{position:relative}
.ct-cc-face{position:absolute;left:16px;top:50%;transform:translateY(-50%);pointer-events:none;
  font:inherit;font-size:15px;color:var(--ink);white-space:nowrap}
@media(max-width:720px){.rs-field-row{grid-template-columns:1fr}}
.rs-field>span{display:block;font-family:var(--font-body);font-size:13.5px;font-weight:600;color:var(--ink);margin-bottom:8px}
.rs-field>span em{font-style:normal;font-weight:400;color:var(--ink-muted)}
/* 16px, not 15: below 16px iOS Safari auto-zooms the viewport on focus */
.rs-field input,.rs-field textarea{width:100%;font:inherit;font-size:16px;line-height:1.5;color:var(--ink);background:rgba(255,255,255,.5);border:1px solid var(--hairline);border-radius:8px;padding:13px 16px;min-height:44px;transition:border-color 200ms var(--ease),background 200ms var(--ease)}
.rs-field textarea{min-height:104px;resize:vertical}
.rs-field input::placeholder,.rs-field textarea::placeholder{color:var(--ink-muted)}
.rs-field input:focus-visible,.rs-field textarea:focus-visible{outline:2px solid var(--ink);outline-offset:2px;border-color:var(--ink)}
.rs-field input:user-invalid,.rs-field textarea:user-invalid{border-color:var(--error)}
.rs-formpanel .ct-form-note{color:var(--ink-muted)}
.rs-formpanel .ct-form-note.ct-ok{color:var(--ink);font-weight:500}
html.a11y-no-motion .rs-field input,html.a11y-no-motion .rs-field textarea{transition:none}
/* Dark theme: the frosted panel + fields hardcoded white literals, so --ink (which flips to
   near-white) rendered on a near-white ground — ~1.4:1, unreadable. Re-tint dark so the glass
   stays but the ink reads (2026-07-23 audit fix; keeps the sanctioned glassmorphism). */
html[data-theme="dark"] .rs-formpanel{background:rgba(20,20,23,.55);border-color:rgba(255,255,255,.12)}
html[data-theme="dark"] .rs-field input,html[data-theme="dark"] .rs-field textarea{background:rgba(255,255,255,.06)}

/* on the record — mono registry strip; codes link to their public registry */
.rs-reg-strip{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));margin-top:10px;border:1px solid var(--hairline);border-radius:var(--radius-card);overflow:hidden}
.rs-reg-strip>div{display:flex;flex-direction:column;gap:5px;padding:16px 18px;border-bottom:1px solid var(--hairline)}
.rs-reg-strip>div:nth-child(odd){border-right:1px solid var(--hairline)}
.rs-reg-strip>div:nth-last-child(-n+2){border-bottom:0}
.rs-reg-strip dt{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted)}
.rs-reg-strip dd{font-size:15px;color:var(--ink);overflow-wrap:anywhere}
.rs-reg-strip dd.num{font-family:var(--font-mono)}
.rs-reg-strip a{color:var(--ink);text-decoration:none;border-bottom:1px solid var(--hairline)}
.rs-reg-strip a:hover{color:var(--red-ink);border-color:var(--red-ink)}

/* ship-from — location ledger under the reused map */
/* facility photo pair under the contact map — same .fig object as career/about, two-up on
   desktop, stacked on phones */
.loc-figs{margin-top:clamp(20px,2.4vw,30px);display:grid;grid-template-columns:1fr 1fr;gap:20px}
@media(max-width:720px){.loc-figs{grid-template-columns:1fr}}
.rs-locs{margin-top:26px;display:grid;gap:0}
.rs-loc{display:grid;grid-template-columns:1fr auto;gap:6px 18px;align-items:baseline;padding:14px 0;border-bottom:1px solid var(--hairline)}
.rs-loc:last-child{border-bottom:0}
.rs-loc-name{font-weight:600;color:var(--ink)}
.rs-loc-meta{font-family:var(--font-mono);font-size:12.5px;font-weight:500;letter-spacing:.02em;color:var(--ink-muted)}

@media(max-width:640px){
  .rs-loc{grid-template-columns:1fr}
  .rs-reg-strip{grid-template-columns:1fr}
  .rs-reg-strip>div:nth-child(odd){border-right:0}
  .rs-reg-strip>div{border-bottom:1px solid var(--hairline)}
  .rs-reg-strip>div:last-child{border-bottom:0}
}

/* standout band — one step UP the paper ramp so a plain teaser section reads as a distinct
   moment rather than another default section. Its nine callers are the ecommerce photo-marquee
   strips ("the network behind it").

   This was .band-tint until 2026-08-31: a 6% red wash over --paper-100, which composites to
   #EEE3DF, a dusty pink. On a band whose whole job is to carry nine photographs that was the
   wrong ground twice over — it tinted the photos' surround, and stacked with the honeycomb
   field on top the band read heavy and stained. --paper-50 is the same move the press kit made
   when it dropped the wash (see ~5387): a neutral step on the ramp, no hue. DO NOT reintroduce
   a red wash here — red is spent on eyebrows, two-tone clauses and hover.

   --paper-50 is opaque, so this no longer needs the gradient-over-opaque-colour dance the wash
   did. The reason that dance existed is still worth knowing: --red-wash was rgba, and a band
   under a sticky hero is lifted over it by the :where() --paper-100 backdrop at ~1338 in
   tpl-critical.css; a shorthand `background` on a class beats that :where() rule (specificity
   0), so a bare rgba let the pinned hero show straight through. An opaque value has no such
   hole. Same family of trap as the .chfield/.csfield notes at ~1810. */
.band-step{background:var(--paper-50)}
/* ————— Region accents — the only non-red accents on the site; regions ONLY —————
   Modifier on .card (or any surface): coloured top rule + kicker. Inside .band-inverse
   (theme-invariant dark) the ink variants would fail contrast, so bright fixed values
   apply there — same convention as the band's other hard-coded surfaces. */
.card--gr{--region:var(--green-ink)}
.card--us{--region:var(--americas-ink)}
.card--in{--region:var(--india-ink)}
.band-inverse .card--gr{--region:#57B786}
.band-inverse .card--us{--region:#7FA8E8}
.band-inverse .card--in{--region:#F8993A}
.card--gr,.card--us,.card--in{border-top:3px solid var(--region)}
.card--gr .kicker,.card--us .kicker,.card--in .kicker{color:var(--region)}
/* region switcher dots (markup injected by tpl.js) */
.nbar-region-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--dot,var(--ink-muted))}

/* noise-textured band — light noise-bg.jpg / dark noisy-dark.webp, softened with the sitewide
   wash + multiply so text stays readable and any grid panel floats on top (same blend family
   as .mf / .aud / .gtee--noise). Used on branches' "Sell everywhere" section. */
.band-noise{background:linear-gradient(rgba(240,240,240,.75),rgba(240,240,240,.75)),#f0f0f0 url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noise-bg.jpg') center/cover no-repeat;background-blend-mode:normal,multiply}
html[data-theme="dark"] .band-noise{background:linear-gradient(rgba(13,13,14,.75),rgba(13,13,14,.75)),#0d0d0e url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp') center/cover no-repeat;background-blend-mode:normal,multiply}

/* ————— Branches globe (#group on branches.html) — the section is called "the group on the
   map", so it gets an actual map. A fixed dark viewport inside the light noise band: the globe
   needs a dark ground, and a bordered card is how this site frames a surface. The bento cells
   below stay the detail layer, so the map carries no copy of its own and nothing is duplicated.
   Canvas paints via assets/js/branches-globe.js (that page only — it embeds the geometry). */
.bglobe{position:relative;margin:0 0 clamp(24px,3vw,40px);border:1px solid var(--hairline);
  border-radius:var(--radius-card);overflow:hidden;background:var(--inverse);
  height:clamp(360px,42vw,520px);touch-action:pan-y;cursor:grab}
.bglobe.dragging{cursor:grabbing}
.bglobe canvas{display:block;width:100%;height:100%}
.bglobe .bglobe-hint{position:absolute;left:20px;bottom:16px;font-family:var(--font-mono);
  font-size:10px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;
  /* .38 white measured 3.48:1 on --inverse (audit H1, 2026-07-30) — .55 lands at 6.25:1 and
     still reads as a quiet hint. Literal, not a paper token: this card is theme-invariant. */
  color:rgba(255,255,255,.55);pointer-events:none}
/* focus buttons under the map: the keyboard/AT route into a canvas that cannot be tabbed */
.bglobe-keys{display:flex;gap:10px;flex-wrap:wrap;margin:-8px 0 clamp(24px,3vw,40px)}
.bglobe-keys button{font-family:var(--font-mono);font-size:10.5px;font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);background:transparent;
  border:1px solid var(--hairline);border-radius:999px;padding:7px 14px;min-height:36px;
  cursor:pointer;transition:color 140ms var(--ease),border-color 140ms var(--ease)}
.bglobe-keys button:hover{color:var(--ink);border-color:var(--ink)}
.bglobe-keys button[aria-pressed="true"]{color:var(--red-ink);border-color:var(--red-ink)}
/* Pin popup — deliberately the SAME object as the contact map's .tpl-pop (paper-0 surface,
   hairline border, 12px radius, the .lang-list shadow, mono kicker / display title / soft
   address). Adaptive tokens, so it flips with the theme like every other popup on the site. */
.bglobe .tip{position:absolute;z-index:2;pointer-events:none;opacity:0;
  transform:translate(-50%,calc(-100% - 16px));
  background:var(--paper-0);color:var(--ink);border:1px solid var(--hairline);border-radius:12px;
  box-shadow:0 12px 32px rgba(0,0,0,.18);padding:14px 16px;min-width:200px;max-width:280px;
  transition:opacity .14s var(--ease)}
.bglobe .tip::after{content:"";position:absolute;top:100%;left:50%;width:11px;height:11px;
  margin:-6px 0 0 -6px;background:var(--paper-0);
  border-right:1px solid var(--hairline);border-bottom:1px solid var(--hairline);
  transform:rotate(45deg)}
.bglobe .tip .pop-k{display:block;font-family:var(--font-mono);font-size:10.5px;font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:5px}
.bglobe .tip .pop-t{display:block;font-family:var(--font-display);font-weight:700;font-size:16px;
  letter-spacing:-.01em;margin-bottom:4px}
.bglobe .tip .pop-a{display:block;font-size:13px;color:var(--ink-soft)}
.bglobe .tip .pop-b{display:block;font-size:13px;line-height:1.5;color:var(--ink-soft);
  margin-top:8px;padding-top:8px;border-top:1px solid var(--hairline)}
@media(max-width:640px){.bglobe{height:clamp(300px,72vw,380px)}}
@media(prefers-reduced-motion:reduce){.bglobe .tip{transition:none}}
html.a11y-no-motion .bglobe .tip{transition:none}

/* ————— Branches bento grid (.bgrid) — hairline-divided directory of icon+title cells,
   the same gap:1px hairline technique as .kpi-band, wrapped in one rounded panel so the
   dividers read as continuous gridlines (ref: "modules for every business function").
   Region cells (.bcell--gr/--us/--in) reuse the sanctioned non-red region accents. */
.bgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--hairline);
  border:1px solid var(--hairline);border-radius:var(--radius-card);overflow:hidden}
.bgrid+.bgrid{margin-top:clamp(24px,3vw,40px)}
.bcell{position:relative;display:flex;flex-direction:column;gap:14px;text-decoration:none;color:inherit;
  min-height:clamp(190px,20vw,240px);padding:clamp(24px,2.2vw,36px);
  background:var(--paper-50);transition:background .3s var(--ease)}
@media(hover:hover) and (pointer:fine){.bcell:hover{background:var(--paper-0)}}
.bcell:focus-within{background:var(--paper-0)}
/* shared red conic-sweep outline, same recipe as .gtee-card::after / .mf-card::after. It hangs
   off the element that actually carries the card fill: .bcell-fly in the fly grid (where .bcell
   is only a static ghost slot), or .bcell itself when there is no fly inner. Region cells sweep
   in their own place colour (--region), matching the rule below that keeps geography — not brand
   chrome — as what those three cells say. */
.bcell-fly,.bcell:not(:has(>.bcell-fly)){position:relative}
.bcell-fly::after,.bcell:not(:has(>.bcell-fly))::after{content:"";position:absolute;inset:-1px;padding:1px;border-radius:4px;pointer-events:none;--sweep:0deg;background:conic-gradient(from 135deg,transparent 0 calc(180deg - var(--sweep)),var(--region,var(--red)) calc(180deg - var(--sweep)) calc(180deg + var(--sweep)),transparent calc(180deg + var(--sweep)));-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;transition:--sweep 280ms cubic-bezier(.2,0,0,1)}
@media(hover:hover) and (pointer:fine){
  .bcell:hover>.bcell-fly::after{--sweep:180deg}
  .bcell:not(:has(>.bcell-fly)):hover::after{--sweep:180deg}
}
.bcell:focus-within>.bcell-fly::after{--sweep:180deg}
.bcell:not(:has(>.bcell-fly)):focus-within::after{--sweep:180deg}
@media(prefers-reduced-motion:reduce){.bcell-fly::after,.bcell::after{transition:none}}
html.a11y-no-motion .bcell-fly::after,html.a11y-no-motion .bcell::after{transition:none}
.bcell-ico{width:44px;height:44px;color:var(--ink-muted);flex:none;
  transition:color .3s var(--ease)}
/* --red is the theme-invariant FILL token (2.26:1 on dark paper); an icon is a graphical object
   that needs 3:1, so hover uses the text-safe --red-ink (identical #AB0000 in light, 4.55:1 in dark) */
.bcell:hover .bcell-ico,.bcell:focus-within .bcell-ico{color:var(--red-ink)}
/* branch wordmark logos replace the icon in region cells — max-width:none guards against the
   global img{max-width:100%} collapsing a viewBox-only SVG; height sets the scale */
.bcell-logo{height:45px;width:auto;max-width:none;flex:none;align-self:flex-start}
/* dark-mode logo swap: the ink-text logos vanish on dark cells, so show the white/reverse
   variants instead (--l = light-theme logo, --d = dark-theme logo) */
.bcell-logo--d{display:none}
html[data-theme="dark"] .bcell-logo--l{display:none}
html[data-theme="dark"] .bcell-logo--d{display:block}
.bcell-body{margin-top:auto}
.bcell h3{font-size:19px;margin-bottom:6px}
.bcell p{font-size:14px;color:var(--ink-muted);line-height:1.5;max-width:34ch}
.bkick{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-muted);display:block;margin-bottom:8px}
/* region cells — icon + kicker take the place colour, and KEEP it on hover (not red),
   so geography, not brand chrome, is what these three cells say */
.bcell--gr{--region:var(--green-ink)} .bcell--us{--region:var(--americas-ink)} .bcell--in{--region:var(--india-ink)}
.bcell--gr .bcell-ico,.bcell--us .bcell-ico,.bcell--in .bcell-ico,
.bcell--gr:hover .bcell-ico,.bcell--us:hover .bcell-ico,.bcell--in:hover .bcell-ico,
.bcell--gr:focus-within .bcell-ico,.bcell--us:focus-within .bcell-ico,.bcell--in:focus-within .bcell-ico{color:var(--region)}
.bcell--gr .bkick,.bcell--us .bkick,.bcell--in .bkick{color:var(--region)}
/* branch cells top-align their text right under the logo (not pushed to the bottom like the
   icon cells) so the logos and kickers line up across the row regardless of copy length —
   Americas' 3-line paragraph just runs further down into the equal-height cell */
.bcell--gr .bcell-body,.bcell--us .bcell-body,.bcell--in .bcell-body{margin-top:0}
/* wide cells — feature row / holding block / closing CTA span the full grid width;
   their copy is not column-capped, so it flows across the free width into ~2 lines */
.bcell--wide{grid-column:1 / -1}
.bcell--wide p{max-width:62ch}
/* --w2 spans 2 of the 3 desktop columns (the Patras feature cell sits beside the 4th icon
   cell in row 2), not the full width --wide gives. This was an inline style on the article
   until 2026-07-29 — and an inline grid-column beats every media query, so at ≤560px the
   1-col grid grew an implicit 2nd track from `span 2` and the whole panel went 2-up in two
   uneven columns (measured 138px/195px at 390px). Any span that is narrower than the grid
   MUST be a class with a 1-col reset, never inline. */
.bcell--w2{grid-column:span 2}
/* across the whole mobile band --w2 is simply full width: in a 2-col grid `span 2` and
   `1 / -1` are the same cell, and in the 1-col floor only `1 / -1` is safe */
@media(max-width:820px){.bcell--w2{grid-column:1 / -1}}
.bcell--cta{grid-column:1 / -1;flex-direction:row;align-items:center;justify-content:space-between;
  gap:28px;flex-wrap:wrap;background:var(--paper-100)}
.bcell--cta .bcell-body{margin-top:0}
.bcell--cta p{max-width:52ch}
.bcell--cta h3{font-size:22px}
.bcell--cta .btn{flex:none}
/* ════ MOBILE BENTO (2026-07-29) ════ see the .gtee-grid block for the full rationale.
   The two-column band now reaches down to 360px instead of stopping at 560px, so the
   panels stay a bento on a phone. .bcell--wide / --cta / --w2 already span 1 / -1, which
   is what gives each panel its mixed rhythm.
   The nth-last-child rule fixes a pre-existing orphan: panel 2 is three region cells plus
   a full-width holding block, so in two columns the third region cell sat alone beside an
   empty half (visible at tablet width before today, not just on phones). Selecting the
   second-to-last cell at an odd position spans exactly that case and leaves panel 1, whose
   fourth cell is even, correctly paired.
   The last-child rule (2026-09-03) catches the OTHER orphan shape: a grid whose odd cell
   count ends on a plain cell — home #global-gateway's three region cells left TPL India
   beside an empty hairline slot. On branches every odd-count grid already ends on a
   --wide/--cta that spans 1/-1, so there the rule is a no-op. */
@media(min-width:360px) and (max-width:820px){
  .bgrid{grid-template-columns:1fr 1fr}
  .bgrid>.bcell:nth-last-child(2):nth-child(odd){grid-column:1 / -1}
  .bgrid>.bcell:last-child:nth-child(odd){grid-column:1 / -1}
}
/* compact half-cells: 126px of content, so type, icon and wordmark all step down */
@media(min-width:360px) and (max-width:559.98px){
  .bcell{padding:14px;gap:10px;min-height:0}
  .bcell h3{font-size:16px;line-height:1.2}
  .bcell p{font-size:13px;max-width:none}
  .bcell-ico{width:38px;height:38px}
  .bcell-logo{height:30px}
  .bkick{font-size:10px}
  /* cells that span the full width keep the original scale */
  .bgrid>.bcell--wide,.bgrid>.bcell--w2,.bgrid>.bcell--cta,
  .bgrid>.bcell:nth-last-child(2):nth-child(odd),
  .bgrid>.bcell:last-child:nth-child(odd){padding:20px}
  .bgrid>.bcell--wide h3,.bgrid>.bcell--w2 h3,.bgrid>.bcell--cta h3,
  .bgrid>.bcell:nth-last-child(2):nth-child(odd) h3,
  .bgrid>.bcell:last-child:nth-child(odd) h3{font-size:19px}
}
/* single-column floor */
@media(max-width:359.98px){
  .bgrid{grid-template-columns:1fr}
  .bgrid>.bcell:nth-last-child(2):nth-child(odd){grid-column:1 / -1}
}

/* ————— Branches bento fly-in — each tile floats in from off-page (L/T/R/B, cycling by
   position) and settles into its slot as the section scrolls in. Same driver + individual-
   translate technique as the home "What we do" bento (.wwd-card): .in is staggered from
   the section's scroll progress in tpl.js, because a tile parked at translate:-88vw would
   never "intersect" an IntersectionObserver.
     Two layers, so the LANDING SPOT reads as an empty outlined slot (not a grey block)
   while the tile is away: the outer .bcell stays put as a static ghost slot — page-colored
   fill (--paper-100) framed by the panel's existing 1px hairline gridlines (the gap:1px
   technique) — and a .bcell-fly inner carries the card's real fill, padding and content in
   and out on `translate`. Landed, the inner exactly overlays its slot (same box geometry, so
   no misalignment); flown, the ghost outline remains. The inner rides `translate` so the
   card's background/ico hover (which uses `background`) stays independent.
     .bgrid--fly drops the panel's overflow:hidden so the inner is visible travelling in from
   the page edges; the wrapping section carries overflow-x:clip (.sec-clip) to keep that
   travel from opening a horizontal scrollbar. Reveal-once, never re-hide on scroll-up. */
.bgrid--fly{overflow:visible}
.bgrid--fly>.bcell{padding:0;background:var(--paper-100)} /* static ghost slot: page-colored, outlined by the gridline gaps */
/* flex:1 1 auto grows the inner to the slot's full (grid-stretched) height, so a landed
   card always fills its ghost slot exactly — no gap when a row is taller than min-height */
.bcell-fly{display:flex;flex-direction:column;gap:14px;flex:1 1 auto;padding:clamp(24px,2.2vw,36px);
  background:var(--paper-50);opacity:0;translate:var(--fx,0) var(--fy,40px);will-change:translate,opacity;
  transition:opacity .6s var(--ease),translate .9s var(--ease),background .3s var(--ease)}
.bgrid--fly>.bcell.in>.bcell-fly{opacity:1;translate:0 0}
/* keyboard escape hatch: a control focused inside a not-yet-arrived tile must stay reachable */
.bgrid--fly>.bcell:focus-within>.bcell-fly{opacity:1;translate:0 0}
.bgrid--fly>.bcell:hover>.bcell-fly,.bgrid--fly>.bcell:focus-within>.bcell-fly{background:var(--paper-0)}
/* region + wide + cta inners inherit their cell modifier's layout/fill */
.bcell--gr>.bcell-fly,.bcell--us>.bcell-fly,.bcell--in>.bcell-fly{gap:16px}
.bcell--cta>.bcell-fly{flex-direction:row;align-items:center;justify-content:space-between;gap:28px;flex-wrap:wrap;background:var(--paper-100)}
.bcell--cta:hover>.bcell-fly{background:var(--paper-100)} /* cta keeps its fill on hover */
/* directional cycle by position — left, top, right, bottom, repeating, so a grid of any
   length pulls tiles from all four edges as the user asked (top/bottom/left/right).
   Set on the slot; the inner inherits --fx/--fy through the cascade. */
.bgrid--fly>.bcell:nth-child(4n+1){--fx:-88vw;--fy:0}    /* from the left  */
.bgrid--fly>.bcell:nth-child(4n+2){--fx:0;--fy:-82vh}    /* from the top   */
.bgrid--fly>.bcell:nth-child(4n+3){--fx:88vw;--fy:0}     /* from the right */
.bgrid--fly>.bcell:nth-child(4n+4){--fx:0;--fy:82vh}     /* from the bottom*/
.sec-clip{overflow-x:clip}
@media(prefers-reduced-motion:reduce){.bcell-fly{opacity:1;translate:none;transition:none}}
html.a11y-no-motion .bcell-fly{opacity:1;translate:none;transition:none}

/* footer — stacked bands on the brand-red primary colour only (no gradient, no art/video).
   Bands read as separate zones via the hairline dividers on .ft-member and .ft-legal. */
/* SANCTIONED red-surface exception (2026-07-29): the full-bleed red footer is the brand bookend
   on all 35 pages and by area the largest red on the site. It predates and outranks a literal
   reading of the 5%-red accent rule; recorded here so audits stop flagging it. The white
   logo-plates, hairline zoning and responsive work below all exist to serve it. Sibling
   exception: .band-red (values.html's company-name section). */
footer{position:relative;background:var(--red);color:rgba(255,255,255,.85);padding:clamp(48px,6vw,96px) 0 clamp(12px,1.4vw,22px);font-size:14px}
/* THE SITEMAP BAND ALONE takes the body measure; bands 2-4 run full-bleed (2026-08-29 owner
   direction, in two steps: all four were capped first, then the wordmark, trust shelf and legal
   row were sent back to full width). So the footer reads as an inset sitemap sitting on three
   edge-to-edge bands, and there IS a deliberate seam between .cols and the .tm-band under it —
   at 1600px the wordmark starts 160px outside the last sitemap column. That is the intent, not
   a regression; do not "fix" it by re-capping the lower three.
   `footer>.wrap`, not `footer .wrap`: the DIRECT CHILD is the sitemap band, and the only other
   .wrap in the footer is .ft-legal's inner one, which is full-bleed and overrides the base .wrap
   cap for itself (see .ft-legal>.wrap below). Below 1280px --max never binds, so tablet and
   phone are unaffected by any of this. */
footer>.wrap{max-width:var(--max);padding-inline:var(--bar-gutter)}
footer .cols{display:grid;grid-template-columns:2fr 1.2fr 1fr 1fr 1fr 1fr;gap:clamp(32px,2.6vw,64px);margin-bottom:48px}
/* Tablet + phone: the sitemap flows as balanced text columns instead of a grid, because a grid
   sizes each row by its tallest group and strands the short lists next to empty cells (at 768px
   "Contact" sat beside a wholly empty cell). Three columns here, two at <=640px. */
@media(max-width:1024px){
  footer .cols{display:block;columns:3;column-gap:32px;margin-bottom:40px}
  footer .cols>div{-webkit-column-break-inside:avoid;break-inside:avoid;margin-bottom:28px}
  footer .cols>div:first-child{-webkit-column-span:all;column-span:all;margin-bottom:30px}
  footer .cols>div:first-child p{max-width:52ch;text-wrap:pretty}
}
/* .ft-col-h, not a bare tag: these were <h4> and skipped the document outline from h2 straight
   to h4 on 24 pages (audit N11, 2026-07-30). They are now <h2 class="ft-col-h"> — structurally
   siblings of the page's section headings — so the type ramp has to be re-declared here rather
   than inherited, since the global h2 is a 30-48px display face.
   line-height is pinned to body's 1.6 on purpose: as <h4> these inherited it, and as <h2> they
   would otherwise pick up the global h1,h2,h3 line-height:1.05 and shorten the footer by 2-6px
   on every page. Measured before/after — 1.6 reproduces the old rendering exactly. */
footer .ft-col-h{font-family:var(--font-mono);font-size:12px;font-weight:500;line-height:1.6;letter-spacing:0.08em;text-transform:uppercase;color:rgba(255,255,255,.85);margin-bottom:14px}
footer a{color:rgba(255,255,255,.85);text-decoration:none;display:block;padding:3px 0}
footer a:hover{color:#fff}
/* roomier footer link tap targets on phones (~40px) — the desktop sitemap density
   (~28px) is fine with a mouse but tight for thumbs. Excludes the trust-logo pills. */
@media(max-width:640px){footer .cols a,footer .ft-legal a,footer .legal a{padding:9px 0}}
footer .legal{padding:24px 0 clamp(28px,4vw,40px);display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px;font-size:13px}
/* ===== the four utility links, in the legal row (2026-08-20) =====
   Terms, Privacy, Sitemap and Accessibility used to sit at the tail of the footer's Contact
   column, where the phone layout folds the whole .cols block into the .ft-acc accordion — so all
   four hid behind one tap on the pages a screen reader or a regulator is most likely to want them
   from. The 2026-08-06 spec had placed them here for exactly that reason and the implementation
   went the other way; this restores the spec (decided by the owner 2026-08-20, ADR-009).
   They move as one group, never split across footer regions.
   The row was already flex/space-between with two spans at its ends, so a third child lands the
   links in what was dead middle ground and the fine print keeps its outer positions.
   No display reset is needed on the links: they are flex items here, and a flex item is blockified
   whatever its own display says, so footer a{display:block} costs nothing and an `inline` here
   would be a dead declaration (measured: computed display is `block` at 1440 either way). Only the
   padding needs undoing. The ≤640px rule three lines above still wins on specificity for the thumb
   padding (0,1,2 over 0,1,1), and the 44px min-height below is what actually carries the touch
   target: measured 44px at 390, 21px at 1440. */
.legal-nav{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 18px}
.legal-nav a{padding:0}
/* The CMP "Privacy settings" control is a <button>, not a link: it opens a dialog and navigates
   nowhere, so an <a href> would be a lie and would put ?cmpscreen in the address bar. It still has
   to read as the fifth item in a row of four plain ones, so it repeats exactly what footer a and
   .legal-nav a give a link — colour, hover, display:block, no padding — on top of a UA reset.
   These live at TOP LEVEL on purpose: the first attempt put them inside the 640px block below,
   which left desktop showing a raw white system button. The touch-target pair is added to that
   block alongside .legal-nav a, because that is where the links get theirs.
   [hidden] is last and NOT redundant — display:block would otherwise beat the attribute. */
.legal-nav .cmp-btn{appearance:none;-webkit-appearance:none;background:none;border:0;margin:0;font:inherit;cursor:pointer;color:rgba(255,255,255,.85);text-decoration:none;display:block;padding:0}
.legal-nav .cmp-btn:hover{color:#fff}
.legal-nav .cmp-btn[disabled]{opacity:.5;cursor:not-allowed}
.legal-nav .cmp-btn[hidden]{display:none}
@media(max-width:640px){.legal-nav{gap:0 18px}.legal-nav a,.legal-nav .cmp-btn{display:flex;align-items:center;min-height:44px}
}
/* Above that breakpoint the same items measured 21px tall, which is under WCAG 2.2 AA 2.5.8's
   24px floor. It is not covered by the "inline target" exemption — this is a nav list, the same
   reasoning already written against `.crumb a` — and the spacing allowance only rescues the wide
   viewports: at 834px and up the row is a single 21px line with no vertical neighbour, but from
   641px it wraps to two rows just 2px apart, so 24px circles round each item would overlap.
   min-height rather than padding, matching the 44px rule above so both breakpoints express the
   floor the same way. The row grows 21px -> 24px; nothing else in the footer moves. */
@media(min-width:641px){.legal-nav a,.legal-nav .cmp-btn{display:inline-flex;align-items:center;min-height:24px}}
.ft-logo{height:var(--logo-h);width:auto;margin-bottom:18px;aspect-ratio:811.82/270.13}
.tm-band{max-width:none;margin:clamp(44px,6vw,76px) auto 0;padding:0 var(--bar-gutter)} /* full-bleed on the chrome gutter — the wordmark spans the whole footer, wider than the sitemap above it */
.tm-band img{display:block;width:100%;height:auto;aspect-ratio:982.79/110.94} /* intrinsic ratio — no CLS while the SVG loads */
.ft-member{border-top:1px solid rgba(255,255,255,.35);max-width:none;margin:clamp(26px,4vw,40px) auto 0;padding:clamp(30px,4.5vw,48px) var(--bar-gutter) clamp(34px,4.5vw,52px);display:flex;flex-direction:column;gap:clamp(16px,2vw,24px);container-type:inline-size}
/* quiet lead line above the trust logos, left-aligned to the page content (ported from option-A) */
.ft-lead{margin:0;max-width:74ch;text-align:left;font-size:clamp(12.5px,.3vw + 11px,14px);line-height:1.55;color:rgba(255,255,255,.82)}
/* ===== One plate model, two rows (2026-08-24 user direction) =====
   Direction: two rows, every box the same height, and "dont strect the contained boxes so hard".
   The 14 marks used to run 5 / 6 / 3 across three rows of 74 / 54 / 72px, each row floating at
   ~75% of the band. They now run 8 / 6 across two rows of ONE height, spanning the band edge to
   edge like the .tm-band wordmark above them (measured 48/1857 at 1920 - the same box). Document
   order is untouched; the split simply falls after SEVE, which is the point that balances the two
   rows best (measured: 8/6 leaves 1.05x of stretch at 1440 against 7/7's 1.24x).
   The height is DERIVED from the row width rather than hard-coded, and that is what removes the
   stretching. --art-h is the shared artwork height: the value at which row two - the width-hungry
   row, carrying both 8:1 EU banners plus PitchBook - fills the band exactly. Every plate sizes
   from it (--pl-h = art + 2 x --pl-py), so the ARTWORK grows into the plates instead of white
   space growing around it, and flex is left with only the few percent row one falls short by.
   Measured stretch: 1.05x at 1440 and 1.15x at 1920, against 1.33x-1.78x under the old model.
   Each row gets the same shape of formula: a divisor that is the sum of its width-to-art ratios
   (24.4628 for row two's five scaling marks with the .ft-sm reduction folded in; 17.344 for row
   one's eight, with the .55 badge-mark and .61 .ft-sm reductions folded in) over the band minus a
   --rN-fixed term for everything that does NOT scale - plate side padding, gaps, the LEI badge's
   fixed box, and row one's four badge labels (296px). Deriving those terms from the same tokens
   the plates use keeps them honest if either is ever retuned.
   The height is the MIN of the two, because which row is binding SWITCHES: row two is the wider
   one on desktop (both 8:1 EU banners), but row one's four fixed labels stop shrinking, so below
   ~1110px of band it is row one that runs out of room first. Taking row two's fit alone wrapped
   row one at 1080 (measured). Whichever row binds fills exactly; the other takes the few percent
   of flex stretch.
   --fit-slack is the one term that is not geometry. Solving the fit exactly leaves the binding
   row's natural width equal to the row to within a rounding error, and flexbox wraps on ANY
   overflow at all - measured 1169.1px of plates in a 1169.0px row at 1280, which cost a whole
   extra line for a tenth of a pixel. 3px aims the fit just short, and since flex-grow is on, the
   growth hands those 3px straight back: the row still fills exactly.
   The floor is 22px and it is a real constraint, not a round number: a floor ABOVE the computed
   value overrides it and makes a row wider than the band, which wraps it. At 28px that is exactly
   what happened between 1080 and ~1195 (measured: 4 lines at 1080). Capped at 64px so ultra-wide
   screens stop growing.
   The padding and gap scale with the viewport for the same reason: they are part of every --rN
   -fixed term, so at the narrow end the rows buy their room out of SPACING before they have to
   take it out of the artwork. Held at a flat 14px the eight plates of row one could not fit at
   1080 at any art height above the floor (measured). At 1440 the clamps land on 13.7 / 10.1 /
   12.2px, which is where they were hand-set before.
   Plate WIDTH still follows each mark's own artwork and never an equal share: the marks span
   1.97:1 to 8.04:1, and an equal-width grid letterboxes the wide ones (measured 2026-07-30). */
.ft-row{--pl-px:clamp(10px,.95vw,16px);--pl-py:clamp(8px,.7vw,12px);--pl-gap:clamp(8px,.85vw,14px);--lei-w:185.4px;--fit-slack:3px;--r2-fixed:calc(12 * var(--pl-px) + 5 * var(--pl-gap) + var(--lei-w) + var(--fit-slack));--r1-fixed:calc(16 * var(--pl-px) + 7 * var(--pl-gap) + 296px + var(--fit-slack));--art-h:clamp(22px,min(calc((100cqw - var(--r2-fixed)) / 24.4628),calc((100cqw - var(--r1-fixed)) / 17.344)),64px);--pl-h:calc(var(--art-h) + 2 * var(--pl-py));display:flex;align-items:stretch;justify-content:center;flex-wrap:wrap;gap:var(--pl-gap)}
.ft-row img,.ft-row>a{flex:0 0 auto;width:auto;height:var(--pl-h);box-sizing:border-box;background:#fff;border-radius:10px;padding:var(--pl-py) var(--pl-px);object-fit:contain;display:block}
/* Plates that link out to the issuing registry (GRECA, Google, SEVE, Crunchbase, PitchBook,
   2026-08-21 user direction): the anchor becomes the plate and the img fills its content box, so
   every sizing rule keeps working on `.ft-row>a` exactly as it does on a bare plate img. */
.ft-row>a{padding-left:0;padding-right:0;overflow:hidden}
.ft-row>a>img{height:100%;width:auto;max-width:100%;object-fit:contain;padding:0 var(--pl-px);background:none;border-radius:0;transition:transform .18s var(--ease-out,ease)}
.ft-row>a:hover>img{transform:scale(1.03)}
.ft-row>a:focus-visible{outline:2px solid var(--paper-100,#fff);outline-offset:3px}
@media(prefers-reduced-motion:reduce){.ft-row>a>img{transition:none}.ft-row>a:hover>img{transform:none}}
.ft-row-white img{filter:none;opacity:1}
/* Composed plates (Claude / Microsoft / Amazon / AWS, 2026-08-21): a brand mark plus a typeset
   label, because no single-file badge artwork exists for any of them. The label uses the site's
   mono kicker so it reads as TPL type, not as part of the vendor lockup, and a hairline keeps the
   two halves from fusing into one mark. The MARK scales with --art-h (.55 of it, which reproduces
   the old 24px mark at the old 46px art height); the LABEL alone stays a fixed size, and it is
   the only thing in either row that does not scale - which is exactly why row one, holding all
   four badges, is the row that comes up a few percent short and takes the small flex stretch. */
.ft-row .ft-badge{flex:0 0 auto;display:flex;align-items:center;justify-content:center;gap:11px;height:var(--pl-h);box-sizing:border-box;padding:0 var(--pl-px);background:#fff;border-radius:10px}
.ft-row .ft-badge img{height:calc(var(--art-h) * .55);width:auto;padding:0;background:none;border-radius:0;flex:0 0 auto}
.ft-row .ft-badge span{font-family:var(--font-mono);font-size:10.5px;font-weight:500;line-height:1.25;letter-spacing:.08em;text-transform:uppercase;color:#4a4a4a;border-left:1px solid #d5d5d5;padding-left:11px;max-width:10ch} /* 10ch, not 8: "CERTIFIED" is nine characters and the plate's overflow:hidden was clipping its last letter */
/* .ft-sm - the wide wordmarks (AmCham, Crunchbase, PitchBook). At the shared art height their
   4.4-6.6:1 ratios would produce plates that dwarf every neighbour, so they take extra vertical
   padding and render at .61 of it. That is the same ratio the three-row model used (24px of art
   against 39.2), now expressed against the derived height instead of hard-coded px. */
.ft-row .ft-sm{padding-block:calc(var(--pl-py) + var(--art-h) * .195)}
/* ===== Live LEI badge (2026-08-21 user direction) =====
   The static lei-verified.png is now only the fallback: leiservice.com's badge widget renders the
   real record (status, expiry, legal name) into #leiservice-badge, clearing whatever it finds
   there first, so the <img> inside doubles as the no-JS / widget-down state at zero extra cost.
   The widget hard-codes its own 343x64 box in inline styles, so it is scaled as a unit rather
   than restyled: --lei-s sets both the transform and the wrapper's laid-out size, which keeps the
   plate's width honest to the flex row (a bare transform would leave a 309px hole in the layout).
   309x93 is the pill's MEASURED size once it holds this record and inherits Inter, not the 343x64
   its inline styles suggest, which is a max-width and a min-height, not the box it settles at.
   The scale stays a FIXED unitless number and cannot be derived from --pl-h: it multiplies 309px
   and feeds scale(), so a length-valued token makes both declarations invalid, the widget renders
   unscaled at 309px and row two overflows by 116px (measured 2026-08-24). .6 is the largest scale
   whose 55.8px box still fits the 58.3px plate the formula produces at 1440, and this plate alone
   carries no vertical padding - padded like its neighbours the badge would have to shrink to ~.41
   and its 12px LEI line would be illegible.
   overflow:hidden on the plate is the guard if the record ever grows a line: it clips, rather
   than pushing the row apart. */
.ft-row .ft-lei{--lei-s:.6;flex:0 0 auto;display:flex;align-items:center;justify-content:center;height:var(--pl-h);box-sizing:border-box;padding:0 var(--pl-px);background:#fff;border-radius:10px;overflow:hidden}
.ft-lei-box{width:calc(309px * var(--lei-s));height:calc(93px * var(--lei-s));flex:0 0 auto}
.ft-lei-box>div{width:309px;height:93px;transform:scale(var(--lei-s));transform-origin:0 0}
/* the widget writes font-family:sans-serif inline on its own nodes; !important is the only way a
   stylesheet reaches past that, and the badge should read in the site's face like everything else */
.ft-lei *{font-family:var(--font-body)!important}
/* The plate rules (real height, plate padding, white ground) must not reach the widget's own
   images. Sizing them back is not `auto`: the widget sizes its marks with width/height ATTRIBUTES,
   and an attribute is only a presentational hint, so the plate rule outranks it and `auto` resolves
   to the source's full natural size (a 1200px flag inside a 58px pill). The two attribute selectors
   below restate the widget's own 22px status mark and 16px jurisdiction flag as real CSS. */
.ft-lei img{padding:0;background:none;border-radius:0}
.ft-lei-box img[width="22"]{width:22px;height:22px}
.ft-lei-box img[width="16"]{width:16px;height:16px}
.ft-lei-fb{width:auto;height:auto;max-width:100%;max-height:56px}
.ft-lei:hover .ft-lei-box{transform:scale(1.03)}
.ft-lei-box{transition:transform .18s var(--ease-out,ease)}
@media(prefers-reduced-motion:reduce){.ft-lei-box{transition:none}.ft-lei:hover .ft-lei-box{transform:none}}
/* ===== Both rows span the band, edge to edge =====
   Each plate's flex-grow is its own natural width at the 1440 art height, so the few percent row
   one falls short by is shared out IN PROPORTION to the artwork: every plate in a row scales by
   the same factor and the plate widths stay true to their marks. Weights are keyed to artwork
   FILENAMES, not nth-child positions, so a future reorder cannot silently mis-assign them.
   Gated at 1080px because the model needs single-line rows, and flex would stretch a wrapped
   orphan plate across the full row. Below the gate the tablet and phone stacks govern. */
@media(min-width:1080px){
  .ft-row>img,.ft-row>a,.ft-row>.ft-badge{flex-grow:var(--fg,0)}
  /* the LEI plate needs its own selector: `.ft-row .ft-lei` above is (0,2,0) and outranks
     `.ft-row>a` (0,1,2), so without this its grow stays 0 and its neighbours in row two
     swallow the whole row's slack (measured 2026-08-24: LEI 260px against a 467px share). */
  .ft-row>.ft-lei{flex-grow:var(--fg,0)}
  /* Auto-centre the art inside an anchor plate, which sits a touch wider than its art. Scoped
     off .ft-badge: a badge anchor has TWO children (art + label) and centres them itself with
     justify-content, so an auto margin there is not centring, it is stealing. Amazon is the
     only a.ft-badge, and it measured 18.3px left against 13.7px right with the img-to-span gap
     inflated from 11px to 15.7px -- flex resolves auto margins BEFORE justify-content, so the
     free space went into the margins instead of splitting evenly around the pair. */
  .ft-row>a:not(.ft-badge)>img{margin-inline:auto}
  /* row one - the partner badges, then the association marks */
  .ft-row>a:has(>img[src$="google-partner.svg"]){--fg:113}
  .ft-row>.ft-badge:has(>img[src$="claude.svg"]){--fg:199}
  .ft-row>.ft-badge:has(>img[src$="microsoft.svg"]){--fg:200}
  .ft-row>.ft-badge:has(>img[src$="amazon.svg"]){--fg:171}
  .ft-row>.ft-badge:has(>img[src$="aws.svg"]){--fg:137}
  .ft-row>a:has(>img[src$="greca.svg"]){--fg:103}
  .ft-row>img[src$="amcham-greece.svg"]{--fg:130}
  .ft-row>a:has(>img[src$="seve-greek-exporters.png"]){--fg:126}
  /* row two - the public registries, the LEI record and the EU banners */
  /* the LEI plate deliberately has NO weight: row two fills by construction, so the badge
     keeps its exact natural width and never stretches around a fixed-size widget. */
  /* the EETT plate links to gr-domain-names.html, so its weight has to be keyed on the
     ANCHOR the way the other linked registries above are: as `.ft-row>img` this stopped
     matching the moment the plate gained its href, and row two handed its slack to the
     neighbours instead.
     134 -> 156 with the registry-number artwork: the weight is a natural WIDTH, so it moves
     with the mark's ratio (2.7778 -> 3.3572) even though nothing about the link changed.
     Fitting fg = 38.0096 * ratio + 28.2410 (the 1440 art height and the two side paddings)
     reproduces all five of row two's committed weights to within .25px, and 3.3572 gives
     155.85. Left at 134 the plate is 14% under-weighted and its neighbours swallow the
     difference -- the same failure as the selector above, from the other direction. */
  .ft-row>a:has(>img[src$="eett-gr-registrar-alt.svg"]){--fg:156}
  .ft-row>a:has(>img[src$="crunchbase.svg"]){--fg:162}
  .ft-row>a:has(>img[src$="pitchbook.png"]){--fg:181}
  .ft-row>img[src$="next-gen.png"]{--fg:238}
  .ft-row>img[src$="espa-banner.png"]{--fg:334}
}
/* tablet: the same derived model, one step smaller. Only the two tokens change - plate
   height, badge marks, the .ft-sm inset and the LEI scale all follow from them, and the
   rows wrap as needed (they no longer have to fit on one line below the 1080 gate). */
@media(max-width:820px){.ft-logo{height:48px}.ft-row{--art-h:38px;--pl-px:16px}}
/* the hairline is back on the full-bleed band, so it spans the footer edge to edge like
   .ft-member's above it. The inner .wrap has to opt OUT of the base .wrap cap explicitly —
   footer>.wrap does not reach it, so without this it would centre at --max inside a full-bleed
   band and pull the legal row in on its own (2026-08-29). */
.ft-legal{border-top:1px solid rgba(255,255,255,.35)}
.ft-legal>.wrap{max-width:none;padding-inline:var(--bar-gutter)}
/* ===== Footer on phones (2026-07-28) =====
   Three measured defects at 390px, all from the desktop grid surviving into a narrow column:
   (1) `grid-template-columns:1fr 1fr` sized every row by its tallest cell, so "Audiences" (4
   links) beside "Company" (8) left a ~160px red void and "Contact" sat next to an empty cell —
   ~400px of dead space in a footer already 1560px tall. Multicol replaces it: the four link
   groups flow as two continuous balanced columns, document order preserved, each group kept
   whole by break-inside. (2) The brand block now spans both columns, so the address reads in
   two lines instead of ragging into five and breaking "New / Jersey" mid-phrase. (3) The three
   wide EU/LEI banners were being letterboxed to ~74x9px inside a 106px plate (object-fit
   contain on a 8:1 artwork in a 33%-basis flex item) — present but unreadable, so the colour
   banners go 1-up and the logo plates 2-up. Must stay after the max-width:820px .ft-row rule
   below it in source order, which it overrides. */
@media(max-width:640px){
  /* min-width:0 is restored HERE only: the natural-width model above leaves min-width at auto
     (= the plate's intrinsic width), which let the widest mark, amcham at 199px, ignore the
     50%-4px basis and push the stack from 3 lines to 4. Zeroing it hands control back to the
     flex basis so the directed 2-1-2-1-1-1 pattern holds; contain-centring inside the wider
     plate is the correct trade on phones, and was the 2026-07-29 direction. */
  .ft-row img,.ft-row>a,.ft-row .ft-badge{min-width:0}
  footer .cols{display:block;columns:2;column-gap:26px;margin-bottom:34px}
  footer .cols>div{-webkit-column-break-inside:avoid;break-inside:avoid;margin-bottom:26px}
  footer .cols>div:last-child{margin-bottom:0}
  footer .cols>div:first-child{-webkit-column-span:all;column-span:all;margin-bottom:30px}
  footer .cols>div:first-child p{max-width:38ch;text-wrap:pretty}
  footer .ft-col-h{margin-bottom:10px}
  /* flex-grow:0 so the odd logo left on the last row stays half-width and centres, instead of
     stretching to a full-width plate twice the size of the four above it.
     Basis is 50% - 4px because the row gap clamps to 8px at this width: at the old
     calc(50% - 7px) (written for the 14px desktop gap) each 2-up line carried 6px of slack,
     which justify-content:center split into a 3px inset per side — so the pill edges missed
     the full-width plates' edges (measured 23/367 against 20/370). */
  .ft-row-white>img,.ft-row-white>a,.ft-row-white>.ft-badge{flex:0 1 calc(50% - 4px)}
  .ft-row-white>.ft-badge{justify-content:center}
  /* The composed badges are the one plate whose content does not shrink with the plate, and a
     50%-basis plate on a phone is narrower than their mark plus label: measured 2026-08-24 at
     390px, Claude and Microsoft ran 39px past the pill's right edge (and ~29px before this
     model, so the squeeze is older than it). Nothing here is a layout trick - the mark, the
     label and the seam all just come down a step so the widest badge fits inside the plate. */
  .ft-row{--pl-px:12px}
  .ft-row .ft-badge{gap:8px}
  .ft-row .ft-badge img{height:calc(var(--art-h) * .39)}
  .ft-row .ft-badge span{font-size:9.5px;padding-left:8px;max-width:9ch}
  /* the two EU banners stay 1-up: at 8:1 a half-width plate letterboxes them to ~9px of art.
     They also go back to the full plate height here — the width-matching scale above is a
     desktop concern, and there is no first row to match once everything is stacked. */
  .ft-row>.ft-eu,.ft-row>.ft-lei{flex:1 1 100%}
  /* the per-row height and the .ft-eu inset are both gone: one --art-h now sets every plate,
     and at 38px art an 8:1 banner is 306px wide, so a full-width phone plate fits it without
     letterboxing - which is all the old padding was buying. gap drops to the 8px seam the
     stack is built around (the 50%-4px basis below is written against it). */
  .ft-row{--art-h:38px;--pl-px:16px;gap:8px}
  /* full-width plate on phones, so the badge gets its natural size back and centres */
  .ft-row .ft-lei{--lei-s:.9;height:auto;min-height:60px;justify-content:center;padding:6px 12px}
  .ft-lei-fb{max-height:74px}
  /* On phones the two logo rows read as ONE stack of plates, so every plate seam must be the
     same 8px the in-row wraps use. The band's default 16px gap was landing mid-stack (between
     Crunchbase and LEI: seams ran 8,8,8,16,8,8). Parent gap drops to the seam value and the
     lead line gets its breathing room back via its own margin. */
  .ft-member{gap:8px}
  .ft-lead{margin-bottom:10px}
}
/* Very narrow phones: even at the reduced step above, a 50%-basis plate is narrower than the
   widest badge's mark plus label - measured 32px over the pill at 320px (63px before the step,
   so this is an old defect, now closed). The four badges take a full-width plate here, exactly
   as the EU banners already do, which keeps every plate the same height. */
@media(max-width:359px){.ft-row-white>.ft-badge{flex:1 1 100%}}

/* ===== Footer sitemap accordion (phones, 2026-07-29) =====
   ≤640px each link group becomes a full-width dropdown row and the brand block centres around
   the white TPL mark instead of the wordmark (user direction). Entirely JS-built (tpl.js
   "Footer sitemap accordion"): the .ft-acc class, the buttons and the .ft-acc-body wrappers
   only exist when the enhancement is active, so with scripting off — or above 640px — the
   multicol rules above this block still describe the whole footer. .ft-acc-body wrappers do
   stay in the DOM at all widths once built; display:contents makes them invisible to the
   desktop/tablet layout. */
.ft-acc-body{display:contents}
footer .cols.ft-acc{display:block;columns:auto;margin-bottom:34px}
footer .cols.ft-acc>div{margin-bottom:0;-webkit-column-break-inside:auto;break-inside:auto}
/* brand block: centred mark, centred address, centred socials */
footer .cols.ft-acc>div:first-child{text-align:center;margin-bottom:26px}
footer .cols.ft-acc>div:first-child p{max-width:none;text-wrap:pretty}
footer .cols.ft-acc .ft-soc{justify-content:center}
/* the mark is square art — kill the wordmark's intrinsic ratio or the box stays 3:1 */
.ft-logo--mark{height:52px;width:52px;aspect-ratio:1/1;margin:0 auto 16px;display:block}
/* group rows: full-bleed hairlines, one per seam (the footer's own .35 separator alpha) */
footer .cols.ft-acc>div+div{border-top:1px solid rgba(255,255,255,.35)}
footer .cols.ft-acc>div:last-child{border-bottom:1px solid rgba(255,255,255,.35);margin-bottom:0}
footer .cols.ft-acc .ft-col-h{margin:0}
.ft-acc-btn{display:flex;width:100%;align-items:center;justify-content:space-between;gap:12px;background:none;border:0;color:inherit;font:inherit;letter-spacing:inherit;text-transform:inherit;padding:15px 0;min-height:48px;cursor:pointer;text-align:left}
.ft-acc-btn .chev{width:16px;height:16px;flex:none;transition:transform 240ms var(--ease)}
.ft-acc-btn[aria-expanded="true"] .chev{transform:rotate(180deg)}
/* 0fr → 1fr row animation; visibility flips discretely at the transition edge so collapsed
   links can't take keyboard focus (WCAG 2.4.3) */
footer .cols.ft-acc .ft-acc-body{display:grid;grid-template-rows:0fr;transition:grid-template-rows 320ms var(--ease)}
footer .cols.ft-acc .ft-acc-inner{overflow:hidden;min-height:0;visibility:hidden;transition:visibility 320ms}
footer .cols.ft-acc>div.open .ft-acc-body{grid-template-rows:1fr}
footer .cols.ft-acc>div.open .ft-acc-inner{visibility:visible}
footer .cols.ft-acc .ft-acc-inner a:last-child{padding-bottom:18px}
@media(prefers-reduced-motion:reduce){
  footer .cols.ft-acc .ft-acc-body,footer .cols.ft-acc .ft-acc-inner,.ft-acc-btn .chev{transition:none}
}
html.a11y-no-motion footer .cols.ft-acc .ft-acc-body,html.a11y-no-motion footer .cols.ft-acc .ft-acc-inner,html.a11y-no-motion .ft-acc-btn .chev{transition:none}

/* misc */
.hairline-list{list-style:none}
.hairline-list li{padding:18px 0;border-bottom:1px solid var(--hairline);display:flex;gap:16px;justify-content:space-between;align-items:baseline;flex-wrap:wrap}
/* manifest list — key/value rows (home "what we do" panel replaced by the bento;
   still used on press.html) */
.manifest{margin:0;border-top:1px solid var(--hairline)}
.manifest>div{display:grid;grid-template-columns:92px 1fr;gap:16px;padding:12px 0;border-bottom:1px solid var(--hairline);align-items:baseline}
/* dt carries .kicker, whose --ink-muted measured 4.15:1 on the warm ground .band-tint used to
   paint (audit H1, 2026-07-30) — --ink-soft takes it to ~7:1 and keeps the label quiet. That
   wash is gone (.band-step, ~3040), but the fix stays: --ink-muted was marginal on paper too. */
.manifest dt{margin:0}
.manifest dt.kicker{color:var(--ink-soft)}
.manifest dd{margin:0;font-family:var(--font-mono);font-weight:500;font-size:13.5px;line-height:1.5;color:var(--ink-soft)}
@media(max-width:560px){.manifest>div{grid-template-columns:1fr;gap:3px}}
/* ── what we do — THE STATEMENT (.wwx): one straight sentence, one animated sketch ──
   Sits at the top of #what-we-do, above the 5-card bento (owner call 2026-08-21, on the
   ponera.fr hero as the reference): a visitor should be able to read ONE sentence and know
   what TPL does, before the bento breaks it into steps. Copy left, sketch right; one column
   below 900px.
   THE CYCLING CLAUSE IS THE EXISTING .hrot ROTATOR, not a second one — markup, motion gating,
   the aria-hidden slot and the .sr-only full sentence all come from the news.html hero
   component (~6868). Two instance overrides only, both about width: .hrot-slot is inline-grid
   there because it sits mid-line in an h1; here it is the whole second line of the heading, and
   an inline-grid sized to its widest phrase ("in our 10 storefronts.") overflows a 320px
   viewport at the h2's clamp floor — the site's own sideways-drag trap. Block grid + normal
   wrapping keeps it inside the column at every width, and stacking still means no reflow as the
   phrases swap.
   THE SKETCH is the .wsm sales map below (assets/js/sales-map.js), which replaced the
   isometric channel fan on 2026-08-21 — see the .wsm block for why.
   The labels are HTML, not <text>: they stay at the page's own type size on a phone, they
   translate, and they are what carries the meaning — so the svg itself is aria-hidden. */
.wwx{display:block;margin:0 0 clamp(36px,4.6vw,60px)}
.wwx:last-child{margin-bottom:0}
/* The masthead: heading left, the two paragraphs and the buttons right. One column below 900px. */
.wwx-copy{display:grid;grid-template-columns:minmax(0,1.02fr) minmax(0,.98fr);
  gap:clamp(24px,3.4vw,56px);align-items:end;margin:0 0 clamp(26px,3.2vw,44px)}
.wwx-lead,.wwx-say{min-width:0}
.wwx-h{margin-bottom:0;max-width:20ch;text-wrap:balance}
.wwx-h .hrot-slot{display:grid;max-width:100%}
.wwx-h .hrot-i{white-space:normal}
/* the shared .hrot transition, restated here because the slot is a block grid in this instance */
.wwx-h .hrot-i{transition:opacity 200ms var(--ease),transform 320ms var(--ease)}
.wwx-h .hrot-i.is-on{transition:opacity 300ms var(--ease) 150ms,transform 420ms var(--ease) 150ms}
.wwx-say .lede{margin:0 0 16px;max-width:58ch}
.wwx-say .lede:last-of-type{margin-bottom:0}
.wwx-cta{display:flex;flex-wrap:wrap;gap:14px;margin:22px 0 0}
/* the figure: the map over its own legend */
.wwx-fig{margin:0;min-width:0;display:flex;flex-direction:column;gap:clamp(14px,1.6vw,22px)}
.wwx-leg{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px 20px}
.wwx-leg li{min-width:0;display:flex;flex-direction:column;gap:4px;padding-left:12px;border-left:2px solid var(--hairline)}
.wwx-leg b{font-family:var(--font-mono);font-weight:500;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--red-ink)}
.wwx-leg span{font-size:13.5px;line-height:1.45;color:var(--ink-soft);overflow-wrap:anywhere}
@media(max-width:900px){
  .wwx-copy{grid-template-columns:minmax(0,1fr);gap:clamp(20px,3.4vw,28px);align-items:start}
  .wwx-h{max-width:none}
  .wwx-leg{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media(max-width:560px){.wwx-leg{grid-template-columns:minmax(0,1fr)}}
/* ── the sales map (.wsm) — the sketch inside .wwx, replacing the isometric channel fan
   (owner call, 2026-08-21). WHY: the fan showed four abstract pads; the statement it illustrates
   is "we sell in 125+ countries, on 11 channels", and a dotted world map where a country blinks
   red and pops the channel's own logo plus a euro amount says that in one glance.
   THE DOTS ARE THE POINT: countries are a dot lattice on the page's own grid, never filled
   shapes — the same restraint as the branches globe, and the same public-domain land geometry
   (assets/js/sales-map.js carries it inline, index.html only). Red is the sale and nothing else:
   the lattice is --ink-soft at 50%, the markets --ink-muted, and only the blink, the ring and the
   amount are brand red — the channel logos in the popups are the one place brand colour other
   than ours appears, because recognising the mark is the whole point of showing it. Popups are HTML, not canvas <text>, so they keep the page's type size on
   a phone and translate with the rest of the copy; the canvas is aria-hidden and the role="img"
   label on .wsm plus the legend below carry the meaning.
   NO FRAME AND NO PLATE (owner call, 2026-08-21): the map sits straight on the section ground, so
   the dots read as part of the page rather than as a widget in a box. It is also the full column
   width, at an aspect ratio close to the projection's own (134 degrees of latitude over 360 of
   longitude), so the continents are not stretched to fill a card.
   MOTION OFF (prefers-reduced-motion or html.a11y-no-motion, re-checked per frame in the module):
   the map draws once with every market lit red and no popups at all — the statement survives
   without a single moving pixel. .is-dead is the geometry-failed fallback: the box collapses
   rather than sitting there empty. */
.wsm{position:relative;width:100%;aspect-ratio:2.55/1;overflow:hidden}
.wsm.is-dead{display:none}
.wsm canvas{display:block;width:100%;height:100%}
.wsm-tips{position:absolute;inset:0;pointer-events:none}
/* Anchored ON the lattice square the module wrote into left/top, then nudged clear of it. --tx /
   --ty flip the card away from whichever edge the marker is near: sideways in the right-hand
   third, downwards in the top strip (Scandinavia, Canada) where a card above the dot would clip. */
.wsm-tip{position:absolute;display:grid;grid-template-columns:minmax(0,1fr);justify-items:start;
  gap:5px;width:110px;text-align:left;padding:9px 10px;
  border:1px solid var(--hairline);border-radius:12px;
  background:var(--paper-0);
  background:color-mix(in srgb, var(--paper-0) 86%, transparent);
  box-shadow:0 8px 22px rgba(0,0,0,0.14);
  opacity:0;
  /* A CROSS-FADE AND NOTHING ELSE (owner call, 2026-08-21): 2s in, 2s out, no slide and no scale —
     the card appears where it belongs and only its opacity moves, so three of them overlapping
     never reads as things flying around the map. The 2000ms here is the same number as FADE in
     sales-map.js, which times the marker's ramps and when .is-off goes on; change both together.
     Left and top are written per sale by the module, clamped inside the box. */
  transition:opacity 2000ms cubic-bezier(.4,0,.6,1)}
.wsm-tip.is-on{opacity:1}
.wsm-tip.is-off{opacity:0}
/* THE CHANNEL LOGO: centred, and as wide as the card allows with 5px of clear space each side
   (owner call, 2026-08-21) — so it breaks out of the card's 10px padding by 5px on each side and
   is capped in height, which keeps a square mark (our own) from filling the card while a long
   wordmark (Walmart, Zalando) still uses the full 100px. */
.wsm-ch{width:100px;margin:0 -5px 1px;justify-self:center;display:grid;place-items:center;
  line-height:0;border-radius:6px}
.wsm-ch img{display:block;width:100%;height:auto;max-height:30px;object-fit:contain}
html[data-theme="dark"] .wsm-ch{background:rgba(255,255,255,.93);padding:4px 5px}
/* THE PRODUCT TILE: the real packshot of what was sold, on a white plate. The files are baked to
   exactly 80x80 (assets/images/products/<category>/NN.png) and the tile renders at 80 CSS px, so
   the image is 1:1 and never resampled — swap in 160px files and drop `image-rendering` if we ever
   want it crisp on 2x screens too. The plate and its ink are theme-invariant literals (the
   band-inverse convention): a product photo does not invert in dark mode, so neither does the tile
   under it. The <svg> is the per-category line glyph, shown only until .has-pic says the photo
   decoded, so a missing file degrades to a drawing instead of a broken image. */
.wsm-pic{width:90px;height:80px;justify-self:center;position:relative;display:grid;place-items:center;
  background:#fff;border:1px solid rgba(0,0,0,.1);border-radius:8px;overflow:hidden}
.wsm-pic img,.wsm-pic svg{grid-area:1/1}
.wsm-pic img{width:80px;height:80px;object-fit:contain;opacity:0;transition:opacity 140ms var(--ease)}
.wsm-pic.has-pic img{opacity:1}
.wsm-pic.has-pic svg{opacity:0}
.wsm-pic svg{width:46px;height:46px;color:#33322f}
@media(prefers-reduced-motion:reduce){.wsm-pic img{transition:none}}
/* The stack, top to bottom (owner call, 2026-08-21): channel logo, the packshot, then country,
   category and price — WHERE it sold, WHAT sold, and WHAT IT MADE as the line the eye lands on
   last. The card is a fixed 110px column so every sale is the same object regardless of how wide
   the channel's wordmark is; the height is whatever the five rows need. */
.wsm-loc{margin-top:1px;font-family:var(--font-display);font-weight:700;font-size:13.5px;
  letter-spacing:-.01em;line-height:1.2;color:var(--ink);overflow-wrap:anywhere}
.wsm-cat{font-family:var(--font-mono);font-weight:500;font-size:9.5px;letter-spacing:.06em;
  text-transform:uppercase;line-height:1.3;color:var(--ink-soft);overflow-wrap:anywhere}
/* The price is the payoff line, and the only red in the card. */
.wsm-amt{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.01em;
  line-height:1.15;color:var(--red-ink);overflow-wrap:anywhere}
/* CARRIER MODE (couriers/dhl.html): the same payoff slot carries a waybill number instead of an
   amount. Mono, because it is a number to read digit by digit and not a value to feel, and small
   enough that 11 characters still fit the card's 90px of content width without wrapping. */
.wsm-amt.wsm-wb{font-family:var(--font-mono);font-weight:600;font-size:12.5px;letter-spacing:.02em;
  line-height:1.2}
.wsm-note{margin:0;font-family:var(--font-mono);font-size:10.5px;font-weight:500;
  letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted)}
@media(max-width:900px){.wsm{aspect-ratio:2.1/1}}
/* PHONES: the card stops chasing the marker. Below 700px the map box is ~350x206 and the card is
   ~190x90, so more than a quarter of the map — no flip can keep it inside next to its dot, and it
   covered whichever country it pointed at (measured 2026-08-21). So it parks in the bottom-left
   corner and reads as a ticker: the country still blinks red where it happened, the card says what
   sold and for how much. Only one card is ever up on a phone (maxTips() in the module returns 1
   below 700px and the sale gap is longer than the card's life), so nothing stacks in that corner.
   The !important overrides are unavoidable — left and top are inline styles written per sale. */
@media(max-width:700px){
  .wsm-tip{left:8px !important;top:auto !important;bottom:8px;transform-origin:0 100%}
}
/* Phones get a taller box: the parked card is ~204px and a 1.7 ratio only gave the box 206px, so
   the card sat 6px out of the frame (measured 2026-08-21 at 390px). 1.28 leaves it air and makes
   the lattice easier to read at the same time. */
@media(max-width:640px){
  .wsm{aspect-ratio:1.28/1}
}
@media(prefers-reduced-motion:reduce){.wsm-tip,.wsm-tip.is-off{transition:none}}
html.a11y-no-motion .wsm-tip,html.a11y-no-motion .wsm-tip.is-off{transition:none}
/* ── what we do — bento: isometric line-icons that assemble as cards scroll in.
   Surface + red-sweep hover mirror .mf-card / .audience-tiles; the heading is a
   standard .two-tone (picks up the site's scroll-fill reveal). ── */
/* same blend-to-soften rule as #write (see there) — paper-100 wash + multiply the bg image
   into the base color at ~25% visible strength, instead of the old flat .22 scrim */
.wwd{background:linear-gradient(rgba(242,241,237,.75),rgba(242,241,237,.75)),url(https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/bg-3d-shapes.webp) center/cover no-repeat var(--paper-100);background-blend-mode:normal,multiply;padding:clamp(40px,4.5vw,64px) 0;overflow:clip;position:relative}
html[data-theme="dark"] .wwd{background:var(--paper-100)} /* light 3D-shapes bg is light-mode only */
.wwd>.wrap{position:relative;z-index:2}
.wwd-head{max-width:820px;margin:0 0 clamp(20px,2.6vw,36px)}
.wwd-head .eyebrow{margin-bottom:10px}
.wwd-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(12px,1.2vw,18px)}
/* Each card flies in from its own --fx/--fy/--fr offset as the section scrolls in
   (.in, staggered in tpl.js). Fly-in rides the individual translate/rotate props
   so the hover lift (transform) stays independent and snappy. */
.wwd-card{position:relative;display:flex;flex-direction:column;gap:10px;padding:clamp(18px,1.9vw,26px);background:color-mix(in srgb,var(--paper-0) 68%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.08);backdrop-filter:blur(14px) saturate(1.08);border:1px solid var(--hairline);border-radius:3px;opacity:0;translate:var(--fx,0) var(--fy,40px);will-change:transform,translate;transition:opacity .6s var(--ease),translate .9s var(--ease),transform .3s var(--ease),border-color .3s var(--ease),box-shadow .3s var(--ease)}
/* solid fallback where backdrop-filter isn't supported, so card text stays legible */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.wwd-card{background:var(--paper-0)}}
.wwd-card.in{opacity:1;translate:0 0}
/* per-position fly-in vectors — cards slide in upright from different edges as the section scrolls in (no rotation) */
.wwd-grid>.wwd-card:nth-child(1){--fx:-95vw;--fy:0}
.wwd-grid>.wwd-card:nth-child(2){--fx:20vw;--fy:-80vh}
.wwd-grid>.wwd-card:nth-child(3){--fx:95vw;--fy:0}
.wwd-grid>.wwd-card:nth-child(4){--fx:-80vw;--fy:35vh}
.wwd-grid>.wwd-card:nth-child(5){--fx:90vw;--fy:25vh}
.wwd-card.wwd-wide,.wwd-card.wwd-wide-r{flex-direction:row;align-items:center;gap:clamp(20px,2.5vw,32px)}
.wwd-card.wwd-wide{grid-column:1 / 3}
.wwd-card.wwd-wide-r{grid-column:2 / 4}
.wwd-copy{display:flex;flex-direction:column;gap:10px}
.wwd-card.wwd-wide .wwd-copy,.wwd-card.wwd-wide-r .wwd-copy{flex:1}
/* hover: border darkens to ink, card lifts, red conic sweep traces the outline (shared @property --sweep) */
.wwd-card:focus-within{border-color:var(--ink);transform:translateY(-3px)}
@media (hover:hover) and (pointer:fine){.wwd-card:hover{border-color:var(--ink);transform:translateY(-3px)}}
.wwd-card:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
.wwd-card::after{content:"";position:absolute;inset:-1px;padding:1px;border-radius:4px;pointer-events:none;--sweep:0deg;background:conic-gradient(from 135deg,transparent 0 calc(180deg - var(--sweep)),var(--red) calc(180deg - var(--sweep)) calc(180deg + var(--sweep)),transparent calc(180deg + var(--sweep)));-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;transition:--sweep 280ms cubic-bezier(.2,0,0,1)}
.wwd-card:focus-within::after{--sweep:180deg}
@media (hover:hover) and (pointer:fine){.wwd-card:hover::after{--sweep:180deg}}
.wwd-kick{font-family:var(--font-mono);font-weight:500;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--red-ink)}
.wwd-kick .wwd-n{color:var(--ink-muted);margin-right:8px}
.wwd-card h3{font-family:var(--font-display);font-weight:800;font-size:clamp(20px,1.55vw,25px);letter-spacing:-.02em;line-height:1.1;color:var(--ink);margin:0}
.wwd-card.wwd-wide h3,.wwd-card.wwd-wide-r h3{font-size:clamp(23px,2vw,29px)}
.wwd-meta{font-family:var(--font-mono);font-size:12px;letter-spacing:.04em;color:var(--ink-muted);margin:0}
.wwd-note{font-family:var(--font-body);font-size:14px;color:var(--ink-soft);margin:0}
/* Hover-reveal brand logos on the WWD platform lines — DESKTOP ONLY (hover-capable, fine pointer).
   The word stays put; its brand mark pops up on a small card above it (bottom:100% → zero layout shift).
   The plate is a FIXED white surface in BOTH themes on purpose: the partner SVGs are full-colour
   wordmarks whose lettering is near-black (Amazon, Meta) and would disappear on a dark card — this is
   the same reason the brand marquee hard-codes a colour hack. On touch (no :hover) the pop never renders. */
.wwd-brand{position:relative;white-space:nowrap}
.wwd-brand>.wwd-brand-pop{display:none}
@media (hover:hover) and (pointer:fine){
  .wwd-brand{transition:color .18s var(--ease)}
  .wwd-brand::after{content:"";position:absolute;left:0;right:0;bottom:-3px;height:1px;background:var(--red-ink);transform:scaleX(0);transform-origin:left center;transition:transform .22s var(--ease)}
  .wwd-brand:hover{color:var(--ink)}
  .wwd-brand:hover::after{transform:scaleX(1)}
  .wwd-brand>.wwd-brand-pop{display:block;position:absolute;left:50%;bottom:100%;margin-bottom:9px;transform:translate(-50%,5px);opacity:0;pointer-events:none;z-index:6;background:var(--paper-fixed);border:1px solid var(--hairline-fixed);border-radius:8px;padding:8px 11px;box-shadow:var(--shadow-popover-fixed);transition:opacity .2s var(--ease),transform .2s var(--ease)}
  .wwd-brand>.wwd-brand-pop::after{content:"";position:absolute;top:100%;left:50%;transform:translateX(-50%);border:5px solid transparent;border-top-color:var(--paper-fixed);filter:drop-shadow(0 1px 0 var(--hairline-fixed))}
  /* Explicit width + max-width:none: partner SVGs are viewBox-only (no intrinsic width) AND the
     global `img{max-width:100%}` reset resolves against this shrink-to-fit absolute chip, which
     would otherwise collapse the mark to 0. All four marks are 240x100 (2.4:1) → 22px tall = 53px. */
  /* width was pinned to 53px alongside height:22px, which squashed every partner wordmark off
     its true aspect: Amazon by 20.9%, Google 29.5%, Meta 18.9% (measured 2026-07-30). Distorting
     a trademark is not a style choice. Height stays fixed so the chips share an optical size and
     width follows the artwork; max-width:none is still required, because the global
     img{max-width:100%} collapses a viewBox-only SVG to zero inside this absolute box. */
  .wwd-brand>.wwd-brand-pop img{display:block;height:22px;width:auto;max-width:none}
  .wwd-brand:hover>.wwd-brand-pop{opacity:1;transform:translate(-50%,0)}
}
@media (hover:hover) and (pointer:fine) and (prefers-reduced-motion:reduce){
  .wwd-brand>.wwd-brand-pop,.wwd-brand:hover>.wwd-brand-pop{transform:translate(-50%,0);transition:none}
  .wwd-brand::after{transition:none}
}
/* in-page "Pause animations" toggle (html.a11y-no-motion) — pure-CSS :hover needs an explicit rule;
   the OS-level media query above doesn't cover it. Mirrors the .wwd-card pattern below. */
html.a11y-no-motion .wwd-brand::after{transition:none}
html.a11y-no-motion .wwd-brand>.wwd-brand-pop{transition:none}
html.a11y-no-motion .wwd-brand:hover>.wwd-brand-pop{transform:translate(-50%,0)}
/* isometric icon — line-art; cube faces filled with the card colour so cubes occlude */
.bico{color:var(--ink-soft);flex:none}
.bico svg{width:100%;height:100%;display:block}
.bico path[fill="#fff"]{fill:var(--paper-0)}
.wwd-card:focus-within .bico{color:var(--red-ink)}
@media (hover:hover) and (pointer:fine){.wwd-card:hover .bico{color:var(--red-ink)}}
/* icon parts scatter+fade until the card reveals (.in), then assemble; hover nudges them */
.wwd-card .pc{transition:transform .9s var(--ease),opacity .5s ease;transition-delay:calc(var(--td,0s) * 4 + .25s)}
.wwd-card:not(.in) .pc{opacity:0;transform:translate(calc(var(--dx,0px) * 9),calc(var(--dy,0px) * 9))}
/* the shared .pc rule above is also this card's one-shot 900ms scroll-entrance (the
   scatter-to-assembled reveal, staggered by --td) — reusing it for the hover nudge meant
   hovering an already-revealed card took nearly a full second to respond. Giving the nudge
   its own fast transition here, scoped to :hover/:focus-within specifically rather than to
   .in alone, keeps the entrance at its original pace: a bare ".in .pc" override would win
   the cascade the instant .in is ADDED too, since that's the "after-change" style browsers
   use to decide a transition's duration — which would have crushed the entrance to 400ms
   as a side effect. One asymmetry remains: leaving hover reverts to the 900ms rule since
   :hover no longer matches at that instant, so the return-to-rest nudge is slower than the
   approach nudge. Fixing that fully needs a JS-added "settled" class after the entrance's
   own transitionend, which touches the scroll-progress reveal loop — judged not worth the
   risk for what the approach direction already fixes (the noticeable, repeated case). */
.wwd-card.in:focus-within .pc{transform:translate(var(--dx,0),var(--dy,0));
  transition:transform .4s cubic-bezier(.22,1,.36,1)}
@media (hover:hover) and (pointer:fine){.wwd-card.in:hover .pc{transform:translate(var(--dx,0),var(--dy,0));
  transition:transform .4s cubic-bezier(.22,1,.36,1)}}
/* support card — chat "typing…" dots. Idle static; pulse L→R on hover (desktop) or
   continuously on touch (no hover). Colour follows currentColor: red on hover, ink on touch. */
.wwd .typ circle{transform-box:fill-box;transform-origin:center}
/* support-card "typing…" dot pulse (wwd-typing) removed 2026-07-22 — motion trim.
   The three dots render static on every device. */
@media(prefers-reduced-motion:reduce){.wwd .typ circle{animation:none}}
html.a11y-no-motion .wwd .typ circle{animation:none}
/* ════ MOBILE BENTO (2026-07-29) ════ see the .gtee-grid block for the full rationale.
   Two authored compositions live under one selector here, and they differ:
     home (has a .wwd-wide feature cell)  ->  [wide full] / [2][3] / [4][5]
     channel pages (five plain cells)     ->  [1][2] / [3][4] / [5 full]
   They cannot share a generic orphan rule. `:last-child:nth-child(odd)` would make cell 5
   span on BOTH, which is right for the channels but leaves cell 4 alone beside an empty
   half on home, because the full-width cell at position 1 shifts the parity of everything
   after it. So :has() splits them: the home grid pins its own arrangement, the channel
   grid gets the odd-last-cell span. :has() is already used elsewhere in this sheet. */
@media(min-width:360px) and (max-width:860px){
  .wwd-grid{grid-template-columns:1fr 1fr}
  /* home: the feature cell spans, and cell 5 gives up its desktop wide-r span to pair with 4 */
  .wwd-grid:has(>.wwd-wide)>.wwd-card.wwd-wide{grid-column:1 / -1}
  .wwd-grid:has(>.wwd-wide)>.wwd-card.wwd-wide-r{grid-column:auto}
  /* channels: five plain cells, so the last one spans rather than sit next to a gap */
  .wwd-grid:not(:has(>.wwd-wide))>.wwd-card:last-child:nth-child(odd){grid-column:1 / -1}
  /* .wwd-wide stays a row (copy left, icon right) because it is full width here, and that
     is what makes it read as the feature cell. .wwd-wide-r becomes a half cell, so it has
     to match its plain siblings: icon on top, copy under it. Its DOM order is
     [copy, icon] (authored for the row layout), so column-reverse is what puts the icon
     first, NOT column, which would leave the icon stranded at the bottom of the card. */
  .wwd-card.wwd-wide-r{flex-direction:column-reverse;justify-content:flex-end;align-items:flex-start}
  .bico{width:120px !important;height:120px !important}
}
/* compact half-cells: 133px of content cannot carry a 20px display heading next to a 120px
   icon, so both step down. The full-width feature cell keeps the larger icon. */
@media(min-width:360px) and (max-width:559.98px){
  .wwd-card{padding:14px;gap:8px}
  .wwd-card h3{font-size:17px}
  /* .wwd-wide-r is a half cell here, but line 1743 sizes both wide variants' headings at
     clamp(23px,2vw,29px) with specificity 0-3-0, which outranks .wwd-card h3 above. Restate
     at matching specificity so a 169px cell is not asked to carry 23px display type. */
  .wwd-card.wwd-wide-r h3{font-size:17px}
  .bico{width:64px !important;height:64px !important}
  .wwd-grid:has(>.wwd-wide)>.wwd-card.wwd-wide{padding:18px}
  .wwd-grid:has(>.wwd-wide)>.wwd-card.wwd-wide h3{font-size:20px}
  .wwd-grid:has(>.wwd-wide)>.wwd-card.wwd-wide .bico{width:84px !important;height:84px !important}
  .wwd-grid:not(:has(>.wwd-wide))>.wwd-card:last-child:nth-child(odd) .bico{width:84px !important;height:84px !important}
}
/* single-column floor — resets every placement the band sets above */
@media(max-width:359.98px){
  .wwd-grid{grid-template-columns:1fr}
  .wwd-card.wwd-wide,.wwd-card.wwd-wide-r{grid-column:auto;flex-direction:column;align-items:flex-start}
  .wwd-grid:has(>.wwd-wide)>.wwd-card.wwd-wide,
  .wwd-grid:has(>.wwd-wide)>.wwd-card.wwd-wide-r,
  .wwd-grid:not(:has(>.wwd-wide))>.wwd-card:last-child:nth-child(odd){grid-column:auto}
  .bico{width:120px !important;height:120px !important}
}
/* dark mode — match the .mf-card surface + swap cube-face fill to the dark card colour */
html[data-theme="dark"] .wwd{background:var(--paper-100)}
html[data-theme="dark"] .wwd-card{background:var(--paper-0);border-color:rgba(255,255,255,.12)}
html[data-theme="dark"] .wwd-card:focus-within{border-color:rgba(255,255,255,.55);box-shadow:0 24px 60px -34px rgba(0,0,0,.7)}
@media (hover:hover) and (pointer:fine){html[data-theme="dark"] .wwd-card:hover{border-color:rgba(255,255,255,.55);box-shadow:0 24px 60px -34px rgba(0,0,0,.7)}}
html[data-theme="dark"] .bico path[fill="#fff"]{fill:var(--paper-0)}
/* motion off — cards + icon parts static, no sweep */
@media(prefers-reduced-motion:reduce){
  .wwd-card,.wwd-card .pc{opacity:1;transform:none;translate:none;rotate:none;transition:none}
  .wwd-card::after{transition:none}
}
html.a11y-no-motion .wwd-card,html.a11y-no-motion .wwd-card .pc{opacity:1;transform:none;translate:none;rotate:none;transition:none}
html.a11y-no-motion .wwd-card::after{transition:none}
html.a11y-no-motion .wwd-card:hover,html.a11y-no-motion .wwd-card:focus-within{transform:none}

/* ——— D2C how-it-works: distinct 4-tile step bento (home .wwd language, different rhythm) ——— */
.wwd-grid.wwd-steps{grid-template-columns:1fr 1fr}
.wwd-grid.wwd-steps>.wwd-card:nth-child(1){grid-column:1;grid-row:1 / span 2}
.wwd-grid.wwd-steps>.wwd-card:nth-child(2){grid-column:2;grid-row:1}
.wwd-grid.wwd-steps>.wwd-card:nth-child(3){grid-column:2;grid-row:2}
.wwd-grid.wwd-steps>.wwd-card:nth-child(4){grid-column:1 / 3;grid-row:3}
.wwd-step-tall{justify-content:flex-start}
.wwd-step-tall .bico{margin-top:auto}
@media(max-width:820px){
  .wwd-grid.wwd-steps{grid-template-columns:1fr}
  .wwd-grid.wwd-steps>.wwd-card:nth-child(n){grid-column:auto;grid-row:auto}
  .wwd-step-tall .bico{margin-top:0}
}
/* D2C how-it-works: flat surface — drop the 3D-shapes bg image the shared .wwd carries on home. */
.wwd--flat{background:var(--paper-100)}

/* ── the model (home #model) — deck story: vision/mission → paradigm grid → solution flow →
   capabilities ticker → 5 steps. Sits directly under #what-we-do (owner call 2026-08-21: the
   bento stays the quick answer, this is the deep dive). Recreated from the standalone design
   handoff (what-we-do-section.html) on house tokens: Archivo→Sofia/Inter/mono, #ec3013→--red
   (fills) + --red-ink (text, theme-aware), page ground #f3f2f2→--paper-100. Radius-0 boxes are
   the design's own grammar and stay — don't soften to --radius-card. The 2px ink-mix rules were
   part of that grammar until 2026-08-24, when the owner retired them: every horizontal SEPARATOR
   and the paradigm grid now run 1px --hairline, the same token every other page's section seam
   uses, so #model stops being the one block on the site with its own line weight. --mdl-line is
   GONE as of 2026-08-24: the connector kit and the chips took the last of its users, so the whole
   module is now one weight and three tones (--hairline / --ink / --red). All motion is ambient,
   decorative, and CSS-only -- there is no SMIL left either; the animateMotion packets went with
   the .mdl-fan SVG they lived in. Every loop is killed below under BOTH prefers-reduced-motion and
   html.a11y-no-motion, and every one of them animates transform or opacity ONLY, so none can
   trigger layout or paint per frame. SVG parts that need theme tokens are styled here by class, never by presentation
   attribute: fill="var(--ink)" as an attribute is invalid CSS-wise and silently falls back.
   Ticker follows the marquee loop law (~1615): one ~1150px set is NARROWER than desktop
   viewports, so 4 sets + translateX(-25%), and the reduced-motion collapse keeps set 1 only. */
.mdl{overflow-x:clip}
/* THE MODULE'S HORIZONTAL SEPARATORS run FULL-BLEED on the site-wide separator token, so the
   rhythm inside #model reads the same as the section seams every other page already uses:
   section{border-bottom:1px solid var(--hairline)} (~267). One weight throughout the module now;
   emphasis comes from TONE instead -- hairline for a seam, ink for an anchor, red for flow.
   Mechanism is the awards-ledger idiom (~2315): the wrap-bound border is swapped for a 100vw
   absolutely-positioned pseudo-element, and .mdl carries overflow-x:clip to catch the
   scrollbar-width spill of 100vw. Abspos pseudo-elements are out of flow, so they are NOT grid
   items and cannot disturb .mdl-para's auto-fit columns or .mdl-step's three tracks. The ticker
   cannot use a pseudo (its own overflow:hidden drives the marquee and would clip it), so that one
   bleeds its own box and restores the gutter as padding, which keeps the static reduced-motion
   set off the page edge. */
.mdl-para,.mdl-step{position:relative}
.mdl-para::before,.mdl-para::after,.mdl-step::after{content:"";position:absolute;left:50%;width:100vw;margin-left:-50vw;height:1px;background:var(--hairline);pointer-events:none}
.mdl-para::before{top:0}
.mdl-para::after,.mdl-step::after{bottom:0}
.mdl h2{max-width:22ch;margin:0}
.mdl-k{font-family:var(--font-mono);font-weight:500;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);display:block;margin-bottom:10px}
.mdl-k--red{color:var(--red-ink)}
.mdl-k--steps{margin-top:56px}
/* end-of-model CTA — closes the five steps and hands off to models.html */
.mdl-cta{margin-top:clamp(28px,3.5vw,44px);display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:20px 32px}
.mdl-cta-t{margin:0;max-width:34ch;font-size:clamp(19px,2vw,24px);line-height:1.32;letter-spacing:-.01em}
.mdl-cta-t span{color:var(--ink-muted)}
@media (max-width:640px){.mdl-cta{flex-direction:column;align-items:flex-start}}
.mdl-vm{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:28px clamp(24px,4vw,72px);margin-top:42px}
.mdl-vm p{margin:0;font-size:17px;line-height:1.65;max-width:52ch;color:var(--ink-soft)}
.mdl-body{margin:18px 0 0;font-size:15.5px;line-height:1.75;max-width:52ch;color:var(--ink-soft)}
.mdl-para h3,.mdl-sol h3{font-size:clamp(24px,2.5vw,32px);line-height:1.22;letter-spacing:-.015em;margin:0;max-width:24ch}
/* the paradigm — copy left, 9-cell burden grid right (gap trick: container bg IS the rules) */
.mdl-para{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:42px clamp(24px,5vw,96px);align-items:start;margin-top:70px;padding:56px 0}
/* gap trick: the container background IS the rules, so weight is a token swap, not a rewrite.
   grid-auto-rows:1fr equalises the three rows -- without it the middle row runs 2px taller,
   because .mdl-cell--mfr carries a 24px icon against everyone else's 22px. Equalising here
   rather than shrinking that icon keeps the emphasis on the manufacturer cell. */
.mdl-grid9{display:grid;grid-template-columns:repeat(3,1fr);grid-auto-rows:1fr;gap:1px;background:var(--hairline);border:1px solid var(--hairline)}
/* min-width:0 + anywhere-wrap on every cell: the 1fr tracks must survive 320px reflow /
   200% zoom (WCAG 1.4.10) — without them the longest label sets the track floor */
.mdl-cell{background:var(--paper-100);padding:20px 16px;display:flex;flex-direction:column;gap:10px;min-width:0}
.mdl-cell-t{font-family:var(--font-mono);font-weight:500;font-size:11px;letter-spacing:.08em;text-transform:uppercase;overflow-wrap:anywhere}
.mdl-cell--mfr{background:var(--red);color:var(--paper-fixed);justify-content:center;position:relative;overflow:hidden}
/* icon normalised to the 22px every other cell uses. It shipped at 24px to emphasise this cell,
   but the solid red fill already does all of that work -- the 2px was invisible on its own and
   yet it drove the whole row 2px taller, which is what knocked the coda label off its
   neighbours' line. Styled by class here, per this module's no-presentation-attribute rule. */
.mdl-cell--mfr svg{width:22px;height:22px}
/* the coda is the multiplier ("...and all of it, per market"), not a tenth job, so it stays
   icon-less on purpose. flex-end drops its label onto the same line as the labels either side
   of it; centred, it floated in dead space and read as a cell that had failed to load. */
.mdl-cell--dim{justify-content:flex-end}
.mdl-cell--dim .mdl-cell-t{color:var(--ink-muted)}
.mdl-scan{position:absolute;bottom:0;left:0;width:100%;height:4px;background:linear-gradient(90deg,var(--paper-fixed) 0 8px,transparent 8px);animation:mdl-scan 2.4s ease-in-out infinite alternate}
/* Rail-to-rail sweep on transform ALONE -- it used to animate `left`, which cannot be composited
   and relaid out every frame, forever, whether or not the section was on screen. The element now
   spans the full parent width and paints its 8px bar as a gradient at its own left edge, so
   translateX(calc(100% - 8px)) -- where 100% is the element's OWN width, i.e. the parent's padding
   box -- lands flush at both rails knowing nothing about the parent's width. Deliberately not
   container-query units: 100cqw resolves against the CONTENT box, so it would stop 32-40px short
   inside .mdl-cell--mfr and .mdl-hub-hd, both of which are padded. Both parents already carry
   overflow:hidden, which clips the overhang. Shared by .mdl-hubscan. */
@keyframes mdl-scan{from{transform:translateX(0)}to{transform:translateX(calc(100% - 8px))}}
/* our solution — manufacturer → hub → customer */
.mdl-sol{padding:56px 0}
/* --lk-w is the single source for the connector column: the grid track reads it AND so do the
   packet keyframes, which need the travel distance as a length. */
.mdl-flow{--lk-w:44px;display:grid;grid-template-columns:minmax(150px,1fr) var(--lk-w) minmax(300px,2.2fr) var(--lk-w) minmax(150px,1fr);align-items:stretch;margin-top:42px}
.mdl-node{border:1px solid var(--hairline);padding:24px 20px;display:flex;flex-direction:column;gap:12px;justify-content:center}
.mdl-node-t{font-family:var(--font-display);font-weight:800;font-size:15px;letter-spacing:.04em;text-transform:uppercase}
.mdl-node-s{font-size:12.5px;line-height:1.5;color:var(--ink-muted)}
/* overflow:clip contains BOTH transform conversions below -- the dash bar overhangs by one
   period at each end, and the packet enters and leaves past the rails. clip, not hidden: it
   does not create a scroll container. */
.mdl-link{position:relative;align-self:stretch;min-height:80px;overflow:clip}
/* dash flow on transform, not background-position (which repaints every frame and never
   composites). The bar is inset by -12px at each end, one full gradient period, so translating it
   by exactly one period loops seamlessly and the visible 44px is covered at both extremes. */
.mdl-link::before{content:"";position:absolute;left:-12px;right:-12px;top:50%;margin-top:-1px;height:2px;background:repeating-linear-gradient(90deg,var(--red) 0 6px,transparent 6px 12px);animation:mdl-dashx 1.2s linear infinite}
@keyframes mdl-dashx{to{transform:translateX(12px)}}
.mdl-pkt{position:absolute;top:50%;left:0;width:8px;height:8px;margin-top:-4px;background:var(--ink);animation:mdl-pktx 1.8s linear infinite}
/* -8px to +44px, the same travel the `left` version had, now on transform alone */
@keyframes mdl-pktx{from{transform:translateX(-100%)}to{transform:translateX(var(--lk-w,44px))}}
/* the hub stays the anchor of the flow, but on TONE (ink against the nodes' hairline) plus its
   filled header -- not on an extra pixel. One line weight across the module, three tones. */
.mdl-hub{border:1px solid var(--ink);display:flex;flex-direction:column}
.mdl-hub-hd{background:var(--ink);color:var(--paper-100);padding:10px 20px;display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap;position:relative;overflow:hidden}
.mdl-hub-t{font-family:var(--font-display);font-weight:800;font-size:15px;letter-spacing:.04em;text-transform:uppercase}
/* the D2C tag needs "red readable on --ink surface", which flips against the theme: light bar
   in dark theme wants the light-theme red. --red-ink-inverse light / brand red dark. */
.mdl-hub-d2c{font-family:var(--font-mono);font-weight:500;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--red-ink-inverse)}
html[data-theme="dark"] .mdl-hub-d2c{color:var(--red)}
.mdl-hubscan{position:absolute;bottom:0;left:0;width:100%;height:3px;background:linear-gradient(90deg,var(--red) 0 8px,transparent 8px);animation:mdl-scan 2.8s linear infinite alternate}
.mdl-hub-grid{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--hairline);flex:1}
.mdl-hub-col{background:var(--paper-100);padding:20px}
.mdl-hub-k{display:block;font-family:var(--font-mono);font-weight:500;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--red-ink);margin-bottom:12px}
.mdl-hub-ls{display:flex;flex-direction:column;gap:8px;font-size:13.5px;line-height:1.4}
/* capabilities ticker */
.mdl-ticker{border-top:1px solid var(--hairline);border-bottom:1px solid var(--hairline);overflow:hidden;padding:14px calc(50vw - 50%);margin-inline:calc(50% - 50vw)}
.mdl-ticker-track{display:flex;align-items:center;width:max-content;animation:mdl-marquee 26s linear infinite}
.mdl-ticker:hover .mdl-ticker-track{animation-play-state:paused}
.mdl-tset{display:flex;align-items:center;gap:28px;padding-right:28px}
.mdl-titem{font-family:var(--font-display);font-weight:800;font-size:14px;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap}
.mdl-sq{width:8px;height:8px;background:var(--red);flex:none}
@keyframes mdl-marquee{to{transform:translateX(-25%)}}
/* 5 steps — number+icon | title+body | ambient mini-diagram */
.mdl-step{display:grid;grid-template-columns:minmax(64px,120px) minmax(0,1fr) minmax(0,1fr);gap:28px clamp(24px,4vw,72px);align-items:start;padding:42px 0}
.mdl-step-no{display:flex;flex-direction:column;gap:14px}
.mdl-n{font-family:var(--font-mono);font-weight:500;font-size:clamp(34px,3.4vw,44px);line-height:1;color:var(--red-ink)}
.mdl-step h4{font-family:var(--font-display);font-weight:800;font-size:clamp(20px,1.9vw,24px);letter-spacing:-.01em;line-height:1.25;margin:0;color:var(--ink)}
.mdl-step .mdl-body{margin-top:14px;max-width:48ch}
/* gap:0 - the connectors below must BUTT UP against the chips they join; a flex gap here would
   open a hole in every drop. Per-element margins do the spacing instead. --mdl-cg is the lane
   column gap AND the value the .mdl-bus insets are derived from, so they cannot drift apart. */
.mdl-fig{display:flex;flex-direction:column;align-items:stretch;gap:0;--mdl-cg:8px}
.mdl-figrow{display:flex;flex-wrap:wrap;gap:8px}
.mdl-figrow--ends{justify-content:space-between;width:min(280px,100%)}
/* A chip inside a .mdl-lane MUST stretch: filling its grid cell is exactly what makes the chip's
   centre equal the cell's centre, which is what .mdl-drop aligns to. So the old
   align-self:flex-start cannot stay global - it is scoped to DIRECT children of the figure, which
   sit outside any lane and should hug their content. 1px --hairline retires the last --mdl-line
   user in the module (2026-08-24). */
.mdl-chip{position:relative;font-family:var(--font-mono);font-weight:500;font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:6px 10px;border:1px solid var(--hairline);color:var(--ink);display:flex;align-items:center;justify-content:center;text-align:center;min-width:0}
.mdl-fig>.mdl-chip{align-self:center}
.mdl-chip--ink{background:var(--ink);border-color:var(--ink);color:var(--paper-100)}
.mdl-chip--red{background:var(--red);border-color:var(--red);color:var(--paper-fixed);animation:mdl-pulse 2.4s ease-in-out infinite}
/* The red flash is an OVERLAY that carries its own light copy of the label, so only opacity
   animates and the whole thing composites. It used to animate 14 properties (the `background`
   shorthand expands to all eight longhands, plus four border colours, plus color) on nine
   elements, every frame, forever. The label has to be duplicated rather than left showing through
   a red fill because the flash flips the text colour too, and ink-on-red measures 2.31:1 -- it
   fails AA, so ink cannot stay put. inset:-2px reaches the chip's BORDER box (an abspos child is
   laid out against the padding box), which is what the old keyframe reddened via border-color.
   `/ ""` is the content alt-text form: it keeps the duplicated label out of the a11y tree, so
   screen readers do not announce each chip twice. Everything typographic inherits from .mdl-chip
   automatically -- only color is overridden. */
.mdl-chip--fl::after{content:attr(data-t) / "";position:absolute;inset:-1px;display:flex;align-items:center;justify-content:center;background:var(--red);color:var(--paper-fixed);opacity:0;pointer-events:none;animation:mdl-flash var(--fd,4.8s) linear infinite var(--fdel,0s)}
/* pulse trough is .72, NOT the handoff's .25: opacity fades text and fill together toward the
   paper, and below ~.68 the white-on-red pair loses 4.5:1 (WCAG 1.4.3) for most of the cycle */
@keyframes mdl-pulse{0%,100%{opacity:1}50%{opacity:.72}}
@keyframes mdl-flash{0%,86%,100%{opacity:0}90%,94%{opacity:1}}
/* ---- connector kit: converge / diverge / balance (2026-08-24) --------------------------
   A chip lane and its connector lane share ONE grid template, so a drop centred in column N is
   centred on the chip in column N at any width - alignment by construction. This replaces three
   things that did not connect their own parts: the old .mdl-vlink was placed by margin-left:92px
   and lined up with no chip at all; the .mdl-fan's apex sat 58px clear of the PLATFORM chip with
   its legs missing their targets by 31-45px; and the SVG beam was never tied to its two chips.
   The bus spans first-column-centre to last-column-centre: for N equal columns with gap g that is
   an inset of (100% - (N-1)g)/2N per side, written out per N because calc() division by a var is
   the fragile part of that formula.
   Marching dashes REUSE mdl-dashv / mdl-dashx: a bar overhanging by exactly one gradient period,
   clipped by its parent, so the loop is seamless by period identity. transform only. */
.mdl-lane{display:grid;gap:var(--mdl-cg);align-items:start}
.mdl-lane--2{grid-template-columns:repeat(2,1fr)}
.mdl-lane--3{grid-template-columns:repeat(3,1fr)}
.mdl-lane--4{grid-template-columns:repeat(4,1fr)}
.mdl-lane--5{grid-template-columns:repeat(5,1fr)}
.mdl-drop,.mdl-stem{position:relative;height:18px;overflow:clip}
.mdl-drop--short{height:12px}
.mdl-drop::before,.mdl-stem::before{content:"";position:absolute;left:50%;margin-left:-1px;width:2px;top:-20px;bottom:-20px;background:repeating-linear-gradient(180deg,var(--red) 0 5px,transparent 5px 10px);animation:mdl-dashv 1.2s linear infinite var(--dd,0s)}
@keyframes mdl-dashv{to{transform:translateY(20px)}}
.mdl-bus{position:relative;height:2px;overflow:clip}
.mdl-bus::before{content:"";position:absolute;top:0;height:2px;left:-24px;right:-24px;background:repeating-linear-gradient(90deg,var(--red) 0 6px,transparent 6px 12px);animation:mdl-dashx 1.2s linear infinite}
.mdl-bus--2{margin-inline:calc((100% - 1 * var(--mdl-cg)) / 4)}
.mdl-bus--3{margin-inline:calc((100% - 2 * var(--mdl-cg)) / 6)}
.mdl-bus--4{margin-inline:calc((100% - 3 * var(--mdl-cg)) / 8)}
.mdl-bus--5{margin-inline:calc((100% - 4 * var(--mdl-cg)) / 10)}
/* balance beam in CSS, not a fixed-width SVG, so its span tracks the two column centres */
.mdl-bal{position:relative;height:40px}
.mdl-bal--2{margin-inline:calc((100% - 1 * var(--mdl-cg)) / 4)}
.mdl-bal-base{position:absolute;left:0;right:0;top:50%;margin-top:-1px;height:2px;background:var(--hairline)}
.mdl-bal-bar{position:absolute;left:0;right:0;top:50%;margin-top:-1px;height:2px;background:var(--ink);transform-origin:center;animation:mdl-tilt 2.6s ease-in-out infinite}
.mdl-bal-bar i{position:absolute;top:-3px;width:8px;height:8px;background:var(--red)}
.mdl-bal-bar i:first-child{left:-4px}
.mdl-bal-bar i:last-child{right:-4px}
.mdl-bal-pivot{position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px 0 0 -4px;background:var(--ink)}
.mdl-endrow{display:flex;flex-wrap:wrap;gap:var(--mdl-cg);justify-content:center}
/* compliance gate (fig 06) — the one figure whose point is that a mess of jurisdictions leaves as
   a SINGLE filing, so it needs a thing in the middle rather than another chip. A bordered band with
   a red sweep reading across it: transform-only, one element, no pseudo-element chain. The travel
   is written in px because the gate is (like .mdl-route) a fixed 280px band centred in the figure,
   so the distance is a constant, not a percentage of an unknown box: 280 + 64 of overhang + 4. */
.mdl-gate{position:relative;align-self:center;width:min(280px,100%);height:44px;border:1px solid var(--hairline);overflow:clip;display:flex;align-items:center;justify-content:center}
.mdl-gate-l{position:relative;font-family:var(--font-mono);font-weight:500;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink);text-align:center;padding:0 10px}
.mdl-gate-scan{position:absolute;top:0;bottom:0;left:-64px;width:64px;background:linear-gradient(90deg,transparent,var(--red),transparent);opacity:.26;animation:mdl-gatescan 3.2s linear infinite}
@keyframes mdl-gatescan{to{transform:translateX(348px)}}
/* Below 640 the connector kit STANDS DOWN. A lane forces N equal columns, and four of them cannot
   hold "WAREHOUSE": at 390px each chip gets 56px against the ~87px the label needs, which showed up
   as 7px of real overflow (scrollWidth - clientWidth) on three chips. Rather than break the label
   mid-word, the lane reverts to the wrapped flex row the figures used before the kit existed, and
   the connectors hide -- they are decorative structure and carry nothing the copy does not say.
   Hiding them keeps the drops and their chips from disagreeing, which is the whole point of the
   shared template. 640 chosen from measurement: at 641px each of four chips still gets ~118px. */
@media(max-width:640px){
  .mdl-fig{gap:10px}
  .mdl-lane{display:flex;flex-wrap:wrap;align-items:flex-start}
  .mdl-drop,.mdl-bus,.mdl-stem{display:none}
  .mdl-bal{margin-inline:0}
  .mdl-fig>.mdl-chip,.mdl-endrow,.mdl-gate{align-self:flex-start;justify-content:flex-start}
}
@keyframes mdl-tilt{0%,100%{transform:rotate(-4deg)}50%{transform:rotate(4deg)}}
.mdl-route{width:min(280px,100%);height:56px;align-self:center}
.mdl-route-base{stroke:var(--hairline);stroke-width:2}
/* transform, not stroke-dashoffset. The line is extended 36 user units to the left in the markup
   (three whole 12-unit dash periods, so the phase at x=10 is unchanged) and clipped back to
   10..270, so translating it by 24 units -- the same two periods per 1.2s the dashoffset version
   ran -- marches the dashes identically. This is the only stroke-dashoffset left to convert: the
   three diagonal .mdl-fan lines that could not be converted cleanly were deleted outright on
   2026-08-24 when fig 02 became a CSS diverge, which is why no stroke-dashoffset animation
   survives in the module. */
.mdl-route-dash{stroke:var(--red);stroke-width:2;stroke-dasharray:6 6;animation:mdl-routedash 1.2s linear infinite}
@keyframes mdl-routedash{to{transform:translateX(24px)}}
.mdl-rn{fill:var(--ink)}
.mdl-rn--end{fill:var(--red)}
/* translate:0 13px is STATIC. The baseline used to live only inside the keyframe, so under
   animation:none - reduced motion - the truck dropped to y=0 and floated 14px clear of the line
   at y=34. Keeping it on the element means the truck rides the road in every state. */
.mdl-truck{fill:none;stroke:var(--ink);stroke-width:2;stroke-linecap:square;translate:0 13px;animation:mdl-travel 4.5s linear infinite}
/* 12px lift (not the handoff's 4px): puts the wheel rims on the y34 road line */
@keyframes mdl-travel{to{translate:236px 13px}}
.mdl-route-lbls{align-self:center;margin-top:6px;font-family:var(--font-mono);font-weight:500;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted)}
.mdl-route-lbl--end{color:var(--red-ink)}
/* stack: flow goes vertical, steps drop the third column (fig wraps under the copy) */
@media(max-width:820px){
  .mdl-flow{grid-template-columns:minmax(0,1fr)}
  .mdl-link{min-height:44px;height:44px;align-self:auto}
  .mdl-link::before{left:50%;right:auto;top:-12px;bottom:-12px;height:auto;width:2px;margin:0 0 0 -1px;background:repeating-linear-gradient(180deg,var(--red) 0 6px,transparent 6px 12px);animation-name:mdl-dashy}
  .mdl-pkt{left:50%;top:0;margin:0 0 0 -4px;animation-name:mdl-pkty}
  .mdl-step{grid-template-columns:64px minmax(0,1fr)}
  .mdl-step .mdl-fig{grid-column:2}
}
@keyframes mdl-dashy{to{transform:translateY(12px)}}
@keyframes mdl-pkty{from{transform:translateY(-100%)}to{transform:translateY(var(--lk-w,44px))}}
/* motion off — ambient loops gone, packets/sweeps hidden, ticker collapses to one wrapped set
   (same shape as the brand marquee ~1644). Static dashes stay: they are drawing, not motion. */
@media(prefers-reduced-motion:reduce){
  .mdl-scan,.mdl-hubscan,.mdl-pkt,.mdl-gate-scan{display:none}
  .mdl-link::before,.mdl-drop::before,.mdl-stem::before,.mdl-bus::before,.mdl-route-dash,.mdl-bal-bar,.mdl-truck,.mdl-chip--fl::after,.mdl-chip--red{animation:none}
  .mdl-ticker{overflow:visible}
  .mdl-ticker-track{animation:none;width:auto}
  .mdl-tset{flex-wrap:wrap;row-gap:12px;padding-right:0}
  .mdl-tset+.mdl-tset{display:none}
}
html.a11y-no-motion .mdl-scan,html.a11y-no-motion .mdl-hubscan,html.a11y-no-motion .mdl-pkt,html.a11y-no-motion .mdl-gate-scan{display:none}
html.a11y-no-motion .mdl-link::before,html.a11y-no-motion .mdl-drop::before,html.a11y-no-motion .mdl-stem::before,html.a11y-no-motion .mdl-bus::before,html.a11y-no-motion .mdl-route-dash,html.a11y-no-motion .mdl-bal-bar,html.a11y-no-motion .mdl-truck,html.a11y-no-motion .mdl-chip--fl::after,html.a11y-no-motion .mdl-chip--red{animation:none}
html.a11y-no-motion .mdl-ticker{overflow:visible}
html.a11y-no-motion .mdl-ticker-track{animation:none;width:auto}
html.a11y-no-motion .mdl-tset{flex-wrap:wrap;row-gap:12px;padding-right:0}
html.a11y-no-motion .mdl-tset+.mdl-tset{display:none}

/* ——— D2C proof band: repeating operations-photo marquee (same mechanism as the
   home platform strip .brand-marquee) paired with the KPI band ——— */
.photo-marquee{position:relative;overflow:hidden;margin:clamp(28px,4vw,48px) 0 0}
.photo-marquee::before,.photo-marquee::after{content:"";position:absolute;top:0;bottom:0;width:9%;z-index:2;pointer-events:none}
.photo-marquee::before{left:0;background:linear-gradient(90deg,var(--paper-100) 12%,transparent)}
.photo-marquee::after{right:0;background:linear-gradient(270deg,var(--paper-100) 12%,transparent)}
.photo-track{display:flex;width:max-content;animation:photo-scroll 55s linear infinite}
.photo-marquee:hover .photo-track{animation-play-state:paused}
.photo-set{display:flex;gap:clamp(12px,1.2vw,18px);padding-right:clamp(12px,1.2vw,18px)}
/* These are real photographs of one building shot on different days, so they arrive at four
   different colour temperatures and exposures — a cool blue exterior, orange-and-blue racking,
   warm wood offices, cold monitor light — and side by side that reads as a folder upload rather
   than a set. A light desaturation plus a touch of contrast pulls the extremes toward each other
   without tipping into a filter: the honesty of un-staged photography is the point of this strip,
   so the grade corrects the mismatch and stops there. */
.photo-set img{height:clamp(200px,26vw,320px);width:auto;object-fit:cover;border-radius:4px;display:block;
  filter:saturate(.84) contrast(1.03)}
@keyframes photo-scroll{to{transform:translateX(-50%)}}
html.a11y-no-motion .photo-marquee,html.a11y-no-motion .photo-track{overflow:visible;width:auto}
html.a11y-no-motion .photo-marquee::before,html.a11y-no-motion .photo-marquee::after{display:none}
html.a11y-no-motion .photo-track{animation:none}
/* Static fallback. It used to be a centred flex-wrap, which lines up rows of variable-width
   photographs on their CENTRE: successive rows started at x=40, 48, 105 and 133px, so the block
   narrowed as it descended and both edges went ragged — it read as a mistake rather than a
   composition. A grid gives every row the same left and right edge. height:auto is required, not
   optional: the imgs carry width/height attributes, which are presentational hints with a real
   used value that would otherwise beat aspect-ratio and keep the intrinsic box. */
html.a11y-no-motion .photo-track{display:block}
html.a11y-no-motion .photo-set{display:grid;grid-template-columns:repeat(auto-fill,minmax(clamp(150px,20vw,236px),1fr));gap:12px;
  padding-inline:max(20px,calc((100% - var(--max)) / 2))}
html.a11y-no-motion .photo-set img{height:auto;width:100%;aspect-ratio:3/2}
html.a11y-no-motion .photo-set+.photo-set{display:none}
@media(prefers-reduced-motion:reduce){
  .photo-marquee{overflow:visible}
  .photo-marquee::before,.photo-marquee::after{display:none}
  /* display:block, not just width:auto — .photo-set is a flex ITEM here, so as a grid it sized to
     max-content (1097px inside a 1440px track) and sat off-centre against a dead right margin. */
  .photo-track{animation:none;width:auto;display:block}
  .photo-set{display:grid;grid-template-columns:repeat(auto-fill,minmax(clamp(150px,20vw,236px),1fr));gap:12px;
    padding-inline:max(20px,calc((100% - var(--max)) / 2))}
  .photo-set img{height:auto;width:100%;aspect-ratio:3/2}
  .photo-set+.photo-set{display:none}
}
.tag{display:inline-block;font-family:var(--font-mono);font-size:11px;letter-spacing:0.08em;text-transform:uppercase;border:1px solid var(--hairline);border-radius:9999px;padding:4px 12px;color:var(--ink-muted)}
.audience-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
@media(max-width:820px){.audience-tiles{grid-template-columns:1fr}}
.audience-tiles a{display:flex;flex-direction:column;text-decoration:none;background:var(--paper-0);border:1px solid var(--hairline);border-radius:3px;padding:36px 32px;transition:border-color 150ms var(--ease)}
@media(hover:hover) and (pointer:fine){.audience-tiles a:hover{border-color:var(--ink)}}
.audience-tiles .go{display:inline-flex;align-items:center;min-height:44px;font-weight:600;color:var(--red-ink);margin:auto 0 -10px}
/* ledger header — mirrors the Discipline (.mf-top) convention: line-icon leads (left),
   mono audience kicker trails (right), both top-aligned */
.tile-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}
.tile-kick{font-family:var(--font-mono);font-size:14px;font-weight:500;letter-spacing:0.04em;text-transform:uppercase;color:var(--ink-muted)}
.tile-icon{color:var(--ink)}
.tile-icon svg{width:30px;height:30px;display:block}
/* icon leads left + softer stroke that shifts to red on hover/focus, matching .mf-ico */
.tile-head .tile-icon{order:-1;color:#4b4b4b;transition:color .3s var(--ease)}
.tile-head .tile-icon svg{width:40px;height:40px}
@media(hover:hover) and (pointer:fine){.audience-tiles a:hover .tile-icon{color:var(--red-ink,#ab0000)}}
.audience-tiles a:focus-visible .tile-icon{color:var(--red-ink,#ab0000)}
html[data-theme="dark"] .tile-head .tile-icon{color:#c9c9c9}
.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
/* the wrapped tables hold no focusable content, so the scroller itself carries
   tabindex="0" in markup — without it keyboard users can't reach the overflow */
.table-scroll:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
table.plain{width:100%;border-collapse:collapse;font-size:15px}
@media(max-width:480px){.table-scroll table.plain{min-width:480px}}
/* editorial photo figures — hairline-framed, restrained colour, grain-unified with the
   hero/footer noise language. Real photos get the grain overlay; .is-ph slots are dashed
   placeholders awaiting real photography. aspect-ratio reserves space (no CLS). */
.fig-grid{display:grid;grid-template-columns:1.7fr 1fr;gap:20px;align-items:start}
.fig-col{display:flex;flex-direction:column;gap:20px}
.fig{margin:0}
.fig-img{position:relative;overflow:hidden;border:1px solid var(--hairline);border-radius:4px;background:var(--paper-200)}
.fig-img img{width:100%;height:100%;object-fit:cover;display:block;filter:saturate(.85) contrast(1.03)}
.fig-img:not(.is-ph)::after{content:"";position:absolute;inset:0;pointer-events:none;background:url("https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noise-grain.png") repeat;background-size:200px 200px;mix-blend-mode:overlay;opacity:.4}
.fig figcaption{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);margin-top:10px}
.fig-img.is-ph{display:flex;align-items:center;justify-content:center;border-style:dashed;background:var(--paper-100)}
.fig-ph{display:inline-flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);text-align:center;padding:0 16px}
.fig-ph svg{width:22px;height:22px;flex:none;opacity:.65}
@media(max-width:820px){.fig-grid{grid-template-columns:1fr}}
/* Two equal columns, for a gallery whose plates are portrait sources too small to fill the
   1.7fr slot's crop (ecommerce/orthodox-art). A class, not an inline style: an inline
   grid-template-columns outranks the stack rule above, which is how that page came to hold two
   185px columns on a 430px phone. Declared after that rule so it still wins on tablet, and it
   stacks 180px later than the rest of the family for a reason — stacking a 393px source into a
   full-width slot upscales it, and 640px is the last width where the single column stays inside
   the 1.56x browser scale this gallery already accepts (600px slot, 1.53x). Above it, two
   columns keep the plates at or below native width. */
.fig-grid--even{grid-template-columns:1fr 1fr}
@media(max-width:640px){.fig-grid--even{grid-template-columns:1fr}}
table.plain th{font-family:var(--font-mono);font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:var(--ink-muted);text-align:left;font-weight:500}
table.plain th,table.plain td{padding:14px 16px 14px 0;border-bottom:1px solid var(--hairline);vertical-align:top}
details{border-bottom:1px solid var(--hairline)}
details summary{cursor:pointer;padding:20px 0;font-family:var(--font-display);font-weight:700;font-size:18px;list-style:none}
/* A content swap ('+' -> '-') can never be transitioned by CSS, so the marker always cut
   instantly. Rotating a permanent '+' by 45deg turns it into an '*' shape instead of
   swapping the glyph -- the same close-affordance idiom hamburger-to-X icons use -- so the
   whole thing becomes transitionable with no markup change and no second icon element. */
details summary::after{content:'+';float:right;color:var(--red-ink);display:inline-block;transition:transform 200ms var(--ease)}
details[open] summary::after{transform:rotate(45deg)}
@media(prefers-reduced-motion:reduce){details summary::after{transition:none}}
html.a11y-no-motion details summary::after{transition:none}
details p{padding:0 0 20px;color:var(--ink-soft);max-width:65ch}

/* ============ motion system ============ */
.reveal{opacity:0;transform:translateY(24px);transition:opacity 600ms var(--ease),transform 600ms var(--ease)}
.reveal.is-visible{opacity:1;transform:none}
/* keyboard escape hatch: a control focused inside a not-yet-revealed block must never be invisible (WCAG 2.4.7) */
.reveal:focus-within{opacity:1;transform:none;transition:none}
/* no-JS escape hatch: .is-visible is only ever added by the observer in tpl.js (~line 35, over
   '.reveal, .stat-bars, .kpi-band'), so with scripting off every .reveal on the site would stay
   at opacity:0 forever — whole pages of invisible content. Only team.html has the
   <noscript><style> guard for this; the media query needs no markup change on any page.
   Deliberately NOT width-gated: .reveal's hidden state above is a top-level rule, so it hides
   on desktop exactly as it does on mobile, and the rescue has to match that scope. Same
   opacity/transform/transition triple the two existing motion guards use (prefers-reduced-motion
   and html.a11y-no-motion, further down this section) — it cancels the entrance to its visible
   end-state, it adds no animation. .reveal-d1/-d2/-d3 only add transition-delay, which is inert
   once transition is none. Engines without the `scripting` feature (pre-Baseline-2024) drop the
   query as invalid, i.e. status quo, never worse. */
@media(scripting:none){.reveal{opacity:1;transform:none;transition:none}}
/* Contact form with scripting off: the submit cannot work (tpl.js composes the mailto, there is
   no server endpoint), so showing a live button that silently discards the visitor's typing is
   worse than not offering it. Hide the button and the "Send opens your email app" hint; the
   <noscript> route in contact.html takes over. Pairs with method="post" on the form, which keeps
   the field values out of the URL if a submit ever does fire. (audit N7, 2026-07-30) */
@media(scripting:none){
  .ct-form [type="submit"],.ct-form [data-ct-hint]{display:none}
  .ct-nojs{color:var(--ink);font-weight:500}
}
/* 40/80/120ms steps: at the old 100/200/300 the last card in a 4-up grid started a full 300ms
   after the first, which reads as the grid loading rather than as one gesture. 40ms keeps the
   cascade legible while the whole row still lands together. */
.reveal-d1{transition-delay:40ms}
.reveal-d2{transition-delay:80ms}
.reveal-d3{transition-delay:120ms}
.reveal-d4{transition-delay:160ms}
.card,.audience-tiles a{transition:border-color 150ms var(--ease),transform 300ms var(--ease)}
@media(hover:hover) and (pointer:fine){
.card:hover,.audience-tiles a:hover{transform:translateY(-4px);border-color:var(--ink)}
.band-inverse .card:hover{border-color:var(--paper-500)}
}

/* people */
/* Leadership runs SIX across from 1240px. Cooper joined the executives on 2026-08-17 (owner
   call) and .grid-5's five tracks orphaned the sixth card onto a row of its own. Scoped to
   #leadership so the other people grids, which are genuinely 5-up, are untouched.
   Measured before choosing six over 3+3, at 1440: the six-track card is 180.67px against the
   five-track 220.8px, leaving 140.67px inside the padding for a 17px name. The longest name,
   "Nikolakopoulos", measures 112.8px, so no name can overflow, and the worst case is two
   lines — which the .grid-5 > .reveal{display:flex} + .card{width:100%} pair already levels
   into an equal-height row. For comparison the team groups directly below carry 20px names in
   156px of inner width, so this is proportionally the roomier of the two.
   1240px is where six measurably fits and is not a round number picked by eye: at 1240 the inner
   width is 134px against that 112.8px name, 21px of slack, while at 1101 it is 112.16px and the
   name would clip. The shared .grid-5 only drops to 3-up at 1100, so 1101-1239 would otherwise
   sit at five tracks and orphan the sixth card all over again; that window takes 3-up instead,
   which gives two clean rows of three at 284-328px of inner width. Below 1100 the shared
   breakpoints already give 3-up then 2-up, and six divides evenly into both. */
@media(min-width:1101px) and (max-width:1239px){#leadership .grid-5{grid-template-columns:repeat(3,1fr)}}
@media(min-width:1240px){#leadership .grid-5{grid-template-columns:repeat(6,1fr)}}
/* team sub-team labels — mono kicker over a hairline, one per functional group */
.tm-team{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);margin:44px 0 20px;padding-top:22px;border-top:1px solid var(--hairline)}
/* person names under a .tm-team group label are h4 (h2 section → h3 group → h4 person);
   visually identical to the h3 names used where no group level exists */
.person h4{font-family:var(--font-display);font-weight:700;font-size:20px;line-height:1.3;letter-spacing:-0.02em}
.tm-team:first-of-type{margin-top:8px;padding-top:0;border-top:0}
/* team carousel — a sub-team wider than the row (>5 at the current breakpoint) scrolls
   horizontally with prev/next arrows and a progress bar; 5-or-fewer stay a static row */
.tm-carwrap{position:relative}
.tm-car{display:grid;grid-auto-flow:column;grid-auto-columns:calc((100% - 80px)/5);gap:20px;overflow-x:auto;padding:6px 2px;scroll-snap-type:x mandatory;scroll-behavior:smooth;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.tm-car::-webkit-scrollbar{display:none}
.tm-car>.reveal{display:flex;scroll-snap-align:start}
.tm-car>.reveal>.card{width:100%}

/* == .tm-car clip mode — ADR-023's wheel fix, in the shape a GRID rail needs ==============
   Fault, measured 2026-08-24 at 1440 over the preview server with controls: a wheel carrying
   deltaX over the team carousel gives a PAGE DELTA OF 0. The identical wheel with deltaX:0 moves
   the page 900px, and the same deltaX wheel over the footer moves it normally, so it is this
   element and not the gesture. Chromium latches a gesture carrying horizontal delta to the nearest
   horizontal SCROLL CONTAINER and then discards the vertical delta it cannot use; overflow-x:auto
   above makes .tm-car exactly that, and every trackpad swipe carries some deltaX. This is the same
   bug the owner reported on the case-study rail on 2026-08-23 — that one was fixed, this one was
   never touched, and team.html is the only page with .tm-car.
   .cs-rail's fix wraps its children in a .cs-track and clips ITSELF. That shape cannot work here:
   .tm-car is a GRID whose columns are calc((100% - 80px)/5), so a wrapper child would resolve that
   100% against its own width and collapse to a fifth of the row. So the clip box goes OUTSIDE
   instead — .tm-clip clips, and .tm-car keeps its own width, therefore its exact column
   arithmetic, and becomes the run that moves. No authored card sizing changes.
   overflow-x:visible also restores overflow-y to visible (it was computed to auto by the
   overflow-x:auto above), so the card lift and focus ring are no longer cropped vertically —
   .tm-clip only ever clips x. */
.tm-clip{overflow-x:clip;overflow-y:visible}
.tm-car.is-clipped{overflow-x:visible;scroll-snap-type:none;scroll-behavior:auto;-webkit-overflow-scrolling:auto;will-change:transform}
.tm-car.is-clipped.is-gliding{transition:transform 420ms var(--ease-out)}
html.a11y-no-motion .tm-car.is-clipped.is-gliding{transition:none}
@media(prefers-reduced-motion:reduce){.tm-car.is-clipped.is-gliding{transition:none}}
@media(max-width:1100px){.tm-car{grid-auto-columns:calc((100% - 40px)/3)}}
@media(max-width:960px){.tm-car{grid-auto-columns:calc((100% - 20px)/2)}}
@media(max-width:640px){.tm-car{gap:14px;grid-auto-columns:calc((100% - 14px)/2)}}
.tm-nav{display:flex;align-items:center;gap:16px;margin-top:18px}
.tm-arrow{width:44px;height:44px;flex:none;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--hairline);border-radius:9999px;background:var(--paper-0);color:var(--ink);cursor:pointer;transition:border-color 140ms var(--ease),color 140ms var(--ease),opacity 140ms var(--ease)}
.tm-arrow:hover{border-color:var(--ink)}
.tm-arrow:focus-visible{outline:2px solid var(--red);outline-offset:2px}
.tm-arrow[disabled]{opacity:.32;cursor:default;pointer-events:none}
.tm-arrow svg{width:18px;height:18px}
.tm-bar{position:relative;flex:1;height:3px;border-radius:9999px;background:var(--hairline);cursor:pointer}
.tm-bar>i{position:absolute;top:0;bottom:0;left:0;width:30%;background:var(--red);border-radius:9999px;transition:transform 140ms var(--ease),width 140ms var(--ease)}
html.a11y-no-motion .tm-car{scroll-behavior:auto}
html.a11y-no-motion .tm-bar>i,html.a11y-no-motion .tm-arrow{transition:none}
@media(prefers-reduced-motion:reduce){.tm-car{scroll-behavior:auto}.tm-bar>i,.tm-arrow{transition:none}}
.person{position:relative}
.person .avatar{width:64px;height:64px;border-radius:50%;background:var(--inverse);color:var(--paper-100);font-family:var(--font-display);font-weight:800;font-size:22px;display:flex;align-items:center;justify-content:center;margin-bottom:18px;letter-spacing:0}
.band-inverse .person .avatar{background:var(--paper-100);color:var(--ink)}
.person .photo{position:relative;aspect-ratio:1/1;border-radius:12px;overflow:hidden;margin-bottom:18px;background:var(--paper-200)}
/* 700ms was ~3x the hover budget: a card zoom should settle in 150-250ms or it reads as lag */
.person .photo img{width:100%;height:100%;object-fit:cover;transition:transform 260ms var(--ease)}
@media(hover:hover) and (pointer:fine){.card:hover .photo img{transform:scale(1.03)}}
.card:focus-within .photo img{transform:scale(1.03)}
/* role icon centered over the photo (injected into .photo by tpl.js from data-icon);
   hidden by default, on hover it scales in and its stroke draws itself */
.person .role-icon{position:absolute;top:50%;left:50%;width:44%;height:44%;transform:translate(-50%,-50%) scale(.6);color:#fff;opacity:0;pointer-events:none;filter:drop-shadow(0 2px 12px rgba(0,0,0,.55));transition:opacity 300ms var(--ease),transform 360ms var(--ease)}
.person .role-icon svg{width:100%;height:100%;display:block}
.person .role-icon path{stroke-dasharray:100 100}
@media(hover:hover) and (pointer:fine){.card:hover .role-icon{opacity:1;transform:translate(-50%,-50%) scale(1)}}
.card:focus-within .role-icon{opacity:1;transform:translate(-50%,-50%) scale(1)}
/* role-icon stroke-draw hover animation (icon-draw) removed 2026-07-22 — the icon still
   scales/fades in on hover (rule above); its stroke no longer self-draws (motion trim). */
/* pencil-sketch portraits — the colour photo is the resting state; hover/focus fades the
   hand-drawn variant (assets/people/sketch/) in over it. Role ghost icon off on sketch cards. */
.person .photo .sketch{position:absolute;inset:0;opacity:0;background:#fff;transition:opacity 300ms var(--ease)}
@media(hover:hover) and (pointer:fine){.card:hover .photo .sketch{opacity:1}}
.card:focus-within .photo .sketch{opacity:1}
.person .photo:has(.sketch) .role-icon{display:none}
html.a11y-no-motion .person .photo .sketch{transition:none}
@media(prefers-reduced-motion:reduce){.person .photo .sketch{transition:none}}
/* the plate ground is --inverse, which is theme-invariant (#0B0B0C both themes, ADR-010 note
   above), so the monogram must be a literal too — var(--paper-100) flips to #101012 in dark
   and the initials vanish on the plate */
.person .photo.mono{display:flex;align-items:center;justify-content:center;background:var(--inverse);color:#F2F1ED;font-family:var(--font-display);font-weight:800;font-size:54px}
.band-inverse .person .photo.mono{background:var(--paper-100);color:var(--ink)}
/* TPL brand shapes mask the portraits (assets/shapes); the rounded rectangle above is the no-mask fallback.
   Three portrait-safe silhouettes cycle by grid position; shape-1 is too perforated for faces. */
@supports ((mask-size:100% 100%) or (-webkit-mask-size:100% 100%)){
  .person .photo{border-radius:0;
    -webkit-mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjE5NyIgdmlld0JveD0iMCAwIDE4NCAxOTciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xODMuMDE5IDExMy4zMjNMMTgzLjAwMSA2Mi41MTE3QzE4My4wMDEgNTYuMDI3OCAxODIuOTk3IDQ3LjE0NjMgMTcwLjg3NiA0MC4xODgzTDE3MC43ODIgNDAuMTM1MUwxMDAuMjY3IDIuODU0MTNDOTYuOTE2NCAwLjk4ODMwOSA5My4xMTgyIDAgODkuMjgwMiAwQzg1LjQ0MjIgMCA4MS41NDIyIDEuMDE0OSA3OC4xNTYyIDIuOTMzOUw1OS4yODUzIDEzLjYyMzZMNTkuMTAzNSAxMy41MjYxTDExLjIxNzEgMzkuMjU3NkwxMS4xMTk2IDM5LjMxNTJDNy42OTgxNyA0MS4zMjI5IDQuODQ0MDQgNDQuNDI5NiAyLjg3MTg2IDQ4LjI5ODZDMC45OTI3MzkgNTEuOTg2IDAgNTYuMTk2MiAwIDYwLjQ4NjNMMC4wNjY0NzU1IDE0MC44MUMwLjA2NjQ3NTUgMTQ2LjYzOCAzLjE2NDM2IDE1Mi4xMzggOC4xNTAyMyAxNTUuMTY1TDguMjc0MzIgMTU1LjI0TDMyLjk0NjYgMTY4LjY2VjE2OC41NEw4Mi44NjczIDE5NC42MDhDODguNDgyNSAxOTcuNTQyIDk1LjE4NzkgMTk3LjUwMiAxMDAuNzY4IDE5NC41MDJMMTcxLjk2NiAxNTYuMjQ2QzE3OC45MTEgMTUyLjU1OSAxODMuMDU0IDE0NS43OTEgMTgzLjA1NCAxMzguMTQ2VjExMy4zMUwxODMuMDE5IDExMy4zMjhWMTEzLjMyM1oiIGZpbGw9IiM3Mjg4OTkiLz4KPC9zdmc+Cg==");mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjE5NyIgdmlld0JveD0iMCAwIDE4NCAxOTciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xODMuMDE5IDExMy4zMjNMMTgzLjAwMSA2Mi41MTE3QzE4My4wMDEgNTYuMDI3OCAxODIuOTk3IDQ3LjE0NjMgMTcwLjg3NiA0MC4xODgzTDE3MC43ODIgNDAuMTM1MUwxMDAuMjY3IDIuODU0MTNDOTYuOTE2NCAwLjk4ODMwOSA5My4xMTgyIDAgODkuMjgwMiAwQzg1LjQ0MjIgMCA4MS41NDIyIDEuMDE0OSA3OC4xNTYyIDIuOTMzOUw1OS4yODUzIDEzLjYyMzZMNTkuMTAzNSAxMy41MjYxTDExLjIxNzEgMzkuMjU3NkwxMS4xMTk2IDM5LjMxNTJDNy42OTgxNyA0MS4zMjI5IDQuODQ0MDQgNDQuNDI5NiAyLjg3MTg2IDQ4LjI5ODZDMC45OTI3MzkgNTEuOTg2IDAgNTYuMTk2MiAwIDYwLjQ4NjNMMC4wNjY0NzU1IDE0MC44MUMwLjA2NjQ3NTUgMTQ2LjYzOCAzLjE2NDM2IDE1Mi4xMzggOC4xNTAyMyAxNTUuMTY1TDguMjc0MzIgMTU1LjI0TDMyLjk0NjYgMTY4LjY2VjE2OC41NEw4Mi44NjczIDE5NC42MDhDODguNDgyNSAxOTcuNTQyIDk1LjE4NzkgMTk3LjUwMiAxMDAuNzY4IDE5NC41MDJMMTcxLjk2NiAxNTYuMjQ2QzE3OC45MTEgMTUyLjU1OSAxODMuMDU0IDE0NS43OTEgMTgzLjA1NCAxMzguMTQ2VjExMy4zMUwxODMuMDE5IDExMy4zMjhWMTEzLjMyM1oiIGZpbGw9IiM3Mjg4OTkiLz4KPC9zdmc+Cg==");
    -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
    -webkit-mask-position:center;mask-position:center;
    -webkit-mask-size:100% 100%;mask-size:100% 100%}
  .grid-3 > :nth-child(3n+2) .person .photo,.grid-4 > :nth-child(3n+2) .person .photo,.grid-5 > :nth-child(3n+2) .person .photo,.ib-people>.reveal:nth-child(2) .person .photo{-webkit-mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTk3IiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDE5NyAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xOTYuNzcxIDc5LjM2NjFWNjUuMDk5OUwxOTYuNzQ5IDY1LjEwODhMMTk2Ljc0IDM1LjkxNkMxOTYuNzQgMzIuMTg4NyAxOTYuNzQgMjcuMDg3NyAxODkuNzczIDIzLjA5MDFMMTg5LjcyIDIzLjA1OTFMMTQ5LjIxMyAxLjYzOTc5QzE0Ny4yODkgMC41NjcyNzkgMTQ1LjEwNCAwIDE0Mi45MDIgMEMxNDAuNjk5IDAgMTM4LjQ1NiAwLjU4NTAwOCAxMzYuNTExIDEuNjg0MTFMMTI1LjY3IDcuODI2N0wxMjUuNTY4IDcuNzczNTJMOTguMDU5OCAyMi41NTM4TDk4LjAwMjIgMjIuNTg0OUM5Ny44ODI1IDIyLjY1NTggOTcuNzgwNiAyMi43NDg4IDk3LjY2MDkgMjIuODE5OEw1Ny41OTY3IDEuNjM5NzlDNTUuNjczMyAwLjU2NzI3OSA1My40ODg0IDAgNTEuMjg1NyAwQzQ5LjA4MzEgMCA0Ni44NDA1IDAuNTg1MDA4IDQ0Ljg5NSAxLjY4NDExTDM0LjA1NDYgNy44MjY3TDMzLjk1MjYgNy43NzM1Mkw2LjQ0Mzk2IDIyLjU1MzhMNi4zODYzNCAyMi41ODQ5QzQuNDE4NTggMjMuNzM3MiAyLjc4MzIyIDI1LjUyMzIgMS42NDg2NiAyNy43NDhDMC41NjcyODEgMjkuODY2NCAwIDMyLjI4NjIgMCAzNC43NTA0TDAuMDM1NDU1OSA4MC44OTUxQzAuMDM1NDU1OSA4NC4yNDU2IDEuODEyNjQgODcuNDA1NiA0LjY4MDA3IDg5LjE0MjhMNC43NTA5OCA4OS4xODcyTDE4LjkyNDEgOTYuODk4NlY5Ni44Mjc3TDIzLjk1ODcgOTkuNDU1OEw2LjQ0Mzk2IDEwOC44NjVMNi4zODYzNCAxMDguODk2QzQuNDE4NTggMTEwLjA0OCAyLjc4MzIyIDExMS44MzQgMS42NDg2NiAxMTQuMDU5QzAuNTY3MjgxIDExNi4xNzcgMCAxMTguNTk3IDAgMTIxLjA2MUwwLjAzNTQ1NTkgMTY3LjIwNkMwLjAzNTQ1NTkgMTcwLjU1NiAxLjgxMjY0IDE3My43MTYgNC42ODAwNyAxNzUuNDU0TDQuNzUwOTggMTc1LjQ5OEwxOC45MjQxIDE4My4yMVYxODMuMTM5TDQ3LjYwMjggMTk4LjExNEM1MC44MjkyIDE5OS43OTggNTQuNjgwNSAxOTkuNzc2IDU3Ljg4OTIgMTk4LjA1Mkw5OC4xMDQxIDE3Ni40NDJMMTEwLjU0IDE4My4yMDVWMTgzLjEzNEwxMzkuMjE5IDE5OC4xMUMxNDIuNDQ1IDE5OS43OTQgMTQ2LjI5NiAxOTkuNzcxIDE0OS41MDUgMTk4LjA0N0wxOTAuNDA3IDE3Ni4wN0MxOTQuMzk2IDE3My45NTEgMTk2Ljc4IDE3MC4wNjUgMTk2Ljc4IDE2NS42NzNWMTUxLjQwNkwxOTYuNzU4IDE1MS40MTVMMTk2Ljc0OSAxMjIuMjIyQzE5Ni43NDkgMTE4LjQ5NSAxOTYuNzQ5IDExMy4zOTQgMTg5Ljc4MiAxMDkuMzk3TDE4OS43MjkgMTA5LjM2NkwxNzEuNjc4IDk5LjgyMzdMMTkwLjQwNyA4OS43NTg5QzE5NC4zOTYgODcuNjQwNCAxOTYuNzggODMuNzUzNyAxOTYuNzggNzkuMzYxN0wxOTYuNzcxIDc5LjM2NjFaIiBmaWxsPSIjNzI4ODk5Ii8+Cjwvc3ZnPgo=");mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTk3IiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDE5NyAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xOTYuNzcxIDc5LjM2NjFWNjUuMDk5OUwxOTYuNzQ5IDY1LjEwODhMMTk2Ljc0IDM1LjkxNkMxOTYuNzQgMzIuMTg4NyAxOTYuNzQgMjcuMDg3NyAxODkuNzczIDIzLjA5MDFMMTg5LjcyIDIzLjA1OTFMMTQ5LjIxMyAxLjYzOTc5QzE0Ny4yODkgMC41NjcyNzkgMTQ1LjEwNCAwIDE0Mi45MDIgMEMxNDAuNjk5IDAgMTM4LjQ1NiAwLjU4NTAwOCAxMzYuNTExIDEuNjg0MTFMMTI1LjY3IDcuODI2N0wxMjUuNTY4IDcuNzczNTJMOTguMDU5OCAyMi41NTM4TDk4LjAwMjIgMjIuNTg0OUM5Ny44ODI1IDIyLjY1NTggOTcuNzgwNiAyMi43NDg4IDk3LjY2MDkgMjIuODE5OEw1Ny41OTY3IDEuNjM5NzlDNTUuNjczMyAwLjU2NzI3OSA1My40ODg0IDAgNTEuMjg1NyAwQzQ5LjA4MzEgMCA0Ni44NDA1IDAuNTg1MDA4IDQ0Ljg5NSAxLjY4NDExTDM0LjA1NDYgNy44MjY3TDMzLjk1MjYgNy43NzM1Mkw2LjQ0Mzk2IDIyLjU1MzhMNi4zODYzNCAyMi41ODQ5QzQuNDE4NTggMjMuNzM3MiAyLjc4MzIyIDI1LjUyMzIgMS42NDg2NiAyNy43NDhDMC41NjcyODEgMjkuODY2NCAwIDMyLjI4NjIgMCAzNC43NTA0TDAuMDM1NDU1OSA4MC44OTUxQzAuMDM1NDU1OSA4NC4yNDU2IDEuODEyNjQgODcuNDA1NiA0LjY4MDA3IDg5LjE0MjhMNC43NTA5OCA4OS4xODcyTDE4LjkyNDEgOTYuODk4NlY5Ni44Mjc3TDIzLjk1ODcgOTkuNDU1OEw2LjQ0Mzk2IDEwOC44NjVMNi4zODYzNCAxMDguODk2QzQuNDE4NTggMTEwLjA0OCAyLjc4MzIyIDExMS44MzQgMS42NDg2NiAxMTQuMDU5QzAuNTY3MjgxIDExNi4xNzcgMCAxMTguNTk3IDAgMTIxLjA2MUwwLjAzNTQ1NTkgMTY3LjIwNkMwLjAzNTQ1NTkgMTcwLjU1NiAxLjgxMjY0IDE3My43MTYgNC42ODAwNyAxNzUuNDU0TDQuNzUwOTggMTc1LjQ5OEwxOC45MjQxIDE4My4yMVYxODMuMTM5TDQ3LjYwMjggMTk4LjExNEM1MC44MjkyIDE5OS43OTggNTQuNjgwNSAxOTkuNzc2IDU3Ljg4OTIgMTk4LjA1Mkw5OC4xMDQxIDE3Ni40NDJMMTEwLjU0IDE4My4yMDVWMTgzLjEzNEwxMzkuMjE5IDE5OC4xMUMxNDIuNDQ1IDE5OS43OTQgMTQ2LjI5NiAxOTkuNzcxIDE0OS41MDUgMTk4LjA0N0wxOTAuNDA3IDE3Ni4wN0MxOTQuMzk2IDE3My45NTEgMTk2Ljc4IDE3MC4wNjUgMTk2Ljc4IDE2NS42NzNWMTUxLjQwNkwxOTYuNzU4IDE1MS40MTVMMTk2Ljc0OSAxMjIuMjIyQzE5Ni43NDkgMTE4LjQ5NSAxOTYuNzQ5IDExMy4zOTQgMTg5Ljc4MiAxMDkuMzk3TDE4OS43MjkgMTA5LjM2NkwxNzEuNjc4IDk5LjgyMzdMMTkwLjQwNyA4OS43NTg5QzE5NC4zOTYgODcuNjQwNCAxOTYuNzggODMuNzUzNyAxOTYuNzggNzkuMzYxN0wxOTYuNzcxIDc5LjM2NjFaIiBmaWxsPSIjNzI4ODk5Ii8+Cjwvc3ZnPgo=")}
  .grid-3 > :nth-child(3n) .person .photo,.grid-4 > :nth-child(3n) .person .photo,.grid-5 > :nth-child(3n) .person .photo,.ib-people>.reveal:nth-child(3) .person .photo{-webkit-mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTk3IiBoZWlnaHQ9IjE5NyIgdmlld0JveD0iMCAwIDE5NyAxOTciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xOTYuNzYyIDEwNC40NzNMMTk2Ljc1MyA4My43ODAzQzE5Ni43NTMgODEuMTM4OSAxOTYuNzUzIDc3LjUyMjUgMTkxLjgxNiA3NC42OTA1TDE5MS43ODEgNzQuNjY4M0wxNzYuNTQ0IDY2LjYxMTJMMTgxLjgyMiA0OS4wNzQyTDE4MS44MzYgNDkuMDI5OUMxODIuMjQzIDQ3LjQ2NTQgMTgyLjE3MiA0NS43NTAzIDE4MS42MjMgNDQuMDY2MkMxODEuMTA0IDQyLjQ2MTggMTgwLjE3NCA0MC45NjM5IDE3OC45NDIgMzkuNzI3NEwxNTUuNzk0IDE2LjYxOTVDMTU0LjExNCAxNC45Mzk5IDE1MS42NDEgMTQuMjQ4NSAxNDkuMzMyIDE0LjgxNThMMTQ5LjI3NSAxNC44MjkxTDEzOC4zMDYgMTguMDY4OEwxMzguMzQxIDE4LjEwNDJMMTMwLjk4NCAyMC40MTMyTDEyMi40NzEgNC41NjkyN0wxMjIuNDQ4IDQuNTI5MzhDMTIxLjYzMyAzLjEzNzc3IDEyMC4zNjUgMS45NzIxOSAxMTguNzg4IDEuMTcwMDJDMTE3LjI4NSAwLjQwMzMwMSAxMTUuNTcgMCAxMTMuODI0IDBMODEuMTE2NyAwLjAyNjU5MDJDNzguNzQxMiAwLjAyNjU5MDIgNzYuNTAzMSAxLjI4NTI0IDc1LjI3MTEgMy4zMTk0OEw3NS4yNCAzLjM2ODIzTDY5Ljc3NTUgMTMuNDE1M0g2OS44MjQzTDY2LjI5MjEgMjAuMTgyOEw0OS4xNDA3IDE1LjAyNDFMNDkuMDk2NCAxNS4wMTA4QzQ3LjUzMTkgMTQuNjAzIDQ1LjgxNjggMTQuNjc0IDQ0LjEzMjcgMTUuMjIzNUM0Mi41MjgzIDE1Ljc0MiA0MS4wMzA0IDE2LjY3MjcgMzkuNzk4MyAxNy45MDQ4TDE2LjY5MDUgNDEuMDUyNUMxNS4wMTA4IDQyLjczMjIgMTQuMzE5NCA0NS4yMDUyIDE0Ljg4MjMgNDcuNTE0MkwxNC44OTU2IDQ3LjU3MThMMTguMTM1MyA1OC41NDA3TDE4LjE3MDcgNTguNTA1M0wyMC40NTMxIDY1Ljc3OEw0LjU2OTI3IDc0LjMwOTNMNC41MjkzOCA3NC4zMzE1QzMuMTMzMzQgNzUuMTQ3IDEuOTcyMTkgNzYuNDE0NSAxLjE3MDAyIDc3Ljk5MjJDMC40MDMzMDEgNzkuNDk0NiAwIDgxLjIwOTggMCA4Mi45NTU5TDAuMDI2NTkxOSAxMTUuNjYzQzAuMDI2NTkxOSAxMTguMDM5IDEuMjg5NjggMTIwLjI3NyAzLjMxOTQ4IDEyMS41MDlMMy4zNjgyMyAxMjEuNTRMMTMuNDE1MyAxMjcuMDA0VjEyNi45NTZMMjAuNjQ4MSAxMzAuNzMyTDE1LjQxODUgMTQ4LjEwOUMxNC40NzkgMTUxLjE3MSAxNS4yMzY4IDE1NC4zMTQgMTcuNDM1IDE1Ni41MTJMMjQuNTg4MSAxNjMuNjY1VjE2My42NTJMMzkuMjMxIDE3OC4yNzdDNDEuMDk2OCAxODAuMTQzIDQzLjY1ODUgMTgyLjcgNDkuMTQ5NiAxODEuMjE1TDQ5LjE4OTQgMTgxLjIwNkw2Ni4wODgyIDE3NS45OTRMNzQuODMyMyAxOTIuMjY0Qzc2LjMzNDcgMTk1LjA5MSA3OS4wOTEzIDE5Ni43OCA4Mi4yMDI1IDE5Ni43OEg5Mi4zMTYxTDkyLjMwNzIgMTk2Ljc2N0wxMTMgMTk2Ljc1OEMxMTUuNjQxIDE5Ni43NTggMTE5LjI1NyAxOTYuNzU4IDEyMi4wODkgMTkxLjgyMUwxMjIuMTEyIDE5MS43ODVMMTMwLjQxMyAxNzYuMDgzTDE0OC4xNzUgMTgxLjQyOEMxNTEuMjM4IDE4Mi4zNjcgMTU0LjM4IDE4MS42MSAxNTYuNTc4IDE3OS40MTFMMTYzLjczMSAxNzIuMjU4SDE2My43MTRMMTc4LjMzOSAxNTcuNjE1QzE4MC4yMDUgMTU1Ljc0NSAxODIuNzYyIDE1My4xODggMTgxLjI3MyAxNDcuNjk3TDE4MS4yNjQgMTQ3LjY1N0wxNzYuMDI1IDEzMC42NzRMMTkyLjI1OSAxMjEuOTUyQzE5NS4wODcgMTIwLjQ1IDE5Ni43NzYgMTE3LjY5MyAxOTYuNzc2IDExNC41ODJWMTA0LjQ2OEwxOTYuNzYyIDEwNC40NzdWMTA0LjQ3M1oiIGZpbGw9IiM3Mjg4OTkiLz4KPC9zdmc+Cg==");mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTk3IiBoZWlnaHQ9IjE5NyIgdmlld0JveD0iMCAwIDE5NyAxOTciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xOTYuNzYyIDEwNC40NzNMMTk2Ljc1MyA4My43ODAzQzE5Ni43NTMgODEuMTM4OSAxOTYuNzUzIDc3LjUyMjUgMTkxLjgxNiA3NC42OTA1TDE5MS43ODEgNzQuNjY4M0wxNzYuNTQ0IDY2LjYxMTJMMTgxLjgyMiA0OS4wNzQyTDE4MS44MzYgNDkuMDI5OUMxODIuMjQzIDQ3LjQ2NTQgMTgyLjE3MiA0NS43NTAzIDE4MS42MjMgNDQuMDY2MkMxODEuMTA0IDQyLjQ2MTggMTgwLjE3NCA0MC45NjM5IDE3OC45NDIgMzkuNzI3NEwxNTUuNzk0IDE2LjYxOTVDMTU0LjExNCAxNC45Mzk5IDE1MS42NDEgMTQuMjQ4NSAxNDkuMzMyIDE0LjgxNThMMTQ5LjI3NSAxNC44MjkxTDEzOC4zMDYgMTguMDY4OEwxMzguMzQxIDE4LjEwNDJMMTMwLjk4NCAyMC40MTMyTDEyMi40NzEgNC41NjkyN0wxMjIuNDQ4IDQuNTI5MzhDMTIxLjYzMyAzLjEzNzc3IDEyMC4zNjUgMS45NzIxOSAxMTguNzg4IDEuMTcwMDJDMTE3LjI4NSAwLjQwMzMwMSAxMTUuNTcgMCAxMTMuODI0IDBMODEuMTE2NyAwLjAyNjU5MDJDNzguNzQxMiAwLjAyNjU5MDIgNzYuNTAzMSAxLjI4NTI0IDc1LjI3MTEgMy4zMTk0OEw3NS4yNCAzLjM2ODIzTDY5Ljc3NTUgMTMuNDE1M0g2OS44MjQzTDY2LjI5MjEgMjAuMTgyOEw0OS4xNDA3IDE1LjAyNDFMNDkuMDk2NCAxNS4wMTA4QzQ3LjUzMTkgMTQuNjAzIDQ1LjgxNjggMTQuNjc0IDQ0LjEzMjcgMTUuMjIzNUM0Mi41MjgzIDE1Ljc0MiA0MS4wMzA0IDE2LjY3MjcgMzkuNzk4MyAxNy45MDQ4TDE2LjY5MDUgNDEuMDUyNUMxNS4wMTA4IDQyLjczMjIgMTQuMzE5NCA0NS4yMDUyIDE0Ljg4MjMgNDcuNTE0MkwxNC44OTU2IDQ3LjU3MThMMTguMTM1MyA1OC41NDA3TDE4LjE3MDcgNTguNTA1M0wyMC40NTMxIDY1Ljc3OEw0LjU2OTI3IDc0LjMwOTNMNC41MjkzOCA3NC4zMzE1QzMuMTMzMzQgNzUuMTQ3IDEuOTcyMTkgNzYuNDE0NSAxLjE3MDAyIDc3Ljk5MjJDMC40MDMzMDEgNzkuNDk0NiAwIDgxLjIwOTggMCA4Mi45NTU5TDAuMDI2NTkxOSAxMTUuNjYzQzAuMDI2NTkxOSAxMTguMDM5IDEuMjg5NjggMTIwLjI3NyAzLjMxOTQ4IDEyMS41MDlMMy4zNjgyMyAxMjEuNTRMMTMuNDE1MyAxMjcuMDA0VjEyNi45NTZMMjAuNjQ4MSAxMzAuNzMyTDE1LjQxODUgMTQ4LjEwOUMxNC40NzkgMTUxLjE3MSAxNS4yMzY4IDE1NC4zMTQgMTcuNDM1IDE1Ni41MTJMMjQuNTg4MSAxNjMuNjY1VjE2My42NTJMMzkuMjMxIDE3OC4yNzdDNDEuMDk2OCAxODAuMTQzIDQzLjY1ODUgMTgyLjcgNDkuMTQ5NiAxODEuMjE1TDQ5LjE4OTQgMTgxLjIwNkw2Ni4wODgyIDE3NS45OTRMNzQuODMyMyAxOTIuMjY0Qzc2LjMzNDcgMTk1LjA5MSA3OS4wOTEzIDE5Ni43OCA4Mi4yMDI1IDE5Ni43OEg5Mi4zMTYxTDkyLjMwNzIgMTk2Ljc2N0wxMTMgMTk2Ljc1OEMxMTUuNjQxIDE5Ni43NTggMTE5LjI1NyAxOTYuNzU4IDEyMi4wODkgMTkxLjgyMUwxMjIuMTEyIDE5MS43ODVMMTMwLjQxMyAxNzYuMDgzTDE0OC4xNzUgMTgxLjQyOEMxNTEuMjM4IDE4Mi4zNjcgMTU0LjM4IDE4MS42MSAxNTYuNTc4IDE3OS40MTFMMTYzLjczMSAxNzIuMjU4SDE2My43MTRMMTc4LjMzOSAxNTcuNjE1QzE4MC4yMDUgMTU1Ljc0NSAxODIuNzYyIDE1My4xODggMTgxLjI3MyAxNDcuNjk3TDE4MS4yNjQgMTQ3LjY1N0wxNzYuMDI1IDEzMC42NzRMMTkyLjI1OSAxMjEuOTUyQzE5NS4wODcgMTIwLjQ1IDE5Ni43NzYgMTE3LjY5MyAxOTYuNzc2IDExNC41ODJWMTA0LjQ2OEwxOTYuNzYyIDEwNC40NzdWMTA0LjQ3M1oiIGZpbGw9IiM3Mjg4OTkiLz4KPC9zdmc+Cg==")}
}
.person .kicker{font-family:var(--font-display);font-weight:700;font-size:11px;letter-spacing:0.05em;color:var(--red-ink)}
.band-inverse .person .kicker{color:var(--red-ink)}
.li-chip{position:absolute;top:24px;right:24px;width:34px;height:34px;background:var(--paper-0);border:1px solid var(--hairline);border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--ink-muted);transition:background 150ms var(--ease),color 150ms var(--ease),border-color 150ms var(--ease)}
.li-chip::before{content:'';position:absolute;inset:-7px} /* 48px hit area, no visual change */
.li-chip:hover{background:#0A66C2;border-color:#0A66C2;color:#fff}
.li-chip svg{width:16px;height:16px}
/* the chip ground is a theme-invariant literal, so the glyph must be one too — var(--paper-500)
   flipped to #55554F in dark and dropped the LinkedIn mark to 2.38:1 */
.band-inverse .li-chip{background:#17171A;border-color:#2a2a2e;color:#C5C3BB}
/* the inverse-band base style used to outrank .li-chip:hover (equal specificity, later in
   the file), so the LinkedIn-blue hover never fired on the advisory board — pin it here */
.band-inverse .li-chip:hover{background:#0A66C2;border-color:#0A66C2;color:#fff}
.grid-5 .li-chip{top:14px;right:14px}
/* person cards sit on the primary surface everywhere — including the inverse board band —
   so the sketch paper merges with the card face */
.card.person{background:var(--paper-0)}
.band-inverse .card.person{background:var(--paper-0);border-color:var(--hairline)}
.band-inverse .card.person h3{color:var(--ink)}
.band-inverse .card.person p{color:var(--ink-soft)}
.band-inverse .card.person .kicker{color:var(--red-ink)}
.band-inverse .card.person .li-chip{background:var(--paper-0);border-color:var(--hairline);color:var(--ink-muted)}
.band-inverse .card.person .li-chip:hover{background:#0A66C2;border-color:#0A66C2;color:#fff}
/* .li-chip sits on the WHITE card, not the dark band — keep the ink focus ring so it stays
   visible (the generic .band-inverse a:focus-visible would repaint it near-white, ~1.1:1). */
.band-inverse .card.person a:focus-visible{outline-color:var(--ink)}
/* people grids: stretch cards so every row is equal height */
.grid-3 > .reveal,.grid-4 > .reveal,.grid-5 > .reveal{display:flex}
.grid-3 > .reveal > .card,.grid-4 > .reveal > .card,.grid-5 > .reveal > .card{width:100%}
/* same wrapper-passthrough as above, for the .mf-grid CSS-grid (audience router + Discipline cards) */
.mf-grid > .reveal{display:flex}
.mf-grid > .reveal > .mf-card{width:100%}
/* same wrapper-passthrough for the news .blog-grid; :has() collapses the wrapper when the
   category filter hides its .post, so filtering out a card doesn't leave an empty gap */
.blog-grid > .reveal{display:flex}
.blog-grid > .reveal > .post{width:100%}
.blog-grid > .reveal:has(.post[hidden]){display:none}
/* compact person cards at 5-across */
.grid-5 .card{padding:20px}
.grid-5 .card h3{font-size:17px}
.grid-5 .person .photo{margin-bottom:14px}
/* phones: people stay 2-up (grid-5 skips the 640px 1-col collapse; tm-car pages 2 cards per
   view) — cards run ~160px wide here, so padding and names tighten to fit */
@media(max-width:640px){
  .grid-5{gap:14px}
  .grid-5 .card,.tm-car .card{padding:14px}
  .grid-5 .card h3{font-size:15px}
  .person h4{font-size:16px}
  .person .photo{margin-bottom:12px}
  .tm-car .li-chip{top:14px;right:14px}
}
.hairline-list .li-inline{display:inline-flex;align-items:center;color:var(--ink-muted);margin-left:8px;vertical-align:-2px}
.hairline-list .li-inline:hover{color:#0A66C2}
.hairline-list .li-inline svg{width:15px;height:15px}

/* stat bars (investors) */
.stat-bars .row{display:grid;grid-template-columns:120px 1fr 90px;gap:16px;align-items:center;padding:12px 0;border-bottom:1px solid var(--hairline)}
.stat-bars .bar{height:10px;border-radius:9999px;background:var(--paper-200);overflow:hidden}
.stat-bars .bar i{display:block;height:100%;background:var(--red);border-radius:inherit;width:0;transition:width 1200ms var(--ease)}
.stat-bars.is-visible .bar i{width:var(--w)}

@media(prefers-reduced-motion:reduce){
  /* !important is required, not stylistic — see the note on html.a11y-no-motion below:
     GSAP ScrollTrigger writes scroll-behavior INLINE on <html>, and an inline declaration
     outranks any stylesheet rule that is not !important. */
  html{scroll-behavior:auto!important}
  .reveal{opacity:1;transform:none;transition:none}
  .card:hover,.audience-tiles a:hover,.audience-tiles a:focus-visible{transform:none}
  .stat-bars .bar i{transition:none;width:var(--w)}
  .person .photo img{transition:none}
  .card:hover .photo img,.card:focus-within .photo img{transform:none}
  .person .role-icon{transition:none;transform:translate(-50%,-50%)}
  .card:hover .role-icon,.card:focus-within .role-icon{transform:translate(-50%,-50%)}
  .card:hover .role-icon path,.card:focus-within .role-icon path{animation:none}
  .theme-switch .ico,.lang-btn .chev{transition:none}
}

/* ============ accessibility widget (REQ-001) ============ */
/* Visually-hidden utility. NOTE: this does NOT work applied directly to a <table>. Auto
   table layout resolves the used width to max(specified, min-content), so width:1px is
   ignored, and clip: only suppresses paint -- the box keeps its content width and stays in
   the scrollable overflow area, which makes the whole page draggable sideways. (That was a
   real 156px horizontal scroll on investors.html at 390px until 2026-08-19.) Wrap the table
   in a <div class="sr-only"> instead; a div honours width:1px + overflow:hidden. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.a11y{position:fixed;left:20px;bottom:20px;z-index:80;font-family:var(--font-body)}
.a11y-toggle{width:52px;height:52px;border-radius:50%;border:0;background:var(--red);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px rgba(0,0,0,0.18);transition:background 150ms var(--ease)}
.a11y-toggle:hover{background:var(--red-hover)}
.a11y-toggle:focus-visible{outline:none;box-shadow:0 0 0 3px var(--paper-0),0 0 0 6px var(--ink)} /* two-ring halo: visible on any backdrop */
.a11y-toggle svg{width:26px;height:26px}
.a11y-panel{position:absolute;left:0;bottom:calc(100% + 12px);width:280px;background:var(--paper-0);border:1px solid var(--hairline);border-radius:var(--radius-card);box-shadow:0 12px 32px rgba(0,0,0,0.14);padding:8px;
  display:none;transform-origin:bottom left;opacity:1;scale:1;
  transition:opacity 150ms var(--ease),scale 150ms var(--ease),display 150ms allow-discrete}
.a11y.open .a11y-panel{display:block}
@starting-style{.a11y.open .a11y-panel{opacity:0;scale:.97}}
@media(prefers-reduced-motion:reduce){.a11y-panel{transition:opacity 150ms var(--ease),display 150ms allow-discrete;scale:1}
  @starting-style{.a11y.open .a11y-panel{scale:1}}}
html.a11y-no-motion .a11y-panel{transition:none;scale:1}
.a11y-panel .head{font-family:var(--font-mono);font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--ink-muted);padding:10px 12px;border-bottom:1px solid var(--hairline);margin-bottom:4px}
.a11y-opt{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;border:0;background:none;font:inherit;font-size:14px;font-weight:500;color:var(--ink);padding:11px 12px;border-radius:10px;cursor:pointer;text-align:left;min-height:44px}
.a11y-opt:hover{background:var(--paper-100)}
.a11y-opt:focus-visible{outline:2px solid var(--red);outline-offset:-2px}
.a11y-opt .state{font-family:var(--font-mono);font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:var(--ink-muted);border:1px solid var(--hairline);border-radius:9999px;padding:2px 9px}
.a11y-opt[aria-pressed="true"] .state{background:var(--red);border-color:var(--red);color:#fff}
.a11y-panel .foot{border-top:1px solid var(--hairline);margin-top:4px;padding:10px 12px 6px;font-size:12px;line-height:1.5;color:var(--ink-muted)}
.a11y-panel .foot a{color:var(--red-ink);font-weight:600;text-decoration:none}
.a11y-panel .foot a:hover{text-decoration:underline}
@media(max-width:640px){.a11y{left:14px;bottom:14px}.a11y-panel{width:min(280px,calc(100vw - 28px))}}

/* accessibility modes (toggled on <html> by the widget) */
html.a11y-large-text body{zoom:1.15}
/* High contrast forces the LIGHT high-contrast scheme in either theme, so every paper token must be
   redefined here. --paper-0 and --red-ink were missing: html.a11y-contrast (0,1,1) and
   html[data-theme="dark"] (0,1,1) are equal specificity, so with dark active the unlisted tokens kept
   their dark values and #000 ink landed on a #1A1A1C surface at 1.21:1 across the 61 rules that paint
   var(--paper-0). color-scheme:light so form controls and scrollbars follow the forced surface. */
html.a11y-contrast{--ink:#000;--ink-soft:#111;--ink-muted:#3d3d3d;--paper-0:#fff;--paper-50:#fff;--paper-100:#fff;--paper-200:#fff;--paper-300:#fff;--paper-500:#595959;--hairline:#767676;--red:#900000;--red-hover:#7a0000;--red-pressed:#5e0000;--red-ink:#900000;color-scheme:light}
html.a11y-underline a{text-decoration:underline!important}
/* !important because ScrollTrigger.min.js save/restores the scroller's scroll-behavior through
   gsap.set(), which lands as an INLINE declaration on <html> — `style="scroll-behavior: smooth"`
   is present at load on every page that ships ScrollTrigger (all but investors/news/privacy/
   terms), and inline beats any non-important stylesheet rule whatever its specificity. Without
   the flag this widget silently failed to stop smooth anchor scrolling on those pages: measured
   2026-07-29, computed scroll-behavior stayed `smooth` with html.a11y-no-motion set and a #hash
   jump animated across 10 frames, while the same probe on investors.html (no ScrollTrigger)
   resolved to `auto` and landed in one frame. These guards can only ever force `auto`, never
   `smooth`, so they cannot fight GSAP's own instant-scroll writes. */
html.a11y-no-motion{scroll-behavior:auto!important}
html.a11y-no-motion .reveal{opacity:1;transform:none;transition:none}
html.a11y-no-motion .card:hover,html.a11y-no-motion .audience-tiles a:hover,html.a11y-no-motion .audience-tiles a:focus-visible{transform:none}
html.a11y-no-motion .stat-bars .bar i{transition:none;width:var(--w)}
html.a11y-no-motion .person .photo img{transition:none}
html.a11y-no-motion .card:hover .photo img,html.a11y-no-motion .card:focus-within .photo img{transform:none}
html.a11y-no-motion .person .role-icon{transition:none;transform:translate(-50%,-50%)}
html.a11y-no-motion .card:hover .role-icon path,html.a11y-no-motion .card:focus-within .role-icon path{animation:none}
html.a11y-no-motion .theme-switch .ico,html.a11y-no-motion .lang-btn .chev{transition:none}
html.a11y-no-motion .s3-ov{transform:none;-webkit-mask:none;mask:none}
html.a11y-no-motion .s3-meta,html.a11y-no-motion .s3-label,html.a11y-no-motion .s3-title,html.a11y-no-motion .s3-body{opacity:1;transform:none}
html.a11y-no-motion .s3-num::after{background:var(--red)}
html.a11y-no-motion .s3-ov .ov-ast,html.a11y-no-motion .s3-ov .ov-node,html.a11y-no-motion .s3-ov .ov-dot,html.a11y-no-motion .s3-ov .ov-key,html.a11y-no-motion .s3-ov .ov-tick,html.a11y-no-motion .s3-ov .ov-tag{animation:none}

/* ============ "three things we don't compromise on" — numbered cards that stack on scroll ============ */
.s3{padding:0;border-bottom:0;background:var(--paper-100)}
.s3-intro{padding:96px 0 8px}
@media(max-width:720px){.s3-intro{padding:48px 0 8px}}
.s3-stack{position:relative}
.s3-card{position:sticky;top:calc(80px + var(--i,0) * 88px);background:var(--paper-100);border-top:1px solid var(--ink);box-shadow:0 -20px 44px rgba(20,20,19,.07)}
.s3-grid{max-width:var(--max);margin:0 auto;padding:0 clamp(20px,4vw,48px);display:grid;grid-template-columns:92px minmax(0,1fr) minmax(0,1.05fr);gap:clamp(24px,4vw,64px);align-items:start}
.s3-num{font-family:var(--font-display);font-weight:700;font-size:19px;color:var(--ink);padding-top:24px;display:flex;gap:9px;position:relative}
.s3-num span{color:var(--ink-muted)}
/* red progress tick under the index number — fills as the card's blueprint prints */
.s3-num::after{content:"";position:absolute;left:0;top:calc(100% + 14px);width:56px;height:2px;background:linear-gradient(90deg,var(--red) 0,var(--red) calc(var(--ov,1) * 100%),var(--hairline) calc(var(--ov,1) * 100%))}
.s3-img{align-self:start;width:100%;aspect-ratio:4/5;max-height:60vh;margin:24px 0;border-radius:4px;position:relative;overflow:hidden;background:var(--paper-0);border:1px solid var(--hairline)}
/* blueprint reveal — as the card pins (--ov 0→1, set by tpl.js) the ink line-art "prints"
   itself in: a soft mask sweeps up while the art rises + scales into place. Defaults to 1
   so the finished blueprint shows without JS / under reduced motion. */
.s3-ov{position:absolute;inset:0;pointer-events:none;padding:clamp(24px,7%,56px);transform:translateY(calc((1 - var(--ov,1)) * 34px)) scale(calc(.975 + var(--ov,1) * .025));transform-origin:50% 62%;-webkit-mask:linear-gradient(0deg,#000 0,#000 calc(var(--ov,1) * 125%),transparent calc(var(--ov,1) * 125% + 12%));mask:linear-gradient(0deg,#000 0,#000 calc(var(--ov,1) * 125%),transparent calc(var(--ov,1) * 125% + 12%))}
.s3-ov svg{display:block;width:100%;height:100%}
.s3-card{--ov:1}
/* overlay artwork palettes (svgs inlined in index.html; classes are per-card scoped) */
.mf-row:nth-of-type(1) .s3-ov .st0{fill:#d9d9d8}
.mf-row:nth-of-type(1) .s3-ov .st1{mask:url(#mask-1)}
.mf-row:nth-of-type(1) .s3-ov .st2{fill:var(--ink)}
.mf-row:nth-of-type(1) .s3-ov .st3{mask:url(#mask)}
.mf-row:nth-of-type(1) .s3-ov .st4{fill:var(--red)}
.mf-row:nth-of-type(1) .s3-ov .st5,.mf-row:nth-of-type(1) .s3-ov .st6{fill:none;stroke:var(--ink);stroke-miterlimit:10}
.mf-row:nth-of-type(1) .s3-ov .st6{stroke-width:5px}
.mf-row:nth-of-type(2) .s3-ov .st0{fill:var(--ink)}
.mf-row:nth-of-type(2) .s3-ov .st1{fill:none;stroke:var(--ink);stroke-miterlimit:10;stroke-width:5px}
.mf-row:nth-of-type(3) .s3-ov .st0{fill:#d9d9d8}
.mf-row:nth-of-type(3) .s3-ov .st1{mask:url(#ovm3b)}
.mf-row:nth-of-type(3) .s3-ov .st2{fill:var(--ink)}
.mf-row:nth-of-type(3) .s3-ov .st3{mask:url(#ovm3a)}
.mf-row:nth-of-type(3) .s3-ov .st4{fill:var(--red)}
.mf-row:nth-of-type(3) .s3-ov .st5{stroke:var(--ink);stroke-width:2px;stroke-miterlimit:10}
.mf-row:nth-of-type(3) .s3-ov .st6{fill:none;stroke:var(--ink);stroke-miterlimit:10;stroke-width:5px}
/* artwork micro-motion, one quiet set per card: 01 — asterisk spins, nodes breathe, a dot
   travels the arc (the how-we-work "order route" language); 02 — the dial needle sweeps its
   tick marks and the ticks shimmer in sequence; 03 — nodes + travelling dot again, and the
   piggy bank rocks gently */
.s3-ov .ov-ast{transform-box:fill-box;transform-origin:center;animation:ov-spin 26s linear infinite}
.s3-ov .ov-node{transform-box:fill-box;transform-origin:center;animation:ov-pulse 3.2s ease-in-out infinite}
.s3-ov g:nth-of-type(2) .ov-node{animation-delay:-1.6s}
.s3-ov .ov-key{transform-box:fill-box;transform-origin:10% 80%;animation:ov-key 4.5s ease-in-out infinite}
.s3-ov .ov-tick{animation:ov-tick 2.6s linear infinite;animation-delay:calc(var(--i,0) * .22s - 2.6s)}
.s3-ov .ov-tag{transform-box:fill-box;transform-origin:50% 90%;animation:ov-wobble 3.8s ease-in-out infinite}
.s3-ov .ov-dot{display:none}
@supports (offset-path:path('M0 0h1')){
  .s3-ov .ov-dot{display:block;animation:ov-travel 6.5s ease-in-out infinite}
}
@keyframes ov-spin{to{transform:rotate(360deg)}}
@keyframes ov-pulse{50%{transform:scale(1.18)}}
@keyframes ov-key{0%,100%{transform:rotate(0deg)}50%{transform:rotate(-3deg)}}
@keyframes ov-tick{0%,50%,100%{opacity:.3}10%{opacity:1}}
@keyframes ov-wobble{0%,100%{transform:rotate(0deg)}30%{transform:rotate(-4deg)}60%{transform:rotate(3deg)}}
@keyframes ov-travel{0%{offset-distance:0%;opacity:0}12%{opacity:1}88%{opacity:1}100%{offset-distance:100%;opacity:0}}
@media(prefers-reduced-motion:reduce){.s3-ov .ov-ast,.s3-ov .ov-node,.s3-ov .ov-dot,.s3-ov .ov-key,.s3-ov .ov-tick,.s3-ov .ov-tag{animation:none}}
/* spacer between cards 01 and 02 — the extra scroll card 01 stays pinned for; the overlay
   wipes up over the first stretch of it, then dwells settled until card 02 arrives */
.s3-hold{height:170vh}
.s3-meta{padding-top:24px;display:flex;flex-direction:column;transform:translateY(calc((1 - var(--ov,1)) * 20px))}
/* label → title → body stagger in on the same scroll scrub */
.s3-label{opacity:clamp(0,calc((var(--ov,1) - .10) * 6),1)}
.s3-title{opacity:clamp(0,calc((var(--ov,1) - .24) * 5),1)}
.s3-body{opacity:clamp(0,calc((var(--ov,1) - .38) * 4.5),1)}
/* desktop: center the copy against its poster instead of top-hugging with a long empty run below */
@media(min-width:821px){.s3-meta{padding-top:0;justify-content:center}}
@media(prefers-reduced-motion:reduce){.s3-ov{transform:none;-webkit-mask:none;mask:none}.s3-meta,.s3-label,.s3-title,.s3-body{opacity:1;transform:none}.s3-num::after{background:var(--red)}}
.s3-label{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink)}
.s3-title{font-family:var(--font-display);font-weight:800;font-size:clamp(28px,3vw,44px);letter-spacing:-.02em;line-height:1.08;margin:20px 0 0;color:var(--ink);max-width:15ch;text-wrap:balance}
.s3-body{font-size:16px;line-height:1.6;color:var(--ink-soft);margin:22px 0 0;max-width:46ch}
@media(max-width:820px){
  .s3-card{top:calc(72px + var(--i,0) * 60px)}
  .s3-grid{grid-template-columns:1fr;gap:16px;min-height:0;padding-top:16px;padding-bottom:36px}
  .s3-num{padding-top:0}
  .s3-img{margin:0}
  .s3-hold{height:120vh}
  .s3-meta{padding-top:0}
}

/* ============ back to top ============ */
.to-top{position:fixed;right:20px;bottom:20px;z-index:80;width:52px;height:52px;border-radius:50%;border:1px solid var(--hairline);background:var(--paper-0);color:var(--ink);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,0.12);opacity:0;visibility:hidden;transform:translateY(8px);transition:opacity 200ms var(--ease),transform 200ms var(--ease),visibility 200ms,background 150ms var(--ease),color 150ms var(--ease),bottom 520ms var(--ease)}
.to-top.show{opacity:1;visibility:visible;transform:none}
.to-top:hover{background:var(--ink);color:var(--paper-0);border-color:var(--ink)}
.to-top:focus-visible{outline:none;box-shadow:0 0 0 3px var(--paper-0),0 0 0 6px var(--ink)} /* two-ring halo: visible on any backdrop */
.to-top svg{width:22px;height:22px}

/* — scroll gauge on the button (2026-08-13) —
   A red arc prints clockwise from 12 o'clock just inside the button's hairline rim, closing into a
   full circle at the bottom of the document. The button does not grow, the touch target does not
   change, and the corner gains no new shape.

   MOVED HERE FROM THE CURSOR RING (.crsr-sp, 2026-08-10 → retired below). On the pointer the
   gauge was gated behind `(pointer: fine)` AND not-reduced-motion, so no phone, no tablet and no
   reduced-motion visitor ever had a page-position readout — the smallest possible audience for
   the one piece of state every visitor is tracking. It also had to share the ring's hover-grow
   and its retone. Here it is stationary, universal, and sits on the control that undoes the
   distance it measures.

   r=24, INSIDE THE DISC, NOT r=25 ON THE BORDER. Painting the arc over the rim was the first
   build and it read better — until the bottom of the home page, where the button parks on the red
   footer and a --red-ink arc on the rim disappears into the band behind it, at exactly the moment
   the closed circle is the whole point (measured: full ring, invisible). Held one unit in, the arc
   sits on the button's OWN --paper-0 disc, so the only surface it ever has to read against is one
   this rule already controls, in either theme, over any band. That is the same failure .crsr
   answers with a per-frame retone(); a fixed element that carries its own background can answer it
   with geometry instead, and no JS. r=24 with a 2px stroke spans 23..25 and the border owns 25..26,
   so the arc lands flush inside the rim with no gap.

   NO TRANSITION on the offset: progress must track the wheel 1:1, and any easing would leave the
   arc lagging the scrollbar. CSS owns the geometry so 150.80 (= 2π×24) stays on the line below
   the r that defines it instead of being duplicated in a .js file where a later r change would
   silently desync it; tpl.js writes --sp, a 0..1 scalar, and nothing else.
   butt caps, not round: a round cap on a zero-length dash paints a stray dot at 12 o'clock when
   progress is 0, and at 100% it would overlap the two ends instead of meeting them. */
/* inset:-1px + the two calcs, NOT inset:0/100%, and the pair is load-bearing. An absolutely
   positioned child is laid out against its ancestor's PADDING box, which on a 1px-bordered button
   is 50px, not 52 — so inset:0 scales the whole 52-unit viewBox by 50/52 and every radius below
   stops meaning what it says (measured: ring 50x50 against a 52x52 button). The explicit widths
   are also what outranks `.to-top svg` above, which would otherwise size this ring like the
   arrow glyph. */
.to-top .tt-ring{position:absolute;inset:-1px;width:calc(100% + 2px);height:calc(100% + 2px);
  overflow:visible;transform:rotate(-90deg);pointer-events:none}
.to-top .tt-ring circle{fill:none;stroke:var(--red-ink);stroke-width:2;stroke-linecap:butt;
  stroke-dasharray:150.80;stroke-dashoffset:calc(150.80px * (1 - var(--sp,0)));transition:stroke 150ms var(--ease)}
/* hover fills the disc with ink, and red-on-ink is the one surface this arc stops reading against.
   Static, not a retone(): unlike a pointer, the button always knows what is under its own arc. */
.to-top:hover .tt-ring circle{stroke:var(--paper-0)}
/* Deliberately NOT suppressed under reduced motion, unlike .crsr. A static arc at a fixed angle
   is a state readout, not motion — and these are the visitors who had no gauge at all before. */
/* audit H6 (2026-07-30): at 390px the two 52px fixed controls each cover ~13% of the viewport
   width at the bottom edge, and were measured clipping body copy (contact.html's #ship-from
   heading lost the "20" of "2026"). Both shrink to 44px on phones — still at the 44px comfort
   target, and matching the nbar controls — with tighter insets, and the last in-flow section
   gains a bottom gutter so end-of-page content can never sit under them. Mid-page overlap is
   inherent to a persistent FAB; removing it entirely needs an auto-hide-on-scroll behaviour. */
@media(max-width:640px){
  .to-top{right:12px;bottom:12px;width:44px;height:44px}
  /* 2.364 = 2 ÷ (44/52) — counter-scales the stroke against the shrunk viewBox so the arc still
     paints 2px, next to a 1px border that does NOT thin with the button. Without it the 52-unit
     box scaled to 44px renders a 1.69px stroke and the gauge reads thinner on the device where
     it matters most. Two static numbers beat vector-effect:non-scaling-stroke here — that keeps
     the width fixed but applies stroke-dasharray in the scaled space, breaking the progress math. */
  .to-top .tt-ring circle{stroke-width:2.364}
  .a11y{left:12px;bottom:12px}
  .a11y-toggle{width:44px;height:44px}
  main > section:last-child{padding-bottom:104px}
}

/* ============ ESPA co-funding bar (REQ-006) ============
   Full-bleed EU / ESPA 2021-2027 publicity banner, announced once per session on the
   first page loaded: slides up from the bottom edge once the loader's red contract has
   cleared, dwells, slides back down. The standing publicity obligation is carried by the
   footer compliance row; this bar is the announcement live www.tpl.gr also runs (live has
   neither motion nor a dismiss control, so both are additions here).
   Surface is --paper-fixed and the glyph is --inverse, NOT the paper/ink ramp: all three
   banner artworks have white baked in, so the bar cannot flip with the theme, and a paper
   token would go near-black on the fixed white and vanish.
   --espa-h is DERIVED from the same numbers that build the bar (viewport minus the dismiss
   gutter, over the artwork's 1956x231 = 8.4675 ratio, floored at the 56px the 44px close
   target needs) rather than measured in JS, so the corner-control lift below costs no
   layout read. 100vw includes the scrollbar, so the lift over-reaches by ~2px, never under. */
:root{
  --espa-gut:44px;                        /* dismiss gutter — both sides above 640px */
  --espa-inset:calc(2 * var(--espa-gut));
  --espa-h:calc(max(56px,(100vw - var(--espa-inset)) / 8.4675) + 1px);
}
.espa-bar{
  position:fixed;left:0;right:0;bottom:0;z-index:85; /* over .a11y/.to-top (80), under the mobile sheet (200) */
  display:flex;align-items:center;min-height:56px;padding-inline:var(--espa-gut);
  background:var(--paper-fixed);border-top:1px solid var(--hairline-fixed);
  transform:translateY(100%);visibility:hidden;
  transition:transform 520ms var(--ease),visibility 520ms;
}
/* visibility in the closed state keeps a translated-off-screen bar out of the tab order
   and out of AT — the same pairing .post-scrim uses */
html.espa-on .espa-bar{transform:none;visibility:visible}
.espa-bar img{flex:1 1 auto;min-width:0;display:block;width:100%;height:auto}
.espa-x{position:absolute;top:50%;right:2px;transform:translateY(-50%);width:44px;height:44px;
  border:0;border-radius:50%;background:none;color:var(--inverse);
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  transition:background 150ms var(--ease)}
.espa-x:hover{background:rgba(0,0,0,.06)}
.espa-x:focus-visible{outline:none;box-shadow:0 0 0 3px var(--paper-fixed),0 0 0 6px var(--inverse)}
.espa-x svg{width:18px;height:18px}
/* the two corner controls sit 20px off the bottom at z-80 and would be covered. The lift
   animates `bottom`, not `transform`: .to-top already owns transform for its own show/hide
   and a second one would clobber it. */
html.espa-on .a11y,html.espa-on .to-top{bottom:calc(20px + var(--espa-h))}
.a11y{transition:bottom 520ms var(--ease)}
@media(max-width:640px){
  :root{--espa-gut:40px;--espa-inset:var(--espa-gut)} /* right gutter only — the phone band is thin */
  .espa-bar{padding-inline:0 var(--espa-gut)}
  html.espa-on .a11y,html.espa-on .to-top{bottom:calc(12px + var(--espa-h))}
}
/* both reduced-motion routes: the bar appears and disappears with no travel, and the
   corner lift snaps. .to-top's own `transition:none` overrides already sit just below. */
@media(prefers-reduced-motion:reduce){.espa-bar,.a11y{transition:none}}
html.a11y-no-motion .espa-bar,html.a11y-no-motion .a11y{transition:none}
html.a11y-no-motion .to-top{transition:none}
@media(prefers-reduced-motion:reduce){.to-top{transition:none}}

/* ============ Material emphasis + red perimeter stroke (trial: audience router tiles) ============ */
/* Match the discipline (.mf-card) treatment: flat surface, border darkens to ink and the
   card lifts on hover/focus, red 2px stroke draws from the top-left corner around both edges,
   meeting at the bottom-right. No drop-shadow. */
@property --sweep{syntax:'<angle>';inherits:false;initial-value:0deg}
.audience-tiles a{position:relative;
  transition:border-color .3s var(--ease),transform .3s var(--ease)}
.audience-tiles a::after{content:'';position:absolute;inset:-1px;padding:1px;
  border-radius:4px;pointer-events:none;--sweep:0deg;
  background:conic-gradient(from 135deg,transparent 0 calc(180deg - var(--sweep)),var(--red) calc(180deg - var(--sweep)) calc(180deg + var(--sweep)),transparent calc(180deg + var(--sweep)));
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;mask-composite:exclude;
  transition:--sweep 280ms cubic-bezier(0.2,0,0,1)}
@media(hover:hover) and (pointer:fine){.audience-tiles a:hover{border-color:var(--ink);transform:translateY(-3px)}}
.audience-tiles a:focus-visible{border-color:var(--ink);transform:translateY(-3px)}
@media(hover:hover) and (pointer:fine){.audience-tiles a:hover::after{--sweep:180deg}}
.audience-tiles a:focus-visible::after{--sweep:180deg}
html.a11y-no-motion .audience-tiles a::after{transition:none}
@media(prefers-reduced-motion:reduce){.audience-tiles a::after{transition:none}}
/* dark-mode parity with .mf-card */
html[data-theme="dark"] .audience-tiles a{border-color:rgba(255,255,255,.12)}
@media(hover:hover) and (pointer:fine){html[data-theme="dark"] .audience-tiles a:hover{border-color:rgba(255,255,255,.55);box-shadow:0 24px 60px -34px rgba(0,0,0,.7)}}
html[data-theme="dark"] .audience-tiles a:focus-visible{border-color:rgba(255,255,255,.55);box-shadow:0 24px 60px -34px rgba(0,0,0,.7)}


/* ============ About + Contact redesign (2026-07-12) ============ */
/* RULE: eyebrows are text-only — never carry an icon/glyph. The old .has-i/.eyi
   glyph pattern (SHARP §2.7 tier-2) is deprecated and removed site-wide. */

/* fact rows — icon | label | value; the contact "company card" and small address cards */
.facts{list-style:none;margin:0;padding:0}
.facts li{display:grid;grid-template-columns:22px minmax(0,1fr);gap:0 14px;padding:13px 0;border-bottom:1px solid var(--hairline);align-items:start}
.facts li:last-child{border-bottom:0;padding-bottom:0}
.facts .fi{width:20px;height:20px;color:var(--ink-muted);margin-top:2px}
.facts .fk{display:block;font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:3px}
.facts .fv{font-size:15px;line-height:1.55;color:var(--ink)}
.facts .fv.num{font-size:14px}
.facts .fv a{color:inherit;text-decoration:none;font-weight:600}
.facts .fv a:hover{color:var(--red-ink)}
.facts .fv small{display:block;font-size:13px;color:var(--ink-muted)}

/* tap-safe text links (the ~24px card-link debt): same look, 44px hit area.
   The hairline underline is AFFORDANCE, not decoration, and it is why these are still links
   rather than buttons (2026-08-05): every one of the 63 is lateral "there is more over there"
   navigation, so promoting them to buttons would put 14 buttons on About and the three real
   CTAs would stop winning. But with text-decoration:none the ONLY thing separating them from
   body text was 600 weight, and the entire hover state was a colour change — WCAG 1.4.1, don't
   signal with colour alone. Underline at rest in --paper-500, both it and the label to
   --red-ink on hover.
   Drawn as a background gradient rather than text-decoration: these are inline-FLEX (for the
   44px box), the label is therefore an anonymous flex item, and underline propagation into it
   is not reliable across engines. The gradient is positioned off the text's own centre line,
   so it tracks the centred label instead of falling to the bottom of the 44px box the way a
   border-bottom does.
   THE LINE IS MIXED FROM currentColor, NOT FROM A PAPER TOKEN, for two measured reasons.
   (1) Contrast: --paper-500 on --paper-100 is 1.59:1, well under the 3:1 that WCAG 1.4.11 asks
   of a non-text indicator. (2) .biglink lives on inverse bands too (e.g. #people.band-inverse on
   about.html), and those bands never flip with the theme — a paper token there is near-invisible
   in dark theme, which is the trap the band-inverse comment warns about. currentColor is correct
   on any ground by construction, and it turns red on hover for free because the label does.
   THE MIX IS 65%, NOT 50%, and that came off a measurement rather than taste: most .biglink
   sit inside body copy coloured --ink-soft, not --ink, and 50% of a mid-grey on --paper-50 is
   only 2.44:1 (measured on #record). At 65% the weakest context clears 3:1 and the strong ones
   sit comfortably above it. Re-measure with a real composite if this value is ever touched —
   the computed value of color-mix serialises as color(srgb ...) in 0-1 floats, so a probe that
   assumes 0-255 will report white as near-black and invent a failure. */
.biglink{display:inline-flex;align-items:center;min-height:44px;font-weight:600;text-decoration:none;color:inherit;
  background-image:linear-gradient(color-mix(in srgb,currentColor 65%,transparent),color-mix(in srgb,currentColor 65%,transparent));
  background-repeat:no-repeat;background-size:100% 1px;background-position:0 calc(50% + 0.66em);
  transition:color .2s var(--ease)}
.biglink:hover{color:var(--red-ink);background-image:linear-gradient(currentColor,currentColor)}
@media(prefers-reduced-motion:reduce){.biglink{transition:none}}
html.a11y-no-motion .biglink{transition:none}
/* stacked biglink lists (404 aisle map): the global p-margin reset leaves consecutive 44px
   targets edge-to-edge — restore the >=8px buffer between them */
.grid-3 > div > p + p{margin-top:8px}

/* story timeline — hairline rail + dots, mono years (diagram language: hairlines, dots, no flourish) */
.tml{list-style:none;margin:0;padding:0;position:relative}
.tml::before{content:'';position:absolute;left:5px;top:8px;bottom:8px;width:1px;background:var(--hairline)}
.tml li{position:relative;padding:0 0 26px 34px}
.tml li:last-child{padding-bottom:0}
.tml .dot{position:absolute;left:0;top:5px;width:11px;height:11px;border-radius:50%;background:var(--paper-100);border:1.5px solid var(--paper-500)}
.tml li.is-red .dot{background:var(--red);border-color:var(--red)}
.tml .yr{display:block;font-family:var(--font-mono);font-size:13px;font-weight:500;letter-spacing:.08em;color:var(--ink);margin-bottom:4px}
.tml p{color:var(--ink-soft);font-size:15px;max-width:56ch}

/* group-structure diagram — the one diagram About is allowed: hairlines, dots, no flourish */
.org{text-align:center}
.org-parent{display:inline-block;background:var(--paper-0);border:1px solid var(--hairline);border-radius:12px;padding:14px 22px}
.org-parent strong{display:block;font-family:var(--font-display);font-weight:700;font-size:16px}
.org-parent span{font-family:var(--font-mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted)}
.org-stem{width:1px;height:26px;background:var(--hairline);margin:0 auto}
.org-kids{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;position:relative;padding-top:26px}
.org-kids::before{content:'';position:absolute;top:0;left:16.7%;right:16.7%;height:1px;background:var(--hairline)}
.org-kids>div{position:relative;background:var(--paper-0);border:1px solid var(--hairline);border-radius:12px;padding:16px 12px}
.org-kids>div::before{content:'';position:absolute;top:-26px;left:50%;width:1px;height:26px;background:var(--hairline)}
.org-kids>div::after{content:'';position:absolute;top:-3.5px;left:50%;transform:translateX(-50%);width:7px;height:7px;border-radius:50%;background:var(--paper-500)}
.org-kids>div.is-live::after{background:var(--red)}
.org-kids strong{display:block;font-family:var(--font-display);font-weight:700;font-size:15px;line-height:1.25}
.org-kids span{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted)}
@media(max-width:640px){
  .org-kids{grid-template-columns:1fr;gap:10px;padding-top:16px}
  .org-kids::before,.org-kids>div::before,.org-kids>div::after{display:none}
  .org-stem{height:16px}
}

/* 27-of-100 dot grid — people-to-output ratio on the inverse band */
.dotg{display:grid;grid-template-columns:repeat(20,1fr);gap:6px;margin:18px 0 10px}
.dotg i{aspect-ratio:1/1;border-radius:50%;background:rgba(255,255,255,.18)}
.dotg i.on{background:#fff}
/* caption sits on the theme-invariant #17171A inverse card — raw white by convention
   (--paper-500 flips to #55554F in dark and fails 4.5:1) */
.dotg-cap{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:rgba(255,255,255,.65)}

/* ===== locations map (a keyless Google embed in an iframe; the self-hosted Leaflet
   map and its vendor JS/CSS were replaced 2026-08-27 and removed 2026-09-01) ===== */
.map-chips{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}
.map-chip{display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:9px 16px;background:var(--paper-50);border:1px solid var(--hairline);border-radius:9999px;font-family:var(--font-body);font-size:14px;font-weight:500;color:var(--ink-soft);cursor:pointer;transition:border-color 150ms var(--ease),color 150ms var(--ease)}
.map-chip svg{width:15px;height:15px;color:var(--ink-soft);flex:none}
.map-chip:hover{border-color:var(--ink);color:var(--ink)}
.map-chip[aria-pressed="true"]{background:var(--paper-0);border-color:var(--ink);color:var(--ink)}
.map-chip[aria-pressed="true"] svg{color:var(--red-ink)} /* red assists the border/text state, never alone */
html.a11y-no-motion .map-chip,html.a11y-no-motion .soc a{transition:none}
@media(prefers-reduced-motion:reduce){.map-chip,.soc a{transition:none}}
.map-wrap{position:relative}
.map-el{height:clamp(380px,50vh,540px);border-radius:var(--radius-card);border:1px solid var(--hairline);overflow:hidden;background:var(--paper-200);z-index:0}
iframe.map-el{display:block;width:100%}
/* our own zoom pair: the keyless Google embed ships no +/- control, so these two swap the
   iframe's z level. Top-right is the one corner the embed leaves empty — its place card sits
   top-left, the fullscreen button bottom-right, and .map-wrap also contains the caption row
   below the frame, so a bottom offset would hang the buttons over that text. */
.map-zoom{position:absolute;right:12px;top:12px;display:flex;flex-direction:column;gap:6px;z-index:1}
.map-zoom-btn{width:44px;height:44px;display:grid;place-items:center;padding:0;background:var(--paper-0);color:var(--ink);border:1px solid var(--hairline);border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.14);cursor:pointer}
.map-zoom-btn svg{width:18px;height:18px}
.map-zoom-btn:hover{color:var(--red-ink)}
.map-zoom-btn[disabled]{opacity:.45;cursor:default;color:var(--ink)}
.map-foot{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-top:12px;font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.06em;color:var(--ink-muted)}
.map-foot a{color:var(--ink-soft);text-decoration:none;font-weight:600;display:inline-flex;align-items:center;min-height:44px}
.map-foot a:hover{color:var(--red-ink)}
.tpl-pop .pop-k{display:block;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:5px}
.tpl-pop .pop-t{display:block;font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.01em;margin-bottom:4px}
.tpl-pop .pop-a{display:block;font-size:13px;color:var(--ink-soft)}

/* social chips — static row variant of the person-card LinkedIn chip */
.soc{display:flex;gap:10px;margin-top:4px}
.soc a{width:44px;height:44px;border-radius:50%;background:var(--paper-0);border:1px solid var(--hairline);display:inline-flex;align-items:center;justify-content:center;color:var(--ink-muted);transition:color 150ms var(--ease),border-color 150ms var(--ease)}
.soc a:hover{color:var(--ink);border-color:var(--ink)}
.soc svg{width:17px;height:17px}

/* the contact router form select, boxed like the other fields */
.ct-field select{width:100%;font:inherit;font-size:15px;line-height:1.5;color:var(--ink);background:var(--paper-0);border:1px solid var(--hairline);border-radius:8px;padding:13px 40px 13px 16px;min-height:44px;cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b6c6c' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;background-size:16px;transition:border-color 200ms var(--ease),background 200ms var(--ease)}
.ct-field select:focus-visible{outline:2px solid var(--ink);outline-offset:2px;border-color:var(--ink)}
html[data-theme="dark"] .ct-field select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23989791' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")}
.ct-field select option{color:var(--ink);background:var(--paper-0)}
/* selected option = ink fill, NOT red — same rule as the .rs-chip checked state (where the
   OS owns the picker, e.g. the iOS wheel, this is advisory and the native highlight wins) */
.ct-field select option:checked{background:var(--ink);color:var(--paper-0)}
/* .ct-cc — the closed phone-country select shows the full "GR +30 — Greece" list only while
   open; the country name is dead weight once collapsed, so the select's own text is made
   transparent and .ct-cc-face overlays it with just the dial code. The face is aria-hidden
   and pointer-events:none — the label a screen reader gets is still the real select's full
   option text underneath, and every click/keypress still lands on that select. */
.ct-cc select{color:transparent}
/* forced-colors (Windows High Contrast) overrides author `color`, including transparent, with
   system text — the real "GR +30 — Greece" text would repaint on top of .ct-cc-face and the
   two would overlap. Drop back to the select as the only visible text there. */
@media(forced-colors:active){
  .ct-cc-face{display:none}
  .ct-cc select{color:CanvasText}
}
/* on the contact glass panel (.rs-formpanel) the select matches the rs-field inputs:
   translucent ground + 16px text, same full container width as every other field */
.rs-field select{background-color:rgba(255,255,255,.5);font-size:16px}
.rs-field select:user-invalid{border-color:var(--error)}
html[data-theme="dark"] .rs-field select{background-color:rgba(255,255,255,.06)}

/* footer legal line (About + Contact carry the registered data) */
.ft-co{margin-top:10px;font-size:12.5px;line-height:1.7;color:rgba(255,255,255,.85)}
.ft-co .num{font-size:12px}

/* ============ Legal / share / LinkedIn / modal layer (2026-07-12) ============ */
/* .consent-gate, .consent-actions, .consent-bar and .consent-status were removed on 2026-08-17
   with the map consent mechanism (ADR-005). .consent-gate had already been dead for some time:
   nothing in any page or in tpl.js ever carried the class. The form tick-boxes below are a
   different thing entirely and stay. */

/* form consent checkboxes (injected into every .ct-form) */
.ct-check{display:flex;gap:10px;align-items:flex-start;margin:0 0 14px;font-size:13.5px;line-height:1.55;color:var(--ink-soft);cursor:pointer}
.ct-check input{width:18px;height:18px;margin-top:2px;accent-color:var(--red);flex:none;cursor:pointer}
.ct-check input:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.ct-check a{color:var(--ink);font-weight:600}
.ct-check a:hover{color:var(--red-ink)}

/* door cards — equal heights; the email line and the page-link line pin to the bottom,
   so all 4 emails share one baseline row across the grid, and all 4 links the next */
.card.door{display:flex;flex-direction:column}
.door-links{margin-top:auto;padding-top:14px;display:flex;flex-direction:column;align-items:flex-start}

/* ===== share strip — per-page share icons + QR (JS-built above the footer) ===== */
/* share-this-page strip — light notification-bar-style band ABOVE the footer (tpl.js inserts it as
   the footer's previous sibling, outside <footer>). Paper surface + hairline borders; theme-aware
   through tokens, so it flips correctly in dark mode. */
.share-band{position:relative;background:var(--paper-100);border-top:1px solid var(--hairline);border-bottom:1px solid var(--hairline)}
.share-wrap{max-width:var(--max);margin:0 auto;padding:26px clamp(20px,4vw,48px);display:flex;align-items:center;gap:16px 24px;flex-wrap:wrap;justify-content:space-between}
.share-lbl{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft)}
.share-btns{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.share-btn{width:44px;height:44px;border-radius:50%;background:var(--paper-0);border:1px solid var(--hairline);display:inline-flex;align-items:center;justify-content:center;color:var(--ink-muted);cursor:pointer;transition:color 150ms var(--ease),border-color 150ms var(--ease),background 150ms var(--ease),transform 150ms var(--ease)}
@media(hover:hover) and (pointer:fine){.share-btn:hover{color:var(--ink);background:var(--paper-0);border-color:var(--ink);transform:translateY(-2px)}}
.share-btn svg{width:18px;height:18px}
.share-copy{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-soft);background:var(--paper-0);border:1px solid var(--hairline);border-radius:9999px;padding:0 18px;min-height:44px;cursor:pointer;transition:color 150ms var(--ease),border-color 150ms var(--ease),background 150ms var(--ease),transform 150ms var(--ease)}
@media(hover:hover) and (pointer:fine){.share-copy:hover{color:var(--ink);background:var(--paper-0);border-color:var(--ink);transform:translateY(-2px)}}
.share-copy.is-copied{color:var(--red-ink);border-color:currentColor}
/* QR popover — the plate stays white in BOTH themes so the code always scans */
.qr-pop{position:absolute;bottom:calc(100% + 12px);right:0;z-index:90;background:var(--paper-0);border:1px solid var(--hairline);border-radius:16px;box-shadow:0 12px 32px rgba(0,0,0,0.18);padding:16px;text-align:center}
.qr-pop[hidden]{display:none}
.qr-plate{background:#fff;border-radius:10px;padding:10px;display:block;line-height:0}
.qr-plate svg{width:164px;height:164px;display:block}
.qr-cap{margin-top:10px;font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted)}
.share-qr-wrap{position:relative}
/* mobile — the strip fills the width, three rows: (1) the 6 network icons spread edge to
   edge, (2) native-share icon | "Copy link" stretched | QR icon — equal 44px flanks, so the
   pill's label sits on the band's centre axis, aligned with the download label below, (3) the
   download pill full-width. ::before is a zero-height 100%-basis item that forces the line
   break after the networks; order re-seats copy between the two flanking icons. The QR popover
   re-anchors to the band itself (wrap goes static) and centres. */
@media(max-width:720px){
  .share-wrap{flex-direction:column;align-items:stretch;gap:16px}
  .share-btns{justify-content:space-between;row-gap:10px}
  .share-btns::before{content:"";order:1;flex-basis:100%;height:0}
  .share-btn[data-native]{order:2}
  .share-copy{order:3;flex:1 1 auto;justify-content:center;text-align:center}
  .share-qr-wrap{order:4;position:static}
  .share-copy[download]{order:5;flex:1 1 100%}
  .qr-pop{left:50%;right:auto;transform:translateX(-50%)}
}
html.a11y-no-motion .share-btn,html.a11y-no-motion .share-copy{transition:none}
html.a11y-no-motion .share-btn:hover,html.a11y-no-motion .share-copy:hover{transform:none}
@media(prefers-reduced-motion:reduce){.share-btn,.share-copy{transition:none}.share-btn:hover,.share-copy:hover{transform:none}}

/* ===== LinkedIn posts section (JS-built above the footer) ===== */
.btn-ext svg{width:15px;height:15px;margin-left:9px;vertical-align:-2px}


/* ===== modal (team "Introduce yourself") — native <dialog> in the house card language =====
   Left-anchored floating drawer: rests one gutter in from the left edge, full height less the
   gutter, and slides in left→right. The margins are spelled out because the global *{margin:0}
   reset (top of this file) kills the UA's dialog{margin:auto} — without it showModal() pins the
   panel hard to the top-left corner. Vertical margins are explicit rather than auto so the top
   gutter is guaranteed; dvh keeps the panel inside the *visible* viewport on mobile, where
   100vh is the tall value and would run the bottom under the browser chrome. */
.tpl-modal{
  --mg:24px; /* gutter, all four sides */
  border:0;border-radius:var(--radius-card);background:var(--paper-0);color:var(--ink);padding:0;
  margin:var(--mg) auto var(--mg) var(--mg);
  width:min(440px,calc(100vw - var(--mg) * 2));max-width:none;
  height:calc(100vh - var(--mg) * 2);height:calc(100dvh - var(--mg) * 2);max-height:none;
  box-shadow:0 24px 70px rgba(0,0,0,.28);
  /* closed + exit state: a panel-width plus the gutter clears the left edge completely */
  translate:calc(-100% - var(--mg)) 0;opacity:0;
  transition:translate .34s var(--ease),opacity .2s linear,
             overlay .34s allow-discrete,display .34s allow-discrete;
}
/* [open] guard: an unguarded display on <dialog> beats the UA :not([open]){display:none} */
.tpl-modal[open]{display:flex;flex-direction:column;overflow:hidden;translate:0 0;opacity:1}
/* entry frame — keep after the [open] rule. Browsers without @starting-style / allow-discrete
   drop the transition and simply show the panel in place, which is a fine degradation. */
@starting-style{.tpl-modal[open]{translate:calc(-100% - var(--mg)) 0;opacity:0}}
.tpl-modal::backdrop{background:rgba(11,11,12,0.58);opacity:0;
  transition:opacity .28s linear,overlay .28s allow-discrete,display .28s allow-discrete}
.tpl-modal[open]::backdrop{opacity:1}
@starting-style{.tpl-modal[open]::backdrop{opacity:0}}
/* phones: a narrow left panel would leave a dead strip of page, so go near-full-width —
   still entering from the left, still on the same gutter. */
@media(max-width:640px){.tpl-modal{--mg:20px;width:calc(100vw - var(--mg) * 2)}}
/* motion guards — fade only, no travel */
@media(prefers-reduced-motion:reduce){
  .tpl-modal{translate:0 0;transition:opacity .15s linear,overlay .15s allow-discrete,display .15s allow-discrete}
  @starting-style{.tpl-modal[open]{translate:0 0}}
}
html.a11y-no-motion .tpl-modal{translate:0 0;transition:opacity .15s linear,overlay .15s allow-discrete,display .15s allow-discrete}
@starting-style{html.a11y-no-motion .tpl-modal[open]{translate:0 0}}
.tpl-modal-head{display:flex;flex:none;justify-content:space-between;align-items:center;gap:16px;padding:20px 16px 20px 28px;border-bottom:1px solid var(--hairline)}
.tpl-modal-head h3{font-size:22px}
.tpl-modal-x{width:44px;height:44px;border:0;background:none;border-radius:10px;color:var(--ink-muted);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;flex:none}
.tpl-modal-x:hover{background:var(--paper-100);color:var(--ink)}
.tpl-modal-x svg{width:20px;height:20px}
.tpl-modal-body{padding:26px 28px 30px;overflow-y:auto;flex:1 1 auto;overscroll-behavior:contain}
.tpl-modal .ct-form-note{text-align:left}
.ct-file-list{list-style:none;margin:8px 0 0;padding:0;font-family:var(--font-mono);font-size:12px;color:var(--ink-soft)}
.ct-file-list li{padding:4px 0;border-bottom:1px dashed var(--hairline)}
.ct-field input[type="file"]{border-bottom:0;padding:6px 0;font-size:14px;cursor:pointer}
.ct-field input[type="file"]::file-selector-button{font:600 13px var(--font-body);color:var(--ink);background:var(--paper-100);border:1px solid var(--hairline);border-radius:8px;padding:9px 14px;margin-right:12px;cursor:pointer}
.ct-field input[type="file"]::file-selector-button:hover{background:var(--paper-200)}
/* This picker is not an upload and never was. It validates types, totals megabytes and lists
   what you chose — every signal of one — while the files reach TPL only if the sender attaches
   them by hand in their own mail app. Said here, at the control, as well as in the hand-off
   panel below (UX review 2026-08-06, finding 1). */
.mm-nofile{display:block;margin-top:8px;font-size:12px;color:var(--ink-muted)}

/* ===== Mail-modal hand-off panel =====
   Shown in place of the form after submit; the dialog deliberately stays open. Sized and spaced
   like the form it replaces so the modal does not jump height when they swap. */
.mm-sent-t{font-size:19px;margin:0 0 14px}
.mm-sent-files{margin:0 0 18px;padding:14px 16px;border:1px solid var(--red-ink);border-radius:var(--radius-card);background:var(--paper-100)}
.mm-sent-warn{margin:0 0 8px;font-weight:600;font-size:14px;color:var(--red-ink)}
.mm-sent-files ul{list-style:none;margin:0;padding:0;font-family:var(--font-mono);font-size:12px;color:var(--ink-soft)}
.mm-sent-files li{padding:3px 0}
.mm-sent-fall{margin:0 0 18px;font-size:14px;color:var(--ink-soft)}
.mm-sent-to{font-family:var(--font-mono);font-size:13px;color:var(--ink);white-space:nowrap}
.mm-sent-acts{display:flex;flex-wrap:wrap;gap:10px}
.mm-copied:empty{display:none}

/* ===== Contact confirmation — "dispatch" =====================================================
   Fires when the contact form's POST succeeds. Until 2026-09-07 that success was one grey line
   of text under the button (.ct-form-note.ct-ok), which on a phone sat below the fold: the one
   moment the visitor most needs an answer was the least visible thing on the page.

   This is a full-viewport takeover on the fixed dark band, and the enquiry is drawn as what the
   company actually does: it leaves the sender, runs the rail, and lands at Patras, which lights
   up. Owner's choice from a set of three (2026-09-07); the other two, a centred card and a corner
   docket, are archived in the gitignored _draft-contact-thanks.html.

   SCOPE: .ct-form exists on contact.html and nowhere else. The pitch / introduce / customer-care
   modals are a different path entirely -- they hand off to the visitor's mail app and swap in the
   .mm-sent-* panel -- so they do NOT use this and must not be wired to it.

   Colour is theme-INVARIANT throughout. --inverse and --red-ink-inverse are both documented as
   deliberately not theme-swapped, and paper tokens would go near-black on this ground in dark
   theme. Do not reach for --ink / --paper-N anywhere inside this block.

   Markup is built once by tpl.js (search: tyBuild) and appended on first success. */
.ty{
  width:100vw;max-width:100vw;height:100dvh;max-height:none;margin:0;border:0;padding:40px 24px;
  background:var(--inverse);color:#fff;display:none;place-items:center;
  /* overflow-x:clip, NOT hidden, and paired with overflow-y:auto so a short screen still scrolls
     down. Two decorations overhang their boxes on purpose -- the arrival ring, which scales past
     the node, and the packet, whose box IS the whole track -- and without this the dialog became
     genuinely draggable 18px sideways on a 390 phone (measured by setting scrollLeft and reading
     it back, not by trusting scrollWidth, which lies under clip). */
  overflow-x:clip;overflow-y:auto;
  opacity:0;transition:opacity .16s linear,overlay .16s allow-discrete,display .16s allow-discrete;
}
.ty[open]{display:grid;opacity:1;
  transition:opacity .3s linear,overlay .3s allow-discrete,display .3s allow-discrete}
@starting-style{.ty[open]{opacity:0}}
.ty::backdrop{background:var(--inverse)}
.ty-stage{width:100%;max-width:760px;text-align:center}
.ty-x{position:fixed;top:14px;right:14px;width:44px;height:44px;border:0;background:none;
  border-radius:10px;color:rgba(255,255,255,.6);cursor:pointer;display:inline-flex;
  align-items:center;justify-content:center;transition:background 150ms var(--ease),color 150ms var(--ease)}
.ty-x:hover{background:rgba(255,255,255,.1);color:#fff}
.ty-x:active{transform:scale(.94)}
.ty-x svg{width:19px;height:19px}

/* ---- the rail ------------------------------------------------------------------------------
   HTML and CSS, not one wide SVG. A single 640-unit SVG scaled down to a 342px phone renders its
   9.5px labels at about 5px, which is unreadable; here the two nodes are fixed-px boxes and only
   the wire between them flexes, so the type is the same size at 390 and at 1440.
   The dash and the packet both travel on transform alone, the technique .mdl-link / .mdl-scan
   already use: the packet is sized to the full track, so `100%` in its keyframe resolves against
   the track's width and it needs to know no number. */
.ty-rail{display:flex;align-items:flex-start;justify-content:center;margin-bottom:44px;overflow-x:clip}
.ty-node{flex:none;width:88px;text-align:center}
.ty-box{position:relative;height:48px;display:grid;place-items:center;border-radius:2px;
  border:1.5px solid rgba(255,255,255,.34)}
.ty-node span{display:block;margin-top:10px;font-family:var(--font-mono);font-size:10px;
  letter-spacing:.14em;color:rgba(255,255,255,.62);white-space:nowrap}
/* each node holds a glyph, or the two boxes read as unfinished placeholders: an envelope at the
   sender, the brand cube at the hub */
.ty-glyph{width:24px;height:24px;fill:none;stroke:rgba(255,255,255,.42);stroke-width:1.6;
  stroke-linejoin:round;stroke-linecap:round}
/* the destination lights only once the packet has arrived */
.ty.ty-go .ty-dest .ty-box{animation:ty-land .45s var(--ease-out) .88s forwards}
.ty.ty-go .ty-dest .ty-glyph{animation:ty-glyphlit .45s linear .88s forwards}
.ty.ty-go .ty-dest span{animation:ty-lit .45s linear .88s forwards}
@keyframes ty-land{to{border-color:var(--red-ink-inverse);box-shadow:inset 0 0 0 1px var(--red-ink-inverse)}}
@keyframes ty-glyphlit{to{stroke:var(--red-ink-inverse)}}
@keyframes ty-lit{to{color:#fff}}

/* the wire: a dashed track that lays itself left to right, then STOPS. It deliberately does not
   keep flowing once the packet has landed -- a rail still carrying traffic says the message is
   still in transit, which is the opposite of what this screen is telling you, and a permanent
   loop is decoration the moment it stops meaning anything. */
.ty-wire{position:relative;flex:1 1 auto;min-width:40px;height:48px;overflow:clip}
.ty-dash{position:absolute;left:-14px;right:-14px;top:50%;margin-top:-1px;height:2px;opacity:.85;
  background:repeating-linear-gradient(90deg,var(--red-ink-inverse) 0 7px,transparent 7px 16px);
  clip-path:inset(0 100% 0 0)}
.ty.ty-go .ty-dash{animation:ty-lay .85s linear forwards}
@keyframes ty-lay{to{clip-path:inset(0 0 0 0)}}
/* the packet. Its box IS the track, and it paints a 12px square at its own left edge, so
   translateX(calc(100% - 12px)) lands it flush at the far rail knowing no width. */
.ty-pkt{position:absolute;left:0;top:50%;width:100%;height:12px;margin-top:-6px;opacity:0;
  background:linear-gradient(90deg,var(--red-ink-inverse) 0 12px,transparent 12px)}
/* LINEAR, and on the dash's exact clock (same 0 delay, same .85s). It used to run on --ease,
   which front-loads hard: at 400ms the packet was 93% down the wire while the rail behind it was
   only 47% drawn, so the parcel was flying over track that did not exist yet. Matched timing puts
   it exactly at the head of the line it is laying, which is also the honest reading -- a parcel on
   a conveyor moves at one speed. */
.ty.ty-go .ty-pkt{animation:ty-run .85s linear forwards}
@keyframes ty-run{
  0%{opacity:0;transform:translateX(0)}
  8%{opacity:1}
  94%{opacity:1;transform:translateX(calc(100% - 12px))}
  100%{opacity:0;transform:translateX(calc(100% - 12px))}
}
/* the arrival pulse: one ring, once, timed with the node lighting so the packet reads as absorbed
   rather than as having evaporated */
.ty-ring{position:absolute;inset:-14px;border:2px solid var(--red-ink-inverse);border-radius:3px;opacity:0}
.ty.ty-go .ty-ring{animation:ty-ping .85s var(--ease-out) .86s forwards}
@keyframes ty-ping{0%{opacity:.9;transform:scale(.55)}100%{opacity:0;transform:scale(1.5)}}

/* ---- the words -----------------------------------------------------------------------------
   No eyebrow above the heading. "DELIVERED" over "It reached us." says the same thing twice, and
   a tracked-out label above a heading is the most generic move available here. The second clause
   is --red-ink-inverse, which is what .band-inverse h2.two-tone already does on every fixed-dark
   band on the site, and it carries the PROMISE -- red is spent on the fact the visitor came for. */
.ty-h{font-family:var(--font-display);font-weight:800;font-size:clamp(32px,6.4vw,56px);
  line-height:1.02;letter-spacing:-.015em;margin:0 0 18px;color:#fff;text-wrap:balance}
.ty-h span{display:block;margin-top:10px;font-size:.52em;letter-spacing:-.005em;
  color:var(--red-ink-inverse)}
.ty-p{font-size:clamp(15px,2vw,17px);line-height:1.6;color:rgba(255,255,255,.72);
  margin:0 auto 28px;max-width:46ch}
.ty-meta{padding-top:20px;border-top:1px solid rgba(255,255,255,.16);margin-bottom:28px;
  font-family:var(--font-mono);font-size:11.5px;font-weight:500;letter-spacing:.1em;
  text-transform:uppercase;color:rgba(255,255,255,.5);line-height:1.95}
.ty-meta b{color:#fff;font-weight:500;text-transform:none;letter-spacing:.04em}
/* The action is WHITE, not the site's red primary. Three red things were competing on this
   ground -- the rail, the promise clause and the button -- and the button was the one that was
   red without meaning anything by it. White is also the strongest affordance available against
   --inverse, so the one thing to do here reads as the one thing to do. */
.ty-acts .btn{border:0;cursor:pointer;min-width:220px;background:#fff;color:var(--inverse)}
.ty-acts .btn:hover{background:rgba(255,255,255,.86)}
.ty-acts .btn:active{background:rgba(255,255,255,.78)}

/* copy arrives as the packet lands, staggered 60ms, settled by ~1.45s */
.ty .ty-in{opacity:0;transform:translateY(10px)}
.ty.ty-go .ty-in{animation:ty-rise .42s var(--ease-out) calc(.88s + var(--i,0) * .06s) forwards}
@keyframes ty-rise{to{opacity:1;transform:none}}

/* Motion guards -- both switches, one place, and kept last. Reduced motion means gentler, not
   zero: the takeover still fades in. What goes is every travelling animation, and every element
   that would otherwise be stranded at its start value (the copy at opacity 0 is unreadable, and
   the rail at clip-path 100% is an empty gap between two boxes). */
@media(prefers-reduced-motion:reduce){
  .ty,.ty[open]{transition:opacity .15s linear,overlay .15s allow-discrete,display .15s allow-discrete}
  .ty.ty-go *{animation:none !important}
  .ty .ty-in{opacity:1;transform:none}
  .ty-dash{clip-path:inset(0 0 0 0)}
  .ty-pkt,.ty-ring{opacity:0}
  .ty-dest .ty-box{border-color:var(--red-ink-inverse);box-shadow:inset 0 0 0 1px var(--red-ink-inverse)}
  .ty-dest .ty-glyph{stroke:var(--red-ink-inverse)}
  .ty-dest span{color:#fff}
}
html.a11y-no-motion .ty,html.a11y-no-motion .ty[open]{transition:opacity .15s linear,overlay .15s allow-discrete,display .15s allow-discrete}
html.a11y-no-motion .ty.ty-go *{animation:none !important}
html.a11y-no-motion .ty .ty-in{opacity:1;transform:none}
html.a11y-no-motion .ty-dash{clip-path:inset(0 0 0 0)}
html.a11y-no-motion .ty-pkt,html.a11y-no-motion .ty-ring{opacity:0}
html.a11y-no-motion .ty-dest .ty-box{border-color:var(--red-ink-inverse);box-shadow:inset 0 0 0 1px var(--red-ink-inverse)}
html.a11y-no-motion .ty-dest .ty-glyph{stroke:var(--red-ink-inverse)}
html.a11y-no-motion .ty-dest span{color:#fff}

/* ===== legal pages (terms / privacy) + FAQ ===== */
/* 70ch, not the old flat 800px: at 15.5px that measured ~95-100 characters a line, well past
   the 60-75 reading range and inconsistent with .lede's own 65ch. Headings keep the wider
   measure so a long section title still sits on one line. */
.legal-doc{max-width:800px}
/* 70ch at 15.5px measured out at 78-106 characters per line across privacy.html (14 of 22
   paragraphs over 78). Inter's `ch` (the 0 advance) is wider than its average character, so ch
   overstates the real measure: 65ch still landed a median of 75. 62ch measures out at a median
   of ~70, inside the 60-75 band. */
.legal-doc p,.legal-doc li,.legal-doc ul,.legal-doc ol{max-width:62ch}
/* these two are long-form reading surfaces, so the muted tier (4.66:1 light / 6.49:1 dark)
   falls short of the 7:1 the body copy here is held to — step them up to --ink-soft
   (7.85 / 11.3), which is what .legal-doc p already uses */
.legal-meta,.legal-doc table.plain th{color:var(--ink-soft)}
.legal-doc h2{font-size:clamp(22px,2.4vw,30px);margin:52px 0 14px}
.legal-doc h2:first-of-type{margin-top:0}
.legal-doc h3{margin:28px 0 8px;font-size:17px}
/* 16px, not 15.5: this is the longest-form reading surface on the site and the one place a
   sub-16px body size costs the most. */
.legal-doc p,.legal-doc li{font-size:16px;line-height:1.7;color:var(--ink-soft);margin-bottom:12px}
.legal-doc ul,.legal-doc ol{padding-left:22px;margin-bottom:16px}
.legal-doc li{margin-bottom:6px}
.legal-doc table.plain td,.legal-doc table.plain th{font-size:14px}
.legal-meta{font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:36px}
.legal-toc{background:var(--paper-50);border:1px solid var(--hairline);border-radius:var(--radius-card);padding:24px 28px;margin-bottom:44px}
.legal-toc ol{margin:0;padding-left:20px;columns:2;column-gap:40px}
@media(max-width:640px){.legal-toc ol{columns:1}}
/* inline-block so the existing line-height:2 becomes real box height: the links measured 17px
   (the inline content box), which fails WCAG 2.2 AA 2.5.8 for a link list. Now 28px, and the
   rows were already spaced by that line-height so nothing moves. (audit H3, 2026-07-30) */
.legal-toc a{color:var(--ink-soft);text-decoration:none;font-size:14px;line-height:2;display:inline-block;transition:color 180ms var(--ease)}
.legal-toc a:hover{color:var(--red-ink)}
@media(prefers-reduced-motion:reduce){.legal-toc a{transition:none}}
html.a11y-no-motion .legal-toc a{transition:none}
/* FAQ */
.faq-group{margin-bottom:48px}
.faq-group h2{margin-bottom:8px}
.faq-teaser{display:flex;align-items:center;justify-content:space-between;gap:20px;flex-wrap:wrap}

/* ===== Google Reviews section ===== */
.gr-row{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.gr-num{font-size:46px;font-weight:500;color:var(--ink);line-height:1}
.gr-stars{display:inline-flex;gap:3px}
.gr-star{position:relative;width:27px;height:27px;display:inline-block}
.gr-star svg{position:absolute;inset:0;width:100%;height:100%}
.gr-bg{fill:var(--paper-500)}
.gr-on{fill:#FBBC04} /* Google's review amber — third-party rating keeps its own colour */
.gr-count{font-size:13px;color:var(--ink-muted)}
.gr-none{font-size:15.5px;line-height:1.6;color:var(--ink-soft);max-width:52ch}

/* ————— Review cards (.rv-*) — individual Google reviews beside the .gr-* aggregate —————
   Shared, not page-scoped, because this rolls out to more pages after sign-off.
   Structurally a promotion of the parked .vvt-* strip from values.html: same scroll-snap
   mechanic, same 44px circular nav, same dark-theme lift. Two deliberate reuses rather than
   new decisions: cards are the sitewide `.card` surface, and the star rows are the existing
   .gr-star/.gr-on/.gr-bg component, so the amber stays the one documented third-party-colour
   exception (see .gr-on above) instead of inventing a second non-red accent.
   REVERSED 2026-08-11: shipped with no hover flourish on the theory these cards are not links.
   Feedback was that a card with no affordance at all just reads as broken next to every other
   card on the site, so it now takes the sitewide red conic-sweep (@property --sweep, defined
   once above with .audience-tiles) like .mf-card/.wwd-card do. */
.rv-strip{display:flex;align-items:flex-start;gap:clamp(12px,1.4vw,18px);overflow-x:auto;
  scroll-snap-type:x mandatory;scrollbar-width:none;-ms-overflow-style:none;
  margin-top:clamp(26px,3vw,36px);padding:6px 4px}
.rv-strip::-webkit-scrollbar{display:none}
/* the strip is focusable so the cards are reachable by keyboard scroll, hence a visible ring */
.rv-strip:focus-visible{outline:2px solid var(--red-ink);outline-offset:4px;border-radius:4px}
.rv-card{position:relative;flex:0 0 min(340px,82vw);scroll-snap-align:start;display:flex;
  flex-direction:column;margin:0;padding:clamp(22px,2.2vw,28px)}
.rv-card::after{content:'';position:absolute;inset:-1px;padding:1px;border-radius:calc(var(--radius-card) + 1px);
  pointer-events:none;--sweep:0deg;
  background:conic-gradient(from 135deg,transparent 0 calc(180deg - var(--sweep)),var(--red) calc(180deg - var(--sweep)) calc(180deg + var(--sweep)),transparent calc(180deg + var(--sweep)));
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;mask-composite:exclude;transition:--sweep 280ms cubic-bezier(.2,0,0,1)}
@media(hover:hover) and (pointer:fine){.rv-card:hover::after{--sweep:180deg}}
.rv-card:focus-within::after{--sweep:180deg}
@media(prefers-reduced-motion:reduce){.rv-card::after{transition:none}}
html.a11y-no-motion .rv-card::after{transition:none}
/* Hovering Read more/Read less should show ONLY that link's own feedback (.biglink turns
   red-ink) — the whole card lifting and sweeping just because the pointer is over a small text
   link inside it implies the card itself is the clickable target, which it is not (2026-08-11,
   card-level hover kept for :focus-within — a keyboard user tabbing to the link benefits from
   the fuller "something inside here is focused" cue that a mouse hovering a link does not). */
@media(hover:hover) and (pointer:fine){
  .rv-card:has(.rv-more:hover){transform:none;border-color:var(--hairline)}
  .rv-card:has(.rv-more:hover)::after{--sweep:0deg}
}
html[data-theme="dark"] .rv-card:has(.rv-more:hover){border-color:rgba(255,255,255,.10)}
.rv-card blockquote{margin:0;padding:0;border:0;flex:1}
/* Closed by default at EXACTLY 5 lines regardless of the review's real length, so ten cards of
   wildly different lengths (a one-line review next to a five-sentence one) read as one uniform
   row instead of the whole strip stretching to match the longest. line-clamp on its own only
   caps the MAXIMUM at 5 — a genuinely 2-line review still only takes up 2 lines of height,
   which is what actually kept short cards shorter than long ones even after the clamp was added
   (caught 2026-08-11: Kyle Law's 4-line review sat visibly taller than Hovard Heimdal's
   2-line one). min-height forces the floor to match the ceiling. */
.rv-card blockquote p{margin:0;font-size:15px;line-height:1.65;color:var(--ink-soft);
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:5;overflow:hidden;
  min-height:calc(1.65em * 5)}
.rv-card.is-expanded blockquote p{-webkit-line-clamp:unset;overflow:visible;display:block;min-height:0}
/* visibility, not the hidden attribute: a card whose review fits in 5 lines still needs the
   SAME reserved row as one that doesn't, or short cards sit shorter than their neighbours and
   the row stops looking aligned. JS confirms real overflow before adding .has-overflow (same
   "no dead affordance" rule as .rv-nav) — invisible means also unfocusable and untappable, so
   nothing points at a control with nothing to do. */
.rv-more{display:inline-block;margin-top:10px;font-size:13px;min-height:0;visibility:hidden}
.rv-more.has-overflow{visibility:visible}
/* Star row gets its OWN line at the top rather than sharing the name row: a long name
   ("Δημήτρης Αντωνίου") plus 5 stars crowds badly inside a min(340px,82vw) card, and it wraps
   differently per reviewer. Attribution sits at the foot, which is the same order the parked
   .vvt-* testimonial card used. */
.rv-card .gr-stars{margin-bottom:14px}
.rv-who{display:flex;align-items:center;gap:12px;margin-top:auto;padding-top:22px}
.rv-ava{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;
  border-radius:50%;background:var(--ink-muted);color:var(--paper-0);font-size:12px;
  font-family:var(--font-mono);font-weight:500;flex:none;letter-spacing:.02em;
  position:relative;overflow:hidden}
/* Reviewer photo, when one is configured: covers the initials underneath, which stay in the
   DOM as the fallback for a broken/expired hotlinked URL (removed via JS onerror). */
.rv-ava img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.rv-name{display:block;font-size:14.5px;font-weight:700;color:var(--ink);line-height:1.3}
/* ink-soft not ink-muted: 13px body text needs 4.5:1 and muted measures 4.34 on --paper-50 */
.rv-date{display:block;font-size:13px;color:var(--ink-soft);line-height:1.4}
/* the in-card stars are a supporting detail, not the headline figure the aggregate shows */
.rv-card .gr-star{width:16px;height:16px}
/* Nav is hidden until JS confirms the strip actually overflows — arrows that cannot move
   anything are the dead affordance the 2026-07-30 audit flagged on the values strip.
   Arrows flank the dots (space-between) rather than sitting side by side — prev at the left
   edge, next at the right, dots centred in the middle (2026-08-11). NOT flex-wrap: with up to
   ten dashes (one per card at 390px) the row can run ~6px past the viewport, and wrap wants to
   solve that by dropping BOTH arrows onto their own line together — exactly the side-by-side
   layout this rule exists to avoid. .rv-dots scrolling internally (below) is what actually
   absorbs the overflow, so the arrows never have anywhere to escape to. */
.rv-nav{display:flex;align-items:center;gap:10px;margin-top:clamp(20px,2.6vw,28px)}
.rv-nav[hidden]{display:none}
/* Dots — styled as short dashes (a loading-track look) rather than round dots, so the active
   one reads as "progress" rather than just "selected". token colours, a width change as the
   non-colour active cue (the same reasoning as .lang-list button[aria-checked]), and the
   ::before hit-area trick .lang-btn/.theme-switch use to reach 44px without changing the visual
   size. flex:1 + justify-content:center centres the group between the two arrows regardless of
   how many dashes there are; min-width:0 lets it actually shrink below its content's natural
   width instead of forcing the row wider than the viewport, and overflow-x scrolls internally
   on the rare width where ten dashes still don't fit in what's left between the two arrows. */
/* padding-block 20px is what makes the 44px hit area below actually reachable, and it is not
   spacing. `overflow-x:auto` on this row makes it a scroll container, and a scroll container
   clips at its PADDING box — so with padding 0 it was cropping `.rv-dots button::before` (the
   inset:-20px expander) back to the dash's own 4px, leaving a 26x4 / 14x4 target. Proved rather
   than reasoned: elementFromPoint 12px above a dash returned DIV.rv-nav, forcing overflow:visible
   made the same point return BUTTON, and clicking the 4px dash itself worked throughout — so the
   handler was fine and only the AREA was lost. Note the two axes cannot be split here: with
   overflow-x:auto, an overflow-y:visible computes to auto, so the fix has to be room, not a
   different overflow value.
   20px is exactly the ::before's own inset, so the expander now ends flush with the padding edge
   and nothing is cropped. It costs no layout: .rv-nav is a 44px row set by its two 44x44
   .rv-arrow children with align-items:center, so this box growing 4px -> 44px changes no
   height anywhere — measured identical at 390 / 834 / 1440. */
.rv-dots{display:flex;flex:1;min-width:0;align-items:center;justify-content:center;gap:12px;
  padding-block:20px;
  overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}
.rv-dots::-webkit-scrollbar{display:none}
/* gap 12px is load-bearing, same math the old round dots used: pitch (14px dash + 12px gap) is
   26px, and the ::before hit-area below is sized to 24px wide — the WCAG 2.2 AA minimum,
   2px shy of the pitch so adjacent targets never overlap and activate the wrong dash. */
.rv-dots button{position:relative;width:14px;height:4px;padding:0;border:0;border-radius:2px;
  background:var(--paper-500);cursor:pointer;transition:width 200ms var(--ease),background 200ms var(--ease)}
/* Hit target: 24x44, exactly the 24x24 WCAG 2.2 AA minimum on the width axis with generous
   vertical reach. Same ::before trick .lang-btn/.theme-switch use — no visual change. */
.rv-dots button::before{content:'';position:absolute;inset:-20px -5px}
/* --red-ink, not --red: this is a persistent state indicator (which dash you're on), not a
   decorative flourish, so it takes the contrast-tuned token (brighter in dark mode) rather than
   the fixed brand hex the conic-sweep uses.
   The active dash is a TRACK; its ::after is the red that fills across it over one autoplay dwell,
   so the dash says how long until the next card as well as which card you are on. The fill's
   resting state is scaleX(1) — identical to the solid red dash this was before — so with autoplay
   off (reduced motion, a strip that cannot scroll, or a visitor who took manual control) nothing
   about the old appearance changes. Only .is-running turns it into a countdown. */
.rv-dots button[aria-current="true"]{width:26px;border-radius:99px;background:var(--paper-500)}
.rv-dots button[aria-current="true"]::after{content:'';position:absolute;inset:0;border-radius:99px;
  background:var(--red-ink);transform-origin:left center;transform:scaleX(1)}
/* --rv-dwell is written by tpl.js from its own AUTOPLAY_MS, so the bar and the timer cannot drift. */
.rv-dots.is-running button[aria-current="true"]::after{animation:rvDashFill var(--rv-dwell,6000ms) linear both}
/* Hover/focus/hidden-tab pause clears the interval and restarts it from zero on resume, so the fill
   freezes here and is restarted by JS rather than resumed — otherwise the dash would promise time
   the timer is not actually counting. */
.rv-dots.is-paused button[aria-current="true"]::after{animation-play-state:paused}
@keyframes rvDashFill{from{transform:scaleX(0)}to{transform:scaleX(1)}}
.rv-dots button:focus-visible{outline:2px solid var(--red-ink);outline-offset:4px}
@media(prefers-reduced-motion:reduce){
  .rv-dots button{transition:none}
  .rv-dots.is-running button[aria-current="true"]::after{animation:none;transform:scaleX(1)}
}
html.a11y-no-motion .rv-dots button{transition:none}
html.a11y-no-motion .rv-dots.is-running button[aria-current="true"]::after{animation:none;transform:scaleX(1)}
.rv-arrow{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;
  border-radius:50%;background:var(--paper-50);border:1px solid var(--hairline);color:var(--ink);
  cursor:pointer;flex:none;transition:border-color 150ms var(--ease)}
.rv-arrow svg{width:18px;height:18px}
.rv-arrow:disabled{opacity:.38;cursor:default}
@media(hover:hover) and (pointer:fine){.rv-arrow:not(:disabled):hover{border-color:var(--ink)}}
.rv-links{display:flex;gap:4px 28px;flex-wrap:wrap;margin-top:clamp(18px,2.4vw,26px)}
/* Dark theme: --paper-50 resolves to #161618 against the #101012 page, which is nearly the
   same value, so the card edge disappears. Same one-step lift + hairline the .vv-duo and
   .vvt cards needed. */
html[data-theme="dark"] .rv-card,
html[data-theme="dark"] .rv-arrow{background:var(--paper-0);border-color:rgba(255,255,255,.10)}
/* No dark override for .rv-dots button: --paper-500 (inactive) and --red-ink (active) are both
   already theme-aware tokens. An earlier draft re-asserted --paper-500 here "for dark mode"
   anyway — harmless while both states used --ink, but once the active state became --red-ink
   this WON on specificity (html + button beats .rv-dots + button[aria-current]) and silently
   overwrote the active dash back to the inactive grey in dark mode only. Removed rather than
   patched with a :not() — it was dead weight even before it started actively breaking things. */
@media(prefers-reduced-motion:reduce){.rv-arrow{transition:none}}
html.a11y-no-motion .rv-arrow{transition:none}
/* .btn on <button> elements */
button.btn{border:0;cursor:pointer;font-family:var(--font-body)} /* kill the UA button border — <a>.btn never had one */
button.btn-ghost{border:1px solid var(--ink);background:none;color:var(--ink)} /* re-assert: button.btn{border:0} outweighs .btn-ghost */
button.btn-ghost:hover{background:var(--ink);color:var(--paper-100)}

/* ===== team open-desk filler cards — blurred anonymous silhouettes ===== */
.tm-sil{display:flex;align-items:flex-end;justify-content:center;background:var(--paper-200)}
.tm-sil svg{width:94%;height:94%;filter:blur(11px);opacity:.9}
/* background-color, not the `background` shorthand: `background:none` resets EVERY background
   sub-property to its initial value, including the size/position/repeat .biglink's own rule
   sets for its underline trick — leaving only background-image re-declared on :hover, which
   then paints edge-to-edge instead of as a thin line (found 2026-08-11 on the first real
   button.biglink instance, .rv-more, to actually hit this). background-color:transparent only
   strips the UA button chrome this rule exists for, leaving .biglink's own background alone. */
button.biglink{border:0;background-color:transparent;padding:0;font:inherit;font-weight:600;color:inherit;cursor:pointer;text-align:left}

/* ===== 404 ===== */
.nf-code{font-family:var(--font-mono);font-weight:500;font-size:clamp(92px,14vw,180px);line-height:1;letter-spacing:.02em;color:var(--ink)}
.nf-code span{color:var(--red-ink)}
.nf-quote{display:block;margin:24px 0 0;font-family:var(--font-mono);font-size:14px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);min-height:22px}
.nf-quote::after{content:"_";color:var(--red-ink);animation:ldr-blink 1.1s steps(1) infinite}
html.a11y-no-motion .nf-quote::after{animation:none}
@media(prefers-reduced-motion:reduce){.nf-quote::after{animation:none}}

/* ===== footer social row (under the TPL info, on brand red) ===== */
.ft-soc{display:flex;gap:10px;margin-top:18px}
.ft-soc a{width:44px;height:44px;border-radius:50%;border:1px solid rgba(255,255,255,.35);color:rgba(255,255,255,.85);display:inline-flex;align-items:center;justify-content:center;padding:0;transition:border-color 150ms var(--ease),color 150ms var(--ease)}
.ft-soc a:hover{border-color:#fff;color:#fff}
.ft-soc svg{width:18px;height:18px}
html.a11y-no-motion .ft-soc a{transition:none}
@media(prefers-reduced-motion:reduce){.ft-soc a{transition:none}}

/* ===== Media & Press — brand kit components ===== */
.dl-tile{background:var(--paper-0);border:1px solid var(--hairline);border-radius:12px;padding:28px;display:flex;align-items:center;justify-content:center;min-height:150px;margin-bottom:18px}
.dl-tile img{max-height:88px;width:auto;max-width:100%}
.dl-tile.on-ink{background:var(--inverse);border-color:#2a2a2e}
.dl-tile.on-red{background:var(--red);border-color:var(--red)}
.dl-links{display:flex;flex-wrap:wrap;gap:4px 18px}
.dl-links a{display:inline-flex;align-items:center;min-height:44px;font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);text-decoration:none}
.dl-links a:hover{color:var(--red-ink)}
/* color swatches */
.sw-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:16px}
.sw{border:1px solid var(--hairline);border-radius:12px;overflow:hidden;background:var(--paper-50)}
.sw i{display:block;height:88px}
.sw div{padding:12px 16px 14px}
.sw strong{display:block;font-family:var(--font-display);font-weight:700;font-size:14px}
.sw span{font-family:var(--font-mono);font-size:12px;color:var(--ink-muted)}
/* type specimens */
.type-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 24px;align-items:baseline;padding:22px 0;border-bottom:1px solid var(--hairline)}
.type-row .demo{font-size:clamp(24px,3vw,38px);line-height:1.1}
.type-row .who{font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted);text-align:right}
@media(max-width:640px){.type-row{grid-template-columns:1fr}.type-row .who{text-align:left}}
/* wallpaper cards */
.wp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:20px}
.wp{background:var(--paper-50);border:1px solid var(--hairline);border-radius:12px;overflow:hidden}
.wp .shot{display:block;background:var(--inverse)}
.wp .shot img{width:100%;height:170px;object-fit:cover;display:block}
.wp div.meta{padding:14px 16px 12px}
.wp strong{display:block;font-family:var(--font-display);font-weight:700;font-size:14.5px;margin-bottom:2px}
.wp span{font-family:var(--font-mono);font-size:11.5px;color:var(--ink-muted)}
/* ── press kit (2026-08-25): the kit as a spec sheet ──
   One signature: a hairline KIT INDEX that stays under the chrome and tracks the section in view
   (tpl.js "Press kit index"). Every section below it is a SPREAD — heading column that stays put
   on the left, content on the right — the same asymmetric register the About dossier's editorial
   spreads use, so the kit reads as one document rather than a stack of card grids.
   Colour: red is spent on eyebrows, two-tone clauses and hover only. The old .band-tint pink wash
   is gone from press; grounds alternate on the paper ramp instead (.pk-ground = --paper-50, one
   step lighter than the --paper-100 body). The ecommerce photo strips followed on 2026-08-31
   (.band-step, ~3040), and on 2026-09-01 the last two callers (.cs-more--tint, .mfsec--tint)
   went too and --red-wash was deleted outright — press was simply first. The wallpapers — five red cube fields — sit on the
   dark band, where they read as screens instead of shouting on white.
   The logo cards are the SECOND caller of .card-arrive (ADR-025); the frame draws the edge, the
   kicker names the version, the download links stay the foot. */
.pk-boiler{background:var(--paper-0)}
.pk-boiler-k2{display:block;margin-top:26px;padding-top:22px;border-top:1px solid var(--hairline)}
/* kit index — sticky under the chrome; --chrome-top is state-derived, so it follows the nbar */
.pk-index{position:sticky;top:var(--chrome-top);z-index:20;background:var(--paper-100);border-bottom:1px solid var(--hairline)}
.pk-index .wrap{display:flex;align-items:center;gap:clamp(16px,3vw,40px);min-height:56px}
.pk-index-l{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);flex:none}
.pk-index ul{list-style:none;margin:0;padding:0;display:flex;gap:clamp(4px,1.6vw,20px);overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.pk-index ul::-webkit-scrollbar{display:none}
/* on a phone the row overflows: fade the right edge so the cut item reads as "more", not "clipped" */
@media(max-width:720px){.pk-index ul{-webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 40px),transparent);mask-image:linear-gradient(90deg,#000 calc(100% - 40px),transparent);padding-right:40px}}
.pk-index li{flex:none}
.pk-index a{position:relative;display:inline-flex;align-items:center;min-height:56px;padding:0 6px;font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted);text-decoration:none;white-space:nowrap;transition:color 200ms var(--ease)}
.pk-index a::after{content:"";position:absolute;left:6px;right:6px;bottom:-1px;height:2px;background:var(--ink);transform:scaleX(0);transform-origin:left;transition:transform 260ms var(--ease)}
.pk-index a:hover{color:var(--ink)}
.pk-index a[aria-current="true"]{color:var(--ink)}
.pk-index a[aria-current="true"]::after{transform:scaleX(1)}
.pk-index a:focus-visible{outline:2px solid var(--ink);outline-offset:-4px}
/* spreads */
.pk-sec{padding:clamp(72px,8vw,112px) 0}
.pk-sec[id]{scroll-margin-top:calc(var(--chrome-top) + 72px)}
/* the body ground is --paper-100, so the alternate ground steps LIGHTER, to the sheet colour */
.pk-ground{background:var(--paper-50)}
.pk-spread{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(32px,5vw,88px);align-items:start}
.pk-head{position:sticky;top:calc(var(--chrome-top) + 88px)}
.pk-head .two-tone{margin-bottom:20px}
.pk-head .lede{max-width:44ch}
@media(max-width:960px){.pk-spread{grid-template-columns:1fr;gap:36px}.pk-head{position:static}}
/* ledger — single column inside the spread, labels wide enough for "Operating since" */
.pk-ledger>div{grid-template-columns:132px 1fr;padding:14px 0}
.pk-ledger dd{color:var(--ink)}
@media(max-width:560px){.pk-ledger>div{grid-template-columns:1fr;gap:3px}}
/* logo cards — 2-up spread; clip, never hidden: hidden is a scroll container (ADR-023) */
.pk-logos{display:grid;grid-template-columns:1fr 1fr;gap:20px;overflow-x:clip}
/* one surface per card: the logo plate runs edge to edge under the frame's top corners, the
   caption sits below it — a product tile, not a box inside a box */
.pk-logos .card-arrive .card{display:flex;flex-direction:column;padding:0;min-height:0;background:var(--paper-0)}
/* --paper-fixed, NOT --paper-100: this plate is a brand spec, and the primary logo's wordmark is
   near-black, so a theme-reactive plate turns dark and the wordmark vanishes — the exact case the
   --paper-fixed token was added for (tpl.css :55, "logo plates"). The card under it still follows
   the theme; only the plate is pinned. */
.pk-logos .card-arrive .dl-tile{margin:0;border:0;border-bottom:1px solid var(--hairline);border-radius:var(--radius-card) var(--radius-card) 0 0;min-height:176px;background:var(--paper-fixed)}
.pk-logos .card-arrive .dl-tile.on-ink{background:var(--inverse)}
.pk-logos .card-arrive .dl-tile.on-red{background:var(--red)}
.pk-logo-b{display:flex;flex-direction:column;flex:1;padding:18px 22px 20px}
.pk-logos .card-arrive .kicker{display:block;margin-bottom:8px;color:var(--ink-soft)}
.pk-logos .card-arrive p{color:var(--ink-soft);font-size:15px;margin:0 0 10px}
.pk-logos .card-arrive .dl-links{margin-top:auto}
@media(max-width:640px){.pk-logos{grid-template-columns:1fr}.pk-logos>.card-arrive[data-dir]{--tx:0;--rot:0deg}}
/* colour & type */
.pk-rules li span{color:var(--ink-soft)}
.pk-speck{display:block;margin:40px 0 14px}
/* swatches without card chrome: a flat colour field, name and hex under it in plain text */
.pk-sw{grid-template-columns:repeat(4,minmax(0,1fr));gap:20px}
.pk-sw .sw{border:0;background:transparent;border-radius:0;overflow:visible}
.pk-sw .sw i{height:128px;border-radius:12px}
.pk-sw .sw i.is-paper{border:1px solid var(--hairline)}
.pk-sw .sw div{padding:12px 2px 0}
@media(max-width:640px){.pk-sw{grid-template-columns:repeat(2,minmax(0,1fr))}}
.pk-type .type-row .demo{font-size:clamp(30px,3.6vw,52px);letter-spacing:-.01em}
.pk-type .type-row .demo.num{font-size:clamp(24px,2.9vw,42px)}   /* mono is ~1.25x wider per glyph; keeps the specimen on one line */
/* wallpapers: two device silhouettes (CSS bezels, --ink so they follow the theme) and a hairline
   file list. The old .wp thumbnail cards are gone from the page; .wp-grid/.wp stay for any other caller. */
/* --inverse, NOT --ink: --inverse is a theme-INVARIANT primitive (tpl.css :60), so the devices stay
   dark objects in both themes. --ink flips to near-white in dark and the bezels turned into white
   frames around dark art. */
.pk-devices{display:grid;grid-template-columns:minmax(0,1fr) clamp(96px,11vw,150px);gap:clamp(20px,3vw,44px);align-items:end;margin-bottom:44px}
.pk-dev{margin:0;position:relative}
.pk-dev img{display:block;width:100%;height:auto}
.pk-dev--laptop{padding:9px 9px 0;background:var(--inverse);border-radius:12px 12px 0 0}
.pk-dev--laptop img{border-radius:5px 5px 0 0}
.pk-dev--laptop::after{content:"";display:block;height:12px;margin:0 -3.5%;background:var(--inverse);border-radius:0 0 10px 10px;box-shadow:inset 0 1px 0 rgba(255,255,255,.16)}
.pk-dev--phone{padding:7px;background:var(--inverse);border-radius:28px}
.pk-dev--phone img{border-radius:22px}
.pk-files{list-style:none;margin:0;padding:0;border-top:1px solid var(--hairline)}
.pk-files li{border-bottom:1px solid var(--hairline)}
.pk-files a{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;gap:6px 28px;align-items:baseline;padding:16px 0;color:inherit;text-decoration:none}
.pk-fn{font-weight:600;color:var(--ink)}
.pk-fd,.pk-fm{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.06em;color:var(--ink-muted);white-space:nowrap}
.pk-fgo{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);transition:color 200ms var(--ease)}
.pk-files a:hover .pk-fgo,.pk-files a:focus-visible .pk-fgo{color:var(--red-ink)}
.pk-files a:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
@media(max-width:640px){.pk-files a{grid-template-columns:minmax(0,1fr) auto}.pk-fm{display:none}.pk-fd{grid-column:1;order:3}.pk-devices{grid-template-columns:minmax(0,1fr) 84px}}
/* news list — category is mono grey, not red: the page's red budget is spent on the chrome */
.pk-news{list-style:none;margin:0;padding:0;border-top:1px solid var(--hairline)}
.pk-news li{border-bottom:1px solid var(--hairline)}
.pk-news a{display:grid;grid-template-columns:150px 1fr auto;gap:6px 24px;align-items:baseline;padding:20px 0;color:inherit;text-decoration:none}
.pk-news time,.pk-news .pk-cat{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted)}
.pk-news .pk-t{font-family:var(--font-display);font-weight:700;font-size:clamp(17px,1.6vw,21px);line-height:1.25;color:var(--ink);transition:color 200ms var(--ease)}
.pk-news a:hover .pk-t,.pk-news a:focus-visible .pk-t{color:var(--red-ink)}
.pk-news a:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
@media(max-width:640px){.pk-news a{grid-template-columns:1fr}.pk-news .pk-cat{order:-1}}
/* press desk — two cards, stacked in the content column */
.pk-desk{display:grid;gap:20px}
.pk-desk .card{background:var(--paper-0)}
.pk-desk .card .kicker{display:block}
.pk-desk .card p{color:var(--ink-soft)}
html.a11y-no-motion .pk-news .pk-t,html.a11y-no-motion .pk-index a,html.a11y-no-motion .pk-index a::after,html.a11y-no-motion .pk-fgo{transition:none}
@media(prefers-reduced-motion:reduce){.pk-news .pk-t,.pk-index a,.pk-index a::after,.pk-fgo{transition:none}}

/* ===== First-load loader (.ldr) — shown via html[data-loading], set by the head pre-paint gate
   (first visit per session only; skipped under prefers-reduced-motion / the a11y toggle).

   REVERTED 2026-08-06 (REQ-007, spec 2026-08-06-loader-revert-original-layout-glyph-typewriter-
   design.md) to the composition that preceded the 2026-08-04 centred column: the mark stands alone
   in the optical centre, and a bottom rail carries the mono typewriter quote on the left with the
   percentage on the right, over a full-bleed 2px bar on the very edge of the viewport.

   The RED is the one thing the revert keeps from 2026-08-04, and it is why every ink and rule
   colour below is a LITERAL rather than a token. A band pinned to one background cannot take ink
   from the theme-reactive set: --ink-soft is #4A4A4A in light (~1.6:1 on red, unreadable in the
   DEFAULT theme), --ink is #181818 in light and #000 under a11y-contrast, and --hairline is
   #2E2E33 in dark. --red itself is NOT overridden in dark, so the screen is #AB0000 in both
   themes and #900000 under a11y-contrast. White clears AA on both: 7.69:1 and ~10:1.

   mono + uppercase + nowrap is also what makes the glyph-churn head free: every random glyph sits
   in the same cell as the letter it resolves to, so the line cannot shift by a pixel.

   The mark is white by CSS fill/stroke, which outranks the fill="#AB0000" presentation attributes
   baked into the SVG, so this whole revert needed no HTML edit. Note the .ldr markup exists ONLY
   on index.html — the other 41 pages never had it, though the head pre-paint gate does burn
   sessionStorage.tpl-seen on all of them (REQ-007 "Still open").

   Timing is in tpl.js (search "First-load loader"; line numbers drift, five streams edit that file)
   and is readiness-governed: 2400ms floor, 5000ms ceiling. The
   percentage reads the SAME value as the bar, so it parks at 92% on a cold load instead of
   counting to 100 and waiting on the network. It is NOT the 6000ms clock this layout originally
   shipped with, which is the whole subject of REQ-007.

   CSS failsafe fades the screen if tpl.js never runs; 6.5s tracks the 5000ms ceiling plus the
   ~1.3s exit. That covers the case where the inline head gate DID run but tpl.js never executed:
   the screen sits blank red for 6.5s, because the mark's paths carry fill-opacity="0" until JS
   animates them. With JavaScript actually disabled there is no loader at all — the head gate never
   runs, data-loading is never set, and .ldr stays display:none, which is the right degradation.
   ===== */
.ldr{display:none;position:fixed;inset:0;z-index:300;background:var(--red);
  transition:opacity 420ms var(--ease)}
html[data-loading] .ldr{display:block;animation:ldr-kill .4s ease 6.5s forwards}
.ldr.done{opacity:0;pointer-events:none} /* noMotion failsafe path in tpl.js */
.ldr-mark{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.ldr-mark svg{width:min(34vmin,220px);height:min(34vmin,220px);overflow:visible}
.ldr-mark svg path{fill:#fff;stroke:#fff}
.ldr-foot{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:flex-end;
  justify-content:space-between;gap:24px;padding:0 clamp(20px,4vw,48px) 18px}
.ldr-quote{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.08em;
  text-transform:uppercase;color:#fff;min-height:18px;min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ldr-quote::after{content:"_";color:#fff;animation:ldr-blink 1.1s steps(1) infinite}
/* belt and braces, matching .nf-quote::after three rules up. The head pre-paint gate already keeps
   this screen away from reduced-motion and a11y-no-motion visitors, but tpl.js keeps a whole noMotion
   branch precisely because that gate can be raced, and a blinking cursor is the one thing here that
   would keep animating if it were. */
html.a11y-no-motion .ldr-quote::after{animation:none}
@media(prefers-reduced-motion:reduce){.ldr-quote::after{animation:none}}
/* min-width reserves the widest value the counter can reach, so growing "9%" to "100%" does not
   steal width from the quote across the space-between row. The 3-to-4 character step is written by
   finish() in the same tick that adds is-out, so without it the settled two-line mobile quote can
   re-wrap during the 200ms exit fade. ch is safe on THIS element because font-size is declared on it
   (the 2026-08-04 comment's warning was about .ldr-foot, which inherited 16px instead). */
.ldr-pct{display:block;flex:none;min-width:4.2ch;font-family:var(--font-mono);
  font-size:clamp(36px,4.2vw,60px);text-align:right;
  line-height:1;font-weight:500;letter-spacing:.02em;color:#fff;font-variant-numeric:tabular-nums}
.ldr-line{position:absolute;left:0;right:0;bottom:0;height:2px;background:rgba(255,255,255,.34)}
.ldr-line i{position:absolute;inset:0;background:#fff;transform-origin:left center;
  transform:scaleX(0)}
.ldr.is-out .ldr-mark,.ldr.is-out .ldr-foot{opacity:0;transition:opacity 200ms var(--ease)}
/* the original truncated with an ellipsis on narrow screens, so a sentence never finished typing.
   Two reserved lines instead. The longest quote is 40 characters ("We compete on operations, not
   discounts."), and at 11px mono plus .06em tracking a 375px screen fits ~27 per line once the
   percentage has grown to "100%", so 40 needs two lines and has roughly 14 to spare.
   min-height is a FLOOR, not a ceiling: a longer quote would grow the box upward rather than clip.
   That is deliberate — the foot is bottom-anchored and the bar is a separate absolute element at
   bottom:0, so upward growth cannot move the bar or push anything off screen. */
@media (max-width:640px){
  .ldr-foot{gap:16px;padding:0 20px 16px}
  .ldr-quote{font-size:11px;letter-spacing:.06em;line-height:1.35;
    white-space:normal;text-overflow:clip;min-height:calc(2 * 1.35em)}
}
/* ldr-blink drives the loader's cursor again as well as the 404 page's .nf-quote::after */
@keyframes ldr-blink{50%{opacity:0}}
@keyframes ldr-kill{to{opacity:0;visibility:hidden}}

/* skip link — visually hidden until keyboard-focused, then a solid chip above all chrome */
.skip{position:fixed;left:-9999px;top:14px;z-index:400;font-family:var(--font-body);font-size:14px;font-weight:600;color:var(--paper-0);background:var(--ink);padding:12px 20px;border-radius:9999px;text-decoration:none}
.skip:focus-visible{left:14px;outline:2px solid var(--paper-0);outline-offset:2px}
/* breadcrumb — every page below home; mirrors the menu tree */
.crumb{display:flex;flex-wrap:wrap;align-items:center;gap:10px;font-family:var(--font-mono);font-size:11.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:18px;grid-column:1/-1}
/* breadcrumb as a slim bar directly under the top nav */
/* padding drops 11px→8px to pay for the 18px→24px link box below, so the bar stays exactly
   40px tall and nothing on the ~18 breadcrumbed pages shifts. */
.crumb-bar{border-bottom:1px solid var(--hairline);padding:8px 0}
.crumb-bar .crumb{margin-bottom:0}
/* WCAG 2.2 AA 2.5.8: the links measured 32x18 (audit H3, 2026-07-30). A breadcrumb is a nav
   list, not prose, so the "inline target" exemption does not apply here. */
.crumb a{color:inherit;text-decoration:none;display:inline-flex;align-items:center;min-height:24px}
.crumb a:hover{color:var(--ink);text-decoration:underline;text-underline-offset:3px}
.crumb [aria-current]{color:var(--red-ink,#ab0000)}
/* step-card icon (model pages) */
.card .ci{width:22px;height:22px;color:var(--red-ink,#ab0000);display:block;margin-bottom:12px}

/* model matrix — the spec ledger (partners.html#models)
   heavy rule under the header, hairline rows, heavy rule before the trade-off row:
   read it like an invoice. Row-label column stays frozen while the sheet scrolls. */
/* textured section container — same blend-to-soften recipe as .mf/.aud/#write:
   paper wash + multiply the noise image into the base. The ledger sheet sits on top. */
#models{background:linear-gradient(rgba(240,240,240,.75),rgba(240,240,240,.75)),#f0f0f0 url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy.webp') center/cover no-repeat;background-blend-mode:normal,multiply}
html[data-theme="dark"] #models{background:linear-gradient(rgba(13,13,14,.75),rgba(13,13,14,.75)),#0d0d0e url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp') center/cover no-repeat;background-blend-mode:normal,multiply}
/* position:relative is load-bearing: the sr-only spans in the cells are absolute, and
   with no positioned ancestor inside the scroller their containing block is the page —
   they escape the scroll box and the whole document drags sideways at tablet widths. */
.mx{border:1px solid var(--hairline);background:var(--paper-0);overflow-x:auto;margin-top:44px;position:relative}
.mx table{width:100%;border-collapse:separate;border-spacing:0;min-width:1150px}
.mx th,.mx td{padding:17px 20px;border:0;border-top:1px solid var(--hairline);border-left:1px solid var(--hairline);vertical-align:top;text-align:left;font-weight:400;background:var(--paper-0);transition:background-color 180ms var(--ease),color 160ms var(--ease)}
.mx thead th{position:relative;border-top:0;border-bottom:2px solid var(--ink);padding-top:20px}
.mx tbody tr:first-child th,.mx tbody tr:first-child td{border-top:0}
.mx th:first-child,.mx td:first-child{border-left:0;position:sticky;left:0;z-index:2;background:var(--paper-100);width:170px;min-width:148px}
.mx th:first-child::after,.mx td:first-child::after{content:"";position:absolute;top:0;bottom:0;right:-1px;width:1px;background:var(--hairline)}
.mx th:nth-child(2),.mx td:nth-child(2){border-left:0}
/* sticky column heads (owner call 2026-09-01): the sheet is taller than a screen, so the five
   model names ride the top edge while it scrolls and the "learn more" row pushes them out at
   the sheet's end — the reader always knows which column is which. ONLY at >=1250px, because
   below that the 1150px table cannot fit the wrap and .mx must stay overflow-x:auto — a scroll
   CONTAINER — and a top-sticky inside a scroll container sticks to that container's scrollport,
   not the page, i.e. never fires. There the frozen first column keeps doing the orientation
   job. At >=1250 the container goes overflow:visible (no h-overflow exists to scroll) which is
   also what frees the sticky; the corner cell's left-sticky goes inert with it, harmlessly.
   top is --chrome-top so the heads tuck under whatever chrome is pinned, following the nbar as
   it hides and returns; z-3 clears the row-label column's z-2. The 2px ink rule under the
   heads travels with them, so the stuck strip keeps the ledger's own edge. */
@media (min-width:1250px){
  .mx{overflow-x:visible}
  /* top transitions at the abar's own 280ms/--ease: --chrome-top FLIPS (72 <-> 111) the instant
     nbar-in lands while the abar slides for 280ms - untransitioned, the heads teleport and a
     39px band of scrolling table text flashes between the two until the abar catches up. In
     lockstep there is no band. */
  .mx thead th{position:sticky;top:var(--chrome-top);z-index:3;transition:top 280ms var(--ease);container-type:scroll-state}
  /* the corner cell inherits z-2 from the frozen-column rule (higher specificity than the line
     above), which ties it with the row-label cells sliding under it - and a tie paints in DOM
     order, so the labels drew over the stuck corner. One step above the column's 2. */
  .mx thead th:first-child{z-index:4}
  /* everything below exists ONLY while the heads are actually pinned (owner call 2026-09-01):
     Chromium understands scroll-state containers, Safari/Firefox simply keep the full-height
     head, the in-table rule and no curtain - the sticky itself still works there. */
  @container scroll-state(stuck: top){
    /* slimmer stuck head: the icon goes, name and tag stay. Hiding it changes the head's
       laid-out height, which is safe: stickiness is decided by the TABLE's top edge, not the
       head's height, so there is no stick/unstick oscillation - just the snap the reader
       already sees. */
    .mx thead th>.ddi{display:none}
    /* opaque curtain from the head's top edge upward: whatever daylight a chrome slide opens
       above the pinned heads, table text must never show through it. Sits under the chrome
       (z50+) at rest, so at steady state it is invisible. -1px eats the hairline seams between
       cells. Corner keeps its own deeper paper. */
    .mx thead th::before{content:"";position:absolute;left:-1px;right:-1px;bottom:100%;height:130px;background:var(--paper-0)}
    .mx thead th:first-child::before{background:var(--paper-100)}
    /* the 2px ink rule grows to the full viewport while pinned. NOT anchor(): a fixed pseudo
       anchored to the sliding head went stale - Chromium resolved anchor() once and never
       again after the 280ms top transition, leaving the line stranded mid-header. An absolute
       pseudo rides the cell itself, so it cannot detach. Geometry: this cell's left edge sits
       at (100vw - min(100vw,--max))/2 wrap margin + 48px pad + 1px .mx border + 170px corner
       column = the offset below; left pulls back to the viewport edge and 100vw spans to the
       other, painted over the in-table border so the two read as one line. */
    .mx thead th:nth-child(2)::after{content:"";position:absolute;bottom:-2px;height:2px;background:var(--ink);left:calc(-1*((100vw - min(100vw, var(--max)))/2 + 219px));width:100vw}
  }
}
.mx-rowlab{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);line-height:1.6}
.mx thead .ddi{width:26px;height:26px;margin:0 0 10px;color:var(--ink)}
.mx-name{display:block;font-family:var(--font-display);font-weight:800;font-size:19px;letter-spacing:-.01em;color:var(--ink);line-height:1.15}
.mx-tag{display:block;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);margin-top:5px}
.mx td{font-size:14.5px;color:var(--ink-soft);line-height:1.5}
.mx td strong{font-weight:700;color:var(--ink)}
.mx td small{display:block;font-size:12.5px;color:var(--ink-muted);margin-top:3px;line-height:1.45}
/* "learn more" row — the link fills the whole cell (negative-margin trick) so the
   entire cell is both the hover target and the click target, not just the text. */
.mx tr.mx-more th,.mx tr.mx-more td{border-top:2px solid var(--ink)}
.mx-more-link{display:block;margin:-17px -20px;padding:17px 20px;font-size:13.5px;font-weight:600;color:var(--ink);text-decoration:none}
.mx-lv{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink)}
.mx-lv i{display:inline-flex;gap:3px}
.mx-lv i b{width:9px;height:9px;background:var(--ink);display:block}
.mx-lv i b.o{background:none;border:1px solid var(--paper-500)}
/* yes/no glyph cells (e.g. Amazon Prime row) — check = ink, cross = muted,
   the same emphasis logic as the filled/hollow pips above */
.mx-yn{display:inline-flex}
.mx-yn-ic{width:19px;height:19px;color:var(--ink)}
.mx-yn--no .mx-yn-ic{color:var(--ink-muted)}
/* trade-off row — the ledger's total line */
.mx tr.mx-to th,.mx tr.mx-to td{border-top:2px solid var(--ink)}
.mx tr.mx-to .mx-rowlab{color:var(--red-ink,#ab0000)}
/* hovered column + the column you came from (#models-<slug>) */
.mx td.cur,.mx th.cur{background:var(--paper-50)}
.mx thead th.cur::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;background:var(--red)}
.mx thead th.cur .mx-name,.mx thead th.cur .mx-tag,.mx thead th.cur .ddi{color:var(--red-ink,#ab0000)}

/* crosshair read — driven by tpl.js: hovering any cell lights its whole row (.xr)
   and column (.xc) as a soft grayscale cross, and snaps the exact cell under the
   cursor (.xhot) to solid red + white text. Table stays untextured — the noise bg
   lives on the #models container. After .cur so hover wins over the "came-from" mark. */
/* the wash is LAYERED over the cell's own paper rather than left translucent: these cells sit
   on sticky planes (the pinned heads, the frozen first column), and a bare rgba background let
   the rows scrolling UNDERNEATH show through the highlighted cell - two rows of type on top of
   each other. Same tone, now opaque. */
.mx td.xr,.mx th.xr,.mx td.xc,.mx th.xc{background:linear-gradient(rgba(24,24,24,.07),rgba(24,24,24,.07)),var(--paper-0)}
.mx th:first-child.xr{background:linear-gradient(rgba(24,24,24,.07),rgba(24,24,24,.07)),var(--paper-100)}
.mx td.xhot,.mx th.xhot{background:var(--red);color:#fff}
.mx td.xhot strong,.mx td.xhot small,.mx td.xhot .mx-lv,.mx td.xhot .mx-more-link,.mx td.xhot .mx-yn-ic{color:#fff}
.mx td.xhot .mx-lv i b{background:#fff}
.mx td.xhot .mx-lv i b.o{background:none;border-color:rgba(255,255,255,.75)}
.mx tbody th.xhot .mx-rowlab{color:#fff}
.mx thead th.xhot .mx-name,.mx thead th.xhot .mx-tag,.mx thead th.xhot .ddi{color:#fff}
html[data-theme="dark"] .mx td.xr,html[data-theme="dark"] .mx th.xr,html[data-theme="dark"] .mx td.xc,html[data-theme="dark"] .mx th.xc{background:linear-gradient(rgba(255,255,255,.09),rgba(255,255,255,.09)),var(--paper-0)}
html[data-theme="dark"] .mx th:first-child.xr{background:linear-gradient(rgba(255,255,255,.09),rgba(255,255,255,.09)),var(--paper-100)}
.mx thead th:hover:not(.xhot) .ddi{color:var(--red-ink,#ab0000)}
/* models-table header icon hover-wiggles removed 2026-07-22 — colour-only hover kept above (motion trim). */
.mx-note{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);margin-top:14px}
html.a11y-no-motion .mx th,html.a11y-no-motion .mx td,html.a11y-no-motion .mx thead th:hover .ddi,html.a11y-no-motion .mx thead th:hover .ddi .mv,html.a11y-no-motion .mx thead th:hover .ddi .mv-a,html.a11y-no-motion .mx thead th:hover .ddi .mv-b{animation:none;transition:none}
@media(prefers-reduced-motion:reduce){.mx th,.mx td{transition:none}.mx thead th:hover .ddi,.mx thead th:hover .ddi .mv,.mx thead th:hover .ddi .mv-a,.mx thead th:hover .ddi .mv-b{animation:none}}
@media(max-width:640px){.mx th,.mx td{padding:14px}.mx-more-link{margin:-14px;padding:14px}.mx th:first-child,.mx td:first-child{width:124px;min-width:124px}}

/* ── mobile compare picker (≤720px): the 6-col grid can't fit a phone, so pick two
   models to read head-to-head. Chip bar + column toggling are injected by tpl.js; the
   desktop grid + crosshair above 720px are untouched (.mx-off is inert there). ── */
.mx-compare{display:none}
@media(max-width:720px){
  .mx-compare{display:block;margin-top:36px}
  .mx-compare-lab{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:12px}
  .mx-compare-chips{display:flex;flex-wrap:wrap;gap:8px}
  .mx-chip{font-family:var(--font-body);font-size:13.5px;font-weight:600;color:var(--ink);background:var(--paper-0);border:1px solid var(--paper-500);border-radius:9999px;padding:8px 15px;min-height:44px;cursor:pointer;transition:background-color 140ms var(--ease),color 140ms var(--ease),border-color 140ms var(--ease)}
  .mx-chip[aria-pressed="true"]{background:var(--red);border-color:var(--red);color:#fff}
  .mx-chip:focus-visible{outline:2px solid var(--red);outline-offset:2px}
  /* Everything from here down assumes the JS picker is present to hide 4 of the 6 columns —
     hence html.mx-picker (set by tpl.js once the chip bar is in the DOM). Without JS the table
     keeps its 1150px min-width and stays horizontally scrollable, which is readable; crushing
     six columns into a fixed-layout 100% width is not. */
  html.mx-picker .mx{overflow-x:visible;margin-top:14px}
  html.mx-picker .mx table{min-width:0;width:100%;table-layout:fixed}
  .mx td.mx-off,.mx th.mx-off{display:none}
  html.mx-picker .mx td,html.mx-picker .mx th{overflow-wrap:break-word}
  html.mx-picker .mx th:first-child,html.mx-picker .mx td:first-child{position:static;width:96px;min-width:96px}
  html.mx-picker .mx th:first-child::after,html.mx-picker .mx td:first-child::after{display:none}
}
html.a11y-no-motion .mx-chip{transition:none}
@media(prefers-reduced-motion:reduce){.mx-chip{transition:none}}

/* ============ Discipline — simple info cards (clear bg · icon · index · title · hover-reveal body · tag) ============ */
/* same blend-to-soften rule as #write/.wwd — wash + multiply the noise image into the base
   color at ~25% visible strength, instead of the old flat .62 scrim */
.mf{background:linear-gradient(rgba(240,240,240,.75),rgba(240,240,240,.75)),#f0f0f0 url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy.webp') center/cover no-repeat;background-blend-mode:normal,multiply;padding:clamp(72px,9vw,120px) 0;border-bottom:0}
.mf-head{max-width:820px;margin:0 0 clamp(36px,5vw,60px)}
.mf-head .eyebrow{color:var(--red-ink,#ab0000);margin-bottom:14px}
.mf-head h2{font-family:var(--font-display);font-weight:800;font-size:clamp(30px,3.6vw,52px);letter-spacing:-.02em;line-height:1.05;margin:0;color:var(--ink);max-width:16ch;text-wrap:balance}
.mf-lede{margin:18px 0 0;font-size:16.5px;line-height:1.6;color:var(--ink-soft);max-width:56ch}
.mf-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(16px,1.6vw,24px)}
/* 42vw resolved to the 520px ceiling at desktop while the resting card holds 302px of visible
   content, so 218px — 42% of the card — was empty, and .mf-foot's margin-top:auto pinned the chip
   to the far bottom which framed the gap rather than hiding it. Even with .mf-body revealed on
   hover (72px) the card still carried 146px of slack. 31vw/450 leaves the reveal its room (374px
   of content against 446px at 1440) without reserving a third of the card for nothing. The
   hover reveal itself is deliberate and untouched — this only stops the card advertising the
   absence while it waits. */
.mf-card{position:relative;display:flex;flex-direction:column;min-height:clamp(380px,31vw,450px);padding:clamp(24px,2.4vw,38px);background:var(--paper-0);border:1px solid var(--hairline);border-radius:3px;transition:border-color .3s var(--ease),transform .3s var(--ease)}
@media(hover:hover) and (pointer:fine){.mf-card:hover{border-color:var(--ink);transform:translateY(-3px)}}
.mf-card:focus-within{border-color:var(--ink);transform:translateY(-3px)}
.mf-card:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
/* primary-red border sweep on hover — same "line" as the audience router */
.mf-card::after{content:"";position:absolute;inset:-1px;padding:1px;border-radius:4px;pointer-events:none;--sweep:0deg;background:conic-gradient(from 135deg,transparent 0 calc(180deg - var(--sweep)),var(--red) calc(180deg - var(--sweep)) calc(180deg + var(--sweep)),transparent calc(180deg + var(--sweep)));-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;transition:--sweep 280ms cubic-bezier(.2,0,0,1)}
@media(hover:hover) and (pointer:fine){.mf-card:hover::after{--sweep:180deg}}
.mf-card:focus-within::after{--sweep:180deg}
@media(prefers-reduced-motion:reduce){.mf-card::after{transition:none}}
html.a11y-no-motion .mf-card::after{transition:none}
.mf-top{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}
.mf-ico{width:clamp(72px,6vw,104px);height:clamp(72px,6vw,104px);color:var(--ink);flex:none}
.mf-idx{font-family:var(--font-mono);font-size:14px;font-weight:500;letter-spacing:.04em;color:var(--ink-muted)}
.mf-title{font-family:var(--font-display);font-weight:700;font-size:clamp(21px,1.7vw,26px);letter-spacing:-.01em;line-height:1.12;color:var(--ink);margin:clamp(44px,5vw,76px) 0 0}
.mf-body{font-size:15px;line-height:1.6;color:var(--ink-soft);margin:14px 0 0;max-width:36ch}
.mf-foot{margin-top:auto;display:flex;justify-content:space-between;align-items:center;gap:12px;padding-top:20px}
.mf-tag{font-family:var(--font-mono);font-size:12.5px;font-weight:500;letter-spacing:.05em;text-transform:uppercase;color:#fff;background:var(--red);padding:4px 8px;border-radius:2px;white-space:nowrap}
.mf-more{font-family:var(--font-mono);font-size:20px;line-height:1;color:var(--ink-muted);transition:transform .3s var(--ease),color .3s var(--ease)}
@media(hover:hover) and (pointer:fine){.mf-card:hover .mf-more{transform:rotate(45deg);color:var(--red-ink,#ab0000)}}
.mf-card:focus-within .mf-more{transform:rotate(45deg);color:var(--red-ink,#ab0000)}
/* hover/focus reveals the body; on touch (no hover) it stays visible */
@media(hover:hover){
  .mf-body{opacity:0;transform:translateY(8px);transition:opacity .38s var(--ease),transform .38s var(--ease)}
  .mf-card:hover .mf-body,.mf-card:focus-within .mf-body{opacity:1;transform:none}
}
/* Two columns through the tablet band. `1fr` is `minmax(auto,1fr)`, so the three tracks cannot
   shrink below the cards' min-content and freeze at 301+269+285 = 855px; with the two 16px gaps
   the row needs 887px of content box, but the 3-column layout used to be held all the way down
   to 821px where .wrap only offers ~740px. Measured result: the page scrolled sideways for the
   whole 821-939px band (clean at 940px with 0.4px to spare, +4px over at 935px). Switching at
   960px leaves ~20px of margin at the narrowest 3-column width, so a slower webfont swap that
   widens min-content slightly cannot reopen the overflow. */
@media(min-width:821px) and (max-width:960px){.mf-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:820px){.mf-grid{grid-template-columns:1fr;gap:14px}.mf-card{min-height:0;padding:26px}.mf-card .mf-body{opacity:1;transform:none;margin-top:14px}.mf-title{margin-top:28px}.mf-card .mf-more{display:none}}
@media(prefers-reduced-motion:reduce){.mf-card,.mf-body,.mf-more{transition:none}.mf-card:hover,.mf-card:focus-within{transform:none}}
html.a11y-no-motion .mf-card,html.a11y-no-motion .mf-body,html.a11y-no-motion .mf-more{transition:none}
html.a11y-no-motion .mf-card:hover,html.a11y-no-motion .mf-card:focus-within{transform:none}
/* dark mode — dark grain bg + dark card surfaces (base card is hard #fff, so override here) */
html[data-theme="dark"] .mf{background:linear-gradient(rgba(13,13,14,.75),rgba(13,13,14,.75)),#0d0d0e url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp') center/cover no-repeat;background-blend-mode:normal,multiply}
html[data-theme="dark"] .mf-card{border-color:rgba(255,255,255,.12)}
@media(hover:hover) and (pointer:fine){html[data-theme="dark"] .mf-card:hover{border-color:rgba(255,255,255,.55);box-shadow:0 24px 60px -34px rgba(0,0,0,.7)}}
html[data-theme="dark"] .mf-card:focus-within{border-color:rgba(255,255,255,.55);box-shadow:0 24px 60px -34px rgba(0,0,0,.7)}
/* Marketplace/partner logos are near-black wordmarks (amazon/meta/zalando/aboutyou) — on the
   dark card (#191919) they render ~1:1 and vanish. Give them a fixed-white chip in dark theme,
   the same approach as the footer compliance logos and .wwd-brand-pop (2026-07-23 audit fix).
   NAMED, not [src*="/partners/"]. That net caught all 24 marks that pass through .mf-top and 10
   of them needed nothing: every mark was rasterised and measured against this card on
   2026-08-31 (share of ink clearing 3:1 on #1A1A1C with no filter at all), and the ten that
   carry their own colour — Skroutz 99%, BOX NOW 100%, Skroutz Point 99%, eBay 94%, Google 93%,
   Allegro 89%, DHL 86%, BestPrice 86%, ACS 76%, eMAG 68% — were wearing a white rectangle for
   nothing, which is the box the owner called out. They now sit straight on the card in their own
   colours, with no filter and no ground. The eight that come in under 40% are the ones this rule
   is about: FedEx 0%, UPS 0%, Walmart 0%, Amazon 15%, Zalando 30%, Geniki 30%, Meta 32%,
   About You 36%.
   The chip itself is gone as of the same day, and hover is why. It sat at the mark's own
   opacity, .78 at rest and 1 on hover, so pointing at a card did not reveal the logo, it lit the
   rectangle: the artifact was brightest exactly when someone looked at it (owner call). A white
   slab that announces itself on hover is worse than a monochrome mark, so these eight take the
   silhouette instead, which is the call the site already made everywhere else in dark theme
   (.brand-track at :1875, .tls-mark at :2073, .bw-cell at :8232): invert to put the ink light,
   grayscale so no hue can rotate, screen to drop whatever the invert turned near-black, which is
   how About You's white plates and the Amazon lockups' baked grounds leave the card too. Brand
   colour is given up in dark for these; legibility comes first and nothing draws a box.
   The six /amazon/ service lockups ride the same rule for a different reason: their white plate
   is BAKED INTO the 250x250 WebP (91-96% near-white on the frame), so no chip decision could
   have helped them, and screen is what deletes it. The orange smile goes grey with it, and the
   fix for that is the asset, not CSS: Amazon publishes a white-knockout lockup for dark grounds.
   Ask for those six and drop them out of this selector. */
html[data-theme="dark"] .mf-top img[src*="partners/amazon"],
html[data-theme="dark"] .mf-top img[src*="partners/meta"],
html[data-theme="dark"] .mf-top img[src*="partners/zalando"],
html[data-theme="dark"] .mf-top img[src*="partners/aboutyou"],
html[data-theme="dark"] .mf-top img[src*="partners/walmart"],
/* SHOPFLIX joins them on the same measured basis: its single #081bc5 measures 1.63:1 on
   this card, well under the 3:1 the ten self-coloured marks clear. */
html[data-theme="dark"] .mf-top img[src*="partners/shopflix"],
html[data-theme="dark"] .mf-top img[src*="carriers/fedex"],
html[data-theme="dark"] .mf-top img[src*="carriers/ups"],
html[data-theme="dark"] .mf-top img[src*="carriers/geniki"],
html[data-theme="dark"] .mf-top img[src*="/mq/amazon-"]{
  filter:invert(1) grayscale(1) brightness(1.65)}
/* icon pieces scatter on hover (per-piece --dx/--dy) + stroke darkens; fills track the card bg per theme */
.mf-ico path{stroke:#4b4b4b;transition:stroke .4s ease}
.mf-ico path[fill="#fff"]{fill:#fff}
.mf-ico .pc{transition:transform .4s cubic-bezier(.22,1,.36,1)}
@media(hover:hover) and (pointer:fine){.mf-card:hover .mf-ico .pc{transform:translate(var(--dx,0),var(--dy,0))}}
.mf-card:focus-within .mf-ico .pc{transform:translate(var(--dx,0),var(--dy,0))}
@media(hover:hover) and (pointer:fine){.mf-card:hover .mf-ico path{stroke:var(--red-ink,#ab0000)}}
.mf-card:focus-within .mf-ico path{stroke:var(--red-ink,#ab0000)}
html[data-theme="dark"] .mf-ico path{stroke:#c9c9c9}
html[data-theme="dark"] .mf-ico path[fill="#fff"]{fill:var(--paper-0)}
@media(prefers-reduced-motion:reduce){.mf-ico .pc{transition:none}.mf-card:hover .mf-ico .pc,.mf-card:focus-within .mf-ico .pc{transform:none}}
html.a11y-no-motion .mf-ico .pc{transition:none}
html.a11y-no-motion .mf-card:hover .mf-ico .pc,html.a11y-no-motion .mf-card:focus-within .mf-ico .pc{transform:none}
/* Discipline as a dark band (2026-08-10 rhythm study): .band-inverse alone does not work on this
   TEXTURED section — .mf's own background is declared here, after .band-inverse, and wins the
   cascade at equal specificity, and .mf-head/.mf-lede have no .band-inverse counterpart at all.
   Reuses the section's own dark-theme recipe verbatim (not a flat colour) since it is already
   contrast-audited and renders identically in both themes. */
#discipline.band-inverse{background:linear-gradient(rgba(13,13,14,.75),rgba(13,13,14,.75)),#0d0d0e url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp') center/cover no-repeat;background-blend-mode:normal,multiply;color:#EBE9E3}
#discipline.band-inverse .mf-lede{color:#C5C3BB}
#discipline.band-inverse .mf-head h2{color:#fff}
#discipline.band-inverse .mf-head .eyebrow{color:#EBE9E3}
/* .mf-card:focus-visible's outline is var(--ink) (tpl.css ~L3777) — near-black in light theme,
   ~1.1:1 on this fixed-dark band and effectively invisible (no compliant fallback: the red
   conic-sweep hover cue is also sub-3:1 here). These cards are plain tabindex="0" articles, not
   a/button, so the .band-inverse a/button:focus-visible rule never reaches them either. Same
   literal light value the rest of this band already uses for text. */
#discipline.band-inverse .mf-card:focus-visible{outline-color:#EBE9E3}
/* The three Discipline cards are <a href="values.html#discipline"> since 2026-08-21 (owner
   direction: "add a link to the three panels"). Link reset only — every text child already
   carries its own explicit colour on this band, so the card element's colour is what
   html.a11y-underline paints its underline from; pinned to the band's own light literal so
   that underline stays visible (same trap documented on .aud-card below). */
.mf-card--link{text-decoration:none;color:#EBE9E3}
/* .mf-card--link is an <a>, so .band-inverse a:focus-visible (tpl.css ~L1447) would repaint the
   ring --paper-100 (near-black in dark theme, ~1.0:1 on this fixed-dark band). The #discipline
   rule above outranks it on the home band; this covers the same card system anywhere else. */
.band-inverse .mf-card--link:focus-visible{outline-color:#EBE9E3}

/* ===== "Who are you?" audience router — reuses the discipline (.mf) card system on a
   noise panel; three deltas: link reset, body always visible (no hover-reveal), a shorter
   card, an index+label top-right, and a red proof-tag stacked above the CTA link. ===== */
/* same blend-to-soften rule as #write/.wwd/.mf — wash + multiply the bg image into the base
   color at ~25% visible strength, instead of the old flat .28 scrim */
.aud{background:linear-gradient(rgba(240,240,240,.75),rgba(240,240,240,.75)),#f0f0f0 url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/aud-bg.webp') center/cover no-repeat;background-blend-mode:normal,multiply;border-bottom:0}
html[data-theme="dark"] .aud{background:linear-gradient(rgba(13,13,14,.75),rgba(13,13,14,.75)),#0d0d0e url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp') center/cover no-repeat;background-blend-mode:normal,multiply}
.aud .mf-grid{grid-template-columns:repeat(2,1fr)} /* 2 audience cards since investors moved off the router */
/* the .aud override above outspecifies the base .mf-grid 820px collapse (2 classes vs 1),
   so without this the router (home) + channel-card grid (channels/*) stayed 2-up on phones
   and overflowed the viewport by ~125-170px. Collapse to one column at the sitewide phone bp. */
@media(max-width:640px){.aud .mf-grid{grid-template-columns:1fr}}
/* ————— .mf-fly: cards FLY IN from off-page and land in their slots (2026-08-21, owner call,
   second take same day: supersedes the stand-up-tilt float, which never left git history).
   "Float from outside the page to land into spot": the first card travels in from the LEFT page
   edge, the last from the RIGHT, any card between rises from BELOW the fold, each drifting at a
   slight rotation that levels out as it decelerates into its grid slot — the levelling, not the
   distance, is what reads as "float". Opt-in per SECTION (class next to .aud/.mf on the tag):
   born on the home audience router, generalised the same day the owner asked for it on the
   discipline trio — which is DUPLICATED on values.html (ADR-015, edit both together), so the
   class rides the section tag on all three copies and a bare .mf section elsewhere stays on
   the sitewide fade-up.
   THE MECHANICS ARE THE BRANCHES BENTO'S (.bgrid--fly), for the same two reasons it works:
   1) TWO LAYERS. The observed .reveal wrapper NEVER moves — an element parked at ±88vw never
      intersects the IntersectionObserver watching it (measured on branches), so the wrapper is
      pinned in place and visible at (0,3,0) and the sitewide observer fires exactly as before;
      the inner .mf-card is what flies. The wrapper still sizes the row (translate is paint-only),
      so the section keeps its height and the flight costs no CLS. The 82vh riser parks BELOW
      mid-page content and needs no y-clip: every consumer has more than a viewport of document
      under it, and opacity:0 keeps the parked card invisible while it overlaps.
   2) THE INNER RIDES `translate`/`rotate`, NOT transform: .mf-card:hover owns transform (the
      -3px lift) and would be clobbered. The transition restates the base card's border-color +
      transform pair so hover keeps transitioning, which is why the d1/d2 staggers below are
      PER-PROPERTY delay lists — a bare 110ms would lag a card's hover forever.
   ±88vw matches branches. On stacked phones (390px) it leaves a ~20px sliver of parked card
   inside the far edge; opacity:0 hides it, and .mf-fly carries overflow-x:clip (same guard as
   .sec-clip/.gtee/.wwd — clip, NOT hidden, so no new scroll container) so a parked card can
   never reopen the sideways-drag trap that banned x-offsets from hidden .reveals in the first
   place. Travel is .9s on the decelerating --ease-out over the house .6s fade, no overshoot.
   Every escape hatch moves DOWN to the inner at >= this weight: reduced-motion, scripting:none,
   a11y-no-motion, and :focus-within because these cards are links on home and tabindex="0"
   articles on values (WCAG 2.4.7). */
.mf-fly{overflow-x:clip}
.mf-fly .mf-grid>.reveal{opacity:1;transform:none;transition:none;--fx:0;--fy:82vh;--fr:0deg} /* pinned slot; middles default to the riser */
.mf-fly .mf-grid>.reveal:first-child{--fx:-88vw;--fy:0;--fr:-2.5deg}
.mf-fly .mf-grid>.reveal:last-child{--fx:88vw;--fy:0;--fr:2.5deg}
.mf-fly .mf-grid>.reveal>.mf-card{opacity:0;translate:var(--fx) var(--fy);rotate:var(--fr);will-change:translate,opacity;
  transition:opacity .6s var(--ease),translate .9s var(--ease-out),rotate .9s var(--ease-out),border-color .3s var(--ease),transform .3s var(--ease)}
.mf-fly .mf-grid>.reveal.is-visible>.mf-card{opacity:1;translate:0 0;rotate:0deg}
/* distinct beats, not one event — delays ONLY the three entrance properties, never the hover pair */
.mf-fly .mf-grid>.reveal.reveal-d1>.mf-card{transition-delay:110ms,110ms,110ms,0s,0s}
.mf-fly .mf-grid>.reveal.reveal-d2>.mf-card{transition-delay:220ms,220ms,220ms,0s,0s}
.mf-fly .mf-grid>.reveal:focus-within>.mf-card{opacity:1;translate:0 0;rotate:0deg;transition:none}
@media(prefers-reduced-motion:reduce){.mf-fly .mf-grid>.reveal>.mf-card{opacity:1;translate:none;rotate:none;transition:none}}
@media(scripting:none){.mf-fly .mf-grid>.reveal>.mf-card{opacity:1;translate:none;rotate:none;transition:none}}
html.a11y-no-motion .mf-fly .mf-grid>.reveal>.mf-card{opacity:1;translate:none;rotate:none;transition:none}
.aud-card{text-decoration:none;color:var(--ink)}
@media(min-width:821px){.aud-card{min-height:clamp(340px,32vw,420px)}}
.aud-card .mf-body{opacity:1;transform:none}
.aud-card .mf-title{margin-top:clamp(24px,3vw,40px)}
.aud-meta{display:flex;flex-direction:column;align-items:flex-end;gap:3px;text-align:right;flex:none}
.aud-kick{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted)}
.aud-card .mf-foot{flex-direction:column;align-items:flex-start;gap:14px}
.aud-cta{display:inline-flex;align-items:center;font-family:var(--font-body);font-weight:600;font-size:14.5px;color:var(--red-ink,#ab0000);text-decoration:none}
/* simpler proof tag — outlined neutral chip, not the solid-red/white discipline tag */
.aud-card .mf-tag{background:transparent;color:var(--ink-soft);border:1px solid var(--hairline);padding:4px 9px}

/* ---- dual-CTA audience card (manufacturer) --------------------------------
   The manufacturer card carries TWO destinations — partners.html (the 5 models)
   and contact.html (talk now) — so it can no longer be one card-wide <a>; nested
   anchors are invalid. The card is a <div>; .aud-cta--main stretches an invisible
   ::after over it to keep the whole surface clickable, and .aud-cta2 lifts above
   that overlay so its own click still lands. Every hover/focus state the card had
   already keyed off .mf-card:focus-within (tpl.css:4088), so moving the link
   inside changes no visuals. The customer card stays a plain card-wide <a>. */
.aud-cta--main::after{content:"";position:absolute;inset:0;z-index:1}
.aud-cta2{position:relative;z-index:2;display:inline-flex;align-items:center;gap:6px;font-family:var(--font-body);font-size:13.5px;font-weight:500;color:var(--ink-muted);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;transition:color .25s var(--ease)}
@media(hover:hover) and (pointer:fine){.aud-cta2:hover{color:var(--red-ink,#ab0000)}}
.aud.band-inverse .aud-cta2{color:#A9A7A0}
@media(hover:hover) and (pointer:fine){.aud.band-inverse .aud-cta2:hover{color:#EBE9E3}}
/* the two CTAs are their own row so the tag keeps its own line above them */
.aud-card .mf-foot{gap:12px}
/* ============ .pgrid / .sfc — the storefront bento ============
   Redesigned 2026-09-01 (owner picked "Cut B" from the storefront-cards draft): the white
   plate stops being a chip and becomes the card's whole upper face — SIGN + PLINTH, a lit
   shop sign on a dark street — and the grid goes bento: TPL Parts and Vatopedi run double
   width and horizontal (sign left), and the old .pgrid-more roster line moved INTO the
   grid as the closing .sfc-say cell. Replaces the .pcell family (this page was its only
   consumer; the "+" add-cell died with it).

   THE BAND STAYS FIXED DARK — .band-inverse, theme-invariant literals throughout — and the
   SIGN IS STILL THE STRUCTURAL FIX it always was: Vatopedi's #5d2e30/#bfa176 mark and every
   other dark wordmark keeps its brand colour on --paper-fixed in both themes, no per-logo
   filter. The sign being a fixed-height box is also what reserves layout before a lazy
   mark lands, so the image inside keeps the dual-maxima form that preserves every ratio. */
.pgrid-sec{padding:clamp(44px,5.6vw,84px) 0}
.pgrid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:clamp(10px,1vw,14px);counter-reset:sfc}

.sfc{position:relative;display:flex;flex-direction:column;min-height:clamp(300px,24vw,364px);
  border-radius:var(--radius-card);overflow:hidden;background:#17171A;border:1px solid #2a2a2e;
  text-decoration:none;counter-increment:sfc;
  transition:border-color .22s var(--ease),transform .22s var(--ease)}
.sfc--f{grid-column:span 2;flex-direction:row;min-height:clamp(280px,21vw,330px)}

/* the sign: fixed-white plate, the mark large and centred, the running number stamped in
   the plate's own quiet ink — counter() numbers the fleet with no markup at all */
.sfc-sign{position:relative;flex:none;display:flex;align-items:center;justify-content:center;
  background:var(--paper-fixed);height:clamp(128px,11vw,168px);padding:22px clamp(20px,2vw,30px)}
.sfc--f .sfc-sign{height:auto;width:46%}
.sfc-sign img{max-width:min(64%,240px);max-height:62%;width:auto;height:auto;transition:transform .28s var(--ease)}
.sfc--f .sfc-sign img{max-width:70%;max-height:40%}
.sfc-sign::after{content:"0" counter(sfc);position:absolute;top:10px;right:14px;
  font-family:var(--font-mono);font-weight:500;font-size:15px;letter-spacing:.08em;
  color:#B9B7AF;transition:color .22s var(--ease)}

/* the plinth */
.sfc-body{display:flex;flex-direction:column;flex:1;min-width:0;padding:clamp(18px,1.7vw,24px)}
.sfc-k{font-family:var(--font-mono);font-weight:500;font-size:10.5px;letter-spacing:.13em;
  text-transform:uppercase;color:#9A9994;transition:color .22s var(--ease)}
.sfc-h{margin:9px 0 0;font-family:var(--font-display);font-weight:400;
  font-size:clamp(20px,1.7vw,24px);line-height:1.14;letter-spacing:-.005em;color:#fff}
.sfc--f .sfc-h{font-weight:700;font-size:clamp(23px,2vw,28px);letter-spacing:-.015em}
.sfc-p{display:block;margin:8px 0 0;font-size:13.5px;line-height:1.5;color:#C5C3BB}
.sfc-foot{margin-top:auto;display:flex;align-items:center;justify-content:flex-end;padding-top:16px}
.sfc-go{display:flex;align-items:center;justify-content:center;flex:none;width:40px;height:40px;
  border:1px solid rgba(255,255,255,.42);border-radius:9999px;color:#EBE9E3;
  transition:background-color .22s var(--ease),color .22s var(--ease),border-color .22s var(--ease),transform .22s var(--ease)}
.sfc-go svg{width:17px;height:17px}

/* the closing cell: the roster line as a grid citizen, dashed like a ledger margin note */
.sfc-say{grid-column:span 2;display:flex;flex-direction:column;justify-content:space-between;
  gap:18px;padding:clamp(18px,1.7vw,24px);border:1px dashed #6b6b75;border-radius:var(--radius-card);
  min-height:clamp(300px,24vw,364px)}
.sfc-say p{margin:0;font-family:var(--font-display);font-weight:400;
  font-size:clamp(21px,1.9vw,27px);line-height:1.3;letter-spacing:-.01em;color:#EBE9E3;max-width:26ch}
.sfc-say p b{font-weight:700;color:#fff}
.sfc-say .biglink{align-self:flex-start}

/* states: the shared red sweep (@property --sweep), the mark breathes, the arrow fills
   brand red — feedback in the accent, once per card */
.sfc::after{content:"";position:absolute;inset:-1px;padding:1px;border-radius:var(--radius-card);
  pointer-events:none;--sweep:0deg;
  background:conic-gradient(from 135deg,transparent 0 calc(180deg - var(--sweep)),var(--red) calc(180deg - var(--sweep)) calc(180deg + var(--sweep)),transparent calc(180deg + var(--sweep)));
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;mask-composite:exclude;transition:--sweep 280ms cubic-bezier(.2,0,0,1)}
@media(hover:hover) and (pointer:fine){
  .sfc:hover{border-color:rgba(255,255,255,.5);transform:translateY(-3px)}
  .sfc:hover::after{--sweep:180deg}
  .sfc:hover .sfc-sign img{transform:scale(1.05)}
  .sfc:hover .sfc-sign::after{color:var(--red)}
  .sfc:hover .sfc-k{color:var(--red-ink-inverse,#E05252)}
  .sfc:hover .sfc-go{background:var(--red);color:#fff;border-color:var(--red);transform:translateX(2px)}
}
.sfc:focus-visible{border-color:rgba(255,255,255,.5)}
.sfc:focus-visible::after{--sweep:180deg}
.sfc:focus-visible .sfc-go{background:var(--red);color:#fff;border-color:var(--red)}
.sfc:active{transform:translateY(0);transition-duration:.1s}
@media(prefers-reduced-motion:reduce){
  .sfc,.sfc::after,.sfc-sign img,.sfc-go{transition:none}
  .sfc:hover{transform:none}.sfc:hover .sfc-sign img{transform:none}
}
html.a11y-no-motion :is(.sfc,.sfc::after,.sfc-sign img,.sfc-go){transition:none}
html.a11y-no-motion .sfc:hover{transform:none}
html.a11y-no-motion .sfc:hover .sfc-sign img{transform:none}

/* tablet: two tracks — the doubles take a full row, no holes to chase. phone: one track,
   the doubles fold back to vertical sign-over-plinth. */
@media(max-width:1180px){.pgrid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){
  .pgrid{grid-template-columns:minmax(0,1fr)}
  .sfc--f,.sfc-say{grid-column:span 1}
  .sfc--f{flex-direction:column}
  .sfc--f .sfc-sign{width:auto;height:clamp(128px,30vw,168px)}
  .sfc--f .sfc-sign img{max-width:64%;max-height:62%}
}
/* Audience router as a dark band — same v2 rhythm-study promotion as #discipline above.
   .lede and h2.two-tone already have .band-inverse coverage; only the aud-specific classes need it. */
.aud.band-inverse{background:linear-gradient(rgba(13,13,14,.75),rgba(13,13,14,.75)),#0d0d0e url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp') center/cover no-repeat;background-blend-mode:normal,multiply;color:#EBE9E3}
/* The two textured bands declare their own ground, so they never see --band and would have stayed
   at #0d0d0e while every plain band widened — an uneven rhythm down one page, which is worse than
   the quiet-but-even state we started from. Same tone as --band, carried through the wash so the
   texture reads identically; the wash goes .75 → .78 because a darker base needs marginally more
   cover to hold the same grain depth. Must stay after BOTH unconditional rules in source order
   (#discipline ~4208, .aud just above) to win. */
html[data-theme="dark"] #discipline.band-inverse,
html[data-theme="dark"] .aud.band-inverse{
  background:linear-gradient(rgba(2,2,3,.78),rgba(2,2,3,.78)),#020203 url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp') center/cover no-repeat;
  background-blend-mode:normal,multiply}
.aud.band-inverse .aud-kick{color:#9A9994}
/* .aud-card sets color on the CARD ELEMENT (tpl.css:4107, .aud-card{...color:var(--ink)}), so it
   does not inherit this band's #EBE9E3. Every text child carries its own explicit colour, so no
   copy was affected and a contrast pass over the visible text finds nothing — but one thing does
   paint from the element's own colour: the underline that html.a11y-underline adds
   (tpl.css:3028, text-decoration:underline!important). Measured on the dark band that underline
   came out rgb(24,24,24) on the rgb(25,25,25) card = 1.01:1, and 1.19:1 under html.a11y-contrast.
   Invisible — on the one control a low-vision visitor switches on in order to see links at all.
   Pre-existing: index.html has carried .aud.band-inverse since the rhythm merge, so this fixes
   that page too, not only the 11 channel pages that just gained the band. */
.aud.band-inverse .aud-card{color:#EBE9E3}
.aud.band-inverse .aud-cta{color:var(--red-ink-inverse)}
/* .aud-card is an <a>, so .band-inverse a:focus-visible (tpl.css ~L1256) outranks .mf-card's own
   focus ring by one type selector and repaints it --paper-100 — a theme TOKEN, which is exactly
   the trap this file's own .band-inverse comment (~L1690) warns about: it resolves near-black in
   dark theme (~1.0:1 on this fixed-dark band, no compliant fallback). Same literal light value
   the rest of this band already uses for text. */
.aud.band-inverse .aud-card:focus-visible{outline-color:#EBE9E3}
/* border is --hairline's dark-theme value (#2E2E33), not the sibling .band-inverse card edge's
   #2a2a2e: measured, unmodified dark-theme index.html paints this chip edge at 1.30:1 and #2a2a2e
   would give 1.23:1 — worse than the page this band is matching. Both are under 1.4.11's 3:1, but
   that is PRE-EXISTING and site-wide (unmodified dark-theme .aud-card/.mf-card edges measure
   1.36:1) — raising it is a decision across every dark surface on the site, not a one-band fix. */
.aud.band-inverse .aud-card .mf-tag{color:#C5C3BB;border-color:#2E2E33}
/* .mf-card is shared by both dark bands (.aud-card extends it, #discipline's cards are plain
   .mf-card) — pinned to dark theme's own card+icon values rather than .band-inverse's generic
   .mf-card (#17171A), because the occlusion fill (path[fill="#fff"], tpl.css ~L3813) must equal
   its card background or the facet shows as a solid block instead of line art. .band-inverse
   covers the icon STROKE already; it never covered the FILL, which only matters once a TEXTURED
   section carries the class — no shipping page combined the two before this. */
.aud.band-inverse .mf-card,#discipline.band-inverse .mf-card{background:#191919;border-color:rgba(255,255,255,.12)}
/* same ID-specificity trap as the icon stroke below: the rule above pins border-color at rest
   ahead of both .band-inverse .mf-card:hover (tpl.css ~L1710, .4) and dark theme's own
   .mf-card:hover (tpl.css ~L3814, .55) — dead in practice (the ::after red sweep already traces
   the ring on hover/focus-within), but kept honest so the cascade doesn't lie about what a future
   edit near L1710/3814 would actually change here. */
@media(hover:hover) and (pointer:fine){.aud.band-inverse .mf-card:hover,#discipline.band-inverse .mf-card:hover{border-color:rgba(255,255,255,.55)}}
.aud.band-inverse .mf-card:focus-within,#discipline.band-inverse .mf-card:focus-within{border-color:rgba(255,255,255,.55)}
.aud.band-inverse .mf-ico path[fill="#fff"],#discipline.band-inverse .mf-ico path[fill="#fff"]{fill:#191919}
.aud.band-inverse .mf-ico path,#discipline.band-inverse .mf-ico path{stroke:#C9C9C9}
/* the ID in #discipline.band-inverse above gives that half of the stroke rule specificity
   (1,2,1) that always outranks the generic hover/focus red rule (.band-inverse .mf-card:hover
   .mf-ico path, tpl.css ~L1719 — no ID, 0,4,1) regardless of source order, so #discipline's icon
   never went red on hover/focus while .aud's (no ID) correctly did. Re-declared here, scoped to
   the ID, to win back the specificity the shared rule above cost it. */
@media(hover:hover) and (pointer:fine){#discipline.band-inverse .mf-card:hover .mf-ico path{stroke:var(--red-ink-inverse)}}
#discipline.band-inverse .mf-card:focus-within .mf-ico path{stroke:var(--red-ink-inverse)}
/* ——— Customer journey as a dark band (2026-08-13) ———
   customers.html ran 5,970px of main content across ten sections with ZERO tonal breaks, while
   index.html — same length, same job, same components — breaks four times after the v2 rhythm
   promotion above. This is that page's first break, on #journey because it sits 23% down, which
   is where index.html's own first one lands. Plain .band-inverse ground (flat --inverse) rather
   than .aud's noisy-dark texture: #discipline is flat too, and a second textured band would make
   the home page's one stop reading as special.
   .eyebrow and h2.two-tone already have .band-inverse coverage, and .step-card reuses .mf-title
   and .mf-idx, which are covered as well — only the step-specific classes need pinning. */
#journey.band-inverse .step-card{background:#191919;border-color:rgba(255,255,255,.12)}
#journey.band-inverse .step-card p{color:#C5C3BB}
#journey.band-inverse .step-foot .kicker{color:#9A9994}
/* .step-card is a div[tabindex="0"], NOT an <a> or <button>, so .band-inverse a:focus-visible
   (~L1351) never matches it and it keeps its own outline:2px solid var(--ink) — a theme TOKEN,
   which resolves near-black on a band that is dark in BOTH themes. Exactly the trap the
   .aud-card comment above documents. Same #EBE9E3 literal this band already uses for text. */
#journey.band-inverse .step-card:focus-visible{outline-color:#EBE9E3}
/* The shared retone at :5568 warms THREE things to var(--red-ink) on hover/focus — .mf-idx, the
   foot .kicker and .mf-title — and --red-ink is #AB0000 in light theme while this band is dark in
   both, so every one of them repaints dark-red on #191919 on a light-theme visit. All three are
   pinned here, not just the title: an earlier version of this block covered .mf-title alone and
   left the index at 2.29:1 (1.83:1 under html.a11y-contrast, where --red-ink is #900000) against
   the 5.51:1 it has at rest — and tpl.css:1815 states that .mf-idx carries real content and needs
   AA. --red-ink-inverse measures 6.30:1 here. Dark theme was never affected, which is precisely
   why a resting-state pass in both themes could not see any of this.
   The ID is load-bearing twice over. It lifts these to (1,4,0)/(1,5,0), which beats the shared
   rule's (0,3,0)/(0,4,0) — and it has to beat the RESTING kicker rule above at (1,3,0), which
   otherwise held the kicker at #9A9994 through hover and focus and silently killed the accent
   entirely. That was the same trap the #discipline .mf-ico comment records two blocks up. */
#journey.band-inverse .step-card:hover .mf-idx,
#journey.band-inverse .step-card:focus .mf-idx,
#journey.band-inverse .step-card:hover .step-foot .kicker,
#journey.band-inverse .step-card:focus .step-foot .kicker,
#journey.band-inverse .step-card:hover .mf-title,
#journey.band-inverse .step-card:focus .mf-title{color:var(--red-ink-inverse)}
/* .step-arrow is a --red (#AB0000) disc holding a white cross. Measured, the disc reads 7.0:1
   against the light card it was drawn for and only 2.29:1 against this band's #191919 — the
   cross inside it stays legible (white on #AB0000), but the disc is what makes the card read as
   interactive at all, and at 2.29 its edge dissolves into the card. Fixed at the boundary rather
   than the fill: brightening to --red-ink-inverse would satisfy the disc and then strand the
   white cross on a light red at ~2.4:1, trading one failure for another. A hairline ring lifts
   only what is actually deficient, and .34 alpha is the value that composites to rgb(101,101,101)
   over #191919 — the exact point 1.4.11's 3:1 is met, not a guess. Brand red is untouched. */
#journey.band-inverse .step-arrow{box-shadow:0 0 0 1px rgba(255,255,255,.34)}
/* KPI band on a dark ground. Deliberately NOT scoped to a page or an id: every one of the four
   properties below resolves from a paper/ink token, so a .kpi-band would break the same way on
   any dark band anywhere, and the generic form fixes it once. .kpi paints --paper-100, which is
   #F2F1ED in LIGHT theme, so on a band that is dark in both themes the cells would come out light
   and the figures near-black on top of them. Same literals the two existing dark bands use:
   #191919 cell, rgba(255,255,255,.12) seam, #fff figure, #9A9994 label (measured 6.16:1). */
.band-inverse .kpi-band{background:rgba(255,255,255,.12)}
.band-inverse .kpi{background:#191919}
.band-inverse .kpi .num{color:#fff}
.band-inverse .kpi small{color:#9A9994}
/* ===== GUARANTEES — slide-in cards, static icon that pops apart on hover (D2C-specific;
   NOT the .mf-card system above). Cards fly in from the section's scroll progress — same
   driver as the home "What we do" bento (.gtee-fly module in tpl.js) — so the reveal is
   identical on mobile (scroll-driven, not hover-gated, per the user's mobile-parity
   requirement). Icons are still on card hover/focus — see .gtee-ico .pc below. */
/* the fly-in parks each card at --fx:±100vw, i.e. a full viewport outside the section, so the
   section MUST clip or that parked art inflates the document's scrollable width and the page
   scrolls sideways on phones (measured 2026-07-29: documentElement.scrollWidth 745 vs
   clientWidth 375 at 390px on all five model pages, widest node ARTICLE.gtee-card right edge
   745). Same guard the sibling fly-in bentos already carry — .wwd (overflow:clip) and
   .sec-clip / .flow-sec / .aw-fx (overflow-x:clip). clip, not hidden: clip does not create a
   scroll container (so pinned/sticky scenes inside keep working) and leaves overflow-y visible,
   so focus rings and the ::after sweep are not cut off vertically. .gtee is on the <section> of
   all five pages (plain, --gray, --noise, --noise --top), so the base class is the one ancestor
   every .gtee-card shares. */
.gtee{overflow-x:clip}
/* bento quilt (adapts the "How it works" .wwd-steps tall+stacked pattern to 3 cells,
   just without its wide-bottom 4th cell): card 1 spans both rows on the left, cards 2/3
   stack on the right — same shape language as the page's other bento, different content. */
.gtee-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(16px,1.6vw,24px)}
.gtee-grid>.gtee-card:nth-child(1){grid-column:1;grid-row:1 / span 2}
.gtee-grid>.gtee-card:nth-child(2){grid-column:2;grid-row:1}
.gtee-grid>.gtee-card:nth-child(3){grid-column:2;grid-row:2}
.gtee-card{position:relative;display:flex;flex-direction:column;gap:14px;padding:clamp(24px,2.4vw,34px);background:var(--paper-0);border:1px solid var(--hairline);border-radius:3px;opacity:0;translate:var(--fx,0) var(--fy,50px);will-change:transform,translate;transition:opacity .6s var(--ease),translate .9s var(--ease),border-color .3s var(--ease),transform .3s var(--ease)}
.gtee-card.in{opacity:1;translate:0 0}
/* fly-in vectors thrown fully off-page (matching the home bento's magnitude) so each
   card visibly travels in from outside the viewport to find its bento slot */
.gtee-grid>.gtee-card:nth-child(1){--fx:-100vw}
.gtee-grid>.gtee-card:nth-child(2){--fx:100vw;--fy:-60vh}
.gtee-grid>.gtee-card:nth-child(3){--fx:100vw;--fy:60vh}
@media(hover:hover) and (pointer:fine){.gtee-card:hover{border-color:var(--ink);transform:translateY(-3px)}}
.gtee-card:focus-within{border-color:var(--ink);transform:translateY(-3px)}
.gtee-card:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
/* the house hover-line — red conic sweep traces the outline top-left→bottom-right,
   same shared @property --sweep as .mf-card/.wwd-card (this is now the default hover
   treatment for any new card component, unless told otherwise) */
.gtee-card::after{content:"";position:absolute;inset:-1px;padding:1px;border-radius:4px;pointer-events:none;--sweep:0deg;background:conic-gradient(from 135deg,transparent 0 calc(180deg - var(--sweep)),var(--red) calc(180deg - var(--sweep)) calc(180deg + var(--sweep)),transparent calc(180deg + var(--sweep)));-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;transition:--sweep 280ms cubic-bezier(.2,0,0,1)}
@media(hover:hover) and (pointer:fine){.gtee-card:hover::after{--sweep:180deg}}
.gtee-card:focus-within::after{--sweep:180deg}
.gtee-ico-wrap{width:clamp(72px,6vw,104px);height:clamp(72px,6vw,104px)} /* matches home .mf-ico — one icon size sitewide */
.gtee-ico{width:100%;height:100%;overflow:visible;color:var(--ink-muted);transition:color .3s var(--ease)}
/* --red-ink, not --red: same graphical-object 3:1 rule as .bcell-ico above */
@media(hover:hover) and (pointer:fine){.gtee-card:hover .gtee-ico{color:var(--red-ink)}}
.gtee-card:focus-within .gtee-ico{color:var(--red-ink)}
/* occlusion fills (the isometric icon's cutaway facets) track the card surface, same convention as .mf-ico/.bico */
.gtee-ico path[fill="#fff"]{fill:var(--paper-0)}
/* static at rest, parts pop apart on card hover — same .pc/--dx/--dy/--td convention as .bico/.wwd-card
   (these icons carry the .wwd-card-style small --td stagger, not the simultaneous .mf-ico one — a
   slower, eased, staggered cascade reads smoother than a same-instant snap), amplified 2.5× since the
   source vectors' own --dx/--dy are subtle; overflow:visible above keeps the popped parts from clipping */
/* .7s + a per-piece stagger put the icon's settle well past half a second on a hover; .32s keeps
   the scatter legible while landing inside budget once the stagger is added on top */
.gtee-ico .pc{transition:transform .32s var(--ease);transition-delay:calc(var(--td,0s) * 2)}
@media(hover:hover) and (pointer:fine){.gtee-card:hover .gtee-ico .pc{transform:translate(calc(var(--dx,0px) * 2.5),calc(var(--dy,0px) * 2.5))}}
.gtee-card:focus-within .gtee-ico .pc{transform:translate(calc(var(--dx,0px) * 2.5),calc(var(--dy,0px) * 2.5))}
.gtee-card h3{font-family:var(--font-display);font-weight:700;font-size:clamp(20px,1.8vw,25px);letter-spacing:-.01em;line-height:1.14;color:var(--ink);margin:0}
.gtee-card p{font-size:15px;line-height:1.6;color:var(--ink-soft);margin:0}
.gtee-tag{margin-top:auto;align-self:flex-start;font-family:var(--font-mono);font-size:12.5px;font-weight:500;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-soft);border:1px solid var(--hairline);padding:4px 9px;border-radius:2px;transition:color .3s var(--ease),border-color .3s var(--ease)}
@media(hover:hover) and (pointer:fine){.gtee-card:hover .gtee-tag{color:var(--red-ink,#ab0000);border-color:var(--red-ink,#ab0000)}}
.gtee-card:focus-within .gtee-tag{color:var(--red-ink,#ab0000);border-color:var(--red-ink,#ab0000)}
.gtee-sign{margin:20px 0 0;font-family:var(--font-mono);font-size:13px;color:var(--ink-muted);text-align:right}
/* ════ MOBILE BENTO (2026-07-29) ════
   The quilt keeps a two-column composition on phones rather than collapsing to a single
   column, because one column stops reading as a bento at all. Composition is AUTHORED,
   not algorithmic: card 1 spans the full width as the feature, cards 2 and 3 sit half
   width beside it. There is no generic "make the orphan span" selector that survives a
   full-width cell shifting the parity of everything after it, so each section states its
   own arrangement.
   Below 360px two columns leave roughly 92px of content per cell, which is unreadable,
   so the single-column stack remains the floor.
   HAZARD: this puts explicit grid-column values back inside a mobile block, which is the
   exact shape of the bug fixed earlier today. Every placement set in the band below MUST
   be reset in the <360px floor at equal-or-higher specificity, or the surviving column
   creates an implicit second track again. Verified by probe at 320/359/360/390/430/560. */
@media(min-width:360px) and (max-width:820px){
  .gtee-grid{grid-template-columns:1fr 1fr;gap:14px}
  .gtee-grid>.gtee-card:nth-child(1){grid-column:1 / -1;grid-row:auto}
  .gtee-grid>.gtee-card:nth-child(2){grid-column:1;grid-row:auto}
  .gtee-grid>.gtee-card:nth-child(3){grid-column:2;grid-row:auto}
  .gtee-sign{text-align:left}
}
/* compact half-cells: 120px of content cannot carry a 20px display heading, so type and
   icon step down inside the band. The full-width feature card keeps its original scale so
   the hierarchy between it and the pair below survives. */
@media(min-width:360px) and (max-width:559.98px){
  .gtee-card{padding:16px;gap:10px}
  .gtee-card h3{font-size:17px;line-height:1.18}
  .gtee-card p{font-size:14px}
  .gtee-tag{font-size:11.5px;padding:3px 7px}
  .gtee-ico-wrap{width:64px;height:64px}
  .gtee-grid>.gtee-card:nth-child(1){padding:22px}
  .gtee-grid>.gtee-card:nth-child(1) h3{font-size:20px}
  .gtee-grid>.gtee-card:nth-child(1) .gtee-ico-wrap{width:80px;height:80px}
}
/* single-column floor — also the reset that keeps the band's placements from leaking */
@media(max-width:359.98px){.gtee-grid{grid-template-columns:1fr;gap:14px}.gtee-grid>.gtee-card:nth-child(n){grid-column:auto;grid-row:auto}.gtee-sign{text-align:left}}
@media(prefers-reduced-motion:reduce){.gtee-card,.gtee-tag,.gtee-card::after{transition:none}.gtee-card:hover{transform:none}.gtee-ico{transition:none}.gtee-ico .pc{transition:none}.gtee-card:hover .gtee-ico .pc,.gtee-card:focus-within .gtee-ico .pc{transform:none}}
html.a11y-no-motion .gtee-card,html.a11y-no-motion .gtee-tag,html.a11y-no-motion .gtee-card::after{transition:none}
html.a11y-no-motion .gtee-card:hover{transform:none}
html.a11y-no-motion .gtee-ico .pc{transition:none}
html.a11y-no-motion .gtee-card:hover .gtee-ico .pc,html.a11y-no-motion .gtee-card:focus-within .gtee-ico .pc{transform:none}
/* the "accounts stay yours" shield icon (.gtee-ico--bob) bobs continuously on hover/focus
   (iconBob, shared with the step cards) instead of the section's one-shot scatter; extra
   .gtee-grid in the selector outspecifies the scatter rule above, transform:none cancels it */
.gtee-grid .gtee-card .gtee-ico--bob .pc{transition:transform .3s cubic-bezier(.2,.8,.2,1);transition-delay:var(--td,0s)}
.gtee-grid .gtee-card:hover .gtee-ico--bob .pc,.gtee-grid .gtee-card:focus-within .gtee-ico--bob .pc{transform:none} /* iconBob loop removed 2026-07-22 (motion trim); transform:none keeps the assembled state on hover */
@media(prefers-reduced-motion:reduce){.gtee-grid .gtee-card:hover .gtee-ico--bob .pc,.gtee-grid .gtee-card:focus-within .gtee-ico--bob .pc{animation:none}}
html.a11y-no-motion .gtee-grid .gtee-card:hover .gtee-ico--bob .pc,html.a11y-no-motion .gtee-grid .gtee-card:focus-within .gtee-ico--bob .pc{animation:none}
html.a11y-no-motion .gtee-ico{transition:none}
/* B2B2C variant — gray-toned sliding guarantee cards (D2C keeps paper-white).
   Neutral gray derived from ink→paper via color-mix, so it adapts in dark mode too. */
.gtee--gray .gtee-card{background:color-mix(in srgb,var(--ink) 6%,var(--paper-0));border-color:color-mix(in srgb,var(--ink) 16%,var(--paper-0))}
@media(hover:hover) and (pointer:fine){.gtee--gray .gtee-card:hover{border-color:var(--ink)}}
.gtee--gray .gtee-card:focus-within{border-color:var(--ink)}
.gtee--gray .gtee-tag{border-color:color-mix(in srgb,var(--ink) 22%,var(--paper-0))}
/* OaaS variant — noisy.webp (the optimized bake of noisy.jpeg), dimmed with the sitewide
   blend-to-soften recipe (same as .mf/.aud/#write: wash + multiply ≈ 25% visible strength;
   ALL section bg images share this dim). Cards stay frosted over it — translucent +
   backdrop-blur (same recipe family as .cta-band>.wrap / .rs-formpanel). */
.gtee--noise{background:linear-gradient(rgba(240,240,240,.75),rgba(240,240,240,.75)),#f0f0f0 url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy.webp') center/cover no-repeat;background-blend-mode:normal,multiply;border-bottom:0}
html[data-theme="dark"] .gtee--noise{background:linear-gradient(rgba(13,13,14,.75),rgba(13,13,14,.75)),#0d0d0e url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp') center/cover no-repeat;background-blend-mode:normal,multiply}
.gtee--noise .gtee-card{background:rgba(255,255,255,.42);-webkit-backdrop-filter:blur(16px) saturate(1.1);backdrop-filter:blur(16px) saturate(1.1);border-color:rgba(255,255,255,.7)}
@media(hover:hover) and (pointer:fine){.gtee--noise .gtee-card:hover{border-color:var(--ink)}}
.gtee--noise .gtee-card:focus-within{border-color:var(--ink)}
/* icon occlusion facets can't stay opaque paper on a translucent card — match the frost */
.gtee--noise .gtee-ico path[fill="#fff"]{fill:#fbfaf7}
html[data-theme="dark"] .gtee--noise .gtee-card{background:rgba(20,20,23,.5);border-color:rgba(255,255,255,.14)}
@media(hover:hover) and (pointer:fine){html[data-theme="dark"] .gtee--noise .gtee-card:hover{border-color:rgba(255,255,255,.55)}}
html[data-theme="dark"] .gtee--noise .gtee-card:focus-within{border-color:rgba(255,255,255,.55)}
html[data-theme="dark"] .gtee--noise .gtee-ico path[fill="#fff"]{fill:#141417}
/* Cross-docking variant — "featured top" bento: card 1 spans the full top row as a wide
   horizontal hero (icon left, copy right, laid out via an internal 2-col grid); cards 2/3
   sit equal beneath it. Fly-in re-aimed: hero drops from above, the two below come from
   left/right. Overrides the default quilt placement (which stays on OaaS/d2c/b2b2c). */
.gtee--top .gtee-grid>.gtee-card:nth-child(1){grid-column:1 / span 2;grid-row:1;--fx:0;--fy:-55vh;
  display:grid;grid-template-columns:auto 1fr;column-gap:clamp(28px,3.5vw,56px);row-gap:10px;align-content:center}
.gtee--top .gtee-grid>.gtee-card:nth-child(1)>.gtee-ico-wrap{grid-column:1;grid-row:1 / span 3;align-self:center}
.gtee--top .gtee-grid>.gtee-card:nth-child(1)>h3{grid-column:2;grid-row:1;align-self:end}
.gtee--top .gtee-grid>.gtee-card:nth-child(1)>p{grid-column:2;grid-row:2;max-width:56ch}
.gtee--top .gtee-grid>.gtee-card:nth-child(1)>.gtee-tag{grid-column:2;grid-row:3;margin-top:0;justify-self:start}
.gtee--top .gtee-grid>.gtee-card:nth-child(2){grid-column:1;grid-row:2;--fx:-100vw;--fy:0}
.gtee--top .gtee-grid>.gtee-card:nth-child(3){grid-column:2;grid-row:2;--fx:100vw;--fy:0}
/* --top in the mobile bento band: same authored composition as the default quilt, restated
   at .gtee--top's own specificity (0-4-0) because the shared band rules above are 0-3-0 and
   would otherwise lose to this variant's desktop placement.
   The featured card drops its internal icon-left grid and becomes a flex column, matching
   the plain quilt's card 1 and the two cards beneath it. Keeping the internal grid put the
   heading in column 2, so it started 100px in from the card's left edge with the icon
   vertically centred beside the paragraph instead of the heading: at 350px that read as a
   misalignment rather than a layout. align-self is reset because those values were written
   for grid rows and mean something different on a flex cross-axis. */
@media(min-width:360px) and (max-width:820px){
  .gtee--top .gtee-grid>.gtee-card:nth-child(1){display:flex;grid-column:1 / -1;grid-row:auto}
  .gtee--top .gtee-grid>.gtee-card:nth-child(1)>.gtee-ico-wrap{align-self:auto}
  .gtee--top .gtee-grid>.gtee-card:nth-child(1)>h3{align-self:auto}
  .gtee--top .gtee-grid>.gtee-card:nth-child(2){grid-column:1;grid-row:auto}
  .gtee--top .gtee-grid>.gtee-card:nth-child(3){grid-column:2;grid-row:auto}
}
/* single-column floor — the reset that stops --top's placements leaking into one column.
   Without it card 3's grid-column:2 forces an implicit second track and all three cards
   render side by side at 146/146/175px on a 375px viewport (measured 2026-07-29). */
@media(max-width:359.98px){
  .gtee--top .gtee-grid>.gtee-card:nth-child(n){grid-column:auto;grid-row:auto}
  .gtee--top .gtee-grid>.gtee-card:nth-child(1){display:flex;grid-column:auto;grid-row:auto}
  .gtee--top .gtee-grid>.gtee-card:nth-child(1)>.gtee-ico-wrap{align-self:auto}
  .gtee--top .gtee-grid>.gtee-card:nth-child(1)>h3{align-self:auto}
}
/* ===== news blog + slide-in reading panel (2026-07-14) ===== */
.blog-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px}
@media(max-width:900px){.blog-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:600px){.blog-grid{grid-template-columns:1fr}}
.post{display:flex;flex-direction:column;text-align:left;background:var(--paper-50);border:1px solid var(--hairline);border-radius:var(--radius-card);overflow:hidden;cursor:pointer;
  opacity:1;transition:border-color .15s var(--ease),opacity 180ms var(--ease)}
/* category-filter fade (tpl.js) — .is-leaving is the outgoing half, held opacity:0 for
   180ms before [hidden] actually lands so the grid's own :has()-driven reflow below never
   races the fade; @starting-style covers the incoming half on its own once [hidden] clears. */
.post.is-leaving{opacity:0}
@starting-style{.post:not([hidden]){opacity:0}}
@media(prefers-reduced-motion:reduce){.post{transition:none}}
html.a11y-no-motion .post{transition:none}
@media(hover:hover) and (pointer:fine){.post:hover{border-color:var(--ink)}}
.post:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
.post-media{aspect-ratio:16/10;overflow:hidden;background:var(--paper-200)}
.post-media img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .45s var(--ease)}
@media(hover:hover) and (pointer:fine){.post:hover .post-media img{transform:scale(1.045)}}
/* award logos aren't photos — show them whole, contained on a neutral tile, not cover-cropped */
.post-media--logo{display:flex;align-items:center;justify-content:center;background:var(--paper-50)}
.post-media--logo img{width:auto;height:auto;max-width:56%;max-height:56%;object-fit:contain}
/* Vatopedi's mark is gold + dark maroon (#bfa176/#5d2e30): the maroon sinks into the dark
   plate, so dark theme lifts the whole mark. A filter, not a recolored file - the artwork
   stays the monastery's own. DNA's mid-orange needs nothing. */
html[data-theme="dark"] .post-media--logo img[src*="vatopedi"]{filter:brightness(1.9)}
@media(hover:hover) and (pointer:fine){.post:hover .post-media--logo img{transform:scale(1.06)}}
/* Poster plates (2026-07-31): the news art is 1:1 / 4:5 social cards with the headline, date
   and partner logos baked into the pixels. A 16/10 cover crop cuts that text off, so these
   are contained whole on a neutral tile — same reasoning as --logo, one step less cropping.

   Squared 2026-08-04. 4/3 was chosen to "keep the letterbox bars narrow", but it fits none of
   the six live posters: measured at 1440 it left 621px of visible empty space, 25% of the box
   width on the three 1:1 cards and 40% on the two 4:5 ones. 1/1 fits four of the six exactly
   (both 1:1 and, once height-bound, the 940x788) and halves the portrait margin to 20%.
   The tile also moves --paper-100 -> --paper-50 to match the card surface, so what margin
   remains stops reading as a grey gutter and the poster reads as a mounted print.
   Rejected: per-card intrinsic aspect-ratio, which removes the bars but pushes the slack into
   the text box instead — rows stretch to the tallest card, leaving ~100px of dead air above
   the CTA and landing a row's three headings at three different heights. */
.post-media--poster{aspect-ratio:1/1;display:flex;align-items:center;justify-content:center;background:var(--paper-50)}
.post-media--poster img{width:auto;height:100%;max-width:100%;object-fit:contain}
@media(hover:hover) and (pointer:fine){.post:hover .post-media--poster img{transform:scale(1.03)}}

/* ---- TPL brand shapes on the card art (2026-08-04) ----
   A PHOTOGRAPH on a .post card is masked into one of the four brand shapes (assets/shapes/),
   cycling by grid position — same set and convention as the history timeline's .hx-dot (:4523),
   the steps-expand cards (:4770) and the team portraits (:2629).

   POSTERS AND LOGOS ARE DELIBERATELY EXCLUDED, and the reason is measured, not aesthetic — do
   not "finish the job" by adding them later:
   • Masking them destroys content. Their headline, date and partner logos are baked into the
     pixels; the mask cut the ESPA card's ΑΝΤΑΓΩΝΙΣΤΙΚΟΤΗΤΑ lockup and the last letters off the
     e-volution wordmark. shape-1's lattice punched holes clean through the middle of a poster.
   • The non-destructive alternative was built and rejected on 2026-08-04: shape as a tinted
     PLATE on ::before with the art riding above it, untouched. It fails on its own terms. A
     poster is an opaque rectangle carrying its own background, so a plate behind it reads as a
     smudge rather than brand language, and clearing room for it cost 18% of the poster's size —
     smaller art, less legible ESPA logo strip. Probed at four strengths (--paper-200, ink 8%,
     red 6%, red 14%) against the plain card; the plain card won every pairing. The red variants
     read more deliberate but clash with the green and navy posters and blow the 5% red budget.
     Sideways consequence that also killed it: a plate only reads when it is wider than the art
     on every side, so shape-2 (X, pinches to ~55% at the waist) and shape-1 (see-through) were
     unusable behind a poster, leaving just two shapes to alternate across six poster cards.

   shape-1 is barred from photographs too — the lattice shreds one into confetti, the same bar
   that keeps it off the team portraits. A photo landing on that step takes the rosette.
   nth-child counts hidden cards, so the category filter never reshuffles the shapes. Behind
   @supports: without mask-size the base rules above stand as the fallback. */
@supports ((mask-size:100% 100%) or (-webkit-mask-size:100% 100%)){
  /* Square box, and it has to be square for ALL THREE tile kinds, not just the masked one.
     A photo needs 1/1 because the four shapes are near-1:1 (197x205 … 184x197) and a 16/10 box
     stretched them 1.6x, flattening the notches out of every silhouette. But posters are
     already 1/1 (:3815) and logos were still inheriting the 16/10 base, so a masked photo next
     to a logo card left the row's kickers and headings at two different heights — the exact
     misalignment the poster note rejected per-card aspect ratios over. Squaring the logo tile
     costs nothing: the logo inside is contained at 56% and is not touched. */
  .post-media--logo{aspect-ratio:1/1}
  .post-media--photo{aspect-ratio:1/1;background:none;overflow:hidden;
    -webkit-mask-image:var(--pm-shape);mask-image:var(--pm-shape);
    -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
    -webkit-mask-position:center;mask-position:center;
    -webkit-mask-size:100% 100%;mask-size:100% 100%}
  /* the cycle. Step 4 repeats the rosette rather than the X or the hex: at 3 columns it lands
     diagonally from step 2, where a repeat is least visible. */
  .blog-grid>:nth-child(4n+1) .post-media--photo{--pm-shape:url(../shapes/shape-2.svg)}
  .blog-grid>:nth-child(4n+2) .post-media--photo{--pm-shape:url(../shapes/shape-3.svg)}
  .blog-grid>:nth-child(4n+3) .post-media--photo{--pm-shape:url(../shapes/shape-5.svg)}
  .blog-grid>:nth-child(4n)   .post-media--photo{--pm-shape:url(../shapes/shape-3.svg)}
}
.post-body{padding:22px 22px 26px;display:flex;flex-direction:column;gap:11px;flex:1}
.post-kick{font-family:var(--font-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);margin:0}
.post-kick b{color:var(--red-ink);font-weight:500}
.post h3{font-size:20px;line-height:1.25;margin:0}
.post-ex{color:var(--ink-soft);font-size:14.5px;line-height:1.55;margin:0}
.post .go{margin-top:auto;color:var(--red-ink);font-weight:600;font-size:14px}
/* panel shell */
.post-scrim{position:fixed;inset:0;background:rgba(10,10,13,.5);opacity:0;visibility:hidden;transition:opacity .35s var(--ease),visibility .35s;z-index:200}
.post-scrim.open{opacity:1;visibility:visible}
/* close uses an ease-OUT curve: cubic-bezier(.4,0,1,1) is ease-in shaped, which delays the
   first frames — the exact moment the user is watching — and reads as a hesitant dismiss */
.post-drawer{position:fixed;top:0;right:0;height:100%;width:80%;max-width:1080px;background:var(--paper-0);box-shadow:-30px 0 80px rgba(0,0,0,.28);transform:translateX(100%);transition:transform .2s cubic-bezier(.32,.72,0,1);z-index:201;overflow-y:auto;overscroll-behavior:contain}
.post-drawer.open{transform:none;transition:transform .28s cubic-bezier(0,0,.2,1)}
html.pd-lock{overflow:hidden}
/* full-width bar driven by scaleX, not width: this updates on every scroll frame of the reading
   panel, and width would force layout each time where a transform stays on the compositor */
.pd-progress{position:fixed;top:0;left:0;height:3px;width:100%;transform:scaleX(0);transform-origin:0 50%;background:var(--red);z-index:6;transition:transform .1s linear}
.pd-grip{display:none}
.pd-close{position:fixed;top:18px;right:18px;width:44px;height:44px;border-radius:50%;background:var(--paper-0);border:1px solid var(--hairline);box-shadow:0 3px 14px rgba(0,0,0,.18);color:var(--ink);display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:7}
.pd-close:hover{border-color:var(--ink)}
.pd-close svg{width:20px;height:20px}
/* article */
/* Fixed cover plate (2026-08-04, owner call): one constant hero height for every article and
   the art fills it edge to edge. Was min(42vh,420px) — viewport-derived, so the same article
   opened on a laptop and a tall monitor gave a different crop, and the poster/logo variants
   letterboxed on a grey tile. --pf-hero-h is the single knob. */
.pf-hero{margin:0;background:var(--paper-200);height:var(--pf-hero-h,400px);overflow:hidden}
.pf-hero img{width:100%;height:100%;object-fit:cover;display:block}
/* Logo plates hold fixed-ink brand art (e.g. #181818 Americas wordmark) — the background
   must stay light in BOTH themes or the art vanishes on dark paper; literal, not a token. */
.pf-hero--logo{display:flex;align-items:center;justify-content:center;background:#FBFAF7}
.pf-hero--logo img{width:auto;height:auto;max-width:32%;max-height:60%;object-fit:contain}
@media(max-width:720px){.pf-hero--logo img{max-width:48%}}
/* Poster heroes: the 2026-07-31 "contained whole, never cover-cropped" rule was REVERSED on
   2026-08-04 (owner call — the letterbox bars were the thing being objected to). Poster heroes
   now inherit the base cover plate above and carry no rule of their own; the class stays on the
   markup as a hook. What this costs: a 1:1 poster in a 400px-tall box shows its middle band
   only, so the baked-in headline and the partner-logo strip are cropped out of the hero. Safe
   on both counts here — every article repeats its headline in .pf-body h2, and the EU/ESPA
   co-funding lockup the ESPA post needs is in the sitewide footer compliance band, not
   dependent on this image. */
@media(max-width:720px){.pf-hero{--pf-hero-h:300px}}
/* article photo gallery — used where a post has a set of images rather than one hero */
.pf-gal{margin:0 0 18px;display:grid;grid-template-columns:repeat(2,1fr);gap:14px}
@media(max-width:560px){.pf-gal{grid-template-columns:1fr}}
.pf-gal figure{margin:0}
.pf-gal img{width:100%;aspect-ratio:1/1;object-fit:cover;display:block;border-radius:var(--radius-card);background:var(--paper-100)}
.pf-gal figcaption{font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;line-height:1.45;color:var(--ink-muted);margin:8px 0 0}
.pf-wrap{max-width:960px;margin:0 auto;padding:0 clamp(20px,4vw,48px)}
.pf-layout{display:grid;grid-template-columns:200px 1fr;gap:clamp(24px,4vw,56px);padding:44px 0 96px;align-items:start}
.pf-body{min-width:0;max-width:680px}
.pf-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;margin:0 0 16px}
.pf-cat{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--red-ink)}
.pf-mi{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:11px;letter-spacing:.05em;color:var(--ink-muted)}
.pf-mi svg{width:14px;height:14px}
.pf-body h2{font-family:var(--font-display);font-weight:800;font-size:clamp(28px,4vw,44px);line-height:1.08;letter-spacing:-.02em;margin:0 0 20px;color:var(--ink)}
.pf-lede{font-size:19px;line-height:1.55;color:var(--ink);margin:0 0 30px}
.pf-sec{scroll-margin-top:24px}
.pf-sec h3{font-family:var(--font-display);font-weight:700;font-size:22px;margin:36px 0 12px;color:var(--ink)}
.pf-sec p{font-size:16.5px;line-height:1.72;color:var(--ink-soft);margin:0 0 18px}
/* "Read the full case study" CTA at the end of a drawer article. Deliberately OUTSIDE .pf-sec
   so the auto-built .pf-toc (which indexes .pf-sec h3s) never lists it as a section. Only
   partners.html's DNA template carries one so far; any article with a dedicated page can.
   No rule of its own: .pf-sec is a <section>, so it already inherits the global
   `section{border-bottom}` (:183) and that hairline closes the article. Adding one here
   printed two rules 28px apart. */
.pf-cta{margin:34px 0 0}
/* section timeline rail */
.pf-toc{position:sticky;top:34px;align-self:start;margin:0;border-left:1px solid var(--hairline)}
.pf-tochead{display:block;font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);padding:0 0 6px 22px}
.pf-toc a{position:relative;display:block;padding:7px 0 7px 22px;font-size:13px;line-height:1.35;color:var(--ink-muted);text-decoration:none;transition:color .15s var(--ease)}
.pf-toc a::before{content:"";position:absolute;left:-5px;top:12px;width:9px;height:9px;border-radius:50%;background:var(--paper-0);border:1.5px solid var(--paper-500);transition:background .15s,border-color .15s}
.pf-toc a:hover{color:var(--ink)}
.pf-toc a.is-active{color:var(--ink);font-weight:600}
.pf-toc a.is-active::before{background:var(--red);border-color:var(--red)}
/* mobile: bottom sheet + horizontal contents strip */
@media(max-width:640px){
  .post-drawer{top:auto;right:0;left:0;bottom:0;width:100%;max-width:none;height:92%;border-radius:20px 20px 0 0;transform:translateY(100%);box-shadow:0 -20px 60px rgba(0,0,0,.3)}
  .post-drawer.open{transform:none}
  .pd-grip{display:block;height:22px;text-align:center}
  .pd-grip::before{content:"";display:inline-block;margin-top:9px;width:40px;height:4px;border-radius:2px;background:var(--paper-500)}
  .pd-close{top:14px;right:14px}
  .pf-hero{--pf-hero-h:240px}
  .pf-layout{grid-template-columns:1fr;gap:0;padding-top:16px}
  .pf-toc{position:sticky;top:0;order:-1;display:flex;gap:8px;overflow-x:auto;border-left:0;border-bottom:1px solid var(--hairline);padding:10px 0 12px;margin:0 0 22px;background:var(--paper-0);-webkit-overflow-scrolling:touch}
  .pf-tochead{display:none}
  .pf-toc a{flex:0 0 auto;padding:8px 14px;border:1px solid var(--hairline);border-radius:9999px;font-size:12px;white-space:nowrap}
  .pf-toc a::before{display:none}
  .pf-toc a.is-active{border-color:var(--red);color:var(--red-ink);font-weight:600}
}
@media(prefers-reduced-motion:reduce){.post-drawer,.post-scrim,.post-media img,.pd-progress{transition:none}}
html.a11y-no-motion .post-drawer,html.a11y-no-motion .post-scrim,html.a11y-no-motion .post-media img,html.a11y-no-motion .pd-progress{transition:none}

/* news post-filter tabs */
.blog-tabs{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 30px}
.blog-tab{font-family:var(--font-mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-muted);background:none;border:1px solid var(--hairline);border-radius:9999px;padding:9px 16px;min-height:44px;cursor:pointer;transition:background .15s var(--ease),border-color .15s var(--ease),color .15s var(--ease)}
.blog-tab:hover{border-color:var(--ink);color:var(--ink)}
.blog-tab[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--paper-0)}
.blog-tab:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.post[hidden]{display:none}

/* ===== D2C "How it works" — scroll-scrubbed step slide (.hiw) =====
   Panels slide in from the right as the stage pins; the manifest rail scrubs a red dot
   across 4 tick stops. Palette: black (--inverse, theme-invariant) / white / transparent;
   red budget = the 10px dot + step-01's .wwd-kick. Actor coding: step 01 (You) is a white
   card; steps 02-04 (We) are one black block with hairline row separators — 1 team, 1 roof.
   DEFAULT = static stacked ledger (no-JS / <721px / reduced-motion / a11y-no-motion);
   tpl.js adds .is-anim on the scene and drives transforms + the rail (?p=0..1 freezes
   progress for headless QA). --hiw-top = measured .abar height so the pin clears the nav.
   NOT coupled to body.fitx-m: these steps are desktop-scrub only (≥721px). They read that
   flag until 2026-07-29, which made d2c the only page whose steps slid sideways on a phone;
   tpl.js's .hiw setMode() no longer reads it, so the <721px gate above is width alone and the
   phone path is the .m-in stacked reveal at the bottom of this block. (The fitx-m flag it used
   to read is gone entirely as of 2026-08-28.) */
.hiw-pw{display:none}
.hiw-panel{padding:52px 0}
/* the row is a 2-row grid: kick on its own line, then TITLE and DESC sharing one first-line
   baseline (the row's main axis), icon centered across both rows — exact alignment for
   1- and 2-line titles alike, no tuned paddings */
.hiw-in{display:grid;grid-template-columns:minmax(200px,.85fr) 1.15fr auto;grid-template-rows:auto auto;
  gap:0 clamp(24px,4vw,64px);align-items:baseline;width:100%}
.hiw-in .wwd-kick{grid-column:1;grid-row:1}
.hiw-in h3{grid-column:1;grid-row:2}
.hiw-in .hiw-desc{grid-column:2;grid-row:2}
.hiw-in .hiw-ico{grid-column:3;grid-row:1 / 3;align-self:center}
.hiw-panel h3{font-size:clamp(24px,2.6vw,38px);font-weight:800;letter-spacing:-.015em;line-height:1.08}
.hiw-panel .wwd-kick{display:block;margin-bottom:12px}
/* no max-width cap: let the desc column fill its grid track so the row's right edge lines
   up with the icon consistently across steps (52ch was capping well short of the actual
   ~547px column at 1280 viewport — confirmed on b2b2c 2026-07-24, rolled out to d2c) */
.hiw-desc{display:flex;gap:14px;font-size:15px;line-height:1.55}
.hiw-desc::before{content:"";flex:0 0 6px;height:6px;border-radius:50%;background:currentColor;margin-top:.5em;opacity:.7}
.hiw-ico{width:clamp(72px,10vh,104px);height:clamp(72px,10vh,104px)}
.hiw-p1{background:var(--paper-0);color:var(--ink);border-block:1px solid var(--hairline)}
.hiw-p1 .hiw-desc{color:var(--ink-soft)}
.hiw-p1 .hiw-ico [fill="#fff"]{fill:var(--paper-0)} /* occlusion fills track the card surface in dark theme */
.hiw-dark{background:var(--inverse);color:var(--paper-fixed)}
.hiw-dark .hiw-desc{color:rgba(255,255,255,.82)}
.hiw-dark .wwd-kick{color:rgba(255,255,255,.75)}
.hiw-dark .wwd-kick .wwd-n{color:rgba(255,255,255,.55)} /* ≥5.8:1 on --inverse — .45 was a 4.51:1 coin-flip */
.hiw-dark .hiw-ico [fill="#fff"]{fill:var(--inverse)} /* fills read as transparent on the black rows */
.hiw-p3,.hiw-p4,.hiw-p5{border-top:1px solid rgba(255,255,255,.14)}
.hiw-ico .pc{transition:transform .4s cubic-bezier(.22,1,.36,1);transition-delay:var(--td,0s)}
@media(hover:hover) and (pointer:fine){.hiw-panel:hover .pc{transform:translate(var(--dx,0),var(--dy,0))}}
/* how-it-works panel "typing" pulse removed 2026-07-22 (shared wwd-typing keyframe deleted — motion trim) */
/* pinned choreography */
/* Panel COUNT is a variable (--hiw-n on .hiw-scene, default 4): the runway, each row's height,
   each row's top offset and the guide-line grid are all derived from it, so a scene can carry a
   different number of steps without a per-page fork. d2c/b2b2c leave it unset (4 → 480vh runway,
   rows at 0/25/50/75%, guides at 0/25/50/75/100%, i.e. byte-identical to the hand-written rules
   this replaced); partners.html's #platform sets 6. Two spare guide layers are declared beyond
   n=4's needs — at n=4 they land at 125%/150%, outside the box, and are simply not painted. */
.hiw-scene.is-anim{height:calc(var(--hiw-n,4) * 120vh)}
.is-anim .hiw-stage{position:sticky;top:var(--hiw-top,0px);height:calc(100vh - var(--hiw-top,0px));overflow:hidden;display:flex;flex-direction:column}
.is-anim .hiw-pw{display:flex;height:14vh;flex:0 0 auto;align-items:center}
.hiw-pw .wrap{display:flex;align-items:center;gap:28px;width:100%}
.hiw-lbl{font-family:var(--font-mono);font-weight:500;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink);white-space:nowrap;min-width:186px}
.hiw-rail{position:relative;flex:1;height:1px;background:var(--hairline)}
.hiw-tick{position:absolute;top:50%;width:1px;height:9px;transform:translateY(-50%);background:var(--paper-500)}
.hiw-dot{position:absolute;top:50%;left:0;width:10px;height:10px;border-radius:50%;background:var(--red);transform:translate(-50%,-50%)}
.hiw-tag{font-family:var(--font-mono);font-weight:500;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);white-space:nowrap}
.is-anim .hiw-panels{position:relative;flex:1 1 auto}
/* guide rails at the top + bottom edge of every card row — drawn OVER the cards
   (z-index), so landed cards sit separated by exactly the 1px guide stroke */
.is-anim .hiw-panels::after{content:"";position:absolute;inset:0;pointer-events:none;z-index:2;
  background-image:linear-gradient(var(--hairline),var(--hairline)),linear-gradient(var(--hairline),var(--hairline)),linear-gradient(var(--hairline),var(--hairline)),linear-gradient(var(--hairline),var(--hairline)),linear-gradient(var(--hairline),var(--hairline)),linear-gradient(var(--hairline),var(--hairline)),linear-gradient(var(--hairline),var(--hairline));
  background-size:100% 1px;background-repeat:no-repeat;
  background-position:0 0,0 calc(100%/var(--hiw-n,4)),0 calc(200%/var(--hiw-n,4)),0 calc(300%/var(--hiw-n,4)),0 calc(400%/var(--hiw-n,4)),0 calc(500%/var(--hiw-n,4)),0 calc(600%/var(--hiw-n,4))}
.is-anim .hiw-panel{position:absolute;left:0;right:0;height:calc(100%/var(--hiw-n,4));display:flex;align-items:center;padding:0;transform:translateX(105%);will-change:transform}
.is-anim .hiw-panel{border-top:0;border-block:0} /* the guide stroke IS the separator */
/* the pinned row is display:flex, so its .wrap is a flex ITEM: without width:100% it
   shrink-wraps to its content and the whole row drifts inward, so short rows no longer share
   the page's left rule with long ones (visible the moment a scene carried 6 rows of uneven
   copy — d2c/b2b2c only escaped it because every row's copy already hit --max). Same fix, same
   reason, as .hiw-pw .wrap above; max-width + margin:auto still do the centring. */
.is-anim .hiw-panel>.wrap{width:100%}
/* row tops by DOM position, not by .hiw-pN — the ramp classes stay purely about colour */
.is-anim .hiw-panels>:nth-child(1){top:0}
.is-anim .hiw-panels>:nth-child(2){top:calc(100%/var(--hiw-n,4))}
.is-anim .hiw-panels>:nth-child(3){top:calc(200%/var(--hiw-n,4))}
.is-anim .hiw-panels>:nth-child(4){top:calc(300%/var(--hiw-n,4))}
.is-anim .hiw-panels>:nth-child(5){top:calc(400%/var(--hiw-n,4))}
.is-anim .hiw-panels>:nth-child(6){top:calc(500%/var(--hiw-n,4))}
@media(max-width:720px){
  .hiw-in{grid-template-columns:1fr;grid-template-rows:none;gap:16px}
  .hiw-in .wwd-kick,.hiw-in h3,.hiw-in .hiw-desc,.hiw-in .hiw-ico{grid-column:auto;grid-row:auto}
  .hiw-ico{width:72px;height:72px;order:-1}
  .hiw-panel{padding:40px 0}
  /* the progress rail was desktop-only in practice (.hiw-pw stays display:none until .is-anim
     is added, which never happened below 721px before the fitx-m mobile-scrub pilot) — .hiw-tag
     ("D2C · how it works") was never width-tested and its nowrap text plus the 186px .hiw-lbl
     don't both fit a phone column. The model name is already established by the page itself
     (title, breadcrumb, hero), so drop the tag rather than truncate or wrap it. */
  .hiw-tag{display:none}
}
@media(prefers-reduced-motion:reduce){
  .hiw-scene.is-anim{height:auto}
  .is-anim .hiw-stage{position:static;height:auto;overflow:visible;display:block}
  .is-anim .hiw-pw{display:none}
  .is-anim .hiw-panels{position:static}
  .is-anim .hiw-panel{position:static;height:auto;padding:52px 0;transform:none !important}
  .hiw-ico .pc{transition:none}
  .hiw-panel:hover .pc{transform:none}
  .hiw-panel:hover .typ circle{animation:none}
}
html.a11y-no-motion .hiw-scene.is-anim{height:auto}
html.a11y-no-motion .is-anim .hiw-stage{position:static;height:auto;overflow:visible;display:block}
html.a11y-no-motion .is-anim .hiw-pw{display:none}
html.a11y-no-motion .is-anim .hiw-panels{position:static}
html.a11y-no-motion .is-anim .hiw-panel{position:static;height:auto;padding:52px 0;transform:none !important}
html.a11y-no-motion .hiw-ico .pc{transition:none}
html.a11y-no-motion .hiw-panel:hover .pc{transform:none}
html.a11y-no-motion .hiw-panel:hover .typ circle{animation:none}
/* mobile scroll reveal — the pinned step-scrub is desktop-only (≥721px) by DEFAULT; on narrow
   widths the stacked step panels rise + fade in as they enter view (.m-in added by tpl.js). */
@media(max-width:720px){
  .hiw-panels .hiw-panel{opacity:0;transform:translateY(26px);transition:opacity .6s var(--ease),transform .6s var(--ease)}
  .hiw-panels .hiw-panel.m-in{opacity:1;transform:none}
  /* Dormant safety net. The step-scrub is desktop-only again as of 2026-07-29: tpl.js's .hiw
     setMode no longer reads body.fitx-m, so .is-anim cannot be set below 721px and this rule
     does not currently match anything. It is kept because it is the correct pairing IF the
     mobile-scrub pilot is ever extended to the steps: the scrub's own translateX choreography
     replaces the .m-in rise-in, and opacity/transition are all it may set, since position and
     transform for every state already come from the ungated .is-anim rules above. A per-frame
     CSS transition here would fight the JS's scroll-locked inline transform and read as laggy
     catch-up rather than a 1:1 scrub. Ties on specificity with the two rules above (3 classes
     each) and sits after them, so it would win. */
  .hiw-scene.is-anim .hiw-panel{opacity:1;transition:none}
}
@media(max-width:720px) and (prefers-reduced-motion:reduce){.hiw-panels .hiw-panel{opacity:1;transform:none;transition:none}}
html.a11y-no-motion .hiw-panels .hiw-panel{opacity:1;transform:none;transition:none}
/* no-JS safety net: .m-in comes from tpl.js and .is-anim (the fitx-m mobile-scrub pilot's
   opacity override) does too, so with scripting off the steps would stay at opacity:0 on a
   phone. Engines without the `scripting` feature drop this query as invalid — no-op there. */
@media(max-width:720px) and (scripting:none){.hiw-panels .hiw-panel{opacity:1;transform:none;transition:none}}

/* ——— .hiw panels → TPL red family (01 = the primary red, like the CTA/logo) + all-white icons + infinite-loop motion ——— */
.hiw-p1{background:#AB0000;color:#fff;border-block:0}
.hiw-p2{background:#920000}
.hiw-p3{background:#7A0000}
.hiw-p4{background:#620000}
/* p5 exists since the 5th step (02 Evaluation) landed on d2c 2026-08-28. Without it the row
   falls through to .hiw-dark's var(--inverse) and the ramp ends on near-black. Keep this list
   in step with the panel count on the page. */
.hiw-p5{background:#4A0000}
.hiw-p2,.hiw-p3,.hiw-p4,.hiw-p5{border-top:1px solid rgba(255,255,255,.16)}
.hiw-panel{color:#fff}
.hiw-panel .wwd-kick{color:#fff}
.hiw-panel .wwd-kick .wwd-n{color:rgba(255,255,255,.6)}
.hiw-panel .hiw-desc{color:rgba(255,255,255,.86)}
/* all-white icons: strokes white (accent groups no longer hardcode stroke:var(--red) inline —
   removed 2026-07-24, they now inherit currentColor like the rest of the icon), occlusion
   faces track each panel's red */
.hiw-ico{color:#fff}
.hiw-p1 .hiw-ico [fill="#fff"]{fill:#AB0000}
.hiw-p2 .hiw-ico [fill="#fff"]{fill:#920000}
.hiw-p3 .hiw-ico [fill="#fff"]{fill:#7A0000}
.hiw-p4 .hiw-ico [fill="#fff"]{fill:#620000}
.hiw-p5 .hiw-ico [fill="#fff"]{fill:#4A0000}
/* B2B2C variant — grayscale step ramp (D2C keeps the red family). Dark → lighter across
   01→04; white text/icons throughout (lightest capped at #6E6E6E so the 15px desc still ≥4.5:1). */
.hiw--gray .hiw-p1{background:#1F1F1F}
.hiw--gray .hiw-p2{background:#3A3A3A}
.hiw--gray .hiw-p3{background:#484848}
.hiw--gray .hiw-p4{background:#565656}
.hiw--gray .hiw-p5{background:#646464}
/* AA contrast (2026-07-23 audit): .6-white ghost numerals fail on the brighter panels
   (red p1 #AB0000 3.33:1, p2 #920000 3.96:1; gray p3/p4 were 2.9-3.9:1). Bump those numerals
   to .82 white, and the two lightest gray panels above are darkened so .hiw-desc body copy
   also clears 4.5:1. Supersedes the per-page #journey / body.ai-neural #how patches.

   2026-07-30 (audit H1): THIS RULE WAS DEAD FOR A WEEK. It was written as `.hiw-p1 .wwd-n`
   (0,2,0) while `.hiw-panel .wwd-kick .wwd-n` at ~3273 is (0,3,0), so the more specific .6
   white always won and the numerals still measured 3.33:1 / 3.95:1 in the browser — the exact
   values the comment above claims to have fixed. Requalified to (0,4,0) so it actually binds;
   re-measured by pixel sampling at 5.41:1 (p1) and 6.31:1 (p2). Do not simplify these
   selectors back to `.hiw-pN .wwd-n` — that is what broke it. */
.hiw-panel.hiw-p1 .wwd-kick .wwd-n,.hiw-panel.hiw-p2 .wwd-kick .wwd-n,
.hiw--gray .hiw-panel.hiw-p3 .wwd-kick .wwd-n,.hiw--gray .hiw-panel.hiw-p4 .wwd-kick .wwd-n,
.hiw--gray .hiw-panel.hiw-p5 .wwd-kick .wwd-n{color:rgba(255,255,255,.82)}
.hiw--gray .hiw-p1 .hiw-ico [fill="#fff"]{fill:#1F1F1F}
.hiw--gray .hiw-p2 .hiw-ico [fill="#fff"]{fill:#3A3A3A}
.hiw--gray .hiw-p3 .hiw-ico [fill="#fff"]{fill:#545454}
.hiw--gray .hiw-p4 .hiw-ico [fill="#fff"]{fill:#6E6E6E}
.hiw--gray .hiw-p5 .hiw-ico [fill="#fff"]{fill:#646464}
/* Partners "What you plug into" variant (partners.html #platform) — SIX rows on the same
   scrubbed scene (--hiw-n:6 on the scene). Its own dark ramp rather than .hiw--gray's, because
   that one only reaches p4 and its top end (#565656) leaves no room for two more steps that
   still clear 4.5:1 for the 14px desc. Ramp tops out at #5A5A5A: desc rgba(255,255,255,.86)
   measures 5.6:1 there, and the ghost numerals are lifted to .82 white (5.3:1) — the same fix,
   and the same specificity trap, as the .hiw--gray numerals above. Six rows split the stage
   four used, so the type and the icon step down a size; scoped to >=721px so the stacked
   phone ledger keeps the full-size treatment from the max-width:720px block. */
.hiw--plug .hiw-p1{background:#1A1A1A}
.hiw--plug .hiw-p2{background:#262626}
.hiw--plug .hiw-p3{background:#333333}
.hiw--plug .hiw-p4{background:#404040}
.hiw--plug .hiw-p5{background:#4D4D4D}
.hiw--plug .hiw-p6{background:#5A5A5A}
.hiw--plug .hiw-panel.hiw-p1 .wwd-kick .wwd-n,.hiw--plug .hiw-panel.hiw-p2 .wwd-kick .wwd-n,
.hiw--plug .hiw-panel.hiw-p3 .wwd-kick .wwd-n,.hiw--plug .hiw-panel.hiw-p4 .wwd-kick .wwd-n,
.hiw--plug .hiw-panel.hiw-p5 .wwd-kick .wwd-n,.hiw--plug .hiw-panel.hiw-p6 .wwd-kick .wwd-n{color:rgba(255,255,255,.82)}
.hiw--plug .hiw-p1 .hiw-ico [fill="#fff"]{fill:#1A1A1A}
.hiw--plug .hiw-p2 .hiw-ico [fill="#fff"]{fill:#262626}
.hiw--plug .hiw-p3 .hiw-ico [fill="#fff"]{fill:#333333}
.hiw--plug .hiw-p4 .hiw-ico [fill="#fff"]{fill:#404040}
.hiw--plug .hiw-p5 .hiw-ico [fill="#fff"]{fill:#4D4D4D}
.hiw--plug .hiw-p6 .hiw-ico [fill="#fff"]{fill:#5A5A5A}
@media(min-width:721px){
  .hiw--plug .hiw-panel h3{font-size:clamp(19px,1.75vw,27px)}
  .hiw--plug .hiw-panel .wwd-kick{margin-bottom:7px}
  .hiw--plug .hiw-desc{font-size:14px}
  .hiw--plug .hiw-ico{width:clamp(56px,7vh,80px);height:clamp(56px,7vh,80px)}
  .hiw--plug .hiw-panel{padding:34px 0} /* static ledger only; .is-anim zeroes it */
}
/* infinite-loop micro-motion — SCOPED to .hiw-ico so the nav/matrix .mv hover animations
   stay untouched. Loops per card: flow-pitch (red plane drift, 2s alternate), flow-buy
   (5s box handoff + red check drift 1.9s), flow-live (clipped bars alternate-sink + red
   spark drift 1.4s), flow-operate (SMIL orbit in the SVG markup — CSS cannot gate SMIL,
   so tpl.js pauses/unpauses it at t=0 with the same motion checks). */
.hiw-ico .mv{animation:hiw-float var(--dur,2s) ease-in-out var(--td,0s) infinite alternate}
@keyframes hiw-float{from{transform:translate(0,0)}to{transform:translate(var(--dx,0px),var(--dy,0px))}}
.hiw-ico .sink{animation:flow-live-sink var(--dur,1.7s) ease-in-out var(--td,0s) infinite alternate}
@keyframes flow-live-sink{from{transform:translateY(var(--sk,20px))}to{transform:translateY(0)}}
/* flow-buy handoff: out slides/fades away over 0-40%, in arrives 30-75%; both rest at the
   same origin so the restart (in-at-rest → out-at-rest) is seamless */
.hiw-ico .bout{animation:flow-buy-out 5s var(--ease) infinite}
.hiw-ico .bin{animation:flow-buy-in 5s var(--ease) infinite}
@keyframes flow-buy-out{0%{opacity:1;transform:translate(0,0)}40%,100%{opacity:0;transform:translate(42.44px,24.5px)}}
@keyframes flow-buy-in{0%,30%{opacity:0;transform:translate(-42.44px,-24.5px)}75%,100%{opacity:1;transform:translate(0,0)}}
/* flow-operate: the red box's 7s orbit + back-half visibility sync live as SMIL
   (animateMotion/animate) in the SVG markup — CSS offset-path does not render on SVG
   children in Chromium/Edge (verified 2026-07-16: even a static offset-distance never
   paints), and CSS offset properties would override animateMotion if both existed.
   SMIL can't be gated by CSS, so tpl.js pauses the svg clock at t=0 for motion-off. */
/* motion off → CSS loops disabled at rest (the SMIL orbit is paused at t=0 by tpl.js,
   which parks the box at the ellipse's left point — no CSS transform here: it would
   COMBINE with the SMIL motion, not replace it) */
@media(prefers-reduced-motion:reduce){
  .hiw-ico .mv,.hiw-ico .sink,.hiw-ico .bout,.hiw-ico .bin{animation:none}
  .hiw-ico .bout,.hiw-ico .bin{transform:none;opacity:1}
  .hiw-ico .sink{transform:none}
}
html.a11y-no-motion .hiw-ico .mv,html.a11y-no-motion .hiw-ico .sink,html.a11y-no-motion .hiw-ico .bout,html.a11y-no-motion .hiw-ico .bin{animation:none}
html.a11y-no-motion .hiw-ico .bout,html.a11y-no-motion .hiw-ico .bin{transform:none;opacity:1}
html.a11y-no-motion .hiw-ico .sink{transform:none}

/* ===== D2C "Fit" — scroll-scrubbed stacked sentence on an inverse band (.fitx) =====
   Head + composition pin together; all 4 lines stay visible; the anchor word slides
   to the active line and flips on every beat (its own per-line word track, driven by
   tpl.js), landing on red We at the finale; Fit/Sweet-spot labels ride
   the captions. On-dark colours follow the hero convention (tpl.css .hero block):
   eyebrow/emphasis #ef7676, heading #fff with a .66 second beat — the band is the
   theme-invariant --inverse, so raw white/rgba-white literals per the site convention.
   DEFAULT = static stacked ledger (no-JS / <721px / reduced-motion / a11y-no-motion);
   tpl.js adds .is-anim on the scene and drives the anchor/lines/captions. */
section.fitx-band{background:var(--inverse);color:#fff;padding:0;border-bottom:0;position:relative;z-index:0}
/* the trailing content (Not-the-fit + CTA + footer) curtains up over the pinned Fit band:
   an opaque paper layer, higher z-index, pulled up to overlay the band while it holds its
   "We" finale. Only when the scrub is active (>=721px + motion on) — otherwise normal flow.
   min-height:100vh is load-bearing: the curtain must cover the FULL 100vh handoff window
   regardless of how tall its own content (Not-the-fit + CTA) happens to be, or the pinned
   band's black background shows through as a gap above the footer on tall viewports
   (confirmed 2026-07-20: 1050px of content vs. a 1208px viewport left a 158px black gap). */
.fit-curtain{position:relative;z-index:2;background:var(--paper-100)}
@media(min-width:721px) and (prefers-reduced-motion:no-preference){html:not(.a11y-no-motion) .fit-curtain{margin-top:-100vh;min-height:100vh}}
/* No phone counterpart. body.fitx-m used to lift the width gate so the pinned scrub ran under
   721px as well, and pulled the curtain up to match; both were retired with the nailed-subject
   rebuild (2026-08-28). That composition needs nowrap display type and uniform row heights in a
   three-row window, and neither survives a phone column — the honest phone answer is the static
   ledger with the .m-in stacked reveal at the bottom of this block. */
/* --fitx-lh is the scrub composition's leading, and it is LOAD-BEARING, not cosmetic: the
   sliding anchor is a slot machine whose window (.fitx-aw) is one line tall with overflow:hidden,
   while tpl.js steps .aw-track by the MEASURED height of a fit line (measure() ->
   lines[0].getBoundingClientRect().height). Window height, entry height and line height must all
   resolve to the same value or the track drifts a little further off its window every beat and the
   last word shows up as a blank slot. Change this one value, never the three rules below, and
   never add vertical padding to .is-anim .fitx-line (padding lands in the measured height but not
   in the window). */
.fitx-scene{position:relative;--fitx-lh:1.15}
.fitx-stick{padding:clamp(64px,9vh,96px) 0}
.fitx-head{margin-bottom:clamp(36px,5vh,56px)}
.fitx-head .eyebrow{color:var(--red-ink-inverse)}
.fitx-head h2.two-tone{color:#fff;margin-bottom:0}
.fitx-head h2.two-tone span:not(.word){color:var(--red-ink-inverse)} /* = the fill's end-state (see :156) */
.fitx-grid{display:grid;grid-template-columns:1fr;column-gap:.4em;align-items:start;
  font-size:clamp(22px,3.2vw,44px)} /* under the h2 cap — a section moment, not a hero */
.fitx-line{display:block;padding:14px 0;border-bottom:1px solid rgba(255,255,255,.12);
  font-family:var(--font-display);font-weight:800;letter-spacing:-.02em;line-height:1.15;
  font-size:inherit;color:#fff;transition:color .4s ease}
.fitx-lines .fitx-line:last-child{border-bottom:0}
.fitx-line .pw{color:rgba(255,255,255,.55)}
.fitx-line--last .pw{color:var(--red-ink-inverse)}
.fitx-aw{display:none;font-family:var(--font-display);font-weight:800;letter-spacing:-.02em;
  line-height:1.15;font-size:inherit;color:#fff}
.fitx-cap{display:block;font-family:var(--font-mono);font-weight:500;font-size:12px;letter-spacing:.08em;
  text-transform:uppercase;color:rgba(255,255,255,.62);line-height:1.7;margin-top:10px}
/* register labels: bold-white, NOT red — weight is the non-color cue, and the band's
   one red payoff stays the You→We pivot (plus the sanctioned eyebrow) */
.fitx-tag{color:rgba(255,255,255,.85);font-weight:700}
.fitx-caps{display:none}
/* ── pinned choreography (rebuilt 2026-08-28, ADR pending) ────────────────────────────────────
   The subject is NAILED and the propositions travel past it: "You" holds dead still on the middle
   row of a three-row window while the four fit lines scroll up to meet it, and it turns over to a
   red "We" on the last one. Progress is read from the letters, not from a separate indicator: each
   proposition and its caption fill left to right through the glyphs as that line is read.

   Three things replaced the previous build, all for the same reason. It read as dead on a mouse
   wheel, and measurement said why:

     1. It was a stepper, not a scrub. Progress was rounded to the nearest line and a 0.5s CSS
        transition animated the gap, so position ran on its own clock: 4 rest positions over 61
        scroll samples. Nothing that carries position gets a CSS duration here any more. tpl.js
        drives it from a damped follower instead, which is what actually smooths a wheel (one
        notch is a ~100px jump, and raw scroll-locking turns that into visible quantisation).
     2. The finale never finished. SCRUB_END was a hardcoded 0.5, which clamped the last line's
        caption fill to t=0.5 permanently, and left ~287px of pin where nothing moved at all.
        Both the scrub end and the row geometry are derived now.
     3. The caption was a word-by-word class toggle on an aria-hidden duplicate of itself. It is
        one gradient stop clipped to the real caption now: sub-pixel, one property write per
        frame, no duplicate DOM, and the fill can sit halfway through a letter.

   LOAD-BEARING, in the sense that getting it wrong is silent:
     - Every rule that makes text transparent is scoped under .is-anim. The fill works by
       clipping a gradient to the glyphs, so an unscoped rule blanks the copy on every path where
       the module does not run: no JS, reduced motion, a11y-no-motion, below 721px.
     - .fitx-pred / .fitx-cap need width:max-content. A gradient is sized to the background box,
       so a full-width box makes --fill:0.5 already read as finished.
     - Never set display on .fitx-pred or .fitx-cap here. They must stay block-level: as
       inline-block they share one line box, which collapses the measured row height and makes
       the window's rows collide.
     - Row height, window height, the subject's top offset and the slot's entry height all come
       from ONE measurement in tpl.js. A track that steps by row height is only correct while
       every row is the same height, and the captions are not the same length. */
.fitx-scene.is-anim{height:360vh}
.is-anim .fitx-stick{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;justify-content:center;overflow:hidden;padding:0}
.is-anim .fitx-grid{grid-template-columns:auto 1fr;column-gap:.45em}

/* beat counter — mono, the register the site already uses for ordinals (.dg-n, .gz-i). The fill
   reads progress WITHIN a proposition; this reads progress THROUGH the section. */
.fitx-count{display:none}
.is-anim .fitx-count{display:block;font-family:var(--font-mono);font-size:12px;letter-spacing:.16em;
  color:rgba(255,255,255,.42);margin-top:clamp(14px,2.2vh,22px)}
.is-anim .fitx-count b{color:#fff;font-weight:700}

/* column 1: an invisible spacer reserving the subject's width, and the nailed subject over it.
   top:0 is required and not decorative — without it the absolute box takes its static position,
   which is after the spacer, and the subject renders one line below its proposition. tpl.js
   overrides top to park it on the middle row. */
.fitx-col1{display:none}
.is-anim .fitx-col1{display:block;position:relative}
.fitx-gutter{visibility:hidden;white-space:nowrap;font-family:var(--font-display);font-weight:800;
  letter-spacing:-.02em;line-height:var(--fitx-lh);font-size:inherit}
.is-anim .fitx-aw{display:block;position:absolute;left:0;top:0;z-index:2;overflow:hidden}
.is-anim .fitx-aw .aw-track{display:flex;flex-direction:column}
.is-anim .fitx-aw .aw{line-height:var(--fitx-lh);white-space:nowrap}
.is-anim .fitx-aw .aw--we{color:var(--red-ink-inverse)}

/* the three-row window. Feathered top and bottom so what is coming and what has gone stay
   readable without competing with the row being read. */
.is-anim .fitx-view{overflow:hidden;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 27%,#000 73%,transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 27%,#000 73%,transparent 100%)}
.is-anim .fitx-line{padding:0;border:0;color:#fff}
.is-anim .fitx-line .pw{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}   /* the nailed subject carries it VISUALLY only:
  this must stay in the accessibility tree, or the read sentence loses its subject (WCAG 1.3.1) */
.is-anim .fitx-pred,
.is-anim .fitx-cap{width:max-content;max-width:100%;white-space:nowrap;
  -webkit-background-clip:text;background-clip:text;color:transparent}
.is-anim .fitx-cap .fitx-tag{color:transparent}  /* or it paints over the clipped gradient */
.is-anim .fitx-pred{background-image:linear-gradient(90deg,
  #fff calc(var(--fill,0) * 100%),
  rgba(255,255,255,.22) calc(var(--fill,0) * 100% + 7%))}
.is-anim .fitx-cap{background-image:linear-gradient(90deg,
  rgba(255,255,255,.92) calc(var(--fill,0) * 100%),
  rgba(255,255,255,.20) calc(var(--fill,0) * 100% + 7%))}
/* Fallbacks. tpl.js gates .is-anim on width AND motion, so these blocks are a safety net rather
   than the live path — but they must still be correct, because everything that makes text
   transparent above would otherwise blank the copy here. */
@media(max-width:720px){
  .fitx-stick{padding:48px 0}
  .fitx-head{margin-bottom:32px}
  .fitx-scene.is-anim{height:auto}
  .is-anim .fitx-stick{position:static;height:auto;overflow:visible;display:block;padding:clamp(64px,9vh,96px) 0}
  .is-anim .fitx-grid{grid-template-columns:1fr}
  .is-anim .fitx-col1{display:none}
  .is-anim .fitx-view{overflow:visible;-webkit-mask-image:none;mask-image:none;height:auto !important}
  .is-anim .fitx-lines{transform:none !important}
  .is-anim .fitx-line{padding:14px 0;border-bottom:1px solid rgba(255,255,255,.12);height:auto !important;color:#fff}
  .is-anim .fitx-lines .fitx-line:last-child{border-bottom:0}
  .is-anim .fitx-line .pw{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
  .is-anim .fitx-pred,
  .is-anim .fitx-cap{width:auto;white-space:normal;background-image:none;color:inherit}
  .is-anim .fitx-cap{color:rgba(255,255,255,.62)}
  .is-anim .fitx-cap .fitx-tag{color:rgba(255,255,255,.85)}
  .is-anim .fitx-count{display:none}
}
@media(prefers-reduced-motion:reduce){
  .fitx-scene.is-anim{height:auto}
  .is-anim .fitx-stick{position:static;height:auto;overflow:visible;display:block;padding:clamp(64px,9vh,96px) 0}
  .is-anim .fitx-grid{grid-template-columns:1fr}
  .is-anim .fitx-col1{display:none}
  .is-anim .fitx-view{overflow:visible;-webkit-mask-image:none;mask-image:none;height:auto !important}
  .is-anim .fitx-lines{transform:none !important}
  .is-anim .fitx-line{padding:14px 0;border-bottom:1px solid rgba(255,255,255,.12);height:auto !important;color:#fff}
  .is-anim .fitx-lines .fitx-line:last-child{border-bottom:0}
  .is-anim .fitx-line .pw{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
  .is-anim .fitx-pred,
  .is-anim .fitx-cap{width:auto;white-space:normal;background-image:none;color:inherit}
  .is-anim .fitx-cap{color:rgba(255,255,255,.62)}
  .is-anim .fitx-cap .fitx-tag{color:rgba(255,255,255,.85)}
  .is-anim .fitx-count{display:none}
}
html.a11y-no-motion .fitx-scene.is-anim{height:auto}
html.a11y-no-motion .is-anim .fitx-stick{position:static;height:auto;overflow:visible;display:block;padding:clamp(64px,9vh,96px) 0}
html.a11y-no-motion .is-anim .fitx-grid{grid-template-columns:1fr}
html.a11y-no-motion .is-anim .fitx-col1{display:none}
html.a11y-no-motion .is-anim .fitx-view{overflow:visible;-webkit-mask-image:none;mask-image:none;height:auto !important}
html.a11y-no-motion .is-anim .fitx-lines{transform:none !important}
html.a11y-no-motion .is-anim .fitx-line{padding:14px 0;border-bottom:1px solid rgba(255,255,255,.12);height:auto !important;color:#fff}
html.a11y-no-motion .is-anim .fitx-lines .fitx-line:last-child{border-bottom:0}
html.a11y-no-motion .is-anim .fitx-line .pw{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
html.a11y-no-motion .is-anim .fitx-pred,
html.a11y-no-motion .is-anim .fitx-cap{width:auto;white-space:normal;background-image:none;color:inherit}
html.a11y-no-motion .is-anim .fitx-cap{color:rgba(255,255,255,.62)}
html.a11y-no-motion .is-anim .fitx-cap .fitx-tag{color:rgba(255,255,255,.85)}
html.a11y-no-motion .is-anim .fitx-count{display:none}
/* mobile scroll reveal — the pinned scrub is desktop-only (≥721px); on narrow widths the
   static ledger lines rise + fade in as they enter view (.m-in added by tpl.js). .m-in is
   inert on desktop, since every reveal rule lives inside these max-width blocks. */
@media(max-width:720px){
  .fitx-lines .fitx-line{opacity:0;transform:translateY(22px);transition:opacity .55s var(--ease),transform .55s var(--ease),color .4s ease}
  .fitx-lines .fitx-line.m-in{opacity:1;transform:none}
  .fitx-lines .fitx-line:nth-child(2){transition-delay:60ms}
  .fitx-lines .fitx-line:nth-child(3){transition-delay:120ms}
  .fitx-lines .fitx-line:nth-child(4){transition-delay:180ms}
  /* Nothing here overrides .is-anim any more: tpl.js gates it on width, so it cannot be set
     below 721px, and the phone-scrub tuning that used to live here (font clamp, 1.45 leading,
     caption min-height) went with body.fitx-m. The .m-in reveal above is the whole phone path. */
}
@media(max-width:720px) and (prefers-reduced-motion:reduce){.fitx-lines .fitx-line{opacity:1;transform:none;transition:none}}
html.a11y-no-motion .fitx-lines .fitx-line{opacity:1;transform:none;transition:none}
/* no-JS safety net: .m-in comes from tpl.js and .is-anim (the fitx-m mobile-scrub pilot's
   opacity override) does too, so with scripting off all 4 Fit lines would stay at opacity:0
   on a phone. Engines without the `scripting` feature drop this query as invalid — no-op. */
@media(max-width:720px) and (scripting:none){.fitx-lines .fitx-line{opacity:1;transform:none;transition:none}}
/* fitx-band--glow (cross-docking + OaaS) — full-bleed red-ribbon backdrop on the Fit band. The pseudo stacks three
   layers: a left→right scrim over the ribbon photo over a --inverse base. The image is cover-fit
   (flipped so its bright corner lands top-right, diagonally opposite the headline); the scrim
   keeps the whole left-hand text column on near-black so the white/red type stays legible while
   the ribbon's energy reads on the right. Self-contained on the pseudo, so the sticky stack's
   isolation can't break it. Pseudo is contained by the sticky stick while the scrub is active
   (clipped to 100vh by its overflow:hidden) and by .fitx-scene in the static / reduced-motion
   fallback — the backdrop fills the section either way, no position:sticky surgery. */
.fitx-band--glow .fitx-stick::after{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:
    linear-gradient(100deg,var(--inverse) 0%,rgba(6,6,8,.82) 24%,rgba(6,6,8,.44) 52%,rgba(6,6,8,.12) 78%,rgba(6,6,8,0) 100%),
    url(https://img.tpl.one/websites/tplgr/v2/assets/images/sections/xd-fit-model.webp) center/cover no-repeat,var(--inverse);
  background-blend-mode:normal,screen}
.fitx-band--glow .fitx-stick>.wrap{position:relative;z-index:1}
/* Top spacing lives on the STICK, not the section, so the red-ribbon ::after (which fills the
   stick) paints behind it. The section:has(.kpi-band)+section rule (padding-top:64px) put the
   gap on the section instead — outside the ::after — so it showed as a black strip above the
   ribbon. Zero the section here and let the stick keep its base clamp() top padding (ribbon-lit). */
section.fitx-band.fitx-band--glow{padding-top:0}
/* D2C fit band — red-ribbon photo backdrop (backgrounds/fit-band-ribbon.jpg). Scoped modifier so the
   sibling model pages' plain inverse bands stay flat. Background rides the pinned
   .fitx-stick (matching --glow) so it holds through the scroll scrub. A left-weighted
   dark scrim keeps the white header + ledger readable over the brighter ribbon that
   climbs toward the lower-right; text literals here are already theme-invariant white. */
section.fitx-band.fitx-band--photo{padding-top:0}
.fitx-band--photo .fitx-stick::after{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:
    linear-gradient(100deg,rgba(6,6,8,.95) 0%,rgba(6,6,8,.8) 32%,rgba(6,6,8,.5) 62%,rgba(6,6,8,.28) 100%),
    url(https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/fit-band-ribbon.jpg) center/cover no-repeat,var(--inverse)}
.fitx-band--photo .fitx-stick>.wrap{position:relative;z-index:1}
/* Pinned scrub stays vertically centered like the other model pages' Fit bands — the earlier
   flex-start override existed only to hide dark dead space above the content, but the ribbon
   backdrop now fills that space, so centering matches the siblings with no black gap. */
/* tighten the panel's inner column gap (keep-left, less spread) */
.hiw-in{gap:0 clamp(20px,2.4vw,40px)} /* row-gap 0 — the kick's 12px margin owns the kick→title space */

/* ==========================================================================
   AI-TRANSFORMATION — "Neural glow" theme.  SCOPED to body.ai-neural ONLY.
   A deliberate, user-sanctioned deviation from the SHARP no-gradient/no-glass
   canon — it applies to nothing else on tpl.gr. Red/ember-led glow on near-
   black, glass panels, drifting neural particle mesh. Fully motion-guarded.
   ========================================================================== */
body.ai-neural{
  /* LIGHT (default). The page loads dark via the pre-paint script, but light is a full variant. */
  --ain-bg:#F4F3EF; --ain-panel:#EDEBE5;
  --ain-ink:#17171B; --ain-soft:#4C4C55; --ain-muted:#78777F;
  --ain-line:rgba(0,0,0,.12); --ain-line-2:rgba(0,0,0,.22);
  --ain-red:#E63B3B; --ain-red-deep:#AB0000; --ain-ember:#FF6A45;
  --ain-kick:#AB0000; --ain-grid:rgba(0,0,0,.05);
  --ain-node:rgba(0,0,0,.5); --ain-wire:rgba(171,0,0,.42);
  --ain-glass:rgba(255,255,255,.72); --ain-glass-2:rgba(255,255,255,.82);
  color:var(--ain-ink);
  background-color:var(--ain-bg);
  background-image:
    radial-gradient(58% 42% at 16% 2%, rgba(230,59,59,.10), transparent 68%),
    radial-gradient(52% 42% at 100% 18%, rgba(200,30,30,.07), transparent 72%),
    radial-gradient(80% 55% at 50% 112%, rgba(171,0,0,.06), transparent 72%);
  background-repeat:no-repeat;
}
html[data-theme="dark"] body.ai-neural{
  --ain-bg:#08080B; --ain-panel:#0E0E14;
  --ain-ink:#F5F4F1; --ain-soft:#C7C5CF; --ain-muted:#8A8994;
  --ain-line:rgba(255,255,255,.10); --ain-line-2:rgba(255,255,255,.18);
  --ain-kick:#ef7676; --ain-grid:rgba(255,255,255,.045);
  --ain-node:rgba(255,255,255,.6); --ain-wire:rgba(255,120,90,.5);
  --ain-glass:rgba(255,255,255,.06); --ain-glass-2:rgba(255,255,255,.11);
  background-image:
    radial-gradient(58% 42% at 16% 2%, rgba(230,59,59,.15), transparent 68%),
    radial-gradient(52% 42% at 100% 18%, rgba(200,30,30,.12), transparent 72%),
    radial-gradient(80% 55% at 50% 112%, rgba(171,0,0,.11), transparent 72%);
}
body.ai-neural section{position:relative;z-index:1;border-bottom:1px solid var(--ain-line)}
body.ai-neural .two-tone{color:var(--ain-ink)}
body.ai-neural .two-tone span:not(.word){color:var(--red-ink)}
body.ai-neural .eyebrow,body.ai-neural .kicker{color:var(--ain-kick)}
body.ai-neural .lede{color:var(--ain-soft)}
body.ai-neural h3{color:var(--ain-ink)}
body.ai-neural p{color:var(--ain-soft)}

/* glass cards ------------------------------------------------------------ */
body.ai-neural .card{
  background:var(--ain-glass);border:1px solid var(--ain-line);border-radius:16px;
  -webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);
  box-shadow:0 8px 22px -16px rgba(0,0,0,.35);
  transition:border-color .3s var(--ease),transform .3s var(--ease),box-shadow .3s var(--ease);
}
html[data-theme="dark"] body.ai-neural .card{box-shadow:0 14px 34px -24px rgba(0,0,0,.7)}
/* .reveal is applied directly ON these cards (not as a wrapper), so cancel the shared
   `.grid-* > .reveal{display:flex}` rule that would otherwise row-split the card content */
body.ai-neural .grid-3>.card.reveal,body.ai-neural .grid-4>.card.reveal{display:block}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  body.ai-neural .card{background:rgba(20,20,26,.86)}
}
@media(hover:hover) and (pointer:fine){
body.ai-neural .card:hover{
  border-color:var(--ain-line-2);transform:translateY(-3px);
  box-shadow:0 12px 28px -18px rgba(0,0,0,.4),0 0 16px -12px rgba(230,59,59,.32);
}
}
/* Footer is the sitewide red band on every page — cancel the page-wide `p{color:--ain-soft}`
   leak below (it flips footer copy to near-black on the red ground; #1 in the 2026-07-23 fix). */
body.ai-neural footer p{color:rgba(255,255,255,.85)}
/* The Fit band is a permanently-dark band in BOTH themes; keep its heading white regardless of
   the ai-neural two-tone token (which flips to near-black --ain-ink in the light variant). */
body.ai-neural .fitx-head h2.two-tone{color:#fff}
body.ai-neural .fitx-head h2.two-tone span:not(.word){color:var(--red-ink-inverse)}
/* How-it-works panels are the red family in BOTH themes — keep their headings white regardless of
   the page-wide `h3{color:--ain-ink}` (which flips near-black in the light variant, over the red). */
body.ai-neural .hiw-panel h3{color:#fff}
/* Fit-band eyebrow: same permanently-dark band as the heading above — the page-wide
   `.eyebrow{color:--ain-kick}` leak (2.6:1 as #AB0000 on near-black in the light variant)
   out-specifies the shared `.fitx-head .eyebrow`, so re-assert the theme-invariant inverse red. */
body.ai-neural .fitx-head .eyebrow{color:var(--red-ink-inverse)}
/* How-it-works ghost step-numbers: .6 white fails AA on the two brightest red panels (p1 #AB0000
   3.3:1, p2 #920000 4.1:1). Mirror the #journey precedent — bump to .82 (~5.4:1) on those two. */
body.ai-neural #how .hiw-p1 .wwd-n,body.ai-neural #how .hiw-p2 .wwd-n{color:rgba(255,255,255,.82)}
/* Guarantees closing line sits on --ain-bg (not a card); the page-wide `p{color:--ain-soft}` leak
   drops it to 3.98:1 in the light variant. Restore the shared component's documented --ink-muted. */
body.ai-neural .gtee-sign{color:var(--ink-muted)}
/* Guarantee cards keep the shared component's documented ink tokens (computed against the card's
   global --paper-0 surface), not the page ain-* tokens (tuned only against --ain-bg/--ain-panel). */
body.ai-neural .gtee-card h3{color:var(--ink)}
body.ai-neural .gtee-card p{color:var(--ink-soft)}

/* buttons ---------------------------------------------------------------- */
/* --ain-red (#E63B3B) as a button fill measured 4.16:1 with white — 0.34 short of AA, on the
   page's primary conversion CTA in both the hero and the closing band (audit H1, 2026-07-30).
   The fill drops to --ain-red-deep (#AB0000, 7.69:1, and already this page's own token for the
   brand red); the ember identity is carried by the glow, which is unchanged. --ain-red stays
   the accent colour everywhere it is decoration (.ain-dot, node rings) rather than a text
   ground. Hover brightens to --red-hover (#B50000, 7.0:1) instead of #ff4a45 (3.2:1), so the
   label passes in the hover state too. */
body.ai-neural .btn-primary{
  background:var(--ain-red-deep);border-color:var(--ain-red-deep);color:#fff;
  box-shadow:0 6px 18px -8px rgba(230,59,59,.42);
}
body.ai-neural .btn-primary:hover{background:var(--red-hover);box-shadow:0 8px 22px -8px rgba(255,90,70,.5)}
body.ai-neural .btn-ghost{
  background:var(--ain-glass-2);color:var(--ain-ink);border:1px solid var(--ain-line-2);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
}
body.ai-neural .btn-ghost:hover{border-color:rgba(255,120,90,.6);color:var(--ain-ink)}
body.ai-neural .biglink{color:var(--ain-kick)}
body.ai-neural .hairline-list li{border-color:var(--ain-line)}
body.ai-neural .hairline-list .num{color:var(--ain-kick)}
body.ai-neural .hairline-list span{color:var(--ain-soft)!important}

/* HERO ------------------------------------------------------------------- */
.ai-hero::before{display:none} /* kill the shared light-noise hero overlay on the dark neural hero */
body.ai-neural .crumb-bar{background:transparent;border-bottom:1px solid var(--ain-line)}
body.ai-neural .crumb a{color:var(--ain-muted)}
body.ai-neural .crumb a:hover{color:var(--ain-ink)}
body.ai-neural .crumb span[aria-hidden]{color:var(--ain-muted)}
body.ai-neural .crumb [aria-current]{color:var(--ain-kick)}
.ai-hero{
  position:relative;overflow:hidden;isolation:isolate;
  min-height:clamp(560px,90vh,940px);display:flex;align-items:center;
  padding:clamp(96px,13vh,170px) 0 clamp(64px,9vh,120px);
  background:
    radial-gradient(52% 60% at 78% 42%, rgba(230,59,59,.13), transparent 66%),
    radial-gradient(46% 55% at 12% 74%, rgba(190,30,40,.08), transparent 70%),
    #EFEDE8;
}
html[data-theme="dark"] .ai-hero{
  background:
    radial-gradient(52% 60% at 78% 42%, rgba(230,59,59,.28), transparent 66%),
    radial-gradient(46% 55% at 12% 74%, rgba(190,30,40,.20), transparent 70%),
    #06060A;
}
.ai-hero .ain-mesh{position:absolute;inset:0;width:100%;height:100%;z-index:0;opacity:.9}
.ai-hero .ain-hero-grid{position:absolute;inset:0;z-index:0;pointer-events:none;
  background-image:linear-gradient(var(--ain-grid) 1px,transparent 1px),linear-gradient(90deg,var(--ain-grid) 1px,transparent 1px);
  background-size:64px 64px;-webkit-mask-image:radial-gradient(70% 70% at 50% 45%,#000,transparent 78%);mask-image:radial-gradient(70% 70% at 50% 45%,#000,transparent 78%)}
.ai-hero .hero-content{position:relative;z-index:2;max-width:52rem}
.ai-hero h1{color:var(--ain-ink)}
html[data-theme="dark"] .ai-hero h1{text-shadow:0 2px 40px rgba(0,0,0,.5)}
.ai-hero .lede{color:var(--ain-soft)}
.ai-hero .eyebrow{color:var(--ain-kick)}
.ai-hero .ain-livechip{
  position:absolute;right:clamp(20px,4vw,64px);bottom:clamp(28px,6vh,64px);z-index:2;
  display:flex;align-items:center;gap:12px;padding:12px 16px;border-radius:12px;
  background:var(--ain-glass-2);border:1px solid var(--ain-line-2);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  font-family:var(--font-mono);font-size:12px;color:var(--ain-soft)}
.ai-hero .ain-livechip b{color:var(--ain-ink);font-size:20px;font-weight:700}
.ain-dot{width:9px;height:9px;border-radius:50%;background:var(--ain-red);box-shadow:0 0 0 0 rgba(230,59,59,.6);animation:ain-pulse 2.4s ease-out infinite}
@keyframes ain-pulse{0%{box-shadow:0 0 0 0 rgba(230,59,59,.55)}70%{box-shadow:0 0 0 12px rgba(230,59,59,0)}100%{box-shadow:0 0 0 0 rgba(230,59,59,0)}}
@media(max-width:860px){.ai-hero .ain-livechip{display:none}}
@media(prefers-reduced-motion:reduce){.ain-dot{animation:none}}
html.a11y-no-motion .ain-dot{animation:none}

/* KPI "Proven" band uses the SHARED sitewide style on this page too (theme-aware --paper/--ink),
   so it reads identically across all model pages — no bespoke neural override (2026-07-23 fix). */

/* CTA band → dark glass panel, red-glow backdrop instead of the light chrome image */
body.ai-neural .cta-band>.wrap{background:var(--ain-glass);border:1px solid var(--ain-line)}
body.ai-neural .fit-curtain{background:var(--ain-bg)}
body.ai-neural .cta-bg::before{display:none}
body.ai-neural .cta-bg::after{background:radial-gradient(ellipse 72% 66% at 50% 38%,rgba(230,59,59,.18),transparent 72%)}
body.ai-neural .cta-ledger{border-color:var(--ain-line)}
body.ai-neural .cta-ledger li:not(:first-child){border-left-color:var(--ain-line)}
body.ai-neural .cta-ledger .num{color:var(--ain-kick)}
body.ai-neural .cta-ledger p{color:var(--ain-soft)}

/* Stack — neural-core node graph (data converges into the red hub) */
.ain-core{max-width:520px;margin:0 auto 40px}
.ain-core svg{width:100%;height:auto;overflow:visible;display:block}
.ain-core .ain-lnks line{stroke:var(--ain-wire);stroke-width:1.4;stroke-dasharray:5 7;animation:ain-flow 2.6s linear infinite}
.ain-core .ain-sat circle{fill:var(--ain-node)}
.ain-core .ain-hub{fill:var(--ain-red);filter:drop-shadow(0 0 5px rgba(230,59,59,.6))}
.ain-core .ain-hub-halo{fill:rgba(230,59,59,.16);transform-box:fill-box;transform-origin:center;animation:ain-breathe 3.2s ease-in-out infinite}
@keyframes ain-flow{to{stroke-dashoffset:-48}}
@keyframes ain-breathe{0%,100%{transform:scale(1);opacity:.5}50%{transform:scale(1.25);opacity:.85}}
/* Reveal on scroll — the graph assembles: satellite nodes pop in staggered, the wires draw
   through, the red hub lands last (.ain-core carries `reveal`, so the shared IO adds is-visible).
   The continuous ain-flow / ain-breathe loops keep running underneath once revealed. */
.ain-core .ain-sat circle,.ain-core .ain-hub{transform:scale(.55);transform-box:fill-box;transform-origin:center;opacity:0;transition:transform .55s var(--ease),opacity .55s var(--ease)}
.ain-core .ain-lnks{opacity:0;transition:opacity .6s var(--ease) .34s}
.ain-core.is-visible .ain-sat circle,.ain-core.is-visible .ain-hub{transform:scale(1);opacity:1}
.ain-core.is-visible .ain-lnks{opacity:1}
.ain-core .ain-sat circle:nth-of-type(1){transition-delay:.06s}
.ain-core .ain-sat circle:nth-of-type(2){transition-delay:.12s}
.ain-core .ain-sat circle:nth-of-type(3){transition-delay:.18s}
.ain-core .ain-sat circle:nth-of-type(4){transition-delay:.24s}
.ain-core .ain-sat circle:nth-of-type(5){transition-delay:.30s}
.ain-core .ain-sat circle:nth-of-type(6){transition-delay:.36s}
.ain-core.is-visible .ain-hub{transition-delay:.48s}

/* (The old .ain-rail pipeline + .ain-steps console cards were retired 2026-07-23 when
   "How it works" adopted the shared .hiw scrubbed-panel component used by the sibling pages.) */

/* motion guards for the graph — also force the reveal end-state so nodes never stay hidden */
@media(prefers-reduced-motion:reduce){.ain-core .ain-lnks line,.ain-core .ain-hub-halo{animation:none}
  .ain-core .ain-sat circle,.ain-core .ain-hub,.ain-core .ain-lnks{transform:none;opacity:1;transition:none}}
html.a11y-no-motion .ain-core .ain-lnks line,html.a11y-no-motion .ain-core .ain-hub-halo{animation:none}
html.a11y-no-motion .ain-core .ain-sat circle,html.a11y-no-motion .ain-core .ain-hub,html.a11y-no-motion .ain-core .ain-lnks{transform:none;opacity:1;transition:none}

/* ————— History route (history.html) — .hx ——————————————————————————
   DEFAULT = vertical ledger (no-JS / reduced-motion / ≤720px all get this).
   tpl.js adds .hx--go on desktop when motion is allowed → pinned horizontal
   scrub (GSAP ScrollTrigger): the page pins and vertical scroll drives the
   track right-to-left. Supersedes the old about.html .rt route (deleted). */
.hx{padding:0;position:relative;z-index:0}
/* the footer curtains up over the pinned timeline as it finishes — same technique as the
   D2C .fit-curtain: an opaque higher layer, pulled up, overlays the pinned scene during the
   handoff instead of the footer simply appearing below it. Only when the scrub is active
   (≥721px per the breakpoint contract at the top of this file, plus motion on) — otherwise
   normal flow. The width here MUST match tpl.js's
   `mqNarrow` for .hx--go: the curtain's -100vh pull only makes sense over a pinned scene,
   so a curtain live where the scrub is not would eat a viewport of the page.
   min-height:100vh is load-bearing (see the .fit-curtain note above the same trick):
   without it the pinned scene's paper background can show through as a gap above the
   footer on tall viewports. */
body.hx-page footer{position:relative;z-index:2}
@media(min-width:721px) and (prefers-reduced-motion:no-preference){html:not(.a11y-no-motion) body.hx-page footer{margin-top:-100vh;min-height:100vh}}
/* red lead-in — a gradient cap living OUTSIDE the footer's own box (bottom:100% sits it
   directly above the footer, not inside), same var(--red) as the footer itself. It's a
   plain part of the footer's own (position:relative) box, so it isn't scroll/JS-driven at
   all — it moves as one rigid piece WITH the footer as the curtain rises, arriving a beat
   ahead of the solid colour instead of the footer's hard edge cutting in.

   GATED TO THE CURTAIN, added 2026-08-05, owner: "on mobile it's completely broken — the
   gradient from the footer doesn't work." The cap used to be declared unconditionally while
   the curtain above it is gated to (min-width:721px + motion). Wherever the curtain is OFF
   there is no pinned scene under the cap and no -100vh pull, so the 28vh of transparent→red
   landed instead on the .share-band — the share strip tpl.js builds as the footer's previous
   sibling on EVERY page (~1645). Measured (CDP, exact CSS px, file at HEAD):
     390×844  cap 236.3px vs a 197.2px share band -> 100% covered, red alpha 0.17→1.00;
              "Copy link" sat under alpha 0.73→0.92, the 44px icon buttons under 0.45→0.64
     1440×900 + prefers-reduced-motion:reduce -> also 100% covered, and WORSE: the
              SHARE THIS PAGE label under alpha 0.80→0.88
     1440×900 + motion -> cap lands on the pinned scene, share band 0% covered (correct)
   So this was never mobile-only: it broke at <721px, under prefers-reduced-motion at any
   width, and under the site's own .a11y-no-motion pause. The footer (z-index:2) outranks
   .share-band (position:relative, z-index:auto), so the wash always won.

   Not "fixed" by retuning the stops or shortening the cap: the share band is an opaque paper
   surface with its own hairline borders and 44px controls, so ANY red fading across it costs
   legibility, and moving the cap above the band would read red → paper → red. The cap only
   ever meant something as the leading edge of the rising curtain, so it now lives and dies
   with it — one gate, same terms as the margin-top rule above. Where the curtain is off the
   footer keeps its own hard red edge, exactly like every other page on the site. */
@media(min-width:721px) and (prefers-reduced-motion:no-preference){
  html:not(.a11y-no-motion) body.hx-page footer::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:28vh;background:linear-gradient(180deg,transparent 0%,var(--red) 96%);pointer-events:none}
}
/* Noise-grain backdrop behind the timeline, in the site's own grain language rather than a
   photograph. Two photo candidates were tried and rejected by the owner on 2026-08-19: the DNA
   helix first (it is DNA Filters' motif, so TPL's story ran behind a partner's branding), then
   story-meeting.webp. A photograph was the wrong class of surface for this band — the timeline's
   own cards are the subject, and a scene behind them competes with them.
   This reuses .band-noise's exact recipe (tpl.css, search "noise-textured band"): the same
   noise-bg.jpg / noisy-dark.webp pair, the same 75% wash and multiply blend, so the band matches
   the one on branches.html instead of inventing a third treatment. No new asset; noise-bg.jpg is
   116 KB against story-meeting.webp's 44 KB, but it is already fetched by any visitor who has
   seen branches.html and it tiles far better under text than a scene does.
   UNLIKE the photo versions this has a real dark treatment rather than falling back to flat
   paper, because the grain reads in both themes. story-meeting.webp and dna-image.webp both
   stay on disk, unreferenced, so either rollback is one line. */
.hx-scene{position:relative;background:linear-gradient(rgba(240,240,240,.75),rgba(240,240,240,.75)),#f0f0f0 url(https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noise-bg.jpg) center/cover no-repeat;background-blend-mode:normal,multiply}
html[data-theme="dark"] .hx-scene{background:linear-gradient(rgba(13,13,14,.75),rgba(13,13,14,.75)),#0d0d0e url(https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp) center/cover no-repeat;background-blend-mode:normal,multiply}
.hx-head{padding:clamp(40px,6vw,64px) clamp(20px,4vw,48px) 6px;max-width:var(--max);margin:0 auto}
.hx-head .eyebrow{margin-bottom:8px}
.hx-h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.02em;line-height:1.05;color:var(--ink);font-size:clamp(30px,5vw,52px)}
.hx-cue{display:none}
.hx-nav{display:none}
/* vertical ledger (default) — NOT phone-only: .hx--go needs ≥721px AND motion AND GSAP, so
   the ledger is also what every reduced-motion / a11y-pause / no-GSAP visitor gets at any
   width. Its static state therefore has to read as a timeline on its own, before any JS.
   Two variables carry the whole geometry, and dot, cube, leg and stem all derive from them:
     --hx-rail  the axis, from the stop's own left edge (so it tracks the track's clamp()
                padding automatically and stays width-independent)
     --hx-node  the node centre, down from the stop's top — set to the optical centre of the
                year line, so the marker points at the date it marks rather than at the
                card's top corner.
   Everything below that touches .hx-seg / .hx-dot / .hx-cube / .hx-card::before is scoped
   :not(.hx--go): the pinned scrub owns those same four with completely different geometry
   (absolute cards, a horizontal axis, stems off the card's near edge) and the two must not
   bleed into each other. .hx--go lives on the .hx section — tpl.js: sec = scene.closest('.hx'). */
.hx-track{--hx-rail:9px;--hx-node:28px;list-style:none;margin:0 auto;padding:56px clamp(20px,4vw,48px) 72px;position:relative;max-width:var(--max)}
.hx-stop{position:relative;padding:0 0 32px 44px;max-width:620px}
/* the rail is per-leg, never one full-height line: each stop draws its own leg from its own
   node down to the NEXT stop's node with .hx-seg — the element the pinned scrub already uses,
   and which has sat inert in the ledger since this page shipped. `bottom` is pulled a whole
   --hx-node PAST the stop's own box, which lands exactly on the next node. Three things fall
   out of that. The route starts on 2006's node and ends on 2027+'s, instead of the old
   full-height rail floating 12px above the first and ~100px below the last. Legs paint in one
   at a time as stops reveal, so scrolling reads as travel rather than as cards appearing. And
   each leg can carry its own meaning, which the past/future split below needs. The scrub works
   the same way — it has no full-length rail either, only segs. */
.hx:not(.hx--go) .hx-seg{position:absolute;left:calc(var(--hx-rail) - 1px);width:2px;top:var(--hx-node);bottom:calc(var(--hx-node) * -1);background:var(--red);z-index:0}
.hx:not(.hx--go) .hx-stop:last-child .hx-seg{display:none}
/* ---- REMOVED 2026-08-05, owner: "the gradient on mobile doesn't work — make it work or remove
   it." The rail is now two solid colours and nothing else. Do not reintroduce a gradient or a
   scroll-driven fill here without a real-device test in hand.

   What was here, and why it is gone. The leg was a grey TRACK carrying a
   repeating-linear-gradient of dashes from Today onward, with a separate red fill layer on top
   whose length followed the scroll — first written per-frame from JS, then (same day) moved onto
   a view-progress timeline to stop it stepping during touch-momentum scrolling. Both versions
   measured perfect in headless Chrome: the fill tracked a 55% reading line to within 0.03% over
   48 samples, legs met their nodes at 0.0px, red dashes landed in phase over grey ones. It still
   did not work on the owner's phone, and none of the three symptoms I could reproduce explained
   it — so the honest reading is that the mechanism, not its tuning, was the problem, and a
   mechanism that cannot be verified on the device it targets does not belong on that device.

   THE RULE THIS ENCODES: mobile matches desktop. The desktop scrub's rail has no gradient at all
   (`.hx--go .hx-seg{background:var(--hairline)}`, going solid red on .is-passed), which is what
   "not working like desktop" meant. Colour alone now carries the whole reading — red for the
   route already travelled, grey for what is still ahead — so the rail states the same thing in
   both modes with no texture channel and no moving parts.

   What this deliberately gives up: the red no longer tracks the reader down the page, and the
   projected legs are no longer distinguishable from the travelled ones by texture. Today is
   still marked by its cube, its red card border and the grey rail below it. Nothing here needs
   JS, so the rail cannot break on any device or in any motion mode. */
.hx:not(.hx--go) :is(.hx-stop--now,.hx-stop--now ~ .hx-stop) .hx-seg{background:var(--paper-500)}
.hx-dot{position:absolute;left:0;top:3px;width:18px;height:18px;border-radius:50%;background:var(--paper-0);border:2px solid var(--red);z-index:1}
/* red for the route already travelled, --paper-500 once past Today — the ledger's equivalent of
   the scrub's idle/.is-passed pair, and the same grey as the legs ahead so the projected part of
   the route reads as one material. Both rules outrank the @supports masked defaults below on
   specificity, so source order here doesn't matter.
   The :not(.hx--follow) qualifier and the two .hx--follow rules that used to sit below were
   dropped with the scroll-driven fill (see the .hx-seg note above): there is only one mode now,
   so the nodes are coloured by chronology unconditionally and never wait on a class from JS.
   border:0 on the projected dots is not decoration: .hx-stop--future .hx-dot sets
   `border:2px dashed` at (0,2,0), which outranks the masked block's own `border:0` at (0,1,0)
   and survives into masked mode — where a border is meaningless anyway, since the mask doesn't
   clip it. Left alone it renders as a hollow dashed square floating around the shape.
   --paper-300 was tried first and is invisible on --paper-100: #E8E6E0 on #F2F1ED. */
.hx:not(.hx--go) .hx-dot{top:calc(var(--hx-node) - 9px)}
.hx:not(.hx--go) .hx-stop--future .hx-dot{border:0}
.hx:not(.hx--go) .hx-dot{background:var(--red)}
.hx:not(.hx--go) .hx-stop--now ~ .hx-stop .hx-dot{background:var(--paper-500)}
.hx-stop--future .hx-dot{border:2px dashed var(--paper-500);background:var(--paper-100)}
.hx-stop--now .hx-dot{display:none}
/* each bullet takes one of the 4 TPL brand shapes (assets/shapes/), cycling by position —
   CSS mask so colour still tracks state (idle/future/active) the way the plain dot did.
   Border makes no sense on an irregular masked shape (it isn't clipped by the mask), so
   masked mode drops it and uses filter:drop-shadow for the active glow instead, which
   hugs the shape outline rather than the dot's rectangular box. @supports keeps the
   plain circle above as the fallback where mask isn't supported. */
@supports ((mask-size:100% 100%) or (-webkit-mask-size:100% 100%)){
  .hx-dot{border:0;border-radius:0;background:var(--paper-500);
    -webkit-mask-image:url(../shapes/shape-1.svg);mask-image:url(../shapes/shape-1.svg);
    -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
    -webkit-mask-position:center;mask-position:center;
    -webkit-mask-size:100% 100%;mask-size:100% 100%}
  .hx-stop--future .hx-dot{background:var(--paper-300)}
  .hx-stop:nth-child(4n+2) .hx-dot{-webkit-mask-image:url(../shapes/shape-2.svg);mask-image:url(../shapes/shape-2.svg)}
  .hx-stop:nth-child(4n+3) .hx-dot{-webkit-mask-image:url(../shapes/shape-3.svg);mask-image:url(../shapes/shape-3.svg)}
  .hx-stop:nth-child(4n) .hx-dot{-webkit-mask-image:url(../shapes/shape-5.svg);mask-image:url(../shapes/shape-5.svg)}
}
.hx-cube{position:absolute;left:-6px;top:-1px;width:27px;height:27px;z-index:2;filter:drop-shadow(0 3px 6px rgba(0,0,0,.18))}
.hx-cube svg{display:block;width:100%;height:100%}
.hx-card{background:var(--paper-50);border:1px solid var(--hairline);border-radius:var(--radius-card);padding:20px 22px}
.hx-y{display:block;font-family:var(--font-mono);font-size:14px;font-weight:500;letter-spacing:.06em;color:var(--ink)}
/* .hx-k is an h2 (2026-09-08): each stop on the rail IS a section of the story, and the page
   had no headings at all — a flat outline for screen readers and crawlers alike. Safe to
   promote because this rule already sets every property a heading would otherwise inherit
   differently: display, family, size, weight, spacing, case, colour and margin. Change one
   of those away and the year rail will start moving. */
.hx-k{display:block;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);margin:3px 0 8px}
.hx-k--award{color:var(--red-ink)}
.hx-k--us{color:var(--americas-ink)}
.hx-k--in{color:var(--india-ink)}
.hx-card p{margin:0;font-size:14.5px;line-height:1.55;color:var(--ink-soft)}
.hx-fig{margin-top:14px;display:flex;align-items:center;gap:14px}
.hx-fig img{height:34px;width:auto}
.hx-fig img.hx-photo{height:64px;border-radius:6px;border:1px solid var(--hairline)}
.hx-stats{display:flex;flex-wrap:wrap;gap:6px 18px;margin-top:12px;font-family:var(--font-mono);font-size:12px;letter-spacing:.04em;color:var(--ink)}
.hx-stop--future .hx-card{opacity:.62}
/* the stem — a hairline branch from the node out to the card's own edge. This is what turns a
   dashed line sitting BESIDE a card stack into cards hanging OFF an axis. 28px reaches ~1px
   back into the dot so the two always touch; the -1.5px on top is the card's own 1px border
   (an absolute child's containing block is the padding box, not the border box) plus half the
   1px line. */
.hx:not(.hx--go) .hx-card{position:relative}
.hx:not(.hx--go) .hx-card::before{content:"";position:absolute;right:100%;top:calc(var(--hx-node) - 1.5px);width:28px;height:1px;background:var(--paper-500)}
/* Today is the page's anchor: extra rail before it so it lands as an arrival, a red hairline on
   its card, and a bigger cube. The extra room is padding-bottom on the PRECEDING stop, not
   margin-top on Today — the preceding leg's `bottom` is measured from that stop's own box, so
   padding stretches the leg with it while margin would leave the leg short of the next node.
   Deliberately no --red-wash background here: that token REPLACES --paper-50 rather than
   tinting it, which would let the DNA backdrop through the one card that most needs to read
   as solid. */
.hx:not(.hx--go) .hx-stop:has(+ .hx-stop--now){padding-bottom:64px}
.hx:not(.hx--go) .hx-stop--now .hx-card{border-color:var(--red)}
.hx:not(.hx--go) .hx-cube{left:calc(var(--hx-rail) - 16px);top:calc(var(--hx-node) - 16px);width:32px;height:32px}
/* mobile scroll reveal — the pinned horizontal scrub runs from 721px up (breakpoint contract);
   at ≤720px the ledger's stops draw themselves in as they enter view (.m-in via tpl.js). Same
   width as tpl.js's `mqNarrow`, inverted.
   The parent carries OPACITY ONLY and the children carry TRANSFORM ONLY. That split is
   load-bearing twice over: it's why .hx-stop--future keeps its own .62 card dim (the reason the
   original code put the whole reveal on the parent), and it's why nothing here ever animates two
   properties against each other. The node grows out of the rail and the card follows its own stem
   outward, so a stop assembles from its axis rather than fading up as a block.
   The leg itself is NOT revealed here — it is simply present, in its own colour, from first paint.
   That was true when the red on it followed the scroll and stays true now that the fill is gone
   (see the .hx-seg note above): the rail is never animated, only the stop's arrival is.
   The hidden states stay inside this ≤720px query on purpose — the ledger at ≥721px
   (reduced motion, no GSAP) then renders complete and unanimated, which is the safest
   degradation available and is unchanged from before. */
@media(max-width:720px){
  .hx-track .hx-stop{opacity:0;transition:opacity .3s var(--ease-out)}
  .hx-track .hx-stop.m-in{opacity:1}
  .hx:not(.hx--go) .hx-stop :is(.hx-dot,.hx-cube){transform:scale(.62);transition:transform .26s var(--ease-out)}
  .hx:not(.hx--go) .hx-stop .hx-card{transform:translate3d(-8px,8px,0);transition:transform .36s var(--ease-out) .08s}
  .hx:not(.hx--go) .hx-stop.m-in :is(.hx-dot,.hx-cube,.hx-card){transform:none}
}
/* the three snap-in guards. Each is prefixed .hx:not(.hx--go) to reach (0,5,0): the child rules
   above are (0,4,0) and would otherwise win, leaving stops permanently offset for exactly the
   visitors who most need them settled. The legs need no guard at all any more: the rail is two
   static colours with nothing animating on it, so all three cases already render it complete. */
@media(max-width:720px) and (prefers-reduced-motion:reduce){
  .hx-track .hx-stop{opacity:1;transform:none;transition:none}
  .hx:not(.hx--go) .hx-track .hx-stop :is(.hx-dot,.hx-cube,.hx-card){transform:none;transition:none}
}
html.a11y-no-motion .hx-track .hx-stop{opacity:1;transform:none;transition:none}
html.a11y-no-motion .hx:not(.hx--go) .hx-track .hx-stop :is(.hx-dot,.hx-cube,.hx-card){transform:none;transition:none}
/* no-JS safety net: .m-in comes from tpl.js, so with scripting off every stop would sit at
   opacity:0 and the whole ledger — the page's only content — would be invisible on a phone.
   Engines without the `scripting` feature drop this query as invalid, i.e. no-op / status quo. */
@media(max-width:720px) and (scripting:none){
  .hx-track .hx-stop{opacity:1;transform:none;transition:none}
  .hx:not(.hx--go) .hx-track .hx-stop :is(.hx-dot,.hx-cube,.hx-card){transform:none;transition:none}
}
/* pinned horizontal mode (tpl.js adds .hx--go) — a scattered "exhibit wall": cards float
   above/below a year-ruler axis at one of 3 fixed distances (tier), each connected back to
   the axis by a thin stem drawn from the CARD's own near edge (.hx-card::before), so the
   stem length stays exact regardless of the card's own (content-driven) height. There is
   no dimming/"active" state — every card reads at full strength once it has entered, same
   as a wall of prints; the horizontal pan is what reveals new ones, not an opacity change. */
.hx--go .hx-scene{height:calc(100vh - var(--hx-top,64px));min-height:700px;display:flex;flex-direction:column;justify-content:center;overflow:hidden}
.hx--go .hx-head{position:absolute;top:clamp(16px,3.5vh,36px);left:0;right:0;padding:0 clamp(20px,4vw,48px);max-width:none;margin:0;z-index:4}
.hx--go .hx-title{max-width:520px}
.hx--go .hx-head .eyebrow{margin-bottom:6px}
.hx--go .hx-h1{font-size:clamp(21px,2.3vw,31px)}
/* --ink-soft, not the original --ink-muted: this cue sits directly on the .hx-scene surface, and
   11px --ink-muted has never passed there on ANY backdrop — 1.77:1 over the old DNA helix,
   2.12:1 over the meeting-room photo, 4.10:1 over the noise grain now (all worst glyph pixel).
   That is the documented --ink-muted-off-a-tinted-ground problem, same as .map-fall p and
   .manifest dt, and no backdrop fixes it. --ink-soft measures 6.90:1 on the grain and is the
   quietest token that clears 4.5:1, so it keeps the cue subordinate as designed rather than
   shouting it in full --ink (13.83:1). Token, so dark still flips: 17.50:1 there. */
.hx--go .hx-cue{display:block;position:absolute;bottom:clamp(22px,4.5vh,38px);left:0;right:0;text-align:center;font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-soft);z-index:4;transition:opacity .4s var(--ease)}

.hx--go .hx-track{display:flex;align-items:stretch;height:min(72vh,660px);padding:0 clamp(20px,4vw,48px);max-width:none;margin:0}
/* (the old full-height .hx-track::before rail this mode used to suppress is gone: both modes
   now draw their axis from per-stop .hx-seg legs, so there was nothing left to hide) */
/* axis drawn as discrete per-stop segments (a real DOM element per gap, plain colour
   toggle) rather than one continuous scaled overlay — each segment runs from THIS stop's
   centre to the NEXT one's (spanning 100% of one stop's own width lands exactly on the
   next stop's centre, since all stops share the same flex-basis), turning red once its
   left-hand stop has been reached. Simpler to verify than a transform-driven fill and
   reads as discrete "milestones reached", matching the reference's tick-marked ruler. */
.hx--go .hx-seg{position:absolute;left:50%;width:100%;top:50%;height:1px;margin-top:0;background:var(--hairline);z-index:0;transition:background .25s var(--ease)}
.hx--go .hx-stop:last-child .hx-seg{display:none}
.hx--go .hx-stop.is-passed .hx-seg{background:var(--red)}

.hx--go .hx-stop{position:relative;flex:0 0 clamp(220px,19vw,280px)}
/* full-height gridline at every stop's year position, behind everything — turns red with
   the rest of the route once its stop is reached, so the whole vertical reads as "travelled" */
.hx--go .hx-stop::before{content:"";position:absolute;left:50%;top:0;bottom:0;width:1px;background:var(--hairline);opacity:.35;z-index:0;transition:background .25s var(--ease),opacity .25s var(--ease)}
.hx--go .hx-stop.is-passed::before{background:var(--red);opacity:1}
/* year labels — single-sourced from data-year so they can never drift from the card's own date */
.hx--go .hx-stop::after{content:attr(data-year);position:absolute;left:50%;top:calc(50% + 15px);transform:translateX(-50%);font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.06em;color:var(--ink-muted);white-space:nowrap;z-index:1}
.hx--go .hx-stop--below::after{top:auto;bottom:calc(50% + 15px)}
/* tick — small ring sitting exactly on the axis */
.hx--go .hx-tick{position:absolute;left:50%;top:50%;width:9px;height:9px;margin:-4.5px 0 0 -4.5px;border-radius:50%;border:1.5px solid var(--paper-500);background:var(--paper-0);z-index:1;transition:background .25s var(--ease),border-color .25s var(--ease)}
.hx--go .hx-stop.is-passed .hx-tick{background:var(--red);border-color:var(--red)}

/* entrance only: a stop fades in once scrolled into view and then STAYS at full strength —
   no re-dimming, matching the reference's uniform, all-visible wall (overrides the plain
   .hx-stop--future .hx-card dimming above, which is deliberately vertical-ledger-only) */
.hx--go .hx-stop .hx-card,.hx--go .hx-stop .hx-dot,.hx--go .hx-stop .hx-cube{opacity:0;transition:opacity .5s var(--ease)}
.hx--go .hx-stop.is-in .hx-card,.hx--go .hx-stop.is-in .hx-dot,.hx--go .hx-stop.is-in .hx-cube{opacity:1}

/* card — anchored by its near edge (bottom for above-cards, top for below-cards) so the
   stem length stays exact regardless of the card's own height; --stem set per tier below */
.hx--go .hx-card{position:absolute;left:50%;transform:translateX(-50%);width:clamp(210px,85%,270px);max-width:270px;z-index:2}
/* stem — carries the red fill from the axis up/down to the dot once the stop is passed, so
   the coloured route visibly reaches (touches) the dot instead of stopping at the tick */
.hx--go .hx-card::before{content:"";position:absolute;left:50%;width:1px;background:var(--hairline);transition:background .25s var(--ease)}
.hx--go .hx-stop--above .hx-card{bottom:calc(50% + var(--stem))}
.hx--go .hx-stop--above .hx-card::before{top:100%;height:var(--stem)}
.hx--go .hx-stop--below .hx-card{top:calc(50% + var(--stem))}
.hx--go .hx-stop--below .hx-card::before{bottom:100%;height:var(--stem)}
.hx--go .hx-stop.is-passed .hx-card::before{background:var(--red)}
.hx--go .hx-stop--near{--stem:64px}
.hx--go .hx-stop--mid{--stem:100px}
.hx--go .hx-stop--far{--stem:150px}

/* dot / cube — sit where the stem meets the card; turns red once the stem's stop is passed,
   so the dot itself reads as the route's leading edge rather than a static marker */
.hx--go .hx-dot{left:50%;width:20px;height:20px;margin-left:-10px;background:var(--paper-500);z-index:3;transition:background .25s var(--ease)}
.hx--go .hx-stop.is-passed .hx-dot{background:var(--red)}
.hx--go .hx-cube{left:50%;width:27px;height:27px;margin-left:-13.5px;top:auto;z-index:3}
.hx--go .hx-stop--above .hx-dot,.hx--go .hx-stop--above .hx-cube{bottom:calc(50% + var(--stem) - 10px)}
.hx--go .hx-stop--below .hx-dot,.hx--go .hx-stop--below .hx-cube{top:calc(50% + var(--stem) - 10px)}

/* THE ROUTE ENDS AT TODAY, in the scrub exactly as in the ledger. .is-passed is a reading
   state — "the scrub has carried you past this stop" — and travel now centres the LAST stop
   (see the dist note in tpl.js), so without these the red would run on through the projected
   part of the route and paint 2027+ as travelled. Same statement as the ledger's rule at
   :7222, expressed against .is-passed instead of chronology: the --now stop's OWN outgoing
   segment is grey, everything after it is grey, and a --future stop keeps its grey node and
   gridline however far the reader has scrubbed. Red therefore reaches the Today node and
   stops there — which is the whole point of the marker. */
.hx--go :is(.hx-stop--now,.hx-stop--now ~ .hx-stop).is-passed .hx-seg{background:var(--hairline)}
.hx--go .hx-stop--now ~ .hx-stop.is-passed::before{background:var(--hairline);opacity:.35}
.hx--go .hx-stop--now ~ .hx-stop.is-passed .hx-tick{background:var(--paper-0);border-color:var(--paper-500)}
.hx--go .hx-stop--now ~ .hx-stop.is-passed .hx-dot{background:var(--paper-500)}
.hx--go .hx-stop--now ~ .hx-stop.is-passed .hx-card::before{background:var(--hairline)}
@supports ((mask-size:100% 100%) or (-webkit-mask-size:100% 100%)){.hx--go .hx-dot{border:0}}

/* prev/next — discrete step navigation alongside the continuous scroll-scrub */
.hx-nav{position:absolute;bottom:clamp(18px,3.5vh,32px);left:50%;transform:translateX(-50%);display:none;gap:8px;z-index:5}
.hx--go .hx-nav{display:flex}
.hx-nav-btn{width:44px;height:44px;border-radius:50%;border:1px solid var(--hairline);background:var(--paper-0);color:var(--ink);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s var(--ease),color .15s var(--ease),border-color .15s var(--ease)}
.hx-nav-btn svg{width:18px;height:18px}
.hx-nav-btn:hover,.hx-nav-btn:focus-visible{background:var(--ink);color:var(--paper-0);border-color:var(--ink)}
.hx-nav-next{background:var(--ink);color:var(--paper-0);border-color:var(--ink)}
.hx-nav-next:hover,.hx-nav-next:focus-visible{background:var(--red);border-color:var(--red)}
/* ---- the same prev/next on the vertical ledger (2026-08-05, owner) ----
   The ledger had no navigation affordance at all: both .hx-nav and .hx-cue were gated to
   .hx--go, so every phone, reduced-motion and no-GSAP visitor got free-scroll only.

   POSITION IS MEASURED, NOT PICKED. The bottom edge is already occupied: .espa-bar spans the
   full width at the very bottom (z 85) and .a11y / button.to-top sit in the two corners (z 80),
   leaving a free centred strip between them in the same band (278px at 390). So the pair sits
   centred, in that band, at the same z-index — no overlap with any of the three, and it reads as
   one row of controls with them rather than a fourth floating thing. Fixed, not absolute: the
   ledger is a long document, so a stage-anchored bar would scroll away from what it drives.

   THE OFFSET IS THE SITE'S OWN, NOT A NUMBER. It reuses the exact rules .a11y and .to-top use
   (:2897, :2902) — 20px, or 12px on phones, lifted by --espa-h while the co-funding bar is up.
   A hardcoded 69px was written first and was wrong: 69px is only what that formula RESOLVES to
   at 390, where --espa-h is floored at 56+1. --espa-h scales with the viewport (it is derived
   from 100vw ÷ 8.4675), so at 1440 the bar is ~161px tall and the nav rendered inside it. Caught
   at 1440 with reduced motion, which is a real case: that combination renders this ledger, not
   the scrub. The bottom transition matches theirs too, so all three lift together when the bar
   is dismissed instead of the nav detaching from the pair.

   Only visible while the track is actually on screen — tpl.js adds .hx-nav--on — otherwise two
   buttons would hover over the hero and the footer, where they navigate nothing. */
.hx:not(.hx--go) .hx-nav{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:80;
  display:none;gap:8px;transition:bottom 520ms var(--ease)}
.hx:not(.hx--go) .hx-nav.hx-nav--on{display:flex}
html.espa-on .hx:not(.hx--go) .hx-nav{bottom:calc(20px + var(--espa-h))}
@media(max-width:640px){
  .hx:not(.hx--go) .hx-nav{bottom:12px}
  html.espa-on .hx:not(.hx--go) .hx-nav{bottom:calc(12px + var(--espa-h))}
}
/* a spent direction is disabled rather than hidden, so the pair never changes width mid-scroll */
.hx-nav-btn[disabled]{opacity:.4;cursor:default}
.hx-nav-btn[disabled]:hover{background:var(--paper-0);color:var(--ink);border-color:var(--hairline)}
.hx-nav-next[disabled]:hover{background:var(--ink);color:var(--paper-0);border-color:var(--ink)}
/* The cue is static on the ledger — it sits above the track in source order already, so it only
   needs un-absoluting. It must NOT join the bottom band: that band is full, and the cue is a
   read-once instruction rather than a control. Its arrow is rewritten by tpl.js, because the
   markup ships the horizontal "travels →" for the scrub and the ledger travels down. */
.hx:not(.hx--go) .hx-cue{display:block;position:static;margin:0 0 clamp(18px,3vw,26px);
  font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;
  /* --ink-soft for the same reason as the scrub variant above: 11px --ink-muted fails on the
     worst glyph pixel of this surface on every backdrop tried */
  color:var(--ink-soft)}


/* ============ Card reveal + rule-trace hover (site-wide motion refresh, pilot: OaaS) ============
   Replaces the old 100vw-fly bento (.wwd-card/.gtee-card) + red conic-sweep on new rollouts.
   Reveal: cards home in from just outside their row position (l/c/r — never arbitrary),
   modest distance (not 100vw), so a normal IntersectionObserver works (unlike the fly-in
   cards, these do actually intersect). Hover: one measured border — the hairline at rest IS
   the card's real rendered edge; corners stay quiet, the 4 straight edges trace red in
   sequence (horizontal completes, then verticals catch). See
   docs/superpowers/specs/2026-07-21-card-reveal-hover-motion-design.md */
.card-arrive{position:relative;display:flex;--tx:0;--ty:56px;--rot:0deg;
  opacity:0;transform:translate(var(--tx),var(--ty)) rotate(var(--rot)) scale(.97)}
.card-arrive.go{opacity:1;transform:translate(0,0) rotate(0deg) scale(1);
  transition:opacity 380ms var(--ease),transform 620ms cubic-bezier(.2,.9,.15,1.06)}
.card-arrive[data-dir="l"]{--tx:-26%;--ty:48px;--rot:-1.4deg}
.card-arrive[data-dir="r"]{--tx:26%;--ty:48px;--rot:1.4deg}
.card-arrive[data-dir="c"]{--tx:0;--ty:64px;--rot:0deg}

/* matches the .reveal > .card pattern (tpl.css ~1029) so every card in a row is equal
   height (CSS Grid only stretches the .card-arrive item itself, not its child) */
.card-arrive .card{width:100%;border-color:transparent;transition:transform 220ms var(--ease)}
/* THE FRAME MOVES WITH THE CARD. .card-arrive .card has border-color:transparent, so this
   svg's .base IS the card's visible edge — and the :hover rule below lifts the card 4px. Until
   2026-08-30 the frame stayed put while the card rose, so on every hover the border detached
   from the card it belongs to and the red trace drew 4px off the edge it is measured from,
   against the one thing the module promises: the trace sits on the card's ACTUAL rendered edge.
   Same distance, same duration and same easing as the .card rule, so the two move as one
   object. Transform, not a re-measure: fitFrame() in tpl.js measures the resting box, and
   re-measuring per hover would put a layout read in the hover path for no visual gain. */
.card-arrive .frame{position:absolute;inset:0;pointer-events:none;overflow:visible;
  transition:transform 220ms var(--ease)}
.card-arrive .frame rect,.card-arrive .frame line{fill:none;vector-effect:non-scaling-stroke}
.card-arrive .frame .base{stroke:var(--hairline);stroke-width:1}
.card-arrive .frame .trace-h,.card-arrive .frame .trace-v{
  stroke:var(--red-ink);stroke-width:1.4;stroke-dasharray:var(--len,0);stroke-dashoffset:var(--len,0)}
.card-arrive .frame .trace-h{transition:stroke-dashoffset 420ms var(--ease)}
.card-arrive .frame .trace-v{transition:stroke-dashoffset 380ms var(--ease)}
.card-arrive:hover .frame .trace-v,.card-arrive:focus-within .frame .trace-v{transition-delay:420ms}
.card-arrive:hover .card,.card-arrive:focus-within .card,
.card-arrive:hover .frame,.card-arrive:focus-within .frame{transform:translateY(-4px)}
.card-arrive:hover .frame .trace-h,.card-arrive:hover .frame .trace-v,
.card-arrive:focus-within .frame .trace-h,.card-arrive:focus-within .frame .trace-v{stroke-dashoffset:0}
.card-arrive:hover .kicker,.card-arrive:focus-within .kicker{color:var(--red-ink)}
/* dark inverted band: the hairline needs a lighter, still-quiet stroke to read at rest */
.band-inverse .card-arrive .frame .base{stroke:#3a3a3e}

@media (prefers-reduced-motion: reduce){
  .card-arrive{opacity:1!important;transform:none!important;transition:none!important}
  .card-arrive .frame{transition:none!important}
  .card-arrive .frame .trace-h,.card-arrive .frame .trace-v{transition:none!important;transition-delay:0ms!important}
}
html.a11y-no-motion .card-arrive{opacity:1;transform:none;transition:none}
html.a11y-no-motion .card-arrive .frame{transition:none}
html.a11y-no-motion .card-arrive .frame .trace-h,html.a11y-no-motion .card-arrive .frame .trace-v{transition:none;transition-delay:0ms}

/* Content upgrade: bigger isometric icon + index + hover-reveal body + "+" affordance —
   reuses the home page's global .mf-top/.mf-ico/.mf-idx/.mf-title/.mf-body/.mf-foot/
   .mf-tag/.mf-more classes (already unscoped base styles) for layout/typography/icon
   sizing, but the interaction is wired to .card-arrive, not .mf-card — the rule-trace
   border above is this component's one hover signature, not .mf-card's conic-sweep. */
.card-arrive .card{min-height:clamp(260px,24vw,340px);flex-direction:column}
.card-arrive .mf-title{margin-top:clamp(20px,3vw,32px)}
.card-arrive .mf-foot{margin-top:auto}
.card-arrive .mf-more{transition:transform .3s var(--ease),color .3s var(--ease)}
.card-arrive:hover .mf-more,.card-arrive:focus-within .mf-more{transform:rotate(45deg);color:var(--red-ink)}
@media(hover:hover){
  .card-arrive .mf-body{opacity:0;transform:translateY(8px);transition:opacity .38s var(--ease),transform .38s var(--ease)}
  .card-arrive:hover .mf-body,.card-arrive:focus-within .mf-body{opacity:1;transform:none}
}
.card-arrive .mf-ico .pc{transition:transform .4s cubic-bezier(.22,1,.36,1)}
.card-arrive:hover .mf-ico .pc,.card-arrive:focus-within .mf-ico .pc{transform:translate(var(--dx,0),var(--dy,0))}
.card-arrive:hover .mf-ico path,.card-arrive:focus-within .mf-ico path{stroke:var(--red-ink)}
@media(max-width:820px){.card-arrive .card{min-height:0}.card-arrive .mf-body{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){
  .card-arrive .mf-ico .pc,.card-arrive .mf-more{transition:none}
  .card-arrive:hover .mf-ico .pc,.card-arrive:focus-within .mf-ico .pc{transform:none}
}
html.a11y-no-motion .card-arrive .mf-ico .pc,html.a11y-no-motion .card-arrive .mf-more{transition:none}
html.a11y-no-motion .card-arrive:hover .mf-ico .pc,html.a11y-no-motion .card-arrive:focus-within .mf-ico .pc{transform:none}

/* STEPS-EXPAND — 4 equal cards; the hovered/focused one grows (flex-grow, width only) via
   :has() (no JS), its siblings shrink. Height never moves: the paragraph lives in a
   fixed-height reserved slot (.step-body) and only fades/slides within it, so align-items:
   stretch never has a new "tallest card" to match. tabindex on the card + :focus (not
   :focus-within — the card itself is the focus target) keeps this keyboard-reachable.
   The corner glyph is a plain +, rotating into an × on reveal — clearer than an arrow for
   "there's more here" with nowhere to navigate to. (hover:none) touch shows every card in its
   revealed state permanently (no hover to gate it on) and hides the now-meaningless +.
   ONE CONSUMER LEFT as of 2026-08-21: customers #journey, the .step-photo variant (photo header,
   no corner art). models/oaas.html used to be the other and is now .bnd, so if #journey ever
   moves off this component the whole block goes with it. */
.steps-expand{display:flex;gap:20px;align-items:stretch}
/* flex-grow is a layout property and does reflow the row on each frame — kept deliberately,
   because the effect IS that the neighbours give up their space, and no transform can reflow a
   sibling. The row is 4 items deep, so the reflow is cheap; what was expensive was the duration.
   840ms -> 420ms brings a hover response inside a sane range without changing the mechanic. */
/* padding-bottom reserves the foot row. .step-foot is absolutely positioned at bottom:28px with
   no background, so flowed content had nothing stopping it running underneath: on customers
   #journey at 2-up the body's last line sat directly on top of the "YOU ORDER" kicker (measured
   40px of overlap, 2026-08-21). 80px = the foot's 28px offset + its 40px height + 12px of gap.
   This is the real fix for that class of overlap; the min-height floor is back to its original
   320px, since the 374px floor only ever existed to make room for a corner plate that no
   consumer of this component has any more. */
.step-card{flex:1 1 0;min-width:0;min-height:clamp(320px,26vw,400px);background:var(--paper-50);border:1px solid var(--hairline);border-radius:var(--radius-card);padding:28px 28px 80px;position:relative;overflow:hidden;transition:flex-grow 280ms var(--ease);outline:none}
/* default look now matches the home page's Discipline (.mf-card) cards: the big isometric
   icon + top-right index, reusing the same global .mf-top/.mf-ico/.mf-idx/.mf-title classes
   (already unscoped base styles — free, no redeclaration needed) instead of the small inline
   .ci icon. Title margin is tightened vs mf-card's own (which is sized for a taller 3-col
   card) to suit this card's more compact height. */
.step-card .mf-top{position:relative;z-index:1}
.step-card .mf-title{margin-top:clamp(20px,3vw,32px);position:relative;z-index:1}
/* 84px, not 72px: the reserved slot has to clear the paragraph's own 12px top margin on top of
   3 * 24px of line-height. At 72px a 3-line body (step 03 on oaas) lost the bottom 12px of its
   last line, slicing the glyphs horizontally. The foot is absolutely positioned at bottom:28px
   and the flowed content tops out at 285px inside a 374px card, so the taller slot moves nothing. */
.step-body{height:84px;overflow:hidden;position:relative;z-index:1}
.step-card p{color:var(--ink-soft);font-size:15px;margin-top:12px;opacity:0;transform:translateY(6px);transition:opacity 260ms var(--ease),transform 260ms var(--ease)}
.step-card:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
/* footer row — the descriptive tag + the +/× affordance, always visible (only the body
   paragraph hides), same as mf-card's .mf-foot */
.step-foot{position:absolute;left:28px;right:28px;bottom:28px;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:12px}
.step-foot .kicker{margin:0}
.step-arrow{position:relative;width:40px;height:40px;border-radius:50%;background:var(--red);flex:none;transition:transform 260ms var(--ease);z-index:1}
.step-arrow::before,.step-arrow::after{content:'';position:absolute;left:50%;top:50%;background:#fff;border-radius:1px}
.step-arrow::before{width:14px;height:2px;transform:translate(-50%,-50%)}
.step-arrow::after{width:2px;height:14px;transform:translate(-50%,-50%)}
.step-card:hover .step-arrow,.step-card:focus .step-arrow{transform:rotate(45deg)}
/* no separating ring on this disc. It briefly had a 5px paper one while a frosted photograph sat
   behind it on oaas (solid red on blurred detail read as a collision); the remaining consumer,
   #journey, has nothing behind the disc, so the ring came out with the photo. */
/* the copy still lands after the card has opened, but 440ms + a 240ms delay put the full hover at
   680ms; 260ms + 120ms keeps the same order of events inside budget */
.step-card:hover p,.step-card:focus p{opacity:1;transform:translateY(0);transition-delay:120ms}
/* step-card ambient icon bob + the iconBob keyframe removed 2026-07-22 — motion trim.
   Icon pieces render static in their assembled SVG positions. */
/* .mf-ico--ink: force every stroke to --ink so a line icon reads all-black in default mode
   (base .mf-ico path is #4b4b4b grey while bare circles/rects inherit currentColor → patchy).
   Dark keeps its lighter path stroke so the icon matches its siblings in that theme. */
.mf-ico--ink path,.mf-ico--ink circle,.mf-ico--ink rect{stroke:var(--ink)}
html[data-theme="dark"] .mf-ico--ink path{stroke:#c9c9c9}
/* hover/focus red accents — the mono kicker, the 01–04 index, the title and the icon
   strokes all warm to the brand accent, matching the sitewide .card-arrive/.mf-card hover
   convention (the .step-arrow circle is already red). --red-ink is theme-aware. */
.step-card .mf-idx,.step-card .step-foot .kicker,.step-card .mf-title{transition:color .3s ease}
.step-card:hover .mf-idx,.step-card:focus .mf-idx,
.step-card:hover .step-foot .kicker,.step-card:focus .step-foot .kicker,
.step-card:hover .mf-title,.step-card:focus .mf-title{color:var(--red-ink,#ab0000)}
.step-card:hover .mf-ico path,.step-card:focus .mf-ico path{stroke:var(--red-ink,#ab0000)}
/* .step-photo — image-led variant (customers #journey): a full-bleed photo header tops the
   card (negative margins swallow the 28px padding; the card's own overflow:hidden clips the
   rounded corners). Replaces the line icon + corner hover reveal on that page; the index
   moves to its own row below the photo. Width flexes with the card's expand animation. */
.step-card .step-photo{display:block;height:clamp(150px,14vw,200px);margin:-28px -28px 0;position:relative;z-index:1}
.step-card .step-photo img{width:100%;height:100%;object-fit:cover;display:block}
.step-card .step-photo + .mf-top{margin-top:22px}
/* The corner brand mark (.step-mark) and the photo plate before it were RETIRED 2026-08-21 with
   the oaas step row itself, which is now .bnd (search "THE BOUNDARY" below). Nothing else used
   them: customers #journey is the .step-photo variant. Kept from that work because #journey
   benefits and both were real defects: the 84px .step-body slot and the padding-bottom that
   reserves the foot row. */
.steps-expand:has(.step-card:hover) .step-card,.steps-expand:has(.step-card:focus) .step-card{flex-grow:.6}
.steps-expand:has(.step-card:hover) .step-card:hover,.steps-expand:has(.step-card:focus) .step-card:focus{flex-grow:2.4}
@media(max-width:960px){.steps-expand{flex-wrap:wrap}.step-card{flex-basis:calc(50% - 10px)}}
@media(max-width:640px){.step-card{flex-basis:100%}}
@media(hover:none){
  .step-card p{opacity:1;transform:none}
  .step-body{height:auto}
  .step-arrow{display:none}
}
@media(prefers-reduced-motion:reduce){.step-card,.step-card p,.step-arrow{transition:none}.step-card .mf-ico .pc{animation:none;transition:none}}
html.a11y-no-motion .step-card,html.a11y-no-motion .step-card p,html.a11y-no-motion .step-arrow{transition:none}
html.a11y-no-motion .step-card .mf-ico .pc{animation:none;transition:none}

/* The .stx typographic ledger (models/oaas.html "How it works", 2026-08-21) lived here.
   Removed 2026-08-24: that page adopted the shared .flow-rows list below and .stx had no
   other consumer. See git history for the ledger if it is ever wanted back. */

/* ==========================================================================
   HOW IT WORKS — operation pipeline (.opx*, ai-transformation #how ONLY).
   An AI-SaaS × crypto reimagining: the 4 steps are stages of a live pipeline an
   order flows through, not cards. A rail with 4 nodes + a travelling pulse (the
   signature), gradient-stroke glass panels, mono STEP labels + a data readout per
   stage, and a red→ember energy gradient (the sole accent family — not purple).
   Uses the body.ai-neural --ain-* tokens, so it only renders on that page. */
.opx-sec{position:relative;overflow:hidden}
/* energy glow behind the pipeline */
.opx-sec::before{content:"";position:absolute;inset:-10% -5%;pointer-events:none;z-index:0;
  background:radial-gradient(46% 60% at 18% -6%, rgba(230,59,59,.13), transparent 70%),radial-gradient(42% 60% at 92% 16%, rgba(255,106,69,.10), transparent 72%)}
/* faint telemetry grid, faded at the edges */
.opx-grid{position:absolute;inset:0;pointer-events:none;z-index:0;opacity:.5;
  background-image:linear-gradient(var(--ain-line) 1px,transparent 1px),linear-gradient(90deg,var(--ain-line) 1px,transparent 1px);background-size:56px 56px;
  -webkit-mask-image:radial-gradient(70% 60% at 50% 30%,#000,transparent 75%);mask-image:radial-gradient(70% 60% at 50% 30%,#000,transparent 75%)}
.opx-status{font-family:var(--font-mono);font-size:12.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--ain-soft);display:inline-flex;align-items:center;gap:10px;margin:2px 0 44px}
.opx-status b{color:var(--ain-ink);font-weight:600}
.opx-dot{width:8px;height:8px;border-radius:50%;background:var(--ain-red);flex:none;animation:opx-live 2.2s ease-out infinite}
@keyframes opx-live{0%{box-shadow:0 0 0 0 rgba(230,59,59,.5)}70%{box-shadow:0 0 0 8px rgba(230,59,59,0)}100%{box-shadow:0 0 0 0 rgba(230,59,59,0)}}
.opx{position:relative;z-index:1}
/* rail: full-width line + 4 nodes (aligned to the stage grid) + travelling pulse */
.opx-rail{position:relative;display:grid;grid-template-columns:repeat(4,1fr);gap:18px;height:24px;margin-bottom:20px}
.opx-line{position:absolute;top:50%;left:0;right:0;height:1px;transform:translateY(-50%);background:linear-gradient(90deg,transparent,var(--ain-line-2) 7%,var(--ain-line-2) 93%,transparent)}
.opx-node{position:relative;justify-self:center;align-self:center;width:12px;height:12px;border-radius:50%;background:var(--ain-bg);border:1.5px solid var(--ain-red);box-shadow:0 0 12px -1px rgba(230,59,59,.65);z-index:1}
/* the pulse used to animate `left` from -70px to 100% on a 4.8s infinite loop, i.e. a layout
   reflow every frame, forever. The element is now a static full-width strip and the 70px gradient
   travels as a background-position instead: paint-only, no layout, visually identical. */
.opx-pulse{position:absolute;top:50%;left:0;right:0;height:2px;transform:translateY(-50%);border-radius:2px;
  background:linear-gradient(90deg,transparent,var(--ain-ember),var(--ain-red),transparent) no-repeat;
  background-size:70px 100%;background-position-x:-70px;
  animation:opx-travel 4.8s cubic-bezier(.45,0,.55,1) infinite}
@keyframes opx-travel{0%{background-position-x:-70px;opacity:0}8%{opacity:1}92%{opacity:1}100%{background-position-x:calc(100% + 70px);opacity:0}}
.opx-stages{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
.opx-stage{position:relative;border-radius:var(--radius-card);padding:24px 22px 20px;display:flex;flex-direction:column;
  background:linear-gradient(var(--ain-panel),var(--ain-panel)) padding-box,linear-gradient(155deg,rgba(230,59,59,.55),rgba(255,106,69,.30) 42%,var(--ain-line) 78%) border-box;
  border:1px solid transparent;transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
@media(hover:hover) and (pointer:fine){.opx-stage:hover{transform:translateY(-4px);box-shadow:0 20px 48px -24px rgba(0,0,0,.6),0 0 46px -16px rgba(255,106,69,.32)}}
.opx-stage:focus-within{transform:translateY(-4px);box-shadow:0 20px 48px -24px rgba(0,0,0,.6),0 0 46px -16px rgba(255,106,69,.32)}
.opx-stage:focus-visible{outline:2px solid var(--ain-red);outline-offset:3px}
.opx-step{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.16em;color:var(--ain-kick)}
.opx-title{font-family:var(--font-display);font-weight:700;font-size:clamp(18px,1.4vw,21px);line-height:1.16;letter-spacing:-.01em;color:var(--ain-ink);margin:13px 0 9px}
.opx-body{color:var(--ain-soft);font-size:14.5px;line-height:1.56;margin:0 0 20px;flex:1}
.opx-data{display:flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:11.5px;letter-spacing:.03em;color:var(--ain-muted);border-top:1px solid var(--ain-line);padding-top:14px}
/* light theme: --ain-muted (#78777F) is only 3.71:1 on --ain-panel; a slightly darker
   mono grey clears AA (5.05:1) without retuning the shared token. Dark keeps --ain-muted
   (5.58:1, already AA). */
html:not([data-theme="dark"]) body.ai-neural .opx-data{color:#63626A}
/* data-chip key: white on --ain-red is only 4.16:1 at 10px; --ain-red-deep (#AB0000) = 7.7:1 */
.opx-k{flex:none;color:#fff;background:var(--ain-red-deep);border-radius:4px;padding:3px 7px;font-size:10px;letter-spacing:.12em}
@media(max-width:900px){.opx-stages{grid-template-columns:repeat(2,1fr)}.opx-rail{display:none}}
@media(max-width:560px){.opx-stages{grid-template-columns:1fr}}
@media(prefers-reduced-motion:reduce){.opx-pulse,.opx-dot{animation:none}.opx-stage{transition:none}}
html.a11y-no-motion .opx-pulse,html.a11y-no-motion .opx-dot{animation:none}
html.a11y-no-motion .opx-stage{transition:none}

/* ==========================================================================
   Model-page "how it works" — full-width row list (.flow-rows).
   Used by models/cross-docking.html and models/oaas.html (which adopted it 2026-08-24, replacing
   its one-off .stx ledger). Rows: [ 01 LABEL ] [ title + copy ] [ isometric
   illustration ]; dashed dividers. Hover lifts a full-bleed white band + blooms a red /shapes
   mask behind the icon, which turns to a white wireframe (fill off, strokes white). Red stays
   the sole accent (step numbers + hovered title/shape).
   ========================================================================== */
.flow-sec{overflow-x:clip} /* contains the full-bleed hover band; clip (not hidden) keeps sticky scenes working */
.flow-rows{list-style:none;margin:26px 0 0;padding:0;border-top:1px dashed var(--hairline)}
.fr-row{position:relative;display:grid;grid-template-columns:minmax(116px,164px) 1fr minmax(190px,240px);
  align-items:center;gap:clamp(18px,3.2vw,52px);padding:clamp(14px,1.8vw,22px) 14px;
  border-bottom:1px dashed var(--hairline);
  transition:background .3s var(--ease),box-shadow .3s var(--ease),border-color .3s var(--ease),
    border-radius .3s var(--ease),transform .55s var(--ease),opacity .55s var(--ease)}
/* hover — a full-bleed white band lifts behind the row (soft drop shadow) */
.fr-row::before{content:"";position:absolute;top:0;bottom:0;left:50%;width:100vw;margin-left:-50vw;
  background:var(--paper-0);box-shadow:0 20px 44px -24px rgba(0,0,0,.30);
  opacity:0;transition:opacity .3s var(--ease);z-index:0;pointer-events:none}
@media(hover:hover) and (pointer:fine){
.fr-row:hover{border-color:transparent;z-index:2}
.fr-row:hover::before{opacity:1}
}
.fr-idtext,.fr-desc,.fr-art{position:relative;z-index:1}
/* left — step number + label */
.fr-idtext{display:flex;flex-direction:column;line-height:1.05}
.fr-idx{font-family:var(--font-mono);font-size:13px;font-weight:500;color:var(--red-ink);letter-spacing:.04em}
.fr-label{font-family:var(--font-mono);font-size:16px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);margin-top:4px}
/* middle — title + copy */
.fr-desc{max-width:48ch}
.fr-title{font-family:var(--font-display);font-weight:800;letter-spacing:-.02em;line-height:1.1;
  font-size:clamp(19px,1.7vw,24px);color:var(--ink);transition:color .3s var(--ease)}
.fr-desc p{color:var(--ink-soft);font-size:15px;line-height:1.55;margin-top:7px}
@media(hover:hover) and (pointer:fine){.fr-row:hover .fr-title{color:var(--red-ink)}}
/* right — unframed isometric illustration; on hover a red /shapes mask blooms behind it */
.fr-art{position:relative;height:clamp(92px,10vw,124px)}
.fr-art::before{content:"";position:absolute;inset:-16% -2%;background:var(--red);opacity:0;transform:scale(.82);
  -webkit-mask-image:var(--shape);mask-image:var(--shape);
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;
  -webkit-mask-size:contain;mask-size:contain;
  transition:opacity .4s var(--ease),transform .45s var(--ease);z-index:0;pointer-events:none}
@media(hover:hover) and (pointer:fine){.fr-row:hover .fr-art::before{opacity:1;transform:scale(1)}}
.fr-motif{position:absolute;inset:0;margin:auto;height:84%;width:auto;color:var(--ink);stroke-width:2;z-index:1}
/* rest: dark line-art, occlusion fills track the page. hover: fill OFF + white strokes (wireframe over the shape) */
.fr-art [fill="#fff"]{fill:var(--paper-100)}
@media(hover:hover) and (pointer:fine){
.fr-row:hover .fr-motif{color:#fff}
.fr-row:hover .fr-art [fill="#fff"]{fill:transparent}
}
/* site .pc convention: parts rest, then scatter by their own --dx/--dy on hover */
.fr-motif .pc{transition:transform .45s cubic-bezier(.22,1,.36,1);transition-delay:var(--td,0s)}
@media(hover:hover) and (pointer:fine){.fr-row:hover .fr-motif .pc{transform:translate(var(--dx,0),var(--dy,0))}}
/* per-row red shape mask (inlined as data URIs so the mask renders under file:// too, not just when served over http — same shapes as /assets/shapes/) */
.flow-rows .fr-row:nth-child(1) .fr-art{--shape:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjE5NyIgdmlld0JveD0iMCAwIDE4NCAxOTciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xODMuMDE5IDExMy4zMjNMMTgzLjAwMSA2Mi41MTE3QzE4My4wMDEgNTYuMDI3OCAxODIuOTk3IDQ3LjE0NjMgMTcwLjg3NiA0MC4xODgzTDE3MC43ODIgNDAuMTM1MUwxMDAuMjY3IDIuODU0MTNDOTYuOTE2NCAwLjk4ODMwOSA5My4xMTgyIDAgODkuMjgwMiAwQzg1LjQ0MjIgMCA4MS41NDIyIDEuMDE0OSA3OC4xNTYyIDIuOTMzOUw1OS4yODUzIDEzLjYyMzZMNTkuMTAzNSAxMy41MjYxTDExLjIxNzEgMzkuMjU3NkwxMS4xMTk2IDM5LjMxNTJDNy42OTgxNyA0MS4zMjI5IDQuODQ0MDQgNDQuNDI5NiAyLjg3MTg2IDQ4LjI5ODZDMC45OTI3MzkgNTEuOTg2IDAgNTYuMTk2MiAwIDYwLjQ4NjNMMC4wNjY0NzU1IDE0MC44MUMwLjA2NjQ3NTUgMTQ2LjYzOCAzLjE2NDM2IDE1Mi4xMzggOC4xNTAyMyAxNTUuMTY1TDguMjc0MzIgMTU1LjI0TDMyLjk0NjYgMTY4LjY2VjE2OC41NEw4Mi44NjczIDE5NC42MDhDODguNDgyNSAxOTcuNTQyIDk1LjE4NzkgMTk3LjUwMiAxMDAuNzY4IDE5NC41MDJMMTcxLjk2NiAxNTYuMjQ2QzE3OC45MTEgMTUyLjU1OSAxODMuMDU0IDE0NS43OTEgMTgzLjA1NCAxMzguMTQ2VjExMy4zMUwxODMuMDE5IDExMy4zMjhWMTEzLjMyM1oiIGZpbGw9IiM3Mjg4OTkiLz4KPC9zdmc+Cg==)}
.flow-rows .fr-row:nth-child(2) .fr-art{--shape:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTk3IiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDE5NyAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xOTYuNzcxIDc5LjM2NjFWNjUuMDk5OUwxOTYuNzQ5IDY1LjEwODhMMTk2Ljc0IDM1LjkxNkMxOTYuNzQgMzIuMTg4NyAxOTYuNzQgMjcuMDg3NyAxODkuNzczIDIzLjA5MDFMMTg5LjcyIDIzLjA1OTFMMTQ5LjIxMyAxLjYzOTc5QzE0Ny4yODkgMC41NjcyNzkgMTQ1LjEwNCAwIDE0Mi45MDIgMEMxNDAuNjk5IDAgMTM4LjQ1NiAwLjU4NTAwOCAxMzYuNTExIDEuNjg0MTFMMTI1LjY3IDcuODI2N0wxMjUuNTY4IDcuNzczNTJMOTguMDU5OCAyMi41NTM4TDk4LjAwMjIgMjIuNTg0OUM5Ny44ODI1IDIyLjY1NTggOTcuNzgwNiAyMi43NDg4IDk3LjY2MDkgMjIuODE5OEw1Ny41OTY3IDEuNjM5NzlDNTUuNjczMyAwLjU2NzI3OSA1My40ODg0IDAgNTEuMjg1NyAwQzQ5LjA4MzEgMCA0Ni44NDA1IDAuNTg1MDA4IDQ0Ljg5NSAxLjY4NDExTDM0LjA1NDYgNy44MjY3TDMzLjk1MjYgNy43NzM1Mkw2LjQ0Mzk2IDIyLjU1MzhMNi4zODYzNCAyMi41ODQ5QzQuNDE4NTggMjMuNzM3MiAyLjc4MzIyIDI1LjUyMzIgMS42NDg2NiAyNy43NDhDMC41NjcyODEgMjkuODY2NCAwIDMyLjI4NjIgMCAzNC43NTA0TDAuMDM1NDU1OSA4MC44OTUxQzAuMDM1NDU1OSA4NC4yNDU2IDEuODEyNjQgODcuNDA1NiA0LjY4MDA3IDg5LjE0MjhMNC43NTA5OCA4OS4xODcyTDE4LjkyNDEgOTYuODk4NlY5Ni44Mjc3TDIzLjk1ODcgOTkuNDU1OEw2LjQ0Mzk2IDEwOC44NjVMNi4zODYzNCAxMDguODk2QzQuNDE4NTggMTEwLjA0OCAyLjc4MzIyIDExMS44MzQgMS42NDg2NiAxMTQuMDU5QzAuNTY3MjgxIDExNi4xNzcgMCAxMTguNTk3IDAgMTIxLjA2MUwwLjAzNTQ1NTkgMTY3LjIwNkMwLjAzNTQ1NTkgMTcwLjU1NiAxLjgxMjY0IDE3My43MTYgNC42ODAwNyAxNzUuNDU0TDQuNzUwOTggMTc1LjQ5OEwxOC45MjQxIDE4My4yMVYxODMuMTM5TDQ3LjYwMjggMTk4LjExNEM1MC44MjkyIDE5OS43OTggNTQuNjgwNSAxOTkuNzc2IDU3Ljg4OTIgMTk4LjA1Mkw5OC4xMDQxIDE3Ni40NDJMMTEwLjU0IDE4My4yMDVWMTgzLjEzNEwxMzkuMjE5IDE5OC4xMUMxNDIuNDQ1IDE5OS43OTQgMTQ2LjI5NiAxOTkuNzcxIDE0OS41MDUgMTk4LjA0N0wxOTAuNDA3IDE3Ni4wN0MxOTQuMzk2IDE3My45NTEgMTk2Ljc4IDE3MC4wNjUgMTk2Ljc4IDE2NS42NzNWMTUxLjQwNkwxOTYuNzU4IDE1MS40MTVMMTk2Ljc0OSAxMjIuMjIyQzE5Ni43NDkgMTE4LjQ5NSAxOTYuNzQ5IDExMy4zOTQgMTg5Ljc4MiAxMDkuMzk3TDE4OS43MjkgMTA5LjM2NkwxNzEuNjc4IDk5LjgyMzdMMTkwLjQwNyA4OS43NTg5QzE5NC4zOTYgODcuNjQwNCAxOTYuNzggODMuNzUzNyAxOTYuNzggNzkuMzYxN0wxOTYuNzcxIDc5LjM2NjFaIiBmaWxsPSIjNzI4ODk5Ii8+Cjwvc3ZnPgo=)}
.flow-rows .fr-row:nth-child(3) .fr-art{--shape:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTk3IiBoZWlnaHQ9IjE5NyIgdmlld0JveD0iMCAwIDE5NyAxOTciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xOTYuNzYyIDEwNC40NzNMMTk2Ljc1MyA4My43ODAzQzE5Ni43NTMgODEuMTM4OSAxOTYuNzUzIDc3LjUyMjUgMTkxLjgxNiA3NC42OTA1TDE5MS43ODEgNzQuNjY4M0wxNzYuNTQ0IDY2LjYxMTJMMTgxLjgyMiA0OS4wNzQyTDE4MS44MzYgNDkuMDI5OUMxODIuMjQzIDQ3LjQ2NTQgMTgyLjE3MiA0NS43NTAzIDE4MS42MjMgNDQuMDY2MkMxODEuMTA0IDQyLjQ2MTggMTgwLjE3NCA0MC45NjM5IDE3OC45NDIgMzkuNzI3NEwxNTUuNzk0IDE2LjYxOTVDMTU0LjExNCAxNC45Mzk5IDE1MS42NDEgMTQuMjQ4NSAxNDkuMzMyIDE0LjgxNThMMTQ5LjI3NSAxNC44MjkxTDEzOC4zMDYgMTguMDY4OEwxMzguMzQxIDE4LjEwNDJMMTMwLjk4NCAyMC40MTMyTDEyMi40NzEgNC41NjkyN0wxMjIuNDQ4IDQuNTI5MzhDMTIxLjYzMyAzLjEzNzc3IDEyMC4zNjUgMS45NzIxOSAxMTguNzg4IDEuMTcwMDJDMTE3LjI4NSAwLjQwMzMwMSAxMTUuNTcgMCAxMTMuODI0IDBMODEuMTE2NyAwLjAyNjU5MDJDNzguNzQxMiAwLjAyNjU5MDIgNzYuNTAzMSAxLjI4NTI0IDc1LjI3MTEgMy4zMTk0OEw3NS4yNCAzLjM2ODIzTDY5Ljc3NTUgMTMuNDE1M0g2OS44MjQzTDY2LjI5MjEgMjAuMTgyOEw0OS4xNDA3IDE1LjAyNDFMNDkuMDk2NCAxNS4wMTA4QzQ3LjUzMTkgMTQuNjAzIDQ1LjgxNjggMTQuNjc0IDQ0LjEzMjcgMTUuMjIzNUM0Mi41MjgzIDE1Ljc0MiA0MS4wMzA0IDE2LjY3MjcgMzkuNzk4MyAxNy45MDQ4TDE2LjY5MDUgNDEuMDUyNUMxNS4wMTA4IDQyLjczMjIgMTQuMzE5NCA0NS4yMDUyIDE0Ljg4MjMgNDcuNTE0MkwxNC44OTU2IDQ3LjU3MThMMTguMTM1MyA1OC41NDA3TDE4LjE3MDcgNTguNTA1M0wyMC40NTMxIDY1Ljc3OEw0LjU2OTI3IDc0LjMwOTNMNC41MjkzOCA3NC4zMzE1QzMuMTMzMzQgNzUuMTQ3IDEuOTcyMTkgNzYuNDE0NSAxLjE3MDAyIDc3Ljk5MjJDMC40MDMzMDEgNzkuNDk0NiAwIDgxLjIwOTggMCA4Mi45NTU5TDAuMDI2NTkxOSAxMTUuNjYzQzAuMDI2NTkxOSAxMTguMDM5IDEuMjg5NjggMTIwLjI3NyAzLjMxOTQ4IDEyMS41MDlMMy4zNjgyMyAxMjEuNTRMMTMuNDE1MyAxMjcuMDA0VjEyNi45NTZMMjAuNjQ4MSAxMzAuNzMyTDE1LjQxODUgMTQ4LjEwOUMxNC40NzkgMTUxLjE3MSAxNS4yMzY4IDE1NC4zMTQgMTcuNDM1IDE1Ni41MTJMMjQuNTg4MSAxNjMuNjY1VjE2My42NTJMMzkuMjMxIDE3OC4yNzdDNDEuMDk2OCAxODAuMTQzIDQzLjY1ODUgMTgyLjcgNDkuMTQ5NiAxODEuMjE1TDQ5LjE4OTQgMTgxLjIwNkw2Ni4wODgyIDE3NS45OTRMNzQuODMyMyAxOTIuMjY0Qzc2LjMzNDcgMTk1LjA5MSA3OS4wOTEzIDE5Ni43OCA4Mi4yMDI1IDE5Ni43OEg5Mi4zMTYxTDkyLjMwNzIgMTk2Ljc2N0wxMTMgMTk2Ljc1OEMxMTUuNjQxIDE5Ni43NTggMTE5LjI1NyAxOTYuNzU4IDEyMi4wODkgMTkxLjgyMUwxMjIuMTEyIDE5MS43ODVMMTMwLjQxMyAxNzYuMDgzTDE0OC4xNzUgMTgxLjQyOEMxNTEuMjM4IDE4Mi4zNjcgMTU0LjM4IDE4MS42MSAxNTYuNTc4IDE3OS40MTFMMTYzLjczMSAxNzIuMjU4SDE2My43MTRMMTc4LjMzOSAxNTcuNjE1QzE4MC4yMDUgMTU1Ljc0NSAxODIuNzYyIDE1My4xODggMTgxLjI3MyAxNDcuNjk3TDE4MS4yNjQgMTQ3LjY1N0wxNzYuMDI1IDEzMC42NzRMMTkyLjI1OSAxMjEuOTUyQzE5NS4wODcgMTIwLjQ1IDE5Ni43NzYgMTE3LjY5MyAxOTYuNzc2IDExNC41ODJWMTA0LjQ2OEwxOTYuNzYyIDEwNC40NzdWMTA0LjQ3M1oiIGZpbGw9IiM3Mjg4OTkiLz4KPC9zdmc+Cg==)}
.flow-rows .fr-row:nth-child(4) .fr-art{--shape:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjE5NyIgdmlld0JveD0iMCAwIDE4NCAxOTciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xODMuMDE5IDExMy4zMjNMMTgzLjAwMSA2Mi41MTE3QzE4My4wMDEgNTYuMDI3OCAxODIuOTk3IDQ3LjE0NjMgMTcwLjg3NiA0MC4xODgzTDE3MC43ODIgNDAuMTM1MUwxMDAuMjY3IDIuODU0MTNDOTYuOTE2NCAwLjk4ODMwOSA5My4xMTgyIDAgODkuMjgwMiAwQzg1LjQ0MjIgMCA4MS41NDIyIDEuMDE0OSA3OC4xNTYyIDIuOTMzOUw1OS4yODUzIDEzLjYyMzZMNTkuMTAzNSAxMy41MjYxTDExLjIxNzEgMzkuMjU3NkwxMS4xMTk2IDM5LjMxNTJDNy42OTgxNyA0MS4zMjI5IDQuODQ0MDQgNDQuNDI5NiAyLjg3MTg2IDQ4LjI5ODZDMC45OTI3MzkgNTEuOTg2IDAgNTYuMTk2MiAwIDYwLjQ4NjNMMC4wNjY0NzU1IDE0MC44MUMwLjA2NjQ3NTUgMTQ2LjYzOCAzLjE2NDM2IDE1Mi4xMzggOC4xNTAyMyAxNTUuMTY1TDguMjc0MzIgMTU1LjI0TDMyLjk0NjYgMTY4LjY2VjE2OC41NEw4Mi44NjczIDE5NC42MDhDODguNDgyNSAxOTcuNTQyIDk1LjE4NzkgMTk3LjUwMiAxMDAuNzY4IDE5NC41MDJMMTcxLjk2NiAxNTYuMjQ2QzE3OC45MTEgMTUyLjU1OSAxODMuMDU0IDE0NS43OTEgMTgzLjA1NCAxMzguMTQ2VjExMy4zMUwxODMuMDE5IDExMy4zMjhWMTEzLjMyM1oiIGZpbGw9IiM3Mjg4OTkiLz4KPC9zdmc+Cg==)}
/* reveal: rows rise + stagger 01 -> 04 */
.flow-rows.reveal .fr-row{opacity:0;transform:translateY(16px)}
.flow-rows.reveal.is-visible .fr-row{opacity:1;transform:none}
.flow-rows.reveal.is-visible .fr-row:nth-child(2){transition-delay:.08s}
.flow-rows.reveal.is-visible .fr-row:nth-child(3){transition-delay:.16s}
.flow-rows.reveal.is-visible .fr-row:nth-child(4){transition-delay:.24s}
/* mobile: stack; illustration drops below the text */
@media(max-width:860px){
  .fr-row{grid-template-columns:1fr;gap:14px;padding:22px 8px}
  .fr-art{height:150px}
}
@media(prefers-reduced-motion:reduce){
  .flow-rows.reveal .fr-row{opacity:1;transform:none;transition:none}
  .fr-row:hover .fr-motif .pc{transform:none}
}
html.a11y-no-motion .flow-rows.reveal .fr-row{opacity:1;transform:none;transition:none}
html.a11y-no-motion .fr-row:hover .fr-motif .pc{transform:none}


.hx--go .hx-scene::after{content:"";position:absolute;top:0;bottom:0;right:0;width:90px;background:linear-gradient(90deg,transparent,var(--paper-100));pointer-events:none;z-index:2}

/* ============ customers page ============ */
/* maker brand marquee (customers.html + partners.html) — the shared .brands component reused verbatim (see :811); only the
   scroll speed and one optical size nudge are scoped here.
   Speed: the marquee travels one .brand-set width per cycle, so speed = setWidth / duration.
   Measured at 1440px: home's channel strip is 1927px over 44s = 43.8 px/s; this set is 2666px
   (15 logos, incl. the Avgerinos nudge below), which at the shared 44s would run 60.6 px/s.
   60s gives 44.4 px/s, within 1.5% of home, so the two strips read as the same object on a
   visit that sees both. */
.brands--makers .brand-track{animation-duration:60s}
/* Avgerinos inks only 24% of the shared 240x100 canvas (next-smallest is 37%, most sit 40-90%),
   so at the common height:52px it reads as a 12px smudge. 68px is the hard cap agreed in the
   spec — it lifts the ink to ~16px without making the box taller than the strip's optical line.
   Applied to both .brand-set copies, or the two sets diverge in width and the loop snaps. */
.brands--makers .bl-lg{height:68px}
/* (the four trust reasons now render as the shared .gtee fly-in bento — see models/d2c.html;
   the old .cust-reason sweep-card + .cust-media photo rules were removed when §"Why your
   purchase is safe" was rebuilt as the icon-only guarantee bento.) */
/* --- models-inspired sections (customers-scoped; shared .hiw/.gtee CSS left untouched) --- */
/* #journey now reuses the OaaS .steps-expand / .step-card component verbatim — no journey-scoped
   overrides needed (it's a self-contained, all-CSS card grid). */
/* ===== "Who you bought from" — diagonal image-tile accordion (.caps) =====
   Full-bleed tiles with slanted seams (fixed-px parallelograms → parallel seams that stay aligned
   at any width). Hover/focus expands a tile, neighbours compress, the image de-mutes and a
   title/description/explore reveal opens. Same expand mechanic as .steps-expand, image-led. */
.caps{overflow:hidden}
.caps-head{margin-bottom:clamp(24px,3vw,40px)}
.caps-row{display:flex;--slant:46px;height:clamp(420px,50vh,560px)}
.caps-tile{position:relative;flex:1 1 0;min-width:0;display:block;overflow:hidden;text-decoration:none;color:#181818;
  margin-left:calc(var(--slant)*-1);
  clip-path:polygon(var(--slant) 0,100% 0,calc(100% - var(--slant)) 100%,0 100%);
  /* flex-grow reflows siblings by design (no transform can), so keep the window short */
  transition:flex-grow .3s var(--ease)}
.caps-tile:first-child{margin-left:0;clip-path:polygon(0 0,100% 0,calc(100% - var(--slant)) 100%,0 100%)}
.caps-tile:last-child{clip-path:polygon(var(--slant) 0,100% 0,100% 100%,0 100%)}
.caps-img{position:absolute;inset:0;z-index:0;width:100%;height:100%;object-fit:cover;
  filter:brightness(.9);transform:scale(1.03);
  /* .5s/.65s on a hover; filter is also the expensive half of the pair, so it pays for less time now */
  transition:filter .3s var(--ease),transform .32s var(--ease)}
/* theme pairs: NN-light.jpg (light render, ink text) in light theme; NN.jpg (dark render, white text) in dark theme */
.caps-img--dark{display:none}
html[data-theme="dark"] .caps-img--light{display:none}
html[data-theme="dark"] .caps-img--dark{display:block}
.caps-tile::after{content:"";position:absolute;inset:0;z-index:1;
  background:linear-gradient(180deg,rgba(251,250,247,.28) 0%,rgba(251,250,247,0) 32%,rgba(251,250,247,.08) 55%,rgba(251,250,247,.66) 100%)}
.caps-in{position:relative;z-index:2;height:100%;display:flex;flex-direction:column;justify-content:center;
  /* left padding covers the slanted clip edge so the kicker never gets cut by the seam */
  padding:0 clamp(22px,2vw,34px) clamp(26px,3vw,40px) calc(var(--slant) + clamp(18px,1.6vw,28px))}
.caps-kick{font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:rgba(24,24,24,.82);white-space:nowrap}
.caps-reveal{overflow:hidden;max-height:0;opacity:0;transform:translateY(8px);
  transition:max-height .3s var(--ease),opacity .28s var(--ease),transform .3s var(--ease)}
.caps-tile h3{font-family:var(--font-display);font-weight:800;font-size:clamp(21px,1.6vw,27px);letter-spacing:-.015em;line-height:1.1;margin:12px 0 0;color:#181818}
.caps-tile p{font-size:14px;line-height:1.5;color:rgba(24,24,24,.78);margin:10px 0 0;max-width:36ch}
.caps-explore{display:inline-flex;align-items:center;gap:8px;margin-top:16px;font-family:var(--font-mono);font-size:11.5px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:#181818}
.caps-explore svg{width:15px;height:15px;flex:none;transition:transform .3s var(--ease)}
@media(hover:hover) and (pointer:fine){.caps-tile:hover .caps-explore svg{transform:translateX(4px)}}
.caps-tile:focus-visible .caps-explore svg{transform:translateX(4px)}
.caps-sign{margin:clamp(20px,2.4vw,28px) 0 0;font-family:var(--font-mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-muted)}
/* expand on hover / keyboard focus — same :has() pattern as .steps-expand */
@media(hover:hover) and (pointer:fine){.caps-row:has(.caps-tile:hover) .caps-tile{flex-grow:.55}}
.caps-row:has(.caps-tile:focus-visible) .caps-tile{flex-grow:.55}
@media(hover:hover) and (pointer:fine){.caps-row:has(.caps-tile:hover) .caps-tile:hover{flex-grow:2.6}}
.caps-row:has(.caps-tile:focus-visible) .caps-tile:focus-visible{flex-grow:2.6}
@media(hover:hover) and (pointer:fine){.caps-tile:hover .caps-img{filter:brightness(1);transform:scale(1)}}
.caps-tile:focus-visible .caps-img{filter:brightness(1);transform:scale(1)}
/* instant press acknowledgment on pointer-down (feedback lives on the press, not the release) */
.caps-tile:active .caps-img{filter:brightness(.82);transition-duration:.1s}
/* on expand, the reveal copy sits mid-left — anchor a strong left scrim so text stays legible over any photo */
@media(hover:hover) and (pointer:fine){.caps-tile:hover::after{background:linear-gradient(90deg,rgba(251,250,247,.88) 0%,rgba(251,250,247,.6) 30%,rgba(251,250,247,.18) 58%,rgba(251,250,247,0) 80%),linear-gradient(180deg,rgba(251,250,247,.2),rgba(251,250,247,0) 45%)}}
.caps-tile:focus-visible::after{background:linear-gradient(90deg,rgba(251,250,247,.88) 0%,rgba(251,250,247,.6) 30%,rgba(251,250,247,.18) 58%,rgba(251,250,247,0) 80%),linear-gradient(180deg,rgba(251,250,247,.2),rgba(251,250,247,0) 45%)}
@media(hover:hover) and (pointer:fine){.caps-tile:hover .caps-reveal{max-height:260px;opacity:1;transform:none}}
.caps-tile:focus-visible .caps-reveal{max-height:260px;opacity:1;transform:none}
.caps-tile:focus-visible{outline:2px solid #181818;outline-offset:-4px}
/* dark theme runs the DARK renders → flip copy to white literals and scrims to near-black
   (text literals per the band-inverse convention: theme tokens would flip the wrong way here) */
html[data-theme="dark"] .caps-tile{color:#fff}
html[data-theme="dark"] .caps-kick{color:rgba(255,255,255,.88)}
html[data-theme="dark"] .caps-tile h3{color:#fff}
html[data-theme="dark"] .caps-tile p{color:rgba(255,255,255,.86)}
html[data-theme="dark"] .caps-explore{color:#fff}
html[data-theme="dark"] .caps-tile::after{background:linear-gradient(180deg,rgba(11,11,12,.30) 0%,rgba(11,11,12,0) 32%,rgba(11,11,12,.10) 55%,rgba(11,11,12,.78) 100%)}
@media(hover:hover) and (pointer:fine){html[data-theme="dark"] .caps-tile:hover::after{background:linear-gradient(90deg,rgba(11,11,12,.86) 0%,rgba(11,11,12,.58) 30%,rgba(11,11,12,.18) 58%,rgba(11,11,12,0) 80%),linear-gradient(180deg,rgba(11,11,12,.22),rgba(11,11,12,0) 45%)}}
html[data-theme="dark"] .caps-tile:focus-visible::after{background:linear-gradient(90deg,rgba(11,11,12,.86) 0%,rgba(11,11,12,.58) 30%,rgba(11,11,12,.18) 58%,rgba(11,11,12,0) 80%),linear-gradient(180deg,rgba(11,11,12,.22),rgba(11,11,12,0) 45%)}
html[data-theme="dark"] .caps-tile:focus-visible{outline-color:#fff}
@media(prefers-reduced-motion:reduce){
  .caps-tile,.caps-img,.caps-reveal,.caps-explore svg{transition:none}
  .caps-img{transform:none}
}
html.a11y-no-motion .caps-tile,html.a11y-no-motion .caps-img,html.a11y-no-motion .caps-reveal,html.a11y-no-motion .caps-explore svg{transition:none}
html.a11y-no-motion .caps-img{transform:none}
/* stacked, always-open on narrow widths — the diagonal accordion needs room */
/* Stacked, copy-open fallback. The width gate alone left a real hole: every rule that OPENS
   .caps-reveal on a pointer is inside @media(hover:hover) and (pointer:fine), and the only other
   one was this block at <=820px — so a COARSE-pointer device WIDER than 820px (iPad landscape at
   1024/1180, a touch-only kiosk) matched neither, and .caps-reveal stayed max-height:0/opacity:0
   with no way in. Tapping does not open it either: .caps-tile is an <a>, so a tap navigates away.
   That left the four reasons on this page unreadable on exactly those devices. `(hover:none)` is
   an OR arm here, not a replacement — it keys off the primary input having no hover, so a desktop
   browser and an iPad with a trackpad both keep the accordion, and only genuinely hover-less
   devices fall back. :focus-visible already had its own ungated path for keyboards. */
@media(max-width:820px),(hover:none){
  .caps-row{flex-direction:column;height:auto;--slant:0px}
  .caps-tile{margin-left:0;clip-path:none;min-height:230px}
  .caps-in{justify-content:flex-end}
  .caps-reveal{max-height:none;opacity:1;transform:none}
  .caps-img{filter:brightness(.9);transform:none}
}

/* ===== "Help with an order" — split lead + icon list (.hlp) =====
   Left: eyebrow + headline + contact CTA block. Right: the four topics as a calm vertical
   list — circled 1.8-stroke line icon, topic title, always-visible answer. No accordion,
   no hairline rows; whitespace carries the rhythm (ref-matched airy layout). */
.hlp{background:linear-gradient(rgba(240,240,240,.75),rgba(240,240,240,.75)),#f0f0f0 url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy.webp') center/cover no-repeat;background-blend-mode:normal,multiply}
html[data-theme="dark"] .hlp{background:linear-gradient(rgba(13,13,14,.75),rgba(13,13,14,.75)),#0d0d0e url('https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/noisy-dark.webp') center/cover no-repeat;background-blend-mode:normal,multiply}
.hlp-grid{display:grid;grid-template-columns:1fr 1.2fr;gap:clamp(40px,6vw,110px);align-items:start}
/* lead pins below the sticky chrome while the topic list scrolls. 38px is this column's own
   gap below the chrome; --chrome-top carries the chrome itself and already grows when the
   notification bar re-pins, so no nbar-in variant of this rule is needed (there was one; it
   restated the same arithmetic with 110px baked in). */
.hlp-lead{position:sticky;top:calc(var(--chrome-top) + 38px)}
.hlp-lead .lede{margin-top:0}
.hlp-cta{display:flex;align-items:center;gap:10px 22px;flex-wrap:wrap;margin:30px 0 0}
.hlp-mail{font-weight:600}
.hlp-sla{margin:18px 0 0}
.hlp-list{display:flex;flex-direction:column;gap:clamp(30px,3.4vw,46px);padding-top:6px}
.hlp-item{display:flex;gap:22px;align-items:flex-start}
.hlp-ico{flex:none;width:54px;height:54px;border:1px solid var(--paper-500);border-radius:50%;display:grid;place-items:center;color:var(--ink)}
.hlp-ico svg{width:24px;height:24px;display:block}
.hlp-txt{min-width:0}
.hlp-item h3{font-family:var(--font-display);font-weight:700;font-size:clamp(16px,1.3vw,18px);letter-spacing:-.01em;color:var(--ink);margin:3px 0 0}
.hlp-item p{margin:8px 0 0;font-size:14.5px;line-height:1.6;color:var(--ink-soft);max-width:52ch}
.hlp-item .biglink{min-height:0;font-size:14.5px}
/* single-column stack at mobile (breakpoint contract, ≤720px): the lead must stop sticking
   here, or the topic list scrolls under it. The two must move together — a sticky lead in a
   one-column stack pins over its own content. Was 900px, a magic number that took the
   sticky mechanic off the shared system for the whole 721–900 band. */
@media(max-width:720px){.hlp-grid{grid-template-columns:1fr;gap:40px}.hlp-lead{position:static;top:auto}}

/* AI-native hero — full black section: headline, lede, graph and CTA share one dark stage.
   Text uses theme-invariant light literals (paper/ink tokens flip in dark theme and would
   vanish on the fixed-black band — same rule as .band-inverse). */
.ainx-sec{background:#0B0B0C}
.ainx-sec .eyebrow{color:#E05252}
.ainx-sec h1.two-tone{color:#F2F1ED}
.ainx-sec h1.two-tone span:not(.word){color:#E05252}
.ainx-sec .lede{color:#C9C8C2}
.ainx-sec .ainx-graph{background:transparent;box-shadow:none}
.ainx-sec .btn-ghost{border-color:rgba(242,241,237,.55);color:#F2F1ED}
.ainx-sec .btn-ghost:hover{background:#F2F1ED;color:#111}
/* CTA framed top+bottom by hairline dividers in the graph's wire style */
.ainx-sec .ainx-cta{margin-top:clamp(28px,4vw,44px);padding:clamp(22px,3vw,30px) 0;align-items:center;
  border-top:1px solid rgba(242,241,237,.16);border-bottom:1px solid rgba(242,241,237,.16)}

/* AI-native "stack" band — brand cube-monogram art (ai-stack-bg.webp, 24KB, baked from
   tpl.jpg) behind FROSTED cards with the shared red conic-sweep hover line. The art is
   already near-black, so the scrim that suited the old bright floor photo would smother it:
   lighter at the top where the heading sits on the art's black left side, heavier at the
   bottom so the cards read against it. */
.ainx-stack{background:linear-gradient(rgba(11,11,12,.58),rgba(11,11,12,.84)),url(https://img.tpl.one/websites/tplgr/v2/assets/images/backgrounds/ai-stack-bg.webp) center/cover no-repeat}
.ainx-stack .card{position:relative;overflow:visible;border-radius:12px;
  background:rgba(255,255,255,.06);backdrop-filter:blur(12px) saturate(1.25);-webkit-backdrop-filter:blur(12px) saturate(1.25);
  border:1px solid rgba(255,255,255,.16);box-shadow:0 12px 44px -26px rgba(0,0,0,.65);
  transition:border-color .2s var(--ease),transform .2s var(--ease)}
@media(hover:hover) and (pointer:fine){.ainx-stack .card:hover{border-color:rgba(255,255,255,.42);transform:translateY(-3px)}}
.ainx-stack .card:focus-within{border-color:rgba(255,255,255,.42);transform:translateY(-3px)}
.ainx-stack .card::after{content:"";position:absolute;inset:-1px;padding:1px;border-radius:12px;pointer-events:none;--sweep:0deg;
  background:conic-gradient(from 135deg,transparent 0 calc(180deg - var(--sweep)),#E24238 calc(180deg - var(--sweep)) calc(180deg + var(--sweep)),transparent calc(180deg + var(--sweep)));
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;
  transition:--sweep 280ms cubic-bezier(.2,0,0,1)}
@media(hover:hover) and (pointer:fine){.ainx-stack .card:hover::after{--sweep:180deg}}
.ainx-stack .card:focus-within::after{--sweep:180deg}
@media(prefers-reduced-motion:reduce){.ainx-stack .card,.ainx-stack .card::after{transition:none}.ainx-stack .card:hover,.ainx-stack .card:focus-within{transform:none}}
html.a11y-no-motion .ainx-stack .card,html.a11y-no-motion .ainx-stack .card::after{transition:none}
html.a11y-no-motion .ainx-stack .card:hover,html.a11y-no-motion .ainx-stack .card:focus-within{transform:none}

/* ————— Cursor companion ring + red flood page transition —————
   The OS cursor stays visible; this ring trails it (JS drives transform per frame —
   transform is deliberately NOT in the transition list) and grows over interactives.
   When the surface underneath is too close to the ring's own colour, tpl.js adds
   .crsr--paper / .crsr--ink so the ring never vanishes into a red band.
   It carried a second concentric scroll gauge from 2026-08-10 until 2026-08-13, when the gauge
   moved to button.to-top — see the retirement note below .crsr--ink.
   NAMED .crsr, not .cur: the models comparison table marks its current column with .cur
   (see .mx td.cur), and a bare .cur rule setting position:fixed/34px/opacity:0 matched those
   cells too. mark() only applies it when the hash id resolves to a real <th>, which it
   currently never does, so this never fired in production — but it would have removed a whole
   column from the table the moment those anchors moved onto the header cells.
   Internal link clicks flood red from the click point; the next page arrives behind
   html[data-pt]::after, which contracts when tpl.js flips the attribute to "out".
   Spec: docs/superpowers/specs/2026-07-27-cursor-red-flood-transition-design.md */
/* left/top were re-animated alongside width/height on every grow, putting four layout properties on
   the pointer's hot path. transform:translate(-50%,-50%) recentres the ring at any size for free, so
   left/top are now static and drop out of the transition. tpl.js writes the individual `translate`
   property (which composes BEFORE `transform`, so the centring is not scaled away), keeping pointer
   tracking compositor-only; width/height still animate, but only on the brief hover grow. */
/* --crsr-c is the ring's one tone. It used to be three separate border-color declarations;
   hoisting it to a custom property means the border, the gauge arc and the gauge track all
   retone together from a single line when retone() decides red is unreadable on the surface
   underneath. The 160ms border fade survives the indirection — a transition interpolates the
   COMPUTED value, and border-color still recomputes red→paper even though it resolves via a var
   (the custom property itself jumps instantly, which is fine; nothing transitions on it). */
.crsr{--crsr-c:var(--red-ink);position:fixed;left:0;top:0;width:34px;height:34px;box-sizing:border-box;transform:translate(-50%,-50%);border:1.5px solid var(--crsr-c);border-radius:50%;pointer-events:none;z-index:500;opacity:0;will-change:translate;transition:opacity 160ms var(--ease),width 200ms var(--ease),height 200ms var(--ease),border-color 160ms linear}
.crsr.on{opacity:1}
.crsr--hov{width:60px;height:60px}
.crsr--paper{--crsr-c:var(--paper-0)}
.crsr--ink{--crsr-c:var(--ink)}

/* — the scroll gauge that used to live here (.crsr-sp / .crsr-trk / .crsr-arc, 2026-08-10) is
   RETIRED as of 2026-08-13; it now prints on button.to-top's border, see "scroll gauge on the
   button" up in the back-to-top block. Recorded rather than silently deleted because the reason
   is a rule worth not relearning: the gauge inherited this ring's gating — `(pointer: fine)` and
   not-reduced-motion — which is fine for a pointer flourish and wrong for a readout of page
   position, the one piece of state a phone visitor tracks just as hard as a desktop one.
   Two findings from that build survive the move and are honoured by the new rules: an SVG stroke
   beats the house conic-gradient+mask arc (@property --sweep, see .gtee-card) because a conic
   gradient's leading edge stair-steps at this width and its transitionable angle is the exact
   opposite of what a gauge wants; and the stroke needs counter-scaling whenever the viewBox is
   drawn at a size other than its own units. Both are re-derived, with new numbers, on .tt-ring. */
.pt-flood{position:fixed;border-radius:50%;background:var(--red);pointer-events:none;z-index:480;transform:scale(0)}
html[data-pt]::after{content:"";position:fixed;inset:0;background:var(--red);z-index:480;pointer-events:none;clip-path:circle(75vmax at 50% 50%);transition:clip-path 620ms cubic-bezier(.4,0,.2,1),opacity 180ms var(--ease)}
/* "in" — ORPHANED since 2026-08-04. It was a transparent staging state for the loader's exit to fade
   the cover in through, and that fade was removed when the loader itself became the same var(--red)
   as the cover, making the handoff invisible and the 180ms fade dead time. Nothing writes "in" any
   more: every data-pt write in tpl.js is '' or 'out'. Kept rather than deleted only because this
   file is under concurrent edit; safe to remove in a quiet pass. */
html[data-pt="in"]::after{opacity:0}
html[data-pt="out"]::after{clip-path:circle(0px at 50% 50%)}
@media (prefers-reduced-motion:reduce){.crsr,.pt-flood,.pt-wait{display:none}html[data-pt]::after{display:none}}
html.a11y-no-motion .crsr,html.a11y-no-motion .pt-flood,html.a11y-no-motion .pt-wait{display:none}
html.a11y-no-motion[data-pt]::after{display:none}

/* Slow-hop waiting state. The flood covers the viewport in ~500ms; if the next document still
   has not committed 800ms after the request went out, tpl.js adds .is-waiting and this fades in
   an indeterminate rail on the bottom edge — the same 2px full-bleed rail the first-load screen
   uses, so a slow navigation reads as the same system and not as a hang. Deliberately
   indeterminate: nothing here can know how far along a document request is, and a fake
   percentage would be the one thing worse than no percentage.
   A SIBLING of .pt-flood on <body>, never a child — the flood carries transform:scale(N), which
   makes it the containing block for fixed-position descendants, so a nested rail would be both
   positioned against the circle and scaled by the same N. */
.pt-wait{position:fixed;left:0;right:0;bottom:0;height:2px;background:rgba(255,255,255,.24);
  opacity:0;transition:opacity 260ms var(--ease);pointer-events:none;z-index:481;overflow:hidden}
.pt-wait.is-on{opacity:1}
.pt-wait i{display:block;height:100%;width:38%;background:#fff;transform:translateX(-100%);
  animation:pt-sweep 1150ms cubic-bezier(.65,0,.35,1) infinite}
@keyframes pt-sweep{0%{transform:translateX(-100%)}100%{transform:translateX(363%)}}
@media (prefers-reduced-motion:reduce){.pt-wait i{animation:none;width:100%;transform:none;opacity:.5}}
html.a11y-no-motion .pt-wait i{animation:none;width:100%;transform:none;opacity:.5}

/* ————— Live heading rotator (.hrot) —————
   A heading clause that cycles a short list of phrases (news hero). Every phrase
   stacks in the SAME inline-grid cell, so the slot is as wide and tall as the
   longest one and nothing on the page reflows as they swap; the swap itself is a
   fade plus a short vertical slide (no overflow clip, which would cut descenders
   at the h1's 1.05 line-height). The full list also sits in .sr-only so assistive
   tech reads one finished sentence and never hears the loop; the animated slot is
   aria-hidden. With no JS or reduced motion the first phrase simply stays put. */
.hrot{display:inline-block}
.hrot-slot{display:inline-grid}
.hrot-i{grid-area:1/1;white-space:nowrap;opacity:0;transform:translateY(0.26em);transition:opacity 380ms var(--ease),transform 520ms var(--ease)}
.hrot-i.is-on{opacity:1;transform:none}
.hrot-i.is-out{opacity:0;transform:translateY(-0.26em)}
/* the cycling clause is this page's one accent: red, like the mid-page h2 second tone.
   id-scoped so it beats `.two-tone span:not(.word)` regardless of source order. */
#main h1.two-tone .hrot-i{color:var(--red-ink)}
@media (prefers-reduced-motion:reduce){.hrot-i{transition:none}}
html.a11y-no-motion .hrot-i{transition:none}

/* ════════════ ICON OPTICAL WEIGHT — the 160-viewBox line-icon families ════════════
   Promoted sitewide 2026-07-29 after review on d2c (was briefly gated behind
   body.ico-legible; that flag is gone).
   .hiw-ico, .gtee-ico, .mf-ico and .bico are all drawn in a 160-unit viewBox at
   stroke-width 1.5. Stroke scales WITH the artwork, so the RENDERED weight is
   1.5 x display / 160 — 0.98px at the 104px desktop size, 0.60px at the 64px mobile step
   size, 0.53px in a 56px bento cell. Sub-pixel strokes are why these read pale and frail
   rather than simply small, and shrinking the icons for the mobile bento made it worse.
   The wrong fix is a bigger stroke-width: that fattens the line at large sizes and turns a
   precise isometric drawing into a soft one. Instead hold a CONSTANT OPTICAL WEIGHT.
   non-scaling-stroke takes the stroke out of the viewBox transform, so stroke-width is read
   in CSS pixels and 1.35 stays 1.35 at every size. Note the authored number goes DOWN (1.5 to
   1.35) while the rendered weight goes UP everywhere. Geometry is untouched: nothing redrawn.
   Deliberately NOT applied to .tile-icon or .bcell-ico — those are 24-unit viewBoxes whose
   1.8 stroke already renders near 1.65px, and non-scaling-stroke would thin them.
   Inner groups carrying their own stroke-width (the 0.88 detail group) keep it, so they stay
   proportionally lighter than the main contour, just crisp. */
/* .hiw-ico and .bico are wrapper DIVs, .gtee-ico and .mf-ico are the svg itself — so the two
   wrappers need `svg` appended. Setting stroke-width on the wrapper does nothing useful: the
   value would inherit, and an inherited value LOSES to the svg's own stroke-width="1.5"
   presentation attribute, which is exactly how .bico kept rendering at 1.5px. */
.hiw-ico svg,.bico svg,.gtee-ico,.mf-ico{stroke-width:1.35}
.hiw-ico svg *,.bico svg *,.gtee-ico *,.mf-ico *{vector-effect:non-scaling-stroke}
/* belt and braces: any inner element that re-declares the base 1.5 gets normalised too, while
   the deliberately lighter/heavier groups (0.88, 2.5) keep their own authored weight */
.hiw-ico svg [stroke-width="1.5"],.bico svg [stroke-width="1.5"],
.gtee-ico [stroke-width="1.5"],.mf-ico [stroke-width="1.5"]{stroke-width:1.35}
/* Dashed elements are the one exception. non-scaling-stroke takes the STROKE out of the
   viewBox transform but leaves stroke-dasharray inside it, so the dashes stretch relative to
   the artwork: the 4-dash motion trail on step 01 collapsed to a single stub and the orbit
   ellipse on step 04 ran long. Keep these on the normal scaling stroke so their rhythm
   survives, and reach the same ~1.35px optical weight through a heavier authored width
   instead (2.5 x 86/160 = 1.34px desktop, 3 x 72/160 = 1.35px mobile). The attribute selector
   carries enough specificity (0-2-2 vs 0-1-2) to beat the blanket rule above. */
.hiw-ico svg [stroke-dasharray],.bico svg [stroke-dasharray],
.gtee-ico [stroke-dasharray],.mf-ico [stroke-dasharray]{vector-effect:none;stroke-width:2.5}
@media(max-width:720px){
  .hiw-ico svg [stroke-dasharray],.bico svg [stroke-dasharray],
  .gtee-ico [stroke-dasharray],.mf-ico [stroke-dasharray]{stroke-width:3}
}

/* ===== case-study fact row (.cs-facts, case-studies/*.html) =====
   One hairline mono row of partnership facts under the case-study hero lede.
   HISTORY: until 2026-08-03 this block also carried .cs-mast/.cs-mark -- a deliberately
   timeless logo-only masthead (no photo, no wash, no pin). Reversed by owner call: the
   dna-filters page now runs the standard .hero.hero-photo pattern (partner product photo,
   dark scrim), which also opts it into the hero pin at :950 like every other photo hero.
   The retired .cs-mast/.cs-mark rules left with the markup in the same change.
   Red discipline unchanged: the .eyebrow is the single red mark; the h1's second clause
   stays muted per the house h1.two-tone rule (:151/:156). */
.cs-facts{display:flex;flex-wrap:wrap;align-items:baseline;list-style:none;margin:clamp(26px,3.4vw,38px) 0 0;padding:0;
  font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted)}
.cs-facts li+li::before{content:"\00b7";margin:0 12px;color:var(--paper-500)}
/* Inside the dark hero the theme tokens above are wrong twice over: --ink-muted flips
   near-white only in dark theme, and --paper-500 flips dark -- but the hero scrim is fixed
   near-black in BOTH themes, so the literals must be theme-invariant (same rule as
   .band-inverse). White at .75 over the scrim's darkest end clears 4.5:1 for the 12px row. */
.hero .cs-facts{color:rgba(255,255,255,.75)}
.hero .cs-facts li+li::before{color:rgba(255,255,255,.42)}
@media(max-width:520px){
  /* stacked rows read better than a middot run that wraps mid-pair on a phone */
  .cs-facts{display:grid;gap:8px}
  .cs-facts li+li::before{display:none}
}

/* ===== partner lockup (.cs-lock, case-studies/passadena.html) =====
   The partner's mark beside the marketplace's, on a plate rather than bare paper.
   --paper-fixed / --hairline-fixed are the deliberate part, and it is the rule .sfc-sign
   already documents at :5743: the Passadena mark is solid black ink and Amazon's is #221f1f,
   so a plate on the theme-following --paper-0 would swallow both in dark theme. A FIXED white
   plate puts them on the same white in either theme, and no dark-theme filter has to exist
   (compare the one at :6106, which is what the alternative costs).
   The art is a single SVG (case-studies/passadena-amazon-lockup-v2.svg), so no flex row here has
   to hold two marks of different heights in optical balance -- that balance is baked into the
   file's own coordinates and moves only when the file is rebuilt.
   Capped at 820px: at full .wrap width the two marks stand too far apart to read as a pair. */
.cs-lock{max-width:820px;margin:clamp(24px,3vw,34px) 0 0}
.cs-lock-plate{background:var(--paper-fixed);border:1px solid var(--hairline-fixed);border-radius:4px;
  padding:clamp(16px,2.6vw,30px) clamp(18px,3.4vw,44px)}
.cs-lock-plate img{display:block;width:100%;height:auto}

/* ===== brands.html - the portfolio page (logo wall + the six categories) =====
   The wall is a plain list of the marks the repo holds artwork for, on paper-0 tiles so a
   mark's own background never fights the page. object-fit:contain plus a fixed box means a
   wide wordmark and a square badge occupy the same cell without either being cropped.
   Marks arrive as a mix of SVG (the customers.html marquee set), PNG (ScanGauge) and WebP
   (the winter marks off img.tpl.one), so the cell cannot assume a format or an aspect. */
/* Per-cell borders with a real gap, NOT a hairline-coloured grid background showing through the
   gaps. The background trick draws a clean table, but auto-fill leaves the last row short at most
   widths and the leftover track paints as a solid grey slab (measured: a 29-mark wall at 1440
   leaves 6 empty slots). Bordered cells have no tail state to get wrong at any column count. */
.brand-wall{list-style:none;margin:0;padding:0;display:grid;gap:10px;
  grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
.bw-cell{background:var(--paper-0);border:1px solid var(--hairline);border-radius:4px;
  display:flex;align-items:stretch;min-height:124px;text-align:center}
/* The cell's padding lives on .bw-face, not on .bw-cell, so that when the face IS a link the
   whole tile is the hit target rather than just the logo inside it — a 118px mark in a 150px
   cell otherwise leaves a dead border the pointer falls into. Both forms exist: a.bw-face where
   we could verify the manufacturer's URL, span.bw-face where we could not, and the two must be
   visually identical, so the padding/gap/centring is declared once here for both. */
/* Top-aligned, not centred: the logo slot is a fixed height (see .bw-cell img), so starting the
   column at the top puts every name at the same offset even in a row a two-line name has made
   taller. Centring would re-introduce the drift there, on those rows only, which is the hardest
   kind to spot. With a full row of one-line names the two are identical anyway - the content
   fills the cell. */
.bw-face{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;
  gap:10px;padding:22px 14px;border-radius:4px;color:inherit;text-decoration:none}
a.bw-face:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}
@media(hover:hover) and (pointer:fine){a.bw-face:hover{background:var(--paper-200)}}
/* Sub-section heads. The wall is grouped by the page's product categories, in the same order as
   #categories below, plus two groups that are deliberately NOT product categories: the
   case-study customers and the three the page's own footnote already says are unassigned. A
   category with no artwork yet (Homewares) has no group here at all. Keep the two sections in
   agreement — if a brand moves category down there, move its mark up here. */
.bw-group+.bw-group{margin-top:34px}
.bw-group-h{display:flex;align-items:baseline;gap:12px;margin:0 0 14px;font-size:16px;
  letter-spacing:-.01em}
.bw-group-n{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-muted);font-weight:400}
/* the global img{max-width:100%} would zero a viewBox-only SVG here, so the box is explicit.
   height is generous because the WebP marks off img.tpl.one are 250x250 canvases with the mark
   sitting well inside their own padding: at height:40px the artwork rendered ~24px wide and read
   as a smudge. A wide wordmark is capped by max-width instead, so it never outgrows the cell.
   max-height went 58 -> 72 on 2026-08-28: the two caps are not symmetric, so a wide mark
   (ratio ~2.4) hits max-width and fills the cell while a SQUARE 250x250 mark hits max-height
   and used only 58 of the 118px available — Nordica, Halti, Eisbaer, Hestra, Lobster,
   Bataleon and BRBL all measured 58x58 against a 20.1%% average fill for the wide ones.
   Raising the height cap moves only marks with ratio under 118/72; the wide ones are still
   width-capped and did not change.
   A FIXED BOX, not two caps (2026-08-31): with max-width/max-height the box shrank to the
   artwork, so a flat wordmark measured 118x11 and a square mark 118x72, and since .bw-face
   centres the column the NAME under it landed anywhere in a 44px band - 55 different offsets
   across the 87 cards, which read as 87 cards each aligned to nothing. Pinning both axes makes
   every logo slot the same 118x72 and puts every name at the same height.
   scale-down, NOT contain: the caps this replaces could only ever shrink a mark, and contain
   also grows one - it took JT Sprockets (114x55) and BSB (115x55), the only two marks smaller
   than the slot on both axes, and blew them up ~3.5% into soft edges. scale-down is contain
   capped at natural size, which is the old min(1, 118/w, 72/h) exactly. All 87 marks measured
   pixel-identical to the two-cap rendering afterwards.
   Both axes are explicit for the reason .brand-track records at :1906 - a viewBox-only SVG has
   no intrinsic size, so any `auto` here resolves against nothing and collapses the mark. */
.bw-cell img{width:118px;height:72px;object-fit:scale-down;
  filter:grayscale(1);opacity:.72;transition:filter 200ms var(--ease),opacity 200ms var(--ease)}
.bw-cell span{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-muted);line-height:1.3}
@media(hover:hover) and (pointer:fine){
  .bw-cell:hover img{filter:grayscale(0);opacity:1}
}
/* dark theme: most marks are black-on-transparent, so they vanish on a dark tile. Invert them
   and drop the grayscale, which is the same treatment the customers.html marquee uses.
   grayscale(1) is NOT optional on EITHER line. invert() on its own rotates the hue of every
   coloured mark, and the hover line used to run without it: ScanGauge's red "Scan" came back
   CYAN and its white "Gauge" went black-on-black, so half the wordmark left the tile
   (reproduced by render, 2026-08-31). THE RULE FOR EVERY LOGO ON THIS SITE: an invert may
   only move a mark along the black-white axis — black to white, white to black. The moment a
   brand colour changes hue the row is showing a logo that does not exist. */
html[data-theme="dark"] .bw-cell img{filter:invert(1) grayscale(1) brightness(1.6)}
html[data-theme="dark"] .bw-cell:hover img{filter:invert(1) grayscale(1) brightness(1.9)}
/* The marks that need no inverting at all, because they were drawn FOR a dark ground. Measured
   2026-08-31 by rasterising all 87 marks in the register, compositing each over this tile
   (--paper-0 dark = #1A1A1C) with NO filter, and taking the share of its ink that still clears
   3:1 there. Five pass, and they are the only ones that do:
       Mammut 95%  ·  ScanGauge 94%  ·  DNA 91%  ·  Elmon 88%  ·  Guess Jeans 45%
   Guess Jeans is the judged one: the 55% that fails is the triangle's black FILL, not ink, and
   the teal outline plus the white lettering carry the mark (checked by render). Everything
   below it drops off a cliff — Vatopedi 35%, then Bilstein 33% and nothing else above 25% —
   so the boundary is not a threshold anyone has to tune.
   Inverting these five is exactly what the note above forbids: it turned ScanGauge's white
   half black and its red half cyan. They take the wall's monochrome rest state instead
   (grayscale, like every other cell in both themes) and reveal their TRUE colours on hover,
   which is what the light theme already does one rule up. The reads figure is luminance-based,
   so a mark that clears 3:1 in colour clears it in grayscale too; brightness() lifts the
   greyed rest a touch because grayscale leaves ScanGauge's red half at ~30% luminance.
   The other three walls (hermivo, teraseka, tpl-parts) are subsets of this register, so the
   src selectors cover them with nothing left over. */
html[data-theme="dark"] .bw-cell img[src*="scangauge"],
html[data-theme="dark"] .bw-cell img[src*="guess-jeans"],
html[data-theme="dark"] .bw-cell img[src*="mammut"],
html[data-theme="dark"] .bw-cell img[src*="customers/dna"],
html[data-theme="dark"] .bw-cell img[src*="customers/elmon"],
html[data-theme="dark"] .bw-cell img[src*="eisbar"],
html[data-theme="dark"] .bw-cell img[src*="nordica"],
html[data-theme="dark"] .bw-cell img[src*="hestra"]{filter:grayscale(1) brightness(1.25)}
html[data-theme="dark"] .bw-cell:hover img[src*="scangauge"],
html[data-theme="dark"] .bw-cell:hover img[src*="guess-jeans"],
html[data-theme="dark"] .bw-cell:hover img[src*="mammut"],
html[data-theme="dark"] .bw-cell:hover img[src*="customers/dna"],
html[data-theme="dark"] .bw-cell:hover img[src*="customers/elmon"],
html[data-theme="dark"] .bw-cell:hover img[src*="eisbar"],
html[data-theme="dark"] .bw-cell:hover img[src*="nordica"],
html[data-theme="dark"] .bw-cell:hover img[src*="hestra"]{filter:none;opacity:1}
/* Eisbaer, Nordica and Hestra joined that list on 2026-08-31, when the three of them arrived as
   real vectors on a transparent ground (they had been white PLATES as WebP): 78%, 87% and 52% of
   their ink clears 3:1 on this tile with no filter at all, measured the same way.
   The three that arrived as vectors and are still PLATES — Bilstein, Fischer and Loeffler are a
   blue/yellow block, a yellow square and a red square — do NOT join it. Un-inverting a plate
   does not free a mark, it drops a bright rectangle on a dark page: Fischer greyed and lifted
   measures 0.9 luminance across the whole cell. So they keep the inverted treatment at rest,
   where the plate goes dark and its knockout lettering goes light, and reveal their true colours
   only on the cell the pointer is actually on. Same two-step as the light theme, and the only
   moment the wall shows a saturated block is the moment someone asked for it. */
@media(hover:hover) and (pointer:fine){
  html[data-theme="dark"] .bw-cell:hover img[src*="bilstein"],
  html[data-theme="dark"] .bw-cell:hover img[src*="fischer"],
  html[data-theme="dark"] .bw-cell:hover img[src*="loffler"]{filter:none;opacity:1}
}
/* Halti and BRBL are the last two marks in this register still shipping as an OPAQUE WHITE
   PLATE (a 250x250 WebP that is 91% near-white pixels). invert() takes that plate to #000, which
   is DARKER than the tile under it (--paper-0 in dark is #1A1A1C), so the cell read as a black
   rectangle with a logo inside it while every other cell read as a bare silhouette (owner call,
   2026-08-31). mix-blend-mode:screen deletes the plate instead: screen leaves the backdrop
   untouched wherever the source is black, and passes the inverted lettering through at its own
   value. The blend group is the tile itself, which is opaque, so nothing behind the wall leaks in.
   Only these two need it. Every other plate in the register became a transparent vector on
   2026-08-31, and their inverted grounds land mid-grey, not black, where screen would LIFT the
   cell instead of clearing it. THE REAL FIX IS A TRANSPARENT SVG FOR BOTH: delete this rule the
   day the files land, do not extend it to a third mark. */
/* The blend itself now lives with the asset at :217, not here, because the same plate showed up
   as a box in LIGHT too the moment a.bw-face:hover swapped the tile to --paper-200 (#EBE9E3):
   a component-scoped fix would have had to be written twice per theme and again for the next
   component. Nothing is needed in this block for those two beyond the default invert above. */

/* Multi-column, not grid. The register is lopsided by nature — Fashion names 47 against
   Tactical's 1 — and in a grid every ROW is as tall as its tallest card, so Fashion's 673px
   left a ~250px band of dead white beside Automotive and Outdoor, and align-items:start fixed
   the stretch but not the band. Columns pack each card directly under the one above it, so the
   ragged heights cost nothing. `3 300px` keeps the old responsive behaviour: at most three
   columns, none narrower than 300px. Reading order becomes column-major, which is what a packed
   layout implies and is fine for a reference list; the register's own order is preserved down
   each column. break-inside:avoid is what stops a card being split across a column boundary. */
.bcat-grid{columns:3 300px;column-gap:20px}
.bcat-grid>.bcat{break-inside:avoid;margin:0 0 20px}
.bcat{background:var(--paper-0);border:1px solid var(--hairline);border-radius:4px;padding:24px}
/* a rule under the head, so the card reads as title / register rather than one soft block.
   padding-bottom carries the line clear of the baseline row; the old 8px margin becomes the
   space BELOW the line, so the note keeps its distance from it. */
.bcat-h{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding-bottom:12px;margin-bottom:14px;border-bottom:1px solid var(--hairline)}
.bcat-h h3{font-size:20px;letter-spacing:-.01em}
.bcat-n{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--red-ink);white-space:nowrap}
.bcat-note{font-size:14px;color:var(--ink-soft);margin:0 0 16px}
.bcat-list{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}
.bcat-list li{font-size:13px;line-height:1.2;padding:6px 10px;border:1px solid var(--hairline);
  border-radius:3px;color:var(--ink)}
.bcat-empty{font-size:13px;color:var(--ink-muted);font-style:italic;margin:0}
.bcat-foot{margin:28px 0 0;font-size:13.5px;color:var(--ink-soft)}

/* ===================================================================================
   MOBILE FULL-SCREEN HERO (≤720px) — the hero IS the first screen
   -----------------------------------------------------------------------------------
   Owner direction 2026-08-06: on a phone the hero must fill the whole first screen and
   the CTAs must be reachable at scroll 0 without a single pixel of scrolling.

   Measured before (CDP, true-px viewports set to each device's real 100svh): 32 of the
   33 hero pages pushed their CTAs past the fold at the 360x560 floor, by +35 to +275px.
   The hero never tracked the viewport at all — it was min-height:clamp(480px,66vh,720px)
   plus the generic `section{padding:48px 0}`.

   THE UNIT IS svh, NOT vh OR dvh, and that is the whole point:
     · vh  == lvh on a phone (toolbars hidden), so a vh-sized hero is TALLER than what
       you can actually see at scroll 0 and the CTA hides under the browser toolbar.
     · dvh resizes the hero while you scroll — the toolbar collapse would animate the
       whole hero, which is visible jank on every page.
     · svh is the viewport with the toolbars SHOWING, i.e. exactly what you see at
       scroll 0, which is what "no scrolling to reach the CTA" means.
   Consequence, accepted deliberately: once you scroll and the toolbar collapses, the
   viewport grows and a sliver of the next section appears under the hero. That is the
   correct trade — the alternative is dvh's resize-on-scroll.

   THE HERO IS PULLED UP UNDER THE CHROME. "Fills the screen" means edge to edge, so the
   bars float over the photo rather than sitting above it in their own strip. The hero
   gets margin-top:-(chrome) and padding-top:+(chrome), so the BOX starts at viewport 0
   while the TEXT still starts clear of the bars. box-sizing:border-box (set globally)
   makes 100svh the total, so hero bottom lands exactly on the fold.

   NO JS IS NEEDED for the bars to become legible again on scroll: `.abar.scrolled`
   already fires at y>8 and frosts the bar, and the nbar is parked off-screen by
   `html.nbar-fix` past y>39. Both states are keyed here so the chrome is transparent
   ONLY while it is over the hero. Do not add a scroll listener for this.
   =================================================================================== */

/* ---- 1. The hero's own inline styles, lifted to classes ---------------------------
   These were `style="margin:28px 0 36px"` and `style="display:flex;gap:16px;
   flex-wrap:wrap"` inline on all 32 hero pages, which no media query can reach. Values
   are byte-identical to the inline ones they replace — desktop is unchanged by design
   (verified by 1440x900 pixel diff on index/partners/faq/amazon/elmon/d2c/brands). */
.hero-lede{margin:28px 0 36px}
.hero-lede--tight{margin:28px 0 0}   /* brands + the six case studies: a .cs-facts row follows,
                                        and on the case studies a .hero-cta row before it */
.hero-cta{display:flex;gap:16px;flex-wrap:wrap;margin:0}
/* The 36px above a hero's CTA row is the NORMAL .hero-lede's bottom margin, not anything .hero-cta
   owns - it is margin:0 on purpose so a hero can sit it anywhere. The tight lede zeroes that margin
   because a .cs-facts row follows and brings its own top margin, so when the six case studies gained
   a CTA row between the two (2026-08-19) it sat flush against the lede: measured 0px against 36px on
   every other hero. Restored on the adjacency, which leaves .hero-cta's own margin alone. */
.hero-lede--tight + .hero-cta{margin-top:36px}

/* ---- 2. How much chrome sits above the hero at scroll 0 ---------------------------
   --chrome-top (declared in the token block at the top of this file) is the FIXED
   chrome and deliberately excludes the nbar. This is a different measure and needs its
   own token: everything in FLOW above the hero at scroll 0, which is what the hero has
   to be pulled up past. nbar (39, its own border included) + abar (--abar-h + its 1px
   border) + the breadcrumb bar where a page has one.
   --nbar-h is republished by tpl.js from a real offsetHeight, so this resolves correctly
   with JS on or off. --crumb-h can only be a constant because of rule 3 below. */
:root{--crumb-h:0px; --hero-top:calc(var(--nbar-h) + var(--abar-h) + 1px + var(--crumb-h))}
html:has(.crumb-bar){--crumb-h:41px}
/* ...and the bar has to BE that height from the first paint, not once the rules that build it
   out of 8+24+8+1 arrive. Both of those live OUTSIDE this critical range — .crumb-bar's
   padding+border at ~5628 and .crumb's 11.5px type plus `.crumb a{min-height:24px}` at
   ~5624-5632 — so between the critical paint and tpl.css landing the bar is one inherited
   16px line: 25.6px. Everything below it, main included, starts 15.4px too high and then
   drops. Measured on team/brands/faq/history at 390/834/1440, all identical: mainTop
   137.6 -> 153. That single drop was the largest CLS contributor on the site.
   Pinning the box to the very token the hero math already trusts makes the two agree by
   construction — they can no longer disagree about how tall the bar is. box-sizing is
   border-box globally (line ~197, inside this range), so 41px here is the same 41px the
   loaded state computes, and nothing moves when tpl.css arrives.
   Scoped to the :has() that sets the token, NOT bare .crumb-bar: where :has() is unsupported
   --crumb-h stays 0px, and a bare selector would then collapse the bar to height:0. This way
   the rule simply never matches there and the bar keeps its natural height. */
html:has(.crumb-bar) .crumb-bar{height:var(--crumb-h)}

/* ---- 3. The breadcrumb must stop wrapping ----------------------------------------
   It wrapped to two lines on narrow screens — 69px on partners/cross-docking/skroutz/
   bestprice/zalando/aboutyou/walmart/allegro and the long case studies, 41px elsewhere.
   A hero pulled up by a variable amount cannot be expressed in CSS, so the bar is
   pinned to one line: every crumb keeps its text, the row scrolls horizontally inside
   its own bar (contained, so the PAGE never scrolls sideways) and the current page
   truncates. Height is then a constant 8 + 24 + 8 + 1 = 41px. */
@media(max-width:720px){
  .crumb-bar .crumb{flex-wrap:nowrap;overflow-x:auto;gap:8px;
    scrollbar-width:none;-ms-overflow-style:none;overscroll-behavior-x:contain}
  .crumb-bar .crumb::-webkit-scrollbar{display:none}
  .crumb-bar .crumb>*{flex:none}
  .crumb-bar .crumb>[aria-current]{flex:0 1 auto;min-width:0;overflow:hidden;
    text-overflow:ellipsis;white-space:nowrap}
}

/* ————— CRITICAL-CSS RANGE: sections 2, 3 and 4 above/here travel together ——————————————————
   site/tools/build-critical-css.mjs copies from the "---- 2." banner down to just before the
   "---- 5." banner into the render-blocking tpl-critical.css. Do not reorder or re-title those
   banners without updating BLOCKS in the builder — it anchors on the comment text, and it will
   refuse to write rather than emit a broken cut.
   All three sections are needed and none is sufficient alone. 4 (below) pulls the hero up by
   calc(-1 * var(--hero-top)) and sizes it to 100svh; 2 DEFINES --hero-top, and an undefined
   custom property makes the whole margin-top declaration invalid at computed-value time, so the
   hero silently falls back to margin 0 and sits 137.6px low; 3 pins the breadcrumb to the single
   line that makes --crumb-h:41px actually true, without which the bar is 25.6px and everything
   below it moves again.
   Measured at 390px. Before any hoist: CLS 0.2954 values / 0.2807 team / 0.2771 career /
   0.2687 about / 0.2654 customers / 0.2526 index — six of seven pages over Google's 0.25 "poor"
   line, `main` the shift source every time. With section 4 alone: 0.13-0.24, source now the hero
   itself. With 2-4 together: see the session log for the final figures. */
@media(max-width:720px){
  /* ---- 4. The hero fills the screen ----------------------------------------------
     Selector set is the site's own definition of "a hero" — the same one the sticky-pin
     gate uses (#main.hero / #main.art-hero / body.chan-page #main), so a page added
     later inherits this by using the house hero patterns. .rs-hero joins it by hand:
     contact's statement hero is a hero in every sense except the class name, and the
     owner brief was "every hero on the site". */
  #main:is(.hero,.art-hero,.rs-hero),
  body.chan-page #main{
    margin-top:calc(-1 * var(--hero-top));
    min-height:calc(100vh);          /* pre-svh fallback: same as today's behaviour */
    min-height:100svh;
    /* Both paddings scale with the SCREEN, not the width: the tightest target is not the
       narrowest device but the shortest one (375x553 has 7px less budget than 360x560).
       Fixed padding spent the same 24px on both and left seven pages a few px over at
       553. svh-relative gives a short screen its px back and a tall one more air. */
    padding-top:calc(var(--hero-top) + clamp(2px,0.8svh,10px));
    padding-bottom:clamp(10px,2svh,26px);
    display:flex;                    /* .art-hero and chan-page #main were plain blocks */
    align-items:center;
  }
  #main:is(.hero,.art-hero,.rs-hero)>.wrap,
  body.chan-page #main>.wrap{width:100%}

  /* The art-directed crops must stop driving the height, or the box is whatever the
     ratio says and the fold means nothing. object-fit:cover (.hero-bg) and center/cover
     (the ::before art) keep filling the new box; only the visible slice changes. */
  .hero-photo.hero-multi,
  .art-hero--chan,
  .art-hero--google,.art-hero--zalando{aspect-ratio:auto}

  /* The bars are earlier siblings and mostly static, so without this the hero we just
     pulled up paints OVER them. */
  .crumb-bar{position:relative;z-index:40}
}

/* ---- 5. Chrome goes transparent while it is over the hero -------------------------
   Keyed to the two states that already exist rather than a new scroll listener:
     · .abar:not(.scrolled)  — .scrolled lands at y>8, where the frosted-glass recipe
       takes over and ink text is legible again.
     · html:not(.nbar-fix)   — past y>39 the nbar is parked off-screen entirely; when it
       is re-pinned by nbar-in it is over page content, never the hero, so it must keep
       its paper background.
   Scoped to pages whose #main IS the hero, so nothing else on the site is touched. */
@media(max-width:720px){
  html:not(.nbar-fix) body:has(#main:is(.hero,.art-hero,.rs-hero)) .nbar,
  html:not(.nbar-fix) body.chan-page:has(#main) .nbar{
    background:transparent;border-bottom-color:transparent}
  body:has(#main:is(.hero,.art-hero,.rs-hero)) .abar:not(.scrolled),
  body.chan-page:has(#main) .abar:not(.scrolled){
    background:transparent;border-bottom-color:transparent}
  body:has(#main:is(.hero,.art-hero,.rs-hero)) .crumb-bar,
  body.chan-page:has(#main) .crumb-bar{
    background:transparent;border-bottom-color:transparent}
}

/* ---- 6. …and goes WHITE over the dark heroes -------------------------------------
   Only #main.hero — every .hero variant is dark (photo + scrim, .hero-flat's --inverse,
   the slides, .ai-hero's neural canvas). .art-hero and the chan-page hero are the
   ink-on-paper family: they stay on the default ink, which is correct on their wash.
   Colours are fixed literals, not paper/ink tokens: those flip with the theme and this
   band is dark in BOTH themes (same rule as .band-inverse). */
@media(max-width:720px){
  html:not(.nbar-fix) body:has(#main.hero) .nbar{color:rgba(255,255,255,.92)}
  html:not(.nbar-fix) body:has(#main.hero) .nbar-arr,
  html:not(.nbar-fix) body:has(#main.hero) .nbar-region-caret{color:rgba(255,255,255,.72)}
  html:not(.nbar-fix) body:has(#main.hero) .nbar .lang-btn,
  html:not(.nbar-fix) body:has(#main.hero) .nbar-region>summary{color:#fff}
  html:not(.nbar-fix) body:has(#main.hero) .nbar .theme-switch .ico{color:#fff}
  /* .nbar-login declares its own `color` in its base rule (line ~275), so unlike
     .nbar-arr above it does NOT inherit the .nbar white and must be named explicitly. */
  html:not(.nbar-fix) body:has(#main.hero) .nbar-login{color:#fff;border-color:rgba(255,255,255,.4)}
  html:not(.nbar-fix) body:has(#main.hero) .nbar .lang-btn:hover,
  html:not(.nbar-fix) body:has(#main.hero) .nbar-region>summary:hover{
    color:#fff;background:rgba(255,255,255,.14)}
  html:not(.nbar-fix) body:has(#main.hero) .nbar-login:hover{
    color:#fff;background:rgba(255,255,255,.14)}

  body:has(#main.hero) .abar:not(.scrolled) .abar-burger{color:#fff}
  /* The wordmark is #181818 + #ab0000; the reverse bake is #fff + #ab0000. content:url()
     swaps what is RENDERED without touching src, so tpl.js's own dark-theme src swap
     (which sets the same file) still works and the alt text is untouched. */
  body:has(#main.hero) .abar:not(.scrolled) .abar-logo img{
    content:url("../logos/tpl-logo-reverse.svg")}

  body:has(#main.hero) .crumb-bar .crumb{color:rgba(255,255,255,.72)}
  body:has(#main.hero) .crumb-bar .crumb a{color:rgba(255,255,255,.88)}
  body:has(#main.hero) .crumb-bar .crumb a:hover{color:#fff}
  body:has(#main.hero) .crumb-bar .crumb [aria-current]{color:var(--red-ink-inverse,#ff8a8a)}
}

/* ---- 6b. focus rings for that white chrome ----------------------------------------
   The default :focus-visible outline is --ink (dark), which vanishes on a dark hero
   photo the same way the un-styled text did above (2.4.7 / 1.4.11). Same scope as the
   block above; #fff to match how each control renders there (some carry it directly,
   .nbar-msg inherits the .nbar white, the logo gets it from the reverse bake). */
@media(max-width:720px){
  html:not(.nbar-fix) body:has(#main.hero) .nbar .lang-btn:focus-visible,
  html:not(.nbar-fix) body:has(#main.hero) .nbar .theme-switch:focus-visible,
  html:not(.nbar-fix) body:has(#main.hero) .nbar .nbar-msg:focus-visible,
  html:not(.nbar-fix) body:has(#main.hero) .nbar .nbar-login:focus-visible{outline-color:#fff}
  body:has(#main.hero) .abar:not(.scrolled) .abar-burger:focus-visible,
  body:has(#main.hero) .abar:not(.scrolled) .abar-logo:focus-visible{outline-color:#fff}
  body:has(#main.hero) .crumb-bar .crumb a:focus-visible{outline-color:#fff}
}

/* ---- 7. A top scrim so the white chrome has something to sit on ------------------
   The hero scrims are all LEFT-to-right washes (see .hero-photo / .hero-slides), tuned
   for the text column. They say nothing about the top strip where the logo, burger and
   breadcrumb now sit, and that strip is the brightest part of several photos. One
   shared ::after covers every .hero variant instead of redeclaring four gradients.
   z-index 1 = the scrim/grain plane, under .hero-content's z-index 2. */
@media(max-width:720px){
  /* Full height, not just the chrome band. Two jobs in one gradient:
     TOP  — a strong cap so the white logo, burger, nbar and breadcrumb have something to
            sit on (that strip is the brightest part of several photos).
     BODY — a flat floor under the text. Every .hero scrim is a 90deg LEFT-to-right wash
            tuned for a desktop text column; at phone width the text runs edge to edge, so
            the wash's light right end sits directly under it. That exact lesson is already
            written up on .hero-photo--soft (2026-07-30): the fix is a gentle fade onto a
            high FLOOR, not a fade toward transparent. Same shape, applied vertically.
            Driven by the 12px #ef7676 eyebrow, the only run that failed: white h1/lede
            clear 4.5:1 on the wash alone, red needs the background below L≈0.033. */
  .hero::after{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
    background:linear-gradient(180deg,
      rgba(8,8,10,.84) 0,
      rgba(8,8,10,.76) calc(var(--hero-top) * .55),
      rgba(8,8,10,.60) var(--hero-top),
      rgba(8,8,10,.30) calc(var(--hero-top) + 78px),
      rgba(8,8,10,.30) 100%)}
  .hero-flat::after{display:none}   /* already flat near-black; a scrim only muddies it */

  /* The grain overlay carries a dissolve-to-light gradient tuned for a hero whose bottom
     edge met the next section. On a full-screen hero that dissolve lands mid-screen, where
     it does two bad things: a grey haze band above the fold, and it lifts the background
     under the red eyebrow (measured bg rgb(65,60,71) on index, so the 12px #ef7676 label
     came out at 3.83:1 against a 4.5 floor). Damped hard here. .hero-photo's .35 and
     .hero-flat's .14 are the same specificity, so source order carries this. */
  .hero::before{opacity:.12}
}

/* ---- 8. Content compression, so it actually fits the box -------------------------
   min-height only guarantees the hero is AT LEAST the screen. If the content overflows
   that box the hero grows and the CTA drops below the fold again, so the type and
   spacing have to come down to fit the tightest target (360x560, the small-Android
   floor: 100svh 560 − 153 chrome − 6 − ~24 padding = ~377px of content).
   Everything here is scoped to the hero — the site's body type is untouched. */
@media(max-width:720px){
  #main:is(.hero,.art-hero,.rs-hero) .eyebrow,
  body.chan-page #main .eyebrow{margin-bottom:8px}

  /* the ≤640 hero step-down, extended to the full mobile range and tightened a little */
  #main:is(.hero,.art-hero,.rs-hero) h1.two-tone,
  body.chan-page #main h1.two-tone{font-size:clamp(27px,7.4vw,33px);line-height:1.08}

  #main:is(.hero,.art-hero,.rs-hero) .lede,
  body.chan-page #main .lede{font-size:16px;line-height:1.45}
  #main:is(.hero,.art-hero,.rs-hero) .hero-lede,
  body.chan-page #main .hero-lede{margin:14px 0 16px}
  #main:is(.hero,.art-hero,.rs-hero) .hero-lede--tight{margin:14px 0 0}
  /* same restoration as the desktop rule, at the 16px this breakpoint gives a normal hero lede */
  #main:is(.hero,.art-hero,.rs-hero) .hero-lede--tight + .hero-cta{margin-top:16px}

  #main:is(.hero,.art-hero,.rs-hero) .hero-cta,
  body.chan-page #main .hero-cta{gap:10px}
  /* 44px is WCAG 2.5.8 AA's floor and this is the one place on the site worth spending it */
  #main:is(.hero,.art-hero,.rs-hero) .hero-cta .btn,
  body.chan-page #main .hero-cta .btn{padding:11px 20px;font-size:14.5px;min-height:44px}

  /* The whole slider furniture block that used to live here (dock geometry, thumb/arrow
     shrink set, and the <480px rule that dropped the arrows to win back 10px against the CTA
     block at the 360x560 floor) went with the picker itself on 2026-08-06 — see the removal
     note beside .hero-slides .hs-slide. .hero-pause outlived that cluster with its own
     mobile corner offset, then was removed itself on 2026-08-11 — nothing left to override
     here. */

  /* The case-study fact row stacks below 520px (see the .cs-facts block) and four facts
     cost ~100px of a 383px content budget. Kept STACKED on purpose: as one row the four
     mono labels run ~660px against a 320px measure, so a scroll row would hide most of
     them behind a gesture nothing signals. Tightened instead — 17px back, which is what
     vatopedi (the only 4-fact page) was over by. */
  .hero .cs-facts{margin-top:18px}
  @media(max-width:520px){ .hero .cs-facts{gap:5px} }
}

/* ═══════════════════════════════════════════════════════════════════════════════
   CASE-STUDY RAIL (#case-studies, partners.html) — 2026-08-06
   A horizontal card rail in place of the 3-column .blog-grid: the section head, then a
   row carrying the lede on the left and the pager on the right, then cards that run off
   the right edge of the viewport so "there is more" needs no label to say it.

   THE TRACK STILL CARRIES .blog-grid, and that is load-bearing rather than legacy: the
   reading-panel IIFE binds its click/keydown delegation to
   document.querySelector('.blog-grid') (tpl.js:2475), so renaming the track would
   silently kill every card's drawer. The grid properties are simply overridden —
   display:flex makes grid-template-columns inert, so the 900/600 column breakpoints at
   :3827 need no cancelling either.

   SOURCE ORDER IS THE OVERRIDE. .cs-rail and .blog-grid are one class each, .cs-card and
   .post likewise, so every rule below wins only because it sits at the end of the file.
   Do not hoist this block above :3826.

   The cards are deliberately UNBOXED — no surface, no border, no radius. The art plate,
   the meta row and the base hairline do the framing, and that is what makes a rail read
   as a rail rather than as a row of boxes that happens to scroll. One consequence worth
   stating: the site's default red conic-sweep card outline has no box left to draw on
   here, so hover is a red rule growing along the card's own base hairline instead. Same
   gesture, no border required.
   ═══════════════════════════════════════════════════════════════════════════════ */
/* contains the rail's right bleed. clip, not hidden: hidden would make the section a
   scroll container and take the page's scroll anchoring with it. */
#case-studies{overflow-x:clip}
.cs-head-row{display:flex;align-items:center;justify-content:space-between;gap:clamp(24px,4vw,64px);margin-top:22px}
.cs-head-row .lede{margin:0;max-width:54ch}
.cs-nav{display:flex;gap:12px;flex:none}
/* [hidden] is a UA rule and loses to .cs-nav's own display, so it needs restating. The
   pager is authored hidden and revealed by JS only when the rail overflows: with no JS,
   or on a viewport wide enough to hold every card, it never appears and the rail is
   still scrollable by touch and trackpad. */
.cs-nav[hidden]{display:none}
.cs-arrow{width:44px;height:44px;flex:none;display:inline-flex;align-items:center;justify-content:center;
  border:1px solid var(--hairline);border-radius:9999px;background:var(--paper-0);color:var(--ink);cursor:pointer;
  transition:border-color 140ms var(--ease),opacity 140ms var(--ease)}
.cs-arrow:hover{border-color:var(--ink)}
.cs-arrow:focus-visible{outline:2px solid var(--red);outline-offset:2px}
.cs-arrow[disabled]{opacity:.3;cursor:default;pointer-events:none}
.cs-arrow svg{width:19px;height:19px}

/* The track. margin-right pulls the right edge out to the viewport edge; padding-right
   hands the last card its gutter back at scroll-end, so the run finishes rather than
   ending flush against the glass. */
.cs-rail{display:flex;gap:clamp(18px,2vw,26px);margin:clamp(38px,5vw,56px) 0 0;
  overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;scroll-behavior:smooth;
  scrollbar-width:none;-webkit-overflow-scrolling:touch}
.cs-rail{margin-right:calc(50% - 50vw);padding-right:clamp(20px,4vw,48px)}
.cs-rail::-webkit-scrollbar{display:none}
/* 76vw, not 80: at 390px the wider card left only a 40px sliver of its neighbour showing,
   which is too thin at the edge of a phone to read as "there is more". 76vw gives 56px. */
.cs-rail .reveal{flex:0 0 min(352px,76vw);scroll-snap-align:start}
html.a11y-no-motion .cs-rail{scroll-behavior:auto}

/* clip mode - the fix for the wheel trap over the rail (2026-08-23, ADR-023) ------------
   Owner report: with the cursor over the case-study cards the page scroll STOPS. Measured over
   the preview server, one property at a time: a perfectly vertical wheel scrolls the page fine,
   but a gesture carrying ANY deltaX - every trackpad swipe, every tilt wheel - moves the page 0px.
   Chromium latches such a gesture to the nearest horizontal SCROLL CONTAINER and then discards
   the vertical delta it cannot use.

   scroll-snap-type:none, overscroll-behavior-x:auto, scroll-behavior:auto and overflow-x:hidden
   were each measured alone and in combination: page delta stayed 0 for all of them. hidden fails
   for the same reason auto does - it still makes a scroll container. Only overflow-x:CLIP, which
   makes none, releases the gesture: page delta goes to the full 900px.

   Clip has no scrollport, so scrollLeft cannot move the cards. The run therefore moves on a
   transform, applied to a .cs-track wrapper the clip-rail module in tpl.js injects INSIDE the clip
   box - the transform cannot go on .cs-rail itself, which IS the clip box and would carry its own
   clipping along with it. tpl.js keeps writing rail.scrollLeft: the module shims that property onto
   the track's transform, so the pager, the drag module and their disabled states are untouched.

   MOUSE ONLY. Touch keeps the authored native scroller above: a touch scroll does not latch this
   way, the horizontal run is the right shape for a phone, and momentum there is the platform's. */
.cs-rail.is-clipped{overflow-x:clip;overflow-y:visible;scroll-snap-type:none;-webkit-overflow-scrolling:auto}
.cs-rail.is-clipped>.cs-track{display:flex;gap:inherit;will-change:transform}
/* The paged glide lives on the track, and only while a move is in flight - a standing transition
   would also animate the drag module's per-frame writes and leave the run lagging the pointer,
   the same trap .cs-rail's own scroll-behavior:smooth sets for scrollLeft. */
.cs-rail.is-clipped>.cs-track.is-gliding{transition:transform 420ms var(--ease-out)}
html.a11y-no-motion .cs-rail.is-clipped>.cs-track.is-gliding{transition:none}
@media(prefers-reduced-motion:reduce){.cs-rail.is-clipped>.cs-track.is-gliding{transition:none}}

/* ————— drag-to-scroll affordance (tpl.js "Drag-to-scroll for the content carousels") —————
   The grab cursor is the only thing on screen that says these rails can be pulled, so it is not
   decoration. Gated to a fine pointer: on touch the rail already scrolls natively and a grab
   cursor there would be a promise to a device with no cursor. user-select is suppressed only for
   the duration of a drag, so text stays selectable at rest. */
@media (hover:hover) and (pointer:fine){
  .cs-rail.is-draggable,.tm-car.is-draggable,.rv-strip.is-draggable{cursor:grab}
  .cs-rail.is-dragging,.tm-car.is-dragging,.rv-strip.is-dragging{cursor:grabbing;user-select:none;-webkit-user-select:none}
  .cs-rail.is-dragging *,.tm-car.is-dragging *,.rv-strip.is-dragging *{cursor:grabbing}
  .cs-rail.is-dragging img,.tm-car.is-dragging img,.rv-strip.is-dragging img{-webkit-user-drag:none}
}
@media(prefers-reduced-motion:reduce){.cs-rail{scroll-behavior:auto}}

/* card — unboxed, and paying for its own base rule */
.cs-card{background:none;border:0;border-radius:0;overflow:visible;position:relative;padding-bottom:22px}
.cs-card::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:var(--hairline)}
.cs-card::before{content:"";position:absolute;left:0;bottom:0;width:100%;height:2px;background:var(--red);
  transform:scaleX(0);transform-origin:0 50%;transition:transform 420ms var(--ease);z-index:1}
@media(hover:hover) and (pointer:fine){.cs-card:hover::before{transform:none}}
.cs-card:focus-visible::before{transform:none}
html.a11y-no-motion .cs-card::before{transition:none}
@media(prefers-reduced-motion:reduce){.cs-card::before{transition:none}}

/* One 4/3 art plate for every card kind, so every meta row, title and base hairline in
   the rail lands on the same line as its neighbours. */
.cs-card .post-media{aspect-ratio:4/3;border-radius:var(--radius-card);overflow:hidden;background:var(--paper-200)}
/* restore the logo plate's lighter tile: .cs-card .post-media (2 classes) outranks the
   base .post-media--logo background (1 class) and would otherwise grey it out */
.cs-card .post-media--logo{background:var(--paper-50)}
/* and give the mark more of the plate than the grid's 56% does. The grid tile is square;
   this one is 4/3, so 56% of a wider box left the mark floating in a field of paper next
   to full-bleed photo neighbours. Still contained, never cropped. */
.cs-card .post-media--logo img{max-width:64%;max-height:60%}

/* BSB is the one mark in this set whose SVG carries a TINY intrinsic size — 115x55, the box it
   was drawn in for the brand wall. width:auto above means an image renders at its intrinsic
   width, and the max-width cap only ever shrinks; 115px never reaches 64% of the plate, so BSB
   sat at a third of Passadena's mark on models/oaas.html (measured 115px against 371px,
   2026-08-29). Owner direction: the two are the same size. Driving the WIDTH to the same 64%
   every other mark lands on is the fix, and height:auto keeps the 115:55 ratio intact — the
   max-height above still catches it if a future plate is short enough to need it.
   THE REAL FIX IS AN ASSET, as with ScanGauge below: a case-study BSB logo drawn on the same
   generous canvas as passadena-logo-v2.svg (1778x982). Until one exists this rule is what makes
   the pair read as a pair. Scoped to .cs-card so the brand wall and the marquee, which size
   this same file by their own rules, are untouched. */
.cs-card .post-media--logo img[src$="/bsb.svg"]{width:64%;height:auto}

/* ————— .cs-more--bare — the logo plate, removed —————
   2026-08-28, OWNER DIRECTION: the white plate is not the site's language. Every other logo
   surface here — .brand-track, the .bmc channel chips, .tls-mark, the .bw-cell customer wall —
   is a BARE MARK ON PAPER at grayscale .55, flipping to full colour on hover. The rail's
   plated tile was the one place that carded its artwork, and the note at :1930 already records
   the call ("a logo surface hovers with the grayscale to colour reveal, not the card sweep").
   This is that treatment, applied to the rail on the three model pages.
   The 4/3 box STAYS. It carries no paint now, but it is what keeps every meta row, title and
   base hairline in the rail on one line - drop it and the cards ladder.
   Scoped to the modifier, not to .cs-card: the case-study pages and partners.html run the same
   component with photo cards mixed into the rail, where a plate still earns its keep. */
/* ————— .cs-grid — the same cards, laid out instead of railed —————
   2026-08-28, OWNER DIRECTION: no scroller and no arrows on the model pages. With three
   studies there is nothing to scroll TO — a rail whose content already fits is an affordance
   that lies, and the arrows it needs are two controls that never do anything. So these
   sections use a plain grid and the rail machinery never touches them: tpl.js binds its
   arrows and its drag-to-scroll to `.cs-rail`, so a different class is the whole opt-out.
   The case-study pages and partners.html keep the rail, where 5-8 cards genuinely overflow.
   Three across is the desktop shape the copy is written for. auto-fit is NOT used: with the
   wrap at its widest a minmax() track count lands on four, which would put the fourth card
   of a six-card section on a line of its own. The count is fixed and the breakpoints move it.
   The two :has() rules are the counts three tracks cannot seat cleanly. Two cards (B2B2C):
   three tracks would leave a visible empty cell at the end of the row, so a pair takes half
   the row each and reads as a pair. Four cards (D2C, Cross-docking): three across strands the
   fourth alone on a second line, so a 2x2 block reads as one shape instead. Both selectors
   outrank the bare .cs-grid, so both breakpoints below have to name them to take the row down. */
.cs-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(18px,2vw,26px);margin:clamp(38px,5vw,56px) 0 0}
.cs-grid:has(>:nth-child(2):last-child),
.cs-grid:has(>:nth-child(4):last-child){grid-template-columns:repeat(2,minmax(0,1fr))}
@media(max-width:980px){
  .cs-grid,.cs-grid:has(>:nth-child(2):last-child),
  .cs-grid:has(>:nth-child(4):last-child){grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media(max-width:640px){
  .cs-grid,.cs-grid:has(>:nth-child(2):last-child),
  .cs-grid:has(>:nth-child(4):last-child){grid-template-columns:minmax(0,1fr)}
}
/* The rail sized its cards with flex-basis; in the grid the track does it, so the card must
   be told to stop being a flex item and just fill its cell. */
.cs-grid>.reveal{min-width:0}

.cs-more--bare .cs-card .post-media,
.cs-more--bare .cs-card .post-media--logo{background:none;border-radius:0}
/* Bare-logo panels centre everything under the mark: the kicker chip stacks onto its own
   line above the title instead of sharing a baseline row with the place name. */
.cs-more--bare .cs-card .post-body{align-items:center;text-align:center}
.cs-more--bare .cs-card .post-kick{flex-direction:column;align-items:center;gap:4px}
.cs-more--bare .cs-card .post-media--logo img{filter:grayscale(1);opacity:.55;
  transition:filter .25s var(--ease),opacity .25s var(--ease)}
/* No scale on hover. With the plate gone there is no frame for the mark to grow inside, and
   none of the bare logo rows on this site scale - the colour reveal IS the hover. */
.cs-more--bare .cs-card .post-media--logo img{transform:none}
@media(hover:hover) and (pointer:fine){
  .cs-more--bare .cs-card:hover .post-media--logo img{filter:none;opacity:1;transform:none}
}
/* Keyboard parity: these plates sit inside a link, so the reveal has to answer :focus-visible
   too, and outside the hover gate - same pairing as .brand-set .bmc:focus-visible at :1915. */
.cs-more--bare .cs-card:focus-visible .post-media--logo img{filter:none;opacity:1}
html.a11y-no-motion .cs-more--bare .cs-card .post-media--logo img{transition:none}
@media(prefers-reduced-motion:reduce){.cs-more--bare .cs-card .post-media--logo img{transition:none}}
/* dark theme: same treatment the case-studies marquee on partners.html uses (.brand-track at
   :1861) — every mark flattened to one white silhouette at .6, lifting to full on reveal. The
   rail carries twelve logos of wildly different build: four are light-on-transparent (DNA,
   Vatopedi, ScanGauge, Elmon), seven are pure-black artwork that measures 0 luminance and
   disappears on --band, and Inter Cars ships as an opaque red plate. No single "show the real
   colours" rule can serve all three; monochrome is what makes them one row instead of twelve
   accidents, and it is already the site's answer for this exact set of brands. */
html[data-theme="dark"] .cs-more--bare .cs-card .post-media--logo img,
html[data-theme="dark"] .cs-more--bare .cs-card .post-media--logo img[src*="scangauge"],
html[data-theme="dark"] .cs-more--bare .cs-card .post-media--logo img[src*="vatopedi"]{
  filter:brightness(0) invert(1);opacity:.6}
/* The two attribute selectors above are not decoration: ScanGauge's light-ground exception
   below and Vatopedi's plated-tile lift at :6173 both outrank the bare selector on specificity
   and would otherwise punch a hole in the row. Restated at the row's own values. */
@media(hover:hover) and (pointer:fine){
  html[data-theme="dark"] .cs-more--bare .cs-card:hover .post-media--logo img,
  html[data-theme="dark"] .cs-more--bare .cs-card:hover .post-media--logo img[src*="scangauge"],
  html[data-theme="dark"] .cs-more--bare .cs-card:hover .post-media--logo img[src*="vatopedi"]{
    filter:brightness(0) invert(1);opacity:1}
}
html[data-theme="dark"] .cs-more--bare .cs-card:focus-visible .post-media--logo img,
html[data-theme="dark"] .cs-more--bare .cs-card:focus-visible .post-media--logo img[src*="scangauge"],
html[data-theme="dark"] .cs-more--bare .cs-card:focus-visible .post-media--logo img[src*="vatopedi"]{
  filter:brightness(0) invert(1);opacity:1}
/* brands/inter-cars-v2.svg needs no keying (see the marquee note), but it is still a PLATE (red square + navy field, knockout lettering), and
   this rail's brightness(0) invert(1) would flatten it to a solid rectangle with no wordmark
   (verified by render, 2026-08-31). The logo wall's own dark treatment is the right one: invert
   puts the plate light and the lettering dark, exactly as BILSTEIN reads two cells over. */
html[data-theme="dark"] .cs-more--bare .cs-card .post-media--logo img[src*="inter-cars"]{
  filter:invert(1) grayscale(1) brightness(1.6);opacity:.6}
@media(hover:hover) and (pointer:fine){
  html[data-theme="dark"] .cs-more--bare .cs-card:hover .post-media--logo img[src*="inter-cars"]{
    filter:invert(1) grayscale(1) brightness(1.6);opacity:1}
}
html[data-theme="dark"] .cs-more--bare .cs-card:focus-visible .post-media--logo img[src*="inter-cars"]{
  filter:invert(1) grayscale(1) brightness(1.6);opacity:1}

/* ScanGauge is the one mark in this rail drawn for a DARK ground: "Scan" is red but "Gauge"
   is WHITE lettering with a drop shadow. The plate was hiding that — on paper, grayscale +
   .55 opacity left the white half as nothing but its own shadow (verified by render,
   2026-08-28). brightness() multiplies, so it darkens white to a readable grey where opacity
   only fades it: .6 at rest matches the weight of its neighbours, and .72 on hover keeps the
   red reading as red while "Gauge" stays legible. Same shape of exception as
   .bmc--aboutyou at :1810, and for the same reason: one asset that does not match the set.
   THE REAL FIX IS AN ASSET — a light-ground ScanGauge lockup. Until one exists this rule is
   what keeps half the wordmark on screen. */
.cs-more--bare .cs-card .post-media--logo img[src*="scangauge"]{
  filter:grayscale(1) brightness(.6);opacity:.7}
@media(hover:hover) and (pointer:fine){
  .cs-more--bare .cs-card:hover .post-media--logo img[src*="scangauge"]{
    filter:brightness(.72);opacity:1}
}
.cs-more--bare .cs-card:focus-visible .post-media--logo img[src*="scangauge"]{
  filter:brightness(.72);opacity:1}
/* Photographs drop the brand-shape mask in the rail: the four silhouettes are near-1:1 and
   a 4/3 plate stretches the notches flat (the same reason the grid squares its tiles at
   :3886). The .blog-grid shape cycle at :3901 still governs the 3-column grid on
   news.html — untouched. */
.cs-card .post-media--photo{-webkit-mask-image:none;mask-image:none;background:var(--paper-200)}

/* body — no card surface to pad against, so the text sits directly under the plate */
.cs-card .post-body{padding:18px 0 0;gap:12px;color:var(--ink-soft)}
/* BASELINE, not center. The chip and the place sit on one line but are not the same
   object: the chip is 10.5px inside 5px/4px of asymmetric padding, the place is bare 11px.
   align-items:center centres the two BOXES, which left the chip's text sitting ~0.9px
   below the place's — invisible per card, obvious once six cards stack it down a rail.
   Baseline aligns the TEXT, which is the thing the eye reads, and costs no height: the
   row is still as tall as the chip. */
.cs-card .post-kick{display:flex;flex-wrap:wrap;align-items:baseline;gap:9px;color:var(--ink-soft)}
/* the category becomes a filled chip and drops its red: this section already spends its
   red on the eyebrow, the heading's second clause, the hover rule and the marquee band */
.cs-card .post-kick b{font-weight:500;font-size:10.5px;color:var(--ink-soft);
  background:var(--paper-200);border-radius:9999px;padding:5px 11px 4px}
.cs-card h3{font-size:clamp(19px,1.5vw,21px);line-height:1.26;letter-spacing:-.012em}
@media(max-width:820px){
  .cs-head-row{flex-direction:column;align-items:flex-start;gap:20px}
  .cs-head-row .lede{max-width:none}
}

/* ── .pd-tight — the drawer's reading rhythm, trialled on partners.html 2026-08-06 ──
   ROOT CAUSE this fixes: .pf-sec is a bare <section>, so it was inheriting the global
   section{padding:96px 0;border-bottom} at :187. Two sections of the SAME article
   therefore sat 192px apart, on top of the h3's own 36px top margin, and the piece read
   as a stack of separate pages rather than one continuous read. Here each section pays
   its own 24px either side of the closing hairline, and the paragraph rhythm carries the
   rest. Kept from the old rule: the hairline itself, which is what closes the article
   above .pf-cta (see the note at :3966).
   The 400px .pf-hero plate is an owner call from 2026-08-04 and is deliberately not
   touched. Promote these rules onto .pf-* and delete the class once news.html signs off. */
.pd-tight .pf-layout{padding:34px 0 72px}
.pd-tight .pf-body{max-width:648px}
.pd-tight .pf-body h2{margin:0 0 14px}
.pd-tight .pf-lede{font-size:18.5px;line-height:1.52;margin:0 0 26px}
.pd-tight .pf-sec{padding:0 0 24px}
.pd-tight .pf-sec h3{font-size:20px;margin:24px 0 10px}
.pd-tight .pf-sec p{font-size:16px;line-height:1.7;margin:0 0 13px}
.pd-tight .pf-sec p:last-child{margin-bottom:0}
.pd-tight .pf-gal{margin:2px 0 4px}
.pd-tight .pf-cta{margin:28px 0 0}
/* the ≤640px bottom sheet sets padding-top:16px on .pf-layout (:3989) with one class, so the
   two-class rule above was silently overriding it. Hand the sheet its own top back. */
@media(max-width:640px){.pd-tight .pf-layout{padding-top:16px}}

/* ═══════════════════════════════════════════════════════════════════════════════
   KEEP READING (.cs-more) — the exit at the foot of a STANDALONE case study.
   Before this, all six case-study pages ended on "Pitch your brand" + "All case studies",
   so the only lateral move between studies was back out to partners.html and re-pick.

   Deliberately NOT added to the drawer panels on partners.html: a drawer is opened FROM the
   rail, so closing it already returns you to the other studies, and a horizontal scroller
   nested inside a scrolling drawer sets two scroll axes fighting.

   Reuses .cs-rail / .cs-card wholesale rather than inventing a second card. Two additions
   are all it needs:

   overflow-x:clip — .cs-rail bleeds past the right edge by calc(50% - 50vw), which needs a
   clipping ancestor or the PAGE scrolls sideways. On partners that job belongs to
   #case-studies. clip, not hidden: hidden would make the section a scroll container and
   take the page's scroll anchoring with it.

   background — the section above is the old last section and still carries
   border-bottom:none, so with a shared ground the two would run together. A tonal step to
   --paper-50 separates the exit from the argument without editing that markup or adding a
   rule, and it reads as "you have reached the end of the story" on its own.

   The cards are LINKS here, not the drawer triggers they are on partners, so they need the
   anchor reset .post never had to carry on an <article>.
   ═══════════════════════════════════════════════════════════════════════════════ */
/* --paper-200, NOT --paper-50: the logo plate is itself --paper-50 (:5897), so a --paper-50
   ground made every tile invisible and left the marks floating in open paper. The ground has
   to sit DARKER than the plate. --paper-200 also holds in dark theme, where the plate is
   #161618 against a #0B0B0C ground and still reads as the lighter tile. */
.cs-more{background:var(--paper-200);overflow-x:clip}
a.cs-card{text-decoration:none;color:inherit}
/* Stretch the cards to equal height so the base hairlines line up across the rail. The 4/3
   plate (:5895) already aligns the art and the meta row, but a title that wraps to two lines
   still pushes its own hairline ~22px below its neighbours', because .cs-card is height:auto
   inside a .reveal that stretches without passing the height on.
   SCOPED to .cs-more on purpose: .cs-rail and .cs-card are shared with #case-studies on
   partners.html, which has the same wrap and is not mine to restyle. */
.cs-more .cs-rail .reveal{display:flex}
.cs-more a.cs-card{flex:1}

/* The single-series .gc-* growth chart lived here until 2026-08-20, when partners.html - its
   last consumer - moved onto the .gcb-* component below, so one claim is no longer drawn two
   different ways on one site (owner call). Its renderer (assets/js/growth-chart.js) and the
   throwaway preview page went with it; git history holds all three. */

/* ============ Growth charts with toggle (.gcb-*) — case-study revenue-index with benchmark ============
   Reveal is SCROLL-DRIVEN, not load-driven: growth-chart-toggle.js adds .is-in to .gcb-inner from an
   IntersectionObserver, and every entrance below is a transition gated on that class. An earlier pass
   used bare `animation:… forwards` on .gcb-h2/.gcb-cta, which (a) played while the section was still
   4 screens down, and (b) measured opacity:0 on the CTA at settle — a paying visitor could not see the
   pitch link at all. Never reintroduce a load-time animation here. What keeps the card visible when JS
   never runs is that the hidden pre-state hangs off .is-armed, a class only the renderer sets; see the
   scroll-driven entrance block further down. */
.gcb-sec{padding:clamp(80px,10vw,120px) 0}
.gcb-sec .gcb-inner{background:var(--paper-50);border:1px solid var(--hairline);border-radius:var(--radius-card);padding:clamp(28px,3.6vw,52px)}
.gcb-reg{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin:0 0 clamp(24px,3vw,38px)}
.gcb-reg-n{font-family:var(--font-mono);font-size:12px;font-weight:600;letter-spacing:.06em;color:var(--ink);white-space:nowrap}
.gcb-reg-n i{font-style:normal;color:var(--ink-muted)}
.gcb-reg-rule{flex:1 1 40px;height:1px;background:var(--hairline)}
.gcb-reg-l{font-family:var(--font-mono);font-size:12px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted)}
@media(max-width:560px){.gcb-reg{gap:8px}.gcb-reg-rule{display:none}.gcb-reg-l{flex:1 1 100%;letter-spacing:.08em}}
.gcb-main{position:relative;display:flex;flex-direction:column;gap:clamp(28px,3.4vw,44px)}
.gcb-headwrap{display:flex;flex-direction:column;align-items:flex-start;gap:18px;width:58%}
.gcb-h2{font-family:var(--font-display);font-weight:800;font-size:clamp(34px,4vw,56px);line-height:1.05;letter-spacing:-.025em;color:var(--ink);margin:0}
.gcb-h2 em{font-style:normal;color:var(--red-ink)}
.gcb-kick{font-family:var(--font-mono);font-size:12px;font-weight:600;letter-spacing:.06em;color:var(--ink-soft);margin:0;max-width:46ch;line-height:1.6}

/* stats: floated top-right on desktop, a plain 2x2 grid once the headline goes full width */
.gcb-stats{position:absolute;top:-6px;right:0;z-index:2;display:grid;grid-template-columns:1fr 1fr;width:38%;max-width:448px;background:var(--paper-50);border-top:1px solid var(--hairline);border-right:1px solid var(--hairline)}
.gcb-stat{display:flex;flex-direction:column;gap:8px;padding:20px 22px;border-bottom:1px solid var(--hairline);border-left:1px solid var(--hairline);overflow:hidden}
.gcb-stat-v{font-family:var(--font-display);font-weight:800;font-size:clamp(24px,2.2vw,34px);line-height:1.15;letter-spacing:-.02em;color:var(--ink);font-variant-numeric:tabular-nums}
.gcb-stat:first-child .gcb-stat-v{color:var(--red-ink)}
.gcb-stat-l{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.06em;color:var(--ink-muted);line-height:1.4}

/* One switch, one label that rewrites itself. role="switch" + aria-checked follows .theme-switch,
   the site's own precedent for a binary control. The track carries the SAME colour as the series it
   selects — red on for the actual bars, light grey off for the estimate — so the control states which
   half of the chart you are looking at instead of just which button is depressed. The off track is
   literally --gcb-bench-fill, the same value the grey bars are painted in, so the tie is exact.
   Neither state needs a border to be identifiable: red-ink is 7.37:1 against this card and the grey
   is 3.02:1, both clearing the 3:1 WCAG 1.4.11 asks of a control. */
.gcb-switch{display:inline-flex;align-items:center;gap:12px;background:none;border:0;padding:6px 0;min-height:44px;cursor:pointer;font:inherit;margin:0 0 14px}
.gcb-sw-track{position:relative;width:46px;height:26px;flex:0 0 auto;border-radius:999px;background:var(--gcb-bench-fill);border:0;transition:background .22s var(--ease)}
/* The knob's POSITION is what states on/off, so the knob itself has to clear 3:1 against its track
   (WCAG 1.4.11 covers the visual information identifying a control's state). A pale knob on the pale
   off-track measured 1.77:1, so it flips: --ink when off (10.1:1 light / 6.8:1 dark), --paper-0 when
   on, riding the red track (7.69:1 / 4.55:1). */
/* Geometry is solved against the 46x26 track with no border: (26-18)/2 = 4px inset, leaving
   46-4-4-18 = 20px of travel. Changing the track size or its border means redoing both numbers. */
.gcb-sw-knob{position:absolute;top:4px;left:4px;width:18px;height:18px;border-radius:50%;background:var(--ink);box-shadow:0 1px 3px rgba(0,0,0,.28);transition:transform .22s var(--ease),background .22s var(--ease)}
.gcb-switch[aria-checked="true"] .gcb-sw-track{background:var(--red-ink)}
.gcb-switch[aria-checked="true"] .gcb-sw-knob{transform:translateX(20px);background:var(--paper-0)}
.gcb-sw-txt{font-family:var(--font-mono);font-size:13px;font-weight:700;letter-spacing:.04em;color:var(--ink-muted);transition:color .22s var(--ease)}
.gcb-switch[aria-checked="true"] .gcb-sw-txt{color:var(--red-ink)}
.gcb-switch:hover .gcb-sw-txt{color:var(--ink)}
.gcb-switch[aria-checked="true"]:hover .gcb-sw-txt{color:var(--red-ink)}
.gcb-switch:focus-visible{outline:2px solid var(--red-ink);outline-offset:3px}

.gcb-basis{font-family:var(--font-mono);font-size:12px;letter-spacing:.03em;color:var(--ink-soft);margin:0 0 14px;max-width:92ch;line-height:1.65;min-height:2.6em}
.gcb-basis b{color:var(--ink);font-weight:600}
.gcb-legend{display:flex;gap:24px;flex-wrap:wrap;margin:0 0 18px}
.gcb-legend span{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.04em;color:var(--ink-soft)}
.gcb-legend i{width:13px;height:13px;flex:0 0 auto;border-radius:2px}
.gcb-legend .sw-with{background:var(--red-ink)}
/* swatch mirrors the chart exactly: same fill, no edge */
.gcb-legend .sw-bench{background:var(--gcb-bench-fill)}

/* These two ratios MUST equal the renderer's own viewH (growth-chart-toggle.js geom(): 320 desktop,
   1100 mobile). Change one without the other and the SVG stretches non-uniformly — an earlier pass
   did exactly that and turned the peak dot into an oval. The mobile ratio is portrait on purpose:
   at 980/620 the phone chart came out SHORTER in real pixels than the desktop one. */
.gcb-plot{position:relative;aspect-ratio:980/320}
@media(max-width:991px){.gcb-plot{aspect-ratio:980/1100}}
.gcb-plot:focus-visible{outline:2px solid var(--red-ink);outline-offset:4px}
.gcb-svg{width:100%;height:auto;display:block;overflow:visible}
.gcb-axis{stroke:var(--hairline);stroke-width:1}
.gcb-grid{stroke:var(--hairline);stroke-width:1;stroke-dasharray:2 3}
.gcb-yearlabel{fill:var(--ink-muted);font-family:var(--font-mono);font-weight:600;letter-spacing:.06em}
/* bench = the without-TPL estimate, always a grey ghost behind the red actual. It stays visible in
   `with` mode on purpose: the whole argument of this component is the GAP between the two, so hiding
   the comparison would throw away the point. */
/* bench = the without-TPL estimate: a light grey bar, NO STROKE (owner's call, 2026-08-12).
   With no edge, the fill alone has to carry the 3:1 that WCAG 1.4.11 wants for a graphic conveying
   data — and in the `without` view these bars ARE the only data on the canvas. That fixes a ceiling:
   solved against this card's own measured luminance, the lightest neutral that still clears 3:1 is
   about #929292 in light and #636363 in dark, so --paper-500 (#C5C3BB, 1.69:1) cannot be used bare.
   These two values sit at that ceiling — as pale as the contrast floor permits, 3.02:1 light and
   4.21:1 dark, verified by measurement. Going lighter is a real WCAG 2.2 AA regression on a site
   whose accessibility.html publicly claims AA; if the look has to be paler than this, put the edge
   back rather than dropping the ratio. Defined as a component token so both themes stay honest. */
:root{--gcb-bench-fill:#919190}
html[data-theme="dark"]{--gcb-bench-fill:#7A7A76}
.gcb-bench{fill:var(--gcb-bench-fill)}
/* --red-ink, not --red: same graphical-object 3:1 rule as .bcell-ico. --red is pinned to #AB0000 in
   every theme, which measures 2.43:1 on the dark surface this card sits on (measured) and fails
   WCAG 1.4.11 for the one element carrying the data. --red-ink flips to #E05252 in dark, 4.73:1,
   and is byte-identical to --red in light so the light rendering is unchanged. */
/* The opacity fade is slower than it looks like it needs to be, and delayed, on purpose: the bar's
   HEIGHT is animating down onto the estimate over ~620ms at the same time, and a quick fade would
   remove it before the eye can follow it there. Falling onto the grey bar is the whole point. */
.gcb-bar{fill:var(--red-ink);transition:opacity .5s var(--ease) .12s}
/* In `without`, the actual series leaves the canvas rather than lingering as a sub-3:1 ghost. The gap
   is the argument, and it is made in the DEFAULT `with` view, where solid red stands inside the grey
   bar it would have been stuck in; the switch is how a reader checks the other half. */
.gcb-plot.is-without .gcb-bar{opacity:0}
.gcb-peak{fill:var(--paper-50);stroke:var(--red-ink);stroke-width:2;opacity:0;transition:opacity .3s var(--ease);transform-box:fill-box;transform-origin:center}
.gcb-peak.is-lit{opacity:1;animation:gcbPeakPulse 1s var(--ease)}
@keyframes gcbPeakPulse{0%{transform:scale(.4)}55%{transform:scale(1.35)}100%{transform:scale(1)}}
.gcb-plot.is-without .gcb-peak{opacity:0}
.gcb-guide{stroke:var(--ink);stroke-width:1;opacity:0;pointer-events:none;transition:opacity 120ms var(--ease)}
.gcb-guide.is-active{opacity:1}
.gcb-hit{fill:transparent;cursor:crosshair}
.gcb-tip{position:absolute;top:0;left:0;transform:translate(-50%,-130%);background:var(--ink);color:var(--paper-0);font-family:var(--font-mono);font-size:12px;font-weight:600;padding:5px 10px;border-radius:4px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity 120ms var(--ease)}
.gcb-tip.is-active{opacity:1}

/* FOOT — the note and the pitch CTA share one row directly under the chart, so the ask lands on the
   reader the moment the proof finishes drawing rather than sitting above it in the headline block. */
.gcb-foot{display:flex;align-items:center;justify-content:space-between;gap:clamp(20px,3vw,48px);flex-wrap:wrap;margin:22px 0 0;padding:22px 0 0;border-top:1px solid var(--hairline)}
.gcb-note{font-size:14px;line-height:1.55;color:var(--ink-soft);margin:0;max-width:56ch;flex:1 1 320px}
/* Renders as either <a href> or <button data-pitch-form> depending on data-cta-pitch, so it carries
   the button resets (border/cursor, and a font stack a <button> would otherwise not inherit). */
.gcb-cta{display:inline-flex;align-items:center;justify-content:center;gap:14px;background:var(--red);color:#fff;text-decoration:none;border:0;cursor:pointer;font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;padding:0 26px;min-height:54px;flex:0 0 auto;transition:background .2s var(--ease),transform .2s var(--ease),box-shadow .2s var(--ease)}
.gcb-cta::after{content:"";width:18px;height:8px;flex:0 0 auto;background:currentColor;clip-path:polygon(0 3px,12px 3px,12px 0,18px 4px,12px 8px,12px 5px,0 5px)}
.gcb-cta:hover{background:var(--ink);transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.16)}
.gcb-cta:focus-visible{outline:2px solid var(--red-ink);outline-offset:3px}
.gcb-empty{font-family:var(--font-mono);font-size:12px;color:var(--ink-muted);border:1px dashed var(--hairline);padding:22px;margin:0}

@media(max-width:991px){
  .gcb-headwrap{width:100%}
  .gcb-stats{position:static;width:100%;max-width:none}
  .gcb-headwrap{order:1}.gcb-stats{order:2}.gcb-comp{order:3}
  .gcb-foot{flex-direction:column;align-items:stretch}
  /* flex-basis resolves against the MAIN axis, so the note's 320px basis becomes 320px of HEIGHT
     the moment this row turns into a column — measured as ~350px of dead space above the CTA at
     390px wide. The row layout needs the basis; the column must not inherit it. */
  .gcb-note{flex:0 1 auto}
  .gcb-cta{justify-content:center}
}

/* ── scroll-driven entrance ──────────────────────────────────────────────────
   The hidden pre-state is gated on .is-armed, which growth-chart-toggle.js sets on the element it
   just built. Two things fall out of that, both deliberate: a visitor with no JS never gets the
   pre-state at all (so the card is simply visible, no rescue rule needed), and this block does NOT
   have to ship in the render-blocking tpl-critical.css to avoid a paint-then-hide flash — the page's
   boot script only runs these scripts once tpl.css has loaded, so .is-armed can never be applied
   before the rules that define it. Keep it that way: this component sits ~2000px below the fold and
   has no business in the critical sheet. .is-in then releases the cascade on scroll. */
.gcb-inner.is-armed .gcb-reg,.gcb-inner.is-armed .gcb-h2,.gcb-inner.is-armed .gcb-kick,
.gcb-inner.is-armed .gcb-stat,.gcb-inner.is-armed .gcb-switch,.gcb-inner.is-armed .gcb-basis,
.gcb-inner.is-armed .gcb-legend,.gcb-inner.is-armed .gcb-foot{
  opacity:0;transform:translateY(14px);
  transition:opacity .6s var(--ease),transform .6s var(--ease)}
.gcb-inner.is-in .gcb-reg,.gcb-inner.is-in .gcb-h2,.gcb-inner.is-in .gcb-kick,.gcb-inner.is-in .gcb-stat,
.gcb-inner.is-in .gcb-switch,.gcb-inner.is-in .gcb-basis,.gcb-inner.is-in .gcb-legend,.gcb-inner.is-in .gcb-foot{
  opacity:1;transform:none}
.gcb-inner.is-in .gcb-reg{transition-delay:0ms}
.gcb-inner.is-in .gcb-h2{transition-delay:90ms}
.gcb-inner.is-in .gcb-kick{transition-delay:150ms}
.gcb-inner.is-in .gcb-stat:nth-child(1){transition-delay:200ms}
.gcb-inner.is-in .gcb-stat:nth-child(2){transition-delay:270ms}
.gcb-inner.is-in .gcb-stat:nth-child(3){transition-delay:340ms}
.gcb-inner.is-in .gcb-stat:nth-child(4){transition-delay:410ms}
.gcb-inner.is-in .gcb-switch{transition-delay:300ms}
.gcb-inner.is-in .gcb-basis{transition-delay:340ms}
.gcb-inner.is-in .gcb-legend{transition-delay:380ms}
.gcb-inner.is-in .gcb-foot{transition-delay:820ms}

/* Motion off, and the no-JS rescue: both must land on the settled state, never the pre-state. */
/* Motion off: land on the settled state, never the pre-state. The pre-state selectors carry
   .is-armed, so these overrides must too, or they lose on specificity and the card stays invisible
   for exactly the visitors who asked for less movement. */
@media(prefers-reduced-motion:reduce){
  .gcb-bar,.gcb-bench,.gcb-sw-track,.gcb-sw-knob,.gcb-sw-txt,.gcb-cta,.gcb-peak{transition:none}
  .gcb-peak.is-lit{animation:none}
  .gcb-inner.is-armed .gcb-reg,.gcb-inner.is-armed .gcb-h2,.gcb-inner.is-armed .gcb-kick,
  .gcb-inner.is-armed .gcb-stat,.gcb-inner.is-armed .gcb-switch,.gcb-inner.is-armed .gcb-basis,
  .gcb-inner.is-armed .gcb-legend,.gcb-inner.is-armed .gcb-foot{
    opacity:1;transform:none;transition:none}
}
html.a11y-no-motion .gcb-bar,html.a11y-no-motion .gcb-bench,html.a11y-no-motion .gcb-sw-track,
html.a11y-no-motion .gcb-sw-knob,html.a11y-no-motion .gcb-sw-txt,
html.a11y-no-motion .gcb-cta,html.a11y-no-motion .gcb-peak{transition:none}
html.a11y-no-motion .gcb-peak.is-lit{animation:none}
html.a11y-no-motion .gcb-inner.is-armed .gcb-reg,html.a11y-no-motion .gcb-inner.is-armed .gcb-h2,
html.a11y-no-motion .gcb-inner.is-armed .gcb-kick,html.a11y-no-motion .gcb-inner.is-armed .gcb-stat,
html.a11y-no-motion .gcb-inner.is-armed .gcb-switch,html.a11y-no-motion .gcb-inner.is-armed .gcb-basis,
html.a11y-no-motion .gcb-inner.is-armed .gcb-legend,html.a11y-no-motion .gcb-inner.is-armed .gcb-foot{
  opacity:1;transform:none;transition:none}

/* ═══════════════════════════════════════════════════════════════════════════════════════════
   PRINT  (added 2026-08-13)
   Until now this sheet had no @media print, @page or print-color-adjust anywhere, and the site
   leans heavily on dark grounds. Browsers print with background graphics OFF by default
   (print-color-adjust:economy), so every dark surface dropped its background while KEEPING its
   light ink: white text on white paper. Surveyed with media:print emulated across ten pages,
   the damage was measured, not guessed — 320 text elements in the red footer alone (it is on all
   46 pages), 82 in .fitx-band--photo on the model pages, 41 on models/ai-transformation.html
   (which is dark end to end), 40 in .band-inverse sections, plus the .mf-card / .aud-card /
   .step-card / .kpi / .hiw-panel--dark / .band-red / .x-tick / .x-node families.
   Two jobs only: give every dark ground back a readable ink, and stop fixed chrome printing on
   top of the content. This is not a full print design — see the note at the end. */
@media print{
  /* 1 · fixed and floating chrome. Measured as position:fixed with a real box, so each of these
     would otherwise stamp itself over the first printed page. .abar is deliberately NOT here:
     it carries the wordmark and is sticky rather than fixed, so it prints once, at the top,
     which is what a printed page wants. */
  .crsr,.skip,.to-top,.espa-bar,.a11y,.a11y-panel,.nbar,.x-rail,
  .ldr,.ms-sheet,.ms-scrim,.post-drawer,.post-scrim,.pt-wait,.pd-progress,.pd-close{
    display:none!important}

  /* 2 · nothing decorative should burn ink or hide type */
  *,*::before,*::after{
    background-image:none!important;box-shadow:none!important;text-shadow:none!important;
    animation:none!important;transition:none!important}

  html,body{background:#fff!important;color:#181818!important}

  /* 3 · every measured dark ground, and everything inside it, back to paper + ink. The
     descendant selector is the point: these components each set their own light colour on
     their own children, so neutralising only the container would leave the children white. */
  footer,footer *,
  .band-inverse,.band-inverse *,
  .band-red,.band-red *,
  .fitx-band,.fitx-band *,
  .ax-act--dark,.ax-act--dark *,
  .hiw-panel,.hiw-panel *,
  .mf-card,.mf-card *,
  .step-card,.step-card *,
  .kpi,.kpi *,
  .gtee-card,.gtee-card *,
  .x-tick,.x-tick *,
  .x-node,.x-node *,
  .vv-chf,.vv-chf *,
  body.aix,body.aix *{
    background-color:transparent!important;
    color:#181818!important;
    border-color:#C9C6BF!important}

  /* 4 · red is the one accent that survives, because it reads correctly on white and it is
     carrying meaning: the second clause of every two-tone headline, and the card CTAs. Restated
     AFTER the flattening above, and as the mid --red rather than --red-ink-inverse, which is
     mixed for dark grounds and would print washed out. */
  .two-tone span:not(.word),.aud-cta,.biglink,.mf-more{color:#AB0000!important}

  /* 4b · solid-red controls carry a WHITE label, so the economy setting drops the fill and
     leaves the label on bare paper. Measured after the flattening above: .btn-primary,
     .abar-cta, .vvb-cta and the .rv-ava reviewer initials were the only light-on-dark text
     left on any page. Reprinted as outlined rather than filled — the label becomes ink and the
     red survives as the rule, which reads correctly and costs almost no toner. */
  .btn-primary,.abar-cta,.vvb-cta,.rv-ava,button.btn-primary{
    background-color:transparent!important;color:#181818!important;
    border:1px solid #AB0000!important}

  /* 5 · sheet hygiene */
  @page{margin:14mm}
  a[href]{text-decoration:underline}
  .card,.mf-card,.step-card,.kpi,.gtee-card,figure,.post{break-inside:avoid}
  h1,h2,h3{break-after:avoid}
  img{max-width:100%!important}
}
/* Deliberately NOT attempted here, and worth its own pass if printing ever becomes a real
   requirement: printing link destinations after each href, collapsing the scrolling marquees to
   a single static row, choosing which of the 12 sales-channel heroes are worth the ink, and a
   proper cover treatment for the legal pages, which are the ones anybody actually prints. */

/* ===== B2B & Partner login front door (.gate-*) — site/login.html
   A door, not an audience page: two role doors and nothing else. Full document height is
   ~2270px because the shared site footer is ~1060px on every page, so judge this page on
   #page-main rather than the document. Specs: docs/superpowers/specs/2026-08-17-b2b-partner-
   login-front-door-design.md and the 2026-08-19 rebuild,
   recorded under "The doors, rebuilt" in
   docs/superpowers/specs/2026-08-19-b2b-coming-soon-holding-page-design.md.
   The doors are whole-card <a> elements. Everything inside is <span>, so there is exactly
   one accessible name and one tab stop per door. */
/* ————— B2B doors, two territories (.gate-*) — site/login.html, reimagined 2026-08-19.
   Two earlier passes drew cards on a page and both read as flat, because a card is a small
   object on a surface and this choice is not small: it is the only decision the page exists to
   get. So the page IS the choice. A full-bleed hard split, two inverted territories, and the
   seam between them is the platform they both open onto. You commit to a side.

   The differentiation is structural, never hue: the two halves are tonally INVERTED, the
   composition MIRRORS around the seam, and each half carries the one verb that defines that
   business at display scale. Red is still the only accent, used on three glyphs per half.

   Fixed literal tones, not paper/ink tokens, on purpose. This is an art-directed band like a
   hero image, and it must not invert when the theme flips: paper tokens go near-black in dark
   mode and would collapse the whole composition. Same reason the site's other inverse bands
   use literals.

   Type: font-variation-settings reaches Sofia Sans's real wght axis, where font-weight alone
   snaps to only three instances (measured: ink +0% to 500, +107% at 600, +141% at 800, and
   nothing further at 900). font-weight stays set as the non-variable fallback. */

.gate-wrap{padding-block:0 clamp(48px,6vw,88px)}

/* ————— hero ————— */
.gate-hero{position:relative;padding-block:clamp(30px,4.4vw,66px) clamp(26px,3.4vw,48px)}
.gate-hero-in{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:clamp(20px,4vw,72px);align-items:end}
@media(max-width:900px){.gate-hero-in{grid-template-columns:minmax(0,1fr);gap:clamp(16px,3vw,26px)}}
.gate-hero h1{margin:10px 0 0;font-family:var(--font-display);font-size:clamp(40px,6.4vw,92px);font-weight:800;font-variation-settings:'wght' 880;line-height:.94;letter-spacing:-.035em;color:var(--ink)}
.gate-hero h1 em{font-style:normal;color:var(--ink-soft)}
/* the holding page's headline is a full sentence rather than two short clauses, so it is set
   smaller: at the login page's 92px it wrapped to four lines and stranded the lede beside a
   gap. Size follows the copy, not the other way round. */
.gate-hero--soon h1{font-size:clamp(30px,4.4vw,58px);line-height:1.0;letter-spacing:-.028em}
.gate-hero .lede{margin:0;max-width:46ch}

/* verified figures only: 11 sales-channel pages exist, the schema on this page states 27
   employees and a 2017 founding, and the nav states 80+ brands. Nothing here is invented. */
.gate-figs{display:flex;flex-wrap:wrap;gap:0;margin-top:clamp(18px,2.2vw,28px);border-top:1px solid var(--hairline)}
.gate-fig{display:flex;flex-direction:column;gap:2px;padding:12px clamp(14px,1.6vw,26px) 0 0;margin-right:clamp(14px,1.6vw,26px);border-right:1px solid var(--hairline)}
.gate-fig:last-child{border-right:0;margin-right:0;padding-right:0}
.gate-fig b{font-family:var(--font-display);font-size:clamp(20px,1.9vw,27px);font-weight:800;line-height:1;letter-spacing:-.02em;color:var(--ink)}
.gate-fig span{font-family:var(--font-mono);font-size:10px;font-weight:500;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-soft)}
/* narrow: the three figures cannot share one flex row, and wrapping left a dangling divider
   after the second one. A 3-column grid keeps them on one line and drops the rules entirely. */
@media(max-width:560px){
  .gate-figs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0 10px}
  .gate-fig{padding:12px 0 0;margin-right:0;border-right:0}
  .gate-fig span{font-size:9px;letter-spacing:.09em}
}

/* --- the split. A direct child of the section, so it is already full width: no 100vw, which
   would add the scrollbar's width and make the page draggable sideways. --- */
.gate-split{display:flex;align-items:stretch;min-height:clamp(450px,62svh,720px);border-top:1px solid rgba(0,0,0,.14);border-bottom:1px solid rgba(0,0,0,.14)}
/* the split keeps fixed tones so the composition never inverts, but that means its dark half
   sits at #121214 against a #101012 page in dark mode and the band's edge disappears. The
   delineating rules are the one thing here that must follow the theme. */
[data-theme="dark"] .gate-split{border-top-color:rgba(242,241,237,.2);border-bottom-color:rgba(242,241,237,.2)}
[data-theme="dark"] .gate-seam{background:rgba(242,241,237,.2)}
@media(max-width:880px){.gate-split{flex-direction:column;min-height:0}.gate-half{flex:0 0 auto}}

/* flex-basis MUST be 0, not 50%. With two 50% bases the container has no positive free space,
   and flex-grow only distributes free space, so the hover expansion computed correctly (1.46)
   and moved nothing at all. With basis 0 the halves divide the available width by grow ratio,
   so 1.46 against 0.7 really is a 68/32 split. */
.gate-half{position:relative;display:flex;flex:1 1 0%;min-width:0;padding:clamp(26px,3.2vw,52px) clamp(20px,4vw,64px);text-decoration:none;overflow:clip;transition:flex-grow .62s var(--ease-out)}
/* overflow MUST be clip, not hidden. `hidden` makes this element a scroll container, which
   becomes the scrollport that the marks' `animation-timeline: view()` is measured against, and
   that box never scrolls, so the float silently freezes at a constant progress. `clip` clips
   the glow the same way without creating a scroll container, so view() resolves to the
   document and the parallax actually runs. */
.gate-half:focus-visible{outline:3px solid currentColor;outline-offset:-8px}

/* upstream: the dark territory. Goods arrive here. */
.gate-half--sell{background:#121214;color:#F2F1ED;--ac:#ef7676;--hair:rgba(242,241,237,.22);--soft:rgba(242,241,237,.66);justify-content:flex-start}
/* light falling in from the top edge, so the dark side reads as a material and not a swatch.
   Its opacity is what the hover lifts, because a gradient cannot be transitioned. */
.gate-half--sell::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.66;background:radial-gradient(118% 74% at 22% -6%,rgba(255,255,255,.11),transparent 62%);transition:opacity .5s var(--ease-out)}

/* downstream: the light territory. Stock leaves from here. */
.gate-half--buy{background:#F2F1ED;color:#121214;--ac:#900000;--hair:rgba(18,18,20,.16);--soft:rgba(18,18,20,.745);justify-content:flex-end}
.gate-half--buy::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.66;background:radial-gradient(118% 74% at 78% -6%,rgba(0,0,0,.07),transparent 62%);transition:opacity .5s var(--ease-out)}

/* fixed ch width so the column does not re-wrap while the half is growing under the cursor */
.gate-halfin{position:relative;z-index:1;display:flex;flex-direction:column;width:min(100%,30rem);transition:opacity .5s var(--ease-out)}
.gate-half--buy .gate-halfin{text-align:right;align-items:flex-end}

.gate-rail{display:flex;align-items:center;gap:11px;font-family:var(--font-mono);font-size:10.5px;font-weight:600;letter-spacing:.15em;text-transform:uppercase;color:var(--soft)}
.gate-rail::after{content:"";flex:1;height:1px;background:var(--hair);order:9;transition:background .4s var(--ease-out)}
/* the mirrored half moves the RULE, not the words: flex-direction:row-reverse also reversed
   the text nodes and produced "B2B client -> Outbound", which reads backwards. */
.gate-half--buy .gate-rail{justify-content:flex-end}
.gate-half--buy .gate-rail::after{order:-1}
.gate-rail svg{width:12px;height:12px;max-width:none;flex:none;color:var(--ac)}

/* the verb. The one word that separates the two businesses, at the size that admits it.
   The wrapper takes the arrival animation and the mark itself takes the scroll float, so the
   two never fight over the single `animation` property. */
.gate-markw{display:block;margin-top:auto;padding-top:clamp(24px,3.4vw,54px)}
.gate-mark{display:block;font-family:var(--font-display);font-size:clamp(58px,10.8vw,176px);font-weight:800;font-variation-settings:'wght' 900;line-height:.82;letter-spacing:-.045em;text-transform:uppercase;transform-origin:left bottom;transition:transform .5s var(--ease-out)}
.gate-half--buy .gate-mark{transform-origin:right bottom}
.gate-qual{display:block;margin-top:clamp(10px,1vw,16px);font-family:var(--font-mono);font-size:clamp(11px,1.05vw,14px);font-weight:500;letter-spacing:.2em;text-transform:uppercase;color:var(--soft)}

.gate-pts{display:flex;flex-direction:column;gap:7px;margin-top:clamp(20px,2.4vw,34px)}
.gate-pt{position:relative;padding-left:15px;font-family:var(--font-body);font-size:clamp(13.5px,1.05vw,15px);line-height:1.5;color:var(--soft)}
.gate-pt::before{content:"";position:absolute;left:0;top:.62em;width:7px;height:1px;background:var(--ac)}
.gate-half--buy .gate-pt{padding-left:0;padding-right:15px}
.gate-half--buy .gate-pt::before{left:auto;right:0}

.gate-enter{display:inline-flex;align-items:center;gap:.5em;margin-top:clamp(24px,2.8vw,40px);padding-top:clamp(16px,1.8vw,24px);border-top:1px solid var(--hair);font-family:var(--font-body);font-size:clamp(14px,1.1vw,16px);font-weight:600;letter-spacing:.01em;transition:border-color .4s var(--ease-out)}
/* the arrow stays pointing forward on both halves. Mirroring it read as "back", which is the
   opposite of what the link does; the mirror is carried by alignment instead. */
.gate-enter svg{width:18px;height:18px;max-width:none;flex:none;color:var(--ac);transition:transform .34s var(--ease-out)}

/* ————— hover: the chosen side comes forward and the other recedes. A split screen only feels
   like a choice if choosing costs the other side something. ————— */
@media(hover:hover) and (pointer:fine){
  a.gate-half:hover{flex-grow:1.46}
  a.gate-half:hover::before{opacity:1}
  a.gate-half:hover .gate-mark{transform:scale(1.028)}
  a.gate-half:hover .gate-rail::after{background:var(--ac)}
  a.gate-half:hover .gate-enter{border-color:var(--ac)}
  a.gate-half:hover .gate-enter svg{transform:translateX(7px)}
  .gate-split:has(a.gate-half:hover) a.gate-half:not(:hover){flex-grow:.7}
  .gate-split:has(a.gate-half:hover) a.gate-half:not(:hover) .gate-halfin{opacity:.38}
  .gate-split:has(a.gate-half:hover) a.gate-half:not(:hover)::before{opacity:.3}
}
a.gate-half:active .gate-mark{transform:scale(1.01)}

/* --- the seam: the platform both doors open onto --- */
.gate-seam{position:relative;flex:0 0 1px;background:rgba(0,0,0,.14)}
@media(max-width:880px){.gate-seam{flex:0 0 auto;height:1px}}

/* --- below the split --- */
.gate-below{padding-top:clamp(26px,3.2vw,44px);display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,1fr);gap:clamp(20px,3vw,52px)}
@media(max-width:780px){.gate-below{grid-template-columns:minmax(0,1fr)}}
.gate-foot h2{font-family:var(--font-body);font-size:16px;font-weight:600;color:var(--ink);margin:0 0 7px}
.gate-foot p{font-size:15px;line-height:1.6;color:var(--ink-soft);margin:0}
.gate-foot a{color:var(--red-ink);text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px}
.gate-safe{display:flex;align-items:flex-start;gap:9px;margin:14px 0 0;font-size:13.5px;line-height:1.55;color:var(--ink-muted)}
.gate-safe svg{width:16px;height:16px;max-width:none;flex:none;margin-top:1px}

/* --- arrival: the seam draws, then each territory's content rises. Runs once on load. --- */
@keyframes gate-seam-in{from{transform:scaleY(0)}to{transform:scaleY(1)}}
@keyframes gate-rise{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}
.gate-seam{transform-origin:top;animation:gate-seam-in .66s var(--ease-out) both}
.gate-halfin>*{animation:gate-rise .74s var(--ease-out) both}
.gate-rail{animation-delay:.16s}
.gate-markw{animation-delay:.24s}
.gate-qual{animation-delay:.3s}
.gate-pts{animation-delay:.36s}
.gate-enter{animation-delay:.42s}

/* ————— scroll float. Native scroll-driven animation, so no vendor JS is added and the work
   stays off the main thread. The two marks drift at different rates, which is what reads as
   parallax: identical rates would just look like the whole band sliding. ————— */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    html:not(.a11y-no-motion) .gate-mark{
      animation:gate-float linear both;
      animation-timeline:view();
      animation-range:cover 0% cover 100%;
    }
    html:not(.a11y-no-motion) .gate-half--buy .gate-mark{animation-name:gate-float-slow}
    @keyframes gate-float{from{translate:0 34px}to{translate:0 -34px}}
    @keyframes gate-float-slow{from{translate:0 15px}to{translate:0 -15px}}

    html:not(.a11y-no-motion) .gate-figs,
    html:not(.a11y-no-motion) .gate-below{
      animation:gate-enter-rise linear both;
      animation-timeline:view();
      animation-range:entry 0% entry 70%;
    }
    @keyframes gate-enter-rise{from{opacity:0;translate:0 26px}to{opacity:1;translate:0 0}}
  }
}

html.a11y-no-motion .gate-seam,html.a11y-no-motion .gate-halfin>*{animation:none}
html.a11y-no-motion .gate-half,html.a11y-no-motion .gate-enter svg,
html.a11y-no-motion .gate-mark,html.a11y-no-motion .gate-halfin{transition:none}
@media(prefers-reduced-motion:reduce){
  .gate-seam,.gate-halfin>*{animation:none}
  .gate-half,.gate-enter svg,.gate-mark,.gate-halfin,.gate-half::before{transition:none}
  @media(hover:hover) and (pointer:fine){
    a.gate-half:hover{flex-grow:1}
    .gate-split:has(a.gate-half:hover) a.gate-half:not(:hover){flex-grow:1}
    a.gate-half:hover .gate-mark{transform:none}
  }
}

/* ————— B2B platform holding page (.gate-half--soon and friends) — site/b2b-coming-soon.html,
   the stand-in destination behind both doors while the platform is still being built.

   It reuses the two-territory split from login.html on purpose, because the page's whole
   message is a comparison: the door you came through is BUILT, the thing behind it is SOON.
   Same composition, same tones, one word each. The unbuilt side carries a drafting hatch,
   which is the drawing convention for an area not yet made, rather than a decorative texture.

   Reached only from the two login.html halves and the footer link, never indexed. When the
   platform ships, those hrefs point at it instead and this block plus b2b-coming-soon.html
   come out together. */

/* neither half is a link here, so only real anchors may expand under the cursor */
.gate-half--soon,.gate-half--ready{cursor:default}

.gate-half--soon{background:#F2F1ED;color:#121214;--ac:#900000;--hair:rgba(18,18,20,.16);--soft:rgba(18,18,20,.745);justify-content:flex-end}
.gate-half--soon::before{content:"";position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(45deg,transparent 0 11px,rgba(18,18,20,.038) 11px 12px)}

.gate-soon-p{margin:clamp(20px,2.4vw,34px) 0 0;font-family:var(--font-body);font-size:clamp(13.5px,1.05vw,15px);line-height:1.55;color:var(--soft);max-width:34ch}
.gate-half--soon .gate-soon-p{margin-left:auto}

.gate-status{display:inline-flex;align-items:center;gap:9px;margin-top:clamp(26px,3vw,44px);padding-top:clamp(16px,1.8vw,24px);border-top:1px solid var(--hair);font-family:var(--font-mono);font-size:10.5px;font-weight:600;letter-spacing:.15em;text-transform:uppercase;color:var(--soft)}

.gate-notify{display:inline-flex;align-items:center;gap:.5em;align-self:flex-end;min-height:44px;margin-top:clamp(24px,2.8vw,38px);padding:12px 22px;border:1px solid #121214;border-radius:2px;background:#121214;color:#F2F1ED;text-decoration:none;font-family:var(--font-body);font-size:15px;font-weight:600;transition:background .22s var(--ease-out),border-color .22s var(--ease-out),transform .16s var(--ease-out)}
.gate-notify svg{width:17px;height:17px;max-width:none;flex:none;transition:transform .22s var(--ease-out)}
@media(hover:hover) and (pointer:fine){.gate-notify:hover{background:#900000;border-color:#900000}.gate-notify:hover svg{transform:translateX(3px)}}
.gate-notify:active{transform:scale(.985)}
.gate-notify:focus-visible{outline:3px solid #121214;outline-offset:3px}
.gate-fine{margin:11px 0 0;align-self:flex-end;text-align:right;font-size:12.5px;line-height:1.5;color:var(--soft);max-width:34ch}

html.a11y-no-motion .gate-notify,html.a11y-no-motion .gate-notify svg{transition:none}
@media(prefers-reduced-motion:reduce){.gate-notify,.gate-notify svg{transition:none}}


/* ==========================================================================
   Ideal brand / ideal product (partners.html) — the supplier profile and the
   product profile, added 2026-08-20 ahead of the model matrix on the same page.

   Both are drawn as SPEC SHEETS on purpose. They now sit directly above the
   .mx comparison matrix (inlined into partners.html in the same change), so
   they borrow its grammar rather than inventing a third card language: a mono
   uppercase row label, one hard value, one line of why. .ibr-grid repeats the
   matrix's 1px-hairline-gap construction (same trick as .kpi-band) so the two
   read as one printed sheet running down the page.

   Markup is a <dl>: these are literally term/value pairs, and the div-wrapped
   dt/dd grouping is what lets the grid place one cell per criterion.
   ========================================================================== */
.ibr-grid{list-style:none;display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--hairline);border:1px solid var(--hairline)}
.ibr{background:var(--paper-0);padding:26px 24px 28px}
.ibr-lab{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);line-height:1.6}
.ibr-val{display:block;font-family:var(--font-display);font-weight:800;font-size:26px;letter-spacing:-.01em;line-height:1.1;color:var(--ink);margin:6px 0 10px}
.ibr p{font-size:14.5px;line-height:1.55;color:var(--ink-soft);max-width:38ch}
.ibr-foot{margin-top:30px;padding-left:18px;border-left:3px solid var(--red);font-family:var(--font-display);font-weight:700;font-size:clamp(19px,1.9vw,25px);letter-spacing:-.01em;line-height:1.25;color:var(--ink);max-width:44ch}
@media(max-width:960px){.ibr-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:620px){.ibr-grid{grid-template-columns:1fr}.ibr{padding:22px 20px 24px}}

/* the product sheet is two panels, not a grid of six: the triad is a ranked list
   (three characteristics, read top to bottom) and the ratio is a single figure. */
.ipr{display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:stretch}
.ipr-panel{display:flex;flex-direction:column;background:var(--paper-0);border:1px solid var(--hairline);border-radius:var(--radius-card);padding:clamp(24px,2.6vw,36px)}
.ipr-lab{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);line-height:1.6}
.ipr-list{list-style:none;margin-top:18px}
.ipr-list li{display:grid;grid-template-columns:46px 1fr;gap:18px;align-items:start;padding:22px 0;border-top:1px solid var(--hairline)}
.ipr-list li:first-child{border-top:0;padding-top:6px}
.ipr-list li:last-child{padding-bottom:0}
.ipr-ico{width:46px;height:46px;border:1px solid var(--hairline);border-radius:50%;display:grid;place-items:center;color:var(--red-ink)}
.ipr-ico svg{width:22px;height:22px}
.ipr-name{display:block;font-family:var(--font-display);font-weight:800;font-size:21px;letter-spacing:-.01em;line-height:1.15;color:var(--ink)}
.ipr-list p{margin-top:6px;font-size:14.5px;line-height:1.55;color:var(--ink-soft);max-width:44ch}
/* the ratio is the one hero numeral on this page, so it takes the red; everything
   else in both panels stays ink, which keeps the section inside the 5% budget. */
.ipr-fig{display:flex;align-items:baseline;gap:14px;margin:26px 0 0;flex-wrap:wrap}
/* two auto margins (here and on .ipr-note) split the panel's leftover height evenly, so the
   figure + its explanation sit centred between the label and the baseline note instead of
   hanging under the label with a hole beneath them. The triad panel sets this panel's height. */
.ipr-ratio .ipr-fig{margin-top:auto}
.ipr-phi{font-family:var(--font-display);font-weight:700;font-size:clamp(42px,4.2vw,58px);line-height:1;color:var(--ink-muted)}
.ipr-num{font-family:var(--font-display);font-weight:800;font-size:clamp(34px,3.8vw,50px);letter-spacing:-.02em;line-height:1;color:var(--red-ink)}
.ipr-ratio .ipr-txt{margin-top:22px;font-size:14.5px;line-height:1.55;color:var(--ink-soft)}
/* the note is the panel's baseline: margin-top:auto keeps the two panels ending on the
   same line whichever one runs longer, instead of leaving a hole under the figure. */
.ipr-note{margin-top:auto;padding-top:22px;font-family:var(--font-mono);font-size:11.5px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted);line-height:1.6}
@media(max-width:860px){.ipr{grid-template-columns:1fr}.ipr-note{margin-top:20px}}

/* #ideal-product runs on a .band-inverse ground (2026-08-23 owner call: the triad is the
   argument the whole pitch turns on, so it gets the page's one tonal break instead of a
   third light spec sheet between #ideal-brand and the .mx matrix). Every colour below is a
   theme-invariant literal for the reason recorded at .band-inverse (~2158): the paper/ink
   tokens flip near-black in dark theme and would vanish on this fixed-dark surface. The
   values are the ones the band already uses — #17171A card, #2a2a2e hairline, #fff title,
   #C5C3BB body, #9A9994 mono — so the panels read as .band-inverse .card, not as a fork. */
.band-inverse .ipr-panel{background:#17171A;border-color:#2a2a2e}
.band-inverse .ipr-lab,.band-inverse .ipr-note{color:#9A9994}
.band-inverse .ipr-list li{border-top-color:#2a2a2e}
.band-inverse .ipr-ico{border-color:#2a2a2e;color:var(--red-ink-inverse)}
.band-inverse .ipr-name{color:#fff}
.band-inverse .ipr-list p,.band-inverse .ipr-ratio .ipr-txt{color:#C5C3BB}
.band-inverse .ipr-phi{color:rgba(255,255,255,.34)}
.band-inverse .ipr-num{color:var(--red-ink-inverse)}
/* the ratio panel is the section's single loudest object, so on the dark ground it takes a
   hairline of the inverse red rather than the neutral one — the only structural difference
   between the two panels, and the reason the eye lands there first. */
.band-inverse .ipr-ratio{border-color:rgba(239,118,118,.38)}
/* closing line, same device as .ibr-foot one section up (red rule, display weight) so the two
   sheets still read as one printed pair across the tonal break. */
.ipr-foot{margin-top:34px;padding-left:18px;border-left:3px solid var(--red);font-family:var(--font-display);font-weight:700;font-size:clamp(19px,1.9vw,25px);letter-spacing:-.01em;line-height:1.25;color:var(--ink);max-width:52ch}
.band-inverse .ipr-foot{color:#fff;border-left-color:var(--red-ink-inverse)}

/* ——— #model split into five sections (2026-08-24, owner call) —————————————————————————
   The deck used to be ONE #model section; it is now five (#model, #paradigm, #our-solution,
   #capabilities, #how-we-do-it) so each beat is its own landmark and its own anchor. The
   sitewide section{padding:96px 0;border-bottom:1px solid var(--hairline)} now does the
   separating that the in-module rules used to do, so those are stood down here or the seams
   double up. Order of the five, and the move of the audience router to AFTER them, is the
   owner sequence of the same day. */
.mdl-split{padding:clamp(48px,6vw,80px) 0}
.mdl-split .mdl-para{margin-top:0;padding:0}
.mdl-split .mdl-para::before,.mdl-split .mdl-para::after{display:none}
.mdl-split .mdl-sol{padding:0}
.mdl-split .mdl-k--steps{margin-top:0}
/* the ticker is a BAND: its own hairlines are the seams, so no section padding and no border,
   or three rules stack inside 20px */
/* The capabilities ticker is a BAND, not a quiet strip (2026-08-24, owner call). Two reasons.
   RHYTHM: after the reorder the page ran NINE sections deep on one ground before the first tonal
   break, with the whole model deck inside that run separated only by 1.21:1 hairlines. Putting
   the marquee on --band punctuates it, and lands the break exactly where the deck turns from
   what the model IS to how we RUN it.
   SEAM: it also removes a measured defect. #our-solution's border-bottom sat at y=3795 and the
   ticker's own border-top started at y=3795 -- a 0px gap, two 1px hairlines stacking into the
   only 2px seam on the page. The tonal step IS the seam now, so the ticker drops its own rules
   and the section before it drops its border.
   LITERALS, not paper tokens, for anything painted ON the band: --band is fixed dark in BOTH
   themes, and paper tokens flip near-black in dark theme and would vanish on it (see ~154). */
.mdl-split--band{padding:0;border-bottom:0;background:var(--band);color:#EBE9E3}
.mdl-split--band .mdl-ticker{border-top:0;border-bottom:0}
.mdl-split--band .mdl-titem{color:#EBE9E3}
/* the separator squares use --red-ink-inverse, the token this file already keeps for red
   that has to sit on an inverse ground: brand --red measures 2.56:1 there, the same figure
   flagged at ~2269. They are decorative rather than informational so AA text rules do not
   bind, but at 2.56 they read muddy on near-black. */
.mdl-split--band .mdl-sq{background:var(--red-ink-inverse)}
.mdl-split:has(+ .mdl-split--band){border-bottom:0}
/* the ticker is full-bleed inside a zero-padding section, so the section IS the band's height;
   .reveal's translateY(24px) would push it clear of its own section and overlap the next one for
   the length of the transition. Fade only. Specificity 0,2,0 beats .reveal's 0,1,0. */
.reveal.reveal--fade{transform:none}

/* ——— .mdl-fly: the five model blocks land from off-page and STAY —————————————————————
   Owner call 2026-08-24. A first take flew them back out again on exit, driven by a scroll
   timeline; it was rejected as dizzy, and the exit is gone. Landing once also makes the motion
   TIME-bounded: 88vw is 1267px over .9s, about 23px a frame whatever the wheel does. The
   scroll-linked version had no such bound - it measured 475-520px PER FRAME and finished in 7
   frames, which reads as a teleport however locked the frame rate is.
   MECHANICS ARE THE HOUSE .mf-fly (~5213), including the reason it works: TWO LAYERS. The
   observed wrapper NEVER moves, because an element parked at 88vw never intersects the
   IntersectionObserver watching it and so would never land. .mdl-fw marks that wrapper; its
   CHILDREN fly. It still sizes the row (translate is paint-only), so the flight costs no CLS.
   A marker class, not a blanket `.mdl-fly .reveal` rule: some .reveals here are LEAVES (the
   eyebrow, the h2, the ticker, the CTA) with no children to fly, and pinning those would kill
   their fade rather than upgrade it. The inner rides translate/rotate, never transform, so
   anything owning transform for hover keeps it. 88vw parked off-page is the sideways-drag trap;
   overflow-x:clip is the guard and .mdl already carries it. */
.mdl-fly .mdl-fw{opacity:1;transform:none;transition:none}
.mdl-fly .mdl-fw>*{opacity:0;translate:var(--fx,0) var(--fy,44px);rotate:var(--fr,0deg);
  transition:opacity .6s var(--ease),translate .9s var(--ease-out),rotate .9s var(--ease-out)}
.mdl-fly .mdl-fw.is-visible>*{opacity:1;translate:0 0;rotate:0deg}
.mdl-fly .mdl-fw>*:first-child{--fx:-88vw;--fy:0;--fr:-2.5deg}
.mdl-fly .mdl-fw>*:last-child{--fx:88vw;--fy:0;--fr:2.5deg}
.mdl-fly .mdl-fw>*:only-child{--fx:0;--fy:44px;--fr:0deg}
/* .mdl-flow is a wrapper in its own right and must not also fly as a child of .mdl-sol, or two
   translates compound on the same pixels */
.mdl-fly .mdl-sol>.mdl-flow{opacity:1;translate:none;rotate:none;transition:none}
.mdl-fly .mdl-fw.is-visible>*:nth-child(2){transition-delay:90ms}
.mdl-fly .mdl-fw.is-visible>*:nth-child(3){transition-delay:180ms}
.mdl-fly .mdl-fw.is-visible>*:nth-child(4){transition-delay:270ms}
.mdl-fly .mdl-fw.is-visible>*:nth-child(5){transition-delay:360ms}
/* every escape hatch lands the flyer, same discipline as .mf-fly. :focus-within matters because
   these blocks hold links and the closing CTA button (WCAG 2.4.7). */
.mdl-fly .mdl-fw:focus-within>*{opacity:1;translate:none;rotate:none;transition:none}
@media(prefers-reduced-motion:reduce){.mdl-fly .mdl-fw>*{opacity:1;translate:none;rotate:none;transition:none}}
@media(scripting:none){.mdl-fly .mdl-fw>*{opacity:1;translate:none;rotate:none;transition:none}}
html.a11y-no-motion .mdl-fly .mdl-fw>*{opacity:1;translate:none;rotate:none;transition:none}

/* ————— Film block (.vfr) — a self-hosted film, fetched only when a visitor asks —————
   Shop & Trade's own film (2026-08-28). SELF-HOSTED on our CDN, deliberately: the source runs as
   a Vimeo background embed on the partner's own site, and an embed of it here would have put the
   block at the mercy of a privacy setting in THEIR account, cost a third-party connection and a
   cookie on every view, and given up the poster and the reduced-motion resting state. So the
   file is ours, the same bargain the ScanGauge hero loop already runs under.
   It stays a FACADE anyway — a poster and a play button until a click — because 2.4 MB of video
   has no business loading for a visitor who never presses play. Nothing autoplays on arrival, so
   SC 2.2.2 has nothing to govern: the film starts from a click and <video controls> stops it.
   The 8/3 box is fixed on all three states (link, button, video), so the facade -> player swap
   moves no pixels and the section cannot shift (CLS). 8/3 is the real shape of the file: the
   source is their full-bleed hero as an ultrawide viewport crops it, not a 16/9 frame. */
.vfr{margin-top:32px}
.vfr-box{display:block;position:relative;width:100%;aspect-ratio:8/3;padding:0;
  border:1px solid var(--hairline);border-radius:var(--radius-card);overflow:hidden;
  background:var(--paper-200);cursor:pointer}
/* the no-JS state is an <a>, the enhanced one a <button> — the button must not inherit UA chrome */
button.vfr-box{-webkit-appearance:none;appearance:none;color:inherit;font:inherit;text-align:left}
/* the player: black behind the letterbox, and no pointer affordance — it has its own controls */
video.vfr-box{cursor:auto;background:#0B0B0C;object-fit:cover}
.vfr-box img{display:block;width:100%;height:100%;object-fit:cover}
/* Red is the action in this block and the only red in it — the 5% rule's intended use. */
.vfr-play{position:absolute;left:50%;top:50%;translate:-50% -50%;width:76px;height:76px;
  border-radius:50%;display:grid;place-items:center;background:var(--red);color:#fff;
  box-shadow:0 8px 24px rgba(0,0,0,.28);transition:scale .35s var(--ease-out),background .2s linear}
.vfr-play svg{width:30px;height:30px;margin-left:3px} /* a triangle's optical centre sits left of its box's */
.vfr-box:is(:hover,:focus-visible) .vfr-play{scale:1.06;background:var(--red-hover)}
.vfr-box:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
/* the caption rides the photograph, so it carries its own scrim rather than trusting the frame */
.vfr-cap{position:absolute;left:0;right:0;bottom:0;padding:56px 20px 16px;color:#fff;
  background:linear-gradient(to top,rgba(0,0,0,.72),rgba(0,0,0,0));
  font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.06em;text-transform:uppercase}
.vfr-cap b{font-weight:700}
/* --ink-soft, not the --ink-muted this row would take from .map-foot: that token only clears
   4.5:1 on --paper-100 and this component is dropped on whatever ground its section runs. */
.vfr-foot{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-top:12px;
  font-family:var(--font-mono);font-size:12px;font-weight:500;letter-spacing:.06em;color:var(--ink-soft)}
.vfr-foot a{color:var(--ink-soft);text-decoration:none;font-weight:600;display:inline-flex;align-items:center;min-height:44px}
.vfr-foot a:hover{color:var(--red-ink)}
/* On a phone the true 8/3 band is only ~131px tall, thinner than the play control wants and too
   short to read as a film. The box takes 16/9 there and object-fit:cover crops the sides of an
   already-cropped montage - a trade this footage can afford, and the ratio is still fixed, so
   still no CLS. Both the poster <img> and the <video> cover, so the crop needs nothing else. */
@media(max-width:640px){.vfr-box{aspect-ratio:16/9}.vfr-play{width:60px;height:60px}.vfr-play svg{width:24px;height:24px}}
/* every motion escape hatch lands the play button, same discipline as the rest of the sheet */
@media(prefers-reduced-motion:reduce){.vfr-play{transition:none}.vfr-box:is(:hover,:focus-visible) .vfr-play{scale:1}}
html.a11y-no-motion .vfr-play{transition:none}
html.a11y-no-motion .vfr-box:is(:hover,:focus-visible) .vfr-play{scale:1}

/* ═══════════════════════════════════════════════════════════════════════════════
   .cs-more GROUND LADDER — case-studies/index.html, one section per co-operation model
   2026-08-29, OWNER DIRECTION: the single black .pgrid band on the case studies index becomes
   four sections, one per model, each on a DIFFERENT GROUND so the split is visible before a
   word is read.

   WHY TONES AND NOT HUES. The palette is paper, ink and red (:50-66) and nothing else. Four
   model colours would have meant four new accent tokens plus an AA pass on every kicker,
   hairline and title drawn on them — a colour system this site has deliberately never had. So
   the ladder is built from grounds that already exist:

     D2C            .cs-more--paper   --paper-0     #FFFFFF
     Cross-docking  .cs-more--warm    --paper-100   #F2F1ED
     OaaS           .cs-more--ink     --band        #0B0B0C   + .band-inverse
     B2B2C          .cs-more--deep    --paper-200   #EBE9E3

   Every one is a token, so dark theme needs no override on any of them — the four flip on
   their own, and the ladder keeps its four distinct steps there too.

   THESE MUST OUTRANK .cs-more{background:var(--paper-200)} at :8649, which is one class, as
   are these. Source order is the whole override, which is why this block sits at the END of
   the sheet. Do not hoist it. The same rule is why .band-inverse cannot paint the OaaS section
   on its own — it is also one class and it loses to .cs-more — so .cs-more--ink carries the
   ground and .band-inverse rides along only for the head, lede and focus-ring colours it
   already defines at :1736 and :2413.

   NO BOTTOM HAIRLINE AND TIGHTER PADDING. section{} at :279 gives every section 96px and a
   rule underneath; four of those stacked is an unreadable run, and a hairline between two
   different grounds is a line drawn on a join that is already visible. .cs-more--run replaces
   both. */
.cs-more--run{padding:clamp(56px,7vw,88px) 0;border-bottom:0}
.cs-more--paper{background:var(--paper-0)}
.cs-more--warm{background:var(--paper-100)}
.cs-more--deep{background:var(--paper-200)}
.cs-more--ink{background:var(--band)}

/* The dark section's own card chrome. .band-inverse covers h2/.eyebrow/.lede and the focus
   ring; it knows nothing about .cs-card, whose base hairline, red hover rule, title and meta
   row are all drawn for paper. --red-ink-inverse, not --red: #AB0000 on #0B0B0C is 1.6:1, and
   the inverse token is the one the sheet already reserves for red on this ground (:190). */
.cs-more--ink .cs-card::after{background:rgba(255,255,255,.18)}
.cs-more--ink .cs-card::before{background:var(--red-ink-inverse)}
.cs-more--ink .cs-card h3{color:#fff}
.cs-more--ink .post-kick{color:#9A9994}
/* The model chip is NOT re-coloured here. .cs-card .post-kick b at :8587 gives it a
   --paper-200 pill and --ink-soft text, and it drops its red on purpose ("this section
   already spends its red on the eyebrow, the heading's second clause, the hover rule").
   That pill is a LIGHT ground, so it needs nothing from --band: the chip reads 8:1 on
   black exactly as it does on paper. An earlier pass here set --red-ink-inverse on it and
   put #ef7676 on #EBE9E3 — 2.6:1, and a fail — which is the trap in overriding a token by
   ground rather than by the surface the text actually sits on. Verified by render. */

/* The two OaaS marks — bsb.svg and passadena-logo-v2.svg — are both near-black ink, so the
   component's paper treatment (grayscale + .55 opacity, :8519) would render them as nothing at
   all on --band. This is the SAME two-step the whole component already runs in dark theme at
   :8534: inverted silhouette at rest, brighter inverted artwork on hover and focus. There is no
   colour reveal here, and that is honest rather than a compromise — the colour of a black
   wordmark on a black ground is still nothing.
   Guarded to light theme so it cannot double up on the dark-theme rules, which already do
   exactly this to every card on the page. ScanGauge's exception at :8553 sits in the D2C
   section, on paper, and never meets this selector. */
html:not([data-theme="dark"]) .cs-more--ink.cs-more--bare .cs-card .post-media--logo img{
  filter:invert(1) grayscale(1) brightness(1.6);opacity:.72}
/* grayscale(1) stays on the reveal for the reason given at the logo wall (:8131): an invert
   without it rotates hue, and the two marks here are only near-black TODAY — the day a
   coloured one joins this rail, a bare invert would hand it the wrong brand colour. */
@media(hover:hover) and (pointer:fine){
  html:not([data-theme="dark"]) .cs-more--ink.cs-more--bare .cs-card:hover .post-media--logo img{
    filter:invert(1) grayscale(1) brightness(1.9);opacity:1}
}
html:not([data-theme="dark"]) .cs-more--ink.cs-more--bare .cs-card:focus-visible .post-media--logo img{
  filter:invert(1) grayscale(1) brightness(1.9);opacity:1}

/* ————— .cs-close — the one ask, after all four sections —————
   The pitch button and the "every brand" link on one baseline row. Replaces .pgrid-more
   (:5835), which is still live on the three pages that kept the partner grid and is scoped to a
   dark band this row does not run on. */
.cs-close{background:var(--paper-0);padding:clamp(40px,5vw,64px) 0}
.cs-close-row{display:flex;align-items:center;justify-content:space-between;gap:clamp(20px,3vw,36px);flex-wrap:wrap}
.cs-close-row p{margin:0;max-width:56ch}
.cs-close-act{display:flex;align-items:center;gap:clamp(16px,2vw,28px);flex-wrap:wrap}

/* ————— .cs-more--quad — four across, one row per model —————
   2026-08-29, OWNER DIRECTION: a model's studies sit on ONE ROW, up to four of them, so the
   grid takes four tracks and the logo plate shrinks to fit rather than the row wrapping.
   This REPLACES the 2x2 fallback for a four-card section that .cs-grid applies at :8499 — the
   note there ("three across strands the fourth alone on a second line, so a 2x2 block reads
   as one shape") was written for a three-track grid, where four cards have no clean seating.
   With four tracks they do, and the 2x2 is no longer the better of two bad shapes.

   A MODIFIER, NOT A PAGE. The call landed on the case studies index first and this block was
   scoped to .cs-more--run, the class that page's stacked sections carry. Extended the same
   day to models/d2c.html and models/cross-docking.html, whose four studies were still
   wrapping to a 2x2 — so the layout moved onto its own class and .cs-more--run went back to
   meaning what it says (that page's section padding and its dropped divider, :9393).
   .cs-more--quad is now the whole opt-in: four tracks, and the plate, title and card height
   that a quarter-width track needs.
   EVERY MODEL PAGE CARRIES IT, including the two with a pair (owner call 2026-08-29, extending
   the same day's four-across call). The size of a case-study card is now a property of the
   component, not of how many studies a model happens to have: a card on models/oaas.html is
   the same 277px object as a card on models/d2c.html, and the five model pages read as five
   views of one shelf rather than five different ones. What that costs is answered below.

   SPECIFICITY, and it is the whole reason these selectors look redundant: :has() takes the
   specificity of its most specific argument, so .cs-grid:has(>:nth-child(4):last-child) at
   :8499 is (0,3,0) and a plain .cs-more--quad .cs-grid at (0,2,0) would lose to it. Every rule
   below therefore has to carry the :has() too. It is also why the breakpoints are restated
   here rather than inherited: the unconditional rule outranks the shared 980/640 media
   queries at :8502, so this block has to step itself down.

   1100px, not 980: four tracks inside the wrap are ~277px each at 1280 and ~215px at 1100,
   which is the narrowest a logo plate reads at. Below that the row halves to a 2x2 and then
   to a single column, the same ladder the shared rules use. */
.cs-more--quad .cs-grid:has(>:nth-child(4):last-child){grid-template-columns:repeat(4,minmax(0,1fr))}
@media(max-width:1100px){
  .cs-more--quad .cs-grid:has(>:nth-child(4):last-child){grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media(max-width:640px){
  .cs-more--quad .cs-grid:has(>:nth-child(4):last-child){grid-template-columns:minmax(0,1fr)}
}

/* THE PAIRS MATCH THE QUADS. OaaS and B2B2C have two studies each, and left alone a pair takes
   half the row apiece — cards more than twice the width of a four-card row's, and logo plates
   to match. One card size everywhere: a pair sits in the first two of the same four tracks and
   the row simply ends early.
   THE EMPTY HALF IS THE TRADE, and it is deliberate. On the case studies index it reads as
   nothing at all, because each model owns its own ground there and the block ends where its
   cards end. On models/b2b2c.html and models/oaas.html the pair sits on open paper with the
   right half of the row empty — accepted on 2026-08-29 for a consistent card size, the same
   call that put the four-card pages on one row. Left-aligned, not centred: the cards stay on
   the .wrap's left edge with the eyebrow, h2 and lede above them, so the row reads as a list
   that ended rather than a block that lost its middle.
   Same specificity problem, same answer: .cs-grid:has(>:nth-child(2):last-child) at :8499 is
   (0,3,0), so this has to be (0,4,0) to reach it. */
.cs-more--quad .cs-grid:has(>:nth-child(2):last-child){grid-template-columns:repeat(4,minmax(0,1fr))}
@media(max-width:1100px){
  .cs-more--quad .cs-grid:has(>:nth-child(2):last-child){grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media(max-width:640px){
  .cs-more--quad .cs-grid:has(>:nth-child(2):last-child){grid-template-columns:minmax(0,1fr)}
}

/* The plate and the mark, resized for a quarter-width track.
   3/2, not the component's 4/3 (:8449): at 277px wide a 4/3 plate is 208px tall and the card
   runs past 400px for a logo the size of a business card — the row reads as four tall empty
   boxes. 3/2 takes ~93px out of every card and puts the meta row back near the mark.
   The mark then takes MORE of the smaller plate, not less: 64%/60% of 277px leaves a wordmark
   like VATOPEDI MONASTERY at ~177px and unreadable. 82%/70% holds it at the size the eye needs
   while keeping the plate's own margin. Both numbers are on .cs-more--quad only, so every
   section still on a three-across row keeps the proportions it was tuned for. */
.cs-more--quad .cs-card .post-media--logo{aspect-ratio:3/2}
.cs-more--quad .cs-card .post-media--logo img{max-width:82%;max-height:70%}
/* THE OaaS PAIR IS MATCHED ON HEIGHT, WITH BSB AS THE REFERENCE (owner call 2026-08-29).
   bsb.svg and passadena-logo-v2.svg are the only two marks that ever sit side by side in this
   component, and inside the shared 82%/70% box they came out at 85px against 125px (measured
   at 1440): BSB carries its own width:64% tuning at :8478 and stops there, while Passadena is
   a near-square 1778x982 that fills the box on HEIGHT instead. One mark half again the size of
   the other, on a row of two, is the one place the eye reads it as a mistake.
   45.9% IS BSB'S OWN HEIGHT, not a number picked by eye: the plate is 3/2, so BSB's 64% of the
   plate WIDTH at its 115/55 ratio is 0.64 / 2.0909 x 1.5 = 45.9% of the plate HEIGHT. Both
   sides derive from the same plate width, so the two stay locked at every viewport instead of
   drifting apart the way two hand-set sizes would. width:auto keeps the wordmark's own ratio
   inside that height, and 82% never binds (the mark lands at ~153px of a 227px allowance).
   CHANGE :8478 AND THIS MOVES WITH IT — it is BSB's 64% restated, and nothing recomputes it.
   Scoped to .cs-more--quad, so it reaches this pair wherever it runs at this card size: the
   case studies section of models/oaas.html and the OaaS section of the case studies index,
   which point at the same two files. The real fix is still an asset (see :8474) — a BSB mark
   drawn on Passadena's canvas would make both of these rules unnecessary. */
.cs-more--quad .cs-card .post-media--logo img[src*="passadena"]{width:auto;height:45.9%}
/* The title has a quarter of the width it had, so it needs the smaller step of its own clamp
   and a tighter measure; without this "Their stock. Their warehouses. A company we built to
   sell it." runs to five lines and drags its card's base hairline below its neighbours'. */
.cs-more--quad .cs-card h3{font-size:clamp(16px,1.15vw,18px);line-height:1.3}
.cs-more--quad .cs-card .post-body{padding:18px 14px 22px;gap:9px}
/* Equal card heights, so the base hairlines line up across the row. .cs-more .cs-rail .reveal
   at :8657 does this for the RAIL and stops there, which was fine while a four-card section
   was a 2x2 of near-identical titles. On one row of four it is not: "Serving as DNA Filters
   international retail gateway." is two lines where "A thousand-year-old brand. Taken live."
   is one, and the card's own hairline followed the text down — three different baselines in a
   row of four (measured, 2026-08-29). The .reveal is a grid item and stretches; .cs-card
   inside it is height:auto and does not, so the flex pair is what passes the height on.
   .cs-more a.cs-card{flex:1} at :8658 already supplies the other half. */
.cs-more--quad .cs-grid>.reveal{display:flex}

/* ═══════════════════════════════════════════════════════════════════════════════
   .mfsec GROUND LADDER — the .mf-card twin of .cs-more (2026-08-30, OWNER DIRECTION)
   couriers/index.html and channels/index.html each ran their whole shelf as ONE .aud band:
   seven carriers, then eleven channels, all equal boxes on one photographic ground, with the
   reader left to work out from the body copy which one crosses a border and which one walks up
   a Greek staircase. Both are now split into grouped sections built exactly like the case
   studies index — a ground per group, an eyebrow, a two-tone h2 and a lede above the row.

   WHY A SECOND CLASS AND NOT .cs-more. .cs-more is welded to .cs-card: its ink-ground block
   re-colours .cs-card::after/::before/h3/.post-kick, and its --quad block resizes
   .post-media--logo. These pages carry .mf-card/.aud-card, a different component with its own
   chrome. Sharing the class would mean either dead selectors or cross-component overrides, so
   this is the same IDEA (tones, never hues — the palette is paper, ink and red and nothing
   else) rebuilt on the primitives .mf-card already uses. Head, eyebrow and lede are the plain
   sitewide ones, so there is nothing new to keep in sync there.

   THE GROUNDS ARE ALL TOKENS, so dark theme needs no override on any of them — the four flip
   on their own and the ladder keeps its four steps there too. None is a dark band: unlike
   .cs-more--ink, no .band-inverse twin is defined here, because .mf-card's hairline, sweep,
   title and foot chip are all drawn for paper and re-toning them is a component rewrite, not
   a ground. --paper-200 is the fourth step instead, which is where the ladder started.

   NO BOTTOM HAIRLINE AND TIGHTER PADDING, for the same reason .cs-more--run drops both: three
   or four stacked 96px sections is an unreadable run, and a rule drawn on a join between two
   different grounds is a line on a seam that is already visible. */
.mfsec{overflow-x:clip}
.mfsec--run{padding:clamp(56px,7vw,88px) 0;border-bottom:0}
.mfsec--paper{background:var(--paper-0)}
.mfsec--warm{background:var(--paper-100)}
.mfsec--deep{background:var(--paper-200)}
/* NO TINTED GROUND HERE, AND NOWHERE ELSE ON THE SITE. There used to be a .mfsec--tint and a
   .cs-more--tint, both painting the --red-wash token as a section ground, and both are gone —
   OWNER DIRECTION 2026-09-01: this site carries no pale-red surface. The token itself was
   deleted with them (:67), so there is nothing left to reach for; the four pages that had
   already refused it one at a time (career, contact, ai-neural, history) were the direction
   arriving piecemeal. If a group needs a ground it does not have, take the next PAPER step or
   repeat one — never a hue.

   KEEP THE LESSON, though, because it outlives the token: the pinned hero on body.chan-page
   (#main is position:sticky at >=821px, :1634) is held back only by a :where() guard giving
   every following section background-color:var(--paper-100) — specificity ZERO. ANY translucent
   `background` shorthand on an .mfsec resets that to transparent and the hero reads straight
   through the band while you scroll. Section grounds here are opaque colours, full stop. */
.mfsec .lede{margin-top:10px}

/* ONE ROW PER GROUP, AND ONE CARD SIZE ACROSS THE PAGE — the whole point of the layout.
   The track count is a page-level variable rather than a per-section count, so every card on
   the page is the same object whatever its group holds: a group with the full count fills the
   row, a smaller group sits in the first tracks and the row simply ENDS EARLY. Left-aligned,
   not centred, so a short row reads as a list that ended rather than a block that lost its
   middle — the same call .cs-more--quad's pairs already made.
   Couriers keeps the default 3 (its largest group is three carriers); channels adds .mfsec--four
   (its largest is four marketplaces). Two classes deep, so this outranks the .aud and base
   .mf-grid track rules at ~5663/5763 without touching either.
   The 1100/640 ladder is the one .cs-more--quad uses: below ~215px a track cannot hold a mark
   and a title, so the row halves and then goes single-column. */
.mfsec{--mfsec-cols:3}
.mfsec--four{--mfsec-cols:4;--mfsec-card-min:580px}

/* ONE CARD HEIGHT DOWN THE WHOLE PAGE. Inside a group the grid stretches its own row, but
   three or four SEPARATE grids each size to their own content, and the rows landed at
   520/500 on channels and 370/342/354 on couriers (measured at 1200) — several different
   card heights down one page, which is the exact raggedness the grouping was meant to remove.
   A floor is the only thing that can level separate grids, so each page sets --mfsec-card-min
   to its own tallest real card and every row meets it. Gated at the same 1100px the row halves
   at: below that the cards are wider, wrap less, and a floor set for a quarter-width track
   would leave dead space under every one of them.
   THE NUMBER IS THE WIDEST-VIEWPORT MAXIMUM, not the one measured at a single width. Natural
   card height does NOT fall monotonically as the viewport grows — it is a V: at 1101 the four
   tracks are ~230px and the copy wraps hard (554px tall), it bottoms out near 1200 (520px),
   then climbs again as the card's own vw-scaled padding grows (545 at 1440, 577 at 1920). No
   clamp() models that shape, so the floor is flat at the peak. What it costs is up to ~60px of
   space around 1200, and it lands between the body copy and the foot chip, which is pinned to
   the card's bottom by margin-top:auto either way.
   The number rides with the TRACK COUNT, not the page, because that is what it is a function of
   — a narrower track wraps more lines into the same copy. So .mfsec--four carries 490px for the
   channels shelf, and couriers, on the default three tracks, sets 380px in couriers.css. A page
   whose copy is longer than either overrides the variable and changes nothing else. */
@media(min-width:1101px){
  .mfsec .mf-card.aud-card{min-height:var(--mfsec-card-min,0)}
}
/* The row needs air under the lede: .cs-grid carries its own margin-top (:8609) and .mf-grid,
   built for a section with a sr-only heading above it, carries none — the cards sat on the
   lede's last line. Same clamp as .cs-grid so the two shelves breathe alike. */
.mfsec .mf-grid{grid-template-columns:repeat(var(--mfsec-cols),minmax(0,1fr));margin-top:clamp(38px,5vw,56px)}
@media(max-width:1100px){.mfsec .mf-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){.mfsec .mf-grid{grid-template-columns:minmax(0,1fr)}}

/* THE MARK ROW, IN A QUARTER-WIDTH TRACK. .aud-meta is flex:none (:5816), set when these cards
   were two or three to a row and the kicker was always shorter than the space it had. At four
   tracks it is not: "EUROPEAN FASHION PLATFORM" on a 262px card refused to shrink and pushed
   the Zalando and ABOUT YOU logos clean out of the card (measured, 2026-08-30). The meta is
   allowed to shrink and wrap, and the mark — which cannot reflow — is the one that keeps
   flex:none. Left-aligned while it is wrapping: two right-aligned lines against the card's own
   left edge read as a mistake. */
.mfsec .aud-meta{flex:1 1 auto;min-width:0;align-items:flex-start;text-align:left}
.mfsec .aud-kick{overflow-wrap:anywhere}
.mfsec .mf-top>img{flex:none}
/* Once the kicker CAN wrap, cards in the same row start their titles at different heights —
   one line of mono against two puts eMAG's title 31px above Zalando's, and the row reads as
   misaligned rather than varied. A floor of two lines on the row levels every card's title
   without forcing a third line's worth of space on the ones that need none. 62px is two mono
   lines (12px / 1.6) plus the .mf-top gap, and it clears the 26px marks. */
@media(min-width:1101px){.mfsec--four .mf-top{min-height:62px}}

/* ————— Page closings: bespoke last stops (2026-09-04) ——————————————————————
   Two dead-end pages get a closing in their own grammar instead of a shared band:
   .pwx-/.pwt- news.html (the press desk: monument cut and ticker cut), .cnx- company-map.html
   (the constellation chain on the fixed dark band). history.html keeps
   its designed finale (rail into the footer curtain): its -100vh curtain contract leaves no
   room for an in-flow closing section. Hover gated to real pointers; reduced motion keeps
   color and drops the draw. Prefixes greped clean against tpl.js and this sheet. */
.pwx-date{font-size:14px;letter-spacing:.02em;color:var(--ink-muted);margin-bottom:18px}
.pwx-h{margin-bottom:clamp(32px,5vw,56px);font-size:clamp(21px,2.4vw,28px)}
.pwx-mail{display:inline-block;font-family:var(--font-display);font-weight:700;font-size:clamp(38px,8.5vw,104px);line-height:1;letter-spacing:-0.025em;text-decoration:none;background:linear-gradient(90deg,var(--red-ink) 0 50%,var(--ink) 50% 100%);background-size:210% 100%;background-position:100% 0;-webkit-background-clip:text;background-clip:text;color:transparent;transition:background-position 480ms cubic-bezier(0.23,1,0.32,1)}
@media(hover:hover) and (pointer:fine){.pwx-mail:hover{background-position:0 0}}
.pwx-mail:focus-visible{background:none;color:var(--red-ink)}
.pwx-sub{margin-top:clamp(24px,3.5vw,40px);font-size:16px;line-height:1.6;color:var(--ink-soft);max-width:62ch}
.pwx-sub a{color:var(--ink);text-underline-offset:3px}
@media(prefers-reduced-motion:reduce){.pwx-mail{transition:none}}

.cnx-h{margin-bottom:clamp(30px,4vw,52px)}
/* the sky: a full-bleed fixed-dark band (an object, like a chart plate: same in both themes) */
.cnx-sky{position:relative;height:clamp(320px,34vw,440px);background:#101014;contain:content}
.cnx-wires{position:absolute;inset:0;width:100%;height:100%}
.cnx-wires line{stroke:rgba(242,241,237,.3);stroke-width:1.5;transition:stroke 200ms ease-out}
/* stars and the here-mark anchor at (--x,--y); the glyph is centered ON the anchor, so the
   SVG line endpoints and the glyphs share one coordinate system */
.cnx-here,.cnx-star{position:absolute;left:var(--x);top:var(--y)}
.cnx-here .g,.cnx-star .g{position:absolute;left:0;top:0;transform:translate(-50%,-50%)}
.cnx-here{color:#8f9096;font-size:13.5px}
.cnx-here .g{width:14px;height:14px;fill:#8f9096}
.cnx-here>span{position:absolute;left:14px;top:-0.6em;white-space:nowrap}
.cnx-star{display:block;text-decoration:none;color:#F2F1ED}
.cnx-star .g{width:clamp(26px,3vw,34px);height:clamp(26px,3vw,34px);fill:#F2F1ED;transition:fill 180ms ease-out,transform 220ms cubic-bezier(0.23,1,0.32,1)}
.cnx-star .t{position:absolute;width:max-content;max-width:min(300px,26vw)}
/* the dossier's lines both pass BELOW its star (in from lower-left, out to lower-right),
   so its label lives ABOVE the glyph where neither line can ever reach */
.cnx-star--r .t{left:-6px;bottom:clamp(20px,2vw,28px)}
/* l2 arrives from upper-left -> label BELOW the star, growing leftward */
.cnx-star--d .t{right:-10px;top:clamp(22px,2.2vw,30px);text-align:right}
.cnx-star strong{display:block;font-family:var(--font-display);font-weight:700;font-size:clamp(24px,2.8vw,32px);line-height:1.1;letter-spacing:-0.015em;transition:color 180ms ease-out}
.cnx-star em{display:block;font-style:normal;margin-top:6px;font-size:14.5px;line-height:1.5;color:#a3a4aa}
@media(hover:hover) and (pointer:fine){
  .cnx-star:hover .g{fill:#E23B48;transform:translate(-50%,-50%) scale(1.18) rotate(22deg)}
  .cnx-star:hover strong{color:#E23B48}
  .cnx-sky:has(.cnx-star--r:hover) .cnx-l1{stroke:#E23B48}
  .cnx-sky:has(.cnx-star--d:hover) .cnx-l2{stroke:#E23B48}
}
@media(max-width:640px){
  .cnx-sky{height:auto;display:flex;flex-direction:column;gap:36px;padding:30px clamp(20px,5vw,32px)}
  .cnx-wires,.cnx-here{display:none}
  .cnx-star{position:static}
  .cnx-star .g{position:static;transform:none;display:block;margin-bottom:12px}
  .cnx-star .t{position:static;transform:none;max-width:none;text-align:left}
}
@media(prefers-reduced-motion:reduce){.cnx-star:hover .g{transform:translate(-50%,-50%)}}

/* the "-1" cuts of the page closings (2026-09-04): .pwt- the press-desk ticker strip (news). */
.pwt{display:block;overflow:hidden;border-top:1px solid var(--hairline);border-bottom:1px solid var(--hairline);padding:clamp(18px,2.6vw,30px) 0;text-decoration:none;color:var(--ink)}
.pwt-track{display:flex;white-space:nowrap;width:max-content;font-family:var(--font-display);font-weight:700;font-size:clamp(26px,3.6vw,44px);letter-spacing:-0.015em;animation:pwt-roll 26s linear infinite}
@keyframes pwt-roll{to{transform:translateX(-50%)}}
@media(hover:hover) and (pointer:fine){
  .pwt:hover .pwt-track{animation-play-state:paused;color:var(--red-ink)}
}
@media(prefers-reduced-motion:reduce){.pwt-track{animation:none}}


/* ————— THE PARADIGM: the ecosystem diagram, inline (the-problem-we-solve.html) —————
   assets/js/ecosystem.js draws the whole figure into <svg data-ecosystem="page"> on a
   1920x1080 canvas. It used to be an iframe; it is inline now, which is why the module
   grew a "page" mode that binds its keys and clicks to the <svg> instead of to document.

   THE PALETTE IS PINNED, DELIBERATELY. ecosystem.js reads these five off its host, and
   scoping them here rather than letting it inherit :root keeps the diagram on its authored
   light palette in both themes. It is a figure — a printed sheet reproduced in the page —
   and it stays a light sheet the way a photograph stays a photograph. It also has to: the
   module bakes the colours in at mount, so a theme flipped afterwards would only half-apply
   until reload. The deck pins the same values for its own reasons (pitchdeck decisions/0021),
   so the two now agree by construction.

   RESPONSIVE: it scrolls sideways rather than shrinking. Forty labelled blocks authored for
   a projector do not reflow — at 390px wide the whole canvas would render its 13px type at
   under 3px. min-width holds the figure at a size its smallest type survives (1180px puts
   13px at ~8px CSS, sharp on any 2x screen) and the container pans. Panning a wide diagram
   is something readers know how to do; reading 3px type is not. overscroll-behavior-x
   contains the pan so a swipe cannot walk the browser back a page. */
.eco-stage{position:relative;overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;
  -webkit-overflow-scrolling:touch;border:1px solid var(--hairline);background:var(--paper);
  --ink:#181818;      /* site --ink,       light theme */
  --paper:#FBFAF7;    /* site --paper-50,  the figure's ground */
  --paper-2:#F2F1ED;  /* site --paper-100, chips and nested bands */
  --red:#AB0000;      /* site --red */
  --steel:#114B98;    /* site --americas, the one cool accent, used for money */
}
.eco-stage svg{width:100%;min-width:1180px}
/* The pan hint exists only when there is something to pan: tpl.js sets .is-pannable off
   scrollWidth vs clientWidth, so it never claims a scroll that isn't there. */
.eco-pan{display:none;margin-top:10px;font-family:var(--font-mono);font-size:12px;
  letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted)}
.eco-stage.is-pannable + .eco-pan{display:block}
