/* v2 OFF+BRAND — global design system. Black ground, white type, 3 greys. Zero colour.
   Every duration lives in :root so the whole site is tunable in one place. */
@font-face{font-family:"BC Fallback";src:local("Arial Narrow"),local("ArialNarrow"),local("Liberation Sans Narrow");size-adjust:94%;ascent-override:100%;descent-override:20%}
@font-face{font-family:"Barlow Fallback";src:local("Arial"),local("Helvetica"),local("Liberation Sans");size-adjust:96%;ascent-override:100%;descent-override:20%}
@font-face{font-family:"CP Fallback";src:local("Menlo"),local("Consolas"),local("Courier New");size-adjust:88%}
:root{
  /* ink: one black, one white, three greys.
     --g1 is the COMPONENT BORDER. It was #2e2e2e = 1.55:1 on black, and WCAG
     1.4.11 asks 3:1 of any boundary that carries meaning — which the tile,
     plate, card and table edges in this system all do.

     Measured rather than taken on trust: the value the audit proposed,
     #4a4a4a, is **2.37:1** on black and does not meet the 3:1 it cites.
     #595959 hits 3.00:1 on black but only 2.70:1 on --tint, and cards such as
     .rg keep a --g1 border while switching to that ground on hover. #606060
     is the first value that clears 3:1 in BOTH states: 3.34:1 on #000,
     3.00:1 on #111.

     --hair keeps the old value for the things that are genuinely hairlines
     rather than boundaries: the unfilled part of a progress track, which is a
     ground for the white fill above it and is never read on its own. */
  /* ---- the two palettes, each value written EXACTLY ONCE ----------------
     `--n-*` is night and `--d-*` is day. Nothing draws through them directly:
     every component still draws through --bk/--wh/--g1..3 and the rest, and
     each scope below simply REMAPS those onto one palette or the other.

     The indirection buys the one thing plain CSS otherwise cannot give. Three
     scopes now need a palette that is not the page's — the day theme, the
     photographic surfaces inside it, and the footer, which inverts against
     whichever theme is running — and before this each of those wrote its own
     copy of the ten values. A palette written three times is a palette that
     drifts; written once and remapped, a corrected grey reaches every scope
     that uses it by construction. */
  --n-bk:#000;--n-wh:#fff;--n-g1:#606060;--n-g2:#8f8f8f;--n-g3:#cfcfcf;
  --n-hair:#2e2e2e;        /* progress-track ground, never a boundary */
  --n-g0:#1a1a1a;          /* softest rule, inside dense grids */
  --n-tint:#111;           /* hover ground */
  --n-tint-2:#0d0d0d;      /* table row hover */
  --n-field:#6a6a6a;       /* form control border — 2.73:1 at #555 */
  --n-on-fill:#333;        /* body text on a white-filled card */
  --n-on-fill-2:#444;      /* its muted sibling */
  --n-deco-stroke:#3a3a3a; /* outline numerals behind cards */
  --n-field-bg:#050505;    /* form control interior */
  /* the 3D mark's extrusion ramp: far end (sunk into the ground) and near
     end (closest to the front face). #222 and #b8b8b8 are exactly the 34
     and 184 the builder used to write as absolute greys. */
  --n-lyr-far:#222;--n-lyr-near:#b8b8b8;
  /* the STRAIGHT run of a bevelled edge, as distinct from the two chamfer
     cuts, which stay at full ink. Night keeps --g1 exactly. */
  --n-edge:var(--n-g1);
  /* the DAY palette. Greys are re-derived rather than inverted, because
     inversion does not preserve contrast: --g3 is body text, and #cfcfcf gives
     11.5:1 on black while its inverse #303030 would sit on an off-white
     ground, so it is set directly. --g1 is the component border and clears the
     3:1 WCAG 1.4.11 asks of a meaningful boundary in BOTH the base and hover
     grounds — the same double-test that chose #606060 for night. */
  --d-bk:#f7f7f5;          /* ground — off-white, not #fff: the plate edges and
                              hairlines disappear against pure white */
  --d-wh:#0a0a0a;          /* ink */
  --d-g1:#878787;          /* boundary — 3.35:1 on the ground AND 3.04:1 on --tint */
  --d-g2:#5e5e5e;          /* secondary text — measures 6.05:1 */
  --d-g3:#333;             /* body text — measures 11.78:1 */
  --d-hair:#d6d6d2;
  --d-g0:#e6e6e2;
  --d-tint:#ececea;
  --d-tint-2:#f1f1ef;
  --d-field:#8f8f8f;
  --d-on-fill:#d0d0d0;     /* text on a black-filled card, now the inverted one */
  --d-on-fill-2:#bdbdbd;
  --d-deco-stroke:#dcdcd8;
  --d-scan:rgba(0,0,0,.028);
  --d-field-bg:#fff;
  /* mirrored, so the stack still sinks INTO the ground rather than out of
     it: far is the light end on a light ground. */
  --d-lyr-far:#ddd;--d-lyr-near:#474747;
  /* #666 (Bailey, 2026-09-23: "should be like a #666 or lighter to make it
     more contrasted"). It is 5.74:1 on the day ground where --g1 was 3.35,
     so the frame itself is far more present, while still sitting 92
     luminance points clear of the #0a0a0a cut so the corner keeps reading. */
  --d-edge:#666;
  /* ---- the ACTIVE palette. :root runs night. ---------------------------- */
  --bk:var(--n-bk);--wh:var(--n-wh);--g1:var(--n-g1);--g2:var(--n-g2);--g3:var(--n-g3);
  --hair:var(--n-hair);
  --g0:var(--n-g0);
  --tint:var(--n-tint);
  --tint-2:var(--n-tint-2);
  --field:var(--n-field);
  --lyr-far:var(--n-lyr-far);--lyr-near:var(--n-lyr-near);--edge-line:var(--n-edge);
  /* Three roles that were hardcoded in a dozen rules and therefore could not
     follow a theme: text sitting ON an inverted (filled) surface, the
     decorative outline numerals, and the scanline. Tokenised so the day
     palette flips them with everything else. */
  --on-fill:var(--n-on-fill);
  --on-fill-2:var(--n-on-fill-2);
  --deco-stroke:var(--n-deco-stroke);
  /* LEFT EXACTLY AS FOUND, and it is not a typo of mine: `--scan:var(--scan)`
     is a self-reference, so per spec the property is invalid at computed-value
     time and `.scanl`'s repeating-linear-gradient collapses with it — the
     night hero's scanline has therefore been painting nothing. Preserved
     rather than repaired, because giving it a real value changes how the night
     hero looks and that is a design change nobody asked for. Day sets a real
     value below, which is why the scanline appears only in day. Recorded in
     ITERATIONS.md round 10 for Bailey to decide. */
  --scan:var(--scan);
  /* Three more that were hardcoded black and therefore could not follow a
     theme either. They are SURFACES rather than ink, which is why the earlier
     tokenising pass missed them: each one is the ground some other element's
     ink is read against, so a ground that stays black under ink that flipped
     is invisible text — the day theme's whole failure mode.
       --nav-bg   the fixed header's backdrop. The whole gradient is the token,
                  not its stops: day needs a longer, stronger ramp than night
                  because it is the only theme where the header can sit over a
                  surface darker than its own ground (a case-study cover), and
                  one shared stop list cannot serve both.
       --bar      the sticky chapter rail on a case study.
       --field-bg the interior of a form control. #050505 under `color:var(--wh)`
                  meant a reader in day theme could not see what they typed. */
  --nav-bg:linear-gradient(180deg,rgba(0,0,0,.85),rgba(0,0,0,0));
  --bar:rgba(0,0,0,.94);
  --field-bg:var(--n-field-bg);
  --g:clamp(16px,2.8vw,44px);
  --disp:"Barlow Condensed","BC Fallback","Arial Narrow",Impact,sans-serif;
  --hud:"Chakra Petch","CP Fallback",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  --body:"Barlow","Barlow Fallback","Helvetica Neue",Helvetica,Arial,sans-serif;

  /* ---- type system -------------------------------------------------------
     Display tracking is set in px inside a clamp, so the em ratio *falls* as
     the type grows — the optical correction condensed italic caps need at
     200px+ but must not get at 44px. One tier per size band. */
  --tr-mega:clamp(-3.4px,-.2vw,-.4px);    /* 90–300px  */
  --tr-lg:clamp(-2px,-.13vw,-.3px);       /* 44–170px  */
  --tr-md:clamp(-.5px,-.035vw,-.05px);    /* 24–64px   */
  --tr-sm:.008em;                          /* under 24px: condensed needs air */
  --lh-mega:.86;--lh-lg:.88;--lh-md:.95;   /* loosened again on phones below */
  /* HUD: two tiers. --hud-* is the label tier; .hud-r is the reading tier. */
  --hud-fs:12.5px;--hud-tr:.11em;
  --hud-xs-fs:11px;--hud-xs-tr:.16em;
  --hud-r-fs:14px;--hud-r-tr:.02em;
  /* measure: prose 65–72ch; display ledes fewer characters, not more */
  --measure:68ch;--measure-wide:72ch;--measure-lede:52ch;

  /* ---- space scale — ONE ladder. Nothing vertical is hand-tuned off it. ----
     Every margin, padding and gap in the three stylesheets resolves to one of
     these steps. The fluid steps are the same ladder with a viewport-relative
     middle, so a value is on the scale at both ends of its range. */
  --sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:16px;--sp-5:24px;--sp-6:32px;
  --sp-7:48px;--sp-8:64px;--sp-9:96px;--sp-10:128px;
  --sp-f-4:clamp(12px,1.6vw,16px);
  --sp-f-5:clamp(16px,2.2vw,24px);
  --sp-f-6:clamp(24px,3vw,32px);
  --sp-f-7:clamp(32px,4vw,48px);
  --sp-f-8:clamp(48px,6vw,64px);
  --sp-f-9:clamp(64px,8vw,96px);
  --sp-f-10:clamp(80px,10vw,128px);
  /* Legacy aliases, so a rule not yet migrated still lands on the ladder. */
  --s-1:var(--sp-2);--s-2:var(--sp-3);--s-3:var(--sp-5);
  --s-4:var(--sp-f-6);--s-5:var(--sp-f-8);
  /* Exactly two section paddings, both off the ladder. Same section type
     always gets the same one. They scale down on phones, never to zero. */
  --sec:var(--sp-f-9);                 /* standard */
  --sec-lg:var(--sp-f-10);             /* major */
  --head-gap:var(--sp-f-7);            /* section head → body */

  /* ---- the edge: one geometry, one corner size, defined once -------------
     The signature v2 chamfer: top-right and bottom-left. Applied only to
     things that deserve an edge — buttons, tiles, plates — and nowhere else.

     The element paints the line colour and a pseudo-element paints the
     interior one --edge inside it; the sliver between them IS the border, and
     it survives on ALL SIX edges including the two diagonals. A real `border`
     cannot: clip-path cuts the border away together with the corner, which is
     why the outlined buttons lost their top-right and bottom-left lines. */
  --clip:12px;
  --edge:1px;
  --bevel:polygon(0 0,calc(100% - var(--clip)) 0,100% var(--clip),100% 100%,var(--clip) 100%,0 calc(100% - var(--clip)));
  /* Top-right cut only. For a FILLED CHILD that bleeds to the top edge of a
     plated parent: a rectangle painted over a chamfered box squares the corner
     back off, and the parent's plate is still drawing its diagonal underneath,
     so the cut is covered rather than removed. Clipping the parent instead
     would shave the plate's own diagonals away and leave the edge broken on
     two of its six sides, which is the trap this whole idiom exists around.
     Only the top-right is cut because these children sit at the TOP of their
     parent; the bottom-left chamfer is never theirs to draw. */
  --bevel-tr:polygon(0 0,calc(100% - var(--clip)) 0,100% var(--clip),100% 100%,0 100%);
  --stroke:1.5px;--stroke-fine:1px;    /* outlined numerals + marks */

  --t-fast:160ms;--t-med:240ms;--t-slow:320ms;--t-ovl:320ms;--t-ovl-out:220ms;
  --ease:cubic-bezier(.2,.8,.2,1);--out:cubic-bezier(.16,1,.3,1);--cut:cubic-bezier(.7,0,.2,1);--snap:cubic-bezier(.2,1.2,.4,1);
}
@media (max-width:760px){:root{--lh-mega:.9;--lh-lg:.92;--lh-md:1}}
/* The 200ms view-transition cross-fade is gone. Round 8 replaced it with the
   broken-signal page transition at the end of this file; the reasoning is
   there, and the short version is that an MPA view transition holds the new
   document's first render until the transition is ready, which is the one
   thing a loading animation under a 1s budget may not do. */
*,*::before,*::after{box-sizing:border-box}
html{color-scheme:dark;-webkit-text-size-adjust:100%;scroll-padding-top:var(--sp-8)}
@media (prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}
html.lock{overflow:hidden}
body{margin:0;background:var(--bk);color:var(--wh);font-family:var(--body);font-size:17px;line-height:1.58;overflow-x:clip;-webkit-font-smoothing:antialiased}
img{display:block;max-width:100%;height:auto}
/* ---------- links: no underlines, but every affordance replaced ------------
   An underline is an affordance, so each one removed is substituted, never
   just deleted:
     · inline prose links   — brighter ink and 600 weight at rest, inverting
                              to a filled chip on hover and focus
     · standalone link rows — the arrow already in the label, which slides
     · keyboard focus       — a visible ring everywhere; on clipped components
                              it is drawn inside the shape, because clip-path
                              would otherwise cut the ring away
   No state is signalled by colour alone: each also changes weight, ground or
   position, so it survives greyscale and forced-colours rendering. */
a{color:inherit;text-decoration:none}
p a,.acc-a a,.art-sec a,.h2h-copy a,.grp-b a,.hv a,.lede a,.rail-c a,.partner-line a,.ptable a,li a:not([class]){
  color:var(--wh);font-weight:600;padding-inline:.18em;margin-inline:-.18em;
  transition:background var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
p a:hover,p a:focus-visible,.acc-a a:hover,.acc-a a:focus-visible,
.art-sec a:hover,.art-sec a:focus-visible,.h2h-copy a:hover,.h2h-copy a:focus-visible,
.grp-b a:hover,.grp-b a:focus-visible,.hv a:hover,.hv a:focus-visible,
.lede a:hover,.lede a:focus-visible,.rail-c a:hover,.rail-c a:focus-visible,
.partner-line a:hover,.partner-line a:focus-visible,.ptable a:hover,.ptable a:focus-visible,
li a:not([class]):hover,li a:not([class]):focus-visible{background:var(--wh);color:var(--bk)}
strong{font-weight:600;color:var(--wh)}
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
/* Highlighting disabled sitewide (Bailey, 2026-09-22). Two separate things,
   both of which read as "highlighting" to a reader:
     1. ::selection — the inverted block on drag-selected text. Set to
        transparent rather than removed, because deleting the rule restores the
        browser's own blue, which is worse: it is the one colour this monotone
        design has nowhere else.
     2. -webkit-tap-highlight-color — the grey flash on tap. There was NO rule
        for it, so every tap on a phone flashed the UA default.
   Text stays SELECTABLE. user-select:none would stop a reader copying an
   address or a quote, which is a different and much worse change than turning
   the highlight off. */
::selection{background:transparent;color:inherit}
::-moz-selection{background:transparent;color:inherit}
html{-webkit-tap-highlight-color:transparent}
:focus-visible{outline:2px solid var(--wh);outline-offset:3px}
[hidden]{display:none!important}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.skip{position:absolute;left:var(--g);top:-80px;z-index:900;background:var(--wh);color:var(--bk);padding:var(--sp-3) var(--sp-4)}
.skip:focus{top:12px}
/* HUD tier 1 — short labels, codes, counters. Tracked and uppercase. */
.hud{font-family:var(--hud);font-size:var(--hud-fs);font-weight:500;letter-spacing:var(--hud-tr);text-transform:uppercase;line-height:1.45}
/* HUD tier 0 — tiny codes only (S1, P01, 04 / 12) */
.hud-xs{font-size:var(--hud-xs-fs);letter-spacing:var(--hud-xs-tr)}
/* HUD tier 2 — the reading tier. Any HUD run that is a sentence rather than a
   label drops the caps and most of the tracking: 12px/.14em uppercase is
   unreadable over a line or more, which is what these all are. */
.crumbs,.foot-nap,.art-meta,.r-meta,.rail-o,.nudge,.partner-line,.phero .cat,
.film-ctl,.foot-legal,.tq figcaption .mute,.side-b p{
  font-size:var(--hud-r-fs);letter-spacing:var(--hud-r-tr);text-transform:none;line-height:1.6}
.mute{color:var(--g2)}
.wrap{padding-inline:var(--g)}
/* standalone link row — the arrow in the label is the affordance, and it
   slides. No underline, and the tap target stays ≥44px tall. */
.ul{display:inline-flex;align-items:center;gap:var(--sp-2);min-height:44px;padding-block:var(--sp-3);
  color:var(--g3);transition:color var(--t-fast) var(--ease),translate var(--t-fast) var(--ease)}
/* background:none is the load-bearing half. .ul sits inside <p>, so the prose-
   link rule above already matched it and supplied `background:var(--wh)`; this
   rule ties that rule on specificity and, being later, wins only `color` — so
   the label was painted in its own fill and vanished. It read white-on-white at
   night and black-on-black in day, which is how Bailey saw it. These links have
   their own affordance — brighten and nudge, the same one .foot-cols uses — so
   the inherited chip fill is declined rather than coloured around. */
.ul:hover,.ul:focus-visible{background:none;color:var(--wh);translate:3px 0}
.o{color:transparent;-webkit-text-stroke:var(--stroke) var(--wh);font-style:inherit}
/* one outlined-numeral treatment, was eight near-copies */
.onum{color:transparent;-webkit-text-stroke:var(--stroke) var(--wh);font-variant-numeric:tabular-nums}
/* Corner ticks. This painted only the top-left and bottom-right corners, so
   every framed element read as a diagonal smear rather than a frame — 148
   instances, and a large part of why the footer looked like it had debris in
   it. All four corners now. */
/* The four corner registration marks are replaced by the BUTTON's own bevel
   (Bailey, 2026-09-22): top-right and bottom-left cut at 45deg and painted
   bright, the four straight sides a subtle grey. One edge language now covers
   buttons, chrome controls and boxes alike.

   Two layers, never a `border`: clip-path removes a border along the chamfer,
   which is the exact defect round 3 fixed on .btn. The element paints the edge
   and ::after paints the interior one --edge inside it.

   The gradient runs at 45deg, which is the axis through the two cut corners:
   bottom-left and top-right are --tk-hi, the middle is --tk-edge. Both are
   custom properties so a host can invert them on hover.

   The interior sits at z-index:-2, NOT -1: .ins-row already uses ::before at
   -1 for its hover fill, and an interior painted above that would swallow it.
   ::after is used rather than ::before for the same reason — ::before is
   taken on the one host that has 11 instances. */
/* ---- DAY THEME -----------------------------------------------------------
   The opposite of the night theme, as Bailey asked: black on white. It is a
   PALETTE FLIP and nothing else — not a single component rule is duplicated
   below, because every one of them already draws through these ten tokens.

   An ATTRIBUTE on <html>, never a class. A class on the root element that also
   has a bare element rule is exactly what put display:none on <html> and
   shipped a blank homepage on 2026-09-22; data-theme cannot collide with any
   selector in this file. The inline boot script sets it before first paint, so
   a reader who chose day never sees a black flash.

   The token NAMES are historical roles, not literal colours: --bk is the
   ground and --wh the ink, so in day they hold white and near-black. Renaming
   them would touch every rule in three stylesheets for no reader-visible gain.

   Greys are re-derived rather than inverted, because inversion does not
   preserve contrast. --g3 is body text: #cfcfcf gives 11.5:1 on black, but its
   inverse #303030 would sit on an off-white ground, so it is set directly.
   --g1 is the component border and must clear the 3:1 that WCAG 1.4.11 asks of
   a meaningful boundary in BOTH the base and hover grounds, the same
   double-test that chose #606060 for night. */
html[data-theme="day"]{
  --bk:var(--d-bk);--wh:var(--d-wh);--g1:var(--d-g1);--g2:var(--d-g2);--g3:var(--d-g3);
  --hair:var(--d-hair);
  --g0:var(--d-g0);
  --tint:var(--d-tint);
  --tint-2:var(--d-tint-2);
  --field:var(--d-field);
  --lyr-far:var(--d-lyr-far);--lyr-near:var(--d-lyr-near);--edge-line:var(--d-edge);
  --on-fill:var(--d-on-fill);
  --on-fill-2:var(--d-on-fill-2);
  --deco-stroke:var(--d-deco-stroke);
  --scan:var(--d-scan);
  /* The header ramp is stronger than night's, and the asymmetry is real rather
     than an oversight. Night's ink is #fff, which is as far from a dark
     backdrop as ink gets, so a .85 scrim that fades to nothing is plenty. Day's
     ink is #0a0a0a, and on a case-study cover the backdrop is ALSO near-black —
     a photograph at brightness(.5) under the 75% black top stop of
     .rhero-scrim, composited to about #030303. Mirroring night's .85→0 ramp
     there gives 3.7:1 and fails. Measured at the nav ink's own vertical centre
     (~50% of a 60px header) this ramp composites to #bebebe and 10.7:1.
     It costs nothing on the other 44 pages: a near-white scrim over a
     near-white ground is invisible whatever its alpha.

     --nav-bg and --bar are gradients and alpha fills rather than flat inks, so
     they are NOT part of the two named palettes: a scrim has to be the colour
     of the ground it sits on AND carry its own ramp, which is one value rather
     than a remap of another.
     (Note the wording: writing the palette prefixes with a star, as
     `--n-<star>/--d-<star>`, puts the two characters `*` and `/` next to each
     other and ENDS THE COMMENT there. That is exactly what happened in the
     first draft of this block, and it swallowed the --nav-bg declaration
     below, so the day theme silently fell back to the night header gradient.) */
  --nav-bg:linear-gradient(180deg,rgba(247,247,245,.94),rgba(247,247,245,.74) 58%,rgba(247,247,245,0));
  --bar:rgba(247,247,245,.94);
  --field-bg:var(--d-field-bg);
}
/* color-scheme follows the theme, or the browser keeps painting the scrollbar,
   the caret, the focus ring and every default form control for a dark page
   under a light one. It is set on the same selector as the palette so the two
   can never disagree. */
html[data-theme="day"]{color-scheme:light}
/* ---- day: the straight edges lighten so the chamfers read ------------------
   The chamfer is a CLIP, not a drawn line, so on a flat edge the diagonal is
   the same colour as the sides and the corner does not register. In day the
   four straight runs drop to --g1, leaving the cut at full ink. Night keeps a
   single-colour edge, where a bright line on black already reads.
   --pl-line on the two plate users that set it to the ink colour on an
   UNFILLED box gets the same treatment. The many other --pl-line:var(--wh)
   rules are deliberately untouched: they sit on boxes that are FILLED white,
   where the line is set to the fill colour precisely to make the plate
   invisible, and lightening those would draw a grey frame across a filled
   chip — the exact fault the chapter-pill rule warns about. */
/* :not(.btn-solid) — a FILLED button needs none of this. Its chamfer is a cut
   in a solid black shape against a near-white page, which is the strongest
   corner on the site already; lightening its sides only adds a grey hairline
   around the fill. Measured: the outlined controls went from a 1-point side/cut
   difference to 126, and the filled button was at 126 before the change. */
html[data-theme="day"] .btn:not(.btn-solid){--btn-edge:var(--edge-line)}
html[data-theme="day"] .x-btn,html[data-theme="day"] .burger,
html[data-theme="day"] .nav-lang,html[data-theme="day"] .nav-theme,
html[data-theme="day"] .tst-ctl button{--ctl-edge:var(--edge-line)}
html[data-theme="day"] .yb,html[data-theme="day"] .f-banner{--pl-line:var(--edge-line)}

/* ---- surfaces the palette must NOT flip: type standing on a photograph ----
   The comment under the day block says the photo scrims are deliberately not
   flipped, and that is right: a venue photograph does not get lighter because
   the page around it did. What it did not carry through is the consequence.
   Type on those scrims draws through the same --wh and --g3 as type on the
   page, so when the palette flipped, eight case-study covers, every venture
   card in the carousel, the closing CTA on 39 pages and the next-project card
   went black on a black photograph. Reported by Bailey on the BOM cover, and
   present on all five components.

   Pinning the night palette to the scrim-bearing components is the same move
   as the day block itself — a palette flip, no component rule duplicated —
   only in the other direction, and it is scoped to day because in night these
   values are already what is in force.

   .foot is in this list for a DIFFERENT reason, and shares the rule only
   because it needs the identical remap. The footer inverts against whichever
   theme is running — light on the night site, dark on the day site — so in day
   it wants the night palette for the same four lines the photographs want it
   for. Its night-theme counterpart is the rule immediately below.

   `color` is re-declared, and that is the load-bearing line. Custom properties
   inherit, but `color` inherits as a COMPUTED value: body's `color:var(--wh)`
   has already resolved to near-black by the time it reaches .rhero, so
   redefining --wh here would leave every heading exactly as black as it was.
   Anything that sets its own colour (--g3 ledes, --g2 HUD rows, the filled
   chips) re-resolves by itself. */
/* Two things the pin above cannot reach, both found by the same pixel
   differential and neither visible in any stylesheet on its own.

   1. The reading-progress bar is position:fixed at the very top of the
      viewport and knows nothing about what it is floating over. Its fill is
      --wh, so in day it is near-black — correct on the page, invisible for the
      several hundred pixels where the top of the viewport sits on the closing
      CTA's photograph, which is one of the surfaces pinned above. It gets an
      opaque track in day, the same idiom as the intro and transition rails,
      which makes the fill legible whatever is behind it. --hair on the day
      ground is 1.23:1, so the track is invisible on the page itself and only
      does work where the work is needed. Night keeps a transparent track: on a
      black ground a #2e2e2e track would be new ornament for no gain.

   2. The proof-card brand logos are `_light` assets — WHITE SILHOUETTES, drawn
      to stand on a black card. Measured rather than assumed: all four in use
      (BOM, Highline, KITCHAI, Maestria) are alpha masks filled 240–255 with
      zero saturation. On the day theme's light card they disappear entirely,
      which is the identical defect the production site shipped in session 19.
      brightness(0) rather than invert(1): it forces black from ANY source, so
      a logo with internal tonal range — auntietang_light is one, mean 167 over
      a 5–255 spread — renders as a clean silhouette instead of a photographic
      negative. The dark-ground siblings, .card-logo and .r-logo, need nothing:
      both sit inside a scope pinned above, where white is still correct.
      Swapping to the non-_light asset was rejected: four of those files carry
      real colour (BOM saturation 207, Silver Pearl 116, KITCHAI 72) and this
      direction is monotone. */
html[data-theme="day"] .read-p{background:var(--hair)}
html[data-theme="day"] .pc-logo{filter:brightness(0)}

html[data-theme="day"] .rhero,
html[data-theme="day"] .close.has-ph,
html[data-theme="day"] .ty-card,
html[data-theme="day"] .card,
html[data-theme="day"] .foot{
  --bk:var(--n-bk);--wh:var(--n-wh);--g1:var(--n-g1);--g2:var(--n-g2);--g3:var(--n-g3);
  --hair:var(--n-hair);--g0:var(--n-g0);--tint:var(--n-tint);--tint-2:var(--n-tint-2);
  --field:var(--n-field);--on-fill:var(--n-on-fill);--on-fill-2:var(--n-on-fill-2);
  --deco-stroke:var(--n-deco-stroke);--field-bg:var(--n-field-bg);
  --lyr-far:var(--n-lyr-far);--lyr-near:var(--n-lyr-near);--edge-line:var(--n-edge);
  color:var(--wh);
}
/* The photo scrims are deliberately NOT flipped. They are dark gradients laid
   over photographs so white type stays legible on them, and that is true in
   either theme: a venue photograph does not become lighter because the page
   around it did. Flipping them would make the type on every hero photo
   unreadable, which is the opposite of a theme working. */

/* ---- the footer inverts against the theme -------------------------------
   Bailey: "part of this design the footer in both themes should be
   contrasting against the theme." So the band is light on the night site and
   dark on the day site, and it is the same mechanism in both directions: a
   palette remap on one subtree, which is all the day theme itself is. The day
   half sits in the rule above, sharing it because the remap is identical; this
   is the night half, and it is the only place on the night site that runs the
   day palette.

   :not([data-theme="day"]) rather than a bare `.foot`, because the theme
   attribute is absent on the night site — night is the default and sets no
   attribute — so there is nothing positive to match on.

   `color` is re-declared here for the same reason it is above: `color`
   inherits as a COMPUTED value, so body's has already resolved to white by the
   time it reaches the footer and remapping --wh alone would leave every line
   of the footer white on a white band.

   background-color, never the `background` shorthand: the shorthand resets
   background-image, and .foot sits in the same stylesheet as the plate idiom
   whose whole edge is a background-image. It costs nothing to be consistent. */
html:not([data-theme="day"]) .foot{
  --bk:var(--d-bk);--wh:var(--d-wh);--g1:var(--d-g1);--g2:var(--d-g2);--g3:var(--d-g3);
  --hair:var(--d-hair);--g0:var(--d-g0);--tint:var(--d-tint);--tint-2:var(--d-tint-2);
  --field:var(--d-field);--on-fill:var(--d-on-fill);--on-fill-2:var(--d-on-fill-2);
  --deco-stroke:var(--d-deco-stroke);--field-bg:var(--d-field-bg);
  --lyr-far:var(--d-lyr-far);--lyr-near:var(--d-lyr-near);--edge-line:var(--d-edge);
  color:var(--wh);
}
/* One declaration serves both directions: inside either remap above, --bk is
   already the OPPOSITE ground. The band is full-bleed because .foot carries
   .wrap, which is padding-inline only and sets no width. */
.foot{background-color:var(--bk)}
/* The junction, day only (Bailey, 2026-09-23). With the footer inverted, a page
   whose closing CTA carries a photograph puts a dark band directly above a dark
   band, and the page-coloured gap between them is the only separation -- at
   --sec-lg it reads as a seam rather than a break. Widened so it reads as
   deliberate.
   `:has()` rather than a body class, because the condition is about what the
   PAGE ENDS WITH, which the markup already states: 39 of the 50 closing CTAs
   carry .has-ph and the other 11 must not pay for it. Night is untouched: there
   the footer is light and the transition is a single edge already.
   The value is two existing ladder steps summed rather than a new one, because
   the comment on --sec/--sec-lg says there are exactly two section paddings and
   a third would make that false. */
html[data-theme="day"] main:has(.close.has-ph) ~ .foot{
  margin-top:calc(var(--sec-lg) + var(--sp-f-7))}

/* SIX LINE LAYERS, not a clipped fill. The first attempt filled the box with a
   gradient and covered the middle with an opaque interior, which is a solid
   shape with a masked centre rather than an outline — Bailey read it as a
   solid background, correctly. This draws the edge itself: four thin lines
   that stop short of the two cut corners, and a bright diagonal laid into each
   chamfer. Nothing sits behind them, so the interior is genuinely transparent
   and a box can now sit over a photograph.
   Same idiom as the corner marks this replaces, which were eight gradient
   layers; the shape changed, the technique did not.
   Both colours are tokens, so the day theme flips the palette rather than
   rewriting any component: --pl-line is the thin dark side, --pl-hi the two
   bright 45deg cuts. */
.tick,.plate{position:relative;--pl-hi:var(--wh);--pl-line:var(--hair);
  background:
    linear-gradient(to top right,transparent calc(50% - .5px),var(--pl-hi) calc(50% - .5px),var(--pl-hi) calc(50% + .5px),transparent calc(50% + .5px)) 100% 0/var(--clip) var(--clip) no-repeat,
    linear-gradient(to top right,transparent calc(50% - .5px),var(--pl-hi) calc(50% - .5px),var(--pl-hi) calc(50% + .5px),transparent calc(50% + .5px)) 0 100%/var(--clip) var(--clip) no-repeat,
    linear-gradient(var(--pl-line),var(--pl-line)) 0 0/calc(100% - var(--clip)) 1px no-repeat,
    linear-gradient(var(--pl-line),var(--pl-line)) 0 0/1px calc(100% - var(--clip)) no-repeat,
    linear-gradient(var(--pl-line),var(--pl-line)) 100% 100%/calc(100% - var(--clip)) 1px no-repeat,
    linear-gradient(var(--pl-line),var(--pl-line)) 100% 100%/1px calc(100% - var(--clip)) no-repeat}

/* ── the bevel, on every card and panel ──────────────────────────────────
   Audited across all 53 built pages: .tick carried the chamfer on 23 pages
   and .plate on NONE, while every card and panel drew a plain square
   `border:1px solid`. So one page ran two edge languages at once, chamfered
   controls and the four-corner registration mark sitting above square
   panels. That mismatch is what the services closing CTA was showing.

   Two techniques, because these components are not built alike.

   FLAT PANELS take the six-gradient plate, declared as background-IMAGE
   rather than the `background` shorthand .tick/.plate uses. Several of them
   fill on hover, and the shorthand resets background-color on every state
   change, so drawing the edge that way would delete the hover fill. The
   interior stays genuinely transparent, so a panel can still sit over a
   photograph.

   PHOTO TILES take a clip-path chamfer and drop the border outright. They
   carry full-bleed media, and a clip-path cuts the whole element, media
   included. It also cuts a border away along the diagonal, which is the
   documented trap behind this whole idiom, so the border goes rather than
   being kept and left broken on two edges of six.

   THE LIST BELOW NAMES COMPONENTS FROM ALL THREE STYLESHEETS, deliberately.
   .swot, .bm-grid, .c-three, .r-tags li, .rail-o, .rail-n and .lap live in
   report.css and .ch-b in showcase.css, but the plate is fourteen lines of
   gradient arithmetic and a second copy of it in another sheet is a second
   thing to keep in step. site.css is loaded by every page, so a selector here
   for a class those pages never use simply never matches. Same precedent as
   `.ty-card,.card` below, where .card is showcase.css's. What each of those
   sheets still owns is its own colours and fills, which is where the two
   traps live: a `background:` SHORTHAND in a later sheet erases the plate,
   and a FILLED box cannot be chamfered by the plate at all.

   Corner size is a PROPORTION of the box, not a constant. 12px reads as a
   chamfer on a 300px panel and as a bitten-off corner on a 28px control, so
   the chips and small controls take 6px and the accordion plus takes 5px. */
.chip,.rg-meta li,.nap-lang,.r-tags li,.rail-o,.ins-cta,.chnav a,.ch-b,.rail-n{--clip:6px}
.acc-x{--clip:5px}
.fq,.pc,.board,.f-banner,.yb,.chip,.rg-meta li,.nap-lang,.pcard::after,
.acc-x,.chnav a,.ins-cta,.rm-table,.rm-table tr,.lap,
.swot,.bm-grid,.c-three,.r-tags li,.rail-o,.rail-n,.ch-b{
  position:relative;--pl-hi:var(--wh);--pl-line:var(--edge-line);
  background-image:
    linear-gradient(to top right,transparent calc(50% - .5px),var(--pl-hi) calc(50% - .5px),var(--pl-hi) calc(50% + .5px),transparent calc(50% + .5px)),
    linear-gradient(to top right,transparent calc(50% - .5px),var(--pl-hi) calc(50% - .5px),var(--pl-hi) calc(50% + .5px),transparent calc(50% + .5px)),
    linear-gradient(var(--pl-line),var(--pl-line)),
    linear-gradient(var(--pl-line),var(--pl-line)),
    linear-gradient(var(--pl-line),var(--pl-line)),
    linear-gradient(var(--pl-line),var(--pl-line));
  background-position:100% 0,0 100%,0 0,0 0,100% 100%,100% 100%;
  background-size:
    var(--clip) var(--clip),var(--clip) var(--clip),
    calc(100% - var(--clip)) 1px,1px calc(100% - var(--clip)),
    calc(100% - var(--clip)) 1px,1px calc(100% - var(--clip));
  background-repeat:no-repeat}
/* the two panels whose edge was white rather than grey keep that weight */
.yb,.f-banner{--pl-line:var(--wh)}
/* Photo tiles: the image defines the interior, the chamfer defines the edge. */
.ty-card,.card{clip-path:var(--bevel)}
/* FILLED boxes — the third case, and the one the first pass of this sweep got
   wrong. The plate draws LINES, and a line the same colour as the fill under
   it is invisible: a filled chip or tab keeps its square corners and ends up
   the only unbevelled thing in a row of bevelled ones, which is the mismatch
   this whole block exists to remove, reintroduced at the exact moment a
   reader is looking at the element. So the fill itself is clipped. clip-path
   takes the plate's lines away with the corner — the documented trap — and
   that is harmless here for the one reason it is fatal elsewhere: at this
   point there is no contrasting edge left to break.
   The report.css and showcase.css halves of this list (.r-tags li.em,
   .rail-n when reached, .ch.on .ch-b) are declared beside their own fills. */
.chnav a.on,.acc[open] .acc-x,.ins-row:hover .ins-cta,.ins-row:focus-visible .ins-cta{
  clip-path:var(--bevel)}
/* Four components draw NO edge at their resting state and gain one at a
   breakpoint (.lap, .rm-table tr) or on a state (.chnav a, .ch-b). Switching
   the plate off through its own colour tokens is what lets the declaration
   above stay single: a transparent gradient paints nothing, so the media
   query only has to name two custom properties instead of repeating fourteen
   lines of background arithmetic it would then have to keep in step. */
.chnav a,.ch-b,.lap,.rm-table tr{--pl-line:transparent;--pl-hi:transparent}
/* .pcard draws its edge on an overlay rather than on itself, because .pcard-a
   (the big stroked initial) sits at z-index:-1 and a negative-z child paints
   BENEATH its own element's background — the plate would have deleted it. */
.pcard::after{content:"";position:absolute;inset:0;pointer-events:none}
.h2{margin:0;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(48px,8.4vw,156px);line-height:var(--lh-lg);letter-spacing:var(--tr-lg);text-transform:uppercase;overflow-wrap:anywhere}
.h3x{margin:0;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(32px,4vw,64px);line-height:var(--lh-md);letter-spacing:var(--tr-md);text-transform:uppercase}
.eyebrow{display:flex;align-items:center;gap:var(--sp-2);margin:0 0 var(--s-2);color:var(--g3)}
.eyebrow b{display:inline-grid;place-items:center;min-width:34px;height:24px;padding:0 var(--sp-2);background:var(--wh);color:var(--bk);font-weight:600;font-variant-numeric:tabular-nums}
.sec-head{margin-bottom:var(--head-gap)}
.sec-sub{margin:var(--sp-4) 0 0;max-width:var(--measure);color:var(--g3)}
.sec{padding-block:var(--sec) 0}
.lede{margin:var(--sp-4) 0 0;max-width:var(--measure-lede);color:var(--g3);font-size:clamp(18px,1.5vw,22px);line-height:1.42}
/* The GEO answer sentence. It is emitted on all 19 sub-pages and NO stylesheet
   defined it, so it set to the full container — about 140 characters a line
   against the 68ch measure every other run of prose on the site uses. It is a
   lede that happens to be quotable, so it is set as one. */
.answer-lede{margin:var(--sp-5) 0 0;max-width:var(--measure);color:var(--g3);
  font-size:clamp(18px,1.4vw,21px);line-height:1.45}
.chip{display:inline-block;margin:0;padding:var(--sp-1) var(--sp-2);--pl-line:var(--g2)}

/* ---------- scroll-driven heading reveal ----------------------------------
   This was `transform:translateX(6vw) skewX(-8deg)`, which parked EVERY
   section heading 6vw right of its resting place until it entered the
   viewport. A transform contributes to scrollable overflow, so `/` measured
   1496px of content in a 1440 viewport and a case study 378 in 360 — on all
   54 pages. The only reason it never showed is body{overflow-x:clip}, and a
   suppression is not an absence.
   The reveal is now a clip-path wipe running right-to-left: the same hard-cut
   broadcast gesture this design already uses on the testimonial blockquotes
   (@keyframes wipe), with the direction of the old slide preserved and no
   geometry moved at all. clip-path never contributes to overflow. */
@supports (animation-timeline:view()){
  .mo .h2,.mo .r-h{animation:headIn linear both;animation-timeline:view();animation-range:entry 0% entry 90%}
}
@keyframes headIn{from{clip-path:inset(0 0 0 100%)}to{clip-path:inset(0 0 0 0)}}
@keyframes rise{from{transform:translateY(16px);opacity:0}to{transform:none;opacity:1}}

/* ---------- THE CTA — one component, every call to action on the site ------
   Three sizes (.btn-sm / .btn / .btn-lg) and two variants (outlined by
   default, .btn-solid filled). Both variants are the same geometry and the
   same corner logic; only the fill differs.

   The edge is drawn as two layers rather than as a `border`, because
   clip-path removes a border along the chamfer: the old outlined button had
   no line at all on its top-right and bottom-left diagonals, which is exactly
   what Bailey reported. Here the element itself is the line colour and
   ::before paints the interior one --edge inside it, so every edge — flat or
   diagonal, filled or outlined — carries the same 1px line.

   Focus: an `outline` would be clipped away by the same clip-path, so the
   ring is drawn INSIDE the shape with a negative offset. Removing the
   underline affordance from links must not cost keyboard users their focus
   ring, so this is deliberate, not incidental. */
.btn{
  --btn-h:50px;--btn-px:var(--sp-5);--btn-fs:12px;
  --btn-ink:var(--wh);            /* the line — and, at the two chamfers, the
                                     ONLY thing drawing them */
  --btn-edge:var(--btn-ink);      /* the four STRAIGHT sides; see below */
  --btn-fill:var(--bk);           /* interior at rest */
  --btn-label:var(--wh);
  --btn-wipe:var(--wh);           /* interior on hover */
  --btn-label-hover:var(--bk);
  position:relative;isolation:isolate;
  display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-3);
  min-height:var(--btn-h);padding:var(--sp-2) var(--btn-px);
  /* Two-tone edge (Bailey, 2026-09-23: "the non bevel edges/border needs to be
     lighter grey so the bevel corners stand out"). The edge used to be one flat
     colour, and because the chamfer is a CLIP rather than a drawn line, the
     diagonal was simply that same colour showing along the cut — so the corner
     had nothing to stand out against. Magnified, .tick was the only component
     in the system reading the way it should: a light side, a dark cut.
     Four line layers in --btn-edge cover the straight runs and stop --clip
     short of each corner, over a base of --btn-ink that is left exposed at
     exactly the two chamfers. Night is unchanged BY CONSTRUCTION: --btn-edge
     defaults to --btn-ink, so the four layers and the base are one colour and
     the composite is the flat edge it has always been. */
  background:
    linear-gradient(var(--btn-edge),var(--btn-edge)) 0 0/calc(100% - var(--clip)) 1px no-repeat,
    linear-gradient(var(--btn-edge),var(--btn-edge)) 0 0/1px calc(100% - var(--clip)) no-repeat,
    linear-gradient(var(--btn-edge),var(--btn-edge)) 100% 100%/calc(100% - var(--clip)) 1px no-repeat,
    linear-gradient(var(--btn-edge),var(--btn-edge)) 100% 100%/1px calc(100% - var(--clip)) no-repeat,
    var(--btn-ink);
  color:var(--btn-label);
  font-family:var(--hud);font-size:var(--btn-fs);font-weight:600;letter-spacing:.12em;
  text-transform:uppercase;white-space:nowrap;text-decoration:none;
  clip-path:var(--bevel);
  transition:color var(--t-fast) var(--ease)
}
.btn::before,.btn::after{content:"";position:absolute;inset:var(--edge);z-index:-1;clip-path:var(--bevel)}
.btn::before{background:var(--btn-fill)}
.btn::after{background:var(--btn-wipe);transform:scaleX(0);transform-origin:right;transition:transform var(--t-med) var(--cut)}
.btn:hover,.btn:focus-visible{color:var(--btn-label-hover)}
.btn:hover::after,.btn:focus-visible::after{transform:scaleX(1);transform-origin:left}
.btn:focus-visible{outline:2px solid var(--btn-label-hover);outline-offset:-6px}
.btn i{font-style:normal;transition:transform var(--t-fast) var(--ease)}
.btn:hover i,.btn:focus-visible i{transform:translateX(4px)}
.btn-solid{--btn-fill:var(--wh);--btn-label:var(--bk);--btn-wipe:var(--bk);--btn-label-hover:var(--wh)}
.btn-lg{--btn-h:60px;--btn-px:var(--sp-6);--btn-fs:13px}
.btn-sm{--btn-h:44px;--btn-px:var(--sp-4);--btn-fs:11px}
.btn[disabled]{opacity:.7;cursor:progress}
.btn.busy i{animation:blinkI .6s steps(2,start) infinite}
@keyframes blinkI{to{visibility:hidden}}
.cta-row{display:flex;flex-wrap:wrap;gap:var(--sp-3);margin-top:var(--sp-6)}
/* A PAIR of buttons never stacks, at any width (Bailey, 2026-09-22). Scoped to
   exactly two children with :has(), so three-button rows keep wrapping rather
   than overflowing the page — the one thing this direction will not do. The
   buttons may shrink and their labels wrap; only the row is held to one line.
   Browsers without :has() fall back to the wrapping default, which is safe. */
.cta-row:has(>:nth-child(2):last-child){flex-wrap:nowrap;align-items:stretch}
/* flex:1 1 0, NOT 0 1 auto. `auto` shrinks only to CONTENT width, so at 320px
   two full labels still exceeded the viewport and the row pushed the page
   wide — clipped invisibly by .phero{overflow:hidden} rather than reported.
   1 1 0 makes the pair share the row as halves whatever the labels say. */
.cta-row:has(>:nth-child(2):last-child)>.btn{flex:0 1 auto;min-width:0;white-space:normal;text-align:center;
  hyphens:auto;overflow-wrap:anywhere}
/* Share the row ONLY where the labels would not otherwise fit. flex:1 1 0 at
   every width turned a pair of buttons into two 670px slabs across a desktop
   cover, which is the opposite of the problem it was added to solve: the pair
   must not STACK, it does not have to fill. Below 560px they split the line. */
@media (max-width:560px){.cta-row:has(>:nth-child(2):last-child)>.btn{flex:1 1 0}}
@media (max-width:420px){.btn{--btn-px:var(--sp-4);white-space:normal}}

/* Chrome controls are buttons too, so they carry the same edge, drawn the
   same two-layer way. Before this there were three edge languages in the
   header alone: the bevelled CTA, a square 1px box on Close and the burger,
   and a square box on the language chip — visible side by side in the menu
   overlay, which is what "make sure it's consistently used" is about. */
.x-btn,.burger,.nav-lang,.nav-theme,.tst-ctl button{
  position:relative;isolation:isolate;border:0;
  /* Same two-tone edge as .btn, same reason, same night-safe default. */
  --ctl-ink:var(--wh);--ctl-edge:var(--ctl-ink);
  background:
    linear-gradient(var(--ctl-edge),var(--ctl-edge)) 0 0/calc(100% - var(--clip)) 1px no-repeat,
    linear-gradient(var(--ctl-edge),var(--ctl-edge)) 0 0/1px calc(100% - var(--clip)) no-repeat,
    linear-gradient(var(--ctl-edge),var(--ctl-edge)) 100% 100%/calc(100% - var(--clip)) 1px no-repeat,
    linear-gradient(var(--ctl-edge),var(--ctl-edge)) 100% 100%/1px calc(100% - var(--clip)) no-repeat,
    var(--ctl-ink);
  color:var(--wh);clip-path:var(--bevel);
  transition:color var(--t-fast) var(--ease)}
.x-btn::before,.burger::before,.nav-lang::before,.nav-theme::before,.tst-ctl button::before{
  content:"";position:absolute;inset:var(--edge);z-index:-1;
  background:var(--bk);clip-path:var(--bevel);transition:background var(--t-med) var(--cut)}
.x-btn:hover,.x-btn:focus-visible,.burger:hover,.burger:focus-visible,
.nav-lang:hover,.nav-lang:focus-visible,.nav-theme:hover,.nav-theme:focus-visible,.tst-ctl button:hover,.tst-ctl button:focus-visible{color:var(--bk)}
.x-btn:hover::before,.x-btn:focus-visible::before,.burger:hover::before,.burger:focus-visible::before,
.nav-lang:hover::before,.nav-lang:focus-visible::before,.nav-theme:hover::before,.nav-theme:focus-visible::before,
.tst-ctl button:hover::before,.tst-ctl button:focus-visible::before{background:var(--wh)}
.x-btn:focus-visible,.burger:focus-visible,.nav-lang:focus-visible,.nav-theme:focus-visible,.tst-ctl button:focus-visible{
  outline:2px solid var(--bk);outline-offset:-6px}

/* ---------- cursor ---------- */
.cur{position:fixed;left:0;top:0;z-index:700;pointer-events:none;display:none;mix-blend-mode:difference}
.has-cur .cur{display:block}
.cur-b{position:absolute;width:22px;height:22px;translate:-50% -50%;transition:width var(--t-fast) var(--out),height var(--t-fast) var(--out)}
.cur-b::before,.cur-b::after{content:"";position:absolute;background:var(--wh)}
.cur-b::before{left:50%;top:-6px;bottom:-6px;width:1px}
.cur-b::after{top:50%;left:-6px;right:-6px;height:1px}
.cur.hov .cur-b{width:40px;height:40px}
.cur.hov .cur-b::before,.cur.hov .cur-b::after{opacity:0}
.cur-l{position:absolute;left:18px;top:14px;white-space:nowrap;opacity:0;color:var(--wh)}
.cur.lab .cur-l{opacity:1}

/* ---------- nav ---------- */
.nav{position:fixed;inset:0 0 auto;z-index:300;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:var(--sp-4);padding:var(--sp-2) var(--g);background:var(--nav-bg);transition:transform var(--t-med) var(--cut)}
.nav.hide{transform:translateY(-130%)}
.logo{display:inline-flex;align-items:center;gap:var(--sp-2);min-height:44px;font-family:var(--disp);font-style:italic;font-weight:900;font-size:22px;text-transform:uppercase}
.logo svg{width:30px;height:30px;fill:var(--wh)}
.nav-links{display:flex;gap:var(--sp-5)}
.nav-links a{position:relative;padding:var(--sp-3) 0}
.nav-links a::after{content:"";position:absolute;left:0;right:0;bottom:8px;height:1px;background:currentColor;transform:scaleX(0);transform-origin:right;transition:transform var(--t-med) var(--cut)}
.nav-links a:hover::after,.nav-links a:focus-visible::after,.nav-links a[aria-current]::after{transform:scaleX(1);transform-origin:left}
/* No bullet on the current item (Bailey, 2026-09-23). The rule that lived here
   drew a 4px square to the left of the active link, and the ::after rule
   directly above already draws its underline at full width — two marks saying
   one thing, on the one item in the nav that needs no help being found. The
   underline is the stronger of the two and it is shared with hover and focus,
   so keeping it is also what keeps the three states one vocabulary. */
.nav-r{display:flex;justify-content:flex-end;align-items:center;gap:var(--sp-2)}
.nav-cta{min-height:44px;padding:0 var(--sp-4)}
.nav-lang{display:inline-grid;place-items:center;min-width:44px;height:44px}
.burger{height:44px;padding:0 var(--sp-4);display:inline-flex;align-items:center;gap:var(--sp-3)}
.burger span{display:grid;gap:var(--sp-1)}
.burger span i{display:block;width:18px;height:1px;background:currentColor;transition:transform var(--t-fast) var(--ease)}
.burger:hover span i:first-child{transform:translateX(4px)}
@media (min-width:1101px){.burger{display:none}}
@media (max-width:1100px){.nav-links{display:none}.nav{grid-template-columns:1fr auto}}
/* The theme control deliberately STAYS in the header at phone widths. A
   screenshot appeared to show it pushing "Start a project" off the right edge
   at 400px, and a rule was written to hide it. That was a measurement error,
   not a defect: headless Chrome enforces a minimum window width, so a direct
   400px capture renders wider and is then scaled, which makes a fixed header
   look clipped. Measured through an iframe at a true 400px, the CTA's right
   edge sits at 332px inside a 400px viewport, with 68px to spare. The rule was
   also inert, since .nav-theme's own display declaration follows it in source
   order at equal specificity. Measure the geometry; do not read the picture. */
/* SCOPED TO THE HEADER (.nav-r), and that scope is load-bearing. This rule
   trims the fixed header row at phone width, but it was written as a bare
   `.nav-lang{display:none}` — and .nav-lang is reused inside the menu
   overlay's .mf-meta row, which is the ONE surface a phone reader can reach
   the language control from, since the header drops it by design. So the bare
   selector hid the control everywhere and left no way to switch language on a
   phone at all. Measured at 400px: the menu's VI chip rendered 0x0.
   No gate caught it, and none would: the markup is present, the page has no
   overflow, and every validator passes a control that is simply invisible.
   That is the same shape as the round-7 finding where an affordance existed
   only on hover and therefore did not exist on touch. */
@media (max-width:560px){.logo span{display:none}.nav-r .nav-lang{display:none}.nav-cta{padding:0 var(--sp-3);gap:var(--sp-2);font-size:11px;letter-spacing:.08em}.burger{width:44px;padding:0;justify-content:center}.burger .bl{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}}

/* ---------- mobile action bar (thumb reach: work + contact, one tap) ---------- */
/* was a 1px grid gap plus a top shadow, both acting as rules */
.mcta{position:fixed;left:0;right:0;bottom:0;z-index:290;display:none;gap:0;background:var(--bk);transform:translateY(110%);transition:transform var(--t-med) var(--cut)}
.mcta.on{transform:none}
.mcta a{flex:1;min-height:58px;display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2);background:var(--bk);font-family:var(--hud);font-size:var(--hud-fs);font-weight:600;letter-spacing:var(--hud-tr);text-transform:uppercase}
.mcta a.p{background:var(--wh);color:var(--bk)}
.mcta i{font-style:normal}
@media (max-width:760px){
  .mcta{display:flex}
  .p-contact .mcta{display:none}
}
html.lock .mcta{transform:translateY(110%)}

/* ---------- menu ---------- */
/* The overlay FITS THE WINDOW (Bailey, 2026-09-23: "i need to scroll down, the
   menu items font sizes should respond and not need to be scrolled to view").
   It did not, and the cause was that the row size read the viewport's WIDTH and
   nothing else: clamp(44px, 9.5vw, 130px) is 130px at 1440 whether the window
   is 1080px tall or 700. Measured before touching it — every desktop size
   overflowed, 1440x900 by 267px and 1440x700 by 467 — and the two smallest
   phones overflowed from the other end, because a 44px floor is too big for a
   568px screen.
   --sp-9 at the foot was clearance for the .mcta action bar, which html.lock
   translates away the moment this opens, so on a short window it was 96px of
   nothing. It stays on tall windows where it is doing real optical work. */
.menu{position:fixed;inset:0;z-index:600;background:var(--bk);display:flex;flex-direction:column;padding:var(--sp-2) var(--g) var(--sp-9);overflow-y:auto;clip-path:inset(0 0 100% 0);transition:clip-path var(--t-ovl-out) var(--cut)}
@media (max-height:820px){.menu{padding-bottom:var(--sp-5)}.menu-list{padding:var(--sp-3) 0}}
.menu.open{clip-path:inset(0);transition-duration:var(--t-ovl)}
.menu-top{display:flex;justify-content:space-between;align-items:center}
/* edge comes from the shared chrome rule — a border here would override it */
.x-btn{min-height:44px;padding:0 var(--sp-5)}
.menu-list{list-style:none;margin:auto 0;padding:var(--sp-5) 0}
/* The menu rows were separated by hairlines — seven of them stacked down the
   overlay. The type is 44–130px display italic; it does not need a rule to
   read as a list. */
.menu-list li{overflow:hidden}
/* The counters were `align-items:baseline` against 130px display type, which
   left each one floating ~45px below the label it belongs to and made the
   overlay read as two unrelated columns. They are bound to their row now. */
.menu-list a{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);padding:var(--sp-1) 0;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(20px,min(9.5vw,(100svh - 420px) / 6),130px);line-height:.98;letter-spacing:var(--tr-lg);text-transform:uppercase;transform:translateY(105%) skewY(6deg);transition:transform var(--t-slow) var(--out),padding var(--t-med) var(--out)}
.menu.open .menu-list a{transform:none}
.menu.open .menu-list li:nth-child(2) a{transition-delay:25ms}
.menu.open .menu-list li:nth-child(3) a{transition-delay:50ms}
.menu.open .menu-list li:nth-child(4) a{transition-delay:75ms}
.menu.open .menu-list li:nth-child(5) a{transition-delay:100ms}
.menu.open .menu-list li:nth-child(6) a{transition-delay:125ms}
.menu-list a:hover,.menu-list a:focus-visible,.menu-list a[aria-current]{padding-left:var(--sp-5)}
/* The menu's foot is now SEPARATED from the list rather than being the last
   thing in it (Bailey, 2026-09-22). margin-top:auto pushes it to the bottom of
   the overlay whatever the list height, and the rule above it is what makes it
   read as a different band instead of a seventh menu item. The email is a CTA
   button inline with "Start a project": it reuses .cta-row, so the standing
   two-button rule (never stack, share the width below 560px) applies here by
   inheritance rather than being restated and allowed to drift. */
.menu-foot{margin-top:auto;padding-top:var(--sp-5);padding-bottom:var(--sp-5);border-top:1px solid var(--hair);
  display:grid;gap:var(--sp-4)}
/* A LANDSCAPE PHONE is the one case the row sizing alone cannot solve: at
   360px tall the fixed chrome — 44px bar plus a 159px footer — leaves about
   130px for six rows, so even the 20px floor overflows. The footer is what
   gives: its two bands sit on one line and the padding halves, which buys back
   roughly 70px and is the difference between scrolling and not. Deliberately
   keyed to height AND orientation, so a narrow portrait phone never gets it. */
@media (max-height:520px) and (orientation:landscape){
  .menu-foot{padding-top:var(--sp-3);padding-bottom:var(--sp-3);gap:var(--sp-2);
    grid-template-columns:1fr auto;align-items:center}
  .mf-meta{justify-content:flex-end}
  .menu-list{padding-block:var(--sp-1)}
}
.mf-cta{margin:0}
.mf-meta{display:flex;align-items:center;gap:var(--sp-2)}
/* The language control carries no URL any more — the chip says VI and the
   accessible name says "Tiếng Việt". Printing ragingmonk.co/vi beside it told
   the reader the implementation and cost a line of the layout. */
/* Same chip as .nav-lang: same 1px box, same bevel, same hover invert, both
   now in the shared selector above. The one difference is width. .nav-lang can
   stay a 44px square because "VI" is two characters; this carries a word
   ("DAY" / "NIGHT") in letter-spaced uppercase, and the shared rule sets no
   horizontal padding, so a fixed square would jam the text against the bevel.
   min-width keeps the 44px touch target, padding lets it grow to the word. */
.nav-theme{display:inline-grid;place-items:center;min-width:44px;width:44px;height:44px;
  padding:0;font:inherit;cursor:pointer}
/* A half-lit disc. currentColor, so it inverts with the chip on hover exactly
   as the "VI" text does, and a fixed 44px square means toggling cannot change
   the control's width or nudge the chip beside it. */
.th-i{display:block;width:16px;height:16px;border-radius:50%;
  border:1.5px solid currentColor;
  background:linear-gradient(90deg,currentColor 0 50%,transparent 50% 100%)}
html[data-theme="day"] .th-i{background:linear-gradient(90deg,transparent 0 50%,currentColor 50% 100%)}
/* ---------- breadcrumbs ---------- */
.crumbs ol{list-style:none;margin:0 0 var(--sp-5);padding:0;display:flex;flex-wrap:wrap;gap:var(--sp-1) 0;color:var(--g3)}
.crumbs li{display:flex;align-items:center}
.crumbs li+li::before{content:"/";margin:0 var(--sp-2);color:var(--g2)}
.crumbs a{display:inline-block;padding:var(--sp-3) 0;color:var(--g3);transition:color var(--t-fast) var(--ease)}
.crumbs a:hover,.crumbs a:focus-visible{color:var(--wh)}
.crumbs [aria-current]{color:var(--wh)}

/* ---------- page hero ---------- */
/* ---------- page hero: a stable fold (INNER-5) ---------------------------
   .ph1 is clamp(56px,10.5vw,196px) with no height cap, so the fold moved with
   the title: "Restaurant Branding Agency In Sydney" sets as four lines at
   1440 and pushed the lede to y≈780 with the stat bar and the CTA entirely
   below the fold, while /work/kitchai/analysis ran to two lines and left the
   screen half empty. Stepping the title down a tier would have shrunk the
   headline on the two pages carrying the site's measured search demand.
   Instead the stat bar and the CTA are painted ABOVE the title, so everything
   before the headline sits at a fixed y whatever the headline does.
   Done with `order`, not by reordering the markup: the <h1> stays the first
   heading in document order, which is what assistive tech and crawlers read. */
/* Every page's first section is a full-height cover (Bailey, 2026-09-22).
   min-height, never height: a hero that cannot grow clips its own headline at
   320px, where .ph1 runs to 56px over several lines. svh rather than vh so the
   mobile browser's collapsing toolbar does not make it overflow the screen. */
/* Full-height cover, content at the top (Bailey, 2026-09-22).
   Two earlier attempts were wrong and both for the same reason: a text-only
   cover cannot fill a viewport. `center` left ~550px of void above the title;
   `flex-end` moved the same void above instead of below. The home hero fills
   its height because it carries the 3D mark and a photograph; /sitemap is a
   breadcrumb, an eyebrow, a title and two lines, and nothing will change that.
   So the section is GUARANTEED a viewport height and the content starts where
   it always did. No void either side of it, and the page scrolls normally.
   min-height, never height: at 320px the title runs to several lines and a
   fixed height would clip it. svh, not vh, so a mobile toolbar collapsing
   cannot push the cover past the screen. */
/* Centred, now that the cover is a coherent stack rather than scattered rows.
   Centring failed twice before because the block read as debris — breadcrumb,
   numbers, buttons, then a title — so the eye had nothing to centre ON. With
   the title leading and the CTAs closing, the group has a centre worth using.
   min-height, never height: at 320px .ph1 runs to several lines and a fixed
   height would clip it. svh, not vh, so a collapsing mobile toolbar cannot
   push the cover past the screen. */
.phero{position:relative;display:flex;flex-direction:column;align-items:stretch;
  justify-content:center;min-height:100svh;
  padding-block:var(--sec) var(--sec);overflow:hidden;isolation:isolate}
/* The cover was a full-height black field with type standing on it, which is
   what Bailey meant by "just plain black solid background". These are the
   design language's OWN elements rather than new ornament: the quarter-column
   hairline grid the page already measures to, and a soft fade off the top edge
   so the header sits on something. Both draw through tokens, so they flip with
   the day theme.

   position:absolute is load-bearing, not tidiness: .phero is a flex container,
   so a static ::before would become a FLEX ITEM, inherit `order:4` from the
   `.phero>*` rule below, and take a row of its own in the middle of the cover.
   Absolutely positioned children are not flex items. */
.phero::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:
    linear-gradient(90deg,var(--g0) 0 1px,transparent 1px) 0 0/25% 100%,
    linear-gradient(180deg,var(--hair),transparent 34%)}
/* Typography: the lede was inheriting the full prose measure, so it ran the
   width of the cover under a headline set to 15ch and read as body copy rather
   than a standfirst. It gets the display measure and a size between the h1 and
   the HUD row, which is the tier that was missing. */
.phero>.eyebrow{margin-bottom:var(--sp-4)}
.phero>.lede{max-width:var(--measure-lede);font-size:clamp(17px,1.35vw,21px);line-height:1.38}
/* Reading order restored to the obvious one. The previous arrangement printed
   eyebrow, then the numbers, then the BUTTONS, and only then the <h1>, so a
   reader was asked to act before being told what the page was, and the title
   sat fifth in its own cover. That ordering was round 2's answer to a
   content-height hero whose fold moved with the headline; a full-viewport
   cover does not have that problem, so the workaround goes with it.
   Order now: breadcrumb, eyebrow, title, lede, readout, CTAs. */
.phero>*{order:4}                       /* lede and anything else after the title */
.phero>.crumbs{order:1}
.phero>.eyebrow,.phero>.ty-tick,.phero>.nf-num{order:2}
.phero>.ph1{order:3}
.phero>.cta-row{order:6;margin-top:var(--sp-6);margin-bottom:0}
/* Moving the stat bar above the title only stabilises the fold if the stat bar
   is small. At its section scale — 44–96px display italic — it became the
   headline and pushed the real <h1> to y≈490 at 1440, which is worse than the
   problem it was fixing. This is the same demotion round 2 applied to the home
   hero's HUD for the same reason ("five readouts at 26–46px read as a second
   headline"), so above a title the readout is a HUD row: one line, values at
   18px, the S1/S2 cell codes dropped as noise at this size. Outside .phero —
   the case-study stat bar — the display treatment is unchanged. */
.phero>.tele-bar{order:5;display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-6);margin:var(--sp-6) 0 0}
.phero>.tele-bar .tele-c{display:flex;align-items:baseline;gap:var(--sp-2)}
.phero>.tele-bar .tele-c>.hud:first-child{display:none}
.phero>.tele-bar b{font-family:var(--hud);font-style:normal;font-weight:600;font-size:18px;
  line-height:1.2;letter-spacing:0;color:var(--wh)}
.phero>.tele-bar b small{font-size:.72em;margin-left:2px}
.phero>.tele-bar .tele-l{color:var(--g2);font-size:13px;letter-spacing:.02em;text-transform:none}
.ph1{margin:0;max-width:15ch;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(56px,10.5vw,196px);line-height:var(--lh-mega);letter-spacing:var(--tr-mega);text-transform:uppercase;overflow-wrap:anywhere}
.mo .ph1{animation:slamH var(--t-slow) var(--out) both}
@keyframes slamH{0%{transform:translateX(-4%) skewX(-12deg)}100%{transform:none}}
.deco{position:absolute;right:var(--g);top:90px;z-index:-1;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(160px,34vw,560px);line-height:.8;letter-spacing:var(--tr-mega);color:transparent;-webkit-text-stroke:var(--stroke-fine) var(--g1);pointer-events:none}
.partner-line{margin:var(--sp-4) 0 0;color:var(--g3)}
/* affordance comes from the shared prose-link rule; this re-declared the
   underline after it and would have won on specificity order */
.phero .cat{margin:var(--sp-4) 0 0;color:var(--g3)}
/* The stat bar was fenced top and bottom and divided by a 1px shadow between
   every cell. The figures are 44–96px display italic against 12.5px HUD
   labels; that contrast groups them without any of the five rules. */
.tele-bar{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:var(--sp-5);margin-top:var(--sp-f-7)}
.tele-c{display:grid;gap:var(--sp-2);align-content:start}
.tele-c b{font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(44px,5.4vw,96px);line-height:var(--lh-lg);letter-spacing:var(--tr-lg);font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
.tele-c b small{font-size:.38em;margin-left:var(--sp-1);letter-spacing:.02em}
.tele-c>.hud:first-child{color:var(--g2)}
.tele-l{color:var(--g3)}
@media (max-width:560px){.tele-bar{grid-template-columns:1fr 1fr;gap:var(--sp-4)}}

/* ---------- chapter nav (sticky race HUD) ---------- */
/* The sticky rail keeps its progress line — that is a state readout, not a
   divider — but loses the full-width hairline under it and the one between
   the readout and the pills. */
.chnav{position:sticky;top:0;z-index:200;background:var(--bar);margin-top:var(--sp-f-6);display:grid;grid-template-columns:auto minmax(0,1fr)}
/* "04 / 12 · SUMMARY" — says where you are without reading the whole rail */
.chnav-cur{display:none;align-items:center;gap:var(--sp-3);padding:0 var(--sp-5) 0 var(--g);white-space:nowrap}
.chnav-cur b{font-family:var(--disp);font-style:italic;font-weight:900;font-size:26px;line-height:1;font-variant-numeric:tabular-nums}
.chnav-cur .chnav-nm{color:var(--g3);max-width:14ch;overflow:hidden;text-overflow:ellipsis}
@media (min-width:900px){.chnav-cur{display:flex}}
.chnav-in{display:flex;gap:2px;overflow-x:auto;padding:var(--sp-1) var(--g) var(--sp-1) var(--sp-2);scrollbar-width:none}
@media (min-width:900px){.chnav-in{padding-left:var(--sp-3)}}
.chnav-in::-webkit-scrollbar{display:none}
.chnav a{flex:0 0 auto;display:inline-flex;align-items:center;gap:var(--sp-1);min-height:44px;padding:0 var(--sp-3);transition:background-color var(--t-fast),color var(--t-fast)}
.chnav a:hover{--pl-line:var(--g2);--pl-hi:var(--wh)}
/* The CURRENT chapter is filled, so its chamfer comes from clipping the fill.
   Its plate tokens are set to the fill colour rather than left transparent,
   and this rule is declared AFTER :hover at equal specificity on purpose:
   pointing at the tab you are already on would otherwise draw a grey edge and
   two grey diagonals across a white ground. */
.chnav a.on{background-color:var(--wh);color:var(--bk);--pl-line:var(--wh);--pl-hi:var(--wh)}
.chnav a.on .mute{color:var(--on-fill-2)}
.chnav-p{position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--wh);transform-origin:left;transform:scaleX(var(--p,0))}
/* ---- the header collapses into the rail once the rail is stuck ------------
   Bailey, 2026-09-23: while the chapter banner is showing, the top nav folds
   into it and its three controls — language, theme, menu — appear at the
   banner's right-hand end; back at the top of the hero, where the banner is
   not yet stuck, the header is its normal self. This applies to every page
   that carries the banner: the eight case studies and the seven service pages.

   Two fixed bars stacked at the top of the viewport was the thing to remove.
   They were never both needed: the banner already says where you are on the
   page, so the header above it was repeating a logo and a nav the reader had
   just scrolled past, and on a phone the two together ate 122px of a 667px
   screen before any content.

   `html:has(.chnav.stuck)` rather than a second flag on the root. Round 9 blanked
   the homepage by putting a class on <html> that a bare element rule also
   matched, and :has() keeps the state on the one element that owns it — the
   rail — with nothing new on the root to collide with. The precedent for
   :has() in this file is .cta-row's two-button case.

   The controls are inert until the rail is stuck: tabindex="-1" in the markup
   and pointer-events:none here, so a keyboard reader at the top of the page
   does not tab through a duplicate set of chrome controls it cannot see. */
.chnav-r{display:flex;align-items:center;gap:var(--sp-2);padding-right:var(--g);
  opacity:0;pointer-events:none;transition:opacity var(--t-fast) var(--ease)}
.chnav.stuck .chnav-r{opacity:1;pointer-events:auto}
/* A column gap so the pill scroller's cut edge reads as 'there is more to
   scroll' rather than as the chips overlapping it. At 400px the two were
   touching and '03 STRA' looked broken rather than scrolled. */
.chnav{grid-template-columns:auto minmax(0,1fr) auto;column-gap:var(--sp-2)}
/* Explicit columns, because the first child DISAPPEARS below 900px. .chnav-cur
   is display:none there, so it stops being a grid item altogether and the two
   remaining children slide up a column: the pill scroller lands on `auto`,
   which on a horizontal scroller resolves to its max-content width, and the
   control cluster is pushed off the right edge. It was invisible at 400px and
   correct at 1440 for exactly that reason. Naming the columns makes the layout
   independent of how many children happen to be rendered. */
.chnav-cur{grid-column:1}
.chnav-in{grid-column:2}
.chnav-r{grid-column:3}
/* The header goes, and takes its own scroll-hide transition with it — the two
   must not fight, or the header slides back in on the next scroll-up while the
   banner is still showing its controls. */
html:has(.chnav.stuck) .nav{transform:translateY(-130%);pointer-events:none}
/* Below 1100px the header shows the burger and hides the link list, so the
   rail's cluster is the whole of the chrome there too. The CTA is deliberately
   NOT duplicated into the rail: it is the one control that is also in the
   menu, in the footer and in the .mcta bar on phones. */
/* The burger shows in the rail at EVERY width, and this one is load-bearing
   rather than cosmetic. `.burger{display:none}` above 1100px is correct for the
   header, where the link list is visible beside it — but the header is exactly
   what has just folded away. Without this the desktop reader loses every route
   off the page the moment the rail sticks: no links, no menu, nothing but the
   chapter pills for the page they are already on. */
.chnav-r .burger{display:inline-flex}
@media (max-width:760px){.chnav-r{gap:var(--sp-1);padding-right:var(--sp-3)}}

/* ---------- closing CTA ---------- */
.close{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-5);align-items:end;margin-top:var(--sec-lg);padding-block:var(--sec);overflow:hidden;isolation:isolate}
/* photographic ground: the one component that puts a venue on every inner page */
.close-ph{position:absolute;inset:0;z-index:-2;overflow:hidden;clip-path:var(--bevel)}
.close-ph img{width:100%;height:100%;object-fit:cover;object-position:center;filter:grayscale(1) contrast(1.16) brightness(.32)}
.close-ph::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,.94) 12%,rgba(0,0,0,.6) 58%,rgba(0,0,0,.86))}
.close.has-ph{padding-block:var(--sec-lg)}
.close.has-ph .close-mark svg{opacity:.85}
@media (max-width:700px){.close-ph::after{background:linear-gradient(180deg,rgba(0,0,0,.78),rgba(0,0,0,.92) 55%)}}
/* a grid child holding display type needs min-width:0, or its min-content
   width sizes the track and stretches the row — the exact mechanism behind
   v7's narrow-width overflow last round */
.close-in{min-width:0}
.close-h{margin:0;max-width:17ch;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(46px,7.6vw,140px);line-height:var(--lh-lg);letter-spacing:var(--tr-lg);text-transform:uppercase}
.close-text{margin:var(--sp-4) 0 0;max-width:var(--measure);color:var(--g3)}
.close-mark svg{width:clamp(90px,14vw,220px);height:auto;aspect-ratio:1;fill:none;stroke:var(--wh);stroke-width:30;opacity:.6}
@media (max-width:700px){.close{grid-template-columns:1fr}.close-mark{display:none}}

/* ---------- accordions (native details) ---------- */
/* Separation was a gap alone, and that was wrong: Bailey read the questions as
   a list of headings rather than as separate things he could open (2026-09-22,
   "separate faqs need to have a plus bullet or something to indicate separate
   questions"). A rule per row draws the boundary, and the plus below is boxed
   so it reads as a control at rest. Same correction as /insights in round 7:
   an affordance that only exists on hover does not exist on a phone. */
.acc-list{display:grid;gap:0}
.acc{padding-block:var(--sp-1);border-top:1px solid var(--g1)}
.acc:last-child{border-bottom:1px solid var(--g1)}
.acc summary{display:grid;grid-template-columns:52px 1fr 28px;gap:var(--sp-3);align-items:center;min-height:64px;padding:var(--sp-3) 0;cursor:pointer;list-style:none;transition:padding var(--t-med) var(--out)}
.acc summary::-webkit-details-marker{display:none}
.acc summary:hover{padding-left:var(--sp-2)}
.acc-n{color:var(--g2)}
.acc-q{font-family:var(--disp);font-weight:800;font-size:clamp(21px,2vw,30px);line-height:1.08;letter-spacing:var(--tr-sm);text-transform:uppercase}
.acc-x{position:relative;width:28px;height:28px;justify-self:end;
  transition:background-color var(--t-fast) var(--ease)}
.acc summary:hover .acc-x,.acc summary:focus-visible .acc-x{--pl-line:var(--wh)}
.acc[open] .acc-x{background-color:var(--wh);--pl-line:var(--wh);--pl-hi:var(--wh)}
.acc-x::before,.acc-x::after{content:"";position:absolute;left:7px;right:7px;top:50%;height:1px;background:var(--wh);transition:transform var(--t-fast) var(--ease),background var(--t-fast) var(--ease)}
.acc[open] .acc-x::before,.acc[open] .acc-x::after{background:var(--bk)}
.acc-x::after{transform:rotate(90deg)}
.acc[open] .acc-x::after{transform:rotate(0)}
.acc-a{padding:0 0 var(--sp-5) var(--sp-8);max-width:calc(var(--measure) + 64px);color:var(--g3)}
.acc-a p{margin:0 0 var(--sp-3)}
.mo .acc[open] .acc-a{animation:rise var(--t-med) var(--out) both}
@media (max-width:560px){.acc summary{grid-template-columns:40px 1fr 24px}.acc-x{width:24px;height:24px}.acc-x::before,.acc-x::after{left:6px;right:6px}.acc-a{padding-left:0}}

/* ---------- shared prose / quote / section blocks -------------------------
   Moved here from report.css and showcase.css. All three are used well beyond
   the page type they were filed under: .r-body and .fq carry the partner
   testimonials and the city-page market copy, and .svc is the section rhythm
   on /services. A page that used them without linking that stylesheet
   rendered them as unstyled markup while every other check still passed —
   check_v2.py now gates stylesheet coverage so this cannot recur. */
.svc{padding-block:var(--sec-lg) var(--sec);overflow:hidden}
.r-body{color:var(--g3);max-width:var(--measure)}
.r-body p{margin:0 0 var(--sp-4)}
.fq{margin:0;padding:var(--sp-f-8)}
.fq p{margin:0 0 var(--sp-4)}
.fq blockquote{margin:0 0 var(--sp-5);max-width:26ch;font-family:var(--disp);font-weight:700;font-size:clamp(28px,3.8vw,68px);line-height:1.02;letter-spacing:var(--tr-lg);text-transform:uppercase}
.fq-body{margin-top:var(--sp-5)}
/* ordered "delivered" list — the case studies and /ho-chi-minh-city share it,
   and the city pages do not load report.css where it used to live */
.svc-del{list-style:none;margin:0;padding:0;display:grid;gap:var(--sp-2)}
.svc-del li{display:grid;grid-template-columns:40px 1fr;gap:var(--sp-3);padding:var(--sp-2) 0}
/* /faq question index — titles only; the answers live once, on the category
   page each title links into */
.faq-qs{list-style:none;margin:0;padding:0;display:grid;gap:0}
.faq-qs li{border-top:1px solid var(--g1)}
.faq-qs li:last-child{border-bottom:1px solid var(--g1)}
/* These navigate rather than expand, so the marker is a caret, not the plus
   the accordions use — a plus here would promise an answer opens in place. */
.faq-qs a{display:grid;grid-template-columns:24px 1fr;gap:var(--sp-3);align-items:baseline;
  padding:var(--sp-3) 0;max-width:var(--measure);color:var(--g3);
  font-family:var(--disp);font-weight:800;font-size:clamp(19px,1.7vw,26px);line-height:1.12;
  letter-spacing:var(--tr-sm);text-transform:uppercase;
  transition:color var(--t-fast) var(--ease),translate var(--t-fast) var(--ease)}
.faq-qs a::before{content:"→";color:var(--g2);font-size:.7em;transition:color var(--t-fast) var(--ease)}
.faq-qs a:hover,.faq-qs a:focus-visible{color:var(--wh);translate:4px 0}
.faq-qs a:hover::before,.faq-qs a:focus-visible::before{color:var(--wh)}

/* ---------- testimonials ---------- */
.tst{padding-block:var(--sec-lg);content-visibility:auto;contain-intrinsic-size:auto 800px}
.tst-top{display:flex;justify-content:space-between;align-items:end;gap:var(--sp-4);flex-wrap:wrap;margin-bottom:var(--sp-f-7)}
.tst-ctl{display:flex;align-items:center;gap:var(--sp-2)}
/* Edge, hover ground and focus ring all come from the shared chrome-edge rule.
   This rule previously re-added `border:1px solid var(--wh)` AFTER it, which
   would have won on the border property and put a square box back on the two
   testimonial arrows — the exact inconsistency being removed. */
.tst-ctl button{width:52px;height:52px;display:grid;place-items:center;font-size:20px}
.tst-ctl .hud b{font-family:var(--disp);font-style:italic;font-weight:900;font-size:34px;letter-spacing:0}
.tst-stage{display:grid;gap:var(--sp-7);padding-top:var(--sp-f-7);touch-action:pan-y}
.tq{margin:0;display:grid;grid-template-columns:repeat(12,1fr);gap:var(--sp-5)}
.tq blockquote{grid-column:1/10;margin:0;font-family:var(--disp);font-weight:700;font-size:clamp(28px,4.2vw,78px);line-height:1;letter-spacing:var(--tr-lg);text-transform:uppercase}
.tq figcaption{grid-column:10/13;align-self:end;display:grid;gap:var(--sp-1)}
.tq-link{display:inline-flex;align-items:center;min-height:44px;padding-block:var(--sp-3);color:var(--g3);
  transition:color var(--t-fast) var(--ease),translate var(--t-fast) var(--ease)}
.tq-link:hover,.tq-link:focus-visible{color:var(--wh);translate:3px 0}
.tq-run .tst-stage{gap:0}
.tq-run .tq{grid-area:1/1}
.tq-run .tq:not(.on){visibility:hidden}
.tq-run .tq.on blockquote{animation:wipe 280ms steps(5,end) both}
@keyframes wipe{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0)}}
@media (max-width:860px){.tq blockquote,.tq figcaption{grid-column:1/-1}}

/* ---------- head-to-head (Foundry/Catalyst, Vision pre/post) ---------- */
.h2h{position:relative;display:grid;grid-template-columns:1fr auto 1fr}
.h2h-side{padding:var(--sp-f-8) var(--g);display:grid;gap:var(--sp-4);align-content:start}
.h2h-side.r{text-align:right;justify-items:end}
.h2h-side h3{margin:0;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(60px,10vw,196px);line-height:var(--lh-mega);letter-spacing:var(--tr-mega);text-transform:uppercase}
.h2h-side h3.vs-h{font-size:clamp(30px,3.4vw,58px);line-height:var(--lh-md);letter-spacing:var(--tr-md);max-width:18ch}
.h2h-copy{max-width:56ch;color:var(--g3)}
.h2h-copy p{margin:0 0 var(--sp-3)}
.h2h-side dl{margin:0;width:100%;display:grid}
.h2h-side dl div{display:flex;justify-content:space-between;align-items:baseline;gap:var(--sp-4);padding:var(--sp-3) 0}
.h2h-side.r dl div{flex-direction:row-reverse}
.h2h-side dd{margin:0;font-family:var(--disp);font-weight:800;font-size:clamp(22px,2.4vw,40px);line-height:1;text-transform:uppercase}
.h2h-vs{position:relative;display:grid;place-items:center;width:clamp(70px,8vw,130px)}
/* the full-height divider between the two sides is gone; the VS disc and the
   column gap already separate them */
.h2h-vs b{position:relative;display:grid;place-items:center;width:clamp(62px,6.2vw,100px);aspect-ratio:1;border-radius:50%;border:1px solid var(--wh);background:var(--bk);font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(24px,2.4vw,40px)}
@supports (animation-timeline:view()){.mo .h2h-vs b{animation:vsIn linear both;animation-timeline:view();animation-range:entry 20% cover 45%}}
@keyframes vsIn{from{transform:scale(.6) rotate(-90deg)}to{transform:none}}
@media (max-width:860px){.h2h{grid-template-columns:1fr}.h2h-side.r{text-align:left;justify-items:start}.h2h-side.r dl div{flex-direction:row}.h2h-vs{width:auto;height:90px}.h2h-vs::before{top:50%;bottom:auto;left:0;right:0;width:auto;height:1px}}

/* ---------- service rows ---------- */
/* Row lists lose their hairlines sitewide. The hover state already inverts
   the whole row to a white ground, which is a far stronger row affordance
   than a 1px rule at rest. */
.svc-list{list-style:none;margin:0;padding:0}
/* The fourth column is `auto`, not a fixed 190px: it held the price badge on
   the home and services rows, and with the badges gone those rows have three
   children and must not leave a 190px hole on the right. */
.svc-row{position:relative;display:grid;grid-template-columns:60px minmax(0,1.4fr) minmax(0,1.1fr) auto;gap:var(--sp-5);align-items:center;padding:var(--sp-f-5) var(--g);isolation:isolate;text-decoration:none;transition:color var(--t-fast) var(--ease)}
.svc-row::before{content:"";position:absolute;inset:0;z-index:-1;background:var(--wh);transform:scaleY(0);transform-origin:bottom;transition:transform var(--t-med) var(--cut)}
.svc-row:hover,.svc-row:focus-visible{color:var(--bk)}
.svc-row:hover::before,.svc-row:focus-visible::before{transform:scaleY(1)}
.svc-row:hover .mute,.svc-row:hover .svc-s,.svc-row:focus-visible .svc-s,.svc-row:focus-visible .mute{color:var(--on-fill)}
.svc-t{font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(34px,5vw,94px);line-height:var(--lh-lg);letter-spacing:var(--tr-lg);text-transform:uppercase}
.svc-s{color:var(--g3)}
.svc-tag{text-align:right}
.r-links .svc-row{grid-template-columns:60px 1fr 40px}
@media (max-width:860px){.svc-row{grid-template-columns:36px 1fr;gap:var(--sp-1) var(--sp-3)}.svc-s,.svc-tag{grid-column:2;text-align:left}.r-links .svc-row{grid-template-columns:36px 1fr 24px}}

/* ---------- race-grid cards (deliverables, capabilities) ---------- */
.race-grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,340px),1fr));border-top:1px solid var(--g1);border-left:1px solid var(--g1)}
.rg{position:relative;display:grid;gap:var(--sp-3);align-content:start;padding:var(--sp-5) var(--sp-5) var(--sp-6);border-right:1px solid var(--g1);border-bottom:1px solid var(--g1);overflow:hidden;isolation:isolate;transition:background var(--t-med) var(--ease)}
.rg:hover{background:var(--tint)}
.rg-code{position:absolute;right:-6px;top:-18px;z-index:-1;font-family:var(--disp);font-style:italic;font-weight:900;font-size:150px;line-height:1;letter-spacing:var(--tr-lg);color:transparent;-webkit-text-stroke:var(--stroke-fine) var(--deco-stroke);font-variant-numeric:tabular-nums;transition:-webkit-text-stroke-color var(--t-med)}
.rg:hover .rg-code{-webkit-text-stroke-color:var(--g2)}
.rg h3{margin:var(--sp-6) 0 0;font-family:var(--disp);font-weight:800;font-size:clamp(26px,2.3vw,36px);line-height:1;text-transform:uppercase;max-width:16ch}
.rg-t{margin:0;color:var(--g3)}
.rg-meta{list-style:none;margin:var(--sp-1) 0 0;padding:0;display:flex;flex-wrap:wrap;gap:var(--sp-1)}
.rg-meta li{padding:var(--sp-1) var(--sp-2);color:var(--g3)}

/* ---------- proof cards / board / honest version ---------- */
.pc-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-4)}
.pc-2{grid-template-columns:repeat(2,minmax(0,1fr))}
.pc{position:relative;display:grid;gap:var(--sp-3);align-content:start;padding:var(--sp-5);overflow:hidden;transition:background-color var(--t-med)}
.pc:hover{--pl-line:var(--g2);background-color:var(--tint)}
/* photographic proof card: the venue itself, not just its logo. The band bleeds
   to the card edge, so the card reads as a still from that venue. */
/* Same fault as .rm-table th, predicted from it and fixed with it: the proof
   photo bleeds to the card's own edges with negative margins, so its square
   top-right corner sat over .pc's chamfer. */
.pc-ph{margin:-26px -26px var(--sp-1);aspect-ratio:16/9;overflow:hidden;border-bottom:1px solid var(--g1);clip-path:var(--bevel-tr)}
.pc-ph img{width:100%;height:100%;object-fit:cover;filter:grayscale(1) contrast(1.15) brightness(.62);transition:filter var(--t-med) var(--ease),transform var(--t-slow) var(--out)}
.pc:hover .pc-ph img{filter:grayscale(1) contrast(1.12) brightness(.78);transform:scale(1.03)}
.pc-logo{width:72px;height:72px;object-fit:contain}
.pc-ph+.pc-logo,.pc-ph+.pc-word{margin-top:2px}
.pc-word{font-family:var(--disp);font-style:italic;font-weight:900;font-size:30px;text-transform:uppercase;line-height:72px}
.pc h3{margin:0;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(30px,3vw,48px);line-height:var(--lh-md);letter-spacing:var(--tr-md);text-transform:uppercase}
.pc-stat{margin:0;font-family:var(--disp);font-weight:800;font-size:26px;line-height:1;letter-spacing:var(--tr-sm);text-transform:uppercase}
.pc p{margin:0;color:var(--g3)}
.pc .hud.mute{color:var(--g3)}
@media (max-width:860px){.pc-grid,.pc-2{grid-template-columns:1fr}}
.board{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr);gap:var(--sp-f-8);padding:var(--sp-f-7)}
.board-l{display:grid;gap:var(--sp-4);align-content:start;justify-items:start}
.board-l p{margin:0}
.board-amt{font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(52px,8vw,150px);line-height:var(--lh-lg);letter-spacing:var(--tr-lg);text-transform:uppercase;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
.board-h{font-size:clamp(40px,5vw,90px)}
.board-note{color:var(--g3);max-width:56ch}
.ww p:first-child{font-family:var(--disp);font-weight:800;font-size:30px;text-transform:uppercase;color:var(--wh)}
.board-inc{list-style:none;margin:0;padding:0;align-self:center;display:grid;gap:var(--sp-2)}
.board-inc li{display:flex;gap:var(--sp-3);padding:var(--sp-2) 0}
@media (max-width:860px){.board{grid-template-columns:1fr}}
.hv-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--sp-f-8)}
.hv{position:relative;display:grid;gap:var(--sp-3);align-content:start;padding-top:var(--sp-5);color:var(--g3)}
.hv p{margin:0}
.hv .h3x{color:var(--wh)}
.hv-n{font-family:var(--hud);font-size:12px;color:var(--g2)}
@media (max-width:860px){.hv-grid{grid-template-columns:1fr}}
.ft-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-6) var(--sp-5)}
.ft{padding:var(--sp-5) 0;display:grid;gap:var(--sp-3);align-content:start}
.ft h3{margin:0;font-family:var(--disp);font-weight:800;font-size:28px;line-height:1;text-transform:uppercase}
.ft p{margin:0;color:var(--g3)}
@media (max-width:860px){.ft-grid{grid-template-columns:1fr}}
.yb{display:grid;grid-template-columns:auto 1fr auto;gap:var(--sp-3) var(--sp-5);align-items:center;margin-top:var(--sp-5);padding:var(--sp-5);text-decoration:none;transition:background-color var(--t-med),color var(--t-med)}
.yb:hover,.yb:focus-visible{background-color:var(--wh);color:var(--bk)}
.yb-h{font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(28px,3.4vw,56px);line-height:.92;text-transform:uppercase}
.yb-t{grid-column:2;color:inherit;opacity:.85}
.yb .btn{grid-row:1/3;grid-column:3}
@media (max-width:760px){.yb{grid-template-columns:1fr}.yb-t,.yb .btn{grid-column:1;grid-row:auto}}

/* ---------- about standings / groups ---------- */
.standings{list-style:none;margin:0;padding:0}
.stand{position:relative;display:grid;grid-template-columns:58px 104px minmax(0,1fr) minmax(0,1.25fr) 140px;grid-template-rows:auto auto;column-gap:var(--sp-5);row-gap:var(--sp-1);align-items:center;padding:var(--sp-4) 0;isolation:isolate;text-decoration:none;transition:color var(--t-fast)}
.stand-img{grid-row:1/3;width:104px;height:68px;object-fit:cover;filter:grayscale(1) contrast(1.12) brightness(.8);transition:filter var(--t-med) var(--ease)}
.stand:hover .stand-img,.stand:focus-visible .stand-img{filter:grayscale(1) contrast(1.06) brightness(1)}
.stand::before{content:"";position:absolute;inset:0 calc(var(--g) * -1);z-index:-1;background:var(--wh);transform:scaleY(0);transform-origin:bottom;transition:transform var(--t-med) var(--cut)}
.stand:hover,.stand:focus-visible{color:var(--bk)}
.stand:hover::before,.stand:focus-visible::before{transform:scaleY(1)}
.stand:hover .stand-c,.stand:hover .stand-s{color:var(--on-fill)}
.stand-p{grid-row:1/3;font-family:var(--disp);font-style:italic;font-weight:900;font-size:40px;line-height:1;font-variant-numeric:tabular-nums}
.stand-n{grid-column:3;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(30px,3.2vw,52px);line-height:var(--lh-md);letter-spacing:var(--tr-md);text-transform:uppercase;align-self:end}
.stand-c{grid-column:3;color:var(--g3);align-self:start}
.stand-s{grid-column:4;grid-row:1/3;color:var(--g3)}
.stand-y{grid-column:5;grid-row:1/3;text-align:right}
@media (max-width:900px){.stand{grid-template-columns:44px 84px minmax(0,1fr) 120px}.stand-img{width:84px;height:56px}.stand-s{grid-column:1/-1;grid-row:auto;margin-top:var(--sp-1)}.stand-y{grid-column:4;grid-row:1/3}}
@media (max-width:620px){.stand{grid-template-columns:38px 68px minmax(0,1fr);column-gap:var(--sp-3)}.stand-img{width:68px;height:48px}.stand-p{font-size:28px}.stand-y{grid-column:3;grid-row:auto;text-align:left}}
.grp{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--sp-f-8);margin-top:var(--sp-f-7)}
.grp-b{padding-top:var(--sp-5);color:var(--g3)}
.grp-b h3{margin:0 0 var(--sp-3);color:var(--wh);font-family:var(--disp);font-weight:800;font-size:clamp(28px,2.6vw,42px);line-height:1;text-transform:uppercase}
.grp-b p{margin:0 0 var(--sp-3)}
@media (max-width:860px){.grp{grid-template-columns:1fr}}

/* ---------- partner cards ---------- */
.pcard-grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));gap:var(--sp-4)}
.pcard{position:relative;height:100%;display:grid;gap:var(--sp-3);align-content:start;padding:var(--sp-5);overflow:hidden;isolation:isolate;text-decoration:none;transition:background-color var(--t-med)}
.pcard:hover,.pcard:focus-visible{background-color:var(--tint)}
.pcard:hover::after,.pcard:focus-visible::after{--pl-line:var(--wh)}
.pcard-a{position:absolute;right:-4px;bottom:-26px;z-index:-1;font-family:var(--disp);font-style:italic;font-weight:900;font-size:130px;line-height:1;letter-spacing:var(--tr-lg);color:transparent;-webkit-text-stroke:var(--stroke-fine) var(--deco-stroke);transition:-webkit-text-stroke-color var(--t-med)}
.pcard:hover .pcard-a{-webkit-text-stroke-color:var(--g2)}
.pcard h3{margin:var(--sp-2) 0 0;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(30px,2.8vw,44px);line-height:.92;text-transform:uppercase}
.pcard p{margin:0;color:var(--g3)}
.pcard-j::after{--pl-line:var(--wh)}
.pcard-j .btn{justify-self:start}

/* ---------- insights ---------- */
/* Each row was a link that read as a paragraph. The only thing saying
   "target" was a 22px arrow in a third column — and that arrow was
   `display:none` below 640px, so on a phone the row carried NO affordance at
   all, and at every width the white-plate invert only arrived once the
   pointer was already on it. A hover-only affordance is invisible to a touch
   user and to anyone scanning the page, which is exactly what Bailey
   reported.

   The row now reads as a target at rest, in vocabulary this design already
   owns and with no underline (standing rule since round 3):

     · a bevelled plate. This said "uniform 1px --g1 on all four sides" and
       was wrong on both counts by the time anyone read it again: the row
       carries `class="ins-row tick"` and declares no `border` at all, and
       .tick paints --hair, not --g1. The edge is the shared six-layer plate —
       four thin lines stopping short of the two cut corners, with a bright
       45deg diagonal laid into each. Still never a one-sided accent rule,
       which remains banned sitewide.
     · that plate IS the registration mark now. The separate four-corner ticks
       were replaced by the chamfer the buttons already drew (2026-09-22), so
       one edge language covers controls, chips, panels and this row alike
     · a persistent HUD action chip, present at EVERY width, replacing the
       arrow that vanished on phones

   Hover then makes the affordance stronger rather than merely present: the
   existing plate inverts, --tk flips the corner ticks to black so the frame
   survives on white, and the chip fills — the same "inverts to a filled chip"
   gesture prose links already use. */
.ins-list{list-style:none;display:grid;gap:var(--sp-4);margin:0;padding:0}
/* The numeral track was a FIXED 90px plus a 24px gap, so every line of copy
   in an insight card began 114px from the card's own left edge, against a
   numeral that is 72px of outlined display type. That is a reserved gutter
   wider than the thing it reserves, and it read as wasted space (Bailey).
   `auto` sizes the track to the numeral itself and the gap drops a step, so
   the copy starts where the numeral ends rather than at a guessed offset.
   Audited against every other fixed-first-track grid in the three sheets:
   the rest are 18-60px and proportionate to what they hold (a Q number, a
   caret, real data columns), so this is the one outlier, not a sweep. */
.ins-row{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--sp-2) var(--sp-4);
  padding:var(--sp-f-5);text-decoration:none;isolation:isolate;
  transition:color var(--t-fast) var(--ease)}
/* inset by --edge, not 0: the bevelled plate draws its edge as the element's
   own background, so a fill at inset:0 would paint straight over it and the
   row would lose its outline at exactly the moment it is being pointed at. */
.ins-row::before{content:"";position:absolute;inset:var(--edge);z-index:-1;background:var(--wh);clip-path:var(--bevel);transform:scaleY(0);transform-origin:bottom;transition:transform var(--t-med) var(--cut)}
.ins-row:hover,.ins-row:focus-visible{color:var(--bk);--tk-hi:var(--bk);--tk-edge:var(--bk)}
.ins-row:hover::before,.ins-row:focus-visible::before{transform:scaleY(1)}
.ins-row:hover p,.ins-row:focus-visible p{color:var(--on-fill)}
.ins-n{grid-row:1/5;font-family:var(--disp);font-style:italic;font-weight:900;font-size:56px;line-height:.8;color:transparent;-webkit-text-stroke:var(--stroke) currentColor;font-variant-numeric:tabular-nums}
.ins-row .hud,.ins-row h2,.ins-row p,.ins-cta{grid-column:2}
.ins-row p{margin:0;max-width:70ch;color:var(--g3)}
.ins-cta{justify-self:start;display:inline-flex;align-items:center;gap:var(--sp-2);margin-top:var(--sp-3);
  padding:var(--sp-2) var(--sp-3);--pl-line:var(--g2);color:var(--wh);font-weight:600;
  transition:background-color var(--t-fast) var(--ease),translate var(--t-fast) var(--ease)}
.ins-cta i{font-style:normal}
.ins-row:hover .ins-cta,.ins-row:focus-visible .ins-cta{background-color:var(--bk);--pl-line:var(--bk);--pl-hi:var(--bk);color:var(--wh);translate:3px 0}
@media (max-width:640px){.ins-row{grid-template-columns:auto 1fr;gap:var(--sp-2) var(--sp-3)}.ins-n{font-size:38px}}
.read-p{position:fixed;left:0;right:0;top:0;z-index:310;height:2px;pointer-events:none}
.read-p i{position:absolute;inset:0;background:var(--wh);transform-origin:left;transform:scaleX(var(--p,0))}
.art{padding-block:var(--sec-lg) 0}
.art-head,.answer-box,.art-body,.art-sec,.art-rel{max-width:72ch;margin-inline:auto}
.art-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2) var(--sp-3);color:var(--g3)}
.art-h1{margin:var(--sp-4) 0 0;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(46px,6.6vw,110px);line-height:var(--lh-lg);letter-spacing:var(--tr-lg);text-transform:uppercase}
.answer-box{margin-top:var(--sp-f-7);padding:var(--sp-f-6);font-size:clamp(18px,1.5vw,21px)}
.answer-box h2{margin:0 0 var(--sp-3);color:var(--g3)}
.answer-box p{margin:0}
.art-sec{margin-top:var(--sp-f-8);color:var(--g3)}
.art-sec h2{margin:0 0 var(--sp-4);color:var(--wh);font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(34px,4vw,60px);line-height:.92;text-transform:uppercase}
.art-sec p,.art-sec ul,.art-sec ol{margin:0 0 var(--sp-4)}
.art-sec li{margin-bottom:var(--sp-2)}
/* The frame survives the scroller: background-attachment defaults to `scroll`,
   which positions the plate against this element's own border box rather than
   against the content inside it, so dragging a wide table sideways does not
   drag its edge off with it. */
.rm-table{overflow-x:auto;margin:var(--sp-5) 0}
.rm-table table{width:100%;min-width:560px;border-collapse:collapse;font-size:15px}
.rm-table th{text-align:left;padding:var(--sp-3) var(--sp-3);background:var(--wh);color:var(--bk);font-family:var(--hud);font-size:12px;font-weight:600;letter-spacing:.12em;text-transform:uppercase}
/* Found in a screenshot, not by a gate: the header band is full-bleed white and
   was painting over the frame's top-right chamfer, squaring it off while the
   bottom-left stayed cut. Only the LAST header cell reaches that corner. */
.rm-table th:last-child{clip-path:var(--bevel-tr)}
.rm-table td{padding:var(--sp-3) var(--sp-3);border-top:1px solid var(--g1);vertical-align:top;color:var(--g3)}
.rm-table td:first-child{color:var(--wh);font-weight:600}
.rm-table tr:hover td{background:var(--tint-2)}
/* ---------- tables below 760px: stacked, never dragged --------------------
   `.rm-table{overflow-x:auto}` around a table with `min-width:560px` meant
   that below ~600px EVERY table on the site had to be dragged sideways to be
   read — three insight pages, 3×9, 3×8 and 2×8 cells. `overflow.py` reported
   `ok` on all of them and was right to: the page never moved, only the table
   did. That is the blind spot `tools/narrow.py` was written to close, and it
   is the owner's oldest standing complaint in miniature.

   Below the breakpoint each row becomes a block plate and each cell prints
   its column heading above its value, from the `data-label` p_pages.rtable()
   stamps on. Nothing is shrunk and nothing is truncated: the font holds at
   15px and every cell keeps its full text.

   760px, not 640: the table needs 560px plus two --g gutters (up to 88px), so
   it only actually fits from ~650px. Choosing the existing .mcta breakpoint
   keeps one narrow-width story rather than inventing a second one, and gives
   the stacked reading to small tablets where the cramped table was worst.

   `thead{display:none}` takes the headings out of the accessibility tree on
   purpose. The ARIA roles rtable() adds keep the structure a table after
   `display:block` strips the implicit ones, and the generated `::before`
   label is then announced exactly ONCE per cell — leaving thead in the tree
   would have a screen reader read "Duration, Duration, 3–4 weeks". */
@media (max-width:760px){
  .rm-table{overflow-x:visible;--pl-line:transparent;--pl-hi:transparent;margin:var(--sp-5) 0}
  .rm-table table{display:block;min-width:0;width:100%}
  .rm-table thead{display:none}
  .rm-table tbody{display:block}
  .rm-table tr{display:block;padding:var(--sp-4);--pl-line:var(--g1);--pl-hi:var(--wh)}
  .rm-table tr + tr{margin-top:var(--sp-3)}
  .rm-table td,.rm-table td:first-child{display:block;padding:0;border-top:0}
  .rm-table td + td{margin-top:var(--sp-3)}
  .rm-table td[data-label]::before{content:attr(data-label);display:block;margin-bottom:var(--sp-1);
    font-family:var(--hud);font-size:var(--hud-xs-fs);font-weight:600;letter-spacing:var(--hud-xs-tr);
    text-transform:uppercase;color:var(--g2)}
  /* The comparison table's first column IS its row label (its header cell is
     empty), so it keeps the emphasis rather than gaining a blank label. */
  .rm-table tr:hover td{background:none}
}
.rm-sep{color:var(--g2)}
.art-rel{margin-top:var(--sp-f-8);padding-top:var(--sp-5)}
.art-rel ul{list-style:none;margin:var(--sp-2) 0 var(--sp-5);padding:0}
.art-rel li a{display:inline-block;padding:var(--sp-2) 0}

/* ---------- faq ---------- */
.faq-nav{margin-top:0}
.faq-wrap{max-width:1180px;margin-inline:auto}
.faq-cat{padding-top:var(--sp-f-9)}
.faq-cat-h{display:grid;grid-template-columns:auto 1fr auto;gap:var(--sp-4);align-items:baseline;margin-bottom:var(--sp-5)}
.nudge{margin:var(--sp-4) 0 0;color:var(--g3)}
.faq-cat .board{margin-top:var(--sp-5)}
/* the 3px inset rule across the top of the process timeline is gone — the
   outlined stage numerals already carry the sequence */
.tl{list-style:none;margin:var(--sp-6) 0 0;padding:0;display:grid;grid-template-columns:repeat(10,minmax(0,1fr));gap:var(--sp-4) var(--sp-2)}
.tl li{display:grid;gap:var(--sp-2);padding:0 var(--sp-2) 0 0;color:var(--g3)}
.tl b{font-family:var(--disp);font-style:italic;font-weight:900;font-size:34px;line-height:1;color:var(--wh)}
@media (max-width:860px){.tl{grid-template-columns:repeat(5,1fr);row-gap:var(--sp-4)}}
@media (max-width:460px){.faq-cat-h{grid-template-columns:1fr}.tl{grid-template-columns:repeat(2,1fr)}}

/* ---------- contact form ---------- */
.contact{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(0,1fr);gap:var(--sp-f-8);align-items:start}
@media (max-width:960px){.contact{grid-template-columns:1fr}}
.form{display:grid;gap:var(--sp-3)}
.fs{margin:0;padding:var(--sp-5) 0 var(--sp-6);border:0}
.fs legend{float:left;width:100%;display:flex;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-4);padding:0}
.fs legend b{display:inline-grid;place-items:center;min-width:34px;height:22px;background:var(--wh);color:var(--bk);font-weight:600}
.fs-g{clear:both;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--sp-4)}
.f{display:grid;gap:var(--sp-2);align-content:start}
.f:has(textarea){grid-column:1/-1}
.fs-g>.f:only-child{grid-column:1/-1}
.f label{color:var(--g3)}
.opt{color:var(--g2);font-size:11px;margin-left:var(--sp-1)}
input,textarea,select{width:100%;min-height:54px;padding:var(--sp-3) var(--sp-4);border:1px solid var(--field);border-radius:0;background:var(--field-bg);color:var(--wh);font:inherit;font-size:17px;transition:border-color var(--t-fast),background var(--t-fast)}
/* No clipped corner on fields: clip-path cuts the 1px border with it, so the
   corners read as torn open rather than bevelled. The bevel stays on buttons,
   which are filled and can carry it. */
textarea{min-height:160px;resize:vertical}
select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--wh) 50%),linear-gradient(135deg,var(--wh) 50%,transparent 50%);background-position:calc(100% - 20px) 50%,calc(100% - 14px) 50%;background-size:6px 6px;background-repeat:no-repeat;padding-right:var(--sp-6)}
input::placeholder,textarea::placeholder{color:var(--g2)}
input:hover,textarea:hover,select:hover{border-color:var(--g2)}
input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--wh);outline-offset:0;border-color:var(--wh)}
[aria-invalid=true]{border-color:var(--wh);box-shadow:inset 0 0 0 1px var(--wh)}
.f-err{margin:0;color:var(--wh)}
.f-err::before{content:"▲ "}
.f-banner{padding:var(--sp-3) var(--sp-4);background-color:var(--tint)}
.opts{grid-column:1/-1;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--sp-2)}
@media (max-width:640px){.fs-g,.opts{grid-template-columns:1fr}}
.opt-c{position:relative;display:flex;align-items:center;gap:var(--sp-3);min-height:54px;padding:var(--sp-2) var(--sp-3);border:1px solid var(--field);cursor:pointer;transition:border-color var(--t-fast),background var(--t-fast),color var(--t-fast)}
.opt-c input{position:absolute;opacity:0;width:1px;height:1px;min-height:0}
.opt-c span::before{content:"";display:inline-block;width:14px;height:14px;margin-right:var(--sp-3);border:1px solid currentColor;vertical-align:-2px}
.opts-radio .opt-c span::before{border-radius:50%}
.opt-c:hover{border-color:var(--g3)}
.opt-c:has(input:checked){background:var(--wh);color:var(--bk);border-color:var(--wh)}
.opt-c:has(input:checked) span::before{background:currentColor;box-shadow:inset 0 0 0 3px var(--wh)}
.opt-c:has(input:focus-visible){outline:2px solid var(--wh);outline-offset:3px}
.hp-field{position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden}
.f-submit{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-4) var(--sp-5);padding-top:var(--sp-2)}
.side{display:grid;gap:var(--sp-3)}
.side-b{padding:var(--sp-5);color:var(--g3)}
.side-b p,.side-b h2{margin:0 0 var(--sp-2)}
/* Same component as the footer email, one size down — family, weight, case and
   the invert-on-hover affordance all come from .mail rather than being
   restated here with an underline. */
.side-b .mail{font-size:clamp(24px,2.4vw,36px)}
.locs{list-style:none;margin:0;padding:0}
.locs li{padding:var(--sp-2) 0;color:var(--wh)}
.steps3{list-style:none;margin:0;padding:0;display:grid;gap:0}
.steps3 li{display:grid;grid-template-columns:56px 1fr;gap:var(--sp-3);padding:var(--sp-4) 0}
.steps3 b{font-family:var(--disp);font-style:italic;font-weight:900;font-size:44px;line-height:.9;color:transparent;-webkit-text-stroke:var(--stroke) var(--wh);font-variant-numeric:tabular-nums}
.steps3 h3{margin:0 0 var(--sp-1);color:var(--wh);font-family:var(--disp);font-weight:800;font-size:24px;text-transform:uppercase;line-height:1}
.steps3 p{margin:0}
.steps-row{grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-5);margin-top:var(--sp-f-7);color:var(--g3)}
@media (max-width:860px){.steps-row{grid-template-columns:1fr;gap:0}}

/* ---------- thank-you / 404 ---------- */
.ty-tick{display:grid;place-items:center;width:84px;height:84px;margin:0 0 var(--sp-5);border:1px solid var(--wh);border-radius:50%;font-size:40px}
.mo .ty-tick{animation:tickIn var(--t-slow) var(--snap) both}
@keyframes tickIn{from{transform:scale(.4) rotate(-40deg)}to{transform:none}}
.ty-grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--sp-3)}
.ty-card{position:relative;display:grid;align-content:end;aspect-ratio:3/4;overflow:hidden;isolation:isolate}
.ty-card img{position:absolute;inset:0;z-index:-2;width:100%;height:100%;object-fit:cover;filter:grayscale(1) contrast(1.2) brightness(.5);transition:transform var(--t-slow) var(--out)}
.ty-card::after{content:"";position:absolute;inset:0;z-index:-1;background:linear-gradient(180deg,transparent 30%,rgba(0,0,0,.92))}
.ty-card:hover img{transform:scale(1.06)}
.ty-l{padding:var(--sp-4)}
@media (max-width:860px){.ty-grid{grid-template-columns:1fr 1fr}}
.nf-num{margin:0 0 -.1em;font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(140px,30vw,440px);line-height:.8;letter-spacing:var(--tr-mega);color:transparent;-webkit-text-stroke:var(--stroke) var(--wh);font-variant-numeric:tabular-nums}
.mo .nf-num{animation:glitch 400ms steps(4,end) both}
@keyframes glitch{0%{transform:translate(-8px,3px) skewX(-18deg)}50%{transform:translate(6px,-2px)}100%{transform:none}}
.nf ~ .sec h2{margin-bottom:var(--sp-4)}

/* ---------- the 3D mark ---------------------------------------------------
   Moved here from showcase.css, which loads only on / and /work: the footer
   now carries the same mark on all 56 pages. `.monk-rot` is the rotating box
   and `.lyr` its extrusion layers, built by site.js. Neither the hero nor the
   footer owns this — the hero positions it, the footer sizes it, and the base
   angles are custom properties so each placement can face it its own way.

   The idle rotation writes --spin/--tilt and JS writes --tx/--ty/--sc; the two
   are added in one calc() rather than fighting over `transform`. Registered
   with @property so the angles interpolate, and gated behind the
   animation-timeline support test, which Chrome shipped long after @property,
   so registration is guaranteed wherever the animation runs. Reduced motion
   gets the extrusion and none of the movement. */
@property --spin{syntax:"<angle>";inherits:false;initial-value:0deg}
@property --tilt{syntax:"<angle>";inherits:false;initial-value:0deg}
.monk-rot{position:relative;transform-style:preserve-3d;
  transform:rotateY(calc(var(--base-y,0deg) + var(--ty,0deg) + var(--spin,0deg))) rotateX(calc(var(--base-x,0deg) + var(--tx,0deg) + var(--tilt,0deg))) scale(var(--sc,1))}
@supports (animation-timeline:view()){
  .mo .monk-rot{animation:monkSpin 15s linear infinite,monkTilt 11s linear infinite}
}
@keyframes monkSpin{0%{--spin:-14deg}50%{--spin:14deg}100%{--spin:-14deg}}
@keyframes monkTilt{0%{--tilt:-5deg}50%{--tilt:5deg}100%{--tilt:-5deg}}
.lyr{position:absolute;inset:0;width:100%;height:100%;
  fill:color-mix(in srgb,var(--lyr-near) var(--t,50%),var(--lyr-far));overflow:visible}
.lyr.front{fill:var(--wh);transform:translateZ(.5px)}
.lyr.wire{fill:none;stroke:var(--wh);stroke-width:34;opacity:.45}

/* ---------- position rail -------------------------------------------------
   Moved here from showcase.css together with the case-study milestones and
   gallery rails: those pages load report.css, not showcase.css, and a
   component used by a page that never loads its stylesheet renders as
   unstyled markup while every other check still passes — the fault check_v2's
   coverage gate exists for. Round 6 removed the last thing showcase.css still
   held, `.pin .prail`: it absolutely positioned the rail against the bottom of
   the sticky race stage, and there is no sticky stage any more. The rail is a
   block under the card list at every width, so this is now the whole
   component. */
.prail{display:grid;gap:var(--sp-1);padding:var(--sp-4) var(--g) 0}
.prail-pos{display:flex;align-items:baseline;gap:var(--sp-2);margin:0;min-width:0}
.prail-pos b{font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(28px,3vw,48px);line-height:.9;font-variant-numeric:tabular-nums}
.prail-nm{margin-left:var(--sp-3);flex:1 1 auto;min-width:0;font-family:var(--disp);font-style:italic;font-weight:900;
  font-size:clamp(19px,2vw,32px);line-height:1;letter-spacing:var(--tr-md);text-transform:uppercase;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.prail-ticks{display:flex;gap:3px;margin:0;padding:0;list-style:none}
.prail-ticks li{flex:1 1 0;min-width:0}
.prail-t{display:block;padding-block:var(--sp-5)}
.prail-t i{display:block;height:2px;background:var(--hair);transition:background var(--t-fast) var(--ease),height var(--t-fast) var(--ease)}
.prail-t:hover i,.prail-t:focus-visible i{background:var(--g2)}
.prail-t.on i{height:4px;background:var(--wh)}
/* The scroller variant — the case-study milestones and gallery. Two things
   differ from the carousel's rail and both are forced rather than chosen: no
   page gutter, because .lapchart and .film-ctl already carry one; and a
   tighter tick row, because these segments are decoration rather than the
   ≥44px jump targets the carousel's are. */
.prail-sc{padding:0;gap:var(--sp-1);margin-bottom:var(--sp-3)}
.prail-sc .prail-pos b{font-size:clamp(22px,2.2vw,34px)}
.prail-sc .prail-t{padding-block:var(--sp-2);cursor:default}

/* ---------- footer: three bands ------------------------------------------
   It was 1020px tall at 1440 — taller than the viewport — carrying 39 links
   in five ragged columns, two 58px clocks outranking the email, a wordmark
   cropped mid-letter by overflow:hidden + nowrap, and a NAP line repeating
   everything above it.

   1 STATEMENT  the email at display scale, one CTA, nothing else
   2 LINKS      Services (7) + Company (5), and one "All pages →" into
                /sitemap. Work, sectors, cities and FAQ topics collapse into
                that single link rather than five columns of their own.
   3 BASE       one line: NAP, both clocks demoted to a 16px HUD readout
                beside the cities they belong to, socials, legal.

   39 links → 21. NAP 7 → 4 (the personal LinkedIn duplicated the company
   one). The wordmark is FIT rather than cropped: at 16vw the two words clear
   the container with ~17% to spare at every width, in the webfont and in the
   size-adjusted fallback, so .foot no longer needs overflow:hidden — and the
   scaleY scroll coupling that squashed it is gone. */
.foot{margin-top:var(--sec-lg);padding-block:var(--sec) var(--sp-6);content-visibility:auto;contain-intrinsic-size:auto 700px}
.p-home .foot,.p-work .foot{margin-top:0}
/* Clearance for the fixed .mcta action bar. This rule used to live up in the
   .mcta block at max-width:760px — which is EARLIER in this file than the
   `padding-block` shorthand above, so the shorthand reset it and the clearance
   never applied at any width. That is why "Back to top ↑" sat underneath the
   58px bar on a phone, and why raising the value there changed nothing. */
@media (max-width:760px){.foot{padding-bottom:var(--sp-9)}}
/* Bands 1 and 2 sit SIDE BY SIDE from 960px. Stacked, the rebuilt footer
   measured 1295px at 1440 — taller than the 1020px the audit was complaining
   about, so the link cull had bought nothing on the metric it named. Paired,
   the tallest of the two bands sets the height instead of their sum. */
.foot-top{display:grid;gap:var(--sp-8)}
/* 1.25fr, not 1fr: at 1fr the statement column is ~480px and the email broke
   mid-domain ("LAUNCH@RAGINGMON / K.CO"). The link band had the slack to give —
   its third column holds one short link. */
@media (min-width:960px){.foot-top{grid-template-columns:minmax(0,1.25fr) minmax(0,2fr);gap:var(--sp-7);align-items:start}}
/* band 1 — statement */
.foot-say{display:grid;gap:var(--sp-4);justify-items:start}
.foot-say p{margin:0}
/* The email was a growing 2px underline on hover — that is an underline, so
   it inverts to a filled chip instead, the same affordance prose links use. */
/* Sized so all 20 characters of the address set on ONE line inside the
   statement column, in the webfont and in the wider size-adjusted fallback
   both. overflow-wrap:anywhere stays as a last resort — breaking is still
   better than pushing the page — but it should never be reached. */
.mail{display:inline-block;font-family:var(--disp);font-weight:800;font-size:clamp(26px,3vw,52px);line-height:1.04;
  text-transform:uppercase;justify-self:start;overflow-wrap:anywhere;
  padding:var(--sp-1) var(--sp-2);margin-inline:calc(var(--sp-2) * -1);
  transition:background var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
.mail:hover,.mail:focus-visible{background:var(--wh);color:var(--bk)}
/* band 2 — links */
.foot-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,170px),1fr));gap:var(--sp-6) var(--sp-5)}
.fcol{min-width:0}
.foot-cols ul{list-style:none;margin:var(--sp-2) 0 0;padding:0}
.foot-cols a{display:inline-block;padding:var(--sp-2) 0;text-decoration:none}
.foot-cols a,.foot-meta a,.foot-legal a{color:var(--g3);transition:color var(--t-fast) var(--ease),translate var(--t-fast) var(--ease)}
.foot-cols a:hover,.foot-cols a:focus-visible{color:var(--wh);translate:3px 0}
.nap-soc a:hover,.nap-soc a:focus-visible,.foot-legal a:hover,.foot-legal a:focus-visible{color:var(--wh)}
.fcol-all a{color:var(--wh);font-weight:600}
@media (max-width:640px){.foot-cols{grid-template-columns:1fr 1fr}}
/* the wordmark: fitted, not cropped, and no longer scroll-coupled */
.foot-word{margin:var(--sp-8) 0 0;display:flex;justify-content:space-between;align-items:baseline;gap:var(--sp-4);
  font-family:var(--disp);font-style:italic;font-weight:900;font-size:min(12vw,240px);line-height:.92;
  padding-block:var(--sp-1);text-transform:uppercase;white-space:nowrap}
/* The 3D mark goes in the gap the split wordmark already leaves between
   RAGING and MONK, so it completes that lockup instead of opening a band of
   its own. Sized in `em` against the wordmark's own font-size, so it scales
   with it, and small enough that the row's .92 line-height is always the
   taller of the two: the mark costs ZERO footer height.
   Measured, not assumed — and the first attempt failed exactly that claim. At
   .86em it added 4px at 1440 (904 -> 908px), which is small enough to have
   shipped unnoticed under a comment saying "no added height". At .82em the
   footer measures 904px at 1440 and 1324px at 400, identical to the round-4
   numbers, with the mark at 127x147px inside a 159px row.
   Deliberately quieter than the hero — fewer layers and less depth
   (core.monk3d), plus .82 opacity — because the footer was just rebuilt to be
   calm and this is a considered addition, not a second hero. */
.foot-mark{position:relative;flex:0 0 auto;width:.82em;aspect-ratio:1;
  perspective:900px;opacity:.82}
.foot-mark .monk-rot{position:absolute;inset:0;--base-y:-16deg;--base-x:5deg}
@media (max-width:560px){.foot-mark{width:.78em}}
/* band 3 — base
   This was one run-on sentence at line-height:2 — seven items strung together
   with middots, wrapping to three ragged lines: "RagingMonk · Sydney, NSW,
   Australia · Ho Chi Minh City, Vietnam · launch@ragingmonk.co · Instagram
   @raging.monk · RagingMonk on LinkedIn · Tiếng Việt → ragingmonk.co/vi",
   with the two clocks sitting beside it as a separate list naming the same
   two cities again.

   It is REDESIGNED, not deleted. The visible "Sydney" and "Ho Chi Minh City"
   text is a real local-SEO signal for both markets and the <address> element
   is what marks it as the organisation's contact block, so both stay. What
   goes is the run-on: the two cities are now paired address blocks with their
   own clock inline (the clock finally sits inside the city it names, and
   .foot-clocks disappears as a component), the socials are their own group,
   and the Vietnamese link is a bordered chip rather than trailing prose.

   The EMAIL is dropped here, and only here: it already appears at display
   scale in band 1, ~400px above, so the footer was publishing it twice.

   Height is the constraint this band is built against, and the first
   attempt failed it. Round 4 got the footer from 1295px to 904px at 1440
   and 1324px at 400. Setting each city as a three-line stack (name / region /
   clock) measured 910px and 1375px — SIX rows where the run-on used four, so
   a redesign sold as "cleaner" had made the thing 51px taller on the phone it
   was meant to help. Each city is now ONE baseline-aligned row and the
   socials share a row with the language chip, which is what brings it back
   under the round-4 figures. */
.foot-base{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:var(--sp-4) var(--sp-6);margin-top:var(--sp-7)}
.foot-nap{margin:0;font-style:normal;color:var(--g3);max-width:var(--measure-wide)}
.nap-org{margin:0 0 var(--sp-2)}
.nap-cities{display:grid;gap:var(--sp-2);margin:0;padding:0;list-style:none}
/* one row per city: name, region, and the clock that finally sits inside the
   city it names rather than in a separate list repeating both names */
.nap-city{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-1) var(--sp-3)}
.nap-c{color:var(--wh);font-weight:600}
.nap-r{color:var(--g3)}
.nap-city time{font-weight:600;font-size:16px;line-height:1.2;font-variant-numeric:tabular-nums;color:var(--wh)}
.foot-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3) var(--sp-5);color:var(--g3)}
.nap-soc{display:flex;flex-wrap:wrap;gap:var(--sp-1) var(--sp-4);margin:0;padding:0;list-style:none}
.nap-soc a{display:inline-block;padding:var(--sp-1) 0}
/* A distinct element, not the tail of a sentence. Bordered so it reads as the
   one thing in this band you are meant to click. */
.nap-lang{display:inline-flex;align-items:center;gap:var(--sp-2);padding:var(--sp-2) var(--sp-3);
  color:var(--g3);
  transition:color var(--t-fast) var(--ease)}
.nap-lang:hover,.nap-lang:focus-visible{--pl-line:var(--wh);color:var(--wh)}
@media (max-width:560px){.nap-cities{gap:var(--sp-4) var(--sp-5)}}
.foot-legal{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:var(--sp-2) var(--sp-5);margin-top:var(--sp-5);color:var(--g3)}
.foot-legal a{display:inline-block;padding:var(--sp-3) 0}

/* ---------- /sitemap ---------- */
/* Fixed column counts by viewport rather than auto-fit (Bailey, 2026-09-22):
   2 on mobile, 4 on tablet and desktop. auto-fit gave 1 column on a phone and
   a count that drifted with content width in between. Used by /sitemap and by
   /work's "browse by what it is", which are the only two instances. */
.smap{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--sp-7) var(--sp-5);margin-top:var(--head-gap)}
@media (min-width:900px){.smap{grid-template-columns:repeat(4,minmax(0,1fr))}}
.smap section{display:grid;align-content:start;gap:var(--sp-2)}
/* The lower clamp bound is 17px, not 26px: two fixed columns at 320px give
   ~148px each, and a 26px display italic heading escaped its column by 14px
   (caught by narrow.py, not by eye). overflow-wrap so a long single word —
   "FUNCTION VENUES", "HO CHI MINH CITY" — breaks instead of pushing out. */
.smap h2,.smap h3{margin:0 0 var(--sp-2);font-family:var(--disp);font-style:italic;font-weight:900;font-size:clamp(17px,4.4vw,38px);line-height:1;letter-spacing:var(--tr-md);text-transform:uppercase;overflow-wrap:anywhere}
.smap ul{list-style:none;margin:0;padding:0}
/* inline-block, NOT block: as a block the anchor fills the column and its
   tracked letter-spacing overhangs the <li> by 3px at 400px. Shrink-wrapping
   keeps the overhang inside the box. max-width so a long label still wraps
   rather than escaping, which is what block was added to fix at 320px. */
.smap li a{display:inline-block;max-width:100%;padding:var(--sp-2) 0;color:var(--g3);overflow-wrap:anywhere;transition:color var(--t-fast) var(--ease),translate var(--t-fast) var(--ease)}
/* Same collision, same fix: these are bare <a> inside <li>, so
   `li a:not([class])` matched them, and this rule won the colour and left the
   fill behind it. The sector list is navigation, not prose. */
.smap li a:hover,.smap li a:focus-visible{background:none;color:var(--wh);translate:3px 0}

/* ---------- the two loading animations (round 8) --------------------------
   Bailey asked for two, which reverses the "no loader" rule this direction
   spent six rounds earning. Both are therefore built so they cannot become the
   friction he objected to:

   · NEITHER CAN HOLD A PAGE. The intro paints over a hero that is already
     rendered underneath; the transition never calls preventDefault on a link,
     so the browser navigates at click speed and the overlay lives for however
     long the network actually takes, not for a duration written here.
   · Both tear down in CSS, with `both` fill ending hidden. site.js adds the
     skip and removes the node; if it never runs, the animation still ends.
   · Both are armed by the inline boot script and are display:none by default,
     so a reduced-motion reader, a repeat visitor, a private window whose
     sessionStorage throws, and a browser with no scripting at all each get the
     page with nothing in front of it.
   · position:fixed throughout, so neither can contribute a pixel of layout
     shift or a pixel of horizontal overflow.

   The 200ms view-transition cross-fade is REPLACED, not extended. Two
   transition systems composite over each other: the cross-fade snapshots the
   outgoing document, static overlay included, and dissolves it, which turns a
   hard cut into a fade. And an MPA view transition holds the new document's
   first render until the transition is ready, which is precisely the "never
   hold a page that is already ready" rule. */

/* ---- 1. first visit to the homepage: the mark, printed --------------------
   The 3D-printer read comes from three things running together rather than one
   effect: a print bed sweeping up the mark in nine discrete steps (clip-path,
   which never contributes to overflow), the extrusion layers site.js builds
   scaling into existence one after another, and a bright head line at the
   build front. The layers ARE the "separate line elements" of the brief: each
   is a contour of the mark at its own depth.

   The clip sits on .intro-mark and never on .monk-rot, because clip-path
   forces transform-style back to flat and would collapse the extrusion it is
   meant to reveal. */
.intro{position:fixed;inset:0;z-index:800;display:none;place-items:center;overflow:hidden;
  pointer-events:none;background:var(--bk);padding:var(--g)}
/* The root flag is `intro-on`, NOT `intro`. A bare `.intro` rule below sets
   position:fixed and display:none, and a bare class selector matches ANY
   element carrying it — including <html>. Naming the root flag `intro` put
   display:none on the root element and blanked the whole homepage in
   production. Never give <html> a class that an element rule also styles. */
/* 2600ms is a BACKSTOP, not the intended lift. site.js owns the teardown now,
   because the plate ends when the load-driven sequence ends rather than on a
   fixed clock. This stays so that a scripting failure still clears the plate
   instead of leaving a black screen over the page — the exact failure that
   blanked this homepage on 2026-09-22, when a stuck overlay was indisting-
   uishable from a dead site. Where JS runs, `.go` lifts it long before this. */
html.intro-on .intro{display:grid;animation:introOut var(--t-slow) var(--cut) 4200ms both}
html.intro-on .intro.go{animation:introOut 150ms var(--cut) 0s both}
@keyframes introOut{from{clip-path:inset(0)}to{clip-path:inset(0 0 100% 0);visibility:hidden}}
.intro-in{display:grid;justify-items:center;gap:var(--sp-5);text-align:center;max-width:min(92vw,520px)}
/* The print bed rises with MEASURED load progress (--p, set by site.js from
   real resource entries), not on a fixed 620ms keyframe. If the page is ready
   in 200ms the bed completes in 200ms: the plate can never hold a page that
   has already arrived, which is the rule this direction spent six rounds
   earning. The step() quantisation stays, so it still reads as a printer
   laying passes rather than a smooth wipe. */
/* The print bed is GONE (Bailey, 2026-09-22): "why is phase progress changing
   before the logo mark is 3d printed layer by layer?" Two devices were
   revealing the same mark — a clip-path bed wiping up from the bottom AND the
   extrusion layers switching on — so the silhouette stayed hidden behind the
   bed while the readout advanced, and the phases appeared to run ahead of the
   print. The layers accreting in depth ARE the print now, one per sub-phase,
   and nothing else gates them. */
.intro-mark{--mk:min(46vmin,300px);position:relative;width:var(--mk);height:var(--mk);perspective:1100px}
.intro-mark .monk-rot{position:absolute;inset:0;--base-y:-18deg;--base-x:6deg}
/* the head. Clipped to the build front by its own parent, which is what makes
   it read as the line being laid rather than a bar floating over the mark. */
/* The print-head rule is gone (Bailey, 2026-09-22): the bed clipping the mark
   into existence is the print, and a bright line riding it was a second device
   saying the same thing. The mark draws itself. */
/* Each layer is switched on by site.js as progress reaches it, rather than on
   a fixed per-layer delay. That is what ties the printed depth to the phases:
   layer i belongs to phase floor(i / layersPerPhase), so the stack thickens as
   the readout advances through Feasibility to Exit. */
html.intro-on .intro .lyr,.tx .lyr{opacity:0;scale:.06;transition:opacity 200ms var(--out),scale 200ms var(--out)}
html.intro-on .intro .lyr.on,.tx .lyr.on{opacity:1;scale:1}
/* The FIRST thing printed is the wire outline, and in the intro it is thinner
   and fainter than the shared mark's (Bailey, 2026-09-22: "initial mark should
   be very thin and light not white bright"). Scoped to .intro deliberately:
   .lyr.wire is shared with the hero and the footer marks, which are static and
   were not asked to change, so thinning it globally would edit two components
   nobody complained about. The build then climbs out of this to the solid white
   front face, which now lands LAST. */
html.intro-on .intro .lyr.wire,.tx .lyr.wire{stroke-width:16;opacity:.22}
@keyframes introLyr{from{scale:.06;opacity:0}to{scale:1;opacity:1}}
/* The slogan as a CAPTION, in the HUD label tier. The hero sets the same line
   at clamp(54px,9vw,176px); printing it twice at display scale inside two
   seconds would read as a stutter, so here it is the stamp under the mark and
   the hero is where it arrives full size. Same wipe as the testimonials. */
.intro-slogan{margin:0;max-width:26ch;font-size:clamp(13px,1.6vw,18px);letter-spacing:.2em;line-height:1.5;
  color:var(--wh);clip-path:inset(0 100% 0 0);animation:wipe 240ms steps(5,end) 380ms both}
/* ONE line, the phase being worked — not six rows (Bailey, 2026-09-22). The
   progress bar went with them: the percentage and the phase advancing are the
   readout, and a rule underneath was a third device saying the same thing.
   Fixed min-height and tabular figures so swapping phases cannot reflow the
   line or shuffle the mark above it. */
/* No flex-wrap and no percentage. The line holds ONE phase and ONE sub-phase,
   and every candidate string is stacked in the same grid cell, so the cell is
   permanently as wide as the longest of them: the readout advances without the
   line changing width, wrapping, or nudging the mark above it. That was the
   defect — the percentage and the joined sub-phase list both changed width on
   every step, so the whole block jumped. */
.intro-now{margin:var(--sp-5) 0 0;display:flex;justify-content:center;
  align-items:baseline;gap:var(--sp-3);min-height:1.4em;
  font-family:var(--hud);font-size:11px;letter-spacing:.14em;text-transform:uppercase}
.in-n{color:var(--g2);font-variant-numeric:tabular-nums}
.in-stk{display:inline-grid;justify-items:center}
.in-stk>*{grid-area:1/1;visibility:hidden}
.in-stk>.on{visibility:visible}
.in-w>*{color:var(--wh);font-weight:600;white-space:nowrap}
.in-s>*{color:var(--g2);white-space:nowrap}
/* The homepage carousel's line pagination (.prail-ticks), reused as the
   progress bar: one tick per phase, 2px hairline track, each filling left to
   right as its four sub-phases print. Deliberately NOT .prail-t's `.on{height:4px}`
   — a tick that grows is exactly the jump this readout was rebuilt to remove,
   so position is carried by the fill alone and every tick keeps its height. */
.intro-rail{display:flex;gap:3px;width:min(100%,320px);margin:var(--sp-4) auto 0;padding:0}
.intro-rail i{flex:1 1 0;min-width:0;height:2px;background:var(--hair);overflow:hidden}
.intro-rail i b{display:block;width:100%;height:100%;background:var(--wh);
  transform:scaleX(var(--f,0));transform-origin:left;transition:transform 110ms steps(4,end)}
.intro-hud{margin:0;display:flex;flex-wrap:wrap;justify-content:center;align-items:baseline;gap:var(--sp-1) var(--sp-3);
  animation:introFade var(--t-med) var(--ease) 700ms both}
@keyframes introFade{from{opacity:0}to{opacity:1}}

/* ---- 2. between internal pages: broken signal ----------------------------
   White static, pixelation and tear bands, all from gradients: no new asset,
   no new font, no library. `html.tx-out` is the page being left (site.js, on
   the click) and `html.tx-in` the page being arrived at (the boot script,
   before first paint). The band wipes DOWN on the way out and UP on the way
   in, so one gesture passes through the navigation.

   The lower 96px is kept clear of the signal so the HUD reads: a broadcast
   lower-third, the same place the home hero puts its telemetry. */
.tx{position:fixed;inset:0;z-index:860;display:none;overflow:hidden;pointer-events:none;background:var(--bk)}
/* A FLOOR ON THE WHOLE GESTURE, not on either half (Bailey, 2026-09-23: "too
   quick ... atleast bare minimum it should be quite visible within 0.7sec").
   The arrival duration is solved for the total rather than clamped on its own:
   the boot script knows how long the navigation actually took, so it sets
   --tx-ms to whatever makes the elapsed time plus the rise reach 700ms, with
   its own 240ms floor so a slow load still gets a readable tail.

   The elapsed figure is measured from the CLICK, not from the new document, so
   it already contains the 120ms fall — which overlaps the navigation rather
   than preceding it. Subtracting that 120 as well was the first version's
   error, and the browser caught it: it measured 640ms against a 700ms target.
   A local click that used to finish in ~480ms now lands at 700; a 300ms
   navigation also lands at 700; a 2s one is unchanged apart from the tail.
   The 2500ms ceiling on the pathological case is untouched.
   --tx-p0 is the progress bar's starting point and is now a fraction of that
   same real total, so the bar still says where the navigation genuinely is
   rather than where it is against a fixed 980ms guess. */
html.tx-out .tx{display:block;animation:txDown 120ms var(--cut) both}
html.tx-in .tx{display:block;animation:txUp var(--tx-ms,320ms) var(--cut) both}
@keyframes txDown{from{clip-path:inset(0 0 100% 0)}to{clip-path:inset(0)}}
@keyframes txUp{from{clip-path:inset(0)}to{clip-path:inset(100% 0 0 0);visibility:hidden}}
/* The between-pages overlay is the SAME PRINTER as the intro (Bailey,
   2026-09-22), not a second visual language. The broken-signal vocabulary it
   replaces, static, pixelation, tear bands and the stepped jag, is gone with
   its four keyframe sets: roughly 40 lines of CSS deleted rather than left
   unused beside the thing that superseded it.

   The layer reveal, the wire-first order and the brightness ramp are SHARED
   with .intro above by selector, so the two printers cannot drift into looking
   like different machines. What differs is only what a navigation can say and
   a first visit cannot: where you are going. */
/* .tx-box, NOT .tx-in: `tx-in` is the flag the boot script puts on <html>, and
   a bare class selector matches the root element too — the exact collision
   that rendered the whole homepage display:none in session 41. */
.tx-box{position:absolute;inset:0;display:grid;justify-items:center;align-content:center;
  gap:var(--sp-4);padding:var(--g);text-align:center}
.tx-mark{--mk:min(26vmin,168px);position:relative;width:var(--mk);height:var(--mk);perspective:900px}
.tx-mark .monk-rot{position:absolute;inset:0;--base-y:-18deg;--base-x:6deg}
/* CENTRED, not width-locked, and that is a deliberate retreat from the intro's
   no-jump guarantee. The intro can stack every phase word in one grid cell
   because its vocabulary is fixed; this line contains the DESTINATION name,
   known only at click time, so 52 of them cannot all ship on every page. It
   changes about three times across a navigation rather than every 140ms, and
   centring makes that growth symmetric so nothing to either side shifts. */
.tx-now{margin:0;display:flex;justify-content:center;align-items:baseline;gap:var(--sp-3);
  min-height:1.4em;font-family:var(--hud);font-size:11px;letter-spacing:.14em;text-transform:uppercase}
.tx-w{color:var(--wh);font-weight:600}
.tx-s{color:var(--g2)}
.tx-dest{margin:0;font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--g3)}
.tx-dest .mute{color:var(--g2)}
/* Same line pagination as the intro rail and the homepage carousel: one tick
   per phase, filling left to right. Uniform height, because a tick that grows
   is the jump the intro readout was rebuilt to remove. */
.tx-rail{display:flex;gap:3px;width:min(70vw,260px);margin:0;padding:0}
.tx-rail i{flex:1 1 0;min-width:0;height:2px;background:var(--hair);overflow:hidden}
.tx-rail i b{display:block;width:100%;height:100%;background:var(--wh);
  transform:scaleX(var(--f,0));transform-origin:left;transition:transform 110ms steps(4,end)}
/* The old narrow-width rule tuned the broken-signal HUD: .tx-sig and .tx-hud
   no longer exist, and .tx-dest means something different now. It was a path
   label truncating inside a bottom-lined HUD, so it carried max-width:40%;
   it is now a centred destination line under the mark, where that width would
   squeeze it for no reason. Replaced rather than deleted, because the mark
   still wants to be smaller on a phone. */
@media (max-width:560px){.tx-mark{--mk:min(30vmin,132px)}.tx-box{gap:var(--sp-3)}.tx-rail{width:min(80vw,220px)}}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation:none!important;transition-duration:.001ms!important;transition-delay:0s!important}
  /* Belt and braces. The boot script never arms either overlay under reduced
     motion, so neither is displayed in the first place; this makes certain
     that a class set by anything else still cannot paint one. */
  .intro,.tx{display:none!important}
}
