/*! 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: swap;     src: url('/fonts/subsets/signifier-light-hero.woff2') format('woff2'); }
/* italic hero face — used by the hero <mark> highlight. points at the
   full light-italic woff2 (small usage, loaded lazily on swap); a
   dedicated hero subset is a possible later optimisation. */
@font-face { font-family: 'Signifier Critical';   font-style: italic; font-weight: 300; font-display: swap;     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: swap;     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);

  /* 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). */
  --fg2: #5F5A53;
  --fg3: #706B66;
  --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;
}

/* 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"] {
  --fg2:  #6C6760;
  --fg3:  #756F69;
  /* 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:        #5F5A53;
      --fg3:        #706B66;

      --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"] {
      --fg2:  #6C6760;
      --fg3:  #756F69;
      --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 {
/* 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;
  /* 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: 1200px;
  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: 54px;
}

/* 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: 920px;
  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: 610px;
}

/* 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 · gated on html.js (set inline pre-paint by the language
   bootstrap, runs synchronously before the first paint frame). hiding
   and the reveal keyframe are now driven by the same class, so a
   failed or slow app.js cannot leave the hero stuck invisible — the
   animation starts in the same paint tick the hide begins. when js
   is disabled entirely the hide rule never matches and the hero
   paints resting, no fallback rule needed. */
html.js .hero-statement,
html.js .hero-body {
  opacity: 0;
  transform: translateY(14px);
}
html.js .hero-statement {
  animation: revealUp 800ms cubic-bezier(.2,.7,.2,1) 90ms forwards;
}
html.js .hero-body {
  animation: revealUp 850ms cubic-bezier(.2,.7,.2,1) 180ms forwards;
}
/* 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;
}

.js .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;
}
.js .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;
}

.js .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 button[lang]        — app.js
   • .site-footer__theme button[data-theme]     — app.js
   • [data-cite-open]                           — cite.js
   • #footerImprint / [data-proof=…]            — app-enhance.js
   • data-lang attrs on homepage language btns  — validate_i18n_runtime r5
   • .site-footer__language button {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:clamp(0.9rem, 2vw, 1.4rem);
  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}
.site-footer__language button{
  font-family:var(--serif);
  font-style:italic;font-weight:400;
  font-size:clamp(14px, 1.6vw, 17px);
  letter-spacing:0.005em;
  color:var(--fg3);background:none;border:0;cursor:pointer;
  padding:2px 6px;border-bottom:1px solid transparent;
  text-transform:none;
  transition:color .2s,border-color .2s;
  min-width:44px;min-height:44px;
}
.site-footer__language button:hover,
.site-footer__language button:focus-visible{color:var(--accent);border-color:var(--accent);outline:0}
.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;
}
.site-footer__bottom .site-footer__imprint a{
  border-bottom-color:transparent;
}
.site-footer__imprint{
  border-top:0;
}

/* imprint · dl · uppercase mono provenance · subordinate layer */
.site-footer__imprint{
  margin:0;display:flex;flex-wrap:wrap;align-items:baseline;justify-content:center;
  column-gap:0.5rem;row-gap:0.25rem;
  font-family:var(--mono);
  font-size:9.5px;letter-spacing:0.14em;text-transform:uppercase;line-height:1.15;
  color:color-mix(in srgb,var(--fg2) 75%,var(--fg3));
  transition:opacity .3s ease;
}
.site-footer__imprint.is-loading{opacity:.45}
.site-footer__imprint dt{
  color:var(--fg3);opacity:.6;font-weight:400;
  margin:0;padding:0;
}
.site-footer__imprint dd{
  color:color-mix(in srgb,var(--fg2) 75%,var(--fg3));
  font-weight:500;letter-spacing:0.14em;
  margin:0;padding:0;
}
.site-footer__imprint dd + dt{margin-inline-start:.4rem}
.site-footer__imprint .v--fresh{color:var(--accent-text,var(--accent))}
.site-footer__imprint a.sha-link{
  color:inherit;text-decoration:none;
  border-bottom:1px solid color-mix(in srgb,var(--accent) 30%,transparent);
  padding-bottom:1px;transition:color .2s,border-color .2s;
}
.site-footer__imprint a.sha-link:hover,
.site-footer__imprint a.sha-link:focus-visible{
  color:var(--accent);border-color:var(--accent);outline:0;
}

/* 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__imprint{justify-content:flex-start}
  body[data-layout="masthead"] .site-footer__bottom > .site-footer__theme{justify-content:flex-end}
}

/* footer · provenance · french-only colophon line.
   declares the page as a machine translation of the canonical
   english edition. hidden by default; revealed when the i18n
   runtime sets <html lang="fr"> (applyLanguage in app.js).
   the grid-template-areas re-flow is also gated on html[lang="fr"]
   so the english footer's bottom-row geometry is byte-for-byte
   unchanged when the line is absent. */
.site-footer__provenance{
  display:none;
  margin:1rem 0 0;
  font-family:var(--serif);font-style:italic;font-weight:400;
  font-size:12px;line-height:1.5;
  color:var(--fg3);text-align:center;max-width:42rem;
}
html[lang="fr"] .site-footer__provenance{display:block}
@media (min-width: 720px) {
  html[lang="fr"] body[data-layout="masthead"] .site-footer__bottom{
    grid-template-areas:
      "imprint    theme"
      "provenance provenance";
    row-gap:1rem;
  }
  html[lang="fr"] body[data-layout="masthead"] .site-footer__bottom > .site-footer__imprint{grid-area:imprint}
  html[lang="fr"] body[data-layout="masthead"] .site-footer__bottom > .site-footer__theme{grid-area:theme}
  html[lang="fr"] body[data-layout="masthead"] .site-footer__provenance{
    grid-area:provenance;justify-self:center;margin:0.4rem 0 0;
  }
}
@media (max-width: 640px) {
  .site-footer__provenance{font-size:11.5px;margin-top:0.85rem}
}

/* mobile <= 640 · centred stack with full hit targets */
@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__imprint{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__imprint{font-size:9px}
  .site-footer__theme,
  .site-footer__theme button{font-size:9px;letter-spacing:0.11em}
}

@media (prefers-reduced-motion: reduce){
  .site-footer__imprint{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 */

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(31, 30, 28, 0);
  backdrop-filter: blur(0);
  -webkit-backdrop-filter: blur(0);
  display: flex;
  align-items: center;
  justify-content: center;
  /* full-screen overlay: pad each side by max(24px, safe-area-inset)
     so the modal content never sits under the notch / home-indicator
     in `viewport-fit=cover` mode. the cite drawer is JS-positioned
     so it does not need this here. */
  padding-top:    max(24px, var(--safe-top));
  padding-right:  max(24px, var(--safe-right));
  padding-bottom: max(24px, var(--safe-bottom));
  padding-left:   max(24px, var(--safe-left));
  pointer-events: none;
  /* backdrop fades on its own slower curve so the scrim "settles"
     after the paper card has arrived. matches the brief's "paper
     emerging from fog" target. */
  transition:
    background           260ms cubic-bezier(.22,.61,.36,1),
    backdrop-filter      260ms cubic-bezier(.22,.61,.36,1),
    -webkit-backdrop-filter 260ms cubic-bezier(.22,.61,.36,1);
}

.modal-overlay.active {
  /* phase 84 · scrim quietened to ~34% warm sepia at the token level
     (see :root --overlay-scrim) and blur eased from 6 px to 3 px so
     the modal reads as an archival card placed gently over paper
     rather than a frosted system sheet. page typography stays
     perceptible underneath; modal panel contrast is unaffected
     because the scrim sits behind the panel surface. */
  background: var(--overlay-scrim);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  pointer-events: auto;
}

.modal {
  /* the panel sits on paper (the artefact in front of the scrim).
     raised-high paper — slightly lighter than --surface-card so an
     overlay opened above either page surface still reads as a
     lifted sheet, not as a flat plate.
     phase 41 · 2px outer radius matches the radius scope (cards,
     trust mark, verify-card). padding tightened ~15% so the panel
     reads as a denser editorial insert rather than a generously
     spaced ui modal. phase 46 · overlay panels promote to
     var(--radius-panel) (10px) — slightly more present than the
     card radius so the modal reads as a lifted panel, not a card.
     phase 62 · padding tightened a further ~12% so the project
     modal sits as a private studio note rather than a generously
     spaced ui panel. less empty lower space. */
  background: var(--paper-raised-high);
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  box-shadow: none;
  padding: clamp(22px, 3.4vw, 34px);
  max-width: 440px;
  width: 100%;
  position: relative;
  opacity: 0;
  transform: translateY(7px);
  /* paper emerging from fog: opacity rises faster than transform so
     the card materialises before it finishes settling. easing chosen
     to feel decelerating without spring overshoot. */
  transition:
    opacity   150ms cubic-bezier(.22,.61,.36,1),
    transform 200ms cubic-bezier(.22,.61,.36,1);
}

.modal-overlay.active .modal {
  opacity: 1;
  transform: translateY(0);
}

/* editorial modal · no accent rail. the card carries no alert state. */
.modal::before {
  content: none;
  display: none;
}

.modal-label {
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fg3);
  /* phase 62 · tighter trailing gap so the panel reads as a
     private studio note. */
  margin-bottom: 16px;
}

.modal-text {
  font-family: var(--serif);
  font-size: clamp(17px, 4.6vw, 21px);
  font-weight: 400;
  line-height: 1.5;
  color: var(--fg2);
  /* phase 41 · hairline rule below the body text anchors the cta
     visually so it reads as the action of the panel, not a quiet
     trailing link.
     phase 62 · spacing tightened ~15% so the cta reads as the
     immediate next gesture, not a separated block. */
  margin-bottom: clamp(16px, 3vw, 22px);
  padding-bottom: clamp(16px, 3vw, 22px);
  border-bottom: 1px solid var(--rule);
}

/* phase 62 · the project modal cta is no longer a directional
   "→ trent@trentpower.fr" link. it reads as a two-line studio
   note: a quiet mono label above ("access by request"), the
   email on the second line. less startup, more archival. */
.modal-cta {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--accent-text);
  text-decoration: none;
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 6px 0;
  border-bottom: 1px solid var(--bd);
  transition: border-color 0.2s, color 0.2s;
}

.modal-cta:hover,
.modal-cta:focus-visible {
  border-color: var(--accent-text);
  outline: 0;
}

.modal-cta-label {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fg3);
}

/* visible email below the label — keeps the action self-evident
   on every locale (the email is the same in all five). */
.modal-cta-email {
  font-family: var(--mono);
  overflow-wrap: anywhere;
  word-break: normal;
}

/* legacy text close , no longer rendered; the project modal now uses
   the top-right × pattern shared with the cite overlay. kept as a
   no-op so any cached html still styles cleanly if it appears. */
.modal-close {
  display: block;
  margin-top: 28px;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--fg3);
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px 4px;
  transition: color 0.2s;
}

.modal-close:hover,
.modal-close:focus-visible {
  color: var(--fg);
}

/* top-right × close , single visible close affordance for the project
   modal. mirrors .cite-modal-close-x so the modal family is consistent.
   phase 41 · colour bumped fg3 → fg2 (same warm grey, ~6% darker) so
   the close glyph reads as deliberate authoritative chrome rather than
   a casual tertiary marker. */
.modal-close-x {
  position: absolute;
  top: 0.5rem;
  right: 0.7rem;
  width: 1.8rem;
  height: 1.8rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--sans);
  font-size: 1.3rem;
  line-height: 1;
  color: var(--fg2);
  background: transparent;
  border: 0;
  cursor: pointer;
  padding: 0;
  transition: color 0.2s;
  z-index: 1;
}
.modal-close-x:hover,
.modal-close-x:focus-visible {
  color: var(--accent-text);
  outline: 0;
}

/* cite overlay , page record
   same modal family as the view project modal; wider card to fit a
   metadata table and an actions strip. lazily injected by cite.js. */

/* cite modal · publication record card. five elements top-down:
   close × (mobile only) · header (eyebrow + title + lede) ·
   meta <dl> · primary actions (verify · view source) · secondary
   actions (copy citation · print). the card is paper lifted by
   colour, not by chrome — no shadow, no border. */
.modal.cite-modal {
  /* shares the .modal family treatment with the homepage view-project
     modal: same background, same 1 px rule border, same panel radius,
     same motion (inherited from .modal). only the cite-specific layout
     overrides live here — text alignment, padding, max-width, internal
     gap. consistency across both modals is the contract. */
  text-align: left;
  padding: clamp(22px, 3.6vw, 32px);
  padding-right: clamp(24px, 4vw, 36px);
  max-width: 26rem;
  width: min(100%, 26rem);
  max-height: 92vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: clamp(14px, 2.4vw, 20px);
}

/* close × — mobile-only affordance; desktop relies on click-outside +
   escape (the brief's preference). mobile placement is a tiny dot in
   the top-right; opacity stays low until hovered / focused so the
   card surface reads as paper, not as a control panel. */
.cite-modal-close-x {
  position: absolute;
  top: 0.55rem;
  right: 0.6rem;
  width: 1.6rem;
  height: 1.6rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  font: 15px/1 var(--mono);
  color: color-mix(in srgb, var(--fg3) 55%, transparent);
  cursor: pointer;
  padding: 0;
  transition: color 180ms ease, opacity 180ms ease;
}
.cite-modal-close-x:hover,
.cite-modal-close-x:focus-visible {
  color: var(--fg2);
  outline: 0;
}
@media (min-width: 700px) {
  /* desktop: the visible × is retired. escape and click-outside
     both close the overlay (wired by app-enhance's TP_OVERLAY).
     keeping it on tap-only surfaces where there is no escape key
     and click-outside is harder with one thumb. */
  .cite-modal-close-x { display: none; }
}

.cite-modal-header {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
}

/* eyebrow · the small-caps publication mark above the title. wider
   tracking, lighter opacity. quieter than the surrounding type. */
.cite-modal-kicker {
  font: 500 9px/1.1 var(--mono);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--fg3) 65%, transparent);
  margin: 0;
}

/* title · serif, slightly smaller than before so the modal does not
   feel like a hero panel. balance hint keeps the mobile wrap clean. */
.cite-modal-title {
  font: 300 clamp(20px, 3.8vw, 24px)/1.22 var(--serif);
  letter-spacing: -0.015em;
  color: var(--fg);
  margin: 0;
  text-wrap: balance;
}

/* lede · softer contrast, one-sentence descriptor, narrower measure. */
.cite-modal-lede {
  font: 300 13.5px/1.5 var(--serif);
  color: var(--fg2);
  margin: 0;
  max-width: 32ch;
}

/* quiet footer line · publication-colophon stamp closing the modal.
   the persistent page footer already carries the per-page signed
   sha-256 and last-verified status; here we only acknowledge the
   edition and signed-SHA256 designation. mirrors the footer proof
   register so the two read as one editorial family. */
.cite-modal-footer-line {
  margin: clamp(0.9rem, 2vw, 1.25rem) 0 0;
  font: 500 9.5px/1.4 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--fg3) 70%, transparent);
  font-variant-numeric: tabular-nums lining-nums;
  text-align: center;
}

/* actions · two tiers with a subtle hierarchy. primary (verify · view
   source) reads as the inspectable record; secondary (copy citation ·
   print) reads as utility. left-aligned stack; no oversized click
   target; restrained hover. */
.cite-modal-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: clamp(2px, 1vw, 8px) 0 0;
  padding: 0;
}
.cite-modal-actions-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
}
.cite-modal-actions-row--secondary {
  margin-top: 6px;
  padding-top: 8px;
  border-top: 1px solid color-mix(in srgb, var(--rule) 35%, transparent);
}
.cite-modal-action {
  appearance: none;
  -webkit-appearance: none;
  font: 400 14px/1.4 var(--serif);
  color: var(--fg);
  background: transparent;
  border: 0;
  text-align: left;
  text-decoration: none;
  padding: 8px 0;
  cursor: pointer;
  border-bottom: 1px solid transparent;
  align-self: flex-start;
  transition: color 180ms ease, border-color 180ms ease;
}
.cite-modal-action--primary {
  color: var(--fg);
  font-weight: 400;
}
.cite-modal-action--secondary {
  font: 400 13px/1.4 var(--serif);
  color: var(--fg2);
}
.cite-modal-action:hover,
.cite-modal-action:focus-visible {
  color: var(--accent);
  border-bottom-color: color-mix(in srgb, var(--accent) 55%, transparent);
  outline: 0;
}
.cite-modal-action[data-state="copied"] {
  color: var(--accent);
}

/* visually-hidden · aria-live region for copy announcements. */
.cite-overlay .visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
  padding: 0;
  margin: -1px;
}

/* 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). */
}
@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;
}

/* ─── 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) {
    body[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) {
    body[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;
  }
}

/* ─── 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 gate (/)
   the gate reuses the .modal-overlay scrim + backdrop blur and the
   .modal paper card. it adds only the gate-specific card layout.
   the page is server-rendered active so it works with no JavaScript.
   ────────────────────────────────────────────────────────────────── */
@layer components {

  /* inert masthead behind the scrim — gives the gate paper to lift off */
  .lang-gate-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: var(--paper);
  }

  /* the gate's display language is fixed at parse time; swap the copy */
  [data-gate="fr"] { display: none; }
  [data-gate-lang="fr"] [data-gate="en"] { display: none; }
  [data-gate-lang="fr"] [data-gate="fr"] { display: inline; }

  .lang-overlay .modal.lang-modal {
    width: min(100%, 26rem);
    max-width: min(100%, 26rem);
    background: var(--paper-raised-high);
    border: 1px solid var(--rule);
    border-radius: var(--radius-panel);
    padding: clamp(28px, 4vw, 40px) clamp(28px, 4vw, 40px) clamp(24px, 3.5vw, 34px);
    box-shadow: 0 30px 80px rgba(0, 0, 0, 0.10), 0 8px 20px rgba(0, 0, 0, 0.05);
    display: flex;
    flex-direction: column;
    gap: clamp(18px, 2.4vw, 24px);
  }
  @media (prefers-color-scheme: dark) {
    .lang-overlay .modal.lang-modal { box-shadow: none; }
  }

  .lang-modal__wordmark {
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--fg3);
    text-align: center;
    margin: 0;
  }
  .lang-modal__title {
    font-family: var(--serif);
    font-size: clamp(28px, 4.5vw, 34px);
    font-weight: 300;
    line-height: 1.08;
    letter-spacing: -0.02em;
    text-align: center;
    text-wrap: balance;
    margin: 0;
  }
  .lang-modal__lede {
    font-family: var(--serif);
    font-size: clamp(14px, 1.5vw, 16px);
    font-weight: 300;
    line-height: 1.55;
    color: var(--fg2);
    text-align: center;
    text-wrap: pretty;
    max-width: 34ch;
    margin: 0 auto;
  }
  .lang-modal__rule {
    border: 0;
    border-top: 1px solid var(--rule);
    height: 0;
    margin: 0;
  }

  .lang-modal__actions {
    display: grid;
    grid-template-columns: 1fr 1px 1fr;
  }
  .lang-modal__divider {
    background: var(--rule);
    width: 1px;
    align-self: stretch;
    justify-self: center;
  }
  .lang-modal__btn {
    appearance: none;
    background: transparent;
    border: 0;
    cursor: pointer;
    text-decoration: none;
    padding: clamp(14px, 2vw, 18px) 14px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    color: inherit;
    border-radius: var(--radius-soft);
    transition: background 200ms ease;
  }
  .lang-modal__btn:hover,
  .lang-modal__btn:focus-visible {
    background: color-mix(in srgb, var(--ink) 3%, transparent);
    outline: none;
  }
  .lang-modal__btn:focus-visible {
    box-shadow: inset 0 0 0 2px var(--focus-colour);
  }
  .lang-modal__label {
    font-family: var(--serif);
    font-size: clamp(20px, 2.8vw, 26px);
    font-weight: 400;
    letter-spacing: -0.012em;
    color: var(--fg);
    display: inline-flex;
    align-items: baseline;
    gap: 0.45em;
    transition: color 200ms ease;
  }
  .lang-modal__arrow {
    font-family: var(--mono);
    font-size: 0.7em;
    color: var(--fg3);
    transform: translateY(-0.05em);
    transition: transform 200ms ease, color 200ms ease;
  }
  .lang-modal__btn:hover .lang-modal__label,
  .lang-modal__btn:focus-visible .lang-modal__label { color: var(--accent-text); }
  .lang-modal__btn:hover .lang-modal__arrow,
  .lang-modal__btn:focus-visible .lang-modal__arrow {
    transform: translateX(3px) translateY(-0.05em);
    color: var(--accent-text);
  }
  .lang-modal__caption {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: 0.10em;
    text-transform: uppercase;
    color: var(--fg3);
    line-height: 1.4;
    font-variant-numeric: tabular-nums;
  }

  @media (max-width: 520px) {
    .lang-overlay .modal.lang-modal { padding: 24px 22px 20px; }
    .lang-modal__actions { grid-template-columns: 1fr; }
    .lang-modal__divider {
      width: 0;
      height: 0;
      align-self: auto;
      border-top: 1px solid var(--rule);
      margin: 6px 0;
    }
  }
}
