/* ==========================================================================
   TWM Legal - main.css - FOUNDATION ONLY (Build order steps 1-4).
   Constitution, tokens, synthetic fallback faces, base, layout primitives, guards.
   No components, no hover layer, no reveal keyframes: those are steps 5-9.
   Every number that could have been taste carries its arithmetic beside it; a value
   without a comment is a bug. Colour values WERE copied verbatim from
   reference/derivation/tokens.css (derive_colors.py). THAT IS NO LONGER TRUE OF THE ACCENT LADDER.
   Three generations of change sit on top of it, all verified by tools/verify_ratios.py, each marked at
   its own lines:
     [GLOW]   the earlier pass. --ink-3 and --brand were each raised one step in LIGHTNESS ONLY
              (hue and saturation held) by reference/derivation/derive_glow.py's inverse solve, to
              pay for the pointer light in 6.26. SUPERSEDED, and kept below as the previous
              generation rather than deleted: it records why the values v7 replaced were chosen.
     [RED v7] 2026-09-05, SUPERSEDED IN ITS VALUES, KEPT IN ITS REASONING. The firm reported that "the
              red used throughout is not a deep enough red" and the measurement agreed: what shipped was
              #EE4C3D = hsl(5.08, 83.9%, 58.6%), five degrees off pure red, and that hue offset is what
              made it read CORAL. scratchpad/{derive_red_v7.py, lock_v7.py} re-solved the whole ladder at
              hue 0 / saturation 100 - the mark's own hue - SPLIT it into a TEXT stop (#FF3D3D, kept
              light so red text could hold AA on a near-black ground) and a FILL stop (#C00000), and
              spent the headroom on a 1.38x stronger pointer light.
     [RED v8] 2026-09-06, THE PASS THAT SHIPS. The firm read v7 on the built site and instructed, in
              full: "The Button color red and title font color in the body is not as dark red as the
              text highlight. All red should be the color of the text highlight color. Fix." The text
              highlight is ::selection, which paints --brand-deep, so EVERY red on the site collapses
              onto the 2020 wordmark's own #BF0000 - fill, rule, mark, selection AND TEXT.
              THE FIRM WAS SHOWN THE MEASUREMENTS FIRST AND INSTRUCTED THE DEEP RED ANYWAY. Red text
              therefore no longer meets WCAG AA anywhere on this site; the elements, their measured
              ratios and the one-line revert are named in full in the token block below, and
              tools/verify_ratios.py asserts that they are BELOW AA so that silently "fixing" the colour
              back fails the build as loudly as letting the comments go stale would. Because red text is
              no longer held to 4.5:1, the pointer light's ceiling is now set by --ink-3 instead, and the
              light runs 1.62x brighter - which the firm asked for in the same breath.
   derive_colors.py has NOT been re-run, so tokens.css carries the pre-glow, pre-v7 palette.
   [FACE v9] 2026-09-06, A SECOND PASS ON THE SAME DAY AND A LARGER ONE: THE TYPEFACES CHANGED. The firm
   instructed, in full, two Google Fonts families - Newsreader for display/headings (400-600 plus a 400
   italic) and Montserrat for body, nav, buttons and labels (300-600) - and supplied both stacks and both
   metric-override sets itself. Titillium Web and Source Serif 4 are retired. This is not a rename: this
   file DERIVES numbers from font metrics, so the per-character advance (0.4179 -> 0.49072em, +17.4%),
   the display face's "0" (which now WIDENS with opsz instead of narrowing), the x-heights, the glyph
   extents and the wordmark's own raster fitting all moved, and every figure standing on one moved with
   them. What changed in VALUES: --measure 29.25 -> 34.35rem, --leading-body 1.6 -> 1.68, --leading-lede
   1.5 -> 1.575, the whole tracking ladder down two rungs (--track-1 0.40 -> 0.32em), .hero-title 19ch ->
   16ch, .display 26ch -> 22ch, .form-grid's track 12 -> 13.78rem, the header CTA query 88 -> 89rem, and
   the third synthetic serif family (--font-display-hero) deleted. What is HELD and now carries a
   recorded exception rather than a satisfied rule: --grid-track (misses its 40-character content rule by
   2.0 characters), --gutter-phone (the 45-55 phone measure is UNREACHABLE at any gutter), and the
   structural breakpoint pair ([REPAIR v10] NO LONGER AN EXCEPTION: the pair moved 64 -> 67rem and
   71 -> 74rem, the rows' own sums rounded up, so the nav no longer overshoots and the header mark holds
   its full 243.63px at every width the nav is up - see 6.1). The firm's supplied fallback overrides ship exactly as given INCLUDING one that
   measures wrong by 22.7% (Newsreader Fallback's ascent); the check is written beside it in block 2.
   One correction is recorded and NOT acted on: the brief calls Montserrat "the face in your logo", and
   the 2013 vector measures the wordmark as Titillium Text + Arvo. The pairing is the firm's decision and
   ships; the provenance claim does not.
   THE ROOT SIZE MOVED IN AN EARLIER PASS: html { font-size: 106.25% } (block 3). Every px figure in
   this file is therefore 1.0625 x its 16px-root value; every rem is unchanged; and because the whole
   system is expressed in rem, every character count and every ratio between two rem quantities is
   unchanged with it. Only comparisons against a VIEWPORT (which did not scale) had to be re-solved.
   Sources: reference/derivation/{tokens.css, derivation.json, type-measurements.json,
   logo-measurements.json, measure_variable.py}, reference/fonts/{font-metrics.json, *.ttf},
   reference/playbook/PLAYBOOK.md.
   Synthesised 2026-09-05 from drafts A (type) and B (layout); see docs/DESIGN-SYSTEM.md.
   ========================================================================== */

/* ---- 0 · THE CONSTITUTION ------------------------------------------------ */

/* LAYER ORDER, NOT SOURCE ORDER, GOVERNS THE CASCADE. Declared first, so the next person
   does not assume "append or it will not ship". A later layer beats an earlier one regardless
   of position or specificity: components beats layout beats base beats tokens, and guards beats
   everything - so a guard never needs a specificity game, only the right layer. A component's
   hover is declared exactly once, in its own block, never appended later to win by position.
   ONE INVERSION TO HOLD IN MIND: for !important declarations the layer order is REVERSED
   (the earlier layer wins). The guards layer's reduced-motion clamp relies on !important, so it
   is safe only because NO OTHER LAYER MAY USE !important. The single exception, [hidden] in
   base, is harmless: nothing in guards contests display, and being earlier makes it stronger. */
@layer tokens, base, layout, components, guards;

/* TYPE SYSTEM - two invariants. Breaking either collapses this into a stock template, so they
   are rules, not preferences:

   1. ALL-CAPS AND POSITIVE LETTER-SPACING BELONG TO MONTSERRAT ONLY, and only at
      --text-label (0.786rem = 13.36px, inside the 0.70-0.80rem label band): eyebrows, nav,
      buttons, field labels, attribution, licence strings, counter units. The mark performs
      exactly ONE tracked-caps gesture - the sub-line "ATTORNEY SERVICES", cap height 0.24 of
      the wordmark's, tracked 0.306em as the raster measures in Montserrat (the ladder's own rung is
      0.32em, the nearest 0.04 step) - and echoing it at 13.4px reads as family resemblance.
      NOT "in the logo's own face": THAT CLAIM IS RETIRED AND MUST NOT COME BACK. The 2013
      vector's wordmark is Titillium Text ("TWM" Light, "Legal" Medium) with Arvo for ".com"
      (logo-measurements.json); Montserrat appears nowhere in the mark. What survives the
      face change is the GESTURE - tracked caps at label size under a display line - not a
      shared typeface. FAILURE MODE: the same gesture at headline scale competes with the
      mark for the same job.
   2. NEWSREADER IS NEVER SET IN CAPS, NEVER TRACKED POSITIVE, AND NEVER RENDERS BELOW
      --display-floor (provisional 1.272rem). Display type is sentence case at zero or negative
      tracking, always. Emphasis inside display type is weight + colour (600 + --brand), never
      slant - the em rule itself is component styling, written in the hero/display block at
      step 7; the serif italic is reserved for citation - case names, statutes, Latin - and the
      TRUE italic is loaded (Newsreader ital 400) so font-synthesis: none can never fake one.
      IT IS LOADED AND UNASSIGNED TODAY: the firm's request carries one italic at one weight,
      and .hero-title em / .display em explicitly set font-style: normal, so nothing claims it
      yet - same status Titillium 700 had, and DESIGN-SYSTEM.md s9's rule applies (drop it from
      the request if nothing claims it by the next pass).
      FAILURE MODE: below its floor a serif's stroke modulation stops resolving - all of the
      second font's cost (download, FOUT, a second voice), none of its benefit.

   THE FAILED AND FORBIDDEN ALTERNATIVES, recorded so neither is re-proposed: futura-pt
   all-caps page titles at 42px re-staged the sub-line's gesture and fought the wordmark; and
   a SANS sentence-case headline would re-stage the wordmark itself, which is set sentence-case
   in a humanist sans ("TWM" Titillium Text Light, "Legal" Titillium Text Medium - the 2013
   vector). Hence the display face is the serif, and Montserrat never carries sentence case
   above --text-1. THAT ARGUMENT IS WEAKER THAN IT WAS AND IS KEPT ANYWAY: it used to be
   airtight because the body face WAS the wordmark's face, so a sans headline was literally
   the mark enlarged. Montserrat is a geometric sans and Titillium Text a humanist one, so a
   Montserrat headline is no longer the mark enlarged - only a near neighbour of it. The rule
   stands on the register argument alone (one voice per job), which is the half that never
   depended on the wordmark's face. */

/* FOUR REGISTERS. The type system speaks in four voices and the motion system gets the same
   four, mapped one to one, heaviest slowest and travelling least. Reveal durations are the
   playbook's register values x 1.5 (a measured brand, and since [MOTION v20] a SLOW one - the
   tokens carry the multiplier's whole argument); they are tokens, not rungs of the ladder.

   REGISTER     TYPE                                              ARRIVAL
   LABEL/RULE   Montserrat --text-label caps, --wt-label 600,     CSS-owned (no script reaches a
                --track-N by role; draws its own hairline on      pseudo-element): hairline draws
                ::before                                          --dur-slow --ease-out
   DISPLAY      Newsreader - the ONLY place it appears;           --reveal-display 1.425s --ease-out;
                sentence case, >= --display-floor, tracking <= 0, the heaviest thing travels least
                text-wrap: balance (hero-title, display only)
   PLATE        a card: serif h3 + sans body + tracked-caps       --reveal-plate 1.2s --ease-out,
                attribution                                       ONE object - never sub-staggered;
                                                                  the cascade comes from the grid
   LIST         enumerated procedure (matter stages, engagement   --dur-slow --ease-out; its hairline
                steps) ENTIRELY in Montserrat - its type sits     draws with a fixed --lag-rule
                below the serif floor; hairline on a pseudo       (0.18s) lag, and since [MOTION v20]
                                                                  its numeral follows at twice that
   + one CSS-only opt-out: [data-reveal="fade"] for imagery - opacity only, no translate,
     --dur-media. Three JS registers, four arrival behaviours. Registry order (step 8): the
     DISPLAY row precedes the PLATE row; first match wins.

   DIVISION OF LABOUR (IntersectionObserver + CSS transitions, no GSAP):
     · CSS owns pseudo-element STATES keyed on .is-revealed, and every duration and ease.
     · JS only ever animates TOWARD visible: it adds .is-revealed and nothing else. It never
       writes opacity: 0 or a transform. If JS never hides anything, no JS failure can leave
       anything hidden. The pre-state lives in CSS under html.js [data-reveal]:not(.is-revealed).
     · Stagger is JS's job; a fixed intra-object lag (--lag-rule) is CSS's - choreography within
       one object, not stagger across a group. JS publishes an element's place in the cascade as
       ONE inherited custom property, --reveal-delay, and CSS spends it: on the element as its
       transition-delay, and on the element's decorated pseudo-elements added to their own lag.
       It cannot be an inline transition-delay, because that never reaches a ::before, and every
       hairline in this stylesheet lives on one (guards 7.4 carries the failure it caused). The
       reduced-motion clamp still flattens the cascade without flattening the object, because it
       resets transition-delay on *, *::before and *::after alike.
   TRAILING-SPACE RULE: CSS adds letter-spacing after the LAST character too, so any centred or
   right-abutted tracked label pulls the thing after it back by exactly its own tracking -
   --eyebrow-track-cancel: calc(-1 * var(--eyebrow-track)) - never a retyped number.
   PLACEMENT - two departures from the build brief, stated so they are not read as drift: the
   reduced-motion clamp lives in @layer guards, not base, because it is the one rule that must win by
   layer over every component and it is the only sanctioned !important; the display-face hook list
   lives in base, not layout, because it is a type GRANT (which selectors may carry the serif), not a
   layout primitive. Cascade unchanged either way: [hidden] in base contests nothing in guards. */

/* ---- 1 · TOKENS ---------------------------------------------------------- */
@layer tokens {
  :root {
    /* ---- Colour. The block from reference/derivation/tokens.css, VERBATIM (values and comments).
       OKLCH appears only in comments; values ship as hex. One reading note the verbatim block does not
       carry: the "x1.6 per step" ladder is --bg 0.00380 -> --bg-2 0.00612 (x1.61, 1.043:1) -> --bg-3
       0.00963 (x1.57, 1.063:1), both pairs inside worksheet #10's 1.04-1.06 window. --bg-0 is
       hex-quantised at this darkness: L 0.00225 against a 0.0038 / 1.6 = 0.00237 target (a x1.69 step,
       1.03:1 over --bg, just under the window). At 3% lightness one channel step is ~0.0003 of luminance,
       more than the 0.00012 shortfall: #0A0808 lands at 0.00256 (farther, on the far side) and #0A0707
       at 0.00232 (nearer) breaks the 12-13% saturation bracket (18%). --bg-0 is a sub-ground, never a
       text ground, so the pair test does not bind; it moves only if derive_colors.py is re-run.
       Second reading note, AND [RED v7] INVERTED IT: --brand-ink's "(beat #FFFFFF at 2.75:1)" was white on
       --brand-BRIGHT, and on that stop the near-black still wins - white there is 2.69:1 against its 7.38:1.
       It does NOT win on the deep fill: on --brand-fill white is 6.53:1 where the near-black is 3.04:1.
       So the two fills took OPPOSITE inks (3.04 and 7.38 for the near-black), which was not an inconsistency
       but the point of keeping TWO fills - one dark enough to carry white, one light enough to carry
       near-black, and each taking the ink it can carry.
       [HOVER v22] THAT SPLIT IS OVER AND THE NOTE IS KEPT AS ITS RECORD. The light fill was --brand-bright
       and it is retired with the rest of the pink; the hover fill is now --brand-fill-hover #8F0000, a
       SHADE, so both fills are dark and both carry #FFFFFF - 6.53:1 resting, 9.69:1 hovered. There is one
       ink on red areas again, --brand-ink has no ground left to sit on and is retired with the stop it was
       solved against, and the paragraph above survives only to explain why two inks ever existed.
       [RED v8] THE THIRD SENTENCE OF THAT NOTE IS GONE, and its removal is the whole of this pass. It read
       "--brand itself is no longer a fill at ALL - not because it could not carry ink (near-black on it
       measures 5.66:1) but because the whole point of the split is that a fill may be deep, so a text stop
       has no business being one." The firm has instructed that the text stop BE the fill. --brand and
       --brand-fill now hold the same hex, and they are still two tokens: see the collapse note below.
       THIRD READING NOTE, AND THE ONE THAT DATES THIS BLOCK: --ink-3 and the whole brand ladder are no longer
       the values derive_colors.py printed. TWO GENERATIONS, in order, both dated 2026-09-05:
         [GLOW] superseded, kept for its reasoning. The pointer light (6.26) is a screen-blended layer over
           the whole viewport, so it lightens the TEXT as well as the ground and both sides of every pair
           move; both tokens sat on the 4.5 floor with ~0.01 of headroom, which made the light unaffordable
           at any useful alpha. derive_glow.py's inverse solve fixed the light at the alpha that reads
           (0.055 peak, against the reference site's 0.063) and asked what the palette must become: one step
           of sRGB LIGHTNESS on each, hue and saturation held. Nothing else in the block moved.
         [RED v7] superseded in its values, kept because it is the record of what v8 traded away. "Deeper"
           turned out to be TWO corrections and only one of them is lightness. (1) HUE 5.08 -> 0 and
           SATURATION 83.9% -> 100%. That is what makes the red read as red rather than as coral, it is the
           mark's own hue, and it costs NOTHING in contrast - it is free, and it applies to every stop on the
           ladder at once. THAT CORRECTION STANDS UNTOUCHED IN v8. (2) LIGHTNESS down, bounded, and only
           where a stop is used as a FILL. On a near-black ground the two uses pull opposite ways: a FILL has
           ink ON it, so deeper RAISES the ink ratio (white on #C00000 was 6.48:1 against the old
           near-black-on-#EE4C3D at 5.43:1 - deeper AND more legible); TEXT sits on the ground, so deeper
           LOWERS its ratio (#BF0000 as text is 2.99:1, a fail). Hence v7's split into --brand-fill and
           --brand - and hence v7 kept the TEXT stop light at #FF3D3D, 5.02:1 on --bg-3, deliberately
           refusing the mark's own red for type.
         [RED v8] what ships. The firm was shown those measurements and instructed the deep red for every
           red on the site, TEXT INCLUDED. So the lightness half of v7 is reversed and the whole ladder
           lands on #BF0000: fill, text, selection, marks. The consequences, all of them measured:
             (a) FILLS GET BETTER. White on #BF0000 is 6.53:1 against #C00000's 6.48:1.
             (b) RED TEXT FAILS AA, EVERYWHERE. 2.99:1 on --bg, 2.86:1 on --bg-2, 2.70:1 on --bg-3. Named
                 element by element below. This is the firm's recorded decision of 2026-09-06, not an
                 oversight, and it is asserted as a failure by tools/verify_ratios.py so it cannot be
                 quietly reverted or quietly forgotten.
             (c) THE LIGHT GETS 1.62x BRIGHTER, because the ceiling was set by the lowest-contrast TEXT and
                 that was red at 5.02:1. With red out of the AA budget the binding pair becomes --ink-3 and
                 the peak goes 0.076 -> 0.123. The firm asked for a stronger glow in the same message; it
                 is a consequence of (b) rather than an independent decision.
             (d) FOUR DERIVED VALUES WERE RE-SOLVED, not re-chosen, because each is defined in this file as
                 a function of the light: --line-control 0.36 -> 0.37, .band--ivory's and print's
                 --line-control 0.47 -> 0.49, [aria-busy]'s opacity 0.70 -> 0.73, and --danger-ivory one
                 rung darker. Each carries its own solve at its own line. ------------ */
    /* TWM Legal - colour tokens. EVERY value below is derived, not picked; the arithmetic is beside it.
       Method: reference/playbook/PLAYBOOK.md, worksheet #07-#14 and section 2. The grounds, the inks other
       than --ink-3, the hairlines, --danger and the ivory family were computed by
       reference/derivation/derive_colors.py on 2026-09-05. The accent ladder, --ink-3, the two glow
       triplets and the ivory accents were re-solved the same day by scratchpad/derive_red_v7.py and locked
       by scratchpad/lock_v7.py (results in scratchpad/v7.json). THE 2026-09-06 COLLAPSE ONTO #BF0000 HAS NO
       PRIVATE SCRIPT AT ALL, deliberately: every figure it moves - the new peaks, the four dependent
       re-solves, and the failing red-text pairs - is re-derived on every run by tools/verify_ratios.py,
       which is checked in, where scratchpad/derive_red_v7.py never was. Do not edit a value without
       re-running the thing that owns it.
       Inputs: dark-primary ground; brand hue 0 / saturation 100% from the logo red #BF0000. The CSS keyword
       red the Squarespace site used, #FF0000, is that SAME hue and saturation and was never the drift; the
       drift was the ~5 degrees of hue the derived ramp had picked up, which v7 removed. Every brand stop
       below is an sRGB tint or shade of hue 0 / saturation 100. */
    /* Grounds - a geometric luminance ladder, x1.6 per step from L0 = 0.0038 (worksheet #10). Hue bracketed
       20 / 0 / 340 at 12-13% saturation so every dark neutral is a red-tinted black, never #000. */
    --bg-0: #090707;      /* hsl(20, 12%, 3.06%)  L 0.00225  footer sub-ground */
    --bg:   #0E0C0B;      /* hsl(20, 12%, 4.73%)  L 0.0038  page ground */
    --bg-2: #161111;      /* hsl(0, 12%, 7.53%)  L 0.00612  band + plate (1.043:1 over --bg) */
    --bg-3: #1E171A;      /* hsl(340, 13%, 10.58%)  L 0.00963  raised plate + focused field (1.063:1 over --bg-2) */
    --bg-rgb: 14, 12, 11;   /* scrims and washes: rgba(var(--bg-rgb), a) - never a new grey */

    /* Inks (worksheet #11) - tinted toward the brand hue; each verified on every ground it can land on. */
    --ink:   #F7F3F4;      /* hsl(335, 18%, 96%)  17.73:1 on --bg, 16.0:1 on --bg-3 */
    --ink-2: #B3A8A8;      /* hsl(0, 7%, 68%)  8.44:1 on --bg, 7.62:1 on --bg-3 */
    /* [GLOW] PREVIOUS GENERATION, kept because it records why the value v7 replaced was chosen: --ink-3
       was #8B7E7E (hsl(0, 5.3%, 51.96%); 5.00:1 on --bg, 4.51:1 on --bg-3). Same hue, same
       5% saturation, +0.98% lightness - one channel step, (139, 126, 126) -> (141, 129, 129). WITH the
       pointer light at its 0.055 peak this pair measures 4.55:1 on --bg-3; at the old value, 4.39:1.
       BOTH LIT FIGURES ON THAT LINE ARE HISTORICAL: they were measured under the v6 light (lead #FF6E5C at
       a 0.055 peak, trail #C91810 at 0.026), and neither the lobes nor the peaks still ship.
       [RED v7] --ink-3 was #8D8181 (hsl(0, 5%, 52.94%); 5.19:1 on --bg, 4.69:1 on --bg-3). Same hue,
       saturation 5.0% -> 4.8%, +2.16% lightness - (141, 129, 129) -> (146, 135, 135). THE CAUSE IS THE
       LIGHT, NOT THE RED: --ink-3 is not a brand stop and did not need the hue correction, but it shares
       the ceiling the light is solved against, so raising the peak 0.055 -> 0.076 moved it too. Two channel
       steps this time, not one, because the light rose 1.38x rather than holding.
       [RED v8] --ink-3 DOES NOT MOVE, AND THAT IS NOW ITS WHOLE JOB. With red text out of the AA budget
       this token IS the ceiling the light is solved against: the binding pair for the entire palette is
       --ink-3 on --bg-3, and 0.123 is the largest lead peak at which that pair still holds - 4.5041:1 with
       both lobes converged. Under v7's dimmer light the same pair sat at 4.7591:1, i.e. the value was
       chosen with a quarter of a point of headroom over the floor; v8 spends every bit of it. Raising the
       light again from here means moving THIS token, exactly as v7 had to move it. */
    --ink-3: #928787;      /* hsl(0, 5%, 55.10%)  5.61:1 on --bg, 5.06:1 on --bg-3 - recede by size, never by dimming further */
    --ink-nav: #CAC4C4;   /* hsl(0, 5%, 78%)  THE HEADER NAV'S RESTING LABEL, and its lightness is solved
                             rather than chosen. The nav sits on .site-header::before, a scrim that thins
                             to rgba(--bg, 0.72) at its 53% stop, and the hero beneath reaches a full 255
                             - so the binding ground is 0.72 over a WHITE pixel, which composites to
                             #51504F, where the obvious muted stop --ink-2 lands under AA for 13.36px
                             type - which is why it could not be used. 78% lightness is the lowest whole
                             step that clears the floor on that worst case; the three grounds it faces
                             are claimed on the declaration itself rather than restated here. Against
                             --ink it reads plainly
                             fainter, which is the whole point - the firm asked for a faded label that
                             brightens on the current page and under the pointer. */
    --ink-rgb: 247, 243, 244;

    /* Hairlines (worksheet #12) - alpha of --ink, ONE OCTAVE APART, split by AFFORDANCE not prominence.
       COMPOSED from --ink-rgb, not retyped: --ink appears at four alphas across this file (0.09, 0.18,
       0.36 and the washes in 6.1/6.9), which is the playbook's own "three or more alphas earns a triplet"
       case, and hand-typed channels would keep pointing at the old colour if --ink is ever re-derived.
       The print block (6.25) and the ivory band (step 5) compose the same rungs the same way. This is a
       FORM change, not a value change - the computed colours are byte-identical to the two literals that
       stood here and to reference/derivation/tokens.css, which still carries the literal form.
       THERE ARE THREE RUNGS SINCE THE v7 REPAIR PASS, AND THE THIRD IS DERIVED RATHER THAN CHOSEN.
       --line and --line-strong are a TONAL pair and neither carries an accessibility obligation: a seam,
       and an emphasis edge that always has a louder indicator beside it (a plate's :focus-within warms
       this edge while the focused LINK inside still draws base's 2px --ink ring, and .svc-card's
       hover tint composites to #682020, the same 1.62 weight warmed). --line-control is the rung for an
       edge that is the ONLY thing identifying a control - a text field's boundary, a ghost button's whole
       affordance, an underline that is a link's only non-colour cue - where WCAG 1.4.11's non-text floor
       binds for real, and where 0.18 measured 1.62 against it. It is the SMALLEST alpha clearing that
       floor on every ground in the palette in BOTH states, lit and unlit - and "in the palette" is the exact
       scope, not a loose one. A CONTROL OVER A PHOTOGRAPH IS NOT COVERED BY IT: .btn--ghost's border in a hero
       or in the closing band sits on a scrim ground, where the pair measures 3.04 unlit / 2.74 lit at the
       hero's action row and 3.23 / 2.99 in the band - under 1.4.11 in the reading state, on the edge that is
       that button's whole affordance. It is recorded here rather than repaired because the repair is a token
       change with site-wide reach (every hairline on every ground moves with this alpha) and the pairs it
       would fix are the same pairs the scrim re-solve of [SCRIM v12] did not have to touch; it is the next
       thing to take to the palette, and it should be taken there deliberately rather than inside a media pass.
       A HAIRLINE composite is quantised
       to 8 bits before measuring, because that is what a browser paints: --line* is an ink at an alpha
       that resolves to ONE painted colour, and the comments name that colour as a hex (#646161 below).
       THE RULE IS SCOPED TO PAINTED COLOURS AND SAYING SO IS NOT PEDANTRY - a review has already read it
       as universal and reported the light's ceiling pair as failing on that reading. It is not universal.
       A scrim veil, an element opacity and the pointer light's screen blend are COMPOSITOR operations
       that never become an authored colour, so they are measured raw, exactly as tools/verify_ratios.py
       measures them (its LINE() quantises, its LIT() and BUSY() do not, and BUSY() says why in one line).
       The ceiling below is therefore 4.5041:1 as written; quantised it would read
       4.48:1, a number about a paint step this pair never takes.
       [RED v8] THAT ALPHA IS 0.37 AND IT USED TO BE 0.36, and the difference costs the ladder a
       coincidence worth naming so nobody restores it out of tidiness: 0.36 was EXACTLY one octave above
       --line-strong's 0.18, and this file used to argue the rung partly from that. It cannot any more.
       The 1.62x brighter light drags a hairline's pair down on the ground it is faintest against, and at
       0.36 the --bg-3 case fell to 2.9514:1 - under the floor, in the reading state, on edges that are
       some controls' only boundary. 0.37 is the re-solve: one hundredth above the octave rather than on
       it. THE ALPHA IS THE DERIVED THING AND THE OCTAVE WAS THE COINCIDENCE, so the alpha wins.
       WHAT THIS IS NOT: a brightening of the site's hairlines. Neither existing rung moved, and nothing
       drawn at 0.09 or 0.18 for TONE is redrawn at 0.37 - the new rung has SIX declarations, each with
       its reason in its own block: .prose and .check-list links (6.6/6.7), 6.16's contact links, 6.17's
       field border, 6.18's ghost button, and 6.21's one rule covering the four footer link families.
       .svc-card and .package keep --line-strong on :focus-within, deliberately: the ring on the focused
       child is the indicator there, and this edge is the quieter parity response the block asks for. */
    --line:         rgba(var(--ink-rgb), 0.09);   /* seams: composites to #232120, 1.22:1 - a tonal seam, not a state */
    --line-strong:  rgba(var(--ink-rgb), 0.18);   /* emphasis edge, never a control's only boundary: #383635, 1.62:1 */
    --line-control: rgba(var(--ink-rgb), 0.37);   /* a control's ONLY boundary: #646161 on --bg. The binding case is --bg-3 under the converged light at 3.03:1; then --bg-2 lit 3.13:1, --bg-0 unlit 3.13:1, --bg lit 3.16:1 */

    /* Brand (worksheet #07-#08) - FOUR stops, split by JOB, on ONE sRGB hue and ONE sRGB saturation, and
       since [RED v8] on ONE LIGHTNESS as well for three of the four.
       [HOVER v22] THE FOURTH STOP IS A DIFFERENT STOP, and the swap gives the ladder something [RED v7]
       recorded giving up. It was --brand-bright #FF7070, a TINT of the mark; it is now
       --brand-fill-hover #8F0000, a SHADE of it. A tint of a hue-0 red drifts in OKLCH - #FF7070 measures
       hue 22.50 there against #BF0000's 29.2339 - where a shade does not, so all four stops now sit on
       OKLCH hue 29.2339 and the ladder holds ONE hue in BOTH spaces for the first time: sRGB by
       construction, OKLCH by consequence. Why the tint left at all is THE RETIREMENT, below the stops.
       [RED v7] THE LADDER IS ANCHORED IN sRGB, NOT IN OKLCH, AND THAT IS A CHANGE OF METHOD, stated rather
       than smuggled. Every stop is hue 0 / saturation 100 - the mark's own coordinates, and the thing the
       firm was actually asking for - so the OKLCH figures below are now a CONSEQUENCE, not the input: hue
       29.23 at every stop since [HOVER v22], and 22.50 at the retired --brand-bright. The old ramp held one
       OKLCH hue (29.2) and one OKLCH chroma, and holding those is precisely what put --brand five degrees
       off pure red in sRGB - which is what read as coral.
       [RED v8] THE LADDER COLLAPSES. The firm's instruction of 2026-09-06 was that every red on the site
       be the red the text highlight paints, which is --brand-deep, which is the 2020 wordmark's #BF0000.
       So --brand-fill, --brand and --brand-deep now hold ONE value and only the fourth stop stands apart.
       WHERE EACH BOUND COMES FROM, so no stop can be nudged without breaking something nameable:
         --brand-fill   the mark's own red, #BF0000, on the firm's instruction. Its own edge no longer
                        clears WCAG 1.4.11's 3:1 against --bg - it measures 2.988:1, missing by 0.012 -
                        and the cost of that miss is a fifth of what the v7 comment implied it was, which
                        is why the instruction was cheap to honour on the FILL. See the stop's own line.
         --brand        THE SAME VALUE, on the firm's instruction, over a measured objection. Red TEXT is
                        now 2.99:1 / 2.86:1 / 2.70:1 on the three grounds and meets AA on none of them.
                        v7 had held this stop at #FF3D3D (5.02:1 on --bg-3) for exactly that reason.
                        See THE AA FAILURE, RECORDED below - it is a decision, not a defect, and the only
                        thing that would make it a defect is a comment that hid it.
         --brand-deep   #BF0000, unchanged since v7 and now the value the other two were collapsed ONTO.
         --brand-fill-hover  #8F0000, and DERIVED from --brand-fill rather than picked: the resting plate's
                        one live channel at three quarters, 0.75 x 191 = 143.25 -> 143 = 0x8F. It is the
                        only stop with no legibility of its own and the only one that is never at rest.
       WHY --brand AND --brand-fill REMAIN TWO TOKENS WHILE HOLDING ONE VALUE. They were split because
       they do different jobs - one has ink on it, one sits on the ground - and every consumer in this file
       is already labelled with which job it is doing. Keeping them split is precisely what makes restoring
       AA on text a ONE-LINE change: put --brand back to #FF3D3D and every red AREA stays exactly as the
       firm asked for it. Merging them would throw that away to save a line.
       [GLOW] PREVIOUS GENERATION, kept for the lesson it records: --brand had been the smallest lightness
       clearing 4.6:1 on --bg-3, the lightest ground a brand-coloured link can sit on, and was raised one
       sRGB lightness step above that floor because the SMALLEST passing value is by construction the one
       with no headroom to spend on a light laid over it. That move was lightness only - hue 4.92 -> 5.08,
       saturation 83.6% -> 83.9%, held to within hex quantisation - so the OKLCH chroma drifted
       0.2070 -> 0.2009 as a consequence rather than as a decision. v7 keeps that lesson and discards the
       method: hue 5.08 -> 0.00 and saturation 83.9% -> 100.0% are DELIBERATE this time, and the chroma
       drifts 0.2009 -> 0.2292 - it RISES, still as a consequence. The one hue no longer holds in OKLCH and
       is no longer asked to: 29.25 -> 26.25 there, 0 -> 0 in sRGB, which is the space the mark is
       specified in. (The retired --brand-bright's chroma was 0.1751, GAMUT-CLIPPED, the same kind of
       rounding; --brand-fill-hover's 0.1675 is not clipped at all, because a shade of an in-gamut colour
       cannot leave the gamut - one more thing the tint-to-shade swap tidies rather than trades.)
       [RED v8] The sRGB hue and saturation that v7 fixed are exactly what v8 keeps; only lightness moved,
       and it moved to the mark's, so the OKLCH figures walk back to #BF0000's own 0.2073 at 29.2. */
    --brand-fill:   #BF0000;   /* oklch(50.5% 0.2073 29.2)  hsl(0, 100%, 37.45%) - every red AREA: .btn, drawn
                                  rules, filled marks, accent borders. THE 2020 WORDMARK'S OWN RED, on the firm's
                                  instruction of 2026-09-06. Ink on it is #FFFFFF at 6.53:1, BETTER than the
                                  6.48:1 #C00000 gave and far better than the 5.43:1 that shipped before v7
                                  (the retired --brand-ink here would be 3.04:1 and fail). NEVER TEXT.
                                  [RED v8] WHY THIS COST ALMOST NOTHING, and the finding is recorded here so it
                                  is not rediscovered from scratch. v7 chose #C00000 over the mark's #BF0000 for
                                  ONE reason - to clear WCAG 1.4.11's 3:1 floor on the control's own edge - and
                                  that margin was mostly illusory. Measured on the file v7 shipped, under v7's
                                  own converged light, #C00000's edge was
                                  3.01:1 on --bg unlit but 2.89:1 lit, 2.89:1 on --bg-2 unlit and 2.73:1 lit,
                                  2.72:1 on --bg-3 unlit and 2.53:1 lit
                                  - so the floor was ALREADY unmet on every tinted ground and in the reading
                                  state on the plain one, and .btn demonstrably ships on --bg-2 (four pages;
                                  nine buttons inside .package on /flat-fee-services alone). #BF0000 measures
                                  2.988:1 on --bg unlit: the collapse forfeits 0.024 on ONE ground in ONE
                                  state and nothing anywhere else.
                                  A LIGHTER RIM TOKEN WAS BUILT FOR THIS AT v7 AND DISCARDED; do not reinvent
                                  it. At 1px a #D90000 rim measures 1.22 against the #BF0000 plate it edges -
                                  invisible - which is a number that passes an audit while helping nobody.
                                  WHAT ACTUALLY IDENTIFIES THE CONTROL is its own label:
                                  #FFFFFF tracked caps at 6.53:1 on the plate,
                                  which is what 1.4.11 allows where a visible label identifies the component.
                                  That is the honest account of where the affordance comes from; the edge
                                  never was. A lighter FILL is still the one lever nobody may pull silently. */
    --brand-fill-hover: #8F0000;  /* oklch(40.8% 0.1675 29.2)  hsl(0, 100%, 28.04%) - the fill a red AREA takes on
                                  HOVER, and nothing else: .btn's plate and .btn's edge. NEVER text, never a rule,
                                  never anything at rest. The name carries "fill" for a reason - this is the one
                                  stop in the ladder with no legibility of its own.
                                  DERIVED, NOT PICKED. It is --brand-fill's one live channel at three quarters:
                                  0.75 x 191 = 143.25 -> 143 = 0x8F. A hue-0 / saturation-100 red HAS exactly one
                                  live channel, so that single multiply is the whole of the shade and both
                                  coordinates come through it untouched - which is why this stop lands on the
                                  ladder's own OKLCH hue (29.2339, #BF0000's own figure) rather than drifting off
                                  it the way the tint it replaced did.
                                  WHAT IT BUYS IS THE WHITE LABEL, and the two figures are the same figure:
                                  #FFFFFF on the plate goes 6.53:1 at rest -> 9.69:1 hovered, a 1.48x lift, and
                                  the two plates measure 1.48:1 against EACH OTHER.
                                  That identity is not a coincidence - darkening a ground scales a white ink's
                                  ratio by exactly the ground's own step - and it is the reason the gesture reads:
                                  nothing else on this control changes colour, so the plate carries the whole
                                  hover and the label's legibility rises with it instead of crossing anything.
                                  WHAT IT COSTS IS THE PLATE'S OWN EDGE, recorded in half (B) of THE AA FAILURE
                                  below rather than left to be discovered: 2.99 / 2.86 / 2.70:1 at rest becomes
                                  2.01 / 1.93 / 1.82:1 on --bg / --bg-2 / --bg-3.
                                  That edge was ALREADY under 1.4.11's 3:1 as a recorded decision, so this
                                  deepens an accepted shortfall rather than opening a new one, and the defence is
                                  --brand-fill's own: what identifies a red plate here is its white LABEL, not
                                  its rim, and the label gets BETTER on hover. A hover is also transient and is
                                  reached only after the control has already been found. DO NOT INVENT A RIM
                                  TOKEN FOR IT - v7 built one and discarded it, six lines up. */
    --brand:        #BF0000;   /* THE SAME VALUE AS --brand-fill, and still its own token - red TEXT on dark:
                                  links, eyebrows, [aria-current], list numerals, arrows.
                                  2.99:1 on --bg, 2.86:1 on --bg-2, 2.70:1 on --bg-3. THOSE ALL FAIL WCAG AA,
                                  and under the light at its shipped peaks they fall further: 2.73:1 on --bg and
                                  2.36:1 on --bg-3. THE FIRM'S RECORDED DECISION OF 2026-09-06, taken
                                  after being shown these measurements. It is not a defect and it is not to be
                                  silently repaired; tools/verify_ratios.py asserts these pairs are BELOW AA so
                                  that a quiet "fix" fails the build. See THE AA FAILURE, RECORDED below.
                                  [RED v7] was #FF3D3D (5.56:1 / 5.33:1 / 5.02:1), chosen as the deepest hue-0
                                  red that still cleared AA under v7's light. THE ONE-LINE REVERT IS THIS LINE:
                                  --brand: #FF3D3D restores AA on every element in the list below and changes
                                  no red AREA anywhere, because the fill is a separate token. */
    --brand-deep:   #BF0000;   /* oklch(50.5% 0.2073 29.2) - and 0.2073 is #BF0000's OWN chroma, the nominal the
                                  old ramp was built to, now shipping as itself. 2.99:1 on --bg. Selection fill,
                                  decorative marks and the trail lobe. [RED v8] This stop did not move; the other
                                  two moved onto it, because the firm named the highlight as the reference. */
    /* ---- THE RETIREMENT: --brand-bright #FF7070 and --brand-ink #110707 are GONE, [HOVER v22] ---------
       THE FIRM'S INSTRUCTION OF 2026-09-06, verbatim: "I do not like the pink color that is used when
       certain links and buttons are hovered over with mouse. They should all be the dark red color which
       is the signature color for the brand. fix throughout site." #FF7070 was the pink, and it held THREE
       JOBS on one token - every hover colour, every focus ring, and the pointer light's lead lobe. The
       firm named one of the three, so the three had to be separated before anything could be answered.
       THE LITERAL INSTRUCTION IS IMPOSSIBLE AND THE FIRM WAS TOLD SO BEFORE CHOOSING. #BF0000 is ALREADY
       the resting colour of every red link and the resting fill of every .btn, so "the hover is #BF0000"
       is "the hover looks exactly like rest" - no feedback at all, on every interactive red on the site.
       Shown that, and shown the measurements below, the firm chose this scheme the same day:
         TEXT HOVERS  -> var(--ink). A red link at 2.99:1 warms to 17.73:1 on --bg
           (17.00:1 on --bg-2, 16.00:1 on --bg-3).
           It is the largest lift the palette can give, and it introduces no colour: --ink is what the copy
           around the link is already set in, so a warming link resolves INTO its paragraph rather than
           away from it. Nav, mobile menu, contact list, .link-arrow, .crumbs, the four footer families
           and the linked stat all take it, so the site keeps ONE hover colour rather than two.
         BUTTON FILL  -> var(--brand-fill-hover), the stop above. The plate DEEPENS toward a shade of the
           signature red instead of inverting to a tint of it, which is as near the literal instruction as
           a hover can be while still being a hover.
         FOCUS RINGS  -> var(--ink), everywhere, AND THIS ONE IS NOT THE FIRM'S AESTHETIC CHOICE TO MAKE.
           A #BF0000 ring measures 2.99:1 / 2.86:1 / 2.70:1 on --bg / --bg-2 / --bg-3 - all three under
           WCAG 1.4.11's 3:1 - and on a #BF0000 button it is 1.00:1, invisible by definition rather than
           by measurement. --ink is 17.73:1. Keyboard users navigate by this ring and by nothing else, so
           it is the one value in this scheme that is a correctness constraint rather than a preference:
           it may not be rebound to a brand colour without taking the site's only keyboard affordance away.
         THE LIGHT    -> UNCHANGED, and deliberately so. The lead lobe keeps #FF7070 as a bare triplet
           (--glow-lead, in worksheet #14/#15 below) and not one stop of 6.26 moves. It is not a hover
           colour and it is not read as pink: at the shipped 0.123 alpha over the near-black ground the
           lobe core composites to #351918, a dark warm red. Dimming it would also undo "make the glow
           stronger", which is the firm's OTHER instruction of the same day.
       WHY THE TOKENS GO RATHER THAN STAYING ON AS DOCUMENTATION. With the hovers and the rings moved off
       it, NO RULE IN THIS FILE READS --brand-bright - its only surviving trace is #FF7070 as --glow-lead's
       triplet, and a triplet cannot be composed from a hex token in CSS anyway, so "the documented source
       of the lead lobe" could only ever have meant a comment. This is that comment. Left in the ladder the
       token would be worse than merely dead: a stop named for hover fills and focus rings, sitting one
       line under the reds, is an invitation to paint the pink straight back. --brand-ink goes with it
       because it was defined as "text ON --brand-bright" and had exactly two consumers, both .btn rules
       that now carry #FFFFFF at rest and hovered alike.
       THE RETIRED VALUES, so they stay measurable rather than becoming folklore: --brand-bright was
       #FF7070 = oklch(71.9% 0.1751 22.5) = hsl(0, 100%, 72.0%), 7.25:1 on --bg and 6.54:1 on --bg-3;
       --brand-ink was #110707, 7.38:1 on --brand-bright where #FFFFFF managed 2.69:1.
       tools/verify_ratios.py keeps both constants and still re-derives every one of those figures.
       .band--ivory (step 5) and @media print (6.25) each lost the two rebindings that went with them. ---- */
    --brand-rgb: 191, 0, 0;    /* = --brand, the TEXT stop, as a triplet - and since [RED v8] also = --brand-fill's
                                  and --brand-deep's channels, so the "an AREA that needs a triplet would need a
                                  --brand-fill-rgb" caveat is dormant rather than gone: it wakes the moment the
                                  one-line revert above is taken. Its one alpha use is 6.9's card edge. */

    /* ---- THE AA FAILURE, RECORDED --------------------------------------------------------------------
       THIS LIST HAS TWO HALVES AND THEY ARE ONE DECISION: red TEXT that misses WCAG 2.1's 1.4.3, and red
       NON-TEXT that misses 1.4.11. It used to have only the first half, and the omission was not harmless -
       two comments elsewhere in this file pointed a reader at "the failure list" for a non-text failure
       that was not in it, and the summary sentence at the foot of the block claimed every focus state
       still passed. Both were repaired on 2026-09-06 by writing half (B), not by softening half (A).
       The colour is the firm's instruction of 2026-09-06 ("All red should be the color of the text
       highlight color"), given after the measurements were put in front of them. It is written out in
       full because a comment that let the reader assume otherwise would be the defect - the colour itself
       is a decision the firm is entitled to make.
       (A) TEXT. AA needs 4.5:1 below 24px (or below 18.66px at 600+), and 3:1 at or above it. Ratios are
       unlit; the pointer light takes each one lower still (2.73:1 on --bg, 2.56:1 on --bg-2, 2.36:1 on --bg-3).
         2.99:1 on --bg / 2.86:1 on --bg-2 / 2.70:1 on --bg-3, at these sizes:
           a, .prose a, .check-list a, .contact-list a, .form-sent a      17px 400   needs 4.5
           .form-note a                                                  13.36px 400  needs 4.5
           .eyebrow, .link-arrow, .num-list li::before,
             .mobile-menu-contact a, .nav-links [aria-current="page"]     13.36px 600  needs 4.5
           .display em                                                   34.99px 600  needs 3, misses by 0.01
           .hero-title em                                                44.51px 600  needs 3, misses by 0.01
           .mobile-menu [aria-current="page"]                            27.51px 400  needs 3, misses by 0.01
           .stat--linked:focus-within .stat-num                          27.51px 400  needs 3, misses by 0.01
         AND ONE PAIR THAT IS WORSE THAN THE GROUND FIGURES SUGGEST: the header's current-page label sits
         over the hero photograph behind a scrim, where against a pure-white pixel it measures 1.23:1 at the
         scrim's 0.72 worst-case stop and 2.64:1 on the 0.94 ground the no-JS gate pins (6.1 carries the
         arithmetic). No veil alpha rescues it - see the hero scrim note in 6.3.
       (B) NON-TEXT. 1.4.11 asks 3:1 of whatever identifies a component or one of its states, and FOUR red
       things answer to it since [HOVER v22]. Three are covered by something louder. The fourth was not,
       and was the only accessibility cost of the colour pass that landed on a KEYBOARD user rather than
       on a reader - it has since been repaired, and the repair is written at the foot of its own row.
           THE PLATE RIM of .btn, .skip-link and .form-sent is 2.99:1 / 2.86:1 / 2.70:1 on the three grounds.
             COVERED: what identifies a red plate is the white label on it at 6.53:1, which is exactly what
             1.4.11 allows where a visible label identifies the component. The rim never was it, and the
             stop's own line above carries the arithmetic of what the collapse cost here (0.024, once).
           THE HOVERED PLATE RIM, [HOVER v22], IS THE SAME EDGE DEEPER, AND IT IS RECORDED HERE RATHER THAN
             DISCOVERED. --brand-fill-hover darkens the plate toward the page, so the rim it draws falls to
             2.01:1 / 1.93:1 / 1.82:1 on --bg / --bg-2 / --bg-3.
             COVERED BY THE SAME ARGUMENT, AND BY ONE MORE THAT ONLY A HOVER GETS. The white label goes the
             other way - 6.53:1 -> 9.69:1 - so the thing 1.4.11 actually accepts as the identifier gets
             BETTER exactly when the rim gets worse; and a hover state is transient and is reached only
             after the control has already been found, which a resting state cannot say. This deepens an
             accepted shortfall on the row above rather than opening a new one. It is NOT a licence to
             deepen the resting fill, and it is not an argument for a rim token: see --brand-fill.
           THE DECORATIVE MARKS - .check-list's diamond (6.7), .quote-card's leading edge (6.14), .form-sent's
             leading edge (6.17) - carry no information the words beside them do not, so no floor binds.
           THE FIELD'S SUBSTITUTED FOCUS BORDER WAS THE ONE WITH NOTHING BEHIND IT, and it was a REGRESSION
             rather than an inheritance: 6.17 sets outline: none on inputs, textareas and selects, so its
             1px edge IS the focus indicator, and at --brand it measured
             2.70:1 against the --bg-3 it encloses and 2.86:1 against the --bg-2 outside it
             - under the floor on both surfaces it separates, where v7's lighter text stop cleared both at
             5.02:1 on --bg-3. Every field in the contact form lost a compliant focus cue on 2026-09-06.
             THAT ONE-LINE REPAIR WAS TAKEN THE SAME DAY and has been taken a second time since. On
             2026-09-06 6.17 moved its border-color to --brand-bright, 6.54:1 inside and 7.25:1 outside;
             [HOVER v22] retired that stop with the rest of the pink, so the border now takes
             var(--ink) - 16.00:1 against the --bg-3 it encloses and 17.00:1 against the --bg-2 outside it.
             The requirement did not change and neither did the reason: this edge is a SUBSTITUTED focus
             indicator, so 1.4.11 binds on it as a function, and the colour that satisfies a function is
             whichever one clears it. The checkbox is excluded by type there and keeps base's ring already.
             THE SENTENCE THIS ROW USED TO END ON IS DEAD AND IS NOT WORTH SALVAGING: "it is the ONLY red
             the collapse did not take". There is no red in a focus indicator anywhere on the site now.
             Every red a visitor READS as brand is #BF0000; every indicator a visitor NAVIGATES by is --ink.
       WHAT IS NOT ON THIS LIST, because it still passes: every red AREA's INK - white on the resting fill
       is 6.53:1 and on the hovered fill 9.69:1 - then every state drawn in --ink since [HOVER v22]: base's
       focus ring, every text hover, the ghost button's hover and focus edge and 6.17's substituted field
       border, at 17.73:1 on --bg, 17.00:1 on --bg-2 and 16.00:1 on --bg-3.
       Then --ink/--ink-2/--ink-3 body copy, --danger, and both light-band accents.
       READ THAT SECOND CLAUSE AS WRITTEN: it is now the --ink set, and it IS the focus set as well as the
       hover set, which is the first time this sentence has been able to say so. It said "every hover and
       focus state" once before and was false, because the substituted border in (B) was then a focus state
       drawn in --brand; the repair above is what makes the wider claim true rather than convenient.
       THE ONE-LINE CHANGE THAT RESTORES COMPLIANCE: --brand: #FF3D3D. That is v7's text stop; it clears AA
       on every row above and leaves every red AREA at the mark's #BF0000 exactly as instructed. It would
       also drop the pointer light's ceiling back to a 0.076 peak, because --brand would rejoin the pairs
       the light is solved against. ---- */

    /* Semantic state (declared, with its ratio - the playbook's source file never did). */
    --danger: #FFA033;   /* hsl(32, 100%, 60%)  9.6:1 on --bg; warm so it cannot be mistaken for the brand red; errors also carry text */

    /* Light band family (worksheet #13) - warm ivory against the cool-red grounds. */
    --ivory:           #F3F1ED;   /* hsl(40, 20%, 94%)  L 0.8808 */
    --ivory-ink:       #150909;   /* 17.32:1 on ivory */
    /* [RED v7] BOTH IVORY ACCENTS WERE RE-SOLVED, for two independent reasons.
       (1) HUE. --brand-ivory was #D4281D = hsl(3.61, 75.93%, 47.25%) - the same few degrees of hue offset
           and the same sub-100 saturation the whole v7 correction removes on the dark ground. An in-band
           accent that is not the mark's hue re-introduces the coral the firm reported, in the one place a
           printed page would show it most plainly.
       (2) HEADROOM. The light got 1.38x stronger, and rule (b) below says a light band LOSES ratio under
           it. The old pair was already under the file's own >= 4.70 bar and fell further. THE BAR ITSELF
           WAS RE-MEASURED and it is hue-dependent, which a single figure hides: the unlit ratio that lands
           on exactly 4.50 lit differs at hue 0 and at --danger's hue 32.
       [RED v8] BOTH ACCENTS MOVE AGAIN, and for the two reasons that generalise:
       (1) THE COLLAPSE REACHES THE BAND. --brand-ivory was #C00000 because that was --brand-fill; the fill
           is now the mark's #BF0000, so the in-band red follows it. On IVORY a deeper red is strictly
           better - dark ink on a light ground - so this is the one place the firm's instruction costs
           nothing at all: 5.74:1 -> 5.79:1 unlit, 5.28:1 lit.
       (2) THE LIGHT GOT 1.62x STRONGER AGAIN AND THE BAR ROSE WITH IT. Re-solved at the shipped peaks, the
           unlit ratio that lands on exactly 4.50 lit is
           4.79:1 at hue 0 and 5.30:1 at --danger's hue 32,
           so the stated bar is therefore raised to >= 5.30:1 on ivory before the light - the worse of the
           two, so one number still covers both hues. It has now been raised three times, 4.50 -> 4.70 ->
           5.00 -> 5.30, once per brightening, which is the clearest evidence that this bar is a FUNCTION
           of the light and not a preference. The retired v6 pair falls to 4.15 and 3.93 lit at these
           peaks, and even the v7 pair does not survive: #995200 measured 5.23:1 unlit and lands at 4.46:1
           lit, UNDER AA on a light band nobody decided to give up.
           So --danger-ivory steps one rung darker to #944F00 = hsl(32, 100%, 29%): 5.51:1 unlit, 4.67:1
           lit. HUE 32 IS STILL HELD ON PURPOSE: --danger exists so an error cannot be read as the brand
           red, and taking it to hue 0 to match would destroy the token. Only its lightness has ever moved.
           All three in-band values clear the new bar; --ink-muted-ivory clears it by the least (5.39:1
           unlit, 4.52:1 lit) and is the band's binding pair now, the way --ink-3 is the dark ground's. */
    --brand-ivory:     #BF0000;   /* hsl(0, 100%, 37.45%)  5.79:1 on ivory - the substitute for --brand in-band, and since [HOVER v22] for --brand alone: --brand-bright was the other consumer of this line and is retired. = --brand-fill, and since [RED v8] = every other red on the site */
    --ink-muted-ivory: #6D5F5F;   /* 5.39:1 on ivory - substitute for --ink-2 and --ink-3 in-band; 4.52:1 under the light, the tightest pair in the band */
    --danger-ivory:    #944F00;   /* hsl(32, 100%, 29%)  5.51:1 on ivory */
    /* ---- end of the block derive_colors.py owns; the [GLOW] and [RED v7] lines above are the exceptions. ---- */

    /* ---- WORKSHEET #14/#15: THE EXCEPTION COLOUR IS LIGHT, NOT PIGMENT ---------------------------
       The earlier answer here was "NONE - no cursor glow ships": red + gold screen-blended goes orange,
       and a metallic suits neither the register nor the accent. That refused the reference site's SECOND
       colour, and it was right to. What ships instead is the reference's STRUCTURE in this palette's own
       two brand stops - a bright lead lobe and a deep trail lobe at different lag rates - so the pair
       still changes shape with velocity without introducing a colour the site does not otherwise own.
       THESE TWO TOKENS EXIST ONLY AS LIGHT. Never a fill, never a rule, never a type colour; nothing but
       6.26 may name them. They are bare RGB triplets because each is composed at four alphas, which is the
       playbook's "three or more alphas earns a triplet" case - and because a triplet cannot be pasted into
       a `background` by mistake, where a hex literal invites exactly that.
       THE ALPHAS ARE DERIVED, NOT PICKED - reference/derivation/derive_glow.py for the [GLOW] generation,
       scratchpad/{derive_red_v7.py, lock_v7.py} for v7, tools/verify_ratios.py for the one that ships;
       worksheet #15. Screen with a source alpha resolves per channel to
           out = b + a * c * (1 - b)
       and the layer lies over the TEXT as well as the ground, so both sides of every pair move together.
       The ground is darker, so it gains proportionally more, and every ratio FALLS. THE CEILING IS
       THEREFORE SET BY THE LOWEST-CONTRAST TEXT THAT IS HELD TO AA, and raising it means moving that
       TOKEN - or removing a token from the budget, which is what happened here. [RED v7] moved a token,
       lifting --brand to #FF3D3D (5.02:1 on --bg-3)
       and --ink-3 to #928787 (5.06:1); that raised the ceiling from 0.0554 to 0.0763, and 0.076 shipped.
       [RED v8] REMOVED A TOKEN FROM THE BUDGET INSTEAD, and this is the honest description of why the
       light got so much brighter so cheaply: red text is no longer held to AA (the firm's decision of
       2026-09-06, recorded in full with --brand above), so --brand is no longer one of the pairs the
       ceiling is solved against. The binding pair becomes --ink-3 on --bg-3, and the converged ceiling
       goes 0.0763 -> 0.1237. It is not that the light became safer; it is that one of the things it was
       being kept safe for was taken out of the calculation, deliberately and on the record.
         shipped peak   0.123 on the lead (v7 shipped 0.076, [GLOW] 0.055; the reference site ships 0.063)
                        - measured against the generation before v7, the light rose 2.24x, and it is
                        1.62x what v7 allowed. That is the firm's "make the glow stronger", paid for by
                        (b) above rather than by a taste argument.
         failure alpha  0.1474 - the lead alpha at which --ink-3 on --bg-3 is dragged to exactly 4.5:1.
                        [RED v8] INVERTED v7's INVERSION: --brand was the first pair to fail (0.0920) and
                        is now not a pair at all, so --ink-3 - the tighter one two generations ago, the
                        looser one at v7 - is the binding pair again, and by default rather than by
                        measurement. The next-tightest thing still in the budget is --ink-2 on --bg-3,
                        which does not fail until 0.3733.
                        THOSE ARE LEAD-LOBE-ONLY FIGURES, stated because reading them under the other
                        convention is the one mistake this paragraph invites. It is the convention the
                        playbook's own "1/5-1/6 of failure alpha" uses and the one verify_ratios.py
                        re-derives: a failure alpha is a property of the LIGHT being solved for, so the
                        trail - a lobe already chosen - is held out of it. Converged, the same two pairs
                        fail at 0.1237 and 0.3242, and that 0.1237 is exactly the ceiling the shipped
                        0.123 is rounded down from four lines below. Both numbers are real and the file
                        quotes both; what it must never do is compare one with the other.
       The playbook ships at 1/5-1/6 of the failure alpha. THIS DOES NOT - 0.123 is 0.83 of failure - and
       that departure is the entire reason the token moves exist: the budget is spent, not banked. (The
       fraction is unchanged from v7 by construction: both generations round the converged ceiling down and
       quote it against the lead-only failure alpha, so 0.83 is a property of the two conventions rather
       than a coincidence.) Two consequences, both measured, both printed by tools/verify_ratios.py:
         (a) AT REST THE TWO LOBES CONVERGE and their peaks compose - screen(screen(x, lead), trail), so
             the stacked case is the READING state, not an edge case. Budgeting the lobes separately (as
             the first derivation did) put --ink-3 at 4.4949:1 and --brand at 4.4773:1 on --bg-3, both
             ~0.005 under AA. FIXED by dimming the TRAIL to a 0.026 peak and holding the lead at 0.055:
             the lead is the lobe a visitor actually sees, so spending the correction on the tail costs
             the effect least (the alternative, scaling both, kept only 93.2% of the lead). Those four
             figures are the [GLOW] generation's and are HISTORICAL; what they established is the RATIO,
             and every generation since holds it - the trail still rides at the same 0.4727 of the lead
             (0.123 x 0.4727 = 0.05814, shipped as 0.0581, i.e. 0.4724 after rounding), so the whole lift
             is again ONE number. Stacked at the [RED v8] peaks, and the red is printed alongside the
             ink it no longer shares a budget with, because omitting it is exactly how a reader would
             conclude the site still passes:
             --brand 2.3597, --ink-3 4.5041 on --bg-3; 2.5571 and 4.8809 on --bg-2; 2.7301
             and 5.2111 on --bg. The binding pair for the entire palette is --ink-3 on --bg-3 at 4.5041;
             the red is not a pair the light is solved against at all any more.
             verify_ratios.py re-derives every one of those, and the stops from the peaks, on every run.
             (The ground at the lobe core goes #0E0C0B -> #351918: 3.99x the unlit ground, 1.58x what the
             v7 light reached and 1.95x the [GLOW] light.)
         (b) A LIGHT BAND IS NOT PROTECTED THE WAY THE GROUND IS. screen can only lighten, so no surface
             can be muddied - but a dark-on-light PAIR loses ratio, because the ink lifts far more in
             relative terms than a near-white ground does (6.26 carries the arithmetic). Measured on the
             .band--ivory rebindings as they now ship: --brand-ivory 5.79 -> 5.37, --danger-ivory
             5.51 -> 4.77 with the lead lobe alone (5.28 and 4.67 with both). RULE, RE-MEASURED UNDER THE
             STRONGER LIGHT AND RAISED AGAIN: any accent that ever ships inside a light band must measure
             >= 5.30:1 on ivory BEFORE the light - not 4.50:1, no longer v7's 5.00:1, no longer 4.70:1.
             The bar is hue-dependent and the single covering figure hides that, so both thresholds are
             kept: the unlit ratio landing on exactly 4.50:1 lit is 4.79:1 at hue 0 and 5.30:1 at
             --danger's hue 32. All three in-band values clear the new bar; the retired v6 pair
             (#D4281D 4.52, #A85A00 4.51) never cleared any version of this bar. ---- */
    --glow-lead:  255, 112, 112;   /* #FF7070 as a triplet. The lead lobe, and nothing else. [HOVER v22] THIS
                                      TRIPLET NOW STANDS ALONE: it used to read "= --brand-bright #FF7070", and
                                      that stop is retired (see THE RETIREMENT in the ladder above). The value
                                      does not move with it - the firm's instruction was about the pink a POINTER
                                      lands on, and a screen-blended lobe at 0.123 over the near-black ground
                                      composites to #351918, a dark warm red nobody reads as pink. The triplet
                                      was never composed FROM the token in any case: CSS cannot decompose a hex,
                                      so the two were only ever kept equal by hand and by comment, which is the
                                      other reason retiring the token costs this line nothing. #FF7070 remains
                                      oklch(71.9% 0.1751 22.5) - hue 22.50 is why it could not stay in a ladder
                                      that now holds one OKLCH hue, and is irrelevant to a light. */
    --glow-trail: 191, 0, 0;       /* = --brand-deep #BF0000 as a triplet. The trail lobe, and nothing else: the
                                      DEEP stop because a darker colour adds less light at the same alpha, which
                                      is what lets the trail carry a warmer weight without buying more budget.
                                      [RED v7] Both lobes moved with the ladder. Solving against the SHIPPING
                                      lobes rather than the retired coral ones is not a formality: a pinker lead
                                      costs a little more contrast than the coral it replaces, and the hue-0
                                      pair happens to allow slightly MORE light overall (0.0763 against 0.0742).
                                      [RED v8] NEITHER LOBE MOVES. --brand-bright and --brand-deep were the two
                                      stops the collapse did not touch, so the light keeps exactly the colours
                                      it had and only its peaks change - which is why the whole v8 solve is two
                                      alphas and not a re-derivation.
                                      [HOVER v22] NEITHER LOBE MOVES AGAIN, and this time the lead's stop is not
                                      merely untouched but gone: --brand-bright is retired and this file's only
                                      #FF7070 is the triplet above. --brand-deep survives and this trail is still
                                      its channels, so the two lobes are no longer symmetric in provenance the
                                      way the [RED v7] line assumed - one names a live stop, one names a colour
                                      the ladder no longer carries. Neither peak, stop or lobe geometry changes. */

    /* ---- Type: faces (worksheet #02-#03). THE PAIRING IS THE FIRM'S DECISION OF 2026-09-06, stated
       verbatim: Newsreader for display/headings (400-600 plus a 400 italic), Montserrat for body, nav,
       buttons and labels (300-600), both from Google Fonts, with the two stacks below written as the
       firm wrote them. It is implemented as instructed and the stacks are not ours to re-derive.
       ONE CORRECTION TO THE BRIEF, RECORDED BESIDE THE DECISION AND NEVER FOLDED INTO IT. The brief
       that carried this pairing says Montserrat "is the face in your logo". For this firm it is not.
       reference/derivation/logo-measurements.json and the 2013 vector ly_proj_193320.svg measure the
       wordmark as Titillium Text - "TWM" Light, "Legal" Medium - with Arvo for ".com", and the
       letterspaced "ATTORNEY SERVICES" sub-line added in GIMP in 2020 as a generic "Sans". Montserrat
       appears nowhere in the mark. So there is NO "same typeface as the wordmark" relation to claim
       here, and nothing downstream may be justified by one. (What IS measured, and is a coincidence
       rather than a provenance: Montserrat's caps fit the 2020 raster's sub-line closer to the way the
       raster's own generic Sans fits it than Titillium's did - see the tracking ladder below.)
       WHAT THE PREVIOUS SETTING WAS, kept because it explains the shapes this file still has:
       Titillium Web (the wordmark's own face, which is why invariant 1 used to be able to call the
       tracked-caps sub-line a family resemblance) + Source Serif 4, chosen by register-avoidance with
       the note "NOT Newsreader - the sister site landcatchers.com owns it". That note is now overruled
       BY THE FIRM: Newsreader ships here, and the landcatchers.com overlap is a consequence the firm
       accepted, not an oversight. ".com" is Arvo INSIDE THE MARK ONLY and never appears in CSS.
       The synthetic fallback family sits SECOND in each stack, ahead of the raw system faces, so
       the pre-swap render occupies the same box (block 2). THERE ARE TWO STACKS AND TWO SYNTHETIC
       FAMILIES, NOT THREE: the serif's third family ("Source Serif 4 Fallback Hero", consumed through
       a --font-display-hero token) is RETIRED with the face that needed it - the firm specified exactly
       two stacks, and the measurement that would have justified re-introducing a third is reported in
       block 2 rather than acted on. */
    --font-body:    "Montserrat", "Montserrat Fallback", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
    --font-display: "Newsreader", "Newsreader Fallback", Georgia, "Iowan Old Style", "Times New Roman", serif;

    /* ---- Type: size scale (worksheet #04). THE RATIO WAS DERIVED AT A 16px BASE AND IS UNCHANGED; only
       the base moved, to 17px, when [RED v7] set html { font-size: 106.25% } (block 3). The original
       derivation, kept because it is what fixes the RATIO: proxima-nova 300 x-height 0.480em -> the body
       face's own x-height. Under Montserrat 400 (0.525em, measured at the resolved variable instance -
       the OS/2 sxHeight 0.517 is the wght-100 default and MVAR moves it) that is 16 x 0.480 / 0.525 =
       16 x 0.9143 = 14.63px, where Titillium's 0.500em used to give 16 x 0.96 = 15.4px. The 16px floor
       binds HARDER than it did, not less: running copy and form fields never go below 16px (iOS zooms a
       sub-16px field), so the floor bound and the scale was rebuilt upward from it. Conclusion unchanged.
       RATIO r = (hero / base)^(1/4). The hero must carry the old 42px futura caps' mass of capital,
       42 x 0.67 = 28.1px, in a sentence-case serif.
       [REPAIR v10] THE CAP FIGURE THIS ROW USED WAS READ OFF THE WRONG INSTRUMENT, AND THE CORRECTION IS
       WRITTEN OUT RATHER THAN PATCHED IN, because it is the one number in this block that was called
       "measured" and was not. What the previous pass measured is true and reproduces exactly: the CSS
       `cap` unit returns 0.67000em for Newsreader at opsz 21.62 / 27.51 / 34.99 / 44.51 and at 1000px,
       and `ex` returns 0.42600em, both dead constant. But `cap` and `ex` read the STATIC OS/2 fields
       sCapHeight and sxHeight, and OS/2 is a table the opsz axis cannot touch - "no MVAR" is precisely
       why the READOUT cannot move, not evidence that the OUTLINES do not. They do. Ink cap of "H",
       served Newsreader at wght 450, canvas supersampled 24x through the CTM so font-size (and with it
       the resolved opsz) is untouched - the rig reproduces Montserrat cap 0.70000 / x 0.52500, Segoe UI
       x 0.50000, Georgia x 0.48167 and Arial cap 0.71583 before it is trusted here:
         opsz 21.62  0.67407      opsz 34.99  0.68415      opsz 44.51  0.69224
         opsz 27.51  0.67775      opsz 41.94  0.69059      opsz 72     0.71528
       The drawn cap rises 2.7% across the display band and is 3.3% over the table figure at the hero.
       No shipped value changes - the ladder is held by the firm's own "slightly bigger" instruction and
       every rung is a rem multiple that does not depend on this number - but three claims that hung off
       it do, and all three are corrected here rather than left to be re-derived by the next reader.
       (1) SOLVING THE ROW HONESTLY. 28.1 = px x capInk(px) is a fixed point, not a division, because the
       cap ratio is itself a function of px; solved against the ink it gives 40.77px, where dividing by
       the table's 0.670 gave 41.94px.
       (2) SO THE sqrt(phi) COINCIDENCE IS AN ARTEFACT OF THE TABLE, NOT A RESULT. (40.77 / 16)^(1/4) =
       1.26343 against sqrt(phi) = 1.27202 - a 0.68% miss. The scale STAYS 1.272 exactly as shipped, and
       it always was the ratio the previous face was solved to; what does not survive is the sentence
       claiming the new face independently re-measured its way back to it. It re-measured its way back to
       1.2634 and the two were read as one because both were compared through a table field.
       (3) THE HERO'S OVER-SHOOT IS BIGGER THAN THE ANNOTATION BELOW SAID - see the 106.25% paragraph.
       Two steps = phi (1.618) and the space ladder below is the same progression. Every step is
       base x 1.272^n, unrounded. There is
       NO rung above --text-4: the derivation matched the loudest thing on the old site, and anything
       louder out-weighs the wordmark.
       WHAT THE 106.25% BASE CHANGES, AND WHAT IT DOES NOT. Every rung is unmoved in rem, so every ratio
       between two of them and every character count anywhere in this file is unmoved with it; only the px
       ANNOTATIONS move, by 1.0625. Two absolutes do not scale and are re-checked: the 16px iOS field floor
       still clears, now by a whole pixel (--text-0 is 17px); and the hero carries 44.51 x 0.69224 =
       30.81px of capital against the old site's 28.1px target - [REPAIR v10] 9.7% OVER, not the 6.1%
       this line carried while it was multiplying by the table's 0.670. The direction and the reason are
       unchanged - it is the instruction ("slightly bigger") landing on the one rung matched to an
       external figure rather than to the ladder - and 9.7% is still comfortably inside what "slightly"
       will carry at this size, which is why the rung is held and only the arithmetic is repaired.
       The ONLY size below --text-0 is --text-label; nothing runs at it. */
    --text-label: 0.786rem;   /* 17 / 1.272 = 13.36px  tracked caps; also sentence-case provenance notes and units (recede by size AND by dropping caps) */
    --text-0:     1rem;       /* 17px                  body, form fields, legal - the floor (the 16px iOS floor clears by 1px) */
    --text-1:     1.272rem;   /* 17 x 1.272 = 21.62px  lede / deck (sans); = the provisional serif floor */
    --text-2:     1.618rem;   /* 17 x 1.618 = 27.51px  card h3, pull-quote, stat numeral - the SMALLEST serif hook, one rung above the floor */
    --text-3:     2.058rem;   /* 17 x 2.058 = 34.99px  section display (old 36px caps = 24.1px of capital -> 36px serif; nearest rung) */
    --text-4:     2.618rem;   /* 17 x 2.618 = 44.51px  hero / page title: 44.51 x 0.69224 (the INK cap at this opsz, not OS/2's 0.670) = 30.81px of capital, 9.7% over the 28.1px the 16px-base target set */

    /* Serif modulation floor (worksheet #05) - PROVISIONAL UNTIL VIEWED ON CLEARTYPE / MAC AT 1x.
       Method: one paragraph at 1rem, 1.272rem, 1.618rem in Newsreader at --wt-display, in --ink on
       --bg and --ink-2 on --bg-3 (light-on-dark erodes hairlines fastest); the floor is the smallest
       size at which a blind A/B against the same string in Montserrat 400 is NOT indistinguishable.
       Set at the first rung above body; the smallest serif hook sits a full rung higher (--text-2),
       so the test can land anywhere in 1.272-1.618rem without moving a hook.
       THE ONE MEASURED NUMBER THAT MOVED ARGUES FOR HOLDING 1.272rem, and it argues harder than the
       previous pair did. The serif/sans x-height ratio was Titillium 0.500 / Source Serif 4 0.475 =
       1.053 - two faces of almost the same apparent size, which is what made a small-size A/B hard.
       It is now Montserrat 0.525 / Newsreader 0.4332 = 1.212 AT THE RUNG THIS FLOOR IS SET AT.
       [REPAIR v10] THE 0.426 THIS ROW CARRIED WAS THE `ex` / OS-2 sxHeight FIGURE, the same table field
       the cap-height row above was reading, so it was constant for the same wrong reason. Newsreader's
       INK x-height moves with opsz exactly as its cap does (same rig, same controls): 0.43316 at opsz
       21.62, 0.44148 at 27.51, 0.45313 at 34.99, 0.46851 at 44.51. The ratio is therefore 1.212 at the
       floor rung (21.62px) and 1.189 at the smallest serif hook (27.51px), not a flat 1.232.
       THE ARGUMENT IS UNCHANGED AND ONLY ITS SIZE MOVES: 1.212 against Titillium/Source Serif 4's 1.053
       is still a far more distinguishable pair than the one it replaces, still pushes the floor DOWN
       rather than up, and still leaves the "if the test lands above 1.618rem" branch unlikely. The
       correction is 2 points of ratio, and it changes no conclusion in this block.
       THE NEW WORRY IS THE OPPOSITE ONE, and the viewing test should look for it: set at the same px,
       Newsreader reads ~17.5% smaller on the x-line than Montserrat at the floor rung and ~15.9%
       smaller at --text-2, so a serif hook there sitting beside body copy can read as a size MISMATCH
       rather than as a change of voice. ([REPAIR v10] This line said "~23% smaller", which was the
       inverse ratio read as a shortfall - 0.525/0.426 = 1.232 makes MONTSERRAT 23% larger; the serif is
       correspondingly 18.9% smaller, and on the ink figures 17.5%. Two errors, one compounding the
       other, in the safe direction both times.) If it does, the answer is a hook rung, not a floor -
       and not a face.
       If the test does land ABOVE 1.618rem, the smallest serif hook rises to --text-3 and the card h3s,
       .quote and .stat-num STAY in the serif - a Montserrat sentence-case heading at --text-2 is exactly
       what the constitution forbids. THAT USED TO COST A WRAP, THEN FITTED BY 0.9px, AND IS NOW
       COMFORTABLE: "Intellectual Property", the longest practice-area name, measures 8.651em in
       Newsreader at the h3's instance (opsz 27.51 / wght 450) = 238.0px against 316.99px of card inner,
       where Source Serif 4's 9.035em gave 316.1px against the same 316.99 - a 0.9px fit that this
       paragraph warned would come back if --grid-track ever fell below ~21.95rem. Newsreader is 4.3%
       narrower than Source Serif 4 at this instance, so the fit is now 79.0px and the wrap returns only
       below ~17.35rem of track ((238.0 + 55.01 padding + 2 border) / 17). The warning stands down.
       ([v7 REPAIR PASS] Both inner figures dropped 2px: the card is a BORDER box and carries its own two
       1px edges, which the sum in the container derivation had omitted.) Never change the face. */
    --display-floor: 1.272rem;   /* = --text-1, 21.62px; provisional */

    /* ---- Type: leading. THE GOVERNOR CHANGED FACES AND SO DID TWO OF THE FIVE VALUES.
       PREVIOUS SETTING, kept because it is why --leading-body was ever 1.6: Titillium's hhea box is
       TALL (ascent 1.133 + descent 0.388 = 1.521em, so `line-height: normal` was already 25.9px at
       17px) and 1.6 was the smallest value that kept adjacent line boxes from touching. Montserrat's
       `normal` box is 1.219em (measured; hhea 0.968 + 0.251), so THAT CONSTRAINT STOPS BINDING
       ENTIRELY - 1.6 would clear it by 0.381em where Titillium was cleared by 0.079em - and the
       x-height ratio, which used to be the slack half of the pair, becomes the only governor.
       Montserrat's x-height is 0.525em = 8.925px at 17px, against Titillium's 0.500em = 8.50px.
       Newsreader's glyph extent is measured per optical size (canvas ink box, d ascender + p
       descender): 0.982em at opsz 27.51, 1.000em at 34.99, 0.989em at 44.51 - against Source Serif 4's
       flat 1.01em. Every figure here is a MULTIPLIER, so the 106.25% base moves only the px. */
    --leading-body:    1.68;   /* 1.6 -> 1.68, FORCED: at 1.6 the pitch is 27.20px against Montserrat's 8.925px x-height = 3.048, a hair BELOW the reference design's 3.05 floor, so the old value no longer satisfies its own rule. 1.68 gives 28.56px = 3.200 x the x-height - the same ratio the previous face was set to, between 3.05 and the old site's 3.75. Clears Montserrat's 1.219em `normal` box by 0.461em (7.84px) */
    --leading-lede:    1.575;  /* 1.5 -> 1.575, same reason: at --text-1 21.62px the x-height is 11.35px, and 1.5 gives 32.44 / 11.35 = 2.857 against this row's stated target of 3.0. 1.575 gives 34.05px = 3.000 exactly - a shorter measure takes tighter leading, and 3.0 is what "tighter" means here */
    --leading-heading: 1.15;   /* HELD. Newsreader at --text-2..--text-3: the governing extent is the larger of the two rungs, 1.000em at opsz 34.99, so 1.15 is a 0.150em pad (5.24px at 34.99px, 4.63px at 27.51px where the extent is 0.982em). The rule the previous face stated was "extent + 0.14em"; Newsreader's extent is 0.02em SHORTER than Source Serif 4's, so re-deriving to the letter would give 1.14 and the change would be 0.01 - below anything a reader can see and not worth moving every heading's rhythm for. Recorded rather than applied */
    --leading-display: 1.1;    /* HELD, same argument. Newsreader at --text-4 (opsz 44.51) has a 0.989em extent, so 1.1 is a 0.111em pad = 4.96px at 44.51px, against the 0.09em / 4.01px the previous face was solved to. A descender never meets the next line's ascender, with more room than before; the literal re-derivation is 1.08 and is not worth a visible change */
    --leading-label:   1;      /* single-line caps: cap height 0.700em (Montserrat, constant across the axis) centred in a 1em box - it was 0.692em under Titillium */

    /* ---- Type: weight per GROUND. BOTH FACES ARE NOW VARIABLE AND BOTH ARE RANGE-LIMITED BY THE
       FIRM'S OWN REQUEST: Montserrat wght 300..600 and Newsreader wght 400..600 are what the loader
       asks for and what fonts.googleapis.com returns (verified 2026-09-06: font-weight: 300 600 and
       400 600 in the served @font-face blocks). Every value below is inside those ranges, so nothing
       here is synthesised. PREVIOUS SETTING: Titillium shipped 200/300/400/600/700/900 as static files
       with NO 500, which is why several of the notes below used to argue from "the next shipped step"
       - a half-step in the sans is now reachable and simply is not wanted.
       Physics: light-on-dark erodes (hold 400 or go heavier), dark-on-light blooms (go lighter).
       Dark-ground values here; .band--ivory rebinds them. Full role x ground table: DESIGN-SYSTEM.md.
       THREE GAPS THE NEW REQUEST OPENS, none of them a token, all of them worth an audit before ship:
         (1) NO SANS ITALIC AT ALL. The previous loader carried real Titillium italics at 300 and 400.
             Montserrat is requested roman-only, so base's `img { font-style: italic }` (broken-image
             alt text) and any future <em> in body copy render as a SYNTHESISED oblique -
             font-synthesis is `none` only inside the display grant. No page ships <em>/<i>/<cite>
             today, so as it stands this touches alt text only.
         (2) NOTHING ABOVE 600. Titillium 700 was loaded-but-unassigned; now there is no 700 to
             inherit. The 27 bare <h3> elements in src/pages inherit the UA's `bold`, and where a rule
             catches them (.svc-card h3, .act-card h3, .package h3 through the display grant; .prose h3
             and .num-list h3 through --wt-strong) they resolve at 600. A bare <h3> outside those
             contexts clamps to Montserrat 600 PLUS synthetic bold.
         (3) NEWSREADER ITALIC 400 IS LOADED AND UNASSIGNED - the same status Titillium 700 had, and
             DESIGN-SYSTEM.md s9's rule applies to it in the same way. */
    --wt-body:    400;   /* never 300 reversed out of near-black - see body. 300 is the light-ground step-down and stays inside the ivory band. IT IS NO LONGER "the wordmark's TWM weight": that was a Titillium Text Light relation and it died with the face - see the correction in the faces block */
    --wt-label:   600;   /* tracked caps at 13.36px reversed: 400 spindles at 0.16-0.32em of air, and 600 is the top of the requested range. 500 now EXISTS (Montserrat is variable 300..600 here) and is not taken: 600 advances 3.7% over 400 (0.50906 / 0.49072em over the 108-character sample) where Titillium's 600 advanced only 1.8%, so the wider face already carries more ink per label and a half-step would be a fourth weight for nothing */
    --wt-display: 450;   /* Newsreader reversed: nominal 400 plus the half-step erosion takes back (the playbook's "425-450 reversed"); a real point on the requested 400..600 axis, never synthesised */
    --wt-strong:  600;   /* emphasis in the sans and inside display type: the top of BOTH axes as requested, so one number covers both faces and neither can be over-asked */

    /* ---- Type: tracking ladder (worksheet #01), indexed to ROLE, never to size. THE WHOLE LADDER DROPS
       TWO RUNGS WITH THE FACE, because its anchor is a FITTING and Montserrat fits the same raster
       differently. Top rung = the sub-line's tracking, DERIVED from the 2020 raster, not read off it:
       tracking = (measured line - untracked line at the implied size) / 16 gaps. The raster is fixed
       (1386px sub-line, 16 gaps, 62px caps); only the face inside the sum moves.
         Montserrat 400 (SHIPPED ANCHOR)  cap 0.700 -> 62 / 0.700 = 88.57px type; "ATTORNEY SERVICES"
                                          untracked = 10.747em x 88.57 = 951.9px;
                                          (1386 - 951.9) / 16 = 27.13px = 0.3063em = 0.438 x cap
         Titillium 400 (PREVIOUS ANCHOR)  cap 0.700 -> 88.57px; untracked = 9.009em x 88.57 = 797.9px;
                                          (1386 - 797.9) / 16 = 36.75px = 0.4150em = 0.593 x cap
         Arial 400 (the width control)    cap 0.716 -> 86.55px; untracked 923.5px;
                                          (1386 - 923.5) / 16 = 28.91px = 0.3340em = 0.466 x cap
       Montserrat's caps are 19.3% wider than Titillium's over that string, so the same raster leaves
       27.13px per gap instead of 36.75px and the top rung falls from 0.415em to 0.306em -> 0.32em at the
       nearest 0.04 step. TWO THINGS TO READ BEFORE MOVING IT. First, the new anchor agrees with the
       raster's own fitting BETTER than the one it replaces: the 2020 sub-line was set in GIMP's generic
       "Sans", and Montserrat's 0.3063em lands beside the Arial-width control's 0.3340em where Titillium's
       0.4150em sat well outside it. (The face-independent observation - a 37px median gap between glyph
       clusters, logo-measurements.json - includes sidebearings, so it corroborates without deciding.)
       Second, the wordmark-check render (reference/fonts/wordmark-check.html) matched the raster at
       0.42em IN TITILLIUM and has NOT been re-run in Montserrat; 0.32em is PROVISIONAL on that re-run,
       exactly as 0.40em was provisional on the render that did happen. See DESIGN-SYSTEM.md.
       THE COST, WHICH IS A DESIGN DECISION AND NOT A MEASUREMENT, STATED SO IT IS NOT DISCOVERED LATER:
       each quieter role still steps down 0.04em in the playbook's role order, so an anchor two rungs
       lower drags the whole ladder two rungs down and the BOTTOM RUNG LANDS AT 0.04em, which does not
       read as tracked caps at all - it reads as a rendering artefact. --track-7 (0.08em) is marginal for
       the same reason. The 8 x 0.04 ladder no longer spans a usable range at this anchor. Two exits, both
       decisions rather than edits: compress the step (0.32 down to ~0.11 in seven steps of 0.03) or drop
       to six rungs and re-map the roles. Neither is taken here, because the step is part of the
       derivation the firm's face change was applied to, and changing both ends at once would leave no
       measured number in the ladder at all. Always em. Narrow-width overrides step DOWN the ladder: a
       label sheds tracking before size, and body copy sheds neither. Positive tracking exists ONLY on
       --font-body at --text-label.
       UNITS: --track-1..8 and --track-nav are LETTER-SPACING in em. The card grid's column width is
       --grid-track (rem) - a different quantity, named so the two can never be confused. */
    --track-1: 0.32em;   /* 0.40 -> 0.32. hero label - the ONE loudest label, sitting under the title as the sub-line sits under the wordmark; = 0.46 x Montserrat's 0.700 cap */
    --track-2: 0.28em;   /* 0.36 -> 0.28. section eyebrow */
    --track-3: 0.24em;   /* 0.32 -> 0.24. footer heads, kicker, scroll cue */
    --track-4: 0.20em;   /* 0.28 -> 0.20. stat / role / info labels, attribution */
    --track-5: 0.16em;   /* 0.24 -> 0.16. nav at rest */
    --track-6: 0.12em;   /* 0.20 -> 0.12. buttons, link-arrows */
    --track-7: 0.08em;   /* 0.16 -> 0.08. field labels, hero meta; the nav's shed value in the compact band. MARGINAL as tracked caps - see the ladder note */
    --track-8: 0.04em;   /* 0.12 -> 0.04. licence strings, counter units - the bottom rung, and BELOW what reads as tracked caps. Recorded, not repaired: see the ladder note's two exits */
    /* Role aliases exist only for the two rungs that are consumed or rebound elsewhere. */
    --eyebrow-track:        var(--track-2);
    --eyebrow-track-cancel: calc(-1 * var(--eyebrow-track));   /* the trailing-space correction; the two can never drift apart */
    --track-nav:            var(--track-5);                     /* rebound to --track-7 in the compact band (block 4) */

    /* ---- Space: the phi ladder - every second step of the type ratio (1.272^2 = 1.618), so a gap and a
       size at the same step agree. 1rem x phi^n. Note --space-3 (27.51px) used to be within 0.31px of one
       body line and is now 1.05px under it: --leading-body moved 1.6 -> 1.68 with the face, so one line of
       copy is 28.56px against the 27.20px it was. Card padding and grid gap are still one line of copy to
       within 3.7%, which is close enough to keep the relation and too far to keep calling it exact. It is
       a relation between a rem quantity and a MULTIPLIER, so the 106.25% base still carries it unchanged
       and only the px below move; what moved it was the leading, not the base. */
    --space-1: 0.618rem;   /* phi^-1 =  10.51px  label-to-rule, micro gaps */
    --space-2: 1rem;       /* phi^0  =  17px     inline gaps, field padding, the compact nav gap */
    --space-3: 1.618rem;   /* phi^1  =  27.51px  grid gap, card padding, desktop gutter, nav gap at rest */
    --space-4: 2.618rem;   /* phi^2  =  44.51px  component stacks, feature-split gap, logo-to-nav clearance, landscape-phone section padding */
    --space-5: 4.236rem;   /* phi^3  =  72.01px  phone section padding */
    --space-6: 6.854rem;   /* phi^4  = 116.52px  desktop section padding */
    --space-7: 11.09rem;   /* phi^5  = 188.53px  hero vertical slack (reserved for step 7) */
    --gutter:       var(--space-3);   /* 27.51px each side */
    /* [FACE v9] THIS TOKEN NOW FAILS ITS OWN STATED CONSTRAINT AND NO VALUE OF IT CAN PASS. Read before
       touching it. The rule is a 45-55 character measure on a 375px phone. Montserrat 400 averages
       0.49072em per character = 8.3422px at the 17px root, against Titillium's 7.1043px - 17.4% wider -
       so 375 - 2 x 21.62 = 331.76px is 39.8 characters where it was 46.7. THE BAND IS UNREACHABLE AT ANY
       GUTTER: 375 / 8.3422 = 45.0 characters with ZERO gutter, and 47.8 even if the root were dropped
       back to 16px. The only lever that reaches 45 is a smaller body size on phones, and body is pinned
       at the 16px iOS field floor (--text-0 is 17px and clears it by one pixel). So this is a
       stated-constraint failure of the kind the palette block records for the red, not a value that can
       be re-solved: the token HOLDS at one type step down, and the 45-55 rule is recorded as broken by
       the firm's face choice rather than quietly re-written to a band 39.8 happens to fall inside. */
    --gutter-phone: 1.272rem;         /* one type step down: 1.618 / 1.272 = 21.62px each side -> a 375px phone holds 331.76px = 39.8 characters, BELOW the 45-55 phone measure (46.7 under Titillium, 50.0 of them at the 16px base). See the note above: no gutter reaches 45 */
    --nav-gap:      var(--space-3);   /* between nav labels at rest; rebound to --space-2 in the compact band */

    /* ---- Measure, container, track, logo (worksheet #18). THE PER-CHARACTER FIGURE IS THE NUMBER THE
       FACE CHANGE MOVED FURTHEST, AND EVERYTHING IN THIS PARAGRAPH HANGS OFF IT. Montserrat 400 averages
       0.49072em per character over the same 108-character prose sample -> 0.49072 x 17 = 8.3422px per
       character at body size, against Titillium 400's 0.4179em / 7.1043px. That is +17.4%: Montserrat is
       a WIDE geometric sans and Titillium a narrow humanist one, so every character count in this file
       moved with it and none of them could be carried over.
       60 characters = 29.443rem = 500.5px (the floor for any text block), 75 = 36.804rem = 625.7px. The
       measure still sits at 70: legal prose runs to long words ("indemnification", "subrogation") with
       few break opportunities, and the upper half of the band rags less. 70 x 0.49072 = 34.350em ->
       34.350rem = 584.0px.
       THE TOKEN HAD TO MOVE AND COULD NOT BE HELD. 29.25rem in Montserrat is 497.3 / 8.3422 = 59.6
       characters - BELOW the 60-character floor - so the previous literal does not merely mean something
       different, it breaks the rule the token exists to keep. The alternative considered and rejected:
       29.443rem (60 characters, 500.5px) is within 3px of the old width and would have preserved every
       downstream px, but it drops the token's meaning from the upper half of the band to its floor, which
       is the opposite of the reason 70 was chosen. The character count is what the token means.
       Declared in rem, NOT ch: Montserrat's "0" advances 0.662em (11.25px), 34.9% wider than the average
       character, so 1ch overstates the measure (34.35rem is only 51.9ch) - the same relation Titillium
       had (0.560 / 0.4179 = 1.340) and the reason the ch caps on .hero-title and .display are set from
       the DISPLAY face's own "0" and not from this one.
       CONTAINER. The derivation still runs, and the wider measure hands (a) the lead back:
         (a) a feature split: text 34.350 + --space-4 gap 2.618 + equal media column 34.350 = 71.318rem
         (b) the 3-up card grid: 3 x 22rem track + 2 x 1.618rem gap = 69.236rem
       The larger governs, rounded UP to the next whole rem so the third track survives at the container's
       own maximum, which now yields 72rem - AND THE DIRECTED VALUE STOPS BEING DIRECTED. [RED v7] set
       72rem against a derivation that asked for 70, on the firm's instruction ("all of the cards on the
       main page are too small"); the same 72rem is now what the derivation itself asks for, and the two
       agree for the first time. Nothing about the shipped value changes; what changes is that it no
       longer needs the instruction to justify it. Third-track check at 72rem: (72 - 3.236) / 3 =
       22.921rem = 389.66px, above the 22rem minimum, so a 3-up row survives the cap.
       THE 60-75 EXCEPTION [RED v7] RECORDED HERE HAS SELF-HEALED, and its removal is the one place in
       this pass where the new face repairs something rather than costing it. The split at 72rem is
       (72 - 2.618) / 2 = 34.691rem = 589.75px, which in Montserrat is 589.75 / 8.3422 = 70.7 characters -
       INSIDE the band, where the same width in Titillium was 83.0 characters and outside it. The
       .feature-split text column carries no --measure cap of its own, and it no longer needs one: a
       .feature-body holding bare <p> at this container width now runs to 70.7 characters, which is what
       the band asks for. Reached at 1224 + 55.01px of gutters = 1279.0px of viewport.
       GRID TRACK (--grid-track). THE CONTENT DERIVATION MOVED ABOVE THE SHIPPED VALUE AND THE SHIPPED
       VALUE IS HELD - a second recorded exception, in the opposite direction to the one that just healed.
       40 characters of body per line = 40 x 8.3422 = 333.69px + 2 x 27.51px card padding + 2 x 1px border
       = 390.70px -> 22.982rem, i.e. 23rem, where Titillium gave 20rem. Against that:
         at the track minimum   374 - 2 border - 55.01 padding = 316.99px of inner = 38.0 characters
         at the grown track     389.66 - 2 - 55.01 = 332.65px = 39.9 characters
       BOTH ARE BELOW 40, so the claim this paragraph used to make - "the content rule is satisfied with
       room rather than met exactly" - is false in both places and has been deleted rather than softened.
       22rem is HELD anyway, and the reason is that it is the firm's own directed card width; raising it
       to 23rem would also re-open the container ((b) becomes 3 x 23 + 3.236 = 72.236 -> 73rem), and at
       the shipped 72rem cap the third track would fall to 22.921rem and the grid would silently drop to
       2-up - which is precisely the failure the round-up rule exists to prevent. So the choice is between
       missing a content rule by 2.0 characters and restructuring the page's whole width system against a
       directed value; the first is recorded, the second is not taken. If it is ever revisited, the pair
       that works is --grid-track 23rem WITH --container 73rem, together, never one alone.
       [v7 REPAIR PASS] THE TWO BORDERS ARE PART OF THE SUM: .svc-card carries `border: 1px` under the
       global border-box, so the 22rem track is a BORDER box. Measured on a 374.98px track: card 374.98 ->
       body border box 372.98 -> content 317.97.
       Heading check: "Intellectual Property", the longest practice-area name, is 8.651em in Newsreader at
       the h3's instance (opsz 27.51 / wght 450) = 238.0px at --text-2 against 316.99px of inner - one
       line with 79.0px to spare, where Source Serif 4's 9.194em/252.9px left 64.1px.
       LOGO: the 2020 horizontal mark is 2505 x 654 (3.830:1). Its header height is fixed by its smallest
       text: the sub-line's cap height is 62 / 654 of the image, and the smallest capital a screen renders
       legibly is ~7.2px (10px type x 0.72 cap height) -> 7.2 x 654 / 62 = 76px. THAT FLOOR IS ABSOLUTE px
       AND DID NOT SCALE, so at the 106.25% base it is 4.471rem, not the 4.75rem it used to be. The token
       HOLDS at 4.75rem: it is now one notch ABOVE its floor rather than sitting on it, which is the same
       direction of travel as everything else in this pass (80.75px tall, 309.3px wide, sub-line caps
       7.66px against the 7.2px floor). It is no longer the smallest legible mark; it is a mark with 0.46px
       of cap in hand. (The 44px tap-target floor is not what governs; the sub-line is.) Assumption stated
       in DESIGN-SYSTEM.md section 9. */
    --measure:   34.35rem;   /* 29.25 -> 34.35. 584.0px, 70 characters in Montserrat 400: max-width on every running-text block. Holding 29.25rem would have meant 59.6 characters, below the band's own floor */
    --container: 72rem;      /* 1224px - now BOTH the derivation's answer (a: 71.318 -> 72) and the firm's directed value; they agree since --measure moved */
    --grid-track: 22rem;     /* 374px: the card grid's minimum column width, DIRECTED (the content derivation now gives 23rem and is MISSED by 2.0 characters - recorded exception above) - a WIDTH in rem; the em letter-spacing ladder is --track-N */
    --logo-h:    4.75rem;    /* 80.75px -> 309.3px wide; 4rem (68px -> 260.4px) once the nav has collapsed */

    /* ---- Breakpoints (worksheet #18): two width numbers, both from content, plus one height query.
       Media queries cannot read custom properties, so the numbers are repeated as literals in block 4
       (66.99rem / 67rem / 73.99rem / 29.99rem) - grep before changing a token here.
       [RED v7] A CORRECTION THAT PRECEDES EVERY SUM BELOW, AND THE REASON THEY NOW READ AS THEY DO: rem
       INSIDE A MEDIA QUERY DOES NOT RESOLVE AGAINST html's font-size. Per CSS Values, font-relative units
       in a media query use the INITIAL value of font-size - the browser default - so `67rem` in block 4 is
       1072px whatever :root says. The note that used to stand here ("they are in rem, not px, because a
       visitor at a 20px root grows every label 25% and the collapse must move up with them") was therefore
       only half true, and the untrue half is the half that matters now: html { font-size: 106.25% } grew
       every label by 6.25% and moved NO query literal with it. Every row below is re-summed at the new
       size, and the two structural literals did not cover their own sums. [REPAIR v10] THAT IS NOW FIXED
       RATHER THAN RECORDED, and the decision is written out in THE OVERSHOOT note below: exit (i) was
       taken and the pair moved 64 -> 67rem and 71 -> 74rem, which is the value each literal's own
       derivation has been asking for since the face changed.
       STRUCTURAL 67rem = 1072px: where the tracked-caps nav labels stop fitting beside the logo, even
         with their tracking shed. THIS SUM IS NOW RUN ON THE STRING THE SITE ACTUALLY SHIPS, not on the
         six retired labels it used to carry - the two frames of reference are what made this block and
         6.1 disagree, and one string ends that. Labels "PRACTICE AREAS / ATTORNEYS / FLAT FEE SERVICES /
         MAKE A PAYMENT / CONTACT US" = 64 characters, five rows, from site-plan.json nav.header (Title
         Case in the markup, uppercased by CSS). MEASURED per label with getBoundingClientRect at
         --text-label 13.36px / --wt-label 600 in Montserrat, tracking included - CSS adds letter-spacing
         after the last character too, so the trailing rung is inside these figures:
           at rest  --track-5 0.16em -> labels 691.39px; 4 gaps x --space-3 27.51 = 110.04; caret 9.62
                    -> nav 811.05px
           compact  --track-7 0.08em -> labels 622.98px; 4 gaps x --space-2 17    =  68.00; caret 10.69
                    -> nav 701.67px
         (The caret is .nav-menu-btn's ::after, 0.38em + (0.5em - track) at the button's own 13.36px; it
         grows as tracking is shed, because the margin cancels the label's trailing rung. 6.1 carries it.)
         Row = gutter 27.51 + logo 243.6 (the shipped 3.75rem header cap, measured) + clearance --space-4
         44.51 (one step above the label gap, or the first label reads as a sixth word of the mark) + nav
         + gutter 27.51 + 17px classic-scrollbar allowance (media queries measure the viewport including
         it):
           at rest  27.51 + 243.6 + 44.51 + 811.05 + 27.51 + 17 = 1171.18 -> full tracking needs 74rem
           compact  27.51 + 243.6 + 44.51 + 701.67 + 27.51 + 17 = 1061.80 -> the compact nav needs 67rem
         THE OVERSHOOT, AND THE ONE THING TO READ BEFORE TOUCHING THE HEADER. IT IS NO LONGER SMALL.
         [RED v7] left this row 8.1px over 1024 and 5.4px over 1136 and held the literals, on the ground
         that the mark absorbs a shortfall that size invisibly. Montserrat 600 is materially wider than
         Titillium 600 across these five labels (554.58px against 457.56px untracked, +21.2%), so at the
         SAME ladder the overshoot would have been 105.1px and 102.5px. Re-deriving the tracking ladder
         from the 2020 raster (two rungs down, above) pays most of that back and is the tightest tracking
         this file can defend - and the rows STILL come to +37.8px at 1024 and +35.2px at 1136.
         WHAT THAT COSTS, MEASURED RATHER THAN WAVED AT. Nothing overflows: .site-logo is
         `flex: 0 1 auto; min-width: 0` and its img is `max-width: 100%; object-fit: contain`, so the MARK
         absorbs the shortfall. But it now absorbs 37.8px of it - the logo renders 205.8px wide against
         its natural 243.6, i.e. 15.5% narrower, and the sub-line's caps fall from 6.04px to 5.10px
         (14.4% and 5.17px at 1136). Under [RED v7] the same mechanism cost 2.5% and 0.15px of cap and
         was fairly called invisible; at 15.5% it is a visibly smaller mark in a ~38px band of widths,
         and it is recorded as a DEGRADATION THAT IS NOW VISIBLE rather than carried forward under the
         old adjective.
         [REPAIR v10] EXIT (i) IS TAKEN. THE LITERALS MOVE TO THE ROWS. Two passes held this pair while
         the shortfall grew from 8.1px to 37.8px, each time on the ground that the fix was "a decision,
         not a token edit". It is a decision, and this pass makes it, because the third reading of the
         same paragraph is the one that settles it: the structural literal is DEFINED, four lines above,
         as "where the tracked-caps nav labels stop fitting beside the logo". The rows say that width is
         1061.80px compact and 1171.18px at rest. 1024 and 1136 are therefore not a judgement about the
         collapse - they are simply the wrong answer to the question the literal asks, inherited from a
         face that is no longer loaded. Holding them was not conservatism; it was shipping a number that
         contradicted its own definition, and paying for it with the mark.
           new literals   67rem = 1072px  against the compact row's 1061.80  -> 10.2px of slack
                          74rem = 1184px  against the at-rest row's 1171.18  -> 12.8px of slack
           rounded UP to the whole rem, the same rule the container uses, so the row survives at the
           boundary rather than at one width inside it.
         WHAT THIS BUYS, VERIFIED ON THE SERVED PAGE AT 4px STEPS: the mark now renders its full 243.63px
         at EVERY width where the nav is up. Before this change it measured 207.84px at 1024 (14.7%
         narrow, sub-line cap 5.15px) and 210.50px at 1136 (13.6%), recovering only at 1060 and 1172 -
         two bands about 36px wide in which the wordmark was visibly the wrong size. Both bands are gone,
         not moved: at 1072 and at 1184 the row now fits with slack, so object-fit never binds.
         WHAT IT COSTS, STATED IN FULL, because it is a real trade and not a free win:
           - the burger persists to 1071px. A 1024x768 tablet in landscape now gets the mobile menu. That
             is the honest answer - the desktop nav genuinely does not fit at 1024 - but it IS a change
             of behaviour at the single most common tablet width on the web, and it is the reason this
             was called a decision rather than a repair.
           - every two-up structure collapses 48px earlier, and the compact band starts and ends 48px
             higher. Only three rules hook the structural number (--logo-h's footer rebinding, the
             nav/burger swap, .stats-row), and all three collapse in the SAFE direction: a structure that
             goes single-column earlier was cramped in the band it gave up. The intrinsic grids
             (.svc-grid, .act-grid, .package-grid, .form-grid) and .feature-split's container query do
             not read this number at all, so nothing else moves.
           - the header tel: CTA's 89rem query does NOT move, and that is the check that proves the two
             numbers are independent: it is derived from the at-rest ROW (1171.18 + 27.51 + 222.24 =
             1420.9 -> 1424px), not from the structural literal, so re-deriving the literal to match the
             same row leaves it exactly where it was.
         THE TWO EXITS NOT TAKEN, kept because they remain the answers if this one is ever reversed:
           (ii)  shed a nav label in site-plan.json. Dropping "FLAT FEE SERVICES" takes 153.95px of label
                 plus one 17px gap off the compact row -> 890.85px, inside 1024 with 133px of slack; the
                 at-rest row goes to 971.56px, inside 1136 with 164px. Cheapest by a wide margin in
                 pixels and the only exit that would have kept the nav up at 1024 - and it is REFUSED
                 here on the ground that it is not a typographic decision at all. Which pages sit in the
                 primary nav is the firm's own information architecture (site-plan.json nav.header), and
                 /flat-fee-services is a priced commercial page. A stylesheet may not quietly drop a
                 route from the nav to make its own sum come out.
           (iii) cap the header mark. CAPPING CANNOT WORK, and that finding stands: the mark would have
                 to fall to 205.8px = 53.85px tall = 3.167rem for the compact row to fit 1024 outright,
                 where sub-line caps are 5.1px against the ~7.2px legibility floor. Capping does not
                 avoid the degradation, it makes it permanent at every width instead of only in a band.
         Between 67 and 74rem the labels shed tracking BEFORE size (0.16 -> 0.08em, gap 27.51 -> 17px);
         size and case never move. Below 67rem the nav collapses and EVERY two-up structure collapses
         with it - one number for every structural collapse (a feature split would survive to ~1267px by
         its own sum, but a second number is exactly the playbook's stats-row bug). Re-sum before changing
         a label; the per-label figures are in reference/derivation/type-measurements.json.
       PHONE 30rem = 480px: the largest width at which a phone re-solve is needed - three conditions, all
         re-summed at the new base:
         hero   the title at its 16ch cap = 16 x 0.5845em (Newsreader's "0" at the hero instance, opsz
                44.51 / wght 450 - it WIDENS with opsz, 0.569em at 27.51, where Source Serif 4 narrowed)
                x 44.51px = 416.2px; + gutters 55.01 = 471.2px. The cap moved 19ch -> 16ch with the face
                and the px it defends is within 4px of what it was, which is why this row still fits
                inside the 480 literal. [REPAIR v10] AND HOLDING 19ch WOULD NOT HAVE MOVED THIS
                BREAKPOINT - the claim that stood here, that it "would have taken over the max() below",
                is arithmetically false against this block's own rows and is withdrawn rather than
                softened. 19 x 0.5845 x 44.51 = 494.3px + 55.01 = 549.3px, and the form row below is
                550.9px: the hero row comes within 1.6px of governing and does not govern. max() is
                550.9 either way, the derivation asks 34.43rem either way, and this literal is exactly
                where it was. The cap moved to 16ch on its OWN ground - 16ch is 21.7 average characters
                against the 22.0 the retired face's 19ch held, i.e. the same line - and that ground is
                sufficient; it never needed a breakpoint argument and now does not have one. Below the
                literal the title spans the whole
                container, the left-weighted scrim stops protecting the copy, and 44.51px is 11.9% of a
                375px viewport (3.1% at 1440): the hero title drops one rung.
         form   two fields abreast, each a 24-character e-mail at 17px: 24 x 8.3422 + 2 x 17 padding =
                234.2px (it was 204.5px in Titillium); 2 x 234.2 + 27.51 gap + 55.01 gutters = 550.9px.
                THIS IS THE CONDITION THAT RUNS AWAY FROM THE LITERAL - see below.
         header collapsed logo 243.63 + --space-3 27.51 + 44px menu target + 43.25 phone gutters = 358.4px
                - comfortably inside a 320px phone once the mark's own max-width guard is counted, where it
                used to read as 8px inside a 375px one. [v7 REPAIR PASS] THE 260.4 THIS LINE CARRIED WAS THE
                TOKEN, NOT THE HEADER: --logo-h does drop to 4rem (68px -> 260.4px) below 67rem and the
                FOOTER mark takes it, but 6.1 caps the header's own mark at min(--logo-h, 3.75rem) = 63.75px
                -> 243.63px on the shipped asset's 3.8216 ratio (244.2 is the 2505x654 master's; 6.1's
                .site-logo img note now names both). Safe direction, wrong number, and it fed the max() below.
         max(471.2, 550.9, 359.0) = 550.9 -> the derivation now asks for 34.43rem where the shipped query
         is 30rem (480px). It asked for 30.72rem before the face changed; the whole of the extra 3.7rem is
         the form row, because a 24-character e-mail is 17.4% wider in Montserrat than it was in Titillium.
         THE LITERAL IS HELD, and the consequence is stated in full because it is bigger than the one this
         paragraph used to describe. The hero and header conditions still fit inside 480 with room (471.2
         and 359.0). The form does not, and .form-grid is INTRINSIC (auto-fit / minmax), so it resolves on
         its own sum wherever the query does not pin it. WHAT ACTUALLY HAPPENS: 6.17's track was
         re-derived with the same character figure that moved this row (12rem -> 13.78rem, its own note
         carries the sum), so 2-up needs 2 x 234.2 + 27.51 = 495.9px of container = 550.9px of viewport,
         and .form-grid is one column from 480 up to 551 - a 71px band where the fields are stacked and
         the nav is not yet collapsed. That is the derivation's own answer arriving 71px late rather than
         a defect: two fields that cannot hold their stated 24 characters are exactly what this breakpoint
         exists to prevent, and stacking them early is the safe direction.
         [v7 REPAIR PASS, STILL LOAD-BEARING] The pin inside the phone query stays. --gutter steps down at
         this SAME 480, handing the container 11.76px, and with the OLD 12rem track the 2-up sum was met
         again from 478.75px of viewport - so 480 was one column, 479 was TWO and 478 was one again
         (measured on /contact-us: 491 two, 490 one, 480 one, 479 two, 478 one). The re-derived 13.78rem
         track puts 2-up at 550.9px and out of reach of that oscillation, but the pin costs nothing and
         removing it would make the phone band depend on a sum the gutter step has already crossed once.
         Nothing between 551 and 1072 needs a width query: card grids are intrinsic. 640 was not chosen:
         no condition lands there.
       LANDSCAPE PHONE (max-height: 480px) and (orientation: landscape): 812 x 375 matches ONLY the
         structural collapse; the height query re-solves the section rhythm (and, in step 7, the hero). */
    --bp-structural: 67rem;   /* [REPAIR v10] 64 -> 67rem = 1072px, the compact row's own 1061.80px sum rounded up to the whole rem */
    --bp-phone:      30rem;

    /* ---- Radius. The mark is rectilinear (the square variant is a hard square; the only curves are the
       gavel's brush strokes) and the old 300px pill buttons re-staged nothing in it. 2px is the smallest
       radius that reads as intended on a 1px hairline edge (1px reads as anti-aliasing). ONE radius. */
    --radius: 2px;

    /* ---- Motion (worksheet #16). [MOTION v20] 2026-09-06. THE LADDER IS THE PLAYBOOK'S x 1.5, AND THE
       MULTIPLIER MOVED 1.2 -> 1.5 ON THE FIRM'S INSTRUCTION OF THE SAME DAY, quoted in full: "all of the
       animations throughout all pages of the website are to fast and too subtle. Make them more
       prominent. Use www.LandCatchers.com as a basis if you can tell from the page".
       THE SISTER SITE IS NOT THE ANSWER AND THAT IS THE FIRST THING THIS BLOCK HAS TO SAY, because the
       instruction names it and the next reader will go and copy it. LandCatchers' own ladder MEASURES
       0.20 / 0.40 / 0.55 / 0.85 - it is the playbook's, unmultiplied - so this site's ladder was ALREADY
       that site's x 1.2 on every rung, and its reveal travel was already byte-identical (16px, and 26px
       in the hero, in both files). Adopting the sister site's numbers would have made every motion on
       this site FASTER, which is the opposite of the instruction. What LandCatchers actually has more of
       is BREADTH (59 transition declarations against this file's 30, 81 transforms against 47, 13
       distinct translate literals against 5) and a harder-front-loaded curve. Both are answered below and
       in block 6; the numbers are not copied from it, because there was nothing there to copy.
       ONE MULTIPLIER, APPLIED TO EVERY RUNG AT ONCE. That is the only thing that keeps a ladder a ladder:
       after the change every ratio inside it is exactly what it was, and every rung is still the
       playbook's own value times one number a reader can check in their head. The multiplier reaches the
       four rungs, both register reveals, --lag-rule, the two hard-coded literals in block 6 (the arrow's
       asymmetric pair and the quote bar's lag), the hero's entrance in 6.28 and main.js's stagger step and
       cap. A rung left behind would not be a small error; it would be a different ladder.
       WHY 1.5 AND NOT MORE. 1.5 is the largest multiplier under which the two rungs a POINTER waits on
       still answer within one frame of what they answer in today - see the ceiling arithmetic below. At
       1.75 --dur-base is 0.70s and the perceived arrival slips past a quarter of a second; at 2.0 the
       card lift is 0.80s and a pointer flicked across a row of three leaves three lifts overlapping,
       which reads as lag rather than as weight. 1.5 is also the point at which the slowest thing on the
       page, --reveal-display, crosses 1.4s: past that a reveal starts to look like a page still loading.
       THE POINTER CEILING, AND THE LADDER DOES SPLIT AFTER ALL. [MOTION v21] REWRITES THIS PARAGRAPH
       RATHER THAN AMENDING IT, because it was headed "AND WHY THE LADDER DOES NOT SPLIT" and a render
       pass then measured one gesture at more than twice the line it claimed to hold. A hover must feel
       answered; a scroll arrival must feel deliberate, and the two have different budgets. The test is
       NOT the nominal duration - by that test this ladder was already over the line at x 1.2, since
       --dur-base was 0.48s - but the PERCEIVED arrival, i.e. when the travel is visibly finished. Every
       curve this file uses reaches 90% of its travel at a fixed fraction of its duration, solved from the
       curve rather than estimated:
         --ease-out    expo-out   cubic-bezier(0.16, 1, 0.3, 1)    90% at 0.329 of the run
         ease          the CSS default                             90% at 0.625
         linear                                                    90% at 0.900
         --ease-inout  cubic-bezier(0.65, 0, 0.35, 1)              90% at 0.714
       (Quint-out, retired this pass, sat at 0.374, and those two numbers are what the x 1.5 move was
       solved against: --dur-base BEFORE 0.48s quint -> 0.1795s perceived, AFTER 0.60s expo -> 0.1974s,
       +18ms and about one frame; --dur-fast 0.0898s -> 0.0987s, +9ms. THE CURVE IS WHAT PAID FOR THE
       DURATION, which is the whole reason those two changes were one decision and not two.)
       THE CEILING IS ROUGHLY 0.40s OF PERCEIVED ARRIVAL ON ANYTHING A POINTER WAITS ON, and it binds the
       PRODUCT rather than the rung: duration x the curve's fraction. Solved per curve, the longest
       duration each may carry on a pointer response is
         --ease-out    0.40 / 0.329 = 1.216s    every rung clears it
         ease          0.40 / 0.625 = 0.640s    --dur-base clears it at 0.375s; --dur-slow would not
         linear        0.40 / 0.900 = 0.444s    --dur-fast clears it at 0.270s; --dur-base would not
         --ease-inout  0.40 / 0.714 = 0.560s    --dur-base clears it at 0.429s; NOTHING ABOVE IT DOES
       WHERE THE SPLIT ACTUALLY BINDS, both cases repairs rather than new policy:
         (i)  6.18's .btn takes --dur-fast for its plate and its ink, not --dur-base. That is what this
              rung's own note has always said it was for ("colour and opacity only"), so the button was
              breaking a rule rather than needing a new one; its LIFT keeps --dur-base, which is the
              rung the card and the footer nudge also rise on. Two moving parts, two timings, one gesture.
         (ii) --ease-inout IS BARRED ABOVE --dur-base ON ANYTHING A POINTER WAITS ON. 6.9a's hover clip
              was --dur-media on --ease-inout: 1.275 x 0.714 = 0.910s before the picture is there, more
              than twice this ceiling, and measured at 0.157 of opacity 431ms after the pointer landed
              while every other property on the same card had settled. It takes --ease-out now - 0.4195s,
              which is the same figure as the photograph push underneath it, so the two arrive together.
       WHAT THE CEILING DELIBERATELY DOES NOT BIND, said out loud rather than left to be found:
         IMAGERY, --dur-media on --ease-out = 0.4195s, 19ms past a ceiling written as "roughly". Three
              gestures, and the subject of each is a photograph rather than a control: 6.29's push on both
              frames, and .act-card's watermark numeral. The pointer is not waiting on any of them - the
              object it is over answered in 0.1974s and the picture is the slow half of a gesture already
              acknowledged. A MATERIAL may take its time; a control may not.
         THE FOUR --ease-inout CONSUMERS, COUNTED RATHER THAN REMEMBERED, because the paragraph this
              replaces named two and there were three. All four are CHROME THAT TOGGLES BETWEEN TWO
              PERSISTENT STATES - opened and closed - which is what "reversible state" in that token's
              comment actually denotes, and none of them is a hover:
                6.2  .nav-menu-btn::after   caret rotate + translate   --dur-fast  0.214s perceived
                6.2  the practice panel     opacity                    --dur-fast  0.214s
                6.2  .nav-toggle            the burger's three bars    --dur-base  0.429s
                6.2  .mobile-menu           the full-screen fade       --dur-base  0.429s
              The first two are inside the ceiling outright; the last two are the accepted pair - tapped,
              once per visit, on a panel that covers the viewport. WHAT MAKES (ii) A RULE AND NOT A
              SPECIAL CASE: after 6.9a moved off it, every remaining consumer is a toggle and no hover
              uses it, so "reversible" stops being a word two different kinds of gesture can both claim.
              A hover is reversible too - the lift, the edge and the push all return - and every one of
              them is on --ease-out, which is the evidence that the distinction was never symmetry.
       IF THE FIRM EVER REPORTS THE MENU AS SLOW, the repair is a second, shorter rung for reversible
       state - not a retune of --dur-base, which every hover on the site is solved against. */
    --ease-out:   cubic-bezier(0.16, 1, 0.3, 1);    /* travel: expo-out, THE PLAYBOOK'S OWN - see the note below; the initial slope is 1/0.16 = 6.25 against quint-out's 1/0.22 = 4.545 */
    --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);   /* reversible state, symmetric: menus, toggles, anything that returns */
    /* [MOTION v20] --ease-out MOVED FROM QUINT-OUT TO EXPO-OUT, AND THE ARGUMENT THAT STOOD HERE IS
       REPLACED RATHER THAN AMENDED, BECAUSE IT ARGUED FOR THE OPPOSITE VALUE. It read: "travel: quint-out
       - one order gentler than the playbook's expo-out (0.16, 1, 0.3, 1); the same decisive arrival
       without the snap". Every word of that was true and the file was right to prefer it while the brief
       was a quiet site. The brief is now "too subtle. Make them more prominent", and GENTLE IS EXACTLY
       WHAT WAS ASKED TO STOP. The eye reads motion by VELOCITY, not by displacement, so the curve's
       initial slope is the single largest lever on whether a movement is noticed at all: expo-out leaves
       37.5% faster (6.25 against 4.545 at the same travel and duration), and against the new travel and
       the new ladder a plate's peak velocity very nearly doubles - 75.8 to 145.8 px per second, computed
       in guards 7.3 where the travel is declared.
       WHAT THE SNAP COSTS AND WHY IT IS NOW WORTH PAYING. Front-loading shortens the phase in which the
       motion is visible: expo finishes 90% of its travel at 0.329 of the run against quint's 0.374, so at
       an unchanged duration each arrival would read 12% SHORTER - the wrong direction for "too fast". The
       duration is not unchanged. 1.20s of plate at 0.329 is a 0.395s visible phase against 0.96s of plate
       at 0.374 = 0.359s, so the visible phase is 10% LONGER and covers 75% more ground. Travel, duration
       and curve were solved together for exactly this reason; each on its own would have moved the wrong
       number.
       AND IT PUTS THE FILE BACK ON THE PLAYBOOK'S VALUE. This was the one place the derivation departed
       from its constitution, and the departure is what the firm has now asked to be reversed. main.js's
       counter tween carries a hand-written copy of this curve and must move with it - it mirrors the
       token in JS so the numeral eases like everything else, and a stale quint there would be the one
       thing on the page still arriving on the old curve. */
    --dur-fast:  0.30s;    /* 0.20 x 1.5  colour and opacity only */
    --dur-base:  0.60s;    /* 0.40 x 1.5  UI state: buttons, header, menu, card lift */
    --dur-slow:  0.825s;   /* 0.55 x 1.5  structural: rule draws, underlines, LABEL/RULE and LIST reveals */
    --dur-media: 1.275s;   /* 0.85 x 1.5  imagery ([data-reveal="fade"]) - slow reads as heavy, heavy as expensive */
    /* Register reveals - not rungs; each is the playbook's register value x 1.5, and each can name why it is
       not a rung: DISPLAY must be the slowest thing on the page; PLATE sits between it and the rule draws. */
    --reveal-display: 1.425s;  /* 0.95 x 1.5 */
    --reveal-plate:   1.2s;    /* 0.80 x 1.5 */
    --lag-rule:       0.18s;   /* 0.12 x 1.5  fixed intra-object lag on a LIST row's hairline (CSS's, not stagger) */
    /* JS stagger (step 8): per-element step = min(0.135s, 0.675s / (n - 1)) - the playbook's 0.09 and 0.45
       x 1.5; the 0.675s cap is ~half of --reveal-display so the last element starts before the first finishes.
       THOSE TWO NUMBERS LIVE IN main.js AND MUST MOVE WITH THIS BLOCK, along with the three duration copies
       main.js keeps for its will-change release timer (1.425 / 1.2 / 0.825, this block's three arrival
       values). A stale copy there does not stop the motion - CSS owns every duration that runs - it
       releases the compositor layer early, which is the benign failure main.js's own comment describes.
       THE WATCHDOG IS UNAFFECTED AND WAS CHECKED, because a longer ladder must not outlive it: the 2.5s in
       src/partials/watchdog.html is a deadline for main.js STAMPING data-motion-ready, which block 0 does
       as its first statement, and not a deadline for anything finishing. The one timer that could have
       collided is the hero's rAF backstop, setTimeout(go, 1000), and it does not move in this pass. */
    /* ---- THE SITE'S ONE NUDGE, NOW A TOKEN. It was three copies of the literal 3px - the card rise, the
       button rise and the arrow's lateral travel - held in step by a sentence in 6.18 ("the site's ONE
       nudge is 3px ... so the page has one nudge rather than five"). [MOTION v20] gives that sentence a
       declaration to point at, because this pass raises it and adds three more consumers, and a prose rule
       with six copies is a rule that drifts on the next edit.
       3px -> 4px, AND THE SIZE IS SOLVED RATHER THAN CHOSEN. 6.18's own test for the rise is "present,
       under the threshold of a hop", and a hop on a control is read against the control's own height: the
       .btn box is 46.64px (6.18 carries the sum), and 10% of a thing's own height is the conventional edge
       at which a shift stops reading as a response and starts reading as a jump. 4.66px is that edge here,
       so 4px is the largest whole pixel under it. It is deliberately NOT the reveal travel's x 1.75 (which
       would give 5.25px and clear the hop threshold): a reveal is watched once and a nudge tracks a
       pointer, so the two distances answer to different limits and only one of them may be scaled freely.
       AN ABSOLUTE px, LIKE EVERY OTHER MOTION DISTANCE IN THIS FILE - it does not take the [RED v7] root
       multiplier, for the reason guards 7.3 gives at length about the reveal travel.
       main.js's magnetic button carries the fourth copy (LIFT = 3, ported into the JS offset because an
       inline transform outranks 6.18's hover) and MUST move to 4 with this token, or the one magnetic
       button on each page becomes the only control on the site that rises less than its neighbours -
       which is the recorded failure 6.18 already exists to prevent, one pixel along. */
    --nudge: 4px;
    /* ---- [STAT v24] 2026-09-07. THE SITE'S ONE POP, AND IT IS A SECOND TOKEN RATHER THAN A SECOND
       NUDGE. The firm asked (2026-09-06) that the two stat numerals "slightly enlarge" under the
       pointer. An enlargement is not a travel, so it cannot take --nudge's VALUE - 4px of scale is not a
       quantity - but it must take --nudge's METHOD, which is 6.18's test: "present, under the threshold
       of a hop", where a hop is read against the object's OWN height and the answer is rounded DOWN to
       the coarsest step the property has.
       THE ARITHMETIC, so nobody re-derives it. The governing object is the wider of the two figures,
       "13+" at --text-4: line box 44.506 x --leading-heading 1.15 = 51.18px, so its hop edge is 10% of
       that, 5.118px. The figure is scaled about `transform-origin: left center` (6.12 says why), so the
       farthest-moving point of a uniform scale is the far corner at r = sqrt(W^2 + (H/2)^2). W is
       MEASURED, not estimated - 75.94px rendered at --text-4 with justify-self: start, the trimmed box;
       the untrimmed grid item is 285.38px wide at 1440 and would have made the sum meaningless - so
       r = sqrt(75.94^2 + 25.59^2) = 80.14px. Solving (s - 1) x r <= 5.118 gives s <= 1.0639, and rounding
       down to the hundredth the way --nudge rounded down to the whole pixel gives 1.06. The "3" is inside
       the cap by construction (W 26.02px, r 36.49px, whose own edge would allow 1.140), and ONE value
       serves both figures exactly as one --nudge serves the card, the button and the arrow.
       A SECOND READING, FOR FREE, FROM THE TYPE SCALE: its own step is x1.272, and ln(1.06) / ln(1.272)
       = 0.242, so the pop moves the figure a QUARTER of one rung. That is a response, not a resize, which
       is the whole of what "slightly" had to mean.
       THE ALTERNATIVE CAP, NAMED AND REJECTED: capping the far corner's travel at --nudge itself (4px)
       would give s <= 1.0499 and therefore 1.04. It was rejected because --nudge is a DISTANCE shared by
       six consumers so that the page has one travel, and a scale is not one of them; what 6.18 exports to
       a new gesture is the hop TEST, not the literal. Recorded so the choice is visible rather than
       silent.
       UNITLESS, so `scale: var(--pop)` reads as the one number it is, and so the token cannot be
       mistaken for a length and dropped into a translate. */
    --pop: 1.06;
  }
}

/* ---- 2 · METRIC-ADJUSTED FALLBACK FACES ---------------------------------- */
/* display=swap renders a system face first, so its metrics are a design surface. Each family below
   aliases a local system font and overrides size, ascent and descent so that a headline set before the
   swap occupies the same box afterwards. One @font-face carries ONE size-adjust, so each local() list
   must resolve to ONE metric set on every platform - which is why each family aliases a single system
   face rather than every face the measurements were taken for.
   THE OVERRIDE VALUES BELOW ARE THE FIRM'S, SUPPLIED WITH THE FACES AND SHIPPED EXACTLY AS GIVEN.
   They were checked against the fonts' own tables and against a Chrome probe; the check is written
   beside each family, INCLUDING where it disagrees, and nothing is silently corrected. line-gap-override
   is added to both (0%, which is what the retired faces carried and what these values assume) so leading
   comes only from line-height. Overrides match line boxes, not per-glyph advances, so step 5's observer
   still refreshes on document.fonts.ready.
   ONE SEMANTICS RESULT THE CHECKS DEPEND ON, probed rather than assumed (Chrome, four @font-face rules
   over local(Arial) at 100px: plain -> fontBoundingAscent 91; size-adjust 50% -> 45; ascent-override
   100% -> 100; both -> 50). SIZE-ADJUST MULTIPLIES THE OVERRIDES: effective ascent = ascent-override x
   size-adjust x font-size, so the correct declaration is override = webfont metric / size-adjust.
   THE RETIRED FACES DECLARED THEM THE OTHER WAY - "ascent-override: 113%" annotated "Titillium hhea
   ascent 1.133em" at size-adjust 94% - which built a line box 6% short of the webfont's. That was a
   pre-existing defect in faces this pass removes; it is recorded because it is why the firm's numbers
   look different in KIND from the ones they replace (the firm's Montserrat pair is scaled, i.e. correct
   under this probe), not because anything still depends on it. */

@font-face {
  font-family: "Montserrat Fallback";
  /* SEGOE UI, and the choice is the firm's stack's, not this file's. The body stack lists Segoe UI ahead
     of Arial, reversing the retired face's own reasoning (Arial resolves on Windows, macOS AND Linux -
     fontconfig aliases it to the metric-compatible Liberation Sans - where Segoe UI is Windows-only), and
     the supplied size-adjust confirms which face it was solved against: 105% is an EXACT x-height match
     to Segoe UI (0.500 x 1.05 = 0.525 = Montserrat's x-height at wght 400, measured at the resolved
     instance). Against Arial the same 105% gives 0.5445 against 0.525, +3.7%. So this family aliases the
     face its number was built for.
     THE COST, STATED: where local("Segoe UI") finds nothing - macOS, Linux - this @font-face contributes
     no face at all and the UA falls straight through to the raw stack ("Helvetica Neue", Arial,
     sans-serif) UNADJUSTED, so those platforms get no metric match at all pre-swap rather than a
     slightly wrong one. Helvetica Neue and Iowan Old Style cannot be measured on the Windows machine
     these figures come from (their canvas rows come back byte-identical to Times New Roman, i.e. the
     face is absent and canvas fell back) and carry NO figure here.
     THE CHECK, run against Montserrat's own tables and shipped as supplied either way:
       size-adjust 105%  the advance ideal over the 108-character sample is 52.9982 / 47.4634 = 111.66%
                         against Segoe UI (110.39% against Arial), so 105% is 6.0% NARROW on the advance
                         basis this file used for every previous synthetic face. It is exact on the
                         x-height basis. The firm adjusted for x-height; reported, not changed.
       ascent 92%        0.92 x 1.05 = 0.966em against Montserrat's real hhea ascent 0.968em. CORRECT.
       descent 24%       0.24 x 1.05 = 0.252em against 0.251em. CORRECT.
                         Synthetic line box 1.2180em against Montserrat's own 1.219em - 0.1%. The exact
                         pair would be 92.2% / 23.9%, which is not worth a decimal. */
  src: local("Segoe UI");
  size-adjust: 105%;         /* THE FIRM'S. Exact x-height match to Segoe UI (0.500 x 1.05 = 0.525 = Montserrat 400); 6.0% narrow on advance, where the ideal is 111.66% */
  ascent-override: 92%;      /* THE FIRM'S. x 1.05 = 0.966em against Montserrat hhea ascent 0.968em */
  descent-override: 24%;     /* THE FIRM'S. x 1.05 = 0.252em against Montserrat hhea descent 0.251em */
  line-gap-override: 0%;     /* Montserrat's hhea lineGap is 0; leading comes only from line-height */
}

@font-face {
  font-family: "Newsreader Fallback";
  /* Georgia, unchanged from the retired serif family and for the same reasons - it is the metric
     neighbour at the sizes the serif actually renders, its stroke contrast is a text serif's where Times
     has a newsprint face's, and it is first in the firm's own display stack. Cost, also unchanged: no
     universal Linux alias (Gelasio is metric-compatible where a distro ships it); where local() finds
     nothing the raw stack falls to Iowan Old Style / Times New Roman / Liberation Serif unadjusted.
     Iowan Old Style is macOS-only and is not on the measuring machine, so it carries no figure.
     THIS FAMILY IS NOW THE ONLY SERIF SYNTHETIC. The retired setting carried TWO - a band family for
     --text-2/--text-3 and a separate "…Fallback Hero" for --text-4, because Source Serif 4's width is a
     function of opsz and it NARROWED as opsz rose. Newsreader's width is also a function of opsz (the
     axis survives the Google Fonts subset - fvar/avar/gvar/HVAR/STAT are all present in the served woff2,
     so font-optical-sizing: auto really does move it) BUT IT WIDENS instead: measured over the
     108-character sample at wght 450, 45.0274em at opsz 27.51, 45.6840 at 34.99, 46.5373 at 44.51,
     +3.4% across the band where Source Serif 4 went the other way. The sign of the hero's correction
     flips, and the firm specified exactly two stacks, so the third family and its --font-display-hero
     token are DELETED rather than re-solved.
     WHAT THAT COSTS, REPORTED RATHER THAN QUIETLY RE-INTRODUCED AS A THIRD FACE. Against Georgia's
     47.5435em the ideal per-rung adjust is 94.71% (--text-2), 96.09% (--text-3), 97.88% (--text-4); a
     band family at the geometric mean of the first two would be sqrt(0.9471 x 0.9609) = 95.40%, and
     using that at --text-4 is 2.61% wrong - above the ~1% threshold at which a separate hero family
     earns its keep. So a balanced multi-line hero title CAN re-wrap on swap, and the fix if it ever
     shows is a third @font-face aliasing Georgia at ~97.9%, consumed by .hero-title alone. It is not
     taken here because the firm's brief is two stacks.
     THE CHECK on the firm's supplied values, shipped as given either way:
       size-adjust 92%   2.9% narrow at --text-2, 4.3% at --text-3, 6.0% at --text-4. It matches neither
                         basis: the advance mean is 95.40% and the x-height match against Georgia would
                         be 88.5% (0.48145 x 0.885 = 0.426 = Newsreader's x-height AS OS/2 REPORTS IT;
                         [REPAIR v10] on the ink figures the serif's x-height is a function of opsz -
                         0.4415 at --text-2 rising to 0.4685 at --text-4 - so there is no single
                         x-height basis to match here either, and the range it would ask for is
                         91.7-97.3%. That brackets the firm's 92% at the small end and misses it at the
                         large, which is one more reading on which the number is defensible at --text-2
                         and narrow above it - the same shape as the advance check beside it). Unlike the
                         Montserrat trio there is no reading of the number that makes it exact.
       ascent 98%        WRONG BY 22.7%, AND SHIPPED ANYWAY BECAUSE IT IS THE FIRM'S. 0.98 x 0.92 =
       descent 26%       0.9016em against Newsreader's real hhea ascent 0.735em; 0.26 x 0.92 = 0.2392em
                         against 0.265em. Newsreader's hhea ascent + descent is exactly 1.000em - the
                         tightest of any face this file has carried - and the synthetic line box these
                         values build is 1.1408em, +14.1%. So every display hook set before the swap
                         occupies a line box 14% TALLER than the one it lands in, which is precisely the
                         reflow a synthetic family exists to prevent. The pair that would match at
                         size-adjust 92% is 79.9% / 28.8%. Reported to the firm, not changed here. */
  src: local("Georgia");     /* family name = PostScript name, so one local() */
  size-adjust: 92%;          /* THE FIRM'S. Ideal against Georgia is 94.71 / 96.09 / 97.88% by rung, band mean 95.40%; 92% is 2.9-6.0% narrow */
  ascent-override: 98%;      /* THE FIRM'S. x 0.92 = 0.9016em against Newsreader hhea ascent 0.735em - see the check above */
  descent-override: 26%;     /* THE FIRM'S. x 0.92 = 0.2392em against Newsreader hhea descent 0.265em */
  line-gap-override: 0%;     /* Newsreader's hhea lineGap is 0; leading comes only from line-height */
}

/* ---- 3 · BASE ------------------------------------------------------------- */
@layer base {
  *, *::before, *::after { box-sizing: border-box; }

  /* [RED v7] 106.25%, and A PERCENTAGE IS THE WHOLE POINT - never a px override. The firm asked for the
     font everywhere to be slightly bigger, and a px value here would have thrown away the visitor's own
     setting to do it: a reader who has set 20px would be forced back to 17. A percentage MULTIPLIES what
     they chose - 20 x 1.0625 = 21.25px - so the preference survives and the instruction is still carried
     out. And because every size, space, measure, container, track and grid figure in this file is
     expressed in rem, ONE multiplier here scales the entire system as a unit: every ratio between two of
     them and every character count in the derivation comments stays exactly true, and only the px
     annotations move (x1.0625). What does NOT scale, and is re-solved case by case rather than assumed:
     the viewport, the 44px tap floor, the 16px iOS field floor, the 17px classic-scrollbar allowance, the
     1px hairlines, the ~7.2px cap-legibility floor, the reveal travel distances - and rem inside a media
     query, which resolves against the initial font-size and never sees this line (see the breakpoints). */
  html {
    font-size: 106.25%;              /* 16 x 1.0625 = 17px = --text-0 at the browser default; a PERCENTAGE so a visitor's own size is multiplied, not replaced */
    -webkit-text-size-adjust: 100%;  /* iOS must not inflate text in landscape - the scale is already derived */
    text-size-adjust: 100%;
    background: var(--bg);           /* the ground reaches the overscroll area too, so a bounce is never white */
    scroll-behavior: smooth;         /* skip link and in-page anchors; clamped to auto under reduced motion (guards) */
  }

  body {
    margin: 0;
    min-height: 100dvh;
    background: var(--bg);
    color: var(--ink);                       /* 17.73:1 on --bg */
    font-family: var(--font-body);
    /* 400, never 300. Light-on-dark antialiasing composites in gamma-incorrect space and erodes stroke
       edges; Montserrat is a geometric sans with no modulation in reserve at all - less than Titillium
       had - so Light goes grey and unstable reversed out of near-black, and 300 is exactly what the old
       site ran in its black footer. 300 belongs to the ivory band, where dark-on-light blooms it back
       up. (It used to belong to "the mark" as well; that was a Titillium Text Light relation and it went
       with the face.) Airiness comes from colour (--ink-2, --ink-3) here, not from weight. */
    font-weight: var(--wt-body);
    font-size: var(--text-0);
    /* 1.68 against Montserrat's 1.219em `normal` box: the box no longer constrains anything (it did
       under Titillium's 1.521em), so the x-height governs alone - 28.56px pitch, 3.20 x the 8.925px
       x-height, 7.84px of clear between adjacent content boxes. */
    line-height: var(--leading-body);
    letter-spacing: 0;               /* Montserrat is wide-set (0.49072em average against Titillium's 0.4179); body carries no tracking, so no display hook has to undo any */
    /* Subpixel (auto), NOT antialiased: grayscale AA thins light-on-dark type a further half-step on
       macOS, and thinning is the one thing reversed type cannot afford. The ivory band flips this,
       because dark-on-light blooms. */
    -webkit-font-smoothing: auto;
    -moz-osx-font-smoothing: auto;
    overflow-wrap: break-word;       /* a long citation URL can never force a horizontal scroll */
  }

  h1, h2, h3, h4, p, ul, ol, dl, figure, blockquote { margin: 0; }   /* rhythm comes from the space scale, set per component */

  /* Form controls inherit the 1rem body font: a field never falls below 16px (iOS Safari zooms the viewport
     on a sub-16px field focus - an ABSOLUTE px floor, so the 106.25% base does not move it; --text-0 now
     runs at 17px and clears it by a whole pixel), and it never picks up a UA face. */
  button, input, select, textarea { font: inherit; color: inherit; letter-spacing: inherit; }
  button { cursor: pointer; }
  textarea { resize: vertical; }

  a { color: var(--brand); }         /* 2.99:1 on --bg, 2.70:1 on --bg-3 (the lightest ground a link sits on) - BELOW AA at 17px, by the firm's decision of 2026-09-06; see THE AA FAILURE, RECORDED in the token block */
  strong, b { font-weight: var(--wt-strong); }

  /* Image defaults: block so no baseline gap, intrinsic ratio kept, never wider than the column. Alt text,
     if the image fails, reads as a caption (italic, --ink-3 at 5.61:1) and not as copy. */
  img, svg, video, canvas { display: block; max-width: 100%; height: auto; }
  /* [FACE v9] THIS ITALIC IS NOW SYNTHESISED. The firm's request loads Montserrat roman only (300..600,
     no ital axis), where the retired loader carried real Titillium italics at 300 and 400, and
     font-synthesis is `none` only inside the display grant below - so broken-image alt text renders as
     a slanted roman rather than as a drawn italic. Kept because the JOB is unchanged (alt text must read
     as a caption and not as copy) and a synthetic oblique still does it; recorded because it is the only
     place on the site the missing sans italic is visible today. No page ships <em>/<i>/<cite> in body
     copy; the day one does, this note is where the decision to load an italic starts. */
  img { font-style: italic; color: var(--ink-3); }

  /* Selection and focus are palette decisions. The deep stop as fill - a full-strength accent behind a
     dragged paragraph is unbearable - with white text: 6.53:1. [RED v7] --brand-deep is now the mark's own
     #BF0000, so a dragged paragraph is highlighted in the firm's literal red, and white on it went from
     5.8:1 to 6.53:1: deeper AND more legible, which is the fill half of the split stated once more.
     [RED v8] THIS DECLARATION IS THE REFERENCE THE WHOLE PASS WAS MEASURED AGAINST. The firm's instruction
     of 2026-09-06 names "the text highlight color" as the red every other red must match, and the text
     highlight is this line. It did not move; everything else moved onto it.
     #FFFFFF is THREE literals outside the token block rather than one - here, .skip-link's ink and
     .btn's ink, every one of them the ink ON --brand-fill or --brand-deep, which are now the same colour -
     and all three are deliberate: white is not a palette decision, it is the absence of one, and it is
     what a fill this deep can carry where the near-black cannot (6.53:1 against 3.04:1). There is no
     --brand-fill-ink token because a token would invite a fourth, lighter fill to reuse it. */
  ::selection { background: var(--brand-deep); color: #FFFFFF; }   /* 6.53:1 */
  /* ---- [HOVER v22] THE RING IS WHITE, AND IT IS THE ONE VALUE IN THE HOVER/FOCUS SCHEME THAT IS NOT A
     TASTE DECISION. The firm's instruction of 2026-09-06 was that the pink hover states become "the dark
     red which is the signature color for the brand"; applied to this line it is not merely unattractive,
     it is unimplementable. #BF0000 measures 2.99:1 on --bg, 2.86:1 on --bg-2 and 2.70:1 on --bg-3
     - every one of them under WCAG 1.4.11's 3:1 for the visual indicator of a component's state - and on
     a .btn, whose plate IS #BF0000, a #BF0000 ring is 1.00:1: not dim, not marginal, ABSENT, and absent
     on the one control the site treats as primary. --ink is 17.73:1 / 17.00:1 / 16.00:1 on the same three
     grounds and 5.93:1 inset on the brand fill.
     A keyboard visitor has no pointer to tell them where they are and this outline is the entire answer,
     so the ring was settled by measurement and the firm was told so rather than asked. Everything else in
     the scheme - the white text hovers, the deepened button plate - is the firm's preference honoured.
     WHY THIS AND NOT A COMPROMISE. Three were measured and all three lose to white. A red ring with a
     white halo doubles the drawn weight of every focus state on the site to rescue a colour. Keeping
     #FF7070 here alone would have left exactly the pink the firm objected to on the states a mouse user
     sees least and a keyboard user sees most. And a thicker red ring does not help at all: 1.4.11 is a
     CONTRAST floor, so 4px of 1.00:1 on a red button is still nothing.
     THE IN-BAND FIGURE IS AUTOMATIC AND STAYS AUTOMATIC. --ink rebinds to --ivory-ink inside .band--ivory
     and in print, so the ring inverts with the band and measures 17.32:1 there without a rule naming it -
     which is a property this ring did not have while it was painted in a brand stop, because that stop
     needed a rebinding of its own to be legible on ivory (it measured 2.39:1 unrebound).
     WHAT THIS BLOCK USED TO ARGUE, kept because the shape of the file depends on it: the ring was 2px of
     --brand-bright at 7.25:1 on --bg and 6.54:1 on --bg-3, and the token block called it "the one red the
     collapse did not take". Both halves of that sentence are gone - the colour is no longer red, and
     --brand-bright is no longer a token. The GEOMETRY is untouched: 2px, offset 3px, radius 1px, and 6.1's
     header-scrim paragraph re-measures where a ring lands over a photograph at the new colour. */
  :focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 1px; }   /* 17.73:1 on --bg, 16.00:1 on --bg-3, 17.32:1 in-band via the rebinding */
  :focus:not(:focus-visible) { outline: none; }

  /* Required, or hiding any grid/flex element via the attribute silently fails: an author display always
     beats the UA-origin [hidden] rule, so !important is the only reliable fix. The single !important outside
     guards - and, because earlier layers win among !important declarations, the strongest place for it. */
  [hidden] { display: none !important; }

  /* Skip link: the label voice on the brand fill, parked above the viewport by position (never display, so
     it stays in the tab order) until it takes focus. */
  .skip-link {
    position: absolute;
    top: 0;
    left: var(--gutter);
    z-index: 100;
    padding: var(--space-2) var(--space-3);
    background: var(--brand-fill);           /* [RED v7] an AREA, so the fill stop */
    color: #FFFFFF;                          /* 6.53:1 on --brand-fill; the retired --brand-ink there is 3.04:1 and fails */
    font-size: var(--text-label);
    font-weight: var(--wt-label);
    letter-spacing: var(--track-6);          /* the button rung */
    text-transform: uppercase;
    text-decoration: none;
    border-radius: 0 0 var(--radius) var(--radius);
    transform: translateY(-110%);            /* 100% of its own height plus its radius and ring */
  }
  /* THE RING IS INSET (at top: 0 a +3px ring is clipped by the viewport edge), and that changes what it
     has to contrast against: at -3px it lands wholly INSIDE the fill, so the ground plays no part and the
     site's ring is measured against --brand-fill instead of against --bg.
     [HOVER v22] THAT USED TO BE A REPAIR AND IS NOW A REFINEMENT, and the difference is worth one line
     because it changes what a future editor may safely delete. The base ring was --brand-bright, which
     measured 2.43:1 on this plate - a failure, and the whole reason this declaration existed. The base
     ring is now --ink, which measures 5.93:1 there and clears 1.4.11 with room; the override therefore
     no longer FIXES anything. It is kept because #FFFFFF is this plate's own label ink and one plate
     should carry one white, and there is still no --brand-fill-ink token for the reason base's
     ::selection note gives. Deleting it would cost nothing measurable, which is exactly why the reason
     for keeping it has to be written down rather than assumed. This is still the ONE control on the site
     whose ring lands on a fill; everywhere else the +3px default puts the ring on a ground. */
  .skip-link:focus-visible { transform: none; outline-offset: -3px; outline-color: #FFFFFF; }   /* 6.53:1 on --brand-fill, where the base ring's --ink measures 5.93:1 and the retired --brand-bright measured 2.43:1 */

  /* ---- The display-face GRANT: a closed, enumerated hook list. The serif appears here and nowhere else;
     everything else inherits --font-body from body. Listed explicitly on purpose: a substring selector like
     [class*="title"] would silently capture .section-subtitle and drag the serif onto its descendants.
     font-synthesis: none because the request is Newsreader ital,opsz,wght@0,6..72,400..600;1,6..72,400 -
     a weight of 700, or anything outside 400..600, must break visibly rather than smear a fake. THE
     LOADER WAS VERIFIED, not assumed: fonts.googleapis.com returns 11 @font-face blocks for that URL,
     Newsreader roman at font-weight: 400 600 and Newsreader italic at 400, every one font-display: swap.
     Optical sizing stays auto so opsz follows font-size (the hero at 44.51px gets the display cut, a card
     h3 at 27.51px the text cut) - and the opsz axis SURVIVES the Google Fonts subset, checked by decoding
     the served woff2's table directory, so this declaration is doing something rather than nothing.
     The width moves with opsz here too, but Newsreader WIDENS as opsz rises where Source Serif 4
     narrowed, which is why there is no longer a separate hero synthetic face (block 2 carries the
     measurement and the 2.61% it costs).
     letter-spacing: 0 is the invariant's ceiling: a hook may go negative in its own
     block, never positive. text-wrap: balance belongs to .hero-title and .display only (step 7, paired with
     a ch max-width) - never to .quote, where a citation's break points matter legally. Components may add
     hooks ONLY by adding them to this list.
     THE LIST IS THE WHOLE ANSWER TO "WHERE DOES THE SERIF APPEAR", so it holds every consumer of
     var(--font-display), including the four hooks that arrived with the components layer and used to be
     declared outside it:
       .svc-card h3, .act-card h3, .package h3   the three card headings the markup contract names as
         DISPLAY hooks. They replace ".card h3", which named a class no page ships - the components
         layer re-declared the identical property set for them (old 6.8) and two copies of a closed
         grant is a grant an edit can silently miss. Folded in here, that block is deleted.
       .act-card[data-numeral]::before           the watermark numeral (6.10). It is set in the serif at
         --text-4 and was the one consumer outside this list: font-synthesis is INHERITED and nothing on
         the .act-card path sets it, so it computed to `auto` there. Listing it takes its weight from the
         inherited --wt-body 400 to --wt-display 450 - a real step on the real 400..600 axis, so nothing
         is synthesised, and at rgba(--ink-rgb, 0.05) the numeral is texture, where 50 units of weight is
         not a visible change. A pseudo-element in this list generates nothing on its own: the numeral's
         own rule owns `content`. */
  .hero-title, .display, .svc-card h3, .act-card h3, .package h3, .quote, .stat-num,
  .mobile-menu a, .act-card[data-numeral]::before {
    font-family: var(--font-display);
    font-weight: var(--wt-display);
    font-synthesis: none;
    font-optical-sizing: auto;
    letter-spacing: 0;                        /* never positive - invariant 2 */
    text-transform: none;                     /* never caps - invariant 2 */
  }
  /* [FACE v9] THE .hero-title REBIND IS GONE. It read `font-family: var(--font-display-hero)` and pointed
     at a third synthetic family sized for --text-4 alone. The firm specified two stacks; Newsreader's
     width moves the opposite way across opsz from Source Serif 4's, so the hero rung is collapsed onto
     --font-display and the token and its @font-face are deleted (block 2 reports the 2.61% pre-swap width
     error that collapse accepts, rather than re-introducing a face to hide it). .hero-title now takes the
     grant above and nothing else, which is also why 6.24's phone rule no longer has a family to reset. */
  /* Emphasis inside display type (600 + --brand, never slant) is a component rule: hero/display block, step 7. */

  /* ---- Inverted band. CURRENTLY UNUSED. Kept deliberately: the token rebindings below are the safety net
     for any future light band (an ivory credentials strip, a letterhead section), and the reasoning is not
     obvious. Deleting it invites someone to reintroduce a light section without them. (tokens.css,
     verbatim, plus the weight step-down it points to.) */
  .band--ivory {
    background: var(--ivory);
    color: var(--ivory-ink);
    /* MANDATORY, not cosmetic. On ivory the dark-ground tokens fail WCAG AA:
       --brand #BF0000 = 5.79:1, the retired --brand-bright #FF7070 = 2.39:1,
       --ink-2 = 2.05:1, --ink-3 #928787 = 3.08:1.
       [RED v8] --brand IS THE ONE THAT STOPPED FAILING HERE, and its line stays anyway. The collapse onto
       the mark's #BF0000 made the dark-ground text stop a legal in-band value by accident - a light ground
       wants a dark accent, which is the exact inverse of the near-black case that made the same colour fail
       as text outside the band. The rebinding is kept because it is a REBINDING TO A NAMED IN-BAND TOKEN,
       not a coincidence of two hexes agreeing: --brand-ivory is where an in-band red is decided, and if the
       one-line revert to #FF3D3D is ever taken, --brand goes straight back to 3.11:1 here.
       [RED v7] --brand-fill IS THE ONE STOP WITH NO LINE HERE, and its absence is the decision, not an
       omission: --brand-ivory is re-solved to the same value --brand-fill holds, so the fill is already the
       correct in-band value and a rebinding would be a no-op that the next editor would have to verify.
       On ivory the dark/light split that forces two stops on a near-black ground has no reason to exist -
       text and fill both want a dark red - so the ladder collapses to one red in-band, and .btn keeps its
       #FFFFFF ink at 6.53:1 inside a light band exactly as it has it outside one. [RED v8] Since the whole
       dark ladder has now collapsed onto that same red, in-band and out-of-band are the same colour for the
       first time - which is a coincidence of this decision and not a licence to delete either rebinding.
       [v7 REPAIR PASS] THREE THINGS THE REBINDING LIST WAS MISSING, all of them consequences of the same
       omission - it rebound the INKS and left everything an ink lands ON at its near-black:
         THE PLATE GROUNDS. --bg-2 and --bg-3 paint .svc-card, .act-card, .package, .contact-card,
           .form-error, .form-sent, .field input and .quote-card. Inside this band the inherited
           --ivory-ink on --bg-2 measured 1.04:1 - a plate of near-black carrying near-black type. All four
           grounds now rebind to --ivory, which is exactly what @media print (6.25) has always done; that
           the print block did it and this one did not is the proof it was an omission and not a budget.
         --brand-ink. THIS ROW IS HISTORY SINCE [HOVER v22] AND IS KEPT AS THE RECORD OF A REAL DEFECT.
           This block used to rebind --brand-bright to --brand-ivory (= --brand-fill), and .btn's hover
           and :focus-visible both paired --brand-bright with --brand-ink - so in-band they recreated
           precisely the near-black-on-#BF0000 pairing the token block records as failing. The fix was to
           rebind --brand-ink to #FFFFFF. BOTH LINES ARE NOW DELETED, because both tokens are: the hover
           fill is --brand-fill-hover #8F0000 and its ink is the resting #FFFFFF, on ivory exactly as on
           the near-black, so there is nothing left for the band to correct. The reason the deletion is
           safe is that the hover no longer inverts anything - it deepens one plate - and a deepening
           reads the same on any ground. Deleting --brand-bright's line back when the token existed would
           have been the wrong repair, and the number that made it wrong is still true: a light pink plate
           on ivory is 2.39:1.
         THE HAIRLINES. --ink-rgb rebinds, so --line and --line-strong recompose automatically - but at
           the DARK ladder's alphas, which on a light ground are far too faint (0.18 gave 1.49:1). All
           three rungs are restated at the light-ground alphas, --line-control re-solved to the same
           1.4.11 floor the dark rung was solved to and against the same converged light - which is why
           [RED v8]'s brighter light moved it, 0.47 -> 0.49, in the same pass and for the same reason it
           moved the dark rung 0.36 -> 0.37. */
    --brand:        var(--brand-ivory);   /* 5.79:1 */
    /* [HOVER v22] --brand-fill-hover IS NOT REBOUND, and the reason is --brand-fill's own: an in-band
       plate wants a dark red and #8F0000 is one, so a rebinding here would be a no-op. Its white label
       measures 9.69:1 on it inside the band exactly as outside it, because an ink on a plate never sees
       the ground. The two lines that USED to sit here, --brand-bright and --brand-ink, went with their
       tokens; the paragraph above says what they were for. */
    --ink:          var(--ivory-ink);     /* 17.32:1 - and since [HOVER v22] this is the focus RING as well as the type, so the ring inverts with the band and no rule has to name it */
    --ink-2:        var(--ink-muted-ivory);   /* 5.39:1 */
    --ink-3:        var(--ink-muted-ivory);
    --danger:       var(--danger-ivory);
    --ink-rgb: 21, 9, 9;
    --bg-rgb:  243, 241, 237;
    --bg: var(--ivory); --bg-0: var(--ivory); --bg-2: var(--ivory); --bg-3: var(--ivory);
    --line:         rgba(var(--ink-rgb), 0.20);   /* 1.55:1 on ivory - the seam rung, the print block's own pair */
    --line-strong:  rgba(var(--ink-rgb), 0.35);   /* 2.29:1 - emphasis, still never a control's only boundary */
    --line-control: rgba(var(--ink-rgb), 0.49);   /* 3.48:1 unlit and 3.08:1 lit: the same non-text floor, solved in the other direction */
    /* Dark-on-light blooms: the identical paragraph reads a half-step heavier here than reversed out of near-black.
       Body weight steps DOWN inside this band (see the type tokens). */
    /* The step-down itself. The physics wants a half-step (350) and Montserrat's variable axis could now
       give it - the retired Titillium shipped as static files where the smallest step down was a whole
       one. 300 is kept anyway: it is the bottom of the range the firm requested, the whole step is what
       the light ground was solved against, and a 350 would add a fifth weight to the system for a
       difference no reader can name. TWO THINGS THIS LINE USED TO SAY AND CANNOT: 300 is NOT "the
       wordmark's own TWM weight" (that was Titillium Text Light, in the mark, not on the page), and its
       italic is NOT loaded - the firm's request carries no sans italic at any weight, so an <em> inside
       this band would be synthesised. The serif
       returns to its nominal 400 - it blooms back to ~450 optically, parity with the dark ground. Labels and
       strong HOLD 600: dark tracked caps at 600 on a light fill is the playbook's own button case, and the
       playbook's band never touched label weight. font-weight is re-declared here because descendants
       inherit the COMPUTED weight, not the token; the rebound tokens carry the rest. */
    --wt-body:    300;   /* 400 -> 300 */
    --wt-display: 400;   /* 450 -> 400 */
    font-weight: var(--wt-body);
    -webkit-font-smoothing: antialiased;   /* dark-on-light blooms; grayscale AA takes the bloom back */
    -moz-osx-font-smoothing: grayscale;
  }
}

/* ---- 4 · LAYOUT PRIMITIVES ------------------------------------------------ */
/* Two primitives carry the whole page: one width constraint, one vertical band with ONE tonal variant (a
   second starts a stripe pattern rather than a rhythm). Markup shape is invariant:
   <section class="section"><div class="container">. The gutter subtraction lives inside .container so no
   component ever sets its own page margin.
   RESTRUCTURE, DON'T STACK: when a row will not fit, the reflex is one column plus text-align: center - that
   is stacking, and it changes the page's alignment axis mid-scroll. Before writing any single-column
   fallback ask (a) does the collapse change the alignment axis relative to what sits directly above it, and
   (b) is the result taller than roughly the sum of its content? If either is yes, re-lay each item as
   [figure | label] on one row instead - it keeps the left axis, keeps figures full size, and roughly halves
   the height. Measure the widest label at its real tracked-caps size against the available track BEFORE
   choosing a breakpoint. */
@layer layout {
  .container {
    width: min(100% - 2 * var(--gutter), var(--container));   /* 100% - 55.01px, capped at 72rem */
    margin-inline: auto;
  }

  .section { padding-block: var(--space-6); position: relative; }   /* 116.52px */
  .section--tight { padding-block: var(--space-5); }                /* 72.01px - a density step, not a tone */
  /* ADJOINING-EDGE TRIMS, added 2026-09-08 on the firm's instruction: "the gap between Experience and
     Accredidations cards and The Disciplines is visually much too large; shrink it."
     WHY NOT .section--tight: that modifier moves BOTH edges of a band, so putting it on #disciplines
     would also have pulled in the gap below it (to #current-practice) and putting it on #credentials
     would have pulled in the gap above it (to #name). The firm named ONE seam. These two trim one edge
     each, so every other gap on the page is left exactly as it was.
     WHY --space-5 AND NOT A NEW NUMBER: the scale is a phi ladder (939-945), and --space-5 / --space-6
     = 4.236 / 6.854 = 0.618 = 1/phi. Stepping each side down one rung takes the measured 233.03px seam
     (116.518 x 2, the two paddings exactly - the bands are flush, no margin between them) to 144.02px,
     which is the same seam divided by phi. Inventing a pixel value would have put a number on the page
     that the ladder cannot explain. The next rung down, if the firm wants it tighter still, is
     --space-4 = 44.51px a side for an 89.02px seam. */
  .section--tight-start { padding-top: var(--space-5); }            /* 72.01px - trims the seam above */
  .section--tight-end { padding-bottom: var(--space-5); }           /* 72.01px - trims the seam below */
  .section--alt {                                                   /* the ONE tonal variant */
    background: var(--bg-2);                 /* 1.043:1 over --bg: the border does the delineating */
    border-block: 1px solid var(--line);     /* seam, not state */
  }

  /* NO `.grid` PRIMITIVE HERE, deliberately. One existed and nothing consumed it: `class="grid"` appears
     in none of the 20 pages and neither partial, while 6.9 declared the identical three-property recipe
     for the three grid classes that ARE shipped (.svc-grid, .act-grid, .package-grid). Two copies of one
     sum is the shape an edit misses, and the copy carrying the derivation was the dead one. The rule and
     its arithmetic now live once, in 6.9. Add a primitive back only if a page consumes it. */

  /* The prose measure: any running-text block is capped here (70 characters). A class, so the cap is visible
     in markup; a component that holds copy at another width states its own arithmetic. */
  .prose { max-width: var(--measure); }

  /* COMPACT NAV BAND (67rem <= width < 74rem): the nav is up but full tracking does not fit (arithmetic in
     the breakpoint tokens). The labels shed tracking before size: 0.16 -> 0.08em, gap one step down. Both
     are token rebindings, so the header component reads var(--track-nav) and var(--nav-gap) and never
     carries a query of its own. The 74rem upper bound is the only number in this file that is neither
     breakpoint; it is the "full tracking fits" row of the same sum.
     [REPAIR v10] BOTH LITERALS NOW SIT ON THE ROWS THEY ARE DERIVED FROM, where [FACE v9] left them
     37.8px and 35.2px below (and [RED v7] 8.1 and 5.4 below). 67rem = 1072px carries the compact row's
     1061.80px with 10.2px of slack; 74rem = 1184px carries the at-rest row's 1171.18px with 12.8px.
     The consequence worth knowing before touching this: the burger now persists to 1071px, so a
     1024x768 tablet in landscape gets the mobile menu. THE OVERSHOOT note in the breakpoint tokens
     carries the decision, the two exits refused, and what the change bought back. */
  @media (min-width: 67rem) and (max-width: 73.99rem) {
    :root { --track-nav: var(--track-7); --nav-gap: var(--space-2); }
  }

  /* STRUCTURAL (below 67rem = 1072px): the desktop nav is gone and every two-up structure restructures.
     Components hook their collapses under THIS number only. The TOKEN drops to 4rem (68px -> 260.4px),
     which is what the FOOTER mark takes below this number; the collapsed header does not need the
     sub-line at 6.5px - the same words sit legibly in the footer. [v7 REPAIR PASS] AND THE HEADER NEVER
     RENDERS 4rem AT ALL: 6.1 caps its mark at min(--logo-h, 3.75rem), so the header mark is 63.75px ->
     243.63px with a 6.04px sub-line cap here, not 68px -> 260.4px with 6.5px. This line describes the
     token, 6.1 describes the bar, and the two were being read as one number. Rebinding
     on :root here beats the token-layer value because `layout` is a later layer, not because it is later in
     the file. */
  @media (max-width: 66.99rem) {
    :root { --logo-h: 4rem; }
  }

  /* PHONE (below 30rem = 480px): the gutter steps down one type step; sections drop one space step (116.52px
     x 2 was 28.7% of a 812px-tall phone). The hero drops one rung in its own component under this number.
     THE GUTTER STEP IS ALSO WHY .form-grid IS PINNED HERE (6.17): stepping the gutter down hands the
     container 11.76px at exactly this width, which was enough to put the form's two OLD 12rem tracks
     back abreast at 479px. [FACE v9] The track was re-derived to 13.78rem (a 24-character e-mail is
     17.4% wider in Montserrat), so 2-up now needs 550.9px of viewport and the gutter step can no longer
     reach it - the pin closes an island that no longer exists and is kept because the sum it guards
     against has been crossed once already. The breakpoint tokens carry both arithmetics. */
  @media (max-width: 29.99rem) {
    :root { --gutter: var(--gutter-phone); }   /* 21.62px each side: 375 - 43.25 = 331.76px of column = 39.8 characters in Montserrat, below the 45-55 phone measure and unreachable at any gutter - the token's own note carries the failure */
    .section { padding-block: var(--space-5); }        /* 72.01px */
    .section--tight { padding-block: var(--space-4); } /* 44.51px */
  }

  /* LANDSCAPE PHONE: 812 x 375 matches only the structural query above and would otherwise get desktop
     padding - 2 x 116.52px on a 375px-tall screen is 62% of the viewport in padding. --space-4 (44.51px) keeps
     it under ~12% per side so a section's content reaches the screen. The hero component MUST add its
     min-height: 0 and padding step inside THIS query, not in a query of its own; and every display: none in
     a later component is checked at 812 x 375 as well. */
  @media (max-height: 480px) and (orientation: landscape) {
    .section, .section--tight { padding-block: var(--space-4); }
  }
}

/* ---- 5 · GUARDS - win by layer, not by position ---------------------------- */
/* prefers-reduced-motion means "do not move things at me", not "remove the interface's responses". The
   pre-state override is limited to the un-revealed phase so it can never outrank a hover transform on a
   revealed element, and the universal clamp resets DELAY as well as duration - clamping only duration
   converts a stagger into a staircase of instant pops spread over the full stagger window. The delay reset
   is also what neutralises the stagger: JS writes only the inherited --reveal-delay custom property,
   and this clamp overrides the transition-delay that property feeds - on the element AND on the
   ::before / ::after that add it to their own lag, which is why the selector list is all three.
   Safe only because no other layer uses !important (block 0). */
@layer guards {
  @media (prefers-reduced-motion: reduce) {
    html.js [data-reveal]:not(.is-revealed) { opacity: 1 !important; transform: none !important; }
    /* THE POINTER LIGHT IS NOT DIMMED HERE, IT IS ABSENT. A light that chases the pointer is precisely
       what "do not move things at me" means, and it is the one thing on this site that moves without
       being asked twice: a reveal happens once per element and then stops, the light follows every
       flick of the wrist for as long as the page is open. Nothing else would do - the clamp below
       flattens transitions, and the lobes are moved by transform from a pointermove handler, which is
       neither a transition nor an animation and would sail straight through it. display: none also
       stops the compositor work and lets main.js skip building the pair at all.
       Layer, not !important: guards is later than components, so this outranks 6.26's opacity rules
       without touching the file's single-!important contract (block 0). */
    .cursor-glow { display: none; }
    /* Same argument, same reason it cannot be left to the clamp: a scroll-linked transform is not a
       transition. The frame keeps its clip and its hairline; the picture simply stops drifting. main.js
       must ALSO stop publishing --parallax-y under this query; this rule is what makes it safe if it
       ever forgets.
       [ZOOM v11] `scale: none` IS NOW PART OF THIS RULE AND WAS NOT OPTIONAL. This line used to read
       `transform: none` alone and that single declaration did two jobs, because the overscan was a
       constant inside the same transform as the drift: killing the transform killed the crop with it, so
       a still page showed the photograph at its natural size rather than a permanent 4% one. 6.27 has
       had to move the overscan into its own `scale` property (6.28 needs `transform` free), and the
       moment it did, `transform: none` stopped reaching it. Without this second declaration a
       reduced-motion reader would have been left holding a 9% crop that buys them nothing - exactly the
       trade 6.27's gate (b) forbids.
       [PHOTO v16] AND THE FIRST DECLARATION IS NOW `translate: none`, BECAUSE THAT IS WHERE THE DRIFT
       LIVES. 6.27 moved it out of `transform` so 6.29's hover push could not multiply it; a guard that
       went on naming `transform` would have been three words of correct-looking text neutralising a
       property nothing declares any more, and the scrub would have survived under this query. Named for
       the property, checked in the browser: computed `translate` reads `none` on all three frames with
       the query on.
       `will-change: auto` IS THE THIRD DECLARATION AND IT CLOSES A LEAK THIS RULE USED TO LEAVE OPEN.
       6.27 promotes every framed image with `will-change: translate` and calls the promotion "permanent
       by design: the scrub runs for the life of the page". Under this query the scrub does NOT run for
       the life of the page - the two declarations above freeze it and main.js's initParallax stops
       publishing --parallax-y - so what was left was one permanent compositor layer per framed image, on
       a desktop reader who has asked for less, for an element nothing will ever move. That is the same
       cost the reveal system's finish() is careful to hand back (it sets will-change: auto the moment an
       element has arrived, and s7.4 argues the release is load-bearing), so the standard already exists
       in this file and this rule was simply not applying it. Three properties, one rule, natural size and
       no layer, as it ships today. */
    html.js [data-parallax] img { translate: none; scale: none; will-change: auto; }
    /* [ZOOM v13] THE ZOOM-OUT (6.28), AND WHY THE CLAMP BELOW IS STILL NOT ENOUGH ON ITS OWN. The clamp
       does neutralise animations - it is the one thing that separates this case from the scrub above,
       which is neither a transition nor an animation and can only be stopped in JS. But it neutralises
       them by collapsing animation-duration to 0.001s, and a 0.001s run of a `both`-filled animation
       does not leave the picture at rest: it SNAPS it to the end value and holds it there.
       WHAT THE REVERSAL FIXED HERE, AND IT IS WHY THIS NOTE IS NOW SHORT. Until today the end value was
       1.19, so the clamp handed the readers most entitled to see no change at all the one framing
       nobody else ever rested at - a 9.5% crop per edge, reachable otherwise only by sitting through the
       24 seconds of motion they had asked not to see. The end value is now 1.09, which IS 6.27's
       overscan and the frame every zoomed hero comes to rest on, so the clamp and this rule finally
       agree about which frame is the resting one: at worst the clamp leaves the picture exactly where a
       moving page leaves it, instead of somewhere no page does. The rule stays explicit anyway, because
       "the same crop as everyone else" is not "no crop", and because a guard should state its intent
       rather than inherit it from whatever an animation's `to` happens to be this month.
       HOW IT BEATS THE CLAMP, precisely: there is nothing to beat. The clamp marks animation-duration,
       animation-delay and animation-iteration-count !important and never touches animation-name; the
       shorthand here sets animation-name: none, which is uncontested, and an animation with no name does
       not run whatever its duration says. `scale` is not in the clamp at all - it is neither a
       transition nor an animation property - so the ordinary cascade decides it: same layer, later
       source position, and 0,1,1 against the clamp's 0,0,0.
       `scale: none` AND NOT `scale: 1.09`. Natural size is what a reduced-motion reader gets everywhere
       else on this site: the more specific rule above already gives it to every parallaxed hero, and
       below the 67rem gate or with JS off 6.27 declares no scale at any time, so there is no resting
       crop here to preserve. `scale: 1.09` would INTRODUCE one - 4.5% per edge on every page whose hero
       is a photograph (6.28 says why that set is named rather than counted), for precisely the readers
       this rule exists to protect. Smaller than the 9.5% the old end value threatened, and still a crop
       bought with no motion at all, which is the trade 6.27's gate (b) forbids by name. The more
       specific rule above says the same thing for the parallaxed case, so the two agree rather than
       race. */
    .hero-media img { animation: none; scale: none; }
    /* [PHOTO v16] 6.29's HOVER PUSH, AND THE CLAMP GENUINELY DOES NOT CATCH IT - MEASURED, NOT ASSUMED,
       because the brief that ordered 6.29 asked for the check rather than the assumption. Under this
       query, hovering /real-estate's first card BEFORE this rule existed: transitionDuration 0.001s (so
       the clamp fires), transform `none` at rest, and matrix(1.028, 0, 0, 1.028, 0, 0) 120ms after
       pointerenter and for as long as the pointer stayed. The clamp collapses DURATION, and a 0.001s
       transition does not cancel a state change - it SERVES THE END VALUE INSTANTLY. That is the same
       failure the .hero-media rule directly above exists for, one property along, and it means a
       reduced-motion reader was getting the 2.8% step as a SNAP: the loudest possible form of it.
       SO THIS IS NOT NEW CAUTION, IT IS A REPAIR. Those three /real-estate cards have snapped on hover
       under this query since the site was built, invisible only because they were the site's only framed
       cards without a video (the six on the home page are already still here - see the note below).
       6.29 would have extended the defect to every framed photograph on the site.
       `scale`, BECAUSE THAT IS THE PROPERTY 6.29 PUSHES. The [data-parallax] rule two above already says
       this for the two .feature-media figures whenever html.js is present and the attribute is on them;
       this rule covers .svc-card-media img, which carries no [data-parallax] and is missed by that
       selector entirely, and covers .feature-media img on the paths the other rule cannot reach (no
       script, or the attribute removed). Both say the same thing, so they agree rather than race.
       LAYER, NOT !important: guards is later than components (block 0), so this outranks 6.29's hover at
       any specificity without touching the file's single-!important contract.
       THE RING IS DELIBERATELY NOT NEUTRALISED. It is a colour, and the clamp will serve it in 0.001s,
       which is a colour change and not a movement. "Do not move things at me" is not "do not show me a
       red line", and removing it would take the whole of the firm's instruction away from a reader who
       asked only for the moving half of it to stop. */
    .svc-card-media img, .feature-media img { scale: none; }
    /* [VIDEO v14] THE PRACTICE-AREA HOVER VIDEO IS ABSENT HERE, NOT FADED - the same distinction the
       pointer light gets four rules up, and for the same reason: the clamp below flattens TRANSITIONS,
       and what it would flatten here is the opacity fade, not the thing that moves. A 0.001s fade to a
       PLAYING video is the loudest possible answer to "do not move things at me" - it hands a
       reduced-motion reader the footage instantly instead of over 1.275s. display: none is the only
       declaration that reaches the motion itself, and it also stops the decode and the compositor work,
       which a dimmed video would go on paying for.
       LAYER, NOT !important, and NOT A SUBSTITUTE FOR THE JS GATE. guards is later than components, so
       this outranks 6.9a without touching the file's single-!important contract (block 0). main.js
       re-asks prefers-reduced-motion on the shared reducedMQ and pauses on a live change, which is the
       half that actually stops a clip already running; this rule is what makes the element vanish in the
       same instant rather than a frame later, and what covers the case where main.js has not been reached.
       [v15] THAT SENTENCE WAS ONLY HALF TRUE UNTIL v15 and the half that failed was this rule's problem
       too. main.js block 14 sat below the file's one early return, so a page LOADED under reduced motion
       never built the feature at all: turn reduced motion off mid-session and this display: none lifted,
       6.9a faded an undecoded <video> to opacity 1, and nothing was ever going to call play() - while the
       subtraction below still held the picture still. Six cards with no hover response of either kind.
       main.js now calls block 14 from both sides of that gate, which is what makes "re-asks on the shared
       reducedMQ" mean the same thing on both paths.
       THE 1.028 PUSH DOES NOT COME BACK, AND THAT IS THE POINT RATHER THAN AN OVERSIGHT. display: none
       does not hide an element from :has() or from a modifier class, so 6.29's --photo-push: 1 still
       applies and the card's picture is STILL. Restoring the push here would re-introduce a 2.8% size
       step - which the clamp below would then serve INSTANTLY, as a snap rather than a movement - for
       precisely the readers who asked for less. The card keeps the border, the lamp, the lift and the
       arrow; the picture holds. Nothing else in this block adds motion, and this rule does not either.
       [PHOTO v16] AND THE ARGUMENT NO LONGER DEPENDS ON THE MARKER AT ALL: the rule directly above sets
       `scale: none` on every framed photograph under this query, so these six are still whether or not
       anything subtracts their push. What the marker still buys here is that the two mechanisms agree
       instead of one silently rescuing the other. */
    .svc-card-video { display: none; }
    /* [MOTION v21] EVERY REMAINING HOVER DISPLACEMENT, AND THIS RULE IS THE GENERALISATION OF THE FIVE
       ABOVE IT RATHER THAN A SIXTH SPECIAL CASE. The three rules directly above each rediscovered one
       instance of one argument - THE CLAMP COLLAPSES DURATION AND A 0.001s TRANSITION DOES NOT CANCEL A
       STATE CHANGE, IT SERVES THE END VALUE INSTANTLY - and each stopped at the element that happened to
       be in front of the editor. A render pass then measured the rest of the file under this query, with
       a real pointer rather than a forced pseudo-state, sampled at +120ms and again at +2.3s (identical
       at both, i.e. an instant jump that then holds):
         .svc-card              translate: 0 -4px      SNAPPED
         .btn                   matrix(1,0,0,1,0,-4)   SNAPPED
         .link-arrow svg        translate: 4px         SNAPPED
         .stat--linked figure   translate: 0 -4px      SNAPPED
         footer links, x4       translate: 4px         SNAPPED, on ~30 links per page
         .act-card::before      translate: 0 -8px      SNAPPED
       Six families, all of them movements, none of them caught. That is the loudest possible form of
       every one of them, handed to the readers who asked for less, and it has been true since the site
       was built - [MOTION v20] only made it louder by taking the nudge 3px -> 4px.
       NAMED, NOT UNIVERSAL. `translate: none` on `*` would reach the reveal system's own pre-state and
       the parallax default in the same stroke, so the list is enumerated; each name is a selector that
       exists one layer up and nowhere else, and the properties are the two those blocks actually declare
       (`.btn` is the file's one hover still on `transform`, because 6.18's magnetic suppression depends
       on an inline `transform` outranking it - see the note there).
       WHAT IS DELIBERATELY ABSENT, AND IT IS THE SAME LINE 6.29 DREW FOR THE RED RING: A RULE THAT DRAWS
       IS A STATE INDICATOR ARRIVING, NOT AN OBJECT MOVING. The nav's ::after, .link-arrow's ::after and
       .svc-card-link's background all grow a hairline from a fraction of its width to full; served
       instantly they read as "the underline is present", which is exactly what a plain text-decoration
       does on any link anywhere, and is the resting appearance of the hovered state rather than a
       displacement of anything. Removing them would take the affordance away from a reader who asked
       only for the moving half of the interface to stop - the same judgement the rule above makes about
       the ring, and the same one 6.10's colour changes get. Colour-only responses stay for that reason
       too, and are not listed here.
       THE ONE THING THIS RULE CANNOT REACH is an inline transform, so blocks 10 and 12 of main.js
       (the pointer light and the magnetic CTA) are torn down in JS on the shared reducedMQ instead. Both
       live below main.js's one early return, so on a page LOADED under this query neither is ever built. */
    .svc-card, .btn,
    .link-arrow svg,
    .stat--linked .stat-num, .stat--linked svg,
    .footer-nav a, .footer-social a, .footer-contact a, .footer-payments a,
    .act-card[data-numeral]::before { translate: none; transform: none; }
    /* [STAT v24] 2026-09-07. THE STAT NUMERAL'S POP, SUBTRACTED - AND ONLY THE POP. 6.12 gives the two
       figures in /attorneys' stats row a hover that does two things: --brand and `scale: var(--pop)`.
       The scale is a displacement and belongs in this block with the six families above it, for the
       identical reason - the clamp at the foot of this rule collapses DURATION, and a 0.001s transition
       does not cancel a state change, it serves the end value INSTANTLY, so an unsubtracted pop would
       reach a reduced-motion reader as a 6% SNAP, the loudest possible form of it.
       THE COLOUR MUST NOT GO WITH IT, and that is not a nicety. This block's own preamble says
       "colour-only responses stay", and 6.12's [HOVER v23] is the record of what it costs to leave an
       object answering the pointer with NOTHING under this query - it happened to the Avvo badge and had
       to be repaired. These two numerals are the loudest figures on the page and the firm asked for a
       response on them by name; red survives here, the pop does not.
       A SEPARATE DECLARATION RATHER THAN A SEVENTH NAME ON THE LIST ABOVE, because that list declares
       `translate` and `transform` and this element declares neither - folding it in would either widen
       that rule's declaration onto six elements that do not scale, or quietly leave the scale standing.
       One property, one selector, one line. */
    .stat-num { scale: none; }
    /* [LIST-HOVER v25] 2026-09-07. THE BULLET GESTURES, SUBTRACTED THE SAME WAY AND FOR THE SAME
       REASON: the clamp below collapses DURATION, and a 0.001s transition does not cancel a state
       change - it serves the end value INSTANTLY. Unsubtracted, a reduced-motion reader would get a
       50% SNAP on every diamond they passed over and a 4px jump on every row they read.
       WHAT SURVIVES IS THE COLOUR, and it is what makes this subtraction affordable: the diamond still
       goes to --ink and the .num-list hairline still goes to --brand-fill, so neither list is left
       answering with nothing. That is this block's "colour-only responses stay" applied deliberately
       rather than by default - the marks were GIVEN a colour channel so that this line could take the
       movement away without taking the response with it.
       THREE SELECTORS AND NOT THREE MORE NAMES ON THE LIST ABOVE, because that list declares
       `translate`/`transform` on ELEMENTS and these declare `scale` on two ::before pseudos. */
    .check-list li { translate: none; }
    .check-list li::before, .num-list li::before { scale: none; }
    *, *::before, *::after {
      animation-duration: 0.001s !important;
      animation-delay: 0s !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.001s !important;
      transition-delay: 0s !important;
      scroll-behavior: auto !important;
    }
  }
}

/* ---- 6 · COMPONENTS (Build order step 7) ---------------------------------------------------------
   Every component is declared ONCE, in its own block, with its hover declared exactly once inside that
   block and gated positively on :where(html:not(.has-touch)) - ON by default, subtracted only when
   touch is proven, so a failed detector costs a touch user one stray hover style rather than costing a
   desktop user the page. Every :hover has a matching :focus-visible / :focus-within OUTSIDE the gate in
   the same block, deliberately quieter: focus indicates POSITION, hover indicates ATTENTION.
   [MOTION v21] WHERE THE `transition` ITSELF GOES, WHICH LOOKED LIKE AN INCONSISTENCY IN A REVIEW AND IS
   A CONSEQUENCE OF THE LINE ABOVE. 6.29 and 6.9a declare their transitions INSIDE the gate, on the stated
   principle that "a device that has proved itself touch never reaches either state and need not carry the
   machinery". 6.16's .stat--linked and 6.22's footer links declare theirs OUTSIDE it. The two are not in
   disagreement: 6.29 and 6.9a have NO focus rule at all (6.29 records that as a decision), so inside the
   gate is the only state they have; .stat--linked and the footer links DO have focus parity, and a focus
   response is not gated on the pointer kind, so a transition declared inside the gate would leave the
   keyboard state snapping on exactly the devices most likely to have a keyboard attached. THE RULE, said
   once: a transition may live inside the pointer gate if and only if hover is the only state that uses it.
   AFFORDANCE vs MATERIAL, decided per component before styling it: .svc-card, .btn, .link-arrow,
   .stat--linked, the nav and .crumbs are AFFORDANCE (lift, brand edge, a drawn rule); .act-card,
   .package, .contact-card, .feature-media and .cta-media are MATERIAL.
   [PHOTO v16] WHAT "MATERIAL" MEANS NARROWED TODAY, AND THE OLD WORDING IS REPLACED RATHER THAN AMENDED
   BECAUSE IT NOW ASSERTS THE OPPOSITE OF WHAT SHIPS. It used to read "(no lift, no brand edge - an
   <article> with no link must not teach a click that does not exist)", and it went on to make the
   register the test for parallax eligibility: "a frame that answers to hover may not also scrub". 6.29
   now gives EVERY framed photograph on the site a hover push and a brand-red edge, on the firm's
   instruction of 2026-09-06 ("every static photo throughout the website should have the same
   animation... a slight zoom in with mouse hover and a red box around the photo"), and .feature-media is
   a framed photograph. Both halves of that sentence therefore had to be restated on ground that survives
   the instruction:
     [CARD-EDGE v36, 2026-09-08] THE CLAUSE "DOES NOT WARM ITS OWN BORDER" IS OVERRIDDEN FOR .act-card ON
     THE FIRM'S INSTRUCTION, AND THIS IS THE SECOND TIME THE FIRM HAS OVERRIDDEN THIS ONE SENTENCE. The
     wording that stood here until today read, verbatim: "MATERIAL STILL MEANS NO LIFT AND NO POINTER
     CURSOR. The SURFACE does not rise, does not warm its own border, does not become a click target and
     does not change the cursor. That is unchanged for all five, and it is the whole of what .package and
     .contact-card get." The instruction that unseats the middle clause, verbatim, 2026-09-08: "Also the 6
     cards need to have a subtle mouse interaction...maybe the red box around the card only." It is the
     same shape as 2026-09-06's photograph instruction quoted three lines up, and it is answered the same
     way - the register is RESTATED on ground that survives it, rather than the instruction refused or the
     rule quietly broken:
       MATERIAL STILL MEANS NO LIFT AND NO POINTER CURSOR. The SURFACE does not rise, does not become a
       click target and does not change the cursor. It MAY warm its own border where the firm has asked
       for that gesture by name - the framed photograph since 2026-09-06, .act-card since 2026-09-08 -
       and the limiting word is the firm's own: the red box around the card ONLY. No translate, no
       cursor, no stretch overlay, nothing that promises a destination.
       .package AND .contact-card ARE UNCHANGED and are now the whole of what the old sentence still
       describes: no lift and no WARM edge, because nobody has asked them for one. Stated precisely,
       because "the edge" is two different things and the sentence used to blur them: both of those
       plates have carried `transition: border-color var(--dur-base) ease` and
       `:focus-within { border-color: var(--line-strong) }` since they were written (6.13, 6.16). A
       NEUTRAL edge marking where the keyboard is was never what the register forbade - what it forbade
       was the plate reaching for the ACCENT under a pointer, which is the thing the firm has now asked
       .act-card for and has not asked of these two.
     THE PATTERN IS WORTH NOTICING RATHER THAN ABSORBED TWICE IN SILENCE. Both overrides come from one
     instinct - the firm wants the pointer answered by a red edge on the thing under it - and both times
     the answer has been to restate this sentence. A third request of the same shape should be read as
     evidence that "does not warm its own border" was this file's preference and not a law of the design,
     and the register should then be rewritten around the test that has never moved: DESTINATION, four
     paragraphs down.
     THE SCOPE, SAID PLAINLY BECAUSE THE FIRM ASKED ABOUT SIX CARDS AND WILL GET SEVENTEEN. .act-card is
     used by /attorneys (five), /business (six) and /employment-law (six) - seventeen plates on three
     pages. The rule is GLOBAL and deliberately so: a page-scoped one would leave three pages in permanent
     disagreement about what one component does, which is the trap this file has refused before. If the
     firm wants the edge on /business alone, that is a different and worse change and it is theirs to
     choose knowingly; it has been put to them in those terms rather than decided here.
     [MOTION v21] .act-card IS NAMED SEPARATELY NOW, BECAUSE THE SENTENCE ABOVE USED TO INCLUDE IT AND A
     REVIEW WAS RIGHT THAT THE NARROWED WORDING DID NOT COVER WHAT IT SHIPS. .act-card:hover moves its
     watermark Roman numeral 8px and lifts it from 5% to 9% alpha, on a surface with no link, and has done
     since the component was written; 6.10 argues it as DEPTH RATHER THAN INVITATION and that argument is
     accepted here rather than reopened. What was missing was a clause in the register that admits it, so
     here is the clause: A DECORATION PAINTED ON A MATERIAL SURFACE MAY ANSWER THE POINTER PROVIDED THE
     SURFACE DOES NOT. It is the same permission the paragraph below already gives a framed photograph
     inside a MATERIAL frame, one object class along - the ring and the push reach the PICTURE and never
     the plate; the numeral is a ::before and never the plate either. The travel is 2 x --nudge, which is
     the same relation it has always had to the token (it read 6px when the nudge was 3px), and it is
     deliberately larger than a control's nudge because it is parallax depth rather than a response: the
     thing that moves furthest is the thing furthest from the reader. VERIFIED under a real pointer when
     that clause was written: the .act-card surface itself measured NO CHANGE - no translate, no border,
     no cursor. [CARD-EDGE v36] THE BORDER TERM OF THAT MEASUREMENT IS SUPERSEDED BY THE FIRM'S 2026-09-08
     INSTRUCTION and the plate now warms its own edge (6.10a); the numeral's own permission is untouched,
     because "a decoration may answer provided the surface does not" was never the reason the surface
     stayed still - the reason was that nobody had asked it to move, and now they have asked for exactly
     one property of it. What still measures NO CHANGE on the surface is the pair that matters: no
     translate, no cursor.
     THE PHOTOGRAPH INSIDE A MATERIAL SURFACE NOW ANSWERS THE POINTER, because the firm asked every
     photograph on the site to. The answer is confined to the PICTURE and to a ring drawn at the picture's
     own edge; it never reaches the plate, the type, the cursor or the component's own boundary.
     WHAT STILL SEPARATES THE TWO REGISTERS IS WHETHER THE SURFACE LEADS ANYWHERE. .svc-card has a link
     and rises; .feature-media has none and does not move. That test is about DESTINATION, not about
     motion, which is exactly why it survives a photograph that moves inside a surface that does not.
     PARALLAX ELIGIBILITY IS NO LONGER DECIDED BY THE REGISTER, and on inspection it never really was:
     the real test is PROPERTY OWNERSHIP. A frame may scrub and zoom together only if the two effects do
     not contend for one property - the drift keeps `transform`, the push takes `scale` - which since
     6.28 split them they do. 6.27 and 6.29 both carry the restated rule.
     THE HONEST COST, SAID ONCE AND NOT BURIED: a hover response on a non-clickable photograph does
     suggest clickability, and .feature-media does not lead anywhere. A visitor who moves the pointer onto
     the portrait on /attorneys and sees it answer has been told something that is not true. That is the
     firm's instruction and it is theirs to give; it is recorded here rather than argued away.
   NO box-shadow anywhere, animated or static: on a ground at L 0.0038 a blurred dark shadow is
   invisible, so hairlines delineate. NO colour literal: every alpha composes from --bg-rgb / --ink-rgb
   / --brand-rgb. Every derived number carries its arithmetic. NO SCROLL STAGGER IS AUTHORED HERE: the
   delays in this layer are fixed intra-object lags (--lag-rule, the LIST numeral's 2 x --lag-rule, and
   the quote bar's 0.27s = 0.18 x 1.5), each added to the inherited --reveal-delay that carries the group
   cascade (guards 7.4) so the lag is measured from its own object's arrival rather than from the batch's.
   [MOTION v20] THAT SENTENCE USED TO READ "NO STAGGER IS AUTHORED HERE" AND THE QUALIFIER IS NEW, because
   6.2 now authors one cascade outright: the mobile menu's 13 rows arrive in sequence, and their delays are
   written here in CSS rather than published by main.js. The rule the qualifier preserves is the one that
   mattered - JS owns a cascade whose ORDER IS ONLY KNOWN AT RUNTIME (which elements entered the viewport
   together, in what order, in a batch of what size), and it publishes it as one inherited custom property
   because nothing else reaches a ::before. The mobile menu is the opposite case in every respect: it is a
   fixed panel, its rows are in document order, their count is a fact about site-plan.json, and no observer
   is involved at any point. A number that is knowable at authoring time and is written at runtime instead
   is a dependency bought for nothing. 6.2 carries the arithmetic and the cap.

   CHROME VOCABULARY - this block defines it; src/partials/{header,footer}.html must match it exactly.
   build.mjs writes no classes beyond the {{nav:*}} rows, and navList's class argument lands on the
   <a>, never on the <li> (build.mjs:45) - so NO <li> hook can come from a macro. That is why the item
   rules below are written as `.nav-links > li` and why `.nav-item` is retired: it was a class the
   generator can never emit. `.nav-item--menu` survives because the header partial hand-writes that one
   <li> itself.
     .site-header[.scrolled] > .header-inner            (NOT .container - see 6.1)
       > a.site-logo > img
       | nav.site-nav > ul.nav-links
           > li.nav-item--menu                          FIRST child of the <ul>, before {{nav:header}}
               > button.nav-menu-btn[aria-expanded][aria-controls="practice-menu"]
               + div.nav-menu#practice-menu > ul > li > a
           > li > a                                     the {{nav:header}} rows, unclassed
       | a.btn.nav-cta                                  .btn only - NO .btn--small (see 6.18)
       | button.nav-toggle[aria-expanded][aria-controls="mobile-menu"] > span
     .mobile-menu#mobile-menu[.is-open]                a SIBLING of .site-header, never a child (6.1)
       > nav[aria-label="Mobile"] > ul > li > a        TWO unclassed <ul>s in the one <nav>:
                                                       {{nav:header}} then {{nav:practice}}, 5 + 8 rows
       + .mobile-menu-contact
     .site-footer > .container > .footer-grid > .footer-col
       > img.footer-logo | p.footer-blurb | p.footer-head | ul.footer-nav
       | address.footer-contact | ul.footer-social
     .site-footer > .container > .footer-bottom        a SIBLING of .footer-grid, not a grid item
       > p.footer-legal | p.footer-disclaimer | p.footer-payments
   WHY THE DISCLOSURE <li> IS WRITTEN FIRST: the button stands exactly where the macro's own PRACTICE
   AREAS row stands, so the five labels keep their site-plan order under BOTH branches of 6.1's gate.
   THE CLOSED STATE IS CSS'S, AND ONLY CSS'S. Neither panel may carry the `hidden` attribute: base's
   [hidden]{display:none!important} outranks every opener here, and main.js writes no attribute. The
   closed state IS the opacity / visibility / pointer-events triple on .nav-menu (6.1) and .mobile-menu
   (6.2); JS's whole contribution is [aria-expanded] on the button and .is-open on the panel, either of
   which opens it. NO-JS IS A DIFFERENT HEADER, not a disabled one: 6.1's three gating rules swap the
   disclosure for the macro's own PRACTICE AREAS link and keep .nav-links visible at every width, so
   neither panel is ever the only route to anything. */
@layer components {

  /* ---- 6.1 SITE HEADER -------------------------------------------------------------------------
     --header-h is a derived layout constant, not a new design token: the 3.75rem logo (63.75px) + 2 x
     --space-2 = 97.75px. It lives on :root because .hero-content must read it to clear the fixed bar.
     Both halves are rem, so 5.75rem is still exact at the 106.25% base; only the px moved.
     LOGO CAP 3.75rem. DESIGN-SYSTEM s9 hands this to the header component. The shipped five labels -
     PRACTICE AREAS / ATTORNEYS / FLAT FEE SERVICES / MAKE A PAYMENT / CONTACT US = 64 chars - are the
     string the foundation's breakpoint block now derives on too, so there is ONE frame of reference and
     these two blocks can no longer disagree.
     [FACE v9] RE-MEASURED IN MONTSERRAT, per label, getBoundingClientRect at --text-label 13.36px /
     --wt-label 600, at the re-derived tracking ladder. These are measurements, not old figures scaled:
       compact (--track-7 0.08em, 4 x --space-2 gaps)   nav labels 622.98px  + 68.00 gaps + 10.69 caret
       at rest (--track-5 0.16em, 4 x --space-3 gaps)   nav labels 691.39px  + 110.04 gaps + 9.62 caret
       the tel button ("Call (916) 932-7338")           222.24px   (label 165.23 at --track-6 0.12em +
                                                        2 x --space-3 55.01 + 2 x 1px border)
     THE RIG WAS CONTROLLED BEFORE IT WAS TRUSTED: the same measurement run on Titillium 600 reproduces
     the figures this block carried before the face change - compact 594.36 + 68 + 9.62 = 671.98 against
     the shipped 671.95, at rest 662.78 + 110.04 + 8.55 = 781.36 against 781.31. Agreement to 0.03px on
     the retired face is what makes the Montserrat rows worth acting on.
     PREVIOUS SETTING, for the record: 671.95 / 781.31 in Titillium at the retired ladder.
     Row = 27.51 gutter + logo + 44.51 clearance + nav + 27.51 gutter + 17 classic-scrollbar allowance.
     The logo widths are measured, and come out ~0.7px under the artwork's nominal 3.830 (the shipped
     asset renders at 3.8216 - a fractional viewBox against an integer intrinsic size), which is why they
     differ slightly from the token block's 309.3 / 260.4 / 244.2:
       logo 3.75rem (243.63px) compact = 1061.8  -> the compact nav needs 67rem (1072px).
       logo 3.75rem (243.63px) at rest = 1171.2  -> full tracking needs 74rem (1184px).
     [REPAIR v10] AND THOSE ARE NOW THE SHIPPED LITERALS. Two passes wrote "the nav does not fit" here
     and held 64rem / 71rem anyway; this pass moved the literals onto the rows instead. The paragraph
     that used to stand here is kept in outline because it is still the argument for the tracking
     ladder: Montserrat 600 is 21.2% wider than Titillium 600 across these five labels (554.58 against
     457.56px untracked), so at the SHIPPED ladder the rows would have been 1129.1 and 1238.5; re-deriving
     the ladder from the 2020 raster (0.40em -> 0.32em anchor, nav shedding to 0.08em compact and 0.16em
     at rest) is the tightest tracking this file can defend and pays two thirds of that back, to 1061.8
     and 1171.2. What has changed is only what happens to the remainder: it is no longer charged to the
     mark, because the query now starts where the row fits.
     WHAT SHIPS, AND WHAT IT COST, MEASURED ON THE SERVED PAGE AT 4px STEPS FROM 1000 TO 1440:
       before   1024 -> mark 207.84px against its natural 243.63 (14.7% narrow, sub-line cap 5.15px)
                1136 -> mark 210.50px (13.6% narrow); full size returning only at 1060 and at 1172
       after    the mark measures 243.63px at EVERY width at which the nav is up; object-fit never binds
     .site-logo is still `flex: 0 1 auto; min-width: 0` with `max-width: 100%; object-fit: contain` on
     its img, so nothing overflows or clips and the guard is still there - it simply has no shortfall
     left to absorb in this component. The cost is that the burger persists to 1071px (a 1024x768 tablet
     in landscape now opens the mobile menu), which is the honest reading of a row that needs 1061.8px
     of chrome plus its gutters. The two exits NOT taken:
       (ii)  shed a nav label in site-plan.json. "FLAT FEE SERVICES" is 153.95px compact plus one 17px
             gap: the compact row would fall to 890.85px and the at-rest row to 971.56px, the cheapest
             exit in pixels and the only one that keeps the nav up at 1024. REFUSED as out of this
             file's authority - which routes sit in the primary nav is the firm's information
             architecture, and a stylesheet may not drop one to make its own sum come out.
       (iii) cap the header mark - AND THIS ONE NEVER WORKED. 3.625rem was the answer under [RED v7];
             the cap that would have fitted 1024 outright is 3.167rem, sub-line caps 5.1px against a
             ~7.2px legibility floor. Recorded so it is not re-proposed.
     THE CARET is why the compact row is as tight as it is, and it now costs MORE, not less. Under html.js
     the macro's PRACTICE AREAS row is swapped for .nav-menu-btn carrying the SAME 14 characters at the
     same size and tracking (so the label costs nothing new), plus the ::after caret. Its em is the
     button's own --text-label 13.36px, and its margin-left cancels the label's trailing letter-spacing
     per block 0's trailing-space rule - the caret abuts a tracked label, so shedding tracking makes the
     caret's own gap wider:
       compact  0.38em box 5.08 + (0.5 - 0.08)em margin 5.61 = 10.69px of the 701.67px nav
       at rest  0.38em box 5.08 + (0.5 - 0.16)em margin 4.54 =  9.62px of the 811.05px nav
     Without the cancel the compact row would be 1062.9 rather than 1061.8: the cancel is worth 1.07px at
     this ladder (it was 4.5px at the old one, because there was more trailing tracking to cancel), and it
     is still the reason to re-run this sum before any label change ships.
     So the header does not guarantee the sub-line's ~6.0px caps - the trade the foundation already made
     for the collapsed header ("the same words sit legibly in the footer"). --logo-h at its full
     4.75rem is consumed by the FOOTER mark, where nothing competes for the row. min() not a query: below
     67rem layout has already rebound --logo-h to 4rem, so one declaration covers both bands. */
  :root { --header-h: 5.75rem; }   /* 3.75rem logo (63.75px) + 2 x --space-2 (17px) = 97.75px = 5.75rem */

  .site-header {
    position: fixed; inset: 0 0 auto 0;
    z-index: 90;   /* above .mobile-menu (80) so the toggle stays clickable over the open overlay;
                      below .skip-link (100, base) so its ring is never clipped by the bar.
                      90 > 80 ONLY BECAUSE THE OVERLAY IS A SIBLING of this element, not a child of it.
                      position: fixed + z-index makes this a stacking context: nested, .mobile-menu's 80
                      would resolve INSIDE it against .header-inner's z-index auto, the 0.98-opaque plate
                      would paint over the row, and a tap on the burger would hit a menu link instead of
                      the only close control (measured that way at 390 x 844 before the partial moved it
                      out - see header.html's overlay comment). If it is ever nested again this
                      comparison stops happening and .header-inner needs a z-index above 80. */
    background: rgba(var(--bg-rgb), 0); border-bottom: 1px solid transparent;
    transition: background-color var(--dur-base) ease, border-color var(--dur-base) ease;
  }
  /* The scrim: hero imagery can be bright at the top and the nav must stay readable over any image on
     any page. Height 190% so the fade completes below the bar, not at its edge; every stop is the
     page's own ground, never black - a veil in a foreign colour reads as a filter. WORST CASE against
     a pure-white pixel: the row occupies 0-52.6% of the gradient (96 of 182px), where alpha never
     drops below 0.72. THE COMPOSITE IS PER CHANNEL and that model is the authoritative one here -
     --bg is (14, 12, 11), not a grey, and collapsing it to grey-13 moves the answer:
       0.72 x (14, 12, 11) + 0.28 x 255 = (81.5, 80.0, 79.3) -> L 0.0808, so --ink (L 0.9041) is 7.29:1.
     (The grey-13 shortcut this comment used to run gives L 0.0818 and 7.24:1; the 7.04:1 recorded
     before matched neither, and every version of the error was in the safe direction.)
     It fades out as the scrolled ground fades in, so the two never stack toward opacity.
     AND WHERE A FOCUS RING ACTUALLY LANDS, measured rather than assumed, because 0.72 is the alpha at the
     row's BOTTOM EDGE and nothing focusable reaches it. This paragraph exists because the --brand
     current-page label is documented to the digit two blocks down and the RING over the same pixels was
     not, and a silence next to that much arithmetic gets misread as a number. Base's ring is 2px
     --ink at outline-offset 3px, so a control's ring bottom is its border box + 5px. Measured on
     dist/index.html (Chrome, getBoundingClientRect): the lowest ring in the row is the LOGO link's, at
     85.75px of a 185.72px gradient - 46.17%, where the ramp from 0.88 has only fallen to 0.7406, and
     --ink against a pure-white pixel there is 7.89:1, clearing 1.4.11's 3:1 floor.
     The nav links' rings sit higher and measure 8.39:1, the tel button's 8.32:1, the logo's at 390px 8.17:1.
     [HOVER v22] ALL FOUR FIGURES MORE THAN DOUBLED and the margin stopped being narrow, which is worth a
     sentence because the OLD margin was this paragraph's whole subject. The ring was #FF7070 and measured
     3.23 / 3.43 / 3.40 / 3.34:1 here - clearing 1.4.11 by between 0.23 and 0.43 - and a ring reaching
     97.5px (52.5% of the gradient) would have fallen to the floor, 11.7px below the lowest one that
     exists. At --ink the floor is not reached until alpha 0.4682, which the ramp does not touch until
     124.0px (66.8%), 38.3px below the lowest ring: the header scrim is no longer a constraint on this
     ring at all. The measured LAYOUT constants are unchanged and still have to be re-measured if the row
     geometry moves; what changed is the colour laid over them.
     THE POINTER LIGHT CANNOT MOVE ANY OF IT, for two independent reasons worth writing down
     because a reviewer has already got them wrong: 6.26's lobes are z-index 70 and this bar is 90, so the
     header paints OVER both; and the worst case is a pure-white pixel, which a screen blend cannot lighten.
     The figures are therefore identical lit and unlit, and v8's brighter light did not touch them.
     tools/verify_ratios.py re-derives all four from these stops and holds them to the floor. */
  .site-header::before {
    content: ""; position: absolute; inset: 0 0 auto 0; height: 190%; pointer-events: none;
    background: linear-gradient(to bottom, rgba(var(--bg-rgb), 0.88) 0%, rgba(var(--bg-rgb), 0.72) 53%,
      rgba(var(--bg-rgb), 0.30) 76%, rgba(var(--bg-rgb), 0) 100%);
    transition: opacity var(--dur-base) ease;
  }
  .site-header > * { position: relative; }   /* the row sits above its own scrim */
  .site-header.scrolled { background: rgba(var(--bg-rgb), 0.94); border-bottom-color: var(--line); }
  .site-header.scrolled::before { opacity: 0; }

  /* FULL-BLEED, NOT .container. The foundation's structural sum is written as "gutter + logo +
     clearance + nav + gutter + 17px scrollbar = VIEWPORT", so the bar spans the viewport and takes the
     same --gutter as .container: up to 1279.0px of viewport the two are identical and the mark aligns
     exactly with the page copy; above that the container caps at 72rem while the bar keeps breathing,
     which is the only reason a six-item row can exist at all (inside a capped 1224px row the mark,
     clearance and at-rest nav alone measure 243.6 + 44.51 + 811.05 = 1099.2px, and the tel button's
     further 249.8px - its own --space-3 plus 222.24px of button - would take the row to 1349.0px, so it
     could never ship there. [FACE v9] Both figures grew with the face and the conclusion grew with them:
     it survives by 125px where it survived by 102px. The 1224px cap is the same). The bar is chrome, not content:
     its scrim and its bottom hairline already run edge to edge. */
  .header-inner { display: flex; align-items: center; gap: var(--nav-gap); min-height: var(--header-h); padding-block: var(--space-2); padding-inline: var(--gutter); }
  .site-logo { flex: 0 1 auto; min-width: 0; display: block; }
  /* HEIGHT is the fixed dimension and width follows the mark's ratio, so the row's arithmetic is a
     known number rather than whatever the file happens to declare - an SVG carrying only a viewBox has
     no intrinsic size and collapses to 0 under an auto height.
     TWO RATIOS, AND EVERY ROW SUM MUST USE THE SECOND. The 2020 master artwork is 2505 x 654 = 3.830:1;
     the asset this element actually loads renders at 3.8216:1, so at the 3.75rem header cap the mark is
     243.63px and not the 244.2px the master would give. [REPAIR v10] The token block used to quote both
     figures at different points in the same derivation (309.3 / 260.4 / 244.2 from the master, 243.6
     from the asset); the row sums in 6.1 are on the ASSET, which is the only one a browser can lay out,
     and the 0.6px difference is now named here rather than left to be rediscovered as a discrepancy.
     max-width and object-fit are the degenerate-case guard only ON A PHONE: the phone rule caps the
     height at 3.1rem (measured 201.3px) against 205.25px of available row at 320px, so max-width does
     not bind there. [REPAIR v10] AND IT NOW BINDS NOWHERE ELSE EITHER. It used to bind hard just above
     1024 and just above 1136 - 14.7% and 13.6% of the mark's width, measured, where the note here
     claimed 2.5% from the retired face's arithmetic - because the structural literals sat below the
     rows they gate. Moving them to 67rem / 74rem (6.1, THE OVERSHOOT) removed both bands, so this guard
     is back to being what its first sentence says it is: a phone-only degenerate-case guard. */
  .site-logo img { display: block; height: min(var(--logo-h), 3.75rem); width: auto; max-width: 100%; object-fit: contain; }
  /* Clearance --space-4 exactly, one step above the label gap, or the first label reads as a seventh
     word of the mark. The row's own flex gap already contributes --nav-gap here, so the padding is the
     remainder: padding + gap = --space-4, and the measured row matches the sum above rather than
     running 16-26px wider than it. */
  .site-nav { margin-inline-start: auto; padding-inline-start: calc(var(--space-4) - var(--nav-gap)); }
  .nav-links { display: flex; align-items: center; gap: var(--nav-gap); list-style: none; padding: 0; }
  /* `.nav-links > li`, not `.nav-item`: build.mjs's navList puts its class argument on the <a> and
     writes the <li> bare, so a class hook on the row can never exist. The structural selector is the
     only one that can cover both the macro rows and the hand-written disclosure row. */
  .nav-links > li { position: relative; }
  /* The child combinator matters: .nav-menu lives INSIDE the first row, so a descendant selector
     would give all eight dropdown links the top row's type and, worse, eight copies of the drawn
     rule below - each positioned against the same <li>. */
  .nav-links > li > a, .nav-menu-btn {
    display: flex; align-items: center; min-height: 44px;   /* the tap floor, absolute px; the label is 13.36px */
    background: none; border: 0; padding: 0; color: var(--ink-nav);   /* 11.34:1 on --bg, 8.40:1 on the scrim's 0.88 stop, 4.66:1 on its 0.72 stop over a white pixel - the binding one */
    font-family: var(--font-body); font-size: var(--text-label); font-weight: var(--wt-label);
    letter-spacing: var(--track-nav); text-transform: uppercase; text-decoration: none; white-space: nowrap;
  }
  /* The rule rests at scaleX(.25) with a left origin, so hovering EXTENDS a rule rather than creating
     one; --dur-slow both ways, because a drawn rule is the slow half of an asymmetric pair. */
  /* [NAV-RULE 2026-09-06] THE RULE IS BRAND RED AT REST, on the firm's instruction ("the lines under
     the links in the header should be red"). It was currentColor, which inherited the label's --ink and
     made the rule the same white as the type above it.
     WHY THIS IS SAFE WHERE RED TEXT IS NOT: 1.4.11 governs a painted rule, not 1.4.3, and even 1.4.11
     does not bind here - the underline is not what identifies these as links. The LABEL does that: it is
     --ink at 17.73:1, tracked caps, in a nav row. The rule is emphasis, and at rest it is only
     scaleX(0.25) of the label's width - a tick, not an underline. Its own measure against --bg is
     2.99:1, the same figure recorded for every other red on this site.
     AND IT MAKES THE HOVER MEAN SOMETHING. Before this the rule was white at rest and --ink on hover,
     so hovering changed only its WIDTH. Now it travels red -> white as it extends, which is the same
     direction every other hover on the site takes since [HOVER v22] and gives the gesture a second,
     redundant channel rather than relying on width alone. */
  .nav-links > li > a::after, .nav-menu-btn::before {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0.5rem; height: 1px;
    background: var(--brand-fill); transform: scaleX(0); transform-origin: left;
    transition: transform var(--dur-slow) var(--ease-out), background-color var(--dur-fast) ease;
  }
  /* [NAV 2026-09-06] THE CURRENT PAGE IS BRIGHT WHITE WITH A FULL-LENGTH RULE, on the firm's
     instruction. It was --brand, and that REMOVES a recorded AA failure rather than adding one: the
     label failed at 13.36px/600 and now carries the full ink stop. The state is also no longer
     carried by colour alone - the rule under it is at full width where every other link's is at
     zero - which is what 1.4.1 asks for and what the red label never gave. */
  .nav-links [aria-current="page"] { color: var(--ink); }   /* 17.73:1 on --bg - was 2.99:1 as --brand */
  .nav-links [aria-current="page"]::after { transform: scaleX(1); }
  /* The caret: two borders of a square, rotated. The margin cancels the label's own trailing
     letter-spacing (block 0's trailing-space rule - CSS adds tracking after the LAST character too,
     and the caret is the thing abutting it), so the gap is an optical 0.5em rather than 0.5em plus
     a rung of tracking. It also buys back ~2px of the compact row; see 6.1's caret arithmetic. */
  .nav-menu-btn::after {
    content: ""; width: 0.38em; height: 0.38em; margin-left: calc(0.5em - var(--track-nav));
    border-right: 1px solid currentColor; border-bottom: 1px solid currentColor;
    rotate: 45deg; translate: 0 -0.12em;
    /* [MOTION v20] `translate` JOINS `rotate` IN THIS LIST, AND ITS ABSENCE WAS A DEFECT RATHER THAN A
       CHOICE. The open state one line down changes TWO properties - the caret turns 45 -> 225deg AND
       drops 0 -0.12em -> 0 0.06em, because a rotated square's optical centre moves when it flips - and
       only the rotation was ever in the transition list. What shipped was a caret that turned over
       --dur-fast while its 0.18em vertical correction snapped in the first frame: not a subtler gesture
       but a visibly broken one, and the longer the rung gets the more obvious it becomes. Both halves of
       one state change, one duration, one curve. */
    transition: rotate var(--dur-fast) var(--ease-inout), translate var(--dur-fast) var(--ease-inout);
  }
  .nav-menu-btn[aria-expanded="true"]::after { rotate: 225deg; translate: 0 0.06em; }
  /* The practice dropdown. THIS BLOCK IS THE CLOSED STATE - there is no `hidden` attribute anywhere in
     the chrome (see the vocabulary note above), so nothing outranks it and nothing has to be removed
     before it can open. Two openers, both authored by main.js on the same interaction:
     [aria-expanded="true"] on the button and .is-open on the panel; either alone is sufficient, so a
     partial write cannot strand a half-open menu.
     THERE IS NO :focus-within FALLBACK AND NO :hover OPENER, and the absence of both is the design.
     Without JS the whole .nav-item--menu row is display:none and the macro's own PRACTICE AREAS link
     carries the route (the gate below); with JS, either one fights main.js's own close path. The
     failures are the same shape and both were measured on the shipped page:
       :focus-within  Escape returns focus to the button, the button is inside the wrapper, and the
                      panel re-opens under the visitor.
       :hover         at 1440 x 900, hovering the button without clicking left aria-expanded="false"
                      while the panel computed visibility: visible / pointer-events: auto - assistive
                      tech told the menu was collapsed while a mouse user saw it open and could click
                      into it. Then, after a click to open, Escape correctly cleared [aria-expanded]
                      and .is-open and the panel STAYED open and clickable until the pointer left,
                      because the hover rule's 0,3,0 beat the closed state's 0,1,0.
     CSS cannot tell "dismissed" from "never opened" - both are aria-expanded="false" - so a hover
     opener cannot be made to yield to Escape without a third signal that main.js does not write. The
     disclosure therefore opens on click and on the keyboard only, which is exactly what its ARIA says.
     Hovering an ALREADY-OPEN panel keeps it open with no rule of its own: [aria-expanded] stays true
     until something closes it. One owner, no race.
     visibility is paired with a delay equal to the fade so a closed panel stops hit-testing;
     pointer-events flips instantly so it can never trap a click during the 0.30s it fades, and
     guards' reduced-motion delay reset repairs the pairing there. padding-top is the pointer bridge -
     part of the panel, so no gap to cross. */
  .nav-menu {
    position: absolute; top: 100%; left: 50%; translate: -50% 0;
    /* The bridge, and the reason the panel clears the bar. The trigger is a 44px row centred in the
       --header-h bar, so its bottom sits (--header-h - 44) / 2 above the bar's own edge; the padding
       spans that plus --space-1, putting the visible box exactly --space-1 below the hairline. It is
       PADDING, not a margin, so the pointer never crosses a gap on its way down. */
    padding-top: calc((var(--header-h) - 44px) / 2 + var(--space-1));
    opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity var(--dur-fast) var(--ease-inout), visibility 0s linear var(--dur-fast);
  }
  /* The panel's own width, re-run for the new face. The rows are Montserrat at --wt-label 600, UPPERCASED
     by the rule below, so the string that governs is "INTELLECTUAL PROPERTY" in caps and not the
     sentence-case name the container derivation measures for the card h3 (13.606em against 8.651em -
     different casing, different face, different job; the two sums must never borrow each other's figure,
     which is the mistake the v7 repair pass caught here).
       label 13.606 x 13.36 = 181.78px + 21 x --track-7 0.08em = 22.44px = 204.22px
       + 2 x --space-2 link padding 34px                       = 238.22px a row
       + the ul's own 2 x --space-1 21.02px + 2 x 1px border    = 261.24px of panel
     PREVIOUS SETTING (Titillium 600 at --track-7 0.16em): rows 229.0px, panel 252.0px, both measured on
     the built page at 1300 - and reproduced to 0.03px by the rig that produced the figures above, which
     is why they are worth acting on. So `min-width: 14rem` (238px) still never binds: the panel is
     shrink-to-fit around white-space: nowrap rows and is 23px wider than its floor, where it used to be
     14px wider. Nothing renders wrong and nothing ever did, but "238px leaves the rows even" is not what
     makes them even; nowrap inside one shrink-to-fit box is. The declaration is KEPT as a floor for a
     future list whose longest label is shorter, which is the only condition under which it can do
     anything - and the face change moved the panel AWAY from that floor, not toward it. */
  .nav-menu ul { list-style: none; padding: var(--space-1); min-width: 14rem; background: rgba(var(--bg-rgb), 0.97); border: 1px solid var(--line); border-radius: var(--radius); }
  .nav-menu a {
    display: flex; align-items: center; min-height: 44px; padding-inline: var(--space-2);
    color: var(--ink); font-size: var(--text-label); font-weight: var(--wt-label);
    letter-spacing: var(--track-7);   /* one rung below the nav: a submenu is a quieter label */
    text-transform: uppercase; white-space: nowrap;
    text-decoration: underline; text-decoration-color: transparent;   /* [HOVER v23] painted by :hover / :focus-visible - see below */
    text-decoration-thickness: 1px; text-underline-offset: 0.18em;
    transition: color var(--dur-fast) ease, background-color var(--dur-fast) ease, text-decoration-color var(--dur-fast) ease;
  }
  .nav-menu-btn[aria-expanded="true"] + .nav-menu, .nav-menu.is-open { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: 0s; }
  :where(html:not(.has-touch)) .nav-links > li > a:hover::after,
  /* [HOVER v22] BOTH TAKE --ink, and the two hits are different KINDS of thing, which is why they are
     annotated together rather than assumed alike.
     THE FIRST IS A DRAWN RULE, NOT TYPE: the 1px ::after / ::before that extends under a nav label. It is
     painted, so 1.4.11's 3:1 governs it and not 1.4.3's 4.5:1 - which made no difference at #FF7070 and
     makes none at --ink's 17.73:1 on --bg. What it buys is that the rule is now the label's OWN colour:
     the gesture is "a white label grows a white rule", where a red rule under a white label was two
     colours doing one job.
     THE SECOND IS TYPE, AND [HOVER v23] GAVE IT SOMETHING TO CHANGE. .nav-menu a rests at --ink already,
     so `color: var(--ink)` moves no pixel on an ordinary submenu row, and what v22 left carrying the hover
     on those rows was the 0.05 ink wash alone. THE WASH IS NOT A HOVER: it composites to #1A1817 and
     measures 1.10:1 against the panel it sits on, which is one part in ten of 1.4.11's step, and a row
     that answers the pointer with that answers with nothing. The rows now carry an UNDERLINE as well,
     declared at rest in `transparent` and painted `currentColor` on hover and on focus. It is drawn in the
     label's own --ink, 17.73:1 on the ground behind the panel; it reserves no space, so the resting panel
     is unchanged to the pixel; it is the same text-decoration-color gesture .contact-list and the four
     footer families already make, one level below the nav bar's own ::after; and being a rule ARRIVING
     rather than an object moving it is left alone by 5.2's reduced-motion guard, which is the test the
     wash and the colour both pass and neither survives on merit. The wash is KEPT as the row-level
     highlight a menu panel wants, now that something else carries the state.
     THE COLOUR DECLARATION IS STILL NOT DEAD CODE: `.nav-links [aria-current="page"]` is a descendant
     selector and this panel sits inside .nav-links, so the row for the practice area you are ON rests at
     --brand's 2.99:1, and that row does warm - to 17.73:1, the only state on it that meets AA. */
  :where(html:not(.has-touch)) .nav-menu-btn:hover::before { transform: scaleX(1); }
  /* [NAV 2026-09-06] THE LABEL BRIGHTENS WITH THE RULE. The two halves of the hover are the same
     gesture: --ink-nav -> --ink on the type, scaleX(0) -> scaleX(1) left-to-right on the rule. The
     rule's own colour does NOT change - it is --brand-fill in both states, because the firm asked
     for red lines and a line that arrives already red is one fewer thing moving. Focus takes the
     same pair outside the pointer gate, so a keyboard reaches the identical state. */
  :where(html:not(.has-touch)) .nav-links > li > a:hover,
  :where(html:not(.has-touch)) .nav-menu-btn:hover { color: var(--ink); }
  .nav-links > li > a:focus-visible, .nav-menu-btn:focus-visible { color: var(--ink); }
  :where(html:not(.has-touch)) .nav-menu a:hover { color: var(--ink); background: rgba(var(--ink-rgb), 0.05); text-decoration-color: currentColor; }
  /* Keyboard parity, outside the gate and quieter: the rule completes but does not warm to the accent. */
  .nav-links > li > a:focus-visible::after, .nav-menu-btn:focus-visible::before { transform: scaleX(1); }
  .nav-menu a:focus-visible { background: rgba(var(--ink-rgb), 0.05); text-decoration-color: currentColor; }

  /* ---- THE NO-SCRIPT GATE. Three rules, and between them they are the whole degradation contract for
     the header. Gated on `js`, never on a media query and never on the presence of a button: the
     inline watchdog adds `js` in <head> and STRIPS it again after 2.5s unless main.js has stamped
     data-motion-ready, so a script that 404s, throws or never parses lands on the no-JS branch rather
     than on a dead control (playbook s5). /404 ships no scripts at all and is therefore always here.
       1. WITH JS   the disclosure stands and the macro's duplicate of it is removed. The two are
                    ALTERNATIVES, never additions - shipping both put "Practice Areas" in the row twice.
                    `.nav-item--menu + li` and not `:first-child`, because the disclosure is written
                    first: the adjacency is the assertion that PRACTICE AREAS is nav.header[0] in
                    site-plan.json, and if that ever stops being true this rule hides the wrong label.
       2. WITHOUT JS the disclosure is removed instead, and the macro's PRACTICE AREAS link - which
                    points at /firm-overview, the only page listing every area and every named service
                    - is the route. The panel's seven hubs are also in the footer on every page.
       3. WITHOUT JS the collapse never happens: .site-nav stays visible below 67rem and .nav-links
                    wraps, because .nav-toggle is display:none by default and only rule 3's twin below
                    ever shows it. Hiding the nav AND the burger is how a 375px /404 ends up with no
                    header destinations at all - the failure the partial's own test recipe hunts for
                    (rename js/main.js, load /404 at 375px, click every header destination).
       4. WITHOUT JS the bar is pinned to the ground .scrolled would otherwise give it. [v7 REPAIR PASS]
                    The scrim above is sized for --ink; .scrolled - which main.js adds at 24px of scroll -
                    is what protects everything else. With no script .scrolled never arrives, so the scrim
                    alone has to carry the row over whatever passes under a FIXED bar, photographs inside
                    cards included, and the one thing in that row which is not --ink is the current-page
                    label in --brand. Against a pure-white pixel it measures
                    1.23:1 at the scrim's own 0.72 worst-case stop, against 2.64:1 on the 0.94 ground
                    pinned here. [RED v8] BOTH OF THOSE USED TO PASS-OR-NEARLY (2.29 and 4.91) AND NEITHER
                    DOES NOW: the collapse onto #BF0000 took the current-page label below AA on every
                    ground, this one included, and no veil alpha recovers it - the label is 2.99:1 even
                    against the fully opaque --bg. The pin is therefore kept for what it still does, which
                    is roughly double the worst case rather than fix it; the label itself is on the token
                    block's recorded-failure list. At scroll 0 the hero's own 0.70 top stop already stacks
                    with the scrim to 0.9402. The cost is that a no-JS visitor sees a solid bar over the
                    hero rather than a veil, which is the right trade - the veil exists to be replaced by
                    this ground on scroll, and without a script it never can be.
     The nav-toggle line is redundant against the base declaration two blocks down and is written
     anyway: it is the assertion that makes rule 3 survive an edit to that declaration. */
  /* RULE REMOVED 2026-09-08 with the Firm Overview page (site-plan item for
     "firm-overview-page-deleted-2026-09-08"). It read:
         html.js .nav-item--menu + li { display: none; }
     and its whole job was to hide the DUPLICATE "Practice Areas" link that nav.header carried for
     the no-JS branch, so a scripted visitor saw the disclosure button and not both. That link was
     nav.header[0] and it pointed at /firm-overview; deleting the page deleted the entry, so there is
     no duplicate left to hide. LEAVING THE RULE IN WOULD HAVE BEEN A DEFECT, not dead weight: the
     selector is an ADJACENCY one, the comment in partials/header.html flags exactly this
     ("'Practice Areas' must stay FIRST in site-plan.json nav.header, or rule 1 hides the wrong
     label"), and with that entry gone the next li is ATTORNEYS - every scripted visitor would have
     lost it from the header.
     The no-JS rule below is KEPT and is unchanged: without a script the disclosure row is still
     hidden outright, and the footer's practice column (partials/footer.html column 3, {{nav:practice}})
     is still how a no-JS visitor reaches the six hubs - which is what that partial's own comment
     already says it is for. */
  html:not(.js) .nav-item--menu { display: none; }
  html:not(.js) .nav-toggle { display: none; }
  html:not(.js) .site-header { background: rgba(var(--bg-rgb), 0.94); border-bottom-color: var(--line); }
  html:not(.js) .site-header::before { opacity: 0; }

  /* The sixth header item, the tel: button - the one thing the re-run sum cannot fit at 1072px. Row =
     1171.2 (the measured at-rest row, above) + 27.51 gap + 222.24 button = 1420.9 -> fits from 89rem
     (1424px, 3.1px slack). The 222.24px is a FULL-SIZE .btn - there is no .btn--small in
     this stylesheet and the partial must not ask for one (6.18). Below it the number is carried by the hero button on every page, the
     closing band, the footer and the mobile menu, so only the shortcut is lost. One number, one more
     row of the same sum that produced 67 and 74rem - not a third structural breakpoint. [FACE v9] THIS
     LITERAL MOVED (88 -> 89rem) ON EXACTLY THE REASONING
     [RED v7] used when it moved it from 86 to 88: this query is nothing but a row of the sum above, so
     leaving it at 1408px would show the button into a row that needs 1420.9 - a visible defect with one
     unambiguous fix. Note the button itself got SMALLER (229.78 -> 222.24px) even though Montserrat is
     the wider face, because --track-6 fell 0.20em -> 0.12em with the re-derived ladder: label 165.23px
     against 172.9px. The nav grew by more than the button shrank.
     [REPAIR v10] THIS LITERAL DOES NOT MOVE WITH THE STRUCTURAL PAIR, and that is the check that proves
     the two are independent quantities rather than one number with three names: 89rem is derived from
     the at-rest ROW (1171.18 + 27.51 + 222.24 = 1420.9), and moving the structural literals onto that
     same row changed the literals, not the row. Re-measured live at 1424px after the change: the whole
     six-item row fits with the mark at its full 243.63px. If the firm wants the header call button back
     at 1072-1424, the
     lever is the label set in site-plan.json: shedding "FLAT FEE SERVICES" takes 172.11px of label plus
     its 27.51px gap off the at-rest row, which brings this query's own sum to 1221.3px = 77rem. */
  /* .btn.nav-cta, not .nav-cta: .btn's own display: inline-flex is declared later in this same layer
     at equal specificity and would otherwise win, leaving the button visible at every width. */
  .btn.nav-cta { display: none; flex: none; }
  @media (min-width: 89rem) { .btn.nav-cta { display: inline-flex; } }   /* 88 -> 89rem = 1424px; the row above needs 1420.9 */

  /* The hamburger. Its only query is the structural number; checked at 812 x 375, which matches it. */
  .nav-toggle { display: none; flex: none; align-items: center; justify-content: center; width: 44px; height: 44px; margin-inline-start: auto; background: none; border: 0; color: var(--ink); }
  .nav-toggle span { position: relative; }   /* the outer bars position from the middle one */
  .nav-toggle span, .nav-toggle span::before, .nav-toggle span::after { display: block; width: 22px; height: 1px; background: currentColor; transition: transform var(--dur-base) var(--ease-inout), rotate var(--dur-base) var(--ease-inout), translate var(--dur-base) var(--ease-inout); }
  .nav-toggle span::before, .nav-toggle span::after { content: ""; position: absolute; }
  .nav-toggle span::before { translate: 0 -7px; }   /* 7px: the smallest gap that still reads as three
                                                       separate bars at a 1px stroke and 22px width.
                                                       ABSOLUTE px, deliberately - the burger is a 44px
                                                       target, not type, so it does not take the root
                                                       multiplier and 22/7/1 are unchanged by [RED v7]. */
  .nav-toggle span::after { translate: 0 7px; }
  .nav-toggle[aria-expanded="true"] span { background: transparent; }
  .nav-toggle[aria-expanded="true"] span::before { translate: 0 0; rotate: 45deg; }
  .nav-toggle[aria-expanded="true"] span::after { translate: 0 0; rotate: -45deg; }
  /* THE COLLAPSE IS A JS FEATURE, so it is gated like one. Trading the nav for a burger is only an
     improvement if the burger opens something; with no script the overlay can never open, so the nav
     stays and wraps instead. Two rows of five 13.36px labels at 375px is not elegant and it is
     reachable, which is the whole of the contract at this width. */
  @media (max-width: 66.99rem) {
    html.js .site-nav { display: none; }
    html.js .nav-toggle { display: inline-flex; position: relative; }
    /* Both wraps are needed and they are different wraps: the row drops the nav BELOW the mark (or
       the mark shrinks under object-fit until the labels are unreadable), and the list then wraps
       within that band. At 375px the row's min-content is 201.8 logo (the phone cap in 6.24, not the
       3.75rem header cap - the two gutters are the phone pair here too, which the figure this comment
       used to carry got wrong in both places) + 27.51 gap + ~148.8 widest label + 43.25 gutters =
       421.4px, so the first wrap always fires here. */
    html:not(.js) .header-inner { flex-wrap: wrap; }
    html:not(.js) .nav-links { flex-wrap: wrap; }
  }

  /* ---- 6.2 MOBILE MENU OVERLAY -----------------------------------------------------------------
     Opened by .is-open (main.js toggles it with .nav-toggle[aria-expanded]). THE VISIBILITY TRAP:
     `visibility 0s linear var(--dur-base)` is what stops a faded-out overlay from staying
     hit-testable, and is also what leaves an invisible full-screen click trap if its delay is clamped
     without being reset. guards resets transition-delay to 0s under reduced motion, which repairs it
     there; pointer-events: none repairs it everywhere else, instantly, on the same class.
     AS WITH THE DROPDOWN, this block IS the closed state and the panel carries no `hidden` attribute:
     visibility: hidden takes its links out of the tab order, which is the whole of what main.js's
     contract asks of CSS ("the closed panel must be display:none or visibility:hidden so its links
     leave the tab order - this file does not manage tabindex").
     THE GROUND LOCK. main.js puts .is-open on <html> as well as on the panel; this is the rule that
     consumes it. body's overflow propagates to the viewport as long as <html>'s own overflow is
     visible (base never sets it), so the page stops scrolling without the scroll-position jump
     position: fixed causes. Declared here rather than left to an inline style so the lock is visible
     in the stylesheet and cannot be lost by an early return in JS. */
  html.is-open body { overflow: hidden; }
  /* SAFE CENTRE, NEVER PLAIN CENTRE. This is a scroll container (overflow-y: auto) and its content is
     taller than its box on ordinary portrait phones, not just in landscape: the overlay repeats BOTH
     lists (5 site rows + 8 practice rows) plus the contact pair. Plain `center` splits the overflow off
     BOTH ends, and scrollTop cannot go negative, so the top of the content is unreachable at the scroll
     origin - measured on dist/ with the menu open at scrollTop 0: 375 x 812 put the first link at
     -16.8px, 360 x 640 at -103px (Practice Areas and Attorneys unreachable) and 320 x 568 at -139px
     (three unreachable). Scrolling to the bottom and back does not recover them.
     Those three measurements were taken at the 16px base and are NOT re-scaled: they are observations,
     not arithmetic. [RED v7] makes every row taller, so the overflow they record is larger now, not
     smaller - the conclusion they were kept for strengthens rather than needing a re-run.
     `safe` falls back to start alignment in exactly the overflow case and leaves the centred look
     everywhere the menu fits, so it subsumes the landscape-only `align-content: start` that used to
     repair this in 6.24 and that never matched a 640px-tall portrait phone. Verified after the change:
     first link top = this rule's own padding-top (--header-h + --space-3 = 125.26px, 118px when it was
     measured) at both 360 x 640 and 320 x 568, nothing unreachable. If `safe` is ever unsupported the
     whole declaration drops and align-content falls to
     `normal`, which starts the rows at the padding edge - the same safe end of the failure. */
  .mobile-menu {
    position: fixed; inset: 0; z-index: 80; display: grid; align-content: safe center; overflow-y: auto;
    padding: calc(var(--header-h) + var(--space-3)) var(--gutter) var(--space-4);
    background: rgba(var(--bg-rgb), 0.98); opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity var(--dur-base) var(--ease-inout), visibility 0s linear var(--dur-base);
  }
  .mobile-menu.is-open { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: 0s; }
  .mobile-menu ul { list-style: none; padding: 0; display: grid; gap: var(--space-1); }
  /* THE SEAM BETWEEN THE TWO LISTS. The <nav> holds {{nav:header}} and {{nav:practice}} as two sibling
     <ul>s, and nothing separated them: .mobile-menu is a grid with no gap and <nav> is a plain block, so
     the lists abutted and the 5 site destinations read as one undifferentiated 13-row list (measured at
     375px: 0px between the lists against 10.51px between rows inside one). The grammar is the one
     .mobile-menu-contact already uses for the same job one block down - a rule, then air on both sides -
     at one step down, because this divides two lists of the same kind rather than a list from a
     different object. */
  .mobile-menu nav > ul + ul { margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--line); }
  /* .mobile-menu a is a granted serif hook (base): the one place a nav label is not tracked caps -
     at full width there is room for the display voice. 27.51 x 1.15 = 31.63px + 20.40px = 52.03px > 44. */
  .mobile-menu a { display: block; padding-block: 0.6rem; font-size: var(--text-2); line-height: var(--leading-heading); color: var(--ink); text-decoration: underline; text-decoration-color: transparent; text-decoration-thickness: 1px; text-underline-offset: 0.18em; transition: opacity var(--dur-base) var(--ease-out), translate var(--dur-base) var(--ease-out), color var(--dur-fast) ease, text-decoration-color var(--dur-fast) ease; transition-delay: calc((var(--row-base, 0) + var(--row, 0)) * var(--menu-step)); }
  /* ---- [MOTION v20] THE ROW CASCADE. THE LARGEST SINGLE BREADTH GAP THIS PASS FOUND, AND IT WAS A GAP
     AGAINST THIS SITE'S OWN VOCABULARY BEFORE IT WAS A GAP AGAINST THE SISTER SITE'S. Every other panel
     on this site arrives as a SEQUENCE - the reveal system staggers a grid of cards, .num-list lags its
     hairline behind its row - and the one panel that covers the whole viewport arrived as a single
     opacity block: 13 destinations appearing at once, with the colour change on :hover not even
     transitioned. On a phone this overlay IS the navigation, so it was the least animated thing on the
     site and the most looked at.
     THE THREE DECLARATIONS ARE THE WHOLE MECHANISM. A row's index is set on the <li> and INHERITS to the
     <a> (a custom property does; transition-delay does not, which is the same fact guards 7.4 is built
     on); the second <ul> adds its own offset so the two lists read as one run; and the <a> spends the sum.
     No JS is involved at any point - see the qualifier in block 6's preamble for why this cascade is
     CSS's where the reveal system's is main.js's.
     nth-child TO 8 AND NOT TO 13, because the second <ul> is where the count actually lives: the panel
     repeats {{nav:header}} (5 rows) then {{nav:practice}} (8 rows), so eight index rules plus one offset
     rule cover 13 positions, and adding a practice area costs nothing at all. A ninth practice row would
     need a ninth index rule; it would fall back to --row: 0 and arrive with the first, which is a visible
     failure rather than a silent one.
     THE STEP IS SOLVED FROM A CAP, NOT CHOSEN. The whole cascade must be over before the panel's own fade
     is, or the last destination arrives after the thing carrying it has finished arriving. The panel fades
     over --dur-base 0.60s; the cascade window is therefore --dur-fast 0.30s, one rung down. There are 14
     positions - 13 rows plus .mobile-menu-contact, which takes the last slot as ONE object because it is
     one object (a labelled pair under a rule, not two more destinations) - so 13 steps divide the window:
     0.30 / 13 = 0.02308, held at 0.023s, and the run ends at 13 x 0.023 = 0.299s. The last position starts
     at 0.299s and settles at 0.299 + 0.60 = 0.899s, so the overlay is fully arrived inside a second.
     Re-derive the step if the practice list ever grows - the window is fixed, the step is what gives.
     14px, WHICH IS HALF THE REVEAL TRAVEL guards 7.3 declares, and the half is the argument: a menu row is
     a sub-gesture inside a panel that is itself arriving, so it travels less than an object arriving on
     its own. Compositor-only - `translate` and `opacity`, nothing else, on 13 short rows that are painted
     for at most a few seconds - so no will-change is declared here and none is wanted.
     REDUCED MOTION NEEDS NO NEW GUARD AND THAT WAS CHECKED RATHER THAN ASSUMED: this is a transition, so
     block 5's clamp collapses its duration AND resets its transition-delay to 0s on *, *::before and
     *::after alike - which is precisely the declaration that flattens this cascade into a plain appearance.
     Nothing here is scroll-linked or pointermove-linked, which are the two kinds the clamp cannot see.
     NO-JS IS UNAFFECTED, ALSO CHECKED: html:not(.js) sets .nav-toggle to display: none (the no-script gate
     above), so with no script this panel can never be opened and its rows can never be reached. The
     pre-state is gated on html.js anyway, on the same belt-and-braces principle guards 7.3 uses.
     THE CLOSE PATH ZEROES THE DELAY, deliberately: on the way out the panel is already invisible, so a
     cascade nobody can see would only keep 13 transitions running for 0.30s after the overlay has gone. */
  .mobile-menu { --menu-step: 0.023s; }   /* 0.30s of window / 13 steps = 0.02308 - see above */
  .mobile-menu nav > ul > li { --row: 0; }
  .mobile-menu nav > ul > li:nth-child(2) { --row: 1; }
  .mobile-menu nav > ul > li:nth-child(3) { --row: 2; }
  .mobile-menu nav > ul > li:nth-child(4) { --row: 3; }
  .mobile-menu nav > ul > li:nth-child(5) { --row: 4; }
  .mobile-menu nav > ul > li:nth-child(6) { --row: 5; }
  .mobile-menu nav > ul > li:nth-child(7) { --row: 6; }
  .mobile-menu nav > ul > li:nth-child(8) { --row: 7; }
  .mobile-menu nav > ul + ul > li { --row-base: 5; }   /* {{nav:header}} is 5 rows, so the practice list starts at index 5 */
  .mobile-menu-contact { --row-base: 13; }             /* one object, one slot, last: 13 x 0.023 = 0.299s */
  html.js .mobile-menu a { opacity: 0; translate: 0 14px; }
  html.js .mobile-menu.is-open a { opacity: 1; translate: none; }
  html.js .mobile-menu:not(.is-open) a { transition-delay: 0s; }
  .mobile-menu [aria-current="page"] { color: var(--brand); }   /* 2.99:1 on --bg at 27.51px - large text, so the floor is 3:1 and it misses by 0.01. Recorded failure */
  .mobile-menu-contact { margin-top: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--line); display: grid; gap: var(--space-1); font-size: var(--text-label); letter-spacing: var(--track-8); text-transform: uppercase; color: var(--ink-2); }
  .mobile-menu-contact a { color: var(--brand); text-decoration: none; }   /* 2.99:1 at 13.36px - BELOW AA. Recorded failure */
  /* [HOVER v22] BOTH TAKE --ink, and the pair is the clearest case in the file for why the scheme puts
     text hovers on the INK rather than on another red. The two rows directly above are the only red type
     in this overlay - the current page at 27.51px and the contact lines at 13.36px, both recorded
     failures at 2.99:1 - and they are exactly the rows a pointer or a tab lands on. --ink takes them to
     17.73:1: the state a visitor reaches is the only state on these rows that meets AA. Every OTHER row
     rests at --ink already, so for those two declarations resolve to no change and the overlay's own
     arrival animation is all the motion there is; that is a real reduction from the pink and it was
     accepted rather than repaired.
     [HOVER v23] THE ACCEPTANCE WAS WRONG AND IS WITHDRAWN. A row whose hovered state is pixel-for-pixel
     its resting state has no hover, and after v22 every ORDINARY row in this overlay was one - no colour,
     no wash, no rule, nothing. The paragraph above went looking for a gesture "no other list on the site
     has" and recommended the 0.05 ink wash; that recommendation is also withdrawn, because the wash
     measures 1.10:1 and is a hover only in the stylesheet. What these rows take instead is the underline
     .nav-menu now takes: declared at rest in `transparent`, so the closed overlay is unchanged, and
     painted `currentColor` on hover and on focus - on a row resting at --ink that is a 1px rule at
     17.73:1. It costs no layout, it is the gesture .contact-list and the four footer families already
     make, and being a rule that ARRIVES it outlives 5.2's reduced-motion guard, where this overlay's own
     travel does not. .mobile-menu-contact a keeps `text-decoration: none` at (0,2,0) and so keeps
     answering in colour alone - it is one of the two red rows, and colour is a real answer there. */
  :where(html:not(.has-touch)) .mobile-menu a:hover { color: var(--ink); text-decoration-color: currentColor; }
  .mobile-menu a:focus-visible { color: var(--ink); text-decoration-color: currentColor; }

  /* ---- 6.3 HERO --------------------------------------------------------------------------------
     THE SCRIM IS A CONTRAST INSTRUMENT AND IT IS VERTICAL, not left-weighted: below 30rem the title
     spans the whole container, so a horizontal veil would darken the subject instead of the negative
     space. Every stop is the page's own ground. COPY PROTECTION against a PURE-WHITE pixel - the worst
     case an unknown photograph can present. THE COMPOSITE IS PER CHANNEL, on --bg's own (14, 12, 11),
     which is the authoritative model for every scrim figure in this file: a x (14, 12, 11) + (1 - a) x 255.
     (The grey-13 shortcut an earlier draft used is close but not the same number - 7.24 against 7.29 at
     a 0.72 veil - so it is not a second way of getting the same answer. 0.72 stopped being one of this
     ramp's stops at [SCRIM v12] below; it is still the header scrim's bottom-edge alpha, which is what
     6.1 and the focus-ring arithmetic mean when they say "the scrim's 0.72 worst-case stop".)
       a 0.68 (22% stop): (91.1, 89.8, 89.1) -> L 0.1022; --ink (L 0.9041) = 6.27:1, and 5.17:1 with the
                          pointer light screened over both sides - the FLOOR of the whole copy region
       a 0.84 (62% stop): (52.6, 50.9, 50.0) -> L 0.0333; --ink 11.45:1, --ink-2 (L 0.4042) 5.45:1
     [SCRIM v12] THE RAMP MOVED, AND IT MOVED BECAUSE THE GEOMETRY THIS BLOCK ASSERTED WAS FALSE. What
     stood here was: "the copy block never starts above the 40% stop ... the eyebrow now lands at ~41.5%
     ... .hero-sub still never sits above the 62% stop". That was derived on .hero--home ALONE, where the
     hero is 100svh and align-items: flex-end holds the copy against the bottom edge. It was never
     re-derived for .hero--page, and there it is simply not true: min-height is 30rem = 510px, the copy
     stack is TALLER than that on every page, so the hero grows to fit the copy and the copy therefore
     fills it FROM THE TOP - starting at padding-top, which is --header-h + --space-4 = 142.26px however
     tall the box gets. Measured in Chrome on the built pages (text-node rects in the REVEALED state,
     the pre-state's 26px rise excluded, .hero-media's own box as 0-100%):
       .hero--page    1440x900 / 1280x800 / 1072x900 / 768x1024   eyebrow 32.52%   .hero-sub 52.11%
       .hero--page    390x844 (real-estate)                       eyebrow 25.19%   .hero-sub 40.70%
       .hero--page    375x667 (business, employment-law)          eyebrow 23.79%   .hero-sub 38.43%  <- worst
       .hero--home    812x375 (the landscape branch sets min-height: 0, so this one fills too)
                                                                  eyebrow 25.60%   .hero-sub 65.08%
       .hero--home    375x667                                     eyebrow 39.78%   .hero-sub 58.95%
     Against the OLD ramp those positions carried 0.4547 / 0.6943 of veil, not 0.72 / 0.84. Every figure
     below is over a pure-white pixel, bare and then with the pointer light screened over both sides:
       eyebrow, --ink 13.36px/600 at 375x667 (a 0.4547)      2.88:1 bare, 2.60:1 lit   floor 4.5
       eyebrow, --ink at 1440x900 (a 0.5976)                 4.63:1 bare, 3.96:1 lit   floor 4.5
       .hero-sub, --ink-2 21.62px/400 at 1440x900 (a 0.7861) 4.47:1 bare, 3.78:1 lit   floor 4.5
     Six pairs under AA on seventeen pages, on the copy this block claims to protect. The claim was the
     defect; the numbers below are the repair.
     THE NEW RAMP IS SOLVED FROM THOSE POSITIONS, NOT CHOSEN. Over a pure-white pixel, with the pointer
     light screened over text and ground alike (6.26 is z-index 70, above the hero), the veil needs
     a >= 0.6375 for --ink at 13.36px/600 and a >= 0.5053 for --ink at title size. The topmost glyph
     anywhere is 23.79%, so the requirement is "0.6375 by 23.79%, and never lighter below it":
       0%   0.70   unchanged - the header's own scrim sits over this and nothing else does
       12%  0.44   THE WINDOW, where the photograph still reads. Moved up from 18% and lifted from 0.36
                   so the climb out of it is no steeper than the climb into it (0.0217 vs 0.0240 of alpha
                   per point of frame, against the old ramp's 0.0189 and 0.0164 - the same order, so no
                   band edge appears where there was none)
       22%  0.68   the copy floor, reached 1.79 points ABOVE the topmost glyph. The ramp climbs back out
                   of its window and first clears 0.6375 at 20.23%, so a hero would have to grow ~180px
                   taller before the eyebrow overtook the guarantee. Re-measure if a hero gains a line
       62%  0.84   unchanged
       100% 0.88   unchanged
     WHAT IT COSTS THE PHOTOGRAPH, stated rather than glossed: nothing at all from 62% down (identical
     alphas), +0.24 at worst (at 20% of the frame), and it is fractionally LIGHTER than the old ramp above
     11%. The darkening is confined to the band where the copy actually sits, which is the only band a
     contrast instrument is for.
     AND .hero-sub HAD TO LEAVE --ink-2 - see the rule below. No ramp that leaves the photograph visible
     can carry a 21.62px/400 muted ink: --ink-2 needs a >= 0.8360 lit, which is a near-opaque veil from
     38% of the frame down - the black-rectangle failure this block already refuses for red.
     Hence the hero eyebrow and title are --ink,
     NOT --brand - and [RED v8] TURNED THAT PREFERENCE INTO A REQUIREMENT. The old argument was a veil
     arithmetic: --brand at L 0.2493 would have needed alpha >= 0.91 over a white pixel to clear 4.5:1,
     the near-opaque black-rectangle failure. The collapsed --brand (L 0.1108) clears it at no alpha at
     all - at a fully opaque veil the ground IS --bg and the pair is still 2.99:1, so the solve has no
     answer rather than an expensive one. Red is simply not available over a photograph now, at any size,
     which is the sharpest single statement of what the collapse costs. The scrim lives on .hero-media::after, so a page with no photograph (404)
     gets no veil, and the veil arrives WITH the image under [data-reveal="fade"]. */
  .hero { position: relative; display: flex; align-items: flex-end; isolation: isolate; }
  .hero--home { min-height: 100svh; }
  /* 30rem: --header-h 97.75 + --space-4 44.51 + eyebrow 13.36 + --space-1 10.51 + two title lines 2 x
     48.96 + --space-2 17 + two deck lines 2 x 32.44 + --space-3 27.51 + actions 44 + --space-5 72.01 =
     489.4px = 28.79rem. [RED v7] The sum FELL in rem (it was 29.2rem) for two reasons that cancel most
     of the root multiplier: the 44px action row does not scale, and the header figure this line used to
     carry was 96 where --header-h is 5.75rem = 97.75px, i.e. it was 4px wide at the old base too. The
     token HOLDS at 30rem (510px) rather than tightening to 29rem: this is a min-height, so slack is free,
     and shortening the page hero is a visible change nobody asked for. 20.6px of slack, stated.
     [v7 REPAIR PASS] AND 30rem HERE IS NOT 30rem IN A QUERY: this is a real length, 510px at the 106.25%
     base, where --bp-phone's 30rem is a media-query literal and stays 480px. Two pixel values behind one
     number - the same collision this file already flags for the two unrelated 22rems in 6.11, recorded
     here so the next reader does not connect them either. */
  .hero--page { min-height: 30rem; }
  .hero-media { position: absolute; inset: 0; z-index: 0; }
  /* ONE SIZING RULE FOR BOTH FILL TYPES, and .hero-video is a second SELECTOR rather than a second rule
     for a reason: the home hero's <video> and every other page's <img> must present an identical box to
     the scrim above them, and two rules would be two places for that box to drift. A replaced element is
     a replaced element - `object-fit: cover` governs a <video> exactly as it governs an <img>.
     [ZOOM v13] THE MARKUP'S INTRINSIC PAIR MOVED ON 2026-09-06 WITH THE CLIP, and it moved for accuracy,
     not because the box did: 1280:720 and 1600:900 are both exactly 16:9, so nothing shifts on load
     either way. The attributes still have to describe the file that ships, which is why they were
     measured rather than copied.
     [HERO v17] THE CLIP CHANGED AGAIN THE SAME DAY, to hero-twml-3.mp4 - ffprobe: 1600x900, 30fps,
     37.833s, 1135 frames, one stream, no audio, 4,045,859 B. The intrinsic pair did NOT move with it
     (still 1600x900), because only the duration changed; the pair is re-stated here anyway so a reader
     checking the file against the markup is checking the CURRENT file.
     [HERO v17] AND THE WEBM SOURCE IS GONE, WHICH IS A MEASUREMENT, NOT AN OMISSION. VP9 was encoded at
     matched quality and came out 4,203,018 B against H.264's 4,045,859 - LARGER, so a <source> ordering
     webm first would have handed most visitors the bigger file. The same test decided the six card
     clips (VP9 14-44% larger there). One source, and the reason is written down so nobody restores a
     second one out of habit.
     [HERO v17] THE ENCODE ITSELF WAS CHOSEN AGAINST THIS BLOCK'S OWN SCRIM. CRF 32 measures SSIM 0.9713
     against CRF 29 on bare frames - a real difference - but composited through 6.3's scrim at its 0.72
     stop, where the picture contributes 28% of each pixel, it rises to 0.9900. Two thirds of the error
     is absorbed by the veil this hero is always seen through, so the 27% of bytes CRF 29 was spending
     bought detail the design deliberately hides. 5.29 MB -> 3.86 MB for a clip 7s longer than the one
     it replaces.
     WHAT THIS COMMENT USED TO SAY THEY BOUGHT - "reserve the aspect ratio before the poster decodes" -
     IS WITHDRAWN; THEY BUY NOTHING HERE, AND THE RULE BELOW IS THE REASON. .hero-media is absolute with
     inset: 0 inside a relative .hero, so its box is definite on both axes from the first layout, and this
     rule then resolves the fill against it on both axes. An intrinsic ratio is consulted only where an
     axis is auto; neither is, so there is no moment - before metadata, after it, or with the poster
     standing in - at which the declared 1600x900 decides anything. Probed at 1440x900 rather than argued:
     the <video> measures 1425 x 900 with the true pair, 1425 x 900 with both attributes removed, and
     1425 x 900 with width="100" height="900" forced on it. getComputedStyle still reports
     `aspect-ratio: auto 1600 / 900` - computed, never used. They stay because the markup contract defines
     them as the encode's real pixels, and because they go load-bearing the instant either axis here
     becomes auto, which is one edit to this rule away and announces itself in no other way.
     THE SCRIM STILL PAINTS ON TOP OF THE FILL, AND SINCE [SCRIM v12] IT IS PINNED THERE RATHER THAN
     LEFT TO THE PAINT ORDER. .hero-media carries `z-index: 0`, so it is a stacking context. The old
     argument was that ::after is POSITIONED at z-index auto while the video/img is not, and a positioned
     descendant paints in a later step than in-flow content whatever the DOM order says. That is true of
     the <video>, which is untransformed - and NOT true of an <img> under 6.28: a transformed element
     joins the same paint step as a positioned z-index:auto one, and the tie then falls to tree order,
     which the ::after happens to win because a generated box comes last. "Happens to win" is not a
     guarantee; a z-index added to that img, or a wrapper around it, would silently put the photograph
     over its own veil and take every ratio in this block with it. `z-index: 1` on the ::after states the
     relationship instead of inheriting it, costs nothing (it is already inside .hero-media's context, so
     it cannot rise past .hero-content's z-index: 1 sibling), and makes the invariant greppable.
     WHY THE SCRIM DID NOT MOVE FOR THE FIRST CLIP, AND DOES NOT MOVE FOR ITS REPLACEMENT EITHER - PLUS
     THE ONE MEASUREMENT THIS BLOCK REFUSES TO QUOTE. The brief the FIRST clip was built from claimed it
     reached 255 only in its top third and "peaks at 176-225" where the copy sits. It did not: sampled
     off that mp4 (106 frames at 4fps across its full 26.53s, per-row max over every frame and column),
     every 5% band reached >= 250 and the band under the desktop copy was white in all but name. The
     defect was not the arithmetic, it was the KIND of measurement - the 176-225 figure came from a
     DOWNSAMPLED probe, and downsampling averages, so every number such a probe yields is a FLOOR on the
     true maximum and none of them is a ceiling. [ZOOM v13] The same kind of probe over the replacement
     clip (hero-twml-3) reports a 255 channel in most horizontal bands, the ones the copy occupies
     included. That is again a floor and again not a ceiling, so this block states no per-band figure for
     it and no reader should add one back without decoding frames at full resolution.
     THE CONCLUSION NEVER NEEDED A PER-BAND FIGURE, WHICH IS WHY IT SURVIVES THE CLIP CHANGE UNTOUCHED.
     Every stop above is derived against a PURE WHITE PIXEL - the worst case any frame of any footage can
     present, and a ceiling nothing can exceed - so a clip is covered by the same arithmetic as a
     photograph and no clip needs a re-derivation of its own. What the false claim did cost is worth
     recording: it was offered as the reason no new arithmetic was needed, and the arithmetic that got
     skipped alongside it was .hero--page's geometry, which is where the [SCRIM v12] failures above were
     hiding. A measurement that excuses a derivation has to be true - and a measurement that cannot bound
     what it is being used to bound should not be quoted at all.
     The video takes no zoom (6.28) and no parallax (6.27): it is already moving, and two causes of motion
     on one object is the fault this file bars everywhere else. */
  .hero-media img, .hero-video { width: 100%; height: 100%; object-fit: cover; }
  /* ---- THE HOME HERO CLIP, 6% BRIGHTER (the firm, 2026-09-09: "slightly brighter, not a lot") ----
     ON THE VIDEO, NOT THE SCRIM, and that is the whole decision. The obvious lever is to thin
     .hero-media::after's gradient, and it is the wrong one: those five stops are the copy's contrast
     mechanism, each is a recorded measurement, and 0.68 at the copy floor is what decision
     "home-hero-label-and-lawyer-in-red-2026-09-09" reasons about. Lifting the clip leaves every one
     of them intact and changes only the pixels underneath.
     .hero-video ONLY, so no photographic hero moves. index.html is the only page with the class.
     WHY 1.06 AND WHAT IT COSTS, measured rather than guessed - the <video> is same-origin, so its
     frames were drawn to a canvas and read back, then composited against the scrim's own stops in
     code (out = a*bg + (1-a)*px, which is what the compositor does), at 1440x900, taking the
     BRIGHTEST pixel in each text box because that is the worst case for light type.
     THE FIGURES ARE IN site-plan decision "hero-video-brightness-2026-09-09" AND DELIBERATELY NOT
     HERE, for the same reason the red label's are not: they are sampled off a MOVING photograph, so
     no arithmetic over the shipped hexes can reproduce them, and site/tools/verify_ratios.py walks
     this file and fails on any ratio it cannot recompute. A number it cannot check does not belong
     in this file. In words: the ivory title's margin over AA is so large the change is immaterial,
     and the red label - already below AA by the firm's recorded decision of 2026-09-06 - loses about
     a fortieth of an already-failing ratio, which is less than the clip's own frame-to-frame swing.
     1.10 and 1.15 were measured too and not taken: the instruction said slightly, and 6% is the
     smallest step that reads as a change.
     A COMPOSITOR OPERATION, like the scrim veil and the pointer light's screen blend - not a repaint.
     `filter` opens a stacking context on the video, which changes nothing here: the scrim is a
     SIBLING pseudo-element at z-index 1 inside .hero-media, so it still paints above. Verified. */
  .hero-video { filter: brightness(1.06); }
  /* [HERO v17] ONE PHOTOGRAPH OVERRIDES THE CENTRED CROP, AND THE REASON IS EDITORIAL RATHER THAN
     COMPOSITIONAL. /make-a-payment's hero is 1600x894 in a frame this page renders at 1425x510, so
     `cover` is width-driven and 573 of the source's 894 rows survive - a 64% band, centred by default.
     Centred, that band includes the invoice letterhead in the upper right, whose small print does not
     survive being read closely. Biasing the crop to the bottom drops it out of frame entirely and
     centres the cheque instead, where every legible word - the firm's name, twice, and the amount - is
     the part worth showing. Nothing else on the site needs this, which is why it is one selector and
     not a token: `object-position` is per-image editorial, and the day a second photograph needs it the
     right move is a second line here, not an abstraction. */
  .hero-media img[src*="make-a-payment"] { object-position: 50% 100%; }
  .hero-media::after {
    content: ""; position: absolute; inset: 0; z-index: 1;   /* pinned above the fill - see the paragraph above */
    background: linear-gradient(to bottom, rgba(var(--bg-rgb), 0.70) 0%,
      rgba(var(--bg-rgb), 0.44) 12%,   /* the window where the photograph reads */
      rgba(var(--bg-rgb), 0.68) 22%,   /* the copy floor - see the stop table above */
      rgba(var(--bg-rgb), 0.84) 62%, rgba(var(--bg-rgb), 0.88) 100%);
  }
  .hero-content { position: relative; z-index: 1; padding-top: calc(var(--header-h) + var(--space-4)); padding-bottom: var(--space-5); }
  /* The hero label is the ONE loudest label on the page, at --track-1, sitting under the title as the
     sub-line sits under the wordmark; --ink here, per the scrim arithmetic above. */
  .hero .eyebrow { color: var(--ink); letter-spacing: var(--track-1); }
  /* ---- 6.33 THE HOME HERO'S LABEL IN RED (.eyebrow--brand) ---------------------------------------
     THE FIRM, 2026-09-09: "Replace the line and 'Attorney Services' with a line and 'TWM LEGAL' all in
     the red color." A MODIFIER, NOT AN EDIT TO THE RULE ABOVE, because the instruction names the home
     page: every other hero label - /attorneys, /real-estate, /cal-osha-defense-and-appeal and the rest -
     keeps --ink and the reasoning that put it there.
     ONE DECLARATION DOES BOTH HALVES OF THE INSTRUCTION. .eyebrow::before paints
     `background: currentColor`, so the rule has always followed its label's colour; it was ivory only
     because the label was. Making the label red makes the line red. There is no second rule.
     SAME SPECIFICITY AS THE RULE ABOVE (0,2,0), so it wins on source order alone and must stay directly
     after it. Written adjacent rather than in the 6.x sequence at the foot of the layer for that reason:
     separating them would make the order load-bearing at a distance.
     THE CONTRAST IS NOT THE TOKEN FIGURE, AND THE DIFFERENCE IS THE POINT. --brand on flat --bg is
     2.99:1, the failure the firm decided to take on 2026-09-06. This label does NOT sit on flat --bg: it
     sits over the hero photograph behind .hero-media::after's gradient, 0.68 alpha at the copy floor. So
     2.99:1 is a FLOOR the composited pixel cannot beat, not the number a visitor sees. The sampled figure
     is recorded in site-plan decision "home-hero-label-and-lawyer-in-red-2026-09-09" rather than asserted
     here, because it depends on a photograph and no token arithmetic can produce it. */
  .hero .eyebrow--brand { color: var(--brand); }
  /* 19ch -> 16ch. THE UNIT IS THE DISPLAY FACE'S "0", SO THE COUNT HAD TO MOVE WITH THE FACE.
     Newsreader's "0" is 0.5845em at the hero instance (opsz 44.51 / wght 450 - it WIDENS with opsz,
     0.569em at 27.51) against Source Serif 4's 0.486em, +20.3%. What the cap actually defends is a line
     length in average characters, and that is what is held: the old 19ch = 412.7px was 22.0 average
     characters at that instance (Source Serif 4 averaged 0.4214em there); 16ch = 16 x 0.5845 x 44.51 =
     416.2px = 21.7 average characters
     (average = 0.43090em x 44.51 = 19.18px). Same line, four fewer zeros' worth of ch.
     [REPAIR v10] THE SECOND HALF OF THIS NOTE WAS WRONG AND IS WITHDRAWN. It claimed keeping 19ch
     "would have moved a breakpoint, not just a wrap": 19 x 0.5845 x 44.51 = 494.3px + 55.01 gutters =
     549.3px, against the form row's 550.9px in --bp-phone's max(). 549.3 < 550.9, so the hero row would
     have missed governing by 1.6px and --bp-phone's derivation would have asked for the same 34.43rem
     it asks for now. The cap change stands on the line-length argument above, which is the argument it
     was actually made on, and needs no help from a breakpoint that never moved.
     balance always operates inside
     an already-sane line length. Tracking stays at base's 0 - the negative-tracking decision is a
     viewing test (DESIGN-SYSTEM s9) that has not been run, and a guess would be taste, not arithmetic. */
  .hero-title { font-size: var(--text-4); line-height: var(--leading-display); margin-top: var(--space-1); max-width: 16ch; text-wrap: balance; color: var(--ink); }
  /* Emphasis inside display type is weight + colour, never slant. font-style is declared explicitly so
     <em> cannot borrow the citation italic. (The prohibition is in block 0; this is the rule.) */
  .hero-title em, .display em { font-style: normal; font-weight: var(--wt-strong); color: var(--brand); }   /* TEXT, so the text stop. 2.99:1 on --bg at 44.51px / 34.99px: large text, floor 3:1, missed by 0.01. Recorded failure */
  /* 34ch HELD, and it is the one ch cap the face change left alone - because this one is set in the BODY
     face, whose "0"-to-average ratio barely moved: Montserrat 0.662 / 0.49072 = 1.349 against Titillium
     0.560 / 0.41793 = 1.340, under 1% apart. 34ch = 34 x 0.662 x 21.62 = 486.7px = 45.9 average
     characters at --text-1, where it was 411.7px = 45.6. The line got wider in px and stayed the same
     line in characters, which is what the cap is for. */
  .hero-sub { margin-top: var(--space-2); font-size: var(--text-1); line-height: var(--leading-lede); max-width: 34ch; text-wrap: pretty; color: var(--ink-2); }
  /* [SCRIM v12] THE DECK GIVES UP ITS MUTING WHEN THERE IS A PHOTOGRAPH UNDER IT, and this is the second
     half of the scrim repair above, not a taste change. --ink-2 at 21.62px/400 is small text by 1.4.3, so
     it needs 4.5:1 - and over a pure-white pixel with the pointer light on it that is a veil of 0.8360.
     .hero-sub's topmost glyph is at 38.43% of the frame (business / employment-law at 375x667), so
     honouring it would mean 0.84+ from 38% down - the near-opaque veil 6.3 already refuses for red, and
     the end of the photograph as anything but a texture. On the ramp that ships (a 0.7457 at 38.43%) the
     two inks read: --ink-2 3.83:1 bare and 3.29:1 lit, against --ink 8.05:1 bare and 6.47:1 lit.
     At the ramp's own floor, the 0.68 stop, --ink is still 5.17:1 lit.
     THE HIERARCHY DOES NOT COLLAPSE, because it was never carried by the tint alone: the deck is
     21.62px/400 in the body face under a 44.51px/450 serif title, and the eyebrow above it is 13.36px
     tracked caps. Three sizes, three faces-or-weights, one ink - the same arrangement .hero .eyebrow and
     .hero-title already have with each other, for the same reason.
     A SIBLING COMBINATOR, NOT :has(). The trigger is "is there a photograph in this hero", and
     .hero-media precedes .hero-content in every hero (the markup contract fixes that order), so a plain
     ~ answers it in every browser that has ever shipped. :has() would have been the more fashionable
     spelling and a worse one: this rule is an accessibility floor, and a floor must not depend on a
     selector whose absence fails OPEN. [ZOOM v13] THE UNPHOTOGRAPHED BRANCH IS EMPTYING, AND MUST NOT
     BE DELETED FOR IT. flat-fee-services.html has been the one page with a .hero-sub and no
     .hero-media; the firm's licensed photograph of 2026-09-06 restores that page's hero, and with it in
     place every deck on the site sits over a scrim and takes --ink. The fall-through is KEPT AND STILL
     MEASURED, because it is the rule the next hero without a picture lands on, and because a branch
     that matches nothing today is not the same as one that cannot be reached (404 is the only other
     hero with no photograph, and it simply has no deck to mute) -
     its deck keeps --ink-2 on the flat --bg ground, where it measures 8.44:1. */
  .hero-media ~ .hero-content .hero-sub { color: var(--ink); }
  .hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
  .section-head--center ~ .hero-actions { justify-content: center; }

  /* ---- 6.4 SECTION HEAD + EYEBROW (LABEL/RULE register) -----------------------------------------
     The hairline is authored in its FINISHED state and knocked back ONLY under proof the script ran
     (html.js + [data-reveal] + :not(.is-revealed)). Written the other way round, every failure path -
     the 404, a blocked script, the 2.5s watchdog - would strand it at zero forever. */
  .section > .container > * + * { margin-top: var(--space-4); }   /* one rhythm rule for every section */
  .section-head .display { margin-top: var(--space-1); }
  .section-head .lede { margin-top: var(--space-2); }
  .eyebrow { display: flex; align-items: center; gap: var(--space-1); font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--eyebrow-track); text-transform: uppercase; color: var(--brand); }   /* TEXT, so the text stop. 2.99:1 on --bg / 2.86:1 on --bg-2 at 13.36px/600 - BELOW AA. Recorded failure */
  /* 2.5rem = 42.5px = 3.18 x the 13.36px label band: long enough to read as a rule, short enough not to
     become a divider. [RED v7] THE TWO HAIRLINES BELOW PAINT `background: currentColor`, so they take the
     TEXT stop and not --brand-fill, even though a rule is an AREA. [RED v8] The two stops now hold the
     same hex, so the distinction paints identically today and is kept for the day the one-line revert is
     taken - at which point these hairlines follow their label to #FF3D3D, which is what currentColor is
     for. That is deliberate and it is
     the one exception to the fill rule: the hairline is part of the same object as the label it extends,
     currentColor is what keeps it that way through every band rebinding, and splitting it off would mean
     naming the eyebrow's rule in a second declaration that a band rebinding could then miss. At 1px on
     --bg it is decoration in any case, so no non-text floor binds. */
  /* The delay is the element's own stagger slot (guards 7.4) and nothing else: the eyebrow's rule has
     no intra-object lag to add, it simply must not draw while its label is still invisible. */
  .eyebrow::before { content: ""; flex: none; width: 2.5rem; height: 1px; background: currentColor; transform-origin: left; transition: transform var(--dur-slow) var(--ease-out) var(--reveal-delay, 0s); }
  html.js .eyebrow[data-reveal]:not(.is-revealed)::before { transform: scaleX(0); }
  /* Centred: the rule is mirrored, and the right-hand one is pulled back by exactly minus the label's
     own tracking, because CSS adds letter-spacing after the LAST character too. Held in
     --eyebrow-track-cancel so the two can never drift apart. */
  .section-head--center { text-align: center; }
  .section-head--center .eyebrow { justify-content: center; }
  .section-head--center .eyebrow::after { content: ""; flex: none; width: 2.5rem; height: 1px; background: currentColor; margin-left: var(--eyebrow-track-cancel); transform-origin: right; transition: transform var(--dur-slow) var(--ease-out) var(--reveal-delay, 0s); }
  html.js .section-head--center .eyebrow[data-reveal]:not(.is-revealed)::after { transform: scaleX(0); }
  .section-head--center .display, .section-head--center .lede { margin-inline: auto; }

  /* ---- 6.5 DISPLAY, LEDE, PROSE ----------------------------------------------------------------- */
  /* 26ch -> 22ch, the same arithmetic as .hero-title one rung down. Newsreader's "0" is 0.5755em at
     opsz 34.99 / wght 450 against Source Serif 4's 0.495em, so 26ch would now be 523.5px = 35.4 average
     characters (average = 0.42300em x 34.99 = 14.80px) where it used to hold 29.9. 22ch = 22 x 0.5755 x
     34.99 = 443.0px = 29.9 average characters - the line the cap was set to defend. balance always
     paired with a ch cap. */
  .display { font-size: var(--text-3); line-height: var(--leading-heading); max-width: 22ch; text-wrap: balance; color: var(--ink); }
  .lede { font-size: var(--text-1); line-height: var(--leading-lede); max-width: var(--measure); text-wrap: pretty; color: var(--ink-2); }   /* 8.44:1 on --bg */
  .prose > * + * { margin-top: var(--space-2); }
  /* h3/h4 in prose stay in the BODY face: --text-1 is the provisional serif floor, and a Montserrat
     sentence-case heading above --text-1 is what the constitution forbids, so this rung is the sans's. */
  .prose h3, .prose h4 { margin-top: var(--space-3); font-size: var(--text-1); font-weight: var(--wt-strong); }
    /* :not(.btn) is load-bearing, not tidiness. A descendant link rule is (0,1,1) and .btn is (0,1,0),
     so WITHOUT it this rule wins on any button that lands inside running copy and paints --brand text
     over the button's own fill. When both were --brand that was 1:1, an invisible button, and it
     shipped that way on /make-a-payment.
     [RED v7] The failure did not go away when the stop split, it changed shape: --brand text on
     --brand-fill measured 1.85:1 - no longer invisible, and WORSE for it, because a red-on-red button
     survives a glance at a screenshot where a blank one does not.
     [RED v8] IT IS BACK TO 1:1 EXACTLY, because the two stops collapsed onto one hex - the original
     invisible-button failure, reconstituted by a change nobody would connect to it. :not(.btn) is the
     only thing between this file and shipping that bug for a second time. A component
     must survive being placed anywhere, so the container yields to it rather than the reverse. */
  /* [HOVER v23] :not(.link-arrow) JOINS :not(.btn), FOR THE REASON THE PARAGRAPH ABOVE ALREADY GIVES.
     .link-arrow declares its own hover at (0,2,0); this rule is (0,2,1) and outranks it, so the component
     lost its colour hover wherever running copy contained one - the same arrow went white on a card and
     stayed red in prose, which is a component behaving differently for no reason but where it was put.
     The container yields to the component here exactly as it yields to .btn, and on the same sentence.
     AND THE PLAIN LINKS HOVER NOW, which they never did: rest and hover were the same red, the same
     --line-control underline, the same everything, on the most numerous link on the site. They take the
     scheme's one hover colour and the underline goes to currentColor with it, which is .contact-list's
     rule verbatim: 2.99:1 -> 17.73:1 on --bg, 17.00:1 on --bg-2, 16.00:1 on --bg-3, and the only state of
     a red body link that meets AA. Focus takes the identical pair outside the pointer gate. */
  .prose a:not(.btn):not(.link-arrow) { color: var(--brand); text-decoration-color: var(--line-control); text-underline-offset: 0.18em; transition: color var(--dur-fast) ease, text-decoration-color var(--dur-fast) ease; }   /* 2.99:1 at 17px - BELOW AA. Recorded failure. The underline is --line-control, so the link is not identified by colour alone */
  :where(html:not(.has-touch)) .prose a:not(.btn):not(.link-arrow):hover { color: var(--ink); text-decoration-color: currentColor; }
  .prose a:not(.btn):not(.link-arrow):focus-visible { color: var(--ink); text-decoration-color: currentColor; }
  /* Two columns INTRINSICALLY, from the container's own derivation (a): each column is one full
     measure, so the pair appears only where 2 x 34.35 + --space-4 2.618 = 71.318rem = 1212.4px of
     container fits (~1267.4px of viewport) and resolves to one column of the same measure below it. No
     breakpoint. [FACE v9] BOTH FIGURES MOVED UP WITH --measure (1039.0 -> 1212.4px of container,
     1094 -> 1267.4px of viewport), because 70 characters of Montserrat is 17.4% wider than 70 of
     Titillium. The two-column prose now appears ~173px of viewport later than it did, which is the same
     cost the feature split pays in 6.11 and for the same reason. */
  .prose--2col { max-width: calc(2 * var(--measure) + var(--space-4)); display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--measure)), 1fr)); gap: var(--space-2) var(--space-4); }
  /* margin-top: 0 because the GRID owns the spacing here - gap is --space-2 row / --space-4 column, and a
     stacked margin on top of it would space the children twice. THE max-width IS A DEFECT REPAIR, added
     2026-09-07 when /real-estate's #intro took this class. In the COLLAPSED state auto-fit drops to one
     track, that track is 1fr - the whole container - and a bare <p> has nothing of its own to stop it
     filling the lot. Measured on the built site, on /index, the class's only consumer until today:
     584px / ~70 characters a line at 1440, then 1045px / ~126 characters at 1100, 969px / ~117 at 1024,
     845px / ~102 at 900, 713px / ~86 at 768. --measure's own token comment in block 3 reads "max-width on
     every running-text block", so four of those five widths break the rule the token exists to state, at
     ordinary laptop sizes. .check-list was never caught by it because it declares max-width: var(--measure)
     itself in 6.6 - which is exactly the thing a bare <p> inside this grid lacks, so the list stayed inside
     the measure while the paragraph beside it did not. THE CAP IS INVISIBLE IN THE TWO-COLUMN STATE, and
     that was measured rather than trusted to the arithmetic: the two tracks come out 583.938px and
     583.953px against a --measure of 583.95px, so the cap sits ABOVE the first track and 0.015625px - one
     sixty-fourth of a CSS pixel, Chrome's own LayoutUnit - BELOW the second. AN EARLIER WORDING OF THIS
     COMMENT READ "the cap sits at or above the track", WHICH IS FALSE OF THE SECOND TRACK AND IS CORRECTED
     HERE: the cap does take that track, by that one quantum, and the honest claim is the narrower one -
     no column narrows by a VISIBLE pixel. That was settled with pixels rather than with arithmetic. At
     1440 the band captured with this declaration and with it deleted out of the live rule is byte-identical
     on both consumers - a diff of 0 differing pixels of 408994 on /index and 0 of 485792 on /real-estate -
     and the line counts, the child heights and the column balance are identical to three decimals.
     DESIGN-SYSTEM 16.3 carries the same figure and always did; this comment was the only record that
     overstated it. One property therefore does both jobs - it lets /real-estate adopt the class
     without propagating the defect, and it repairs /index's own paragraphs at 768-1100px. The home page
     changing is a consequence stated here rather than slipped in; reverting it is deleting this one
     declaration.
     TWO HAZARDS THIS RULE CARRIES, WRITTEN AT THE RULE BECAUSE THAT IS WHERE THE NEXT EDITOR WILL BE.
     (1) THE TWO-TRACK FIT HAS ZERO MARGIN. The wrapper's max-width above is calc(2 * --measure +
     --space-4), which is exactly what two tracks need, so (1212.406 - 44.506) / 583.95 lands on 2.000
     with nothing to spare, and block 2 puts border-box on everything. ONE pixel of padding or a 1px
     border added to .prose--2col therefore spends the whole allowance and drops BOTH consumers to one
     column at every width, with no other visual signal that the firm's instruction has stopped being
     delivered. Pad or border a wrapper INSIDE the grid, never this element. (2) THE PAIR IS ROOT-SIZE
     GATED AS WELL AS WIDTH GATED, because --measure is in rem while the window is not: measured, a
     visitor at 125% root type needs about 1481px of window for the second column and one at 150% about
     1770px, where the shipped 106.25% needs 1268px. That is the system behaving correctly - the cap
     scales with the type, so the repair holds at every root size - but it is why "wide desktop" cannot
     be read off the viewport alone. DESIGN-SYSTEM 16.5 carries both with their measurements. */
  .prose--2col > * { margin-top: 0; max-width: var(--measure); }

  /* ---- 6.6 CHECK LIST (LIST register) -----------------------------------------------------------
     A diamond: a square rotated 45 degrees, side 0.30em so its diagonal is 0.424em - a bullet's
     optical mass without a bullet's roundness, which the rectilinear mark forbids. It paints from
     currentColor with its own colour set on the pseudo-element, so a band rebinding reaches it with no
     rule naming it. Vertical centring on the first line: 0.5 x 1.6em leading - 0.15em = 0.65em.
     [RED v7] The diamond is a FILLED MARK, so it takes --brand-fill, not the text stop. Its 0.424em
     diagonal is 7.2px of ink and it carries no information the row's own text does not - a bullet is
     redundant by definition - so WCAG's non-text boundary criterion does not bind on it. Stated because
     the arithmetic is close enough to invite the question. [RED v8] IT NO LONGER CLEARS THAT FLOOR ON ANY
     GROUND - 2.99:1 on --bg, 2.86:1 on --bg-2 - where at v7 it cleared on --bg and missed on --bg-2. The
     exemption is unchanged and is the only reason this is fine: a mark that DID carry information would
     now need var(--ink), because there is no longer a red stop of any kind that clears 3:1 - [HOVER v22]
     retired --brand-bright, which was the answer this sentence used to give, so an informative mark has
     the same one answer every indicator on the site now has. */
  .check-list { list-style: none; padding: 0; max-width: var(--measure); display: grid; gap: var(--space-1); }
  .check-list li { position: relative; padding-left: var(--space-3); }
  /* [MOTION v20] THE DIAMOND NOW ARRIVES. It was painted at page build and never moved again, on a
     component that carries [data-reveal] on seven lists on /firm-overview alone - so the rows travelled
     in and their marks were simply already there, which is the one arrival on this site that had no
     choreography at all. OPACITY ONLY, AND AT FULL SIZE. The obvious version of this gesture pops the
     mark up from a scale, and it is refused for the reason the sister site refused it after shipping it:
     a mark that springs is a cheap effect on a page trying to look expensive, and this file has no other
     scale-in anywhere. --dur-base rather than --dur-slow because the mark is the smallest object in the
     register and the register's own rule (7px of ink) - the LIST hairline gets the slow rung, its bullets
     do not. --lag-rule so the marks follow the text they belong to rather than racing it, added to the
     inherited --reveal-delay exactly as .num-list's hairline adds it. */
  .check-list li::before { content: ""; position: absolute; left: 0; top: calc(0.5 * var(--leading-body) * 1em - 0.15em); width: 0.30em; height: 0.30em; rotate: 45deg; color: var(--brand-fill); background: currentColor; transition: opacity var(--dur-base) var(--ease-out) calc(var(--reveal-delay, 0s) + var(--lag-rule)),
                          scale var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
  html.js .check-list[data-reveal]:not(.is-revealed) li::before { opacity: 0; }

  /* ---- [LIST-HOVER v25] 2026-09-07 - EVERY BULLET ANSWERS THE POINTER ------------------------------
     The firm asked that "all bullet points in this page and all other pages" be interactive on hover.
     That is 21 .check-list instances and the single .num-list on /commercial-real-estate-services; the
     nav, footer and contact lists are LINK lists and already answer through 6.5 / 6.22 / 6.16.
     [COUNT CORRECTED IN THE 2026-09-07 CLOSE-OUT] That census read 23 when this block was written that
     morning and is corrected here rather than left standing: /employment-law's two twenty-item
     .check-lists went with decision 49 the same day, so the shipped figure is 21 - re-counted over
     src/pages with HTML comments stripped, and it agrees with docs/MARKUP-CONTRACT.md section 3 and with
     decision 52. The RULE below is untouched; only the count was stale.

     WHY NOT THE TEXT. A .check-list item is ALREADY --ink - 17.73:1 on --bg, 16.00:1 on --bg-3 - before
     the pointer arrives, so there is no brightening left to give the row: it sits at the top of the ink
     ladder at rest, on every ground it ships on. The response is therefore carried by the MARK and, on
     .check-list, by the row's position.

     ONE MULTIPLIER PER ELEMENT, AND THEY DIFFER BY A FACTOR OF THREE ON PURPOSE. The site already has a
     magnification gesture - 6.12's --pop 1.06 on a --text-4 figure - and what it is really worth is not
     the ratio but the ABSOLUTE growth it puts on screen:
         .stat-num   44.51px x 1.06 = 47.18px   ->  +2.67px
         numeral     13.36px x 1.15 = 15.36px   ->  +2.00px
         diamond      5.09px x 1.50 =  7.64px   ->  +2.55px
     Roughly two and a half pixels, on all three. That is the quantity the eye is actually reading, and
     it is why --pop cannot simply be reused here: 1.06 on a 5.09px diamond is 0.31px of growth, which is
     below the threshold of anything a reader would call a response. The ratios are derived; the ~2.5px
     is the constant.

     THE DIAMOND CANNOT COLLIDE AND CANNOT BE RE-ORIGINED. It is 0.30em (5.09px at the 17px body) at
     `left: 0` inside a --space-3 (27.51px) padding-left. `scale` grows it about its own centre at 2.545,
     so at 1.5 it spans -1.27px to 6.36px: 21.15px of clear space still separates it from the text, and
     the 1.27px that crosses the row's left edge lands in the container gutter, which is --space-3 wide.
     Centre-scaling is not a choice - `rotate: 45deg` already pivots on that origin, and moving
     transform-origin to correct the bleed would swing the diamond off its own baseline. `scale` and not
     `transform` for the same reason 6.9 gives: one property, one owner.

     .num-list DOES NOT TAKE THE NUDGE, AND THIS IS THE ONE REAL FINDING IN THIS PASS. Its row carries
     `li::after { inset: 0 0 auto 0 }` - a full-measure hairline at the TOP of each row, so the rows read
     as a single ruled column. Translating the row would carry its rule 4px right while the rules above
     and below it stayed put, putting a visible jog in that column. The row instead answers with the rule
     it already has: --line goes to --brand-fill, which is 6.29's exact idiom (a --line becoming
     --brand-fill under the pointer) applied to a hairline instead of a border. No var()-with-fallback
     dance is needed the way 6.29 needed one - that block's border-color had a second owner in the clip
     and the scale; here `background` is declared in exactly one rule and overridden in exactly one.
     ITS TRANSITION CARRIES NO DELAY. li::after's existing transition is the REVEAL's, delayed by
     calc(--reveal-delay + --lag-rule); appending `background` to that entry would make a hover wait out
     an 0.18s intra-object lag that has nothing to do with the pointer. It gets its own undelayed one.

     .check-list DOES take the nudge - it has no rule to drag - and it is --nudge (4px), LATERALLY: these
     rows sit at --space-1 (10.51px) gaps and a vertical nudge would carry one row toward the one above.

     REDUCED MOTION - subtracted in 5.2 beside .stat-num, on that block's stated line that "colour-only
     responses stay". The scales and the translate are displacements and go; the diamond's --ink and the
     hairline's --brand-fill stay, so neither list is left answering with NOTHING - the failure 6.12's
     [HOVER v23] had to repair on the Avvo badge. Giving the diamond a colour channel at all is what
     makes that subtraction affordable, and is the reason `color` is in its transition list.

     NO :focus-visible PARITY, RECORDED AS A DECISION RATHER THAN AN OMISSION. An <li> is not focusable,
     so there is no state to pair with. A .check-list item that CONTAINS a link keeps that link's own
     hover and focus rules from the block below; the two compose rather than compete.
     AND THESE ROWS ARE NOT AFFORDANCES. No lift, no brand edge, no pointer cursor - 6's
     AFFORDANCE-vs-MATERIAL split holds: this is emphasis on the thing being read, not the promise of a
     click that would not happen. */
  .check-list li { transition: translate var(--dur-fast) var(--ease-out); }
  :where(html:not(.has-touch)) .check-list li:hover { translate: var(--nudge) 0; }
  :where(html:not(.has-touch)) .check-list li:hover::before { scale: 1.5; color: var(--ink); }   /* 17.73:1 on --bg, 16.00:1 on --bg-3 - the mark takes the same stop .check-list a:hover takes below, and the mark is exempt from 1.4.11 either way (6.6) */
  :where(html:not(.has-touch)) .num-list li:hover::before { scale: 1.15; }
  :where(html:not(.has-touch)) .num-list li:hover::after { background: var(--brand-fill); }
    /* [HOVER v23] THE SAME TWO REPAIRS 6.5 TAKES, AND THIS LIST NEEDED THEM MORE: it is the practice-area
       link list on /firm-overview and every service page, so these are the links a visitor actually
       follows. :not(.link-arrow) so the row yields to the component; a hover and a focus because there
       was neither. Same pair, same figures: 2.99:1 -> 17.73:1 on --bg, 17.00:1 on --bg-2. */
    .check-list a:not(.btn):not(.link-arrow) { color: var(--brand); text-decoration-color: var(--line-control); text-underline-offset: 0.18em; transition: color var(--dur-fast) ease, text-decoration-color var(--dur-fast) ease; }   /* TEXT, so the text stop. 2.99:1 at 17px - BELOW AA. Recorded failure */
    :where(html:not(.has-touch)) .check-list a:not(.btn):not(.link-arrow):hover { color: var(--ink); text-decoration-color: currentColor; }
    .check-list a:not(.btn):not(.link-arrow):focus-visible { color: var(--ink); text-decoration-color: currentColor; }

  /* ---- 6.7 NUMBERED PROCEDURE (.num-list, LIST register) ----------------------------------------
     Entirely Montserrat: the numeral sits at --text-label, far below the serif floor, where Newsreader's
     figures are anaemic reversed out of near-black - all cost, no benefit. The row hairline
     is this register's arrival, drawing with the fixed --lag-rule (0.18s) intra-object lag, which is
     choreography inside one object rather than stagger across a group. Finished by default.
     [RED v7] THE NUMERAL IS TEXT, NOT A MARK, so it takes --brand and not --brand-fill. That is the one
     place the v7 brief's own two lists disagreed, and this is the resolution: `content: counter(...)`
     generates GLYPHS, set at --text-label with a weight and a tracking rung, sitting on the page ground -
     everything about it is type. [RED v8] The two stops now paint the same colour, so the distinction
     changes nothing on screen and is kept for exactly the reason the token block gives: it is what makes
     the one-line revert put the numeral back at 5.56:1 without touching a single fill. As it ships the
     numeral is 2.99:1 at 13.36px/600 - BELOW AA, a recorded failure. */
  .num-list { list-style: none; padding: 0; counter-reset: num; max-width: var(--measure); }
  .num-list li { position: relative; counter-increment: num; padding: var(--space-3) 0 var(--space-3) 3.5rem; }
  .num-list li::before { content: counter(num, decimal-leading-zero); position: absolute; left: 0; top: var(--space-3); font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-8); color: var(--brand); font-variant-numeric: lining-nums tabular-nums; }   /* 2.99:1 - recorded failure */
  /* The two delays COMPOSE: --reveal-delay is where this object sits in the group's cascade (JS's, via
     guards 7.4) and --lag-rule is where the hairline sits inside the object (CSS's). Adding them is
     what makes the 0.18s a lag rather than a number measured from an accidental zero. [MOTION v20] The
     numeral follows the rule at twice that lag; the rule below it carries the sequence. */
  .num-list li::after { content: ""; position: absolute; inset: 0 0 auto 0; height: 1px; background: var(--line); transform-origin: left; transition: transform var(--dur-slow) var(--ease-out) calc(var(--reveal-delay, 0s) + var(--lag-rule)),
                        background var(--dur-fast) var(--ease-out); }
  html.js .num-list[data-reveal]:not(.is-revealed) li::after { transform: scaleX(0); }
  /* [MOTION v20] THE NUMERAL ARRIVES TOO, AND IT ARRIVES AFTER ITS OWN RULE. Until this pass the LIST
     register's whole choreography was one hairline: the row's rule drew, and the numeral that names the
     row was simply present from the first frame. So the register had a sequence of one, on the component
     whose entire subject is sequence.
     THE DELAY IS 2 x --lag-rule AND THAT IS THE POINT OF IT. The hairline already lags the row by
     --lag-rule 0.18s; putting the numeral at 0.36s makes the object read row -> rule -> numeral, three
     beats out of two literals, and keeps the second beat measured in the same unit as the first rather
     than in a new one. It composes with the inherited --reveal-delay for the same reason .num-list's
     hairline does - the lag is measured from this object's own arrival, never from the batch's.
     IT TRAVELS THE WAY THE RULE DRAWS. The rule's transform-origin is `left`, so the row is built from the
     left edge; the numeral therefore comes IN from the left, --nudge of travel, rather than rising like a
     plate. One direction per object. `translate` and `opacity` only, and a transition rather than an
     animation, so block 5's clamp neutralises it with nothing new to write.
     WHY THIS IS AN ARRIVAL AND NOT A HOVER. The sister site moves this same numeral on `li:hover`, and
     that is refused here on 6's own AFFORDANCE-vs-MATERIAL test: a .num-list row is not a link and leads
     nowhere, so a pointer response on it would teach a click that does not exist - the exact defect this
     block's preamble is written against. An arrival is not a pointer response, so the register is
     untouched by adding one. */
  .num-list li::before { transition: opacity var(--dur-slow) var(--ease-out) calc(var(--reveal-delay, 0s) + 2 * var(--lag-rule)), translate var(--dur-slow) var(--ease-out) calc(var(--reveal-delay, 0s) + 2 * var(--lag-rule)),
                         scale var(--dur-fast) var(--ease-out); }
  html.js .num-list[data-reveal]:not(.is-revealed) li::before { opacity: 0; translate: calc(-1 * var(--nudge)) 0; }
  .num-list h3 { font-size: var(--text-1); font-weight: var(--wt-strong); line-height: var(--leading-lede); }
  .num-list p { margin-top: var(--space-1); color: var(--ink-2); }

  /* ---- 6.8 DISPLAY-FACE GRANT, EXTENSION: RETIRED --------------------------------------------------
     This block used to re-declare the whole display grant for .svc-card h3, .act-card h3 and .package
     h3 because base's list named ".card h3", a class no page ships. The three are now IN base's list
     (the UNRESOLVED note this block carried, resolved), so a grep of that one list still answers
     "where does the serif appear". Nothing replaces it here; do not re-add a second copy. */

  /* ---- 6.9 SERVICE CARDS (.svc-grid, .svc-card) - AFFORDANCE ------------------------------------
     Exactly one <a> per card, stretched by an ::after overlay - never a wrapping anchor (it becomes
     the grid item and silently reflows the body at one width only) and never display: contents (no
     box, so no focus ring at all).
     THE INTRINSIC CARD GRID LIVES HERE, and this is its only copy. It was written twice - once as a
     `.grid` primitive in @layer layout that no page and no partial ever used, once here for the three
     real grid classes - so the sum below governed a rule nobody consumed while its duplicate did the
     work. The unused primitive is deleted and its derivation moved here intact:
       track count follows space, never a breakpoint. min(100%, track) so a track can never overflow a
       320px phone (the 374px track resolves to the 276.75px column there). Implied points (viewport =
       tracks + gaps + 2 x 27.51px gutters; +17px where a classic scrollbar is present):
         2-up: 2 x 374 + 27.51 = 775.5px of container -> 830.5px viewport (847.5 with scrollbar)
         3-up: 3 x 374 + 2 x 27.51 = 1177.0px -> 1232.0px viewport (1249.0); the container caps at 1224
               from 1279.0px and the tracks grow to 22.921rem (389.66px of track, 332.65px of inner =
               39.9 characters in Montserrat, where the same track held 46.8 of Titillium)
       so 1-up below ~831px, 2-up to ~1232px, 3-up above. The phone gutter never matters here (2-up
       needs 818.8px even at 21.62px gutters, and the phone number is 480). NONE OF THIS SUM MOVED WITH
       THE FACE - every term is --grid-track, --space-3 or --gutter, all held - but the CHARACTER COUNTS
       it used to quote did: the grown track's inner is 39.9 characters of Montserrat where it was 46.8
       of Titillium. The container derivation carries that, and it is why the 40-character content rule
       is now a recorded exception rather than a satisfied one.
     [RED v7] BOTH IMPLIED POINTS MOVED UP WITH --grid-track, and that is the trade the wider card buys:
     3-up now begins at 1232px of viewport where it began at 1081px, so between roughly 1081 and 1232 a
     three-card row is a two-card row instead. That is a consequence of a directed minimum, not a
     regression - the instruction was "the cards are too small", and a card that stays 374px wide by
     dropping to 2-up is the instruction being obeyed rather than evaded.
     The 78.1rem specular-edge query below is a row of THIS sum: do not change --grid-track or the gap
     without redoing both. */
  .svc-grid, .act-grid, .package-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--grid-track)), 1fr)); gap: var(--space-3); }
  .svc-card { position: relative; display: flex; flex-direction: column; overflow: hidden; background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); transition: border-color var(--dur-base) ease, translate var(--dur-base) var(--ease-out); }
  /* THE SPECULAR EDGE - one lamp above each row of three. Three cards with an identical highlight are
     three copies of a card; three whose highlights track one implied light source are one photographed
     object. A NEUTRAL light from --ink-rgb, never the accent: an accent rim reads as SELECTED about an
     <article>. z-index 2 because .svc-card-media is a positioned sibling starting at the top edge.
     Held to THIS grid only: on every card the page acquires a permanent glinting rim. */
  .svc-card::before {
    content: ""; position: absolute; inset: 0 0 auto 0; height: 1px; z-index: 2; pointer-events: none;
    background: linear-gradient(90deg, rgba(var(--ink-rgb), 0) 0%, rgba(var(--ink-rgb), 0.17) var(--spec, 50%), rgba(var(--ink-rgb), 0) 100%);
    opacity: 0.55; transition: opacity var(--dur-base) ease;
  }
  /* 3 x 374 track + 2 x 27.51 gap = 1177.0px of container + 2 x 27.51 gutters = 1232.0, + 17 for a
     classic scrollbar = 1249.0 -> 78.1rem (1249.6px, 0.6px slack). Per ROW, not per grid: the
     practice-area grid runs to seven cards, and a lamp lighting only the first three would leave the
     rest unlit. [RED v7] MOVED WITH THE TRACK, as the note in the grid derivation requires: left at
     67.6rem the 3n lamp offsets would have been assigned across a grid that is 2-up from 1081 to 1232px,
     which is a visible mis-lighting with one unambiguous fix. The rem here is media-query rem, i.e.
     16px-based and untouched by html { font-size: 106.25% }, which is exactly what this line wants -
     the sum it encodes is in real px. */
  @media (min-width: 78.1rem) {
    .svc-grid > .svc-card:nth-child(3n + 1) { --spec: 76%; }
    .svc-grid > .svc-card:nth-child(3n + 2) { --spec: 50%; }
    .svc-grid > .svc-card:nth-child(3n + 3) { --spec: 26%; }
  }
  .svc-card-media { position: relative; overflow: hidden; }
  .svc-card-media img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }
  .svc-card-body { display: flex; flex-direction: column; gap: var(--space-2); flex: 1; padding: var(--space-3); }
  .svc-card-body h3 { font-size: var(--text-2); line-height: var(--leading-heading); }
  .svc-card-body p { color: var(--ink-2); }
  .svc-card-body .svc-card-link { margin-top: auto; }
  /* ---- [LINK v15] THE CARD IS ONE CLICK TARGET, AND UNTIL v15 IT WAS NOT ------------------------
     MEASURED, NOT SUSPECTED. On all six home cards and all four on /real-estate,
     getComputedStyle(a, "::after") read 332.66 x 1px at top 0 with z-index 3; document
     .elementFromPoint at the picture's centre returned <img>; and a real click dispatched onto the
     photograph over CDP left location.pathname at "/". The whole-card affordance that this block,
     6.9a and docs/MARKUP-CONTRACT.md all assert had never worked on any card on the site.
     THE CAUSE IS ONE PSEUDO-ELEMENT ASKED TO DO TWO JOBS. The anchor carries BOTH classes, so
     `.svc-card-link::after` (here, the stretch overlay) and `.link-arrow::after` (6.19, the drawn
     rule) name the SAME box, at the same specificity (0,1,1), in the same layer - and 6.19 is ~840
     lines later, so source order handed it every property it declares. What shipped was the drawn
     rule wearing the overlay's z-index: left/right/bottom/height/background/transform from 6.19, and
     from here only `top: 0` - which survived the `inset` shorthand, then over-constrained against
     height and bottom so that `bottom` was dropped. Hence the second, visible half of the same bug:
     the rule drew along the TOP edge of the anchor box instead of 0.4em below it, a 25%-width red
     stub lying across "REAL ESTATE LAW" on every service card on the site.
     ONE ANCHOR CANNOT HOST BOTH AS PSEUDO-ELEMENTS, and that is a fact about containing blocks, not a
     preference: ::before and ::after of one element share one containing block. The overlay needs the
     CARD (inset: 0 must reach 390 x 489), the drawn rule needs the ANCHOR (left/right/bottom are the
     link's own 332.66px box). 6.19 makes .link-arrow position: relative, so the overlay could only
     ever have covered the anchor even had it won the cascade - the collision hid a second defect.
     Moving the overlay to ::before changes nothing (same containing block) and 7.2 has already spent
     that pseudo-element on the touch hit strip.
     SO THE CARD LINK'S RULE IS PAINTED AS A BACKGROUND AND ::after IS FREED FOR THE OVERLAY. A
     background needs no containing block, which is what lets this anchor go position: static and hand
     `inset: 0` to .svc-card. The gesture is term-for-term 6.19's, and the pair is written out because
     this file now holds TWO spellings of one decoration - a duplication accepted with its eyes open,
     and smaller than the one it replaces (the line retired below duplicated 6.19's hover VALUE across
     two blocks; this duplicates the mechanism in one place that can be diffed):
       6.19   ::after, left 0 / right 0, height 1px, background currentColor,
              transform scaleX(0.25) -> scaleX(1), origin left, over --dur-slow --ease-out
       here   background-size 25% 1px -> 100% 1px over --dur-slow --ease-out, positioned left bottom
              (a background grows from its position, so "origin: left" is structural, not declared)
     [MOTION v21] ONE OF THE TWO SPELLINGS REPAINTS AND THE OTHER DOES NOT, AND THAT ASYMMETRY WAS THE
     UNRECORDED HALF OF THIS DUPLICATION. A census of every animated property on the built pages found
     `background-size` as the file's ONLY animated geometry - 10 declarations, all of them this rule -
     against 485 `translate`, 409 `opacity`, 351 `transform`, 28 `rotate` and 11 `scale`. It is genuinely
     tweened rather than stepped (measured mid-transition at `71.8216% 1px`), so it is genuinely a repaint
     per frame where 6.19's scaleX is a compositor transform.
     IT IS KEPT, AND THE REASON IS THE SIZE OF THE THING BEING PAINTED RATHER THAN A SHRUG. The file's
     standing rule is NOTHING MAY TRIGGER LAYOUT, and this does not: `background-size` is a paint-only
     property, in the same class as the 344 `color`, 249 `border-color` and 148 `background-color`
     transitions this site could not exist without. What it repaints is a 1px-tall strip about 290px wide
     inside a box that is already being composited, on ONE card at a time, and only while a pointer rests
     on it. THE ALTERNATIVE IS WHAT MAKES THE TRADE CLEAR: 6.19's spelling needs an absolutely positioned
     pseudo-element, this anchor's ::after is the card's stretch overlay, and its ::before cannot be used
     because 6.9 makes the anchor `position: static` on purpose - so an absolute ::before would resolve
     against .svc-card and paint a rule across the middle of the picture. Buying back 290 painted pixels
     by breaking the card's single click target is not a trade worth taking. Recorded here so the next
     reader gets the measurement instead of rediscovering the question.
     THE 0.4em OFFSET IS KEPT AND PAID FOR IN PADDING, because a background is clipped to the box and
     6.19's rule deliberately hangs OUTSIDE it. --text-label is 0.786rem = 13.36px, so 0.4em = 5.34px:
     padding-bottom: 0.4em puts the padding-box edge exactly where the ::after's lower edge was (the
     26.78px top offset a plain .link-arrow computes = 22.44px content box - 1px + 5.34px), and
     margin-bottom: -0.4em hands those 5.34px back to the layout, so no card gets 5px taller.
     WHAT CHANGES BESIDES THE HIT TARGET, said out loud because it is visible: hovering anywhere on
     the card now puts the ANCHOR in :hover - that is what a working stretch means - so the card's
     hover completes the whole link gesture (rule, colour, glyph) instead of only the rule. That is
     why the cross-block line 6.9 used to carry is retired rather than translated. Text inside the
     card also stops being selectable, which is the standing cost of this pattern and the reason
     MARKUP-CONTRACT asks for exactly one <a> per card rather than a wrapping one. */
  /* THE DESCENDANT SELECTOR IS NOT DECORATION, AND THE FIRST CUT OF THIS REPAIR SHIPPED WITHOUT IT AND
     FAILED ON THE HARNESS: at `.svc-card-link` alone this rule is (0,1,0) and 6.19's `.link-arrow
     { position: relative }` is also (0,1,0), ~840 lines later - the same source-order loss that caused
     the bug in the first place, one property along. The overlay came back 332.66 x 27.78 (the anchor's
     own box, padding included) instead of 390 x 489. `.svc-card .svc-card-link` is (0,2,0) and wins on
     specificity, so it cannot be undone by anything moving in 6.19. The class is card-only by contract
     anyway (MARKUP-CONTRACT: exactly one <a> per card), so naming the card costs nothing and says where
     `inset: 0` is expected to land. */
  .svc-card .svc-card-link {
    position: static;                                 /* so ::after's containing block is .svc-card, not the anchor */
    padding-bottom: 0.4em; margin-bottom: -0.4em;     /* 5.34px of paint, 0px of layout - see above */
    background-image: linear-gradient(currentColor, currentColor);
    background-repeat: no-repeat; background-position: left bottom; background-size: 25% 1px;
    transition: background-size var(--dur-slow) var(--ease-out);
  }
  .svc-card-link::after { content: ""; position: absolute; inset: 0; z-index: 3; border-radius: var(--radius); }
  :where(html:not(.has-touch)) .svc-card-link:hover { background-size: 100% 1px; }
  .svc-card-link:focus-visible { background-size: 100% 1px; color: var(--ink); }   /* [HOVER v23] colour parity - see 6.22 */
  /* Hover, once. A --nudge rise - present, under the threshold of a hop. The border warms to the accent:
     the one place a card may say SELECTED, because this one can be. Never box-shadow.
     THE RISE RE-CONFIRMED against the playbook when 6.26/6.27 were added, because the audit that makes a
     hover family cohere is a comparison, not an inspection: the nudge is the site's ONE travel distance,
     byte-identical to .btn's and to .link-arrow's lateral travel.
     [MOTION v20] IT IS 4px NOW AND IT IS A TOKEN, NOT A LITERAL. The prose that used to say "3px is the
     site's ONE nudge" was carrying the whole of that rule across three blocks and, after this pass, six
     consumers; --nudge is that sentence made enforceable, and the tokens block carries the solve for why
     4 and not 5.25 (a nudge is capped by the hop threshold of the control it moves, where a reveal's
     travel is not). Nothing about the card's own logic changed - one nudge, one duration, one curve.
     [MOTION v20] AND THE LIFT MOVED FROM `transform` TO `translate`, WHICH IS NOT COSMETIC. This card
     carries [data-reveal], and guards 7.4 declares `transition` on every [data-reveal] element in a LATER
     LAYER - a shorthand, so it resets every transition-* longhand and the line above was being discarded
     outright. Measured on dist/ before the repair: a revealed .svc-card computed
     transitionProperty "opacity, transform", so the border SNAPPED to the accent on hover and the rise ran
     at the plate's reveal duration. guards 7.4 now carries border-color and translate at --dur-base for
     exactly this, and it can only do that for a property the reveal does not itself need - `transform` is
     the reveal's own travel and can hold one duration. So the lift takes `translate` and gets its tempo
     back. 7.4 carries the whole finding; this is the half of it that lives in the component.
     [PHOTO v16] THE PICTURE'S HALF OF THIS BLOCK MOVED TO 6.29, AND THE FILE'S ONLY COPY OF 1.028 WENT
     WITH IT. What stood here was `transition: transform --dur-media --ease-out` on .svc-card-media img
     plus `transform: scale(1.028)` on card hover - correct, and the card's private property. The firm
     has since asked for the same push on EVERY framed photograph on the site, so a rule that belonged to
     one component became a site-wide pattern, and a pattern left in the component that happened to
     invent it is how the next frame acquires a near-duplicate of it. 6.29 declares it once for
     .svc-card-media and .feature-media together, at one magnitude and one tempo, and it moved the push
     from `transform` to `scale` on the way (6.29 says why: .feature-media's `transform` is 6.27's and
     cannot be shared). The TEMPO argument that stood here is unchanged in KIND and now lives there -
     --dur-media, which is 2.1x the plate's own --dur-base whatever the ladder's multiplier is, so the
     picture always moves slower than the card it sits in. [MOTION v20] The token itself is 1.275s now
     (1.02s before), which puts the push ABOVE the playbook's "1.1s, and under ~0.7s it reads as a stock
     hover zoom" band rather than just inside it. The band's floor is the one that carries the warning and
     it is cleared by a wide margin; the ceiling is a note about where a zoom stops being felt, and a
     photograph is the one object on the site the firm asked to be slower and larger. 6.29 carries the
     same re-check on its own side. Nothing
     about the CARD's own response changed, which is why this note is the only trace left here.
     THIS IMG MAY NEVER TAKE [data-parallax] (6.27), AND THE REASON NARROWED WITH THE MOVE. It is no
     longer "because it already scales on hover" - .feature-media img now does both, on the firm's
     instruction. It is that this img sits inside a CONTROL that already answers the pointer four ways
     (the accent border, the --nudge rise, the specular lamp, the arrow's drawn rule) before its picture
     answers at all; an ambient scroll drift on top of that would make it the busiest object on the site.
     6.27 carries the same rule restated on its own side. */
  /* --brand-rgb at an alpha: at this weight the declaration is not painting the accent, it is TINTING a
     hairline toward it. THE ALPHA IS SOLVED AGAINST A STATED WEIGHT, not chosen - the target is
     --line-strong's own 1.62:1 on this ground, warmed, so the hover edge and the :focus-within edge two
     rules down are the same weight in two temperatures. That is what makes them read as parity rather
     than as two different ideas.
     [RED v8] THE ALPHA MOVED 0.35 -> 0.57 BECAUSE THE TOKEN GOT DARKER, and the target did not move. The
     old 0.35 composited the light v7 stop to #682020 = 1.62:1, dead on; the same 0.35 over the collapsed
     #BF0000 gives #510B0B = 1.25:1, which is the SEAM rung's weight - a hover response quietly demoted
     to a hairline nobody would notice moving. 0.57 composites to #760707 = 1.61:1 and puts it back.
     THIS IS AN ALPHA, NOT A COLOUR: nothing here contradicts the firm's instruction, because the colour
     being tinted toward is exactly the red they named. A deeper pigment simply needs more of itself to
     reach a given weight over a near-black plate.
     (An AREA that wanted the fill stop at an alpha would need a --brand-fill-rgb; there is none,
     deliberately, until something needs one - and since the two stops now hold one value, one triplet is
     genuinely serving both jobs rather than merely appearing to.) */
  :where(html:not(.has-touch)) .svc-card:hover { border-color: rgba(var(--brand-rgb), 0.57); translate: 0 calc(-1 * var(--nudge)); }
  /* [VIDEO v14 / PHOTO v16] THE 1.028 USED TO BE DECLARED ON THIS LINE AND SUBTRACTED IN 6.9a. Both
     halves are 6.29's now, and the census that justified the direction is kept here because it is still
     the evidence: dist/index.html carries six .svc-card and every one of them has a video;
     dist/real-estate.html carries four, three with a .svc-card-media frame and none with a video;
     /business mentions the class in a comment only.
     THE DIRECTION SURVIVED THE MOVE, WHICH IS THE HALF WORTH KEEPING. The argument was that the push
     must be declared unconditionally and subtracted where a video earns the exemption, because written
     the other way round - scoped to frames WITHOUT video - /real-estate's three framed cards would
     depend on a negation to keep a response they have had since the site was built, and any failure of
     that selector would strip the push from every card on the site instead of from none. 6.29 keeps
     exactly that failure direction while making the exemption STRUCTURAL rather than enumerated: the
     magnitude is a value, the marker sets it to 1, and a dropped marker rule leaves every card scaling -
     the site as it stood before the video shipped, not a site-wide loss. */
  :where(html:not(.has-touch)) .svc-card:hover::before { opacity: 1; }
  /* [LINK v15] RETIRED, AND THE REPAIR ABOVE IS WHY. This line existed because "hovering anywhere on
     the card must finish the arrow's rule, which the arrow's own :hover cannot see" - and the arrow's
     own :hover could not see it only because the stretch overlay was broken. With the overlay working,
     the pointer resting anywhere on the card IS on the anchor's ::after, so .svc-card-link:hover fires
     on its own and completes the rule, the colour and the glyph together. The file's only cross-block
     duplication of a hover VALUE is gone with it; do not re-add it. */
  /* Keyboard parity OUTSIDE the gate and quieter - a keyboard is not a pointer, and .svc-card is an
     <article> whose only focusable child is its link, so :focus-within is the correct hook. */
  .svc-card:focus-within { border-color: var(--line-strong); }
  .svc-card:focus-within::before { opacity: 1; }

  /* ---- 6.9a THE PRACTICE-AREA HOVER VIDEO (.svc-card-video) --------------------------------------
     [VIDEO v14] 2026-09-06. The firm instructed, in full: "On the Main Page under the Areas of Practice
     section, replace all six static images for each card with the short videos for each that activate
     when mouse hovers." IT SHIPS AS AN OVERLAY, NOT A REPLACEMENT - index.html states that decision and
     its reasons at length (the <img> keeps a three-rung srcset and a sizes attribute; <video> has one
     `poster` url and no srcset, so replacing the still would hand a 390px phone an 800px image for an
     affordance a phone can never use). This block is the visual half of it and owns exactly one property
     on the new element: OPACITY. main.js owns play() and pause() and writes no style at all. Neither
     layer can do the other's job, so each can be held responsible for its own: a CSS bug leaves a
     playing video invisible, a JS bug leaves a visible layer that never moves, and neither can produce
     the failure where the two disagree about whether the card is hovered.

     THE FRAME IS STILL SIZED BY THE <img>, AND THAT IS THE WHOLE OVERLAY IN ONE SENTENCE. .svc-card-media
     is position: relative with no height of its own; the img above sets width: 100% and aspect-ratio: 3/2
     and is the only element in the well that is IN FLOW. So the well is whatever box the still asks for,
     and this element is absolutely positioned into it. Delete the img and the card collapses - which is
     the correct coupling, because the still is the resting state and the responsive one.
     width AND height ARE LOAD-BEARING, NOT DECORATION ON inset: 0. An absolutely positioned REPLACED
     element with left/right pinned and width: auto does not stretch: it takes its intrinsic width and the
     over-constrained edge is dropped, so inset: 0 alone would lay an 800px video across a 374px card.
     The pair is what resolves it against the well on both axes; the markup's width="800" height="450"
     attributes then never get a vote, exactly as the hero's probe found for .hero-video in 6.3.
     object-fit: cover BECAUSE THE STILL HAS IT. Two layers dissolving into each other must fill one
     frame the same way or the swap reads as a jump-cut. The cost is measured and it is not symmetric:
     the still is 800x533, already 3/2, so cover crops it by a quarter of a pixel; the clip is 800x450,
     16/9, so cover fits it by HEIGHT and shows (3/2)/(16/9) = 27/32 = 84.375% of its width - 675 of 800
     columns, 62.5 clipped from each side, 7.8125% per side at every track width, since the fraction
     depends only on the two ratios and not on the card. index.html carries the device-pixel consequence
     of that crop and the encode width that would close it.
     THE TWO LAYERS ARE DIFFERENT PICTURES, NOT TWO FRAMES OF ONE SHOT, and the duration below is chosen
     for that. The handover offered card-<slug>-800.jpg - each clip's own first frame - as the still, and
     the markup declined it and kept the photographs, because a poster-shaped still has no srcset. So this
     is a DISSOLVE between two images of the same subject, which is the one transition that has to be slow
     or it reads as a flicker.

     z-index: 1 STATES A RELATIONSHIP RATHER THAN INHERITING ONE, which is the rule [SCRIM v12] settled
     for the hero fill three blocks up. A positioned element would paint over the in-flow img at z-index
     auto anyway, so the declaration buys no paint order it did not already have; what it buys is that the
     order is greppable and survives someone adding a z-index or a wrapper. The two numbers it sits
     between are both in this block: .svc-card::before, the specular lamp, is 2 - the lamp must glint over
     the footage exactly as it glints over the photograph - and .svc-card-link::after is 3, so the card
     stays one click target. THAT LAST CLAUSE WAS ASPIRATIONAL WHEN IT WAS WRITTEN AND IS TRUE SINCE v15:
     the overlay it names was being overwritten by 6.19's drawn rule and was a 1px line at the top of the
     anchor, so the video shipped into a card whose "sole hit target" did not exist. 6.9's [LINK v15]
     note carries the measurement and the repair; nothing in this block had to change for it.
     All three are descendants of .svc-card, so they share a stacking context
     whatever .svc-card's own is (it acquires one from will-change while revealing, and again from the
     hover translate), and 1 < 2 < 3 holds in every one of those states.
     pointer-events: none FOR THE SAME REASON THE ELEMENT IS aria-hidden: it is not part of the interface.
     It keeps .svc-card-link::after the sole hit target, keeps the UA's own video affordances and context
     menu off a decorative layer, and - the part that matters to the other half of the feature - it means
     the pointer events main.js delegates from the grid always originate on the frame or the still, never
     on a layer that appears and disappears under the cursor.

     THE DURATION IS --dur-media AND THE CURVE IS --ease-inout, and both are arguments, not defaults.
     DURATION: this is imagery arriving by opacity alone, which is precisely what the [data-reveal="fade"]
     opt-out means by --dur-media (guards 7.4), and it is BYTE-IDENTICAL to the --dur-media the scale it
     replaces was running at. So the media layer of this card keeps the tempo it has always had, including
     the invariant the scale was chosen for: the picture moves at 2.1x the plate's --dur-base, slower than
     the card it sits in. --dur-fast is the token comment's answer for "opacity", and it is the wrong one
     here - it describes chrome (a menu, a button, the specular lamp), and this file already distinguishes
     imagery's opacity from chrome's by giving imagery its own number.
     CURVE: [MOTION v21] --ease-out, AND THE PARAGRAPH THAT ARGUED FOR --ease-inout IS REPLACED RATHER
     THAN AMENDED BECAUSE BOTH OF ITS REASONS FAIL ON THIS FILE'S OWN EVIDENCE. It read: "--ease-out is
     named in the register table as the TRAVEL curve and nothing here travels. --ease-inout's own token
     comment reads 'reversible state, symmetric: menus, toggles, anything that returns', and a hover fade
     is exactly that... The symmetric curve also pays for the one thing preload='none' cannot avoid:
     nothing is fetched until play() is called, so at the instant of pointerenter this element has no
     frame to show. Solving cubic-bezier(0.65, 0, 0.35, 1) at a quarter of the run - 0.32s of the 1.275s -
     gives 0.072 of opacity, so the layer is still effectively absent while the first frame lands."
     REASON ONE PROVES TOO MUCH. Every hover on this site is reversible - the card's lift returns, the
     border cools, the photograph's push unwinds, the arrow slides back - and every one of them is on
     --ease-out. If "runs forward on enter and backward on leave" selected --ease-inout, it would select
     it for all of them. What --ease-inout actually distinguishes, once its consumers are counted rather
     than remembered (the tokens block now counts them), is CHROME THAT TOGGLES BETWEEN TWO PERSISTENT
     STATES: a menu that stays open until it is closed again. A hover is not a toggle; it is a response
     that lasts exactly as long as the pointer does.
     REASON TWO IS ANSWERED FOUR PARAGRAPHS BELOW, IN THIS SAME BLOCK. "a mouse user's hover raises a
     layer with no decoded frame - TRANSPARENT, so the still simply stays." An undecoded <video> paints
     nothing, so its opacity is irrelevant while the first frame is in flight: at 0.072 and at 0.72 alike
     the photograph underneath is what is on screen. The slow start was buying invisibility for something
     already invisible.
     AND THE COST WAS REAL AND MEASURED. --ease-inout reaches 90% of its travel at 0.714 of the run
     against expo-out's 0.329, so at --dur-media this fade needed 0.910s - more than twice the pointer
     ceiling the tokens block now states, and the slowest pointer response on the site by a factor of two.
     On --ease-out the same 1.275s arrives at 0.4195s, which is the figure 6.29's push underneath it
     arrives at, so the picture and the footage become one gesture instead of two. Re-measured with a real
     pointerover after the change: opacity 0.354 at 0ms, 0.721 at 140ms, 90% at 372ms.

     FOUR GATES, AND ONLY ONE OF THEM IS WRITTEN HERE.
       POINTER - :where(html:not(.has-touch)), the file's gate, unchanged and free.
       html.js  - ADDED TO THE SAME COMPOUND rather than as a second ancestor, because both classes are on
                  <html> and because :where() zeroes the whole thing, which is what keeps the subtraction
                  below at the specificity index.html's note claims for it. Its reason is 6.27's gate (b)
                  inverted: with no script there is no playback, so there must be no reveal - and, more
                  importantly, no subtraction of the push. The watchdog strips .js at 2.5s; a card on a
                  page that has given up on motion must go back to being the card it was, not become one
                  with no picture response at all. main.js block 14 described this gate as the bare
                  :where(html:not(.has-touch)) until v15 and now quotes it in full; it is the pointer gate
                  PLUS .js, which is a strengthening and not a disagreement - html.js is one of the five
                  conditions block 14 re-asks at sync time, and the two layers would otherwise differ
                  about a page whose script never arrived.
       REDUCED MOTION - guards, block 5, where the pointer light and the parallax scrub are also removed
                  rather than dimmed. The clamp there cannot do this job: it collapses transition-duration,
                  which would make the reveal INSTANT rather than absent, and a video is not a transition.
       NARROW   - JS ONLY, DELIBERATELY, AND THIS IS THE PARAGRAPH THAT SAYS WHY. main.js gates playback at
                  BP_STRUCTURAL, and this block adds no media query, because 67rem is written in exactly
                  three places (--bp-structural, 6.27's query, main.js's BP_STRUCTURAL) and a fourth
                  spelling is how the 64/67rem drift [ZOOM v11] repaired happened in the first place. What
                  that costs is stated rather than hidden: below the breakpoint a mouse user's hover raises
                  a layer with no decoded frame - transparent, so the still simply stays - while the push
                  is already gone, so those six cards answer with the border, the lift, the lamp and the
                  arrow and a still picture. That is the honest trade against re-spelling a breakpoint,
                  and it is also more consistent than the alternative: a card whose picture pushes at
                  1071px and holds still at 1072px would be two different cards on one resize.

     NO :focus-within RULE, AND IT IS A DECISION. 6.9 declares keyboard parity deliberately quieter -
     --line-strong and the lamp, not the accent border, not the --nudge rise, and never the push, which has
     been hover-only since the block was written. Giving focus the video would make the keyboard state the
     LOUDEST thing this card can do, inverting the convention instead of matching it. It would also break
     the state split in the one direction that shows: CSS can raise this element on focus, but playback is
     main.js's and its handlers are pointer handlers, so what would fade up is an empty box over the
     photograph. Quieter is the convention; nothing at all is the correct quiet here. */
  .svc-card-video {
    position: absolute; inset: 0; z-index: 1;   /* between the still and the specular lamp - see above */
    width: 100%; height: 100%;                  /* NOT redundant with inset: a replaced element does not stretch */
    object-fit: cover;                          /* the still's own fit; 27/32 of the clip's width survives it */
    pointer-events: none;                       /* decorative: the link's ::after stays the hit target */
    opacity: 0;                                 /* the <img> underneath IS the resting image; there is no poster */
  }
  /* The transition is declared INSIDE the gate, exactly as .svc-card-media img's is one screen up: a
     device that has proved itself touch never reaches either state, so it need not carry the machinery.
     [MOTION v21] --ease-out, NOT --ease-inout, AND THIS IS THE ONE POINTER RESPONSE THE x 1.5 LADDER
     GENUINELY BROKE. A render pass timed every hover on the site from the real pointerover event: twelve
     exceed 0.4s by total settle, THREE by perceived arrival, and only this one badly. --ease-inout has no
     front-load - it reaches 90% of its travel at 0.714 of the run against expo-out's 0.329 - so at
     --dur-media this fade was 0.910s before the picture was there. Measured: opacity 0.157 at 431ms,
     0.866 at 864ms, while the card's border, lift, lamp and arrow had all finished by ~400ms. The clip
     was still visibly dissolving a second after the pointer landed, which is lag rather than weight, and
     it is the only place on the site where that was true.
     THE FIX IS THE CURVE, NOT THE DURATION, AND THAT IS DELIBERATE. Shortening the rung would have
     desynchronised this fade from the 1.028 push it plays over - two motions on one object arriving at
     two different times, which is the fault 6.27 and 6.9 both exist to prevent. The curve costs nothing
     structural: --dur-media x 0.329 = 0.4195s, the SAME perceived arrival as the push underneath, so the
     picture and the footage now become one gesture instead of two.
     AND IT WAS THE ODD ONE OUT. Every other hover on this site is on --ease-out; --ease-inout's stated
     job in the tokens block is "reversible state, symmetric: menus, toggles, anything that returns", and
     the two consumers that paragraph enumerates are the burger and the .mobile-menu fade. This rule was a
     third consumer the paragraph did not know about - a HOVER, faster-triggered than either, and the only
     one over the pointer ceiling. With it moved, that enumeration is true again. */
  :where(html.js:not(.has-touch)) .svc-card-video { transition: opacity var(--dur-media) var(--ease-out); }
  :where(html.js:not(.has-touch)) .svc-card:hover .svc-card-video { opacity: 1; }
  /* THE SUBTRACTION OF THE 1.028 LIVES IN 6.29 NOW, AS `--photo-push: 1` ON THIS FRAME'S OWN MARKER.
     [PHOTO v16] The rule that stood here was
       :where(html.js:not(.has-touch)) .svc-card:hover .svc-card-media--video img { transform: none; }
     and its reasoning is kept in full below, because every word of it still decides something - the
     replacement is a FOURTH spelling of the same decision, not a different decision.
     .svc-card-media--video IS STILL THE MARKER, AND IT IS STILL A MODIFIER ON THE FRAME rather than a
     test on the video, because CSS cannot look upward from the video to the img's own ancestor.
     index.html adds the modifier and the <video> in one edit and would remove them in one; neither is
     ever alone. main.js does NOT use the modifier - it selects by structure (".svc-card-media video") so
     a class rename cannot silently stop playback - so this class is a CSS-only hook, and renaming it
     breaks the CSS silently. That has not changed either.
     THREE SPELLINGS WERE ON THE TABLE WHEN THE VIDEO SHIPPED, all three worked against the markup, and
     the choice was made on FAILURE DIRECTION and BLAST RADIUS. Recorded in full, because two of them are
     written down in the other two files and a reader will find them:
       (i)   a class, and a separate rule subtracting after the 1.028. THIS IS WHAT SHIPPED.
       (ii)  the same shape with :has(.svc-card-video) IN PLACE OF the class. Rejected AS A REPLACEMENT,
             and the rejection still holds: a browser that drops :has() drops the OVERRIDE, and what it
             leaves is a card that scales AND plays - the busy failure, i.e. exactly the fault the rule
             exists to remove. Firefox is the live case, not a hypothetical: `scale` shipped there in 72
             and :has() not until 121, and ESR 115 was current into late 2024.
             [PHOTO v16] IT NOW SHIPS AS AN ADDITION, WHICH IS A DIFFERENT PROPOSITION AND HAS NONE OF
             THE ABOVE AS ITS COST. 6.29 carries `:where(html.js) .svc-card-media:has(video)` as a SECOND
             rule beside the marker rule, never as a selector in the same comma list - a list is invalid
             whole if any selector in it is, so a shared list would have taken the marker rule down with
             it in exactly the browsers this note is about. Two rules setting one value: the marker keeps
             working everywhere, and where :has() parses, the push is keyed on the STRUCTURE. What that
             buys is the property the firm's instruction actually asks for - a frame that GAINS a video
             loses the push in the same edit, with no marker to remember - and what it costs, in a
             browser without :has(), is precisely today's behaviour and not a step worse.
       (iii) a negative FILTER on the 1.028 itself - `.svc-card:not(:has(.svc-card-video)):hover ... img
             { transform: scale(1.028) }` - one rule with one owner, and its failure direction IS the
             better one: dropped whole, nothing scales anywhere, which is quiet. main.js block 14's
             comment proposes it and the argument is sound as far as it goes. It loses on BLAST RADIUS,
             which is the half that argument does not price. The 1.028 is an OLD, unconditional,
             site-wide rule that predates this instruction; (iii) makes it newly conditional on a 2022-23
             selector feature for every .svc-card on the site, so the cards that failed would be the ones
             on /real-estate, which have no video, are not what the firm asked about, and would lose a
             response they have had since the site was built.
       (iv)  [PHOTO v16] THE MARKER SETS THE PUSH'S MAGNITUDE TO 1 AND THE PATTERN MULTIPLIES BY WHAT IT
             FINDS. This is what ships today, and it dominates all three: it keeps (i)'s failure
             direction EXACTLY (a <video> added without the marker scales and plays at once - busy, loud,
             and confined to a card that actually has a clip) while removing (i)'s one weakness, which
             was that the exemption was a SELECTOR naming one frame class and therefore an enumeration
             that a new frame would have to be added to by hand. As a VALUE it needs no negation, no
             :has() and no new condition on the site-wide rule, so (iii)'s blast radius never arises:
             drop this one line and every card scales, which is the site as it stood before the video
             shipped. The firm's 2026-09-06 instruction asked for the rule to be exactly this shape -
             "if there is an overlayed video that plays from a fixed image with mouse hover, there should
             be no zooming in" is a test on the FRAME, not a list of cards - and (iv) is that sentence
             transcribed. 6.29 carries the rule and its reasoning; this note is the history it replaces.
     THE RESIDUAL RISK IS UNCHANGED and is an authoring one, not an engine one: a <video> added without
     the modifier scales and plays at once. It is answered where it arises, in the markup, and it is the
     risk this file's own habit accepts everywhere else - 6.27 enumerates its parallax frames in two
     places rather than inferring them, for the same reason.
     WHAT THE SUBTRACTION IS FOR, in 6.9's own words: "a second motion on the same object, with a
     different cause, is the definition of busy". A playing video IS the hover response on these six
     cards, so the 2.8% push became the second cause. The firm's instruction and this file's own
     principle agree here, which is precisely why the rule was safe to express structurally rather than
     as a list. */

  /* ---- 6.10 INFORMATION CARDS (.act-grid, .act-card) - MATERIAL ---------------------------------
     An <article> with no link. The service card's LIFT would teach a click that does not exist, and it
     is still refused here for that reason.
     [CARD-EDGE v36, 2026-09-08] THE SENTENCE THAT STOOD HERE ALSO REFUSED THE BRAND BORDER AND NO LONGER
     CAN. It read: "The service card's lift and brand border would teach a click that does not exist and
     spend the affordance colour on a non-affordance. Only the watermark responds". The firm instructed
     otherwise - "Also the 6 cards need to have a subtle mouse interaction...maybe the red box around the
     card only." - so the plate's edge answers the pointer as of today and the watermark is no longer the
     only response. The declaration and the whole of the argument are in 6.10a; 6.9's register carries the
     override, the fact that it is the SECOND time this clause has been overridden, and the scope
     (seventeen plates on three pages). WHAT IS UNCHANGED IS THE HALF THE SENTENCE GOT RIGHT: no lift, no
     pointer cursor, no click target, and every word below about the watermark still holds exactly as
     written. The watermark responds - DEPTH, not invitation: it moves FARTHER (2 x --nudge = 8px, twice the card lift) and SLOWER
     (--dur-media, 2.1x --dur-base) than any affordance response on the site, which is what makes it read
     as distant. The RELATION is the value, not the literal: [MOTION v20] raised the nudge 3 -> 4px, so
     this had to go 6 -> 8px in the same commit or the numeral would have stopped being twice the card
     lift and started being 1.5 times it - a number nobody could name. It is written as the arithmetic so
     the next move of --nudge carries it automatically. The
     brightening is a COLOUR alpha, never opacity: opacity would MULTIPLY the resting 0.05 and fade the
     numeral toward invisible - the playbook's own recorded bug.
     [MOTION v20] AND THE NUMERAL NOW ARRIVES AS WELL AS ANSWERING. It had a hover and no entrance at all:
     on /attorneys' three discipline cards the plate travelled in and its watermark was already painted,
     full strength, from the first frame - the largest object on the card and the only one not arriving.
     THE ARRIVAL DOES NOT BREACH THE MATERIAL REGISTER, and the distinction is worth stating because the
     register is what this whole block is about. MATERIAL means the surface does not answer a POINTER; an
     arrival is not a pointer response, it is the object coming into being, and every MATERIAL plate on
     this site already arrives (they are the PLATE row of guards 7.4's register table). What is added here
     is choreography INSIDE an object that was already arriving, not a new response to a visitor.
     IT USES THE SAME TWO PROPERTIES AS THE HOVER, which is what keeps this one gesture rather than two:
     the colour alpha runs 0 -> 0.05 and the translate runs +8px -> 0, i.e. the numeral falls INTO place
     from the direction the hover lifts it out of. No opacity, for the reason two paragraphs up.
     NO DELAY OF ITS OWN, AND THE DURATION IS THE LAG. --dur-media 1.275s against the plate's own
     --reveal-plate 1.2s means the numeral is still settling after the card has stopped, so it trails
     without a literal to maintain. What the transition DOES carry is var(--reveal-delay, 0s) - mandatory,
     not decorative: without it a pseudo-element inside a staggered group starts moving at t=0 while its
     card is still invisible, which is the exact defect guards 7.4 records for the hairlines. After
     arrival main.js's finish() removes the property and guards 7.4's `--reveal-delay: 0s` default returns,
     so the hover below is undelayed - checked, because a hover that waited on a stagger slot would be the
     worst possible way to discover this. */
  /* [CARD-EDGE v36] THE TRANSITION IS NEW, AND THE SENTENCE THAT JUSTIFIED IT WAS FALSE OF EVERY CARD
     THAT SHIPS. It read "it had no state of its own to serve - so the hover edge added in 6.10a would
     SNAP without this", and the close-out of the same day MEASURED IT in Chrome on dist/ rather than
     deducing it. All seventeen .act-cards carry [data-reveal]; guards 7.4 declares the `transition`
     SHORTHAND on html.js [data-reveal] in a LATER LAYER, and a shorthand resets every transition-*
     longhand - so on a shipped card this declaration is discarded outright, and the edge fades anyway
     because guards 7.4's own list already carries `border-color var(--dur-base)`, the identical value.
     Measured both ways, which is what makes it a finding and not a guess: a shipped card reports
     transition-property "opacity, transform, border-color, translate" at 1.2s / 1.2s / 0.6s / 0.6s; a
     synthetic .act-card with no [data-reveal] reports "border-color" at 0.6s, which is this line and
     nothing else. IT IS KEPT, ON THE ONLY GROUND THAT SURVIVES THE MEASUREMENT: it is the component's
     FLOOR - the correct behaviour for an .act-card authored without [data-reveal], which the markup
     contract permits and where the edge really would snap - and it is the declaration guards 7.4's
     border-color rung is pinned TO rather than a duplicate of it. That is the same relation .svc-card,
     .package and .contact-card already have with that block, and guards 7.4's enumeration of the
     collision is corrected in this same pass to name FOUR components rather than three.
     It is declared HERE, on the component, and OUTSIDE the pointer gate, by 6.9's own
     stated rule: "a transition may live inside the pointer gate if and only if hover is the only state
     that uses it", and hover is not the only state - the :focus-within in 6.10a uses it too, and a focus
     response gated on the pointer kind would snap on exactly the devices most likely to have a keyboard.
     var(--dur-base) is .svc-card's own tempo on the same property, so the two card families answer at one
     speed rather than at two nobody chose. border-color ONLY: naming the property keeps the plate's
     background, padding and radius out of it, and there is nothing else here that changes.
     THE DECLARATION IS BYTE-IDENTICAL TO ONE THE FILE ALREADY WRITES TWICE, which is the strongest thing
     that can be said for it. .package (6.13) and .contact-card (6.16) - the other two MATERIAL plates -
     have each carried `transition: border-color var(--dur-base) ease` beside a `:focus-within` edge since
     they were written. .act-card was the ONLY plate on the site with neither, and the focus half of that
     gap (6.10a) is a hole this edit closes rather than a new idea it introduces. */
  .act-card { position: relative; overflow: hidden; padding: var(--space-3); background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); transition: border-color var(--dur-base) ease; }
  /* The face, the weight and font-synthesis: none come from base's display grant, which now enumerates
     this pseudo-element by name - it is a serif hook like any other and belongs in the one list that
     answers "where does the serif appear". Nothing about the face is re-declared here. */
  /* [v7 REPAIR PASS] THE ALT TEXT IS THE EMPTY STRING, and it is what makes "texture, not type" true
     rather than merely intended. Generated content is exposed to the accessibility tree in Chrome and in
     Firefox, so `content: attr(data-numeral)` on its own put a spoken numeral in front of every card
     heading - and a numeral a screen reader announces is TYPE, not texture:
     the resting alpha measures 1.11:1 on --bg-2.
     The `/ ""` alt-text form takes it out of the tree, which settles that and brings the declaration
     squarely under 1.4.3's decoration exemption instead of leaning on it. An engine too old for the
     syntax drops the whole declaration and paints no numeral at all, which is the correct failure mode
     for a decoration.
     [RIBBON v17] THE RULE IS IN USE NOW AND THIS LINE USED TO SAY THE OPPOSITE. It read "No page ships
     [data-numeral] today; this is the rule being made safe to use", which was true when it was written
     and stopped being true on 2026-09-06: /attorneys' #disciplines ribbon ships three, data-numeral="I",
     "II" and "III", and the page's own comment quoted this sentence as its evidence that the component
     was untouched - so one stale clause was propagating into a second file. Two consequences worth
     stating rather than leaving to be found: the values are ROMAN, so the
     font-variant-numeric: lining-nums tabular-nums below is inert against them (it belongs to the rule,
     not to that markup, and is left alone); and because the numerals are aria-silent by the `/ ""` form,
     NO FACT MAY LIVE ONLY IN ONE - a screen reader gets the sequence as document order and nothing
     else. */
  .act-card[data-numeral]::before {
    content: attr(data-numeral) / ""; position: absolute; top: var(--space-1); right: var(--space-2);
    font-size: var(--text-4);   /* the top rung; nothing here is set larger, and at 5% alpha it is
                                   texture, not type, so it out-weighs nothing */
    line-height: 1; font-variant-numeric: lining-nums tabular-nums; color: rgba(var(--ink-rgb), 0.05);
    pointer-events: none;
    transition: color var(--dur-media) var(--ease-out) var(--reveal-delay, 0s), translate var(--dur-media) var(--ease-out) var(--reveal-delay, 0s);
  }
  html.js .act-card[data-numeral][data-reveal]:not(.is-revealed)::before { color: rgba(var(--ink-rgb), 0); translate: 0 calc(2 * var(--nudge)); }
  .act-card h3 { font-size: var(--text-2); line-height: var(--leading-heading); }
  .act-card p { color: var(--ink-2); }

  /* ---- 6.10a THE PLATE'S CONTENT MODEL [BUSINESS v24] -------------------------------------------
     WHAT ARRIVED, AND WHY THE COMPONENT DID NOT HAVE TO. The firm rewrote /business on 2026-09-06 -
     "less lengthy and more to the point for the prospective client reading it" - and instructed:
     "After the Overview section on its own ribbon, make all other sections from Formation through
     Licensing, their own cards in the same section." Six full-width .section bands collapse into one
     .act-grid of six plates. Nothing about the COMPONENT changes: it is still MATERIAL by 6.9's
     AFFORDANCE-vs-MATERIAL split, still no lift and no pointer cursor. What changes is the TYPE INSIDE
     the plate, and that is the whole of this block.
     [CARD-EDGE v36] TWO WORDS OF THAT SENTENCE WENT STALE ON 2026-09-08 AND ARE STRUCK RATHER THAN LEFT
     TO PROPAGATE. It also said "no brand edge" and "6.10's watermark is still the only pointer response
     it has"; the firm asked for the edge on these very six cards and both clauses are now false. The rule
     is at the foot of this section and 6.9's register carries the override. The rest of the sentence -
     MATERIAL, no lift, no pointer cursor - is untouched and is the reason the edge is the whole of it.
     THE PLATE NOW CARRIES FIVE PARTS WHERE 6.10 SHIPPED TWO: a LABEL ("Formation"), the h3 ("Formation
     & Operation"), a LEAD LINE ("Start it right. Keep it running."), body copy, and on two of the six
     a .link-arrow. Every one of them is served WITHOUT a per-card override, which was the requirement:
     six cards differing only in copy must not differ in CSS.
     AND IT COSTS TWO RULES, WHICH IS THE POINT OF THE FOUR PARAGRAPHS BELOW. .act-card-label and its
     spacing rung are the whole addition; the rhythm rule REPLACES two existing declarations with one
     general one, and the lead line and the link are served by classes the site already has. Three of
     the five parts needed nothing. Each paragraph says which alternative was tried and what measured
     it out, because "nothing was needed" is a claim that is worthless without the working.

     THE BOX THE ARITHMETIC IS RUN IN. .act-grid is 6.9's shared recipe, so the track is
     `minmax(min(100%, --grid-track), 1fr)` at a --space-3 gap and the plate's padding is --space-3 on
     all four sides. Three widths matter and all three are content width, i.e. track minus 2 x 27.51:
       3-up at the 72rem container   (1224 - 2 x 27.51) / 3 = 389.66 track ->  334.64px of content
       the grid's own minimum track  --grid-track 22rem = 374            ->  318.98px  (a floor, not a
                                                                             width any breakpoint hits)
       1-up at a 375px phone         375 - 2 x 21.62 = 331.76 container  ->  276.74px  <- the worst case
     Six cards therefore fill THREE COLUMNS AND TWO ROWS at desktop (4-up needs 4 x 374 + 3 x 27.51 =
     1578.53 and the container caps at 1224), 2-up from 775.51px of container, and 1-up below it. No
     grid CSS is added here and none is needed; that is why the component was the right answer.
     THE TWO END CASES ARE CONFIRMED IN A BROWSER, not just summed: tools/probe-actcard.mjs reads the
     BUILT /business back and gets 389.656 / 389.672 / 389.656px of track (334.66px of content) at
     1440x900 and 276.74px of content 1-up at 375x667. The sums above, to the hundredth; the 0.02px is
     `1fr` distributing a remainder across three columns and is not a term anyone has to carry.

     (1) THE RHYTHM RULE IS GENERAL NOW, AND THIS IS THE THIRD TIME OF ASKING. It began as a margin on
     <p> alone. [CREDENTIALS v18] found that a .check-list directly under an h3 sat FLUSH against it,
     because .check-list sets padding: 0 with no margin of its own (6.7 - its `gap` spaces the ITEMS,
     and a grid's gap never reaches outside the box), and added a second declaration naming
     .check-list. That paragraph is kept below because the defect it records is real and the reason it
     gives - "the two card kinds then open on one rhythm, which is the whole reason #credentials was
     built out of .act-card" - is the reason this rule exists. But enumerating the child types is what
     failed twice: the label and the lead line and the .link-arrow are a THIRD, FOURTH and FIFTH, and
     naming them one by one would be the same edit a third time. 6.4 already owns the idiom -
     `.section > .container > * + * { margin-top: var(--space-4); }`, "one rhythm rule for every
     section" - so the plate takes the same shape one rung down the ladder and stops enumerating.
     IT IS BYTE-IDENTICAL ON EVERYTHING SHIPPING TODAY, checked rather than assumed: /attorneys is the
     only page with .act-card (5 of them), #experience and #accreditation are h3 + ul.check-list and
     the three disciplines are h3 + p (+ p) - every one a DIRECT child, every gap --space-2 before and
     --space-2 after. The `>` combinator is the one narrowing: a .check-list wrapped in a <div> would
     lose its margin where the old descendant rule would have kept it. Nothing wraps one today and
     nothing should; the flat plate is the contract (docs/MARKUP-CONTRACT.md).
     WHY --space-2 IS THE ONLY INTERNAL RUNG, which is a decision and not a default. The head (label,
     h3, lead) wants to read as one group and the body as the next, and the ladder's next step up is
     --space-3 27.51px - EXACTLY the plate's own padding. An internal gap equal to the padding stops
     being a gap and starts being a seam: the card reads as two plates stacked inside one border. So
     the group break is carried by INK, not by air - --ink through the head, --ink-2 for the body, the
     same two-stop separation 6.5 uses for .display over .lede - and the largest gap inside the plate
     stays one phi rung below the padding that contains it (27.51 / 17 = 1.618). This is the file's own
     rule applied, not a new one: "Airiness comes from colour (--ink-2, --ink-3) here, not from weight."

     (2) THE LABEL IS ITS OWN CLASS AND NOT .eyebrow, AND THE CHECK THAT SETTLED IT IS WORTH KEEPING.
     The string does exactly what an eyebrow does - a tracked-caps kicker naming a category above a
     heading - so reusing 6.4's class was the obvious move and was tried first. It fails on what
     .eyebrow BUNDLES: the type register, a drawn hairline, and a reveal contract. Only the first of
     those belongs in a plate.
     THE HAIRLINE IS A BAND-OPENING DEVICE AND A PLATE IS NOT A BAND. That sentence is not invented
     here - attorneys.html records it, on 2026-09-06, as the reason the eyebrows "Career" and
     "Licenses" were RETIRED when those two sections became .act-cards: "an eyebrow is a band-OPENING
     device - main.css 6.4 draws it a 2.5rem hairline to announce a new band across 116px of padding -
     and a card announces itself with its own plate edge". The difference here is only that the firm
     SUPPLIED these six labels, so the string stays where /attorneys' were dropped.
     IT IS NOT AN OVERFLOW REPAIR, and saying so is the honest form of the check. The rule is 2.5rem =
     42.5px plus the flex `gap` --space-1 10.51 = 53.01px consumed before the first character. The
     longest label the firm supplied is "CONFIDENTIALITY", 15 characters, and it is MEASURED rather
     than estimated - Range.getBoundingClientRect over the label's own contents, headless Chrome at
     --text-label 13.362px / --wt-label 600 in Montserrat, tools/probe-actcard.mjs: 166.20px at the
     --track-4 it ships, and 182.24px at .eyebrow's --track-2 (each rung is 15 x 0.04em x 13.362 =
     8.02px, and CSS adds the last character's rung too). With the hairline that is 235.25px against
     276.74px of plate in the worst case - 1-up on a 375px phone, ONE line box, 41.49px of slack. It
     FITS everywhere, at every rung, with or without the rule. The rule goes anyway: 53.01px is 15.84%
     of the 334.66px plate at desktop and 19.16% of the 276.74px one on a phone, spent announcing a
     band that is not there.
     AND THE REVEAL CONTRACT IS THE HALF THAT MAKES A SUPPRESSION UNSAFE RATHER THAN MERELY UNTIDY.
     .eyebrow::before is authored in its finished state and knocked back ONLY under
     `html.js .eyebrow[data-reveal]:not(.is-revealed)` (6.4, and that direction is deliberate - see the
     note there). A label inside a plate MUST NOT carry its own [data-reveal]: guards 7.4's PLATE row
     is "ONE object - never sub-staggered; the cascade comes from the grid". So inside a card the
     knock-back can never match, the rule paints at scaleX(1) from the first frame, and its transition
     is dead weight carrying a --reveal-delay nothing publishes. `content: none` would have hidden all
     of that behind one declaration and left the next reader to re-derive it.
     (3) SO IT JOINS A FAMILY OF FOUR RATHER THAN OPENING A FIFTH REGISTER, and the family was counted
     rather than remembered. EVERY tracked-caps label that lives INSIDE a plate on this site is already
     --text-label / --wt-label / --track-4 / uppercase / --ink-2, byte-identical on all five
     declarations: .stat-label (6.13), .package-price small (6.12), .quote-attrib (6.14) and
     .contact-list dt (6.20). Not one of them reuses .eyebrow either - each states the five and stands
     on its own, which is precisely the shape (2) argues for. .act-card-label is the fifth member, not
     a new thing. The token block splits the two rungs itself: --track-4 reads "stat / role / info
     labels, attribution" and --track-2 reads "section eyebrow", so the band/plate distinction is in
     the ladder and is not invented here.
     THAT ALSO SETTLES THE COLOUR, AND IT SETTLES IT THE GOOD WAY. --ink-2 on --bg-2 is comfortably
     inside AA, where .eyebrow's --brand is a RECORDED failure on that exact ground (6.4 measures the
     plate case by name). Six red kickers inside plates would have multiplied an accepted failure for a
     decoration; six quiet ones clear the floor and leave the plate's --ink h3 as the loudest thing in
     it, which is what a card heading should be. The register was chosen for consistency and the
     contrast came free - worth saying, because the reverse (choosing on contrast and calling it
     consistency) is how a system acquires a fifth label style.
     THE SELECTOR IS SCOPED AT (0,2,0) AND THAT IS LOAD-BEARING, NOT TIDINESS - the same sentence
     :not(.btn) gets in 6.5. A bare .act-card-label is (0,1,0) and .act-card p is (0,1,1), so on a <p>
     the body rule would take the colour and the label would ship the body's own grey at the body's own
     size with nobody able to see why from the declaration. Measured, on a draft that left it unscoped,
     it did exactly that. Scoping also makes the part element-agnostic: a <span> or a <div> carrying
     the class paints exactly what a <p> does.

     (4) THE LEAD LINE GETS NO CSS AT ALL, AND THAT CONCLUSION WAS REACHED THE HARD WAY. The firm's
     note is explicit that it is a distinct element - "the card's hook, the body is its detail" - so
     the only question is whether the site already has the part. It does: .lede (6.5) IS the deck, and
     it is the class this page used for these very lines before the rewrite ("Start it off right &
     keep it running smoothly" sat under every one of the six h2s). Moving from a band to a plate
     changes its REGISTER, not its job: --text-3 display over --text-1 lede becomes --text-2 h3 over
     --text-1 lede. Nothing in .lede misbehaves in the smaller box - .section-head .lede and
     .section-head--center .lede need a .section-head ancestor and there is none, and its
     max-width: var(--measure) 584px never binds inside a 334.66px column.
     A NEW CLASS WAS WRITTEN FIRST AND THEN DELETED, which is why this paragraph exists. The argument
     for it was that .lede paints var(--ink-2) and so does .act-card p, so the hook and the detail
     would share an ink and the distinction would rest on size alone - and that the --text-2 over
     --text-1 pair is a 1.272 step where 6.5 pairs .display with .lede at 1.618. Both facts are true.
     THE CONCLUSION DRAWN FROM THEM WAS NOT, and a screenshot settled it rather than a second argument:
     21.62px over 17px in one face is a whole rung of the ladder and reads immediately as a hook over
     its detail (tools/probe-actcard.mjs writes tools/out/actcard-row-1440.png; look before reopening
     this). Sending the lead to --ink would ALSO have made it louder than the label above it and equal
     to the serif h3, which is a hierarchy the plate does not want. The plate's ink hierarchy is
     therefore the one every other card on this site already has - --ink for the heading, --ink-2 for
     everything else - and the lead separates by SIZE because size is the axis it has.
     WHAT WAS REJECTED ALONGSIDE IT, so it is not re-proposed: `.act-card .lede { color: var(--ink) }`.
     That makes one class paint two inks depending on where it is put, which is the defect 6.5 records
     about .link-arrow in prose. The choice was a new part or nothing; nothing won.
     .svc-card-body (6.9) HAS NO LEAD IDIOM to borrow either - what it has is a flex column with a
     `gap`, a different spacing model from this plate's margins, which would have had to replace (1)
     rather than join it.

     (5) WHAT IS DELIBERATELY NOT DONE HERE.
     THE .link-arrow IS NOT PINNED TO THE PLATE'S FOOT. .svc-card-link gets `margin-top: auto` because
     .svc-card-body is a flex column; .act-card is not, and making it one to serve two cards of six
     would change all five plates shipping today. Only cards 1 and 4 carry a link, so pinning would
     leave a lone arrow at the foot of two plates under a gap the other four do not have - the grid
     stretches every card to the tallest, and that gap would be the tallest card's, not the copy's.
     The link follows its copy. Margin reaches it either way, and both ways were measured: the page
     wraps it in a <p>, so the <p> takes (1)'s 17px and the anchor takes 0; as a direct child the
     anchor takes the 17px itself, because .link-arrow is display: inline-flex - an atomic inline-level
     box, which vertical margins do apply to.
     AN INLINE <a> IN CARD BODY COPY NOW HAS A RULE, AND THIS PARAGRAPH IS THE ONE THAT ORDERED IT.
     [BUSINESS v24] wrote the omission deliberately - .prose a and .check-list a both add a
     --line-control underline for one stated reason, "the link is not identified by colour alone"
     (1.4.1), and .act-card p was neither - on the ground that "the rule would be written for markup
     nobody ships", which is the mistake 6.10's own [RIBBON v17] note records going stale. It also
     named its own trigger: "if a card body ever needs an inline link, THIS is the paragraph that says
     what it needs: .prose a's three declarations, scoped to .act-card p."
     THE TRIGGER FIRED ON 2026-09-07. The firm had four /employment-law subtopic ribbons become cards
     and the copy carried across with them, so three of those four card bodies now hold six inline
     anchors - three tel: and three /contact-us. The markup nobody shipped is shipping on a live page,
     and the three declarations are at the FOOT of this section.
     ONE CLAIM IN THE OLD PARAGRAPH WAS WRONG AND IS CORRECTED RATHER THAN QUIETLY DROPPED. It said a
     bare <a> in a card would fall to base's `a { color: var(--brand) }` "with no underline". Measured
     on the built page, the UA's own underline SURVIVES there - nothing in this file removes it, and
     the global `text-decoration: none` belongs to .skip-link - so 1.4.1 was never actually at risk
     inside a plate. What was missing is everything else, and that is what the rule below supplies. */
  .act-card > * + * { margin-top: var(--space-2); }
  .act-card .act-card-label { font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-4); text-transform: uppercase; color: var(--ink-2); }
  /* The label-to-heading rung, and it is 6.4's `.section-head .display { margin-top: var(--space-1) }`
     restated for the plate - the same pair, the same token ("label-to-rule, micro gaps"), one register
     down. (0,2,0) over (1)'s (0,1,0), so it wins on specificity and the two can be written in either
     order. KEYED ON THE LABEL'S PRESENCE, which is what leaves /attorneys' five .act-cards - none of
     which has one - untouched. `+ *` where business.html's own note writes `+ h3`: identical on the
     markup that ships, and general because the rung belongs to whatever the label introduces. The two
     files do not disagree; this is the broader of two selectors with the same effect. */
  .act-card .act-card-label + * { margin-top: var(--space-1); }
  /* [CREDENTIALS v18] A LIST DIRECTLY UNDER A CARD HEADING NEEDS THE MARGIN THE PARAGRAPH ALREADY HAS.
     This block gave .act-card a top margin on <p> alone, which was complete for as long as every card in
     the component held prose - the three discipline cards still do. /attorneys #credentials, added
     2026-09-06, puts a .check-list directly under the h3 instead, and .check-list sets padding: 0 with no
     margin of its own (6.7 - its `gap` spaces the ITEMS, and a grid's gap never reaches outside the box),
     so Experience and Accreditation sat FLUSH against their own headings while the cards twelve lines
     below them opened on --space-2. The previous pass found this, could not fix it because main.css was
     another agent's file in that pass, and wrote the exact missing declaration into the page's own
     comment; this is that declaration.
     THE SAME TOKEN AS THE PARAGRAPH, deliberately: the two card kinds then open on one rhythm, which is
     the whole reason #credentials was built out of .act-card. SCOPED TO .act-card, so the .check-list in
     .prose (6.6/6.7) and the one in #ratings keep the spacing their own containers give them.
     [SUPERSEDED BY 6.10a, 2026-09-06] THE DECLARATION THIS PARAGRAPH ARGUES FOR IS GONE AND ITS EFFECT
     IS NOT: `.act-card .check-list { margin-top: var(--space-2); }` is subsumed by 6.10a's
     `.act-card > * + *`, which gives the list the same token from the same ladder. The paragraph is
     kept in full because the DEFECT it records - a grid's gap never reaching outside its own box - is
     the thing a reader needs when the next child type arrives, and because "the two card kinds open on
     one rhythm" is the requirement 6.10a's general rule inherits. Two declarations for one rhythm was
     the shape an edit misses; one is not. */
  :where(html:not(.has-touch)) .act-card:hover { border-color: rgba(var(--brand-rgb), 0.57); }
  :where(html:not(.has-touch)) .act-card:hover::before { color: rgba(var(--ink-rgb), 0.09); translate: 0 calc(-2 * var(--nudge)); }
  /* [CARD-EDGE v36, 2026-09-08] THE RULE DIRECTLY ABOVE IS THE PLATE'S OWN EDGE ANSWERING THE POINTER,
     on the firm's instruction: "Also the 6 cards need to have a subtle mouse interaction...maybe the red
     box around the card only." It overrides a recorded decision and 6.9's register carries the override
     in full - the old wording quoted, the instruction dated, what still holds, that this is the SECOND
     time the firm has unseated that one clause, and the scope (seventeen plates across /attorneys,
     /business and /employment-law, not the six the firm was looking at). Four things about the
     declaration itself:
     THE VALUE IS .svc-card's AND NOT A NEW ONE, WHICH IS WHY THIS CHANGE WRITES NO NEW ARITHMETIC.
     rgba(var(--brand-rgb), 0.57) is the alpha [RED v8] solved for this exact tint on this exact plate
     ground - 6.9 carries the solving and tools/verify_ratios.py already carries the row that proves it.
     A second alpha invented here for the same gesture on a sibling card would have been two edges that
     look identical today and drift apart at the next move of the token.
     THE LIFT IS DROPPED, AND THAT IS THE WHOLE DIFFERENCE FROM .svc-card's LINE. 6.9 pairs the edge with
     `translate: 0 calc(-1 * var(--nudge))`; the rise is the AFFORDANCE half - it says "this leads
     somewhere" - and .act-card still leads nowhere. The firm wrote "the card only" and that word does the
     limiting: colour on one property, nothing else.
     REDUCED MOTION SUBTRACTS NOTHING HERE, by the guard's own stated rule. Its enumerated list is six
     TRANSLATE families, and it records in terms that cover this rule exactly: "Colour-only responses stay
     for that reason too, and are not listed here." Nothing moves; there is nothing to take away.
     THE WATERMARK IS UNTOUCHED. Its ::before still moves and still brightens, still has no focus parity
     (the paragraph below), and the two responses sit on different objects - the plate's border and a
     decoration painted on the plate - so neither has to know about the other. What the pair now needs
     saying once is that they share a tempo boundary and do not share a duration: the edge runs
     --dur-base (the transition is on .act-card in 6.10), the numeral runs --dur-media, so the near thing
     settles first and the distant thing trails, which is the same depth relation 6.10 argues for the
     travel.
     A THIRD OBJECT ANSWERS AT THE SAME TIME ON /attorneys, AND IT IS WRITTEN DOWN BECAUSE A CHECK OF THIS
     RULE FOUND IT AND THE LIST ABOVE DID NOT HAVE IT. The two #credentials plates each hold a
     ul.check-list, and [LIST-HOVER v25] (6.6, the firm's instruction of 2026-09-07 to make every bullet
     answer the pointer) gives every ROW its own hover - so pointing at ONE BULLET now fires three
     responses from one pointer position. Measured on #experience and #accreditation: the plate's border
     to rgba(var(--brand-rgb), 0.57), the row 4px laterally (--nudge), the diamond to --ink at scale 1.5.
     NOTHING COLLIDES, and the reason is the register's own clause rather than luck: the row and the
     diamond are CHILDREN of the plate, covered by "a decoration painted on a MATERIAL surface may answer
     the pointer provided the surface does not", while the border is the surface answering the ONE
     property the firm named. No shared element, no shared property, no shared duration. It is however
     the busiest object this edge lands on and the only place on the site where one pointer position buys
     both a red edge and a displacement, so it is stated rather than left to be rediscovered - and it is
     the reason the edge was kept to colour on one property when the firm's own word was "only". */
  /* NO FOCUS PARITY FOR THE WATERMARK, AND THE REASON IS NOT THE ONE THIS LINE USED TO GIVE. It read
     "there is nothing focusable inside, by design", which was already untrue when it was written -
     /attorneys' #experience card has carried <a href="/land-catchers"> inside its .check-list since
     2026-09-06 - and /business's cards 1 and 4 now add a .link-arrow each. The real reason is that the
     hover response belongs to the CARD and focus never lands on a card: it lands on a link INSIDE one,
     which is a different object being addressed, and .link-arrow brings its own focus-visible state
     (6.19). Brightening the plate's watermark because a link inside it took focus would say the plate
     was the thing focused. The numeral stays a pointer-only DEPTH response.
     [CARD-EDGE v36] AND THAT REASONING STOPS AT THE NUMERAL - IT IS NOT AN ARGUMENT AGAINST THE BORDER
     RULE BELOW, which is worth stating because the two look like the same question and are not. The
     watermark is DEPTH: brightening it is a claim about the object's distance from the reader, which a
     keyboard has no view on. The plate's edge is a CONTAINER OUTLINE: drawing it says "the thing you are
     typing into is in here", which is the one thing a keyboard reader most needs and which .svc-card has
     said the same way since the site was built. */
  /* [CARD-EDGE v36] KEYBOARD PARITY, OUTSIDE THE GATE AND QUIETER - 6.9's rule for .svc-card, restated
     term for term because the components are siblings and a card that answered the keyboard differently
     from its neighbour would be the defect and not the fix. Focus indicates POSITION, hover indicates
     ATTENTION, so the focus edge takes --line-strong (the neutral rung) where hover takes the warm tint,
     and 6.9 records why that pair reads as parity rather than as two ideas: one weight, two temperatures.
     IT IS MEANINGFUL ON THIS COMPONENT RATHER THAN CEREMONIAL, and the markup was counted rather than
     assumed: /attorneys' #experience card holds <a href="/land-catchers"> inside its .check-list,
     /business's cards 1 and 4 hold a .link-arrow each, and /employment-law's card bodies hold six inline
     anchors plus a .link-arrow of their own. :focus-within is the correct hook on all of them - the plate
     is an <article> and is never itself focusable, so there is no :focus-visible to write.
     6's preamble REQUIRES this rule, not merely permits it: "Every :hover has a matching :focus-visible /
     :focus-within OUTSIDE the gate in the same block". A hover added without it would have been the file
     breaking its own contract in the same edit that quoted the firm. */
  .act-card:focus-within { border-color: var(--line-strong); }
  /* [EMPLOYMENT v27, 2026-09-07] THE INLINE LINK IN A CARD BODY - THE DECLARATION THE PARAGRAPH AT THE
     HEAD OF THIS SECTION SPECIFIED IN ADVANCE, WRITTEN NOW THAT ITS TRIGGER HAS FIRED. Three
     declarations, and they are 6.5's `.prose a` three copied selector-for-selector rather than
     reinvented: same colour, same --line-control underline, same 0.18em offset, same two parity
     states, same tokens and durations. Written that way ON PURPOSE - a card body and a prose body are
     the same reading register at the same size, so a link that behaved differently in one of them
     would be the defect and not the fix.
     :not(.btn):not(.link-arrow) IS LOAD-BEARING AND IS NOT DECORATION COPIED ALONG WITH THE REST.
     .act-card p is exactly where a .link-arrow lives - /business's cards 1 and 4 and /employment-law's
     Cal/OSHA card each wrap one in a <p> - and .link-arrow draws its own rule with a ::after transform
     (6.19). Without the two exclusions this block would lay a --line-control underline UNDER that
     drawn rule and hand the arrow a second, contradictory hover. Same two exclusions, same reason, as
     6.5's.
     WHAT IT REPAIRS. Until this rule, a selector census of this file returned NO rule of any kind
     matching a link inside .act-card, so the six anchors took base's colour and stopped there: no
     hover, no focus colour, no second cue beyond the UA underline. They were the only link family on
     the site answering neither a pointer nor the keyboard, which is [HOVER v23]'s colour-parity rule -
     the firm's own instruction of 2026-09-06 - broken by markup that only arrived on 2026-09-07.
     THE RESTING COLOUR IS THE RECORDED FAILURE, UNCHANGED AND NOT RE-ARGUED HERE. --brand on --bg-2 is
     the same DECIDED failure every red link on this site has carried since the firm's all-red
     instruction; 6.5 and the token block both state its figure, and this rule neither improves nor
     worsens it. What it adds is the second cue and the two state changes - the half that was missing. */
  .act-card p a:not(.btn):not(.link-arrow) { color: var(--brand); text-decoration-color: var(--line-control); text-underline-offset: 0.18em; transition: color var(--dur-fast) ease, text-decoration-color var(--dur-fast) ease; }
  :where(html:not(.has-touch)) .act-card p a:not(.btn):not(.link-arrow):hover { color: var(--ink); text-decoration-color: currentColor; }
  .act-card p a:not(.btn):not(.link-arrow):focus-visible { color: var(--ink); text-decoration-color: currentColor; }

  /* ---- 6.11 FEATURE SPLIT ----------------------------------------------------------------------
     Intrinsic, from the container's derivation (a): text 34.35 + --space-4 2.618 + equal media column
     34.35 = 71.318rem = 1212.4px of container, reached at ~1267.4px of viewport, so it resolves to one
     column without a query of its own. [FACE v9] THE SEAM BETWEEN "NAV UP" AND "SPLIT STACKED" IS NOW
     ~243px WIDE, and it is the single largest layout consequence of the face change. History, because
     the direction of travel is the point: the pair needed ~1030px against a 1024px collapse when the
     split first shipped; the 106.25% root multiplier took it to ~1094px (a ~70px seam); Montserrat's
     +17.4% per character took --measure from 29.25 to 34.35rem and the pair to ~1267.4px. [REPAIR v10]
     The structural literal then moved 1024 -> 1072, which narrows this seam by 48px rather than opening
     it: between 1072 and 1267 the nav is up and the split is already one column, ~195px where it was
     ~243px. The seam is still the largest layout consequence of the face change and is still owned by
     --measure, not by the breakpoint.
     THE CAUSE IS THE MEASURE, NOT --container: every term of this sum is --measure or --space-4, and the
     72rem cap (1224px) never binds below 1279px of viewport. The alternative that would have preserved
     the old seam is the 60-character measure the container derivation records and rejects; it was
     rejected there for a reason that is about reading, not layout, and this seam is the price.
     The split was always intrinsic and always allowed to resolve on its own sum, so this is a wider
     seam of a behaviour the file already had, not a new behaviour.
     THE SPLIT'S MEASURE EXCEPTION IS GONE - see the container derivation: at 1224px of container each
     half is now 70.7 characters, inside the 60-75 band, where in Titillium it was 83.0 and outside it.
     A .feature-body holding bare <p> at this width is no longer a bug waiting to happen. */
  .feature-split { display: grid; align-items: center; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--measure)), 1fr)); container-type: inline-size; }
  .feature-split--reverse .feature-media { order: 2; }
  /* THE EDGE IS THE FRAME'S, NOT THE PICTURE'S. It used to sit on the img, which is the same rectangle
     while the picture is still - but 6.27 clips this frame and drifts the picture inside it, and a
     scaled child carries its own border out through the clip, so a 1px edge on the img would be cropped
     away top and bottom for the whole scrub. Declared on the frame it is a single owner in both cases,
     and there is no [data-parallax] variant of this line to keep in step. The img loses 2px of width to
     the border box; at the 584.0px column --measure now gives that is 0.34% and no derivation depends on it.
     [PHOTO v16] THIS EXISTING BORDER IS ALSO 6.29's RED EDGE, WHICH IS WHY THE COLOUR IS A var() WITH A
     FALLBACK AND NOT A SECOND DECLARATION SOMEWHERE ELSE. --photo-edge is unset at rest, so the fallback
     paints --line exactly as it always has; 6.29 sets it to --brand-fill on hover and the same one
     property changes. ONE OWNER OF border-color, in the block that owns the border. Measured on
     /attorneys, and on the /employment-law figure the firm had removed on 2026-09-07 - /attorneys and
     /cal-osha-defense-and-appeal are the surviving pair: changing border-COLOR moves the img by
     exactly zero, which is the
     whole reason the edge is carried by colour and never by width (2px moves the picture 1.09px and
     grows the frame 0.672px). And the ring is never hidden by the clip: overflow: hidden on the
     parallaxed frame clips descendants to the PADDING box, and the border sits outside it - the drifting
     img measures 640.647px inside a 589.75px frame and the border still shows. */
  .feature-media { margin: 0; border: 1px solid var(--photo-edge, var(--line)); border-radius: var(--radius); }
  .feature-media img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }
  /* A PORTRAIT KEEPS ITS PORTRAIT SHAPE AT EVERY WIDTH: object-fit: cover on a 3:4 source in a 16:9
     box keeps ~42% of its height - on this crop, everything from the chin down. The viewport concern
     is answered by capping WIDTH, never by flattening aspect. */
  /* 22rem (374px) at 3/4 = 498.7px tall. The cap is UNCONDITIONAL, not tied to the structural number:
     the split is intrinsic, so it resolves to one column at ~1267px of viewport ([FACE v9]: ~1094px
     before --measure moved with the face), and an uncapped portrait would be 1224 x 1632px there.
     A head is not scenery; it has a size.
     THE 22rem HERE AND --grid-track's 22rem ARE UNRELATED and now numerically identical by coincidence
     ([RED v7] moved the track from 20rem). This one is a PORTRAIT WIDTH derived from a head at 3:4; that
     one is a CARD COLUMN derived from 40 characters and then directed wider. Neither may be edited to
     "match" the other, and neither is a var() of the other for exactly that reason. */
  .feature-media--person { max-width: 22rem; }
  .feature-media--person img { aspect-ratio: 3 / 4; }
  .feature-body > * + * { margin-top: var(--space-3); }
  /* CENTRED ONLY ONCE IT IS A FULL-WIDTH ROW; in the two-column split it stays on the left axis.
     [v7 REPAIR PASS] A CONTAINER QUERY, NOT THE STRUCTURAL LITERAL, because the collapse this
     rule tracks is the split's OWN intrinsic sum and never was the nav's: 2 x --measure + --space-4.
     The two were within 6px of each other at the 16px base and the literal stood in for the sum; at
     106.25% the sum moved (977.9 -> 1039.0px of container) and the literal did not, opening an 85px
     band - 1080 x 810, an iPad in landscape, sat in the middle of it - where the row was ALREADY one
     column while the portrait stayed cropped 3/4 and pinned left with ~636px of dead space beside it.
     Measured on /attorneys at 1080: one 1009.98px column, figure 374px at margin 0, aspect-ratio 3/4.
     [FACE v9] THE SUM MOVED AGAIN AND THE LITERAL MOVES WITH IT, 1039 -> 1212px: --measure went
     29.25 -> 34.35rem with the face, so 2 x 34.35 + 2.618 = 71.318rem = 1212.4px of container
     (~1267px of viewport). This literal is nothing but that sum, so it is re-derived rather than
     recorded - the same distinction the header CTA query is held to, and the opposite of the
     structural pair. Left at 1039 it would have re-opened the identical band, 173px wide this time.
     A VIEWPORT QUERY CANNOT CLOSE THAT BAND - the collapse point moves with the scrollbar - and the
     container's own width is what the rule is actually about. 1212px and not 1212.406: the 0.406px
     sliver at the boundary matches neither, the same order of dead zone as the 66.99/67rem pair.
     container-type: inline-size is safe here: .feature-split is a block-level grid whose inline size
     comes from its parent, and every absolutely positioned descendant a split can hold already has a
     positioned ancestor of its own (.link-arrow, .num-list li, .check-list li), so the containing block
     it introduces changes nothing. The 29.99rem repeat below still wins by source order, unchanged. */
  @container (max-width: 1212px) { .feature-media--person img { aspect-ratio: 4 / 5; } .feature-media--person { margin-inline: auto; } }
  @media (max-width: 29.99rem) {
    .feature-media img { aspect-ratio: 3 / 2; }
    /* Repeated inside this block ON PURPOSE: the --person rule above lives in the @container block, so
       [REPAIR v10] - it has NOT lived in a 63.99rem viewport block since [v7 REPAIR PASS] moved it to
       @container (max-width: 1212px), and this line kept naming the block it left. The cascade argument
       is unaffected: that rule is still earlier in the file at equal specificity, which is all it needs -
       at 29.99rem the generic 3/2 rule here is later in the cascade at equal specificity and would win
       - which is exactly how the face got cropped out of the portrait on the source site. */
    .feature-media--person img { aspect-ratio: 4 / 5; }
  }

  /* ---- 6.12 STATS -------------------------------------------------------------------------------
     Figures that ACCRUE carry data-count-to + data-since and are derived at runtime; figures that were
     VERIFIED are static and carry their date in .stat-note. The note is NOT dimmer than --ink-2: it
     carries real information, so it recedes by SIZE and by dropping the label's tracked caps, never by
     fading below the text floor. The hairlines belong to the ROW, which is where [data-reveal] sits.
     [ALIGN v26] 2026-09-07, TWO CORRECTIONS TO THE TWO SENTENCES ABOVE, BOTH OVERTAKEN BY THE FOURTH STAT.
       - THE TRACK SUM IS A 4-UP SUM NOW: 4 x 14 + 3 x 1.618 = 60.854rem = 1034.5px, inside the same
         1224px container. It was written as "3 x 14 + 2 x 1.618 = 45.236rem = 769.0px ... at 3-up", which
         stopped being the row's arithmetic the day it grew a fourth item and is the number that had to be
         re-derived to find the 1072-1089px orphan band the >= 67rem block below now closes.
       - "FIGURES THAT WERE VERIFIED ARE STATIC AND CARRY THEIR DATE IN .stat-note" IS NO LONGER THE ONLY
         KIND OF NOTE, and the general rule underneath it is the one to keep: A NOTE NAMES THE BASIS OF
         THE FIGURE ABOVE IT. A date is the basis of a verified figure; for the two Avvo badges, which are
         re-fetched on every load and so are neither static nor datable, the basis is the SCALE the score
         is out of, which the artwork does not print. Both badges gained one on 2026-09-07 - and they
         gained it as a LAYOUT repair as much as a copy one: the third rung was empty under both, so two
         slots ended at the label and two ended 33px lower, a ragged bottom edge in a row the firm asked
         to be even. */
  .stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: var(--space-3); }
  .stat { position: relative; display: grid; gap: var(--space-1); padding-top: var(--space-2); }
  /* --reveal-delay inherits from .stats-row, which is where [data-reveal] sits; --lag-rule is the
     hairline's own lag inside the object. See .num-list for why they add. */
  .stat::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 1px; background: var(--line); transform-origin: left; transition: transform var(--dur-slow) var(--ease-out) calc(var(--reveal-delay, 0s) + var(--lag-rule)); }
  html.js .stats-row[data-reveal]:not(.is-revealed) .stat::before { transform: scaleX(0); }
  /* Tabular lining figures so digits cannot jitter while the counter runs.
     [STAT v24] 2026-09-07. THE RUNG IS --text-4 AND BOTH FIGURES TAKE IT. The firm, 2026-09-06: "The 13+
     needs to be much bigger" and "The 3 for disciplines should also be larger". Two asks, one answer,
     and the answer is off the existing scale rather than a literal invented for this row.
     WHY THE TOP RUNG AND NOT THE NEXT ONE. --text-4 is 2.618rem = 44.506px, the hero / page-title rung,
     and the type block states there is NO rung above it. The 13+ was at --text-3 34.986px, so --text-4
     is +27.2% and is the only step the scale can offer that answers "much bigger"; a second step does
     not exist to be taken. Once the 13+ sits at the ceiling there is nowhere left for the 3 that is both
     LARGER than the --text-2 27.506px it had and still subordinate, so it takes the same rung (+61.8%)
     and the row's two figures finally set at one size - which is what a row of statistics wants anyway.
     THAT IS A PRECONDITION, NOT A CONVENIENCE, and it is the sentence that makes the alignment below
     legal. [REPAIR v19] two rules down records that two font sizes at one leading do NOT put their
     baselines in the same place inside their line boxes - it measured 2.59px of rag and moved the figure
     rung to `baseline` to hide it. Centring cannot hide it: two rungs centred in one track put their
     baselines 1.70px apart (measured on this markup). One rung removes the disagreement at the SOURCE,
     and only then is the rung free to align on anything but a baseline.
     AND THE HIERARCHY THE OLD SIZE DIFFERENCE CARRIED IS NOT LOST, IT MOVED. [STAT v18] made the
     accruing figure a rung taller so it would read first; the firm's own instruction now gives that job
     to the RED, BOLD PLUS below - "the plus is the flourish, the figure is the fact" - so size is free to
     become uniform and the one mark that distinguishes the two figures is the one the firm chose.
     justify-self: start TRIMS THE BOX TO THE GLYPHS, and it is load-bearing three times over rather than
     tidiness. A .stat-num is a grid item and stretches: measured at 1440 the "13" span was 285.38px wide
     - the whole column - for 75.94px of ink. (1) The hover target would have been a full-width invisible
     band rather than the figure the firm named. (2) `scale` about any origin on a 285px box moves ink
     that is nowhere near the pointer. (3) --pop's own derivation is solved against W, and a W that is the
     column rather than the figure would have made that sum meaningless. Nothing moves visually: the text
     was left-aligned inside the stretched box already.
     transform-origin: left center IS ALSO LOAD-BEARING AND NOT A DEFAULT (the default is `50% 50%`).
     Growing from the centre would push the LEFT edge out by (--pop - 1) / 2 x W = 2.28px, and the left
     axis of this row is shared by four stats and by every label under them. A figure that steps off its
     own label's axis under the pointer is a worse defect than no hover at all. Pinned left, the figure
     grows to the right into space it already has and 1.54px up and down, which nothing else can see
     because a transform does not reflow.
     THE TRANSITION IS DECLARED HERE, ON THE ELEMENT, NOT INSIDE THE POINTER GATE, and 6's own rule at
     the head of this section is why: a transition may live inside the gate if and ONLY if hover is the
     only state that uses it. That is true here - a .stat-num in a plain .stat is not focusable and has no
     focus twin - but the linked stat's numeral shares this selector and DOES have one, so the declaration
     stays outside and one element cannot end up with two transition rules. `scale`, not `transform`, for
     the reason [MOTION v20] gives one rule down: this element declares neither, the standalone property
     is compositor-only, and leaving `transform` free is exactly what let a translate land here without a
     fight. */
  .stat-num { font-size: var(--text-4); line-height: var(--leading-heading); font-variant-numeric: lining-nums tabular-nums; color: var(--ink); justify-self: start; transform-origin: left center; transition: color var(--dur-fast) ease, scale var(--dur-base) var(--ease-out); }
  /* [ALIGN v26] 2026-09-07. THE LAST 5px OF THE FIRM'S "same height", AND IT IS A LINE BOX, NOT A BADGE.
     With both badges re-registered on their own ink (see (2) above), the four figure BOXES centre to
     0.02px and the four painted centres still did not: the two numerals read 4.995px HIGH of the two
     badges. align-items: center centres a LINE BOX, and a line box is not centred on its type. Measured
     in the shipped page with canvas actualBoundingBox rather than a stored constant: the box is
     51.182px (44.506 x --leading-heading 1.15) but Newsreader's ascent/descent split at this size is
     33 / 12, so the half-leading is symmetric while the content is not, and the ink lands high by
     (33 - 12) / 2 - the 4.995px, near enough exactly.
     THE FIX IS TO TRIM THE BOX TO THE TYPE, WHICH IS WHAT text-box EXISTS FOR - not a compensating
     margin. A margin would be a number nobody could re-derive, would be wrong the moment the face or
     the rung changed, and is the exact move [ALIGN v24] was told not to make on the badge.
     WHY cap alphabetic AND NOT ex OR text: the figures here are LINING (declared above), so their ink
     runs cap band to baseline. Trimmed, the box is 29.813px and the baseline IS its bottom edge - both
     measured in the shipped page, the second one because the whole correction rests on it and a spec
     guarantee is worth confirming once.
     WHAT IS LEFT, MEASURED AND NOT ROUNDED AWAY: 0.594px, the numerals still that much high of the two
     badges. It is Newsreader disagreeing with its own metrics, and no CSS reaches it: the trim uses the
     font's declared cap (29.813px = 0.6699em, OS/2's figure), while the LINING FIGURES are 32.0px of ink
     and the round-bottomed ones overshoot the baseline by 1.0px - so the type is 2.19px taller than the
     box it was trimmed to at the top and 1.0px longer at the bottom, and its centre sits (2.19-1.0)/2
     above. 5.25px of misregistration becomes 0.594px, which is under this row's own 0.81px letterbox and
     under a device pixel at 1x. Chasing the rest would mean a hand-set number again, and the trade is
     the wrong way round.
     @supports, AND THE BARE RULE ABOVE IS THE FALLBACK ON PURPOSE. text-box is Chromium 133+ / Safari
     18.2+ and Firefox does not have it yet; where it is missing this block does not exist and the row
     renders exactly as it did before this pass - centred boxes, ink 5px high. That is the state the firm
     saw and did not object to; the enhancement is worth having and is worth nobody depending on. */
  @supports (text-box: trim-both cap alphabetic) {
    .stat-num { text-box: trim-both cap alphabetic; }
  }
  /* [STAT v18] 2026-09-06. THE ACCRUING FIGURE IS ONE IDEA, SO IT GETS ONE HOOK AND TWO DECLARATIONS.
     The firm, in the same instruction: the years figure "should be bigger" and "should read with a '+'
     after the number of years", and it is now "automatically calculated from the date the December 1,
     2012". The third clause is what supplies the selector. [data-since] is ALREADY this site's marker
     for "this figure is derived from a date at runtime" - main.js block 3 reads it, and writes both
     data-count-to and the visible text from it - so it is also the only honest answer to "which numeral
     on this site is OPEN-ENDED". A count that keeps climbing is exactly the thing that earns a plus, and
     the same figure is the one the firm wants read first. One attribute, both answers, and no third
     class for the markup to remember or an editor to strip.
     THE SIBLINGS MUST NOT INHERIT EITHER, WHICH IS THE POINT OF SCOPING IT THIS WAY AND NOT TO
     .stat-num. "3 Disciplines" is a closed count of things held at once - initCounters' own editorial
     rule already refuses it a counter for that reason - so "3+" would be a false statement, not a
     flourish; and the row's third slot is now a graphic with no numeral at all. Nothing else on the site
     carries [data-since]: one element, checked across all 20 built pages.
     THE PLUS IS A PSEUDO-ELEMENT BECAUSE THE TEXT NODE IS NOT OURS. setFigure() ends `el.textContent =
     str` on every counter frame and again on land, so a typed "+" would survive exactly until the script
     ran - a defect that looks correct in the source and in the no-JS render, and only ever shows up in
     the one state a visitor actually gets. Generated content is outside that assignment's reach.
     WHAT THE PLUS'S ANNOUNCEMENT IS, AND WHAT WAS ACTUALLY MEASURED, KEPT APART. All three engines
     include ::before / ::after content in the accessible name computation, so the intended reading is
     "13+" - the same string a reader sees, which is the whole reason a pseudo-element is legitimate here
     and an image of a plus would not be. THAT WAS NOT CONFIRMED ON THIS PAGE. The tooling available
     reports an EMPTY name for every short numeric node - measured against three controls appended to
     this row ("77" as .stat-num, "88" as .stat-label, "99" with no class, all three blank, while a
     longer string beside them read back fine) - so it cannot see the numeral's name with or without the
     plus, and the "3" beside it is equally invisible to it. The exposure is therefore the same as the
     digits' own, whatever that turns out to be: the plus can only be lost if the numeral is. Left as the
     one thing here to put in front of a real screen reader.
     ONE CONSEQUENCE, RECORDED RATHER THAN HIDDEN: the plus sits there through the count-up, so the
     tween reads "0+ ... 13+". initCounters hides a .unit child for precisely that reason, and a
     <span class="unit">+</span> would have survived setFigure too (it rewrites the text node and leaves
     the child) - but it would put the plus in the markup, where an edit can drop it while [data-since]
     stays, and the intermediate frames of a count-up are already not facts. The transient is the same
     kind the numeral itself has always had, not a new one.
     BIGGER IS ONE RUNG UP THE SCALE THAT EXISTS - --text-2 to --text-3, 27.51px to 34.99px - and not a
     literal invented for this stat. --text-3 is the section-display rung, which is the right register:
     it makes the figure read at the weight of a heading without inventing a size that would then have to
     be defended on its own. There is no rung between the two, so this is also the smallest step the type
     scale can express.
     [STAT v24] 2026-09-07. THE SIZE HALF OF v18 IS SUPERSEDED AND THE RULE THAT CARRIED IT IS GONE. The
     firm asked for BOTH figures to grow, .stat-num above now sets --text-4 for both, and a
     `font-size: var(--text-3)` here would have been a rule that made the loudest figure on the page
     SMALLER than its neighbour - the exact inversion of the instruction. Deleted rather than left as a
     no-op: a declaration that happens to be harmless today is the one an editor restores tomorrow.
     THE SELECTOR STAYS, AND THE PARAGRAPH ABOUT WHY IT MUST NOT BE .stat-num IS NOW CARRYING ITS FULL
     WEIGHT RATHER THAN HALF OF IT. v18 scoped two declarations to [data-since]; one has left, and the
     one that remains is the one the scoping was really for. "3 Disciplines" is a closed count and "3+"
     would be a false statement; the two badge slots have no numeral at all. One attribute, one
     consequence, and nothing else on the site carries it. */
  /* [STAT v24] 2026-09-07. THE PLUS IS RED AND HEAVY AT REST, ON THE FIRM'S INSTRUCTION OF 2026-09-06:
     "the PLUS symbol should be bold and red". It is the ONE red thing in this row at rest, which is the
     firm's own split made visible - the plus is the flourish, the figure is the fact - and it is why the
     figure itself stays --ink.
     600 IS NOT "the bold one", IT IS THE TOP OF THE AXIS THAT IS ACTUALLY LOADED. --wt-strong is 600;
     base's display grant (which enumerates .stat-num) sets font-synthesis: none, and the request is
     Newsreader wght 400..600 - so an ask heavier than 600 would not render heavier, and no faked bold is
     available to cover for it. This is the heaviest real point the face has.
     IT INHERITS ITS SIZE, so the plus grows with the figure and cannot drift from it: only `color` and
     `font-weight` are declared, and --text-4 arrives through .stat-num above. */
  .stat-num[data-since]::after { content: "+"; color: var(--brand); font-weight: var(--wt-strong); }   /* TEXT, so the text stop. 2.99:1 on --bg and 2.86:1 on --bg-2 - the ground this row actually ships on, .section--alt - at 44.506px/600: large text, floor 3:1, missed by 0.01 and by 0.14. Recorded failure, on the firm's decision of 2026-09-06 */
  .stat-label { font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-4); text-transform: uppercase; color: var(--ink-2); }   /* 8.44:1 on --bg */
  .stat-note { font-size: var(--text-label); letter-spacing: 0; text-transform: none; color: var(--ink-2); }   /* the SAME ink as the label - never fainter */
  /* [STAT v18] THE BADGE SLOT. One stat in the row now carries a GRAPHIC where the other two carry a
     numeral (the firm, 2026-09-06: 'Instead of "2 states admitted to practice law", list the following
     badge and say "Rating by AVVO"'), and the badge's own hygiene note says to move its inline
     height/width into CSS. THIS RULE IS THAT BOX, AND IT IS NOT OPTIONAL TIDINESS: measured on the probe
     with the inline style stripped and nothing put in its place, the svg takes its containing block's
     width and solves its height from the viewBox - 389.64 x 306.16px in a 389.64px column, roughly eight
     times the numeral beside it. An svg with a viewBox and no size is not a small svg; it is an
     unbounded one.
     155 x 120 IS THE ARTWORK'S OWN DECLARED BOX, moved out of the attribute rather than redesigned - the
     hygiene instruction says move, and a size invented here would be a second decision smuggled in with
     a first. It does not match the viewBox exactly (280 / 220 would put 155 wide at 121.8 tall), so the
     default preserveAspectRatio letterboxes it by under two pixels, exactly as it does on Avvo's own
     page. max-width keeps it inside a narrow column; height: auto there so the aspect survives the clamp.
     KEYED ON THE ELEMENT, NOT ON A CLASS. There is exactly one <svg> inside a .stat anywhere on the
     site, and the alternative - .avvo-badge - is a third-party class name this file does not own and a
     rename would silently unsize the badge rather than fail loudly.
     AND THE SIZE CANNOT GO BACK IN THE ATTRIBUTE EVEN IF SOMEONE WANTED IT THERE: the site ships
     style-src 'self', so a style="" on the svg is refused by the CSP with "Applying inline style
     violates..." in the console and no size is applied at all. Read off the probe, not off the header.
     [STAT v24] 2026-09-07. TWO SENTENCES ABOVE WENT STALE ON THE SAME DAY AND ARE CORRECTED HERE RATHER
     THAN QUIETLY REWRITTEN, because the reasoning they carry is still the reasoning.
       (1) "exactly one <svg> inside a .stat" IS NOW TWO - the firm asked for a fourth stat carrying the
           Avvo client-reviews badge (2026-09-06). Keying on the element is not weakened by that; it is
           VINDICATED by it. One rule sized both badges the day the second arrived, with nothing to add
           and no class to invent, which is exactly what keying on a structure rather than on a
           third-party name buys.
       (2) THE LETTERBOX ARITHMETIC IS NO LONGER 280 / 220's. src/pages/attorneys.html re-declared BOTH
           badges over one viewBox SIZE, 275.303 x 210.256, so that the two Avvo exports stop meeting
           this box at two different scales. Against 155 x 120 that size is the SHORTER ratio, so the fit
           is width-constrained rather than height-constrained and the letterbox moves from the sides to
           the top and bottom: 155 / 275.303 = 0.56302, artwork 155 x 118.38, 0.81px of nothing above and
           below. Under a minute either way, and both badges take the identical scale.
           [ALIGN v26] 2026-09-07 CORRECTS THE OTHER HALF OF THIS SENTENCE, WHICH WAS WRONG THE DAY IT
           WAS WRITTEN. It also said the two badges took the identical REGISTRATION, over one shared
           min-y of 1.484 called "the tight union of the two artworks' ink". A union is tight for the
           union and for neither member. Hit-tested: 0.90px of dead space under the rating artwork,
           6.03px under the client-reviews artwork, which floated 2.61px high - the firm's own
           "too high up and looks uneven" of 2026-09-06, reintroduced by the pass that fixed it.
           The cause is that THE TWO BADGES DO NOT SHARE A MARK - a 209.17-unit shield against a
           201-unit circle - so min-y is now per badge (1.867 and -3.145), each putting that badge's own
           ink centre on the box centre. The SIZE, and so the scale, is still shared and still the point.
           The derivation, the measurements and the rejected alternative are in attorneys.html beside the
           two attributes, because that is where the numbers are typed.
       (3) A SIDE EFFECT WORTH RECORDING, because it removes an inconsistency nobody had noticed: the
           narrow-column rule below sets width: 100%, max-width: 155px, height: auto, which under the old
           viewBox rendered the badge 121.79px tall - 1.5% BIGGER below 480px than above it, for no
           reason anyone chose. Under the shared viewBox the same rule gives 118.38px, the same artwork
           size as the desktop box. Measured at 390: 118.38 both sides of the breakpoint. */
  .stat svg { display: block; width: 155px; height: 120px; max-width: 100%; }
  @media (max-width: 29.99rem) { .stat svg { width: 100%; max-width: 155px; height: auto; } }
  /* [STAT v18] THE ROW ALIGNS ON ITS LABELS, WHATEVER THE FIGURE ABOVE THEM IS - AND THIS IS THE RULE
     THAT KEEPS "a set" TRUE NOW THAT ONE SLOT HOLDS A GRAPHIC. Each .stat is its own grid, so before
     this the three columns only ever agreed by accident: auto rows plus the default stretch pushed every
     row of the SHORTER stats down by an equal share of whatever the tallest stat had spare, so the
     moment one figure stopped matching the others the labels went ragged. MEASURED on the probe at 1440,
     with the row the firm asked for (13+, 3, the badge): label tops 378.37 / 372.64 / 431.56 and note
     tops 437.90 / 435.03 / 464.50 - the badge slot's label a clear 55px below its neighbours', which is
     not a rhythm anyone would have chosen. Under this rule all three land on 631.52 and 664.45 exactly.
     THE ENLARGED NUMERAL IS NOT WHAT BROKE IT, and the numbers say so rather than the author: with three
     numerals in the row, --text-2 against --text-3 moved the label by 1.75px - invisible, and it would
     have stayed invisible. The badge's 120px against a 40px numeral is the whole of the 55px.
     SUBGRID, SO THE ROW OWNS THE THREE RUNGS AND EACH STAT INHERITS THEM rather than each stat solving
     its own. That is also why this is not badge-specific and does not need to be: put three numerals
     back in the row and every track is content-height again, align-items: end has nothing to push
     against, and the rule computes to exactly what shipped before it existed. It repairs the general
     case and happens to be the general case.
     align-items: baseline ON THE STAT, align-self: start ON THE TWO TEXT RUNGS. Only the FIGURE rung is
     ever taller than its content, so the figure alignment binds there and nowhere else. The two text
     rungs are put back to start explicitly because a label that wraps in one column and not another
     would otherwise drop the short one to the bottom of the taller track, away from the figure it
     names. .stat-stretch is untouched by either: it is absolutely positioned with inset: 0, so it is
     not a grid item and align-self reaches only a static position it does not use.
     [REPAIR v19] IT SAID `end` UNTIL 2026-09-06, AND end WAS THE WRONG EDGE - found by measuring, not by
     reading. `end` aligns the bottom of each item's LINE BOX; two font sizes at one leading do not put
     their baselines at the same height inside their line boxes, so the row's two numerals sat 2.59px
     apart, "3" at --text-2 hanging BELOW "13+" at --text-3 because the smaller face has the shallower
     descender space and its baseline therefore sits nearer its own box bottom. Ragged baselines across
     three columns is the one thing a stats row cannot have, and 2.59px is small enough to read as
     sloppiness rather than as intent.
     `baseline` FIXES IT AND DELIVERS WHAT THE PARAGRAPH ABOVE ALREADY CLAIMED. The old wording said end
     "hangs the numerals on the badge's bottom edge"; it hung their LINE BOXES there and left the
     numerals themselves floating 6.5 and 9.1px above it. An svg has no baseline of its own, so grid
     synthesises one from its border box and it lands exactly on the badge's bottom edge - which means
     under `baseline` the two numerals sit ON that edge and the sentence becomes true. MEASURED at 1440
     by tools/probe-statrow.mjs with a zero-size vertical-align: baseline probe span rather than
     estimated: baselines 368.80 / 371.39 (spread 2.59) before; 381.02 / 381.02, badge bottom 381.02
     (spread 0.00) after. `last baseline` measures identically - every figure here is one line - so the
     plain keyword is used.
     WHAT IT COSTS: the row grows 12.21px, 202.88 to 215.09, because the numerals drop by their own
     descender depth and the label rung follows. THE BADGE DOES NOT MOVE (top 261.02, bottom 381.02 in
     both), and the three labels stay locked to each other (spread 0.00 before AND after) - which is the
     property this whole subgrid block exists to hold, and the check that the fix did not trade one
     alignment away for another.
     GATED TO THE DESKTOP LAYOUT because below 67rem the row is not three columns at all - see the
     restructure below, which gives the badge its own answer.
     ================================================================================================
     [ALIGN v24] 2026-09-07. `baseline` BECOMES `center`, AND THE DIAGNOSIS COMES FIRST BECAUSE THE FIRM
     DESCRIBED A SYMPTOM ("The Avvo symbol is too high up and looks uneven. We need to lower it so all
     three stats are at the same height", 2026-09-06) AND A SYMPTOM IS NOT A CAUSE. Three candidate
     causes were named and all three were MEASURED against the rendered page at 1440 before anything
     moved - tools/probe-statrow4.mjs for the layout, tools/probe-badge-ink2.mjs for the artwork.
       (a) IS THE ROW A GRID WITH A SHARED BASELINE? YES, AND IT IS DOING EXACTLY WHAT IT SAYS - so the
           first thing the measurement did was clear the rule of the charge. Numeral baselines 377.02 and
           377.02, spread 0.00; labels 399.73 x3, spread 0.00; notes 432.67 x2, spread 0.00. Nothing is
           ragged. IT IS STILL THE CAUSE, and that is the finding: a baseline is a NUMERAL'S FEET and a
           PICTURE'S BOTTOM EDGE, and those are not the same kind of place. Hang a 120px picture and a
           24.22px cap on one line and their optical centres come to rest 49.51px apart - badge ink
           centre 315.40, "13" cap centre 364.91 - and that offset is the thing the eye is calling
           uneven. The rule is right and the AXIS is wrong.
       (b) IS THE SVG'S LINE BOX THE CULPRIT? NO, and it is ruled out by measurement rather than by
           argument. `.stat svg` is `display: block`, so there is no inline line box, no strut and no
           vertical-align descender gap under it; `vertical-align` computes to `baseline` and is inert on
           a block. What grid uses instead is the SYNTHESISED baseline of a block-level replaced item,
           its bottom MARGIN edge - and the measurement pins that exactly: badge box bottom 377.02
           against numeral baseline 377.02, zero. Nor is anything leading it down: badge box top 257.02
           against a stat top of 240.02 is --space-2 (17.00px) and not a pixel more.
       (c) DOES THE ARTWORK HAVE WHITESPACE IN ITS OWN BOX? YES, AND IT WAS AT THE BOTTOM, NOT THE TOP.
           This is where a guess would have gone wrong: the hypothesis on the table was air at the TOP
           pushing the badge down, and air at the top would have been harmless under a bottom-referenced
           axis. Hit-tested with elementsFromPoint at quarter-pixel steps AND confirmed by rasterising
           the badge at 8x and scanning for the first and last inked row - two methods because a <path>'s
           getBBox reports Blink's LOOSE bezier bounds and would have answered for geometry the renderer
           never paints - the ink spanned user-unit y 2.29..211.74 inside a declared viewBox of 0..220.
           At the rendered 120px box that is 1.25px of nothing above the artwork and 4.50px below it, so
           the badge floated 4.50px ABOVE the very line the row was aligning on. Real, and 9% of what the
           firm is looking at. A margin tuned until it looked right at 1440 would have been (a)'s defect
           wearing (c)'s number, and would have been wrong at 390 and wrong again at four items.
     BOTH CAUSES ARE FIXED AT THEIR OWN LAYER, WHICH IS WHY NEITHER FIX IS A NUDGE.
       (c) IS NOT MAIN.CSS'S TO FIX AND IS NOT FIXED HERE. src/pages/attorneys.html re-declares BOTH
           badges over one shared viewBox that is the tight union of the two artworks' ink, so the
           declared box IS the ink box and "align the boxes" and "align the pictures" become one
           statement. No path, polygon or coordinate moved; only the window onto them. Measured after:
           the badges' bottom dead space falls 4.50px -> 0.82px.
       (a) IS FIXED HERE, BY CHANGING THE AXIS. `center` aligns the figure rung on the thing a numeral
           and a picture genuinely share - the middle of the mark - instead of on a line only one of them
           has. Measured at 1440 over four stats: the two numeral boxes land 433.42..484.59 (centre
           459.01, the rung's own centre to the hundredth) with the cap band at 438.61..469.42 for a cap
           centre of 454.02, and the two badges' ink centres at 459.26 and 456.41 - 5.24px and 2.39px
           from the numerals' cap centre, against 49.51px before. Baseline spread stays 0.00 because
           there is now only ONE numeral rung to disagree.
     THAT LAST CLAUSE IS THE WHOLE LICENCE FOR THIS CHANGE, AND IT IS WHY THIS RULE COULD NOT HAVE BEEN
     WRITTEN BEFORE .stat-num MOVED TO --text-4. Centring does not preserve baselines: two rungs centred
     in one track put their feet 1.70px apart, which is the same defect [REPAIR v19] measured at 2.59px
     and moved this rung to `baseline` to remove. `baseline` was the right answer to a two-rung row; it
     is the wrong answer to a row of one rung and two pictures. The firm's two instructions - make both
     figures bigger, and make the row read level - turn out to be one instruction.
     WHAT "LOWER IT" ACTUALLY MEANS HERE, STATED RATHER THAN GLOSSED, BECAUSE THE ROW DOES NOT MOVE THE
     BADGE DOWN. The badge IS the figure rung: at 120px it is the tallest thing in it, so the track is its
     own height and there is no room inside that track to lower it into. What centring does is bring the
     NUMERALS UP to the badge's band, which is the only construction in which four marks of different
     heights can be "at the same height". The alternative - a taller track with the badge pushed to the
     bottom - moves the numerals down by exactly as much and changes no relationship at all; and
     shrinking the badge to the numeral's own height renders its 13-unit name at 3.2px, which the
     restructure below already rejected for the same reason at 390. The firm's ask is met in the sense
     available to it: the badge's mass is no longer sitting above the figures beside it.
     WHAT IT COSTS, MEASURED AND NOT HIDDEN: the numerals now sit 34.43px clear of the bottom of the
     figure track, where before they sat on it, so the gap between a numeral and its own label grows by
     that much while the badges' stays as it was. That is the price of one shared band, it is paid on the
     two slots whose figure is smallest, and it is the reason `align-self: start` on the label and note
     rungs below is not optional - those two rungs must keep hugging the top of their tracks or the row
     would open a second hole under them.
     ================================================================================================ */
  @media (min-width: 67rem) {
    /* [ALIGN v26] 2026-09-07. THE TRACK COUNT IS STATED HERE BECAUSE THE ROW HAS FOUR ITEMS AND THIS
       BLOCK IS WHAT MAKES A ROW OF THEM ONE BAND. A REAL 18px-WIDE BREAK WAS MEASURED AT 1072-1089px:
       subgrid switches on at this query while the track count is decided by the base rule's
       repeat(auto-fit, minmax(min(100%, 14rem), 1fr)), and those two answered differently. Four 14rem
       tracks plus three --space-3 gaps need 4 x 238 + 3 x 27.51 = 1034.52px of container, and at 1072px
       of viewport the container is only 1016.98 - so auto-fit gave THREE columns, the fourth stat
       dropped to a second band, and its label sat 230.38px below the other three. The row the firm asked
       to be even, orphaned, in the one block whose whole job is evenness.
       MOVING THE BREAKPOINT WOULD NOT HAVE FIXED IT, AND THAT IS THE REASON FOR THE SHAPE OF THIS FIX.
       1034.52px of container is 1089.53px of viewport HERE, with no scrollbar; on a Windows desktop with
       a classic scrollbar it is ~1104.5px, and the file has met this before - [FACE v9]: "A VIEWPORT
       QUERY CANNOT CLOSE THAT BAND - the collapse point moves with the scrollbar". A query tuned to
       1089.53 would simply move the orphan to 1090-1104 for anyone with a scrollbar.
       SO THE TWO ARE MADE INCAPABLE OF DISAGREEING INSTEAD. Whenever the subgrid exists, the columns are
       exactly four, declared in the same block, so no width and no scrollbar can produce a band. The
       cost, stated: between 1072px and 1089.53px the track runs 233.61px rather than the derived 238px
       minimum - 1.8% under, with the badge at 155px and 78px still spare - and a label that wraps to two
       lines wraps identically in all four, because the subgrid is holding the rung. A 4.4px squeeze on a
       preferred minimum against a 230px orphan is not a close call.
       minmax(0, 1fr) AND NOT 1fr: 1fr is minmax(auto, 1fr), whose auto floor is min-content, and a track
       that cannot go below its content is how a grid overflows its container rather than shrinking. */
    .stats-row { grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-rows: auto auto auto; }
    .stats-row .stat { grid-row: span 3; grid-template-rows: subgrid; align-items: center; }
    .stats-row .stat-label, .stats-row .stat-note { align-self: start; }
  }
  .stat--linked { position: relative; }
  .stat-stretch { position: absolute; inset: 0; border-radius: var(--radius); }
  /* The ring draws on the overlay, which HAS a box - the reason this beats display: contents on a
     wrapping anchor, which has none. 6px of offset so it clears the figure it covers.
     [HOVER v22] --ink, exactly as base's, and this rule exists ONLY for the 6px: it restates the ring so
     the offset can differ, so its colour must not drift from base's or the site grows a second ring. */
  .stat-stretch:focus-visible { outline: 2px solid var(--ink); outline-offset: 6px; }
  /* [MOTION v20] THE FIGURE NOW MOVES AS WELL AS WARMING, AND THE GAP WAS AGAINST THIS SITE'S OWN
     GRAMMAR. .stat--linked is AFFORDANCE by 6's own list - it carries a stretch anchor and a pointer
     cursor - and every other affordance on the site answers with colour AND travel: the card rises, the
     button rises, the arrow slides, the nav rule extends. This one answered with colour alone, so the
     loudest-looking target in the stats row was the quietest-responding control on the page.
     THE NUDGE, UPWARD, ON THE FIGURE ONLY - never on the whole .stat. Lifting the stat would move its
     label and its note with it, and the row's whole construction (the subgrid, the baseline alignment,
     the two measured spreads recorded above) exists to hold those three lines locked to their
     neighbours'. A transform does not reflow, so nothing measured up there changes; but the LOOK of the
     alignment would, and it is the alignment that the block is spending its argument on. So the figure
     rises and the type it labels stays put, which is also the honest reading of the gesture: the numeral
     is the link, the label is the caption.
     `translate`, NOT `transform`, because .stat-num and the badge svg declare neither today and the
     shorter property leaves `transform` free for whatever the row needs next - the same separation 6.27
     and 6.29 had to make retroactively on the framed photographs. Compositor-only either way. */
  .stat--linked :is(.stat-num, .stat-label, svg) { transition: color var(--dur-fast) ease, translate var(--dur-base) var(--ease-out); }   /* colour, opacity and the one nudge */
  /* [HOVER v22] THE COLOUR HALF OF THIS RULE WENT NO-OP AND THE NUDGE WAS LEFT CARRYING THE WHOLE HOVER.
     .stat-num rests at --ink and the badge svg sets no colour at all, so it inherits --ink too; sending
     both to --ink changed no pixel's colour on today's markup. v22 kept the declaration because it states
     the end colour the way every other hover in this file states it, and leaned on the 4px rise
     [MOTION v20] had put here first to carry the gesture.
     [HOVER v23] THE NUDGE COULD NOT CARRY IT, AND THE COUNTER-EXAMPLE IS ONE SECTION UP IN THIS FILE.
     5.2 names `.stat--linked .stat-num, .stat--linked svg` in its `translate: none` list, so under
     prefers-reduced-motion the rise does not happen - and with the colour already resolving to the
     resting one, the only .stat--linked on the site answered the pointer with NOTHING for exactly the
     readers who had asked for less motion. That object is the Avvo badge: 389 x 203px, a pointer cursor,
     an outbound link, and no .stat-num at all. It is the defect [REPAIR v19] was written to fix, let back
     in through a narrower door, and "the nudge carries it" is the sentence that let it in.
     THE FIX IS .stat-label, THE ONE PART OF THIS OBJECT THAT HAD SOMEWHERE TO GO. It rests at --ink-2,
     8.44:1 on --bg, and warms to --ink at 17.73:1 - a real colour step on a text run, which 5.2 keeps
     ("colour-only responses stay ... and are not listed here"), and which needs no pseudo-element, no
     positioning and no new token. The label does NOT take the translate: the block above spends its whole
     argument on the figure rising while the type it labels stays put, and colour is not that argument -
     so the hover is split in two, colour over the three, travel over the two that were always travelling.
     FOCUS STOPS DIMMING THE BADGE. It used to send the figure to --brand: 2.99:1, a recorded failure, and
     the one state on this site where focusing a thing made it HARDER to read than leaving it alone. It
     takes the same --ink the hover takes, so the two states finally sit on one ramp; the focus INDICATOR
     is what it always was and what the old comment already said it was - .stat-stretch's 2px ring at 6px
     offset, 17.73:1 on --bg. A recorded failure leaves the file and nothing is given up for it. */
  /* [STAT v24] 2026-09-07. THE svg IS SPLIT OUT OF THAT :is() LIST AND TAKES --brand INSTEAD, on the
     firm's instruction of 2026-09-06: the badge should turn red "with mouse hover", "not just on click".
     ONE RULE, TWO BADGES, NO NEW MECHANISM - and how the FIRST badge was recoloured was checked before
     anything was added, which is the whole point. Every mark in both badges is fill="currentColor", so
     `color` on the svg repaints the entire graphic; [REPAIR v19] already established that lever and this
     rule only changes the colour it sends. The client-reviews badge added in the same pass is built by
     the same treatment, so it inherits this by being the same kind of object - no second selector, no
     class, no per-badge token.
     THE ONE MARK THAT DOES NOT FOLLOW currentColor is the knocked-out name, fill="var(--bg)" in both
     badges. It is the page's ground showing THROUGH a solid mark, so it must not follow the mark - and
     that is what keeps the name readable when the banner behind it turns red rather than white.
     FOCUS DOES NOT TAKE THE RED, and the rule below is deliberately left alone. [HOVER v23] moved focus
     off --brand precisely because it was the one state on this site where focusing a thing made it
     HARDER to read than ignoring it; handing that back would undo a repair in the same file that records
     it. Hover is transient, pointer-only and gated; focus is where a keyboard user is parked. The
     keyboard path must never be the harder one to read. */
  :where(html:not(.has-touch)) .stat--linked:hover :is(.stat-num, .stat-label) { color: var(--ink); }
  /* .stats-row IS IN THIS SELECTOR ON PURPOSE AND IT IS NOT SCOPING FOR ITS OWN SAKE - IT IS THE FIX FOR
     A COLLISION THAT WAS MEASURED, NOT REASONED ABOUT. The focus rule below reaches the svg through
     :is(.stat-num, .stat-label, svg), which :is() prices at its most specific argument - so that rule is
     0,3,0 where a bare `.stat--linked:hover svg` is only 0,2,1, and focus would win at any source order.
     THAT IS NOT AN ABSTRACT LOSS. .stat-stretch is an anchor, a MOUSE CLICK FOCUSES IT, and the badge
     opens Avvo in a new tab - so the moment a visitor uses the thing once and comes back, the stat is
     :focus-within for the rest of the visit and the red would never appear again. Measured on the built
     page with a real pointer: rest --ink, hover --brand, focus --ink, focus + hover --ink. The last of
     those four is the defect, and it is precisely the complaint the firm made ("with mouse hover, it
     should turn red, not just on click") arriving from the other side.
     0,3,1 BEATS 0,3,0 IN EVERY ORDER, which is why an ancestor was added rather than the rules being
     shuffled: source order is the kind of guard that a later insertion silently breaks. .stats-row is a
     real, always-present ancestor of every .stat--linked on the site rather than a hook invented to win
     a fight, and if a linked stat is ever built outside a stats row it loses the hover rather than the
     focus colour, which is the safe direction to fail in.
     THE FOCUS RULE IS NOT WEAKENED TO MAKE ROOM. Dropping svg from its :is() list would have worked
     equally well - the badge rests at --ink and inherits it anyway, so that declaration is already a
     no-op - but it is half of [REPAIR v19], and unpicking a documented repair to win a specificity
     argument is how repairs get lost. It stays; hover simply outranks it.
     WHAT THE KEYBOARD KEEPS: everything that matters. The focus INDICATOR is .stat-stretch's 2px --ink
     ring, which no rule here touches, and a keyboard user who is not also pointing at the badge still
     gets --ink on the graphic. Only the both-at-once case changed. */
  :where(html:not(.has-touch)) .stats-row .stat--linked:hover svg { color: var(--brand); }   /* [ALIGN v26] 2026-09-07 CORRECTS THIS ACCOUNTING, NOT THE RULE - the rule is the firm's, of 2026-09-06, and stays. It read "NON-TEXT ... the knocked-out name ... 2.99:1", which was wrong three ways: (a) THREE OF THE FOUR THINGS THIS RULE REPAINTS ARE TEXT, two of them far under 18.66px, so 1.4.11's 3:1 is not the floor that applies to them; (b) 2.99 is the --bg figure and this row ships inside .section--alt, on --bg-2; (c) the client-reviews badge landed in the same pass with two text runs of its own and nothing recorded them. ALL FOUR, ON THE GROUND THEY RENDER ON: the marks - shield, circle, laurels, stars - are non-text, floor 3:1, and measure 2.86:1, short by 0.14; "9.6" renders 36.60px/600, large text, floor 3:1, same 2.86:1, short by 0.14; "Reviews" at 11.26px/600 and "out of 7 reviews" at 7.32px/600 are NORMAL text, floor 4.5:1, still 2.86:1, short by 1.64; and the knocked-out name at 7.32px/600 is --bg on the mark's own red against that same 4.5:1, 2.99:1, short by 1.51. AT REST none of it fails: the name is --bg on --ink at 17.73:1 and every other mark is --ink on --bg-2 at 17.00:1. Transient, pointer-only, carrying nothing the resting badge does not. Recorded failure, on the firm's decision of 2026-09-06 */
  :where(html:not(.has-touch)) .stat--linked:hover :is(.stat-num, svg) { translate: 0 calc(-1 * var(--nudge)); }
  /* [STAT v24] 2026-09-07. THE TWO STAT NUMERALS ANSWER THE POINTER, AND THIS RULE DELIBERATELY
     CONTRADICTS THE ONE THREE LINES ABOVE IT. Do not reconcile them.
       - 2026-09-06, the pink instruction: every hover on the site goes to --ink. [HOVER v22] / v23 are
         that pass, and the rule above is its local form. It stands everywhere else, this row's own badge
         labels included.
       - 2026-09-06, the stat-row note: "when a mouse hovers the 13+ it should have an interaction that
         slightly enlarges it and turns the 13 red too", and the same for the 3. Worked 2026-09-07.
     BOTH ARE RIGHT BECAUSE THEY ANSWER DIFFERENT QUESTIONS. The first asks "what colour is a hover once
     pink is gone" and the answer is a site-wide default. The second asks "what should the two loudest
     figures on this page do under the pointer" and the answer is specific to two elements the firm named
     by name. A later, narrower instruction wins on its own two elements and nowhere else.
     THEY CANNOT BOTH FIRE, AND THE CASCADE SETTLES IT IN THE RIGHT DIRECTION WITHOUT A :not(). Today
     every .stat-num on the site is in a plain .stat and no .stat--linked has one, so the two selectors
     do not overlap at all. Should a linked stat ever grow a numeral, `.stat--linked:hover .stat-num` is
     0,3,0 against this rule's 0,2,0 and the white rule keeps its own territory automatically. The
     specificity is the guard; it is written down so nobody "tidies" it away.
     THIS IS A RESPONSE, NOT AN AFFORDANCE, which is why it breaks none of section 6's rules about hover.
     A plain .stat carries no link, no pointer cursor and nothing to click, so there is no promise being
     made and nothing to keep - and it needs no :focus twin because there is nothing focusable to give
     one to. That is also why the pointer gate is the whole of its protection: a touch device never
     reaches it, so no reader ever gets a red figure they cannot dismiss.
     THE ENLARGEMENT IS `scale: var(--pop)`, derived in the tokens block against 6.18's hop threshold the
     way --nudge was, and it is subtracted under prefers-reduced-motion in 5.2 while the colour stays. */
  :where(html:not(.has-touch)) .stat-num:hover { color: var(--brand); scale: var(--pop); }   /* TEXT, and at --text-4 it is LARGE text: --brand misses the 3:1 large-text floor by 0.01 on --bg and by 0.14 on --bg-2, the ground this row ships on. Taken because the RESTING figure is --ink at 17.73:1 on --bg and 17.00:1 on --bg-2 - fully legible whenever anybody is READING it - and the red is a transient pointer response carrying no information the resting state does not. Recorded failure, on the firm's decision of 2026-09-06 */
  .stat--linked:focus-within :is(.stat-num, .stat-label, svg) { color: var(--ink); }   /* the hover's colour half, outside the pointer gate. 8.44:1 -> 17.73:1 on the label; a no-op on the numeral and the badge, which rest at --ink. The focus INDICATOR is .stat-stretch's ring above, not this */
  /* [REPAIR v19] THE svg HALF OF THOSE THREE RULES WAS ADDED 2026-09-06, BECAUSE THE ONE LINKED STAT ON
     THE SITE HAD STOPPED HAVING A .stat-num. They were written when every stat's figure was a numeral.
     The firm's instruction of 2026-09-06 turned the third slot into the Avvo badge, and the badge slot
     has NO .stat-num - so from that moment the only .stat--linked on the site carried a pointer cursor
     over a 389.66 x 202.88px target and answered the pointer with NOTHING. Measured with a real
     Input.dispatchMouseEvent move rather than a forced pseudo-state: svg colour, stat colour and label
     colour were byte-identical resting and hovered. A cursor that promises a click while the surface
     answers nothing is the exact affordance defect this section's own preamble is written against.
     COLOUR WAS THE RIGHT LEVER AND NEEDED NO NEW TOKEN. Every mark in the badge is fill="currentColor"
     (the page's own comment derives why), so `color` on the svg repaints the whole graphic; the single
     exception, the knocked-out name at fill="var(--bg)", stays dark on whatever the banner becomes -
     which was legible on --brand-bright and is legible on --ink at 17.73:1. The badge therefore answered
     the pointer with exactly what the numeral beside it answered with - same token, same duration, same
     gate - and the row kept ONE hover language instead of two. The alternative tried and rejected was an
     opacity fade: it works, but nothing else on the site fades on hover, so it would have taught a
     second vocabulary for one element.
     [HOVER v22] THE PAST TENSE IN THAT PARAGRAPH IS THE POINT. The lever this repair chose is the one
     the firm's pink instruction took away: with the hover colour now --ink, and both the numeral and the
     svg resting at --ink, the colour half of the repair resolves to nothing on today's markup. THE
     REPAIR ITSELF IS NOT UNDONE - it survives through its OTHER half, the :is(.stat-num, svg) reach,
     which is what lets [MOTION v20]'s nudge land on a badge that has no numeral. The control still
     answers the pointer; it answers with 4px of travel instead of a colour. And the opacity fade stays
     rejected for the reason it was rejected: one element must not teach a second vocabulary.
     FOCUS STAYS QUIETER THAN HOVER, per this section's preamble - and since [HOVER v22] it is quieter in
     the other direction as well: focus DIMS the figure to --brand where hover LIFTS it, rather than both
     warming by different amounts along one ramp. The actual focus INDICATOR is still the 2px ring on
     .stat-stretch above, which is why the dimmer graphic costs nothing there. :is() rather than a second
     pair of rules, so a hover and its focus twin cannot drift apart. */
  /* [ALIGN v26] 2026-09-07. THE NARROW ROW STACKS - ALL FOUR OF IT, THE SAME WAY. THIS SUPERSEDES
     [STAT v18]'s "RESTRUCTURE, DON'T STACK" AND ITS THREE :has() RULES, AND THE OLD ARGUMENT IS QUOTED
     HERE BECAUSE IT WAS RIGHT FOR THE ROW IT WAS WRITTEN FOR AND IS NOT RIGHT FOR THIS ONE.
     WHAT IT SAID: "one column plus text-align: center would change the alignment axis relative to the
     section head directly above and roughly double the height. Number left, label right, one row per
     stat - keeps the left axis, keeps the figures full size, halves the height." Then v18 met a badge
     that would not fit a minmax(5.5rem, auto) column without rendering its name at 4.6px, and let that
     ONE slot stack while the other two stayed two-column.
     WHY IT NO LONGER HOLDS, ON ITS OWN TERMS AND ON MEASUREMENTS:
       1. THE AXIS IT PROTECTS IS THE ONE THAT BROKE. The firm asked for a FOURTH stat on 2026-09-06, so
          the row is two numerals and two badges - half of it stacking, half of it not. Measured at 390:
          labels at x 142.63, 142.63, 21.63, 21.63. A 121px ragged left edge down the middle of a row
          the firm asked to be even, and the exception is now half the row rather than one slot in three.
       2. THE HEIGHT OBJECTION WAS TO STACKING *PLUS text-align: center*, AND CENTRING IS THE HALF THAT
          MOVED THE AXIS. Stacked and left-aligned, every part of every stat sits on 21.63px - the
          section head's own axis - so the thing v18 was defending is better served by stacking than by
          the two columns, not worse.
       3. AND IT NO LONGER COSTS THE HEIGHT. Two things changed under it: the trimmed line box above
          gives the figure a 29.81px box instead of 51.18px, and a stacked label gets the full 346.75px
          of column at 390 instead of 225.75px, so the copy that used to wrap stops wrapping. MEASURED AT
          390, THE TWO NUMERAL STATS: 136.91 + 114.47 = 251.38px two-column, 129.69 + 129.69 = 259.38px
          stacked. EIGHT PIXELS, not the doubling the old note feared - and the taller stat is the one
          that got SHORTER. The row itself goes 673 -> 746.88px, and 65.88 of that 73.88 is the two new
          .stat-notes the level bottom edge required, which the two-column form would have had to pay for
          just the same. Stacking, on its own, costs 8px across the whole row.
     SO THE WHOLE BLOCK IS THREE DECLARATIONS. The base .stat above is ALREADY a one-column grid with
     gap: var(--space-1) - the desktop stack - so a narrow stat needs nothing said about its internals,
     and every grid-column / grid-row / :has() rule that used to live here is deleted rather than
     neutralised. padding-block is the one addition: the base sets padding-top only, and a stacked row
     of four needs breathing room under the last note as well as over the first figure.
     ONE COLUMN AND NOT TWO-BY-TWO BETWEEN 480px AND 1072px, DERIVED RATHER THAN DEFAULTED: a 2 x 2 band
     would be a better SHAPE at ~900px, but the rung alignment this pass exists to deliver comes from
     grid-template-rows: subgrid, which is declared only at >= 67rem. Two bands below that breakpoint
     would size their rungs independently and the second band's labels would not meet the first band's -
     trading a tall row for a new ragged edge, which is the defect, not the fix. */
  @media (max-width: 66.99rem) {
    .stats-row { grid-template-columns: 1fr; gap: var(--space-2); }
    .stats-row .stat { padding-block: var(--space-2); }
  }

  /* ---- 6.13 PACKAGES - MATERIAL ------------------------------------------------------------------
     A priced plate with its own call to action inside it. The plate is not clickable, so no lift and
     no brand edge; the .btn inside carries the affordance. The price is a FIGURE, not display type -
     Montserrat with tabular lining numerals so a column of prices aligns; only the h3 is a serif hook. */
  /* [MOTION v20] THE EDGE TRANSITIONS NOW. :focus-within below has always changed border-color and the
     change was always INSTANT - the plate's edge snapped from --line to --line-strong the moment the .btn
     inside took focus, while the byte-identical change on .svc-card two blocks up faded over --dur-base.
     Two plates, one gesture, two behaviours, and the one that snapped was the one a keyboard user drives.
     border-color only: MATERIAL means no lift and no pointer cursor (6's preamble), and nothing here
     changes that - this is the existing state change being served properly rather than a new state. */
  .package { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); transition: border-color var(--dur-base) ease; }
  /* HALF-WIDTH SOLO PACKAGE, added 2026-09-08 on the firm's instruction: "Reduce the width of the box
     around the 'For Sale by Owner' card to half, and do the same for the 'Will & Trust' card."
     WHY THOSE TWO AND NOT THE OTHER FOUR: they are the only packages ALONE in their .package-grid.
     #business-and-employment holds three and they share the row at 389.66px each; #real-estate and
     #estate-planning hold one apiece, and auto-fit collapses the empty tracks to 0px so the lone card
     takes the whole 1224px container. Measured before the change: both were 1224.00px, the full
     container, against a trio of 389.66px on the same page. Half of 1224 is 612.
     WHY IT IS NOT A BARE 50%. On a phone the container is about 335px and half of that is ~167px, which
     is not a card - it is a column of broken words, and these boxes carry a heading, a price and a list.
     `max(50%, ...)` puts a floor under it, and the floor is --grid-track (22rem = 374px), which is the
     width the package grid already treats as the narrowest a package may be. The inner min(100%, ...)
     keeps the floor from exceeding the container on a phone, where the card should simply be full width.
     SO IT READS: half the container wherever half is at least a normal package track; a normal package
     track through the middle sizes; the whole container once even that will not fit. Crossover is a
     748px container - above it the 50% wins, below it the track floor does.
     LEFT-ALIGNED, NOT CENTRED, because an explicit width overrides the grid item's default
     justify-self: stretch and resolves to the inline start - which is the container's left edge, the
     same edge the section heading and every other card on the page begin on. */
  .package--half { width: max(50%, min(100%, var(--grid-track))); }
  .package h3 { font-size: var(--text-2); line-height: var(--leading-heading); }
  /* [REPAIR v10] --measure IS LOAD-BEARING HERE, and its absence was a real defect rather than a missing
     nicety. .package-grid is `repeat(auto-fit, minmax(min(100%, --grid-track), 1fr))`, so a grid holding
     ONE card resolves that card to the full 1224px container - which /flat-fee-services does twice, at
     #for-sale-by-owner-package and #will-and-trust-plan, and once more per card in #bundled-packages.
     Measured at 1440 before this line existed: the body <p> in those cards ran 1167px = 139.9 characters
     at 8.3422px per character, nearly twice the top of the 60-75 band this file spends the whole measure
     derivation defending. The tell was inside the same card: .check-list two elements below it already
     carries max-width: var(--measure) (6.7) and was setting at 583.94px, so one card was running its
     prose at 140 characters and its bullets at 70. Capping the RUNNING TEXT and not the plate is
     deliberate and is what every other component here does - the card keeps the grid's width, the line
     keeps the measure. Narrow cards are unaffected: at the 22rem track the inner box is 316.99px and
     this cap never binds. */
  .package p { max-width: var(--measure); color: var(--ink-2); }
  .package .btn { margin-top: auto; align-self: start; }
  .package-price { font-size: var(--text-2); font-weight: var(--wt-strong); line-height: var(--leading-heading); font-variant-numeric: lining-nums tabular-nums; color: var(--ink); }
  .package-price small { display: inline-block; margin-left: var(--space-1); font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-4); text-transform: uppercase; color: var(--ink-2); }
  .package-note { font-size: var(--text-label); line-height: var(--leading-body); color: var(--ink-2); }
  .package:focus-within { border-color: var(--line-strong); }

  /* ---- 6.14 QUOTE --------------------------------------------------------------------------------
     The signature bar is authored FINISHED and knocked back only under the gate; its 0.27s lead
     (0.18 x 1.5, the same measured brand as every other duration - [MOTION v20] moved the multiplier,
     and this literal moves with it or it stops being on the ladder) is a fixed intra-object lag, not
     stagger. .quote is a granted serif hook but never takes text-wrap: balance - a citation's break
     points matter legally, not visually. --brand-deep carries no information at 2px, so its 2.99:1 is
     the decorative-mark case the token's own comment allows - and since [RED v7] that bar is the mark's
     literal #BF0000. [RED v8] It stopped being "the one place on the site that paints the firm's own red
     at full strength" the moment every other red joined it, so that argument for keeping the component
     is gone; the arguments in the paragraph below are not, and they are the ones that matter.
     NO PAGE SHIPS .quote-card TODAY (counted across all 20 built pages, 2026-09-05: 0), so this is the one
     component the CSS/JS pair cannot be validated against by looking at a page - which is exactly why
     it must not be half-deleted. It is named in FOUR places that move together: this rule and its
     ::before, the print break-inside list (6.25), guards 7.4's PLATE selector list, and main.js's
     CHOREO PLATE row, which 7.4 requires to stay byte-identical. Use it (a pull-quote on /attorneys or
     /firm-overview is its obvious home) or delete all four in one commit; never one of them. */
  .quote-card { position: relative; padding: var(--space-4) var(--space-3) var(--space-3); background: var(--bg-3); border: 1px solid var(--line); border-radius: var(--radius); }
  .quote-card::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 2px; background: var(--brand-deep); transform-origin: left; transition: transform var(--dur-slow) var(--ease-out) calc(var(--reveal-delay, 0s) + 0.27s); }
  html.js .quote-card[data-reveal]:not(.is-revealed)::before { transform: scaleX(0); }
  .quote { font-size: var(--text-2); line-height: var(--leading-heading); color: var(--ink); }
  .quote-attrib { margin-top: var(--space-2); font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-4); text-transform: uppercase; color: var(--ink-2); }

  /* ---- 6.15 CLOSING CTA BAND ---------------------------------------------------------------------
     The terminal stop is the EXACT --bg-0 the footer paints, so the band is a recess cut into one
     continuous surface rather than a rectangle pasted above a black box. It is not a .section, so it
     carries its own padding AND its own small-screen steps - the omission that made the source site's
     closing band more than half padding on a phone.
     [RED v8] THE MIDPOINT TINT IS GONE, on the firm's instruction of 2026-09-06. The gradient ran
     --bg 0% -> --bg-2 46% -> --bg-0 100%, which LIGHTENED through the middle of the band before falling
     to the footer's sub-ground; with the home page's contact section deleted the band now follows a
     .section--alt band that is itself --bg-2, and the lift read as a continuation of that plate rather
     than as a recess. The firm asked for this band to be black, so the ramp is monotonic now:
     --bg 0% -> --bg-0 100%, darkening the whole way to the footer stop. Both ends are unchanged, so the
     seam with the section above and the seam with the footer below are exactly as they were - only the
     middle moved, and it moved DOWN. There is no third stop to keep in sync with anything. */
  .cta-band { padding-block: var(--space-6); border-top: 1px solid var(--line); background: linear-gradient(to bottom, var(--bg) 0%, var(--bg-0) 100%); text-align: center; }
  .cta-inner > * + * { margin-top: var(--space-3); }
  .cta-inner .display, .cta-inner .lede { margin-inline: auto; }
  .cta-inner .hero-actions { justify-content: center; }
  @media (max-width: 29.99rem) { .cta-band { padding-block: var(--space-5); } }
  @media (max-height: 480px) and (orientation: landscape) { .cta-band { padding-block: var(--space-4); } }

  /* ---- 6.15a CLOSING CTA BAND, PHOTOGRAPHIC VARIANT (.cta-band--media) --------------------------
     THE REQUEST, AND WHAT IT IS NOT. The firm sent the closing band from their sister site
     landcatchers.com and asked for "buttons and image in background". Everything the reference does
     with structure this file ALREADY OWNS and this sub-block reuses rather than reissues:
     .section-head--center .eyebrow (6.4) draws the centred rule on BOTH sides, each half scaleX(0)
     until the reveal; .cta-inner .hero-actions centres the pair of buttons; .cta-band above owns the
     padding, the hairline and the seam arithmetic. What is genuinely new here is exactly three things -
     a frame, a picture inside it, and a scrim over it - so that is all this block contains.
     A MODIFIER CLASS, AND THE MODIFIER IS THE POINT. /contact-us ALREADY uses contact-*.jpg as its own
     hero. If the photograph were attached to .cta-band itself, that page would open and close on one
     picture the day it gains a closing band. (THIS SENTENCE CARRIED A FACTUAL ERROR FROM THE DAY IT WAS
     WRITTEN, corrected 2026-09-07 rather than deleted, because the argument is sound and only its census
     was wrong: it named /make-a-payment beside /contact-us, and /make-a-payment's hero is
     make-a-payment-*.jpg, not contact-*.jpg - verified in src/pages/. The whole of the argument rests on
     /contact-us alone and is unweakened by losing the second page. Note also that NEITHER of those two
     pages carries a .cta-band at all today, so the collision the modifier prevents is latent rather than
     live - which is a reason to keep the gate, not to relax it: a closing band added to /contact-us
     later must not silently arrive photographic.)
     WHAT THE CLASS GATES SINCE 2026-09-07, AND IT IS NO LONGER ONE PAGE. The firm asked that the home
     page's closing photograph be used "identically" on the practice area pages, and chose the set: the
     six practice areas, their five sub-pages, and /firm-overview, which the nav labels "All Practice
     Areas". So the modifier now rides THIRTEEN bands - the home page's and those twelve - and the gate
     is still doing the whole of its work. Fifteen pages carry a .cta-band; the two it deliberately
     withholds the picture from are /attorneys, a bio page, and /flat-fee-services, pricing - neither is
     a practice area, and neither was in what the firm asked for. Styling the base class would take both
     of those in as a side effect of a decision nobody made about them, and would take /contact-us in
     the moment that page closes with a band. THAT is why this stays a modifier and not a style switch
     someone may reuse: what it gates has grown, what it is for has not.
     WHY contact-*.jpg AND NOT THE OTHER TWO CANDIDATES. hero-capitol-*.jpg and the new hero video's
     poster are both the TOP of this same page; either one at the foot would print one picture twice on
     a single scroll. contact-*.jpg is unused on the home page and carries the subject the band is
     actually about. 1600x774, four widths already built, sizes="100vw", loading="lazy" - not the hero's
     fetchpriority, because this is the last thing on the page.
     THE TERMINAL STOP IS OPAQUE var(--bg-0) AND THAT IS LOAD-BEARING. 6.15's note above is the whole
     argument: the band is a recess cut into one continuous surface, not a rectangle pasted above a
     black box, and it is the EXACT footer colour at 100% that makes the seam disappear. A photograph
     cannot be allowed to reopen that seam, so the ramp still ends at the footer's own ground with no
     alpha left in it - the picture is fully extinguished before the band ends.
     THE SCRIM IS DERIVED, NOT CHOSEN, and it holds the ink against a PURE WHITE pixel, because that is
     the worst case an unknown photograph can put under any glyph. Composite per channel on --bg's own
     (14, 12, 11): a x (14, 12, 11) + (1 - a) x 255 - the same model every scrim figure in this file
     uses (6.3). The pointer light (.cursor-glow, z-index 70) sits ABOVE this band, and at rest the two
     lobes converge, so the honest reading state is screen(screen(x, lead), trail) applied to the TEXT
     and the GROUND alike. Both columns are recorded; the composed one is the one that binds.
                                          bare scrim              with both lobes screened (at rest)
       0%    rgba(--bg-rgb, 0.94)     --ink 15.66  --ink-2 7.46    --ink 12.46  --ink-2 6.34
       30%   rgba(--bg-rgb, 0.88)     --ink 13.13  --ink-2 6.25    --ink 10.28  --ink-2 5.23   <- lightest
       62%   rgba(--bg-rgb, 0.90)     --ink 13.99  --ink-2 6.66    --ink 10.98  --ink-2 5.59
       100%  var(--bg-0), opaque      --ink 18.26  --ink-2 8.69    --ink 15.47  --ink-2 7.87
     WORST PAIR ANYWHERE: --ink-2 5.23:1 at the 30% stop, where the photograph is allowed to read.
     Clears AA with 0.73 to spare, and every other cell clears it by more. (The brief this block was
     written from carried a slightly kinder table - 11.34 / 5.63 at the 30% stop - which lands between
     the two columns above; the measured pair is recorded instead, and the conservative column is the
     one the decision rests on.)
     HEAVIER THAN THE HERO'S SCRIM ON PURPOSE. The hero puts only its title in the light 0.36 window;
     here EVERY line - eyebrow, display, lede, both buttons - sits over the photograph, so there is no
     window to give away.
     RED IS NOT USED AS TEXT HERE, AND THAT IS A MEASURED DEPARTURE FROM THE REFERENCE. The reference
     site puts its accent colour on the eyebrow. --brand over this scrim measures 2.64 / 2.21 / 2.36 /
     3.08 bare and 2.31 / 1.90 / 2.03 / 2.87 composed - failing at every stop, at every size, with no
     alpha that fixes it (at a fully opaque veil the ground IS --bg and the pair is still 2.99:1). So
     the eyebrow takes --ink, exactly as .hero .eyebrow already does and for exactly the same reason.
     TWO FAILURE MODES, both benign. If the photograph never loads, 6.15's original gradient is still
     painted underneath and the band reads as it does today. If `isolation` were dropped, the negative
     z-indices would escape the band and paint behind the section entirely - which is why it is here.
     isolation: isolate creates the stacking context that keeps -2 and -1 BEHIND the band's content and
     IN FRONT of the band's own background. */
  .cta-band--media { position: relative; overflow: hidden; isolation: isolate; }
  /* THE EYEBROW IS --ink HERE, AND THIS LINE IS THE ONE THE DERIVATION ABOVE ACTUALLY BUYS. 6.4 sets
     .eyebrow to var(--brand) and that is a site-wide RECORDED failure at 2.99:1 on --bg, accepted on the
     firm's instruction; over a photograph it is not the same failure. Measured on this band's own scrim
     the pair is 2.64 / 2.21 / 2.36 / 3.08 bare and 2.31 / 1.90 / 2.03 / 2.87 with the pointer light -
     roughly a third worse than the figure that was accepted, on the one element of this band that is
     small, tracked and set at 600. .hero .eyebrow already takes --ink for exactly this reason and this
     is the same rule applied to the same problem; the reference site's accent-coloured eyebrow is the
     one thing in that screenshot TWM's palette cannot copy, and that is the measured departure.
     SCOPED TO THE MODIFIER, not to .cta-band, because it is the PHOTOGRAPH that forces it. A plain
     closing band has no picture under it and would keep 6.4's --brand and 6.4's own recorded failure.
     Today only the home page carries an eyebrow in this band at all, so the two never disagree in
     shipped markup - but the scope is the argument, not the census. */
  .cta-band--media .eyebrow { color: var(--ink); }
  .cta-band--media .cta-media { position: absolute; inset: 0; z-index: -2; }
  .cta-band--media .cta-media img { width: 100%; height: 100%; object-fit: cover; }
  .cta-band--media::after {
    content: ""; position: absolute; inset: 0; z-index: -1;
    background: linear-gradient(to bottom, rgba(var(--bg-rgb), 0.94) 0%,
      rgba(var(--bg-rgb), 0.88) 30%,   /* the window where the photograph reads */
      rgba(var(--bg-rgb), 0.90) 62%, var(--bg-0) 100%);
  }
  /* THE PHONE STEP IS GONE - REVERSED BY THE FIRM ON 2026-09-08, AND IT WAS NEVER A DEFECT. A
     `@media (max-width: 29.99rem)` block stood here and took this band's photograph AND its scrim away
     on every phone in portrait. That was a deliberate step, argued for at length; the firm has
     overruled it, which is theirs to do. The argument is kept below rather than deleted, so what was
     traded away stays visible to whoever reads this next.
     THE INSTRUCTION, VERBATIM: "the hero image for the 'Talk to an Attorney Who Has Litigated, Sold and
     Built.' ribbon on every page disappears in mobile. Fix. It should still show in mobile."
     WHAT WAS DELETED, both declarations of it:
       @media (max-width: 29.99rem) { .cta-band--media .cta-media { display: none; }
                                      .cta-band--media::after     { content: none; } }
     BOTH TOGETHER, and that is the one part of this reversal with no room in it: ::after IS the veil the
     ink sits on, so restoring the photograph alone would put white display type straight onto a
     photograph - the exact failure the derivation above exists to prevent. 29.99rem resolved against the
     browser's initial 16px, so the step fired below 479.84px; measured, the picture was hidden at 390,
     360 and 414 and already visible at 480.
     THE ARGUMENT THAT PRODUCED IT, QUOTED IN FULL because it is the record of the trade: "THE PHONE STEP
     TAKES THE PICTURE AND THE VEIL AWAY TOGETHER, and 29.99rem is not a new breakpoint: it is the step
     6.15 itself already takes one line above, so the band has ONE small-screen story rather than two
     that can drift. Removing only the photograph would leave a second scrim stacked on the first,
     darkening the gradient in exchange for nothing; removing only the scrim would put copy on bare
     photograph. So both go, and the band falls back to exactly the gradient it ships today.
     `display: none` rather than a positioning undo, because a lazy image inside a display: none subtree
     never intersects the viewport and is never fetched - the phone pays nothing at all. THE
     LANDSCAPE-PHONE QUERY IS DELIBERATELY NOT JOINED. At 812 x 375 the band is 812px wide, the
     photograph reads across the whole of it, and the argument above (a picture too small to see) simply
     does not apply; only the vertical padding needs to step, which 6.15 already does."
     WHAT HAPPENS INSTEAD, at every width and with no query of its own: the band is exactly what it is on
     a desktop - .cta-band's own gradient underneath, the photograph over it at z-index -2, and the
     ::after scrim over that. The step was reaching for ONE small-screen story; there is now no
     small-screen story at all, which is the same end approached from the other side. 6.15's SEPARATE
     29.99rem step is untouched and still steps this band's vertical padding, the landscape-phone query
     is untouched, and no motion is switched on by the change: 6.27's drift and 6.28's settle are both
     gated at 67rem, so a phone gets a still picture under a static veil.
     NO NEW RATIO IS WRITTEN, and that is construction rather than omission. The stops above are solved
     against a PURE WHITE pixel with both pointer-glow lobes screened - the worst case ANY photograph can
     put under any glyph - so they do not care which photograph is behind them or how much of it shows.
     Every figure in the table holds at 390 exactly as it held at 1440, and verify_ratios.py's existing
     rows stand untouched. Checked rather than assumed: on the shipped build, with .cta-inner hidden by
     `visibility` so nothing reflowed and the ground read pixel by pixel under each element's own box,
     the worst pair on a phone is the lede at 6.68 (360) and 6.93 (390 and 414), and the eyebrow, the
     display, and both buttons all sit above 14. Those are measurements of THIS photograph with no
     pointer on the band, not a replacement for the table's 5.23 floor, which assumes a white pixel and a
     converged glow; they sit above it, which is what a real photograph under a derived veil should do.
     WHAT IT COSTS, MEASURED. contact-*.jpg ships four rungs - 480w 11.4 KB, 800w 22.6 KB, 1200w 39.3 KB,
     1600w 58.7 KB - and with sizes="100vw" a 390px phone at DPR 2 takes the 800w rung, 22.6 KB (observed
     on the wire: contact-800.jpg, 200, 22.9 KB). At DPR 3 it takes the 1200w, 39.3 KB. Beside the 2.4 MB
     portrait hero rendition the same phone already fetches, "the phone pays nothing at all" was true and
     was defending very little.
     THE CROP, MEASURED AND THEN ACTUALLY LOOKED AT. The band is 390x514 on a 390px phone and the
     photograph is 1600x774, so object-fit: cover shows a 587x774 slice - 36.7% of the frame's width. At
     414 it is 39.0%; at 360 the display wraps to a third line, the band grows to 360x580, and the slice
     narrows to 30.0%. Numbers that small are an invitation to assert, so the band was screenshotted at
     360, 390 and 414, and captured a second time with the copy hidden, and the picture was looked at. IT
     READS AS A PHOTOGRAPH: three pinned notes carrying a telephone handset, an "@" and a mobile phone.
     The subject sits in the middle of the frame, so a centre crop keeps the "@" note whole at all three
     widths and the two flanking notes are what the narrowing eats - down to slivers at 360. Dark, which
     is the scrim doing its job; not the unrecognisable smear a 30% slice invites. So NO `object-position`
     is set here. The lever exists if the firm ever wants the flanking notes kept, and it is left as an
     offer rather than applied unasked. */

  /* ---- 6.16 CONTACT CARD -------------------------------------------------------------------------
     <address> defaults to italic; the italic belongs to the serif and is reserved for citation. The
     links carry a RESTING underline, not a hover-only one: on touch :hover never fires, and an
     underline is the only thing that marks them as links there - so it is drawn in --line-control, the
     rung solved for an edge that carries a control on its own. */
  .contact-card { display: grid; gap: var(--space-2); max-width: var(--measure); padding: var(--space-3); background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); font-style: normal; transition: border-color var(--dur-base) ease; }
  /* [MOTION v20] THE THIRD MATERIAL PLATE JOINS THE OTHER TWO. .svc-card and .package both answer
     :focus-within with --line-strong on the edge; .contact-card is the third plate on --bg-2 with
     focusable children (its phone, fax and e-mail links are the whole of its content) and it answered
     nothing at all - a keyboard user tabbing into it got the base ring on the link and no signal from
     the surface holding it, where the same tab into a .package warms the plate.
     THE EDGE AND NOTHING ELSE, because MATERIAL is a rule about the SURFACE and not about the register's
     loudness: no lift, no accent, no cursor - the same three declarations .package is allowed and the same
     three .svc-card is denied on focus. --line-strong is already this file's :focus-within edge on both of
     the others, so no colour is introduced and no floor moves. */
  .contact-card:focus-within { border-color: var(--line-strong); }
  /* [REPAIR v10] ONE UNBREAKABLE TOKEN, AND IT IS NOT A UTILITY CLASS. "Suite 220-136" contains a
     hyphen, and a hyphen is a break opportunity, so the footer's narrow address column was breaking the
     suite number in half: measured at 1440 the line set as "2222 Francisco Drive, Suite 220-" (265.5px)
     with a 26.1px second line carrying "136" alone. A suite designator is one token and reads as a
     wrong number when it is split.
     white-space, not U+2011: a non-breaking hyphen would render identically and would also travel into
     anyone's clipboard, so an address pasted into a form would carry a character most validators
     reject. The plain ASCII hyphen stays in the text; only the LINE BREAKING is suppressed.
     SCOPED TO THE SUITE, NOT THE LINE. "2222 Francisco Drive, Suite 220-136" is 291.6px unbroken
     against ~261.75px of footer column at 320px, so making the whole line nowrap would overflow;
     "Suite 220-136" is ~100px and simply moves the break to the comma after "Drive". Named rather than
     generic (.nowrap would invite a second, unrelated use and then a third) - this is the address's
     own rule and lives with the address. */
  .addr-unit { white-space: nowrap; }
  /* [REPAIR v10] minmax(0, 1fr) AND NOT 1fr, and the two words are the other half of the reflow fix
     below. `1fr` is shorthand for `minmax(auto, 1fr)`, and that `auto` MINIMUM is the value track: it
     floors the column at its own min-content and then the grid refuses to be narrower than the sum.
     Measured at 320px on the served page: tracks resolved to `64.78px 153.41px`, so this list's
     min-content was 245.69px inside a 204.74px card - and because .contact-card is itself a grid with
     an auto column, that floor propagated up and made the CARD's children paint 315px wide. With
     minmax(0, 1fr) the same measurement gives 92.28px and nothing is floored; at every width where
     there is room the fr track still takes all of it, so this is invisible above the phone band.
     The pair matters: minmax(0, 1fr) stops the grid forcing the card wide, and the mailto rule below
     stops the e-mail spilling out of the narrower track it now gets. Either alone leaves a defect. */
  .contact-list { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-1) var(--space-3); }
  .contact-list dt { font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-4); text-transform: uppercase; color: var(--ink-2); }
  .contact-list a { color: var(--brand); text-decoration: underline; text-decoration-color: var(--line-control); text-decoration-thickness: 1px; text-underline-offset: 0.18em; transition: color var(--dur-fast) ease, text-decoration-color var(--dur-fast) ease; }   /* TEXT, so the text stop. 2.86:1 on the card's --bg-2 at 17px - BELOW AA. Recorded failure; the resting underline is what marks these as links */
  /* [MOTION v20] COLOUR AND THE UNDERLINE, AND NO NUDGE - A RECORDED NON-CANDIDATE, NOT AN OVERSIGHT.
     These are AFFORDANCE and the footer's links took the 4px lateral nudge in this same pass, so the
     question was asked here and answered the other way: .contact-list is a two-column <dl> whose left
     edge IS the card's construction (6.16's own minmax(0, 1fr) derivation is about nothing else), and a
     value that slid sideways under a fixed label would break the one alignment the block spends its
     argument defending. The footer's links sit in single-column lists with nothing to the left of them,
     which is why the same gesture is right there and wrong here. */
  /* [HOVER v22] --ink, and this is one of the rows the scheme actually rescues: the phone, e-mail and
     address links rest at --brand's 2.86:1 on the card's --bg-2 - a recorded AA failure - and the hover
     takes them to 17.00:1. `text-decoration-color: currentColor` is unchanged and now does MORE work
     than it did: the underline goes from --line-control to the same white as the type, so the whole link
     resolves into one colour instead of warming toward a second one. */
  :where(html:not(.has-touch)) .contact-list a:hover { color: var(--ink); text-decoration-color: currentColor; }
  .contact-list a:focus-visible { color: var(--ink); text-decoration-color: currentColor; }   /* [HOVER v23] colour parity - see 6.22 */
  /* [REPAIR v10] THIS ONE LINE IS A 1.4.10 REFLOW REPAIR, and it is the only WCAG failure this pass
     found that was not already on the record. Measured on the served /contact-us at a 320px viewport
     (305px of client): the card's content box is 204.74px, and .contact-list's MIN-CONTENT is
     314.98px - dt 64.78 ("PHONE", the widest label) + --space-3 27.51 column gap + 222.69px for the
     dd column, all of which is the e-mail. "Info@TWMLegal.com" carries no break opportunity, so the
     `1fr` track cannot go below the whole token, the auto track on .contact-card is floored at that,
     and the CARD's children paint 315px wide inside a 261.8px card. The page scrolled horizontally
     by 60px. SC 1.4.10 is defined at 320px, so this is a failure at the criterion's own width, and
     unlike the reds it was never decided - it was never noticed.
     `anywhere` AND NOT `break-word`, and the difference is the whole fix: `break-word` breaks the
     glyphs but does NOT contribute to intrinsic sizing, so the 314.98px track - and therefore the
     overflow - would have survived it. `anywhere` lowers the min-content contribution, the track
     collapses to what the card can give it, and the address wraps instead of the page scrolling.
     SCOPED TO mailto:, because the e-mail is the only unbreakable token here - the phone and fax
     numbers break at their spaces and the hours line at its commas, and none of them should be
     allowed to break mid-token. It costs nothing above ~360px, where the address never runs out of
     room and so never breaks. */
  .contact-list a[href^="mailto:"] { overflow-wrap: anywhere; }

  /* ---- 6.17 FORM ---------------------------------------------------------------------------------
     A working HTML POST first; main.js only enhances it, and declines to intercept an unconfigured
     action rather than silently swallowing an enquiry. Fields never fall below 16px (iOS Safari zooms
     the viewport on a sub-16px field focus). The resting border is --line-control, NOT --line-strong,
     and that distinction is why the third hairline rung exists: the field ground is --bg-2, a tonal step
     the page cannot be told apart from, so this border is the only thing that says a field is there.
     WCAG 1.4.11 names a text input's visual boundary explicitly, and --line-strong measured 1.62 against
     that floor - a field edge really is something a pointer and a keyboard can act on, which was always
     the intent here; the rung it pointed at was simply not solved for it. Focus substitutes BORDER + GROUND for the ring,
     on :focus rather than :focus-visible, because a field that was clicked still has to show which one
     is live; --bg-3 is the third tonal step doing double duty as the focus surface.
     13.78rem track = 234.3px = a 24-character e-mail at 17px (24 x 8.3422 = 200.2) + 2 x 17px padding =
     234.2px - the same figure that sets the phone breakpoint's form row, and the one condition of that
     breakpoint that has now run away from its own literal (see the phone note in the tokens).
     [FACE v9] 12rem -> 13.78rem, RE-DERIVED, because 12rem's whole meaning is "24 characters of e-mail"
     and Montserrat is 17.4% wider per character than Titillium: 12rem (204px) now holds 170.0px of
     inner = 20.4 characters, so a real address would scroll inside its own field at the 2-up threshold.
     WHAT IT COSTS, stated because it is a threshold and not an annotation: 2-up now needs
     2 x 234.2 + 27.51 = 495.9px of container = 550.9px of viewport, where 12rem put it at 435.5px of
     container / 490.5px of viewport. The form is one column from 480 (where the query pins it) all the
     way to 551, a 71px band that used to be 11px. That is the derivation's own answer arriving later
     rather than a defect: two fields too narrow to hold the address they ask for is exactly what this
     track exists to prevent, and stacking early is the safe direction. The alternative - hold 12rem and
     record that the track no longer means 24 characters - was rejected because the number would then be
     a literal with no arithmetic behind it, which is the one thing this file does not ship. */
  .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 13.78rem), 1fr)); gap: var(--space-3); }
  /* [v7 REPAIR PASS] PINNED to one column below the phone breakpoint. Left to its own intrinsic sum the
     OLD 12rem track re-crossed the gutter step that fires at the same 480: measured on /contact-us, 491
     was two columns, 490 one, 480 one, 479 TWO and 478 one again. The re-derived 13.78rem track puts
     2-up at 550.9px and out of reach of that oscillation, so the pin no longer has an island to close -
     it is KEPT because it costs nothing and because removing it would make the phone band depend on a
     sum the gutter step has already crossed once. The breakpoint tokens carry both sums. */
  @media (max-width: 29.99rem) { .form-grid { grid-template-columns: 1fr; } }
  .field { display: grid; gap: var(--space-1); }
  .field--full { grid-column: 1 / -1; }
  .field label { font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-7); text-transform: uppercase; color: var(--ink-2); }
  .field input, .field textarea, .field select { width: 100%; min-height: 44px; padding: var(--space-2); font-size: var(--text-0); background: var(--bg-2); color: var(--ink); border: 1px solid var(--line-control); border-radius: var(--radius); transition: border-color var(--dur-fast) ease, background-color var(--dur-fast) ease; }
  /* THE SUBSTITUTION IS ONLY FOR CONTROLS THAT CAN CARRY IT, and the checkbox cannot. This rule is in
     @layer components, a LATER layer than base's `:focus-visible { outline: 2px solid var(--ink) }`,
     so `outline: none` here wins outright - it is not a preference, it is the removal of the site's only
     focus indicator, and whatever replaces it has to be at least as loud. Border + ground does that for
     a bordered box: --line-control -> the indicator colour on a 1px edge plus --bg-2 -> --bg-3 under it.
     .field--check input sets `border: 0`, so on the acknowledgement checkbox that gates submission the
     border half paints nothing and the ground half is a 1.06:1 change behind a UA-painted control -
     under the 3:1 non-text floor, i.e. no visible focus indicator on a REQUIRED control. Excluded by
     type here, so it keeps base's ring; nothing else is needed, and a second ring declared here would
     be a second owner of the same decision.
     select is INCLUDED deliberately, closing the other half of the inconsistency: it is a bordered box
     on the same ground as the text fields, so it takes the same substitution rather than being the one
     control in the form that answers focus with a ring. No page ships a <select> today - the resting
     rule one line up already dresses one, and naming it here keeps the resting and focused halves of
     that decision in the same commit.
     [RED v7] said THIS IS THE ONE AREA THAT KEEPS --brand AND MUST NOT TAKE --brand-fill, because the
     border is a SUBSTITUTED FOCUS INDICATOR - this block's whole argument is that it replaces the site's
     only ring - so the non-text boundary criterion binds on it for real, against both surfaces it
     separates: --bg-3 inside and --bg / --bg-2 outside. At v7 the text stop cleared that (5.02:1 on
     --bg-3) where the fill stop did not.
     [RED v8] THE COLLAPSE TOOK THAT AWAY AND THE BORDER NOW MISSES 1.4.11. With --brand at the mark's
     #BF0000 this edge measures 2.70:1 on --bg-3 and 2.86:1 on --bg-2, under the 3:1 floor on both
     surfaces it separates - on the ONLY focus indicator these controls have. THE RULE STILL SAYS --brand,
     because the firm's instruction was that every red on the site be the highlight red and this is a red;
     it is recorded in the token block's failure list - specifically in half (B), the NON-TEXT half, which
     was written on 2026-09-06 for exactly this row after a review found this sentence pointing at a list
     that did not contain it - and asserted by tools/verify_ratios.py (DECIDED_NON_TEXT) rather than
     quietly repaired. THAT REPAIR WAS THEN TAKEN, the same day, and this paragraph's last sentence used
     to be the proposal for it: border-color: var(--brand-bright) - 6.54:1 against the --bg-3 it encloses
     and 7.25:1 against --bg outside it. The ground half of the pair (--bg-2 -> --bg-3, 1.06:1) is
     unchanged and was never carrying this on its own.
     [HOVER v22] AND THE REPAIR HAS NOW BEEN TAKEN A SECOND TIME, TO A SECOND COLOUR, WITHOUT THE
     REQUIREMENT MOVING AN INCH. The firm's pink instruction retired --brand-bright, so the colour that
     satisfied 1.4.11 here on 2026-09-06 no longer exists as a token. The border takes var(--ink):
     16.00:1 against the --bg-3 it encloses and 17.00:1 against the --bg-2 outside it.
     Both surfaces clear the 3:1 floor by more than five times over, and the border is once again THE SAME
     COLOUR BASE'S RING PAINTS - which was half the argument for --brand-bright and is the whole argument
     for --ink. THE SENTENCE THAT MUST NOT SURVIVE THIS EDIT is the one that called this "the one red the
     collapse did not take": there is no red in this rule any more, and a comment still describing the
     repair by the colour it used to use would send the next reader looking for a token that is gone. */
  /* THIS BORDER IS NOT BRAND PIGMENT, IT IS A STATE INDICATOR, and since [HOVER v22] it is not pigment of
     any kind - it is the ink. outline: none above means the 1px border is the only mark of which field
     has focus, so 1.4.11 binds on it as the whole affordance, and an affordance takes whichever colour
     clears the floor rather than whichever colour is on brand. The arithmetic and the reasoning are
     recorded once, in the AA FAILURE block in 2.2. --ink is what every other :focus-visible on this site
     paints, so this declaration is no longer the odd one out in any respect: same colour as the ring it
     substitutes for, same colour as every hover, and the one indicator on the site that does not depend
     on the palette's reds at all. */
  /* [HOVER v23] THE CONTROLS ANSWER THE POINTER NOW. A field's rest and hover were byte-identical, which
     is the same defect as a dead link hover with one mitigation a link does not have: the box, the label
     and the I-beam cursor already declare the control, so this is a supplementary cue and not the thing
     1.4.11 binds on. It is a BORDER step and not a ground step, because the ground is what FOCUS moves
     (--bg-2 -> --bg-3) and two states must not converge: rest is --line-control over --bg-2 = #696565 at
     3.25:1, hover is --ink-2 at 8.09:1, focus is --ink at 16.00:1. Three rungs on one property, in one
     direction, each brighter than the last. --ink-2 rather than --ink so that hover cannot be mistaken
     for focus, and the focus rule below - which comes after at equal (0,3,1) specificity - still wins on
     a field that is focused AND pointed at, so the stronger state never regresses to the weaker one. */
  :where(html:not(.has-touch)) .field input:not([type="checkbox"]):hover, :where(html:not(.has-touch)) .field textarea:hover, :where(html:not(.has-touch)) .field select:hover { border-color: var(--ink-2); }
  .field input:not([type="checkbox"]):focus, .field textarea:focus, .field select:focus { outline: none; border-color: var(--ink); background: var(--bg-3); }   /* the substituted indicator, 16.00:1 on --bg-3 */
  .field textarea { min-height: 8rem; }
  .field--check { grid-template-columns: auto 1fr; align-items: start; gap: var(--space-2); }
  /* 1.375rem = 23.38px: the smallest box whose row still reads as one 44px target beside 17px copy.
     [RED v7] accent-color is a FILL - the UA paints the checked box in it - so it takes --brand-fill, and
     the ink that lands on it is the UA's own white tick, at exactly the white-on-fill figure the token
     block carries - 6.53:1 since [RED v8], slightly better than before. That is the deep half of the split working as intended on a control this file does not
     draw. The fill's own edge does not carry the control's boundary: the .field rule above gives this
     input a 1px --line-control border, which is what delineates it. */
  .field--check input { width: 1.375rem; height: 1.375rem; min-height: 0; margin-top: 0.15rem; padding: 0; accent-color: var(--brand-fill); }
  /* Running legal copy, not a label: sentence case, no tracking, the 16px floor. */
  .field--check label { font-size: var(--text-0); font-weight: var(--wt-body); letter-spacing: 0; text-transform: none; color: var(--ink-2); }
  .field-hint { font-size: var(--text-label); line-height: var(--leading-body); color: var(--ink-2); }
  /* The error carries TEXT as well as colour, and --danger is warm (hsl 32) so it can never be read as
     the brand red; 9.6:1 on --bg. :has() rather than a state class, so main.js need only insert the
     message and the field edge follows. */
  .field-error { font-size: var(--text-label); font-weight: var(--wt-strong); color: var(--danger); }
  .field:has(.field-error) input, .field:has(.field-error) textarea { border-color: var(--danger); }
  /* THE FORM-LEVEL ALERT. main.js builds this <p role="alert"> and inserts it as the FIRST child of
     .form-actions, above the button, where the visitor is already looking. Everything about it is one
     rung louder than .field-error, because a field error says "fix this line" and this one says "your
     message did not send":
       flex: 1 0 100%   .form-actions is a WRAPPING FLEX ROW. Without a full-width basis the alert
                        becomes a column beside the submit button and the row's --space-3 gap squeezes
                        both. This is the declaration that makes the alert a band across the top of the
                        row; the row's own gap is then its separation from the button, so no margin.
       --text-0         the 16px floor, not --text-label: a field error annotates a label, this
                        replaces an outcome.
       the plate        the same grammar as .form-sent - --bg-2 ground, --line edge, a 2px accent on
                        the leading edge - with --danger where .form-sent takes --brand, so the two
                        outcomes are the same object in two colours rather than two inventions.
     COLOUR IS NOT THE SIGNAL: the sentence names the failure, and --danger is warm (hsl 32) so it
     cannot be read as the brand red. 9.20:1 on --bg-2 (--danger L 0.4665 / --bg-2 L 0.00612).
     The embedded tel: link takes the alert's OWN ink rather than --brand: red-on-orange-on-plate is
     three accents in one sentence, and --brand would measure 2.86:1 here against the alert's 9.20:1 -
     an argument [RED v8] made considerably stronger without touching this line.
     Its underline is the affordance - resting, because on touch :hover never fires. */
  .form-error {
    flex: 1 0 100%;
    padding: var(--space-2) var(--space-3);
    background: var(--bg-2); border: 1px solid var(--line); border-left: 2px solid var(--danger);
    border-radius: var(--radius);
    font-size: var(--text-0); font-weight: var(--wt-strong); line-height: var(--leading-body);
    color: var(--danger);
  }
  /* [MOTION v20] THE THICKNESS STAYS INSTANT, AND IT IS A DECISION RATHER THAN THE ONE LINK THIS PASS
     MISSED. text-decoration-thickness is a PAINT property: animating it repaints the text run on every
     frame, and 6's preamble allows this file exactly two animated properties, transform and opacity.
     (.svc-card-link's background-size sweep is the single standing exception and is a 1px strip, not a
     glyph run; it is not a licence to generalise.) The alternative that WOULD be compositor-safe is
     tweening text-decoration-color instead, and it is refused because the colour here is the alert's own
     --danger and the thickness is the whole signal - a link inside a failure message should get louder,
     not warmer. So this one stays a step. Recorded so the next audit finds the reasoning instead of the
     omission. */
  .form-error a { color: inherit; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
  :where(html:not(.has-touch)) .form-error a:hover { text-decoration-thickness: 2px; }
  .form-error a:focus-visible { text-decoration-thickness: 2px; }
  /* THE HONEYPOT - three layers, all required. Off-screen beats display: none (bots skip hidden
     inputs), but Chrome's autofill visibility test reads the input's OWN border box and ignores
     ancestor overflow and negative offsets, so the input itself must also be 1px; the third layer is
     main.js blanking the field before POST. Its id, label and autocomplete carry no semantics an
     address parser could match. */
  .hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
  .hp input { width: 1px; height: 1px; min-height: 0; padding: 0; border: 0; }
  .form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-top: var(--space-3); }
  .form-note { font-size: var(--text-label); color: var(--ink-2); }
  .form-note a, .form-sent a { color: var(--brand); transition: color var(--dur-fast) ease; }   /* TEXT, so the text stop. 2.86:1 on --bg-2 at 13.36px and 17px - BELOW AA. Recorded failure */
  /* [HOVER v23] AND THEY HOVER. These two had no hover and no focus colour of any kind - the privacy
     note's link and the link inside the sent confirmation, both red on --bg-2, both dead under the
     pointer. --ink lifts them to 17.00:1, the only state either reaches that meets AA. .form-error a is
     deliberately NOT here: it rests at --danger and answers with its underline thickening 1px -> 2px,
     which is a real change, and warming a warning to --ink would take the warning out of it. */
  :where(html:not(.has-touch)) .form-note a:hover, :where(html:not(.has-touch)) .form-sent a:hover { color: var(--ink); }
  .form-note a:focus-visible, .form-sent a:focus-visible { color: var(--ink); }
  /* Submit state: opacity and pointer-events only, no transform, so nothing moves under the pointer
     mid-submit; the label swap main.js performs is the real signal.
     [v7 REPAIR PASS] THE DIM IS SOLVED, not chosen. opacity multiplies the ink and the plate alike, so a
     busy .btn composites BOTH against the ground and the pair inside it closes: at 0.60 the white-on-red
     label measures
     4.28:1 unlit and 3.81:1 with both lobes converged
     on --bg-2 - under AA, on a control that is still FOCUSABLE. main.js sets aria-busy and NOT disabled,
     deliberately (a disabled button drops a keyboard user to the top of the document mid-submit), so
     1.4.3's inactive-component exemption cannot be claimed for its text. 0.7238 is the smallest opacity
     holding 4.5:1 lit on --bg-2, the lightest ground a .btn ships on; 0.73 is that solve rounded UP - the
     same direction of care the glow's peak is rounded down in. It measures
     5.12:1 unlit and 4.53:1 lit.
     WHY --bg-2 IS THE BOUND WHEN THE PALETTE HOLDS TWO LIGHTER SURFACES, stated because the sentence above
     is only true while both stay out of reach. --bg-3 is the focused field and .quote-card, and no .btn is
     inside either; its solve would be 0.7251, which the shipped 0.73 clears anyway at 4.53:1 lit, so that
     one is covered even if it ever arrives. .band--ivory is NOT covered - a busy button in the band would
     composite to 4.12:1 lit - and it is out of reach twice over: the band ships on no page, and main.js
     sets [aria-busy] on exactly one control, the contact form's submit, which sits on --bg (a plain
     .section) and measures 4.55:1 lit. If a second consumer is ever given this attribute, re-solve first.
     [RED v8] THE SOLVE MOVED 0.70 -> 0.73 AND THE LIGHT IS THE WHOLE REASON. The deeper plate actually
     HELPS this pair (white on #BF0000 is 6.53:1 at rest against #C00000's 6.48:1); the 1.62x brighter
     glow more than eats that, and at the old 0.70 the busy label fell to 4.37:1 lit. This is one of the
     four values the v8 pass re-solved rather than re-chose.
     WHAT IS NOT HELD, stated so it does not read as an oversight: the plate's own edge dims with
     everything else and no non-text floor is claimed for it here. pointer-events: none makes the control
     genuinely inoperable for the duration, which is the case 1.4.11 exempts; its TEXT is held because a
     keyboard still reaches it and a screen reader still reads it. */
  [aria-busy="true"] { opacity: 0.73; pointer-events: none; }
  /* Success REPLACES the form (main.js sets [hidden] on it), never appends below it: on a phone
     scrolled to the button, a message appended under a long form is off-screen. */
  /* The 2px leading edge is an ACCENT BORDER, so [RED v7] gives it --brand-fill - the same object and the
     same grammar as .form-error's --danger edge one block up. It carries no information (the heading
     inside says the message sent), so it is the decorative-mark case and no non-text boundary floor
     binds on it - which matters, because since [RED v8] the fill stop clears that floor against no ground
     at all (2.86:1 on the --bg-2 this plate sits on). */
  /* [MOTION v20] THE SUCCESS PLATE NOW ARRIVES, AND IT WAS THE MOST IMPORTANT STATE CHANGE ON THE SITE
     WITH NO MOTION OF ANY KIND. Everything a visitor scrolls past on this site travels in; the one screen
     they actually waited for - the answer to a form they filled in and submitted - was swapped into place
     by main.js setting [hidden] on the form, and simply appeared. That is the reward at the end of the
     only conversion path the site has.
     AN ANIMATION AND NOT A [data-reveal], DELIBERATELY. The reveal system is scroll-driven: an
     IntersectionObserver, a batch, a stagger slot and a will-change release. None of that applies to an
     element that is unhidden by a click, and routing this through it would put a form response at the
     mercy of an observer, a watchdog and a class main.js writes for a different purpose. A keyframed
     animation on the element itself is one declaration with no owner but CSS, and it costs nothing when
     the page never submits: an element inside display: none runs no animation at all, and it starts
     cleanly the moment [hidden] comes off - which is exactly the trigger wanted.
     THE PLATE'S OWN REGISTER, THE PLATE'S OWN NUMBERS: --reveal-plate 1.2s and the 28px guards 7.3
     declares, because this IS a plate arriving and a second set of figures for the same gesture is how a
     file acquires two vocabularies. It is legible long before it settles - expo-out finishes 90% of both
     the travel and the fade at 0.329 of the run, i.e. 0.395s, so nobody waits 1.2s to read "message sent".
     `both`, AND IT IS ALSO THE REDUCED-MOTION ANSWER, which is why no guard line is added for this. The
     backwards fill holds opacity 0 for the frame before the run starts, so there is no flash of the plate
     at rest; the forwards fill holds the END state, and the end state IS the plate's natural appearance.
     Block 5's clamp collapses animation-duration to 0.001s without touching animation-name, so under that
     query this resolves, in one frame, to the plate exactly as it would have looked with no animation at
     all. That is the property .hero-media's zoom had to be given an explicit rule to obtain (its `to` was
     not its resting value); here it is structural.
     COMPOSITOR-ONLY - opacity and transform, nothing else - and FINITE, so the browser promotes and
     releases the layer on its own and no will-change is declared. 6.28 makes the same argument for the
     same reason, and the file still carries exactly one permanent promotion. */
  .form-sent { padding: var(--space-4) var(--space-3); background: var(--bg-2); border: 1px solid var(--line); border-left: 2px solid var(--brand-fill); border-radius: var(--radius); animation: form-sent-arrive var(--reveal-plate) var(--ease-out) both; }
  .form-sent .display { font-size: var(--text-2); max-width: none; }
  .form-sent p { margin-top: var(--space-2); color: var(--ink-2); }
  .form-sent:focus-visible { outline-offset: 6px; }

  /* ---- 6.18 BUTTONS ------------------------------------------------------------------------------
     44px minimum in both axes. Padding is SYMMETRIC, and MORE symmetric than it was: at --leading-label
     1 the half-leading centres the content area in the line box, so with Montserrat's hhea pair
     (0.968 / 0.251) the cap sits 0.0085em below the line-box centre - 0.11px at 13.36px, against
     Titillium's 0.027em / 0.36px. The asymmetry the source site needed does not exist here
     (DESIGN-SYSTEM s4). Height = 13.36px x --leading-label 1 + 2 x 0.92rem (15.64px) + 2 x 1px border
     = 46.64px - no face term in that sum, so the 44px floor is still cleared by 2.6px. Weight 600 on
     BOTH fills: 500 now EXISTS (Montserrat is variable 300..600 as requested) and is still not taken,
     because 400 spindles at 13.36px under 0.12em of air and 600 is the top of the range - the previous
     face made the same choice for the different reason that it shipped no 500 at all.
     On a dark ground the hover goes BRIGHTER, never to the deep stop. The site's
     ONE nudge is --nudge - card rise, button rise, arrow travel, the footer links, the linked stat and
     (at twice the value) the watermark all use it, so the page has one nudge rather than seven. Never
     box-shadow. A 2px button rise was proposed when the hover layer was
     refined and REFUSED on exactly that sentence: 2px against the card's 3px is not a subtler button,
     it is a second nudge, and divergent-but-similar values are what make a site feel assembled rather
     than designed. The rise a visitor should notice getting quieter is the FOCUS response, which moves
     no plate at all.
     [MOTION v20] THE NUDGE IS 4px AND IT IS A TOKEN NOW, AND THIS SENTENCE IS WHY IT HAD TO BECOME ONE:
     the rule above was prose with three copies of a literal under it, the firm has since asked for more
     motion in more places, and this pass takes the consumer count to six. The size is solved in the
     tokens block against THIS button's own 46.64px box - the hop threshold is what caps a nudge, and it
     is the reason the nudge does not take the reveal travel's multiplier (which would have given 5.25px
     and cleared the threshold). The magnetic button's LIFT lives in main.js and must be 4 as well, or the
     one control per page that main.js owns becomes the only one that rises less than its neighbours.
     [RED v7] THE RESTING PLATE AND ITS HOVER USED TO INVERT, AND IT WAS THE DESIGN rather than a
     leftover. At rest the button was --brand-fill with #FFFFFF ink; on hover and focus it was
     --brand-bright with --brand-ink. Deep plate with light ink becoming light plate with dark ink is a
     single legible gesture and it was LOUDER than the brand-to-bright warm-up before it, which only
     changed the temperature of the plate. [RED v8] The resting end got BETTER again when the plate went
     to the mark's own #BF0000: white on it is 6.53:1, against #C00000's 6.48:1 and the 5.43:1 that
     shipped before v7. This is the button the firm's instruction was aimed at, and it was the half of
     that instruction that cost nothing.
     [HOVER v22] THE INVERSION IS OVER. The firm's second instruction of 2026-09-06 - the pink one - took
     --brand-bright off the site, and a hover cannot invert into a colour that no longer exists. The
     button now DEEPENS instead: --brand-fill #BF0000 -> --brand-fill-hover #8F0000, one plate getting
     darker, with #FFFFFF ink at both ends and therefore no ink change at all. Three things follow, and
     each one deletes an argument this block used to make.
       (1) THE INK IS NO LONGER RE-DECLARED ANYWHERE, and the sentence demanding that it be ("THE INK MUST
           BE RE-DECLARED WHEREVER THE FILL CHANGES") is retired with the two-ink era that produced it.
           Both fills carry white, so the resting `color: #FFFFFF` covers every state; a re-declaration
           would be a line that cannot change anything.
       (2) THE LABEL GETS BETTER RATHER THAN MERELY STAYING LEGIBLE: 6.53:1 -> 9.69:1. The old inversion
           moved the label from 6.53:1 to 7.38:1 by swapping ink AND plate; this moves it further by
           moving one of the two.
       (3) THE HOVER PATH STOPS BEING A PROBLEM, which is the subject of the [MOTION v21] block below and
           the reason that block is now mostly history.
           [FIX 2026-09-06] A comment terminator stood at the end of the line above and closed this
           comment 25 lines early, so everything down to 6.19's real terminator parsed as CSS. With no
           braces in that prose the parser's error recovery ran on until it found a block to discard -
           which was .btn itself - and every button on the site rendered as an underlined link.
           Two characters, every CTA on every page. Do not write a comment terminator inside a comment,
           not even when describing one: this note originally did, and reproduced the bug it documents.
     THERE IS EXACTLY ONE SIZE, and .btn--small is refused rather than merely absent. The header's tel:
     CTA is the only candidate for one, and 6.1's row sum already sizes that button at 222.2px in
     the full size: label 165.23 + 2 x --space-3 padding 55.01 + 2 x 1px border. Adding a small modifier
     would not free space the sum has already banked - it would invalidate the 89rem query the sum
     produced (89rem since [FACE v9]; 88 since [RED v7]), and it would put a second button size on a site whose one button is also its one tap
     target. The header partial ships class="btn nav-cta"; a .btn--small in any partial is a bug in the
     partial, not a missing rule here. The one modifier that exists, .btn--ghost, changes weight, not
     scale.
     [REPAIR v10] --leading-label IS A SINGLE-LINE VALUE ON A CONTROL WHOSE LABEL CAN WRAP, and the one
     place that shows is recorded here rather than repaired, because the repair costs more than the
     defect. The token is 1 by its own derivation ("cap height 0.700em centred in a 1em box"), which
     assumes one line. At 320px viewport the widest ghost label - "Request a consultation" / "Start with
     a consultation", 241.9px intrinsic - exceeds the 206.74px of inner width a .package leaves it
     (261.75 container less 2 x --space-3 padding), so it wraps to two lines and those two lines sit
     13.36px apart carrying 9.35px caps: 4.01px between them, which reads as one dense block rather than
     two lines. Measured on /flat-fee-services at 320: eight buttons, all .btn--ghost, all 2 lines, no
     overflow and no clipping anywhere.
     WHY IT IS NOT FIXED. Raising line-height fixes the gap and grows EVERY button on the site by the
     same amount (1 -> 1.25 takes the box 46.66 -> 50.0px); compensating with padding-block keeps the
     one-line box identical but replaces a derived 0.92rem with 0.8217rem and breaks the token's stated
     centring relation. Both are site-wide geometry changes to a control that is also the site's only
     tap target, bought for a cosmetic tightness at a width 55px below the 375px phone every phone rule
     here is derived at, in a band with no overflow. Recorded on the same principle as --gutter-phone
     and --grid-track: the constraint is named where it fails, not quietly re-solved. If a 320px pass is
     ever commissioned, the label set is the cheaper lever than the leading. */
  /* ---- [HOVER v22] THE PROBLEM THE BLOCK BELOW SOLVES NO LONGER EXISTS, AND THE MACHINERY IT BUILT IS
     DELETED RATHER THAN LEFT DORMANT. The whole of [MOTION v21] is about a hover that INVERTS - a plate
     travelling from a deep red to a light pink while its ink travels from white to near-black, crossing
     a luminance at which neither ink is legible. The firm's instruction of 2026-09-06 removed the pink,
     so the plate now travels #BF0000 -> #8F0000 with #FFFFFF ink at both ends and no ink change at all.
     THE PATH IS MONOTONE AND ITS WORST INSTANT IS ITS FIRST. Darkening a ground can only raise a white
     ink's ratio, so contrast climbs from 6.53:1 to 9.69:1 across the run and the minimum over the whole
     hover IS the resting figure, 6.53:1 - measured the same way v21's trough was, by walking the
     interpolation rather than by asserting it. There is no crossing to place a step at, and nothing on
     the path to protect.
     SO THREE THINGS GO: --btn-ink-step (the 0.489 crossing point), --btn-ink-timing (the discrete swap
     and the ghost's override of it), and `color` from the transition list, because no state of either
     button changes a colour any more - the solid one carries #FFFFFF at rest and hovered, the ghost
     carries --ink at rest, hovered and focused, and a transition on a property nothing changes is
     dead code that reads as intent.
     WHAT STAYS, AND WHY, BECAUSE ITS ORIGINAL REASON WENT WITH THE STEP: `linear` at --dur-fast on the
     plate and its edge. v21 argued `linear` was load-bearing because it made the step's delay exactly a
     fraction of the duration; with no step that argument is gone, and the SECOND one is now the whole
     reason - linear reaches 90% of its travel at 0.900 of the run, so --dur-base would give 0.54s of
     perceived arrival, over the pointer ceiling, where --dur-fast gives 0.270s inside it. The rung was
     solved and shipped; re-timing the site's one primary control is not this pass's business.
     tools/verify_ratios.py replaced its three path rows with one that walks the new plate and asserts
     the minimum equals the resting pair, so the claim above is checked and not merely made.
     WHAT FOLLOWS IS THE v21 RECORD, kept because it is the evidence that a colour transition has a PATH
     and that this file measures one - the next time two states are chosen for a control, this is the
     paragraph that says to walk between them. -------------------------------------------------------
     [MOTION v21] THE HOVER INVERTS THE POLARITY OF THIS CONTROL, AND UNTIL TODAY IT TWEENED THROUGH
     THE CROSSING. THE LABEL WAS EFFECTIVELY INVISIBLE ON ITS OWN PLATE FOR A QUARTER OF A SECOND, on
     every button hover on every page. Both END STATES were recorded and both clear AA - white on
     --brand-fill at rest, --brand-ink on --brand-bright hovered - and the PATH BETWEEN THEM was not.
     MEASURED, with a real Input.dispatchMouseEvent hover sampled every 25ms rather than a forced
     pseudo-state. /contact-us's .btn, as (ms, contrast) pairs: (0, 6.53) (155, 4.09) (207, 2.18)
     (258, 1.06) (361, 3.03) (465, 5.56) (678, 7.38) - about 259ms below AA, and six of those samples
     below the 3.0 non-text floor as well. Four buttons on three pages, all the same shape. [MOTION v20] made the window 25% longer by taking
     --dur-base 0.48 -> 0.60s. :focus-visible shares this transition list, so a keyboard reader tabbing
     through buttons hit the same trough on every stop.
     WHY IT IS UNAVOIDABLE BY TWEENING, AND THE BOUND IS WORTH KNOWING BECAUSE IT DECIDES THE REPAIR. The
     plate's path is continuous, so it passes through every luminance between the two fills, including
     the ONE luminance at which white and --brand-ink are equally legible on it (#DE3737, which the plate
     reaches at 0.489 of its run). Whichever ink is in force there, the contrast at that instant is
     sqrt(1.05 / (L(--brand-ink) + 0.05)) and no path can do better. So 4.46:1 is the CEILING on the
     worst moment of any hover between these two states, and it is reached only by a STEP. The tween was
     not near it: taking both colours across together, the true minimum is 1.00:1.
     THREE REPAIRS THAT WERE REFUSED, each because it moves something the firm decided. Darkening the
     hover plate so white can stay (no inversion, monotone path) changes the lamp the firm sees. Keeping
     white throughout floors at 2.69:1, and keeping --brand-ink throughout floors at 3.04:1 - both worse
     than the step and both against a state the token block has already ruled on. Lightening
     --brand-bright until white clears 4.5:1 on it would move a token that eight other rules read.
     SO THE INK STEPS AND THE PLATE TWEENS. `color 0s linear <delay>` is a discrete swap: there is no
     intermediate ink, and the swap is placed at the plate's crossing point. THE PLATE IS THEREFORE
     `linear` AND THAT IS LOAD-BEARING, not a taste change - with a linear plate, progress IS the time
     fraction, so the delay is --btn-ink-step of the duration by construction. On `ease` the same
     placement would need the inverse of a bezier written out as a magic constant, and the next duration
     change would silently move the step off the crossing.
     AND THE COLOURS DROP A RUNG, WHICH THE `linear` FORCES RATHER THAN MERELY PREFERS. linear reaches 90%
     of its travel at 0.900 of the run, so this plate at --dur-base would be 0.54s of perceived arrival -
     over the pointer ceiling - where at --dur-fast it is 0.270s, comfortably inside it. That the rung's
     own note in the tokens block reads "colour and opacity only" is a second, independent reason and not
     the load-bearing one.
     THE BORDER GOES WITH THE PLATE, AND NOT BECAUSE EVERY BORDER SHOULD. This is not the claim that
     --dur-base is wrong for a border-color anywhere - 6.1's bar, 6.9's card, 6.12's package, 6.24's
     contact card and 6.29's photograph ring all warm a HAIRLINE at --dur-base and all keep it: a 1px edge
     tinting toward the accent carries no legibility floor and is paired with a --dur-base lift, so both
     halves of that gesture share one rung. On .btn the "border" is not a hairline at all - it is declared
     `1px solid var(--brand-fill)`, the plate's own colour, so it is the plate's EDGE. Left on --dur-base
     it would lag the fill by 2x and the control would wear a visibly mismatched rim for 0.3s of every
     hover. One surface, one timing.
     THE LIFT KEEPS --dur-base AND --ease-out, so the button still rises on the same rung as the card and
     the footer nudge. Two moving parts, two timings, one gesture - 6.19's own sentence, applied to the
     control it was written beside.
     WHAT IT MEASURES OUT AT: the plate is done in 0.30s (0.270s perceived, inside the pointer ceiling),
     the ink flips at 0.1467s, and the worst instant on the path is the ceiling rather than a hole.
     ---- end of the [MOTION v21] record. */
  .btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
    min-height: 44px; padding: 0.92rem var(--space-3);
    font-family: var(--font-body); font-size: var(--text-label); font-weight: var(--wt-label);
    letter-spacing: var(--track-6); line-height: var(--leading-label);
    text-transform: uppercase; text-decoration: none; text-align: center;
    background: var(--brand-fill); color: #FFFFFF;   /* 6.53:1 at rest and 9.69:1 hovered - ONE ink now, declared once; the retired --brand-ink would have been 3.04:1 here */
    border: 1px solid var(--brand-fill); border-radius: var(--radius);
    transition: background-color var(--dur-fast) linear, border-color var(--dur-fast) linear,
                transform var(--dur-base) var(--ease-out);
  }
  /* The ghost takes --line-control and is the only .btn declaration that names a hairline at all:
     with `background: transparent` its border is the ENTIRE control, so 1.4.11 binds on it for the
     same reason it binds on a field edge, and --line-strong is not solved for that job. */
  /* [MOTION v21] PUT THE GHOST'S INK BACK ON A TWEEN because the ghost does not invert; [HOVER v22] takes
     the property off the list entirely, for both buttons, because nothing changes a button's ink now -
     the ghost rests, hovers and focuses at --ink. Its own custom property goes with the machinery. */
  .btn--ghost { background: transparent; color: var(--ink); border-color: var(--line-control); }
  /* Keyboard parity outside the gate, quieter: the ring is the signal, so the plate does not move.
     [HOVER v22] AND NOW THE PLATE REALLY DOES NOT MOVE. This rule used to repaint the plate with the
     hover's fill and re-declare the hover's ink; it now RESTATES THE RESTING FILL, which is a change of
     behaviour and not a tidy-up. A keyboard visitor must not be handed a state a mouse visitor can only
     reach while actively pointing at the control: focus and hover answer different questions - "where am
     I" against "this is clickable" - and giving both the same plate made the louder of the two answers
     the wrong one. The keyboard answer is base's 2px --ink ring at 17.73:1, which is louder than any
     plate change on this control and is the same indicator every other control on the site gives.
     IT IS A NO-OP BY VALUE AND LOAD-BEARING BY POSITION, which is why it is declared rather than deleted.
     Declared, it is an assertion a checker can read: tools/verify_ratios.py resolves what
     `.btn:focus-visible` paints and fails if it is anything but --brand-fill, so "focus keeps the resting
     plate" cannot quietly stop being true. And it sits BEFORE :hover deliberately - the two selectors
     have equal specificity, so the later one wins, and a button that is focused AND hovered must show
     the HOVER. Move this line below the hover rules and pointing at a tabbed-to button silently stops
     deepening it.
     THE GHOST KEEPS A REAL FOCUS RULE, and the asymmetry is deliberate: what the solid button withholds
     from focus is a FILL, and the ghost has no fill to withhold - its hover changes an edge and a label,
     which a ring sits beside without contradiction. Hover and focus were already identical on the ghost
     before this pass, so that pair is unchanged in relation as well as in value. */
  .btn:focus-visible { background: var(--brand-fill); border-color: var(--brand-fill); }   /* 6.53:1 - the RESTING pair, restated so the rule can be read as the decision it is */
  .btn--ghost:focus-visible { background: transparent; border-color: var(--ink); color: var(--ink); }   /* 17.73:1 on --bg */
  /* [HOVER v22] THE SOLID BUTTON DEEPENS AND THE GHOST'S EDGE BRIGHTENS - one gesture each, and each is
     the only thing on its control that changes colour.
     THE SOLID: plate and edge both to --brand-fill-hover, ink untouched at #FFFFFF, so the label goes
     6.53:1 -> 9.69:1 while the rim it sits inside goes 2.99:1 -> 2.01:1 against --bg. That trade is the
     recorded regression of this pass and it is argued in the token block, at --brand-fill-hover and in
     half (B) of THE AA FAILURE: the white LABEL is what 1.4.11 accepts as identifying this control, and
     the label improves exactly where the rim worsens. `transform` is unchanged - the site's one --nudge.
     THE GHOST: border-color --line-control -> --ink, i.e. the composited #646161 becoming #F7F3F4, which
     is a far louder edge change than the pink ever gave it. `color: var(--ink)` is a NO-OP by value here,
     because the ghost already rests at --ink, and it is stated anyway so that the two buttons' hover
     rules read as one pair rather than as one rule with a hole in it - and so that a future change to
     the ghost's resting ink cannot silently make its hover state disappear. Its ground stays
     transparent, so both figures are read against the page: --ink is 17.73:1 on --bg. */
  :where(html:not(.has-touch)) .btn:hover { background: var(--brand-fill-hover); border-color: var(--brand-fill-hover); transform: translateY(calc(-1 * var(--nudge))); }   /* 9.69:1 */
  :where(html:not(.has-touch)) .btn--ghost:hover { background: transparent; border-color: var(--ink); color: var(--ink); }   /* 17.73:1 on --bg */
  .btn:active { transform: none; }
  /* MAGNETIC CTA - the CSS side, which is only the SUPPRESSION. One button per page opts in with
     [data-magnetic]; main.js then owns that button's `transform` for the life of the page, and an inline
     transform outranks every author rule, so the :hover rise above would be dead code on it. It is turned
     off here so the outcome is AUTHORED rather than left to whichever declaration happens to win - the
     playbook's recorded failure is that without this line the magnetic button was the only one on the
     site that did not rise, and because the pull follows the pointer, entering from above actually pushed
     it DOWN, which reads as the opposite of an affordance. The rise is ported into the JS offset instead:
     LIFT = the site's one nudge, so at rest and on press it is indistinguishable from its neighbours.
     [MOTION v20] THAT PORTED COPY IS THE ONE PLACE --nudge CANNOT REACH, and it is named here as well as
     in the token because a cross-file constant that only one file mentions is a constant that drifts:
     main.js's LIFT must read 4, not 3, from this pass onward. Its MAX = 8px is a different quantity with
     a different derivation (MAX < this button's 27.51px of horizontal padding, or the plate travels out
     from under the pointer and oscillates at its own edge) and is NOT governed by the nudge; 8px is
     0.291 of that padding and the pull may go as far as 10px - 0.364, the sister site's figure - without
     touching the rule. Left where it is in this pass because it is JS's number and this pass is CSS's.
     THE CAP IS DERIVED FROM THIS BUTTON'S OWN PADDING. Horizontal padding is --space-3 = 1.618rem =
     27.51px a side, and the rule is MAX < padding: if the plate can travel out from under the pointer it
     starts an enter/leave oscillation at its own edge. MAX = 8px, an ABSOLUTE px travel that does not
     take the root multiplier (0.291 of the padding now, where it was 0.31; the playbook's
     reference is 10px against 33.6px = 0.30), GAIN = 0.25 (under 0.2 it is imperceptible, over ~0.4 it
     reads as a toy), 0.4s lerp home through the same interpolators.
     Focus parity needs nothing new: .btn:focus-visible moves no plate, so it is already correct here, and
     it is declared outside the gate two lines up. Touch needs nothing either - main.js never builds the
     pull without a mouse, and this rule is inside the gate. */
  :where(html:not(.has-touch)) .btn[data-magnetic]:hover { transform: none; }

  /* ---- 6.19 LINK-ARROW ---------------------------------------------------------------------------
     The rule RESTS at scaleX(0.25) with a left origin, so hovering EXTENDS a rule rather than creating
     one - a partly-drawn rule is a better idle state than nothing and gives the hover something to
     complete. ASYMMETRIC TIMING, because physical things do not retract along their approach curve:
     the small fast part (the glyph) leaves at --dur-fast with no delay and arrives over 0.42s
     (0.28 x 1.5) after a 0.09s (0.06 x 1.5) delay, so it FOLLOWS the rule rather than accompanying
     it; the large slow part (the rule) runs --dur-slow both ways.
     RE-CONFIRMED, and stated because the arithmetic is easy to mis-assign: the playbook's 0.28s-in /
     0.06s-lead-in / out-at-fast triple belongs to the GLYPH, and this block already ships it at the
     site's measured multiplier ([MOTION v20]: x1.5, where it was x1.2 - the pair moves with the ladder or
     it stops being the playbook's triple. 0.42s / 0.09s / --dur-fast 0.30s = 0.71x the approach, the
     playbook's ~0.7, and the ratio is unchanged because a single multiplier cannot change a ratio).
     THE GLYPH'S TRAVEL IS --nudge, WHICH IS 4px SINCE [MOTION v20] - the same token the card rise and the
     button rise take, so the site still has one distance rather than a lateral one and a vertical one.
     The sister site slides its glyph 5px; that is not adopted, because 5px here would mean either a
     second travel literal or a card rise past its own hop threshold, and one distance is worth more than
     one pixel of glyph.
     The RULE is deliberately NOT asymmetric: a drawn rule is the large slow part, and the playbook gives
     the large part one duration in both directions - an underline that snapped back faster than it drew
     would read as an erasure rather than a retraction. Two moving parts, two timings, one gesture. */
  /* TEXT, so the text stop - on --bg and on the --bg-2 card ground it usually sits on:
     2.99:1 and 2.86:1 at 13.36px/600, BELOW AA, a recorded failure. Its
     ::after rule paints `background: currentColor` and therefore inherits the same red - the same
     currentColor exception the eyebrow's hairline takes, for the same reason (one object, one colour,
     reachable by a band rebinding with no rule naming it). The hover to --ink at 17.73:1 is what
     carries the affordance now; the resting rule at scaleX(0.25) is the other half of it.
     [HOVER v22] THE UNDERLINE FOLLOWS THE TYPE FOR FREE, and it is the currentColor exception above that
     does it: the ::after is painted from currentColor, so when the label goes white the rule goes white
     with it and extends to scaleX(1) in the same gesture. One object, one colour, still - and the arrow
     is the clearest case on the site for why the hover had to be a LIFT rather than another red: the
     resting state is a recorded AA failure, so the hovered state is the only one a reader can be sure of,
     and it now clears AA by nearly four times. */
  /* [LINK v26] THE WRAPPED STATE, WHICH THIS COMPONENT HAS SHIPPED BROKEN SINCE IT WAS WRITTEN.
     WHAT WAS WRONG. The anchor is a flex row of two items - an anonymous TEXT item and the glyph - so
     when the label needs two lines the text item takes the whole flexed width, its last line ends
     wherever it ends, and the glyph stays pinned to the item's RIGHT EDGE, vertically centred on the
     two-line block. On /business at 1440 that put the arrow 285.43px past the end of "FEE", floating
     11px above that line's centre (tools/out/linkarrow/business-1440-employment--0-today.png).
     IT IS NOT A /business DEFECT AND THE CENSUS IS THE REASON THIS BLOCK CHANGED RATHER THAN THAT PAGE.
     tools/probe-linkarrow.mjs walked all 20 built pages at 1440 and at 390: 54 instances, 12 of them
     wrapped, 10 of those with the glyph detached - /business (both card links at 390, the Employment one
     at 1440), /flat-fee-services (BOTH bundled-packages links at 1440, i.e. on a desktop), /attorneys,
     /employment-law, /intellectual-property and /wills-trusts at 390. The firm's rewrite did not
     introduce this; it walked into it, because "Employer Starter Package, flat fee" is 8px too long for
     a 334.66px plate. A label that wraps is the NORMAL state on a phone and no page can avoid it.
     THE REPAIR IS TWO DECLARATIONS AND IT MOVES NOTHING THAT WAS ALREADY RIGHT.
       text-align-last: right   sends the LAST line of the text item to the item's right edge - which is
                                the edge the glyph already sits at, so the two meet. The item is a block
                                container, so it takes the property; when the label FITS, that item's
                                width IS its max-content width and the declaration cannot move anything.
       align-self: flex-end     puts the glyph on the last line's box instead of the block's centre,
       + margin-bottom          and the margin gives back the half-leading `align-items: center` was
                                supplying: (1lh - 16px) / 2 = (22.4482 - 16) / 2 = 3.2241px. On a
                                one-line link that is EXACTLY where centring already put it (top 3.22,
                                bottom 19.22 of a 22.4482 line box), so the single-line geometry is
                                unchanged by construction, not by luck. The em literal is the fallback
                                for engines without the `lh` unit (Safari < 16.4, Firefox < 120):
                                0.241em x 13.362 = 3.2203px, 0.004px from the exact value.
     MEASURED, all 54 instances before and after (tools/probe-linkarrow2.mjs, before.json / after.json):
     the 42 single-line instances move 0.00px on every axis - box, glyph, rule width, rule position - and
     the 12 wrapped ones close from a 145.97-474.35px gap to the component's own 6.67px.
     WHY NOT PUT THE GLYPH BACK IN THE INLINE FLOW, which is the obvious answer and was benched first
     (tools/probe-linkarrow3.mjs, candidates A-F). `display: inline` on the anchor alone drops the glyph
     onto a line of its own, because base 1503 sets `img, svg, video, canvas { display: block }` and a
     block inside an inline splits the inline - measured, glyph +15 to +38px down on every instance. Give
     the glyph `display: inline-block` back and the arrow does trail the last word, at the cost of the
     RULE: an absolutely positioned child of a fragmented inline resolves against the bounding box of the
     first and last fragments, so ::after collapsed from 332.66px to 53.91px on /business and from
     289.75px to 22.67px at 390 - a hairline the width of the last word. It also re-bases the rule from
     the line box to the font's content area (3.08px of half-leading), which 6.9's background spelling
     does NOT follow, so the two halves of one gesture would stop being the same geometry. Rejected on
     those numbers, not on taste.
     AND NOT `text-wrap: balance`, which makes the wrapped pair read far better (the /flat-fee-services
     link breaks at its question mark) and is NOT AVAILABLE HERE: block 2 closes that property to
     .hero-title and .display, "never to .quote", and this is a LABEL-register element. Widening that
     grant is a decision for the display invariant, not something a repair may take in passing. */
  .link-arrow { position: relative; display: inline-flex; align-items: center; gap: 0.5em; text-align-last: right; font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-6); text-transform: uppercase; text-decoration: none; color: var(--brand); }
  /* [LINK v15] :not(.svc-card-link) ON ALL THREE ::after RULES, AND IT IS A CESSION RATHER THAN AN
     EXCEPTION. On a service card this anchor carries both classes, and 6.9 needs its ::after for the
     stretch overlay that makes the card one click target; these three and 6.9's rule named the same box
     at the same specificity, so before v15 the later block simply took it and the overlay never existed.
     One pseudo-element, one owner: on a card link 6.9 owns ::after and paints this same rule as a
     background instead (identical geometry, timing and curve - the arithmetic is written out there).
     Everywhere else - 18 of the 28 .link-arrow instances in src/pages - nothing about this changes. */
  .link-arrow:not(.svc-card-link)::after { content: ""; position: absolute; left: 0; right: 0; bottom: -0.4em; height: 1px; background: currentColor; transform: scaleX(0.25); transform-origin: left; transition: transform var(--dur-slow) var(--ease-out); }
  /* align-items: center stays on the container and still governs the TEXT item; it is simply never the
     shorter of the two, so only the glyph needed its own alignment. The two margin-bottoms are one
     declaration twice - see the [LINK v26] note above for the arithmetic and for the fallback. */
  .link-arrow svg { flex: none; align-self: flex-end; margin-bottom: 0.241em; margin-bottom: calc((1lh - 16px) / 2); transition: translate var(--dur-fast) var(--ease-out); }
  :where(html:not(.has-touch)) .link-arrow:hover { color: var(--ink); }
  :where(html:not(.has-touch)) .link-arrow:not(.svc-card-link):hover::after { transform: scaleX(1); }
  :where(html:not(.has-touch)) .link-arrow:hover svg { translate: var(--nudge) 0; transition: translate 0.42s var(--ease-out) 0.09s; }
  .link-arrow:not(.svc-card-link):focus-visible::after { transform: scaleX(1); }
  /* [HOVER v23] COLOUR PARITY ON FOCUS, HERE AND ON .svc-card-link AND .contact-list. This block's own
     rule - "Every :hover has a matching :focus-visible OUTSIDE the gate" - was kept by every family that
     rests NEUTRAL (nav, mobile menu, crumbs, the four footer families all send focus to --ink) and broken
     by exactly the three that rest RED. Their focused state kept --brand's 2.99:1 while every other
     focused link on the site sat at 17.73:1, which is the inversion nobody would choose: the three links
     that need the warm-up most were the three that only got it from a mouse. Focus now takes --ink here
     too. The RING is unaffected and is still the indicator 2.4.7 and 1.4.11 are satisfied by; this is the
     colour half, and it is a legibility gain on a recorded failure rather than an indicator. */
  .link-arrow:focus-visible { color: var(--ink); }

  /* ---- 6.20 BREADCRUMB - the quietest rung: a trail is orientation, not a destination -------------
     [MOTION v20] THE COLOUR CHANGE IS TRANSITIONED NOW AND WAS NOT BEFORE. Both states below were already
     authored - hover and focus both warmed to --brand-bright - and neither had a transition of any kind, so
     the breadcrumb was the one AFFORDANCE in this layer that answered a pointer with a hard cut while
     every other link on the site faded. That is not quietness, it is the loudest possible version of the
     same change, and the block's own sentence is what makes the repair obvious rather than the addition.
     COLOUR ONLY, AND NO NUDGE, on that same sentence. The footer links two blocks down DO take the
     lateral nudge in this pass and this one deliberately does not: a footer link is a destination and a
     crumb is a position on a map. The register argument is the heading of this block and it decides the
     question without needing a second one. */
  .crumbs ol { display: flex; flex-wrap: wrap; align-items: center; list-style: none; padding: 0; font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-8); text-transform: uppercase; color: var(--ink-2); }
  .crumbs li + li::before { content: "\002F"; margin-inline: var(--space-1); color: var(--ink-3); }   /* 5.61:1 on --bg */
  .crumbs a { color: var(--ink-2); text-decoration: none; transition: color var(--dur-fast) ease; }
  .crumbs [aria-current="page"] { color: var(--ink-3); }
  /* [HOVER v22] --ink for both, and the crumb is the one place in the file where the new scheme is
     QUIETER than the old rather than louder, which is exactly right for this register. A crumb rests at
     --ink-2 (8.44:1) and used to warm to a pink that was a different colour from anything around it; it
     now resolves to the ink of the page, 17.73:1 - a step UP the same neutral ramp it already sits on
     rather than a step sideways into the accent. A trail is orientation, not a destination, and a
     destination colour was the wrong promise for it. */
  :where(html:not(.has-touch)) .crumbs a:hover { color: var(--ink); }
  .crumbs a:focus-visible { color: var(--ink); }

  /* ---- 6.21 SITE FOOTER --------------------------------------------------------------------------
     --bg-0 (L 0.00225) is the footer sub-ground, never a text ground for a marginal colour - which is
     why the pair test does not bind on it - and every ink measures HIGHER here than on --bg (--ink-2
     8.69:1 against 8.44:1, --ink-3 5.78:1 against 5.61:1; recomputed from the shipped tokens, the
     8.90 / 5.28 recorded here before belonged to a darker sub-ground than the one that shipped). The
     direction is what this block spends, and it holds. The hairline top is the seam the CTA band's
     gradient runs into, so the two surfaces read as one. FOUR COLUMNS -> TWO -> ONE, on the two structural numbers and nothing
     else: four straight to one is the source site's clearest case of stacking where it should
     restructure - it tripled the footer's height on a tablet.
     TWO CHILDREN OF .container, NOT ONE, and that is what `> * + *` is a rule about: the four-column
     .footer-grid, then .footer-bottom below it. Collapsing .container and .footer-grid onto one
     element turns that rule into a rule about grid ITEMS and gives every column after the first a
     stray --space-4 inside its own cell, so the four columns stop sharing a baseline. .footer-bottom
     is a SIBLING of the grid rather than a grid item with `grid-column: 1 / -1` (which an earlier
     draft of the partial promised): a full-width row that owes nothing to the column track is
     honestly a block below the grid, and `* + *` already spaces it.
     THE VOCABULARY MOVED TO MEET THE PARTIAL IN TWO PLACES, both because the partial was right:
       .footer-nav sits on the <ul>, not on a <nav> wrapper. Column 3's hub list is deliberately NOT a
         second landmark (two <nav>s in one footer is landmark clutter), so the styled thing is the
         LIST, and `nav.footer-nav > ul` could only ever have matched column 2.
       .footer-logo sits on the <img>. The footer mark is deliberately not a link - the header mark
         already goes to / and a second identical destination is noise for a keyboard and a screen
         reader - so `a.footer-logo` names an element that must not exist. */
  .site-footer { background: var(--bg-0); border-top: 1px solid var(--line); padding-block: var(--space-6) var(--space-4); color: var(--ink-2); }
  .site-footer > .container > * + * { margin-top: var(--space-4); }
  /* COLUMN 1 IS THE MARK'S OWN WIDTH, not a quarter. At `repeat(4, 1fr)` the narrowest four-column
     container (1016.99px: .container is min(100% - 55.01, 1224) and this band starts at 1072px
     [REPAIR v10]; it was 968.99px at 1024) gives 254.2px a column - so the 309.3px mark would be capped by max-width and letterboxed by object-fit
     at ~62px tall, which is the HEADER's cap, and the claim below that the sub-line is legible here
     would be false. Sizing the track from the mark instead: 4.75rem x 3.830 (the artwork's 2505x654
     ratio) = 309.3px, leaving (968.99 - 309.3 - 3 x 27.51) / 3 = 192.4px for each link column, against a
     widest hub label of "Intellectual Property" = 10.200em x 17px = 173.4px in Montserrat 400 (footer
     links inherit the body face at --text-0, sentence case, untracked). FITS WITH 19.0px, where the same
     label in Titillium 400 measured 8.555em / 145.4px and fitted with 47.0. [FACE v9] THE MARGIN IS DOWN
     TO A FIFTH OF A LABEL AND IS THE TIGHTEST FIT IN THIS FILE: Montserrat is 19.2% wider on this string,
     the mark's track is fixed by the artwork's ratio and cannot yield, and the container at this width is
     capped by the viewport rather than by --container. It still fits, so nothing moves; but a hub label
     longer than "Intellectual Property" by more than two characters would wrap here, and the next label
     added to site-plan.json must be measured against 192.4px before it ships.
     [v7 REPAIR PASS, still the governing correction] This label is the BODY face, not the display face:
     the sum once used Source Serif 4's 9.194em for a string that is set in the sans, and the same
     substitution error is the one the nav panel's sum made (6.1). Measure the face the element actually
     renders in.
     Written as calc() off --logo-h so the track can never drift from the mark it exists to hold. */
  .footer-grid { display: grid; grid-template-columns: calc(var(--logo-h) * 3.830) repeat(3, 1fr); gap: var(--space-4) var(--space-3); }
  .footer-col { display: grid; align-content: start; gap: var(--space-2); }
  .footer-head { font-size: var(--text-label); font-weight: var(--wt-label); letter-spacing: var(--track-3); text-transform: uppercase; color: var(--ink); }
  .footer-nav, .footer-social { list-style: none; padding: 0; display: grid; gap: var(--space-1); }
  .footer-social { grid-auto-flow: column; justify-content: start; gap: var(--space-2); }
  /* A RESTING affordance: on touch nothing else marks these as links - and in THIS block nothing else
     marks them at all. .site-footer sets color: var(--ink-2) and these links take the same --ink-2, so
     link and non-link are the same colour, and footer.html puts the un-linked fax number between the
     linked phone and the linked e-mail where all three read identically. The underline is therefore the
     sole differentiator, which is exactly --line-control's job; at --line-strong it measured 1.57 on
     --bg-0. Raising the LINK colour instead was refused: --ink over --ink-2 is only 2.0 apart, and the
     footer's quiet is a decision this block makes twice over. */
  /* [MOTION v20] THE FOOTER LINKS TAKE THE NUDGE. This is the site's longest list of destinations - four
     columns of them on every page - and until this pass the only thing that happened when a pointer
     reached one was a colour change. They are AFFORDANCE by 6's own test (they lead somewhere), and every
     other affordance on the site answers with colour AND travel; the footer was quiet on the wrong axis.
     THE QUIET THIS BLOCK DEFENDS IS A COLOUR DECISION AND SURVIVES INTACT, which is the only reason the
     nudge is allowed in here at all. The two paragraphs above refuse to raise the link colour and refuse
     to drop --line-control, and neither is touched: at rest these links are still --ink-2 with a
     --line-control underline and still indistinguishable from the fax number beside them. Motion is a
     different axis from tone, and a 4px lateral step on hover says "this one is a link" without making
     the resting footer any louder - which is exactly what the block wanted and could not get from colour.
     LATERAL, NOT VERTICAL, and the direction is the argument: these sit in tight grids at --space-1
     (10.51px) of row gap, so a rise would carry a link into the row above it. Sideways there is nothing
     to collide with. `translate` over --dur-base, the same rung the card and the button rise on. */
  .footer-nav a, .footer-social a, .footer-contact a, .footer-payments a { color: var(--ink-2); text-decoration: underline; text-decoration-color: var(--line-control); text-underline-offset: 0.18em; transition: color var(--dur-fast) ease, text-decoration-color var(--dur-fast) ease, translate var(--dur-base) var(--ease-out); }
  .footer-nav [aria-current="page"] { color: var(--ink); }
  .footer-contact { font-style: normal; display: grid; gap: var(--space-1); }
  /* The gap is load-bearing: the glyph and its word are two flex items with nothing between them, so
     without it the icon touches the F of Facebook. --space-1 is the same air the lists take. */
  .footer-social a { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1); min-width: 44px; min-height: 44px; text-decoration: none; }
  /* currentColor only: a rebound scope gets the safe colour with no rule naming the icon. 20px, and
     the partial's width/height attributes must say 20 too - the box that is reserved and the box that
     is painted disagreeing is how an 18px icon later reads as intentional. */
  .footer-social svg { width: 20px; height: 20px; fill: currentColor; }
  /* The full 4.75rem mark: the sub-line's caps are legible here, where nothing competes for the
     row - which is what pays for the header's 3.75rem cap (6.1) - and the track above is sized to let
     that be true. [RED v7] They are 7.66px now rather than the ~7.2px floor exactly, because the mark
     grew with the root while the floor (an absolute px fact about screens) did not: this is the one
     place on the site where the bigger root buys legibility outright. On the <img> itself: there is no
     wrapper to hang it on. */
  img.footer-logo { height: var(--logo-h); width: auto; max-width: 100%; object-fit: contain; }
  /* The one running-text block in the footer, so it takes the measure every running-text block takes.
     [RED v7] IT NO LONGER BINDS ANYWHERE, and the cap is kept anyway. The widest column this paragraph
     can occupy is the two-column band's 494.7px (at 1072px of viewport, (1016.99 - 27.51) / 2), against
     --measure's 584.0px; the four-column band gives it the mark's own 309.3px track and the one-column
     band 331.76px. [FACE v9] The margin WIDENED - --measure went 497.3 -> 584.0px while the columns it
     is measured against did not move - so the cap is further from binding than it was, not nearer. The
     rule is now a guard rather than an active constraint - which is the correct state
     for "any running-text block takes the measure", and cheaper to keep than to re-justify later. */
  .footer-blurb { max-width: var(--measure); }

  /* ---- THE BOTTOM ROW. A band below the grid, separated by `* + *` above and re-stated as its own
     surface by the rule it hangs under. It carries the size and ink for all three paragraphs so each
     one only has to declare what makes it different. NO --measure CAP, deliberately and as an
     exception: the disclaimer is 700 characters of verbatim bar-advertising text that may not be
     edited, it is scanned rather than read, and holding it to the 70-character rule at --text-label
     (70 x 0.49072 x 13.36 = 458.9px = 26.99rem in Montserrat; it was 390.8px = 22.99rem in Titillium -
     a wider ribbon of the same ten lines, because 700 characters at 70 a line is ten lines in any face)
     would stand it up as a ten-line ribbon under a four-column footer. Full width
     is the shipped decision for .footer-legal already; this keeps the three lines to one measure
     rather than inventing a second.
     WHAT EACH OF THE THREE PARAGRAPHS TAKES FROM THIS RULE, stated here because two of them have no
     rule of their own and their silence is the decision, not an omission:
       .footer-disclaimer  everything - it carries real information, so it stays at --ink-2's 8.69:1
                           and sentence case, INTENTIONALLY INHERITED. It recedes by SIZE and by sitting
                           below a rule, never by dimming toward --ink-3. Do not add a rule for it just
                           to satisfy the vocabulary list.
       .footer-payments    everything but its links, which join the footer link group below.
       .footer-legal       everything but the tracking, caps and ink it declares one line down. */
  .footer-bottom { padding-top: var(--space-3); border-top: 1px solid var(--line); font-size: var(--text-label); line-height: var(--leading-body); color: var(--ink-2); }   /* 8.69:1 on --bg-0 */
  .footer-bottom > * + * { margin-top: var(--space-2); }
  /* The copyright is a LICENCE STRING - block 0's grant lists them beside eyebrows and counter units -
     so it takes the bottom tracking rung and the quietest ink. It recedes by size and by tracking,
     never by dimming below the floor. ONE NAME: the .footer-copy this block used to carry was a second
     name for this same line and is deleted rather than left as a hook nothing writes. */
  .footer-legal { letter-spacing: var(--track-8); text-transform: uppercase; color: var(--ink-3); }   /* 5.78:1 on --bg-0 */
  :where(html:not(.has-touch)) .footer-nav a:hover,
  :where(html:not(.has-touch)) .footer-social a:hover,
  :where(html:not(.has-touch)) .footer-contact a:hover,
  :where(html:not(.has-touch)) .footer-payments a:hover { color: var(--ink); text-decoration-color: currentColor; translate: var(--nudge) 0; }
  /* Keyboard parity, outside the gate and quieter per this layer's convention: the colour, not the
     travel. The ring is the focus indicator and it is already the loudest thing in the footer; a row that
     also slid sideways under a ring would move the indicator with it.
     [HOVER v22] --ink for all eight declarations - four families, hover and focus. These links rest at
     --ink-2, which is 8.69:1 on the footer's own --bg-0 sub-ground rather than 8.44:1 on --bg, and they
     resolve to --ink's 18.26:1 there. The underline follows through currentColor exactly as it did, so
     the whole link - type and rule - arrives at one colour instead of at a red the footer contains
     nowhere else. */
  .footer-nav a:focus-visible, .footer-social a:focus-visible, .footer-contact a:focus-visible, .footer-payments a:focus-visible { color: var(--ink); }
  /* The mark's track goes back to 1fr once the grid is two columns: --logo-h has already rebound to
     4rem (260.4px) by then and the widest two-column column is (968.99 - 27.51) / 2 = 470.7px. */
  @media (max-width: 63.99rem) { .footer-grid { grid-template-columns: repeat(2, 1fr); } }
  @media (max-width: 29.99rem) { .footer-grid { grid-template-columns: 1fr; gap: var(--space-3); } }

  /* ---- 6.22 SKIP LINK: declared ONCE, in base, and deliberately NOT re-declared here. Its z-index
     100 already clears the fixed header's 90 and its inset ring already clears the viewport edge.
     Repeating it to win by position is exactly what the layer contract exists to prevent. */

  /* ---- 6.23 404 ----------------------------------------------------------------------------------
     The page carries no `js` class, no [data-reveal] and no scripts, so every decoration renders from
     its FINISHED default - the eyebrow's hairline, the link-arrow's resting rule, the button hover -
     with nothing to un-hide. The only styling it needs of its own is a hero with no photograph: no
     .hero-media means no scrim (the scrim lives on .hero-media::after), so the copy sits on the plain
     ground at 17.73:1, and the min-height comes down because there is no establishing shot to hold. */
  .hero--page:not(:has(.hero-media)) { min-height: 0; }
  .hero--page:not(:has(.hero-media)) .hero-content { padding-bottom: var(--space-4); }

  /* ---- 6.24 PHONE AND LANDSCAPE RE-SOLVES --------------------------------------------------------
     Both queries use the foundation's own numbers. Every display: none written above is checked at
     812 x 375: .site-nav and .nav-cta are hidden there (812 < 1072), .nav-toggle is shown, and the
     hero's min-height is released HERE rather than in a query of its own. */
  @media (max-width: 29.99rem) {
    /* The title drops one rung: at --text-4 it is 44.51px on a 375px viewport (11.9%, against 3.1% at
       1440) and its 16ch cap no longer protects the line length. [FACE v9] THE font-family REBIND IS
       DELETED, not forgotten: it existed to move the phone hero off the --text-4-only synthetic family
       ("Source Serif 4 Fallback Hero") and back onto --font-display's band family, and there is no
       second family left to move off - .hero-title inherits --font-display from the grant at every
       width. One family, one size-adjust, nothing to rebind. */
    .hero-title { font-size: var(--text-3); max-width: none; }
    .hero-sub { max-width: none; }
    .hero-actions .btn { flex: 1 1 100%; }   /* two full-width buttons beat two clipped ones */
    /* [RED v7] 3.4rem NO LONGER FITS AND HAD TO MOVE - the one rem value in this file the root multiplier
       forced down rather than merely re-annotated. The available row at 320px is a VIEWPORT quantity that
       did not scale while everything in it did: 320 - 43.25 phone gutters - 44px toggle - 27.51 --nav-gap
       = 205.25px, against 3.4rem x 3.830 = 221.4px. (The figure this comment used to carry, "16px gap",
       named --space-2; the row's gap is --nav-gap = --space-3 below the compact band. It was wrong then
       and happened not to matter, because 3.4rem still fitted against the true 209.4px.) 3.1rem x 17 =
       52.7px x 3.830 = 201.8px fits with 3.4px, so that is the cap. Sub-line caps 5.0px here: not
       legible, and not meant to be - the same trade the collapsed header already made. */
    .site-logo img { height: 3.1rem; }
    .num-list li { padding-left: 2.75rem; }
    .display { font-size: var(--text-2); }   /* one rung down: --text-3 is 9.3% of a 375px viewport */
  }
  @media (max-height: 480px) and (orientation: landscape) {
    :root { --header-h: 4.5rem; }            /* 3rem logo (51px) + 2 x 0.75rem (12.75px) = 76.5px; the full 97.75px was 26% of a 375px height */
    .site-logo img { height: 3rem; }
    .header-inner { padding-block: 0.75rem; }
    .hero, .hero--home, .hero--page { min-height: 0; }
    .hero-content { padding-top: calc(var(--header-h) + var(--space-2)); padding-bottom: var(--space-3); }
    /* NO align-content REPAIR HERE ANY MORE: 6.2's base rule is `safe center`, which starts the rows at
       the padding edge whenever the content overflows - in this query and in every portrait phone the
       landscape-only version never matched. One owner.
       THE ROWS SHED PADDING, NOT SIZE. --text-2 stays, and the rejected alternative is recorded because
       it is the obvious one: dropping to --text-1 would put a GRANTED SERIF HOOK exactly on
       --display-floor (both are 1.272rem), and the floor is provisional until the ClearType/Mac viewing
       test - the whole reason the smallest serif hook sits a full rung above it is so that test can land
       anywhere in 1.272-1.618rem without moving a hook. It would also break the tap floor: 21.62 x 1.15
       leading = 24.87 + 2 x 0.45rem (15.30) = 40.17px, under 44. At --text-2 the shed padding still
       clears it: 27.51 x 1.15 = 31.63 + 15.30 = 46.93px (44.14px was the figure measured at 812 x 375 at
       the 16px base; the rule is unchanged and the margin widened). The panel scrolls in landscape either
       way (13 rows was ~2.5 screens at 375px tall: 1042px of content in 375px, measured at the old base,
       and every row is 6.25% taller now), so size was never what made the links reachable - `safe center`
       is. */
    .mobile-menu a { padding-block: 0.45rem; }
  }

  /* ---- 6.25 PRINT --------------------------------------------------------------------------------
     A printed page must work when everything else has gone. Rather than a parallel light stylesheet -
     a second set of colour decisions that would drift - print REBINDS the same tokens the .band--ivory
     safety net rebinds, so every component inherits a WCAG-checked light value with no rule naming it.
     Chrome is removed: a fixed header reprints on every sheet, and an overlay and a closing CTA carry
     nothing on paper. */
  @media print {
    :root {
      --bg: var(--ivory); --bg-0: var(--ivory); --bg-2: var(--ivory); --bg-3: var(--ivory);
      --ink: var(--ivory-ink);         /* 17.32:1 on ivory */
      --ink-2: var(--ink-muted-ivory); /* 5.39:1 */
      --ink-3: var(--ink-muted-ivory);
      /* [RED v7] --brand-fill IS DELIBERATELY NOT REBOUND HERE, exactly as in .band--ivory: --brand-ivory
         holds the same value --brand-fill does, so the fill already prints its correct in-band value
         and a line here would be a no-op the next editor would have to verify. Consequence, stated
         because it costs toner: a .btn prints as a solid #BF0000 plate with its white ink rather
         than as an outline. That is the right answer - a filled control that prints as an outline reads
         as a disabled one - and print-color-adjust: exact below is what makes it survive. */
      --brand: var(--brand-ivory);     /* 5.79:1 */
      --brand-deep: var(--brand-ivory);
      /* [HOVER v22] TWO LINES LEFT THIS BLOCK and both left because their tokens did: --brand-bright,
         which used to rebind here beside --brand-deep, and --brand-ink, whose [v7 REPAIR PASS] note read
         "--brand-ink follows --brand-bright ... near-black on it fails where white does not". The list is
         shorter, not weaker - print and .band--ivory are still byte-identical in what they rebind, which
         is the only thing that has ever stopped the two light contexts drifting apart, and the reason
         neither needs --brand-fill-hover is the same one --brand-fill's own line gives: an in-band plate
         wants a dark red and #8F0000 is one. There is no hover on paper in any case. */
      --danger: var(--danger-ivory);   /* 5.51:1 */
      --ink-rgb: 21, 9, 9; --bg-rgb: 243, 241, 237;
      /* Composed from the ink rebound two lines above: var() resolves against this element's own
         computed custom properties, so no grey is retyped. THE ALPHAS ARE THE LIGHT-GROUND RUNGS, not the
         shipped dark ones: dark-on-light needs far more alpha for the same weight, and --line-control is
         solved to the non-text floor exactly as its dark twin is. .band--ivory carries the three ratios. */
      --line: rgba(var(--ink-rgb), 0.20); --line-strong: rgba(var(--ink-rgb), 0.35); --line-control: rgba(var(--ink-rgb), 0.49);
      --wt-body: 300;                  /* dark-on-light blooms: the sans steps down one shipped file */
      --wt-display: 400;
    }
    html, body { background: var(--ivory); color: var(--ivory-ink); font-weight: var(--wt-body); -webkit-print-color-adjust: exact; print-color-adjust: exact; }
    /* .cursor-glow joins the list for two independent reasons: a fixed overlay reprints on every sheet,
       and this block rebinds every ground to ivory - where a screen-blended light costs a dark-on-light
       pair ~4% of its contrast ratio (the token block's rule (b)) and buys nothing on paper, where there
       is no pointer to follow. */
    .site-header, .mobile-menu, .nav-toggle, .skip-link, .cta-band, .hero-media, .form, .form-sent,
    .cursor-glow { display: none; }
    .hero, .hero--home, .hero--page { min-height: 0; }
    .hero-content { padding-block: 0 var(--space-3); }
    .section, .section--tight { padding-block: var(--space-3); }
    .svc-card, .act-card, .package, .contact-card, .quote-card { break-inside: avoid; background: none; }
    a { text-decoration: underline; }
  }

  /* ---- 6.26 THE POINTER LIGHT (.cursor-glow) -----------------------------------------------------
     Numbered after the print block so no existing number moves; source order is irrelevant inside a
     layer, and 6.25 is a media query that contains nothing of this.
     WHAT IT IS: two lobes of light, not one - a bright LEAD at #FF7070 and a deep TRAIL
     at --brand-deep (#BF0000, the mark's own red), painted once each into a fixed-size box and then
     MOVED. [RED v7] Both lobes moved with the accent ladder and both peaks rose 1.38x; the solve was run
     against THESE colours and not the coral pair they replace, which matters because a pinker lead costs
     slightly more contrast than a coral one at the same alpha - though the hue-0 pair happens to allow
     marginally more light overall (a 0.0763 ceiling against 0.0742). [RED v8] NEITHER LOBE COLOUR MOVED
     AGAIN - #FF7070 and --brand-deep were the two stops the red collapse did not touch - and both
     peaks rose another 1.62x, for the reason the token block gives at length: red text left the AA budget
     on the firm's instruction, so the ceiling is now --ink-3's. The two are chased at
     different rates (main.js: lead 0.45s power3-out, trail 1.05s power2-out, i.e. 2.33x), and THE
     DIFFERENT RATES ARE THE EFFECT: fast movement stretches the pair into a coloured head with a warmer
     tail, and stopping lets them converge into a single light. One object that changes shape with
     velocity, rather than two decorations.
     POINTER-DRIVEN, NEVER AMBIENT. Nothing here loops, pulses or breathes. At an idle reader the light
     is perfectly still, which is what keeps it from becoming a screensaver on a page of legal copy.
     THREE THINGS ARE LOAD-BEARING:
     1 · COMPOSITOR ONLY. The obvious implementation - one full-viewport layer with
         `radial-gradient(circle at var(--x) var(--y))` - repaints the entire viewport every frame and is
         the single easiest way to make a site feel cheap on a laptop. Here the gradient is painted ONCE
         into a box of a fixed size and the box is moved with `transform`, which is a compositor
         operation: no paint, no layout, no style recalc on the page.
     2 · THE CENTRING IS `translate`, NOT A MARGIN AND NOT PART OF THE TRANSFORM. A percentage margin
         resolves against the CONTAINING BLOCK'S INLINE SIZE in both axes, so `margin-top: -50%` would
         be half the viewport WIDTH rather than half the lobe - wrong in the wrong direction, and wrong
         by a different amount at every viewport. The independent `translate` property halves the
         element's OWN box and, because it composes with `transform` rather than competing for it, main.js
         keeps `transform` entirely to itself. That is the playbook's one-owner-per-transform rule used in
         reverse: CSS owns the constant, JS owns the variable, and neither can silently erase the other.
     3 · mix-blend-mode: screen IS ARITHMETIC, NOT A LOOK. screen(b, c) = 1 - (1 - b)(1 - c), so with a
         white backdrop screen(1, c) = 1 EXACTLY, for every c: the light can only ever LIGHTEN, and it is
         mathematically incapable of muddying, tinting or dirtying a white surface. That is protection
         nobody can forget to maintain, as opposed to an exclusion list of selectors that the next
         component forgets to join - and it matters here because the .band--ivory rebinding exists,
         waiting for the first light band (an ivory credentials strip, a letterhead section) to ship.
         THE GUARANTEE HAS A PRECISE EDGE, so it is written down rather than trusted: it protects the
         SURFACE, not the PAIR. On a dark ground the ink and the ground rise together and the ratio barely
         moves; on a light ground the near-white ground is already near its ceiling while the dark ink
         still has its whole range to rise through, so the pair LOSES contrast - measured, --brand-ivory
         on ivory 5.79:1 -> 5.37:1 with the lead lobe alone. The rule that follows from it is in the token
         block: a light band's accents need >= 5.30:1 before the light, not 4.50:1, and [RED v8] raised
         that bar again - from v7's 5.00, which had itself come from 4.70 - because each brighter light
         lifts the true threshold, and that threshold is hue-dependent. Both re-measured thresholds are
         recorded with the ivory tokens; --danger-ivory had to move a rung to stay over its own.
     THE ALPHAS AND THE STOPS are re-derived on every run by tools/verify_ratios.py (worksheet #15,
     originally reference/derivation/derive_glow.py, then scratchpad/{derive_red_v7.py, lock_v7.py}) and
     are recorded in full beside the tokens: peak
     0.123 on the lead against a 0.1474 failure alpha, with the
     falloff mirroring the reference site's 0 / 34 / 58 / 76% (lead) and 0 / 36 / 60 / 78% (trail). Do not
     nudge one of these eight numbers without re-running that check - each stop is a fraction of the peak
     (0.4764 and 0.1276 for the lead; the trail rides at 0.4727 of the lead and halves twice), so a
     hand-edited stop breaks a relationship rather than just a value. THE 0.63 THIS LINE USED TO CARRY WAS
     ALREADY STALE before v7: it described the pre-repair 0.035 trail, while the shipped pair has been
     0.4727 since the stacked-lobe repair. The verifier does not assert this sentence,
     which is exactly how it survived - it re-derives the STOPS, not the prose about them. [RED v8] The
     lead's two inner fractions are written to FOUR places here where they used to be written to three:
     0.476 and 0.127 happened to round to the same stops as 0.4764 and 0.1276 at a 0.076 peak and stop
     doing so at 0.123, which is precisely the kind of relationship a peak change is supposed to expose.
     SIZE: 36vmax and 24vmax, the reference's exact 3:2 - 518px and 346px at 1440 x 900. vmax rather than
     px so the light is the same fraction of the page on a laptop and on a 27-inch display; the lobe is
     always about a third of the longer edge, which is what makes it read as ambient light rather than as
     a cursor ornament.
     Z-INDEX 70, AND THIS IS THE ONE FIGURE NOT TAKEN FROM THE REFERENCE. It ships at 80; 80 in THIS stack
     is .mobile-menu (6.2), and a light appended to <body> would paint over an open overlay while the
     header at 90 stayed unlit - chrome half-lit, which is worse than either extreme. The stack here is
     content (0-3) < LIGHT (70) < overlay (80) < header (90) < skip link (100), and the rule it encodes is
     that the light decorates CONTENT and never chrome: when the menu is open the light is behind it.
     BUILT IN JS, so a phone never receives the markup at all, and lit by TELEPORTING to the pointer on
     first sighting - a placement, never a tween, or the pair flies in from the origin on the first move.
     The opacity fade is CSS's, at --dur-slow, and is the only thing here that transitions. */
  .cursor-glow {
    --glow-size: 36vmax;                 /* the lead's size; the trail rebinds it below */
    position: fixed;
    top: 0;
    left: 0;
    width: var(--glow-size);
    height: var(--glow-size);
    translate: -50% -50%;                /* see 2 above: half of the LOBE, not half of the viewport */
    z-index: 70;
    pointer-events: none;                /* it may never take a click, a hover or a text selection */
    contain: strict;                     /* size + layout + paint + style: safe because the box is sized
                                            explicitly above, and paint containment clips to a border box
                                            the gradient has already faded to zero well inside */
    mix-blend-mode: screen;
    opacity: 0;                          /* unlit until main.js has actually seen a pointer */
    transition: opacity var(--dur-slow) ease;
    will-change: transform;              /* permanent, and correct: the chase never ends, so unlike the
                                            reveal's promotion there is nothing to release. Two elements. */
  }
  .cursor-glow.is-lit { opacity: 1; }
  /* THE STOPS ARE FRACTIONS OF THE PEAK, so the shape survives a change of peak: the lead runs
     peak x (1, 0.4764, 0.1276, 0), which is the reference site's own 0.063 / 0.030 / 0.008 / 0 falloff
     expressed as ratios, at its own 0 / 34 / 58 / 76% positions. Re-derive, never retype. */
  .cursor-glow--lead {
    background: radial-gradient(closest-side circle,
      rgba(var(--glow-lead), 0.123) 0%,
      rgba(var(--glow-lead), 0.0586) 34%,
      rgba(var(--glow-lead), 0.0157) 58%,
      rgba(var(--glow-lead), 0) 76%);
  }
  /* Two thirds the size, 0.4724 of the lead's peak, and 2.33x the lag - always behind, always softer, so
     it can never read as a second cursor. Its own falloff is peak x (1, 0.5, 0.1, 0) at 0 / 36 / 60 / 78%
     - a shorter shoulder than the lead's, which is what keeps the tail reading as a tail. */
  .cursor-glow--trail {
    --glow-size: 24vmax;
    background: radial-gradient(closest-side circle,
      rgba(var(--glow-trail), 0.0581) 0%,
      rgba(var(--glow-trail), 0.0291) 36%,
      rgba(var(--glow-trail), 0.0058) 60%,
      rgba(var(--glow-trail), 0) 78%);
  }

  /* ---- 6.27 PARALLAX FOR FRAMED MEDIA ([data-parallax]) ------------------------------------------
     THE FRAME CLIPS, THE CHILD MOVES. A parallaxing element must overflow its frame or there is nothing
     to reveal as it drifts, so the two jobs belong to two elements: the parent owns `overflow: hidden`,
     the radius and the hairline; the child owns the translation. clip-path appears nowhere in this file
     and is not wanted here - plain overflow is cheaper and does not create a new containing block.
     THE OVERSCAN IS DERIVED FROM THE TRAVEL, not chosen - and THE DERIVATION HAS CHANGED SHAPE TWICE,
     BOTH TIMES BECAUSE THE DRIFT MOVED PROPERTIES. Both derivations are kept, because the shipped number
     is the same one and a reader who finds only the last of them cannot tell whether it was checked.
       ORIGINALLY, drift and magnification in ONE `transform` (matrix T x S), so the percentage translate
       was NOT scaled: s >= 1 + 2p = 1.08 at the shipped p = 0.04 - 4% of overspill at each edge against a
       4% excursion, exact, nothing spare.
       [ZOOM v11], magnification moved to its own `scale` because 6.28 needed `transform` free. The
       individual transform properties compose in a fixed order - translate, rotate, SCALE, then
       `transform` - so the matrix became S x T and the drift was magnified along with the picture:
           overspill per edge = (s - 1) / 2      visual excursion = s x p
           (s - 1) / 2 >= s x p   ->   s >= 1 / (1 - 2p)   ->   s >= 1 / 0.92 = 1.08696
       1.08 no longer covered it; 1.09 was the smallest two-decimal value that did, with 0.0450 of
       overspill against 0.0436 of excursion and 0.0014 of the frame's height spare.
       [PHOTO v16], THE DRIFT MOVED OUT OF `transform` INTO THE `translate` PROPERTY, and the order
       reverted with it. `translate` composes OUTSIDE `scale` in that same fixed order, so the drift is
       once again NOT magnified and the floor is once again s >= 1 + 2p = 1.08. WHY IT MOVED IS 6.29's
       and is not an aesthetic: with the drift inside `transform`, 6.29's hover push multiplied it, so
       hovering a parallaxed figure changed the scrub's amplitude by 2.8% and shifted the picture inside
       its frame by up to 0.6055px - measured, scroll-position dependent, and the one number 6.29's own
       binding constraint asks to be zero. Independent properties, independent effects.
     THE SHIPPED VALUE IS STILL 1.09 AND DID NOT MOVE WITH THE FLOOR, deliberately: it now clears 1.08 by
     0.0050 of the frame's height (0.0450 of overspill against 0.0400 of excursion) instead of by 0.0014,
     so the change BOUGHT margin rather than spending it, and re-cutting it to 1.08 would put the site
     back on a floor met with exactly zero to spare while changing every downstream figure - 6.28's
     `to` value, --frame-scale, 6.29's 1.12052 - for nothing a visitor could see. (The playbook's own
     frames run +-6% and 1.12; the calmer figure is still the one this site's register asks for.)
     THE SCALE IS HELD ON THESE FRAMES, NEVER ANIMATED - and that sentence now needs a name on it. It is
     held for .feature-media and .cta-media, where the picture DRIFTS and does not zoom: tween the scale
     there and the effect stops being parallax and becomes a Ken Burns pan, which is a different register.
     .hero-media img is the ONE deliberate exception, and it is an exception the property split is what
     makes safe: 6.28 animates `scale` and nothing else, this block animates nothing and owns `translate`
     and nothing else, so the two motions have one owner each and cannot erase one another. [ZOOM v13]
     6.28's keyframes now END on this block's 1.09 rather than starting from it - the zoom reversed and
     shortened to a 1.5s entrance ([MOTION v20]; it was one second until then) - so on a page carrying both, the picture settles onto exactly
     the framing a still page holds, and the resting value here and the animation's `to` are one number
     rather than two kept in step by hand.
     THE JS CONTRACT IS ONE NUMBER, exactly as the reveal cascade's is. main.js publishes the scrub as
     `--parallax-y: <n>%` on the img and writes NOTHING else - no inline transform, no inline style. The
     alternative is the playbook's own shape (JS owning the whole `transform`, scale restated identically
     in both objects so it is held), and it works, but it costs this file the arithmetic: an inline
     transform outranks every author rule, so the scale above would be dead text while the number that
     actually shipped lived in a JS literal, with nothing to keep the two in step. Publishing a number
     instead keeps ONE owner per property - CSS owns `translate`, JS owns `--parallax-y` - and gives the
     effect a correct failure mode: if the script never runs, or the observer never fires, the property
     keeps the 0% default declared here and the picture sits centred and still.
     TWO GATES, both deliberate. (a) min-width: 67rem - the structural number, repeated as a literal
     because a media query cannot read a token (grep --bp-structural before changing it).
     [ZOOM v11] THIS LITERAL WAS STALE AND IS REPAIRED HERE. [REPAIR v10] moved --bp-structural 64 ->
     67rem and moved main.js's TWO mirrors of it (navUp and initParallax, whose comment already claimed
     "the same 67rem the stylesheet uses") and MARKUP-CONTRACT.md's parallax paragraph - but this query
     was left at 64rem. The window it opened was real: from 1024px to 1071px CSS applied the overscan and
     the `--parallax-y: 0%` default while main.js's own 67rem gate kept the scrub switched off, so every
     parallax frame in that 48px band carried a permanent 4% crop of the photograph in exchange for no
     motion at all - which is precisely the trade gate (b) below exists to forbid. Three consumers now
     read one number; a fourth must read it too. Below it the
     split is one column, the frames are the full width of a small screen, and a scrub is a style write
     per scroll frame on the device least able to afford one - the same argument that keeps the effect
     off the closing band: an ambient effect that runs whether or not anyone is looking has to justify
     its cost, and here it cannot. (b) html.js - with no script there is no scrub, so there must be no
     overscan either: an unconditional 1.08 would crop 4% off a photograph forever in exchange for
     nothing.
     AN ELEMENT ALREADY SCRUBBING AND ALSO ZOOMING ON HOVER - [PHOTO v16] THIS PARAGRAPH USED TO FORBID
     THE COMBINATION AND NOW GOVERNS IT, BECAUSE THE FIRM ASKED FOR IT BY NAME. What stood here was: "Two
     motions on one object with two different causes read as busy however small each one is, and here they
     would also be two owners of one property. That is why .feature-media is MATERIAL (it has no hover
     response at all)". The second sentence is now flatly false - 6.29 gives .feature-media img a hover
     push, on the instruction that EVERY framed photograph on the site answer the pointer - so the first
     is restated on the half of it that survives:
       THE PROPERTY-OWNERSHIP HALF SURVIVES INTACT AND IS THE REAL TEST. A frame may scrub and answer a
       pointer only if the two effects do not contend for one property. They do not. The drift is
       `translate` and only ever `translate` ([PHOTO v16]: it was `transform` until the composition ORDER
       turned out to couple the two even though the property names did not collide - see the derivation
       above). The push is `scale` - and the overscan below is NOT a second
       owner of `scale`, it is the RESTING TERM of 6.29's single expression, published as --frame-scale
       and multiplied there. One property, one expression, composition rather than collision: the same
       split 6.28 made for the hero, used a second time and for the same reason.
       A TRANSITION ON THE DRIFT'S PROPERTY IS STILL FORBIDDEN ON THIS ELEMENT, for the reason it always
       was: main.js rewrites --parallax-y every scroll frame, so a tweened `translate` would smear the
       scrub into a laggy lag. 6.29 transitions `scale` alone, which never touches it. That is not a nicety of the
       design; it is the constraint that decided which property the push could use.
       THE "TWO MOTIONS, TWO CAUSES" RULE IS NOT REPEALED - IT CARRIES ONE RECORDED EXCEPTION, given
       deliberately by the firm and confined to the frames it named. The cost, stated: a visitor who
       hovers a feature photograph while scrolling gets an ambient drift and a pointer-caused push at
       once. The one-line revert, if the firm ever prefers the old rule to the new instruction, is to drop
       [data-parallax] from the two .feature-media figures - which is what the old wording would have
       forced. It is recorded here rather than taken, because taking it would remove an effect the firm
       did not ask to lose.
       .svc-card-media img STILL MAY NEVER TAKE THIS ATTRIBUTE (noted in 6.9 too), now on its own grounds
       rather than on the register's: it sits inside a CONTROL that already answers the pointer four ways
       before its picture answers at all, and an ambient scroll drift on top of that would make it the
       busiest object on the site. Nothing has asked for that exception; this one is not it.
     The frame stays put; the picture carries on doing what it was already doing, plus 2.8% for as long
     as a pointer rests on it.
     REDUCED MOTION removes it in the guards block, and that rule is not optional tidiness: the clamp
     there neutralises TRANSITIONS and ANIMATIONS, and a scroll-linked transform is neither. */
  /* THE FRAMES ARE ENUMERATED, NEVER WILDCARDED, and a new frame must join BOTH lists or the attribute
     is inert in CSS while main.js goes on publishing --parallax-y to it. [ZOOM v11] .cta-media - the
     closing band's photograph (6.15a), the home page's when this line was written and, since
     2026-09-07, the twelve practice-area bands' as well - is the third frame. THE ROLLOUT COST THIS
     BLOCK NOTHING, which is the point of enumerating a CLASS and not a page: twelve pages gained the
     frame and no selector here moved. [PHOTO v16] THE ELIGIBILITY TEST IT
     QUALIFIED ON IS RESTATED HERE TOO, because the old spelling would now disqualify a frame that is on
     this very list. It read "a frame may take this attribute only if it is MATERIAL, with no hover
     response of any kind"; .feature-media has one since 6.29 and is still eligible. The test that
     actually held all along is PROPERTY OWNERSHIP - a frame may scrub AND answer a pointer provided the
     drift keeps `transform` to itself, which 6.29 guarantees by pushing on `scale`. .cta-media clears it
     twice over, having no hover response at all: it is class (b) in 6.29's survey - full-bleed, under a
     scrim, behind its own band's copy at z-index -2 (it was ALSO display: none below 29.99rem until
     the firm reversed that step on 2026-09-08 - see 6.15a - and the picture now shows at every width;
     this frame's eligibility is unmoved either way, because every rule in this block is gated at
     67rem and a phone never scrubbed it) - and 6.29 records
     it as EXCLUDED FROM THE FIRM'S "every static photo" WITH REASONS, for the firm to overrule if it
     wants to. It EARNS the effect because a still photograph needs a reason to
     feel alive, which is also the exact asymmetry with the home hero: that frame's <video> is barred
     from the attribute because moving footage is already moving, and scrubbing it would be the same
     two-motions-one-object fault that bars .svc-card-media img. Still picture, give it drift; moving
     picture, leave it alone. (On the home page .hero-media may keep the attribute harmlessly - it has no
     <img> child any more, so the rule below matches nothing and main.js's querySelector("img") returns
     null and skips the frame.) */
  .feature-media[data-parallax], .hero-media[data-parallax], .cta-media[data-parallax] { overflow: hidden; }
  @media (min-width: 67rem) {
    html.js .feature-media[data-parallax] img,
    html.js .hero-media[data-parallax] img,
    html.js .cta-media[data-parallax] img {
      --parallax-y: 0%;                  /* main.js overwrites this, and only this */
      translate: 0 var(--parallax-y);    /* THE DRIFT. One property, one owner, and the property the
                                            reduced-motion guard in block 5 neutralises by name.
                                            [PHOTO v16] `translate`, NOT `transform: translateY()`. The
                                            two render identically on their own; they differ in where
                                            they sit in the composition. `transform` is applied INSIDE
                                            `scale` (the order is translate, rotate, scale, transform),
                                            so while the drift lived there 6.29's hover push multiplied
                                            it - the scrub gained 2.8% of amplitude and the picture
                                            shifted up to 0.6055px inside its frame for as long as a
                                            pointer rested on it, varying with scroll position. In
                                            `translate` the drift composes OUTSIDE `scale` and the two
                                            are independent by construction. The derivation above moved
                                            with it: the floor is 1 + 2p again, not 1 / (1 - 2p). */
      --frame-scale: 1.09;               /* THE OVERSCAN, re-derived above for the S x T composition
                                            order. On .hero-media img 6.28's animation takes the `scale`
                                            property over for 1.5s and hands it back on exactly this
                                            value: its `to` IS this 1.09 ([ZOOM v13]).
                                            [PHOTO v16] PUBLISHED AS A CUSTOM PROPERTY RATHER THAN
                                            DECLARED AS A `scale`, so that 6.29's hover push can COMPOSE
                                            with it - calc(var(--frame-scale, 1) * var(--photo-lift, 1)) -
                                            instead of declaring a second literal (1.12052) that would
                                            have to be kept in step with this one by hand. The number is
                                            still written exactly once, and still written here, where its
                                            derivation is. Everything outside this query leaves
                                            --frame-scale unset and 6.29's fallback of 1 applies, which is
                                            the correct resting scale for a frame with no overscan. Who
                                            CONSUMES it differs by frame, and that split is the rule
                                            below. */
      will-change: translate;            /* permanent by design: the scrub runs for the life of the page.
                                            [PHOTO v16] NAMED FOR THE PROPERTY THAT ACTUALLY MOVES, which
                                            is what this declaration is for; it read `transform` while
                                            the drift did.
                                            [MOTION v20] AND THE COMMENT TERMINATOR THAT USED TO SIT AT
                                            THE END OF THAT LINE IS GONE - it closed this block 13 lines
                                            early,
                                            and left everything below it being parsed as CSS. It cost
                                            nothing - the orphan was the last thing in the block, so the
                                            parser discarded one invalid declaration and no real one -
                                            but a stylesheet that contains text the engine is throwing
                                            away is a stylesheet that will eventually throw away a
                                            declaration. Found by a comment-balance scan of the whole
                                            file after this pass introduced the same fault in 6.9 and a
                                            browser check caught it.
                                            Bounded by the count of [data-parallax] frames, which the
                                            markup contract keeps to the hero, the feature splits, and the
                                            closing band. `scale` is deliberately NOT added.
                                            [2026-09-07] THE CLOSING BAND'S FRAME IS NOW ON THIRTEEN
                                            PAGES rather than on the home page alone, and the bound is
                                            unchanged BECAUSE IT IS A PER-PAGE BOUND: no page gained a
                                            second closing band, so the worst page moves from two frames
                                            to three (the hero, a feature split and the band, on
                                            /cal-osha-defense-and-appeal) and the promotion budget this
                                            paragraph is defending is counted per document, not per
                                            site.
                                            [PHOTO v16] THE REASON CHANGED AND THE DECISION DID NOT. It
                                            used to be "it is constant here"; on a .feature-media img it
                                            is no longer constant - 6.29 pushes it while a pointer rests
                                            on the figure. But a hover push is a BOUNDED, pointer-caused,
                                            one-frame-at-a-time change on at most one element at a time,
                                            which is the case will-change is explicitly documented not to
                                            be used for, and the element is already promoted by the
                                            declaration above for the whole life of the page anyway. The
                                            standing rule is unchanged: the file carries one permanent
                                            promotion as a stated cost and must not acquire a second. */
    }
    /* THE OVERSCAN, CONSUMED - AND .feature-media IS DELIBERATELY ABSENT FROM THIS SECOND SELECTOR LIST.
       [PHOTO v16] `scale` HAS EXACTLY ONE OWNER PER ELEMENT ON THIS SITE, and on a framed photograph that
       owner is 6.29, which reads --frame-scale itself and multiplies it by the hover lift. Declaring
       `scale` here as well would be the second owner block 6 forbids - and it is not a theoretical
       objection: it was MEASURED as a live defect first. With both rules present, this one is
       (0,3,2) - `.js`, `.feature-media` and `[data-parallax]` on the class rung, `html` and `img` on the
       element rung - and 6.29's pointer-gated hover rule is (0,2,1) because :where() contributes nothing.
       (Both places wrote this pair as (0,3,1) until the trailing `img` was noticed; the comparison it
       decides is unaffected.) So THIS RULE WON AND THE PUSH SILENTLY DID
       NOT HAPPEN on the three parallaxed figures: --photo-edge went red, the picture did not move. The
       repair is not a specificity game (adding weight here or there is how the next collision gets
       written); it is to give the property one owner, which is what this split does.
       The hero and the closing band keep theirs here because neither takes a hover push: .hero-media img
       is 6.28's for the second the animation runs and `both` holds it on this exact value afterwards, and
       .cta-media img has no hover response at all - drop this line and the closing band's +-4% scrub
       would start exposing the frame edge. */
    html.js .hero-media[data-parallax] img,
    html.js .cta-media[data-parallax] img { scale: var(--frame-scale); }
  }
  /* The hero's scrim is .hero-media::after, a SIBLING of the img and not a child of it, so it does not
     drift with the picture. That is the correct outcome and not an accident: the scrim is a contrast
     instrument sized to where the copy sits (6.3's stop arithmetic), and a veil that slid up the frame
     while the copy stayed put would break every ratio in that block. [ZOOM v11] THE SAME SEPARATION IS
     WHAT MAKES 6.28 SAFE, and it was checked rather than assumed: a transform property applies to the
     element it is declared on and to that element's own descendants, and ::after is a child of
     .hero-media, not of the <img>. So the picture may scale from 1.19 down to 1.09 ([ZOOM v13] reversed
     the direction; the separation argument is indifferent to which way it runs) underneath a veil that
     never moves a pixel, and every ratio in 6.3 survives the zoom untouched. .cta-band--media::after
     (6.15a) stands in the same relation to .cta-media for the same reason. */

  /* ---- 6.28 THE ONE-SECOND ZOOM-OUT ON INTERIOR HERO PHOTOGRAPHS ----------------------------------
     THE INSTRUCTION CAME IN TWO PARTS, AND THE FIRST ONE CARRIES AN HONEST NOTE ABOUT ITS CITATION. The
     firm asked first for "a slow zoom in effect" on the hero photographs of every page but the home
     page, citing landcatchers.com as the example. THE REFERENCE DOES NOT CONTAIN THE EFFECT:
     landcatchers.com ships no @keyframes at all and no hero zoom - its hero motion is the parallax scrub
     and nothing else. So this is authored for TWM from the firm's own description, and it is recorded
     that way rather than credited to a site that would not survive being looked at.
     [ZOOM v13] 2026-09-06, THE SECOND PART, IN FULL: "All of the Hero images on all pages other than
     main should start slightly zoomed in and then slowly pan out to final view over 1 second." So the
     effect REVERSES and SHORTENS - 1.09 -> 1.19 over 24s becomes 1.19 -> 1.09 over 1s - and this block
     is rewritten rather than amended, because three of the paragraphs that stood here argued FOR things
     the reversal removes: a permanent 9.5% crop defended as the price of the motion, `linear` defended
     as the only honest curve for an imperceptible drift, and a hand-copied `from` named as the effect's
     one fragile joint. None of those arguments may be left standing beside a rule that no longer makes
     them. What is UNCHANGED and still argued below, each on its own grounds: `both`, one-way rather than
     looping, the selector, and the explicit reduced-motion rule in block 5.
     NO MARKUP EXPRESSES THIS, AND NONE SHOULD. The selector is .hero-media img, which by definition
     matches every photograph hero and only those. The home hero is a <video> (6.3), so it does not match
     and takes no zoom - which IS the firm's "all other pages besides the home page", expressed by the
     element type instead of by a modifier class threaded through every interior page at once. 404.html
     carries no .hero-media at all and is untouched.
     THE SET IS NAMED HERE, NOT COUNTED, AND THAT IS DELIBERATE. This block used to say "seventeen
     files" in three places and the number was about to rot twice in one day: /flat-fee-services is
     regaining the hero [REPAIR v10] stripped when a watermarked comp was found, which takes it to
     eighteen. A cardinality that moves whenever a page gains or loses a photograph is a comment with a
     shelf life, so what is written down is the SET - every page whose hero is a photograph - and the way
     to take its size where the answer is true, which is dist/ and not src/ for the reason index.html's
     hero comment sets out: `grep -l 'hero-media" data-parallax' dist/*.html | wc -l`. Re-take it; never
     copy it forward.
     WHY `scale` AND NOT `transform`. 6.27 already owns `transform` on this exact element, and block 6's
     rule about .feature-media vs .svc-card-media forbids two owners of one property. The independent
     transform properties are the way out: `scale` is its own property, applied before `transform`, so
     the scrub keeps `translate` and the zoom keeps `scale`, one owner each, no
     collision. The alternative - animating a bare custom property - was rejected because without
     @property registration it interpolates DISCRETELY, so the zoom would jump in steps wherever the
     registration is unsupported; and @property for a value this simple is a lot of machinery to buy a
     number that a real property already animates smoothly everywhere.
     THE PAIR IS NOT A TASTE DECISION, AND SINCE THE REVERSAL THE END OF IT IS 6.27's FLOOR RATHER THAN
     THE START. The brief asked for 1.06 -> 1.16; 1.06 is BELOW the overscan the drift needs, so the pair
     was shifted up to the smallest two-decimal value clearing that floor and now runs backwards down the
     same 0.10 of travel: 1.19 -> 1.09.
     THE FLOOR, RE-CHECKED AGAINST THE SHIPPED TRAVEL RATHER THAN RESTATED. main.js's initParallax
     declares TRAVEL = 4 (percent, +-), so the one-way travel fraction is p = 0.04. [PHOTO v16] 6.27's
     drift now lives in `translate`, which composes OUTSIDE `scale`, so the floor is s >= 1 + 2p = 1.08
     rather than the 1 / (1 - 2p) = 1.08696 that held while the drift was inside `transform`. Both
     numbers, said out loud: +-4% of travel, 1.08 of floor. 1.09 still ships and now clears it by more -
     0.0450 of overspill per edge against 0.0400 of excursion, 0.0050 of the frame's height spare, where
     the S x T order left 0.0014.
     WHAT THE REVERSAL REPAIRS, AND IT IS THE WHOLE REASON THIS IS A FIX AND NOT JUST A DIRECTION SWAP.
     The picture now SETTLES AT 1.09, which is exactly 6.27's declared resting overscan, so:
       - THE PERMANENT CROP FALLS FROM 9.5% TO 4.5% PER EDGE. (s - 1) / 2 at 1.19 is 0.0950; at 1.09 it
         is 0.0450. And the 4.5% is no longer a cost this block invents - it is the crop every parallaxed
         frame on the site already carries, the minimum the +-4% scrub requires. (At the bare floor,
         1.08696, it would be 0.0435; the two-decimal rounding buys the 0.0014 of slack above.)
       - THE UPSCALE ARGUMENT DISAPPEARS. Measured in Chrome on the built pages at 1440x900: the hero
         frame is 1425 x 510 - full bleed, so the viewport less a 15px scrollbar, by .hero--page's 30rem
         (510px) min-height, which the copy does not exceed at desktop. `object-fit: cover` is
         WIDTH-driven there for every hero source the site ships, and that test is an ASPECT one, so
         srcset's density correction cannot move it: cover takes the width when frameH/frameW <=
         srcH/srcW, and 510/1425 = 0.3579 sits below even the flattest source here (1600x894 = 0.5588).
         The on-screen width is therefore just the frame's width times the scale. At rest that is
         1425 x 1.09 = 1553px of picture drawn from 1600 source pixels: a downscale, no interpolation,
         at any width. The old resting 1.19 asked for 1696px out of 1600 - 6.0% of interpolation - and
         asked for it permanently; it is now asked for only in the first frames of a 1.5s entrance.
       - THE FRAGILE JOINT BECOMES STRUCTURAL. The old `from: 1.09` had to clear the floor on its own,
         hand-copied out of 6.27, with nothing to catch it if 6.27's number moved; the animation now
         ENDS on the value the resting rule declares, so the two are the same number by construction and
         a mismatch is a visible settle to the wrong framing rather than a silent edge-exposure at the
         extremes of a scrub.
     THE ONE COST THAT SURVIVED IS PAID OFF AS OF [MOTION v21], AND THE PARAGRAPH IS REWRITTEN RATHER
     THAN AMENDED BECAUSE IT ARGUED FOR ACCEPTING SOMETHING THAT NO LONGER HAPPENS. It read: "6.27's
     overscan is gated on html.js and 67rem; this zoom is gated on neither, so outside those gates - on
     phones, and with JS off - the resting 1.09 is a 4.5% crop that 6.27 would not have declared... it is
     accepted here for the same reason it was before". Every word was an accurate description of what
     shipped, and a review measured it on 18 pages: phone, no-JS, and phone-with-no-JS all rested at 1.09.
     THE GATES ARE STILL NOT ADDED - the reason not to add them stands, and it is that gating the zoom the
     way 6.27 gates its overscan would take the effect away from phones, which is where the firm will look
     first. What changed is the KEYFRAME PAIR, which is now relative to --frame-scale instead of absolute,
     so the animation settles on whatever framing the element rests at: 1.09 where there is a scrub to
     overscan for, natural size where there is not. The entrance plays everywhere, on the same 0.10 of
     travel; the permanent crop exists only where 6.27 declares one. The arithmetic is at the @keyframes.
     --ease-out, NOT `linear`, AND THE ARGUMENT INVERTS WITH THE DURATION. What stood here was "a slow
     zoom should be an imperceptible constant drift; an ease front-loads or back-loads the travel and
     makes the motion noticeable" - true of 0.10 of magnification spread over 24s (0.42% a second, below
     the rate at which the eye reads motion rather than presence), and false of the same 0.10 delivered
     in one and a half. At 1.5s this is not ambient drift at all, it is an ENTRANCE: it will be seen, it
     is meant to be, and the only question left is how it arrives. A constant rate arrives by stopping
     dead. So it takes --ease-out - cubic-bezier(0.16, 1, 0.3, 1) since [MOTION v20], the same curve every
     reveal in the choreography uses and the same curve this line named before the token moved - which is
     also the literal reading of the instruction: "slowly pan out to FINAL VIEW" is a settling, and an
     expo-out spends most of the run already at the framing the page then keeps. The curve change works
     HARDER here than anywhere else on the site, because this is the one motion whose whole subject is the
     settle: expo reaches 90% of the magnification at 0.329 of the run - 0.49s of the 1.5s - and spends
     the remaining second easing the last 1% of it, which is what "slowly pan out" describes.
     THE DURATION NAMES ITS STEP, as block 4 requires of every literal. 1s WAS THE FIRM'S OWN NUMBER,
     quoted above, and it is 1.5s now - the firm's number times this pass's one multiplier.
     [MOTION v20] TWO FIRM INSTRUCTIONS FROM THE SAME DAY MEET ON THIS LINE, SO THE CHOICE IS WRITTEN OUT
     RATHER THAN TAKEN QUIETLY. The first gave this effect a duration ("slowly pan out to final view",
     one second). The second, later the same day, is general and names no exception: "all of the
     animations throughout all pages of the website are to fast and too subtle. Make them more prominent."
     A hero zoom on every photograph hero on the site is squarely inside "all of the animations", and an
     instruction to slow everything down cannot be obeyed by exempting the first thing a visitor sees. So
     the 1s follows the ladder: 1 x 1.5 = 1.5s, which is the firm's own figure carried on the same
     multiplier as every other duration in the file rather than replaced by a token. It is STILL not
     rounded to --dur-media (1.275s), for the reason it was not rounded before - the instruction gave a
     duration, and a token that happens to sit near it is not the same thing as the number asked for.
     THE ONE-LINE REVERT, stated so it is a decision the firm can take back: `hero-slow-zoom 1s` on the
     declaration below restores their original figure exactly, and nothing else in this block depends on
     the duration.
     WHAT THE LONGER RUN COSTS, AND THE ALTERNATIVE THAT WAS REFUSED. The travel is unchanged (1.19 ->
     1.09, 0.10 of magnification), so spreading it over 1.5s drops the mean rate from 0.10 to 0.0667 per
     second. That is the instruction's own direction - slower - but it is NOT more prominent per unit
     time, and the obvious repair would have been to hold the rate by raising the `from` to 1.24. It is
     refused on image quality: the frame is 1425px wide from a 1600px source, so 1.19 asks for 1696px and
     6.0% of interpolation in the first frames, where 1.24 would ask 1767px and 10.4%. Prominence bought
     by softening the first image on the page is not prominence. The `to` could not move at all - it IS
     6.27's --frame-scale by construction, and the floor below binds it.
     ONE-WAY AND FINITE, NOT LOOPING OR ALTERNATING. A perpetual animation is permanent compositor work
     and a hero that never settles; this one is done in a second and a half. Finishing is also what lets the browser
     release the layer on its own, which is why no will-change is declared here at all - a running
     transform-property animation self-promotes and de-promotes, and 6.27 already records one permanent
     promotion as a deliberate cost that must not be joined by a second.
     `both`, AND IT NOW DOES BOTH OF ITS JOBS FOR THE SAME REASON. Backwards fill holds 1.19 before the
     animation starts, so there is no flash of an unscaled frame; forwards fill holds 1.09 after it ends,
     so the picture keeps the framing 6.27 declares instead of snapping back to natural size. Drop it and
     both ends break, in opposite directions.
     COMPOSITOR-ONLY. `scale` is a transform property, so this never triggers layout or paint. It is one
     animated property on one element per page, for one second.
     REDUCED MOTION IS HANDLED IN THE GUARDS BLOCK (block 5) and, unlike the scroll-linked scrub, it is
     genuinely caught by CSS there - a scrub is neither a transition nor an animation and can only be
     stopped in JS, whereas this is an animation and the clamp sees it. It still ships an EXPLICIT rule
     rather than leaning on the clamp; block 5 carries that argument, which the reversal made shorter. */
  .hero-media img { animation: hero-slow-zoom 1.5s var(--ease-out) both; }   /* the firm's 1s x this pass's 1.5 - see above */

  /* ---- 6.29 THE PHOTOGRAPH TREATMENT - ONE PATTERN, EVERY FRAMED PHOTOGRAPH ON THE SITE -------------
     [PHOTO v16] 2026-09-06. The firm instructed, in full: "every static photo throughout the website
     should have the same animation... a slight zoom in with mouse hover and a red box around the photo."
     THE WORD THAT DECIDES THE SHAPE OF THIS BLOCK IS "SAME". Eleven framed photographs on four pages sit
     (twelve on five until 2026-09-07, when /employment-law's figure went)
     in two different containers, and the obvious implementation - a hover rule inside 6.9 and a second
     one inside 6.11 - would be two near-copies of one idea, free to drift by a magnitude or a curve the
     first time either block was edited. So the treatment is declared ONCE, here, against both frames at
     once, and every per-frame difference is carried as a VALUE rather than as a separate rule:
       --frame-scale  the scale the picture ALREADY RESTS AT. Unset (so 1) everywhere except a parallaxed
                      .feature-media img above 67rem, where 6.27 declares 1.09 of overscan and publishes
                      it under this name for exactly this purpose.
       --photo-push   the hover MAGNITUDE this frame is entitled to. 1.028 by default; 1 on a frame
                      carrying a video overlay - see THE VIDEO RULE below, which is the whole of Part 4
                      of the instruction. Read in exactly one place.
       --photo-lift   the multiplier ACTUALLY IN FORCE: 1 at rest, --photo-push while hovered. Set on the
                      frame, inherited by the picture.
       --photo-edge   the ring COLOUR. Unset at rest (so transparent, or --line where a frame already had
                      a border); --brand-fill on hover.
     One magnitude, one duration, one curve, one colour, and exactly one declaration of `scale` on a
     framed photograph anywhere on the site. 1.028 is still written exactly once in this file; it has
     simply moved out of the component that happened to invent it.

     THE SURVEY IS PART OF THE RULE, so it is written down rather than left to the next reader's grep.
     [RE-DERIVED 2026-09-07, AND THE WHOLE BLOCK RATHER THAN THE ONE CLAUSE THAT MOVED, because a
     partition that states its own sum cannot be half-corrected. Two changes reached it: retiring
     /personal-injury and /products-liability took two hero images out of class (b), and the firm's
     removal of /employment-law's Cal/OSHA figure took one out of class (a). Counted on the files as
     they now stand, comments stripped.]
     [RE-COUNTED 2026-09-07, SECOND PASS, FOR THE SAME REASON AND NOT AS A TOUCH-UP. The firm had the
     home page's closing photograph rolled out to the twelve practice-area pages, so class (b) gains
     twelve <img> in one move - the largest single change this census has taken - and every total in it
     moves with them. Classes (a), (c) and (d) are untouched, and that is a finding rather than an
     omission: a .cta-media is full-bleed, scrimmed and pointer-inert, so twelve more of them add twelve
     rows to the EXCLUDED class and not one frame to the treatment. Counted on the files as they now
     stand, comments stripped, and the built pages agree at the other end.]
     Forty-two <img> elements are AUTHORED - 40 across the 18 page files (the 404 carries none), 2 in the
     partials, which the generator inlines into all 18 built pages (76 <img> in dist/ = 40 + 18 x 2,
     the same census counted at the other end of the build). There is no <picture> anywhere on the site and no PHOTOGRAPHIC
     background-image: both clauses were written shorter once and both were then literally FALSE, so they
     are stated exactly. The site does ship two <source> elements - the home hero's WebM and MP4, which
     belong to a <video> and not to a responsive image - and this file does ship one background-image,
     .svc-card-link's linear-gradient(currentColor, currentColor) at 25% x 1px, which is a drawn RULE and
     not a picture. Neither is a framed photograph and neither is reachable by anything below.
     Every one of the 42 falls into exactly one of four classes, and the four sum to 42:
       (a) FRAMED CONTENT PHOTOGRAPH, a bounded box a pointer can land on. Five: .feature-media on
           /attorneys (the portrait) and /cal-osha-defense-and-appeal, and three .svc-card-media frames
           on /real-estate. It was six until 2026-09-07, when the firm had /employment-law's Cal/OSHA
           figure removed. THESE TAKE THE WHOLE TREATMENT.
       (d) A FRAMED PHOTOGRAPH UNDER A HOVER VIDEO, the six practice-area cards on the home page. RING
           YES, PUSH NO - the clip is the push's replacement, not its companion. See THE VIDEO RULE.
       (b) FULL-BLEED BACKGROUND, .hero-media (16 <img>: 17 pages carry the element, and the home
           page's own holds the <video>) and .cta-media (13 <img>: the home page's closing band, plus
           the twelve practice-area bands the firm had the same photograph rolled out to on 2026-09-07 -
           the six practice areas, their five sub-pages and /firm-overview. Fifteen pages carry a
           .cta-band; /attorneys and /flat-fee-services are the two that close plain). 16 + 13 = 29.
           EXCLUDED - AND REPORTED TO THE FIRM AS EXCLUDED RATHER THAN DECIDED QUIETLY, because "every"
           was the instruction and this is the one place it is not being obeyed literally. Six reasons
           were given and one of them is now spent (below); any two of the remaining five would do: these frames ARE the band (.hero-media is inset: 0 on the section;
           .cta-media sits at z-index -2 BEHIND its own band's copy), so a "box around the photo" would be
           a red box around the viewport; both sit under a scrim, so a ring would paint either beneath the
           veil or over the copy, and neither is a picture frame; both are alt="" mood shots nobody points
           at, with the meaning entirely in the text on top; the hero already carries 6.28's one-second
           settle, so a hover scale would be a second cause of motion on one element; .cta-media WAS
           display: none below 29.99rem, so its hover response would have vanished on a resize - THAT
           REASON IS SPENT, the firm having reversed the phone step on 2026-09-08 (6.15a), and it is
           struck rather than quietly reworded because five of the six still stand and the exclusion
           needs two; and both carry
           [data-parallax], a motion with no pointer in it at all. If the firm wants them anyway, the RING
           is the half that could ship - the push cannot, and 6.28 is why.
       (c) THE LOGO, twice - the header lockup and the footer one. Marks, not photographs. Excluded
           without a question needing to be asked. 5 + 6 + 29 + 2 = 42, which is the whole census.
           (The site's ~28 arrow glyphs are DELIBERATELY NOT IN THIS PARTITION, and an earlier draft put
           them here, which quietly stopped the four classes summing: they are inline <svg>, not <img>,
           so they were never among the 30. Marks too, and equally excluded - but by element type, not by
           a decision anyone had to take.)
     NO FRAMED PHOTOGRAPH SITS IN A CONTAINER NOBODY HAS STYLED. Every <img> on the site resolves into
     .hero-media, .cta-media, .svc-card-media, .feature-media, a.site-logo or .footer-col; the two
     selectors below are the whole of (a) plus the whole of (d), not a sample of them. One shape to know
     about: /real-estate's fourth .svc-card has NO .svc-card-media at all (the frame went with an
     unlicensed comp), so the frame is an optional child and nothing here may assume it exists - which
     is why nothing below is written as `.svc-card > *`.

     THE EDGE: WHY A BORDER COLOUR, AND WHY ONE OF THE TWO FRAMES NEEDS A RING TO HAVE A BORDER TO COLOUR.
     Measured in Chrome at 1440x900 against dist/, injecting each candidate and removing it again. The
     binding constraint is that THE TREATMENT MUST NOT MOVE THE PICTURE INSIDE ITS FRAME BY ONE PIXEL
     when the pointer arrives: a photograph that jumps 1px on hover is worse than no effect at all.
     "INSIDE ITS FRAME" IS A CORRECTION, NOT A HEDGE. Said flatly - "the picture must not move" - the
     sentence is not true of what ships and never was: a .svc-card-media frame sits inside .svc-card,
     whose own hover takes the site's one nudge (6.9; --nudge, 4px since [MOTION v20] and 3px when this
     sentence was written), so on those nine frames the picture and its frame travel a nudge up together. That is the CARD moving, not the
     treatment moving the picture within it. The number that decides this block is therefore the
     picture's offset from its own frame's origin, read at rest and again while hovered - measured
     {dx:0, dy:0, dw:0, dh:0} at rest on .svc-card-media and unchanged in x/y by the ring, and symmetric
     about the centre under the push. Every candidate below is judged on that number and on nothing else.
       border on the frame     ZERO SHIFT where the frame already HAS one - on .feature-media, changing
                               border-COLOR moves the img by exactly 0 (changing border-WIDTH to 2px moves
                               it 1.09px and grows the frame 0.672px, which is why only the colour is ever
                               touched). ADDING one where there is none moves .svc-card-media's picture
                               +1px in x and +1px in y and shrinks it by 2px of WIDTH and 1.344px of
                               HEIGHT - NOT "2px in both", which is what this line claimed until it was
                               re-measured: the frame itself GROWS 0.656px at the same time, because
                               aspect-ratio re-solves against the narrowed content box, and the height
                               shrink is the difference of the two. Rejected there on that measurement
                               alone; the arithmetic is spelled out because a number quoted from memory
                               is how the next reader gets misled.
       outline, offset -1px    Zero shift, and IT SILENTLY DIES IN EXACTLY THE TWO STATES THIS BLOCK
                               CREATES. Painted at rest; invisible over an img at scale(1.028); invisible
                               over a playing <video> - while getComputedStyle goes on reporting
                               "rgb(191, 0, 0) solid 1px". A compositor-promoted child rasterises above an
                               ancestor's outline. No error, no console warning. This is the obvious first
                               choice and it would have shipped as a bug.
       box-shadow: inset       Zero shift and BYTE-IDENTICAL SCREENSHOTS TO NO RULE AT ALL: an inset
                               shadow paints behind the content and the img is width: 100% of the frame.
                               Barred outright anyway by block 6's "NO box-shadow anywhere, animated or
                               static", which is the rule that should have been consulted first.
       border on the img       Clipped away the moment the img scales - the same finding 6.11 already
                               records for the parallaxed case: a scaled child carries its own border out
                               through the frame's clip.
       ::after ring, inset 0   Zero shift, and PAINTED IN ALL THREE STATES - at rest, at 1.028, and over
                               the playing clip (sampled on all four edges of each). This is what
                               .svc-card-media gets, and it is the only mechanism that survives.
     So ONE declaration warms in both cases - border-color, from --line or transparent to --brand-fill -
     and the only difference is WHICH BOX CARRIES THE BORDER: the frame's own where it has one, a ring
     pseudo-element where it does not. The ring is not a second treatment. It is the border
     .svc-card-media never had, supplied so that the one pattern has something to warm.
     z-index 1 ON THE RING, chosen against the three numbers a card already contains: 1 is
     .svc-card-video, 2 is .svc-card::before (the specular lamp), 3 is .svc-card-link::after. THE RING
     STILL PAINTS OVER THE VIDEO AT THE SAME NUMBER, and that is tree order rather than luck: neither
     .svc-card-media nor .svc-card-video establishes a stacking context of its own, so both the clip and
     this pseudo-element are positioned descendants of .svc-card at z-index 1, and an ::after is generated
     after every element child. Verified in a real browser over a clip at readyState 4, not reasoned from
     the spec alone.
     2 WAS TRIED FIRST AND IS WRONG, for a reason this file had already written down. At 2 the ring ties
     .svc-card::before and wins on tree order, so on a hovered card the specular lamp's 1px is painted
     over along the whole top edge - and 6.9a's z-index note requires the opposite in as many words: "the
     lamp must glint over the footage exactly as it glints over the photograph". A rule that quietly
     switches the row's light source off on whichever card the pointer is nearest is the one thing the
     lamp exists to avoid. At 1 the order reads video < ring < lamp < hit overlay, and nothing that was
     already on the card loses anything to the new arrival.
     pointer-events: none keeps the link overlay the sole hit target, which [LINK v15] had to repair once
     already and must not lose again.

     THE COLOUR IS --brand-fill #BF0000, AND ITS RATIO IS WRITTEN DOWN RATHER THAN LEFT BARE: 2.86:1
     against --bg-2 (the card plate, and the .section--alt ground) and 2.99:1 against --bg (the plain
     section ground). Against the thing it actually edges - a photograph - it is unmeasurable, because
     the ground is a picture and not a colour.
     SC 1.4.11 DOES NOT BIND ON IT, AND SAYING SO IS THE POINT OF THIS PARAGRAPH: without the sentence, a
     sub-3 number sits beside every AA-passing ratio in this file looking like a defect nobody caught.
     1.4.11 asks 3:1 of a visual indicator of a USER-INTERFACE COMPONENT'S BOUNDARY OR STATE, and this
     ring is neither. The component's boundary is .svc-card's own 1px var(--line) and .feature-media's own
     1px var(--line), BOTH UNCHANGED AT REST. The affordance's state is carried by the card's
     brand-tinted border, its --nudge rise, the specular lamp and the arrow's drawn rule, every one of which
     stands untouched. This ring is a decorative hover flourish on a photograph: it adds no information,
     it delineates no control, and a reader who never sees it has lost nothing. 2.86:1 is therefore a
     MEASUREMENT and not a shortfall - the distinction tools/verify_ratios.py draws between an EXEMPT
     figure and a DECIDED failure, and this is the first kind. For scale, the seam this ring replaces on
     .feature-media - --line, composited to #232120 - measures 1.22:1 against --bg, and nobody has ever
     called that a defect either.

     THE PUSH: `scale`, NOT `transform`, AND THIS IS THE LARGEST ENGINEERING CONSEQUENCE OF THE
     INSTRUCTION. 6.27 already owns the parallax drift on .feature-media img - main.js rewrites that
     offset on every scroll frame - and block 6's own rule forbids two owners of one property. Worse,
     transitioning the drift's own property would smear the scrub into a laggy tween, which is not a
     hypothetical: rewriting it every frame is what it is for. The independent transform properties are
     the way out, exactly as 6.28 used them for the hero: `scale` is its own property, so the scrub keeps
     the translation and the push keeps `scale`, one owner each, and the transition names `scale` alone.
     AND THE DRIFT MOVED FROM `transform: translateY()` INTO THE `translate` PROPERTY IN THE SAME PASS,
     WHICH IS THE HALF THAT WAS WRONG UNTIL IT WAS MEASURED. Two owners was never the only hazard; ORDER
     was the other. The individual transform properties compose as translate, then rotate, then scale,
     then `transform` - the final matrix is T x R x S x M - so anything written in `transform` is applied
     INSIDE `scale` and is therefore MULTIPLIED BY IT. With the drift in `transform`, hovering a
     parallaxed .feature-media figure changed the drift's amplitude from 1.09x to 1.12052x and moved the
     picture inside its frame by up to +-0.6055px on /attorneys: with the scrub frozen at --parallax-y
     -4% / 0% / +4% the picture's centre sat at 453.2845 / 475.5156 / 497.7467 hovered against
     453.8900 / 475.5156 / 497.1412 at rest, which is 19.84px of travel times the 0.03052 difference in
     scale. Under a pixel, so the constraint above survived; ZERO, which is what that constraint is
     actually asking for, it did not - and the error was scroll-position dependent, so it drifted
     continuously for a visitor who scrolled with the pointer resting on the picture and changed sign
     through the midpoint. The drift now lives in `translate`, which composes OUTSIDE `scale`, so the two
     are independent by construction: the push is a pure scale about the picture's centre and the
     measured offset is exactly 0 at every scroll position. It is a repair to 6.27 made here because 6.29
     is what exposed it - a constant scale hid the coupling for as long as nothing else touched it.
     THAT FORCED .svc-card-media img TO MOVE TOO, and the move is stated rather than smuggled in with the
     new work: its push has been `transform: scale(1.028)` since the site was built and is now `scale`.
     Nothing a visitor sees changes - the same 2.8% at the same tempo on the same trigger - and what is
     bought is that "the photograph push" is ONE PROPERTY everywhere on the site, which is the only way
     "the same animation" can be true of two frames that do not otherwise resemble one another.
     THE COMPOSITION IS calc(), WHICH IS WHY THERE IS STILL ONE LITERAL. A parallaxed .feature-media img
     rests at 1.09 (6.27's overscan, the crop a +-4% scrub requires); a .svc-card-media img rests at 1.
     Written as two hover values those would be two magnitudes - 1.12052 and 1.028 - and this file's
     promise that 1.028 appears once would be gone, with nothing to keep the pair in step. Written as
     calc(var(--frame-scale, 1) * var(--photo-lift, 1)) it is ONE push applied to whatever framing the
     picture already rests at, 6.27 declares its 1.09 exactly once as the value of --frame-scale, and the
     failure mode is quiet: if either custom property were ever invalid the whole declaration becomes
     invalid at computed-value time, `scale` falls back to its initial `none`, and the picture simply does
     not move.
     AND THE STATE IS A CUSTOM PROPERTY ON THE FRAME RATHER THAN A `scale` IN THE HOVER RULE, WHICH IS THE
     CORRECTION THAT MADE THIS BLOCK ACTUALLY WORK. The first cut declared `scale` twice - once resting in
     6.27, once on hover here - and the resting one WON on every .feature-media figure (three at the
     time; two since 2026-09-07), because
     `html.js .feature-media[data-parallax] img` is (0,3,2) - `.js`, `.feature-media` and `[data-parallax]`
     on the class rung, `html` and `img` on the element rung; it stood here as (0,3,1) until the element
     count was re-done and the trailing `img` turned up missing - while `:where(html:not(.has-touch))
     .feature-media:hover img` is (0,2,1). Measured on /employment-law's figure, which the firm then had
     removed on 2026-09-07 - the finding stands, its evidence page does not: --photo-edge went to
     #BF0000 and the picture did not move a pixel. Two owners of one property, exactly as block 6 warns, discovered
     the way that rule says such things are discovered. The fix is not more specificity - that is how the
     NEXT collision gets written - it is to leave `scale` with one owner and pass the state in as a value
     nothing else declares.
     COMPOSITOR-ONLY, AND TRANSITIONED WITH THE FILE'S OWN TOKENS: --dur-media (1.275s since
     [MOTION v20], the imagery rung, 2.1x the plate's --dur-base at any multiplier) on --ease-out,
     byte-identical to what .svc-card-media img has always run. THE PLAYBOOK'S BAND IS NOW CLEARED AT THE
     TOP RATHER THAN SAT INSIDE - "1.1s, and under ~0.7s it reads as a stock hover zoom" - and the
     direction is what matters: the floor is the half that names a failure (a fast zoom reads as a stock
     effect) and 1.275s is nowhere near it, while the ceiling is a note about the point at which a hover
     zoom stops being felt as a response. The firm's instruction of 2026-09-06 asked for exactly that
     trade on exactly this kind of object, and the push is 2.8%: at that magnitude the risk at the top of
     the band is that the picture is still moving when the pointer leaves, which costs nothing, where the
     risk at the bottom is that it reads as a template.
     THE EDGE RUNS AT --dur-base ease AND NOT AT --dur-media, and the split is deliberate rather than an
     oversight: --dur-base ease is byte-identical to .svc-card's own border-color transition, so on a
     service card the picture's ring and the card's border warm together as one gesture while the picture
     moves slower than both. Chrome tempo for a hairline, imagery tempo for imagery - the distinction
     6.9a's duration note already draws, applied to the two halves of one treatment.

     THE VIDEO RULE, AND IT IS A RULE RATHER THAN AN EXCEPTION. The firm, 2026-09-06, in the same pass:
     "throughout the site, if there is an overlayed video that plays from a fixed image with mouse hover,
     there should be no zooming in. Only subtly zoom in on static photo cards with no overlaying video."
     THE TEST IS NOT "THE HOME PAGE" AND NOT "THESE SIX CARDS" - it is DOES THIS FRAME HAVE A VIDEO OVER
     IT - so it is asked of the FRAME, twice, in two rules that set one value.
     THE FIRST ASKS THE STRUCTURE: `.svc-card-media:has(video)`. A frame that GAINS a <video> loses the
     push in the SAME EDIT, with no marker to remember and no selector to add; a frame that LOSES one
     gets the push back the moment the element goes. That is the property the instruction actually asked
     for, and until [PHOTO v16] this block did not have it: the rule was keyed on the class alone, so a
     <video> appended to a bare .svc-card-media both PLAYED (main.js binds playback structurally, on
     ".svc-card-media video") and ZOOMED - the exact pair the firm forbade, reachable by an edit that
     looks complete. Measured that way before the fix, on /real-estate: append a bare <video>, change
     nothing else, and --photo-push stayed unset while --photo-lift went to 1.028 and the picture
     scaled.
     THE SECOND ASKS THE MARKUP: `.svc-card-media--video`, the class 6.9a already ships. It is not
     redundant and it is not belt-and-braces for its own sake - it is what the rule falls back to in a
     browser that does not parse :has(), where the first rule simply does not exist. The two are separate
     rules and MUST STAY separate: a selector list is invalid as a whole if any selector in it is, so
     writing them as one comma-separated rule would take the marker down with the structure in precisely
     the engines the marker is there for (Firefox shipped `scale` in 72 and :has() in 121). Two rules,
     one declaration, one value - and no way for them to disagree, because there is nothing to disagree
     about.
     THE FIRM'S INSTRUCTION AND THIS FILE'S OWN PRINCIPLE AGREE HERE, WHICH IS WHY IT IS SAFE TO EXPRESS
     STRUCTURALLY. 6.9's rule is that "a second motion on the same object, with a different cause, is the
     definition of busy". A playing video IS the hover response on those six cards, so a simultaneous 2.8%
     push would be the second cause. The instruction is that principle arrived at from the other side, and
     a rule two independent arguments reach is a rule worth building the selector on.
     .svc-card-media--video IS THE MARKER 6.9a ALREADY SHIPPED, and the value form is a FOURTH SPELLING
     of that subtraction rather than a new marker; the :has() rule beside it is 6.9a's REJECTED (ii),
     admitted on the one footing that never made it dangerous - as an addition rather than a replacement.
     6.9a records all of that. The residual authoring risk is now confined to engines without :has(): a
     <video> added there without the marker scales and plays at once, which is loud, and confined to a
     card that actually has a clip. Everywhere else the structure answers, and there is nothing to
     remember.
     html.js IS PART OF THE MARKER'S GATE AND IS LOAD-BEARING, carried over verbatim from the rule this
     replaces. The watchdog strips .js at 2.5s on a page whose script never stamped data-motion-ready, and
     a card on a page that has given up on motion must go back to being the card it was rather than become
     one with no picture response at all. The pointer gate is NOT repeated on it: --photo-push is only
     ever read inside a rule that is already gated, so a second copy would be noise.
     THE WHOLE SITE WAS CHECKED FOR OTHER VIDEO-OVER-IMAGE FRAMES AND THERE ARE EXACTLY TWO POPULATIONS OF
     <video>. .svc-card-video (these six) and .hero-video (index.html, one). The hero is NOT a
     video-over-image frame: the clip REPLACES the <img>, so .hero-media on the home page has no <img>
     child at all and every rule here keyed on `img` matches nothing there STRUCTURALLY - a stronger
     guarantee than an exclusion by selector, and independent of the class (b) reasons that exclude the
     frame anyway, and of 6.28's separate reason for excluding it from a hover zoom. There is no third
     population and none is planned. A FUTURE .feature-media--video, OR ANY OTHER FRAME THAT ACQUIRES A
     CLIP, MUST DECLARE --photo-push: 1 THE SAME WAY - that is the whole of the convention, and it is why
     the marker rule below is a declaration ON THE FRAME and not a card selector.

     REDUCED MOTION NEEDS AN EXPLICIT RULE AND THE CLAMP IS NOT ENOUGH - CHECKED, NOT ASSUMED. The clamp
     collapses transition-duration to 0.001s, which SERVES THE END VALUE INSTANTLY rather than cancelling
     it: measured on /real-estate's first card before the guard existed, matrix(1.028, 0, 0, 1.028, 0, 0)
     120ms after pointerenter and held. That is a snap, not a stillness, and it is the identical failure
     block 5 already documents for 6.28's animation. Block 5 now carries the matching `scale: none` for
     these two frames; it is not optional tidiness, and it also repairs a defect that predates this pass.
     The RING is deliberately left alone there: a colour arriving in 0.001s is a colour change, not a
     movement.

     NO :focus-within, FOR THE PUSH OR FOR THE RING, AND IT IS A DECISION RATHER THAN AN OMISSION. 6.9's
     convention is that the keyboard state is deliberately QUIETER than the pointer state -
     .svc-card:focus-within takes --line-strong and the lamp and has never taken the accent border, the
     --nudge rise or the push - so adding the push on focus in the very pass that merely EXTENDS it for the
     pointer would invert the convention instead of matching it. On .feature-media the question cannot
     even arise: a <figure> holding one <img> has nothing focusable inside it, so a :focus-within rule
     there would be dead code that reads as intent. And nothing is lost by the omission: the ring carries
     no information, and the real keyboard indicator is base's 2px --ink ring at 17.73:1 plus the
     card's own --line-strong edge, both untouched. If the firm asks for a keyboard answer later, the
     quieter half is the RING WITHOUT THE PUSH on .svc-card:focus-within - and it would be a departure
     from the convention, to be recorded as one. */
  .svc-card-media::after {
    content: ""; position: absolute; inset: 0; z-index: 1;   /* over the video on tree order, UNDER the lamp - see above */
    border: 1px solid var(--photo-edge, transparent);        /* the border this frame does not have */
    border-radius: inherit;                                  /* 0 today; follows the frame if it ever gains one */
    pointer-events: none;                                    /* .svc-card-link::after stays the sole hit target */
  }
  /* THE VIDEO RULE, AS A VALUE, ASKED OF THE FRAME TWO WAYS - THE STRUCTURE FIRST. Frame-level, so the
     frame's own markup decides; html.js because a page with no script has no playback and must keep the
     response it already had. SEPARATE RULES, NEVER ONE SELECTOR LIST: an invalid selector invalidates the
     whole list, so pairing them would take the fallback down with the structure in every engine without
     :has(). See THE VIDEO RULE above for why both, and 6.9a for the four spellings this replaces. */
  :where(html.js) .svc-card-media:has(video) { --photo-push: 1; }
  :where(html.js) .svc-card-media--video    { --photo-push: 1; }
  /* The machinery is declared INSIDE the pointer gate, exactly as 6.9a's fade is: a device that has
     proved itself touch never reaches either state and need not carry it. Two properties, two tempos -
     see the duration note above. */
  :where(html:not(.has-touch)) .svc-card-media::after,
  :where(html:not(.has-touch)) .feature-media { transition: border-color var(--dur-base) ease; }
  :where(html:not(.has-touch)) .svc-card-media img,
  :where(html:not(.has-touch)) .feature-media img { transition: scale var(--dur-media) var(--ease-out); }
  /* THE ONE DECLARATION OF `scale` ON A FRAMED PHOTOGRAPH, ANYWHERE ON THIS SITE. Not gated, because a
     resting frame computes calc(1 * 1) and a touch device never sets the lift - and because the whole
     point is that this property has one owner rather than a rule per state. --frame-scale is the resting
     framing 6.27 publishes (1.09 of overscan on a parallaxed frame above 67rem, unset and therefore 1
     everywhere else); --photo-lift is the hover multiplier the rule below sets on the FRAME, which the
     img inherits. Set neither and the picture sits at natural size, which is what it did before this
     block existed. */
  .svc-card-media img,
  .feature-media img { scale: calc(var(--frame-scale, 1) * var(--photo-lift, 1)); }
  /* THE TREATMENT, ONCE. The TRIGGER differs because the hover TARGET differs, and nothing else does: a
     .svc-card is one click target, so its picture answers the pointer anywhere on the card - which is
     what the rule this replaces did, and changing it would have been a second, unasked-for change; a
     .feature-media leads nowhere, so it answers only on itself.
     BOTH PROPERTIES ARE SET ON THE FRAME AND CONSUMED BY DESCENDANTS, AND THAT IS WHAT MAKES ONE PATTERN
     POSSIBLE. A custom property inherits, so the frame's hover state reaches the picture and the ring
     without either of them needing a selector of its own - and, unlike a `scale` declared on the img in a
     hover rule, it cannot lose a specificity contest to a resting rule that also declares `scale` (which
     is exactly what happened when this was first written: see 6.27's second selector list). --photo-push
     is read HERE, once, so the video rule at the top of this block reaches both frames through one line
     rather than through a selector per card. */
  :where(html:not(.has-touch)) .svc-card:hover .svc-card-media,
  :where(html:not(.has-touch)) .feature-media:hover {
    --photo-edge: var(--brand-fill);
    --photo-lift: var(--photo-push, 1.028);
  }

  /* ---- 6.30 THE PARTNER MARK (img.partner-logo) --------------------------------------------------
     Numbered after 6.29 so no existing number moves, on 6.26's own precedent; source order is
     irrelevant inside a layer.

     TWO PASS NOTES OF 2026-09-08 (AFTERNOON) THAT BELONG TO NO OTHER FILE THIS PASS MAY WRITE, AND ARE
     THEREFORE RECORDED HERE RATHER THAN NOWHERE.
       THE WORKTREE. The firm asked for this afternoon's two changes to be made in a git worktree. THEY
       CANNOT BE: this project is not a git repository - `git rev-parse` finds no .git anywhere up the
       tree - and a worktree is a checkout of something that does not exist. The firm was given three
       options on 2026-09-08 (work in place / `git init` and then branch / `git init` with the reference
       binaries excluded) AND CHOSE TO WORK IN PLACE, so both changes are edits to the tracked-by-nothing
       working files and no `git init` was run. Written down because an unrecorded impossible request
       reads later as an ignored one.
       THE OTHER CHANGE IN THIS PASS TOOK NO CSS AT ALL. /construction gained a photograph beside the
       Contractor Defense body in the same instruction; it is a figure.feature-media inside a
       .feature-split--reverse and every line it needs already exists in block 4 and 6.27/6.29. Noted so
       that a reader diffing this file against that day's page changes does not go looking for a rule
       that was deliberately never written. THAT is the ordinary path, and this block is the exception:
       a bare figure is what forces both a DOM-order swap and a declaration of its own.

     FOUR DECLARATIONS, AND THE MARK IS A COLUMN NOW RATHER THAN A TOKEN. It was TWO until the afternoon
     of 2026-09-08 - the third pass on this ribbon that day added the other two, and they are argued at
     THE MEASURED EFFECT and at THE CENTRING below. The firm, 2026-09-08, verbatim:
     "I hate where you put the logo for broker services section. It should be large and to the left of
     the text body just like the picture on Real Property Disputes and Litigation section". That
     replaces an arrangement shipped HOURS EARLIER THE SAME DAY, and the earlier reading is worth naming
     because correcting it is the whole of this rule: the mark was taken for a small brand token, sized
     off --logo-h at 80.75px tall and about 138px wide, and placed in a flex row of its own - .brand-row,
     which was block 6.31, and which goes with this change. It had exactly one consumer, /real-estate's
     #brokerage band, and the markup there no longer writes it; the number 6.31 retires with it and is
     not reused, so a later block takes 6.32. The firm was not asking for a token beside a paragraph.
     They were asking for what the PHOTOGRAPH two bands above does.

     SO THE PLACEMENT IS NOT THIS RULE'S AT ALL - IT IS .feature-split's (block 4), THE SAME CLASS THE
     PHOTOGRAPH TAKES. The split already owns the column, the --space-4 gap and the align-items: center
     that sets the mark against the middle of the paragraph rather than against its first line. What is
     left over for this block is the one question the split does not answer: how big the picture is
     inside the column it is handed - and, since the afternoon of 2026-09-08, where the space left over
     inside that column goes.

     THE SIDE IS DOM ORDER AND DELIBERATELY NOT --reverse, AND THE HALF-SENTENCE THAT STOOD HERE IS
     STRUCK RATHER THAN LEFT TO CONTRADICT THE PAGE, because the firm reversed the instruction it
     recorded. It read: "WITHOUT --reverse ... `.feature-split--reverse .feature-media { order: 2 }` is
     the line that puts the disputes photograph on the RIGHT, so the plain class - image first in the
     DOM, first in the grid - is the 'to the left' the firm asked for, and it needed no rule to get
     there." The firm, 2026-09-08 afternoon, verbatim: "on Real estate page, swap the location of the
     Land Catchers logo and the text body so that the text is on left and the logo on right."
     THE MECHANISM THAT SENTENCE CITED IS RIGHT AND IS EXACTLY WHY --reverse IS STILL NOT USED HERE -
     which is worth its own line, because reaching for the modifier is the first thing the next reader
     will do. That rule targets `.feature-media`. This mark is a bare, classless <figure> holding the
     <img> (see the wrapper note at the end of this block), so `--reverse` would match NOTHING and the
     page would not move. The side changed by SWAPPING THE SOURCE ORDER in src/pages/real-estate.html -
     .feature-body first, the <figure> second - since a grid places items in source order. ZERO CSS for
     the side; one declaration changed for the size. No shared rule was widened to serve one page, which
     is the trade a `--reverse` extended to bare figures would have made. The disputes band two bands
     above lands its picture on the same side through `--reverse` because that one IS a .feature-media:
     same result, two mechanisms, and the difference is written down here so it does not later read as
     an accident.

     `width: 85%` TAKES MOST OF THE COLUMN AND `height: auto` TAKES THE FILE'S OWN PROPORTION. It read
     `width: 100%` from the midday pass until the afternoon of the same day, and the argument that put a
     WIDTH here at all is unchanged: sizing by height made the mark small because a height is a number
     the artwork knows nothing about; sizing by width makes it large in the only sense that survives a
     change of viewport - as large as its column, or a stated fraction of it. What moved is the fraction,
     and only the fraction. The firm asked for the mark "slightly" reduced in the same breath as the swap.
     THE MEASURED EFFECT. At 1440 the container is 1224px and .feature-split's two auto-fit tracks are
     589.75px each once --space-4's 44.51px gap is taken out - read off the computed style, not derived.
     The shipped box was 589.75 x 345.00; at 85% it is 501.29 x 293.25, beside the photograph's frame at
     589.75 x 393.83 (its picture 587.75 x 391.83 inside .feature-media's 1px edge, block 4). At 390 the
     two tracks have stacked into one of 346.75px and the mark goes 346.75 x 202.84 -> 294.74 x 172.42.
     The heights are the RENDITION's ratio, 600 x 351 = 1.70940 out of tools/img-manifest.json, NOT the
     2000 x 1169 master's 1.71086 - the page never sees the master and the two disagree by 0.3px at this
     width. The paragraph that stood here took the master's and printed 344.7 for a box that measures
     345.00; the markup pass caught it, and it is corrected here rather than repeated at a new width.
     THE HONEST NUMBER, WHICH IS NOT THE BOX NUMBER, AND THIS NOTE IS NOT TO BE READ AS SAYING IT IS.
     The supplied PNG carries transparent margins, so the BOX and the MARK A VISITOR SEES are two
     different widths, and it is the second one the firm's "slightly" is a judgement about. Measured off
     the shipped renditions rather than assumed: on land-catchers-logo-white-1200.png the ink runs
     x=101..1108 of 1200 - 8.42% of empty at the left, 7.58% at the right, 84.00% of ink between them,
     and 73.0% of the file's height. (The 600w rung measures 8.33 / 7.50 / 84.17: half a pixel of
     rounding at this size, not a disagreement.) So the ink a reader actually sees goes from about 495px
     wide to about 421px. The cut is 15% either way - a percentage of a percentage - but the pair of
     numbers that describes what changed on screen is 495 -> 421, not 590 -> 501, and an already-inset
     mark is what the 15% comes off.
     THE CENTRING IS STRUCTURAL AND NOT DECORATION, WHICH IS WHY `display: block; margin-inline: auto`
     IS PART OF THE SAME CHANGE RATHER THAN A TIDY-UP AFTER IT. 85% of a track leaves 88.46px over and
     that space has to go somewhere. An <img> is a replaced inline-level box by default, so unmargined it
     sits hard against the START edge of its column and the whole 88.46px falls on the far side. WHICH
     side that is changed this afternoon, and that is precisely why the declaration is needed now and was
     not needed at 100%: the mark is the RIGHT-hand column now, so its start edge is the GUTTER it shares
     with the paragraph and its end edge is the page edge. Unmargined, the narrowed mark would crowd the
     text it is meant to sit beside and leave 88.46px of dead ground at the outside of the page - the
     worse of the two asymmetries. Centred, it is 44.23px either side, and the same line does the same
     job for the 52.01px left over on the stacked phone layout. `display: block` RESTATES base (block 3
     sets it on every img on the site) and is kept for the reason `height: auto` is kept: margin-inline
     auto centres a block box and nothing else, so the precondition belongs at the site of the
     declaration that depends on it, not five thousand lines up.
     85% IS A JUDGEMENT ON THE WORD "slightly" AND NOT A DERIVED VALUE. Said plainly rather than dressed
     up: nothing in the type scale, the space scale or the artwork produces it, and a reader is entitled
     to disagree with it in a way they are not entitled to disagree with 589.75. It appears EXACTLY ONCE
     in this file, which is the whole design of it - moving it to 80% or 90% is one number in one place,
     and every figure in the three paragraphs above is 589.75 (or 346.75) times whatever it becomes.
     NO max-width AND NO object-fit, AND BOTH ABSENCES ARE DECISIONS RATHER THAN OMISSIONS. Base (block
     3) already sets `img { display: block; max-width: 100%; height: auto }` on every image on the site,
     so a width of 85% - or of 100%, which is what stood here - cannot exceed a cap it is measured
     against, both being percentages of the same box; restating it here would guard nothing
     (which is why the old rule's `max-width: 100%`, kept for legibility on 6.21's precedent, does not
     survive into a rule whose width is a percentage of the same box). `object-fit` had a job only while
     a HEIGHT was declared: it is the answer to "what happens when the box and the picture disagree", and
     with the height derived from the width there is no box left to disagree with. A fit mode that can
     never fire reads as a guard and is not one. `height: auto` IS restated against base, deliberately -
     this line is where a reader comes to find out how tall the mark is, and "block 3 decides" has to be
     legible at the site of the question; it is also the exact declaration the old `height: var(--logo-h)`
     had to lose, so the diff says what changed.
     THIS RULE NO LONGER SHARES --logo-h WITH THE FOOTER MARK (6.21), and that is the change rather than
     a casualty of it. What stood here argued the shared token as a virtue - one decision, so the two
     marks can never drift - and it was the right argument about the wrong pair: the footer mark is
     CHROME at a fixed height, this one CONTENT at column width. Binding them routes every future change
     to either one through the other.
     --logo-h is untouched and still owns the header mark (6.1, capped at min(--logo-h, 3.75rem)) and the
     footer's (6.21, with .footer-grid's first track sized off it), so nothing else moved when this left.

     ELEMENT-QUALIFIED FOR 6.21's REASON, RESTATED BECAUSE IT IS THE SAME DECISION TWICE: the class sits
     on the <img>. `a.partner-logo` names an element that must not exist, and writing the tag into the
     selector is how that stays true without a second rule to enforce it.

     THE MARK TAKES .feature-split's POSITION AND SIZE WITHOUT TAKING .feature-media, AND THE TWO REASONS
     ARE MECHANICAL RATHER THAN AESTHETIC. This is the question every new picture on this site has to
     answer - which of the existing treatments it takes - and here it answers itself twice, in arithmetic:
       1. THE FRAME WOULD CROP IT. `.feature-media img` declares `aspect-ratio: 3 / 2` with
          `object-fit: cover` (block 4). This artwork is 1.7109, not 1.5. `cover` fills the 3:2 box from
          the short side and takes the overflow off the long one, so the mark loses its own SIDES: the
          ratio of the two aspects is 1.1406, which puts 12.3% of the mark's width outside the box -
          6.2% off each edge, which is 61.8px at the 501.29px this rule now draws and was 82.6px while
          it drew 589.75. THE PERCENTAGE IS THE ARGUMENT AND THE PIXEL FIGURE IS NOT: it moves with the
          width, and both are given here so the second is not read as a fixed property of the artwork.
          (The rendition's 1.70940 against 1.5 gives 12.3% too - the crop does not care which of the two
          ratios above is used.) A cropped photograph is a crop; a cropped
          mark is a broken mark. Undoing it costs an `object-fit: contain` override, and that is a rule
          written to cancel a class rather than to use one.
       2. THE HOVER WOULD BREAK ITS EDGES - AND THE ARGUMENT THAT STOOD HERE NAMED THE WRONG MECHANISM,
          SO IT IS REPLACED RATHER THAN QUIETLY DROPPED (site-plan decision 68). It read: "THE FRAME
          WOULD CLIP IT. The frame is a clipping box by design - `.feature-media[data-parallax]` sets
          `overflow: hidden` (6.27) precisely so the drift has something to be clipped by - and on top
          of that 6.29 scales the picture to 1.028 while the pointer is on it, over 6.27's 1.09 of
          overscan." The cite was right and the conclusion did not follow: `overflow: hidden` is bound
          to `.feature-media[data-parallax]` (6.27), NOT to `.feature-media`, and this mark takes no
          data-parallax - so there would be no clipping box and no 1.09 overscan to compose with. What
          6.29's 1.028 actually does to an UNPARALLAXED frame is push the picture 2.8% PAST the 1px
          edge drawn around it, which is a mark bleeding over its own plate rather than one losing its
          corners. Either way the edges are the defect, and either way it is mechanical: a photograph
          can give up 2.8% of its edges, or overrun them, and still be that photograph - the margin is
          the composition's. A mark has no margin: its edges ARE the artwork.
       data-parallax is likewise NOT applied, on the same mechanical footing rather than a taste one: it
       is a scroll-linked drift written for a picture inside a clipping frame, and with no frame here
       there is nothing for it to drift inside.
     AND THE FRAME AND THE HOVER ARE AN OPEN OFFER TO THE FIRM, NOT A CLOSED DECISION. Recorded that way
     on purpose: a refusal written down without its status hardens into a rule nobody remembers agreeing
     to, and this one is three lines of CSS from being reversed. The bordered plate and the zoom are
     available and have been offered to the firm in one line; what they cost is `object-fit: contain` in
     place of `cover` and no parallax, and the firm should see it before it ships. If the answer is yes,
     this block gains a class and loses nothing it argues here. The only thing the arithmetic above
     actually forbids is the frame WITHOUT those two conditions.

     THE MARK IS WHITE ON TRANSPARENT AND IT READS ONLY BECAUSE OF THE GROUND UNDER IT. Written down
     here because nothing else in this file would say so, and because a dependency nobody states is a
     dependency somebody discovers. #brokerage is .section--alt, whose ground is --bg-2 (the token is
     #161111): white on near-black is the whole of why this mark needs no treatment of its own. But
     --bg-2 IS REBOUND TO --ivory IN TWO PLACES, and in both of them a white-on-transparent mark
     disappears completely - not dimmed, gone:
       .band--ivory   block 3, on the safety net's one line that sends all four grounds to --ivory
                      together. HYPOTHETICAL: #brokerage is not ivory today, nothing plans to make it
                      so, and the band is still waiting for the first light section to ship. (Grep the
                      four-ground line rather than this note - it is quoted here nowhere, on purpose.)
       @media print   6.25, the byte-identical line by design - print reuses the band's rebindings
                      rather than keeping a second set of colour decisions that would drift. NOT
                      hypothetical: 6.25 hides the chrome and .hero-media but NOT this mark, and it
                      does not reset .section--alt's background, so this mark prints white on ivory
                      and prints as nothing.
     The print case is STATED HERE RATHER THAN REPAIRED, deliberately: every repair is a second rule and
     a second decision - a dark rendition of the mark, or a print-only `display: none` that concedes it
     carries nothing on paper - and that is the firm's to take, not this pass's. Whoever takes it, the
     fix belongs to the MARK and not to the ground: both rebindings exist to GUARANTEE legibility, and
     an exception carved into either one is a hole in that guarantee. THIS NOTE SURVIVES BOTH RESIZES AND
     THE SWAP UNCHANGED IN SUBSTANCE AND IS RE-CHECKED EACH TIME, NOT INHERITED: re-verified at the
     afternoon pass of 2026-09-08 that #brokerage is still `section section--alt`, that the four-ground
     rebinding line is still present in BOTH places byte-identical, and that nothing in @media print
     hides or re-grounds this mark (grep `partner-logo`: every hit is inside this comment except the
     selector at its foot - no print rule and no other block anywhere in the file names it). The mark is still bigger than the token it
     replaced, which keeps the print case worse rather than better: what prints as nothing now prints as
     a 501.29px-wide nothing - 589.75 until this afternoon, and still more than three times the ~138px
     token the case was born under. SWAPPING THE COLUMNS CHANGES NOTHING HERE, and that is worth one
     clause rather than silence: the ground is the BAND's and the mark carries its own transparency into
     whichever column it lands in, so moving it left to right neither creates the problem nor repairs it.
     One phrase moved:
     the print line said "NOT a content figure", and what it names is this mark itself, whatever wraps
     it. CORRECTED AT THE 2026-09-08 CLOSE-OUT (site-plan decision 68), because the sentence that stood
     here was FALSE ABOUT THE SHIPPED PAGE rather than merely dated. It read: "the mark is not a <figure>
     and never was - a bare <img>, a grid item of .feature-split now and of .brand-row before". The
     markup that landed with this rule wraps the <img> in a BARE, CLASSLESS <figure> and argues the
     wrapper at length (src/pages/real-estate.html #brokerage): the FIGURE is .feature-split's grid item
     and the <img> is its only child, the same two elements the disputes split uses. It was the split's
     FIRST item until the afternoon of 2026-09-08 and is its SECOND one now, which is the entirety of the
     swap - the elements did not change, their order did, and the two bands consequently no longer read
     in the same source order even though they now show the same arrangement. The <img> was the grid item
     itself only while .brand-row existed, which is the state that sentence was written in.
     THE WRAPPER IS ALSO WHY THE SWAP HAD TO BE DOM ORDER, so the two notes are one fact seen twice: a
     classless <figure> is invisible to `.feature-split--reverse .feature-media`. And the wrapper is why
     the percentage still lands where it should - `width: 85%` is a percentage of whatever box holds the
     img, base zeroes figure's UA margin (1516), and a figure with no class takes no border, no
     aspect-ratio and no hover, so it hands its grid track straight through to the img and the img's own
     `margin-inline: auto` centres inside the full track rather than inside some narrower thing. What the
     wrapper is NOT is a figure.feature-media: the frame's treatment is keyed on that class, never on a
     bare tag, so every refusal argued above stands unchanged.
     THE PERCENTAGE IS GONE AND SO IS THE CENTRING, on the firm's instruction of 2026-09-08, verbatim:
     "move the logo in line with the title and the text body and shrink the logo to 75% of current size.
     The logo should be under title but above text body."
     WHAT THAT RETIRES, KEPT RATHER THAN DELETED because two paragraphs above still reason about it: the
     rule was `width: 85%; margin-inline: auto`, and BOTH halves were about living in a grid track. The
     85% was a share of .feature-split's column and the auto margin centred the mark inside that column,
     which is what produced the 44.23px of air either side that the afternoon note tabulates. There is no
     column any more - the band is one stacked run in .feature-body (see real-estate.html #brokerage) -
     so a percentage would now resolve against the FULL container and the auto margin would centre the
     mark against text that is left-aligned. Both had to go together; neither makes sense without the
     grid that motivated it.
     WHY A FIXED rem AND NOT A PERCENTAGE. "75% of current size" is an instruction about the RENDERED
     mark, not about a share of a box that no longer exists. Measured on the shipped page before this
     change, at 1440x900 with root 17px: the img was 501.28px wide (x=778.98), while the h2 and the
     paragraph both began at x=100.5. 0.75 x 501.28 = 375.96px, and 375.96 / 17 = 22.115rem. Expressed in
     rem it scales with the visitor's own root setting like every other length in this system, which a
     px value would not.
     THE min() IS THE OVERFLOW GUARD, not a second opinion about size. .container is
     `min(100% - 55.01px, 72rem)` (1765), so below a 430.97px viewport the container is narrower than
     375.96px and a bare width would push the mark out of it. min(22.115rem, 100%) pins the mark at
     375.96px everywhere it fits and hands it the container width where it does not.
     THE NEGATIVE START MARGIN IS AN OPTICAL CORRECTION AND IT IS MEASURED, NOT EYEBALLED. `margin-inline:
     0` alone puts the img BOX on the container's left edge, but the artwork does not fill its own box:
     land-catchers-logo-white-1200.png is 1200x701 and its first non-transparent column is 101, measured
     by reading the alpha channel off a canvas (>8/255 counts as ink). So 101/1200 = 8.41667% of the
     mark's width is transparent bleed on the left, which at the 375.95px render is 31.64px of nothing.
     Box-aligned, the mark therefore READS as indented against a title and a paragraph that both start at
     x=100.5 - which is the opposite of the "in line with the title and the text body" the change was
     asked for. Pulling the box left by exactly that fraction puts the first INK column on x=100.5
     instead, so what the eye lines up is the logo itself rather than its padding. The fraction is applied
     to the same min() the width uses, so the correction tracks the mark at every width instead of being
     a desktop-only constant. TO REVERT TO BOX ALIGNMENT, delete the margin-inline-start line: nothing
     else depends on it. */
  img.partner-logo {
    width: min(22.115rem, 100%);
    height: auto;
    display: block;
    margin-inline: 0;
    margin-inline-start: calc(min(22.115rem, 100%) * -0.0841667);
  }

  /* ---- 6.32 CENTRE ACTIVATION ON TOUCH ([data-centred]) --------------------------------------------
     THE FIRM, 2026-09-08, asked for the hover craft to happen by itself on a phone: the six practice
     clips to play "when they become proximately centered on screen", and then "the same thing" for "all
     cards and bullet point lists throughout the website (e.g. static photos cards zoom in and add the red
     box on mobile automatically at center screen)". They specified the band: rootMargin
     "-33% 0px -33% 0px" with threshold 0, which leaves the middle 34% of the viewport as the trigger.

     EVERY RULE BELOW IS A MIRROR, NOT A NEW TREATMENT, and that is the design. Each pair is the existing
     :hover rule and its touch twin, written adjacent so they cannot drift: the same declarations, the
     same tokens, the same magnitudes. Nothing here invents a value. js/main.js block 16 sets and clears
     data-centred and does nothing else; the six clips are PLAYED by block 14, which already owns the
     one-at-a-time slot.

     THE GATES ARE THE MIRROR OF THE HOVER GATES. Hover is `:where(html:not(.has-touch))`; this is
     `:where(html.has-touch)`. The two are mutually exclusive by construction, so a device can never get
     both, and a hybrid that proves a mouse mid-session drops out of one and into the other because block
     16 tears down on pointerkindchange. Reduced motion needs no rule here: block 16 never builds under
     it, so the attribute is never set, and the guards layer's own reduced-motion clamps are untouched.

     :where() KEEPS SPECIFICITY AT ZERO for the html part, exactly as the hover rules do, so these do not
     outrank anything the resting state declares - which is the whole reason 6.29's --photo-lift pattern
     works through a custom property rather than through a `scale` that could lose a specificity contest.

     THE VIDEO OPACITY IS KEYED ON THE CARD BEING IN THE BAND, NOT ON IT PLAYING, and that is deliberate
     rather than sloppy. Two cards can share the band; only the nearest to the viewport centre plays. A
     banded-but-not-playing card therefore shows its <video> at opacity 1, paused at frame 0 - and frame 0
     of each clip IS the still <img> underneath it (6.9a's rewind depends on the same fact), so there is
     nothing to see. Keying on a "playing" attribute instead would have meant block 14 writing DOM state
     it does not otherwise write, to prevent an artifact that does not exist.

     THE BULLET LISTS ARE NOISIER HERE THAN ON HOVER, said plainly. A hover moves one item; the band is
     about 287px on an 844px phone and check-list items are roughly 44px tall, so six or seven nudge
     together. That is what the instruction asks for. If it reads as busy, the dial is the last selector
     pair below - remove it and lists go back to resting on touch - not the JavaScript. */

  /* (a) THE PHOTOGRAPH: zoom + red ring. Mirrors 6.29's pair exactly - the same two custom properties,
     inherited by the picture and the ring without either needing a selector. */
  :where(html.has-touch) .svc-card[data-centred] .svc-card-media,
  :where(html.has-touch) .feature-media[data-centred] {
    --photo-edge: var(--brand-fill);
    --photo-lift: var(--photo-push, 1.028);
  }

  /* (b) THE PRACTICE-AREA CLIP. Mirrors 6.9a's pair. The transition rule is mirrored too, or the clip
     would appear by cutting rather than by fading - 6.9a calls that fade the gesture. */
  :where(html.js.has-touch) .svc-card-video { transition: opacity var(--dur-media) var(--ease-out); }
  :where(html.js.has-touch) .svc-card[data-centred] .svc-card-video { opacity: 1; }

  /* (c) THE DISCIPLINE CARDS on /attorneys. Mirrors 6.11's pair: the border warms and the big numeral
     lifts by twice --nudge. */
  :where(html.has-touch) .act-card[data-centred] { border-color: rgba(var(--brand-rgb), 0.57); }
  :where(html.has-touch) .act-card[data-centred]::before { color: rgba(var(--ink-rgb), 0.09); translate: 0 calc(-2 * var(--nudge)); }

  /* (d) THE BULLET LISTS. Mirrors 6.7's pair: the item nudges one --nudge and its diamond takes the 1.5
     multiplier and the --ink stop. 1.5 is 6.7's own number, derived there against a 5.09px mark; it is
     reused rather than re-derived because the mark renders at the same size on a phone - the figure that
     would need re-deriving is --pop on the stats row, which is NOT mirrored here. */
  :where(html.has-touch) .check-list li[data-centred] { translate: var(--nudge) 0; }
  :where(html.has-touch) .check-list li[data-centred]::before { scale: 1.5; color: var(--ink); }

  /* (e) THE STATS ROW, added 2026-09-09: "make the years of experience, 3 disciplines, and AVVO badges
     animate upon entering mid screen". Those are four elements of one component - all .stat - so the
     observer takes one selector and these mirror 6.12's and 6.19's four hover rules.
     THE SOURCE ORDER IS COPIED, NOT INVENTED, and it is the reason these four are written in this
     sequence rather than grouped by property. In the hover rules the linked-stat colour comes BEFORE
     .stat-num's own, so on a linked stat the number resolves to --brand and not --ink. Reordering them
     here would quietly give the two Avvo badges a different colour on a phone than a mouse gives them,
     which is exactly the drift this whole section is written to prevent.
     WHAT --pop IS WORTH HERE IS SMALLER THAN ON A DESKTOP, and it is stated rather than left to be
     discovered. 1.06 was derived against a --text-4 figure at desktop metrics, where it buys roughly two
     and a half pixels of growth - the amount the derivation says the eye reads as a deliberate gesture.
     --text-4 is smaller on a phone, so the same multiplier buys proportionally less. It is REUSED rather
     than re-derived because re-deriving it would put a second magnification constant into the system for
     one row on one breakpoint, and because the firm asked for the desktop gesture on mobile rather than
     for a new one. If it reads as too subtle on the phone, the dial is a phone-scoped --pop, not this
     selector list. */
  :where(html.has-touch) .stat--linked[data-centred] :is(.stat-num, .stat-label) { color: var(--ink); }
  :where(html.has-touch) .stats-row .stat--linked[data-centred] svg { color: var(--brand); }
  :where(html.has-touch) .stat--linked[data-centred] :is(.stat-num, svg) { translate: 0 calc(-1 * var(--nudge)); }
  :where(html.has-touch) .stat[data-centred] .stat-num { color: var(--brand); scale: var(--pop); }   /* TEXT at --text-4: large text, and --brand misses the 3:1 large-text floor - the same recorded failure the hover rule at 6.19 carries, on the firm's decision of 2026-09-06 */

  /* (f) THE RECOGNITION MARKS, added 2026-09-09. The firm asked for the Super Lawyers logo "all white
     and turn red upon mouse hover". A phone has no hover, so the mark joins the data-centred pattern for
     the same reason everything else in 6.32 did: without it, a touch visitor never sees the state the
     firm asked for. The hover rule is in 6.34; this is only its touch twin. */
  /* BOTH MARKS, NOT JUST THE WORDMARK. The Avvo badge carries .recognition-badge, not
     .recognition-mark, so the first version of this rule missed it entirely - it never went red on
     hover and never responded on touch. The firm reported both. :is() names the two rather than
     giving the badge a class it does not otherwise need. */
  :where(html.has-touch) .recognition[data-centred] :is(.recognition-mark, .recognition-badge) { color: var(--brand); }
}

/* ---- 6.34 THE RECOGNITION MARKS (.recognitions) --------------------------------------------------
   THE FIRM, 2026-09-09: the Super Lawyers logo, "make the logo all white and turn red upon mouse hover",
   linked to their profile and captioned.
   WHITE IS --ink AND RED IS --brand, THROUGH ONE PROPERTY. The inline SVG paints fill: currentColor, so
   the mark takes whatever `color` the link carries and one declaration moves the whole logo. That is
   also why the logo is inline rather than an <img>: an <img> cannot inherit color, and the alternatives
   are two files or a CSS filter, both of which make the red a guess rather than the token.
   THE MARK IS EXEMPT FROM CONTRAST, WHICH IS WHY THIS IS NOT ANOTHER RECORDED FAILURE. WCAG 1.4.11 says
   text that is part of a logo or brand name has no contrast requirement, and this is the brand mark
   itself. The CAPTION next to it is ordinary text and stays --ink-2 at 8.44:1 on --bg, untouched by the
   hover.
   THE WIDTH IS CAPPED IN rem AND NOT IN ch OR vw. The artwork is 576 x 127.84 - a long horizontal
   wordmark, about four and a half times wider than it is tall. 18rem = 306px holds it at a legible
   height (67.9px) inside the prose column without ever exceeding it, and min(...) hands it the column on
   a phone. (The proportion is written in WORDS on purpose. Set as a number against a colon against a
   one, it reads to verify_ratios.py as a CONTRAST claim and fails the build - which it did, twice: once
   for the proportion itself and once for the sentence that explained the first failure while quoting the
   very form it was warning about. An aspect ratio and a contrast ratio share a notation the checker
   cannot disambiguate, so the aspect one gives way.) */
@layer components {
  .recognitions { list-style: none; padding: 0; margin-top: var(--space-4); display: grid; gap: var(--space-4); }
  .recognition { display: inline-flex; flex-direction: column; gap: var(--space-2); align-items: flex-start; text-decoration: none; color: var(--ink); }
  .recognition-mark { width: min(18rem, 100%); height: auto; display: block; fill: currentColor; transition: color var(--dur-base) var(--ease-out); }
  /* THE BADGE IS A DIFFERENT SHAPE AND TAKES A DIFFERENT CAP. The wordmark is a long horizontal lockup;
     this one is an upright shield whose artwork is 275.303 wide by 210.256 tall, so the same 18rem
     would render it roughly 234px tall and tower over the wordmark beside it. 9.5rem = 161.5px holds it
     at about 123px tall, which sits close to the wordmark's 68px cap-height band without either mark
     dominating. It already paints currentColor throughout - 25 fills, plus one var(--bg) for the
     knocked-out name, which is why that name stays legible when the shield turns red. */
  .recognition-badge { width: min(9.5rem, 100%); height: auto; display: block; fill: currentColor; color: var(--ink); transition: color var(--dur-base) var(--ease-out); }
  .recognition-text { color: var(--ink-2); }   /* 8.44:1 on --bg - ordinary text, and it keeps its stop through the hover */
  :where(html:not(.has-touch)) .recognition:hover :is(.recognition-mark, .recognition-badge) { color: var(--brand); }
  .recognition:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }   /* --ink, not --brand: the focus ring is an indicator a visitor NAVIGATES by, and 6.6's rule is that those take the ink stop */
  /* ---- 6.35 THE RATINGS DISCLAIMER (.ratings-note) -----------------------------------------------
     The firm, 2026-09-09: "add the disclaimer for me". Drafted for them to approve, not asserted as
     compliant - the reasoning and the two open jurisdictional questions are in site-plan decision
     "ratings-disclaimer-2026-09-09".
     --ink-2, NOT --ink-3, AND THAT IS THE WHOLE DESIGN DECISION HERE. Every instinct says a legal note
     recedes, and --ink-3 is the stop this system keeps for text that may. It is the wrong stop for this
     paragraph: Texas r. 7.01(d) and cmt 6 require a disclaimer to be legible and "likely to be noticed",
     and California r. 7.1 judges the section as one impression - a note dimmed until it reads as
     boilerplate is arguably worse than none, because it looks like disclosure while working as decoration.
     --ink-2 on --bg is 8.44:1, the same ink .recognition-text and .stat-note take, so the note recedes by
     SIZE alone and never by dimming. This is the same argument _headers' .footer-disclaimer note already
     makes about the footer's three paragraphs, applied to a fourth.
     --text-label for size, --measure for width: both are the system's existing answers and neither needed
     a new value. No new token, no new stop. */
  .ratings-note { font-size: var(--text-label); color: var(--ink-2); max-width: var(--measure); margin-top: var(--space-4); }
}

/* @keyframes FOR 6.28 AND 6.17, DECLARED AT TOP LEVEL AND DELIBERATELY OUTSIDE EVERY LAYER. @keyframes is
   not subject to cascade layers at all - name resolution is by document order, last definition wins - so
   putting them inside @layer components would imply a precedence that does not exist, and has historically
   tripped implementations that looked the name up layer-first. The rules that USE them stay in the
   components layer, where guards can still beat them.
   [MOTION v20] THERE ARE TWO NOW, AND THE SENTENCE THAT STOOD HERE HAS TO GO RATHER THAN BE QUALIFIED. It
   read "This is the first and only @keyframes in the file: grep it before adding a second", and the
   instruction inside it is the half worth keeping: BOTH declarations live here, in this one block, so the
   grep that sentence asked for still answers in one place. The bar for a third is unchanged and is worth
   restating, because a stylesheet acquires keyframes the way it acquires colours - one reasonable case at
   a time. A transition is the default and a @keyframes has to earn itself by being a state change CSS
   cannot express as a transition between two selectors: 6.28 animates from a value nothing declares
   (there is no "before load" selector), and 6.17's success plate arrives out of display: none on a click,
   where there is no class change for a transition to key on. Anything with two selectable states is a
   transition. */
@keyframes hero-slow-zoom {
  /* [ZOOM v13] THE NAME IS KEPT THOUGH IT NOW DESCRIBES THE OLD DIRECTION, AND THAT IS A DECISION, NOT
     AN OVERSIGHT. `hero-slow-zoom` is the handle DESIGN-SYSTEM.md s13.3 and MARKUP-CONTRACT.md use for
     this effect; renaming an identifier in one of the three files that talk about it is how a cross-file
     grep goes stale. 6.28's heading carries the current description - one second, outward.
     THE `to` IS 6.27's OVERSCAN, AND THE HAND-COPY THAT USED TO BE THIS EFFECT'S ONE FRAGILE JOINT WENT
     WITH THE REVERSAL. While the animation runs it takes the `scale` property over completely, so
     6.27's own resting `scale` - `var(--frame-scale)` since [PHOTO v16], the same 1.09 by another
     spelling - is never the computed value on a zooming hero. When 1.09 was the `from`
     that mattered: 6.27 could not rescue a start value that dropped below the floor, and the two numbers
     stayed in step only because a reader remembered to keep them there. Now the animation ENDS on 1.09
     and `both` holds it, so the value the resting rule declares and the value the animation leaves
     behind are one number by construction. The failure mode improved with it - get this wrong and the
     picture visibly settles at a framing no still page uses, instead of silently exposing a frame edge
     at the extremes of a scrub nobody is watching for.
     THE FLOOR, re-checked against the shipped travel: main.js's initParallax declares TRAVEL = 4
     (percent, +-), so p = 0.04 and 6.27's s >= 1 / (1 - 2p) = 1 / 0.92 = 1.08696. If either number
     moves, move BOTH, and re-check that the SMALLER of the two - which is now the `to`, not the `from` -
     still clears the result.
     [MOTION v21] THE TWO LITERALS ARE NOW ONE LITERAL AND ONE REFERENCE, AND THAT REPAIRS "THE ONE COST
     THAT SURVIVES" 6.28 NAMES AND ACCEPTS. What shipped was `to: 1.09` - 6.27's overscan, written out by
     hand - on an animation gated on NEITHER html.js NOR the 67rem query, with `both` parking the picture
     there forever. So on a phone, and on any page with JS off, every photograph hero rested at 1.09: a
     4.5% crop per edge bought with no scrub at all, which is the exact trade 6.27's gate (b) forbids by
     name ("an unconditional 1.08 would crop 4% off a photograph forever in exchange for nothing").
     Measured before the repair on /employment-law's .hero-media img: desktop+JS scale 1.09 (intended);
     desktop no-JS 1.09 with --frame-scale unset; phone 390 with and without JS, 1.09 likewise. 18 of the
     20 built pages carry the element.
     6.28's OWN DEFENCE OF THAT COST WAS THAT GATING THE ZOOM WOULD "take the effect away from phones,
     which is where the firm will look first", AND IT IS RIGHT. So the zoom is NOT gated. What changes is
     that the pair stops being absolute and becomes RELATIVE TO WHATEVER FRAMING THE ELEMENT RESTS AT:
       `to`   = var(--frame-scale, 1)               1.09 where 6.27 declares overscan, 1 everywhere else
       `from` = that + the firm's own 0.10 of travel
     Desktop with script is byte-identical to what shipped (1.19 -> 1.09). Phone and no-JS now run
     1.10 -> 1.00 and SETTLE AT NATURAL SIZE, so the entrance survives everywhere and the permanent crop
     exists only where a scrub needs it. The 0.10 of travel - the whole of the firm's gesture - is the
     same number in every case.
     AND THE FRAGILE JOINT IS FINALLY GONE RATHER THAN MERELY IMPROVED. The paragraph above says the `to`
     and 6.27's resting rule "are one number by construction"; they were one number by COINCIDENCE, kept
     in step by a reader remembering. They are now one DECLARATION, so 6.27 can move --frame-scale and
     this animation follows it. The floor check follows it too: the scrub only exists where
     --frame-scale is 1.09, and 1.09 clears the floor; where there is no scrub there is no floor to meet.
     THE UPSCALE ARGUMENT IMPROVES IN THE SAME STROKE. 6.28 computes the old resting 1.19 as 6.0% of
     interpolation and calls it "asked for only in the first frames"; on a phone it was asked for
     permanently, out of a 480px rendition. The transient is now 10% of a phone rendition for 1.5s and
     0% at rest. */
  from { scale: calc(var(--frame-scale, 1) + 0.10); }
  to   { scale: var(--frame-scale, 1); }
}

/* [MOTION v20] THE SECOND, FOR 6.17's SUCCESS PLATE. Two properties, both compositor, and the `to` is the
   element's own resting appearance rather than a value invented for the animation - which is what makes
   `both` safe under block 5's clamp (0.001s of a both-filled run leaves the plate exactly where a page
   with no animation leaves it) and is the property 6.28's `to` had to be REVERSED to obtain.
   THE 28px IS guards 7.3's REVEAL TRAVEL, NOT A NEW LITERAL. This is a PLATE arriving, it takes the plate
   register's duration at the use site, and it takes the same distance every other plate on the site
   travels. `transform` rather than `translate` because nothing else on this element declares either and
   the shorthand keeps the pair readable as one gesture. */
@keyframes form-sent-arrive {
  from { opacity: 0; transform: translateY(28px); }
  to   { opacity: 1; transform: none; }
}

/* ---- 7 · GUARDS (continued) - win by layer, not by position -------------------------------------
   A SECOND @layer guards block; layers merge, so it lands beside the reduced-motion clamp in block 5
   and beats every component regardless of position.
   THE POINTER GATE. Whether to run hover craft is a fact about the INPUT DEVICE, and no media query
   can establish it: a Windows touchscreen laptop reports (hover: none) and (pointer: coarse) for its
   PRIMARY pointer even with a mouse attached. So the gate is a runtime class main.js writes from real
   pointerType events, and every hover in the components layer sits inside :where(html:not(.has-touch))
   - ON by default, subtracted only on positive proof of touch, so the failure mode is "a touch user
   briefly sees a hover style", never "a desktop user gets a dead page". :where() contributes zero
   specificity, so the gate is free. Keyboard parity is declared OUTSIDE the gate in the same component
   block, deliberately quieter. Nothing may be guarded by a different signal in CSS than in JS - one
   would silently negate the other while every assertion passed. */
@layer guards {

  /* 7.1 In-page targets under the fixed header. Every section id in site-plan.json is a _redirects
     target and an in-page anchor, and a fixed 97.75px bar would swallow the heading it lands on.
     Declared once, on the attribute, so no component has to remember it. */
  [id] { scroll-margin-top: calc(var(--header-h) + var(--space-2)); }

  /* 7.2 Touch subtraction. Because every hover is authored inside the positive gate there is no
     latched :hover to undo; what touch still needs is the affordance hover was carrying: expand
     .link-arrow's hit area, and only in the axis where it cannot steal a tap from the end of an
     adjacent word.
     [LINK v15] THE .svc-card-link HALF OF THIS BLOCK IS RETIRED, PREMISE AND ALL. It used to raise the
     card link to a 44px floor on the stated grounds that ".svc-card is an <article>, not an anchor, so
     on a tall full-width card the only tappable region is the ~30px link strip at the bottom". That
     sentence was an accurate description of a BUG - 6.9's stretch overlay had been overwritten by
     6.19's drawn rule since the block was written - and it is false now that the overlay works: the
     tappable region is the whole 390 x 489 card, which clears the 44px floor by an order of magnitude.
     Both rules are therefore withdrawn rather than kept as insurance. A 44px minimum on a link that is
     not the tap target is 14px of phantom card height, and .link-arrow's own align-items: center
     already said the other half. The ::before is excluded for the harder reason: 6.9 makes the card
     link position: static so its ::after can reach the card, so an unscoped ::before here would
     position a 44px strip against .svc-card - a second, invisible hit box across the middle of the
     picture, overhanging the card by 1rem into an overflow: hidden edge. One pseudo-element, one owner,
     on this element as on its sibling. */
  :where(html.has-touch) .link-arrow:not(.svc-card-link)::before { content: ""; position: absolute; left: 0; right: -1rem; top: 50%; translate: 0 -50%; height: 44px; }

  /* 7.3 THE REVEAL PRE-STATE. CSS owns it; JavaScript only ever animates TOWARD visible - it adds
     .is-revealed and nothing else, and never writes opacity: 0 or a transform. If JS never hides
     anything, no JS failure can leave anything hidden. Three conditions, all load-bearing:
       html.js            proof the script tag ran; the 2.5s watchdog strips it if main.js never stamps
                          data-motion-ready, and every reveal target then renders finished.
       [data-reveal]      opt-in per the markup contract; the 404 carries none.
       :not(.is-revealed) so the pre-state applies only until the element arrives. Without it these
                          rules apply for the life of the page, which forces the reveal to hold an
                          inline transform forever - and an inline transform outranks every author
                          rule, silently killing every CSS :hover transform on a revealed element. The
                          signature: hovers work before an element scrolls in and stop working after.
     will-change sits on the UNCONDITIONAL selector, one line below, and NOT in this pre-state: a hint
     scoped to :not(.is-revealed) is withdrawn by the very class change that starts the transition, so
     it would be gone for the whole 0.825-1.425s it is wanted and present only while nothing is moving.
     On the unconditional selector it covers the transition and main.js's release (finish() sets
     style.willChange = "auto") becomes load-bearing: the argument is sized against the site's worst
     case, which is a page carrying enough [data-reveal] targets that an uncleared promotion would leave
     that many permanent compositor layers.
     [CARD-EDGE v36, 2026-09-08] THE THREE FIGURES THAT STOOD HERE WERE RE-COUNTED AND ALL THREE HAD
     MOVED - the paragraph's own closing instruction is "re-count the worst case whenever any page
     grows", and this pass grew one, so it was obeyed rather than quoted. It read: "dist/business.html,
     which carries 38 [data-reveal] targets... dist/attorneys.html is next at 37 and dist/index.html
     carries 28 (counted on the built pages, 2026-09-05)". THE WORST CASE IS NO LONGER /business AND HAS
     NOT BEEN SINCE THE FIRM REWROTE IT INTO SIX CARDS ON 2026-09-06 - a card is ONE object and is never
     sub-staggered, so collapsing six bands into six plates removed targets rather than adding them.
     Counted on the built pages with HTML comments stripped, 2026-09-08: dist/firm-overview.html 32,
     dist/attorneys.html 30, dist/flat-fee-services.html 30, dist/employment-law.html 28,
     dist/index.html 28, dist/business.html 21 - the three the section head added on 2026-09-08 are
     INSIDE that 21, and the page is still eleven targets short of the worst case even with them, which
     is the measured answer to the obvious worry about adding a head to a card band. THE ARGUMENT IS
     UNCHANGED AND IS WHY THE NUMBERS ARE KEPT AT ALL: it is sized
     against the heaviest page, the heaviest page is 32 and not 38, and main.js's finish() releasing
     will-change is still load-bearing. The home page is NOT the heaviest and never was - re-count the
     worst case whenever any page grows, and note that it is now a page nobody would guess. */
  /* 28px and the hero's 45.5px below are MOTION DISTANCES in absolute px and they do NOT take the root
     multiplier: a reveal's travel is calibrated against the eye, not against the type it carries, and
     scaling it with [RED v7] would have made every arrival 6.25% further for no reason anyone could
     name. Stated explicitly because the next editor's reflex will be to scale them, and NOTHING BELOW
     WEAKENS IT - a deliberate motion decision that moves these two numbers is a different act from a type
     change dragging them along behind it, and the rule exists to stop the second, not the first.
     [MOTION v20] 16 -> 28px AND 26 -> 45.5px, x 1.75, ON THE FIRM'S "too subtle. Make them more
     prominent". THE RATIO BETWEEN THE TWO IS HELD EXACTLY (45.5 / 28 = 26 / 16 = 1.625) - one multiplier
     on both, for the same reason the ladder takes one multiplier on every rung: the hero travels farther
     than the page because it has the room, and that relationship is the design, not the two literals.
     WHY 1.75 AND NOT 2, and this is the joint solve the duration could not be done without. Apparent
     speed is travel over duration, so raising travel alone makes an arrival FASTER, which is half of what
     the firm complained about. The ladder went x 1.5 in the same pass, so:
       travel x 1.75 / duration x 1.5 = apparent speed x 1.17
     - 17% quicker to cover 75% more ground, which reads as weight. At x 2 travel the same duration gives
     x 1.33 and the arrivals start to read as thrown rather than placed; at x 1.5 travel the apparent
     speed would not move at all and the extra distance would be invisible, which is the failure this
     whole pass exists to correct.
     WHAT IT MEASURES OUT AT, on the PLATE register (1.2s), so the claim is checkable rather than
     asserted. Both easings finish 90% of their travel at a fixed fraction of the run - quint-out at
     0.374, expo-out at 0.329, solved from the curves in the tokens block:
       BEFORE  16px over 0.96s quint : visible phase 0.359s, 14.4px covered, peak velocity 4.545 x 16 / 0.96 =  75.8 px/s
       AFTER   28px over 1.20s expo  : visible phase 0.395s, 25.2px covered, peak velocity 6.25  x 28 / 1.20 = 145.8 px/s
     The visible phase is 10% LONGER, the distance 75% greater and the peak velocity 1.92x. "Too fast" and
     "too subtle" are answered by different terms of the same sum, which is why travel, duration and curve
     had to be decided together rather than one at a time.
     A CHECK, AND EXPLICITLY NOT THE DERIVATION: 28px happens to sit within a pixel of one line of body
     leading (--text-0 17px x --leading-body 1.68 = 28.56px). That is a coincidence worth knowing when
     judging the distance by eye and it is NOT what the number is derived from - deriving it from type is
     precisely what the paragraph above forbids, because the next root change would then scale it. */
  html.js [data-reveal]:not(.is-revealed) { opacity: 0; transform: translateY(28px); }
  html.js [data-reveal="fade"]:not(.is-revealed) { transform: none; }   /* imagery: opacity only */
  /* The hero has vertical room for a longer lift - but the fade opt-out must survive it, or
     .hero-media (position: absolute, inset 0, data-reveal="fade") would arrive 45.5px off its frame and
     leave a band of bare ground under the scrim. The :not() is why this rule cannot simply be more
     specific than the one above it. [MOTION v20] THE OPT-OUT MATTERS MORE AT 45.5px THAN IT DID AT 26,
     and it is the one thing in this pass that was re-checked in the browser rather than reasoned about:
     a 45.5px band of bare ground is not a subtle defect, it is the bottom of the hero missing for the
     length of a reveal. The :not([data-reveal="fade"]) is unchanged and still carries it. */
  html.js .hero [data-reveal]:not([data-reveal="fade"]):not(.is-revealed) { transform: translateY(45.5px); }

  /* 7.4 THE REGISTER TRANSITIONS. Durations are tokens; the SELECTORS mirror main.js's CHOREO registry
     and must stay byte-identical to it. Copy this list into main.js:
       DISPLAY  ".hero-title, .display, .quote"   -> --reveal-display 1.425s
       PLATE    ".svc-card, .act-card, .package, .contact-card, .feature-media, .quote-card"
                                                  -> --reveal-plate 1.2s
       default  everything else                   -> --dur-slow 0.825s
       opt-out  [data-reveal="fade"]              -> --dur-media 1.275s
     ORDER IS REVERSED HERE ON PURPOSE: main.js takes the FIRST matching row, CSS the LAST matching
     rule at equal specificity, so writing PLATE first and DISPLAY second makes the two agree - an
     element matching both lands in DISPLAY either way. Nothing in the shipped markup matches both
     today (the contract puts [data-reveal] on the card, never inside it, because a card is ONE object
     and is never sub-staggered), but the ordering is what keeps that true when a component is added.
     THE STAGGER ARRIVES AS A CUSTOM PROPERTY, NOT AS A DECLARATION. main.js owns the cascade across a
     group (step = min(0.135s, 0.675s / (n - 1))) and publishes each element's place in it by writing
     ONE inline custom property - `--reveal-delay: <n>s` - which this block turns into the element's
     transition-delay and every decorated pseudo-element adds to its own fixed intra-object lag. The
     reason it cannot be an inline `transition-delay` (which is what it used to be): transition-delay
     is not inherited and never reaches ::before / ::after, so every hairline keyed on
     :not(.is-revealed) - .eyebrow::before, .num-list li::after, .stat::before, .quote-card::before -
     started drawing at t=0 while its element was still holding opacity 0 through the stagger. The
     draw ran to completion invisibly and the object arrived with its rule already finished; only
     index 0 of each batch ever showed the gesture. A custom property inherits, so the pseudo-elements
     see it; --lag-rule and the quote bar's 0.27s then measure from the element's own zero, which is
     what makes them intra-object lags rather than accidents of batch position.
     The default is declared HERE as well as defaulted at every use site: without it a [data-reveal]
     nested inside another would inherit its ancestor's stagger. Inline style outranks it, so main.js
     always wins where it writes; finish() removes the property and the 0s default returns.
     The reduced-motion clamp still neutralises the whole thing (guards resets transition-delay to 0s
     !important on *, *::before and *::after), which is exactly what it is for.
     Everything here is a compositor property - opacity and transform - and nothing repaints. */
  /* ---- [MOTION v20] THE TRANSITION LIST HAS FOUR PROPERTIES NOW, AND THE TWO NEW ONES ARE A REPAIR THIS
     PASS FOUND RATHER THAN A FEATURE IT ADDED. MEASURED IN CHROME ON dist/, not deduced: on a revealed
     .svc-card, getComputedStyle(...).transitionProperty read "opacity, transform" and transitionDuration
     read the plate's own reveal figure. THE CARD'S OWN transition FROM 6.9 WAS NOT THERE AT ALL.
     THE CAUSE IS THIS RULE, AND IT IS A LAYER COLLISION RATHER THAN A SPECIFICITY ONE. `transition` is a
     shorthand, so declaring it here resets every transition-* longhand on the element; guards is a later
     layer than components, so it wins outright and a component's own `transition:` is discarded on any
     element that also carries [data-reveal]. Nothing was wrong with either declaration - they were
     written in different blocks, years apart in reading order, and one of them silently deleted the other.
     WHAT IT COST, ENUMERATED, because the set is small and knowable (grep of dist/: no .btn and no
     .link-arrow carries [data-reveal], and .package carries it on no page today, so the collision reached
     exactly three components ON THE DAY THIS WAS WRITTEN - see the [CARD-EDGE v36] paragraph below it,
     which makes four):
       .svc-card       border-color SNAPPED to the accent on hover on all nine cards on the site (ten
                       until 2026-09-07 - /real-estate's fourth card went with decision 51), and the
                       lift ran at the reveal duration instead of --dur-base
       .feature-media  the red ring 6.29 fades in on hover SNAPPED on all three framed figures (two
                       since 2026-09-07) - i.e. half of the firm's own instruction of 2026-09-06 was
                       arriving as a hard cut
       .contact-card   the :focus-within edge added earlier in this same pass would have been dead on
                       arrival on /contact-us, which is how the collision was found
     [CARD-EDGE v36, 2026-09-08] IT REACHES FOUR NOW, AND THE ENUMERATION IS CORRECTED RATHER THAN THE
     REASONING. .act-card gained `transition: border-color var(--dur-base) ease` in 6.10 on the day the
     firm asked its edge to answer the pointer, and every one of the seventeen carries [data-reveal], so
     this rule discards that declaration on every card that ships:
       .act-card       the hover edge added in 6.10a and the :focus-within edge added beside it are BOTH
                       served from the list below rather than from the component - and they are served
                       CORRECTLY, at the --dur-base rung 6.10 asked for, so this one cost NOTHING. It is
                       named anyway, because the sentence above says the set is "small and knowable" and
                       an unlisted member falsifies exactly that. Measured on dist/: a shipped .act-card
                       reports "opacity, transform, border-color, translate" / 1.2s, 1.2s, 0.6s, 0.6s
     THE SET GREW THE MOMENT A MATERIAL PLATE WAS GIVEN A STATE OF ITS OWN, which is the general lesson
     and the reason this paragraph is here rather than a number being edited in place: every component
     that acquires a `transition` from now on joins this list if it also carries [data-reveal], and the
     check is one grep of the built pages.
     THE FIX IS TO CARRY THE CLOBBERED PROPERTIES HERE, PINNED TO THEIR OWN RUNG. border-color and
     translate join the list; the three register rules below now give a duration PER PROPERTY, so the two
     reveal properties take the register's figure and the two component properties take --dur-base
     whatever register the element is in. That is byte-identical to what 6.9, 6.16 and 6.29 asked for, and
     it costs nothing on the ~370 [data-reveal] elements that have no border and never translate: a
     transition on a property that does not change is not work.
     WHAT IS STILL NOT REPAIRED, AND IT IS NAMED RATHER THAN LEFT TO BE FOUND. `transform` can only carry
     ONE duration here and the reveal needs the register's, so any component hover using `transform` on a
     [data-reveal] element still runs at the register's tempo. .svc-card's lift is the only one, and 6.9
     is therefore moved off `transform` and onto `translate` in this pass - the same property split 6.27
     and 6.29 had to make on the framed photographs, for the same reason: one property, one owner.
     THE PROPER REPAIR, for a pass that may touch main.js: the reveal wants to be an ANIMATION rather than
     a transition. An animation does not touch transition-* at all, so no component would ever lose its
     own, and `both` fill would hold the arrival exactly as the pre-state does. It is not taken here
     because main.js's finish() is driven by `transitionend`, and moving the reveal to a keyframe changes
     that event to `animationend` - a JS-contract change, and this pass is CSS's. */
  html.js [data-reveal] {
    --reveal-delay: 0s;
    transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out),
                border-color var(--dur-base) ease, translate var(--dur-base) var(--ease-out);
    transition-delay: var(--reveal-delay);
    will-change: opacity, transform;
  }
  html.js :is(.svc-card, .act-card, .package, .contact-card, .feature-media, .quote-card)[data-reveal] { transition-duration: var(--reveal-plate), var(--reveal-plate), var(--dur-base), var(--dur-base); }
  html.js :is(.hero-title, .display, .quote)[data-reveal] { transition-duration: var(--reveal-display), var(--reveal-display), var(--dur-base), var(--dur-base); }   /* the heaviest travels least and slowest */
  /* [data-reveal] is repeated so this matches the :is() rules' specificity and, being last, wins:
     figure.feature-media carries data-reveal="fade" AND is a PLATE, and the opt-out must govern it. */
  html.js [data-reveal][data-reveal="fade"] { transition-duration: var(--dur-media), var(--dur-media), var(--dur-base), var(--dur-base); }

  /* [MOTION v21] THE PROMOTION IS HANDED BACK UNDER REDUCED MOTION, AND UNTIL TODAY IT WAS NOT. This is
     the largest single defect this pass repairs and it only ever affected the readers who asked for less.
     WHAT SHIPPED. `will-change: opacity, transform` sits on the UNCONDITIONAL selector above, by the
     argument three paragraphs up, and the ONLY thing that ever released it was main.js's finish(), which
     lives BELOW the file's one early return. Under this query main.js takes that return: it adds
     .is-revealed, promotes the containers, and stops - by an explicit decision recorded at its own call
     site ("A class and nothing else. NO INLINE STYLES ON THIS PATH"). So the hint was set on every target
     and withdrawn from none. MEASURED, 5s after load, whole page scrolled, 1440x900: dist/index.html 1
     promoted layer in the normal path against 25 under this query; /attorneys 2 against 27;
     /employment-law 2 against 36; /business 1 against 34. The reduced-motion count is EXACTLY the page's
     [data-reveal] count on every page, will-change reads "opacity, transform", and it never clears.
     [RE-DERIVED 2026-09-07, tools/probe-reveal-census.mjs] TWO OF THOSE FOUR PAGES HAVE MOVED SINCE, so
     the figures are restated rather than left to rot: /business went 34 -> 15 targets in the firm's
     rewrite of 2026-09-06, and /employment-law 36 -> 27 when four subtopic ribbons became four plates
     on 2026-09-07. dist/index.html 25 and /attorneys 27 still measure exactly as written. The
     normal-path column today is index 2, /attorneys 2, /employment-law 1 - it lost a [data-parallax]
     figure with the Cal/OSHA picture - and /business 1.
     [RE-DERIVED AGAIN THE SAME DAY, close-out of the SECOND /employment-law pass] /employment-law IS 28,
     NOT 27, and the same script says so rather than an argument about it. The second pass of 2026-09-07
     deleted the Circumstances ribbon (-4 targets), moved its three-child .section-head onto the card
     band (+3) and added two plates (+2): 27 + 1. index 25, /attorneys 27 and /business 15 re-measure
     EXACTLY as restated above, which is what makes the one page that moved worth naming. THE
     NORMAL-PATH COLUMN IS RESTATED TOO, and it was the misleading half: it reads 4 on all four pages
     today, and tools/probe-promoted-v28.mjs NAMES the layers instead of counting them - the two
     .cursor-glow elements, which any wheel-driven probe arms itself the moment its own events move the
     pointer, plus one translate per framed image, where index's hero <picture> contributes two and the
     other three each carry two [data-parallax] figures since the photographic closing band rolled out
     on 2026-09-07. None of that is the defect this rule repairs - it is the baseline the defect was
     measured against, and the reduced-motion column is still the one that matters.
     THE IDENTITY ABOVE IS THE DURABLE HALF and is
     the thing to re-check: the reduced-motion count under the defect is the page's [data-reveal] count,
     whatever that count has become. AND THE REPAIR IS RE-MEASURED RATHER THAN ASSUMED: with the rule
     below shipping, the same probe reads 0 promoted layers under this query on every page but the home
     one, which keeps 1 for its hero clip.
     IT IS THE SAME COST BLOCK 5 ALREADY NAMES AND FIXES FOR THREE IMAGES. The [data-parallax] guard there
     spends a paragraph on "one permanent compositor layer per framed image, on a desktop reader who has
     asked for less, for an element nothing will ever move... the same cost the reveal system's finish()
     is careful to hand back". That argument was applied to 3 elements and not to the 36 beside them
     (2 and 28 today).
     WHY IT LIVES HERE AND NOT IN BLOCK 5 WITH ITS SIBLINGS, which is the one thing about this rule that
     is not obvious: block 5 is EARLIER IN THIS SAME LAYER, and the declaration it has to beat is (0,2,1)
     - identical specificity - so a copy of this rule up there would lose on source order and read as
     working. The [data-parallax] guard wins from up there because the promotion it beats is in @layer
     components. Layer, specificity, then position: two of the three tie here, so position decides, and
     the guard has to sit below the thing it releases.
     A MEDIA QUERY AND NOT A .js CHECK, so it also covers the case main.js cannot: a reader whose script
     never arrives keeps no hint either, because with no .js the selector above never matches at all.
     main.js releases it inline on BOTH early-return paths as well - this rule cannot see the second one
     (no IntersectionObserver, reduced motion off), and that path leaves exactly the same layers behind. */
  @media (prefers-reduced-motion: reduce) {
    html.js [data-reveal] { will-change: auto; }
  }

  /* 7.5 THE POINTER LIGHT'S OTHER SUBTRACTION. main.js never builds the lobes on a page whose pointer
     has proved itself touch, so this is the second half of a belt-and-braces pair rather than the only
     defence - and it earns its place because the pointer kind is a RUNTIME TRI-STATE that can change
     mid-session (a mouse unplugged, a touchscreen laptop's first tap on a page that has been driven by
     a trackpad for ten minutes). The class flips on that event without rebuilding the DOM, and this
     rule is what makes the flip complete. It is also the honest answer for touch on its own terms:
     there is no pointer to follow, so there is nothing for the light to say.
     :where() so the gate stays free, and the POSITIVE class here - not :not() - because this is a
     subtraction on proof, exactly like 7.2. The failure mode is a mouse user keeping the light, never
     a touch user getting a light nailed to the last place they tapped. */
  :where(html.has-touch) .cursor-glow { display: none; }
}
