/*! 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 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) keeps `font-display: swap` because it is the single preloaded font and its arrival is brand-critical. the other two critical aliases use `optional`: if the subset is not delivered within 100 ms, the system fallback holds for that page load — acceptable for nav and small mono labels because the fallback stacks were chosen to mirror the brand metrics. */ @font-face { font-family: 'Signifier Critical'; font-style: normal; font-weight: 300; font-display: swap; src: url('/fonts/subsets/signifier-light-hero.woff2') format('woff2'); } @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
or . 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: #F7F3EA; /* card on record surface · paper sheet */ --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); --overlay-scrim: rgba(28, 25, 22, 0.46); /* 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); --rule-strong: rgba(42, 38, 33, 0.22); /* ink and accent — semantic names for the new system. */ --ink: #211F1C; --ink-muted: #67625B; --accent: #6E1A14; /* legacy aliases — older rules (verify-command-block, .modal, etc.) reference these names; new code should prefer the semantic --paper-* / --ink-* / --rule tokens above. */ --bg-public: var(--paper-main); --bg-archive: var(--paper-record); --bg: var(--surface-page); --bg2: var(--paper-record); --card: var(--paper-raised-high); --fg: var(--ink); /* phase 40 · slightly warmer body register (#5f5a53). same warm hue as phase 39 (#5e5952), 1 unit lighter — preserves the editorial warmth while keeping aaa-equivalent serif legibility on mobile. dark mode --fg2 unchanged (handled in dark block). */ --fg2: #5F5A53; --fg3: #706B66; --ac: var(--accent); --ac2: #8B2218; /* warmer oxblood · hover only */ /* accent split: --accent is decorative (rules, dividers, focus- ring fills, button backgrounds, ::after underline tints). in light mode --accent-text aliases --accent (oxblood reads on paper at aaa). in dark mode --accent-text is a brighter coral so accent-coloured text (link hover, code-string, trust bullets, project cta labels, etc.) preserves wcag aa contrast on dark paper, while --accent stays deep crimson for decorative use. see @media (prefers-color-scheme: dark) below. */ --accent-text: var(--accent); --accent-hover: var(--ac2); /* 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; /* 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. tokenised so dark mode and prefers-contrast can override them without touching the component rules. */ --code-string: #2563EB; --code-var: #7C3AED; /* 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 , 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; } /* 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); } body[data-surface="archive"] { --surface-page: var(--paper-record); --surface-card: var(--surface-archival); --bg: var(--paper-record); } /* Back-of-house pages calm the supporting-text and rule tokens by a small amount. primary tokens (--fg / --ink / --link) stay unchanged so headings, filenames, hashes, fingerprints and action links remain authoritative. the override fires on every page that wears the brand-only masthead — that includes privacy, verify, integrity, source, releases, security and the error pages. the homepage carries no data-masthead attribute and so keeps the original (slightly stronger) muted tones. Contrast: --fg2 #6c6760 vs --paper-record #e9e5dc ≈ 4.6:1; vs --paper-main #faf7f0 ≈ 5.4:1. --fg3 #756f69 vs the same backgrounds ≈ 4.0:1 and 4.7:1. both safely above the 3:1 large- text bar; --fg3 sits at the aa edge for very small mono labels on the warm-grey surface. */ body[data-masthead="brand-only"] { --fg2: #6C6760; --fg3: #756F69; /* phase 39 · brand-only masthead pages get an even quieter rule register (8% mix) so the brand-only chrome stays nearly subliminal against editorial type. */ --rule: color-mix(in srgb, var(--ink) 8%, transparent); } } /* ─── Accessibility-preference overlays ───────────────────────── a third @layer tokens block holds three media-query branches that redefine the canonical token set under user preferences. the surface variants (body[data-surface=…], body[data-masthead=…]) reference these tokens via var(); they automatically inherit the redefined values without their own overrides. - prefers-color-scheme: dark — warm editorial dark, aaa contrast - prefers-contrast: more — pure black/white, thicker rules - dark + contrast (intersect) — pure white on pure black - forced-colors: active — windows high contrast mode */ @layer tokens { @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; --overlay-scrim: rgba(0, 0, 0, 0.55); /* 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); --rule-strong: rgba(240, 234, 224, 0.30); /* cream ink — warm, editorial, aaa against #1b1916. */ --ink: #F0EAE0; /* ~14.6:1 vs --paper-main */ --ink-muted: #C9C2B7; /* ~9.6:1 vs --paper-main */ /* deep crimson — lacquered ink, oxblood leather, archival annotation. replaces the earlier bright-coral dark-mode accent (#e68675). used for decorative accent: focus-ring fills, hover ::after underlines, button backgrounds with text on top (aaa contrast white-on-crimson), divider tints, glyph fills. text use of accent (color: var(--accent-text)) migrates to var(--accent-text) below — at #9e2f2a on dark paper #1b1916 contrast is ~2.4:1, which fails wcag aa for text. the split-token architecture keeps the editorial crimson register on decoration and keeps every accent- coloured text use AA-compliant. */ --accent: #9E2F2A; /* ~2.4:1 vs --paper-main · decorative only */ --accent-text: #D86459; /* ~4.9:1 vs --paper-main · text only · aa */ --accent-hover: #B13A34; /* hover · brightens subtly, not glow */ --ac2: #B13A34; /* legacy alias, anchored on hover hue */ --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 brighter --accent-text hue (#d86459) so the ring composites to ≥3:1 against dark paper (wcag 1.4.11 non-text contrast). the deep crimson #9e2f2a is too dark for focus rings on dark paper — anchored at 2.4:1 raw, which fails 1.4.11 even with high alpha. */ --focus-colour: rgba(216, 100, 89, 0.85); /* Code-token colours — desaturated for editorial calm. */ --code-string: #99D5A1; /* mint, ~7.5:1 */ --code-var: #C8AEEA; /* warm violet, ~8.9:1 */ } /* selection contrast on dark paper: paper-on-deep-crimson (the light-mode `color: var(--bg)` default) drops to ~2.4:1 in dark mode. override to cream-on-deep-crimson (~6.2:1, aa). */ ::selection { color: var(--ink); } /* Brand-only masthead overrides on dark paper: calm fg2/fg3 slightly more so they sit gently above body text. */ body[data-masthead="brand-only"] { --fg2: #B8B0A4; --fg3: #968F86; /* phase 39 · brand-only masthead on dark paper softened to 10% ink mix (matches the brand-only register on light). */ --rule: color-mix(in srgb, var(--ink) 10%, transparent); } } @media (prefers-contrast: more) { :root { --ink: #000000; --ink-muted: #1A1A1A; --paper-main: #FFFFFF; --paper-record: #F4F2EC; --rule: rgba(0, 0, 0, 0.45); --rule-strong: rgba(0, 0, 0, 0.7); } 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; } } @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); --rule-strong: rgba(255, 255, 255, 0.8); } } /* 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: Canvas; --paper-raised-high: Canvas; --accent: LinkText; --rule: CanvasText; --rule-strong: CanvasText; } a { color: LinkText; } .cite-btn, .site-footer__language button, .verify-record-action { color: ButtonText; border-color: ButtonText; } :focus-visible { outline-color: Highlight; } } } @layer layout { /* Brand-only masthead. the full trent power + nav strip is the homepage identity. every other page (privacy, the entire record layer, error pages) shows only trent power. same height, hairline rule, mobile two-line stack — the brand stays exactly as on the homepage; the wayfinding list disappears. (Retired: the nav-toggle / nav-links system is gone site-wide; the masthead now stands alone in every header.) */ } @layer base { /* opentype defaults wherever signifier renders editorial prose: kerning, ligatures, contextual alternates, old-style figures. */ .hero-statement, .hero-body, .principle-title, .principle-body, .chapter-title, .chapter-detail, .project-desc, .project-subline, .preview-editorial, .page-title, .page-lede, .page-body p, .page-subtitle, .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, .technical-list, .code-path, .fingerprint, .section-label, .chapter-label, .page-meta, .preview-meta, .preview-title, .site-footer__colophon, .nav-mark, .contact-email { font-variant-numeric: tabular-nums lining-nums; } } @layer reset { /* reset & base */ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } ::selection { background: var(--ac); color: var(--bg); } html { font-size: 16px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } } } @layer tokens { /* anchor offset for the fixed nav. 52px nav height plus a small breathing room so section labels land cleanly just under the nav, not flush. */ :root { --nav-offset: 60px; /* Anchor-offset tokens for native anchor navigation on the home section ids. mobile nav-inner height varies with the device — ~58 px on non-notched displays and ~102 px on notched iphones (env(safe-area-inset-top) + padding + content). the mobile value is dynamic: `calc(env(safe-area-inset-top) + 4.5rem)` = 72 px non-notch / ~116 px notched, landing the section heading ~14 px below the nav at every viewport. desktop nav is `position: fixed` at 52 px — the 7 rem fixed value lands the heading with calm breathing room and doesn't need an env() lookup (desktop browsers don't expose a meaningful safe-area-inset-top). */ --anchor-offset-mobile: calc(env(safe-area-inset-top) + 4.5rem); --anchor-offset-desktop: 7rem; /* Safe-area-inset tokens — surface env() values as named tokens so edge-touching elements (sticky/fixed nav, footer, overlays) can refer to them via var() with a sane 0 fallback. `viewport-fit= cover` (set in ) 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 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. `` 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); } /* `` marks computed output — sha-256 hashes, pgp fingerprints, validation timestamps. mono register so it sits inside the technical-mono family with ``, 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;
}
/* `` 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;
}
/* 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, page-back) 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,
.trust-mark a:focus-visible,
.contact-secondary a:focus-visible,
.site-footer__actions a:focus-visible,
.page-back: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 tiny inline 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. data-url inside a stylesheet bypasses script-src. */
background-image:
radial-gradient(ellipse 120% 90% at 50% 50%, transparent 55%,
color-mix(in srgb, var(--ink) 4%, transparent) 100%),
url("data:image/svg+xml;utf8,");
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-header,
.print-footer,
.print-citation,
.print-meta,
.print-kicker,
.print-title,
.print-seal {
display: none;
}
}
@layer components {
/* /verify/ public verification route.
verify.js renders into #verify-root using only textcontent and
setattribute on safe attributes. Same-origin only. */
/* phase 56 · .verify-kicker rule retired (the html element was
removed in phase 55; the rule was an orphan). */
/* /verify/ scoped layout
verify.js renders real 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. */
.verify-page .page-body {
max-width: 880px;
margin: 0 auto;
/* top padding tightened from clamp(48,7vw,88) so the kicker sits
closer to the masthead , /verify/ is a utility-record page, not
a hero-driven landing. */
padding: clamp(40px, 6vw, 64px) 0;
}
@media (max-width: 700px) {
.verify-page .page-body {
max-width: none;
padding: 32px 0 48px;
}
/* card actions stack vertically on narrow viewports — flex column
keeps the hairline-underline rhythm of each link intact. */
.verify-page .verify-record-actions {
flex-direction: column;
gap: 0.5rem 0;
line-height: 1.4;
}
.verify-page .verify-record-action { align-self: flex-start; }
/* evidence line splits "validated YYYY-MM-DD" onto its own line so
the row reads as three calm lines (source / file·size / validated)
rather than wrapping mid-row. desktop keeps the inline " · " join. */
.verify-page .verify-rg-evidence-sep { display: none; }
.verify-page .verify-rg-evidence-validated { display: block; }
}
/* hero. compressed so the record card appears sooner. title uses
the utility-page scale (clamp 42 to 76px), the intro paragraph
sits close beneath, and the gap to the card is small. */
.verify-page .page-title {
max-width: 720px;
font-size: clamp(42px, 7vw, 76px);
line-height: 0.95;
margin-bottom: clamp(20px, 3vw, 28px);
}
.verify-page .page-lede {
max-width: 620px;
margin-bottom: 0;
}
/* ─────────────────────────────────────────────────────────────
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 {
margin-block-start: clamp(2rem, 5vw, 3.25rem);
margin-block-end: clamp(2rem, 6vw, 4rem);
/* phase 50 · padding compressed to match the integrity record
card register (clamp 1.2-1.9rem). archival rhythm unifies.
phase 52 · background promoted to the canonical archival
surface so verify + integrity cards read as one paper sheet
family. border colour promoted to the canonical --rule token
so the divider register matches across pages. */
width: 100%;
max-width: 100%;
min-width: 0;
padding: clamp(1.2rem, 4vw, 1.9rem);
background: var(--surface-archival);
border: 1px solid var(--rule);
/* phase 39 · 2px outer radius matches the project-card register
so verify, integrity and sw-reset records read as the same
family of mounted archival sheets. .record-grid__row stays
sharp — only the outer container is softened. phase 46 ·
literal 8px → var(--radius-soft) (7px). */
border-radius: var(--radius-soft);
/* paper-raise via a quiet light-mode shadow only; dark mode
drops the shadow so it doesn't muddy the page. */
box-shadow: 0 18px 40px rgb(0 0 0 / 0.06);
}
@media (prefers-color-scheme: dark) {
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card {
/* phase 52 · dark-mode card surface promoted to canonical
archival surface for verify / integrity parity. */
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,
.verify-intro-panel,
.verify-intro-list,
.integrity-record-dl,
.integrity-rg,
.integrity-record-actions {
min-width: 0;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-card__header {
margin-block-end: clamp(1.25rem, 4vw, 2rem);
}
: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 {
/* the user warned the theatrical 9vw spec; tightened to a
clamp that still feels composed at desktop without bullying
the rest of the page. */
margin: 0.45rem 0 0.4rem;
font-family: var(--serif);
font-size: clamp(1.75rem, 6.5vw, 3.25rem);
font-weight: 300;
line-height: 0.98;
letter-spacing: -0.012em;
color: var(--fg);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .verify-status {
margin: 0;
font-family: var(--mono);
font-size: 0.72rem;
letter-spacing: 0.08em;
color: var(--fg3);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid {
margin: 0;
display: grid;
/* phase 52 · rule colour unified across verify / integrity. */
border-top: 1px solid var(--rule);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid__row {
display: grid;
grid-template-columns: minmax(7.25rem, 0.34fr) minmax(0, 1fr);
gap: clamp(1rem, 4vw, 2rem);
/* phase 51 · row padding tightened 0.85 → 0.8rem.
phase 52 · rule colour unified to var(--rule). */
padding-block: 0.8rem;
border-bottom: 1px solid var(--rule);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid dt {
font-family: var(--mono);
font-size: 0.64rem;
line-height: 1.2;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--fg3);
/* recede comes from --fg3 + 0.64rem + tracking; no further
opacity reduction — at this size 0.82 would push the blend
below aa (4.5:1) in both modes. */
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. */
line-height: 1.28;
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;
}
.page-hash {
overflow-wrap: anywhere;
word-break: break-word;
font-size: 0.88rem;
line-height: 1.45;
letter-spacing: 0.01em;
}
: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. */
margin-block-start: 0.35rem;
font-family: var(--mono);
font-size: 0.58rem;
line-height: 1.25;
letter-spacing: 0.055em;
color: var(--fg3);
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-tools > * {
display: inline-flex;
align-items: baseline;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-tools .record-inline-action + .record-inline-action::before {
content: "·";
margin-right: 0.55rem;
opacity: 0.45;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .edition-panel {
/* phase 56 · further ~25% reduction on both block margins so
current edition reads as a tight extension of the page record,
and the page closes immediately afterwards. */
margin-block-start: clamp(0.9rem, 3vw, 1.9rem);
margin-block-end: clamp(0.85rem, 2.3vw, 1.4rem);
}
: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). */
border-top-color: var(--rule);
}
: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. */
@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.35rem;
padding-block: 0.9rem;
}
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid dt { font-size: 0.62rem; }
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid dd { font-size: 0.95rem; }
: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
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
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;
}
@media (max-width: 700px) {
.verify-page .other-records {
margin-block-end: clamp(2.5rem, 9vw, 4rem);
}
.verify-page .other-records-grid {
column-gap: clamp(1.5rem, 10vw, 3rem);
}
}
/* ─────────────────────────────────────────────────────────────
legacy verify-page rules (kept for backwards compat with
archived/frozen content under /integrity/releases//
that may still reference the .verify-thispage / .verify-rg
class system). the new active /verify/ dom uses .verify-card
+ .record-grid above.
─────────────────────────────────────────────────────────────
*/
/* /verify/ reads as three composed objects: hero → record card →
chooser → back. the card carries the visual weight; the chooser
sits outside it as quiet secondary nav. related records is gone ,
the card already shows source mirror and release archive, and the
chooser links to integrity and releases. */
.verify-page .verify-record-card {
background: var(--surface-card);
border: 1px solid var(--rule);
/* shared card padding scale (matches /integrity/ signed release card
and /security/ architecture card so the trust system uses one
paper-card register). */
padding: clamp(28px, 4.5vw, 48px);
/* phase 21: card is now the hero object below the lede. tighter
top margin so the lede flows directly into the card; bottom
margin opens the gap to the supporting infrastructure (current
edition) panel that now sits below. */
margin: clamp(16px, 2.2vw, 24px) 0 clamp(32px, 5vw, 56px);
max-width: 860px;
/* subtle paper-raise , restrained shadow, not modern card. */
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.035);
}
.verify-page .verify-record-card .verify-thispage {
margin: 0;
}
/* ── editorial card internals ──
the record reads as a publication colophon, not a metadata table.
page record kicker (mono, restrained) → page title (serif, large)
→ short status line → grouped editorial rows (citation / location /
evidence / fingerprint / archive) → mono-utility actions footer.
hairlines separate groups, never rows. */
.verify-page .verify-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;
}
.verify-page .verify-record-title {
font-family: var(--serif);
font-size: clamp(28px, 4.4vw, 38px);
font-weight: 300;
letter-spacing: -0.012em;
line-height: 1.12;
color: var(--fg);
margin: 0 0 0.35rem;
}
.verify-page .verify-record-status {
font-family: var(--mono);
font-size: 0.74rem;
letter-spacing: 0.025em;
color: var(--fg3);
margin: 0 0 clamp(24px, 4vw, 32px);
}
.verify-page .verify-rg {
/* default groups have no top hairline; rules are added selectively
via .verify-rg--ruled at boundaries the editorial rhythm wants
(status→citation, evidence→fingerprint, fingerprint→archive). */
padding: 0.85rem 0 0.6rem;
}
.verify-page .verify-rg--ruled {
border-top: 1px solid var(--bd);
padding-top: 1rem;
}
.verify-page .verify-rg-label {
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--fg3);
margin: 0 0 0.4rem;
}
.verify-page .verify-rg-value {
margin: 0;
color: var(--fg);
overflow-wrap: anywhere;
word-break: break-word;
}
.verify-page .verify-rg-value--mono {
font-family: var(--mono);
font-size: 0.82rem;
line-height: 1.55;
}
.verify-page .verify-fingerprint {
/* Single-line visual abbreviation; full value lives on the
title/aria-label and is what the copy action returns. */
white-space: nowrap;
overflow-wrap: normal;
word-break: normal;
}
.verify-page .verify-rg-value--serif {
font-family: var(--serif);
font-size: clamp(15px, 1.6vw, 17px);
font-weight: 300;
line-height: 1.5;
}
.verify-page .verify-rg-link {
color: var(--fg);
text-decoration: none;
border-bottom: 1px solid var(--bd);
transition: color 0.2s, border-bottom-color 0.2s;
word-break: break-all;
overflow-wrap: anywhere;
}
.verify-page .verify-rg-link:hover,
.verify-page .verify-rg-link:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
outline: 0;
}
.verify-page .verify-rg-value--serif .verify-rg-link {
border-bottom-width: 1px;
}
.verify-page .verify-record-actions {
border-top: 1px solid var(--rule);
padding-top: 0.85rem;
margin: 1rem 0 0;
font-family: var(--mono);
font-size: 0.7rem;
letter-spacing: 0.025em;
color: var(--fg2);
line-height: 1.6;
/* flex + gap, no inline " · " separators (verify.js stopped
emitting them). wrapped actions never orphan a bullet. */
display: flex;
flex-wrap: wrap;
gap: 0.4rem 1.4rem;
}
.verify-page .verify-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;
}
.verify-page .verify-record-action:hover,
.verify-page .verify-record-action:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
outline: 0;
}
.verify-page .verify-record-action[data-state="copied"] {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
}
/* legacy separator class kept hidden so cached html never paints a
stray bullet; new verify.js doesn't emit them. */
.verify-page .verify-record-actions-sep { display: none; }
/* /verify/ "other records" uses the shared .related-nav grammar
above; only the unknown-route fallback needs its own bottom margin
so it doesn't collide with back. */
.verify-page .verify-unknown { margin: 0 0 clamp(36px, 5vw, 64px); }
/* ─────────────────────────────────────────────────────────────
.trust-disclosure — shared collapsible component
─────────────────────────────────────────────────────────────
one quiet disclosure grammar across /verify/ and /integrity/:
small mono summary, single right-aligned +/− indicator, hairline
top rule, no native browser triangle, no duplicated marker.
Variants:
.trust-disclosure--compact /verify/ "choose another record"
.trust-disclosure--technical /integrity/ "verify release locally"
*/
.trust-disclosure {
/* silent utility row — no top hairline (was a section divider).
quiet vertical margin lets the disclosure sit as a single line
beneath the card, not as a separate page section. */
margin: clamp(14px, 2.2vw, 20px) 0 clamp(14px, 2.2vw, 20px);
padding-top: 0;
}
/* when a disclosure follows the integrity record-card, the paper card
above already provides the visual anchor; tighten further so the
"verify release locally" line reads as a small footer of the card.
phase 57 · gap halved so the accordion reads as an extension of
the release record card, not a new section beneath it. */
.integrity-record-card + .trust-disclosure {
margin-top: clamp(3px, 0.8vw, 8px);
}
.trust-disclosure summary {
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
cursor: pointer;
font-family: var(--mono);
font-size: 0.7rem;
line-height: 1.35;
letter-spacing: 0.025em;
color: var(--fg2);
transition: color 0.2s;
}
.trust-disclosure summary::-webkit-details-marker { display: none; }
.trust-disclosure summary::marker { content: none; }
.trust-disclosure summary::after {
/* fine editorial chevron — rotates 90deg when open. replaces the
earlier +/− marker so the disclosure reads as archival, not as
a generic accordion control. */
content: '›';
display: inline-block;
flex: 0 0 auto;
font-family: var(--mono);
font-size: 1em;
line-height: 1;
color: var(--fg3);
transform: translateY(-0.06em);
transition: transform 0.22s ease, color 0.15s;
}
.trust-disclosure[open] > summary::after {
transform: rotate(90deg) translateX(-0.06em);
}
.trust-disclosure summary:hover,
.trust-disclosure summary:focus-visible {
color: var(--accent-text);
outline: 0;
}
.trust-disclosure summary:hover::after,
.trust-disclosure summary:focus-visible::after { color: var(--accent-text); }
.trust-disclosure__label { display: inline; }
/* phase 50 · summary → body gap tightened ~30% so the disclosure
reads as one continuous technical appendix rather than two
stacked blocks. */
.trust-disclosure__body { margin-top: clamp(10px, 1.5vw, 14px); }
/* technical — short note + command block + copy button. */
.trust-disclosure--technical .trust-disclosure__body > p {
font-family: var(--mono);
font-size: 0.74rem;
color: var(--fg2);
margin: 0 0 1rem;
line-height: 1.55;
}
.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); }
@media (max-width: 700px) {
.trust-code { font-size: 0.66rem; padding: 0.85rem 0.95rem; line-height: 1.55; }
}
/* the unscoped .verify-thispage / .verify-related margin rules below
no longer apply , related records is removed and .verify-thispage's
spacing is owned by the card. */
/* chooser strip , same calm mono utility-link family as
.verify-related-strip and .verify-thispage-actions. the current
record reads as a muted span (no underline, no hover state) so the
row never points back at itself awkwardly. */
.verify-chooser-strip {
font-family: var(--mono);
font-size: 0.68rem;
letter-spacing: 0.025em;
color: var(--fg2);
margin: 0;
line-height: 1.6;
}
.verify-chooser-link {
color: var(--fg2);
text-decoration: none;
border-bottom: 1px solid var(--bd);
transition: color 0.2s, border-bottom-color 0.2s;
}
.verify-chooser-link:hover,
.verify-chooser-link:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
outline: 0;
}
.verify-chooser-link--current {
color: var(--fg3);
border-bottom: 0;
cursor: default;
}
.verify-chooser-sep {
color: var(--fg3);
margin: 0 0.4em;
}
/* page record , the centrepiece. top + bottom hairlines from the dl
already render the frame; widen the grid so labels and values have
room without becoming a card. */
.verify-page .verify-thispage-list { padding: 26px 0 24px; }
@media (min-width: 720px) {
.verify-page .verify-thispage-row {
grid-template-columns: 190px minmax(0, 1fr);
column-gap: 48px;
row-gap: 16px;
}
}
.verify-page .verify-thispage-label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.04em;
color: var(--fg3);
text-transform: uppercase;
}
.verify-page .verify-thispage-value { font-size: 15px; line-height: 1.4; }
.verify-page .verify-thispage-value--mono { font-size: 13px; }
/* connected records , closing nav line, separated by a single hairline
so it reads as the editorial endpoint, not as a stranded paragraph. */
.verify-page .verify-related {
border-top: 1px solid var(--bd);
padding-top: 28px;
}
/* back link , placed, not abandoned. */
/* phase 52 · back link pulls another 15% closer to the closing
content so the page closes tight rather than floating. */
.verify-page .page-back { margin-top: clamp(16px, 2.5vw, 24px); }
/* 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 39 · outer radius matches .verify-card and .project-
card. internal kickers, dl rows, action rows stay sharp.
phase 46 · literal 8px → var(--radius-soft) (7px). */
border-radius: var(--radius-soft);
/* phase 47 · card padding tightened ~25% so the record reads as
a compact archival ledger rather than a spaced-out ui panel.
phase 50 · further compression to compact archival register. */
width: 100%;
max-width: min(100%, 720px);
min-width: 0;
padding: clamp(1.2rem, 4vw, 1.9rem);
margin: clamp(24px, 3.5vw, 36px) 0 clamp(18px, 2.5vw, 26px);
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.035);
}
.integrity-record-kicker {
font-family: var(--mono);
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--fg3);
margin: 0 0 0.55rem;
}
.integrity-record-title {
font-family: var(--serif);
font-size: clamp(28px, 4.4vw, 38px);
font-weight: 300;
letter-spacing: -0.012em;
line-height: 1.12;
color: var(--fg);
margin: 0 0 0.35rem;
}
.integrity-record-status {
font-family: var(--mono);
font-size: 0.74rem;
letter-spacing: 0.025em;
color: var(--fg3);
margin: 0 0 clamp(20px, 3vw, 28px);
}
.integrity-record-dl { margin: 0; padding: 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. */
.integrity-rg {
/* phase 50 · row padding tightened 0.85 → 0.72rem so the ledger
reads as a denser archival sheet. fingerprint row keeps its
touch more breathing via .integrity-rg--fingerprint below. */
padding-block: 0.72rem;
}
.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 {
font-family: var(--mono);
/* phase 45 · slightly larger + wider tracking + quieter colour so
the labels read as quiet archival headings, with the value below
carrying the weight.
phase 47 · bottom margin tightened from 0.45 → 0.35.
phase 50 · further tightened 0.35 → 0.2.
phase 54 · alpha moved from opacity to color-mix.
phase 59 · letter-spacing 0.14 → 0.12em so the row labels
match the dominant label register (kicker, history-heading
pre-section-kicker, etc.). one tracking value across the
small-caps system.
phase 60 · color-mix 78% → plain --fg3. at 0.72rem the mix
blended below aa contrast in light mode. recede comes from
size + uppercase + tracking + --fg3 alone. */
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--fg3);
margin: 0 0 0.2rem;
}
.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 {
color: var(--fg);
text-decoration: none;
border-bottom: 1px solid var(--bd);
transition: color 0.2s, border-bottom-color 0.2s;
word-break: break-all;
}
.integrity-rg-link:hover,
.integrity-rg-link:focus-visible {
color: var(--accent-text);
border-bottom-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: 720px) {
@supports (grid-template-columns: subgrid) {
.integrity-record-dl {
display: grid;
grid-template-columns: 200px 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;
}
}
}
.fingerprint-grid {
display: grid;
grid-template-columns: repeat(3, max-content);
justify-content: start;
align-items: baseline;
column-gap: clamp(1.1rem, 5vw, 2rem);
row-gap: 0.55rem;
width: max-content;
max-width: 100%;
overflow-x: auto;
padding-block: 0.25rem;
font-family: var(--mono);
font-size: clamp(1rem, 4.1vw, 1.15rem);
line-height: 1.35;
letter-spacing: 0.01em;
color: var(--fg);
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum" 1;
font-variant-ligatures: none;
background: transparent;
}
.fingerprint-grid span {
white-space: nowrap;
}
@media (max-width: 360px) {
.fingerprint-grid {
grid-template-columns: repeat(2, max-content);
}
}
.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;
}
.release-card .fingerprint-section,
.record-card .fingerprint-section {
margin-top: 1.35rem;
padding-top: 1rem;
border-top: 1px solid var(--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 never paints a
bullet. new html omits the separators entirely. */
.integrity-record-actions-sep { display: none; }
@media (max-width: 700px) {
.integrity-record-actions {
flex-direction: column;
gap: 0.5rem 0;
line-height: 1.4;
}
.integrity-record-action { align-self: flex-start; }
}
/* 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;
}
@media (max-width: 700px) {
.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 */
}
}
/* phase 49 · the standalone .integrity-record-copy strip was retired.
the copy action now sits inline on the fingerprint row's
header (see .integrity-rg--fingerprint .integrity-rg-label above
and the .copy-fingerprint rule further down). */
/* /integrity/ Verify-release-locally disclosure: chrome lives in the
shared .trust-disclosure rules above. no /integrity/-specific summary
styling needed — the page just adds the technical variant. */
/* 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 support-navigation pattern — .related-nav
─────────────────────────────────────────────────────────────
single grammar for every secondary link strip across the trust
pages: small mono label, one quiet hairline top rule, inline
links with " · " separators. used by /integrity/ related records
and /verify/ related records. reads as a finishing footer —
never a content section, never a second module. */
.related-nav,
.integrity-related,
.related-grid {
/* give the closing support nav one clear breath below the primary
object so the page ends composed rather than cramped. */
margin: clamp(16px, 2.8vw, 24px) 0 clamp(10px, 1.8vw, 16px);
}
.related-nav-label,
.integrity-related-label {
/* Support-nav label register — quieter than the in-card metadata
labels (page record, manifest, history) so support nav never
competes with the page-record card or the section it follows.
title case, gentle tracking, mono very small, fg3. the final
shelf of the archive — should read as a footnote, not a
section. */
font-family: var(--mono);
font-size: 0.6rem;
font-weight: 500;
letter-spacing: 0.05em;
text-transform: none;
color: var(--fg3);
margin: 0 0 0.35rem;
}
.related-nav-list,
.integrity-related-list,
.related-grid-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.8rem 1.5rem;
font-family: var(--mono);
font-size: 0.82rem;
color: var(--fg3);
line-height: 1.7;
}
.related-nav-list li,
.integrity-related-list li,
.related-grid-list li { display: block; }
@media (min-width: 760px) {
.related-nav-list,
.integrity-related-list,
.related-grid-list {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.related-nav-list a,
.integrity-related-list a,
.related-grid-list a {
color: var(--fg2);
font-family: var(--mono);
font-size: 0.82rem;
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.22em;
transition: color 0.2s, text-decoration-color 0.2s;
}
.related-nav-list a:hover,
.related-nav-list a:focus-visible,
.integrity-related-list a:hover,
.integrity-related-list a:focus-visible,
.related-grid-list a:hover,
.related-grid-list a:focus-visible {
color: var(--accent-text);
outline: 0;
}
.related-nav-list span[aria-current="page"] {
color: var(--fg3);
cursor: default;
}
@media (max-width: 700px) {
.related-nav-list,
.integrity-related-list,
.related-grid-list { font-size: 0.78rem; }
}
/* ── shared command component (used on /verify/ and /integrity/) ──
header strip (title left + copy button right) sits above the
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-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.6rem 1rem;
flex-wrap: wrap;
margin: 0 0 0.4rem;
}
.verify-command-title {
font-family: var(--serif);
font-size: clamp(14px, 1.5vw, 16px);
font-weight: 400;
letter-spacing: 0;
line-height: 1.3;
color: var(--fg);
margin: 0;
text-transform: none;
}
.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-command-block {
background: var(--bg2);
border: 1px solid var(--bd);
border-radius: 4px;
padding: 0.85em 1em;
margin: 0;
overflow-x: auto;
max-width: 100%;
}
.verify-command-block code {
font-family: var(--mono);
font-size: 0.78rem;
line-height: 1.6;
color: var(--fg);
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
}
.verify-command-note {
font-family: var(--serif);
font-size: clamp(13px, 1.4vw, 15px);
font-weight: 300;
line-height: 1.45;
color: var(--fg2);
max-width: 64ch;
margin: 0.6rem 0 1rem;
}
/* general routes mode (no path) + unknown-route fallback. */
.verify-general-body {
font-family: var(--serif);
font-size: clamp(15px, 1.6vw, 18px);
font-weight: 300;
line-height: 1.55;
color: var(--fg2);
max-width: 62ch;
margin: 0 0 1.5rem;
}
.verify-unknown-path {
font-family: var(--mono);
font-size: 0.85rem;
color: var(--fg2);
margin: 0 0 0.8rem;
word-break: break-all;
}
/* trust routes , compact mental-model block
mirrors the homepage approach-list pattern: mono numerals, signifier
label, calm serif descriptor. restrained editorial composition; not
a dashboard, not a sitemap. */
.trust-routes {
margin: clamp(28px, 4vw, 40px) 0 clamp(20px, 3vw, 28px);
max-width: 64ch;
}
.trust-routes-heading {
font-family: var(--mono);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--fg3);
margin: 0 0 0.7rem;
}
.trust-routes-list {
list-style: none;
margin: 0;
padding: 0;
}
.trust-routes-item {
display: grid;
grid-template-columns: 2.4rem minmax(0, 1fr);
column-gap: 0.9rem;
padding: 0.65rem 0;
align-items: baseline;
border-top: 1px solid var(--bd);
}
.trust-routes-item:last-child {
border-bottom: 1px solid var(--bd);
}
.trust-routes-num {
font-family: var(--mono);
font-size: 0.78rem;
font-weight: 500;
letter-spacing: 0.04em;
color: var(--accent-text);
line-height: 1.3;
}
.trust-routes-body {
min-width: 0;
}
.trust-routes-title {
display: inline-block;
font-family: var(--serif);
font-size: clamp(15px, 1.55vw, 17px);
font-weight: 400;
color: var(--fg);
line-height: 1.3;
text-decoration: none;
border-bottom: 1px solid var(--bd);
transition: color 0.2s, border-bottom-color 0.2s;
}
.trust-routes-title:hover,
.trust-routes-title:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
outline: 0;
}
/* Current-page state , non-interactive, no underline, slightly muted
so the row reads as the visitor's current location, not a self-link. */
.trust-routes-title--current {
color: var(--fg3);
border-bottom: 0;
}
.trust-routes-desc {
display: block;
margin: 0.18rem 0 0;
font-family: var(--serif);
font-size: clamp(13px, 1.4vw, 14.5px);
font-weight: 300;
color: var(--fg2);
line-height: 1.45;
}
/* 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. */
/* .verify-thispage block-rhythm is owned by the scoped `.verify-page
.verify-thispage` clamp() rule near the top of this file. */
/* inline status strip , three short mono chips, fg3, no badges, no
icons. sits between the section heading and the metadata list. */
.verify-thispage-status {
font-family: var(--mono);
font-size: 0.74rem;
letter-spacing: 0.025em;
color: var(--fg3);
margin: -0.1rem 0 0.7rem;
line-height: 1.4;
}
.verify-thispage-status-sep {
color: var(--fg3);
opacity: 0.7;
}
.verify-thispage-status-chip {
white-space: nowrap;
}
.verify-thispage-list {
display: grid;
grid-template-columns: 1fr;
gap: 0;
margin: 0;
padding: 0.7rem 0;
border-top: 1px solid var(--bd);
border-bottom: 1px solid var(--bd);
}
.verify-thispage-row {
display: grid;
grid-template-columns: 1fr;
gap: 0.05rem;
padding: 0.45rem 0;
}
@media (min-width: 720px) {
.verify-thispage-row {
grid-template-columns: 200px 1fr;
column-gap: 1.4rem;
padding: 0.5rem 0;
align-items: baseline;
}
}
.verify-thispage-label {
margin: 0;
font-family: var(--serif);
font-size: 0.92rem;
font-weight: 400;
color: var(--fg3);
line-height: 1.4;
}
.verify-thispage-value {
margin: 0;
font-family: var(--serif);
font-size: clamp(14px, 1.55vw, 16px);
font-weight: 300;
color: var(--fg);
line-height: 1.4;
min-width: 0;
word-break: break-word;
overflow-wrap: anywhere;
}
.verify-thispage-value--mono {
font-family: var(--mono);
font-size: 0.85em;
word-break: break-all;
}
.verify-thispage-link {
font-family: inherit;
font-size: inherit;
color: var(--fg);
text-decoration: none;
border-bottom: 1px solid var(--bd);
word-break: break-all;
overflow-wrap: anywhere;
transition: color 0.2s, border-bottom-color 0.2s;
}
.verify-thispage-link:hover,
.verify-thispage-link:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
}
/* page fingerprint row , value stacks: hash on its own line, then a
small copy fingerprint button below. same pattern as the cite-overlay
hash treatment so it never overlaps. */
.verify-thispage-fingerprint-value {
display: flex;
flex-direction: column;
gap: 0.45rem;
align-items: flex-start;
}
.verify-thispage-fingerprint {
font-family: var(--mono);
font-size: 0.78rem;
line-height: 1.5;
color: var(--fg2);
word-break: break-all;
overflow-wrap: anywhere;
background: transparent;
width: 100%;
}
.verify-thispage-fingerprint-copy {
appearance: none;
font-family: var(--mono);
font-size: 0.7rem;
letter-spacing: 0.03em;
color: var(--fg3);
background: transparent;
border: 0;
border-bottom: 1px solid var(--bd);
padding: 0.1rem 0;
cursor: pointer;
transition: color 0.2s, border-bottom-color 0.2s;
text-transform: none;
}
.verify-thispage-fingerprint-copy:hover,
.verify-thispage-fingerprint-copy:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
outline: 0;
}
.verify-thispage-fingerprint-copy[data-state="copied"] {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
}
/* compact actions strip beneath the page-record dl. same restrained
mono utility-link style as .verify-related-strip, so the two strips
sit as a calm pair at the bottom of /verify/. */
.verify-thispage-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-thispage-action {
appearance: none;
font: inherit;
letter-spacing: inherit;
color: var(--fg);
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;
}
.verify-thispage-action:hover,
.verify-thispage-action:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
outline: 0;
}
.verify-thispage-action[data-state="copied"] {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
}
.verify-thispage-actions-sep {
color: var(--fg3);
margin: 0 0.4em;
}
/* ── Section: verify locally , closing line ── */
.verify-local-closing {
font-family: var(--serif);
font-size: clamp(13px, 1.4vw, 14.5px);
font-weight: 300;
line-height: 1.5;
color: var(--fg2);
margin: 1.2rem 0 0;
max-width: 64ch;
}
/* ── proof summary card ──
compact two-column dl , label column content-led on desktop, fixed
on mobile so long urls and hashes don't crush labels. status row at
the top carries a thin oxblood mark (when the page is found). one
hairline above and below the dl, no per-row dividers. */
/* ── "verify locally" , 2 command blocks with descriptions ── */
/* .verify-local block-rhythm is owned by the scoped `.verify-page
.verify-local` clamp() rule. */
.verify-local-desc {
font-family: var(--serif);
font-size: clamp(13px, 1.4vw, 14.5px);
font-weight: 300;
color: var(--fg2);
line-height: 1.45;
margin: 0 0 0.55rem;
max-width: 60ch;
}
.verify-local-desc--second {
margin-top: 1.4rem;
}
/* ── related records , compact link strip ──
.verify-related block-rhythm + top hairline owned by the scoped
`.verify-page .verify-related` rule. */
.verify-related-strip {
font-family: var(--mono);
font-size: 0.78rem;
letter-spacing: 0.025em;
color: var(--fg);
margin: 0;
line-height: 1.5;
}
.verify-related-link {
color: var(--fg);
text-decoration: none;
border-bottom: 1px solid var(--bd);
transition: color 0.2s, border-bottom-color 0.2s;
}
.verify-related-link:hover,
.verify-related-link:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
}
.verify-related-sep {
color: var(--fg3);
margin: 0 0.4em;
}
/* "route not in map" calm notice (renderunknown). */
.verify-unknown {
margin: 0 0 1.8rem;
}
@media (max-width: 600px) {
.verify-command-block {
padding: 0.75em 0.9em;
}
.verify-command-block code {
font-size: 11px;
line-height: 1.55;
}
.verify-local-desc--second { margin-top: 1.1rem; }
}
/* accessibility */
/* subtle highlight for keyboard-navigated sections */
:target {
outline-offset: 4px;
}
.skip-link {
position: absolute;
left: -9999px;
top: 0;
z-index: 999;
font-family: var(--mono);
font-size: 12px;
padding: 8px 16px;
background: var(--fg);
color: var(--bg);
text-decoration: none;
}
.skip-link:focus {
left: 8px;
top: 8px;
}
/* legacy :focus-visible rule replaced by the editorial focus-visible
system in @layer base above. keep .skip-link's focus override
only — it needs to remain prominent so keyboard users can spot
the bypass. */
:focus:not(:focus-visible) {
outline: none;
}
/* layout */
.site {
max-width: 1200px;
margin: 0 auto;
padding: 0 clamp(24px, 5vw, 80px);
}
/* Safe-area bottom on the main content surface so the last in-flow cta
(e.g. the homepage `view project` button) never sits beneath ios
safari's bottom chrome when the user has not yet scrolled to retract
it. the footer carries its own safe-area-inset-bottom via .footer. */
main.site {
padding-bottom: env(safe-area-inset-bottom);
}
section {
padding: clamp(80px, 12vh, 160px) 0;
border-bottom: 1px solid var(--bd);
}
section:last-of-type {
border-bottom: none;
}
/* hero → approach divider · the hero is a , so the generic
section { border-bottom } rule never paints a rule below it. add
a top hairline on the first section so the hero-to-approach
transition reads as resolved as the section-to-section breaks
that follow. matched to the same --bd token + clamp padding the
rest of the spine uses. */
.home-profile > section:first-of-type {
border-top: 1px solid var(--bd);
}
/* nav
── the nav family lives inside @layer components so the homepage
disclosure rules in @layer pages (later in cascade order) can win
normally for the open-state layout (display:grid · text-align:right)
without needing !important. per css cascade layers level 5, normal
declarations cascade later-layer-wins; !important declarations
cascade earlier-layer-wins, with unlayered author rules sitting
above all layered author rules. leaving these rules unlayered would
silently override @layer pages every time. ── */
@layer components {
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: var(--bg);
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);
}
.nav-inner {
max-width: 1200px;
margin: 0 auto;
/* horizontal safe-area padding: the inset-left/right tokens are
non-zero on landscape iphone (notch on one side); max() with the
existing clamp() keeps the editorial register on every other
orientation while preventing the nav from slipping under the
notch in landscape. */
padding-block: 0;
padding-inline-start: max(clamp(24px, 5vw, 80px), var(--safe-left));
padding-inline-end: max(clamp(24px, 5vw, 80px), var(--safe-right));
display: flex;
justify-content: space-between;
align-items: center;
/* phase 40 · 52 → 54px gives a touch more vertical breathing
so the header reads as deliberate rather than utilitarian.
within the 60px --nav-offset buffer, anchor scrolling stays
correct. */
height: 54px;
}
/* the wordmark is identity · söhne mono carries the archival voice.
phase 40 · tracking 0.10 → 0.09em pulls the wordmark slightly
tighter so it reads as set type rather than spread-out caps. */
.nav-mark {
font-family: var(--mono);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--fg);
text-decoration: none;
}
/* nav-toggle / nav-links / nav-toggle-glyph / nav-toggle-label —
retired site-wide. the masthead is the only header element now;
the homepage scrolls naturally and discovery is by reading. */
/* narrow phones: tighten the masthead horizontally so the stacked
trent / power lockup sits cleanly against the page edge. */
@media (max-width: 600px) {
.nav-inner {
padding-inline: 15px;
height: 58px;
align-items: center;
}
.nav-mark {
display: block;
max-width: 4.55rem;
white-space: normal;
line-height: 1.08;
font-size: 11px;
letter-spacing: 0.09em;
}
.nav-mark span { display: block; }
}
@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;
/* widened from 52px so the hero text always clears the nav at every
width (52px desktop nav, 58px mobile nav, plus a small breathing
budget). with sticky positioning the nav reserves flow space, so
this padding-top is partly belt-and-braces — keeps text away from
the very top edge even when the nav scroll-hides. */
padding-top: clamp(60px, 8vh, 80px);
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 eyebrow. the "trent power" label is rendered via CSS-generated
content so reader view extracts only one author signal (from the
article body / metadata), not a duplicate eyebrow text node. the
in html is empty + aria-hidden. */
.hero-name::before {
content: "Trent Power";
}
.hero-name {
font-family: var(--mono);
font-size: clamp(11px, 1.2vw, 13px);
font-weight: 500;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--fg3);
margin-bottom: clamp(24px, 4vh, 48px);
}
.hero-statement {
font-family: var(--serif);
font-size: clamp(42px, 10vw, 82px);
font-weight: 300;
line-height: 0.98;
letter-spacing: -0.035em;
color: var(--fg);
max-width: 920px;
margin-bottom: clamp(32px, 5vh, 56px);
}
.hero-body {
font-family: var(--serif);
font-size: clamp(15px, 1.6vw, 19px);
font-weight: 300;
line-height: 1.65;
color: var(--fg2);
/* phase 40 · ~5% wider measure (580 → 610px). on mobile this
gives the hero paragraph fewer awkward short-line wraps; on
desktop it stays well below the 720px page-title cap so the
editorial hierarchy is preserved. */
max-width: 610px;
}
/* hero entrance · single reveal contract (phase 37)
──────────────────────────────────────────────────────────
default: hero pieces start invisible and lifted. when js is
off (no .js class on ), the override below paints them
at rest immediately so no-JS visitors see the hero. when js
is on and html.enhanced has been added (after DOMContentLoaded
in app.js), each piece animates up in sequence with the trust
mark participating as the last beat. reduced-motion users see
the resting state regardless. */
/* hero reveal · gated on html.js (set inline pre-paint by the language
bootstrap, runs synchronously before the first paint frame). hiding
and the reveal keyframe are now driven by the same class, so a
failed or slow app.js cannot leave the hero stuck invisible — the
animation starts in the same paint tick the hide begins. when js
is disabled entirely the hide rule never matches and the hero
paints resting, no fallback rule needed. */
html.js .hero-name,
html.js .hero-statement,
html.js .hero-body,
html.js .trust-mark {
opacity: 0;
transform: translateY(14px);
}
html.js .hero-name {
animation: revealUp 700ms cubic-bezier(.2,.7,.2,1) forwards;
}
html.js .hero-statement {
animation: revealUp 800ms cubic-bezier(.2,.7,.2,1) 90ms forwards;
}
html.js .hero-body {
animation: revealUp 850ms cubic-bezier(.2,.7,.2,1) 180ms forwards;
}
html.js .trust-mark {
animation: revealUp 850ms cubic-bezier(.2,.7,.2,1) 270ms forwards;
}
/* hero entrance keyframe · animates from the initial-hidden state
set above (opacity: 0; transform: translateY(14px)) to resting.
referenced by each of the four hero pieces with a staggered delay. */
@keyframes revealUp {
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.hero-name,
.hero-statement,
.hero-body,
.trust-mark {
opacity: 1 !important;
transform: none !important;
animation: none !important;
}
}
/* ─── trust mark ──────────────────────────────────────────
bordered editorial tag under the hero body. reads as an
intentional certification badge, not three plain links.
sits inside in dom order after
.hero-statement and .hero-body. participates in the
`html.enhanced` hero reveal sequence defined above. links
to /privacy/, /source/, /integrity/ at link-inherit colours
(never default blue). */
.trust-mark {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
width: fit-content;
max-width: 100%;
margin-block-start: clamp(1.2rem, 3vw, 1.8rem);
/* phase 40 · slightly more present so the certification strip
reads as deliberate rather than apologetic.
phase 46 · radius elevated to var(--radius-pill) (999px).
phase 49 · reversed back to var(--radius-soft) (7px).
phase 53 · subtle background tint promotes the trust mark
from bordered text strip to archival label — visually matches
the verify / integrity card surface (--surface-archival) so
the mark reads as a small object from the same paper family. */
background: var(--surface-archival);
padding: 0.6rem 0.8rem;
border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
border-radius: var(--radius-soft);
font-family: var(--mono);
font-size: 0.7rem;
line-height: 1.2;
letter-spacing: 0.07em;
color: var(--fg3);
}
.trust-mark a {
color: inherit;
text-decoration: none;
}
.trust-mark a:visited {
color: inherit;
}
.trust-mark a + a::before {
content: "·";
display: inline-block;
margin-inline: 0.55rem;
opacity: 0.55;
color: currentColor;
/* phase 39 · optical-centre the dot between cap heights;
matches the same pattern used on the footer actions row. */
transform: translateY(-0.02em);
}
.trust-mark a:hover,
.trust-mark a:focus-visible {
color: var(--fg);
text-decoration: underline;
text-underline-offset: 0.18em;
}
.trust-mark a:focus-visible {
outline: 1px solid currentColor;
outline-offset: 0.25rem;
}
@media (max-width: 700px) {
.trust-mark {
font-size: 0.64rem;
letter-spacing: 0.06em;
}
}
/* 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;
}
/* section 2 · focus & approach */
.principles {
display: grid;
gap: 0;
list-style: none;
}
/* principles, trajectory chapters, and project cards are visible by default
so the page renders correctly without javascript. the `.js` class
(added by the inline language script before first paint) opts into the
scroll-reveal behaviour. */
.principle {
padding: clamp(28px, 4vh, 48px) 0;
border-bottom: 1px solid var(--bd-soft);
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
.js .principle {
opacity: 0;
transform: translateY(16px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.js .principle.visible {
opacity: 1;
transform: translateY(0);
}
.principle:last-child {
border-bottom: none;
}
.principle-title {
font-family: var(--serif);
font-size: clamp(22px, 3vw, 30px);
font-weight: 400;
line-height: 1.2;
color: var(--fg);
letter-spacing: -0.018em;
}
.principle-body {
font-family: var(--serif);
font-size: clamp(14px, 1.4vw, 17px);
font-weight: 300;
line-height: 1.6;
color: var(--fg2);
max-width: 560px;
}
@media (min-width: 768px) {
.principle {
grid-template-columns: 1fr 1fr;
gap: clamp(24px, 4vw, 80px);
align-items: baseline;
}
}
/* section 3 · trajectory */
.trajectory-grid {
display: grid;
gap: 0;
}
/* trajectory · scroll-reveal initial state. the IntersectionObserver
in app.js promotes each .trajectory-item to .visible as it enters
the viewport. gating on .js means the items are visible by default
when scripting is unavailable — no orphaned hidden cards. */
.js .trajectory-item {
opacity: 0;
transform: translateY(16px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.js .trajectory-item.visible {
opacity: 1;
transform: translateY(0);
}
/* legacy .chapter-* typography retired — the new .trajectory-item /
.trajectory-kicker / .trajectory-title / .trajectory-detail rules
in the trajectory chronology block downstream now carry the same
register. anchors to #clienteling-definition still resolve to the
in the principles list above. */
/* 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;
}
.trajectory-list {
list-style: none;
padding: 0;
margin: 0;
}
.trajectory-item {
min-width: 0;
position: relative;
/* belt-and-braces marker reset · ios safari occasionally paints
decimal markers even when the parent ol has list-style: none.
setting list-style on the li and emptying ::marker kills the
"1. 2. 3." numbering for good. */
list-style: none;
}
.trajectory-item::marker {
content: "";
}
.trajectory-year {
display: block;
font: 500 11px/1.2 var(--mono);
letter-spacing: 0.04em;
color: var(--trajectory-muted);
font-variant-numeric: tabular-nums lining-nums;
margin: 0;
}
.trajectory-item--current .trajectory-year {
color: var(--trajectory-current);
font-weight: 500;
}
.trajectory-marker {
display: block;
width: 0.55rem;
height: 0.55rem;
border: 1px solid var(--trajectory-marker);
border-radius: 999px;
background: var(--bg);
box-sizing: border-box;
}
.trajectory-item--current .trajectory-marker {
width: 0.7rem;
height: 0.7rem;
background: var(--trajectory-current);
border-color: var(--trajectory-current);
}
.trajectory-card {
display: flex;
flex-direction: column;
gap: 0.45rem;
max-width: var(--trajectory-card-max);
margin: 0;
padding: 0;
}
.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;
}
.trajectory-org {
font: 500 10px/1.2 var(--mono);
letter-spacing: 0.06em;
text-transform: none;
color: var(--trajectory-current);
margin: 0;
}
.trajectory-detail {
font: 300 clamp(13px, 1vw, 14px)/1.55 var(--serif);
color: var(--fg2);
margin: 0;
}
.trajectory-period {
font: 500 9.5px/1.4 var(--mono);
letter-spacing: 0.06em;
color: var(--trajectory-muted);
font-variant-numeric: tabular-nums lining-nums;
margin: 2px 0 0;
white-space: nowrap;
}
.trajectory-period time { font: inherit; color: inherit; }
/* ── mobile · vertical archival chronology ───────────────────
rail is a left border on the list; markers pull into the gutter
over the rail. a subtle border-top separates each row. spacing
is generous but never oversized. */
@media (max-width: 759.98px) {
.trajectory-list {
margin: clamp(2.4rem, 9vw, 3.6rem) 0 0;
padding: 0 0 0 1.25rem;
border-left: 1px solid var(--trajectory-line);
}
.trajectory-item {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.5rem;
padding: 0 0 clamp(1.6rem, 6vw, 2.2rem) clamp(1rem, 3.5vw, 1.4rem);
}
.trajectory-item + .trajectory-item {
border-top: 1px solid var(--trajectory-line);
padding-top: clamp(1.6rem, 6vw, 2.2rem);
}
.trajectory-year {
order: 1;
}
.trajectory-marker {
position: absolute;
top: clamp(1.6rem, 6vw, 2.2rem);
left: calc(-1.25rem - 0.275rem);
}
.trajectory-item:first-child .trajectory-marker {
top: 0;
}
.trajectory-item--current .trajectory-marker {
left: calc(-1.25rem - 0.35rem);
}
.trajectory-card {
order: 2;
gap: 0.45rem;
}
.trajectory-title {
font-size: clamp(15px, 4vw, 17px);
max-width: 24rem;
}
.trajectory-detail {
font-size: clamp(13px, 3.7vw, 15px);
max-width: 32rem;
}
}
/* ── tablet · two-column editorial grid (760–1099) ────────────
the section label sits in a quiet left rail; the four items
stack 2×2 on the right. no horizontal axis (it would clip at
this width); the chronology reads as a structured editorial
index. */
@media (min-width: 760px) and (max-width: 1099.98px) {
.section-trajectory {
display: grid;
grid-template-columns: minmax(10rem, 14rem) minmax(0, 1fr);
column-gap: clamp(2rem, 4vw, 3.5rem);
align-items: start;
}
.section-trajectory > .section-label {
grid-column: 1;
margin: 0;
padding-right: clamp(1rem, 2vw, 2rem);
border-right: 1px solid var(--trajectory-line);
min-height: 2rem;
}
.trajectory-list {
grid-column: 2;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: clamp(1.5rem, 3vw, 2.5rem);
row-gap: clamp(1.8rem, 3.5vw, 2.8rem);
margin: 0;
}
.trajectory-item {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto 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-card {
grid-row: 3;
max-width: 24rem;
}
}
/* ── desktop · four-column horizontal chronology (≥ 1100) ─────
rail is a single horizontal hairline across the top of the list
via ::before. each item is a column: year above marker above card.
marker margins are calibrated so the dot's vertical centre lands
on the rail regardless of font metrics. */
@media (min-width: 1100px) {
.trajectory-list {
position: relative;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: var(--trajectory-gap);
margin: clamp(3rem, 7vw, 5rem) 0 0;
}
.trajectory-list::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 2.4rem;
border-top: 1px solid var(--trajectory-line);
}
.trajectory-item {
display: flex;
flex-direction: column;
align-items: flex-start;
min-width: 0;
}
.trajectory-year {
margin: 0 0 1rem;
}
.trajectory-marker {
margin: 0 0 1.25rem;
/* year-bottom + 1rem gap + 0.55rem dot puts dot centre at the rail
(rail sits at top: 2.4rem on the list). */
align-self: flex-start;
}
.trajectory-item--current .trajectory-marker {
margin: -0.075rem 0 1.175rem;
}
.trajectory-card {
max-width: var(--trajectory-card-max);
}
}
/* ── print · existing print profile owns the trajectory ─────── */
@media print {
.trajectory-list { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.trajectory-item { animation: none; transition: none; }
}
/* section 4 · personal projects */
.project-card {
/* editorial insert on the homepage — its own --paper-project tone
so it lifts visibly off the front-of-house ivory and reads as
a sheet placed on the page, never as a record-layer card.
phase 39 · 2px outer radius softens the object quality just
enough to read as a mounted archival sheet rather than a
mathematically sharp web card. internal rules, metadata
rows and buttons stay sharp. phase 46 · literal 8px →
var(--radius-soft) (7px). */
background: var(--paper-project);
border: 1px solid var(--rule);
border-radius: var(--radius-soft);
padding: clamp(36px, 8vw, 48px) clamp(28px, 8vw, 44px) clamp(34px, 9vw, 52px);
margin-top: clamp(40px, 7vh, 64px);
position: relative;
}
@media (min-width: 768px) {
.project-card { padding: clamp(48px, 5vw, 72px); }
}
.js .project-card {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.js .project-card.visible {
opacity: 1;
transform: translateY(0);
}
.project-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background: var(--ac);
}
/* editorial cards opt out of the accent rail. */
.project-card--editorial::before { content: none; display: none; }
.project-card--editorial { border-left: 1px solid var(--rule); }
.project-name {
font-family: var(--mono);
/* phase 40 · slightly larger and tighter so the project title
reads as the clear visual anchor of the card (söhne mono is
only available in 400/500 — no heavier weight option, so the
hierarchy bump comes from size + tracking instead of weight).
stays mono caps, stays at full --fg ink. */
font-size: clamp(15px, 1.7vw, 19px);
font-weight: 500;
letter-spacing: 0.10em;
text-transform: uppercase;
color: var(--fg);
margin-bottom: 18px;
}
.project-desc {
font-family: var(--serif);
font-size: clamp(19px, 5vw, 22px);
font-weight: 400;
line-height: 1.58;
letter-spacing: -0.01em;
color: var(--fg2);
max-width: 32ch;
margin-bottom: 34px;
}
.project-subline {
font-family: var(--serif);
font-size: clamp(14px, 1.6vw, 16px);
font-style: italic;
font-weight: 400;
line-height: 1.55;
color: var(--fg2);
margin-top: 0;
margin-bottom: 28px;
}
.project-link {
font-family: var(--mono);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent-text);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 0;
border-bottom: 1px solid var(--bd);
transition: border-color 0.2s, gap 0.2s;
}
.project-link:hover {
border-color: var(--accent-text);
gap: 12px;
}
/* Defensive: anywhere a typographic mark like → is rendered, force it
through söhne mono so font-fallback can't substitute the glyph. */
.arrow {
font-family: var(--mono);
}
.project-link .arrow {
transition: transform 0.2s;
}
.project-link:hover .arrow {
transform: translateX(2px);
}
/* button variant · replaces inline style="" for csp compliance */
.project-link-btn {
margin-top: 24px;
background: none;
border: none;
border-bottom: 1px solid var(--bd);
cursor: pointer;
}
/* project preview · mimics the app ui */
.project-preview {
margin-top: 34px;
border-top: 1px solid var(--bd);
padding-top: 26px;
}
.preview-header {
font-family: var(--mono);
font-size: 10px;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--fg3);
margin-bottom: 16px;
}
.preview-items {
display: grid;
gap: 0;
list-style: none;
}
.preview-item {
padding: 10px 0;
border-bottom: 1px solid var(--bd-soft);
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
}
.preview-item:last-child {
border-bottom: none;
}
.preview-title {
font-family: var(--mono);
font-size: 11px;
font-weight: 500;
color: var(--fg);
}
.preview-meta {
font-family: var(--mono);
font-size: 10px;
color: var(--fg3);
white-space: nowrap;
}
.preview-editorial {
font-family: var(--serif);
font-style: italic;
font-size: 11px;
color: var(--fg3);
margin-top: 2px;
}
.project-preview-caption {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.08em;
color: var(--fg3);
margin-top: 14px;
line-height: 1.5;
}
.preview-tier {
font-size: 9px;
letter-spacing: 0.08em;
padding: 1px 5px;
border: 1px solid var(--bd);
color: var(--fg3);
margin-right: 6px;
}
.tier-walk { border-color: var(--bd); color: var(--fg3); }
.tier-metro { border-color: var(--accent-text); color: var(--accent-text); }
/* contact */
.section-contact {
padding: clamp(80px, 12vh, 160px) 0 clamp(60px, 8vh, 100px);
}
@media (max-width: 767px) {
.section-contact {
padding-top: clamp(64px, 9.5vh, 112px);
}
}
/* small editorial sentence above the email — gives the contact
section a reason to write rather than three abrupt anchors.
serif, muted, with a narrow measure so it reads as a brief
abstract, not a paragraph. */
.contact-note {
font-family: var(--serif);
font-size: clamp(15px, 1.5vw, 17px);
font-weight: 300;
line-height: 1.55;
color: var(--fg2);
max-width: 32rem;
margin: 0 0 clamp(1.1rem, 3vw, 1.5rem);
}
.contact-address {
font-style: normal;
margin: 0;
}
.contact-email {
font-family: var(--mono);
font-size: clamp(14px, 2vw, 22px);
font-weight: 400;
color: var(--fg);
text-decoration: none;
border-bottom: 1px solid var(--bd);
padding-bottom: 4px;
transition: border-color 0.2s;
/* Long-text resilience: at 200% zoom on a 320 px viewport the rendered
mailto could otherwise overflow the column. word-break stays normal
so spaces in the readable form are preserved. */
overflow-wrap: anywhere;
word-break: normal;
}
.contact-email:hover {
border-color: var(--accent-text);
}
/* Long-token resilience for narrow viewports / 200% zoom. mono url
and project-preview titles can each overrun their column at 320px
width without `overflow-wrap: anywhere`. fingerprint grids are
component-controlled and therefore excluded here. */
.preview-title {
overflow-wrap: anywhere;
word-break: normal;
}
.integrity-record-status,
.verify-status,
.record-meta,
.integrity-rg-value,
.integrity-rg-desc,
.integrity-rg-link,
.integrity-rg-value code,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid dd,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid a,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid code,
:is(.verify-page, .sw-reset-page, .security-page, .source-page) .record-grid samp,
.verify-intro-row dd,
.verify-intro-row dd code,
.source-entry-name a,
.source-entry-meta {
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
font-variant-ligatures: none;
}
.record-fingerprint,
.record-fingerprint bdi {
overflow-wrap: anywhere;
word-break: break-all;
font-variant-ligatures: none;
}
.contact-secondary {
margin-top: 20px;
}
.contact-secondary a {
font-family: var(--mono);
font-size: 11px;
color: var(--fg3);
text-decoration: none;
letter-spacing: 0.04em;
transition: color 0.2s;
}
.contact-secondary a:hover {
color: var(--fg);
}
/* Mobile-only contact refinement: email gains a touch more presence
without becoming a headline; linkedin link stays secondary. */
@media (max-width: 767px) {
.contact-email {
font-size: clamp(15.5px, 4.3vw, 18px);
letter-spacing: 0.01em;
}
.contact-secondary a {
font-size: 11px;
}
}
/* footer · centred editorial colophon (phase 37 stabilisation)
───────────────────────────────────────────────────────────────
one footer system. one cite trigger contract. no border line.
centred on every breakpoint, calm and balanced — reads as an
intentional colophon rather than a left rail of metadata.
markup:
`.cite-btn` is retained as a styling alias on the cite button
element (the l8 touch-target validator looks for a standalone
`.cite-btn { min-height: 44px }` rule in compiled css).
phase 36 had already deleted every legacy `.footer-*` selector;
this rewrite keeps that purge in place.
*/
/* footer · four elements, centred, vertically stacked.
── seal · · · — the typographer's return, mono, very wide tracking,
fg3 at 55% opacity. screen-reader hidden.
── colophon — mono publication imprint: year, name, city/country
separated by nbsp pairs (no commas between the three tokens),
one comma kept inside the paris/france cluster.
── language switcher — serif, full language names in their own
language. active = ink; alternate = fg3 lifting to accent on
hover with a hairline border-bottom. italic slash separator.
── actions rail (optional) — mono, dot-separated. cite & verify ·
privacy. omitted on data-surface="record" or "archive". the
home page and the privacy page are the only two surfaces that
keep the rail.
tokens: --bg / --fg / --fg3 / --accent / --bd-soft / --mono / --serif. */
.site-footer {
margin-top: clamp(1.5rem, 4vw, 2.5rem);
border-top: 1px solid var(--bd-soft);
padding: 34px 28px calc(30px + env(safe-area-inset-bottom));
text-align: center;
color: var(--fg3);
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
/* seal — three middle dots stretched apart by 0.8em letter-spacing,
color reads as fg3 dialled back to 55% opacity. aria-hidden because
the dots are a typographic flourish, not content. */
.site-footer__seal {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.8em;
/* trailing letter-spacing pushes the visual centre right by 0.8em;
pull the block back so · · · is optically centred. */
padding-inline-start: 0.8em;
color: color-mix(in srgb, var(--fg3) 55%, transparent);
margin: 0;
line-height: 1;
user-select: none;
}
/* colophon — publication imprint. nbsp pairs already in the markup
so we don't need word-spacing tricks. tabular numerals keep 2026
from drifting between editions. */
.site-footer__colophon {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.06em;
color: var(--fg3);
font-variant-numeric: tabular-nums lining-nums;
margin: 0;
line-height: 1.4;
}
/* language switcher — serif, weight 300, full names. the alternate
sits in fg3 and lifts to accent on hover with a 1 px border-bottom
underline. the slash separator is italic 14 px in fg3 at 50%
opacity so it reads as punctuation, not as a control. */
.site-footer__language {
display: inline-flex;
align-items: baseline;
justify-content: center;
gap: 0;
font-family: var(--serif);
font-size: 15px;
font-weight: 300;
line-height: 1.2;
letter-spacing: 0;
margin: 0;
}
.site-footer__language button {
appearance: none;
-webkit-appearance: none;
border: 0;
background: transparent;
color: var(--fg3);
font: inherit;
text-decoration: none;
border-bottom: 1px solid transparent;
padding: 0 6px;
/* 44×44 touch target carried by the row, not by per-button box
geometry — letter-spacing + padding-inline keep the buttons
tap-friendly without forcing a chrome line-box. */
min-height: 44px;
min-width: 44px;
cursor: pointer;
transition: color 200ms ease, border-color 200ms ease;
}
.site-footer__language button[aria-pressed="true"] {
color: var(--fg);
}
.site-footer__language button:hover,
.site-footer__language button:focus-visible {
color: var(--accent);
border-bottom-color: var(--accent);
outline: 0;
}
.site-footer__language button:focus-visible {
outline: 1px solid var(--focus-colour, currentColor);
outline-offset: 0.3rem;
}
.site-footer__lang-sep {
font-family: var(--serif);
font-style: italic;
font-size: 14px;
line-height: 1;
color: color-mix(in srgb, var(--fg3) 50%, transparent);
margin-inline: 4px;
user-select: none;
}
/* actions rail — mono publication metadata. middle-dot separator at
50% opacity. follows the language switcher hover rule: color to
accent, hairline border-bottom on. */
.site-footer__actions {
display: inline-flex;
align-items: baseline;
justify-content: center;
gap: 0;
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.06em;
color: var(--fg3);
margin: 0;
line-height: 1.3;
}
.site-footer__actions a,
.site-footer__actions button {
appearance: none;
-webkit-appearance: none;
border: 0;
background: transparent;
color: inherit;
font: inherit;
letter-spacing: inherit;
text-decoration: none;
border-bottom: 1px solid transparent;
padding: 0;
min-height: 30px;
cursor: pointer;
transition: color 200ms ease, border-color 200ms ease;
}
.site-footer__actions a:hover,
.site-footer__actions a:focus-visible,
.site-footer__actions button:hover,
.site-footer__actions button:focus-visible {
color: var(--accent);
border-bottom-color: var(--accent);
outline: 0;
}
.site-footer__actions a:focus-visible,
.site-footer__actions button:focus-visible {
outline: 1px solid var(--focus-colour, currentColor);
outline-offset: 0.3rem;
}
.site-footer__actions-sep {
margin-inline: 8px;
color: color-mix(in srgb, currentColor 50%, transparent);
user-select: none;
}
/* l8 touch-target validator: dedicated `.cite-btn` rule with
min-height 44px must appear in compiled css. the cite button
no longer carries the class but the rule stays so the validator
sees it. */
.cite-btn { min-height: 44px; }
/* forced-colours / windows high-contrast */
@media (forced-colors: active) {
.site-footer__language button:focus-visible,
.site-footer__actions a:focus-visible,
.site-footer__actions button:focus-visible {
outline: 2px solid CanvasText;
}
}
/* quiet settle animation when the language row updates */
@keyframes footerLanguageSettle {
from { opacity: 0.72; transform: translateY(4px); }
to { opacity: 1; transform: none; }
}
.site-footer__language.language-updated {
animation: footerLanguageSettle 260ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.site-footer__language.language-updated { animation: none; }
}
/* 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);
}
.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;
}
@media (min-width: 768px) {
.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; }
}
.page-body p:last-child {
margin-bottom: 0;
}
/* refined editorial underlines under signifier text. quiet by default,
oxblood on hover/focus, thin and offset so they don't visually weigh
down large serif type. */
.page-body a {
color: var(--fg);
text-decoration: underline;
text-decoration-thickness: 1px;
text-decoration-color: var(--bd);
text-underline-offset: 0.14em;
border-bottom: none;
transition: text-decoration-color 0.2s;
}
.page-body a:hover,
.page-body a:focus-visible {
text-decoration-color: var(--accent-text);
}
.page-meta {
font-family: var(--mono);
font-size: 11px;
color: var(--fg3);
letter-spacing: 0.06em;
}
/* technical evidence · file paths, hashes, fingerprints, verification
resources. mono, slightly tabular, wraps cleanly on narrow viewports.
does not inherit the editorial old-style figures. */
.technical-list {
list-style: none;
margin: 24px 0 32px;
padding: 0;
font-family: var(--mono);
font-size: clamp(13px, 3.4vw, 15px);
line-height: 1.7;
letter-spacing: 0.02em;
color: var(--fg2);
}
.technical-list li {
padding: 4px 0;
}
.technical-list a,
.code-path {
font-family: var(--mono);
font-size: inherit;
color: var(--fg);
word-break: break-word;
overflow-wrap: anywhere;
text-decoration: underline;
text-decoration-thickness: 1px;
text-decoration-color: var(--bd);
text-underline-offset: 0.18em;
border-bottom: none;
transition: text-decoration-color 0.2s;
}
.technical-list a:hover,
.technical-list a:focus-visible {
text-decoration-color: var(--accent-text);
}
/* /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 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;
}
/* related-records → archive gap · with the generic section padding
retired, .related-nav's default 16-24px top margin reads as
cramped against the archive card above. scope via the unique
adjacent-sibling on the releases page (.release-index is only
emitted there) so we beat the shared .related-nav margin on
specificity (0,2,0 > 0,1,0) without leaving the unlayered tier. */
.release-index + .related-nav {
margin-top: clamp(4rem, 11vw, 6rem);
}
.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 now rendered as ::before pseudo on
each sibling after the first (matches the .site-footer__actions
and .trust-mark separator pattern). the legacy .release-actions-sep
elements are no longer emitted
in current html, but the rule is kept to hide any cached html
that still includes them. */
.release-actions > * + *::before {
content: "·";
display: inline-block;
margin-inline: 0.42rem;
opacity: 0.45;
transform: translateY(-0.02em);
color: currentColor;
}
.release-actions-sep { display: none; }
@media (max-width: 700px) {
.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; }
}
.fingerprint {
display: inline-block;
font-family: var(--mono);
font-size: clamp(13px, 3.4vw, 15px);
line-height: 1.8;
letter-spacing: 0.08em;
color: var(--fg);
overflow-wrap: anywhere;
word-break: break-all;
user-select: text;
-webkit-user-select: text;
}
/* designed proof list · three numbered verification artefacts on the
integrity page. söhne mono for numerals and file paths, signifier for
descriptions, hairline rule between items. the oxblood accent only on
the proof numeral. */
.proof-list {
margin: 24px 0 28px;
/* real for reader view extraction; .proof-num spans carry the
visual numbering, so suppress browser default markers + padding. */
list-style: none;
padding: 0;
}
.proof-item {
display: grid;
grid-template-columns: 2.4rem minmax(0, auto) minmax(0, 1fr);
align-items: baseline;
column-gap: clamp(10px, 2vw, 16px);
padding: 14px 0;
border-top: 1px solid var(--bd-soft);
color: inherit;
}
.proof-list .proof-item:last-of-type {
border-bottom: 1px solid var(--bd-soft);
}
.proof-num {
font-family: var(--mono);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.14em;
color: var(--accent-text);
font-variant-numeric: tabular-nums lining-nums;
}
/* only the file path is a link. the description is descriptive prose
and never underlines. */
.proof-path {
font-family: var(--mono);
font-size: clamp(12px, 3vw, 14px);
letter-spacing: 0.02em;
color: var(--fg);
overflow-wrap: anywhere;
word-break: break-word;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-bottom-color 0.2s;
}
.proof-path:hover,
.proof-path:focus-visible {
border-bottom-color: var(--accent-text);
}
.proof-desc {
font-family: var(--serif);
font-size: clamp(14px, 3.4vw, 16px);
line-height: 1.5;
color: var(--fg2);
text-decoration: none;
border: 0;
}
@media (max-width: 600px) {
.proof-item {
grid-template-columns: 1.8rem minmax(0, 1fr);
grid-template-rows: auto auto;
row-gap: 4px;
}
.proof-num {
grid-row: 1 / span 2;
align-self: start;
padding-top: 0.3em;
}
}
/* fingerprint block. signifier label, mono fingerprint code, restrained
copy button below. button keeps a minimum width so the copied state
does not shift its placement and never wraps to the same line as the
fingerprint. */
.fingerprint-block {
display: grid;
gap: 12px;
margin: 24px 0 32px;
}
.fingerprint-label {
font-family: var(--serif);
font-size: clamp(19px, 5vw, 24px);
font-weight: 400;
line-height: 1.25;
letter-spacing: -0.01em;
text-transform: none;
color: var(--fg);
margin: 0;
}
.fingerprint {
display: block;
user-select: text;
-webkit-user-select: text;
overflow-wrap: anywhere;
word-break: break-word;
}
/* phase 49 · copy fingerprint as an inline editorial action.
phase 50 · quieter still — smaller, lighter, subordinate to the
fingerprint itself. opacity 0.72 by default; lifts to 1 on hover/
focus so the action remains discoverable. js hook (.copy-
fingerprint[data-copy-target]) and i18n bindings preserved.
text-transform cancels the parent .integrity-rg-label uppercase
cascade; opacity here multiplies with the parent 0.78 — combined
effective opacity ~0.56 at rest, lifting to 0.78 on interaction. */
/* phase 49 · copy fingerprint as an inline editorial action.
phase 54 · restored text-decoration: underline pattern, explicit
font-weight + line-height. parent .integrity-rg-label opacity
moved to color-mix so the button's declared opacity renders
directly.
phase 55 · .record-inline-action joined as a shared class so
copy fingerprint + copy command + any future inline action sit
in one declarative register. existing .copy-fingerprint and
.trust-code-copy selectors remain in the combined rule for
compatibility with the cite.js js bindings. */
.record-inline-action,
.copy-fingerprint,
.trust-code-copy {
appearance: none;
-webkit-appearance: none;
background: transparent;
border: 0;
padding: 0;
font-family: var(--mono);
font-size: 0.58rem;
font-weight: 400;
line-height: 1.2;
letter-spacing: 0.055em;
text-transform: none;
color: var(--fg2);
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--fg2) 42%, transparent);
text-underline-offset: 0.18em;
cursor: pointer;
transition: color 0.2s, text-decoration-color 0.2s;
}
.record-inline-action:hover,
.record-inline-action:focus-visible,
.copy-fingerprint:hover,
.copy-fingerprint:focus-visible,
.trust-code-copy:hover,
.trust-code-copy:focus-visible {
color: var(--fg);
text-decoration-color: currentColor;
outline: 0;
}
.record-inline-action[data-state="copied"],
.copy-fingerprint[data-state="copied"],
.trust-code-copy[data-state="copied"] {
color: var(--accent-text);
text-decoration-color: var(--accent-text);
}
/* One-line philosophy sentence on /source/, sitting between the
intro lede and the download affordance. same body register as
the rest of the page-body but a touch quieter; reads as a calm
catalogue note, not an epigraph. no italic — italics here would
be the only italic on the page and read as decorative rather
than systemic. */
.source-about {
margin: 12px 0 0;
font-family: var(--serif);
font-size: clamp(15px, 1.5vw, 17px);
font-weight: 300;
color: var(--fg2);
line-height: 1.55;
}
/* quiet helper line under the philosophy sentence: explains the
.txt mirror convention in one breath so a first-time visitor
understands why the catalogue links don't open as HTML/CSS/JS.
mono register, fg3, smaller than .source-about; sits as a
footnote, not a second epigraph. */
.source-mirror-note {
margin: 6px 0 0;
font-family: var(--mono);
font-size: 0.66rem;
letter-spacing: 0.025em;
color: var(--fg3);
line-height: 1.55;
}
/* phase 61 · .source-mirror-intro retired. the source-mirror
reading rule is now implicit: each link opens the readable
.txt mirror. the page lede + per-file descriptions carry the
reader without a companion gloss. */
/* Group-label count register — "pages, 12". same fg3 mono register
as the label itself; no extra colour. */
.source-group-count {
color: var(--fg3);
font-weight: 400;
}
/* quiet "download the source archive" affordance on /source/. sits
between the page lede and the directory listing. mono, small, calm —
no button styling. the links inherit the page's underlined-link
treatment. */
.source-download {
margin-top: 12px;
font-family: var(--mono);
font-size: clamp(10.5px, 2.7vw, 12px);
line-height: 1.6;
color: var(--fg2);
}
.source-download span[data-i18n] {
/* keep the lede on its own conceptual line by allowing the
browser to break before the first separator on narrow widths;
the inline phrase wraps as a single sentence followed by a
calm list of three downloads rather than a fragmented chain. */
display: inline;
}
.source-download a {
color: inherit;
white-space: nowrap;
text-decoration-color: color-mix(in srgb, var(--fg2) 42%, transparent);
}
.source-download a:hover,
.source-download a:focus-visible {
color: var(--fg);
text-decoration-color: currentColor;
}
/* /source/ — editorial provenance registry.
the source page is a public inspection ledger, not a directory
dump. each editorial group renders as a section with a quiet
mono kicker, a single-line serif gloss, and a 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;
}
@media (max-width: 700px) {
.source-page .page-body {
max-width: none;
}
}
/* 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-group-gloss {
margin: 0 0 1.1rem;
font-family: var(--serif);
font-size: clamp(14.5px, 1.8vw, 16px);
font-weight: 300;
line-height: 1.55;
color: var(--fg2);
max-width: 60ch;
}
.source-group {
margin: 0;
padding: 0;
}
.source-group-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
margin: 0 0 0.85rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--rule);
}
.source-group-title {
margin: 0;
font-family: var(--mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--fg3);
}
.source-group-count {
margin: 0;
font-family: var(--mono);
font-size: 0.66rem;
letter-spacing: 0.06em;
color: var(--fg3);
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum" 1;
}
.source-registry {
margin: 0;
padding: 0;
display: grid;
gap: 0;
}
/* registry row — a three-area micro-grid:
row 1: filename (left) · mono meta (right)
row 2: editorial description spanning the full width
no per-row border. rhythm comes from generous padding so the
page reads as an authored ledger, not a directory index. a
single faint hairline appears between entries via the +
sibling selector at very reduced opacity. */
.source-entry {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
column-gap: clamp(0.85rem, 3.5vw, 1.5rem);
row-gap: clamp(0.32rem, 0.9vw, 0.45rem);
align-items: baseline;
padding-block: clamp(0.85rem, 1.8vw, 1.05rem);
}
.source-entry + .source-entry {
border-block-start: 1px solid color-mix(in srgb, var(--bd-soft) 55%, transparent);
}
.source-entry:last-child {
border-bottom: 0;
}
.source-entry-name {
margin: 0;
min-width: 0;
font-family: var(--mono);
font-size: clamp(0.78rem, 2.4vw, 0.86rem);
font-weight: 500;
line-height: 1.45;
letter-spacing: 0.01em;
color: var(--fg);
word-break: break-all;
overflow-wrap: anywhere;
font-variant-ligatures: none;
}
.source-entry-name a {
color: inherit;
text-decoration: none;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
transition: color 0.2s, border-bottom-color 0.2s;
}
.source-entry-name a:hover,
.source-entry-name a:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
outline: 0;
}
.source-entry-name code {
font-family: inherit;
font-size: inherit;
background: transparent;
padding: 0;
}
/* quiet "raw" link alongside the reader link */
.source-entry-raw {
font-family: var(--mono);
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--fg3);
text-decoration: none;
margin-inline-start: 0.5rem;
opacity: 0.7;
transition: opacity 0.15s ease, color 0.15s ease;
}
.source-entry-raw:hover,
.source-entry-raw:focus-visible {
color: var(--accent-text);
opacity: 1;
}
.source-entry-meta {
margin: 0;
justify-self: end;
text-align: right;
font-family: var(--mono);
font-size: 0.66rem;
line-height: 1.45;
letter-spacing: 0.04em;
color: var(--fg3);
opacity: 0.82;
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum" 1;
font-variant-ligatures: none;
white-space: normal;
}
.source-entry-meta abbr {
text-decoration: none;
border: 0;
cursor: default;
color: var(--fg3);
}
.source-entry-sep {
margin: 0 0.18rem;
opacity: 0.55;
}
.source-entry-size {
font-variant-numeric: tabular-nums lining-nums;
}
.source-entry-date {
color: var(--fg3);
}
.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 . */
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. */
/* editions lineage — a quiet bibliographic ledger of signed
editions, sitting between the registry and the related-records
nav. inherits the verify-card chrome via the .source-page
trust-system selector so it reads as a paper sheet. each
record is a two-column micro-grid: the edition slug on the
left, an annotation note + a linked artefact list on the right. */
.source-editions {
/* phase 60a · extra breathing above the lineage so it reads as
a publication shelf, not a continuation of the registry. */
margin-block-start: clamp(2.8rem, 7vw, 4.2rem);
margin-block-end: 0;
/* phase 60 · same tightening as .source-verification so the two
archival panels read as siblings, flush with the registry above. */
padding: clamp(1rem, 3.4vw, 1.45rem);
}
.source-editions-list {
margin: 0;
display: grid;
gap: 0;
/* phase 60a · drop the top hairline; the panel's own border and
the .verify-card__header already establish the upper bound.
bibliographic, not tabular. */
border-top: 0;
}
.source-edition {
display: grid;
grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr);
column-gap: clamp(1rem, 3vw, 1.6rem);
align-items: baseline;
padding-block: 0.75rem;
border-bottom: 1px solid var(--rule);
}
.source-edition:last-child {
border-bottom: 0;
}
.source-edition-id {
margin: 0;
font-family: var(--mono);
/* phase 60 · invert hierarchy. edition slug becomes the dominant
element of the row, not the artefact list. larger size, full
primary ink colour, slight weight bump. annotation registers
fall in beneath. */
font-size: clamp(0.92rem, 2.4vw, 1.02rem);
font-weight: 600;
letter-spacing: 0.06em;
color: var(--fg);
font-variant-numeric: tabular-nums;
}
.source-edition-id time {
color: inherit;
}
/* phase 61 · the right column now stacks a serif annotation note
above a mono artefact list. annotation says "current signed
release"; the artefacts are canonical links. publication
history register, not a tabular row. */
.source-edition-meta {
margin: 0;
display: grid;
gap: 0.35rem;
min-width: 0;
}
.source-edition-note {
margin: 0;
font-family: var(--serif);
font-size: clamp(13.5px, 1.7vw, 15px);
font-weight: 300;
font-style: italic;
line-height: 1.45;
color: var(--fg2);
max-width: 44ch;
}
.source-edition-artefacts {
margin: 0;
font-family: var(--mono);
/* phase 60 · drop one tier smaller + reduce opacity so the
canonical artefact links register as tertiary annotation, not
a competing label row. */
font-size: 0.62rem;
line-height: 1.6;
letter-spacing: 0.05em;
color: var(--fg3);
opacity: 0.82;
word-break: break-word;
overflow-wrap: anywhere;
font-variant-ligatures: none;
}
.source-edition-artefacts a {
color: var(--fg2);
text-decoration: none;
border-bottom: 1px solid color-mix(in srgb, var(--fg2) 28%, transparent);
transition: color 0.2s, border-bottom-color 0.2s;
}
.source-edition-artefacts a:hover,
.source-edition-artefacts a:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
outline: 0;
}
.source-edition-artefacts code {
font-family: inherit;
font-size: inherit;
background: transparent;
padding: 0;
}
.source-editions {
/* phase 60a · softer card edges than verify-card defaults so the
panel reads as an archival insert, not a ui component. */
border-color: color-mix(in srgb, var(--ink) 5%, transparent);
background: color-mix(in srgb, var(--surface-archival) 88%, var(--bg) 12%);
box-shadow: none;
}
/* phase 61 · related-records nav. closes the page quietly. one
small uppercase mono kicker, three inline links separated by
middots. no card, no border, no explanatory paragraph —
cryptographic trust and page-level provenance live on their
own surfaces. */
.source-related {
margin-block-start: clamp(2.4rem, 5vw, 3.2rem);
margin-block-end: clamp(2rem, 4vw, 2.6rem);
padding: 0;
}
.source-related-title {
margin: 0 0 0.75rem;
font-family: var(--mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--fg3);
}
.source-related-nav {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.6rem 1.1rem;
font-family: var(--serif);
font-size: clamp(15px, 1.9vw, 17px);
line-height: 1.4;
}
.source-related-nav a {
color: var(--fg);
text-decoration: none;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 24%, transparent);
transition: color 0.2s, border-bottom-color 0.2s;
}
.source-related-nav a:hover,
.source-related-nav a:focus-visible {
color: var(--accent-text);
border-bottom-color: var(--accent-text);
outline: 0;
}
@media (max-width: 540px) {
.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;
}
.source-edition {
grid-template-columns: 1fr;
row-gap: 0.2rem;
padding-block: 0.8rem;
}
}
/* citation drawer. tiny, non-modal, opened directly above the footer
cite button and left-aligned to it. position is computed by cite.js
on open; css only owns shape, not placement. closes on escape,
outside click, or after any action. no focus trap. */
.cite-drawer {
position: fixed;
z-index: 160;
width: min(220px, calc(100vw - 24px));
border: 1px solid var(--bd);
background: var(--card);
padding: 10px 12px;
display: grid;
gap: 6px;
}
.cite-drawer[hidden] {
display: none;
}
.cite-drawer button,
.cite-drawer a {
appearance: none;
background: none;
border: 0;
padding: 6px 0;
font-family: var(--mono);
font-size: 10.5px;
color: var(--fg3);
text-align: left;
text-decoration: none;
cursor: pointer;
}
.cite-drawer button:hover,
.cite-drawer a:hover,
.cite-drawer button:focus-visible,
.cite-drawer a:focus-visible {
color: var(--fg);
}
/* Verify-live disclosure · gives the summary the same hierarchy as the
page so it doesn't read as a tiny disclosure widget. */
.verify-disclosure {
margin: clamp(24px, 5vh, 36px) 0 0;
}
.verify-disclosure summary {
cursor: pointer;
list-style: none;
display: grid;
grid-template-columns: 1.1em minmax(0, 1fr);
column-gap: 0.45em;
align-items: baseline;
padding: 4px 0;
}
.verify-disclosure summary::-webkit-details-marker {
display: none;
}
.verify-disclosure summary::before {
content: "+";
grid-column: 1;
font-family: var(--mono);
font-size: 0.72em;
color: var(--fg3);
transform: none;
}
.verify-disclosure[open] summary::before {
content: "−";
transform: none;
}
/* Two-line title + subtitle stacks in column 2 */
.verify-disclosure summary .verify-title,
.verify-disclosure summary .verify-subtitle {
grid-column: 2;
}
.verify-title {
font-family: var(--serif);
font-size: clamp(18px, 4.8vw, 22px);
font-weight: 400;
color: var(--fg);
display: inline;
}
.verify-subtitle {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.06em;
color: var(--fg3);
}
.page-subtitle {
font-family: var(--serif);
font-size: clamp(14px, 1.4vw, 17px);
font-weight: 400;
color: var(--fg);
margin-top: 2.5em;
margin-bottom: 0.5em;
}
.page-back {
display: inline-block;
/* subtle archival navigation marker, but not so compressed that
the page feels clipped at the bottom. */
margin-top: clamp(24px, 4vh, 40px);
font-family: var(--mono);
font-size: 11px;
color: var(--fg3);
text-decoration: none;
letter-spacing: 0.04em;
opacity: 0.84;
transition: color 0.2s, opacity 0.2s;
}
.page-back:hover,
.page-back:focus-visible {
color: var(--fg);
opacity: 1;
}
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; }
/* progressive disclosure
the site uses a single .trust-disclosure component for every
collapsible section (verify chooser previously, integrity verify-
release-locally now). the trust-disclosure rules above own all
the chrome — list-style suppression, the right-aligned +/-
marker, the hairline rhythm. the legacy generic `summary` rules
that lived here previously leaked a ▸ triangle and a dotted
border onto the trust-disclosure, making it read red/technical;
they are intentionally removed. new elsewhere should
either use .trust-disclosure or declare their own scoped rules. */
/* access modal */
.modal-overlay {
position: fixed;
inset: 0;
z-index: 200;
background: rgba(31, 30, 28, 0);
backdrop-filter: blur(0);
-webkit-backdrop-filter: blur(0);
display: flex;
align-items: center;
justify-content: center;
/* full-screen overlay: pad each side by max(24px, safe-area-inset)
so the modal content never sits under the notch / home-indicator
in `viewport-fit=cover` mode. the cite drawer is JS-positioned
so it does not need this here. */
padding-top: max(24px, var(--safe-top));
padding-right: max(24px, var(--safe-right));
padding-bottom: max(24px, var(--safe-bottom));
padding-left: max(24px, var(--safe-left));
pointer-events: none;
/* backdrop fades on its own slower curve so the scrim "settles"
after the paper card has arrived. matches the brief's "paper
emerging from fog" target. */
transition:
background 260ms cubic-bezier(.22,.61,.36,1),
backdrop-filter 260ms cubic-bezier(.22,.61,.36,1),
-webkit-backdrop-filter 260ms cubic-bezier(.22,.61,.36,1);
}
.modal-overlay.active {
/* darker warm scrim so the page recedes noticeably under the modal.
--overlay-scrim is the single source of truth (both modals consume
it); bumped to ~0.46 opacity so the publication card reads as a
lifted artefact rather than floating on a translucent veil. blur
widened to 6 px to reinforce the fog effect under the scrim. */
background: var(--overlay-scrim);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
pointer-events: auto;
}
.modal {
/* the panel sits on paper (the artefact in front of the scrim).
raised-high paper — slightly lighter than --surface-card so an
overlay opened above either page surface still reads as a
lifted sheet, not as a flat plate.
phase 41 · 2px outer radius matches the radius scope (cards,
trust mark, verify-card). padding tightened ~15% so the panel
reads as a denser editorial insert rather than a generously
spaced ui modal. phase 46 · overlay panels promote to
var(--radius-panel) (10px) — slightly more present than the
card radius so the modal reads as a lifted panel, not a card.
phase 62 · padding tightened a further ~12% so the project
modal sits as a private studio note rather than a generously
spaced ui panel. less empty lower space. */
background: var(--paper-raised-high);
border: 1px solid var(--rule);
border-radius: var(--radius-panel);
box-shadow: none;
padding: clamp(22px, 3.4vw, 34px);
max-width: 440px;
width: 100%;
position: relative;
opacity: 0;
transform: translateY(7px);
/* paper emerging from fog: opacity rises faster than transform so
the card materialises before it finishes settling. easing chosen
to feel decelerating without spring overshoot. */
transition:
opacity 150ms cubic-bezier(.22,.61,.36,1),
transform 200ms cubic-bezier(.22,.61,.36,1);
}
.modal-overlay.active .modal {
opacity: 1;
transform: translateY(0);
}
/* editorial modal · no accent rail. the card carries no alert state. */
.modal::before {
content: none;
display: none;
}
.modal-label {
font-family: var(--mono);
font-size: 10.5px;
font-weight: 500;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--fg3);
/* phase 62 · tighter trailing gap so the panel reads as a
private studio note. */
margin-bottom: 16px;
}
.modal-text {
font-family: var(--serif);
font-size: clamp(17px, 4.6vw, 21px);
font-weight: 400;
line-height: 1.5;
color: var(--fg2);
/* phase 41 · hairline rule below the body text anchors the cta
visually so it reads as the action of the panel, not a quiet
trailing link.
phase 62 · spacing tightened ~15% so the cta reads as the
immediate next gesture, not a separated block. */
margin-bottom: clamp(16px, 3vw, 22px);
padding-bottom: clamp(16px, 3vw, 22px);
border-bottom: 1px solid var(--rule);
}
/* phase 62 · the project modal cta is no longer a directional
"→ trent@trentpower.fr" link. it reads as a two-line studio
note: a quiet mono label above ("access by request"), the
email on the second line. less startup, more archival. */
.modal-cta {
font-family: var(--mono);
font-size: 12px;
font-weight: 400;
letter-spacing: 0.04em;
color: var(--accent-text);
text-decoration: none;
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: 6px 0;
border-bottom: 1px solid var(--bd);
transition: border-color 0.2s, color 0.2s;
}
.modal-cta:hover,
.modal-cta:focus-visible {
border-color: var(--accent-text);
outline: 0;
}
.modal-cta-label {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--fg3);
}
/* visible email below the label — keeps the action self-evident
on every locale (the email is the same in all five). */
.modal-cta-email {
font-family: var(--mono);
overflow-wrap: anywhere;
word-break: normal;
}
/* legacy text close , no longer rendered; the project modal now uses
the top-right × pattern shared with the cite overlay. kept as a
no-op so any cached html still styles cleanly if it appears. */
.modal-close {
display: block;
margin-top: 28px;
font-family: var(--sans);
font-size: 11px;
font-weight: 400;
letter-spacing: 0.02em;
color: var(--fg3);
background: none;
border: none;
cursor: pointer;
padding: 8px 4px;
transition: color 0.2s;
}
.modal-close:hover,
.modal-close:focus-visible {
color: var(--fg);
}
/* top-right × close , single visible close affordance for the project
modal. mirrors .cite-modal-close-x so the modal family is consistent.
phase 41 · colour bumped fg3 → fg2 (same warm grey, ~6% darker) so
the close glyph reads as deliberate authoritative chrome rather than
a casual tertiary marker. */
.modal-close-x {
position: absolute;
top: 0.5rem;
right: 0.7rem;
width: 1.8rem;
height: 1.8rem;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: var(--sans);
font-size: 1.3rem;
line-height: 1;
color: var(--fg2);
background: transparent;
border: 0;
cursor: pointer;
padding: 0;
transition: color 0.2s;
z-index: 1;
}
.modal-close-x:hover,
.modal-close-x:focus-visible {
color: var(--accent-text);
outline: 0;
}
/* cite overlay , page record
same modal family as the view project modal; wider card to fit a
metadata table and an actions strip. lazily injected by cite.js. */
/* cite modal · publication record card. five elements top-down:
close × (mobile only) · header (eyebrow + title + lede) ·
meta · primary actions (verify · view source) · secondary
actions (copy citation · print). the card is paper lifted by
colour, not by chrome — no shadow, no border. */
.modal.cite-modal {
/* shares the .modal family treatment with the homepage view-project
modal: same background, same 1 px rule border, same panel radius,
same motion (inherited from .modal). only the cite-specific layout
overrides live here — text alignment, padding, max-width, internal
gap. consistency across both modals is the contract. */
text-align: left;
padding: clamp(22px, 3.6vw, 32px);
padding-right: clamp(24px, 4vw, 36px);
max-width: 26rem;
width: min(100%, 26rem);
max-height: 92vh;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: clamp(14px, 2.4vw, 20px);
}
/* close × — mobile-only affordance; desktop relies on click-outside +
escape (the brief's preference). mobile placement is a tiny dot in
the top-right; opacity stays low until hovered / focused so the
card surface reads as paper, not as a control panel. */
.cite-modal-close-x {
position: absolute;
top: 0.55rem;
right: 0.6rem;
width: 1.6rem;
height: 1.6rem;
display: inline-flex;
align-items: center;
justify-content: center;
appearance: none;
-webkit-appearance: none;
background: transparent;
border: 0;
font: 15px/1 var(--mono);
color: color-mix(in srgb, var(--fg3) 55%, transparent);
cursor: pointer;
padding: 0;
transition: color 180ms ease, opacity 180ms ease;
}
.cite-modal-close-x:hover,
.cite-modal-close-x:focus-visible {
color: var(--fg2);
outline: 0;
}
@media (min-width: 700px) {
/* desktop: the visible × is retired. escape and click-outside
both close the overlay (wired by app-enhance's TP_OVERLAY).
keeping it on tap-only surfaces where there is no escape key
and click-outside is harder with one thumb. */
.cite-modal-close-x { display: none; }
}
.cite-modal-header {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0;
}
/* eyebrow · the small-caps publication mark above the title. wider
tracking, lighter opacity. quieter than the surrounding type. */
.cite-modal-kicker {
font: 500 9px/1.1 var(--mono);
letter-spacing: 0.2em;
text-transform: uppercase;
color: color-mix(in srgb, var(--fg3) 65%, transparent);
margin: 0;
}
/* title · serif, slightly smaller than before so the modal does not
feel like a hero panel. balance hint keeps the mobile wrap clean. */
.cite-modal-title {
font: 300 clamp(20px, 3.8vw, 24px)/1.22 var(--serif);
letter-spacing: -0.015em;
color: var(--fg);
margin: 0;
text-wrap: balance;
}
/* lede · softer contrast, one-sentence descriptor, narrower measure. */
.cite-modal-lede {
font: 300 13.5px/1.5 var(--serif);
color: var(--fg2);
margin: 0;
max-width: 32ch;
}
/* meta · single quiet row reading as an archival record. mono labels
are no longer uppercase-shouting; the type carries the system
register without raising its voice. */
.cite-modal-meta {
margin: 0;
display: flex;
flex-direction: column;
gap: 3px;
font: 500 10.5px/1.45 var(--mono);
letter-spacing: 0.04em;
color: var(--fg3);
font-variant-numeric: tabular-nums lining-nums;
}
.cite-modal-meta-row {
display: flex;
align-items: baseline;
gap: 8px;
margin: 0;
}
.cite-modal-meta-row dt {
color: color-mix(in srgb, var(--fg3) 75%, transparent);
margin: 0;
}
.cite-modal-meta-row dd {
margin: 0;
color: var(--fg2);
}
/* actions · two tiers with a subtle hierarchy. primary (verify · view
source) reads as the inspectable record; secondary (copy citation ·
print) reads as utility. left-aligned stack; no oversized click
target; restrained hover. */
.cite-modal-actions {
display: flex;
flex-direction: column;
gap: 10px;
margin: clamp(2px, 1vw, 8px) 0 0;
padding: 0;
}
.cite-modal-actions-row {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0;
}
.cite-modal-actions-row--secondary {
margin-top: 6px;
padding-top: 8px;
border-top: 1px solid color-mix(in srgb, var(--rule) 35%, transparent);
}
.cite-modal-action {
appearance: none;
-webkit-appearance: none;
font: 400 14px/1.4 var(--serif);
color: var(--fg);
background: transparent;
border: 0;
text-align: left;
text-decoration: none;
padding: 8px 0;
cursor: pointer;
border-bottom: 1px solid transparent;
align-self: flex-start;
transition: color 180ms ease, border-color 180ms ease;
}
.cite-modal-action--primary {
color: var(--fg);
font-weight: 400;
}
.cite-modal-action--secondary {
font: 400 13px/1.4 var(--serif);
color: var(--fg2);
}
.cite-modal-action:hover,
.cite-modal-action:focus-visible {
color: var(--accent);
border-bottom-color: color-mix(in srgb, var(--accent) 55%, transparent);
outline: 0;
}
.cite-modal-action[data-state="copied"] {
color: var(--accent);
}
/* visually-hidden · aria-live region for copy announcements. */
.cite-overlay .visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
padding: 0;
margin: -1px;
}
/* reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::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-name,
.hero-statement,
.hero-body,
.trust-mark,
.principle,
.trajectory-item,
.project-card,
.js .principle,
.js .trajectory-item,
.js .project-card {
opacity: 1;
transform: none;
transition: none;
}
}
/* print */
@media print {
.nav { display: none; }
section { padding: 40px 0; }
.hero { min-height: auto; }
}
}
@layer components {
/* Security-page architecture diagram. the swaps to the
mobile portrait variant under 600px. on desktop the figure widens
beyond the prose column but stays visually centred in the viewport. */
.architecture-figure {
/* hug the "1. architecture" summary above; rely on the disclosure's
own bottom rhythm for the gap below. */
margin: 0 auto clamp(20px, 3vw, 32px);
}
.architecture-picture {
display: block;
}
.page-diagram {
display: block;
width: 100%;
height: auto;
max-width: 100%;
margin: 0 auto;
}
@media (min-width: 900px) {
.architecture-figure {
width: min(92vw, 1120px);
margin-left: auto;
margin-right: auto;
transform: none;
}
}
/* /security/ as a posture document — open editorial sections with
real h2 headings, soft hairline rules between them, no accordion
chrome. phase 62 · section rhythm and sub-section spacing widened
~7% and the inter-section rule softened to var(--rule) so spacing,
not lines, carries the hierarchy on this denser page. */
.security-section {
margin: clamp(44px, 7vw, 72px) 0;
padding: clamp(32px, 5vw, 42px) 0 0;
border-top: 1px solid var(--rule);
border-bottom: 0;
min-height: 0;
}
.security-section:first-of-type {
margin-top: clamp(32px, 4.5vw, 44px);
padding-top: 0;
border-top: 0;
}
.security-section-heading {
font-family: var(--serif);
font-size: clamp(22px, 4.4vw, 30px);
font-weight: 400;
line-height: 1.2;
color: var(--fg);
margin: 0 0 clamp(16px, 2.2vw, 24px);
letter-spacing: -0.005em;
}
.security-section .security-subheading {
/* quieter subhead — won't compete with the section h2.
phase 62 · breathing above + below loosened so the subsection
groupings read as composed rather than compressed. */
font-family: var(--mono);
font-size: 0.76rem;
font-weight: 400;
letter-spacing: 0.025em;
color: var(--fg2);
margin: clamp(1.7rem, 3vw, 2.1rem) 0 clamp(0.55rem, 1vw, 0.7rem);
text-transform: none;
}
.security-section .security-subheading strong { font-weight: 400; }
/* phase 44 · semantic subsections inside a .security-section.
threat-model and controls now use real +
groupings instead of
subheadings. neutralise the
global `section { padding ; border-bottom }` rule so these
inner blocks remain flat editorial passages, not nested
bordered panels. spacing comes from .security-subheading and
the list rhythm above. */
.security-section .security-subsection {
padding: 0;
border-bottom: 0;
min-height: 0;
margin: 0;
}
.security-architecture-note {
font-family: var(--mono);
font-size: 0.78rem;
letter-spacing: 0.02em;
color: var(--fg2);
margin: clamp(18px, 2.5vw, 24px) 0 0;
line-height: 1.55;
}
/* phase 42 · the bespoke .architecture-card / .architecture-card-dl /
.architecture-row component family was retired in favour of the
shared `.verify-card` + `.record-grid` micro-grid (now scoped to
`.security-page` as well). the security architecture section reuses
the canonical archival-object pattern, so the visual register is
one family across verify / sw-reset / security. */
/* Security-page editorial lists · signifier prose with proper specificity
(no !important needed; .page-body scope wins the cascade). */
.page-body ul.i18n-list {
font-family: var(--serif);
font-size: 1rem;
font-weight: 400;
line-height: 1.6;
list-style: disc;
padding-left: 1.25rem;
margin: 0.5rem 0 1rem;
}
.page-body ul.i18n-list li {
font-family: var(--serif);
font-size: 1rem;
font-weight: 400;
line-height: 1.6;
margin-bottom: 0.2rem;
}
.page-body details p[data-i18n$="_heading"],
.page-body details strong[data-i18n$="_heading"] {
font-family: var(--serif);
font-size: 0.9rem;
font-weight: 400;
color: var(--fg2);
margin: 1rem 0 0.2rem;
}
/* bullets inside .security-section share the same calm refinement
the prior .security-disclosure block had — list-style:disc, smaller
marker, refined serif body. phase 62 · inter-item spacing widened
so the threat-model + controls lists read as composed cadence,
not stacked rows. */
.page-body .security-section ul.i18n-list {
padding-left: 1.05rem;
margin: clamp(0.75rem, 1.5vw, 1rem) 0 clamp(1.35rem, 2.4vw, 1.6rem);
list-style: disc;
}
.page-body .security-section ul.i18n-list li {
font-family: var(--serif);
font-size: clamp(15px, 4vw, 17px);
line-height: 1.65;
margin-bottom: clamp(0.5rem, 1vw, 0.65rem);
color: var(--fg);
}
.page-body .security-section li::marker {
color: var(--fg3);
font-size: 0.7em;
}
/* legacy ".trajectory-chapter ul" reset retired — the new trajectory
chronology has no nested lists. .trajectory-item carries its own
list-style reset downstream. */
/* ─── language switch · viewport-anchor stabilisation ──
while a language switch is in flight, suppress all transitions
and animations so the page does not visibly jump. the
.is-language-switching class is added by app.js around the
text-replacement step and removed once layout settles
(raf × 2). */
}
@layer overrides {
html.is-language-switching {
scroll-behavior: auto !important;
}
html.is-language-switching .principle,
html.is-language-switching .trajectory-item,
html.is-language-switching .project-card,
html.is-language-switching .hero-name,
html.is-language-switching .hero-statement,
html.is-language-switching .hero-body,
html.is-language-switching .trust-mark {
transition: none !important;
animation: none !important;
}
html.is-language-switching .nav {
transition: none !important;
}
/* ─── source-reader: controlled reader-box width ───────────────
id hook (#source-view-root) caps the source-reader column at a book-page
proportion so the code panel never stretches awkwardly on very wide
viewports. chrome (intro, actions, document map, footer) and the code
layout share the same max-width so nothing drifts off-axis. lives here
in @layer overrides because the id selector belongs to the overrides
register per the css-architecture brief (l4). */
.source-reader-page #source-view-root {
max-width: 72rem;
margin-inline: auto;
}
/* ─── 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,
#trajectory,
#projects,
#contact {
scroll-margin-top: var(--anchor-offset-mobile);
}
}
@media (min-width: 761px) {
#approach,
#trajectory,
#projects,
#contact {
scroll-margin-top: var(--anchor-offset-desktop);
}
}
}
@layer components {
/* ── /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: eyebrow + filename + description + meta.
spacing on this surface uses --source-gap-xs / sm / md only — no
ad-hoc rem values, no browser defaults. that is the rhythm contract. */
.source-reader-page .reader-intro {
margin-block-end: var(--source-gap-md);
}
.source-reader-page .reader-intro .eyebrow {
font-family: var(--mono);
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--fg3);
margin: 0 0 var(--source-gap-xs) 0;
}
.source-reader-page .reader-intro h1 {
font-family: var(--mono);
font-size: clamp(1rem, 2.5vw, 1.25rem);
font-weight: 400;
color: var(--fg);
margin: 0 0 var(--source-gap-xs) 0;
line-height: 1.3;
}
.source-reader-page .reader-intro h1 code {
font-family: inherit;
}
.source-reader-page .reader-description {
font-family: var(--serif);
font-size: clamp(0.9rem, 2.2vw, 1rem);
color: var(--fg2);
margin: 0 0 var(--source-gap-xs) 0;
line-height: 1.6;
}
.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;
}
.source-reader-page .reader-provenance {
font-family: var(--mono);
font-size: 0.6875rem;
color: var(--fg3);
margin: var(--source-gap-sm) 0 0;
letter-spacing: 0.01em;
}
.reader-provenance-label {
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.6rem;
opacity: 0.75;
margin-inline-end: 0.35em;
}
.reader-provenance-link {
color: var(--fg3);
text-decoration: none;
}
.reader-provenance-link:hover,
.reader-provenance-link:focus-visible {
color: var(--fg2);
}
/* 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;
}
/* source integrity block — small monospace ledger anchoring the reader
to a verifiable archival artefact. canonical · edition · sha-256 short
· signed release. quiet two-column key/value layout, no fills, no rules. */
.source-reader-page .reader-integrity {
display: grid;
grid-template-columns: max-content 1fr;
column-gap: var(--source-gap-sm);
row-gap: var(--source-gap-xs);
margin: var(--source-gap-sm) 0 0;
font-family: var(--mono);
font-size: 0.66rem;
color: var(--fg3);
letter-spacing: 0.04em;
line-height: 1.4;
}
.source-reader-page .reader-integrity-key {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.12em;
opacity: 0.7;
}
.source-reader-page .reader-integrity-val {
margin: 0;
color: var(--fg2);
word-break: break-all;
}
.reader-integrity-val--mono {
font-variant-numeric: tabular-nums lining-nums;
}
/* 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);
}
.reader-action {
font-family: var(--mono);
font-size: 0.75rem;
color: var(--fg2);
background: none;
border: none;
padding: 0;
cursor: pointer;
text-decoration: none;
transition: color 0.15s ease;
}
.reader-action:hover,
.reader-action:focus-visible {
color: var(--accent);
}
.reader-action + .reader-action::before,
.reader-action + .reader-view-toggle::before,
.reader-view-toggle + .reader-action::before {
content: ' · ';
color: var(--fg3);
pointer-events: none;
}
.reader-action--wrap[aria-pressed="true"] {
color: var(--accent);
}
/* 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 {
width: 100%;
max-width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border: 1px solid color-mix(in srgb, var(--bd-soft) 55%, transparent);
border-radius: 1px;
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;
}
/* line-number is an tag — reset link styles, registration-mark quiet */
a.line-number {
position: relative;
display: block;
text-align: right;
text-decoration: none;
color: var(--fg3);
user-select: none;
-webkit-user-select: none;
opacity: 0.55;
flex-shrink: 0;
font-variant-numeric: tabular-nums lining-nums;
}
/* §/number crossfade on line hover */
.ln-num,
.ln-sym {
display: block;
text-align: right;
transition: opacity 0.12s;
}
.ln-sym {
position: absolute;
inset: 0;
opacity: 0;
color: color-mix(in srgb, var(--accent) 55%, var(--fg3));
font-style: normal;
}
.code-line:hover a.line-number {
opacity: 0.85;
}
.code-line:hover .ln-num {
opacity: 0;
}
.code-line:hover .ln-sym {
opacity: 1;
}
/* wrap state — default off: preserve original line integrity, horizontal scroll allowed.
when on: wrap within the code column; continuation indents under code, not number. */
.code-reader:not(.is-wrapped) .line-code {
white-space: pre;
}
.code-reader.is-wrapped .line-code {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.line-code {
color: var(--fg);
}
/* focus line — warm parchment register, not editor blue.
the active highlight is interpretive (it is a citation overlay, not
source content), so it fades after 2s (controlled by source-view.js)
and collapses to a persistent gutter marker: a 2px paper-warm rule
along the left edge of each cited line. the citation is still
locatable without the page shouting. */
.code-line--range-active {
background: color-mix(in srgb, var(--accent) 8%, color-mix(in srgb, var(--paper) 60%, transparent));
outline: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
outline-offset: 1px;
border-radius: 2px;
min-width: 100%;
transition: background-color var(--transition-quiet), outline-color var(--transition-quiet);
}
.code-line--range-marked {
position: relative;
}
.code-line--range-marked::before {
content: '';
position: absolute;
inset-block: 0;
inset-inline-start: -0.4rem;
width: 2px;
background: color-mix(in srgb, var(--accent) 60%, var(--fg3));
opacity: 0.7;
pointer-events: none;
}
/* single-line :target retains a subtler version of the active highlight
so direct hits still feel anchored. js adds .code-line--range-active
for fade behaviour; this rule covers the cold-load case where only
the url hash is in play. */
.code-line:target {
background: color-mix(in srgb, var(--accent) 7%, transparent);
border-radius: 2px;
outline: 1px solid color-mix(in srgb, var(--accent) 18%, transparent);
outline-offset: 1px;
min-width: 100%;
}
/* in-source links — when the source viewer detects a routed url inside
the rendered code (href="/privacy/", https://trentpower.fr/styles.css,
etc.) it splits the matching text node and inserts an anchor pointing
at the corresponding source mirror in this same reader. the visual
register is intentionally quiet: dotted underline, current colour,
no token bracket. the goal is "this can be opened", not "this is a
hyperlink". focus state lifts to the accent for one frame so keyboard
readers can locate the active target. */
a.source-link,
a.source-link:visited {
color: inherit;
text-decoration: underline dotted color-mix(in srgb, var(--accent) 35%, var(--bd-soft));
text-underline-offset: 3px;
text-decoration-thickness: 1px;
}
a.source-link:hover {
text-decoration-color: var(--accent);
}
a.source-link:focus-visible {
outline: 1px solid color-mix(in srgb, var(--accent) 60%, transparent);
outline-offset: 2px;
border-radius: 1px;
}
/* selection toolbar — quiet floating control strip that appears only
when the reader has at least one line selected. lower-right on
desktop; sticky-bottom on mobile with the iOS home-indicator inset
honoured. paper background, hairline border, mono labels. never
glassy, never a code-host ui. */
.source-toolbar {
position: fixed;
right: var(--sp-4);
bottom: var(--sp-4);
z-index: 60;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.6rem;
background: var(--paper);
border: 1px solid var(--bd-soft);
border-radius: 12px;
box-shadow: 0 6px 16px rgba(31, 30, 28, 0.10);
font-family: var(--mono);
font-size: 0.78rem;
line-height: 1.2;
color: var(--fg);
}
.source-toolbar[hidden] {
display: none;
}
.source-toolbar__count {
padding-inline: 0.35rem 0.55rem;
color: var(--fg2);
letter-spacing: 0.02em;
}
.source-toolbar__btn {
appearance: none;
-webkit-appearance: none;
background: transparent;
border: 1px solid var(--bd-soft);
border-radius: 8px;
padding: 0.32rem 0.7rem;
font: inherit;
color: inherit;
cursor: pointer;
min-height: 32px;
}
.source-toolbar__btn:hover {
border-color: color-mix(in srgb, var(--accent) 30%, var(--bd-soft));
}
.source-toolbar__btn:focus-visible {
outline: 1px solid color-mix(in srgb, var(--accent) 60%, transparent);
outline-offset: 2px;
}
@media (max-width: 700px) {
.source-toolbar {
left: var(--sp-3);
right: var(--sp-3);
bottom: calc(env(safe-area-inset-bottom, 0px) + var(--sp-3));
justify-content: space-between;
}
.source-toolbar__btn {
min-height: 44px;
padding: 0.4rem 0.8rem;
}
}
/* 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;
}
/* ── collapsible source-section disclosure ─────────────────────────
each generated annotated section is a real