/* GENERATED by site/tools/build-critical-css.mjs — DO NOT EDIT BY HAND.
   Render-blocking subset of tpl.css. The full tpl.css still loads asynchronously on every
   page and always wins the cascade, so editing tpl.css remains the only thing you need to do;
   just re-run the builder afterwards so the first frame keeps matching. */

/* ---- tpl.css 1-2160: fonts, tokens, dark theme, base type, nbar/abar chrome, nav dropdowns, mobile sheet, buttons, every hero family (video, photo, art, pinned) ---- */
/* 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}

/* ---- tpl.css 2623-2645: the KPI band, on 26 pages. Block 1 already carries `section:has(.kpi-band){padding:48px 0}` but stops ~480 lines before the .kpi rules themselves, so the cells paint as bare 26px lines and jump to 105px when tpl.css lands (.kpi padding 28px/24px + .num at 32px). That growth is what was moving DIV.wrap on team/brands/d2c/contact — measured 0.347 on team at 1440. Not markers on a banner, because the surrounding banner spans 577 lines of ai.html work; these two anchors cut the KPI rules alone, and the builder asserts the cut lands at depth 0 ---- */
.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}
}

/* ---- tpl.css 4479-4517: motion system: .reveal hidden state + no-JS rescue (omitting it flashes every reveal visible, then hides it when the full sheet lands) ---- */
/* ============ 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)}
}


/* ---- tpl.css 5816-5900: first-load loader — literally the first thing painted on a first visit — and the skip link ---- */
/* ===== 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}

/* ---- tpl.css 8377-8450: cursor ring + html[data-pt]::after arrival overlay, which covers the viewport on inbound page transitions ---- */
/* ————— 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}


/* ---- tpl.css 8511-8534: the case-study fact row, which sits INSIDE the hero on 20 pages. Without it the <ul> is a default stacked list and the row renders 102px tall, then collapses to 19px when tpl.css lands, dropping the hero lede 28px and everything under it: measured 0.152 CLS on case-studies/scott at 1440. 1.7KB buys all 20 pages, unlike the history rail (31KB, one page) which is a PAGE_SHEETS entry instead ---- */
/* ===== 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}
}


/* ---- tpl.css 8752-8860: the mobile full-screen hero and the things it depends on. Sections 2-4 have to travel together: 4 pulls the hero up by calc(-1 * var(--hero-top)) and sizes it to 100svh, 2 DEFINES --hero-top/--crumb-h (an undefined var makes the whole margin-top declaration invalid, so the hero drops back to margin 0), and 3 pins the breadcrumb to the one line that makes --crumb-h:41px true. Measured at 390px: hoisting 4 alone took CLS from 0.25-0.30 to 0.13-0.24 and left a 137.6px hero jump plus a 15.4px crumb-bar growth; the full 2-4 run is what removes it. Section 1 was added to the range on 2026-09-01: .hero-lede{margin:28px 0 36px} and .hero-cta{display:flex;gap:16px} are the ONLY rules sizing the hero content box, and being outside the range they landed late — the box grew 297->361px on models/d2c at 834 and 241->360px on contact, which is what was left moving DIV.wrap after the KPI band was hoisted. They are the styles that replaced per-page inline attributes, so every hero page depends on them ---- */
/* ---- 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}
}

