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

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

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

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

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

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

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

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

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

  /* phase 84 · scrim quietened to a warm sepia at ~34% alpha so the
     modal reads as an archival card placed gently over paper rather
     than a system dialog. previous value rgba(28,25,22,0.46) was
     reading as a darkened dim layer on mobile safari.
     gate v2 · further lifted to 18% so the publication shows through
     and the gate reads as a same-page overlay being lifted off the
     hero, not as a setup screen covering it. */
  --overlay-scrim: rgba(34, 27, 20, 0.18);

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

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

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

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

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

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

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

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

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

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

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

  /* ─── modal system tokens ─── one shell shared by the language gate,
     verify menu and access. forbidden: glassmorphism, backdrop-filter,
     slide-in, bounce, publication blur. the scrim alone (warm-brown at
     18% alpha) carries the modal seal; the publication beneath stays
     crisp so the modal reads as a card lifted above the page, not a
     glass plate covering it. */
  --modal-paper:   var(--paper-raised-high);
  --modal-radius:  12px;
  --modal-edge:    clamp(20px, 4vw, 32px);
  --modal-pad:     clamp(28px, 4.4vw, 38px);
  --modal-rule:    1px solid var(--rule);
  --modal-shadow:  0 30px 80px rgb(0 0 0 / .10), 0 8px 20px rgb(0 0 0 / .05);
  --modal-scrim:   38%;
  /* gate v2 · 400ms cubic-bezier(0.22, 1, 0.36, 1) is the editorial
     decelerating curve used by the language gate's dismissal — slow
     enough that the panel reads as lifting upward, fast enough that
     the publication doesn't feel held up. verify and access modals
     inherit the same vocabulary so the whole modal family decelerates
     identically. */
  --modal-motion:  400ms cubic-bezier(0.22, 1, 0.36, 1);
  /* gate v2 · 150ms colour/background/border ease for hover and focus
     on the gate's interactive elements (.lang-cell, .shell-close).
     intentionally short — colour changes should register, not perform. */
  --hover-motion:  150ms cubic-bezier(0.22, 1, 0.36, 1);

  /* ─── unified surface tokens (--tp-* namespace) ──────────────────
     one publication's visual grammar. every surface rule below
     references these — no hard-coded radii, opacities, or shadow
     values anywhere. resolve to existing site tokens where they map;
     introduce new values only where the brief's vocabulary doesn't
     exist yet. the warm-brown scrim and the established cream + ink
     are the locked palette anchors. */

  /* radii — four-tier scale + pill.
     record = 0 (quoted material, sharp corners read as document).
     interactive = 2px (focus rings, hover backgrounds).
     inline-card = 8px (in-flow cards: timeline, project insert).
     overlay = 12px (gate / verify-modal / access modal panels).
     pill = 999px (circular controls). */
  --tp-radius-record:        0;
  --tp-radius-interactive:   2px;
  --tp-radius-inline-card:   8px;
  --tp-radius-overlay:       var(--modal-radius);
  --tp-radius-pill:          var(--radius-pill);

  /* colour — aliases to the established editorial palette so the
     site keeps its tested warmth and contrast ratios. */
  --tp-paper:                var(--paper-main);
  --tp-paper-deeper:         var(--paper-record);
  --tp-ink:                  var(--ink);
  --tp-ink-soft:             var(--fg3);
  --tp-rule:                 var(--rule-strong);
  --tp-rule-faint:           var(--rule-faint);
  --tp-oxblood:              var(--accent);
  --tp-scrim:                var(--overlay-scrim);

  /* shadow — soft cast, not a drop. */
  --tp-shadow-overlay:
    0 1px 2px rgba(28, 24, 20, 0.04),
    0 8px 24px rgba(28, 24, 20, 0.08);

  /* motion — five durations + one ease. micro/hover/macro/settle/
     typewriter cover the spectrum from focus-state hint (150ms)
     through hero settle (600ms) to long-form arrival like the
     footer typewriter (800ms). */
  --tp-ease:                 cubic-bezier(0.22, 1, 0.36, 1);
  --tp-duration-micro:       150ms;
  --tp-duration-hover:       200ms;
  --tp-duration-macro:       400ms;
  --tp-duration-settle:      600ms;
  --tp-duration-typewriter:  800ms;

  /* ─── modal system · canonical --tp-modal-* namespace ────────────
     one publication-layer vocabulary consumed by every overlay
     (language gate, verify modal, project access modal, future
     diagnostics / source overlays). resolves to the existing
     --modal-* + --tp-* internals so the underlying values stay in
     one place; the --tp-modal-* prefix is the public contract every
     overlay reads. */
  --tp-modal-bg:             var(--modal-paper);
  --tp-modal-border:         var(--tp-rule-faint);
  --tp-modal-radius:         var(--modal-radius);
  --tp-modal-shadow:         var(--tp-shadow-overlay);
  --tp-modal-padding:        var(--modal-pad);
  --tp-modal-divider:        var(--rule-faint);
  /* close-button tokens — paper-warm utility, pressed into the
     surface. nearly-invisible warm-brown border (rgba 0.08), muted
     ink glyph (not oxblood). three states (rest / hover / active)
     differ enough to register a tap without ever reading as a
     glossy app chip. reads as one canonical site control across
     every overlay. */
  --tp-modal-close-bg:        color-mix(in srgb, var(--paper-record) 70%, transparent);
  --tp-modal-close-border:    rgba(110, 90, 70, 0.10);
  --tp-modal-close-color:     var(--fg2);
  --tp-modal-close-hover-bg:  var(--paper-record);
  --tp-modal-close-active-bg: color-mix(in srgb, var(--paper-record) 78%, var(--ink) 12%);
}

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}

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

}

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

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

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

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

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

/* selection · the publication's identity colour shows where readers
   don't expect it. oxblood at 18% on oxblood text. light + dark mode
   both anchor to --accent, which already retunes per theme; the rule
   is the same in both. */
::selection {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent-text);
}
::-moz-selection {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent-text);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

: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;
  /* phase 92 · hero compressed: top padding −18%, bottom asymmetric −35%
     vs the prior symmetric clamp(40,6vw,64). pulls the PAGE RECORD card
     into the first viewport on laptop without disturbing the rhythm. */
  padding: clamp(32px, 4.9vw, 52px) 0 clamp(26px, 3.9vw, 42px);
}
@media (max-width: 700px) {
  .verify-page .page-body {
    max-width: none;
    /* mobile · matching −18% top / −35% bottom against the prior 32 / 48. */
    padding: 26px 0 31px;
  }

  .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 {
  /* phase 92 · heading column trimmed 640 → 590 (−8%) so the title
     wraps tighter and the lede sits within a shorter measure. */
  max-width: 590px;
  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 {
  /* phase 92 · intro narrower than the heading column (−12% off the
     hero's 590px) so the prose reads as a caption to the title. */
  max-width: 520px;
  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. cohesion sprint ·
     rejoined the 8px inline-card family (--tp-radius-inline-card);
     the brief 7px detour left verify 1px adrift of integrity /
     project / trajectory, which all sit on the inline-card tier. */
  border-radius: var(--tp-radius-inline-card);
  /* 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.
     phase 92 · trimmed 12% so the kicker/title/record sit tighter. */
  margin-block-end: clamp(0.79rem, 2.1vw, 1.1rem);
}
: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);
  /* phase 92 · label↔value column gap compressed 12% (~0.79-1.58rem). */
  gap: clamp(0.79rem, 3.5vw, 1.58rem);
  /* phase 92 · row padding compressed 10% (0.55 → 0.5rem) — denser
     stack so citation/canonical/source/fingerprint can sit inside
     the first viewport without sacrificing line-height. */
  padding-block: 0.5rem;
}
: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. */
  /* phase 92 · gap to last record row eased so the rail reads as a
     single archival footer line beneath the record, not as a second
     block. */
  margin-block-start: 0.95rem;
  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;
}

/* phase 94 · release-archive disclosure beneath the verify card.
   the page record is the reader-facing trust unit; the release
   archive is the deeper technical reference, kept folded behind a
   <details> so readers who want it can open it without it competing
   with the actions strip above. mono summary chip + soft body
   beneath. matches the calm of the actions rail. */
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__archive {
  margin-block-start: 1.2rem;
  padding-block-start: 0.7rem;
  border-top: 1px dotted var(--rule);
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.045em;
  color: var(--fg3);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__archive-summary {
  cursor: pointer;
  list-style: none;
  text-transform: none;
  color: var(--fg2);
  padding: 0.25rem 0;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__archive-summary::-webkit-details-marker {
  display: none;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__archive-summary::before {
  content: "▸ ";
  display: inline-block;
  width: 0.9em;
  color: var(--fg3);
  opacity: 0.65;
  transition: transform 0.2s;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__archive[open] .verify-card__archive-summary::before {
  content: "▾ ";
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__archive-body {
  margin: 0.6rem 0 0;
  word-break: break-word;
}

/* phase 95 · page availability history · the per-page provenance
   dossier inside the archive disclosure. quiet mono record, one row
   per edition, hairline-dotted separators, oxblood "current" marker.
   reads like a small archival drawer opening — never a panel inside
   a panel. tokens only (no new ones added). */
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__history-summary {
  margin: 0.55rem 0 0;
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.06em;
  color: var(--fg3);
  text-transform: uppercase;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__history-status {
  margin: 0.4rem 0 0;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--fg3);
  letter-spacing: 0;
  text-transform: none;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__history {
  list-style: none;
  margin: 0.85rem 0 0;
  padding: 0;
  border-top: 1px dotted var(--rule);
  font-variant-numeric: tabular-nums lining-nums;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__history-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  padding: 0.5rem 0;
  border-bottom: 1px dotted var(--rule);
  min-width: 0;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__history-row:last-child {
  border-bottom: 0;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .he-edition {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  color: var(--fg2);
  text-transform: uppercase;
  white-space: nowrap;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .he-current {
  font-family: var(--mono);
  font-size: 0.55rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent-text);
  border: 1px solid color-mix(in srgb, var(--accent-text) 28%, transparent);
  background: color-mix(in srgb, var(--accent-text) 5%, transparent);
  padding: 0.08rem 0.42rem;
  border-radius: 999px;
  white-space: nowrap;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .he-sha {
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--fg3);
  overflow-wrap: anywhere;
  min-width: 0;
  flex: 1 1 12ch;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .he-link {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg2);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--fg3) 32%, transparent);
  padding-bottom: 1px;
  margin-inline-start: auto;
  transition: color 0.2s, border-color 0.2s;
  white-space: nowrap;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .he-link:hover,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .he-link:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
  outline: 0;
}

/* mobile · let the link drop beneath the sha line on narrow viewports
   so neither overflows the card. the row stays one block; the link
   gets its own line under the sha. */
@media (max-width: 520px) {
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__history-row {
    gap: 0.3rem 0.7rem;
  }
  :is(.verify-page, .sw-reset-page, .security-page, .source-page) .he-link {
    margin-inline-start: 0;
    flex-basis: 100%;
  }
}

: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.
     surface-unification · 8px inline-card radius + soft-cast shadow
     so this in-flow card reads as the same object-family as the
     project / trajectory cards. */
  border-radius: var(--tp-radius-inline-card);
  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: var(--tp-shadow-overlay);
  /* phase 80 · uniform vertical rhythm. the card is a grid; one
     consistent gap between sections, so the seam is even regardless
     of which group sits where. no margin-block cascades, no
     align-content distribution, no per-group height. */
  display: grid;
  gap: clamp(1.1rem, 2vw, 1.5rem);
  align-content: start;
}
.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. */
.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;
      /* second column bounded (minmax 0 1fr, not max-content) so long mono
         values — sha-256 hashes, the pgp fingerprint, the /tests/ desc —
         wrap inside the card instead of overflowing the box on desktop. */
      grid-template-columns: 180px minmax(0, 1fr);
      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;
}

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

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

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

/* the wordmark is identity · söhne mono carries the archival voice.
   phase 40 · tracking 0.10 → 0.09em pulls the wordmark slightly
   tighter so it reads as set type rather than spread-out caps. */
.nav-mark {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fg);
  text-decoration: none;
  /* wordmark hover · 1px oxblood underline drawn left-to-right.
     mirrors .page-body a (line ~4257). the publication's name is
     typeset like text in a sentence, not a brand mark. */
  display: inline-block;
  background-image: linear-gradient(to right, var(--accent-text), var(--accent-text));
  background-size: 0 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  padding-bottom: 1px;
  transition: background-size var(--tp-duration-hover) var(--tp-ease);
}
.nav-mark:hover,
.nav-mark:focus-visible { background-size: 100% 1px; }

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

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

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

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

/* section 1 · hero / positioning */

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


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

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

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

/* hero entrance · single reveal contract (phase 37)
   ──────────────────────────────────────────────────────────
   default: hero pieces start invisible and lifted. when js is
   off (no .js class on <html>), the override below paints them
   at rest immediately so no-JS visitors see the hero. when js
   is on and html.enhanced has been added (after DOMContentLoaded
   in app.js), each piece animates up in sequence with the trust
   mark participating as the last beat. reduced-motion users see
   the resting state regardless. */
/* hero reveal · two-phase gate. html.js is set pre-paint by the
   appearance bootstrap and hides the hero so a fonts-driven reflow
   never lands inside the reveal. the animation only starts once
   html.fonts-ready is set (by /js/fonts.js, after document.fonts.ready
   resolves or a 1.5s safety timeout). by then the metric-stable
   italic for "growth systems" / "systèmes de croissance" is committed,
   so the reveal plays from a fully-composed line. when js is disabled
   the hide rule never matches and the hero paints resting. */
html.js .hero-statement,
html.js .hero-body {
  opacity: 0;
  transform: translateY(14px);
}
html.js.fonts-ready .hero-statement {
  animation: revealUp 800ms cubic-bezier(.2,.7,.2,1) 90ms forwards;
}
html.js.fonts-ready .hero-body {
  animation: revealUp 850ms cubic-bezier(.2,.7,.2,1) 180ms forwards;
}
/* gate v2 · per-line hero settle.
   render_pages.py wraps each <br>-separated line of .hero-statement
   in <span class="hero-line" style="--line-index: n;">. each line
   starts opacity:0 + translateY(2px) and settles to rest over 600ms
   with the editorial decelerating curve, 80ms stagger per line. the
   inner spans inherit the parent .hero-statement's opacity:0
   initial state but the per-line settle is the visible motion —
   the outer keyframe is still kept so the no-split fallback works.
   .hero-body keeps its single-element revealUp (body copy is prose,
   not a typographic statement, so per-line wrapping doesn't add
   meaning). */
html.js .hero-line {
  display: inline;
  opacity: 0;
  transform: translateY(2px);
  transition: opacity 600ms cubic-bezier(0.22, 1, 0.36, 1),
              transform 600ms cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: calc(var(--line-index, 0) * 80ms);
}
html.js.fonts-ready .hero-line {
  opacity: 1;
  transform: none;
}
/* hero-static · arrival from the language gate.
   the gate-background already plays the reveal behind the scrim,
   so when the visitor dismisses the gate the destination should
   paint the hero AT REST instead of repeating the animation.
   language-gate.js writes sessionStorage.tp-skip-hero-anim before
   navigating; the bootstrap inline <script> in <head> reads &
   removes it pre-paint and adds html.hero-static.
   direct visitors (no gate handoff) carry no flag → full reveal.
   higher specificity (.js.hero-static) than the .js / .js.fonts-ready
   rules above so this wins without !important.
   gate v2 · also pins .hero-line at rest, since the per-line settle
   would otherwise replay one-by-one after a gate handoff. */
html.js.hero-static .hero-statement,
html.js.hero-static .hero-body,
html.js.hero-static .hero-line {
  opacity: 1;
  transform: none;
  animation: none;
  transition: none;
}
/* hero entrance keyframe · animates from the initial-hidden state
   set above (opacity: 0; transform: translateY(14px)) to resting.
   referenced by each of the four hero pieces with a staggered delay. */
@keyframes revealUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
@media (prefers-reduced-motion: reduce) {
  .hero-statement,
  .hero-body,
  .hero-line {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
    transition: none !important;
  }

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

  /* gate v2 · the gate-exit choreography collapses to an instant
     dismissal under reduced-motion; the JS still runs the swap, but
     the visual cross-fade is suppressed. */
  .modal-shell-scrim[data-gate-exiting],
  .modal-shell-scrim[data-gate-exiting] .shell {
    transition: none !important;
  }
  .lang-cell[data-tap-ack] {
    transition: none !important;
  }

  *, *::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,
  .hero-line,
  .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, including the
   hero → approach top hairline on the first section. */
.home-profile .section-label {
  padding-bottom: 16px;
  border-bottom: 1px solid var(--rule-default);
  margin-bottom: clamp(28px, 4vh, 48px);
  display: flex;
  align-items: baseline;
  gap: 14px;
}
.home-profile .section-label .label-num {
  margin-right: 0;
}
.home-profile > section {
  border-bottom: 0;
}
.home-profile > section:first-of-type {
  border-top: 0;
}

/* section 2 · focus & approach */

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

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

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

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

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

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

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


/* section 3 · trajectory */

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

.trajectory-marker {
  display: block;
  width: 0.55rem;
  height: 0.55rem;
  border: 1px solid var(--trajectory-marker);
  border-radius: 999px;
  background: var(--bg);
  box-sizing: border-box;
  /* micro-int #5 · marker fills oxblood when the card is hovered
     or has keyboard focus inside. nothing else on the card moves
     — scale, shadow, position all stay. the marker IS the timeline
     marker; making it the only thing that changes keeps the card
     reading as a record, not a button. */
  transition: background-color var(--tp-duration-hover) var(--tp-ease),
              border-color var(--tp-duration-hover) var(--tp-ease);
}
.trajectory-item--current .trajectory-marker {
  width: 0.7rem;
  height: 0.7rem;
  background: var(--trajectory-current);
  border-color: var(--trajectory-current);
}
.trajectory-card:hover .trajectory-marker,
.trajectory-card:focus-within .trajectory-marker {
  background: var(--tp-oxblood);
  border-color: var(--tp-oxblood);
}

.trajectory-card {
  /* surface-unification · the timeline cards join the inline-card
     family: 8px radius and a barely-visible --tp-rule-faint hairline
     border (the previous border:0 left them reading as floating
     paper tones; the hairline grounds them as objects in the flow).
     paper-raised-high surface kept for editorial warmth distinct
     from the front-of-house ivory. */
  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: 1px solid var(--tp-rule-faint);
  border-radius: var(--tp-radius-inline-card);
}

.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;
    /* cohesion sprint · timeline vertical breathing +~12% (was 1.6/2.2rem) —
       the chronology read compressed against the surrounding section rhythm. */
    padding: 0 0 clamp(1.8rem, 6.7vw, 2.5rem) clamp(1rem, 3.5vw, 1.4rem);
  }
  .trajectory-item + .trajectory-item {
    border-top: 1px solid var(--trajectory-line);
    padding-top: clamp(1.8rem, 6.7vw, 2.5rem);
  }
  .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(2rem, 3.9vw, 3.15rem);
    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);
    /* cohesion sprint · +~12% top separation so the horizontal chronology
       sits with more air beneath the credentials block above it. */
    margin: clamp(3.4rem, 7.8vw, 5.6rem) 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 {
    /* phase 92 · laptop title→lede gap −10% so the record card surfaces
       inside the first viewport. */
    margin-bottom: 1rem;
  }
  .verify-page .verify-root {
    /* phase 92 · lede→card gap −10% (1.6 → 1.45rem). */
    margin-top: 1.45rem;
  }

  /* 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. surface-unification · 8px (the inline-
     card tier — visually distinct from the 12px overlay modal that
     the same content uses when accessed via the access dialog). */
  background: var(--paper-project);
  border: 1px solid var(--rule);
  border-radius: var(--tp-radius-inline-card);
  padding: clamp(36px, 8vw, 48px) clamp(28px, 8vw, 44px) clamp(34px, 9vw, 52px);
  margin-top: clamp(40px, 7vh, 64px);
  position: relative;
}


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

.preview-title {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  color: var(--fg);
  /* long-token resilience · project-preview titles can overrun their
     column at 320px width without `overflow-wrap: anywhere`. */
  overflow-wrap: anywhere;
  word-break: normal;
}

.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 {
  /* cohesion sprint · +~15% bottom padding so the contact line settles
     with more separation above the footer colophon. */
  padding: clamp(80px, 12vh, 160px) 0 clamp(72px, 9vh, 116px);
}

@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
   values can overrun their column at 320px width without
   `overflow-wrap: anywhere`. fingerprint grids are
   component-controlled and therefore excluded here. */
.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;
}

/* consecutive secondary links (linkedin → github) read as one group:
   the first keeps the 20px separation, the rest tuck in close. */
.contact-secondary + .contact-secondary {
  margin-top: 6px;
}

.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. */
  /* phase 92 · all gaps trimmed ~10% so the hero stack reads denser
     and the PAGE RECORD card surfaces inside the first viewport. */
  --hero-kicker-gap: 0.55rem;
  --hero-title-gap-mobile: 0.75rem;
  --hero-title-gap-tablet: 1.05rem;
  --hero-title-gap-desktop: 1.45rem;
  --hero-lede-gap-mobile: 1.4rem;
  --hero-lede-gap-tablet: 1.75rem;
  --hero-lede-gap-desktop: 2.3rem;
  /* 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); }

/* construction-rail clearance · obsolete. the rail is now position:
   static and contributes its own height to normal document flow on
   every page except the language gate (where it is fixed and there
   is no normal-flow content to clear). no body bottom-padding is
   needed; the rail simply sits below the footer at end of document. */

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

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

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

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

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

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

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

/* phase 96 · same hit-area treatment for the fr machine-translation
   inline note's link. the link sits inside an <em> inside the colophon
   row, so we scope the pseudo-element to the anchor only. */
.site-footer__translation-inline a{
  position:relative;
  display:inline-flex;
  align-items:center;
  min-height:44px;
  color:inherit;
  text-decoration:none;
  border-bottom:1px solid color-mix(in srgb, var(--accent) 28%, transparent);
  padding-bottom:1px;
  transition:color 0.2s, border-color 0.2s;
}
.site-footer__translation-inline a::before{
  content:"";
  position:absolute;
  inset:-8px;
}
.site-footer__colophon-link code{
  font-family:inherit;font-size:inherit;letter-spacing:.04em;
  color:inherit;
}

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

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

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

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

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

/* mobile <= 680 · centred stack with full hit targets. matches the
   prototype's narrow-viewport behaviour for the colophon. */
@media (max-width: 680px) {
  .site-footer__colophon{
    justify-self:center;
    text-align:center;
  }
}
@media (max-width: 640px) {
  .site-footer__inner{
    /* cohesion sprint · slightly more mobile breathing so the footer strata
       don't read crowded at small breakpoints. (global; footer is canonical.) */
    gap:0.85rem;
    padding-inline:clamp(1rem, 4vw, 1.5rem);
    padding-block:1.35rem;
    padding-bottom:max(1rem, env(safe-area-inset-bottom, 0px));
  }
  .site-footer__identity,
  .site-footer__nav,
  .site-footer__colophon{max-width:20rem;line-height:1.3}
}

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

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

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

/* phase 94 · footer direction a · institutional continuity,
   fr machine-translation disclosure.
   two additions scoped to existing footer selectors or new
   classes (.since · .site-footer__translation-inline).
   no existing rules modified · no new tokens · no !important. */

/* 1 · institutional-continuity year. © 1997 – 2026 · the "1997 –"
   sits in a fainter register so it reads as duration, not the
   active year. */
.site-footer__identity .year .since {
  color: var(--fg3);
  opacity: 0.5;
  letter-spacing: 0.06em;
}

/* 2 · machine-translation inline note. replaces "publiée
   aujourd'hui" in the fr colophon row. italic serif against the
   mono caps date — the typographic gesture of a printed colophon. */
.site-footer__translation-inline {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 300;
  font-size: 13.5px;
  line-height: 1.45;
  color: color-mix(in srgb, var(--fg) 50%, transparent);
  text-transform: none;
  letter-spacing: 0;
  text-wrap: pretty;
  font-feature-settings: "onum" 1;
}
.site-footer__translation-inline a:hover,
.site-footer__translation-inline a:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
  outline: 0;
}

/* mobile · the fr colophon row otherwise wraps mid-sentence between
   the mono date and the italic note. stack vertically. */
@media (max-width: 640px) {
  .site-footer__colophon-row {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
  }
  .site-footer__colophon-row .site-footer__colophon-sep {
    display: none;
  }
  .site-footer__translation-inline {
    font-size: 13px;
    max-width: 36ch;
    text-align: center;
  }
}

/* print · the translation note is screen furniture, not part of the
   printed page. the existing .site-footer__colophon is already
   suppressed in print; add this to that posture. */
@media print {
  .site-footer__translation-inline {
    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. */
/* phase 96 · per-line stagger index without inline style="--line-index"
   (which violates csp style-src-attr 'none'). general-sibling chain
   counts only .hero-line predecessors in the same parent; the base
   --line-index: 0 rides on the block rule, and six rules cover up to
   six lines per language. specificity ladder (0,1,0 → 0,6,0) means
   the longest matching selector wins, so each line lands on the
   right --line-index value at zero cost. */
.hero-line { display: block; --line-index: 0; }
.hero-line + .hero-line { margin-top: 0.06em; }
.hero-line ~ .hero-line { --line-index: 1; }
.hero-line ~ .hero-line ~ .hero-line { --line-index: 2; }
.hero-line ~ .hero-line ~ .hero-line ~ .hero-line { --line-index: 3; }
.hero-line ~ .hero-line ~ .hero-line ~ .hero-line ~ .hero-line { --line-index: 4; }
.hero-line ~ .hero-line ~ .hero-line ~ .hero-line ~ .hero-line ~ .hero-line { --line-index: 5; }
.hero-stack {
  line-height: 0.98;
  letter-spacing: -0.035em;
  text-wrap: pretty;
}
.hero-stack .hero-line { text-wrap: balance; }

/* phase 94 · the animation override at `html.js .hero-line` flips
   the line back to display:inline so the homepage hero (which uses
   <br> between lines + per-line --line-index stagger) reads as a
   single fading block. .page-title.hero-stack pages have NO <br>
   between their lines, so that inline override collapses the
   composition into a single run-on line. force the block layout
   back for .hero-stack contexts — specificity (0,3,1) beats the
   (0,2,1) `html.js .hero-line` rule above. */
html.js .hero-stack .hero-line { display: block; }
.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. micro-int v1 ·
   replaces the static text-decoration with a gradient-backed underline
   that draws left-to-right on hover/focus over 200ms. quiet at rest
   (the underline starts at width 0), arrives only when the reader
   reaches for the link. degrades to a hover colour change under
   prefers-reduced-motion (handled at the bottom of the file). */
.page-body a {
  color: var(--fg);
  text-decoration: none;
  background-image: linear-gradient(to right, var(--accent-text), var(--accent-text));
  background-size: 0 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  padding-bottom: 1px;
  border-bottom: none;
  transition: background-size var(--tp-duration-hover) var(--tp-ease);
}

.page-body a:hover,
.page-body a:focus-visible {
  background-size: 100% 1px;
}

.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: 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-word;
  user-select: text;
  -webkit-user-select: text;
}

/* 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. */

/* 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);
}
/* group-label count register — "pages, 12". same fg3 mono register
   as the label itself; no extra colour. */
.source-group-count {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.66rem;
  font-weight: 400;
  letter-spacing: 0.06em;
  color: var(--fg3);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.source-registry {
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
}
/* registry row — a three-area micro-grid:
   row 1: filename (left) · mono meta (right)
   row 2: editorial description spanning the full width
   no per-row border. rhythm comes from generous padding so the
   page reads as an authored ledger, not a directory index. a
   single faint hairline appears between entries via the +
   sibling selector at very reduced opacity. */
.source-entry {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: clamp(0.85rem, 3.5vw, 1.5rem);
  row-gap: clamp(0.32rem, 0.9vw, 0.45rem);
  align-items: baseline;
  padding-block: clamp(0.85rem, 1.8vw, 1.05rem);
}
.source-entry + .source-entry {
  border-block-start: 1px solid color-mix(in srgb, var(--bd-soft) 55%, transparent);
}
.source-entry:last-child {
  border-bottom: 0;
}
.source-entry-name {
  margin: 0;
  min-width: 0;
  font-family: var(--mono);
  font-size: clamp(0.78rem, 2.4vw, 0.86rem);
  font-weight: 500;
  line-height: 1.45;
  letter-spacing: 0.01em;
  color: var(--fg);
  word-break: break-all;
  overflow-wrap: anywhere;
  font-variant-ligatures: none;
}
.source-entry-name a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
  transition: color 0.2s, border-bottom-color 0.2s;
}
.source-entry-name a:hover,
.source-entry-name a:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
  outline: 0;
}
.source-entry-name code {
  font-family: inherit;
  font-size: inherit;
  background: transparent;
  padding: 0;
}
/* quiet "raw" link alongside the reader link */
.source-entry-raw {
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg3);
  text-decoration: none;
  margin-inline-start: 0.5rem;
  opacity: 0.7;
  transition: opacity 0.15s ease, color 0.15s ease;
}
.source-entry-raw:hover,
.source-entry-raw:focus-visible {
  color: var(--accent-text);
  opacity: 1;
}
.source-entry-meta {
  margin: 0;
  justify-self: end;
  text-align: right;
  font-family: var(--mono);
  font-size: 0.66rem;
  line-height: 1.45;
  letter-spacing: 0.04em;
  color: var(--fg3);
  opacity: 0.82;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  font-variant-ligatures: none;
  white-space: normal;
}
.source-entry-meta abbr {
  text-decoration: none;
  border: 0;
  cursor: default;
  color: var(--fg3);
}
.source-entry-sep {
  margin: 0 0.18rem;
  opacity: 0.55;
}
.source-entry-size {
  font-variant-numeric: tabular-nums lining-nums;
}
.source-entry-hash {
  /* the hash is a citation, not a primary label. soft enough to
     recede when the eye is scanning filenames; selectable and
     full-precision via the title attribute on the <samp>. */
  margin: 0.18rem 0 0;
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.045em;
  color: color-mix(in srgb, var(--fg3) 70%, transparent);
  line-height: 1.4;
}
.source-entry-hash abbr {
  text-decoration: none;
  border: 0;
  cursor: default;
  margin-right: 0.35rem;
  opacity: 0.7;
}
.source-entry-hash samp {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
}

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

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

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

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

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

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

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

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

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


/* reduced motion */


/* print */


}
@layer components {
/* /security/ as a posture document — open editorial sections with
   real h2 headings, soft hairline rules between them, no accordion
   chrome. phase 62 · section rhythm and sub-section spacing widened
   ~7% and the inter-section rule softened to var(--rule) so spacing,
   not lines, carries the hierarchy on this denser page. */
.security-section {
  /* phase 94 · section rhythm widened (~35%) so /security/ reads
     more like a white paper and less like a dense ledger. the
     margin opens the gap above the rule; the padding opens the
     gap between rule and h2. */
  margin: clamp(64px, 9vw, 96px) 0;
  padding: clamp(40px, 6vw, 56px) 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;
}

/* supporting-note · muted mono aside placed under a page lede, pointing
   security + privacy readers at the public documentation surface. shares
   the architecture-note register; the link carries the oxblood underline. */
.supporting-note {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  color: var(--fg2);
  margin: clamp(14px, 2vw, 20px) 0 0;
  line-height: 1.55;
}
.supporting-note a {
  color: var(--ink);
  text-decoration-color: color-mix(in srgb, var(--ink) 40%, transparent);
  text-underline-offset: 3px;
  transition: text-decoration-color 160ms var(--rail-ease);
}
.supporting-note a:hover,
.supporting-note a:focus-visible {
  text-decoration-color: var(--ink);
}

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

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

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

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

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

/* ─── phase 92 · modal-system shell ───────────────────────────
   one paper-card shell shared by the language gate, the verify
   action menu and the access modal. the shell never changes;
   only the .body-region inside it varies per modal. surface-
   unification · the warm-brown scrim at 18% alpha alone carries
   the modal seal — no backdrop-filter, no publication blur. the
   publication beneath stays crisp so the modal reads as a card
   lifted above the page, not a glass plate covering it. tokens
   used here are declared in @layer tokens above. */

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

/* shell · the paper card. width caps at 32rem so it never feels
   like a dialog box; padding clamps so the inner gutter scales
   with viewport. surface-unification · the hairline border drops
   to --tp-rule-faint (3% ink, barely visible but does meaningful
   work at the edge) and the shadow becomes the soft cast
   --tp-shadow-overlay rather than the older two-layer shape. the
   panel reads as a tipped-in card placed on the page, not as a
   floating system sheet. */
.shell {
  width: min(100%, 32rem);
  background: var(--modal-paper);
  border: 1px solid var(--tp-rule-faint);
  border-radius: var(--tp-radius-overlay);
  padding: var(--modal-pad);
  box-shadow: var(--tp-shadow-overlay);
  position: relative;
  opacity: 0;
  transform: translateY(6px) scale(0.992);
  transition: opacity var(--modal-motion), transform var(--modal-motion);
}
.modal-shell-scrim.is-active .shell {
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* × close · top-right · the canonical site close control.
   consumes the --tp-modal-close-* tokens so every overlay (language
   gate, verify modal, project access modal, future diagnostics
   layers) reads as one control — no oxblood pinking, no app-style
   chip on any one modal. pressed-into-paper utility: paper-warm
   ground, near-invisible warm-brown border, muted ink glyph at
   light weight. three states register a tap without bouncing:
   rest, hover (desktop only — :hover is gated by (hover: hover) so
   iOS never sticks the focus state and forces a second tap), and
   :active for the press itself. 40×40 desktop, 44×44 at ≤520px so
   the touch target clears wcag's 44px floor at every viewport. */
.shell-close {
  position: absolute;
  top: clamp(1rem, 3vw, 1.5rem);
  right: clamp(1rem, 3vw, 1.5rem);
  inline-size: 40px;
  block-size: 40px;
  display: grid;
  place-items: center;
  border-radius: var(--tp-radius-pill);
  border: 1px solid var(--tp-modal-close-border);
  background: var(--tp-modal-close-bg);
  color: var(--tp-modal-close-color);
  cursor: pointer;
  font-family: var(--mono);
  font-weight: 300;
  font-size: 1.25rem;
  line-height: 1;
  /* suppress ios safari's grey tap flash; our own :active state
     carries the press feedback. */
  -webkit-tap-highlight-color: transparent;
  transition: background-color var(--tp-duration-hover) var(--tp-ease);
}
/* desktop hover · gated by (hover: hover) so touch devices skip
   this branch entirely. without the gate, ios safari emulates :hover
   on first tap → the button stays in the hover state and the user
   has to tap again to fire click. */
@media (hover: hover) {
  .shell-close:hover {
    background: var(--tp-modal-close-hover-bg);
  }
}
.shell-close:focus-visible {
  background: var(--tp-modal-close-hover-bg);
  outline: 0;
}
/* press feedback · momentary deeper tonal shift. registers on both
   mouse and touch the instant the press lands, so the user sees
   the modal acknowledge the tap before the dismissal animation. */
.shell-close:active {
  background: var(--tp-modal-close-active-bg);
}
@media (max-width: 520px) {
  .shell-close {
    inline-size: 44px;
    block-size: 44px;
    font-size: 1.35rem;
  }
}

/* title · signifier 300 ~32px, balanced. the title carries the
   modal — no kicker, no wordmark above. */
.shell-title {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(26px, 4.4vw, 32px);
  line-height: 1.08;
  letter-spacing: -0.014em;
  color: var(--fg);
  text-align: center;
  text-wrap: balance;
  /* tightened to 0.4rem now that the page-name subtitle is removed —
     the title sits closer to the menu border with no intermediate
     line to span over. */
  margin: 0 auto 0.4rem;
  /* an ~18ch inline measure lets text-wrap:balance settle on two even
     lines ("verify, cite or / read the source.") rather than three. */
  max-inline-size: 18ch;
  font-feature-settings: "kern" 1, "liga" 1, "onum" 1;
}

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

/* welcome line · the return-reader acknowledgement. replaces the lede
   on return visits where the bucket is non-null and the per-day session
   sentinel is unset. quieter than the lede — mono small-caps in fg3 /
   fg2 — and ephemeral: fades in over 300ms, holds 4s, fades out over
   250ms. the heading + choices remain after fadeout; the lede slot is
   empty for the rest of the session because the welcome line IS the
   acknowledgement. */
.shell-welcome {
  display: block;
  max-width: 34ch;
  margin: 0 auto 1.8rem;
  text-align: center;
}
.shell-welcome .welcome-back,
.shell-welcome .welcome-last {
  display: block;
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1.6;
  opacity: 0;
  animation: tp-welcome 4.55s ease forwards;
  font-variant-numeric: tabular-nums lining-nums;
}
.shell-welcome .welcome-back  { color: var(--fg3); }
.shell-welcome .welcome-last  { color: var(--fg2); }
@keyframes tp-welcome {
  0%    { opacity: 0; }
  6.6%  { opacity: 0.72; }   /* 300ms fade-in */
  94.5% { opacity: 0.72; }   /* hold ~4s */
  100%  { opacity: 0; }      /* 250ms fade-out */
}
@media (prefers-reduced-motion: reduce) {
  .shell-welcome .welcome-back,
  .shell-welcome .welcome-last {
    animation: none;
    opacity: 0.72;
  }
}

/* editorial line container · short sentences stacked vertically without
   manual <br>. used inside .shell-lede first-visit copy and .contact-
   headline on the homepage. composes with any wrapper that sets
   line-height / max-width; the only thing it adds is column-flow plus
   a tight vertical gap. */
.stack-copy {
  display: flex;
  flex-direction: column;
  gap: 0.15em;
}
@media (max-width: 480px) {
  .stack-copy { gap: 0.1em; }
}

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

/* verify modal uses a padded title wrapper (so the menu rows can
   be full-bleed) holding the symbolic anchor + the title. no
   subtitle — the title alone speaks. */
.shell-pad {
  /* section gap tightened slightly so the title block reads as one
     unit with the menu beneath, now that the page-name subtitle is
     gone. */
  padding: 0 0 0.62rem;
  text-align: center;
}
/* symbolic anchor · hairline outline mark above the title. muted ink,
   centred, slightly faded so it reads as a quiet stamp rather than a
   feature icon. used by the verify modal; available to any future
   overlay that wants the same anchor. */
.shell-anchor {
  display: flex;
  justify-content: center;
  align-items: center;
  color: var(--fg2);
  opacity: 0.55;
  margin: 0 auto 0.72rem;
  line-height: 0;
}
.shell-anchor svg {
  display: block;
}

/* ─── body region · neutral container for whatever body the modal
   ships ──── per-modal sub-rules follow, starting with the
   language gate. */

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

/* ─── under-construction notice · global construction rail ───
   the construction marker is an authored signature from the early
   web — visible across every public page, pinned to the bottom of
   the viewport. originally lived only behind the language gate as
   .home-ribbon; lifted to global render-time injection so the rail
   appears once per page (root gate, /en-au/, /fr/, verify,
   integrity, privacy, security, source viewer, error pages,
   /local/) without per-template duplication.

   visual rules below are byte-identical to the historical gate-
   ribbon (stripe angle, dot, label, opacity, transition curve);
   only the positioning context changed from absolute-inside-
   gate-background to fixed-to-viewport. on the gate page the two
   read the same because .gate-background is itself fixed: inset 0.

   z-index: 50 — sits above page content (default 0–10) and below
   the modal scrim (200) and source-view toolbar (80). decorative,
   pointer-events disabled, aria-hidden, no focusable elements. */

:root {
  /* rail height — used by main / footer / source-toolbar bottom
     offsets so nothing on a short page lands underneath the rail. */
  --rail-height: 30px;
  --rail-opacity: 1;
  /* editorial easing — reuses the cubic the rail has used since
     the v1 gate-ribbon fade. exposed as a token so the first-load
     animation keyframes can share the curve. */
  --rail-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
@media (max-width: 520px) {
  :root { --rail-height: 26px; }
}

.construction-rail {
  /* default: in normal document flow at end of body — readers only
     see the rail when they scroll all the way to the page foot. on
     the language gate (which has no normal-flow content; gate-
     background and modal scrim are both fixed:inset:0) the rail is
     repositioned to viewport bottom by the scoped override below. */
  position: static;
  width: 100%;
  padding: 10px clamp(24px, 5vw, 80px);
  /* safe-area pad — pushes the label clear of the iOS home indicator
     so it never hides underneath the bottom inset on iPhones when the
     rail is in view (whether end-of-document scroll or fixed on the
     gate). */
  padding-bottom: max(10px, calc(10px + env(safe-area-inset-bottom, 0px)));
  border-top: 1px solid var(--rule);
  background: color-mix(in srgb, var(--paper-record) 70%, transparent);
  display: flex;
  align-items: center;
  gap: 14px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg3);
  pointer-events: none;
  opacity: var(--rail-opacity);
  transition: opacity 460ms var(--rail-ease);
  will-change: opacity;
}
.construction-rail__stripe {
  display: inline-block;
  flex: 1;
  height: 6px;
  min-width: 40px;
  transform-origin: center;
  background-image: repeating-linear-gradient(
    135deg,
    color-mix(in srgb, var(--accent) 45%, transparent) 0 3px,
    transparent 3px 7px
  );
}
.construction-rail__label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--accent-text);
  font-weight: 500;
  letter-spacing: 0.18em;
  white-space: nowrap;
}
.construction-rail__dot {
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 15%, transparent);
  flex: 0 0 auto;
}
/* language-gate exception · the gate covers the viewport (gate-
   background and modal scrim are both position:fixed inset:0), so
   the rail must be pinned to viewport bottom there or it would sit
   in the empty body flow above. on every other page it stays in
   normal flow at end of document. the rule itself lives in the
   nearest @layer pages block (see body[data-page="language-gate"]
   .construction-rail at the head of the /local/ pages block below)
   because the css-architecture validator requires body[data-page=…]
   selectors to live inside @layer pages. */

/* dismiss-triggered fade · language-gate.js sets
   body[data-gate-dismissed] in startSwap() the same frame the scrim
   starts exiting. the 460ms fade above runs concurrently with the
   scrim/shell 400ms exit, finishing ~60ms after — a soft trailing
   close that reads as part of the editorial choreography. */
body[data-gate-dismissed] .construction-rail {
  opacity: 0;
}

/* first-load assembly · one-time, subtle, archival. with JS enabled
   (every public page sets html.js synchronously in head bootstrap)
   the rail starts at opacity:0 and animates in 250ms after first
   paint — stripes extend from the centre, dot settles, label
   tightens from a wider letter-spacing. without JS the rail is
   visible statically from first paint (the .js gate never matches),
   so a script failure never hides the construction signature.
   pure-CSS trigger, no per-page JS hook required. */
.js .construction-rail {
  animation: rail-fade-in 720ms var(--rail-ease) 250ms both;
}
.js .construction-rail .construction-rail__stripe {
  animation: rail-stripe-reveal 820ms var(--rail-ease) 250ms both;
}
.js .construction-rail .construction-rail__label,
.js .construction-rail .construction-rail__dot {
  animation: rail-mark-reveal 620ms var(--rail-ease) 250ms both;
}
@keyframes rail-fade-in {
  from { opacity: 0; }
  to   { opacity: var(--rail-opacity); }
}
@keyframes rail-stripe-reveal {
  from { transform: scaleX(0); opacity: 0; }
  to   { transform: scaleX(1); opacity: 1; }
}
@keyframes rail-mark-reveal {
  from { opacity: 0; letter-spacing: 0.32em; }
  to   { opacity: 1; letter-spacing: 0.18em; }
}

@media (prefers-reduced-motion: reduce) {
  .construction-rail { transition: none; }
  /* gate the first-load assembly entirely — readers with reduced-
     motion see the rail at its rest state from first paint, never
     animated. */
  .js .construction-rail,
  .js .construction-rail .construction-rail__stripe,
  .js .construction-rail .construction-rail__label,
  .js .construction-rail .construction-rail__dot {
    animation: none !important;
    opacity: var(--rail-opacity);
    transform: none;
    letter-spacing: 0.18em;
  }
}
@media (max-width: 520px) {
  .construction-rail {
    font-size: 9px;
    letter-spacing: 0.12em;
    gap: 8px;
    padding: 8px 16px;
    padding-bottom: max(8px, calc(8px + env(safe-area-inset-bottom, 0px)));
  }
}
@media print {
  .construction-rail { display: none; }
}

/* ─── .invite-link ──── editorial invite to a notable internal
   destination (used today by the privacy page's "browser storage
   is local to your device" → /local/ link). an oxblood underline
   that intensifies on hover/focus; no trailing dot. */
.invite-link {
  color: var(--accent-text);
  text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent);
  text-underline-offset: 3px;
  transition: text-decoration-color 160ms var(--rail-ease),
              color 160ms var(--rail-ease);
}
.invite-link:hover,
.invite-link:focus-visible {
  text-decoration-color: var(--accent);
}

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

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

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

/* reduced motion is already handled site-wide by the global
   @media (prefers-reduced-motion: reduce) rule at line ~2707,
   which sets transition-duration: 0.01ms !important on *,
   *::before, *::after. that rule covers .modal-shell-scrim,
   .shell, and the publication-blur targets at zero new budget. */
}
@layer overrides {
/* phase 94 · verify modal mobile cap. the action menu is a page
   utility, not archive navigation — bound it to ~70vh on mobile so
   it never eats the viewport, with internal scroll if rows overflow.
   #-scoped (id selectors live in the overrides layer per the css
   architecture brief) so the language-gate shell (meant to fill the
   screen on first visit) and the access modal stay unaffected. */
@media (max-width: 520px) {
  #verify-modal {
    max-height: 70vh;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

html.is-language-switching {
  scroll-behavior: auto !important;
}

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

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

/* publication blur · removed in the surface-consistency unification.
   the warm-brown scrim at 18% alpha (--tp-scrim / --overlay-scrim)
   alone carries the modal seal; the publication beneath stays crisp
   so the modal reads as a card lifted above the page, not a glass
   plate covering it. consistent with the brief's blur ban. */

/* ─── 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 size, measure, line-height + bottom-margin.
   font-family is 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;
  font-size: clamp(14px, 3.6vw, 16px);
  max-width: 38ch;
}
.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-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;
    margin-bottom: 0.35rem;
  }
  .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;
  }
}

/* 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);
  /* lets the grid code cell shrink below its intrinsic width so wrapped
     mobile rows cannot push past the .code-shell overflow wrapper. */
  min-width: 0;
}

/* 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) 9%, var(--paper-main));
  outline: 0;
  outline-offset: 1px;
  border-radius: 0;
  transition: background-color var(--transition-quiet), outline-color var(--transition-quiet);
}
/* the active highlight only spans the full scroll width when wrap is off;
   in wrapped mobile mode min-width:100% resolves against intrinsic width
   and would push the row past the .code-shell, so scope it out there. */
.code-reader:not(.is-wrapped) .code-line--range-active {
  min-width: 100%;
}
.code-line--range-marked {
  position: relative;
}
.code-line--range-marked::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: -0.32rem;
  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);
  /* surface-unification · opaque background, no backdrop blur.
     the brief bans backdrop-filter site-wide; this was the last
     surviving violation. an opaque --paper-raised-high reads as
     site furniture against the code panel beneath. */
  background: var(--paper-raised-high);
  border: 1px solid var(--rule-soft);
  box-shadow: 0 6px 20px color-mix(in srgb, var(--ink) 14%, transparent);
  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 desktop rule is also opaque post surface-unification, so the
   only difference here is layout, not appearance. */
@media (max-width: 720px) {
  .source-toolbar {
    position: fixed;
    /* the construction rail is now position:static at end of document
       on every non-gate page, so the toolbar no longer needs to clear
       it — natural safe-area + 1rem bottom inset is enough. */
    inset-block: auto calc(env(safe-area-inset-bottom, 0px) + 1rem);
    inset-inline: 1rem;
    inline-size: auto;
    max-inline-size: none;
    justify-content: center;
  }
}
/* 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: clamp(3rem, 6vw, 4rem) calc(clamp(2.5rem, 8vw, 4rem) + env(safe-area-inset-bottom));
  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;
  row-gap: 0.35rem;
  flex-wrap: wrap;
  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-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: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; }
  .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; }
  }

  /* /security/ · compact mono contents. a quiet table of contents, not
     navigation chrome: hairline rules above and below, mono labels, the
     numbering already lives in the copy so the list marker is suppressed.
     no box, no badge, whitespace and rule-lines carry it. */
  body[data-page="security"] .security-contents {
    margin: clamp(1.6rem, 3vw, 2.2rem) 0 clamp(2.2rem, 4vw, 3rem);
    padding: clamp(0.9rem, 2vw, 1.2rem) 0;
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
  }
  body[data-page="security"] .security-contents-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: clamp(0.4rem, 1.2vw, 0.6rem);
  }
  body[data-page="security"] .security-contents-list a {
    font-family: var(--mono);
    font-size: 0.78rem;
    letter-spacing: 0.02em;
    color: var(--fg2);
    text-decoration: none;
  }
  body[data-page="security"] .security-contents-list a:hover,
  body[data-page="security"] .security-contents-list a:focus-visible {
    color: var(--fg);
  }

  /* /source/ · group jump-list. same quiet register as the security
     contents list — hairline rules above and below, mono labels, no box,
     no badge, whitespace carries it. the catalogue is long on a phone, so
     this exists to spare scroll fatigue; on wider viewports the groups sit
     close enough that the jump-list is unneeded, so it is hidden there. */
  body[data-page="source"] .source-contents {
    margin: clamp(1.6rem, 3vw, 2.2rem) 0 clamp(2.2rem, 4vw, 3rem);
    padding: clamp(0.9rem, 2vw, 1.2rem) 0;
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
  }
  body[data-page="source"] .source-contents-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: clamp(0.4rem, 1.2vw, 0.6rem);
  }
  body[data-page="source"] .source-contents-list a {
    font-family: var(--mono);
    font-size: 0.78rem;
    letter-spacing: 0.02em;
    color: var(--fg2);
    text-decoration: none;
  }
  body[data-page="source"] .source-contents-list a:hover,
  body[data-page="source"] .source-contents-list a:focus-visible {
    color: var(--fg);
  }
  /* wide viewports · the groups read at a glance, so retire the jump-list. */
  @media (min-width: 760px) {
    body[data-page="source"] .source-contents {
      display: none;
    }
  }

  /* /security/acknowledgments/ · last-reviewed cue. one quiet mono line,
     same restrained register as the contents labels; sparse page kept
     sparse. */
  body[data-page="acknowledgments"] .ack-last-reviewed {
    font-family: var(--mono);
    font-size: 0.78rem;
    letter-spacing: 0.02em;
    color: var(--fg2);
    margin-top: clamp(1.6rem, 3vw, 2.2rem);
  }

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

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

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

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

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

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

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

  /* anchor the slip stamp to .page-body / .verify-root, not .verify-card,
     so the torn-paper mask applied to .verify-card doesn't clip the
     overhanging stamp. .verify-root holds .verify-card and the stamp
     as siblings; the stamp is positioned against .verify-root. */
  [data-verify-treatment="slip"] .page-body { position: relative; }
  [data-verify-treatment="slip"] .verify-root { position: relative; }

  /* card shell · phase 92 · the dramatic torn paper retired in favour
     of a restrained deckle edge — museum-archive stock, not ripped
     notebook. mask asset rewritten at /images/textures/torn-edge-top.svg
     (viewBox 400×6, quadratic wave amplitude ≈1.2px, low frequency).
     mask-size top reduced 32px → 6px so the deckle is just legible at
     the top of the sheet without competing with the record itself.
     padding-block-start reclaimed accordingly; the perforation column
     stays. paper raise becomes a single soft drop-shadow per spec. */
  --paper-edge-height: 6px;
  [data-verify-treatment="slip"] .verify-card {
    background: var(--paper-raised-high);
    border: none;
    border-radius: 0;
    padding-block-start: clamp(1.6rem, 3.5vw, 2.4rem);
    padding-inline-start: clamp(2.4rem, 5vw, 3.4rem);
    position: relative;

    -webkit-mask-image: url("/images/textures/torn-edge-top.svg"),
                        linear-gradient(#000, #000);
    -webkit-mask-position: top, bottom;
    -webkit-mask-repeat: no-repeat, no-repeat;
    -webkit-mask-size: 100% var(--paper-edge-height), 100% calc(100% - var(--paper-edge-height));
    -webkit-mask-composite: source-over;
    mask-image: url("/images/textures/torn-edge-top.svg"),
                linear-gradient(#000, #000);
    mask-position: top, bottom;
    mask-repeat: no-repeat, no-repeat;
    mask-size: 100% var(--paper-edge-height), 100% calc(100% - var(--paper-edge-height));
    mask-composite: add;

    filter: drop-shadow(0 2px 10px rgb(0 0 0 / 0.03));
  }

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

  /* citation row · the first dd of .record-grid carries the bibliographic
     citation. serif italic gives it the document-of-record register.
     phase 92 · scale eased 10% (1.02-1.15 → 0.92-1.04rem) so the
     citation reads as opening line, not as a second hero. */
  [data-verify-treatment="slip"] .record-grid__row:first-child dd {
    font-family: var(--serif);
    font-style: italic;
    font-size: clamp(0.92rem, 2.1vw, 1.04rem);
    color: var(--fg);
  }

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

  /* phase 95 · the .verify-slip-chrome wrapper and the .verify-card__header
     right-padding (which reserved space for the stamp seal) are gone with
     the stamp itself. card header now uses the standard padding so the
     eyebrow + h2 sit flush. the rest of the slip treatment — torn paper
     edge, perforation column, italic-serif citation, inset fingerprint
     panel — remains; only the seal was decorative-gimmicky. */

  /* framing · phase 92 · quiet serif italic line beneath the record
     card. not a CTA, not a button — a single sentence that converts
     the card from decorative artefact into "the actual proof of
     authenticity" for the page the reader came from. only shows when
     verify.js has actually rendered a record card; the noscript +
     unknown-route paths suppress it via :has(). */
  .verify-page .verify-framing {
    max-width: 44rem;
    margin: clamp(1.4rem, 3.5vw, 2rem) 0 0;
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: clamp(0.86rem, 1.95vw, 0.98rem);
    line-height: 1.45;
    color: var(--fg);
    opacity: 0.7;
    text-wrap: balance;
  }
  .verify-page .page-body:not(:has(.verify-card)) .verify-framing {
    display: none;
  }

  /* phase 95 · stamp removed. the .verify-slip-stamp svg + its
     positioning / animation / dark-mode / print / forced-colors /
     reduced-motion / keyframe rules were dropped wholesale. dark-
     mode card background still flips via the rule below; only the
     decorative seal is gone. */

  /* dark mode · card swaps to the dark --paper-raised-high token;
     drop-shadow drops away so the dark paper doesn't sit in a halo. */
  :root[data-theme="dark"] [data-verify-treatment="slip"] .verify-card {
    background: var(--paper-raised-high);
    filter: none;
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) [data-verify-treatment="slip"] .verify-card {
      background: var(--paper-raised-high);
      filter: none;
    }
  }

  /* print · the slip card's mask + drop-shadow drop so the printed
     sheet reads as a flat document, not a screen-styled card. */
  @media print {
    [data-verify-treatment="slip"] .verify-card::after { display: none !important; }
    [data-verify-treatment="slip"] .verify-card {
      -webkit-mask-image: none;
              mask-image: none;
      filter: none;
    }
  }

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

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

  /* ── /integrity/releases/ · direction c ────────────────────
     current edition as record, earlier editions as ledger.
     parallel with /integrity/ (same paper, same hairlines).
     scoped to body[data-page="releases"] — no leak. all tokens
     already in :root; no overrides, no !important. */

  body[data-page="releases"] .hero {
    /* the global .hero rule forces min-height: 100vh + flex-end so the
       homepage masthead anchors to the viewport bottom; on /releases/
       the same wrapper would push the entire dossier down to the
       second screen. collapse the box back to natural flow so the
       hero sits directly under the site-header like /privacy/ and
       /integrity/. */
    min-height: auto;
    display: block;
    max-width: 64ch;
    margin-bottom: clamp(40px, 5vw, 64px);
  }
  body[data-page="releases"] .hero-kicker {
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0 0 0.9rem;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="releases"] .hero-title {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(40px, 7vw, 72px);
    line-height: 0.98;
    letter-spacing: -0.022em;
    color: var(--fg);
    margin: 0 0 1.4rem;
  }
  body[data-page="releases"] .hero-title .line { display: block; }
  body[data-page="releases"] .hero-lede {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(17px, 1.9vw, 21px);
    line-height: 1.55;
    color: var(--fg2);
    max-width: 54ch;
    margin: 0;
    font-feature-settings: "onum" 1;
    text-wrap: pretty;
  }

  /* current edition · record card (mirrors /integrity/) */
  body[data-page="releases"] .record {
    background: var(--paper-raised-high);
    border: 1px solid var(--rule);
    border-radius: var(--radius-soft);
    padding: clamp(28px, 3.6vw, 40px);
    margin-bottom: clamp(48px, 6vw, 72px);
    box-shadow: 0 14px 36px rgb(0 0 0 / 0.04);
    position: relative;
  }
  body[data-page="releases"] .record-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 1rem;
    padding-bottom: 1.4rem;
    margin-bottom: 1.6rem;
    border-bottom: 1px solid var(--rule);
  }
  body[data-page="releases"] .record-header .left {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
  }
  body[data-page="releases"] .record-header .kicker {
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--accent-text);
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="releases"] .record-header .date {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(28px, 3.4vw, 36px);
    line-height: 1.04;
    letter-spacing: -0.014em;
    color: var(--fg);
    margin: 0;
    font-feature-settings: "onum" 1;
  }
  body[data-page="releases"] .record-header .title {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: clamp(17px, 1.8vw, 19px);
    line-height: 1.4;
    color: var(--fg2);
    margin: 0;
    max-width: 38ch;
    font-feature-settings: "onum" 1;
  }
  body[data-page="releases"] .record-header .right {
    text-align: right;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fg3);
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="releases"] .record-header .right .tag {
    display: inline-block;
    color: var(--fg);
    border: 1px solid var(--accent);
    padding: 3px 7px;
    border-radius: 2px;
    background: color-mix(in srgb, var(--accent) 7%, transparent);
    font-size: 9.5px;
  }
  body[data-page="releases"] .record-header .right .acc {
    display: block;
    margin-top: 0.5rem;
    color: var(--fg3);
  }

  /* two-column group grid · collapses to one column at <= 680px */
  body[data-page="releases"] .record-groups {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: clamp(20px, 2.4vw, 32px) clamp(24px, 3vw, 48px);
  }
  body[data-page="releases"] .group { padding: 0; }
  body[data-page="releases"] .group-label {
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0 0 0.9rem;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="releases"] .group dl { margin: 0; }
  body[data-page="releases"] .rg {
    display: flex;
    justify-content: space-between;
    gap: 0.8rem;
    padding: 0.55rem 0;
    border-top: 1px solid var(--rule-faint);
  }
  body[data-page="releases"] .rg:first-of-type {
    border-top: 0;
    padding-top: 0;
  }
  body[data-page="releases"] .rg dt {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: 14px;
    color: var(--fg2);
    margin: 0;
    font-feature-settings: "onum" 1;
  }
  body[data-page="releases"] .rg dd {
    font-family: var(--mono);
    font-size: 11px;
    color: var(--fg);
    text-align: right;
    letter-spacing: 0.02em;
    margin: 0;
    overflow-wrap: anywhere;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="releases"] .rg dd a {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
  }
  body[data-page="releases"] .rg dd a:hover {
    border-bottom-color: var(--accent);
    color: var(--accent-text);
  }
  body[data-page="releases"] .rg dd code {
    font-family: var(--mono);
    font-size: 1em;
    color: inherit;
  }

  /* fingerprint group · full width across the bottom of the card */
  body[data-page="releases"] .record-fp {
    grid-column: 1 / -1;
    padding-top: 1.4rem;
    margin-top: 0.6rem;
    border-top: 1px solid var(--rule);
  }
  body[data-page="releases"] .record-fp .fingerprint {
    font-family: var(--mono);
    font-size: 13px;
    color: var(--fg);
    font-variant-numeric: tabular-nums lining-nums;
    letter-spacing: 0.06em;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem 1.4rem;
    margin: 0.4rem 0 0.8rem;
  }
  body[data-page="releases"] .record-fp .actions {
    display: flex;
    gap: 1.2rem;
    flex-wrap: wrap;
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }
  body[data-page="releases"] .record-fp .actions .primary {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 400;
    font-size: 17px;
    color: var(--accent-text);
    text-decoration: none;
    text-transform: none;
    letter-spacing: -0.01em;
    border-bottom: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
    padding-bottom: 1px;
    line-height: 1;
    font-feature-settings: "onum" 1;
  }
  body[data-page="releases"] .record-fp .actions .primary .arr {
    font-family: var(--mono);
    font-style: normal;
    font-size: 0.7em;
    margin-left: 0.3em;
  }
  body[data-page="releases"] .record-fp .actions .primary:hover {
    border-bottom-color: var(--accent);
  }
  body[data-page="releases"] .record-fp .actions button,
  body[data-page="releases"] .record-fp .actions .secondary {
    background: transparent;
    border: 0;
    padding: 0 0 1px;
    font: inherit;
    color: var(--fg2);
    text-decoration: none;
    letter-spacing: inherit;
    text-transform: inherit;
    cursor: pointer;
    border-bottom: 1px solid var(--rule-default);
  }
  body[data-page="releases"] .record-fp .actions button:hover,
  body[data-page="releases"] .record-fp .actions .secondary:hover {
    color: var(--accent-text);
    border-bottom-color: var(--accent);
  }
  /* copy-fingerprint feedback · the trigger transforms in place via
     copy.js's default textContent swap; never a floating stamp/toast.
     while the feedback window is open the button carries
     data-state="copied", which is our css hook for the subtle colour
     shift. matches feedback_no_floating_feedback_trentpower. */
  body[data-page="releases"] .record-fp .actions button[data-state="copied"] {
    color: var(--accent-text);
    border-bottom-color: var(--accent);
  }
  body[data-page="releases"] .record-fp .actions button[data-state="failed"] {
    color: var(--fg2);
    border-bottom-color: var(--rule-default);
  }

  /* earlier editions · hairline ledger */
  body[data-page="releases"] .earlier-section {
    margin-top: clamp(40px, 5vw, 56px);
  }
  body[data-page="releases"] .earlier-label {
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0 0 1rem;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="releases"] .earlier-label .count { color: var(--fg3); }
  body[data-page="releases"] .earlier-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border-top: 1px solid var(--rule-default);
  }
  body[data-page="releases"] .earlier-row {
    display: grid;
    grid-template-columns: 4.6rem 1fr auto;
    gap: 1.4rem;
    padding: 1.1rem 0;
    border-bottom: 1px solid var(--rule);
    align-items: start;
    transition: background 0.18s ease;
  }
  body[data-page="releases"] .earlier-row:hover {
    background: color-mix(in srgb, var(--accent) 3%, transparent);
  }
  body[data-page="releases"] .earlier-row .acc {
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--accent-text);
    padding-top: 0.25rem;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="releases"] .earlier-row .stack .date {
    font-family: var(--serif);
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    color: var(--fg);
    margin: 0 0 0.25rem;
    letter-spacing: -0.012em;
    font-feature-settings: "onum" 1;
  }
  body[data-page="releases"] .earlier-row .stack .title {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: 15.5px;
    color: var(--fg2);
    margin: 0 0 0.35rem;
    text-wrap: pretty;
    font-feature-settings: "onum" 1;
  }
  body[data-page="releases"] .earlier-row .stack .sha {
    font-family: var(--mono);
    font-size: 10.5px;
    color: var(--fg3);
    letter-spacing: 0.04em;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="releases"] .earlier-row .stack .sha .k {
    letter-spacing: 0.12em;
    text-transform: uppercase;
    font-size: 9.5px;
    margin-right: 0.5em;
  }
  body[data-page="releases"] .earlier-row .stack .sha.is-historical {
    font-style: italic;
    font-family: var(--serif);
    font-size: 13px;
    letter-spacing: 0;
    text-transform: none;
  }
  body[data-page="releases"] .earlier-row .actions {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.5rem;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }
  body[data-page="releases"] .earlier-row .actions .verb {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 400;
    font-size: 15px;
    color: var(--accent-text);
    text-decoration: none;
    text-transform: none;
    letter-spacing: -0.01em;
    border-bottom: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
    padding-bottom: 1px;
    line-height: 1;
    font-feature-settings: "onum" 1;
  }
  body[data-page="releases"] .earlier-row .actions .verb .arr {
    font-family: var(--mono);
    font-style: normal;
    font-size: 0.7em;
    margin-left: 0.3em;
  }
  body[data-page="releases"] .earlier-row .actions .verb:hover {
    border-bottom-color: var(--accent);
  }
  body[data-page="releases"] .earlier-row .actions .downloads {
    display: flex;
    gap: 0.9rem;
    color: var(--fg3);
  }
  body[data-page="releases"] .earlier-row .actions .downloads a {
    color: var(--fg2);
    text-decoration: none;
    border-bottom: 1px solid var(--rule-default);
    padding-bottom: 1px;
  }
  body[data-page="releases"] .earlier-row .actions .downloads a:hover {
    color: var(--accent-text);
    border-bottom-color: var(--accent);
  }

  body[data-page="releases"] .foot-note {
    margin-top: 1.4rem;
    font-family: var(--serif);
    font-weight: 300;
    font-style: italic;
    font-size: 14.5px;
    line-height: 1.55;
    color: var(--fg3);
    max-width: 54ch;
    font-feature-settings: "onum" 1;
  }
  body[data-page="releases"] .foot-note a {
    color: var(--accent-text);
    text-decoration: none;
    border-bottom: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
  }
  body[data-page="releases"] .foot-note a:hover {
    border-bottom-color: var(--accent);
  }

  /* responsive · single-column groups, action block under stack */
  @media (max-width: 680px) {
    body[data-page="releases"] .record-groups {
      grid-template-columns: 1fr;
    }
    body[data-page="releases"] .earlier-row {
      grid-template-columns: 4.6rem 1fr;
      gap: 0.6rem 1.2rem;
    }
    body[data-page="releases"] .earlier-row .actions {
      grid-column: 1 / -1;
      padding-top: 0.5rem;
      border-top: 1px solid var(--rule-faint);
      margin-top: 0.3rem;
      align-items: flex-start;
    }
  }

  /* print falls back to .print-utility-sheet — hide the screen card */
  @media print {
    body[data-page="releases"] .record,
    body[data-page="releases"] .earlier-section { display: none; }
  }
}

/* ─── /local/ · local device console ──── authored device dossier.
   reuses the publication-grade .record shell from /integrity/releases/
   (border + radius + faint shadow + header hierarchy) and layers a
   slightly quieter ink on top. three editorial zones — publication
   state, local preferences, actions — plus a compact validators card
   and a closing disclaimer. mono single-line status strip carries the
   page's at-a-glance summary just below the hero. dossier register:
   serif page titles, mono metadata, söhne body. oxblood appears only
   on confirm-armed destructive buttons. */
@layer pages {
  /* language-gate exception for the global construction rail. on
     every other page the rail is position:static at end of body
     (so it appears only when the reader scrolls past the footer);
     on the gate the body has no normal-flow content (gate-background
     and modal scrim are both position:fixed inset:0), so the rail
     must be pinned to viewport bottom there. lives in the pages
     layer per the css-architecture rule for body[data-page=…]
     selectors. */
  body[data-page="language-gate"] .construction-rail {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 50;
  }

  body[data-page="local"] .local-console-page {
    /* match the standard page container: full --page-max width + the .site
       gutter so the hero left-aligns with the nav/logo, and the standard
       .page top offset so /local/ starts where every other page starts. */
    max-width: var(--page-max);
    margin-inline: auto;
    padding: clamp(80px, 12vh, 140px) clamp(24px, 5vw, 80px) clamp(60px, 8vh, 100px);
  }

  /* ── hero ── matches the /integrity/releases/ register: mono kicker
     in --fg3 (neutral, never oxblood), serif title, serif lede. */
  body[data-page="local"] .local-hero {
    max-width: 64ch;
    margin-bottom: clamp(28px, 4vw, 44px);
    /* reset the inherited full-screen .hero geometry so this console hero
       begins under the masthead like the other technical pages. */
    min-height: auto;
    justify-content: flex-start;
    padding-top: 0;
  }
  body[data-page="local"] .local-hero .hero-kicker {
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0 0 0.9rem;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-hero .hero-title {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(34px, 5.6vw, 56px);
    line-height: 1.0;
    letter-spacing: -0.02em;
    color: var(--fg);
    margin: 0 0 1.2rem;
  }
  body[data-page="local"] .local-hero .hero-title .line { display: block; }
  body[data-page="local"] .local-hero .hero-lede {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(16px, 1.7vw, 19px);
    line-height: 1.55;
    color: var(--fg2);
    max-width: 54ch;
    margin: 0 0 0.7rem;
    font-feature-settings: "onum" 1;
    text-wrap: pretty;
  }
  body[data-page="local"] .local-hero__lede-quiet {
    color: var(--fg3);
    font-size: clamp(14.5px, 1.5vw, 17px);
  }

  /* ── status strip ─────────────────────────────────────────── */
  body[data-page="local"] .local-strip {
    margin: 0 0 clamp(28px, 4vw, 44px);
    padding: clamp(0.85rem, 1.6vw, 1.05rem) 0;
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
    display: flex;
    flex-wrap: wrap;
    gap: 0.45rem clamp(1.2rem, 2.8vw, 2.4rem);
    font-family: var(--mono);
    font-size: 11px;
    line-height: 1.4;
    color: var(--fg2);
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-strip__item {
    display: inline-flex;
    align-items: baseline;
    gap: 0.55em;
    margin: 0;
  }
  body[data-page="local"] .local-strip__key {
    color: var(--fg3);
    text-transform: uppercase;
    letter-spacing: 0.14em;
    font-size: 0.85em;
    margin: 0;
  }
  body[data-page="local"] .local-strip__value {
    color: var(--fg);
    letter-spacing: 0.02em;
    margin: 0;
    overflow-wrap: anywhere;
  }

  /* ── record card · publication state / preferences / actions / validators.
     padding + margin match /integrity/releases/ so the cards read as
     the same furniture across the publication. */
  body[data-page="local"] .local-record {
    background: var(--paper-raised-high);
    border: 1px solid var(--rule);
    border-radius: var(--radius-soft);
    padding: clamp(28px, 3.6vw, 40px);
    margin-bottom: clamp(48px, 6vw, 72px);
    box-shadow: 0 14px 36px rgb(0 0 0 / 0.04);
  }
  body[data-page="local"] .local-record .record-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 1rem;
    padding-bottom: 1.2rem;
    margin-bottom: 1.4rem;
    border-bottom: 1px solid var(--rule);
  }
  body[data-page="local"] .local-record .record-header .kicker {
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0 0 0.4rem;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-record .record-header .date {
    font-family: var(--serif);
    font-weight: 300;
    font-size: clamp(24px, 3vw, 32px);
    line-height: 1.06;
    letter-spacing: -0.012em;
    color: var(--fg);
    margin: 0;
    font-feature-settings: "onum" 1;
  }
  body[data-page="local"] .local-record .record-header .title {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: clamp(15.5px, 1.6vw, 18px);
    line-height: 1.45;
    color: var(--fg2);
    margin: 0.5rem 0 0;
    max-width: 48ch;
    font-feature-settings: "onum" 1;
  }
  body[data-page="local"] .local-record .record-header .right .tag {
    display: inline-block;
    color: var(--fg);
    border: 1px solid var(--accent);
    padding: 3px 8px;
    border-radius: 2px;
    background: color-mix(in srgb, var(--accent) 7%, transparent);
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
  }

  /* ── disclosures (details/summary) ────────────────────────── */
  body[data-page="local"] .local-record__sections {
    display: grid;
    gap: 0.25rem;
  }
  body[data-page="local"] .local-disclosure {
    border-top: 1px solid var(--rule-faint);
    padding: 0.2rem 0;
  }
  body[data-page="local"] .local-disclosure:first-child {
    border-top: 0;
  }
  body[data-page="local"] .local-disclosure__summary {
    cursor: pointer;
    list-style: none;
    padding: 0.85rem 0;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    font-family: var(--serif);
    font-size: clamp(17px, 1.6vw, 19px);
    line-height: 1.3;
    color: var(--fg);
    font-feature-settings: "onum" 1;
    transition: color 160ms var(--rail-ease, cubic-bezier(0.22, 1, 0.36, 1));
  }
  body[data-page="local"] .local-disclosure__summary::-webkit-details-marker { display: none; }
  body[data-page="local"] .local-disclosure__summary::before {
    content: "▸";
    display: inline-block;
    margin-right: 0.55em;
    color: var(--fg3);
    opacity: 0.7;
    font-size: 0.78em;
    transition: transform 200ms var(--rail-ease, cubic-bezier(0.22, 1, 0.36, 1));
  }
  body[data-page="local"] .local-disclosure[open] > .local-disclosure__summary::before {
    transform: rotate(90deg);
  }
  body[data-page="local"] .local-disclosure__summary:hover { color: var(--accent-text); }
  body[data-page="local"] .local-disclosure__label {
    font-family: var(--serif);
    font-weight: 400;
    flex: 1 1 auto;
  }
  body[data-page="local"] .local-disclosure__hint {
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: 0.04em;
    color: var(--fg3);
    text-transform: none;
    flex: 0 1 auto;
    overflow-wrap: anywhere;
    text-align: right;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-disclosure__body {
    padding: 0.5rem 0 1rem 1.4rem;
    border-left: 1px solid var(--rule-faint);
    margin-left: 0.5rem;
  }
  body[data-page="local"] .local-disclosure__body .record-grid {
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0;
  }
  body[data-page="local"] .local-disclosure__body .record-grid__row {
    display: grid;
    grid-template-columns: minmax(8rem, 0.32fr) 1fr;
    gap: 1rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--rule-faint);
    align-items: baseline;
  }
  body[data-page="local"] .local-disclosure__body .record-grid__row:last-child { border-bottom: 0; }
  body[data-page="local"] .local-disclosure__body .record-grid__row dt {
    font-family: var(--sans);
    font-size: 0.78rem;
    color: var(--fg3);
    margin: 0;
    letter-spacing: 0.005em;
  }
  body[data-page="local"] .local-disclosure__body .record-grid__row dd {
    font-family: var(--mono);
    font-size: 0.86rem;
    color: var(--fg);
    margin: 0;
    line-height: 1.4;
    overflow-wrap: anywhere;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-disclosure__body .record-grid__row dd a {
    color: var(--fg2);
    text-decoration: none;
    border-bottom: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
  }
  body[data-page="local"] .local-disclosure__body .record-grid__row dd a:hover {
    color: var(--accent-text);
    border-bottom-color: var(--accent);
  }
  body[data-page="local"] .local-disclosure__actions {
    margin-top: 0.9rem;
    display: flex;
    gap: 0.7rem;
    flex-wrap: wrap;
  }
  body[data-page="local"] .local-disclosure--nested > .local-disclosure__summary {
    font-size: clamp(15px, 1.4vw, 16.5px);
    padding-block: 0.6rem;
  }
  body[data-page="local"] .local-disclosure--nested {
    margin-top: 0.6rem;
    border-top: 1px dotted var(--rule-faint);
  }
  body[data-page="local"] .local-disclosure--raw {
    margin-top: 0.4rem;
    border-top: 1px dotted var(--rule-faint);
  }

  /* small action button (mono, hairline, used inside disclosures) */
  body[data-page="local"] .local-action {
    /* cohesion sprint · controls demoted from bordered uppercase pills to
       quiet underlined text-actions, matching the fingerprint copy-action
       grammar. this page is a reading of local state, not a settings panel —
       interaction stays available but understated; understanding leads. */
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: 0.03em;
    color: var(--fg2);
    background: transparent;
    border: 0;
    border-bottom: 1px solid color-mix(in srgb, var(--bd) 50%, transparent);
    border-radius: 0;
    padding: 0.12rem 0;
    cursor: pointer;
    transition: color 160ms ease, border-color 160ms ease;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-action:hover,
  body[data-page="local"] .local-action:focus-visible {
    color: var(--accent-text);
    border-bottom-color: var(--accent-border);
    outline: none;
  }
  body[data-page="local"] .local-action.is-acknowledging {
    color: var(--accent-text);
    border-color: var(--accent);
  }

  /* ── preferences card · four rows + raw view ──────────────── */
  body[data-page="local"] .local-prefs { display: grid; gap: 0.4rem; }
  body[data-page="local"] .local-pref {
    position: relative;
    display: grid;
    grid-template-columns: minmax(8rem, 0.24fr) 1fr auto;
    grid-template-areas:
      "label value tag"
      "controls controls controls";
    gap: 0.35rem 1.2rem;
    padding: 1.0rem 0;
    border-bottom: 1px solid var(--rule-faint);
    align-items: baseline;
  }
  body[data-page="local"] .local-pref:last-of-type { border-bottom: 0; }
  body[data-page="local"] .local-pref__label {
    grid-area: label;
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0;
  }
  body[data-page="local"] .local-pref__value {
    grid-area: value;
    font-family: var(--serif);
    font-size: clamp(16px, 1.5vw, 18px);
    color: var(--fg);
    margin: 0;
    font-feature-settings: "onum" 1;
    overflow-wrap: anywhere;
  }
  body[data-page="local"] .local-pref__tag {
    grid-area: tag;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0;
    white-space: nowrap;
  }
  body[data-page="local"] .local-pref__controls {
    grid-area: controls;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.35rem;
  }
  body[data-page="local"] .local-pref__btn {
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: 0.06em;
    color: var(--fg2);
    background: transparent;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.4rem 0.75rem;
    cursor: pointer;
    transition: color 160ms ease, border-color 160ms ease, background 160ms ease;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-pref__btn:hover,
  body[data-page="local"] .local-pref__btn:focus-visible {
    color: var(--accent-text);
    border-color: var(--accent);
    outline: none;
  }
  body[data-page="local"] .local-pref__btn.is-current {
    color: var(--fg);
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 6%, transparent);
  }
  body[data-page="local"] .local-pref__btn--clear { color: var(--fg3); }
  body[data-page="local"] .local-pref--readonly .local-pref__tag { color: var(--fg3); opacity: 0.8; }
  body[data-page="local"] .local-prefs-saved {
    position: absolute;
    right: 0;
    bottom: -0.2rem;
    font-family: var(--mono);
    font-size: 9.5px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--accent-text);
    transition: opacity 200ms ease;
  }

  /* raw storage block · monospace JSON dump */
  body[data-page="local"] .local-raw {
    margin: 0.6rem 0 0;
    padding: 0.85rem 1rem;
    background: color-mix(in srgb, var(--paper-record) 60%, transparent);
    border: 1px solid var(--rule-faint);
    border-radius: 4px;
    font-family: var(--mono);
    font-size: 11px;
    line-height: 1.5;
    color: var(--fg2);
    overflow-x: auto;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
    max-height: 24rem;
    font-variant-numeric: tabular-nums lining-nums;
  }

  /* ── actions card · destructive buttons (oxblood on confirm) ── */
  body[data-page="local"] .local-record--actions { background: var(--paper-record); }
  body[data-page="local"] .local-actions { display: grid; gap: 0.3rem; }
  body[data-page="local"] .local-action-row {
    display: grid;
    grid-template-columns: minmax(13rem, 0.4fr) 1fr;
    gap: 1rem 1.4rem;
    align-items: baseline;
    padding: 1.05rem 0;
    border-bottom: 1px solid var(--rule-faint);
  }
  body[data-page="local"] .local-action-row:last-of-type { border-bottom: 0; }
  body[data-page="local"] .local-action-row--final { margin-top: 0.4rem; }
  body[data-page="local"] .local-action-row__btn {
    font-family: var(--mono);
    font-size: 12px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--fg2);
    background: transparent;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 0.7rem 1rem;
    cursor: pointer;
    text-align: left;
    transition: color 180ms ease, border-color 180ms ease, background 180ms ease;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-action-row__btn:hover,
  body[data-page="local"] .local-action-row__btn:focus-visible {
    color: var(--fg);
    border-color: var(--fg3);
    outline: none;
  }
  body[data-page="local"] .local-action-row__btn.is-armed {
    color: #fff;
    border-color: var(--accent);
    background: var(--accent);
    letter-spacing: 0.1em;
  }
  body[data-page="local"] .local-action-row__hint {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: clamp(14.5px, 1.4vw, 16px);
    line-height: 1.45;
    color: var(--fg2);
    margin: 0;
    font-feature-settings: "onum" 1;
  }

  /* ── validators card · grouped editorial links ────────────── */
  /* site validators · moved here from /local/ to the foot of /tests/.
     page-agnostic (the block only renders on /tests/ now). grouped
     editorial links; serif italic so they read as references, not ui. */
  .site-validators {
    display: grid;
    gap: 1.3rem;
  }
  .site-validators-group {
    display: grid;
    grid-template-columns: minmax(7rem, 0.22fr) 1fr;
    gap: 0.4rem 1.4rem;
    padding-block: 0.7rem;
    border-top: 1px solid var(--rule-faint);
    align-items: baseline;
  }
  .site-validators-group:first-child { border-top: 0; padding-top: 0; }
  .site-validators-group__label {
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fg3);
    margin: 0;
  }
  .site-validators-group__items {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.1rem;
  }
  .site-validators-group__items li { margin: 0; }
  .site-validators-group__items a {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 400;
    font-size: clamp(14.5px, 1.4vw, 16px);
    color: var(--fg2);
    text-decoration: none;
    border-bottom: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
    padding-bottom: 1px;
    line-height: 1.3;
    font-feature-settings: "onum" 1;
    transition: color 160ms ease, border-color 160ms ease;
  }
  .site-validators-group__items a:hover {
    color: var(--accent-text);
    border-bottom-color: var(--accent);
  }

  /* ── nested cache details (generated by js) ───────────────── */
  body[data-page="local"] .local-cache {
    margin-top: 0.6rem;
    border-top: 1px dotted var(--rule-faint);
    padding-top: 0.5rem;
  }
  body[data-page="local"] .local-cache:first-of-type { border-top: 0; }
  body[data-page="local"] .local-cache__summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    padding-block: 0.5rem;
    font-family: var(--mono);
    font-size: 11px;
    line-height: 1.45;
    color: var(--fg);
  }
  body[data-page="local"] .local-cache__summary::-webkit-details-marker { display: none; }
  body[data-page="local"] .local-cache__name {
    color: var(--fg);
    word-break: break-all;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-cache__meta {
    color: var(--fg3);
    font-size: 10px;
    letter-spacing: 0.06em;
  }
  body[data-page="local"] .local-cache__body { padding: 0.5rem 0 0.4rem; }
  body[data-page="local"] .local-cache__entries {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.4rem;
  }
  body[data-page="local"] .local-cache__entry {
    padding: 0.3rem 0;
    border-bottom: 1px dotted var(--rule-faint);
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    font-family: var(--mono);
    font-size: 10.5px;
    color: var(--fg2);
  }
  body[data-page="local"] .local-cache__url {
    color: var(--fg);
    word-break: break-all;
    font-variant-numeric: tabular-nums lining-nums;
  }
  body[data-page="local"] .local-cache__fields { color: var(--fg3); font-size: 9.5px; }

  /* ── empty + disclaimer ───────────────────────────────────── */
  body[data-page="local"] .local-empty {
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: 15px;
    color: var(--fg3);
    margin: 0;
    font-feature-settings: "onum" 1;
  }
  body[data-page="local"] .local-disclaimer {
    margin: clamp(2.4rem, 4.5vw, 3.5rem) 0 0;
    font-family: var(--serif);
    font-style: italic;
    font-weight: 300;
    font-size: clamp(14.5px, 1.5vw, 16.5px);
    line-height: 1.55;
    color: var(--fg3);
    text-align: center;
    max-width: 48ch;
    margin-inline: auto;
    font-feature-settings: "onum" 1;
  }

  /* ── mobile ───────────────────────────────────────────────── */
  @media (max-width: 720px) {
    body[data-page="local"] .local-pref {
      grid-template-columns: 1fr auto;
      grid-template-areas:
        "label tag"
        "value value"
        "controls controls";
    }
    body[data-page="local"] .local-action-row {
      grid-template-columns: 1fr;
    }
    .site-validators-group {
      grid-template-columns: 1fr;
    }
    body[data-page="local"] .local-disclosure__body {
      padding-left: 0.9rem;
    }
    body[data-page="local"] .local-disclosure__summary {
      flex-wrap: wrap;
    }
    body[data-page="local"] .local-disclosure__hint {
      text-align: left;
      flex-basis: 100%;
      padding-left: 1.1em;
    }
  }

  /* ── reduced motion ───────────────────────────────────────── */
  @media (prefers-reduced-motion: reduce) {
    body[data-page="local"] .local-disclosure__summary,
    body[data-page="local"] .local-disclosure__summary::before,
    body[data-page="local"] .local-action,
    body[data-page="local"] .local-pref__btn,
    body[data-page="local"] .local-action-row__btn,
    .site-validators-group__items a,
    body[data-page="local"] .local-prefs-saved {
      transition: none;
    }
  }

  /* ── print falls back to the existing .print-utility-sheet ─── */
  @media print {
    body[data-page="local"] .local-strip,
    body[data-page="local"] .local-record,
    body[data-page="local"] .local-disclaimer { display: none; }
  }
}

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

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

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

  /* first-visit vs returning-visitor vs welcomed-back copy — three
     branches in .shell-lede, gated by html[data-returning] and
     html[data-welcome] set pre-paint by the inline boot script in
     templates/pages/language-gate.html.
       data-when="first"   · first-ever visit; no stored edition.
                              explains the relationship between editions.
       data-when="return"  · returning visit, but bucket is null
                              (same day) or the per-day sentinel has
                              already fired this session. quiet single
                              line ("last read in english.").
       data-when="welcome" · returning after >= 1 day AND not yet shown
                              this session. ephemeral welcome line that
                              fades in/out once. replaces the lede.
     no JavaScript = always first-visit. */
  [data-when="return"],
  [data-when="welcome"] { display: none; }
  html[data-returning="true"] [data-when="first"]  { display: none; }
  html[data-returning="true"] [data-when="return"] { display: inline; }
  html[data-welcome="true"] [data-when="first"]    { display: none; }
  html[data-welcome="true"] [data-when="return"]   { display: none; }
  html[data-welcome="true"] [data-when="welcome"]  { display: block; }

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

  /* background · the sliced homepage masthead + hero. no transform,
     no scale, no extra padding — the homepage's own .hero geometry
     (min-height:100svh; justify-content:flex-end) bottom-anchors it
     exactly as on /en/. inert, no pointer events — a surface.
     gate v2 · NO blur. the brief bans blur from the gate; the gate
     reads as a same-page overlay being lifted off the hero, not a
     glass plate covering it. the scrim alone (--overlay-scrim, 18%
     warm-brown) carries the modal-state signal; the publication
     beneath stays crisp so the hero is recognisably already-rendered
     when the gate dissolves. */
  .gate-background {
    position: fixed;
    inset: 0;
    z-index: 1;
    overflow: hidden;
    pointer-events: none;
    background: var(--bg);
  }
  /* historically the gate-background pinned the hero at rest because
     no behaviour script was driving the reveal here. now that
     fonts.js + reveal.js run on the gate page too, the hero animates
     naturally behind the scrim — same revealUp keyframes as
     /en-au/. when the visitor then dismisses the gate, the
     destination skips its own animation (the hero already played
     behind the scrim), governed by sessionStorage.tp-skip-hero-anim
     handed off by language-gate.js. */

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

  /* legacy gate-* and per-modal compound CSS were replaced by the
     shared .shell + .modal-shell-scrim family in @layer components
     (phase 92 · modal-system overhaul). the gate now uses the
     shell shape end-to-end; the only gate-specific rules that
     survive are .gate-background (the inert homepage backdrop)
     above and the gate-v2 motion rules below. */

  /* gate v2 · dismissal exit choreography.
     the scrim listens for [data-gate-exiting] and fades to transparent
     while the shell card lifts upward (translateY(-12px)) and dissolves.
     400ms cubic-bezier(0.22,1,0.36,1) — the decelerating editorial curve
     used by the whole modal family. the scrim's own opacity transition
     at .modal-shell-scrim is 220ms ease (.is-active toggle); here we
     also transition background-color so the scrim's warm-brown drains
     in lockstep with the shell. */
  .modal-shell-scrim[data-gate-exiting] {
    background-color: rgba(0, 0, 0, 0);
    transition: background-color var(--modal-motion),
                opacity var(--modal-motion);
    opacity: 0;
    pointer-events: none;
  }
  .modal-shell-scrim[data-gate-exiting] .shell {
    opacity: 0;
    transform: translateY(-12px) scale(1);
    transition: opacity var(--modal-motion),
                transform var(--modal-motion);
  }

  /* gate v2 · 80ms tap-acknowledgement.
     when the visitor taps a .lang-cell choice, the JS adds [data-tap-ack]
     for 80ms before adding [data-gate-exiting] on the scrim. the brief
     space lets the chosen row hold its tap-state background — the
     publication-side hint that the gate registered the choice before the
     dismissal motion begins. */
  .lang-cell[data-tap-ack] {
    background: color-mix(in srgb, var(--ink) 3%, transparent);
    transition: background-color 80ms linear;
  }

  /* gate v2 · 150ms hover/focus on gate interactive elements.
     keeps colour changes registering quickly without performing. scoped
     to the gate's interactive elements; site-wide hover transitions are
     not in this brief's scope. */
  .modal-shell-scrim .lang-cell,
  .modal-shell-scrim .shell-close {
    transition: color var(--hover-motion),
                background-color var(--hover-motion),
                border-color var(--hover-motion);
  }
}

/* ──────────────────────────────────────────────────────────────────
   view transitions API (gate v2)
   document-root pseudo-elements that govern the cross-fade between
   /, /en-au/ and /fr/ when language-gate.js + footer-switcher.js
   wrap a swap in document.startViewTransition(). 400ms with the
   same decelerating curve as --modal-motion so the cross-fade reads
   as part of the modal-system motion vocabulary, not a separate
   browser flourish. invisible when the swap content is identical
   (e.g. english-default → /en-au/); visible only when content
   genuinely differs (cross-edition swaps). browsers without the
   view transitions API fall back to the JS-side full-navigation
   path — these rules are inert there. wrapped in @layer overrides
   because pseudo-elements at the document root don't fit any
   narrower component scope.
   ────────────────────────────────────────────────────────────────── */
@layer overrides {

  /* view transitions API (gate v2) — document-root pseudo-elements.
     reduced-motion clamped at the bottom of this @layer block. */
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 400ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  }

  /* ────────────────────────────────────────────────────────────────
     editorial micro-interactions (close-button refresh sibling set)
     ten small attentive details. each brief enough to be felt, not
     noticed. all bfcache-safe and reduced-motion-aware via
     micro-interactions.js (which short-circuits the JS-driven
     behaviours under prefers-reduced-motion; the css below only
     needs one explicit @media override at the bottom, for the
     underline-draw final state).
     ──────────────────────────────────────────────────────────────── */

  /* #2 · section rules · hairlines fade from 0.12 → 0.20 as they
     scroll into view. once-per-page, no replay. */
  .tp-rule {
    opacity: 0.12;
    transition: opacity var(--tp-duration-typewriter) var(--tp-ease);
  }
  .tp-rule.is-settled { opacity: 0.20; }

  /* #6 · numbered section headers reveal number first, label 120ms
     later. above-the-fold sections also stagger on initial load via
     the same observer with a post-paint delay set in JS. the markup
     already splits the numeral (.label-num) from the label text
     (.section-label__text); both start invisible + translateY(2px)
     and settle on .is-revealed. */
  .section-label .label-num,
  .section-label .section-label__text {
    display: inline-block;
    opacity: 0;
    transform: translateY(2px);
    transition: opacity var(--tp-duration-settle) var(--tp-ease),
                transform var(--tp-duration-settle) var(--tp-ease);
  }
  .section-label.is-revealed .label-num {
    opacity: 1;
    transform: translateY(0);
  }
  .section-label.is-revealed .section-label__text {
    opacity: 1;
    transform: translateY(0);
    transition-delay: 120ms;
  }

  /* #8 · reading progress hairline. injected by JS as the first
     child of <body> on long-form pages. 1px oxblood line that
     advances with scroll. shows/hides over 400ms; hidden until the
     reader has scrolled past the first viewport.

     phase 96 · the per-frame `bar.style.width = pct + '%'` mutation
     was a csp style-src-attr violation. the bar now advances via
     css scroll-driven animation (`animation-timeline: scroll(root)`),
     so the js only injects the element and toggles .is-visible —
     zero inline-style writes. browsers without animation-timeline
     support (older safari / firefox) silently render no bar; we'd
     rather lose a quiet ux nicety on those edge cases than ship
     csp violations site-wide. */
  .tp-progress {
    position: fixed;
    top: 0; left: 0;
    block-size: 1px;
    inline-size: 100%;
    transform: scaleX(0);
    transform-origin: left;
    background: var(--tp-oxblood);
    opacity: 0;
    z-index: 100;
    transition: opacity var(--tp-duration-macro) var(--tp-ease);
    pointer-events: none;
  }
  .tp-progress.is-visible { opacity: 0.6; }
  @keyframes tp-progress-scroll {
    to { transform: scaleX(1); }
  }
  @supports (animation-timeline: scroll()) {
    .tp-progress {
      animation: tp-progress-scroll linear;
      animation-timeline: scroll(root);
    }
  }

  /* phase 96 · CSP-clean utilities ─────────────────────────────
     each of these replaces a runtime style.* mutation that used
     to trigger style-src-attr 'none' violations. */

  /* hidden textarea for execCommand copy fallback (copy.template.js,
     verify-modal.template.js). offscreen, invisible, untouchable. */
  .tp-copy-fallback {
    position: fixed;
    top: -1000px;
    left: 0;
    inline-size: 1px;
    block-size: 1px;
    opacity: 0;
    pointer-events: none;
  }

  /* body scroll-lock when a modal is open (overlay.template.js).
     class is already added/removed in pair with overlay open/close;
     we only need the style declaration. */
  body.modal-open { overflow: hidden; }

  /* reveal stagger via data-reveal-index instead of per-element
     style="transition-delay: n-ms" (reveal.template.js). sixteen
     rows cover any realistic stagger sequence (largest current
     group is six principles on the homepage). */
  [data-reveal-index="0"]  { transition-delay:    0ms; }
  [data-reveal-index="1"]  { transition-delay:   80ms; }
  [data-reveal-index="2"]  { transition-delay:  160ms; }
  [data-reveal-index="3"]  { transition-delay:  240ms; }
  [data-reveal-index="4"]  { transition-delay:  320ms; }
  [data-reveal-index="5"]  { transition-delay:  400ms; }
  [data-reveal-index="6"]  { transition-delay:  480ms; }
  [data-reveal-index="7"]  { transition-delay:  560ms; }
  [data-reveal-index="8"]  { transition-delay:  640ms; }
  [data-reveal-index="9"]  { transition-delay:  720ms; }
  [data-reveal-index="10"] { transition-delay:  800ms; }
  [data-reveal-index="11"] { transition-delay:  880ms; }
  [data-reveal-index="12"] { transition-delay:  960ms; }
  [data-reveal-index="13"] { transition-delay: 1040ms; }
  [data-reveal-index="14"] { transition-delay: 1120ms; }
  [data-reveal-index="15"] { transition-delay: 1200ms; }

  /* #9 · mono-uppercase label letter-spacing breath. widens by 0.5px
     when the containing card or row is hovered. site has several
     existing label classes; use :where() to target them collectively
     without bumping specificity. each class keeps its own base
     tracking (calc() over current letter-spacing). */
  :where(.page-kicker, .eyebrow, .verify-kicker, .code-label,
         .source-reader-kicker, .record-kicker, .label-num) {
    transition: letter-spacing 300ms var(--tp-ease);
  }
  .trajectory-card:hover :where(.page-kicker, .eyebrow, .record-kicker, .label-num),
  .trajectory-card:focus-within :where(.page-kicker, .eyebrow, .record-kicker, .label-num),
  .project-card:hover :where(.page-kicker, .eyebrow, .record-kicker, .label-num),
  .project-card:focus-within :where(.page-kicker, .eyebrow, .record-kicker, .label-num),
  .menu .row:hover :where(.eyebrow, .code-label),
  .menu .row:focus-within :where(.eyebrow, .code-label) {
    letter-spacing: calc(0.10em + 0.5px);
  }

  /* #10 · citation italic flash. copy.template.js dispatches
     'tp:citation-copied' after its in-place "cited · edition …"
     label fires; micro-interactions.js toggles .is-acknowledging
     on .tp-citation for 200ms. the citation source text flips
     italic → roman → italic. NO floating pill (per project rule
     feedback_no_floating_feedback_trentpower.md). */
  .tp-citation {
    font-style: italic;
    transition: font-style 200ms var(--tp-ease);
  }
  .tp-citation.is-acknowledging {
    font-style: normal;
  }

  /* #11 · external-link arrow peel. ↗ glyphs translate +2/-2 and
     rotate 8° on hover/focus of the parent link. extends existing
     .arr (verify-modal injected arrows) and .arrow (project / gate
     forward arrows — those are internal but already had a partial
     peel rule, so unifying here keeps both arrows consistent).
     adds .tp-external-arrow for the bare ↗ on the LinkedIn link. */
  .menu .row .verb .arr,
  .arrow,
  .tp-external-arrow {
    display: inline-block;
    transform-origin: 50% 50%;
    transition: transform var(--tp-duration-hover) var(--tp-ease),
                color var(--tp-duration-hover) var(--tp-ease);
  }
  .menu .row:hover .verb .arr,
  .menu .row:focus-visible .verb .arr,
  :where(a):hover > .arrow,
  :where(a):focus-visible > .arrow,
  :where(a):hover > .tp-external-arrow,
  :where(a):focus-visible > .tp-external-arrow {
    transform: translate(2px, -2px) rotate(8deg);
    color: var(--accent-text);
  }

  /* reduced motion · the global `*, *::before, *::after {
     animation-duration: 0.01ms !important; transition-duration:
     0.01ms !important; }` rule at the top of the file kills every
     transition and animation on its own. for the JS-driven
     behaviours (.tp-rule observer, .section-label reveal,
     .tp-tick counter, .tp-progress bar, .tp-citation flash,
     footer typewriter) micro-interactions.js checks matchMedia
     and applies final state without animation OR skips entirely.
     the one rule below is the only css-side reduced-motion fix:
     keep the link underline visible at rest, since the underline
     IS the link affordance and a zero-width gradient with no
     animation would render unlinked. */
  @media (prefers-reduced-motion: reduce) {
    .page-body a { background-size: 100% 1px; }
    ::view-transition-old(root),
    ::view-transition-new(root) {
      animation-duration: 1ms;
    }
  }
}

@layer pages {

/* ─── /tests/ · signed publication test record ──────────────────
   native to the site: the shell, sections, links and record cards
   all come from the shared page-kicker / page-title / page-lede /
   security-section / integrity-record-* classes. only the genuinely
   new elements live here - status chips, scorecard panels, the
   per-target table, severity tags and the verify drawer - all on the
   site tokens so light and dark follow the document theme. the single
   review accent is a restrained gold; pass and fail reuse site ink and
   oxblood. no dashboard green or red. body text sits on --fg / --fg2,
   never the faint --fg3 (which is reserved for labels and kickers). */
.tests-page {
  --tests-review: #956A12;
  --tests-review-soft: color-mix(in srgb, var(--tests-review) 10%, transparent);
  --tests-review-border: color-mix(in srgb, var(--tests-review) 34%, transparent);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tests-page { --tests-review: #C9A24B; }
}
:root[data-theme="dark"] .tests-page { --tests-review: #C9A24B; }

/* confidence tally beneath the lede */
.tests-page .tests-tally {
  max-width: none; margin: 0 0 1.6em;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .4rem .6rem;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 400;
  letter-spacing: 0.12em; text-transform: uppercase;
}
.tests-page .tests-tally__item { display: inline-flex; align-items: center; gap: .45em; color: var(--fg2); }
.tests-page .tests-tally__item::before { content: ""; width: .5em; height: .5em; flex: 0 0 auto; }
.tests-page .tests-tally__item--pass::before { border: 1.5px solid var(--fg3); border-radius: 50%; }
.tests-page .tests-tally__item--review { color: var(--tests-review); }
.tests-page .tests-tally__item--review::before { background: var(--tests-review); border-radius: 50%; }
.tests-page .tests-tally__item--fail::before { border: 1.5px solid var(--fg3); border-radius: 1px; }
.tests-page .tests-tally__sep { color: var(--fg3); opacity: .5; }

/* status chips - shape + colour + text, never colour alone */
.tests-page .tests-chip {
  display: inline-flex; align-items: center; gap: .42em;
  font-family: var(--mono); font-size: 0.62rem; font-weight: 400;
  letter-spacing: 0.14em; text-transform: uppercase;
  padding: .16rem .5rem .16rem .42rem; border: 1px solid; border-radius: 2px;
  white-space: nowrap; line-height: 1;
}
.tests-page .tests-chip::before { content: ""; width: .5em; height: .5em; flex: 0 0 auto; }
.tests-page .tests-chip--pass { color: var(--fg2); border-color: var(--bd); }
.tests-page .tests-chip--pass::before { border: 1.5px solid var(--fg3); border-radius: 50%; }
.tests-page .tests-chip--review { color: var(--tests-review); border-color: var(--tests-review-border); background: var(--tests-review-soft); }
.tests-page .tests-chip--review::before { background: var(--tests-review); border-radius: 50%; }
.tests-page .tests-chip--fail { color: var(--accent-text); border-color: var(--accent-border); background: color-mix(in srgb, var(--accent) 8%, transparent); }
.tests-page .tests-chip--fail::before { background: var(--accent-text); }
.tests-page .tests-chip--na { color: var(--fg3); border-color: var(--rule-default); border-style: dotted; }

/* scorecards - small archival dossier panels (not a table, not widgets) */
.tests-page .tests-scorecards {
  display: grid; gap: clamp(.7rem, 1.6vw, 1rem);
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  margin-block-start: clamp(1rem, 2.4vw, 1.4rem);
}
.tests-page .tests-card {
  /* cohesion sprint · diagnostic card joins the 8px inline-card family
     (--tp-radius-inline-card) so test scorecards read as the same
     archival-sheet object as verify / integrity / project records. */
  background: var(--surface-archival); border: 1px solid var(--rule);
  border-radius: var(--tp-radius-inline-card); padding: clamp(1rem, 2.6vw, 1.25rem);
  display: flex; flex-direction: column; gap: .55rem; min-height: 104px;
}
.tests-page .tests-card__top { display: flex; align-items: flex-start; justify-content: space-between; gap: .6rem; }
.tests-page .tests-card__name { font-family: var(--sans); font-weight: 500; font-size: 0.86rem; color: var(--fg); }
.tests-page .tests-card__detail { margin: auto 0 0; font-family: var(--sans); font-size: 0.8rem; line-height: 1.45; color: var(--fg2); }

/* per-target table - quiet, mono, hairline seams */
.tests-page .tests-perf {
  border-collapse: collapse; width: 100%; max-width: 30rem;
  font-family: var(--mono); margin-block-start: clamp(1rem, 2.4vw, 1.4rem);
}
.tests-page .tests-perf th,
.tests-page .tests-perf td { text-align: left; padding: .6rem .7rem; border-bottom: 1px solid var(--rule); }
.tests-page .tests-perf thead th { font-size: 0.7rem; font-weight: 400; letter-spacing: .04em; color: var(--fg2); border-bottom: 1px solid var(--rule-strong); text-transform: none; }
.tests-page .tests-perf tbody th { font-family: var(--mono); font-weight: 400; font-size: 0.82rem; color: var(--fg); white-space: nowrap; }
.tests-page .tests-perf tbody th code { font-family: var(--mono); color: var(--fg); }
.tests-page .tests-perf__score { text-align: right; font-variant-numeric: tabular-nums; color: var(--fg); }
.tests-page .tests-perf__status { text-align: right; white-space: nowrap; }
.tests-page .tests-threshold { margin: .9rem 0 0; max-width: none; font-family: var(--mono); font-size: 0.7rem; letter-spacing: .04em; color: var(--fg3); }

/* why-review - an editorial annotation, not a dashboard caption */
.tests-page .tests-why {
  margin: 1.1rem 0 0; max-width: 60ch; font-family: var(--sans);
  font-size: 0.9rem; line-height: 1.55; color: var(--fg2);
  padding-left: .9rem; border-left: 2px solid var(--tests-review-border);
}
.tests-page .tests-why__label { display: inline-block; margin-right: .5em; font-family: var(--mono); font-size: 0.62rem; font-weight: 400; letter-spacing: .12em; text-transform: uppercase; color: var(--tests-review); }

/* what needs attention - annotated notes */
.tests-page .tests-notes { list-style: none; margin: clamp(1rem, 2.4vw, 1.4rem) 0 0; padding: 0; display: grid; gap: 0; }
.tests-page .tests-note { display: flex; flex-wrap: wrap; align-items: baseline; gap: .55rem .9rem; padding: .85rem 0; border-top: 1px solid var(--rule); }
.tests-page .tests-note:first-child { border-top: 0; padding-top: 0; }
.tests-page .tests-sev { font-family: var(--mono); font-size: 0.62rem; font-weight: 400; letter-spacing: .1em; text-transform: uppercase; display: inline-flex; align-items: center; gap: .4em; white-space: nowrap; }
.tests-page .tests-sev::before { content: ""; width: .5em; height: .5em; border-radius: 50%; }
.tests-page .tests-sev--high { color: var(--accent-text); } .tests-page .tests-sev--high::before { background: var(--accent-text); }
.tests-page .tests-sev--medium { color: var(--tests-review); } .tests-page .tests-sev--medium::before { background: var(--tests-review); }
.tests-page .tests-sev--low { color: var(--fg3); } .tests-page .tests-sev--low::before { background: var(--fg3); }
.tests-page .tests-note__text { font-family: var(--sans); font-size: 0.9rem; line-height: 1.5; color: var(--fg); flex: 1; min-width: 14rem; }

/* verify command - collapsed by default; wraps instead of overflowing */
.tests-page .tests-verify { margin-block-start: clamp(1rem, 2.4vw, 1.4rem); }
.tests-page .tests-verify > summary {
  cursor: pointer; list-style: none; font-family: var(--mono);
  font-size: 0.68rem; font-weight: 400; letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent-text); display: inline-flex; align-items: center; gap: .55em;
}
.tests-page .tests-verify > summary::-webkit-details-marker { display: none; }
.tests-page .tests-verify > summary::before {
  content: ""; width: .45rem; height: .45rem; flex: 0 0 auto;
  border-right: 1.5px solid var(--accent-text); border-bottom: 1.5px solid var(--accent-text);
  transform: rotate(-45deg); transition: transform .2s; margin-top: -2px;
}
.tests-page .tests-verify[open] > summary::before { transform: rotate(45deg); }
.tests-page .tests-verify > p { max-width: 60ch; font-family: var(--sans); font-size: 0.82rem; line-height: 1.5; color: var(--fg2); margin: .8rem 0 .6rem; }
.tests-page .tests-cmd {
  font-family: var(--mono); font-size: 0.78rem; line-height: 1.7; color: var(--fg2);
  background: var(--paper-record); border: 1px solid var(--rule); border-radius: 2px;
  padding: .7rem .8rem; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
}
.tests-page .tests-cmd__op { color: var(--accent-text); }

/* downloads sentence + signature note */
.tests-page .tests-downloads { margin-block-start: clamp(1rem, 2.4vw, 1.4rem); }
.tests-page .tests-signed { max-width: none; font-family: var(--mono); font-size: 0.72rem; line-height: 1.7; color: var(--fg3); }
.tests-page .tests-signed strong { color: var(--fg2); font-weight: 500; }
.tests-page .tests-fpr { color: var(--fg3); letter-spacing: .03em; overflow-wrap: anywhere; word-break: break-word; }

/* print - ink on paper */
@media print {
  .tests-page .tests-card,
  .tests-page .integrity-record-card,
  .tests-page .tests-note { break-inside: avoid; }
  .tests-page .tests-verify { display: none; }
}

}  /* @layer pages · /tests/ */
