/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE CORRECTIONS — phone widths only, migrated build only
   ═══════════════════════════════════════════════════════════════════════════

   WHY THIS FILE EXISTS SEPARATELY FROM assets/site.css

   assets/site.css is the SHIPPED legacy stylesheet. It carries all 54 of this
   site's media queries, and every mobile rule below would naturally belong in
   it. Two independent things make editing it unacceptable:

     1. verify/baseline/legacy-root/ is a LIVE SYNC of the working tree, not a
        frozen copy — verify/snapshot.mjs re-syncs it before every capture. An
        edit to assets/site.css therefore changes how the LEGACY side renders as
        well as the migrated side. Both halves of every legacy-versus-migrated
        diff would move together, the comparison would go blind at exactly the
        moment it mattered, and the 133 committed screenshot digests would stop
        reproducing from the legacy tree. That is the project's only immutable
        rendering evidence, and it would be destroyed silently.

     2. The `legacy intact` gate runs `git diff --exit-code -- assets/ …`. It is
        what the Deletion Guardrail rests on until the very last plan of the
        phase. Loosening it to admit an edit, at the exact moment it matters
        most, is not a trade worth making.

   So mobile rules live here, in a NEW file that the legacy HTML never
   references. The legacy tree renders exactly as it did at capture — proven
   independently by `npm run baseline:restore-shots -- --force` still restoring
   133 screenshots with every digest identical.

   This file lives under assets/ and NOT under src/. Anything under src/ goes
   through the bundler, which fingerprints filenames and can rewrite contents;
   tools/sync-public.mjs carries assets/ into public/ verbatim, which is the
   behaviour required. (tools/sync-public.mjs's header states the same rule.)

   THE HARD RULE

   EVERY rule in this file lives inside an @media block bounded at
   max-width: 767.98px or narrower. No top-level rule outside a media query, no
   min-width above zero. This is asserted by PARSING the file, not by reading
   it — see verify/mobile-overflow.mjs. It is the second of three independent
   guarantees that nothing at or above 768px moves; the first is the
   media="(max-width: 767.98px)" attribute on the <link> itself, and the third
   is the `pixel parity (>=768px)` gate still reporting 76 comparisons at 0
   differing pixels after every correction here.

   767.98px rather than 767px: the site's own breakpoints are integer min-width
   queries, and a fractional upper bound closes the gap a fractional viewport
   width (a zoomed browser, a device pixel ratio that lands off-integer) would
   otherwise fall into, where neither the mobile rules nor the desktop ones
   would apply.

   WHY CSS AND NEVER MARKUP

   Every correction here is expressed in CSS. A markup change inside any page's
   main region is simultaneously an edit to the shipped HTML file: tools/lint.mjs
   group 8 re-derives all 18 spliced regions from routes.json's mainBodyLines and
   requires byte equality. A correction that genuinely cannot be expressed in CSS
   is recorded as a finding, not pushed into shipped markup.

   WHAT MAY NOT BE DONE HERE

   No new colour, typeface, card, decorative element or copy. MOB-08 requires
   mobile content, brand colours, typography and information hierarchy to be
   UNCHANGED, and verify/mobile-invariants.mjs asserts it as set membership
   rather than as a judgement. Design values come from the custom properties
   already declared in assets/site.css — never a hardcoded hex, never a
   hardcoded spacing scale.

   Every rule group below names the requirement it serves, the routes it
   affects, and the measured value from 02-05-EVIDENCE.md or 02-06-EVIDENCE.md
   that it corrects.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── MOB-02 / MOB-04 · vertical rhythm · routes: all 19 ──────────────────────
   MEASURED at 390: every `.band` section renders 88px of padding above and 88px
   below its first and last painted content — on /about/ that is 178px of blank
   space inside a 368px section, so 48% of the band is padding. `.page-top`
   renders 135px above the breadcrumb on all 12 inner routes.

   The cause is a clamp FLOOR that never yields, which is why this closes MOB-04
   as well as MOB-02. `assets/site.css:96` sets

       .band { padding-block: clamp(88px, 12vw, 168px); }

   and at 390px the preferred term is 12vw = 46.8px — well BELOW the 88px floor.
   So the clamp stops scaling exactly where a phone needs it to, and 88px stops
   being responsive spacing and becomes a fixed, desktop-derived height. Same
   shape at `assets/site.css:597`, where `clamp(112px, 15vh, 178px)` resolves to
   15vh = 135px on a 900px-tall viewport.

   The correction lowers the FLOOR and leaves the preferred term and the shape
   alone, so spacing still scales with the viewport and still reaches the same
   values the moment the query stops applying:

       .band       88px floor -> 44px   =>  46.8px at 390 (was 88)
       .page-top  112px floor -> 84px   =>  99px at 390   (was 135)

   Nothing is set to a constant: both remain clamps. The nav capsule's bottom
   edge measures 68px at phone widths, so 99px of `.page-top` padding still
   clears the fixed nav by 31px. */
@media (max-width: 767.98px) {
  .band {
    /* floor halved, 88 -> 44; preferred term 12vw and ceiling 168px untouched */
    padding-block: clamp(44px, 12vw, 88px);
  }
  .page-top {
    /* floor 112 -> 84; 15vh resolves to 99px at a 900px viewport, clearing the
       68px nav capsule by 31px. The bottom padding is deliberately untouched —
       it measured 86px, which is proportionate to what follows it. */
    padding-top: clamp(84px, 11vh, 112px);
  }
}

/* ── MOB-03 / MOB-05 · text clipped by a grid that does not stack ────────────
   route: /ecosystem/ · MEASURED: 7 self-clipping findings, and the only ones on
   the whole site.

       ecosystem@390  span.pstat__k  scrollWidth 88 vs clientWidth 84  (x3)
       ecosystem@375  span.pstat__k  scrollWidth 88 vs clientWidth 80  (x3)
       ecosystem@375  span.pstat__k  scrollWidth 81 vs clientWidth 80  (x1)

   The clipped strings are "Participants", "Contributors", "Contribution edges"
   and "Delegations issued" — single unbreakable words in a 10px mono face with
   .13em letter-spacing. `assets/site.css:535` holds `.pstat` at
   `repeat(3, minmax(0, 1fr))` at every width, so at 390 each column is 84.4px
   and at 375 it is 80px, against labels that need 88px.

   Every other grid on this site already stacks to one column below 768 — .projs,
   .pcards, .caps, .tenets, .stats and .roles were all measured single-column at
   390. `.pstat` is the one that does not, which is why this is a stacking
   correction (MOB-05) and the clipping (MOB-03) is its symptom.

   Two columns rather than one: at 390 that gives (281 - 14) / 2 = 133.5px per
   column, comfortably over the 88px the longest label needs, and it keeps the
   figures reading as a related set rather than as three separate rows. Going to
   one column would have been the larger change to information hierarchy, which
   MOB-08 forbids. `minmax(0, 1fr)` is preserved so a long label still wraps
   rather than forcing the track wider. */
@media (max-width: 767.98px) {
  .pstat {
    /* 3 -> 2 tracks. 281px content width - 14px gap = 267 / 2 = 133.5px each,
       against a longest measured label of 88px. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ── MOB-06 · touch-target sizing · route: / ─────────────────────────────────
   MEASURED: a#skip-graphity renders at 149.9 x 42 CSS px at 430, 390 and 375.
   Its height comes from `min-height: 42px` (assets/site.css, the SKIP GRAPHITY
   block) — 2px under the 44 x 44 minimum this phase holds every touch target to.

   The control is the only way past ~7500px of pinned Graphity prologue for a
   visitor who does not want it, it is `position: fixed` so it is on screen for
   that entire distance, and on a phone it is hit with a thumb. Two pixels is the
   whole defect and two pixels is the whole fix: 42 -> 44 is +2px of padding-box
   height, the width is already 149.9, and nothing else on the page moves because
   the element is fixed and out of flow.

   Width is untouched: 149.9 already clears 44. Colour, radius, backdrop filter,
   type size and copy are all untouched — this changes one length. */
@media (max-width: 767.98px) {
  .skip-graphity {
    /* 44 - 42 = 2px taller. WCAG 2.5.8 Target Size (Minimum) floor. */
    min-height: 44px;
  }
}

/* ── MOB-06 · touch-target sizing · routes: all 19 ───────────────────────────
   MEASURED across 57 route-width probes: 1305 undersized interactive elements
   in seven distinct groups. Every one is below the 44 x 44 CSS px minimum this
   phase holds touch targets to (WCAG 2.5.8 Target Size (Minimum)).

     684  .ftr__cols li a            widths 45-159, HEIGHT 17
     228  .ftr__social li a          17 x 17
     171  .ftr__rule nav a           widths 81-88,  HEIGHT 21
      57  .capsule .mobile-menu      50.58 x 40
      57  .capsule .brand            80.28 x 34
      60  .crumb a                   40.83 x 17.81
      48  .proj__actions .lnk        widths 79-236, HEIGHT 23.5-25.1

   Links genuinely set in a sentence are NOT in this list and are not touched:
   the gate applies WCAG's own "Inline" exception, measured as non-target text
   rendering on the same line box, which exempts prose links and the contact
   page's "or write to …" hint. 75 elements are exempt on that basis. Enlarging
   those would break the paragraphs they sit in, which is precisely why the
   standard excludes them.

   TECHNIQUE, AND WHY NOT A HIT-AREA PSEUDO-ELEMENT

   Each fix enlarges the element's own rendered box, via min-height / min-width
   plus inline-flex centring where the element was inline. Extending the hit area
   with an absolutely positioned `::after` overlay was considered and rejected:
   it satisfies a pointer but leaves `getBoundingClientRect()` reporting the
   original 17px box, so the element would still be too small for anything that
   measures it — including a screen magnifier, including this gate — while
   looking fixed. A target that is 44px only to code that does not check is not
   a corrected target.

   Type size, colour, weight, letter-spacing, decoration and copy are untouched
   throughout; only box geometry changes. `align-items: center` keeps each label
   optically where it already sat inside its new, taller box. */
@media (max-width: 767.98px) {
  /* Nav capsule. The brand lock-up and the menu button are the two controls a
     phone visitor reaches for first, and both sit inside a fixed capsule, so
     growing them costs no page height at all. 34 -> 44 and 40 -> 44. */
  .capsule .brand,
  .capsule .mobile-menu {
    min-height: 44px;
    min-width: 44px;
  }

  /* Breadcrumb. `.crumb` is a flex row, so its links are flex items rather than
     text in a sentence — the inline exception does not apply and the gate is
     right not to grant it. 40.83 x 17.81 -> at least 44 x 44. Centring keeps the
     label on the same optical baseline as the "/" separator beside it. */
  .crumb a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    /* `.crumb` is a flex row, so its items shrink below their content width by
       default. As a plain block that only reflowed the text; as an inline-flex
       box it CLIPS it — measured immediately after the change above, at
       /insights/quorum-selection-changes/ 390 and 375, where "Insights" needed
       64px in a 63px box. `flex: none` opts this item out of shrinking, which is
       what keeps a wider touch target from becoming a narrower label. */
    flex: none;
  }

  /* Footer link columns. Each anchor is alone inside its <li>, so there is no
     surrounding text constraining its height — it was simply 17px tall because
     that is the line box. Widths already clear 44 (the narrowest measured 45.17,
     "GitHub"), so only the height needs a floor. */
  .ftr__cols li a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* Footer brand lock-up, 81.28 x 34.02. It is display:inline-flex and shares a
     block with the footer tagline, which is why the FIRST version of the gate's
     inline exception wrongly excused it; tightening that exception to WCAG's
     actual condition — non-target text on the same LINE — surfaced it. It sits
     on its own line, so it gets no exception and needs the same 44px floor as
     every other standalone target. */
  .ftr__brand {
    min-height: 44px;
  }

  /* Footer social icons — the worst offenders on the site at 17 x 17, and the
     only targets failing BOTH dimensions. The 16px SVG inside is left at its
     own size; the anchor grows around it and centres it, so the mark does not
     change size, weight or colour. */
  .ftr__social li a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
  }

  /* Footer legal row. `.ftr__rule nav` is a flex container, so these are flex
     items and not inline text; 21px -> 44px. */
  .ftr__rule nav a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* Project card actions. `.proj__actions` holds two anchors separated only by
     whitespace — no sentence, so no inline exception. Widths already clear 44
     (narrowest measured 78.9); heights of 23.5 and 25.1 do not. */
  .proj__actions .lnk {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}
