/*! trentpower.fr · authored source */
/*
  trentpower.fr
  screen design system

  Role:
  defines the public editorial interface, trust surfaces and
  responsive layout.

  Constraints:
  - no external assets
  - no analytics, cookies, trackers or third-party embeds
  - active css, js and font filenames revalidate; frozen release
    archives carry immutable dated assets
  - oxblood is an accent, not decoration
*/

/*!
  trentpower.fr css architecture
  cascade order: reset → tokens → base → layout → components → pages → utilities → overrides
  · tokens define decisions (colours, type, spacing, motion)
  · base styles target elements; reset normalises
  · layout positions; components are reusable; pages scope by body[data-page]
  · utilities are minimal helpers; overrides are rare and documented
  @font-face declarations sit in @layer fonts (added implicitly at the
  end of the cascade order — font-face lookups ignore layers anyway).
*/

@layer reset, tokens, base, layout, components, pages, utilities, overrides;

@layer fonts {
/* klim type foundry fonts used under purchased web licences for trentpower.fr.
   display strategy:
   - styles.css declares only the three critical subset @font-face
     entries below. the full editorial weights live in /fonts-full.css
     and are loaded after lcp by /app-enhance.js (which also adds
     `.fonts-loaded` to <html> so the variables in this file resolve
     to the full families on the next style recalculation).
   - until /fonts-full.css arrives, every site-wide
     `font-family: var(--serif|--sans|--mono)` resolves to the critical
     subset, then to a system fallback. this guarantees no full woff2
     enters the lcp critical chain.
   - hero (signifier critical) uses `font-display: swap` because it
     is the brand-critical hero face. söhne critical (nav) keeps
     `optional` because the system sans fallback is close enough at
     the small nav size for the optional contract to be invisible.
     phase 73 · söhne mono critical promoted from `optional` to
     `swap` so the .page-kicker eyelid on internal pages (privacy /
     security / integrity / verify / source-reader) — which is the
     only mono surface visible above the fold and is not preloaded
     on those pages — renders consistently in söhne mono rather
     than getting locked to the system mono fallback for the page
     lifetime when the subset doesn't deliver inside the 100 ms
     optional window. accepts a tiny first-visit fout on internal
     pages; cache-warm reloads see no swap. */
@font-face { font-family: 'Signifier Critical';   font-style: normal; font-weight: 300; font-display: optional; src: url('/fonts/subsets/signifier-light-hero.woff2') format('woff2'); }
/* italic hero face — used by the hero <mark> highlight. preloaded in
   home.html so it arrives with the first paint; font-display: optional
   prevents a fallback→real swap (and the line-width reflow on "growth
   systems" / "systèmes de croissance" the swap caused). */
@font-face { font-family: 'Signifier Critical';   font-style: italic; font-weight: 300; font-display: optional; src: url('/fonts/signifier-light-italic.woff2') format('woff2'); }
@font-face { font-family: 'Söhne Critical';       font-style: normal; font-weight: 500; font-display: optional; src: url('/fonts/subsets/soehne-kraftig-nav.woff2') format('woff2'); }
@font-face { font-family: 'Söhne Mono Critical';  font-style: normal; font-weight: 400; font-display: optional; src: url('/fonts/subsets/soehne-mono-buch-labels.woff2') format('woff2'); }

}
@layer tokens {
:root {
  color-scheme: light;
  /* Two-surface model with explicit positive scoping.
     the public profile sits on warm editorial paper. the trust
     system, archive and source surfaces sit on a slightly cooler
     warm grey — the records room of the same artefact. overlays
     use a grey scrim above either surface; the panel itself
     returns to paper.

     every public page declares its surface explicitly via
     <body data-surface="editorial"> or <body data-surface="record">.
     the :root default below is ivory so a page that forgets the
     attribute falls through to the editorial surface, never to
     the record surface — grey is never the global default. */
  /* three paper tones — main paper for the public face, record
     paper for the archive, raised paper for inspected objects.
     Raised-high is reserved for overlay panels that sit above the
     scrim regardless of the underlying page surface. */
  --paper-main:        #FAF7F0;   /* front of house · editorial paper */
  --paper-record:      #E9E5DC;   /* back of house · warm archival grey */
  --paper-raised-high: #FBF8F1;   /* overlay panels (.modal) · highest lift on the record surface */
  --paper-project:     #FFFDF8;   /* homepage editorial insert (what's on in paris card) — warmer than raised-high so it visibly lifts off front-of-house ivory without going stark white */
  /* phase 52 · single canonical archival surface so the verify and
     integrity record cards (plus the command block beneath them) read
     as one verification-system family of paper sheets. */
  --surface-archival:  #F4F1EA;

  --surface-page: var(--paper-main);
  --surface-card: var(--surface-archival);

  /* phase 84 · scrim quietened to a warm sepia at ~34% alpha so the
     modal reads as an archival card placed gently over paper rather
     than a system dialog. previous value rgba(28,25,22,0.46) was
     reading as a darkened dim layer on mobile safari. */
  --overlay-scrim: rgba(34, 27, 20, 0.34);
  /* one shared blur for every dimming layer — the project / verify
     modal (.modal-overlay) and the language gate (.gate-layer) read
     identically. */
  --overlay-scrim-blur: 3px;

  /* rules / hairlines tied to ink density rather than a hard
     opaque grey, so they read consistently on every paper tone.
     phase 39 · softened from 0.14 to ~10% mix for editorial calm.
     phase 45 · further reduced to 8% — dividers now read as
     deliberate registration marks rather than ui wireframe.
     phase 60 · softened a further ~10% (8% → 7%) so the hairlines
     stop competing with typography on dense surfaces (/source/,
     /verify, /integrity). */
  --rule:        color-mix(in srgb, var(--ink) 7%, transparent);

  /* ink and accent — semantic names for the new system.
     --ink-muted is referenced by tools/validate_css_architecture.py
     for wcag aa contrast verification; keep defined even if no rule
     uses it directly. */
  --ink:        #211F1C;
  --ink-muted:  #67625B;
  --accent:     #6E1A14;

  /* legacy aliases — older rules still reference these names; new
     code should prefer the semantic --paper-* / --ink-* / --rule
     tokens above. */
  --bg:         var(--surface-page);
  --bg2:        var(--paper-record);
  --card:       var(--paper-raised-high);

  --fg:  var(--ink);
  /* phase 40 · slightly warmer body register (#5f5a53). same warm
     hue as phase 39 (#5e5952), 1 unit lighter — preserves the
     editorial warmth while keeping aaa-equivalent serif legibility
     on mobile. dark mode --fg2 unchanged (handled in dark block). */
  /* secondary + tertiary text tokens darkened (phase 93 · WCAG AA
     sweep). previous values #5f5a53 / #706b66 sat at 5.7:1 / 4.8:1
     on --paper-main — the latter was borderline and lighthouse
     flagged mono micro-cap labels using --fg3. new values #575149
     / #696158 give ~6.3:1 / ~5.3:1 with editorial warmth preserved. */
  --fg2: #575149;
  --fg3: #696158;
  --ac:  var(--accent);
  --ac2: #8B2218;     /* warmer oxblood · hover only · legacy alias */

  /* accent tonal hierarchy · five derived tokens so every red on
     the site reads from the same editorial reservoir.

       --accent        · decorative reservoir (rules, focus fills,
                         button backgrounds, ::after underline tints).
                         the deepest, most concentrated tone.
       --accent-text   · text-foreground accent (link colour, code
                         strings, label kickers, project cta labels).
                         aliases --accent in light mode where oxblood
                         reads aaa on cream; dark mode opens a
                         separate, warm-restrained value so aa text
                         contrast holds on dark paper. never coral.
       --accent-hover  · hover state. deeper / warmer / slightly
                         denser than the resting value, never neon.
                         light: a darker oxblood; dark: a denser red
                         at similar luminance to --accent-text.
       --accent-soft   · decorative dots, timeline markers, quiet
                         metadata flecks. derived via color-mix on
                         the page record paper so the tone reads as
                         embedded in the surface, not stamped on it.
       --accent-border · hairline borders, focus rules, fingerprint
                         underlines. derived via color-mix on
                         transparent so it composites against any
                         surface tone. */
  --accent-text:   var(--accent);
  --accent-hover:  #5C1813;   /* deeper oxblood — darker than --accent, never brighter */
  --accent-soft:   color-mix(in srgb, var(--accent) 60%, var(--paper-record));
  --accent-border: color-mix(in srgb, var(--accent) 28%, transparent);
  /* phase 39 · --bd moved from solid #d8d4cc to a 10% ink mix so
     metadata-row dividers, record-grid hairlines, table edges and
     security-section borders all read as quieter editorial pacing
     rather than wireframe scaffolding. --bd-soft kept at the
     solid lighter tint for surfaces that need a deliberate plate. */
  --bd:        color-mix(in srgb, var(--ink) 10%, transparent);
  --bd-soft:   #E6E1D8;

  /* divider tonal hierarchy · three semantic rules so every joint
     in the publication reads as part of the same paper system.

       --rule-faint   · the most subtle. inter-row separators inside
                        compact archival tables — must read as
                        rhythm, not as a divider.
       --rule-soft    · the quietest visible rule, used for
                        in-content section separators and editorial
                        pacing.
       --rule-default · the standard footer / page-end rule. one
                        step stronger so the footer never appears
                        to float. dark surfaces and record paper
                        each open a slightly stronger override
                        below.
       --rule-strong  · archival card joins and structural seams
                        that need to read as a deliberate divider.

     all four derive from --ink so the warm-paper hue carries
     through; nothing is a cold neutral grey. */
  --rule-faint:   color-mix(in srgb, var(--ink) 3%, transparent);
  --rule-soft:    color-mix(in srgb, var(--ink) 7%, transparent);
  --rule-default: color-mix(in srgb, var(--ink) 11%, transparent);
  --rule-strong:  color-mix(in srgb, var(--ink) 16%, transparent);
  /* editorial focus-visible tokens. 45 % alpha oxblood reads as a
     calm typographic accent rather than a browser-debug rectangle.
     2 px ring + 3 px offset gives obvious keyboard visibility
     without dominating the page. component rules consume these via
     var(--focus-colour) etc.; dark-mode overrides land below. */
  --focus-colour:     rgba(110, 26, 20, 0.45);
  --focus-ring-width: 2px;
  --focus-offset:     3px;
  --focus-radius:     0;
  /* phase 46 · semantic radius tokens.
     three categories — cards/archival objects, larger overlay panels,
     and true pill objects (the trust-mark certification strip). every
     literal radius on a card or overlay surface now flows through
     these tokens so the curvature stays intentional and easy to
     refine in one place. layout surfaces (body, main, footer, nav,
     buttons, rules) remain sharp — radius identifies objects, not
     layout. */
  --radius-soft:      7px;
  --radius-panel:     10px;
  --radius-pill:      999px;
  /* code-token colours · used by .code-str / .code-var inside the
     trust-command and verify-command panels, and by the .tok-string /
     .tok-number token classes inside /source/view/. tokenised so dark
     mode and prefers-contrast can override them without touching the
     component rules.
     strings (urls, paths, attribute values) get lapis blue; numbers
     (literals, integers, decimals) get gall purple. both pulled back
     from srgb-max chroma to read as deep ink pigments on warm cream
     paper, not as digital ui accents.
     --code-var is preserved as an alias of --code-number so the
     legacy /verify/ trust-code .code-var class continues to resolve. */
  --code-string: #2E5BBF;
  --code-number: #6B47B0;
  --code-var:    #6B47B0;
  /* Critical-only stacks. the full editorial family names ('signifier',
     'söhne', 'söhne mono') are intentionally absent until /fonts-full.css
     loads after lcp and adds `.fonts-loaded` to <html>, at which point
     the override at the bottom of this file flips these stacks to put
     the full weights first. keeps every full woff2 out of the lcp
     critical request chain. */
  --serif: 'Signifier Critical', Georgia, 'Times New Roman', serif;
  --sans:  'Söhne Critical', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --mono:  'Söhne Mono Critical', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;

  /* motion · ui transitions sit in the 120–180ms quiet band so they
     register but never call attention. used by source-reader hovers,
     code-line range transitions, etc. */
  --transition-quiet: 180ms ease;

  /* spacing · small scale used by absolutely-positioned source reader
     markers (sp-3) and edge insets (sp-4). expanded inline elsewhere
     to avoid an over-eager spacing system; tracked here so future
     audits can find them. */
  --sp-3: 0.75rem;
  --sp-4: 1rem;

  /* ─── modal system tokens ─── one shell shared by the language gate,
     verify menu and access. forbidden: glassmorphism, backdrop-filter,
     slide-in, bounce. the scrim is paper-tinted; the publication itself
     blurs via filter:blur() on .site/#main when body.modal-open is set. */
  --modal-paper:   var(--paper-raised-high);
  --modal-radius:  10px;
  --modal-edge:    clamp(20px, 4vw, 32px);
  --modal-pad:     clamp(28px, 4.4vw, 38px);
  --modal-rule:    1px solid var(--rule);
  --modal-shadow:  0 30px 80px rgb(0 0 0 / .10), 0 8px 20px rgb(0 0 0 / .05);
  --modal-scrim:   38%;
  --modal-blur:    8px;
  --modal-motion:  320ms cubic-bezier(.2, .7, .2, 1);
}

/* modal token bumps on narrow viewports — slightly softer radius,
   tighter viewport edge. matches the brief's mobile ≤520px contract. */
@media (max-width: 520px) {
  :root {
    --modal-radius: 12px;
    --modal-edge:   20px;
  }
}

/* explicit positive surface scoping.
   - data-surface="editorial" → /, /privacy/
   - data-surface="record"    → /verify/, /source/, /integrity/,
                                /integrity/releases/, /security/,
                                /security/acknowledgments/, error pages
   - data-surface="archive"   → kept as a synonym of "record" so any
                                cached html still resolves correctly. */
body[data-surface="editorial"] {
  --surface-page: var(--paper-main);
  --surface-card: var(--surface-archival);
  --bg:           var(--paper-main);
}
body[data-surface="record"] {
  --surface-page: var(--paper-record);
  --surface-card: var(--surface-archival);
  --bg:           var(--paper-record);
  /* record paper is one stop warmer / darker than editorial paper,
     so the default footer rule compresses against it. open the
     joint a touch (11% → 14%) so the seam stays legible. */
  --rule-default: color-mix(in srgb, var(--ink) 14%, transparent);
}
body[data-surface="archive"] {
  --surface-page: var(--paper-record);
  --surface-card: var(--surface-archival);
  --bg:           var(--paper-record);
  --rule-default: color-mix(in srgb, var(--ink) 14%, transparent);
}

/* Back-of-house pages calm the supporting-text and rule tokens by a
   small amount. primary tokens (--fg / --ink / --link) stay
   unchanged so headings, filenames, hashes, fingerprints and
   action links remain authoritative. the override fires on every
   page that wears the brand-only masthead — that includes privacy,
   verify, integrity, source, releases, security and the error
   pages. the homepage carries no data-masthead attribute and so
   keeps the original (slightly stronger) muted tones.

   Contrast: --fg2 #6c6760 vs --paper-record #e9e5dc ≈ 4.6:1; vs
   --paper-main #faf7f0 ≈ 5.4:1. --fg3 #756f69 vs the same
   backgrounds ≈ 4.0:1 and 4.7:1. both safely above the 3:1 large-
   text bar; --fg3 sits at the aa edge for very small mono labels
   on the warm-grey surface. */
body[data-masthead="brand-only"] {
  /* phase 93 · removed the lighter --fg2 / --fg3 override.
     previously brand-only pages used #6c6760 / #756f69 — the latter
     was 4.0:1 on paper, below WCAG AA. now brand-only pages
     inherit the default darker tokens. the brand-only quiet feel
     is still carried by the --rule opacity reduction below (8% vs
     the default 7%), which is where the editorial softness
     actually lives. */
  /* phase 39 · brand-only masthead pages get an even quieter rule
     register (8% mix) so the brand-only chrome stays nearly
     subliminal against editorial type. */
  --rule: color-mix(in srgb, var(--ink) 8%, transparent);
}

}

/* ─── Accessibility-preference overlays ─────────────────────────
   a third @layer tokens block holds three media-query branches that
   redefine the canonical token set under user preferences. the
   surface variants (body[data-surface=…], body[data-masthead=…])
   reference these tokens via var(); they automatically inherit the
   redefined values without their own overrides.

   - prefers-color-scheme: dark    — warm editorial dark, aaa contrast
   - prefers-contrast: more        — pure black/white, thicker rules
   - dark + contrast (intersect)   — pure white on pure black
   - forced-colors: active         — windows high contrast mode
*/
@layer tokens {

  /* dark-mode tokens — applied when the system prefers dark.
     the new footer toggle writes data-theme="light"|"dark" onto
     <html> (or removes it for "system"). the explicit overrides
     inside this media block re-apply the opposite palette when the
     user has pinned a theme:
       · no data-theme attr → system dark wins via the :root rule
       · data-theme="light" → :root[data-theme="light"] re-applies
         the canonical light palette (higher specificity, later in
         document order) so the page tracks the explicit choice
       · data-theme="dark"  → handled by the explicit block below
         this media query so it wins regardless of system preference
     keep the dark token list, the explicit light re-apply block,
     and the explicit dark block in sync. */
  @media (prefers-color-scheme: dark) {
    :root {
      color-scheme: dark;

      /* warm editorial paper, not pure black. the hue (~33° / 9% / 9%)
         mirrors the light paper-main inverted with the warm cast preserved. */
      --paper-main:        #1B1916;
      --paper-record:      #22201B;
      --paper-raised:      #26231F;
      --paper-raised-high: #2C2925;
      --paper-project:     #1F1D1A;
      /* phase 52 · dark-mode canonical archival surface mirrors light. */
      --surface-archival:  #26231F;

      /* phase 84 · dark scrim eased off pure black to a deep warm
         sepia at ~42% alpha. pure black was dragging the page into
         mechanical contrast on oled; warm-ink ambient now survives. */
      --overlay-scrim: rgba(20, 16, 12, 0.42);

      /* hairlines flip to ink-on-paper opacities expressed in the
         dark ink hue, so they read consistently on every paper tone.
         phase 39 · softened to 12% (vs 10% in light).
         phase 45 · further reduced to 10% — dark dividers now match
         the same registrational register as the light :root above.
         phase 60 · softened a further ~10% (10% → 9%) in step with
         the light variant; dense surfaces breathe a touch more. */
      --rule:        color-mix(in srgb, var(--ink) 9%, transparent);

      /* cream ink — warm, editorial, aaa against #1b1916. */
      --ink:        #F0EAE0;     /* ~14.6:1 vs --paper-main */
      --ink-muted:  #C9C2B7;     /* ~9.6:1  vs --paper-main */

      /* dark-mode accent system · deeper, warmer, less coral.
         every red on the dark publication reads from this reservoir.

         --accent        · the decorative seed, iron-rich oxblood.
                           used for focus-ring fills, button backings,
                           divider tints, ::after underline tints.
                           ~2.0:1 on dark paper — never reads as text,
                           always reads as ink mark on paper.
         --accent-text   · aa-text foreground (~4.59:1). hue shifted
                           away from coral / salmon toward a warm
                           restrained red. less orange than the prior
                           #d86459; reads as aged editorial print red
                           rather than ai-product red.
         --accent-hover  · the hover state — denser, slightly more
                           chromatic at similar luminance. "deeper"
                           in the saturation sense, never neon glow.
         --accent-soft   · decorative dots / timeline markers /
                           metadata flecks. mixed with record paper
                           so the tone reads embedded in the surface.
         --accent-border · hairline borders. mixed with transparent
                           so it composites against any surface. */
      --accent:        #7A241C;   /* iron oxblood · decorative only */
      --accent-text:   #D06058;   /* warm restrained red · aa text 4.55:1 */
      --accent-hover:  #D9685A;   /* denser hover · same luminance, more chroma */
      --accent-soft:   color-mix(in srgb, var(--accent) 75%, var(--paper-record));
      --accent-border: color-mix(in srgb, var(--accent) 40%, transparent);
      --ac2:           #D9685A;   /* legacy alias · tracks --accent-hover */

      --fg2:        #B8B0A4;     /* body copy on dark paper, ~8.6:1 (aaa) */
      --fg3:        #A39C92;     /* tertiary mono labels */
      /* phase 39 · --bd moved from solid #3a352f to a 12% ink mix
         (paired with --rule above). --bd-soft retained as a solid
         tone for surfaces that need a plate. */
      --bd:         color-mix(in srgb, var(--ink) 12%, transparent);
      --bd-soft:    #332F2A;

      /* focus-visible token: anchored on the new --accent-text hue
         (#d06058) so the ring composites to ≥3:1 against dark paper
         (wcag 1.4.11 non-text contrast). the deep oxblood #7a241c is
         too dark for focus rings on dark paper. */
      --focus-colour: rgba(208, 96, 88, 0.85);

      /* dark-mode divider hierarchy · warm graphite, not cold grey.
         derived from --ink (#f0eae0) so the warmth of the cream
         carries into every joint. opens slightly above the light
         values so dark surfaces still anchor the footer cleanly. */
      --rule-faint:   color-mix(in srgb, var(--ink) 5%, transparent);
      --rule-soft:    color-mix(in srgb, var(--ink) 9%, transparent);
      --rule-default: color-mix(in srgb, var(--ink) 13%, transparent);
      --rule-strong:  color-mix(in srgb, var(--ink) 20%, transparent);

      /* code-token colours — sage and lavender, the existing brand
         values; already tuned for warm coal paper. */
      --code-string: #99D5A1;    /* sage, ~7.5:1 */
      --code-number: #C8AEEA;    /* lavender, ~8.9:1 */
      --code-var:    #C8AEEA;
    }

    /* selection contrast on dark paper: paper-on-deep-crimson (the
       light-mode `color: var(--bg)` default) drops to ~2.4:1 in
       dark mode. override to cream-on-deep-crimson (~6.2:1, aa). */
    ::selection {
      color: var(--ink);
    }

    /* brand-only masthead overrides on dark paper: calm fg2/fg3
       slightly more so they sit gently above body text. */
    body[data-masthead="brand-only"] {
      --fg2:  #B8B0A4;
      --fg3:  #968F86;
      /* phase 39 · brand-only masthead on dark paper softened to
         10% ink mix (matches the brand-only register on light). */
      --rule: color-mix(in srgb, var(--ink) 10%, transparent);
    }

    /* explicit data-theme="light" override — when the user has
       pinned light from the footer switch, re-apply the canonical
       light palette inside the dark media query. specificity 0,1,1
       defeats the bare :root rule (0,0,1); document order also
       favours this rule. keep in sync with the light :root block
       at the top of the tokens layer. */
    :root[data-theme="light"] {
      color-scheme: light;

      --paper-main:        #FAF7F0;
      --paper-record:      #E9E5DC;
      --paper-raised-high: #FBF8F1;
      --paper-project:     #FFFDF8;
      --surface-archival:  #F4F1EA;

      /* phase 84 · lockstep with the base light-mode scrim. */
      --overlay-scrim: rgba(34, 27, 20, 0.34);

      --rule:        color-mix(in srgb, var(--ink) 7%, transparent);

      --ink:        #211F1C;
      --ink-muted:  #67625B;

      --accent:        #6E1A14;
      --accent-text:   var(--accent);
      --accent-hover:  #5C1813;
      --accent-soft:   color-mix(in srgb, var(--accent) 60%, var(--paper-record));
      --accent-border: color-mix(in srgb, var(--accent) 28%, transparent);
      --ac2:           #8B2218;

      --fg2:        #575149;
      --fg3:        #696158;

      --bd:         color-mix(in srgb, var(--ink) 10%, transparent);
      --bd-soft:    #E6E1D8;

      --focus-colour: rgba(110, 26, 20, 0.45);

      --rule-faint:   color-mix(in srgb, var(--ink) 3%, transparent);
      --rule-soft:    color-mix(in srgb, var(--ink) 7%, transparent);
      --rule-default: color-mix(in srgb, var(--ink) 11%, transparent);
      --rule-strong:  color-mix(in srgb, var(--ink) 16%, transparent);

      --code-string: #2E5BBF;
      --code-number: #6B47B0;
      --code-var:    #6B47B0;
    }
    /* selection colour returns to the light-mode default under
       explicit-light inside the dark media query. */
    :root[data-theme="light"] ::selection {
      color: var(--bg);
    }
    /* brand-only masthead under explicit-light: light tone. */
    :root[data-theme="light"] body[data-masthead="brand-only"] {
      /* phase 93 · brand-only --fg2/--fg3 override dropped under
         explicit-light too (same AA reasoning as the default
         block at body[data-masthead="brand-only"]). --rule stays. */
      --rule: color-mix(in srgb, var(--ink) 8%, transparent);
    }
  }

  /* explicit data-theme="dark" override — applies the dark palette
     even when the system prefers light. token list duplicates the
     media-query branch above; keep the two in sync when editing. */
  :root[data-theme="dark"] {
    color-scheme: dark;

    --paper-main:        #1B1916;
    --paper-record:      #22201B;
    --paper-raised:      #26231F;
    --paper-raised-high: #2C2925;
    --paper-project:     #1F1D1A;
    --surface-archival:  #26231F;

    /* phase 84 · lockstep with the prefers-color-scheme: dark scrim
       — deep warm sepia, no pure black. */
    --overlay-scrim: rgba(20, 16, 12, 0.42);

    --rule:        color-mix(in srgb, var(--ink) 9%, transparent);

    --ink:        #F0EAE0;
    --ink-muted:  #C9C2B7;

    --accent:        #7A241C;
    --accent-text:   #D06058;
    --accent-hover:  #D9685A;
    --accent-soft:   color-mix(in srgb, var(--accent) 75%, var(--paper-record));
    --accent-border: color-mix(in srgb, var(--accent) 40%, transparent);
    --ac2:           #D9685A;

    --fg2:        #B8B0A4;
    --fg3:        #A39C92;
    --bd:         color-mix(in srgb, var(--ink) 12%, transparent);
    --bd-soft:    #332F2A;

    --focus-colour: rgba(208, 96, 88, 0.85);

    --rule-faint:   color-mix(in srgb, var(--ink) 5%, transparent);
    --rule-soft:    color-mix(in srgb, var(--ink) 9%, transparent);
    --rule-default: color-mix(in srgb, var(--ink) 13%, transparent);
    --rule-strong:  color-mix(in srgb, var(--ink) 20%, transparent);

    --code-string: #99D5A1;
    --code-number: #C8AEEA;
    --code-var:    #C8AEEA;
  }
  :root[data-theme="dark"] ::selection {
    color: var(--ink);
  }
  :root[data-theme="dark"] body[data-masthead="brand-only"] {
    --fg2:  #B8B0A4;
    --fg3:  #968F86;
    --rule: color-mix(in srgb, var(--ink) 10%, transparent);
  }

  @media (prefers-contrast: more) {
    :root {
      --ink:        #000000;
      --ink-muted:  #1A1A1A;
      --paper-main: #FFFFFF;
      --paper-record: #F4F2EC;
      --rule:        rgba(0, 0, 0, 0.45);
    }
    a {
      text-decoration-thickness: 2px;
      text-underline-offset: 0.18em;
    }
    :focus-visible {
      /* keep: wcag 2.4.7 — increased focus visibility under user-
         requested high contrast must override component-level rings. */
      outline-width: 3px !important;
      outline-offset: 3px !important;
    }
    /* drop the paper-noise + vignette so high-contrast users see a
       clean flat nav, no texture interference with the system palette. */
    body, .nav { background-image: none; }
  }

  /* dark + contrast intersect — bare :root applies when system is
     dark+more-contrast. explicit data-theme overrides win when the
     user has pinned, mirroring the split in the main dark block. */
  @media (prefers-color-scheme: dark) and (prefers-contrast: more) {
    :root {
      --ink:        #FFFFFF;
      --paper-main: #000000;
      --paper-record: #0A0A0A;
      --rule:        rgba(255, 255, 255, 0.55);
    }
    /* explicit light override under dark+contrast: re-apply the
       light-contrast palette (matches `prefers-contrast: more` on light). */
    :root[data-theme="light"] {
      --ink:        #000000;
      --ink-muted:  #1A1A1A;
      --paper-main: #FFFFFF;
      --paper-record: #F4F2EC;
      --rule:        rgba(0, 0, 0, 0.45);
    }
  }
  /* explicit dark under contrast when system prefers light. */
  @media (prefers-contrast: more) {
    :root[data-theme="dark"] {
      --ink:        #FFFFFF;
      --paper-main: #000000;
      --paper-record: #0A0A0A;
      --rule:        rgba(255, 255, 255, 0.55);
    }
  }

  /* windows high contrast mode (and other forced-colors environments).
     remap key tokens to system colour keywords so user-controlled
     palettes win. forced-color-adjust is intentionally not set;
     interactive controls keep their own visible borders so they remain
     distinguishable from body text. */
  @media (forced-colors: active) {
    :root {
      --ink:        CanvasText;
      --paper-main: Canvas;
      --paper-record: Canvas;
      --paper-raised-high: Canvas;
      --accent:     LinkText;
      --rule:       CanvasText;
    }
    /* drop the paper-noise + vignette so the windows high-contrast
       canvas palette wins without texture interference. */
    body, .nav { background-image: none; }
    a {
      color: LinkText;
    }
    .cite-btn {
      /* min-height: 44px restated here so the l8 invariant matches
         the first .cite-btn rule the validator finds via re.search. */
      min-height: 44px;
      color: ButtonText;
      border-color: ButtonText;
    }
    :focus-visible {
      outline-color: Highlight;
    }
  }

}

@layer layout {
/* Brand-only masthead.
   the full trent power + nav strip is the homepage identity. every
   other page (privacy, the entire record layer, error pages) shows
   only trent power. same height, hairline rule, mobile two-line
   stack — the brand stays exactly as on the homepage; the wayfinding
   list disappears. (Retired: the nav-toggle / nav-links system is gone
   site-wide; the masthead now stands alone in every header.) */

}

@layer base {
/* phase 91 · display typography contract.
   display surfaces share a single, low-specificity (:where, 0,0,0)
   declaration of the serif face + font-synthesis: none so heading
   metrics never twitch during the critical-subset → full-font swap.
   .hero-statement mark keeps its own font-synthesis: style override
   (line ~2590) automatically — :where() carries zero specificity.
   lives in @layer base rather than a new layer so the canonical
   layer order (validated by tools/validate_css_architecture.py L1)
   stays untouched. */
:where(.hero-statement, .page-title, .page-lede, .page-kicker,
       .release-title, .reader-h1--source, [data-display-title]) {
  font-family: var(--serif);
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

/* opentype defaults wherever signifier renders editorial prose:
   kerning, ligatures, contextual alternates, old-style figures. */
.hero-statement, .hero-body,
.principle-title, .principle-body,
.chapter-title, .chapter-detail,
.project-desc, .project-subline,
.preview-editorial,
.page-title, .page-lede, .page-body p,
.modal-text {
  font-feature-settings: "kern" 1, "liga" 1, "onum" 1, "calt" 1;
  font-kerning: normal;
  font-variant-numeric: oldstyle-nums proportional-nums;
  text-rendering: optimizeLegibility;
}

/* tabular lining figures wherever evidence renders, so columns of numerals
   align and dates / hashes / file paths read precisely. */
code, .fingerprint,
.section-label, .chapter-label, .page-meta,
.preview-meta, .preview-title, .site-footer__colophon,
.nav-mark, .contact-email {
  font-variant-numeric: tabular-nums lining-nums;
}

}
@layer reset {
/* reset & base */

*, *::before, *::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

::selection {
  background: var(--ac);
  color: var(--bg);
}

html {
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

}
@layer tokens {
/* anchor offset for the fixed nav. 52px nav height plus a small breathing
   room so section labels land cleanly just under the nav, not flush. */
:root {
  --nav-offset: 60px;

  /* homepage layout geometry · single source of truth. the masthead
     and hero declarations below consume these, and the language
     vestibule (/) inherits the same numbers because its background is
     a verbatim slice of the rendered homepage using the same classes
     and the same compiled stylesheet — / and /en/ are one geometry. */
  --page-max:           1200px;  /* .nav-inner editorial register */
  --nav-height:         54px;    /* .nav-inner row height */
  --hero-statement-max: 920px;   /* .hero-statement measure */
  --hero-body-max:      610px;   /* .hero-body measure */

  /* Anchor-offset tokens for native anchor navigation on the home
     section ids. mobile nav-inner height varies with the device —
     ~58 px on non-notched displays and ~102 px on notched iphones
     (env(safe-area-inset-top) + padding + content). the mobile
     value is dynamic: `calc(env(safe-area-inset-top) + 4.5rem)` =
     72 px non-notch / ~116 px notched, landing the section heading
     ~14 px below the nav at every viewport.

     desktop nav is `position: fixed` at 52 px — the 7 rem fixed
     value lands the heading with calm breathing room and doesn't
     need an env() lookup (desktop browsers don't expose a
     meaningful safe-area-inset-top). */
  --anchor-offset-mobile:  calc(env(safe-area-inset-top) + 4.5rem);
  --anchor-offset-desktop: 7rem;

  /* Safe-area-inset tokens — surface env() values as named tokens so
     edge-touching elements (sticky/fixed nav, footer, overlays) can
     refer to them via var() with a sane 0 fallback. `viewport-fit=
     cover` (set in <meta name="viewport">) lets the layout reach the
     full visual viewport including the notch/Dynamic island and the
     ios home-indicator strip; the tokens give us a single point to
     reason about edge clearance.

     use only where the layout actually reaches a viewport edge — the
     brief's guidance is "do not blindly add safe-area padding
     everywhere." most centred / max-width content does not need it. */
  --safe-top:    env(safe-area-inset-top,    0px);
  --safe-right:  env(safe-area-inset-right,  0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left:   env(safe-area-inset-left,   0px);
}

}
@layer base {
[id] {
  scroll-margin-top: var(--nav-offset);
}

/* native anchor navigation on the home section ids lives in
   @layer overrides below — bare id selectors (per the brief) are
   legal there. search "anchor landing offsets" to find the
   declarations. */

html, body {
  /* `overflow-x: clip` is the modern variant — does not establish a
     scroll context the way `hidden` does, so positioned descendants
     remain reachable via fragment navigation. older browsers ignore
     `clip` and fall through to the next declaration. */
  overflow-x: hidden;
  overflow-x: clip;
}

/* editorial abbreviation register. native html <abbr title="…">
   gets a quiet dotted underline so the conventional "hover for
   expansion" affordance reads correctly without shouting. browsers
   without dotted-underline support fall through to nothing —
   acceptable degradation. */
abbr[title] {
  text-decoration: underline dotted var(--rule);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  cursor: help;
}

/* phase 43 · semantic enrichment register.
   `<dfn>` marks the first definitional occurrence of a concept
   (integrity manifest, source mirror, signed release, page record,
   detached signature). browser-default italic is overridden — the
   element should read as a quiet definitional anchor, not as a
   typographic flourish. */
dfn {
  font-style: normal;
  font-weight: 500;
  color: var(--fg);
}

/* `<samp>` marks computed output — sha-256 hashes, pgp fingerprints,
   validation timestamps. mono register so it sits inside the
   technical-mono family with `<code>`, but slightly smaller so the
   eye reads it as derived output rather than authored input. */
samp {
  font-family: var(--mono);
  font-size: 0.92em;
  color: var(--fg);
  overflow-wrap: anywhere;
}

/* `<cite>` marks publication titles in editorial register. browser-
   default italic preserved — in serif body prose this reads as
   published-work convention; in mono surfaces the slant is barely
   perceptible. */
cite {
  font-style: italic;
  color: inherit;
}

/* mark · restrained editorial emphasis. one phrase on the homepage
   hero ("growth systems") and only there — not a highlighter but a
   soft warm tint occupying the bottom third of the line height, like
   a pencil mark in an editor's hand. the 10% accent mix keeps the
   effect barely visible in light mode and essentially invisible in
   dark mode; the emphasis rewards close reading without announcing
   itself. color: inherit preserves text contrast unchanged in both
   themes so wcag contrast is unaffected. background-image (not the
   shorthand) leaves room for an inherited background-color, and
   no-repeat keeps the gradient single-shot rather than tiling on
   wraps. */
mark {
  /* the browser default for <mark> is background-color: yellow with
     black text. background-image alone is layered on top of that
     default — wherever the gradient is transparent, the yellow leaks
     through. explicit background-color: transparent kills the default
     so only the editorial tint remains.

     atmospheric, not graphic: a 4% warm wash occupies only the lower
     ~22% of the line, low enough that the eye reads it as a paper-
     tone shift rather than a marker. tuned for mobile safari at large
     serif display sizes on the hero h1, where any stronger value
     started reading as a yellow-highlighter rectangle.

     box-decoration-break: clone applies the wash to each line box
     independently when a marked phrase wraps, so wrapped headlines
     keep their rhythm and never inherit a heavy continuous bar across
     line breaks. */
  background-color: transparent;
  background-image: linear-gradient(
    to top,
    color-mix(in srgb, var(--accent) 4%, transparent) 22%,
    transparent 22%
  );
  background-repeat: no-repeat;
  color: inherit;
  padding: 0 .03em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

@media print {
  /* the browser default for mark is a yellow background with black
     text. on print this would land as a highlighter artefact on
     "growth systems", contradicting the editorial intent. drop the
     background entirely; print stays plain prose. */
  mark {
    background: transparent;
    color: inherit;
  }
}

/* editorial focus-visible system. calm oxblood ring at 45 % alpha,
   2 px width, 3 px offset. component overrides land in @layer
   components below (text links → typographic underline-on-focus
   instead of a box; buttons / nav controls / ctas → token-based
   outline tightened to the same width and offset).

   :focus:not(:focus-visible) is stripped so a touch tap on ios
   safari doesn't leave a persistent outline behind on the tapped
   element. keyboard users still get :focus-visible. the
   prefers-contrast: more block elsewhere in this file overrides
   these to 3 px solid var(--ac) for high-contrast at users. */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-colour);
  outline-offset: var(--focus-offset);
  border-radius: var(--focus-radius);
}
:focus:not(:focus-visible) {
  outline: none;
}
@media (pointer: coarse) {
  :focus:not(:focus-visible) {
    outline: none;
  }
}

/* inline text links (body copy, hero body, trust-mark, contact
   secondary, footer privacy) prefer a typographic underline-on-focus
   over a box outline. the underline reads as continuous emphasis
   with the typeface; an outline would look like a form control.
   component selectors enumerated explicitly so the typographic
   treatment doesn't leak into buttons or icon controls that need a
   visible ring. */
.page-body a:focus-visible,
.hero-body a:focus-visible,
.contact-secondary a:focus-visible {
  outline: none;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.18em;
  text-decoration-color: var(--focus-colour);
}

body {
  font-family: var(--sans);
  background-color: var(--bg);
  /* paper · two fixed background layers sit above the flat hex:
     a soft corner vignette so the page feels held in two hands,
     and a small svg fractal-noise giving the surface tooth at
     ~4.5% alpha. both are background-attachment:fixed so they do not
     scroll. the vignette uses var(--ink) so dark mode inverts it
     automatically; the noise reads near-identically on cream and on
     near-black. the noise lives at /images/textures/paper-noise.svg
     because img-src 'self' in our csp blocks data: urls in background
     images — keeping the texture as a real file preserves the strict
     csp and the existing browser cache. */
  background-image:
    radial-gradient(ellipse 120% 90% at 50% 50%, transparent 55%,
                    color-mix(in srgb, var(--ink) 4%, transparent) 100%),
    url('/images/textures/paper-noise.svg');
  background-attachment: fixed, fixed;
  background-size: auto, 260px 260px;
  background-repeat: no-repeat, repeat;
  color: var(--fg);
  line-height: 1.6;
  font-synthesis: none;
}

}
@layer overrides {
/* Print-only elements never render on screen. print.css restores them
   inside @media print. defined here so the rule applies even though
   print.css is loaded with media="print" and never reaches screen.

   no !important: cascade-layer order makes this rule sit in the
   `overrides` layer, which beats every other named screen layer for
   normal-importance declarations. removing !important is critical so
   that print.src.css's @layer print-overrides {!important} rules can
   override these in print mode — !important across cascade layers
   reverses precedence (earlier layer wins), so a screen-layer
   !important here would beat the print-layer !important and the
   homepage would print as a blank page. */
.print-only,
.print-profile,
.print-trust-sheet,
.print-utility-sheet,
.print-citation,
.print-seal {
  display: none;
}

}
@layer components {
/* phase d · shared base classes for repeated visual patterns.
   .card / .action / .meta-row capture cross-page taxonomy.
   wrapped in :where() so specificity is 0,0,0 — variant rules
   declared later in this same layer reliably win on conflict.
   property-level deduplication of variants is deferred to a
   later pass; these rules are additive only. */
:where(.card) {
  padding: clamp(1.2rem, 4vw, 1.9rem);
  background: var(--surface-archival);
  border: 1px solid var(--rule);
  border-radius: var(--radius-soft);
}

:where(.action) {
  display: inline-flex;
  align-items: center;
  font-family: var(--mono);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  color: var(--fg2);
  transition: color 200ms ease, border-color 200ms ease;
}

:where(.action):hover,
:where(.action):focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}

:where(.meta-row) {
  display: grid;
  border-top: 1px solid var(--rule);
  gap: 0.5rem;
  padding-top: 1rem;
}

/* /verify/ public verification route.
   verify.js renders into #verify-root using only textcontent and
   setattribute on safe attributes. Same-origin only. */

/* eyebrow above page-title on utility pages (error · maintenance ·
   sw-reset). small mono caps, fg3-muted, paired immediately above
   the h1 page-title. matches the eyebrow register used elsewhere
   without inheriting any section-label margins. */
.page-kicker {
  /* phase 84 · ROOT CAUSE of "too heavy" kicker on verify (and every
     other internal page): font-weight: 500 with a font face that
     only ships weight 400. browser synthesises a faux-bold rendering
     of söhne mono critical, which renders heavier and visually
     different from the designed face. dropped to 400 so the kicker
     renders at the designed weight with no synthesis. matches the
     buch-labels subset (buch = book = 400 weight in german type
     terms). user has been seeing this discrepancy across many
     iterations · the fix is not in the loading pipeline but in the
     weight request itself. */
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
  font-style: normal;
  line-height: 1.2;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg3);
  margin: 0 0 0.4rem;
  font-variant-numeric: tabular-nums lining-nums;
}

/* /verify/ scoped layout
   verify.js renders real <section> children inside #verify-root, which
   would otherwise inherit the global `section { padding: clamp(80px,
   12vh, 160px) 0 }` rule from the homepage hero rhythm. the scoped
   reset below cancels that inheritance and re-establishes a tighter
   editorial rhythm specific to the verification page. */
.verify-page section {
  padding: 0;
  border-bottom: 0;
  min-height: 0;
}

/* editorial column. wider than prose pages so the page record can
   carry presence, but narrow enough to keep line length readable.
   phase 70 · removed `margin: 0 auto` — the body used to centre while
   the kicker/H1 (siblings of .page-body) stretched to the full .site
   width, producing two competing left edges. the new .verify-hero
   wrapper owns the shared axis; .page-body now sits flush-left so
   the lede/card stay on that axis. */
.verify-page .page-body {
  max-width: 880px;
  margin: 0;
  /* top padding tightened from clamp(48,7vw,88) so the kicker sits
     closer to the masthead , /verify/ is a utility-record page, not
     a hero-driven landing. */
  padding: clamp(40px, 6vw, 64px) 0;
}
@media (max-width: 700px) {
  .verify-page .page-body {
    max-width: none;
    padding: 32px 0 48px;
  }

  .verify-page .other-records {
    margin-block-end: clamp(2.5rem, 9vw, 4rem);
  }
  .verify-page .other-records-grid {
    column-gap: clamp(1.5rem, 10vw, 3rem);
  }

  .trust-code { font-size: 0.66rem; padding: 0.85rem 0.95rem; line-height: 1.55; }

  .integrity-record-actions {
    flex-direction: column;
    gap: 0.5rem 0;
    line-height: 1.4;
  }
  .integrity-record-action { align-self: flex-start; }

  .integrity-record-actions.integrity-record-actions--strip {
    display: block;        /* override the legacy column rule above */
  }
  .integrity-record-actions--strip .integrity-record-action {
    align-self: auto;      /* override the legacy flex-start rule */
  }

  .release-row--current { padding-bottom: clamp(1.3rem, 5vw, 1.8rem); }
  /* mobile stacks each action on its own line — middots are hidden
     and links sit as a clean four-line list. the wrapping behaviour
     of an inline strip becomes unnecessary, and "checksums &
     signature" is given room to breathe on the smallest screens. */
  .release-actions {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    font-size: 0.66rem;
    line-height: 1.5;
  }
  .release-action { align-self: flex-start; }
  .release-actions > * + *::before { display: none; }

  .source-page .page-body {
    max-width: none;
  }
}

/* phase 70 · verify hero · single editorial axis.
   kicker + H1 + lede share one .verify-hero wrapper with one max-width
   so the left edge is consistent and the right edges are coherent.
   the page record card sits as a sibling below, aligned to the same
   left edge. desktop tokens carry the rhythm; mobile + tablet media
   blocks below tighten the h1→lede and lede→card gaps. */
.verify-page .verify-hero {
  max-width: 640px;
  margin: 0;
}
.verify-page .verify-hero .page-kicker {
  margin: 0 0 var(--hero-kicker-gap);
}
.verify-page .verify-hero .page-title {
  max-width: none;
  margin: 0 0 var(--hero-title-gap-desktop);
  overflow-wrap: break-word;
  text-wrap: balance;
}
.verify-page .verify-hero .page-lede {
  max-width: none;
  margin: 0;
}
.verify-page .verify-root {
  margin-left: 0;
  margin-top: var(--hero-lede-gap-desktop);
}

/* mobile rhythm · remove the visible dead zone under the H1, tighten
   lede→card so the page stops feeling sparse on small viewports. */
@media (max-width: 759.98px) {
  .verify-page .verify-hero .page-title {
    margin-bottom: var(--hero-title-gap-mobile);
  }
  .verify-page .verify-hero .page-lede {
    margin-bottom: 0;
  }
  .verify-page .verify-root {
    margin-top: var(--hero-lede-gap-mobile);
  }
}

/* tablet rhythm · 10-15% tighter than desktop so iPad widths read
   as a controlled editorial layout, not a stretched desktop. */
@media (min-width: 760px) and (max-width: 1099.98px) {
  .verify-page .verify-hero .page-title {
    margin-bottom: var(--hero-title-gap-tablet);
  }
  .verify-page .verify-root {
    margin-top: var(--hero-lede-gap-tablet);
  }
}

/* ─────────────────────────────────────────────────────────────
   phase 33 · verify-page micro-grid record system
   ─────────────────────────────────────────────────────────────
   the page record is a signed technical certificate. the layout
   is a definition-list grid where each row pairs a small mono
   uppercase label with a readable value. inspired by swiss
   archival records, museum object labels, financial terminals
   and printed release ledgers. the current-edition panel below
   reuses the same grid via the `--quiet` modifier.
*/
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card {
  /* phase 68 · mobile floor bumped 2 → 2.4rem so the lede-to-card gap
     breathes a touch more on small viewports without expanding desktop. */
  margin-block-start: clamp(2.4rem, 5vw, 3.25rem);
  margin-block-end: clamp(2rem, 6vw, 4rem);
  /* phase 50 · padding compressed to match the integrity record
     card register (clamp 1.2-1.9rem). archival rhythm unifies.
     phase 52 · background promoted to the canonical archival
     surface so verify + integrity cards read as one paper sheet
     family. border colour promoted to the canonical --rule token
     so the divider register matches across pages. */
  width: 100%;
  max-width: 100%;
  min-width: 0;
  padding: clamp(1.2rem, 4vw, 1.9rem);
  background: var(--surface-archival);
  border: 1px solid var(--rule);
  /* phase 39 · 2px outer radius matches the project-card register
     so verify, integrity and sw-reset records read as the same
     family of mounted archival sheets. .record-grid__row stays
     sharp — only the outer container is softened. phase 46 ·
     literal 8px → var(--radius-soft) (7px). */
  border-radius: var(--radius-soft);
  /* paper-raise via a quiet light-mode shadow only; dark mode
     drops the shadow so it doesn't muddy the page. */
  box-shadow: 0 18px 40px rgb(0 0 0 / 0.06);
}
@media (prefers-color-scheme: dark) {
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card,
  .integrity-record-card {
    /* phase 52 · dark-mode card surface promoted to canonical
       archival surface for verify / integrity parity.
       phase 64 · .integrity-record-card joins the same group so
       it adopts the archival surface tone and drops the shadow
       in dark mode. */
    background: var(--surface-archival);
    box-shadow: none;
  }
}

:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__header,
.integrity-record-dl,
.integrity-rg,
.integrity-record-actions {
  min-width: 0;
}

:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__header {
  /* phase 67 · header gap to grid tightened so the card reads as
     one composed record rather than two stacked panels. */
  margin-block-end: clamp(0.9rem, 2.4vw, 1.25rem);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__header .eyebrow {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg3);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__header h2 {
  /* phase 68 · card title eased so the page title doesn't dominate the
     evidence rows below. mobile floor 1.75 → 1.45rem, growth slowed
     6.5vw → 5.4vw, ceiling 3.25 → 3.0rem, line-height 0.98 → 1.02. */
  margin: 0.45rem 0 0.4rem;
  font-family: var(--serif);
  font-size: clamp(1.45rem, 5.4vw, 3.0rem);
  font-weight: 300;
  line-height: 1.02;
  letter-spacing: -0.012em;
  color: var(--fg);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-status {
  /* phase 68 · "signed · source · archived" pulled back to a whisper:
     smaller (0.72 → 0.68rem), looser tracking (0.08 → 0.1em), and 80%
     opacity on fg3 so it reads as supporting metadata, not a banner. */
  margin: 0;
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  color: color-mix(in srgb, var(--fg3) 80%, transparent);
}

:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid {
  margin: 0;
  display: grid;
  /* phase 67 · grid hairlines retired.
     phase 68 · seam token eased to --rule-faint.
     phase 84 · seam retired entirely. on mobile safari retina at low
     brightness the 1 px var(--rule-faint) hairline read as a pale
     horizontal band — "eyelid" — across the top of the card content.
     spacing rhythm alone now carries the transition from header to
     records, matching the integrity card refresh (phase 82). */
  padding-block-start: 0.95rem;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid__row {
  display: grid;
  grid-template-columns: minmax(7.5rem, 0.32fr) minmax(0, 1fr);
  gap: clamp(0.9rem, 4vw, 1.8rem);
  /* phase 67 · rows now lean on label margin + line-height rather
     than per-row rules. tight symmetric padding-block keeps the
     rhythm calm without manufacturing a table. */
  padding-block: 0.55rem;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid__row:first-child {
  padding-top: 0;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid__row:last-child {
  padding-bottom: 0;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid dt {
  /* phase 67 · labels move from mono uppercase to quiet sans —
     archival, not forensic. dt continues to provide an accessible
     definition label; the visual register matches the integrity
     group-label refresh.
     phase 68 · label register softened a step: smaller (0.78 →
     0.74rem) and quieter colour (fg2 → fg3) so the value column
     is unmistakably the protagonist. */
  font-family: var(--sans);
  font-size: 0.74rem;
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: 0.005em;
  text-transform: none;
  color: var(--fg3);
  margin: 0;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid dd {
  margin: 0;
  min-width: 0;
  font-size: clamp(0.9rem, 2.4vw, 1rem);
  /* phase 51/57 · ledger line-height progressively tightened:
     1.5 → 1.35 → 1.28 so the mono metadata rows match the
     integrity .integrity-rg-value register across both cards.
     phase 68 · loosened a notch (1.28 → 1.32) so multi-line
     values (canonical URLs, hashes) read with a touch more air. */
  line-height: 1.32;
  color: var(--fg);
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* phase 51 · long mono outputs (paths, hashes) wrap cleanly on
   narrow viewports so the value column never overflows the card.
   .record-grid code already had this; samp inherits the same rule. */
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid samp {
  font-family: var(--mono);
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* phase 51 · the verify page's fingerprint as compact ledger hex.
   smaller than .integrity-fingerprint (which has wbr-broken 4-char
   groups inside the larger integrity record card); this one sits
   as a single inline-wrapped hash in the verify card's value
   column. */
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-fingerprint {
  display: block;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  color: var(--fg);
  overflow-wrap: anywhere;
  /* phase 68 · explicit hyphens:none so user-agent hyphenation never
     inserts dashes mid-hash on narrow viewports. */
  hyphens: none;
}
/* phase 67 · the fingerprint as a calm two-part archival mark.
   small mono kicker ("sha256") sits above a softened hash body.
   the hash itself wraps cleanly and shrinks one step so it reads
   as inspectable evidence, not a forensic blob. */
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-fingerprint-block {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
  max-width: 100%;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-fingerprint-algo {
  display: inline-block;
  align-self: flex-start;
  font-family: var(--mono);
  font-size: 0.62rem;
  line-height: 1.2;
  letter-spacing: 0.06em;
  color: var(--fg3);
  text-transform: lowercase;
}
.page-hash {
  overflow-wrap: anywhere;
  word-break: break-word;
  hyphens: none;
  font-size: 0.8rem;
  line-height: 1.5;
  letter-spacing: 0.005em;
  color: var(--fg2);
  /* phase 68 · ~7% contrast pulled out so the hash reads as
     inspectable evidence rather than a forensic blob. */
  opacity: 0.88;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
  transition: color 0.2s, border-bottom-color 0.2s;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid a:hover,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid a:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid code {
  font-family: var(--mono);
  font-size: 0.86em;
  background: transparent;
  padding: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-meta {
  display: block;
  margin-block-start: 0.3rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  color: var(--fg3);
}

/* phase 51 · record-actions refactored as a dot-separator inline
   phrase.
   phase 56 · class renamed to .record-tools; border-top divider
   above the rail retired (spacing alone marks the transition from
   card to actions); register aligned with the integrity inline-
   action vocabulary (.record-inline-action / .copy-fingerprint /
   .trust-code-copy) so the verify rail joins one declarative
   system across the site. button + anchor children now carry the
   .record-inline-action class and inherit styling from the shared
   rule. */
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.55rem;
  /* phase 57 · gap to the card above tightened so the utility rail
     sits as the immediate footer of the card.
     phase 58 · further compressed so the rail reads as a tight
     archival footer rhythm against the last record row above.
     phase 68 · tracking eased 0.055 → 0.045em so the rail reads
     as a quiet record action, not a dashboard control strip.
     phase 75 · gap bumped 0.35rem → 1.2rem so the actions sit on
     a clear new line below the last record row (release archive)
     rather than reading as part of the same rhythm. */
  margin-block-start: 1.2rem;
  font-family: var(--mono);
  font-size: 0.58rem;
  line-height: 1.25;
  letter-spacing: 0.045em;
  color: var(--fg3);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-tools > * {
  display: inline-flex;
  align-items: baseline;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-tools .record-inline-action + .record-inline-action::before {
  content: "·";
  margin-right: 0.55rem;
  opacity: 0.45;
}

:is(.verify-page, .sw-reset-page, .security-page, .source-page) .edition-panel {
  margin-block-start: clamp(0.9rem, 3vw, 1.9rem);
  margin-block-end: clamp(0.85rem, 2.3vw, 1.4rem);
  /* a second archival record mounted on the same paper family as
     .verify-card above. no box-shadow (this is a secondary card,
     not the primary record); rule border softer than --rule so the
     two cards read as parent and continuation, not as two equal
     boxes. */
  padding: clamp(1.2rem, 4vw, 1.9rem);
  background: var(--surface-archival);
  border: 1px solid color-mix(in srgb, var(--rule) 65%, transparent);
  border-radius: var(--radius-soft);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .edition-panel h2 {
  /* phase 52 · h2 scale reduced so current edition no longer reads
     as section-heavy. it supports the page record above, doesn't
     compete with it. */
  margin: 0 0 0.85rem;
  font-family: var(--serif);
  font-size: clamp(1.1rem, 3.5vw, 1.4rem);
  font-weight: 300;
  line-height: 1.1;
  letter-spacing: -0.008em;
  color: var(--fg);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet {
  color: var(--fg2);
  /* phase 52 · rule colour unified to var(--rule).
     phase 84 · base .record-grid border-top retired (see above); this
     border-top-color override is no longer load-bearing and is
     removed. */
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet .record-grid__row {
  /* phase 53 · per-row hairlines retired in the --quiet variant.
     phase 55 · closing :last-child hairline also retired so the
     current-edition panel ends without a floating divider between
     itself and the back link. spacing alone carries the close. */
  padding-block: 0.85rem;
  border-bottom: 0;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet .record-grid__row--fingerprint {
  padding-block-start: 1.05rem;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet dt {
  font-size: 0.6rem;
  /* recede comes from --fg3 + 0.6rem + tracking; opacity reduction
     would drop the blend below aa at this size. */
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet .record-grid__row--fingerprint dt {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.45rem;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet dd {
  font-size: clamp(0.78rem, 2vw, 0.88rem);
}

/* mobile stacking · labels above values when the row gets tight.
   phase 67 · row padding compressed; sans labels stay legible at
   0.74rem; values keep a comfortable 0.92rem read. */
@media (max-width: 620px) {
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card {
    padding: clamp(1.25rem, 6vw, 1.8rem);
  }
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid__row {
    grid-template-columns: 1fr;
    gap: 0.18rem;
    /* phase 68 · mobile row padding eased 0.55 → 0.7rem so stacked
       rows breathe with the calmer label register set above. */
    padding-block: 0.7rem;
  }
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid dt { font-size: 0.7rem; }
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid dd { font-size: 0.92rem; }
  /* phase 68 · the per-page meta line (kind · size · validated date)
     quieter on mobile: smaller, more line-height, same fg3 colour. */
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-meta {
    font-size: 0.68rem;
    line-height: 1.45;
  }
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet .record-grid__row {
    padding-block: 0.7rem;
  }
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet .record-grid__row--fingerprint {
    padding-block-start: 0.95rem;
  }
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet dt { font-size: 0.58rem; }
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid--quiet dd { font-size: 0.84rem; }
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-tools {
    display: grid;
    gap: 0.4rem;
  }
  /* phase 58 · when the utility rail stacks vertically on mobile,
     the dot pseudo before each non-first item would render as a
     leading bullet and read as an accidental list. hide it. */
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-tools .record-inline-action + .record-inline-action::before {
    content: none;
  }
}

/* ─────────────────────────────────────────────────────────────
   verify · other-records + current-edition section structure
   ─────────────────────────────────────────────────────────────
   two distinct sections sit below the page-record card: the
   "other records" sibling-route index and the "current edition"
   panel. spacing alone separates them — no bridging rule, no
   shared border. releases must read as the close of other
   records, never as the lead of current edition.
*/
.verify-page .other-records {
  /* tightened block-end · a visible <hr> divider now carries the
     section break; spacing only completes the rhythm. */
  margin: clamp(2rem, 5vw, 3rem) 0 clamp(1.75rem, 6vw, 2.75rem);
}
/* shared peer-heading register · other records and current edition
   sit as siblings at the same hierarchy level, divided by the
   visible <hr> section-divider below. */
.verify-page .other-records-title,
.verify-page .current-edition .current-edition-title {
  margin: 0 0 1.25rem;
  font-family: var(--serif);
  font-size: clamp(1.55rem, 5vw, 2rem);
  font-weight: 400;
  line-height: 1.12;
  letter-spacing: -0.015em;
  color: var(--fg);
  text-transform: none;
}

/* peer-section divider · soft hairline between other records and
   current edition. matched to --rule, with generous bottom margin
   so the divider clearly closes the section above and opens the
   one below rather than floating between them. */
.verify-page .section-divider--after-records {
  margin: 0 0 clamp(2rem, 7vw, 3.25rem);
  border: 0;
  border-top: 1px solid var(--rule);
  height: 0;
}
.verify-page .other-records-grid {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(2.5rem, 12vw, 5rem);
  row-gap: 1rem;
}
.verify-page .other-records-grid li { display: block; }
.verify-page .other-records-grid a {
  display: inline-block;
  width: fit-content;
  color: var(--fg2);
  font-family: var(--mono);
  font-size: 0.86rem;
  line-height: 1.35;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: color 0.2s, text-decoration-color 0.2s;
}
.verify-page .other-records-grid a:hover,
.verify-page .other-records-grid a:focus-visible {
  color: var(--accent-text);
  outline: 0;
}
.verify-page .other-records-grid [aria-current="page"] {
  display: inline-block;
  width: fit-content;
  color: var(--fg3);
  text-decoration: none;
  cursor: default;
  font-family: var(--mono);
  font-size: 0.86rem;
  line-height: 1.35;
}

/* current-edition · sits as a fresh section, not as a
   continuation of the other-records list. no top margin — the
   block-end margin on .other-records carries the separation.
   the inner record-grid drops its top hairline so the h2 and
   the rows read as one editorial block. */
.verify-page .edition-panel.current-edition {
  margin-top: 0;
  padding-top: 0;
  margin-block-end: clamp(0.85rem, 2.3vw, 1.4rem);
}
.verify-page .current-edition .record-grid {
  border-top: 0;
}
/* override the shared .record-card .fingerprint-section border so
   edition and signing key read as one continuous record block —
   the visible section break belongs above the h2, not between
   data rows. natural row padding from .record-grid--quiet keeps
   the breathing without painting a line. */
.verify-page .current-edition .fingerprint-section {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* /verify/ unknown-route fallback needs its own bottom margin so it
   doesn't collide with the page padding. */
.verify-page .verify-unknown { margin: 0 0 clamp(36px, 5vw, 64px); }

.trust-code-block {
  /* a little breathing room above the command block so it reads as
     an archival printout, not a stacked terminal panel. */
  margin: 0;
  padding-top: 0.15rem;
}
.trust-code-block-header {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 0.45rem;
}
/* phase 55 · standalone .trust-code-copy rules retired. all
   inline-action styling now lives in the combined .record-inline-
   action / .copy-fingerprint / .trust-code-copy rule further down,
   keeping the integrity command-block copy and the fingerprint
   copy in lockstep. */
.trust-code {
  /* phase 50 · command block aligned with the card-object family:
     7px outer radius (var(--radius-soft)). background promoted to
     --surface-card (which now resolves to --surface-archival in
     phase 52). border at canonical --rule. phase 52 · padding
     compressed ~8% so the command slip reads as a terminal printout
     rather than a developer card. */
  margin: 0;
  padding: clamp(0.9rem, 2.75vw, 1.25rem);
  background: var(--surface-card);
  border: 1px solid var(--rule);
  border-radius: var(--radius-soft);
  font-family: var(--mono);
  font-size: 0.74rem;
  line-height: 1.6;
  color: var(--fg);
  /* display wraps long urls cleanly; copy returns the original. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
  max-width: 100%;
}
.trust-code .code-cmd { color: var(--fg); }
.trust-code .code-var { color: var(--fg2); }
.trust-code .code-str { color: var(--accent-text); }

/* Unknown-route fallback , small actions strip mirrors the page-record
   actions strip so the fallback never reads as broken. */
.verify-page .verify-unknown-actions {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.025em;
  color: var(--fg);
  margin: 0.7rem 0 0;
  line-height: 1.5;
}
.verify-page .verify-unknown-action {
  color: var(--fg);
  text-decoration: none;
  border-bottom: 1px solid var(--bd);
  transition: color 0.2s, border-bottom-color 0.2s;
}
.verify-page .verify-unknown-action:hover,
.verify-page .verify-unknown-action:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
  outline: 0;
}
.verify-page .verify-unknown-actions-sep {
  color: var(--fg3);
  margin: 0 0.4em;
}

/* legacy reset
   the .verify-page section rule above is the load-bearing fix. these
   selectors are retained for any future contributor who reintroduces
   the older div-based render. */
#verify-root,
#verify-root > div,
.verify-command {
  padding: 0;
  border: 0;
}

.verify-root { margin-top: 1rem; }

/* ─────────────────────────────────────────────────────────────
   /integrity/ — the signed-release hub
   ─────────────────────────────────────────────────────────────
   same editorial dna as the /verify/ page record card: warm paper
   background, large serif headline, mono labels, hairline rules,
   oxblood only as accent on hover. the card is the central object;
   the command block is collapsed by default below it; history and
   related sit as quiet closing utilities.                         */

.integrity-record-card {
  background: var(--surface-card);
  border: 1px solid var(--rule);
  /* phase 65 · padding tightened further (1.2-3.4vw-1.7 → 1.1-2.6vw-1.4)
     so the card reads as a compact signed-release record rather
     than a spaced ui card. max-width 52rem holds; mobile fills the
     available width. */
  border-radius: var(--radius-soft);
  width: 100%;
  max-width: min(100%, 52rem);
  min-width: 0;
  padding: clamp(1.1rem, 2.6vw, 1.4rem) clamp(1.2rem, 3.2vw, 1.6rem);
  margin: clamp(20px, 3vw, 30px) 0 clamp(16px, 2.2vw, 22px);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.035);
}
.integrity-record-kicker {
  font-family: var(--mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg3);
  margin: 0 0 0.55rem;
}
.integrity-record-title {
  font-family: var(--serif);
  font-size: clamp(26px, 4vw, 34px);
  font-weight: 300;
  letter-spacing: -0.012em;
  line-height: 1.12;
  color: var(--fg);
  margin: 0 0 0.4rem;
}
.integrity-record-status {
  /* status line now reads as a quiet structural caption rather
     than a mono tag — sans, small, fg3, no uppercase tracking. */
  font-family: var(--sans);
  font-size: 0.82rem;
  letter-spacing: 0.005em;
  color: var(--fg3);
  margin: 0 0 clamp(14px, 2vw, 20px);
}

.integrity-record-dl  { margin: 0; padding: 0; }

/* section grouping inside the signed-release card · verification
   records / source archives / release fingerprint. groups read as
   one composed ledger. one quiet hairline only between the first
   group (verification records) and the closing two (source archives
   + release fingerprint, which sit close together as one composed
   release block). label + whitespace alone separate the closing
   two. */
/* phase 80 · integrity record card · uniform vertical rhythm.
   the card is a grid; one consistent gap between sections; sections
   after the first carry a hairline rule + small padding-top, so
   the seam is even regardless of which group sits where. no
   margin-block cascades, no align-content distribution, no per-
   group height. */
.integrity-record-card {
  display: grid;
  gap: clamp(1.1rem, 2vw, 1.5rem);
  align-content: start;
}
.integrity-record-card > .integrity-record-group {
  margin: 0;
  padding: 0;
}
/* phase 82 · one archive, one card, no internal rules.
   the grid `gap` on .integrity-record-card already gives clear
   section separation; the previous border-top seam read as a
   table divider and fragmented the card. spacing + typography
   carry the hierarchy now. */
.integrity-record-card > .integrity-record-group + .integrity-record-group {
  padding-top: 0;
  border-top: 0;
}
.integrity-record-card > .integrity-record-group--fingerprint {
  padding-top: 0;
}
.integrity-record-group-label {
  /* quieter structural label · sans, small, no uppercase tracking.
     the records below carry the weight; the label only marks the
     section boundary. */
  font-family: var(--sans);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--fg3);
  margin: 0 0 0.45rem;
}
.integrity-record-archives {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  color: var(--fg);
}
.integrity-record-group--fingerprint .copy-fingerprint {
  display: inline-block;
  margin-block-start: 0.6rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  color: var(--fg3);
  background: transparent;
  border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--bd) 50%, transparent);
  padding: 0.1rem 0;
  cursor: pointer;
  transition: color .2s, border-color .2s;
}
.integrity-record-group--fingerprint .copy-fingerprint:hover,
.integrity-record-group--fingerprint .copy-fingerprint:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-border);
  outline: 0;
}

/* phase 47 · record-row rhythm tightened so the rows read as one
   ledger system, not a spaced list. Phase-45 inter-row 2rem margin
   retired; the symmetric padding-block + ruled border now carries
   the inter-row separation. fingerprint row gets a touch more
   breathing room via .integrity-rg--fingerprint below.
   phase 66 · rows now read as one compact table: tight symmetric
   padding-block + one quiet hairline between rows, suppressed on
   the last row so the section closes flush against the next group
   seam — no doubled rule, no accidental stack. */
.integrity-rg {
  display: grid;
  padding-block: 0.42rem;
  /* phase 82 · no inter-row rule. the dl reads as a single ledger
     held together by tight padding + the subgrid alignment;
     borders fragmented it into a table. */
  border-bottom: 0;
  gap: 0.15rem;
}
.integrity-rg:first-child {
  padding-top: 0;
}
.integrity-rg:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}
.integrity-rg--ruled {
  border-top: 1px solid var(--bd);
}
.integrity-rg--fingerprint {
  padding-block: 1.05rem 0.95rem;
}
.integrity-rg--fingerprint .integrity-rg-label {
  display: block;
}
/* span exists only so the copy button can sit as a non-uppercased
   sibling on the same flex row — inherits label register. */
.integrity-rg-label-text {
  display: inline-block;
}

.integrity-rg-label {
  /* phase 64 · labels move from uppercase mono to quiet sans so
     the file paths below dominate the row. no uppercase, no
     letter-spacing, no overshadowing of the path. */
  font-family: var(--sans);
  font-size: 0.78rem;
  font-weight: 400;
  letter-spacing: 0.005em;
  text-transform: none;
  color: var(--fg2);
  margin: 0 0 0.15rem;
}
.integrity-rg-value {
  font-family: var(--mono);
  font-size: 0.82rem;
  /* phase 47/57 · ledger line-height tightened progressively:
     1.55 → 1.35 → 1.28 so metadata reads as a dense archival
     ledger. */
  line-height: 1.28;
  color: var(--fg);
  margin: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.integrity-rg-desc {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--fg2);
  /* phase 47 · note sits closer to the value above + tighter
     internal leading. */
  margin: 0.18rem 0 0;
  line-height: 1.25;
}
.integrity-rg-link {
  /* phase 85 · link affordance changed from a full-width border-bottom
     to a text-width underline. across four .integrity-rg rows the
     border-bottoms read as stacked horizontal dividers cutting the
     card into segments; text-decoration: underline confines the
     affordance to the actual link text width and the card reads as
     one continuous record again. */
  color: var(--fg);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--bd) 60%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color 0.2s, text-decoration-color 0.2s;
  word-break: break-all;
}
.integrity-rg-link:hover,
.integrity-rg-link:focus-visible {
  color: var(--accent-text);
  text-decoration-color: var(--accent-text);
  outline: 0;
}
.integrity-rg-sep {
  display: inline-block;
  margin: 0 0.4em;
  color: var(--fg3);
}
/* Cross-row alignment via subgrid at desktop breakpoints. below 720px
   the label/value/description stack as before. at ≥720px with subgrid
   support, the label sits left and the value+description stack on the
   right, with column edges aligned across every record. browsers
   without subgrid keep the stacked layout — no visual regression. */
@media (min-width: 768px) {
  @supports (grid-template-columns: subgrid) {
    .integrity-record-dl {
      display: grid;
      grid-template-columns: 180px max-content;
      column-gap: 1.4rem;
    }
    .integrity-rg {
      display: grid;
      grid-template-columns: subgrid;
      grid-column: 1 / -1;
      align-items: baseline;
    }
    .integrity-rg-label {
      grid-column: 1;
      align-self: baseline;
      margin-bottom: 0;
    }
    .integrity-rg-value,
    .integrity-rg-desc {
      grid-column: 2;
    }
  }

  .principle {
    grid-template-columns: 1fr 1fr;
    gap: clamp(24px, 4vw, 80px);
    align-items: baseline;
  }

  .project-card { padding: clamp(48px, 5vw, 72px); }

  .page-lede   { font-size: clamp(24px, 2.6vw, 30px); max-width: 50ch; }
  .page-body p { font-size: clamp(18px, 2vw, 21px); line-height: 1.65; max-width: 62ch; }
}

/* phase 70 · tablet card shrink.
   on ipad portrait (~820) the 52rem max-width pulls the integrity
   card close to full width while the subgrid value column is much
   shorter than the label/value pair, producing asymmetric whitespace
   that reads as an empty lower half. the same applies to the verify
   page record card. constrain to 38rem and freeze the padding clamp
   so the card sits as a compact archival object, not a stretched
   panel. desktop (>=1100) keeps the existing 52rem ceiling. */
@media (min-width: 760px) and (max-width: 1099.98px) {
  .integrity-record-card {
    max-width: 38rem;
    padding: 1.15rem 1.35rem;
    margin-block: clamp(18px, 2.4vw, 24px);
  }
  .integrity-record-dl { row-gap: 0.1rem; }
  .integrity-rg        { padding-block: 0.38rem; }

  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card {
    max-width: 38rem;
    padding: 1.15rem 1.35rem;
    margin-block-start: clamp(1.8rem, 3vw, 2.4rem);
    margin-block-end: clamp(1.6rem, 3.5vw, 2.6rem);
  }
}
.fingerprint-grid {
  /* fingerprint composes as five non-breaking 9-char chunks. on
     desktop and roomy mobile they group as 3+2 (preferred); on
     narrow mobile they fall to 2+2+1 — never one long unbroken
     string, never tiny scaling, never clipped. ceremonial line-
     height keeps the geometry breathing. */
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.55rem clamp(0.9rem, 3.5vw, 1.4rem);
  padding-block: 0.3rem;
  max-width: 100%;
  font-family: var(--mono);
  font-size: clamp(0.95rem, 2.6vw, 1.1rem);
  line-height: 1.45;
  letter-spacing: 0.02em;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  font-variant-ligatures: none;
  background: transparent;
  overflow-wrap: normal;
  word-break: normal;
}
.fingerprint-grid span {
  white-space: nowrap;
  flex: 0 0 auto;
}
.record-action-row {
  margin-block: 0.25rem 0.75rem;
}
.text-action {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  text-decoration: underline;
  text-underline-offset: 0.18em;
}
/* phase 66 · legacy .fingerprint-section ruleset retired —
   /integrity/ uses .integrity-record-group--fingerprint, and the
   group-level rule already governs spacing without a top rule. */

.integrity-record-actions {
  border-top: 1px solid var(--rule);
  /* phase 47 · tighter rhythm between the last record row, the
     hairline, and the actions strip below.
     phase 57 · wrapper register aligned with .record-inline-action
     so the separator spans + child anchors all read at one
     typographic level (0.58rem / 0.055em).
     phase 59 · vertical rhythm tightened so the strip sits as a
     close archival footer to the data rows above, matching the
     verify utility rail's compact register (phase 58). */
  padding-top: 0.55rem;
  padding-bottom: 0.14rem;
  margin: 0.55rem 0 0;
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.055em;
  color: var(--fg3);
  line-height: 1.4;
  /* flex + gap, no inline " · " separators — a wrapped action
     can never orphan a bullet at the start of a new line. */
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.4rem;
}
/* shared utility-action register — copy citation, copy fingerprint,
   open mirror, view manifest, view releases all read as one family. */
.integrity-record-action {
  appearance: none;
  font: inherit;
  letter-spacing: inherit;
  color: var(--fg2);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--bd);
  padding: 0;
  cursor: pointer;
  text-decoration: none;
  text-transform: none;
  transition: color 0.2s, border-bottom-color 0.2s;
}
.integrity-record-action:hover,
.integrity-record-action:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
  outline: 0;
}
.integrity-record-action[data-state="copied"] {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}
/* legacy separator class kept for any cached html that still emits
   it; a hidden display rule means a stray <span> never paints a
   bullet. new html omits the separators entirely. */
.integrity-record-actions-sep { display: none; }

/* compact inline strip variant — download zip · TAR.GZ · view
   manifest · view releases. Single-line on desktop with explicit
   middots between items (so a wrap shows the structure clearly);
   on mobile, keeps the inline flow rather than stacking, so the
   row reads as one calm strip even at 320px. restrained underline
   weight (uses --rule, not --bd) keeps the row quieter than the
   data-bearing dl above it. no top rule on the strip itself —
   .integrity-record-copy above it carries the single rule that
   separates fingerprint hex from the grouped action area. */
.integrity-record-actions.integrity-record-actions--strip {
  display: block;
  margin: 0.25rem 0 0;
  font-size: 0.66rem;
  letter-spacing: 0.02em;
  line-height: 1.7;
}
.integrity-record-actions--strip .integrity-record-action {
  color: var(--fg3);
  border-bottom-color: var(--rule);
}
.integrity-record-actions--strip .integrity-record-action:hover,
.integrity-record-actions--strip .integrity-record-action:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}
.integrity-record-actions--strip .integrity-record-actions-sep {
  display: inline;
  margin: 0 0.45em;
  color: var(--fg3);
  border: 0;
}
/* two grouped lines inside the strip — download zip · TAR.GZ on one
   line, view manifest · view releases on another. the grouping by
   purpose (downloads vs nav) gives the strip an intentional rhythm
   instead of a single long string of accidental footnotes. */
.integrity-record-actions--strip .integrity-record-actions-line {
  display: block;
}
.integrity-record-actions--strip .integrity-record-actions-line + .integrity-record-actions-line {
  margin-top: 0.2rem;
}

/* phase 49 · the standalone .integrity-record-copy strip was retired.
   the copy action now sits inline on the fingerprint row's <dt>
   header (see .integrity-rg--fingerprint .integrity-rg-label above
   and the .copy-fingerprint rule further down). */

/* Page-level note linking out to /verify/. a quiet bridge — sits
   close to the disclosure above so it reads as a footnote to local
   verification, not a fresh body paragraph. */
.integrity-page-level-note {
  font-family: var(--mono);
  font-size: 0.64rem;
  letter-spacing: 0.03em;
  color: var(--fg3);
  margin: clamp(6px, 1vw, 10px) 0 clamp(18px, 2.6vw, 24px);
  line-height: 1.55;
}
.integrity-page-level-note a {
  color: var(--fg);
  border-bottom: 1px solid var(--bd);
  text-decoration: none;
  transition: color 0.2s, border-bottom-color 0.2s;
}
.integrity-page-level-note a:hover,
.integrity-page-level-note a:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
  outline: 0;
}

/* history — small section heading + body + link, no hairlines.
   tighter top margin so the page reads as a continuous editorial
   thread, not three separate modules stacked. mobile reduces ~20%
   so the long card+disclosure+history stack feels finished, not
   bottom-heavy. */
.integrity-history {
  /* phase 57 · top margin further compressed ~15% so the history
     continuation sits as a soft transition, not a section break. */
  margin: clamp(0.9rem, 2.5vw, 1.7rem) 0 clamp(10px, 1.6vw, 16px);
}
.integrity-history-heading {
  /* phase 56 · history label drops to the section-kicker register —
     smaller, wider tracking so the label reads as an editorial
     section marker rather than a competing heading.
     opacity reduction retired — at 0.58rem on this token the blend
     would fall below aa contrast (lighthouse remediation). recede
     now comes from size + tracking + --fg3 alone. */
  font-family: var(--mono);
  font-size: 0.58rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg3);
  margin: 0 0 0.35rem;
}
.integrity-history-body {
  font-family: var(--serif);
  font-size: clamp(15px, 1.5vw, 17px);
  font-weight: 300;
  color: var(--fg);
  line-height: 1.55;
  /* phase 50 · symmetric block margin pulls the body closer to
     heading above and link below — heading, body, link read as
     one compact archival entry. */
  margin: 0 0 0.45rem;
}
.integrity-history-link {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--fg);
  text-decoration: none;
  border-bottom: 1px solid var(--bd);
  transition: color 0.2s, border-bottom-color 0.2s;
}
.integrity-history-link:hover,
.integrity-history-link:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
  outline: 0;
}

/* ── shared command component (used on /verify/ and /integrity/) ──
   header strip (title left + copy button right) sits above the <pre>
   so the button never overlaps the command. Mobile-safe wrapping. */

.verify-command {
  margin: 0 0 1.6rem;
  max-width: 100%;
}

.verify-command:last-of-type {
  margin-bottom: 0.6rem;
}

.verify-command-copy {
  appearance: none;
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--fg3);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--bd, #D8D4CC);
  border-radius: 0;
  padding: 0.15rem 0;
  cursor: pointer;
  transition: color 0.2s, border-bottom-color 0.2s;
  text-transform: none;
  line-height: 1.3;
}

.verify-command-copy:hover,
.verify-command-copy:focus-visible {
  color: var(--ac, #6E1A14);
  border-bottom-color: var(--ac, #6E1A14);
  outline: 0;
}

.verify-command-copy[data-state="copied"] {
  color: var(--ac, #6E1A14);
  border-bottom-color: var(--ac, #6E1A14);
}

.verify-unknown-path {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--fg2);
  margin: 0 0 0.8rem;
  word-break: break-all;
}

/* verify
   Page-level record card for citation, source mirror, fingerprint and
   archived release. hero, white record card, quiet record selector,
   back link. no command blocks; release verification lives on
   /integrity/. */

/* section heading shared across verify-* sub-sections. editorial
   signifier, same family as the privacy/integrity/security subheads
   so /verify/ feels native to that family. */
.verify-section-heading {
  font-family: var(--serif);
  font-size: clamp(18px, 2.2vw, 22px);
  font-weight: 300;
  letter-spacing: -0.012em;
  line-height: 1.2;
  color: var(--fg);
  margin: 0 0 0.5rem;
  text-transform: none;
}
.verify-section-heading--small {
  font-size: clamp(15px, 1.7vw, 17px);
}

/* short serif intro paragraph that sits below a section heading. */
.verify-section-intro {
  font-family: var(--serif);
  font-size: clamp(14px, 1.5vw, 16px);
  font-weight: 300;
  line-height: 1.5;
  color: var(--fg2);
  margin: 0 0 1.1rem;
  max-width: 64ch;
}

/* ── Section: this page ──
   editorial public record. no per-row borders , let typography and
   spacing carry the rhythm. identity rows then evidence rows; combined
   file row; page fingerprint with inline copy button. status strip
   above the list replaces the standalone verification chain section. */
/* "route not in map" calm notice (renderunknown). */
.verify-unknown {
  margin: 0 0 1.8rem;
}

/* accessibility */

/* subtle highlight for keyboard-navigated sections */
:target {
  outline-offset: 4px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 999;
  font-family: var(--mono);
  font-size: 12px;
  padding: 8px 16px;
  background: var(--fg);
  color: var(--bg);
  text-decoration: none;
}

.skip-link:focus {
  left: 8px;
  top: 8px;
}

/* legacy :focus-visible rule replaced by the editorial focus-visible
   system in @layer base above. keep .skip-link's focus override
   only — it needs to remain prominent so keyboard users can spot
   the bypass. */

:focus:not(:focus-visible) {
  outline: none;
}

/* layout */

.site {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 clamp(24px, 5vw, 80px);
}

/* Safe-area bottom on the main content surface so the last in-flow cta
   (e.g. the homepage `view project` button) never sits beneath ios
   safari's bottom chrome when the user has not yet scrolled to retract
   it. the footer carries its own safe-area-inset-bottom via .footer. */
main.site {
  padding-bottom: env(safe-area-inset-bottom);
}

section {
  padding: clamp(80px, 12vh, 160px) 0;
  border-bottom: 1px solid var(--bd);
}

section:last-of-type {
  border-bottom: none;
}

/* hero → approach divider · the hero is a <header>, so the generic
   section { border-bottom } rule never paints a rule below it. add
   a top hairline on the first section so the hero-to-approach
   transition reads as resolved as the section-to-section breaks
   that follow. matched to the same --bd token + clamp padding the
   rest of the spine uses. */
.home-profile > section:first-of-type {
  border-top: 1px solid var(--bd);
}

/* nav
   ── the nav family lives inside @layer components so the homepage
   disclosure rules in @layer pages (later in cascade order) can win
   normally for the open-state layout (display:grid · text-align:right)
   without needing !important. per css cascade layers level 5, normal
   declarations cascade later-layer-wins; !important declarations
   cascade earlier-layer-wins, with unlayered author rules sitting
   above all layered author rules. leaving these rules unlayered would
   silently override @layer pages every time. ── */
@layer components {
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  /* paint the same paper-noise + corner vignette as body so the
     viewport-fixed nav band shares its texture register with the
     page beneath it. background-color stays opaque so scrolling
     content underneath remains hidden; the texture layer sits on
     top, registered to the same 260px tile + same fixed attachment
     as body — the two surfaces meet at the hairline rule with no
     seam visible. */
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 120% 90% at 50% 50%, transparent 55%,
                    color-mix(in srgb, var(--ink) 4%, transparent) 100%),
    url('/images/textures/paper-noise.svg');
  background-attachment: fixed, fixed;
  background-size: auto, 260px 260px;
  background-repeat: no-repeat, repeat;
  border-bottom: 1px solid var(--bd);
  /* Safe-area top padding so the nav row sits below the ios notch /
     dynamic island in `viewport-fit=cover` mode on non-home pages.
     homepage @layer pages override (mobile) sets its own padding-
     block via env(safe-area-inset-top) — that wins via cascade. */
  padding-top: var(--safe-top);
}

/* touch devices · drop the radial-gradient vignette on body and nav
   in both light and dark mode. previously the dark-mode override
   alone retired the radial — but ios safari's "full page" screenshot
   capture renders viewport-fixed backgrounds against the entire
   document height, which stretches the 120% × 90% ellipse into a
   long dark vertical cone running the full length of the captured
   image. the paper-noise texture is tile-based (260px × 260px,
   scroll attachment) and survives full-page capture intact, so it
   alone carries the surface depth on a phone-sized canvas. desktop
   mouse-pointer users still get the vignette via the outer rule
   above; this query only matches coarse-pointer devices. */
@media (hover: none) and (pointer: coarse) {
  body,
  .nav {
    background-image: url('/images/textures/paper-noise.svg');
    background-size: 260px 260px;
    background-repeat: repeat;
    background-attachment: scroll;
  }
}

.nav-inner {
  max-width: var(--page-max);
  margin: 0 auto;
  /* horizontal safe-area padding: the inset-left/right tokens are
     non-zero on landscape iphone (notch on one side); max() with the
     existing clamp() keeps the editorial register on every other
     orientation while preventing the nav from slipping under the
     notch in landscape. */
  padding-block: 0;
  padding-inline-start: max(clamp(24px, 5vw, 80px), var(--safe-left));
  padding-inline-end:   max(clamp(24px, 5vw, 80px), var(--safe-right));
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* phase 40 · 52 → 54px gives a touch more vertical breathing
     so the header reads as deliberate rather than utilitarian.
     within the 60px --nav-offset buffer, anchor scrolling stays
     correct. */
  height: var(--nav-height);
}

/* the wordmark is identity · söhne mono carries the archival voice.
   phase 40 · tracking 0.10 → 0.09em pulls the wordmark slightly
   tighter so it reads as set type rather than spread-out caps. */
.nav-mark {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fg);
  text-decoration: none;
}

/* nav-toggle / nav-links / nav-toggle-glyph / nav-toggle-label —
   retired site-wide. the masthead is the only header element now;
   the homepage scrolls naturally and discovery is by reading. */

/* narrow phones: tighten the masthead horizontally so the stacked
   trent / power lockup sits cleanly against the page edge. */
@media (max-width: 600px) {
  .nav-inner {
    padding-inline: 15px;
    height: 58px;
    align-items: center;
  }
  .nav-mark {
    display: block;
    max-width: 4.55rem;
    white-space: normal;
    line-height: 1.08;
    font-size: 11px;
    letter-spacing: 0.09em;
  }
  .nav-mark span { display: block; }

  .source-entry {
    grid-template-columns: 1fr;
    row-gap: 0.35rem;
    padding-block: 0.95rem;
  }
  .source-entry-meta {
    justify-self: start;
    text-align: left;
  }
  .source-entry-desc {
    max-width: none;
  }
}
@media (max-width: 390px) {
  .nav-inner {
    padding-inline: 13px;
  }
  .nav-mark {
    max-width: 4.35rem;
    font-size: 10.5px;
    letter-spacing: 0.08em;
  }
}

}
/* end @layer components — masthead. */

/* section 1 · hero / positioning */

.hero {
  min-height: 100vh;
  min-height: 100svh; /* ios safari ≥15.4 retracts chrome via the small viewport unit */
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* clears the fixed nav at every width and gives the hero statement
     breathing room now that the trent-power eyelid above it is gone. */
  padding-top: clamp(80px, 10vh, 110px);
  padding-bottom: clamp(60px, 10vh, 120px);
  /* no border-bottom: the trust-mark sits inside the hero in dom
     flow after .hero-body, so a 1px line here would cut between
     hero text and the trust mark. section-to-section separation
     comes from the `section { border-bottom }` rule on
     .section-approach below. */
}


.hero-statement {
  font-family: var(--serif);
  font-size: clamp(42px, 10vw, 82px);
  font-weight: 300;
  line-height: 0.98;
  letter-spacing: -0.035em;
  color: var(--fg);
  max-width: var(--hero-statement-max);
  margin-bottom: clamp(32px, 5vh, 56px);
}
/* the homepage hero breaks on authored <br>s into a stacked four-line
   composition; nowrap holds each line so only the manual breaks land.
   below 480px the manual breaks relax to soft wrapping so a narrow
   viewport never overflows. */
.hero-statement--broken {
  max-width: 14ch;
  text-wrap: nowrap;
  line-height: 0.92;
}
@media (max-width: 480px) {
  .hero-statement--broken {
    text-wrap: pretty;
    max-width: none;
  }
}
/* the highlighted phrase is italic — a real italic signifier face is
   registered in @layer fonts; font-synthesis: style lets the browser
   slant a fallback if that face has not yet loaded. the existing
   accent highlight background is untouched. */
.hero-statement mark {
  font-style: italic;
  font-synthesis: style;
}

.hero-body {
  font-family: var(--serif);
  font-size: clamp(15px, 1.6vw, 19px);
  font-weight: 300;
  line-height: 1.65;
  color: var(--fg2);
  /* phase 40 · ~5% wider measure (580 → 610px). on mobile this
     gives the hero paragraph fewer awkward short-line wraps; on
     desktop it stays well below the 720px page-title cap so the
     editorial hierarchy is preserved. */
  max-width: var(--hero-body-max);
}

/* hero · name eyelid above the statement. quiet mono register so it
   names the entity without competing with the display line. */
.hero-kicker {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg3);
  margin: 0 0 clamp(0.9rem, 2vh, 1.4rem);
  font-variant-numeric: tabular-nums lining-nums;
}

/* hero entrance · single reveal contract (phase 37)
   ──────────────────────────────────────────────────────────
   default: hero pieces start invisible and lifted. when js is
   off (no .js class on <html>), the override below paints them
   at rest immediately so no-JS visitors see the hero. when js
   is on and html.enhanced has been added (after DOMContentLoaded
   in app.js), each piece animates up in sequence with the trust
   mark participating as the last beat. reduced-motion users see
   the resting state regardless. */
/* hero reveal · two-phase gate. html.js is set pre-paint by the
   appearance bootstrap and hides the hero so a fonts-driven reflow
   never lands inside the reveal. the animation only starts once
   html.fonts-ready is set (by /js/fonts.js, after document.fonts.ready
   resolves or a 1.5s safety timeout). by then the metric-stable
   italic for "growth systems" / "systèmes de croissance" is committed,
   so the reveal plays from a fully-composed line. when js is disabled
   the hide rule never matches and the hero paints resting. */
html.js .hero-statement,
html.js .hero-body {
  opacity: 0;
  transform: translateY(14px);
}
html.js.fonts-ready .hero-statement {
  animation: revealUp 800ms cubic-bezier(.2,.7,.2,1) 90ms forwards;
}
html.js.fonts-ready .hero-body {
  animation: revealUp 850ms cubic-bezier(.2,.7,.2,1) 180ms forwards;
}
/* hero-static · arrival from the language gate.
   the gate-background already plays the reveal behind the scrim,
   so when the visitor dismisses the gate the destination should
   paint the hero AT REST instead of repeating the animation.
   language-gate.js writes sessionStorage.tp-skip-hero-anim before
   navigating; the bootstrap inline <script> in <head> reads &
   removes it pre-paint and adds html.hero-static.
   direct visitors (no gate handoff) carry no flag → full reveal.
   higher specificity (.js.hero-static) than the .js / .js.fonts-ready
   rules above so this wins without !important. */
html.js.hero-static .hero-statement,
html.js.hero-static .hero-body {
  opacity: 1;
  transform: none;
  animation: none;
}
/* hero entrance keyframe · animates from the initial-hidden state
   set above (opacity: 0; transform: translateY(14px)) to resting.
   referenced by each of the four hero pieces with a staggered delay. */
@keyframes revealUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
@media (prefers-reduced-motion: reduce) {
  .hero-statement,
  .hero-body {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }

  .trajectory-item { animation: none; transition: none; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;   /* keep: wcag 2.3.3 motion-reduction override */
    transition-duration: 0.01ms !important;  /* keep: wcag 2.3.3 motion-reduction override */
  }

  .hero-statement,
  .hero-body,
  .principle,
  .trajectory-item,
  .project-card,
  .js .principle,
  .js .trajectory-item,
  .js .project-card {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .reader-view-mode,.source-section-body,.section-divider-gloss,.inspection-path-link,.code-line,.line-code,.code-line--range-active {
    transition: none !important;
  }
}

/* section labels */

/* section labels render in söhne mono. söhne mono does not ship `smcp` /
   `c2sc` opentype features in this licence cut, so we use measured css
   uppercase + restrained tracking rather than faux small caps. */
.section-label {
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--fg3);
  margin-bottom: clamp(40px, 6vh, 72px);
}

/* the single static oxblood mark on the page: the section index numeral. */
.section-label .label-num {
  color: var(--accent-text);
  margin-right: 8px;
}

/* homepage section labels — the hairline rule sits directly under each
   label ("02 approach", "06 contact" …) rather than at the foot of the
   whole section, so each block opens on its own clear register. the
   section-level borders are dropped in favour of this. */
.home-profile .section-label {
  padding-bottom: 16px;
  border-bottom: 1px solid var(--rule-default);
  margin-bottom: clamp(28px, 4vh, 48px);
  display: flex;
  align-items: baseline;
  gap: 14px;
}
.home-profile .section-label .label-num {
  margin-right: 0;
}
.home-profile > section {
  border-bottom: 0;
}
.home-profile > section:first-of-type {
  border-top: 0;
}

/* section 2 · focus & approach */

.principles {
  display: grid;
  gap: 0;
  list-style: none;
}

/* principles, trajectory chapters, and project cards are visible by default
   so the page renders correctly without javascript. the `.js` class
   (added by the inline language script before first paint) opts into the
   scroll-reveal behaviour. */
.principle {
  padding: clamp(28px, 4vh, 48px) 0;
  border-bottom: 1px solid var(--bd-soft);
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}

.js .principle {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

/* phase 91 · reveal-gate fix: the shown state requires fonts-ready
   so the IntersectionObserver cannot animate during the full-font
   swap. hidden state stays on .js (no-JS users have no .js class and
   so paint resting). */
.js.fonts-ready .principle.visible {
  opacity: 1;
  transform: translateY(0);
}

.principle:last-child {
  border-bottom: none;
}

.principle-title {
  font-family: var(--serif);
  font-size: clamp(22px, 3vw, 30px);
  font-weight: 400;
  line-height: 1.2;
  color: var(--fg);
  letter-spacing: -0.018em;
}

.principle-body {
  font-family: var(--serif);
  font-size: clamp(14px, 1.4vw, 17px);
  font-weight: 300;
  line-height: 1.6;
  color: var(--fg2);
  max-width: 560px;
}


/* section 3 · trajectory */

.trajectory-grid {
  display: grid;
  gap: 0;
}

/* trajectory · scroll-reveal initial state. the IntersectionObserver
   in app.js promotes each .trajectory-item to .visible as it enters
   the viewport. gating on .js means the items are visible by default
   when scripting is unavailable — no orphaned hidden cards. */
.js .trajectory-item {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
/* phase 91 · reveal-gate fix: shown state requires fonts-ready. */
.js.fonts-ready .trajectory-item.visible {
  opacity: 1;
  transform: translateY(0);
}

/* legacy .chapter-* typography retired — the new .trajectory-item /
   .trajectory-kicker / .trajectory-title / .trajectory-detail rules
   in the trajectory chronology block downstream now carry the same
   register. anchors to #clienteling-definition still resolve to the
   <dfn> in the principles list above. */

/* 03 credentials · quiet editorial bridge between approach (02) and
   trajectory (04). compact two-entry block; smaller serif than the
   principles list, no per-item rule, no card. visually calmer so it
   reads as formation context rather than a cv module. tokens only —
   inherits dark-mode colours from :root[data-theme="dark"]. */

.section-credentials {
  padding: clamp(56px, 8vh, 112px) 0;
  border-bottom: 1px solid var(--bd);
}

.credentials-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 5vw, 4.5rem);
}

.credential-item {
  display: grid;
  gap: 0.45rem;
}

.credential-title {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(18px, 2vw, 22px);
  line-height: 1.18;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--fg);
}

.credential-detail {
  margin: 0;
  max-width: 26rem;
  font-family: var(--serif);
  font-size: clamp(14px, 1.4vw, 16px);
  font-weight: 300;
  line-height: 1.6;
  color: var(--fg2);
}

@media (max-width: 700px) {
  .credentials-list {
    grid-template-columns: 1fr;
    gap: 1.75rem;
  }
}

/* trajectory · resilient editorial chronology. three breakpoint
   modes share one dom — a vertical archival chronology under 760 px,
   a two-column editorial grid at 760–1099 (label + rail on the left,
   items stacked 2×2 on the right), and a four-column horizontal
   timeline at 1100 and above with a rail running across the top of
   each column. all layout sits inside css grid + flex; no viewport-
   percentage anchoring, no transform hacks, no per-card absolute
   placement. custom properties handle the few tunable knobs. */

.section-trajectory {
  --trajectory-line:    var(--bd-soft);
  --trajectory-marker:  var(--fg3);
  --trajectory-current: var(--accent);
  --trajectory-muted:   var(--fg3);
  --trajectory-gap:     clamp(1.5rem, 3.5vw, 4rem);
  --trajectory-card-max: 22rem;
  /* phase 82 · deliberate compact-vs-current hierarchy.
     1997 / 2004 / 2017 share the compact min-height; the current
     card sits ~14% taller so it reads as an intentional editorial
     emphasis on the active role, not an accidental content-size
     stretch. mobile keeps intrinsic content sizing (no min-height
     applied below 760 px). */
  --trajectory-card-min:         clamp(12rem, 14vw, 14rem);
  --trajectory-card-current-min: clamp(14rem, 16vw, 16rem);
}

.trajectory-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.trajectory-item {
  min-width: 0;
  position: relative;
  /* belt-and-braces marker reset · ios safari occasionally paints
     decimal markers even when the parent ol has list-style: none.
     setting list-style on the li and emptying ::marker kills the
     "1. 2. 3." numbering for good. */
  list-style: none;
}
.trajectory-item::marker {
  content: "";
}

.trajectory-year {
  display: block;
  font: 500 11px/1.2 var(--mono);
  letter-spacing: 0.04em;
  color: var(--trajectory-muted);
  font-variant-numeric: tabular-nums lining-nums;
  margin: 0;
}
.trajectory-item--current .trajectory-year {
  color: var(--trajectory-current);
  font-weight: 500;
}

.trajectory-marker {
  display: block;
  width: 0.55rem;
  height: 0.55rem;
  border: 1px solid var(--trajectory-marker);
  border-radius: 999px;
  background: var(--bg);
  box-sizing: border-box;
}
.trajectory-item--current .trajectory-marker {
  width: 0.7rem;
  height: 0.7rem;
  background: var(--trajectory-current);
  border-color: var(--trajectory-current);
}

.trajectory-card {
  /* override the .card base for editorial restraint: drop the harsh
     hairline border, lift onto the warmer paper-raised-high surface,
     restore the card's internal padding so content breathes inside
     the rule. each card reads as an editorial record, not a ui tile. */
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  max-width: var(--trajectory-card-max);
  margin: 0;
  padding: clamp(1rem, 2.5vw, 1.35rem);
  background: var(--paper-raised-high);
  border: 0;
  border-radius: var(--radius-soft);
}

.trajectory-kicker {
  font: 500 10px/1.1 var(--mono);
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--trajectory-muted);
  margin: 0;
}

.trajectory-title {
  font: 400 clamp(15px, 1.4vw, 17px)/1.3 var(--serif);
  letter-spacing: -0.012em;
  color: var(--fg);
  margin: 0;
  text-wrap: balance;
}

/* quiet internal link inside trajectory cards. used only to point
   the first occurrence of "clienteling" at the canonical homepage
   definition. inherits its colour from the surrounding serif title;
   a hairline underline keeps the link discoverable without louder
   visual weight. accent on hover/focus matches the rest of the site
   inline-link grammar. */
.trajectory-card a[href*="clienteling-definition"] {
  color: inherit;
  text-decoration-line: underline;
  text-decoration-thickness: 0.06em;
  text-underline-offset: 0.16em;
  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
  transition: color .2s, text-decoration-color .2s;
}
.trajectory-card a[href*="clienteling-definition"]:hover,
.trajectory-card a[href*="clienteling-definition"]:focus-visible {
  color: var(--accent);
  text-decoration-color: var(--accent);
  outline: 0;
}

.trajectory-org {
  font: 500 10px/1.2 var(--mono);
  letter-spacing: 0.06em;
  text-transform: none;
  color: var(--trajectory-current);
  margin: 0;
}

.trajectory-detail {
  font: 300 clamp(13px, 1vw, 14px)/1.55 var(--serif);
  color: var(--fg2);
  margin: 0;
}

.trajectory-period {
  font: 500 9.5px/1.4 var(--mono);
  letter-spacing: 0.06em;
  color: var(--trajectory-muted);
  font-variant-numeric: tabular-nums lining-nums;
  margin: 2px 0 0;
  white-space: nowrap;
}
.trajectory-period time { font: inherit; color: inherit; }

/* ── mobile · vertical archival chronology ───────────────────
   rail is a left border on the list; markers pull into the gutter
   over the rail. a subtle border-top separates each row. spacing
   is generous but never oversized. */
@media (max-width: 759.98px) {
  .trajectory-list {
    margin: clamp(2.4rem, 9vw, 3.6rem) 0 0;
    padding: 0 0 0 1.25rem;
    border-left: 1px solid var(--trajectory-line);
  }
  .trajectory-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.5rem;
    padding: 0 0 clamp(1.6rem, 6vw, 2.2rem) clamp(1rem, 3.5vw, 1.4rem);
  }
  .trajectory-item + .trajectory-item {
    border-top: 1px solid var(--trajectory-line);
    padding-top: clamp(1.6rem, 6vw, 2.2rem);
  }
  .trajectory-year {
    order: 1;
  }
  .trajectory-marker {
    position: absolute;
    top: clamp(1.6rem, 6vw, 2.2rem);
    left: calc(-1.25rem - 0.275rem);
  }
  .trajectory-item:first-child .trajectory-marker {
    top: 0;
  }
  .trajectory-item--current .trajectory-marker {
    left: calc(-1.25rem - 0.35rem);
  }
  .trajectory-card {
    order: 2;
    gap: 0.45rem;
  }
  .trajectory-title {
    font-size: clamp(15px, 4vw, 17px);
    max-width: 24rem;
  }
  .trajectory-detail {
    font-size: clamp(13px, 3.7vw, 15px);
    max-width: 32rem;
  }
}

/* ── tablet · two-column editorial grid (760–1099) ────────────
   the section label sits in a quiet left rail; the four items
   stack 2×2 on the right. no horizontal axis (it would clip at
   this width); the chronology reads as a structured editorial
   index. */
@media (min-width: 760px) and (max-width: 1099.98px) {
  .section-trajectory {
    display: grid;
    grid-template-columns: minmax(10rem, 14rem) minmax(0, 1fr);
    column-gap: clamp(2rem, 4vw, 3.5rem);
    align-items: start;
  }
  .section-trajectory > .section-label {
    grid-column: 1;
    margin: 0;
    padding-right: clamp(1rem, 2vw, 2rem);
    border-right: 1px solid var(--trajectory-line);
    min-height: 2rem;
  }
  .trajectory-list {
    grid-column: 2;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: clamp(1.5rem, 3vw, 2.5rem);
    row-gap: clamp(1.8rem, 3.5vw, 2.8rem);
    margin: 0;
    /* phase 82 · back to align-items: start so cards don't stretch
       to fill the grid row; the min-height tokens below give the
       3 compact cards a shared deliberate height and let the
       current card sit slightly taller. */
    align-items: start;
  }
  .trajectory-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: 1.2rem 0.9rem auto;
    row-gap: 0.55rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--trajectory-line);
  }
  .trajectory-year {
    grid-row: 1;
    margin: 0;
  }
  .trajectory-marker {
    grid-row: 2;
    justify-self: start;
    margin: 0;
  }
  .trajectory-item--current .trajectory-marker {
    margin-top: -0.075rem;
  }
  .trajectory-card {
    grid-row: 3;
    max-width: 24rem;
    min-height: var(--trajectory-card-min);
  }
  .trajectory-item--current .trajectory-card {
    min-height: var(--trajectory-card-current-min);
  }
}

/* ── desktop · four-column horizontal chronology (≥ 1100) ─────
   rail is a single horizontal hairline across the top of the list
   via ::before. each item is a column: year above marker above card.
   marker margins are calibrated so the dot's vertical centre lands
   on the rail regardless of font metrics. */
@media (min-width: 1100px) {
  .trajectory-list {
    position: relative;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    column-gap: var(--trajectory-gap);
    margin: clamp(3rem, 7vw, 5rem) 0 0;
  }
  .trajectory-list::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 2.4rem;
    border-top: 1px solid var(--trajectory-line);
  }
  .trajectory-item {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    min-width: 0;
  }
  .trajectory-year {
    margin: 0 0 1rem;
  }
  .trajectory-marker {
    margin: 0 0 1.25rem;
    /* year-bottom + 1rem gap + 0.55rem dot puts dot centre at the rail
       (rail sits at top: 2.4rem on the list). */
    align-self: flex-start;
  }
  .trajectory-item--current .trajectory-marker {
    margin: -0.075rem 0 1.175rem;
  }
  .trajectory-card {
    max-width: var(--trajectory-card-max);
    width: 100%;
    /* phase 82 · compact cards share one deliberate min-height; the
       current card sits ~14% taller via the --current variant so
       2023 reads as an intentional emphasis, not a stretched peer. */
    min-height: var(--trajectory-card-min);
  }
  .trajectory-item--current .trajectory-card {
    min-height: var(--trajectory-card-current-min);
  }
}

/* ─────────────────────────────────────────────────────────────
   phase 71 · laptop continuity pass (>= 1100px)
   ─────────────────────────────────────────────────────────────
   establishes the desktop "plate" — content lifted closer to the
   masthead, footer raised toward content, verify hero + card de-
   islanded, integrity subgrid card capped so the value column stops
   leaving a dead right half, security widened to read as a document
   not a receipt, source reader code panel constrained so long lines
   stop dictating the page feeling, and footer controls clustered so
   they read as one group rather than four scattered islands.
   privacy is the benchmark — no privacy-specific overrides; it just
   inherits the new shared --page-end-space + .page padding-top. */
@media (min-width: 1100px) {
  /* shared plate · lift content closer to the masthead and raise the
     footer slightly so the page reads as one editorial composition. */
  :root {
    --page-end-space: clamp(2.5rem, 5vw, 4.5rem);
  }
  .page {
    padding-top: clamp(72px, 8vh, 100px);
    padding-bottom: clamp(48px, 6vh, 80px);
  }

  /* verify hero · drop the inherited .page-body vertical padding
     (was clamp(40,6vw,64) top + bottom) — the .page padding alone
     owns the rhythm at laptop widths. tighten h1->lede and lede->
     card so the kicker / H1 / lede / card read as one plate not
     two islands. */
  .verify-page .page-body {
    padding: 0;
  }
  .verify-page .verify-hero .page-title {
    margin-bottom: 1.1rem;
  }
  .verify-page .verify-root {
    margin-top: 1.6rem;
  }

  /* verify page record card · cap so the card doesn't stretch wider
     than the editorial story needs. hashes + urls still fit at 44rem
     (~704px). aligned to the same left edge as the hero. */
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card {
    max-width: min(100%, 44rem);
  }

  /* integrity card · the subgrid (200px label + 1fr value) stretches
     the value column to ~564px when the card is 832px wide, leaving
     short values like /integrity.json with a visually empty right
     half. capping the card to 44rem (~704px) shrinks the value column
     to ~482px — close enough to the label-text inertia that the dead
     space reads as deliberate margin, not orphaned canvas. */
  .integrity-record-card {
    max-width: min(100%, 44rem);
  }

  /* phase 73 · footer cluster handled by the universal flex+wrap+
     center layout at the base rule. no laptop-only overrides needed
     — the row reads as one continuous colophon line at every width
     above ~700px, and wraps gracefully below. */

  /* phase 72 · approach as two parallel editorial columns.
     six principles pair horizontally row-by-row (default row-flow)
     so each row reads as a related couplet:
       row 1 · growth ↔ clienteling
       row 2 · ai ↔ adoption
       row 3 · governance ↔ taste
     row borders that worked in single-column become noise across
     two columns and read as a ui card grid, so they drop on laptop;
     vertical rhythm comes from row-gap alone. items remain content-
     led (no align-items stretch, no min-height). */
  .principles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: clamp(2.5rem, 5vw, 4.5rem);
    row-gap: clamp(1.2rem, 2.4vh, 2rem);
    align-items: start;
  }
  .principle {
    padding: 0;
    border-bottom: none;
    gap: 0.5rem;
  }
}

/* ── print · existing print profile owns the trajectory ─────── */
@media print {
  .trajectory-list { display: none; }

  .nav { display: none; background-image: none; }
  body { background-image: none; }
  section { padding: 40px 0; }
  .hero { min-height: auto; }
}


/* section 4 · personal projects */

.project-card {
  /* editorial insert on the homepage — its own --paper-project tone
     so it lifts visibly off the front-of-house ivory and reads as
     a sheet placed on the page, never as a record-layer card.
     phase 39 · 2px outer radius softens the object quality just
     enough to read as a mounted archival sheet rather than a
     mathematically sharp web card. internal rules, metadata
     rows and buttons stay sharp. phase 46 · literal 8px →
     var(--radius-soft) (7px). */
  background: var(--paper-project);
  border: 1px solid var(--rule);
  border-radius: var(--radius-soft);
  padding: clamp(36px, 8vw, 48px) clamp(28px, 8vw, 44px) clamp(34px, 9vw, 52px);
  margin-top: clamp(40px, 7vh, 64px);
  position: relative;
}


.js .project-card {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.7s ease, transform 0.7s ease;
}

/* phase 91 · reveal-gate fix: shown state requires fonts-ready. */
.js.fonts-ready .project-card.visible {
  opacity: 1;
  transform: translateY(0);
}

.project-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 3px;
  height: 100%;
  background: var(--ac);
}

/* editorial cards opt out of the accent rail. */
.project-card--editorial::before { content: none; display: none; }
.project-card--editorial { border-left: 1px solid var(--rule); }

.project-name {
  font-family: var(--mono);
  /* phase 40 · slightly larger and tighter so the project title
     reads as the clear visual anchor of the card (söhne mono is
     only available in 400/500 — no heavier weight option, so the
     hierarchy bump comes from size + tracking instead of weight).
     stays mono caps, stays at full --fg ink. */
  font-size: clamp(15px, 1.7vw, 19px);
  font-weight: 500;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--fg);
  margin-bottom: 18px;
}

.project-desc {
  font-family: var(--serif);
  font-size: clamp(19px, 5vw, 22px);
  font-weight: 400;
  line-height: 1.58;
  letter-spacing: -0.01em;
  color: var(--fg2);
  max-width: 32ch;
  margin-bottom: 34px;
}

.project-subline {
  font-family: var(--serif);
  font-size: clamp(14px, 1.6vw, 16px);
  font-style: italic;
  font-weight: 400;
  line-height: 1.55;
  color: var(--fg2);
  margin-top: 0;
  margin-bottom: 28px;
}

.project-link {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-text);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 0;
  border-bottom: 1px solid var(--bd);
  transition: border-color 0.2s, gap 0.2s;
}

.project-link:hover {
  border-color: var(--accent-text);
  gap: 12px;
}

/* Defensive: anywhere a typographic mark like → is rendered, force it
   through söhne mono so font-fallback can't substitute the glyph. */
.arrow {
  font-family: var(--mono);
}

.project-link .arrow {
  transition: transform 0.2s;
}

.project-link:hover .arrow {
  transform: translateX(2px);
}

/* button variant · replaces inline style="" for csp compliance */
.project-link-btn {
  margin-top: 24px;
  background: none;
  border: none;
  border-bottom: 1px solid var(--bd);
  cursor: pointer;
}

/* project preview · mimics the app ui */
.project-preview {
  margin-top: 34px;
  border-top: 1px solid var(--bd);
  padding-top: 26px;
}

.preview-header {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg3);
  margin-bottom: 16px;
}

.preview-items {
  display: grid;
  gap: 0;
  list-style: none;
}

.preview-item {
  padding: 10px 0;
  border-bottom: 1px solid var(--bd-soft);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}

.preview-item:last-child {
  border-bottom: none;
}

.preview-title {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  color: var(--fg);
}

.preview-meta {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg3);
  white-space: nowrap;
}

.preview-editorial {
  font-family: var(--serif);
  font-style: italic;
  font-size: 11px;
  color: var(--fg3);
  margin-top: 2px;
}

.project-preview-caption {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--fg3);
  margin-top: 14px;
  line-height: 1.5;
}

.preview-tier {
  font-size: 9px;
  letter-spacing: 0.08em;
  padding: 1px 5px;
  border: 1px solid var(--bd);
  color: var(--fg3);
  margin-right: 6px;
}

.tier-walk  { border-color: var(--bd); color: var(--fg3); }
.tier-metro { border-color: var(--accent-text); color: var(--accent-text); }

/* contact */

.section-contact {
  padding: clamp(80px, 12vh, 160px) 0 clamp(60px, 8vh, 100px);
}

@media (max-width: 767px) {
  .section-contact {
    padding-top: clamp(64px, 9.5vh, 112px);
  }

  .contact-email {
    font-size: clamp(15.5px, 4.3vw, 18px);
    letter-spacing: 0.01em;
  }
  .contact-secondary a {
    font-size: 11px;
  }
}

/* contact — an authored serif headline sits above the address, with the
   resolving phrase in accent italic. the email itself is lifted to the
   accent register so the one action on the page reads as the action. */
.home-contact .contact-headline {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(28px, 4.6vw, 60px);
  line-height: 1.02;
  letter-spacing: -0.025em;
  color: var(--fg);
  max-width: 18ch;
  margin: 0 0 clamp(24px, 3.5vh, 36px);
  text-wrap: pretty;
}
.home-contact .contact-headline em {
  font-style: italic;
  color: var(--accent-text);
  font-synthesis: style;
}
.home-contact .contact-address .contact-email {
  color: var(--accent-text);
  border-bottom-color: color-mix(in srgb, var(--accent-text) 35%, transparent);
}
.home-contact .contact-address .contact-email:hover {
  border-bottom-color: var(--accent-text);
}

.contact-address {
  font-style: normal;
  margin: 0;
}

.contact-email {
  font-family: var(--mono);
  font-size: clamp(14px, 2vw, 22px);
  font-weight: 400;
  color: var(--fg);
  text-decoration: none;
  border-bottom: 1px solid var(--bd);
  padding-bottom: 4px;
  transition: border-color 0.2s;
  /* Long-text resilience: at 200% zoom on a 320 px viewport the rendered
     mailto could otherwise overflow the column. word-break stays normal
     so spaces in the readable form are preserved. */
  overflow-wrap: anywhere;
  word-break: normal;
}

.contact-email:hover {
  border-color: var(--accent-text);
}

/* Long-token resilience for narrow viewports / 200% zoom. mono url
   and project-preview titles can each overrun their column at 320px
   width without `overflow-wrap: anywhere`. fingerprint grids are
   component-controlled and therefore excluded here. */
.preview-title {
  overflow-wrap: anywhere;
  word-break: normal;
}

.integrity-record-status,
.verify-status,
.record-meta,
.integrity-rg-value,
.integrity-rg-desc,
.integrity-rg-link,
.integrity-rg-value code,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid dd,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid a,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid code,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid samp,
.source-entry-name a,
.source-entry-meta {
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: break-word;
  white-space: normal;
  font-variant-ligatures: none;
}

.record-fingerprint,
.record-fingerprint bdi {
  overflow-wrap: anywhere;
  word-break: break-all;
  font-variant-ligatures: none;
}

.contact-secondary {
  margin-top: 20px;
}

.contact-secondary a {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg3);
  text-decoration: none;
  letter-spacing: 0.04em;
  transition: color 0.2s;
}

.contact-secondary a:hover {
  color: var(--fg);
}

/* Mobile-only contact refinement: email gains a touch more presence
   without becoming a headline; linkedin link stays secondary. */

/* ─── footer ──────────────────────────────────────────────────────
   two lines, no decoration.
     · line 1 — utility row · colophon · nav · language · theme.
                11px söhne mono buch, the page's actual sign-off.
     · line 2 — proof imprint · computed from /integrity.json.
                9.5px, centered, the colophon stamp of the publication.
   every colour derives from tokens already on :root in the tokens
   layer — do not introduce new hex values. wrapped in @layer
   components so it loses the unlayered-author advantage and so
   later page-scoped rules in @layer pages can override politely. */
@layer components {
/* one global spacing contract — every page hands the footer the
   same approach. main owns the bottom rhythm of its own content via
   --page-end-space; the footer owns its own internal padding via
   --footer-pad-block. no page-specific margin-before-footer hacks. */
:root {
  --page-end-space: clamp(3rem, 7vw, 6rem);
  --footer-pad-block: clamp(2rem, 4vw, 3.5rem);
  /* shared hero spacing tokens · used by verify + source-reader hero
     scopes so the rhythm contract stays explicit and tunable. campaign
     pages (privacy / security / integrity) keep the global .page-title /
     .page-lede scale; these tokens only govern the utility surfaces. */
  --hero-kicker-gap: 0.6rem;
  --hero-title-gap-mobile: 0.85rem;
  --hero-title-gap-tablet: 1.2rem;
  --hero-title-gap-desktop: 1.6rem;
  --hero-lede-gap-mobile: 1.6rem;
  --hero-lede-gap-tablet: 2rem;
  --hero-lede-gap-desktop: 2.6rem;
  /* phase 83 · source reader title back at hero scale.
     the prior --mobile/--tablet sizes (20-28px) read as metadata,
     not as the page's main h1. the source reader title is the
     page's primary editorial subject — the file being viewed —
     so it should command the same authority as other internal
     page titles. closer to .hero-statement than .page-title. */
  --source-title-size-mobile: clamp(2.4rem, 9vw, 3.6rem);
  --source-title-size-tablet: clamp(3rem, 7vw, 4.6rem);
  --source-title-size-desktop: clamp(3.6rem, 9vw, 5.8rem);
}
@media (max-width: 700px) {
  :root {
    --page-end-space: clamp(2.5rem, 10vw, 4rem);
    --footer-pad-block: 1.75rem;
  }
}
main { padding-block-end: var(--page-end-space); }

/* phase 90 · masthead colophon. stratified editorial composition:
   typography family + size + opacity drive hierarchy, never colour.
   structure:
     <footer .site-footer> > <div .site-footer__inner>
       <div .site-footer__top>    identity · nav · language
       <hr  .site-footer__break>
       <div .site-footer__bottom> imprint · theme
   centred stack by default; at >= 720 with body[data-layout="masthead"]
   the top row goes 3-col (start · centre · end) and the bottom row
   goes 2-col (start · end), baseline-aligned.
   markup invariants preserved:
   • .site-footer__language a[href]             — static edition links
   • .site-footer__theme button[data-theme]     — app.js
   • [data-cite-open]                           — cite.js
   • #footerImprint / [data-proof=…]            — app-enhance.js
   • .site-footer__language a {min 44px}        — lighthouse validator
*/
.site-footer{
  border-top:1px solid var(--rule-default);
  margin-block-start:0;
}
.site-footer__inner{
  max-width:62rem;
  margin-inline:auto;
  padding:clamp(1.2rem, 2.8vw, 2rem) clamp(24px, 5vw, 80px);
  padding-bottom:max(1rem, env(safe-area-inset-bottom, 0px));
  display:flex;
  flex-direction:column;
  /* gap between the top stratum (nav + language switcher) and the
     bottom stratum (colophon + theme) was too generous on desktop
     since the simplification of the colophon to a single linked
     edition row. tightened from clamp(0.9, 2vw, 1.4rem) → clamp
     (0.55, 1.2vw, 0.85rem). */
  gap:clamp(0.55rem, 1.2vw, 0.85rem);
  font-variant-numeric:tabular-nums lining-nums;
  line-height:1.3;
}

/* centred-stack default for both strata */
.site-footer__top,
.site-footer__bottom{
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  text-align:center;
  gap:clamp(0.5rem, 1.4vw, 0.9rem);
}

.site-footer .sep{color:var(--fg3);opacity:.45}

/* identity · mono base register */
.site-footer__identity{
  margin:0;
  font-family:var(--mono);
  font-size:11px;letter-spacing:0.07em;
  color:var(--fg2);
  display:flex;flex-wrap:wrap;align-items:baseline;justify-content:center;
  column-gap:0.55rem;row-gap:0.25rem;
}
.site-footer__identity .year{color:var(--fg3)}
.site-footer__identity .wm{
  color:var(--fg);font-weight:500;letter-spacing:0.12em;text-decoration:none;
  /* wcag 2.5.5 aa touch target. inline-block + padding extend the
     click box without enlarging the visible glyph; vertical-align
     keeps baseline alignment in the inline row. */
  display:inline-block;vertical-align:baseline;min-height:24px;padding:4px 6px;
}
.site-footer__identity a.wm:hover{color:var(--fg2)}
.site-footer__identity a.wm:focus-visible{
  outline:1px solid var(--accent);outline-offset:3px;color:var(--accent);
}

/* nav · same mono register, with privacy + verify actions */
.site-footer__nav{
  display:flex;flex-wrap:wrap;align-items:baseline;justify-content:center;
  column-gap:0.55rem;row-gap:0.3rem;
  font-family:var(--mono);font-size:11px;letter-spacing:0.07em;
  color:var(--fg2);
}
.site-footer__action{
  font-family:var(--mono);font-size:11px;letter-spacing:0.07em;
  color:var(--fg2);text-decoration:none;background:none;border:0;
  padding:4px 6px 6px;border-bottom:1px solid transparent;cursor:pointer;
  transition:color .2s,border-color .2s;
  display:inline-block;vertical-align:baseline;min-height:24px;
}
.site-footer__action:hover,
.site-footer__action:focus-visible{color:var(--accent);border-color:var(--accent);outline:0}

/* language · serif italic · forward layer */
.site-footer__language{
  list-style:none;margin:0;padding:0;
  display:flex;flex-wrap:wrap;align-items:baseline;justify-content:center;
  column-gap:clamp(0.6rem, 1.4vw, 1.1rem);row-gap:0.2rem;
  font-family:var(--serif);
}
.site-footer__language li{display:inline-flex;align-items:baseline}
/* the switcher is two siblings — static <a> links on the main
   pages (real-link contract) and <button> elements on the source
   reader + error pages (JS-driven switching, no navigation). both
   shapes get the same italic-serif editorial styling so neither
   reads as a button. the current edition carries aria-current
   ("a") or aria-pressed="true" ("button"). lighthouse touch-target
   ≥44px is satisfied by padding-block:12px + ~17px line-height —
   ample for a finger without inflating the natural inline width
   into a 44×44 centred box (the older min-width:44px shape made
   the links read as buttons). */
.site-footer__language a,
.site-footer__language button{
  background:none;border:0;cursor:pointer;
  font-family:var(--serif);
  font-style:italic;font-weight:400;
  font-size:clamp(14px, 1.6vw, 17px);
  letter-spacing:0.005em;
  color:var(--fg3);text-decoration:none;
  padding:12px 6px;border-bottom:1px solid transparent;
  text-transform:none;
  transition:color .2s,border-color .2s;
  display:inline-block;
}
.site-footer__language a:hover,
.site-footer__language a:focus-visible,
.site-footer__language button:hover,
.site-footer__language button:focus-visible{color:var(--accent);border-color:var(--accent);outline:0}
.site-footer__language a[aria-current="page"],
.site-footer__language button[aria-pressed="true"]{color:var(--fg)}

/* the two footer strata are no longer divided by a rule — the lighter
   ink-weight of the bottom stratum (below) is enough separation. the
   <hr> is left in markup but hidden site-wide. */
.site-footer__break{
  display:none;
}
/* bottom stratum · provenance + theme · held at a quiet ~38% ink so it
   recedes behind the top stratum without a divider rule. */
.site-footer__bottom{
  border-top:0;
}
.site-footer__bottom,
.site-footer__bottom dt,
.site-footer__bottom dd,
.site-footer__bottom a,
.site-footer__bottom button,
.site-footer__bottom .site-footer__provenance,
.site-footer__bottom .site-footer__theme button{
  color:color-mix(in srgb,var(--fg) 38%,transparent);
  font-weight:400;
}
/* the source reader's provenance disclaimer ("traduit automatiquement
   de l'original anglais.") is only meaningful when viewing the
   machine-translated FR rendering. hide it on the EN view — the
   source-view js toggles <html lang> between en/fr when the user
   uses the language switcher, so this attribute selector follows
   the active edition without needing additional js. */
html:not([lang="fr"]) .site-footer__provenance[lang="fr"]{
  display:none;
}
/* colophon · two-row signed publication mark · subordinate layer.
   each row carries a key (edition / signed) and a value (date · age
   on row one, SHA-256 prefix on row two). reads as a printed
   colophon, not a verification dashboard. the parent is uppercase
   mono caps; .site-footer__colophon-note explicitly resets
   text-transform so "published today" / "publiée aujourd'hui" do
   not render as shouting machine labels. */
.site-footer__colophon{
  margin:0;padding:0;list-style:none;
  display:grid;gap:.34rem;
  font-family:var(--mono);
  font-size:10px;font-weight:400;line-height:1.55;
  letter-spacing:.10em;text-transform:uppercase;
  color:var(--fg2);
  font-variant-numeric:tabular-nums lining-nums;
  transition:opacity .3s ease;
}
.site-footer__colophon-row{margin:0}
.site-footer__colophon-key{
  color:var(--fg3);opacity:.7;font-weight:400;
  margin-right:.55em;
}
.site-footer__colophon-sep{
  color:var(--fg3);opacity:.4;margin:0 .55em;
}
.site-footer__colophon-val{
  color:var(--fg2);font-weight:500;letter-spacing:.06em;
}
.site-footer__colophon-note{
  color:var(--fg3);opacity:.78;font-weight:400;letter-spacing:.04em;
  text-transform:none;
}
.site-footer__colophon-link{
  color:var(--fg2);text-decoration:none;
  border-bottom:1px solid color-mix(in srgb,var(--accent) 28%,transparent);
  padding-bottom:1px;
  padding-block:.25rem;margin-block:-.25rem;
  transition:color .2s,border-color .2s;
}
.site-footer__colophon-link:hover,
.site-footer__colophon-link:focus-visible{
  color:var(--accent-text);
  border-bottom-color:var(--accent-text);outline:0;
}
.site-footer__colophon-link code{
  font-family:inherit;font-size:inherit;letter-spacing:.04em;
  color:inherit;
}

/* === phase 91 · verify slip + composed publication sprint · quiet machine-ack link === */
/* one footer line — "verified against release" → /verify/. sits in
   the existing colophon register but without the accent underline,
   slightly dimmed at rest, lifting to the accent on hover/focus.
   no icon, no rotation, no animation — quiet by design. */
.site-footer__colophon-link--quiet {
  border-bottom: none;
  opacity: 0.72;
  color: var(--fg3);
}
.site-footer__colophon-link--quiet:hover,
.site-footer__colophon-link--quiet:focus-visible {
  opacity: 1;
  color: var(--accent-text);
  border-bottom: none;
}

/* theme · subordinate display controls */
.site-footer__theme{
  list-style:none;margin:0;padding:0;
  display:flex;flex-wrap:wrap;align-items:baseline;justify-content:center;
  column-gap:0;row-gap:0.2rem;
  font-family:var(--mono);
  font-size:9.5px;letter-spacing:0.13em;text-transform:lowercase;
  color:var(--fg3);opacity:.72;
}
.site-footer__theme li{display:inline-flex;align-items:baseline}
.site-footer__theme button{
  background:none;border:0;cursor:pointer;font-family:var(--mono);
  font-size:9.5px;letter-spacing:0.13em;color:var(--fg3);
  text-transform:lowercase;padding:2px 4px;line-height:1.2;
  min-height:44px;min-width:44px;transition:color .2s;
}
.site-footer__theme button[aria-pressed="true"]{color:var(--fg)}
.site-footer__theme button:hover,
.site-footer__theme button:focus-visible{color:var(--accent);outline:0}

/* l8 touch-target validator placeholder · keep selector compiled */
.cite-btn { min-height: 44px; }

/* masthead variant · asymmetric grid at >= 720px.
   top:    identity (start) · nav (centre) · language (end)
   bottom: imprint (start) · theme (end), baseline-aligned. */
@media (min-width: 720px) {
  body[data-layout="masthead"] .site-footer__top{
    display:grid;
    grid-template-columns:auto 1fr auto;
    align-items:center;
    text-align:left;
    gap:clamp(0.8rem, 2vw, 1.6rem);
  }
  body[data-layout="masthead"] .site-footer__top > .site-footer__identity{justify-content:flex-start}
  body[data-layout="masthead"] .site-footer__top > .site-footer__nav{justify-content:center}
  body[data-layout="masthead"] .site-footer__top > .site-footer__language{justify-content:flex-end}

  body[data-layout="masthead"] .site-footer__bottom{
    display:grid;
    grid-template-columns:1fr auto;
    align-items:baseline;
    text-align:left;
    gap:clamp(0.6rem, 1.6vw, 1.2rem);
  }
  body[data-layout="masthead"] .site-footer__bottom > .site-footer__colophon{justify-self:flex-start}
  body[data-layout="masthead"] .site-footer__bottom > .site-footer__theme{justify-content:flex-end}
}

/* mobile <= 680 · centred stack with full hit targets. matches the
   prototype's narrow-viewport behaviour for the colophon. */
@media (max-width: 680px) {
  .site-footer__colophon{
    justify-self:center;
    text-align:center;
  }
}
@media (max-width: 640px) {
  .site-footer__inner{
    gap:0.7rem;
    padding-inline:clamp(1rem, 4vw, 1.5rem);
    padding-block:1.1rem;
    padding-bottom:max(1rem, env(safe-area-inset-bottom, 0px));
  }
  .site-footer__identity,
  .site-footer__nav,
  .site-footer__colophon{max-width:20rem;line-height:1.3}
}

/* very narrow <= 380 · type compression */
@media (max-width: 380px) {
  .site-footer__identity,
  .site-footer__nav{font-size:10.5px}
  .site-footer__colophon{font-size:9.5px}
  .site-footer__theme,
  .site-footer__theme button{font-size:9px;letter-spacing:0.11em}
}

@media (prefers-reduced-motion: reduce){
  .site-footer__colophon{transition:none}
}

@media print {
  .site-footer__theme { display: none }
}

/* phase 90 · masthead footer · supersedes phase 78 centred colophon.
   stratification is family + size + opacity driven; legacy
   .site-footer__row / .site-footer__meta / .site-footer__lang
   selectors removed in this revision. */

}
/* end @layer components — footer. */


/* privacy page */

.page {
  padding-top: clamp(80px, 12vh, 140px);
  padding-bottom: clamp(60px, 8vh, 100px);
}

.page-title {
  font-family: var(--serif);
  font-size: clamp(40px, 10.5vw, 58px);
  font-weight: 300;
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--fg);
  margin-bottom: clamp(32px, 5vh, 56px);
}

/* editorial headline composition · low-specificity utilities applied
   to .page-title to force deliberate per-phrase line breaks. each
   .hero-line renders as its own block so the composition is
   intentional rather than viewport-dependent. text-wrap: balance
   protects against orphan words when a single line still has to wrap
   on narrow viewports. .measure-tight keeps hero stacks from running
   to ultra-wide reading measure on desktop. */
.hero-line { display: block; }
.hero-line + .hero-line { margin-top: 0.06em; }
.hero-stack {
  line-height: 0.98;
  letter-spacing: -0.035em;
  text-wrap: pretty;
}
.hero-stack .hero-line { text-wrap: balance; }
.measure-tight { max-width: 20ch; }

/* narrow viewports: relax letter-spacing slightly so per-line
   characters still breathe at smaller renders. */
@media (max-width: 480px) {
  .hero-stack { letter-spacing: -0.025em; }
  .measure-tight { max-width: none; }
}

.page-body {
  max-width: 580px;
}

.page-lede {
  font-family: var(--serif);
  font-size: clamp(24px, 6.3vw, 30px);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: var(--fg);
  max-width: 31ch;
  margin-bottom: 1.6em;
}

.page-body p {
  font-family: var(--serif);
  font-size: clamp(17px, 4.6vw, 20px);
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: -0.008em;
  color: var(--fg2);
  max-width: 34ch;
  margin-bottom: 1.4em;
}

/* phase 86 · the .page-body p rule above clobbers .page-kicker when
   the kicker sits inside .page-body — which is the case on /verify/
   (the kicker lives in .verify-hero, which is inside .page-body,
   unlike privacy / integrity / security where the kicker sits
   outside .page-body). this rule restores the kicker register
   (mono, 11px, fg3, 0.14em tracking, uppercase) at higher
   specificity so the cascade no longer leaks serif body type into
   the kicker. */
.page-body .page-kicker {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg3);
  max-width: none;
}


.page-body p:last-child {
  margin-bottom: 0;
}

/* refined editorial underlines under signifier text. quiet by default,
   oxblood on hover/focus, thin and offset so they don't visually weigh
   down large serif type. */
.page-body a {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--bd);
  text-underline-offset: 0.14em;
  border-bottom: none;
  transition: text-decoration-color 0.2s;
}

.page-body a:hover,
.page-body a:focus-visible {
  text-decoration-color: var(--accent-text);
}

.page-meta {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg3);
  letter-spacing: 0.06em;
}

/* /integrity/releases/ — release register.
   the page is a quiet archival index, not a download surface. two
   groups (current release / archive) each carrying release rows.
   no cards, no boxes — hairline rules between rows do the work.
   vertical rhythm tightened ~25-30% from the first-pass values so
   the page reads as a finished register on mobile rather than a
   sparse list. whitespace remains the hierarchy; it is just no
   longer being used as emptiness. */
/* releases page sections opt out of the generic section { padding +
   border-bottom } rule — three nested <section> elements (release-
   index + two release-groups) were each inheriting 80-160px of
   vertical padding and a hairline border-bottom, producing large
   empty voids and unnecessary horizontal rules between the intro,
   current release, archive, and related records. .release-index
   and .release-group only exist on the releases page so class
   specificity (0,1,0) beats the generic section selector (0,0,1)
   without needing the body[data-page=…] layer wrap. */
.release-index {
  /* intro → current release · compact editorial gap. */
  padding: 0;
  border-bottom: 0;
  margin: clamp(3rem, 9vw, 4.5rem) 0 0;
}

.release-group {
  /* current → archive · slightly larger than the in-card rhythm so
     the two groups read as peer sections, but no full section-block
     void between them. */
  padding: 0;
  border-bottom: 0;
  margin: clamp(3rem, 9vw, 4.5rem) 0 0;
}
.release-group:first-child { margin-top: 0; }

.release-group-label {
  /* shared metadata-label register — matches page record, manifest,
     history across the trust system. title-case mono, very small,
     muted; never competes with the release titles below it.
     border-top + padding-top retired — the rule was floating between
     sections and fragmenting the page. spacing alone separates
     current release from archive. */
  font-family: var(--mono);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg3);
  margin: 0 0 1rem;
}

.release-row {
  /* phase 42 · each release is now an archival object — framed,
     padded, subtly lifted off the page surface so the register
     reads as a release registry of preserved snapshots rather
     than a loose list of rows. matches the verify-card / project-
     card / integrity-record-card object family. internal
     hierarchy (date / title / meta / actions) stays sharp.
     phase 46 · literal 8px → var(--radius-soft) (7px). */
  background: color-mix(in srgb, var(--surface-page) 92%, white 8%);
  border: 1px solid var(--rule);
  border-radius: var(--radius-soft);
  padding: clamp(1.4rem, 4vw, 2rem);
  margin-bottom: clamp(0.85rem, 2vw, 1.25rem);
}
.release-row:last-child { margin-bottom: 0; }

.release-row--current {
  /* current release gets a touch more presence than the archive
     rows — slightly heavier title, marginally more breathing room. */
  padding-bottom: clamp(1.5rem, 4.5vw, 2.2rem);
}

.release-date {
  /* shared metadata-label register again — date sits as the small
     uppercase locator above the human-readable title. */
  font-family: var(--mono);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg3);
  font-variant-numeric: tabular-nums lining-nums;
  margin: 0 0 0.3rem;
}

.release-title {
  font-family: var(--sans);
  font-style: normal;
  font-weight: 400;
  font-size: clamp(15px, 3.6vw, 17px);
  line-height: 1.35;
  color: var(--fg);
  margin: 0 0 0.25rem;
}
.release-row--current .release-title {
  font-size: clamp(16px, 3.8vw, 18px);
}

.release-meta {
  font-family: var(--mono);
  font-size: 0.64rem;
  letter-spacing: 0.04em;
  color: var(--fg3);
  margin: 0;
  line-height: 1.5;
}

/* compact action strip — download zip · download TAR.GZ · checksums
   & signature · view release. inline middots between actions, wraps
   cleanly on narrow screens; restrained underline weight (--rule)
   so the row reads as a footnote to the release title above. */
.release-actions {
  margin: 0.7rem 0 0;
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.025em;
  line-height: 1.7;
  color: var(--fg3);
}
.release-action {
  color: var(--fg2);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  transition: color 0.2s, border-bottom-color 0.2s;
}
.release-action:hover,
.release-action:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
  outline: 0;
}
/* phase 42 · actions separator rendered as ::before pseudo on each
   sibling after the first (matches the .site-footer__actions and
   .trust-mark separator pattern). */
.release-actions > * + *::before {
  content: "·";
  display: inline-block;
  margin-inline: 0.42rem;
  opacity: 0.45;
  transform: translateY(-0.02em);
  color: currentColor;
}


.fingerprint {
  display: inline-block;
  font-family: var(--mono);
  font-size: clamp(13px, 3.4vw, 15px);
  line-height: 1.8;
  letter-spacing: 0.08em;
  color: var(--fg);
  overflow-wrap: anywhere;
  word-break: break-all;
  user-select: text;
  -webkit-user-select: text;
}

.fingerprint {
  display: block;
  user-select: text;
  -webkit-user-select: text;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* phase 49 · copy fingerprint as an inline editorial action.
   phase 50 · quieter still — smaller, lighter, subordinate to the
   fingerprint itself. opacity 0.72 by default; lifts to 1 on hover/
   focus so the action remains discoverable. js hook (.copy-
   fingerprint[data-copy-target]) and i18n bindings preserved.
   text-transform cancels the parent .integrity-rg-label uppercase
   cascade; opacity here multiplies with the parent 0.78 — combined
   effective opacity ~0.56 at rest, lifting to 0.78 on interaction. */
/* phase 49 · copy fingerprint as an inline editorial action.
   phase 54 · restored text-decoration: underline pattern, explicit
   font-weight + line-height. parent .integrity-rg-label opacity
   moved to color-mix so the button's declared opacity renders
   directly.
   phase 55 · .record-inline-action joined as a shared class so
   copy fingerprint + copy command + any future inline action sit
   in one declarative register. existing .copy-fingerprint and
   .trust-code-copy selectors remain in the combined rule for
   compatibility with the cite.js js bindings. */
.record-inline-action,
.copy-fingerprint,
.trust-code-copy {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  padding: 0;
  font-family: var(--mono);
  font-size: 0.58rem;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.055em;
  text-transform: none;
  color: var(--fg2);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--fg2) 42%, transparent);
  text-underline-offset: 0.18em;
  cursor: pointer;
  transition: color 0.2s, text-decoration-color 0.2s;
}
.record-inline-action:hover,
.record-inline-action:focus-visible,
.copy-fingerprint:hover,
.copy-fingerprint:focus-visible,
.trust-code-copy:hover,
.trust-code-copy:focus-visible {
  color: var(--fg);
  text-decoration-color: currentColor;
  outline: 0;
}
.record-inline-action[data-state="copied"],
.copy-fingerprint[data-state="copied"],
.trust-code-copy[data-state="copied"] {
  color: var(--accent-text);
  text-decoration-color: var(--accent-text);
}

/* phase 61 · .source-mirror-intro retired. the source-mirror
   reading rule is now implicit: each link opens the readable
   .txt mirror. the page lede + per-file descriptions carry the
   reader without a companion gloss. */

/* Group-label count register — "pages, 12". same fg3 mono register
   as the label itself; no extra colour. */
.source-group-count {
  color: var(--fg3);
  font-weight: 400;
}

/* quiet "download the source archive" affordance on /source/. sits
   between the page lede and the directory listing. mono, small, calm —
   no button styling. the links inherit the page's underlined-link
   treatment. */
.source-download {
  margin-top: 12px;
  font-family: var(--mono);
  font-size: clamp(10.5px, 2.7vw, 12px);
  line-height: 1.6;
  color: var(--fg2);
}
.source-download span[data-i18n] {
  /* keep the lede on its own conceptual line by allowing the
     browser to break before the first separator on narrow widths;
     the inline phrase wraps as a single sentence followed by a
     calm list of three downloads rather than a fragmented chain. */
  display: inline;
}
.source-download a {
  color: inherit;
  white-space: nowrap;
  text-decoration-color: color-mix(in srgb, var(--fg2) 42%, transparent);
}
.source-download a:hover,
.source-download a:focus-visible {
  color: var(--fg);
  text-decoration-color: currentColor;
}

/* /source/ — editorial provenance registry.
   the source page is a public inspection ledger, not a directory
   dump. each editorial group renders as a section with a quiet
   mono kicker, a single-line serif gloss, and a <dl> body of
   .source-entry rows. each entry is a two-column micro-grid:
   filename left, mono meta right (kind · size · validated date),
   with an optional short SHA-256 below. typography is mono for
   identifiers and metadata so the page reads as a typeset archival
   catalogue. */

/* widen the page body on /source/ to match /verify/. the catalogue
   is the longest editorial run on the site; the default 580px
   prose column reads as an austere ribbon and over-isolates the
   right field. 880px gives the registry room to breathe without
   becoming a sprawl. */
.source-page .page-body {
  max-width: 880px;
}

/* the about line that follows the page lede on /source/. names the
   page's editorial taxonomy in one calm sentence so the reader has
   a mental map before scrolling into the registry. matched to the
   lede register (serif, 300, fg2) but slightly smaller and below
   max-width so it reads as continuation, not a new paragraph. */
/* phase 61 · .source-intro_about, .source-fingerprint-note,
   .source-mirror-intro and .source-philosophy retired. /source/
   now opens on a single editorial lede plus the download line,
   and the files themselves carry the page. */

.source-registry-wrap {
  margin-block-start: clamp(28px, 4.5vw, 40px);
  display: grid;
  gap: clamp(2.4rem, 5.5vw, 3.6rem);
}
/* /source/ adopts the .integrity-record-card shell for the four
   file-group lists. the default record-card max-width is sized for
   the integrity overview (52rem / 44rem / 38rem at the three
   breakpoints) — file rows want the full reading column. widen the
   card when it sits inside .source-page, and tighten the inter-card
   rhythm so the four cards read as a single ledger rather than four
   floating panels. */
.source-page .integrity-record-card,
.source-page .integrity-record-card.source-group-card {
  max-width: 100%;
}
@media (min-width: 760px) and (max-width: 1099.98px) {
  .source-page .integrity-record-card { max-width: 100%; padding: 1.35rem 1.55rem; }
}
@media (min-width: 1100px) {
  .source-page .integrity-record-card { max-width: 100%; }
}
.source-page .source-registry-wrap {
  margin-block-start: clamp(1.8rem, 3.5vw, 2.4rem);
  gap: clamp(1.2rem, 2.4vw, 1.6rem);
}
.source-group-gloss {
  margin: 0 0 1.1rem;
  font-family: var(--serif);
  font-size: clamp(14.5px, 1.8vw, 16px);
  font-weight: 300;
  line-height: 1.55;
  color: var(--fg2);
  max-width: 60ch;
}
.source-group {
  margin: 0;
  padding: 0;
}
.source-group-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 0 0 0.85rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--rule);
}
.source-group-title {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg3);
}
.source-group-count {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  color: var(--fg3);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.source-registry {
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
}
/* registry row — a three-area micro-grid:
   row 1: filename (left) · mono meta (right)
   row 2: editorial description spanning the full width
   no per-row border. rhythm comes from generous padding so the
   page reads as an authored ledger, not a directory index. a
   single faint hairline appears between entries via the +
   sibling selector at very reduced opacity. */
.source-entry {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: clamp(0.85rem, 3.5vw, 1.5rem);
  row-gap: clamp(0.32rem, 0.9vw, 0.45rem);
  align-items: baseline;
  padding-block: clamp(0.85rem, 1.8vw, 1.05rem);
}
.source-entry + .source-entry {
  border-block-start: 1px solid color-mix(in srgb, var(--bd-soft) 55%, transparent);
}
.source-entry:last-child {
  border-bottom: 0;
}
.source-entry-name {
  margin: 0;
  min-width: 0;
  font-family: var(--mono);
  font-size: clamp(0.78rem, 2.4vw, 0.86rem);
  font-weight: 500;
  line-height: 1.45;
  letter-spacing: 0.01em;
  color: var(--fg);
  word-break: break-all;
  overflow-wrap: anywhere;
  font-variant-ligatures: none;
}
.source-entry-name a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
  transition: color 0.2s, border-bottom-color 0.2s;
}
.source-entry-name a:hover,
.source-entry-name a:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
  outline: 0;
}
.source-entry-name code {
  font-family: inherit;
  font-size: inherit;
  background: transparent;
  padding: 0;
}
/* quiet "raw" link alongside the reader link */
.source-entry-raw {
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg3);
  text-decoration: none;
  margin-inline-start: 0.5rem;
  opacity: 0.7;
  transition: opacity 0.15s ease, color 0.15s ease;
}
.source-entry-raw:hover,
.source-entry-raw:focus-visible {
  color: var(--accent-text);
  opacity: 1;
}
.source-entry-meta {
  margin: 0;
  justify-self: end;
  text-align: right;
  font-family: var(--mono);
  font-size: 0.66rem;
  line-height: 1.45;
  letter-spacing: 0.04em;
  color: var(--fg3);
  opacity: 0.82;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  font-variant-ligatures: none;
  white-space: normal;
}
.source-entry-meta abbr {
  text-decoration: none;
  border: 0;
  cursor: default;
  color: var(--fg3);
}
.source-entry-sep {
  margin: 0 0.18rem;
  opacity: 0.55;
}
.source-entry-size {
  font-variant-numeric: tabular-nums lining-nums;
}
.source-entry-hash {
  /* the hash is a citation, not a primary label. soft enough to
     recede when the eye is scanning filenames; selectable and
     full-precision via the title attribute on the <samp>. */
  margin: 0.18rem 0 0;
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.045em;
  color: color-mix(in srgb, var(--fg3) 70%, transparent);
  line-height: 1.4;
}
.source-entry-hash abbr {
  text-decoration: none;
  border: 0;
  cursor: default;
  margin-right: 0.35rem;
  opacity: 0.7;
}
.source-entry-hash samp {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
}

/* editorial description row — the why-this-file-exists line. sits
   below the filename/meta on its own row, spanning the full grid
   width so the prose has room to breathe. serif register tied to
   the page's narrative copy. width capped at 44ch for editorial
   discipline: short measures force prose to be precise and let
   the registry breathe between filenames and their commentary. */
.source-entry-desc {
  grid-column: 1 / -1;
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(13.5px, 1.65vw, 15px);
  font-weight: 300;
  line-height: 1.5;
  color: var(--fg2);
  max-width: 44ch;
}

/* quiet role label placed inline at the head of the description
   for trust-critical files. text only — no badge, no border, no
   colour wash. mono uppercase tracking signals "this is a system
   role, not editorial prose"; the trailing middot lets the prose
   flow without a hard break. */
.source-entry-role {
  display: inline;
  margin-inline-end: 0.45rem;
  font-family: var(--mono);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg3);
  opacity: 0.82;
  white-space: nowrap;
}
.source-entry-role::after {
  content: " ·";
  margin-inline-start: 0.1rem;
  color: color-mix(in srgb, var(--fg3) 55%, transparent);
}

/* critical-file row. quietly distinguish the few files that carry
   the trust system (signed manifest, detached signature, public
   signing key, canonical identity record, disclosure surface).
   the brief forbids badges; the distinction is a hair more
   contrast on the role kicker. no extra chrome, no left border —
   restraint is the point. the filename already reads as primary
   ink across the registry, so no override there. */
.source-entry[data-critical="true"] .source-entry-role {
  color: var(--fg2);
  opacity: 1;
}

/* phase 61 · .source-philosophy retired — explanatory density
   removed in favour of the catalogue itself. */

pre {
  font-family: var(--mono);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  padding: 1em 1.2em;
  color-scheme: light;
  -webkit-appearance: none;
  background-color: var(--bg2);
  border: 1px solid var(--bd);
  border-radius: 4px;
  font-size: 0.72em;
  line-height: 1.7;
  margin: 0;
  color: var(--fg);
}

code {
  font-family: var(--mono);
}

.code-cmd { color: var(--accent-text); }
.code-str { color: var(--code-string); }
.code-var { color: var(--code-var); font-weight: 500; }

/* access modal · old .modal-overlay / .modal / .modal-cta / .modal-close-x
   and verify-overlay's .cite-modal-* / .cite-overlay rules removed in
   phase 92 (modal-system overhaul) — the gate, access and verify
   modals now share the .shell + .modal-shell-scrim family defined in
   @layer components further down. preserved below are two
   .record-tools rules that pre-existed inside the cite-overlay block
   but are still consumed by the dedicated /verify/ page. */

/* citation-copy confirmation · in-place row transformation on the
   /verify/ page's record-tools strip. preserves the existing
   min-inline-size reservation so the row does not jiggle as the
   label inflates from "copy citation" to the cited inscription. */
.record-tools .record-inline-action[data-copy-mode="cite"] {
  min-inline-size: max-content;
  white-space: nowrap;
}
.record-tools .record-inline-action[data-state="failed"] {
  color: var(--accent-text);
}


/* reduced motion */


/* print */


}
@layer components {
/* /security/ as a posture document — open editorial sections with
   real h2 headings, soft hairline rules between them, no accordion
   chrome. phase 62 · section rhythm and sub-section spacing widened
   ~7% and the inter-section rule softened to var(--rule) so spacing,
   not lines, carries the hierarchy on this denser page. */
.security-section {
  margin: clamp(44px, 7vw, 72px) 0;
  padding: clamp(32px, 5vw, 42px) 0 0;
  border-top: 1px solid var(--rule);
  border-bottom: 0;
  min-height: 0;
}
.security-section:first-of-type {
  margin-top: clamp(32px, 4.5vw, 44px);
  padding-top: 0;
  border-top: 0;
}
.security-section-heading {
  font-family: var(--serif);
  font-size: clamp(22px, 4.4vw, 30px);
  font-weight: 400;
  line-height: 1.2;
  color: var(--fg);
  margin: 0 0 clamp(16px, 2.2vw, 24px);
  letter-spacing: -0.005em;
}
.security-section .security-subheading {
  /* quieter subhead — won't compete with the section h2.
     phase 62 · breathing above + below loosened so the subsection
     groupings read as composed rather than compressed. */
  font-family: var(--mono);
  font-size: 0.76rem;
  font-weight: 400;
  letter-spacing: 0.025em;
  color: var(--fg2);
  margin: clamp(1.7rem, 3vw, 2.1rem) 0 clamp(0.55rem, 1vw, 0.7rem);
  text-transform: none;
}
.security-section .security-subheading strong { font-weight: 400; }
/* phase 44 · semantic subsections inside a .security-section.
   threat-model and controls now use real <section> + <h3>
   groupings instead of <p><strong> subheadings. neutralise the
   global `section { padding ; border-bottom }` rule so these
   inner blocks remain flat editorial passages, not nested
   bordered panels. spacing comes from .security-subheading and
   the list rhythm above. */
.security-section .security-subsection {
  padding: 0;
  border-bottom: 0;
  min-height: 0;
  margin: 0;
}
.security-architecture-note {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  color: var(--fg2);
  margin: clamp(18px, 2.5vw, 24px) 0 0;
  line-height: 1.55;
}

/* phase 42 · the bespoke .architecture-card / .architecture-card-dl /
   .architecture-row component family was retired in favour of the
   shared `.verify-card` + `.record-grid` micro-grid (now scoped to
   `.security-page` as well). the security architecture section reuses
   the canonical archival-object pattern, so the visual register is
   one family across verify / sw-reset / security. */

/* Security-page editorial lists · signifier prose with proper specificity
   (no !important needed; .page-body scope wins the cascade). */
.page-body ul.i18n-list {
  font-family: var(--serif);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.6;
  list-style: disc;
  padding-left: 1.25rem;
  margin: 0.5rem 0 1rem;
}
.page-body ul.i18n-list li {
  font-family: var(--serif);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.6;
  margin-bottom: 0.2rem;
}
.page-body details p[data-i18n$="_heading"],
.page-body details strong[data-i18n$="_heading"] {
  font-family: var(--serif);
  font-size: 0.9rem;
  font-weight: 400;
  color: var(--fg2);
  margin: 1rem 0 0.2rem;
}

/* bullets inside .security-section share the same calm refinement
   the prior .security-disclosure block had — list-style:disc, smaller
   marker, refined serif body. phase 62 · inter-item spacing widened
   so the threat-model + controls lists read as composed cadence,
   not stacked rows. */
.page-body .security-section ul.i18n-list {
  padding-left: 1.05rem;
  margin: clamp(0.75rem, 1.5vw, 1rem) 0 clamp(1.35rem, 2.4vw, 1.6rem);
  list-style: disc;
}
.page-body .security-section ul.i18n-list li {
  font-family: var(--serif);
  font-size: clamp(15px, 4vw, 17px);
  line-height: 1.65;
  margin-bottom: clamp(0.5rem, 1vw, 0.65rem);
  color: var(--fg);
}
.page-body .security-section li::marker {
  color: var(--fg3);
  font-size: 0.7em;
}

/* legacy ".trajectory-chapter ul" reset retired — the new trajectory
   chronology has no nested lists. .trajectory-item carries its own
   list-style reset downstream. */

/* ─── language switch · viewport-anchor stabilisation ──
   while a language switch is in flight, suppress all transitions
   and animations so the page does not visibly jump. the
   .is-language-switching class is added by app.js around the
   text-replacement step and removed once layout settles
   (raf × 2). */

/* ─── phase 92 · modal-system shell ───────────────────────────
   one paper-card shell shared by the language gate, the verify
   action menu and the access modal. the shell never changes;
   only the .body-region inside it varies per modal. the scrim
   is a paper-tinted veil — NEVER backdrop-filter. the blur lives
   on the publication itself (.site / #main) when body.modal-open
   is set, so the publication recedes rather than being covered.
   tokens used here are declared in @layer tokens above. */

/* scrim · fixed overlay, paper-tinted at --modal-scrim alpha. flex
   centres the shell with safe-area-aware edge padding. opens via
   the .is-active class (toggled by overlay.js); pointer-events
   gate prevents clicks bleeding to the publication while closed. */
.modal-shell-scrim {
  position: fixed;
  inset: 0;
  z-index: 200;
  /* unified scrim across all three modals (gate, access, verify):
     dark warm-brown overlay matching --overlay-scrim. previously
     paper-tinted at 38% which read as no scrim at all because
     the publication showed through crisply. publication blur is
     applied separately below so this layer carries the darkening
     while the underlying tree carries the blur — together they
     read as a real modal seal, not a glass plate. */
  background: var(--overlay-scrim);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(var(--modal-edge), env(safe-area-inset-top))
           max(var(--modal-edge), env(safe-area-inset-right))
           max(var(--modal-edge), env(safe-area-inset-bottom))
           max(var(--modal-edge), env(safe-area-inset-left));
  opacity: 0;
  transition: opacity 220ms ease;
  pointer-events: none;
}
.modal-shell-scrim.is-active {
  opacity: 1;
  pointer-events: auto;
}

/* shell · the paper card. width caps at 32rem so it never feels
   like a dialog box; padding clamps so the inner gutter scales
   with viewport. two-layer shadow is a hint, not a lift — the
   hairline border carries the edge. */
.shell {
  width: min(100%, 32rem);
  background: var(--modal-paper);
  border: var(--modal-rule);
  border-radius: var(--modal-radius);
  padding: var(--modal-pad);
  box-shadow: var(--modal-shadow);
  position: relative;
  opacity: 0;
  transform: translateY(6px) scale(0.992);
  transition: opacity var(--modal-motion), transform var(--modal-motion);
}
.modal-shell-scrim.is-active .shell {
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* × close · top-right · mono glyph. 28px desktop ; 40px mobile so
   the type-only chrome still clears the 44px touch floor with the
   hit area extending into the shell padding. */
.shell-close {
  position: absolute;
  top: 14px;
  right: 16px;
  width: 28px;
  height: 28px;
  border: 0;
  background: transparent;
  cursor: pointer;
  font-family: var(--mono);
  font-size: 18px;
  line-height: 1;
  color: var(--fg3);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  transition: color .18s ease, background .18s ease;
}
.shell-close:hover,
.shell-close:focus-visible {
  color: var(--accent-text);
  background: color-mix(in srgb, var(--ink) 3%, transparent);
  outline: 0;
}

/* title · signifier 300 ~32px, balanced. the title carries the
   modal — no kicker, no wordmark above. */
.shell-title {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(26px, 4.4vw, 32px);
  line-height: 1.08;
  letter-spacing: -0.014em;
  color: var(--fg);
  text-align: center;
  text-wrap: balance;
  /* tightened ~10% from 0.6rem → 0.54rem so the subtitle sits closer. */
  margin: 0 auto 0.54rem;
  /* a 10ch inline measure lets text-wrap:balance pick a graceful
     2-3 line break ("verify, cite / or read the source.") instead
     of the awkward single-break the verify modal title fell into. */
  max-inline-size: 10ch;
  font-feature-settings: "kern" 1, "liga" 1, "onum" 1;
}

/* lede · signifier 300 ~15px italic centred. one or two short
   sentences; ragged-right, balanced. */
.shell-lede {
  font-family: var(--serif);
  font-weight: 300;
  font-style: italic;
  font-size: 15px;
  line-height: 1.55;
  color: var(--fg2);
  text-align: center;
  text-wrap: pretty;
  max-width: 34ch;
  margin: 0 auto 1.8rem;
  font-feature-settings: "onum" 1;
}

/* hairline rule · separates the title/lede header from the body
   region. zero margin so the spacing comes from the regions, not
   from the rule. */
.shell-rule {
  border: 0;
  border-top: var(--modal-rule);
  margin: 0;
}

/* verify modal uses a padded title wrapper (so the menu rows can
   be full-bleed) and a "this page" line in place of the lede. */
.shell-pad {
  /* section gap tightened ~8% from 0.8rem → 0.74rem so the title
     block reads as one unit with the menu beneath. */
  padding: 0 0 0.74rem;
  text-align: center;
}
/* "this page" subtitle · signifier roman (was italic), slightly
   smaller, subtle tracking. reads as an archival page-record label
   rather than a decorative italic. */
.shell-this-page {
  font-family: var(--serif);
  font-style: normal;
  font-weight: 400;
  font-size: 0.94em;
  line-height: 1.4;
  letter-spacing: 0.015em;
  color: var(--fg2);
  margin: 0.36rem 0 0;
  text-align: center;
}
.shell-this-page .pn {
  color: var(--fg);
}

/* body region · neutral container for whatever body the modal
   ships. per-modal sub-rules live below. */
.body-region {}

/* ─── language gate body ──── two-up edition selector. desktop
   shows english | français side by side separated by a vertical
   hairline. mobile (≤520px) stacks them with a horizontal rule
   between cells. ported verbatim from redesigns/modals/A-language.html. */
.lang-row {
  display: grid;
  grid-template-columns: 1fr 1px 1fr;
  align-items: stretch;
}
.lang-row .divider {
  background: var(--rule);
  align-self: stretch;
  width: 1px;
}
.lang-cell {
  background: transparent;
  border: 0;
  cursor: pointer;
  font: inherit;
  padding: 14px 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  border-radius: var(--radius-soft);
  /* width allowance · min(18rem, 100%) lets each cell expand to fit
     the longest per-line descriptor ("australian english",
     "Machine-translated") without wrapping, while never overflowing
     the grid column on narrowest mobile. */
  min-inline-size: min(18rem, 100%);
  /* .lang-cell is an <a> on the gate (real-link contract — works
     without js). reset the default underline so the row reads as
     an editorial choice, not a link. */
  text-decoration: none;
  color: inherit;
  transition: background .18s ease, color .18s ease;
}
.lang-cell:hover {
  background: color-mix(in srgb, var(--ink) 3%, transparent);
}
.lang-cell .lang-name {
  font-family: var(--serif);
  font-size: 22px;
  font-weight: 400;
  letter-spacing: -0.012em;
  color: var(--fg);
  display: inline-flex;
  align-items: baseline;
  gap: 0.45em;
}
.lang-cell:hover .lang-name {
  color: var(--accent-text);
}
.lang-cell .lang-name .arrow {
  font-family: var(--mono);
  font-size: 0.7em;
  color: var(--fg3);
  transition: transform .2s ease, color .2s ease;
}
.lang-cell:hover .lang-name .arrow {
  transform: translateX(3px);
  color: var(--accent-text);
}
.lang-cell .lang-caption {
  /* exactly two lines per descriptor — never three or four.
     vertical flex column with per-line nowrap children so the
     editorial imprint reads as a deliberate two-row stack rather
     than a phrase wrapping under a max-width budget. brief's exact
     values: gap .22rem, margin-top 1rem (breathing room from the
     language name above), letter-spacing .16em, line-height 1.15. */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.22rem;
  margin-top: 1rem;
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg3);
  line-height: 1.15;
  text-align: center;
}
/* the data-gate switch is logical — the per-line spans inside are
   the actual flex children of .lang-caption. display:contents
   makes the visible [data-gate] wrapper transparent to layout so
   the inner per-line <span>s become direct flex children. the
   site-wide [data-gate] visibility rules (around line ~7121) use
   display:inline; these higher-specificity rules override them
   inside the lang-caption so visible cells get display:contents
   while the wrong-language cell stays hidden. */
.lang-cell .lang-caption > [data-gate="en"] { display: contents; }
.lang-cell .lang-caption > [data-gate="fr"] { display: none; }
[data-preferred-lang="fr"] .lang-cell .lang-caption > [data-gate="en"] { display: none; }
[data-preferred-lang="fr"] .lang-cell .lang-caption > [data-gate="fr"] { display: contents; }
.lang-cell .lang-caption > [data-gate] > span {
  display: block;
  white-space: nowrap;
}

/* ─── verify menu body ──── five-row action menu. each row is a
   real <a> or <button>. .row.primary gets italic oxblood; the
   focus-visible state lays a 2px inset oxblood marker on the
   LEFT edge. "copied" is hidden by default and revealed when JS
   sets data-copied="true" on the row — inline, never a floating
   popover. ported from redesigns/modals/A-verify.html. */
.menu {
  border-top: var(--modal-rule);
}
.menu .row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  /* row padding tightened ~6% from 1rem → 0.94rem; touch-target
     floor preserved (~51px including 19px serif verb + line-height). */
  padding: 0.94rem clamp(28px, 4.4vw, 38px);
  border-bottom: 1px solid var(--rule-faint);
  background: transparent;
  border-left: 0;
  border-right: 0;
  border-top: 0;
  width: 100%;
  text-align: left;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  font: inherit;
  transition: background .15s ease;
}
.menu .row:last-child {
  border-bottom: 0;
}
.menu .row:hover,
.menu .row:focus-visible {
  background: color-mix(in srgb, var(--accent) 4%, transparent);
  outline: none;
}
.menu .row:focus-visible {
  box-shadow: inset 2px 0 0 var(--accent);
}
.menu .row .verb {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: 19px;
  color: var(--fg);
  line-height: 1.1;
  letter-spacing: -0.01em;
  display: inline-flex;
  align-items: baseline;
  gap: 0.45em;
  font-feature-settings: "onum" 1;
  transition: color .18s ease;
}
.menu .row:hover .verb,
.menu .row:focus-visible .verb {
  color: var(--accent-text);
}
.menu .row .verb .arr {
  font-family: var(--mono);
  font-style: normal;
  font-size: 0.7em;
  color: var(--fg3);
  transition: transform .2s ease, color .2s ease;
}
.menu .row:hover .verb .arr,
.menu .row:focus-visible .verb .arr {
  transform: translate(2px, -1px);
  color: var(--accent-text);
}
.menu .row .meta {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--fg3);
  text-align: right;
  white-space: nowrap;
  /* the citation preview is middle-truncated in js to fit a ~44ch
     budget, so the css end-ellipsis cap (max-width:18ch + overflow:
     hidden + text-overflow:ellipsis) is no longer needed — those
     rules cropped the most useful tail (the edition date). */
  font-variant-numeric: tabular-nums lining-nums;
}
/* responsive meta · keyboard shortcut on hover-capable devices,
   publication-language label on touch. media query matches the
   project precedent at @media (pointer: coarse) + (hover: none). */
.menu .row .meta .meta-keyboard { display: inline; }
.menu .row .meta .meta-touch    { display: none;   }
@media (hover: none), (pointer: coarse) {
  .menu .row .meta .meta-keyboard { display: none;   }
  .menu .row .meta .meta-touch    { display: inline; }
}
/* .row.primary keeps its class hook for any future visual treatment,
   but no longer carries a permanent accent — all five rows read at
   equal weight at rest; oxblood is reserved for hover/focus, applied
   by the .menu .row:hover .verb and :focus-visible .verb rules above. */
.menu .row .copied {
  display: none;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.menu .row[data-copied="true"] .meta {
  display: none;
}
.menu .row[data-copied="true"] .copied {
  display: inline;
}

/* ─── access modal body ──── short serif note + single italic
   oxblood underlined mailto. no CTA-button shape — the email is
   the affordance. ported from redesigns/modals/A-access.html. */
.ax-note {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 17px;
  line-height: 1.55;
  color: var(--fg);
  max-width: 32ch;
  margin: 0 auto 1.4rem;
  text-align: center;
  text-wrap: pretty;
  font-feature-settings: "onum" 1;
}
.ax-cta {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: 20px;
  color: var(--accent-text);
  text-decoration: none;
  border-bottom: 1px solid var(--accent);
  padding-bottom: 2px;
  transition: color .18s ease, border-color .18s ease;
}
.ax-cta:hover {
  color: var(--accent-hover);
  border-bottom-color: var(--accent-hover);
}

/* mobile · ≤520px bumps. larger × hit target (40px clears the
   44px touch floor with chrome that is type only) and a softer
   radius. row padding tightens; the meta caption drops below
   the verb on a second line, padding stays 22px sides. */
@media (max-width: 520px) {
  .shell {
    border-radius: 12px;
    padding: clamp(24px, 6vw, 32px);
  }
  .shell-close {
    width: 40px;
    height: 40px;
    font-size: 24px;
  }
  .lang-row {
    grid-template-columns: 1fr;
  }
  .lang-row .divider {
    width: auto;
    height: 1px;
  }
  .menu .row {
    padding: 1.05rem 22px;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.25rem;
  }
  .menu .row .meta {
    text-align: left;
    max-width: 100%;
  }
}

/* reduced motion is already handled site-wide by the global
   @media (prefers-reduced-motion: reduce) rule at line ~2707,
   which sets transition-duration: 0.01ms !important on *,
   *::before, *::after. that rule covers .modal-shell-scrim,
   .shell, and the publication-blur targets at zero new budget. */
}
@layer overrides {
html.is-language-switching {
  scroll-behavior: auto !important;
}

html.is-language-switching .principle,
html.is-language-switching .trajectory-item,
html.is-language-switching .project-card,
html.is-language-switching .hero-name,
html.is-language-switching .hero-statement,
html.is-language-switching .hero-body {
  transition: none !important;
  animation: none !important;
}

html.is-language-switching .nav {
  transition: none !important;
}

/* publication blur · when any modal is open, overlay.js sets
   body.modal-open on <body>. the publication beneath gets a
   filter: blur() so it recedes — NOT backdrop-filter on the scrim
   (forbidden by brief). selectors target the masthead (.site-header),
   the legacy .site wrappers and the #main landmark — whichever the
   page uses receives the blur, so the modal seal covers the entire
   publication including the header (per the unified-scrim brief).
   lives in @layer overrides because of the id selector (per
   css-architecture l4). */
body.modal-open .site-header,
body.modal-open .site,
body.modal-open .site-footer,
body.modal-open #main {
  filter: blur(var(--modal-blur));
  transition: filter var(--modal-motion);
}

/* ─── source-reader: controlled reader-box width ───────────────
   id hook (#source-view-root) caps the source-reader column at a book-page
   proportion so the code panel never stretches awkwardly on very wide
   viewports. chrome (intro, actions, document map, footer) and the code
   layout share the same max-width so nothing drifts off-axis. lives here
   in @layer overrides because the id selector belongs to the overrides
   register per the css-architecture brief (l4). */
.source-reader-page #source-view-root {
  max-width: 72rem;
  margin-inline: auto;
  /* containing block for the absolutely-positioned selection toolbar,
     so it floats within the source frame and scrolls with the code. */
  position: relative;
}

/* phase 71 · laptop continuity · cap the source-reader column at
   64rem on laptop so the code panel + hero share a deliberate axis
   and long source lines stop dictating the page feeling. */
@media (min-width: 1100px) {
  .source-reader-page #source-view-root {
    max-width: 64rem;
  }
}

/* ─── anchor landing offsets · home section ids ────────────────
   native anchor navigation lands the section heading below the
   sticky header with elegant breathing room. bare id selectors
   here in @layer overrides per the brief; specificity (1,0,0)
   is fine because the rule is intentionally narrow (four ids,
   one property, no other rule competes for these targets).

   no js scrollintoview, no scrollto, no preventdefault on
   anchor clicks — inithomenav() in app.js closes the menu but
   lets the browser perform native anchor navigation. css owns
   the offset register. */
@media (max-width: 760px) {
  #approach,
  #credentials,
  #trajectory,
  #projects,
  #contact {
    scroll-margin-top: var(--anchor-offset-mobile);
  }
}
@media (min-width: 761px) {
  #approach,
  #credentials,
  #trajectory,
  #projects,
  #contact {
    scroll-margin-top: var(--anchor-offset-desktop);
  }
}

/* ── .context-link · destination caption ─────────────────────
   reusable opt-in pattern for important navigational, trust,
   verification, identity and external links where a small
   bibliographic destination caption clarifies where the link
   leads. quieter than a tooltip, accessible without js, prints
   well, degrades to readable text without css.

   usage (do NOT apply indiscriminately — see definition-of-done
   guard at the bottom of the component):

     <a class="context-link" href="/integrity/"
        aria-label="Integrity, signed release records and verification">
       <span class="context-link__label">Integrity</span>
       <span class="context-link__meta" aria-hidden="true">/integrity/</span>
     </a>

   variants:
     .context-link--external    domain-level meta for off-site
     .context-link--compact     denser nav rows

   skip:  inline body prose, dl/dt/dd value cells, footer rows
          (footer rhythm is deliberately compact), small utility
          links where the path is noise, skip links, language
          buttons. */
.context-link {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  text-decoration: none;
  color: inherit;
}
.context-link__label {
  text-decoration-line: underline;
  text-decoration-thickness: 0.045em;
  text-underline-offset: 0.16em;
  text-decoration-color: currentColor;
}
.context-link__meta {
  max-inline-size: 100%;
  font-family: var(--mono);
  font-size: 0.68em;
  line-height: 1.2;
  letter-spacing: 0.035em;
  color: var(--fg3);
  opacity: 0.78;
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums lining-nums;
}
.context-link:hover .context-link__label,
.context-link:focus-visible .context-link__label {
  text-decoration-color: var(--accent);
}
.context-link:hover .context-link__meta,
.context-link:focus-visible .context-link__meta {
  color: var(--accent);
  opacity: 1;
}
.context-link:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 0.2rem;
  border-radius: 1px;
}

/* compact variant · use inside dense nav rows so the meta line
   doesn't add a second tall row of mono caption to every item. */
.context-link--compact {
  gap: 0.04rem;
}
.context-link--compact .context-link__meta {
  font-size: 0.62em;
  line-height: 1.1;
  opacity: 0.7;
}

/* external variant · differentiates off-site destinations via the
   meta tone, not via an icon or "opens in new tab" string. paired
   with aria-label that names the destination + "opens an external
   site" where the link uses target=_blank (we do not set _blank
   by default). */
.context-link--external .context-link__meta {
  letter-spacing: 0.04em;
}

/* print · keep the destination caption legible, not noisy. site-
   wide print rules already append href via `a[href^="http"]::after`
   in print.css — when a context-link is used we already have the
   destination visible in the meta line, so suppress the duplicate
   ::after output specifically for this pattern. */
@media print {
  .context-link {
    break-inside: avoid;
  }
  .context-link__meta {
    font-size: 8pt;
    opacity: 1;
    color: var(--fg3);
  }
  /* avoid double-printing destination · the meta line already
     carries the path/domain. */
  .context-link[href^="http"]::after,
  .context-link[href^="/"]::after,
  .context-link[href^="mailto:"]::after {
    content: none !important;
  }
}

/* ── /source/view/ — source code reader ────────────────────────
   archival inspection surface. warm paper background, mono type,
   muted line numbers. intentionally not ide-like. */

/* source code sizing — single source of truth.
   token classes change colour only; no size, weight, or spacing overrides.
   spacing rhythm — three tokens, used everywhere in this surface so that
   gaps between labels, annotation lines, and code groups read as one
   mathematical system. browser defaults are forbidden.
   the section-divider rhythm uses --source-rule-rise above the rule
   and --source-rule-fall below, so every annotation block (author
   comment, structural tag, generated) breathes identically. */
.source-reader-page {
  --source-code-size: clamp(0.78rem, 2.2vw, 0.92rem);
  --source-code-line-height: 1.55;
  --source-gap-xs: 0.45rem;
  --source-gap-sm: 0.85rem;
  --source-gap-md: 1.4rem;
  --source-rule-rise: 2.75rem;
  --source-rule-fall: 1.25rem;
}
/* the matching reader-box max-width rule lives at the top of this file in
   the existing top-level @layer overrides block (search "#source-view-root").
   ID-keyed selectors must live in @layer overrides per l4 of the css-
   architecture validator. */

/* reader intro: page-kicker + page-title.hero-stack.reader-h1 +
   page-lede.reader-description + meta. the hero pattern matches the
   privacy / security / source / integrity pages — global rules carry
   the scale; the rules below adjust spacing on the source-reader
   surface so the rhythm contract (--source-gap-*) is honoured. */
.source-reader-page .reader-intro {
  margin-block-end: var(--source-gap-md);
}
/* the page-kicker rule (defined globally) carries font / colour /
   case. only the bottom-margin is tightened so the kicker sits
   flush above the H1 within the reader's compact rhythm. */
.source-reader-page .reader-intro .page-kicker {
  margin: 0 0 var(--source-gap-xs) 0;
}
/* reader-h1 inherits the global .page-title.hero-stack scale.
   the contained <code> needs explicit overflow handling so long
   paths like /integrity/releases/2026-05-09/index.html wrap at
   any character instead of overflowing the card. */
.source-reader-page .reader-intro h1.reader-h1 {
  margin: 0 0 var(--source-gap-sm) 0;
}
.reader-h1 code {
  font-family: inherit;
  font-weight: inherit;
  background: none;
  padding: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
  line-height: 1.05;
}
/* description carries .page-lede (global scale) plus this scope-
   level adjustment for line-height + bottom-margin. font-family
   and size are inherited from .page-lede so the description
   matches the editorial register on the other hero pages. */
.source-reader-page .reader-description {
  color: var(--fg2);
  margin: 0 0 var(--source-gap-xs) 0;
  line-height: 1.45;
}
.source-reader-page .reader-meta {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--fg3);
  margin: var(--source-gap-xs) 0 0;
}
.source-reader-page .reader-meta abbr {
  text-decoration: none;
  cursor: default;
}
.source-reader-page .reader-meta-date {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--fg3);
  margin: var(--source-gap-xs) 0 0;
}
/* reader-intent — one quiet italic-serif conceptual line under the file
   description. interpretive content: hidden in raw mode (see body[data-
   source-mode="raw"] .reader-intent rule above). */
.source-reader-page .reader-intent {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(0.85rem, 2vw, 0.95rem);
  color: var(--fg3);
  margin: var(--source-gap-xs) 0 0;
  line-height: 1.5;
  opacity: 0.85;
}

/* action row: canonical · verify · raw · copy code · source / annotated · wrap lines */
.source-reader-page .reader-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0;
  margin-block-end: var(--source-gap-md);
}

/* phase 83 · source reader title is the page h1.
   the viewed file name is the page's primary subject and deserves
   hero authority. serif (inherited from .page-title), hero-scale
   size, tight letter-spacing for long path names. desktop / tablet /
   mobile share the same family + line-height; only size shifts. */
.source-reader-page .reader-h1.reader-h1--source {
  font-family: var(--serif);
  font-size: var(--source-title-size-mobile);
  font-weight: 300;
  line-height: 0.96;
  letter-spacing: -0.025em;
  color: var(--fg);
  max-width: 18ch;
  text-wrap: pretty;
  overflow-wrap: anywhere;
  word-break: normal;
}
.source-reader-page .reader-h1.reader-h1--source code {
  font-family: inherit;
  font-weight: inherit;
  font-size: inherit;
  letter-spacing: inherit;
  background: none;
  padding: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.source-reader-page .reader-description {
  font-size: clamp(14px, 3.6vw, 16px);
  line-height: 1.45;
  max-width: 38ch;
}
.source-reader-page .reader-meta,
.source-reader-page .reader-meta-date {
  margin-top: 0.35rem;
}

@media (min-width: 760px) and (max-width: 1099.98px) {
  .source-reader-page .reader-h1.reader-h1--source {
    font-size: var(--source-title-size-tablet);
    max-width: 20ch;
  }
  .source-reader-page .reader-description {
    font-size: clamp(15px, 1.8vw, 17px);
    max-width: 48ch;
  }
  .source-reader-page .reader-intro .page-kicker {
    margin-bottom: var(--hero-kicker-gap);
  }
  .source-reader-page .reader-intro h1.reader-h1 {
    margin-bottom: 0.7rem;
  }
  .source-reader-page .reader-description {
    margin-bottom: 0.35rem;
  }
}

/* phase 83 · desktop · further bump the source reader h1 so it
   reads at full hero scale alongside the description and reading-
   mode meta below. */
@media (min-width: 1100px) {
  .source-reader-page .reader-h1.reader-h1--source {
    font-size: var(--source-title-size-desktop);
    max-width: 24ch;
  }
}

/* code shell — horizontal scroll wrapper, never overflows the page.
   on mobile (≤700px) the wrap toggle defaults to on (set by source-view.js),
   so the shell width simply fills the viewport with no inner scroll. on
   desktop, the shell sits inside the 72rem reader-box and any line that
   exceeds the column scrolls within this wrapper, never the page. */
.code-shell {
  /* archival panel · soft hairline + soft radius matching .verify-card
     and .edition-panel so the source reader sits as the same family
     of mounted paper rather than as a developer-tool boxed widget. */
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid color-mix(in srgb, var(--rule) 55%, transparent);
  border-radius: var(--radius-soft);
  background: var(--surface-archival);
}
.code-reader {
  margin: 0;
  padding-block: clamp(1.15rem, 3vw, 1.6rem);
  /* on mobile the code panel respects the iphone home-bar / notch insets so
     the reader never feels optically pressed against the viewport edge. */
  padding-inline: max(clamp(0.875rem, 3vw, 1.25rem), env(safe-area-inset-left));
  font-family: var(--mono);
  font-size: var(--source-code-size);
  line-height: var(--source-code-line-height);
  background: transparent;
  overflow-x: visible;
}
/* hard size lock — every character in the code panel shares identical metrics.
   token classes may change colour only; no size, weight, letter-spacing, or
   text-transform overrides are permitted. */
.code-reader,
.code-reader code,
.code-reader span,
.code-line,
.line-code,
.line-code * {
  font-family: var(--mono);
  font-size: var(--source-code-size);
  line-height: var(--source-code-line-height);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
}
.code-reader code {
  display: block;
}
/* line layout: 4ch number column + flexible code column.
   align-items: start keeps line number at top of wrapped lines. */
.code-line {
  display: grid;
  grid-template-columns: 4ch minmax(0, 1fr);
  column-gap: 0.8rem;
  align-items: start;
  /* coarse-pointer devices treat the row as a single tap target (the
     touch branch of the click handler accepts closest('.code-line')).
     touch-action: manipulation removes the legacy 300ms tap delay on
     ios safari and suppresses double-tap-to-zoom on rows (pinch-zoom
     remains for whole-page zoom). tap-highlight-color: transparent
     replaces the gray system flash with the editorial range-active
     background. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

@media (pointer: coarse) {
  .code-line { cursor: pointer; }
}
/* line-number is an <a> tag — reset link styles, registration-mark quiet */
a.line-number {
  /* phase 85 · sticky gutter. when wrap is off and a long line forces
     horizontal scroll inside .code-shell, the line-number used to
     scroll out of view alongside the code, exposing the grid column
     boundary as a visible vertical seam against the panel background.
     position: sticky; left: 0 anchors the number cell to the gutter
     as the code column scrolls; the matching background covers the
     code text scrolling underneath so the panel reads as one
     continuous surface again. z-index keeps the gutter above the
     scrolled code. wrap-mode behaviour unchanged because there is no
     horizontal scroll to engage with. */
  position: sticky;
  left: 0;
  display: block;
  text-align: right;
  text-decoration: none;
  color: var(--fg3);
  background: var(--surface-archival);
  user-select: none;
  -webkit-user-select: none;
  opacity: 0.42;
  flex-shrink: 0;
  z-index: 1;
  font-variant-numeric: tabular-nums lining-nums;
}
/* §/number crossfade on line hover */
.ln-num,
.ln-sym {
  display: block;
  text-align: right;
  transition: opacity 0.12s;
}
.ln-sym {
  position: absolute;
  inset: 0;
  opacity: 0;
  color: color-mix(in srgb, var(--accent) 55%, var(--fg3));
  font-style: normal;
}
.code-line:hover a.line-number {
  opacity: 0.85;
}
.code-line:hover .ln-num {
  opacity: 0;
}
.code-line:hover .ln-sym {
  opacity: 1;
}
/* wrap state — default off: preserve original line integrity, horizontal scroll allowed.
   when on: wrap within the code column; continuation indents under code, not number. */
.code-reader:not(.is-wrapped) .line-code {
  white-space: pre;
}
.code-reader.is-wrapped .line-code {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.line-code {
  color: var(--fg);
}

/* focus line — warm parchment register, not editor blue.
   the active highlight is interpretive (it is a citation overlay, not
   source content), so it fades after 2s (controlled by source-view.js)
   and collapses to a persistent gutter marker: a 2px paper-warm rule
   along the left edge of each cited line. the citation is still
   locatable without the page shouting. */
.code-line--range-active {
  background: color-mix(in srgb, var(--accent) 8%, color-mix(in srgb, var(--paper-main) 60%, transparent));
  outline: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
  outline-offset: 1px;
  border-radius: 2px;
  min-width: 100%;
  transition: background-color var(--transition-quiet), outline-color var(--transition-quiet);
}
.code-line--range-marked {
  position: relative;
}
.code-line--range-marked::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: -0.4rem;
  width: 2px;
  background: color-mix(in srgb, var(--accent) 60%, var(--fg3));
  opacity: 0.7;
  pointer-events: none;
}
/* single-line :target retains a subtler version of the active highlight
   so direct hits still feel anchored. js adds .code-line--range-active
   for fade behaviour; this rule covers the cold-load case where only
   the url hash is in play. */
.code-line:target {
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  border-radius: 2px;
  outline: 1px solid color-mix(in srgb, var(--accent) 18%, transparent);
  outline-offset: 1px;
  min-width: 100%;
}

/* in-source links — when the source viewer detects a routed url inside
   the rendered code (href="/privacy/", https://trentpower.fr/styles.css,
   etc.) it splits the matching text node and inserts an anchor pointing
   at the corresponding source mirror in this same reader. the visual
   register is intentionally quiet: dotted underline, current colour,
   no token bracket. the goal is "this can be opened", not "this is a
   hyperlink". focus state lifts to the accent for one frame so keyboard
   readers can locate the active target. */
a.source-link,
a.source-link:visited {
  color: inherit;
  text-decoration: underline dotted color-mix(in srgb, var(--accent) 35%, var(--bd-soft));
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
a.source-link:hover {
  text-decoration-color: var(--accent);
}
a.source-link:focus-visible {
  outline: 1px solid color-mix(in srgb, var(--accent) 60%, transparent);
  outline-offset: 2px;
  border-radius: 1px;
}

/* selection toolbar — a contextual floating action bar, not a fixture.
   it has no presence at all until at least one line is selected: no
   reserved row, no gap above the code. on selection it appears as a
   compact pill near the selection.

   desktop: position: absolute inside the source frame (#source-view-
   root, position: relative), placed just above the first selected line
   by source-view.js. being absolute inside the frame, it is glued to
   the document and scrolls with the code — no scroll handler needed.
   the js writes --st-top / --st-left; the mobile rule below ignores
   them.

   mobile (<= 720px): position: fixed, a centred bottom action bar
   clear of the ios home indicator.

   register: a quiet translucent pill — soft warm surface, hairline
   rule, gentle lift. the count and actions inside read as one line of
   mono text — "8 lines · copy · copy link · clear". */
.source-reader {
  display: block;
}
.source-toolbar {
  display: none;
  position: absolute;
  top: var(--st-top, 0);
  left: var(--st-left, 0);
  /* below the fixed nav (100) and the cite modal (200). */
  z-index: 80;
  inline-size: max-content;
  max-inline-size: min(92vw, 42rem);
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.7rem;
  /* the site's standard soft corner — same radius as the code panel,
     verify card and edition panel, so the bar reads as site furniture
     rather than a free-floating pill. */
  border-radius: var(--radius-soft);
  /* theme-aware translucent surface — works light and dark. */
  background: color-mix(in srgb, var(--paper-raised-high) 94%, transparent);
  border: 1px solid var(--rule-soft);
  box-shadow: 0 6px 20px color-mix(in srgb, var(--ink) 14%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  font-family: var(--mono);
  font-size: 0.74rem;
  line-height: 1.3;
  letter-spacing: 0.01em;
  color: var(--fg3);
}
.source-toolbar.is-visible {
  display: flex;
}
/* the running selection count — an archival label, not a badge. */
.source-toolbar__count {
  color: var(--fg3);
  font-variant-numeric: tabular-nums;
}
/* low-contrast middot dividers. aria-hidden in the markup so screen
   readers never voice them between the actions. */
.source-toolbar__sep {
  margin-inline: 0.4rem;
  color: var(--fg3);
  opacity: 0.6;
  user-select: none;
}
/* actions are text, not buttons — transparent, unboxed, mono; weight
   carried by typography and a quiet hover, never by chrome. */
.source-toolbar__btn {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  margin: 0;
  padding: 0;
  font: inherit;
  color: var(--fg2);
  cursor: pointer;
  text-decoration: none;
  transition: color 0.15s;
}
.source-toolbar__btn:hover {
  color: var(--accent-text);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
/* copy confirmation — js adds .is-copied to the pressed action for a
   moment after a successful copy; the existing color transition eases
   the burgundy flash in and out. */
.source-toolbar__btn.is-copied {
  color: var(--accent-text);
}
.source-toolbar__btn:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-colour);
  outline-offset: 2px;
  border-radius: 1px;
}
/* mobile — a centred bottom action bar. position: fixed overrides the
   desktop absolute placement; top: auto drops the --st-top anchor.
   the blur is dropped here: ios safari fails to composite a
   position: fixed element that carries a backdrop-filter — it has
   layout but paints nothing. an opaque surface renders reliably. */
@media (max-width: 720px) {
  .source-toolbar {
    position: fixed;
    inset-block: auto calc(env(safe-area-inset-bottom, 0px) + 1rem);
    inset-inline: 1rem;
    inline-size: auto;
    max-inline-size: none;
    justify-content: center;
    background: var(--paper-raised-high);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}
/* coarse pointers — comfortable tap targets via padding alone, so the
   actions stay text-like and never reacquire button chrome. */
@media (pointer: coarse) {
  .source-toolbar {
    font-size: 0.8rem;
  }
  .source-toolbar__btn {
    padding-block: 0.5rem;
    padding-inline: 0.3rem;
  }
}
/* subtle entrance — a calm opacity fade as the bar appears on
   selection; direction-neutral so it suits both the desktop pill and
   the mobile bottom bar. */
@media (prefers-reduced-motion: no-preference) {
  .source-toolbar.is-visible {
    animation: source-toolbar-in 160ms ease-out both;
  }
}
@keyframes source-toolbar-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* visually-hidden aria-live region for selection announcements. the
   toolbar count already carries aria-live="polite" for the running
   selection size; this one is for transient confirmations ("copied 3
   lines", "link copied"), kept off-screen so the layout stays calm. */
.source-announcer {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  border: 0;
  white-space: nowrap;
}

/* section divider — single unified rhythm for every annotation block
   (author comments, structural tags, generated markers). one contract:
       margin-block: var(--source-rule-rise) var(--source-rule-fall)
   so 2.75rem rises above the rule, 1.25rem falls below the label, no
   matter where the marker came from. author / structure / tier variants
   adjust only opacity and letter-spacing — never the rhythm itself. */
.section-divider,
.code-reader .section-divider {
  display: block;
  margin-block: var(--source-rule-rise) var(--source-rule-fall);
  padding-block: var(--source-gap-sm) 0;
  padding-inline: 0;
  border-top: 1px solid color-mix(in srgb, var(--bd-soft) 22%, transparent);
  font-family: var(--mono);
  font-size: 0.68rem;
  line-height: 1.3;
  font-weight: 400;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--fg3);
  user-select: none;
}
.section-divider:first-child {
  margin-block-start: 0;
  border-top: none;
  padding-block-start: 0;
}
/* contrast lifted ~7–10% so labels sit calmly visible rather than
   washed out, while the author/structure differentiation is
   preserved. paint colour stays the same fg3 token; only opacity
   shifts. */
.section-divider--author    { opacity: 0.92; }
.section-divider--structure { opacity: 0.78; }

/* density tiers — three editorial weights, sharing the same rhythm.
   tier-1 (major structural anchors: head / main / footer / body / header
   / structured): wider letter-spacing and a touch more font size — reads
   as a chapter-marker register.
   tier-2 (architectural notes): the baseline section-marker register.
   tier-3 lives on the gloss span (italic serif, quieter still) — the
   inline semantic note that follows the marker.
   no tier changes the vertical rhythm: the rise/fall contract is one
   for all three so the system never gains a second spacing register.
   the .code-reader prefix lifts the tier selector above the hard size
   lock on .code-reader span so the tier font-size actually applies. */
.code-reader .section-divider--tier-1 {
  font-size: 0.78rem;
  letter-spacing: 0.18em;
  font-weight: 500;
}
.code-reader .section-divider--tier-2 {
  font-size: 0.68rem;
  letter-spacing: 0.10em;
}

.section-divider-label {
  /* the label itself — inherits all type metrics from the divider.
     content provenance: this text is sourced from the file itself
     (an html comment or a structural tag). it is the canonical layer. */
  display: inline;
}

/* the body span wrapping a section's code-lines. renders as a flow
   region (display: block) inside the <pre><code> stream; the hidden
   attribute folds it. in raw mode we override hidden so source content
   is never invisible regardless of the annotated-mode collapse state. */
.source-section-body {
  display: block;
}
body[data-source-mode="raw"] .source-section-body[hidden] {
  display: block;
}

/* in raw mode and on oversize files: the reader-view-toggle disclosure
   control is suppressed. source content remains visible via the
   .source-section-body[hidden] override above. */
body[data-source-large="true"] .reader-view-toggle {
  display: none;
}

/* annotation gloss — interpretive layer (tier-3, the editorial micro line).
   provenance contract: this text is generated by the reader, not authored
   in the source file. the em-dash mark and italic-serif treatment make
   the boundary visible to the reader: source-derived labels are mono
   uppercase, generated glosses are serif italic with a leading dash, so
   the eye distinguishes canonical from interpretive at a glance.
   hidden by default; revealed only under body[data-source-mode="annotated"]. */
.section-divider-gloss,
.code-reader .section-divider .section-divider-gloss,
.code-reader code .section-divider .section-divider-gloss {
  display: none;
  margin-inline-start: var(--source-gap-sm);
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--fg3);
  opacity: 0.82;
  font-size: 0.85rem;
  line-height: 1.45;
}
.section-divider-gloss-mark,
.code-reader .section-divider .section-divider-gloss-mark,
.code-reader code .section-divider .section-divider-gloss-mark {
  /* the em-dash provenance mark — typographic signal that what follows
     is editorial annotation, not source content. */
  opacity: 0.55;
  font-style: normal;
  margin-inline-end: 0.05em;
}
body[data-source-mode="annotated"] .section-divider-gloss,
body[data-source-mode="annotated"] .code-reader .section-divider .section-divider-gloss,
body[data-source-mode="annotated"] .code-reader code .section-divider .section-divider-gloss {
  display: inline;
}
body[data-source-mode="annotated"] .section-divider[data-collapsed="true"] .section-divider-gloss {
  display: none;
}
@media (max-width: 36rem) {
  body[data-source-mode="annotated"] .section-divider-gloss,
  body[data-source-mode="annotated"] .code-reader .section-divider .section-divider-gloss,
  body[data-source-mode="annotated"] .code-reader code .section-divider .section-divider-gloss {
    display: block;
    margin-inline-start: 0;
    margin-block-start: var(--source-gap-xs);
  }
  body[data-source-mode="annotated"] .section-divider[data-collapsed="true"] .section-divider-gloss {
    display: none;
  }

  .source-reader-page .page {
    padding-bottom: calc(3rem + env(safe-area-inset-bottom));
  }
}

/* ── mode separation ─────────────────────────────────────────────
   raw mode is the literal canonical mirror. hide every interpretive
   surface (annotations, section dividers, document map, intent line,
   synthetic spacing) so what the reader sees is the source file as
   bytes, with line numbers + syntax highlighting only.
   annotated mode lifts these surfaces back into view. */
body[data-source-mode="raw"] .section-divider,
body[data-source-mode="raw"] .reader-intent,
body[data-source-mode="raw"] .inspection-path {
  display: none;
}
body[data-source-mode="raw"] .code-line--section-marker {
  /* in raw mode the line that originally carried a divider is just a
     normal source line — no synthetic soften, no synthetic spacing. */
  opacity: 1;
  margin-block-end: 0;
}
body[data-source-mode="raw"] .code-line--comment-lead {
  /* no inserted vertical rhythm in raw mode — preserve literal source
     spacing exactly. the interpretive breathing room belongs to the
     annotated layer. */
  margin-block-start: 0;
}
body[data-source-mode="raw"] .source-reader-layout {
  /* raw mode collapses the layout grid to a single column. */
  grid-template-columns: 1fr;
}
/* the source comment line directly below a divider — soften further in
   annotated mode only, so the marker reads as the dominant entry point
   of the new section. in raw mode the rule above already overrides. */
.code-line--section-marker {
  opacity: 0.7;
  margin-block-end: 0.3em;
}

/* syntax token colours — editorial hierarchy, not ide rainbow.
   only four classes carry colour: tags, attrs, comments, strings.
   keywords, numbers, and punctuation return to neutral fg so the
   reader stays calm over long reading sessions.
   token classes may change colour only. font metrics (size, line-height,
   weight, letter-spacing, text-transform) are forced to inherit so the
   hard size lock further up is never broken by accident or override. */
.tok-tag,
.tok-attr,
.tok-string,
.tok-comment,
.tok-keyword,
.tok-number,
.tok-punctuation {
  font-size: inherit;
  line-height: inherit;
  font-weight: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
}
/* source reader · token colours · v2 — illuminated-manuscript model.
   ink for structure, oxblood reserved for trust claims, two jewel
   pigments for the meaningful payload (lapis/sage for strings,
   purple/lavender for numbers), italic for marginalia. hierarchy
   runs on three axes — colour, weight, value — never colour alone.
   the tokenizer's semantic classes are now visible. */
.tok-tag         { color: var(--fg2); }
.tok-attr        { color: color-mix(in srgb, var(--fg) 60%, var(--fg3)); }
.tok-string      { color: var(--code-string); }
.tok-comment     { color: var(--fg2); font-style: italic; }
.tok-keyword     { color: var(--fg); font-weight: 500; }
.tok-number      { color: var(--code-number); }
.tok-punctuation { color: var(--fg3); }


.code-line--comment-technical .tok-comment {
  color: color-mix(in srgb, var(--fg2) 82%, var(--fg3) 18%);
  letter-spacing: -0.006em;
}
.code-line--comment-generated .tok-comment {
  color: color-mix(in srgb, var(--fg2) 88%, transparent);
  letter-spacing: -0.004em;
}
.code-line--comment-editorial-prose .tok-comment {
  color: color-mix(in srgb, var(--fg2) 76%, var(--accent) 24%);
  letter-spacing: -0.01em;
  font-style: italic;
  opacity: 0.92;
  line-height: 1.62;
}
.code-line--rhythm-break { margin-block-start: 0.42rem; }
@media (max-width: 42rem) { .code-line--rhythm-break { margin-block-start: 0.52rem; } }
.reader-view-mode,.source-section-body,.section-divider-gloss,.inspection-path-link,.code-line,.line-code {
  transition: opacity var(--transition-quiet), background-color var(--transition-quiet), border-color var(--transition-quiet);
}
body[data-source-mode="annotated"] .section-divider-gloss { opacity: 0.82; }
body[data-source-mode="raw"] .section-divider-gloss { opacity: 0; }
/* semantic highlighting — restrained typographic register, not ide theme.
   colour-only delta, no size/weight changes. the reader teaches
   architecture visually:
     · structural tags  (<header>, <main>, <footer>, …)   – a touch sharper
     · metadata tags    (<meta>, <link>, <title>, …)      – quieter
     · trust attributes (integrity, crossorigin, rel="canonical", …)
       carry a subtle oxblood (--accent) tint
     · accessibility attributes (aria-*, role, tabindex)  – soft highlight
   each treatment is one step away from the base token colour so the
   page never reads as a syntax-highlighter demo. */
/* structural tags read as full ink — these are the architecture of
   the document. mono 500 promotes them one step above ordinary tags. */
.tok-tag--structural {
  color: var(--fg);
  font-weight: 500;
}
/* head-only tags describe rather than render — quieter. */
.tok-tag--metadata {
  color: var(--fg3);
}
/* trust register: attribute names and promoted string values that
   carry cryptographic / canonical meaning. uses --accent-text (not
   --accent) so the rule renders correctly in both themes; --accent in
   dark drops below aa on coal paper, while --accent-text is the
   theme-aware lifted register. */
.tok-attr--trust,
.tok-string--trust {
  color: var(--accent-text);
  font-weight: 500;
}
/* accessibility register: attribute name and its value read as one
   phrase — same soft-ink register on both sides of the equals sign. */
.tok-attr--a11y {
  color: color-mix(in srgb, var(--fg) 78%, var(--accent));
}
.tok-string--a11y {
  color: color-mix(in srgb, var(--fg) 60%, var(--fg2));
}

/* breathing room before comment blocks — editorial section dividers */
.code-line--comment-lead {
  margin-block-start: 0.75em;
}
/* range highlight — subtle accent tint for #L12-L24 deep-link selections */
.code-line--range-highlight {
  background-color: color-mix(in srgb, var(--accent) 7%, transparent);
  border-radius: 2px;
}
.code-line--range-highlight a.line-number {
  opacity: 0.72;
}

/* closing editorial footer — the seal line, the validated edition,
   and a single closure row: canonical · verify · plain text · top. */
.source-reader-footer {
  margin-block-start: clamp(2rem, 5vw, 3rem);
  padding-block-start: 1rem;
  border-top: 1px solid color-mix(in srgb, var(--bd-soft) 40%, transparent);
}
.footer-end {
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--fg3);
  margin: 0;
  letter-spacing: 0.04em;
  opacity: 0.92;
}
.footer-edition {
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--fg3);
  opacity: 0.7;
  margin: 0.25rem 0 0;
}
.footer-links {
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--fg3);
  margin: 0.7rem 0 0;
  letter-spacing: 0.04em;
}
.footer-links a {
  color: var(--fg3);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--bd-soft) 60%, transparent);
}
.footer-links a:hover,
.footer-links a:focus-visible {
  color: var(--fg2);
  border-bottom-color: color-mix(in srgb, var(--fg2) 70%, transparent);
}
.footer-links a.back-to-top-inline {
  /* visually identical to the other closure links — no special
     treatment, the position in the row is its own meaning. */
  font-weight: 400;
}
/* legacy class kept for any cached markup; the new closure renders
   the top link inline above. */
.back-to-top {
  display: none;
}

/* loading / error / unknown states */
.reader-loading,
.reader-error {
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--fg3);
  margin-block: 1rem;
}
.reader-unknown {
  padding-block: 2rem;
}

/* one-line conceptual sentence under the file description — sits as
   a quiet authorial line, never an instruction. serif italic so the
   reader hears it as the publication's voice, not as ui. high
   specificity so global p reset / font-style: normal cannot win. */
.source-reader-page .reader-intro .reader-intent {
  margin: 0.4rem 0 1rem;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(15px, 1.6vw, 16px);
  line-height: 1.55;
  color: var(--fg3);
  max-width: 54ch;
  opacity: 0.92;
}

/* view mode toggle — source (default) / annotated. quiet inline
   segmented control in the actions row. no fills, no rounded pills,
   no animations — reads as a typographic switch, not a ui control. */
.reader-view-toggle {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  margin-inline-start: 0;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  color: var(--fg3);
}
.reader-view-toggle-label {
  /* small contextual eyebrow that names what the two buttons control.
     uppercase mono, calm letter-spacing — the user reads "reading mode"
     and the toggle below it, so the action is interpretation, not
     navigation to another file. */
  font-size: 0.6rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg3);
  opacity: 0.72;
  margin-inline-end: 0.2rem;
}
.reader-view-mode {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  padding: 0 0.1em;
  margin: 0;
  font: inherit;
  letter-spacing: inherit;
  color: var(--fg3);
  cursor: pointer;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 0.12s, border-bottom-color 0.12s, opacity 0.12s;
}
.reader-view-mode + .reader-view-mode {
  margin-inline-start: 0.1rem;
}
.reader-view-mode + .reader-view-mode::before {
  content: '\B7';
  margin-inline-end: 0.45rem;
  margin-inline-start: 0.05rem;
  opacity: 0.45;
}
.reader-view-mode.is-active,
.reader-view-mode[aria-pressed="true"] {
  color: var(--fg2);
  border-bottom-color: color-mix(in srgb, var(--fg2) 60%, transparent);
}
.reader-view-mode:hover,
.reader-view-mode:focus-visible {
  color: var(--fg2);
  outline: 0;
}

/* document map (inspection path) — rendered above the code shell on
   narrow viewports; hidden on wide viewports where the sticky
   minimap takes over (see :has() rule below).

   on mobile the map reads as a calmly wrapped block of printed
   index terms — each term sits on its own baseline rule, no fills,
   no rounded pills, no hover theatrics. compare with index entries
   in the back of a printed archive: term, hairline, term, hairline. */
.inspection-path {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--fg3);
  letter-spacing: 0.04em;
  margin-block-end: var(--source-gap-md);
  line-height: 1.5;
}
.inspection-path-label {
  margin: 0 0 var(--source-gap-xs);
  font-size: 0.6rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--fg3);
  opacity: 0.78;
}
.inspection-path-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--source-gap-md);
  row-gap: 0;
}
.inspection-path-item {
  flex: 0 1 auto;
}
/* index-term link. opacity 0.62 by default keeps every term quieter than the
   surrounding chrome; the .is-current class lifts opacity slightly so the
   eye anchors on the section the reader is presently passing through. no
   sticky-toc affordance, no animation theatrics — purely optical. */
.inspection-path-link {
  display: inline-block;
  padding-block: var(--source-gap-xs);
  color: var(--fg2);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--fg3) 55%, transparent);
  opacity: 0.62;
  transition: color 0.18s, border-bottom-color 0.18s, opacity 0.18s;
}
.inspection-path-link:hover,
.inspection-path-link:focus-visible {
  color: var(--fg2);
  border-bottom-color: color-mix(in srgb, var(--fg2) 70%, transparent);
  opacity: 1;
  outline: 0;
}
.inspection-path-link.is-current {
  opacity: 1;
  color: var(--fg);
  border-bottom-color: color-mix(in srgb, var(--fg) 65%, transparent);
}
/* source-reader layout: a single column on every viewport. the inline
   inspection-path above the code carries the document map; there is no
   sticky sidebar form (minimaps signal ide chrome rather than editorial
   publication, so they have been removed deliberately). */
.source-reader-layout {
  display: grid;
  grid-template-columns: 1fr;
  align-items: start;
}

/* mobile: safe-area bottom padding; font-size governed by the clamp rule above */

/* ── /sw-reset/ — local recovery utility ──────────────────────
   the page reuses the editorial record-grid system from /verify/
   via `.sw-reset-page` scope (see :is(.verify-page, .sw-reset-page, .security-page, .source-page)
   rules above). only the local marker tag survives here as a quiet
   eyebrow above the page title. the reset button is rendered as a
   record-actions item so it reads as a quiet mono action, not a
   browser-default form button. */
/* phase 87 · .sw-reset-marker retired. the page now uses the shared
   .page-kicker register; the bundle 5 .page-body .page-kicker rule
   keeps it mono even though the kicker on /sw-reset/ sits inside
   .page-body. */

/* phase 87 · sw-reset reset button. previously had zero styling and
   rendered as a bare browser default that read as faint/disabled.
   editorial action register: mono uppercase, soft warm surface,
   1px rule border, oxblood on focus. min-height 36px for touch.
   disabled state visibly dimmed only when the JS sets disabled
   after a successful clear. */
.sw-reset-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 0.74rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg);
  background: var(--paper-raised-high);
  border: 1px solid var(--bd-soft);
  border-radius: 8px;
  padding: 0.55rem 0.9rem;
  min-height: 36px;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.sw-reset-btn:hover,
.sw-reset-btn:focus-visible {
  color: var(--accent-text);
  border-color: color-mix(in srgb, var(--accent) 50%, var(--bd-soft));
  background: var(--paper-main);
  outline: 0;
}
.sw-reset-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  color: var(--fg3);
  background: var(--paper-raised-high);
  border-color: var(--bd-soft);
}

/* sw-reset action row: reset button plus the home and reload links
   sit on one row with a quiet rule above separating them from the
   status dl. flex-wrap lets the row collapse onto two rows at narrow
   ipad widths without losing rhythm. */
.sw-reset-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(0.9rem, 2.5vw, 1.4rem);
  margin-top: clamp(0.9rem, 2vh, 1.25rem);
  padding-top: clamp(0.9rem, 2vh, 1.25rem);
  border-top: 1px solid var(--rule-faint);
}
.sw-reset-link {
  font-size: 0.78rem;
}

.verify-intro-fpr {
  word-break: break-word;
}

  /* source reader stabilization pass */
  .reader-view-toggle { flex-wrap: wrap; row-gap: 0.35rem; }
  .reader-view-mode--wrap { white-space: nowrap; }
  .reader-view-mode--copy { white-space: nowrap; }
  @media (max-width: 520px) {
    .reader-view-toggle-label { flex-basis: 100%; margin-inline-end: 0; }
    .reader-view-mode--wrap,
    .reader-view-mode--copy { flex: 0 0 auto; }
  }
  .section-divider { margin-block: 1rem 0.45rem; border-top-color: color-mix(in srgb,var(--bd-soft) 14%,transparent); }
  .code-line--range-active { outline: 0; border-radius: 0; background: color-mix(in srgb,var(--accent) 9%, var(--paper-main)); }
  .code-line--range-marked::before { inset-inline-start: -0.32rem; }
  .code-line--range-active:first-of-type { border-top-left-radius:2px;border-top-right-radius:2px; }
  .code-line--range-active:last-of-type { border-bottom-left-radius:2px;border-bottom-right-radius:2px; }
  .source-reader-footer { margin-block: clamp(3rem,6vw,4rem) calc(clamp(2.5rem, 8vw, 4rem) + env(safe-area-inset-bottom)); }
  .footer-rendered-link { margin-top: 1rem; }
}

/* Page-scoped rules collected here so the cascade-layer order
   makes them defeat any same-specificity components rule, and so
   readers can see all body[data-page=…] declarations in one place. */
@layer pages {
  body[data-page="security"] .page-body { max-width: 720px; }

  /* phase 71 · laptop continuity · security widens to read as a
     document not a receipt. hero keeps its measure; only the body
     widens at >= 1100. */
  @media (min-width: 1100px) {
    body[data-page="security"] .page-body { max-width: 880px; }
  }

  /* /integrity/ · quiet the footer so the signed-release card stays
     the dominant object. the seam softens a step without disappearing
     entirely. */
  body[data-page="integrity"] .site-footer {
    border-top-color: color-mix(in srgb, var(--ink) 8%, transparent);
  }

  /* /integrity/ · hero block sits tighter to the release card. drop
     the page-body bottom margin so the lede flows into the card
     with editorial cadence rather than dashboard spacing. */
  body[data-page="integrity"] .page-lede {
    margin-bottom: clamp(1.4rem, 3vw, 2.2rem);
  }

  /* nav disclosure system retired — the homepage and every other
     page now present only the masthead at the top. no toggle, no
     panel, no responsive disclosure rules. mobile masthead spacing
     is owned by the @layer components @media rules above. */

  /* mobile / iOS · anchor the homepage hero text strongly above the
     safari bottom chrome so the first viewport reads as one composed
     editorial frame. svh already governs min-height; this pad
     respects env(safe-area-inset-bottom) so chrome retract/extend
     stays neutral and the text never sits behind the address bar.
     desktop unchanged. */
  @media (max-width: 700px) {
    [data-page="home"] .hero {
      padding-bottom: max(5rem, calc(env(safe-area-inset-bottom) + 4rem));
    }
  }

  /* desktop · lift the homepage hero ~8vh off the bottom anchor so
     the H1 reads as deliberately placed in the upper-middle of the
     viewport rather than suspended in the lower half. the hero is
     justify-content: flex-end (set in @layer components), so the
     visible content shifts upward as padding-bottom grows. tablet
     range (701–959px) intentionally keeps the original padding so
     iPad portrait/landscape composition is unchanged; mobile keeps
     its safe-area-inset rule above. */
  @media (min-width: 960px) {
    [data-page="home"] .hero {
      padding-bottom: clamp(140px, 18vh, 260px);
    }
  }

  /* short-page footer · 403 / 404 / 500 / sw-reset. without this,
     the footer floats wherever the short content ends. the body
     becomes a vertical flex column whose <main> fills any remaining
     space, so the footer sits at the natural bottom of the viewport
     — never stranded mid-screen. svh keeps iOS chrome out of the
     equation. desktop pages with rich content reach the bottom
     naturally; this scope keeps blast radius minimal. */
  body[data-page="forbidden"],
  body[data-page="not-found"],
  body[data-page="server-error"],
  body[data-page="sw-reset"] {
    display: flex;
    flex-direction: column;
    min-height: 100svh;
  }
  body[data-page="forbidden"] > main,
  body[data-page="not-found"] > main,
  body[data-page="server-error"] > main,
  body[data-page="sw-reset"] > main {
    flex: 1 1 auto;
  }
  /* phase 85 · push the error content lower into the single-plate
     frame. the base .page padding-top is clamp(80px, 12vh, 140px);
     on ipad/laptop that left the message floating near the top of
     <main> with an oversized empty middle zone before the footer.
     biased the error content to ~22vh from the top so it reads as
     a deliberate composition without resorting to justify-content:
     center (which the brief explicitly excluded).
     scoped to the three error pages; sw-reset keeps the base
     padding so its longer content has room. */
  body[data-page="forbidden"] > main > .page,
  body[data-page="not-found"] > main > .page,
  body[data-page="server-error"] > main > .page {
    padding-top: clamp(140px, 22vh, 220px);
  }
  /* quiet homepage link sitting inline at the end of the lede. text
     decoration confined to the link itself, no button affordance. */
  .error-home-link {
    color: var(--accent-text);
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, var(--accent-text) 40%, transparent);
    text-underline-offset: 3px;
    transition: text-decoration-color 0.2s, color 0.2s;
  }
  .error-home-link:hover,
  .error-home-link:focus-visible {
    text-decoration-color: var(--accent-text);
    outline: 0;
  }

  /* === phase 91 · verify slip + composed publication sprint · /verify/ stamped slip === */
  /* opt-in via <body data-verify-treatment="slip"> on /en-au/verify/
     and /fr/verifier/ only. the shared :is(.verify-page, .sw-reset-page,
     .security-page, .source-page) .verify-card block at line ~1168
     continues to govern every other surface — slip rules win on
     ancestor-attribute specificity (data-verify-treatment) without
     touching the shared base. all selectors are prefixed with
     [data-verify-treatment="slip"] so /sw-reset/, /security/,
     /source/, /integrity/ render unchanged. */

  /* anchor the slip stamp to .page-body, not .verify-card, so the
     deckle edge can bleed above the card without clipping the stamp. */
  [data-verify-treatment="slip"] .page-body { position: relative; }

  /* card shell · drop the rounded border + heavy lift, swap to the
     lighter raised-paper tone, replace the shadow with a tighter
     two-layer lift. padding-block-start / -inline-start clear the
     deckle bleed and the perforation column. overflow: visible so
     the ::before can render outside the card. */
  [data-verify-treatment="slip"] .verify-card {
    background: var(--paper-raised-high);
    border: none;
    border-radius: 0;
    box-shadow:
      0 1px 0 color-mix(in srgb, var(--ink) 4%, transparent),
      0 12px 26px rgb(0 0 0 / 0.05);
    padding-block-start: clamp(2.4rem, 4.8vw, 3.2rem);
    padding-inline-start: clamp(2.4rem, 5vw, 3.4rem);
    position: relative;
    overflow: visible;
  }

  /* deckle top edge · ~10px ragged-baseline mask sitting above the
     card. path ported verbatim from redesigns/verify/e-stamped-slip.html
     (viewBox 0 0 200 12). mask lives at /images/textures/slip-deckle.svg
     rather than as an inline data: URI — the site's CSP pins img-src
     to 'self' and mask-image is fetched as an image, so a data: URI
     would be blocked. one extra request on /verify/ only (slip is opt-in). */
  [data-verify-treatment="slip"] .verify-card::before {
    content: "";
    position: absolute;
    inset: -6px 0 auto 0;
    height: 10px;
    background: var(--paper-raised-high);
    -webkit-mask-image: url("/images/textures/slip-deckle.svg");
            mask-image: url("/images/textures/slip-deckle.svg");
    -webkit-mask-size: 100% 100%;
            mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    pointer-events: none;
  }

  /* perforation column · faint dotted hairline at left:18px running
     the full card height; the inline padding above clears it visually. */
  [data-verify-treatment="slip"] .verify-card::after {
    content: "";
    position: absolute;
    top: 0; bottom: 0; left: 18px; width: 0;
    border-left: 1px dotted color-mix(in srgb, var(--ink) 22%, transparent);
    opacity: 0.7;
    pointer-events: none;
  }

  /* citation row · the first dd of .record-grid carries the bibliographic
     citation. serif italic gives it the document-of-record register. */
  [data-verify-treatment="slip"] .record-grid__row:first-child dd {
    font-family: var(--serif);
    font-style: italic;
    font-size: clamp(1.02rem, 2.3vw, 1.15rem);
    color: var(--fg);
  }

  /* fingerprint row · designed evidence treatment.
     the fingerprint becomes a quiet inset panel inside the record
     card — the most technical field in the publication, given an
     archival presentation: a museum accession label, not a github
     code block. fine top + bottom hairlines, a mono SHA-256 sub-
     label rendered by verify.js as .record-fingerprint-algo, and
     the hash itself in mono with controlled wrapping. full value
     remains selectable; no truncation, no hover-reveal. wraps
     cleanly on mobile through anywhere-overflow-wrap so the row
     never causes a horizontal scroll. */
  [data-verify-treatment="slip"] .record-grid__row:has(.record-fingerprint-block) {
    background: color-mix(in srgb, var(--ink) 1.8%, var(--paper-raised-high));
    border-block-start: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
    border-block-end:   1px solid color-mix(in srgb, var(--ink) 12%, transparent);
    padding-block:  clamp(0.85rem, 2vw, 1.2rem);
    padding-inline: clamp(0.85rem, 2vw, 1.2rem);
    margin-block: clamp(0.8rem, 2vw, 1.2rem) 0.25rem;
  }
  [data-verify-treatment="slip"] .record-grid__row:has(.record-fingerprint-block) dt {
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fg3);
    margin-block-end: 0.4rem;
  }
  [data-verify-treatment="slip"] .record-fingerprint-block {
    display: block;
  }
  /* "SHA-256" sub-label emitted by verify.js inside the block. mono
     micro-cap, paired down so it reads as a metadata tag above the
     actual hash. */
  [data-verify-treatment="slip"] .record-fingerprint-algo {
    display: inline-block;
    font-family: var(--mono);
    font-size: 9.5px;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fg3);
    margin-block-end: 0.3rem;
  }
  /* the hash itself · mono, slightly heavier, wide tracking so the
     8-char groupings read as evidence rather than a wall of hex.
     overflow-wrap: anywhere keeps the value clean on phones without
     truncation or horizontal scroll. */
  [data-verify-treatment="slip"] .record-fingerprint {
    display: block;
    font-family: var(--mono);
    font-size: clamp(11.5px, 1.4vw, 13px);
    font-weight: 500;
    letter-spacing: 0.06em;
    line-height: 1.55;
    color: var(--fg);
    overflow-wrap: anywhere;
    word-break: break-all;
    user-select: text;
  }

  /* chrome wrapper · zero-height positioning frame between .verify-hero
     and #verify-root. the stamp inside is absolutely positioned with
     negative offsets that overlap the top-right of the rendered card.
     the accession strip that used to live here was removed — that
     metadata already lives in the citation + edition systems. */
  [data-verify-treatment="slip"] .verify-slip-chrome {
    position: relative;
    height: 0;
    pointer-events: none;
  }

  /* hide the slip stamp + the card's deckle/perforation pseudo-
     elements whenever verify.js hasn't rendered a record card
     into #verify-root — i.e. the "route not in verification map"
     fallback, the noscript path, or any error state. without
     this guard the stamp dangles in empty space, suggesting a
     record has been signed when there's nothing to stamp.
     uses :has() (safari 15.4+, chrome 105+, firefox 121+ — modern
     enough; gracefully degrades to the previous always-visible
     stamp on older browsers). */
  [data-verify-treatment="slip"] .page-body:not(:has(.verify-card)) .verify-slip-stamp {
    display: none;
  }

  /* stamp · publication-record mark physically pressed onto the
     upper-right edge of the record card. updated per the engineering
     brief: the stamp belongs to the PAPER coordinate system, not the
     viewport. the chrome wrapper sits in normal flow just before
     #verify-root, so chrome-top ≈ card-top minus the card's
     margin-block-start (clamp(2.4rem, 5vw, 3.25rem) ≈ 38-52px).
     brief asks for top: clamp(-12px, -1vw, 8px) RELATIVE TO CARD.
     translate to chrome-relative: card-margin-top + brief-top
     ≈ clamp(26px, 4vw, 60px). right and width come straight from
     the brief — wider stamp (110-138px) extending further past the
     card edge (44-72px) so the overlap reads as ~40% inside / 60%
     outside the card. -8deg rotation. opacity 0.88. mix-blend-mode
     multiply so the oxblood ink bites the warm paper. */
  [data-verify-treatment="slip"] .verify-slip-stamp {
    position: absolute;
    z-index: 2;
    width: clamp(110px, 8vw, 138px);
    /* anchor target: stamp's top edge sits ~0.6rem BELOW the card's
       top edge, so the body of the stamp is pressed firmly onto the
       paper with only a sliver dangling above. chrome (parent
       positioning context) sits at hero-bottom in flow, which is
       exactly card-margin-top above card-top. so stamp-top
       (chrome-relative) = card-margin-top + 0.6rem. previous
       compositions with a negative brief-offset clamp put the
       stamp body too high above the card; this lands it on the
       paper at every viewport.
       right is unchanged: stamp extends 44-72px past the card edge
       so ~50-60% of the width dangles off the right of the paper. */
    top: calc(clamp(2.4rem, 5vw, 3.25rem) + 0.6rem);
    right: clamp(-56px, -3.5vw, -28px);
    height: auto;
    color: var(--accent);
    transform: rotate(-8deg);
    transform-origin: center;
    mix-blend-mode: multiply;
    opacity: 0.88;
    pointer-events: none;
  }

  /* mobile · stamp moves INSIDE the paper, near the top-right of the
     card. reserve right-side padding on the card header so the title
     does not collide with the stamp. */
  @media (max-width: 760px) {
    [data-verify-treatment="slip"] .verify-slip-stamp {
      width: clamp(70px, 18vw, 88px);
      /* card-margin-top 2.4rem + 1rem inside = 3.4rem */
      top: 3.4rem;
      right: 1rem;
      transform: rotate(-6deg);
      opacity: 0.9;
    }
    [data-verify-treatment="slip"] .verify-card__header {
      padding-right: 5.5rem;
    }
  }

  /* dark mode · cover BOTH triggers — explicit [data-theme="dark"]
     AND prefers-color-scheme: dark (guarded so an explicit light
     setting wins over system dark). card swaps to the dark
     --paper-raised-high token (declared at ~line 346); deckle ::before
     background tracks the new card colour; stamp swaps to --accent-text,
     drops mix-blend-mode, slightly lower opacity. */
  :root[data-theme="dark"] [data-verify-treatment="slip"] .verify-card {
    background: var(--paper-raised-high);
    box-shadow: none;
  }
  :root[data-theme="dark"] [data-verify-treatment="slip"] .verify-card::before {
    background: var(--paper-raised-high);
  }
  :root[data-theme="dark"] [data-verify-treatment="slip"] .verify-slip-stamp {
    color: var(--accent-text);
    mix-blend-mode: normal;
    opacity: 0.88;
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) [data-verify-treatment="slip"] .verify-card {
      background: var(--paper-raised-high);
      box-shadow: none;
    }
    :root:not([data-theme="light"]) [data-verify-treatment="slip"] .verify-card::before {
      background: var(--paper-raised-high);
    }
    :root:not([data-theme="light"]) [data-verify-treatment="slip"] .verify-slip-stamp {
      color: var(--accent-text);
      mix-blend-mode: normal;
      opacity: 0.88;
    }
  }

  /* print · slip chrome is screen-only; the existing
     .print-utility-sheet handles printed verification. */
  @media print {
    [data-verify-treatment="slip"] .verify-slip-stamp,
    [data-verify-treatment="slip"] .verify-slip-chrome,
    [data-verify-treatment="slip"] .verify-card::before,
    [data-verify-treatment="slip"] .verify-card::after { display: none !important; }
  }

  /* forced colors · decorative chrome drops away; record content
     uses system colours unchanged. */
  @media (forced-colors: active) {
    [data-verify-treatment="slip"] .verify-card::before,
    [data-verify-treatment="slip"] .verify-card::after,
    [data-verify-treatment="slip"] .verify-slip-stamp { opacity: 0; }
  }

  /* reduced motion · defensive — no transitions on the stamp. */
  @media (prefers-reduced-motion: reduce) {
    [data-verify-treatment="slip"] .verify-slip-stamp { transition: none; }
  }

  /* === phase 91 · verify slip + composed publication sprint · edition archaeology === */
  /* /integrity/releases/ archive · current edition foregrounded, the
     earlier strata folded behind a native <details>. no animation,
     no icon, no [open] transition. mono micro-caps for labels +
     kickers, serif for the edition titles, tabular mono for dates. */
  .edition-archive {
    display: grid;
    gap: clamp(1.6rem, 4vw, 2.4rem);
    border-top: 1px solid var(--rule);
    padding-top: clamp(1.6rem, 4vw, 2.4rem);
    margin-top: clamp(2rem, 5vw, 3.2rem);
  }
  .edition-archive__label {
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0;
  }
  .edition-current__kicker {
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0 0 0.4rem;
  }
  .edition-current__date {
    font-family: var(--mono);
    font-size: 12px;
    font-variant-numeric: tabular-nums lining-nums;
    color: var(--fg2);
    margin: 0 0 0.25rem;
  }
  .edition-current__title {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(22px, 3vw, 30px);
    margin: 0 0 0.35rem;
  }
  .edition-current__meta {
    font-family: var(--mono);
    font-size: 10.5px;
    color: var(--fg3);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    margin: 0 0 0.9rem;
  }
  .edition-current__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.9rem;
  }
  .edition-earlier__summary {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--fg3);
    cursor: pointer;
    list-style: none;
    padding-block: 0.35rem;
  }
  .edition-earlier__summary::-webkit-details-marker { display: none; }
  .edition-earlier__list {
    list-style: none;
    margin: 0.9rem 0 0;
    padding: 0;
    display: grid;
    gap: 0.6rem;
  }
  .edition-earlier__link {
    display: grid;
    grid-template-columns: 9rem 1fr;
    gap: 0.8rem;
    text-decoration: none;
    color: var(--fg2);
    border-bottom: 1px solid color-mix(in srgb, var(--ink) 6%, transparent);
    padding-block: 0.55rem;
  }
  .edition-earlier__link:hover,
  .edition-earlier__link:focus-visible {
    color: var(--fg);
    outline: 0;
    border-bottom-color: var(--accent-text);
  }
  .edition-earlier__date {
    font-family: var(--mono);
    font-size: 11px;
    font-variant-numeric: tabular-nums lining-nums;
    color: var(--fg3);
  }
  .edition-earlier__title {
    font-family: var(--serif);
    font-size: 15px;
    font-weight: 400;
  }
  @media (max-width: 520px) {
    .edition-earlier__link { grid-template-columns: 1fr; gap: 0.2rem; }
  }
}

/* ─── utilities ────────────────────────────────────────────────
   minimal accessibility-only helpers. Visually-hidden removes
   content from the page but preserves it for assistive tech —
   used as a target for aria-live status announcements (copy-toast,
   etc.). one declaration; no surface or page scoping. */
@layer utilities {
  .visually-hidden {
    /* phase 88 · clip alone leaves a 1×1 box that can capture touch
       clicks on ios safari, blocking adjacent buttons (the cite
       overlay regression: visually-hidden description spans sitting
       next to the footer cite button intercepted taps before the
       button handler fired). pointer-events: none releases all
       clicks back to the underlying button; clip-path: inset(50%)
       is the modern equivalent of the legacy clip and reinforces
       the visual containment. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
    pointer-events: none;
  }
}

/* ──────────────────────────────────────────────────────────────────
   language vestibule (/)
   the root is a deliberate language-choice page shown on every visit.
   behind the choice card sits the real homepage: the build slices the
   rendered masthead + hero out of the homepage and injects it into
   .gate-background, which carries data-page="home" so the homepage's
   own geometry rules resolve. / and /en/ are one composition — the
   card introduces state, not a new layout. server-rendered visible:
   works with no JavaScript, and it never auto-redirects.
   ────────────────────────────────────────────────────────────────── */
@layer components {

  /* display language — set pre-paint on <html data-preferred-lang>.
     both en and fr copy ship in the markup; CSS reveals one. with no
     JavaScript the attribute is absent and english (default) shows. */
  [data-gate="fr"] { display: none; }
  [data-preferred-lang="fr"] [data-gate="en"] { display: none; }
  [data-preferred-lang="fr"] [data-gate="fr"] { display: inline; }

  /* first-visit vs returning-visitor copy — set pre-paint on
     <html data-returning="true"> if a tp-lang choice has been stored.
     the first-visit lede explains the relationship between editions;
     the returning lede is a quiet single line ("last read in english.").
     no JavaScript = always first-visit. */
  [data-when="return"] { display: none; }
  html[data-returning="true"] [data-when="first"] { display: none; }
  html[data-returning="true"] [data-when="return"] { display: inline; }

  /* the homepage scrolls and so reserves a scrollbar gutter; the
     vestibule does not scroll. reserve the gutter here too so the
     centred editorial register lands at the identical x as /en/ —
     without this the masthead and hero shift by half a scrollbar. */
  html:has(body.language-gate) { scrollbar-gutter: stable; }

  /* background · the sliced homepage masthead + hero. no transform,
     no scale, no extra padding — the homepage's own .hero geometry
     (min-height:100svh; justify-content:flex-end) bottom-anchors it
     exactly as on /en/. inert, no pointer events — a surface.
     publication blur · the gate is the only page where the modal
     is permanently open, so we don't go through overlay.js's
     body.modal-open path. apply the blur directly here so the
     publication recedes behind the paper-tinted scrim and the
     gate reads as a modal, not a flat overlay. */
  .gate-background {
    filter: blur(var(--modal-blur));
    position: fixed;
    inset: 0;
    z-index: 1;
    overflow: hidden;
    pointer-events: none;
    background: var(--bg);
  }
  /* historically the gate-background pinned the hero at rest because
     no behaviour script was driving the reveal here. now that
     fonts.js + reveal.js run on the gate page too, the hero animates
     naturally behind the scrim — same revealUp keyframes as
     /en-au/. when the visitor then dismisses the gate, the
     destination skips its own animation (the hero already played
     behind the scrim), governed by sessionStorage.tp-skip-hero-anim
     handed off by language-gate.js. */

  /* the gate's background hero is a static snapshot of the homepage
     behind the modal scrim. critical signifier subset is
     font-display: optional — on slow networks the fallback (georgia)
     renders, and georgia has no 300-weight, so font-synthesis: none
     (set globally on body) traps the fallback at 400 which reads
     bold against the eventual 300. allow synthesis here, scoped to
     the gate background only, so the fallback weight matches
     expectation. (the live homepage already loads the full font
     before the user sees it, so this override only kicks in when
     the gate is the first paint and the fallback is in play.) */
  .gate-background .hero-statement,
  .gate-background .hero-body,
  .gate-background .hero-kicker {
    font-synthesis: weight style;
  }

  /* legacy gate-* and per-modal compound CSS were replaced by the
     shared .shell + .modal-shell-scrim family in @layer components
     (phase 92 · modal-system overhaul). the gate now uses the
     shell shape end-to-end; the only gate-specific rules that
     survive are .gate-background (the inert blurred homepage
     backdrop) above. the french-hover background warm-shift
     also moves to the new .lang-cell selector if needed; for now
     dropped as a non-essential nicety. */
}
