/* =========================================================================
   Doois 2026
   Stage 1 global system + Stage 2/3 blocks.

   Written as plain CSS with custom properties. No build step: the outgoing
   theme shipped a .scss with a stale .css.map beside it and the two had
   drifted, which is a class of bug nobody notices until a colour is wrong in
   production.

   Order:
     01 tokens · 02 reset · 03 type · 04 layout · 05 surfaces
     06 primitives · 07 header/footer · 08 blocks · 09 motion · 10 a11y
   ========================================================================= */

/* ------------------------------------------------------------------ 00 faces */
/* PP Mori, self-hosted. Licensed webfont — replaces Instrument Sans, which was
   loaded from Google Fonts (see inc/settings.php). JetBrains Mono stays on
   Google; only the sans moved.

   The purchase covers Regular (400) only. The scale below still asks for 500
   (.display, .h2, .page-title, .overlay__item, buttons) and 600 (.prose h4),
   so the browser SYNTHESISES those by smearing the 400 outlines. That is a
   deliberate, accepted trade — not an oversight. Buying PP Mori Medium and
   SemiBold and adding two @font-face rules here is the whole fix; no rule in
   §03 has to change.

   No italic face either, so editor-authored <em> in post content is obliqued
   by the browser. Instrument Sans previously supplied a real italic 400. */
@font-face {
    font-family: 'PP Mori';
    src: url('../fonts/PPMori-Regular.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;   /* never blank text while the face loads */
}

/* ---------------------------------------------------------------- 01 tokens */
:root {
    /* Surface / light */
    --light-base:   #F6F5F2;
    --light-muted:  #EDEBE6;
    --light-text:   #14161A;   /* 16.4:1 on base */
    --light-second: #5C6066;   /*  5.3:1 on base */
    --light-accent: #3E7A00;   /*  4.8:1 on base */
    --light-rule:   #DDDBD5;

    /* Surface / dark */
    --dark-base:    #14161A;
    --dark-muted:   #1C2025;
    --dark-text:    #F6F5F2;   /* 17.0:1 on base */
    --dark-second:  #9AA0A6;   /*  7.0:1 on base */
    --dark-accent:  #C6F53F;   /* 14.4:1 on base */
    --dark-rule:    #2A2E34;

    /* Highlight green. Surface-independent because it is never a fill and never
       carries text — it exists only as the hover ring on a CTA edge. Same hue the
       dark surface already uses as its accent. */
    --ring-hi: #C6F53F;

    /* Validation — the only additions to the palette, contact block only. */
    --error:   oklch(0.5 0.17 30);
    --success: oklch(0.5 0.1 155);

    /* Resolved per surface by §05. Everything below reads these, never the
       palette directly, so one block never has to know which surface it is on. */
    --surface:  var(--light-base);
    --fill:     var(--light-base);
    --text:     var(--light-text);
    --second:   var(--light-second);
    --accent:   var(--light-accent);
    --rule:     var(--light-rule);
    --inset:    var(--light-muted);

    --font-sans: 'PP Mori', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
    --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

    --container: 1280px;
    --gutter:    64px;
    --rail:      296px;
    --gap:       64px;

    /* Vertical rhythm. Dark carries 1.25× — Stage 1 rule 3. */
    --pad-y:      96px;
    --pad-y-dark: 120px;
    --pad-y-open: 152px;   /* first block on a page: clears the 72px header */

    --header-h: 72px;

    --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

@media (max-width: 1023px) {
    :root { --gutter: 32px; --gap: 40px; --rail: 100%; --pad-y: 72px; --pad-y-dark: 88px; --pad-y-open: 96px; }
}
@media (max-width: 767px) {
    :root { --gutter: 24px; --gap: 28px; --pad-y: 56px; --pad-y-dark: 68px; --pad-y-open: 72px; }
}

/* ----------------------------------------------------------------- 02 reset */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    background: var(--light-base);
    color: var(--light-text);
    font-family: var(--font-sans);
    font-size: 17px;
    line-height: 1.55;
    font-weight: 400;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

img, video, svg { max-width: 100%; height: auto; display: block; }
button { font: inherit; color: inherit; }

a { color: var(--accent); text-decoration: none; }

/* ------------------------------------------------------------------ 03 type */
.display, .page-title, .h2, .h3 {
    margin: 0;
    font-weight: 500;
    text-wrap: balance;
}

.display     { font-size: 88px; line-height: 1.02; letter-spacing: -0.028em; max-width: 24ch; }
.page-title  { font-size: 56px; line-height: 1.06; letter-spacing: -0.022em; max-width: min(20ch, 100%); }
.h2          { font-size: 36px; line-height: 1.12; letter-spacing: -0.016em; max-width: min(28ch, 100%); }
.h3          { font-size: 21px; line-height: 1.30; letter-spacing: -0.008em; max-width: min(32ch, 100%); }

/* Responsive steps. Display and H1 step down; H2 and below hold their desktop
   value until <768. Stage 1 §01. */
@media (max-width: 1439px) { .display { font-size: 72px; } .page-title { font-size: 48px; } }
@media (max-width: 1023px) { .display { font-size: 56px; } .page-title { font-size: 40px; } .h2 { font-size: 32px; } }
@media (max-width: 767px)  { .display { font-size: 40px; } .page-title { font-size: 32px; } .h2 { font-size: 28px; } }

.prose { max-width: min(62ch, 100%); margin-top: 28px; color: var(--second); text-wrap: pretty; }
.prose :where(p, ul, ol) { margin: 0 0 1em; }
.prose :where(p, ul, ol):last-child { margin-bottom: 0; }
.prose a { color: var(--accent); border-bottom: 1px solid currentColor; }

.prose--lead, .lead { font-size: 19px; line-height: 1.62; max-width: min(52ch, 100%); }

/* Mono label. Never a heading element — always a sibling of one. */
.label {
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.6;
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text);
}

.numeral {
    display: block;
    font-family: var(--font-mono);
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.08em;
    color: var(--second);
    margin-bottom: 14px;
}

/* ---------------------------------------------------------------- 04 layout */
.container {
    width: 100%;
    max-width: var(--container);
    margin-inline: auto;
    padding-inline: var(--gutter);
}

.split { display: grid; grid-template-columns: var(--rail) minmax(0, 1fr); gap: var(--gap); }
.split--top { align-items: start; }

@media (max-width: 1023px) {
    .split { grid-template-columns: minmax(0, 1fr); }
}

.measure { max-width: min(62ch, 100%); }

/* -------------------------------------------------------------- 05 surfaces */
/* Resolved centrally in inc/surfaces.php; a block never sets its own colours. */
.section {
    position: relative;
    padding-block: var(--pad-y);
    background: var(--fill);
    color: var(--text);
}

.is-light {
    --text:    var(--light-text);
    --second:  var(--light-second);
    --accent:  var(--light-accent);
    --rule:    var(--light-rule);
    --inset:   var(--light-muted);
    --surface: var(--light-base);
}
.is-light.is-fill-base  { --fill: var(--light-base); }
.is-light.is-fill-muted { --fill: var(--light-muted); }

.is-dark {
    --text:    var(--dark-text);
    --second:  var(--dark-second);
    --accent:  var(--dark-accent);
    --rule:    var(--dark-rule);
    --inset:   var(--dark-muted);
    --surface: var(--dark-base);
    padding-block: var(--pad-y-dark);   /* rule 3: 1.25× */
}
.is-dark.is-fill-base  { --fill: var(--dark-base); }
.is-dark.is-fill-muted { --fill: var(--dark-muted); }

/* Rule 5: a boundary is either a surface change or a hairline, never both.
   .has-seam is only ever set where there is NO change of resolved fill. */
.has-seam { border-top: 1px solid var(--rule); }

/* The first section on a page opens against the header rather than against
   another section. */
.site-main > .section:first-child { padding-top: var(--pad-y-open); }

/* ------------------------------------------------------------ 06 primitives */
.cta {
    display: inline-block;
    margin-top: 32px;
    font-size: 15px;
    font-weight: 500;
    color: var(--accent);
    border-bottom: 1px solid currentColor;
    padding-bottom: 3px;
    transition: color 160ms var(--ease);
}
.cta:hover { color: var(--text); }
.cta--after-grid { margin-top: 36px; }

/* Rule 4: accent never FILLS an area outside banner_full, the contact submit,
   and the optional hero action. All three opt in explicitly.

   The hero is the third and it is deliberate: it is the page's primary
   conversion above the fold, it appears at most once per page, and unlike the
   featured_text run there is no repetition case for it to compete with. The
   rule exists to stop six filled buttons reading as six competing conversions;
   one hero action does not do that. Do not extend this to a fourth block
   without the same argument. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 15px 26px;
    font-size: 15px;
    font-weight: 500;
    transition: background-color 160ms var(--ease), color 160ms var(--ease);
}
.btn--fill { background: var(--text); color: var(--fill); }
.is-dark .btn--fill { background: var(--dark-text); color: var(--dark-base); }
.btn--block { display: flex; width: 100%; }

/* The CTA border micro-interaction (Stage 3 handoff). Hover-gated: at rest there
   is a hairline or a bare edge and nothing else, so the effect costs nothing on a
   page carrying several CTAs. On hover a muted green ring draws itself clockwise
   over 550ms and holds, and a streak of light runs the perimeter.

   An SVG overlays the button because a running dash cannot be expressed on a
   border. All three rects share one geometry — inset half a pixel and 1px short
   in both axes, which centres the 1px stroke ON the button edge rather than
   outside it — and each carries pathLength="100", so dasharray values are
   percentages of the perimeter and one rule serves every button size.

   The overlay is pointer-events:none and the wrapper is the hover target, so the
   button keeps exactly the hit area it had. */
.btn-ring { position: relative; display: inline-flex; }
/* For buttons that fill their row (.btn--block, the banner button on mobile).
   The wrapper must carry the width — the ring hugs the wrapper, so a full-width
   button inside a shrink-wrapped wrapper would burst out of its own ring. */
.btn-ring--block { display: flex; width: 100%; }
.btn-ring__svg {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
}
.btn-ring__svg rect {
    x: 0.5px;
    y: 0.5px;
    width: calc(100% - 1px);
    height: calc(100% - 1px);
    fill: none;
}
.btn-ring__rest { stroke: var(--light-rule); stroke-width: 1; }
.btn-ring__draw {
    /* --ring-hi at 40%. Written flat rather than as color-mix so the value is the
       same one the handoff specifies, on every surface. */
    stroke: rgba(198, 245, 63, 0.4);
    stroke-width: 1;
    stroke-dasharray: 100;
    stroke-dashoffset: 100;
    transition: stroke-dashoffset 550ms cubic-bezier(.5, 0, .2, 1);
}
.btn-ring__runner { opacity: 0; transition: opacity 300ms ease; }

/* The runner is a comet, not a block: a short hot core, a soft glow over its
   head, and two longer, fainter layers hanging behind it, so brightness falls
   away down the tail the way light does in a neon chase. Every layer runs the
   same keyframes; a negative animation-delay staggers each one backwards along
   the path by (its length − the core's length), which pins all four leading
   edges together while the tails trail. Rounded caps so the ends of the streak
   are light, not saw cuts.

   The animation sits on the rects paused rather than being attached on hover:
   the stagger lives in animation-delay, and re-declaring the shorthand on
   hover would reset it. A paused dashoffset animation costs nothing — the
   property repaints, it never composites. (Keyframes over var()-typed offsets
   would have been tidier than four delays, but Chrome refuses to interpolate
   them and snaps between the endpoints instead.) */
.btn-ring__runner rect {
    stroke: var(--ring-hi);
    stroke-linecap: round;
    animation: 1.1667s linear infinite paused btn-ring-run; /* handoff's 1.05s − 10% speed */
}
.btn-ring__line  { stroke-width: 1; stroke-dasharray: 6 94; }
.btn-ring__glow  { stroke-width: 3; stroke-dasharray: 10 90; filter: blur(3px); opacity: 0.6; }
.btn-ring__tail1 { stroke-width: 1; stroke-dasharray: 13 87; opacity: 0.4; }
.btn-ring__tail2 { stroke-width: 1; stroke-dasharray: 22 78; opacity: 0.16; }

/* One lap is 1.1667s over 100 path units, so a unit is 11.667ms and each delay
   is (length − 6) units minus a full lap: negative, so the stagger holds from
   the first frame instead of leaving the layer parked until the delay elapses.
   The selectors are one class deeper than the rect rule above so the
   shorthand's delay reset cannot win. */
.btn-ring__runner .btn-ring__glow  { animation-delay: -1.12s; }
.btn-ring__runner .btn-ring__tail1 { animation-delay: -1.085s; }
.btn-ring__runner .btn-ring__tail2 { animation-delay: -0.98s; }

.btn-ring:hover .btn-ring__draw,
.btn-ring:has(:focus-visible) .btn-ring__draw { stroke-dashoffset: 0; }
.btn-ring:hover .btn-ring__runner,
.btn-ring:has(:focus-visible) .btn-ring__runner { opacity: 1; }
.btn-ring:hover .btn-ring__runner rect,
.btn-ring:has(:focus-visible) .btn-ring__runner rect { animation-play-state: running; }

/* A control the browser will not activate does not get the affordance — CF7
   disables the submit for the length of the request. */
.btn-ring:has(:disabled) .btn-ring__draw { stroke-dashoffset: 100; }
.btn-ring:has(:disabled) .btn-ring__runner { opacity: 0; }
.btn-ring:has(:disabled) .btn-ring__runner rect { animation-play-state: paused; }

@keyframes btn-ring-run {
    from { stroke-dashoffset: 0; }
    to   { stroke-dashoffset: -100; }
}

/* Reduced motion keeps the affordance and drops the movement: the ring appears
   whole and instantly, the runner holds still. */
@media (prefers-reduced-motion: reduce) {
    .btn-ring__draw { transition: none; }
    /* No animation at all (not merely paused), so the comet parks whole at the
       path start. Later in the file than the rule that attaches it, which is
       all the override needs. */
    .btn-ring__runner rect { animation: none; }
}

.tags { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 18px; }
.tag {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--second);
    border: 1px solid var(--rule);
    padding: 6px 10px;
    transition: color 160ms var(--ease), border-color 160ms var(--ease);
}
a.tag:hover { color: var(--text); border-color: var(--accent); }

/* Media. One ratio custom property, one treatment, image and video identical. */
.media {
    position: relative;
    aspect-ratio: var(--media-ratio, 16 / 9);
    overflow: hidden;
    background: var(--inset);
    border: 1px solid var(--rule);
}
.media__video,
.media__poster,
.media__image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
/* The poster sits above the video until the video is mounted and playing, so
   the still and the moving frame are literally interchangeable. */
.media__poster { z-index: 1; transition: opacity 200ms var(--ease); }
.media.is-playing .media__poster { opacity: 0; }
.media--cover { border: 0; aspect-ratio: auto; height: 100%; }
.media--empty { background: var(--inset); }
/* For a drawing that runs off its own edges: no content edge, so a frame draws a
   boundary that is not there. The ratio, the clip and the positioning all stay. */
.media--flat { border: 0; background: transparent; }

/* ------------------------------------------------------- 07 header / footer */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 100;
    background: var(--light-text);
    color: var(--light-base);
    padding: 12px 20px;
}
.skip-link:focus { left: 0; }

.site-header {
    position: sticky;
    top: 0;
    z-index: 50;
    background: var(--light-base);
    border-bottom: 1px solid var(--light-rule);
    overflow-anchor: none;
}
.site-header__utility { padding-top: 8px; }
.site-header__utility .container { display: flex; justify-content: flex-end; }

.site-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 48px;
    height: var(--header-h);
    transition: height 200ms var(--ease);
}
.site-header.is-scrolled .site-header__inner { height: 60px; }
.site-header.is-scrolled .site-header__utility { display: none; }

.brand { display: flex; align-items: center; gap: 10px; }
.brand__type--invert, .brand__mark--invert { filter: invert(1); }

.lang { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; }
.lang__item { color: var(--light-second); }
.lang__item.is-current { color: var(--light-text); }
.lang__item + .lang__item::before { content: '/'; color: var(--light-rule); margin-right: 8px; }

.nav { display: flex; align-items: center; gap: 32px; font-size: 15px; }
.nav__item { color: var(--light-second); padding-bottom: 3px; transition: color 160ms var(--ease); }
.nav__item:hover { color: var(--light-text); }
.nav__item.is-current { color: var(--light-text); border-bottom: 1px solid var(--light-accent); }
.nav__action { margin-left: 0; }

/* The trigger and the close label both reserve 7ch, so PT "Menu" / "Fechar" and
   EN "Menu" / "Close" cannot shift the bar or the overlay row. */
.menu-toggle,
.menu-close {
    background: none;
    border: 0;
    padding: 12px 0;
    min-width: 7ch;
    text-align: right;
    font-size: 15px;
    cursor: pointer;
}
.menu-toggle { display: none; }

/* Below 1024 the nav collapses and PT/EN moves into the bar beside the trigger.
   The wordmark is the whole lockup at every width — the mark was dropped from the
   bar, so §04's mark-only mobile variant no longer applies. */
.site-header__compact { display: none; align-items: center; gap: 20px; }

@media (max-width: 1023px) {
    .nav { display: none; }
    .menu-toggle { display: block; }
    .site-header__compact { display: flex; }
    .site-header__utility { display: none; }
}
@media (max-width: 767px) {
    .site-header__compact { gap: 16px; }
}

.overlay {
    position: fixed;
    inset: 0;
    /* Above the cookie notice (z 60), which is injected after load and otherwise
       paints over the overlay's pinned action and utility row. */
    z-index: 70;
    background: var(--dark-base);
    color: var(--dark-text);
    overflow-y: auto;
}
.overlay[hidden] { display: none; }
.overlay__inner {
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    padding: 20px 24px 40px;
}
.overlay__bar { display: flex; align-items: center; justify-content: space-between; }
.overlay__nav { display: grid; gap: 4px; margin-top: 56px; }
.overlay__item { display: flex; align-items: center; min-height: 44px; font-size: 32px; line-height: 1.35; font-weight: 500; letter-spacing: -0.016em; color: var(--dark-text); }
.overlay__foot { margin-top: auto; padding-top: 48px; display: grid; gap: 20px; }
.overlay__utility { display: flex; justify-content: space-between; align-items: center; }
.overlay__cities { margin: 0; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.06em; color: var(--dark-second); }
.overlay .lang { font-size: 12px; }
.overlay .lang__item { color: var(--dark-second); }
.overlay .lang__item.is-current { color: var(--dark-text); }
.overlay .lang__item + .lang__item::before { color: var(--dark-rule); }
.overlay .menu-close { color: var(--dark-text); font-weight: 500; }
/* The action repeats as a filled block pinned to the bottom, so the primary path
   survives the collapse. Slightly larger than the bar action: 16px / 500, 16px pad. */
.overlay .btn--fill { background: var(--dark-text); color: var(--dark-base); font-size: 16px; padding: 16px; }
body.is-menu-open { overflow: hidden; }

.site-footer {
    background: var(--dark-base);
    color: var(--dark-text);
    --text: var(--dark-text);
    --second: var(--dark-second);
    --accent: var(--dark-accent);
    --rule: var(--dark-rule);
    padding-block: var(--pad-y-dark) 32px;
}
.site-footer__top { display: grid; grid-template-columns: 2fr repeat(4, 1fr); gap: 40px; }
.site-footer__group ul { list-style: none; margin: 20px 0 0; padding: 0; display: grid; gap: 10px; font-size: 15px; }
.site-footer__group a { color: var(--dark-text); }
.site-footer__group a:hover { color: var(--dark-accent); }
.site-footer__group .label { color: var(--dark-second); }
.social { list-style: none; display: flex; gap: 12px; margin: 24px 0 0; padding: 0; }
.social__link img { opacity: 0.7; transition: opacity 160ms var(--ease); }
.social__link:hover img { opacity: 1; }

/* Auto-fit, so a third office lands without touching the link columns. */
.site-footer__offices {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 40px;
    margin-top: 64px;
    padding-top: 40px;
    border-top: 1px solid var(--dark-rule);
}
.office__line { font-size: 15px; line-height: 1.6; color: var(--dark-second); max-width: 26ch; }
.office__line:first-of-type { margin-top: 16px; }
.office__phone { display: inline-block; font-family: var(--font-mono); font-size: 15px; margin-top: 14px; color: var(--dark-accent); }
.office__qualifier { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--light-second); margin-top: 6px; }

.site-footer__bottom {
    margin-top: 56px;
    padding-top: 24px;
    border-top: 1px solid var(--dark-rule);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    color: var(--dark-second);
}

@media (max-width: 1023px) {
    .site-footer__top { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 767px) {
    .site-footer__top { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------- 08 blocks */

/* hero — no media slot; media enters the page at the block below. */
.hero { padding-top: var(--pad-y-open); }
/* The eyebrow. 32px to the H1 below it, which is the same air the rail label on
   featured_text has to its H2 — the hero has no rail, so the relationship is
   vertical instead. Empty on both live rows. */
.hero__label { margin-bottom: 32px; }
.hero__text { margin-top: 36px; }
/* Optional; absent on all 4 live rows. 40px rather than the .cta 32px because a
   filled block needs more air off a 52ch paragraph than a text link does. */
.hero__action { margin-top: 40px; }

/* featured_text · text_blocks · accordion · insights · people all share the
   296px rail, so they stack without a seam. */

/* text_media_block */
.tmb { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 80px; align-items: center; }
.tmb--media-left .tmb__media { order: -1; }
@media (max-width: 1023px) {
    /* Media always below the text regardless of the toggle — a media-first stack
       on mobile pushes every H2 off the first screen. */
    .tmb { grid-template-columns: minmax(0, 1fr); gap: 32px; }
    .tmb--media-left .tmb__media { order: 0; }
}

/* text_blocks — a stated column count per item count, never auto-fit, so a
   short final row cannot produce an orphan cell. */
.grid--hairline {
    display: grid;
    gap: 1px;
    background: var(--rule);
    border-top: 1px solid var(--rule);
    border-left: 1px solid var(--rule);
}
.grid__cell { background: var(--fill); padding: 28px 28px 36px; }
.grid__text { margin: 14px 0 0; color: var(--second); text-wrap: pretty; }

.grid--hairline[data-items="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--hairline[data-items="2"] .h3 { font-size: 24px; line-height: 1.28; }
.grid--hairline[data-items="2"] .grid__text { max-width: 44ch; }
.grid--hairline[data-items="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid--hairline[data-items="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }  /* 2×2, not 3+1 */
.grid--hairline[data-items="5"],
.grid--hairline[data-items="6"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid--hairline[data-items="7"],
.grid--hairline[data-items="8"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid--hairline[data-items="8"] .grid__text { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.grid--hairline:not([data-items]) { grid-template-columns: repeat(3, minmax(0, 1fr)); }

@media (max-width: 1023px) { .grid--hairline { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } }
@media (max-width: 767px)  { .grid--hairline { grid-template-columns: minmax(0, 1fr) !important; border-left: 0; } }

/* The per-item eyebrow, in the slot the computed numeral occupies. Same mono
   line, uppercased because it can carry words and not just a year range.
   Deliberately --second and not --accent: the eyebrow is not a link, and accent
   as decoration is the rule this stylesheet spends three comments defending. */
.numeral--label { text-transform: uppercase; }

/* sector_table — the SAP page's three-column matrix. A native table, so the
   hairlines are collapsed borders rather than a 1px grid gap; the visual result
   is the grid--hairline treatment and the semantics are a real table. */
.sectors { width: 100%; border-collapse: collapse; table-layout: fixed; }
.sectors th, .sectors td {
    border: 1px solid var(--rule);
    padding: 28px 24px 32px;
    text-align: left;
    vertical-align: top;
    font-weight: 400;
    text-wrap: pretty;
}
.sectors thead th {
    padding: 14px 24px;
    background: var(--inset);
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--second);
}
.sectors__sector { width: 216px; }
/* The two body columns carry the same distinction the design draws with colour:
   the process that hurts is stated in the muted voice, the delivery in the
   primary one. It is the only cue left once the header row is off-screen at
   <768, which is why it is a rule and not a nicety. */
.sectors__pain { color: var(--second); }
.sectors__intro { margin: 24px 0 0; color: var(--second); max-width: 26ch; text-wrap: pretty; }

@media (max-width: 1023px) {
    .sectors__sector { width: 176px; }
    .sectors th, .sectors td { padding: 20px 18px 24px; }
}
/* Stacked. table-layout and fixed widths go with it, and so does the header row:
   three 24ch columns do not fit 375px and a horizontal scroller inside a section
   is worse than a stack. The row header stays first in each block, so the sector
   name still introduces its two cells. */
@media (max-width: 767px) {
    .sectors, .sectors thead, .sectors tbody, .sectors tr, .sectors th, .sectors td { display: block; width: auto; }
    .sectors thead { display: none; }
    .sectors tr { border-top: 1px solid var(--rule); padding-block: 24px 28px; }
    .sectors tr:last-child { border-bottom: 1px solid var(--rule); }
    .sectors th, .sectors td { border: 0; padding: 0; }
    .sectors td { margin-top: 12px; }
    .sectors__sector { width: auto; }
}

/* banner_full — full bleed while every other block holds the measure. The width
   change is what makes it read as an ending. */
.banner { position: relative; padding-block: 140px; overflow: hidden; }
.banner__media { position: absolute; inset: 0; z-index: 0; }
.banner__media .media { height: 100%; border: 0; }
.banner__scrim { position: absolute; inset: 0; z-index: 1; background: rgba(20, 22, 26, 0.78); }
.banner__inner { position: relative; z-index: 2; display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 80px; align-items: end; }
.banner__title { max-width: min(22ch, 100%); }
.banner__text { margin: 0; color: var(--second); max-width: min(48ch, 100%); text-wrap: pretty; }
.banner .btn-ring { margin-top: 32px; }
.banner--statement .banner__inner { grid-template-columns: minmax(0, 1fr); }
.banner--statement .banner__title { max-width: min(20ch, 100%); }
/* No title: 12 of the 17 case uses, where the block is a media band carrying the
   outbound project link (FIXES.md F-05). The two-column grid exists to set a title
   against a body, so without the title element the body would fall into the 1.2fr
   title column and the action would sit at the wrong measure. One column, and the
   padding closes up — the band is shorter than a full banner because it says less. */
.banner--untitled .banner__inner { grid-template-columns: minmax(0, 1fr); }
.banner--untitled { padding-block: 96px; }
@media (max-width: 1023px) {
    .banner { padding-block: var(--pad-y-dark); }
    .banner__inner { grid-template-columns: minmax(0, 1fr); gap: 32px; }
    .banner .btn-ring { width: 100%; }
    .banner .btn--fill { width: 100%; }
}

/* accordion — DOM contract per Stage 2 tier 2. The answer node is NEVER removed
   or emptied when closed: collapse is grid-template-rows 0fr with overflow
   hidden. */
.accordion__items { border-top: 1px solid var(--rule); }
.accordion__item { border-bottom: 1px solid var(--rule); }
.accordion__trigger,
.accordion__static {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 40px;
    width: 100%;
    padding: 28px 0;
    background: none;
    border: 0;
    text-align: left;
    cursor: pointer;
}
.accordion__static { cursor: default; }
.accordion__question { max-width: min(44ch, 100%); }
.accordion__indicator { font-family: var(--font-mono); font-size: 18px; line-height: 1; color: var(--second); }

.accordion__answer {
    display: grid;
    grid-template-rows: 0fr;
    overflow: hidden;
    transition: grid-template-rows 240ms var(--ease);
}
.accordion__item[data-open="true"] .accordion__answer { grid-template-rows: 1fr; }
.accordion__answer-inner { min-height: 0; }
.accordion__item[data-open="true"] .accordion__answer-inner { padding-bottom: 28px; }
.accordion__item[data-open="true"] .accordion__indicator::after { content: ''; }

@media (max-width: 767px) {
    .accordion__trigger { flex-wrap: wrap; gap: 12px; }
}

/* insights rows — the one row design (RECONCILIATION.md §1). */
.rows { border-top: 1px solid var(--rule); }
.row {
    display: grid;
    grid-template-columns: 132px minmax(0, 1fr);
    gap: 40px;
    padding: 32px 0;
    border-bottom: 1px solid var(--rule);
    color: var(--text);
    transition: background-color 200ms var(--ease);
}
.rows--tight .row { padding: 24px 0; grid-template-columns: 120px minmax(0, 1fr); gap: 28px; }
.row:hover { background: var(--inset); }
.row:hover .row__title { color: var(--accent); }
.row__meta { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--second); line-height: 1.7; }
.row__title { max-width: min(46ch, 100%); transition: color 160ms var(--ease); }
.row__standfirst { margin: 14px 0 0; color: var(--second); max-width: min(62ch, 100%); text-wrap: pretty; }
@media (max-width: 767px) {
    .row { grid-template-columns: minmax(0, 1fr); gap: 8px; }
    .row__meta { font-size: 11px; }
}

/* who_trust — normalisation, not placement. The uniform band is what hides that
   the marks are not uniform. */
.logos {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 1px;
    background: var(--rule);
    border-top: 1px solid var(--rule);
    border-left: 1px solid var(--rule);
}
.logos__cell {
    background: var(--fill);
    height: 104px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.logos__mark {
    max-height: 48px;
    max-width: 140px;
    width: auto;
    opacity: 0.7;
    transition: opacity 160ms var(--ease), filter 160ms var(--ease);
    /* Rendered as a single flat colour, never at its own brand colour. */
    filter: grayscale(1) brightness(0) invert(var(--logo-invert, 0));
}
.is-dark .logos__mark { --logo-invert: 1; }
.logos__cell:hover .logos__mark { opacity: 1; }
@media (max-width: 1279px) { .logos { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 767px)  { .logos { grid-template-columns: repeat(2, minmax(0, 1fr)); } .logos__cell { height: 88px; } }

/* cases */
.cases__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 48px; flex-wrap: wrap; }
.cases__head-actions { display: flex; align-items: center; gap: 24px; }
.cases__head .cta { margin-top: 0; white-space: nowrap; }
.cases__track { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px; margin-top: 56px; }
.cases__track[data-count="1"],
.cases__track[data-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* The arrows are the scroll affordance the design specifies, so the native
   scrollbar is hidden. Hidden, not disabled: overflow-x stays `auto` so touch
   swipe, scroll-snap and keyboard scrolling all keep working, and the arrows'
   scrollBy() still has somewhere to scroll. */
.cases--carousel .cases__track {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    scrollbar-width: none;      /* Firefox */
    -ms-overflow-style: none;   /* legacy Edge */
}
.cases--carousel .cases__track::-webkit-scrollbar { display: none; } /* Chromium, Safari */
.cases--carousel .card { flex: 0 0 calc((100% - 64px) / 3); scroll-snap-align: start; }
.carousel__controls { display: flex; gap: 8px; }
.carousel__controls button { min-width: 44px; min-height: 44px; background: none; border: 1px solid var(--rule); color: var(--text); cursor: pointer; }
.carousel__controls button:hover { border-color: var(--accent); }

.card { display: block; color: var(--text); transition: background-color 200ms var(--ease); }
.card__media { overflow: hidden; }
.card .media { transition: transform 200ms var(--ease); }
.card:hover .media { transform: scale(1.02); }
.card:hover .card__client { color: var(--accent); }
.card__client { margin-top: 20px; color: var(--second); transition: color 160ms var(--ease); }
.card__title { margin-top: 12px; }

/* card--post and card--page: the grid treatments, for search results and term
   archives. Posts get components/insights-row.php everywhere else — that is the
   designed editorial treatment — so these are deliberately quieter: in a mixed grid
   they sit beside case cards and must not out-weigh them. */
.card__meta {
    display: flex;
    gap: 12px;
    align-items: baseline;
    margin-top: 20px;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--second);
}
.card__term { padding-left: 12px; border-left: 1px solid var(--rule); }
.card__excerpt { margin-top: 12px; color: var(--second); max-width: min(42ch, 100%); text-wrap: pretty; }
/* No media slot at all on a page card — pages carry no featured image in this
   content set, and a grid of empty frames reads as broken images. */
.card--page { padding-top: 4px; border-top: 1px solid var(--rule); }
.card--page:hover .card__title { color: var(--accent); }
.card--post:hover .card__term { border-color: var(--accent); }

/* Load more — the only pagination control on the site. Centred on its own row so it
   is never mistaken for the last card in the grid. */
.pagination { display: flex; justify-content: center; margin-top: 64px; }
.btn--ghost {
    background: none;
    border: 1px solid var(--rule);
    color: var(--text);
    cursor: pointer;
    transition: border-color 160ms var(--ease), color 160ms var(--ease);
}
/* No hover border/color swap: hover on every button is the ring
   micro-interaction, and the ghost's own 1px border is its rest hairline —
   the ring draws over the same edge. */
.btn--ghost[disabled] { opacity: 0.5; cursor: default; border-color: var(--rule); color: var(--second); }
.is-dark .btn--ghost { border-color: var(--dark-rule); color: var(--dark-text); }

/* Cookie notice. Injected after load (inc/ajax.php), so it must not shift layout
   when it arrives — it is fixed to the viewport, not in the document flow. */
.cookie-banner {
    position: fixed;
    z-index: 60;
    inset: auto 20px 20px;
    max-width: 560px;
    padding: 24px;
    background: var(--dark-base);
    color: var(--dark-text);
    border: 1px solid var(--dark-rule);
}
.cookie-banner__text { margin: 0; color: var(--dark-second); font-size: 14px; text-wrap: pretty; }
.cookie-banner__text a { color: var(--dark-accent); }
.cookie-banner__actions { display: flex; gap: 12px; margin-top: 20px; }
/* Both choices at equal weight. "Essential only" must be exactly as easy to reach
   as acceptance — the analytics gate in Doois_Utils::posthog_allowed() assumes a
   real choice was made. */
.cookie-banner__actions .btn-ring { flex: 1; }
.cookie-banner__actions .btn { width: 100%; min-height: 44px; padding: 12px 18px; }
@media (max-width: 767px) {
    .cookie-banner { inset: auto 12px 12px; padding: 20px; }
    .cookie-banner__actions { flex-direction: column; }
}

@media (max-width: 1023px) {
    .cases__track { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .cases--carousel .card { flex-basis: calc((100% - 24px) / 2); }
}
@media (max-width: 767px) {
    .cases__track { grid-template-columns: minmax(0, 1fr); gap: 40px; }
    .cases--carousel .card { flex-basis: 82%; }
}

/* featured_video — a run collapses into ONE dark section with hairline rows. */
.featured-video__row { display: grid; grid-template-columns: var(--rail) minmax(0, 1fr); gap: var(--gap); align-items: start; }
.featured-video__row--full { grid-template-columns: minmax(0, 1fr); }
.featured-video__row + .featured-video__row { margin-top: 64px; padding-top: 64px; border-top: 1px solid var(--rule); }
.featured-video__text { margin: 20px 0 0; text-wrap: pretty; }
@media (max-width: 1023px) { .featured-video__row { grid-template-columns: minmax(0, 1fr); } }

/* case header */
.case-header__meta { font-family: var(--font-mono); font-size: 11px; line-height: 1.8; color: var(--second); margin-top: 14px; }
.case-header__media { margin-top: 72px; }
.case-header .tags { margin-top: 36px; }

/* people — a directory, not a team grid. */
.people__list { display: grid; grid-template-columns: 1fr 1fr; gap: 0 48px; }
.people__row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 24px;
    padding: 22px 0;
    border-top: 1px solid var(--rule);
}
.people__row:nth-last-child(-n+2) { border-bottom: 1px solid var(--rule); }
.people__role { color: var(--second); text-align: right; }
@media (max-width: 1023px) {
    .people__list { grid-template-columns: minmax(0, 1fr); }
    .people__row:nth-last-child(-n+2) { border-bottom: 0; }
    .people__row:last-child { border-bottom: 1px solid var(--rule); }
}
@media (max-width: 767px) {
    .people__row { flex-direction: column; gap: 6px; }
    .people__role { text-align: left; }
}

/* contact — fill is FIXED muted; inputs sit on base so the field is lighter than
   its ground. Everything below is styled through CF7's own classes plus the three
   wrappers the form template supplies (.form-pair, .form-field, .form-actions);
   CF7 passes arbitrary markup through untouched, so the grid is ours.

   The rules are written to survive a form template that has NOT been rewritten —
   the outgoing templates wrap each field in a bare <fieldset> and let CF7's autop
   add <p>/<br> — so an un-migrated form degrades to a single column of correct
   fields rather than to browser fieldset chrome. */
.contact__subtitle { margin-top: 24px; color: var(--second); }
.contact__form { max-width: 620px; }

/* CF7's own containers carry browser defaults that draw boxes nobody designed. */
.contact__form fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
.contact__form form > p,
.contact__form fieldset > p { margin: 0; }
.contact__form .screen-reader-response { position: absolute; left: -9999px; }
.contact__form .hidden-fields-container { display: none; }

/* One field. 24px between fields, and between pairs, is the whole vertical rhythm.
   Scoped to the form's own children: inside a pair the two fields are siblings too,
   and the margin there would drop the second column half a field lower than the
   first — which is exactly what the pair grid exists to prevent. */
.contact__form form > .form-field + .form-field,
.contact__form form > .form-pair + .form-pair,
.contact__form form > .form-pair + .form-field,
.contact__form form > .form-field + .form-pair,
.contact__form form > fieldset + fieldset { margin-top: 24px; }
/* CF7's hidden input carrier is a fieldset too, and it is always first. */
.contact__form form > .hidden-fields-container + fieldset { margin-top: 0; }

/* Two-up pairs; single column at 768. */
.contact__form .form-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; }

.contact__form label { display: block; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--second); }
.contact__form .form-req { color: #9AA0A6; }
.contact__form .wpcf7-form-control {
    display: block;
    width: 100%;
    min-height: 56px;
    margin-top: 10px;
    padding: 0 16px;
    font: inherit;
    font-size: 17px;
    color: var(--light-text);
    background: var(--light-base);
    border: 1px solid var(--light-rule);
    border-radius: 0;
    transition: border-color 160ms var(--ease), box-shadow 160ms var(--ease);
}
/* 168px is the drawn height. CF7 emits rows="10", which is taller than that, so
   the height is set outright — the reader can still drag it down. */
.contact__form textarea.wpcf7-form-control { height: 168px; min-height: 168px; padding: 16px; line-height: 1.55; resize: vertical; }
.contact__form select.wpcf7-form-control { appearance: none; }
/* Focus is the accent ring alone — the ink border it used to draw with it read as
   a second, competing outline. */
.contact__form .wpcf7-form-control:focus {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--light-accent) 18%, transparent);
}
.contact__form .wpcf7-not-valid { border-color: var(--error); }
.contact__form .wpcf7-not-valid-tip { font-family: var(--font-mono); font-size: 11px; line-height: 1.7; color: var(--error); margin-top: 8px; }

/* Consent. A checkbox is not a 56px field — the block rule above has to be undone
   for it, or the acceptance row draws an empty input the size of a name field. */
.contact__form .form-consent { margin-top: 24px; font-size: 11.25px; line-height: 1.55; color: var(--second); }
.contact__form .wpcf7-acceptance,
.contact__form .wpcf7-checkbox,
.contact__form .wpcf7-radio {
    display: inline;
    width: auto;
    min-height: 0;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
}
.contact__form .wpcf7-list-item { display: inline; margin: 0 8px 0 0; }
.contact__form .wpcf7-list-item label { display: inline; font-family: inherit; font-size: inherit; letter-spacing: normal; text-transform: none; color: inherit; }
.contact__form input[type="checkbox"],
.contact__form input[type="radio"] {
    display: inline-block;
    width: 16px;
    min-height: 0;
    height: 16px;
    margin: 0 8px 0 0;
    padding: 0;
    vertical-align: -2px;
    accent-color: var(--light-accent);
}
.contact__form .form-consent a { color: var(--light-accent); text-decoration: none; }
/* The consent row is one sentence that starts with a box, not a list: the 8px
   list-item gap plus the checkbox's own margin plus the space in the template
   read as a hole between the box and "Eu". */
.contact__form .form-consent .wpcf7-list-item { margin: 0; }
.contact__form .form-consent input[type="checkbox"] { margin-right: 2px; }

/* Submit row: the one filled accent button outside banner_full, and the note that
   explains the asterisks beside it. */
.contact__form .form-actions { display: flex; align-items: center; gap: 28px; margin-top: 32px; flex-wrap: wrap; }
.contact__form .form-note { font-family: var(--font-mono); font-size: 11px; line-height: 1.7; color: var(--second); max-width: 34ch; }
.contact__form .wpcf7-submit {
    display: inline-block;
    width: auto;
    min-width: 0;
    min-height: 0;
    margin: 0;
    padding: 17px 32px;
    font-size: 16px;
    font-weight: 500;
    color: var(--light-base);
    background: var(--light-accent);   /* the one filled accent outside banner_full */
    border: 0;
    border-radius: 0;
    cursor: pointer;
    transition: background-color 160ms var(--ease);
}
/* No hover fill-swap: hover on this button is the border micro-interaction, and
   the handoff spells out exactly two things that happen on it. */
/* Submitting: CF7 disables the control and shows its own spinner. */
.contact__form .wpcf7-submit:disabled { background: var(--light-second); cursor: default; }
/* CF7 draws a 24px dark spinning disc. The motion budget has no disc in it: an
   8px square at utility grey, blinking under reduced motion like CF7's own. */
.contact__form .wpcf7-spinner {
    width: 8px;
    height: 8px;
    margin: 0;
    background: #9AA0A6;
    border-radius: 0;
    opacity: 1;
}
.contact__form .wpcf7-spinner::before { display: none; }
/* CF7's script injects the spinner as the submit's next sibling, which puts it
   inside the ring wrapper and would stretch the ring past the button's edge.
   Out of flow, the wrapper stays exactly the size of the button and the spinner
   still reads to its right, in the gap before the note. */
.btn-ring .wpcf7-spinner {
    position: absolute;
    top: 50%;
    left: calc(100% + 10px);
    transform: translateY(-50%);
}

/* CF7's own stylesheet targets .wpcf7 form .wpcf7-response-output, so the bar has
   to be addressed at least as specifically or its 2px blue border survives. */
.contact__form .wpcf7 form .wpcf7-response-output {
    margin: 20px 0 0;
    padding: 20px 22px;
    background: var(--light-base);
    border: 1px solid var(--light-rule);
    font-size: 17px;
    line-height: 1.55;
}
/* The bar carries a mono status line above CF7's own sentence. It is a
   pseudo-element because CF7's script rewrites the div's text on every AJAX
   submission and would wipe any real element rendered inside it; the label text
   arrives from the block template as a custom property so it stays translatable. */
.contact__form .wpcf7-response-output::before {
    display: block;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    margin-bottom: 10px;
}
.contact__form .wpcf7 form.sent .wpcf7-response-output { border-color: var(--success); }
.contact__form form.sent .wpcf7-response-output::before { content: var(--form-sent, 'Sent'); color: var(--success); }
.contact__form .wpcf7 form.invalid .wpcf7-response-output,
.contact__form .wpcf7 form.unaccepted .wpcf7-response-output { border-color: var(--error); }
.contact__form .wpcf7 form.failed .wpcf7-response-output,
.contact__form .wpcf7 form.aborted .wpcf7-response-output,
.contact__form .wpcf7 form.spam .wpcf7-response-output { border-color: var(--light-rule); }
.contact__form form.invalid .wpcf7-response-output::before,
.contact__form form.unaccepted .wpcf7-response-output::before { content: var(--form-invalid, 'Not sent'); color: var(--error); }
/* A send that never reached the server is not a validation error and is not drawn
   as one: hairline border, secondary label, the sentence carries the fallback. */
.contact__form form.failed .wpcf7-response-output::before,
.contact__form form.aborted .wpcf7-response-output::before,
.contact__form form.spam .wpcf7-response-output::before { content: var(--form-failed, 'Delivery failed'); color: var(--second); }

@media (max-width: 767px) {
    .contact__form .form-pair { grid-template-columns: minmax(0, 1fr); }
}

/* hero_schematic — "field resolution". An animated inline SVG in place of a
   video, drawn inside the same .media frame (aspect-ratio: var(--media-ratio,
   16 / 9)) and the same row structure as featured_video.

   NO new tokens: --spec-2/-3 are hue rotations DERIVED from the resolved
   --accent via relative colour syntax, so all three stay bound to the surface
   switch in §05. Where oklch(from ...) is unsupported all three fall back to
   --accent — monochrome, still correct on both surfaces, and deliberate; no
   polyfill.

   Loop: 12s seamless. Reduced motion: the base styles ARE the resolved frame,
   so switching the animations off leaves the finished state, not a blank one.

   SURFACE. The drawing sits on the section's own --fill, which is what the §01
   ratios are quoted against. It reaches that through .media--flat (§06) rather
   than a fork of .media or an override here: the ratio, the clip and the
   positioning are all still the primitive's, and only the border and the inset
   background come off. The first render did use bare .media, and the inset
   panel read as a lighter rectangle in a hairline, competing with the hero
   above it — hence the modifier. */
.schem-d {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-d {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-d .d-node {
  fill: var(--text);
  opacity: 1;
  transform: translate(0, 0);
  animation: d-node-1 12s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.schem-d .d-node.n2 { animation-name: d-node-2; }
.schem-d .d-node.n3 { animation-name: d-node-3; }
.schem-d .d-node.mut { fill: var(--second); }
.schem-d .d-node.s1 { fill: var(--spec-1); }
.schem-d .d-node.s2 { fill: var(--spec-2); }
.schem-d .d-node.s3 { fill: var(--spec-3); }

.schem-d .d-link {
  fill: none;
  stroke: var(--second);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
  opacity: 0.55;
  animation: d-link 12s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.schem-d .d-bridge {
  fill: none;
  stroke: var(--spec-1);
  stroke-width: 1.25;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
  opacity: 0.9;
  animation: d-bridge 12s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.schem-d .d-bridge.s2 { stroke: var(--spec-2); }
.schem-d .d-bridge.s3 { stroke: var(--spec-3); }

@keyframes d-node-1 {
  0%        { transform: translate(var(--sx), var(--sy)); opacity: 0.3; }
  30%, 68%  { transform: translate(0, 0); opacity: 1; }
  98%, 100% { transform: translate(var(--sx), var(--sy)); opacity: 0.3; }
}
@keyframes d-node-2 {
  0%        { transform: translate(var(--sx), var(--sy)); opacity: 0.3; }
  36%, 64%  { transform: translate(0, 0); opacity: 1; }
  98%, 100% { transform: translate(var(--sx), var(--sy)); opacity: 0.3; }
}
@keyframes d-node-3 {
  0%        { transform: translate(var(--sx), var(--sy)); opacity: 0.3; }
  42%, 60%  { transform: translate(0, 0); opacity: 1; }
  98%, 100% { transform: translate(var(--sx), var(--sy)); opacity: 0.3; }
}
@keyframes d-link {
  0%, 38%   { stroke-dashoffset: 1; opacity: 0; }
  52%, 62%  { stroke-dashoffset: 0; opacity: 0.55; }
  78%, 100% { stroke-dashoffset: 1; opacity: 0; }
}
@keyframes d-bridge {
  0%, 50%   { stroke-dashoffset: 1; opacity: 0; }
  60%, 68%  { stroke-dashoffset: 0; opacity: 0.9; }
  78%, 100% { stroke-dashoffset: 1; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-d .d-node,
  .schem-d .d-link,
  .schem-d .d-bridge { animation: none; }
}

/* Variant E — "braided convergence". Nine streams stage through three junctions
   into one ordered flow, and the carriers keep the hue of the junction that
   staged them. Same band, same palette, same 12s loop as D.

   The --spec-* pattern is repeated here rather than hoisted to a selector shared
   with D and F. That is deliberate: scoping it per variant is what lets one of
   them diverge later without editing the other two.

   E is the only variant that animates position along a path. offset-path takes
   the SAME path data as the drawn .e-track, and the two are emitted together by
   tools/generate-schematic-variants.py — hand-editing one without the other puts
   the carriers on a line that is not drawn. Where offset-path is unsupported the
   carriers are hidden rather than left stacked at the origin; the tracks, the
   junctions and the flow line still read as the diagram. */
.schem-e {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-e {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-e .e-track {
  fill: none;
  stroke: var(--second);
  stroke-opacity: 0.35;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.schem-e .e-junction.s1 { fill: var(--spec-1); }
.schem-e .e-junction.s2 { fill: var(--spec-2); }
.schem-e .e-junction.s3 { fill: var(--spec-3); }
.schem-e .e-flowline {
  stroke: var(--accent);
  stroke-width: 1.25;
  vector-effect: non-scaling-stroke;
}
.schem-e .e-hub { fill: var(--accent); }

.schem-e .e-dot {
  animation: e-dot 12s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--d, 0s);
  offset-rotate: 0deg;
}
.schem-e .e-dot.s1 { fill: var(--spec-1); }
.schem-e .e-dot.s2 { fill: var(--spec-2); }
.schem-e .e-dot.s3 { fill: var(--spec-3); }

.schem-e .et1 { offset-path: path('M -80 -18 C 220 -18 440 150 700 150 C 850 150 945 240 1075 240 L 1680 240'); }
.schem-e .et2 { offset-path: path('M -80 44 C 220 44 440 150 700 150 C 850 150 945 240 1075 240 L 1680 240'); }
.schem-e .et3 { offset-path: path('M -80 104 C 220 104 440 150 700 150 C 850 150 945 240 1075 240 L 1680 240'); }
.schem-e .et4 { offset-path: path('M -80 164 C 220 164 440 240 700 240 C 850 240 945 240 1075 240 L 1680 240'); }
.schem-e .et5 { offset-path: path('M -80 240 C 220 240 440 240 700 240 C 850 240 945 240 1075 240 L 1680 240'); }
.schem-e .et6 { offset-path: path('M -80 316 C 220 316 440 240 700 240 C 850 240 945 240 1075 240 L 1680 240'); }
.schem-e .et7 { offset-path: path('M -80 376 C 220 376 440 330 700 330 C 850 330 945 240 1075 240 L 1680 240'); }
.schem-e .et8 { offset-path: path('M -80 436 C 220 436 440 330 700 330 C 850 330 945 240 1075 240 L 1680 240'); }
.schem-e .et9 { offset-path: path('M -80 498 C 220 498 440 330 700 330 C 850 330 945 240 1075 240 L 1680 240'); }

@keyframes e-dot {
  0%   { offset-distance: 0%; opacity: 0; }
  6%   { opacity: 1; }
  94%  { opacity: 1; }
  100% { offset-distance: 100%; opacity: 0; }
}

@supports not (offset-path: path('M 0 0 L 10 10')) {
  .schem-e .e-dot { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-e .e-dot { animation: none; }
}

/* Variant F — "dense narrowing". Twenty-four routes are pruned in four waves;
   two chromatic runners outlast them and one accent path holds. The opacity
   floor is 0.08 rather than 0, so the discarded routes stay faintly present —
   pruned, not deleted, which is the whole point of the drawing.

   Nothing here animates geometry: F is 27 opacity animations, the cheapest of
   the three variants by a wide margin. */
.schem-f {
  display: block; width: 100%; height: 100%;
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-f {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-f .f-path {
  fill: none;
  stroke: var(--second);
  stroke-width: 1.1;
  vector-effect: non-scaling-stroke;
  opacity: 0.08;
  animation: f-prune-1 12s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.schem-f .f-path.f2 { animation-name: f-prune-2; }
.schem-f .f-path.f3 { animation-name: f-prune-3; }
.schem-f .f-path.f4 { animation-name: f-prune-4; }

.schem-f .f-runner {
  fill: none;
  stroke-width: 1.4;
  vector-effect: non-scaling-stroke;
  opacity: 0.35;
  animation: f-runner 12s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.schem-f .f-runner.s2 { stroke: var(--spec-2); }
.schem-f .f-runner.s3 { stroke: var(--spec-3); }

.schem-f .f-survivor {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  opacity: 1;
  animation: f-survivor 12s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

@keyframes f-prune-1 {
  0%, 6%    { opacity: 0.45; }
  18%, 84%  { opacity: 0.08; }
  96%, 100% { opacity: 0.45; }
}
@keyframes f-prune-2 {
  0%, 14%   { opacity: 0.45; }
  28%, 84%  { opacity: 0.08; }
  96%, 100% { opacity: 0.45; }
}
@keyframes f-prune-3 {
  0%, 24%   { opacity: 0.45; }
  40%, 84%  { opacity: 0.08; }
  96%, 100% { opacity: 0.45; }
}
@keyframes f-prune-4 {
  0%, 34%   { opacity: 0.45; }
  52%, 84%  { opacity: 0.08; }
  96%, 100% { opacity: 0.45; }
}
@keyframes f-runner {
  0%, 8%    { opacity: 0.5; }
  44%, 56%  { opacity: 0.9; }
  70%, 84%  { opacity: 0.35; }
  100%      { opacity: 0.5; }
}
@keyframes f-survivor {
  0%, 12%   { opacity: 0.3; }
  58%, 86%  { opacity: 1; }
  100%      { opacity: 0.3; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-f .f-path,
  .schem-f .f-runner,
  .schem-f .f-survivor { animation: none; }
}

/* Variant G — "thicket". F kept as it is and this drawn beside it: ninety
   routes dissolving on a 5s loop, four chromatic runners and the same accent
   survivor.

   NO FRAME. The viewBox is a 800x240 crop at the middle of a 2120x840 field, so
   the nearest endpoint of any route is 300 units outside the window — the
   drawing has no visible edge, no margin and no end, only a zoom into the
   middle of it. That is markup, not CSS: nothing in this file scales or crops,
   in line with the rule the second-round variants set. `slice` on the SVG keeps
   it true when the box ratio drifts off 10/3.

   SMOOTH, NOT STEPPED. F prunes in four wave classes; ninety routes in a
   handful of classes would step in blocks, which reads as clunky. Each route
   instead carries its own --pd, a continuous ramp spread over 2.6s of the
   loop, and every segment eases — so what is animated is one dissolve rather
   than a sequence of events. The opacity floor is 0.05, not F's 0.08, because
   ninety faint routes accumulate into a wash where twenty-four read as a
   memory of a thicket.

   Still no animated geometry — 95 opacity animations and no layout. */
.schem-g {
  display: block; width: 100%; height: 100%;
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-g {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-g .g-path {
  fill: none;
  stroke: var(--second);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  opacity: 0.05;
  animation: g-prune 5s cubic-bezier(0.45, 0, 0.55, 1) var(--pd, 0s) infinite;
}

.schem-g .g-runner {
  fill: none;
  stroke-width: 1.3;
  vector-effect: non-scaling-stroke;
  opacity: 0.35;
  animation: g-runner 5s cubic-bezier(0.45, 0, 0.55, 1) var(--pd, 0s) infinite;
}
.schem-g .g-runner.s2 { stroke: var(--spec-2); }
.schem-g .g-runner.s3 { stroke: var(--spec-3); }

.schem-g .g-survivor {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  opacity: 1;
  animation: g-survivor 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

/* One ramp down, one back, both eased, with a short settled stretch between —
   no held-then-snapped edges anywhere in the cycle. */
@keyframes g-prune {
  0%       { opacity: 0.40; }
  44%      { opacity: 0.05; }
  62%      { opacity: 0.05; }
  100%     { opacity: 0.40; }
}
@keyframes g-runner {
  0%       { opacity: 0.45; }
  50%      { opacity: 0.9; }
  78%      { opacity: 0.4; }
  100%     { opacity: 0.45; }
}
@keyframes g-survivor {
  0%       { opacity: 0.35; }
  56%      { opacity: 1; }
  84%      { opacity: 1; }
  100%     { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-g .g-path,
  .schem-g .g-runner,
  .schem-g .g-survivor { animation: none; }
}

/* ── The second round: H, K, L ───────────────────────────────────────────────
   Three variants from the later candidate set, added to the same two blocks.
   They keep everything that makes D/E/F one family — inline SVG on the
   section's own --fill, --spec-2/-3 derived from the resolved --accent by
   relative colour syntax with a monochrome @supports fallback, no new tokens,
   reduced motion leaving the resolved frame rather than a blank one.

   Two things they do NOT share, and both are deliberate:

   The LOOP is 5s, not 12s. Each of the three is seamless at 5s by
   construction — H's pulses crawl one full trace length, L's dashes advance
   exactly one dash period and its decagon precesses 36°, which is its own
   symmetry — so 12s is not a duration to swap in; it is a different set of
   wrap conditions. Two loop lengths on one page is the cost, and it is
   cheaper than three redrawn animations.

   The CANVAS is 1280x720, the pilot's, not the 10/3 band D/E/F were retargeted
   to. In a 10/3 box `meet` fits by height, so these render centred at roughly
   half the box width with section padding either side. Changing that is a
   viewBox change in the component, not an override here — no CSS in this file
   scales, crops or letterboxes a drawing, and adding one would make the two
   families disagree about what --media-ratio means. */

/* Variant H — "sequenced handoff". An orthogonal circuit on three traces, nine
   gates, seven chromatic pulses crawling them at even spacing; each gate
   flashes as a pulse reaches it. The gate delays are inline per element, so the
   sequence is geometry the drawing carries, not a rule per gate here.

   H animates stroke-dashoffset on seven long paths, plus nine opacity
   animations — still cheaper than D by an order of magnitude. */
.schem-h {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-h {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

/* THREE traces, not one. The first cut ran a single spine between y=220 and
   y=500 — 40% of a 720-tall canvas, with the rest carrying nothing but stubs,
   and it read as empty rather than as sparse. The upper and lower traces reach
   y=60 and y=690, four .h-link risers tie them to the spine so the three read
   as one circuit rather than three stripes, and the minor pair is a step
   lighter than the spine so the hierarchy survives the extra ink. */
.schem-h .h-trace { fill: none; stroke: var(--second); stroke-opacity: 0.6; stroke-width: 1.1; vector-effect: non-scaling-stroke; }
.schem-h .h-trace--minor { stroke-opacity: 0.42; stroke-width: 1; }
.schem-h .h-link { stroke: var(--second); stroke-opacity: 0.32; stroke-width: 1; vector-effect: non-scaling-stroke; }
.schem-h .h-branch { stroke: var(--second); stroke-opacity: 0.36; stroke-width: 1; vector-effect: non-scaling-stroke; }
.schem-h .h-term { fill: none; stroke: var(--second); stroke-opacity: 0.5; stroke-width: 1; vector-effect: non-scaling-stroke; }
.schem-h .h-core { fill: var(--text); }
.schem-h .h-gate {
  fill: none;
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
  opacity: 1;
  animation: h-gate 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--gd, 0s);
}
.schem-h .h-gate.s1 { stroke: var(--spec-1); }
.schem-h .h-gate.s2 { stroke: var(--spec-2); }
.schem-h .h-gate.s3 { stroke: var(--spec-3); }
/* pathLength="1" on the drawn path is what makes the dash pair a FRACTION of
   the trace rather than user units, so the pulse is the same length whatever
   the circuit's true arc length is. --o holds each pulse's resting offset, so
   reduced motion freezes three pulses distributed along the trace rather than
   three stacked at its start. */
.schem-h .h-pulse {
  fill: none;
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 0.06 0.94;
  stroke-dashoffset: var(--o, 0);
  animation: h-pulse 5s linear infinite;
  animation-delay: var(--d, 0s);
}
.schem-h .h-pulse.s1 { stroke: var(--spec-1); }
.schem-h .h-pulse.s2 { stroke: var(--spec-2); }
.schem-h .h-pulse.s3 { stroke: var(--spec-3); }
/* The two minor traces are shorter than the spine, so a pulse of the same
   fractional length covers them faster and reads as a heavier mark. Thinner
   and a touch shorter puts them back at the spine's apparent weight. */
.schem-h .h-pulse--minor { stroke-width: 1.3; stroke-dasharray: 0.05 0.95; }

@keyframes h-pulse {
  0%   { stroke-dashoffset: 1; }
  100% { stroke-dashoffset: 0; }
}
@keyframes h-gate {
  0%, 2%       { opacity: 1; }
  8%, 31%      { opacity: 0.4; }
  33.3%, 35.3% { opacity: 1; }
  41%, 64%     { opacity: 0.4; }
  66.7%, 68.7% { opacity: 1; }
  75%, 98%     { opacity: 0.4; }
  100%         { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-h .h-pulse,
  .schem-h .h-gate { animation: none; }
}

/* Variant K — "quasicrystal bloom". A Penrose P3 patch: three deflations of a
   decagonal sun, 124 unique edges grouped into five radial bands. An opacity
   wave leaves the seed and crosses the bands in order; eight rhombic films
   bloom in the spectrum behind it.

   Five band animations and nine element animations in total — the edges are
   batched into one <path> per band with M-separated subpaths, which is why a
   124-edge tiling costs five animations rather than 124. */
.schem-k {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-k {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-k .k-band {
  fill: none;
  stroke: var(--second);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  opacity: 0.22;
  animation: k-band 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--bd, 0s);
}
.schem-k .k-inner { stroke: var(--text); opacity: 0.3; }
.schem-k .k-rhomb {
  opacity: 0.28;
  animation: k-rhomb 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--pd, 0s);
}
.schem-k .k-rhomb.s1 { fill: var(--spec-1); }
.schem-k .k-rhomb.s2 { fill: var(--spec-2); }
.schem-k .k-rhomb.s3 { fill: var(--spec-3); }
.schem-k .k-core {
  fill: var(--accent);
  animation: k-core 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

@keyframes k-band {
  0%        { opacity: 0.14; }
  8%        { opacity: 0.55; }
  22%, 100% { opacity: 0.14; }
}
@keyframes k-rhomb {
  0%, 100%  { opacity: 0.08; }
  45%, 55%  { opacity: 0.55; }
}
@keyframes k-core {
  0%, 100%  { opacity: 0.5; }
  6%        { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-k .k-band,
  .schem-k .k-rhomb,
  .schem-k .k-core { animation: none; }
}

/* Variant L — "pentagrid". De Bruijn's dual construction: five families of
   parallel lines at 72°, the object a Penrose tiling is the dual OF. Forty
   atoms blink at grid coincidences, the dashes crawl one full period, and the
   decagon precesses 36°.

   The crawl and the rotation both wrap exactly at their own period, which is
   the whole reason this variant does not run at 12s: the crawl distance and
   the rotation are quantities that must equal one period per iteration, not
   per second. The atoms are on a separate, faster clock — see below.

   The line families extend far past the viewBox on purpose — the grid is
   infinite and the canvas is a window on it. The <svg> clips them; no CSS
   overflow rule is involved. Families 2 and 4 crawl in reverse so opposing
   slopes do not read as one sheet sliding. */
.schem-l {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-l {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-l .l-line {
  stroke: var(--second);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  opacity: 0.22;
  stroke-dasharray: 7 11;
  animation: l-crawl 2.5s linear infinite;
}
.schem-l .l-fam.f2 .l-line, .schem-l .l-fam.f4 .l-line { animation-direction: reverse; }
/* The atoms run on their OWN clock — 1.5s, not the 5s the grid and the decagon
   share. They are the only element here that reads as a state change rather
   than as drift, and at 5s that change was slower than the eye reads as
   blinking. 1.5s does not divide 5s and does not need to: an atom's opacity
   returns to its start value every period, so nothing has to line up with the
   crawl or the spin. The --pd delays are 0.22s apart, which still spreads the
   forty atoms across a 1.5s period once wrapped. */
.schem-l .l-atom {
  opacity: 0.55;
  animation: l-atom 1.5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--pd, 0s);
}
.schem-l .l-atom.mut { fill: var(--second); }
.schem-l .l-atom.s1 { fill: var(--spec-1); }
.schem-l .l-atom.s2 { fill: var(--spec-2); }
.schem-l .l-atom.s3 { fill: var(--spec-3); }
.schem-l .l-deca {
  fill: none;
  stroke: var(--text);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  opacity: 0.35;
}
/* The rotation is on a WRAPPER group, not on the polygon: transform-origin
   here is the canvas centre in user units, and putting it on the shape would
   make the origin the shape's own box in some engines. */
.schem-l .l-deca-group {
  transform-origin: 640px 360px;
  animation: l-spin 5s linear infinite;
}

@keyframes l-crawl {
  to { stroke-dashoffset: 18; }
}
/* On and OFF, not a swell: the atom sits dark for most of the period, snaps to
   full, holds two frames' worth and drops. A symmetric 0.3 -> 1 -> 0.3 ramp
   never reads as a point switching state, which is what the drawing is about —
   the grid coincidences are discrete events. */
@keyframes l-atom {
  0%, 100% { opacity: 0.1; }
  14%      { opacity: 1; }
  30%      { opacity: 1; }
  44%      { opacity: 0.1; }
}
@keyframes l-spin {
  to { transform: rotate(36deg); }
}

@media (prefers-reduced-motion: reduce) {
  .schem-l .l-line,
  .schem-l .l-atom,
  .schem-l .l-deca-group { animation: none; }
}

/* ── The third round: Q, O, R ───────────────────────────────────────────────
   Same family again, and taken as authored: the palette, the 5s loop, the
   1280x720 canvas and every timing function below are the candidates' own.
   Q ships exactly as authored. O and R did not survive review untouched — see
   their blocks below — so docs/candidate-{o,r}.{svg,css} are the sources
   received, not a description of what renders. */

/* Variant Q — "wavelength selection". Six sinusoidal perturbations grow about
   one interface at the rates sigma^2 = k - k^3 gives them; the k* = 1/sqrt(3)
   mode in --accent outruns the rest and the mode past the cutoff k_c = 1 only
   decays, which is why one of the six is dashed and animates q-fade instead.

   The growth is scaleY about the interface, so transform-box: view-box is
   load-bearing: without it transform-origin resolves against each path's own
   bounding box and the modes grow about six different lines. */
.schem-q {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-q {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-q .q-axis { fill: none; stroke: var(--second); stroke-opacity: 0.4; stroke-width: 1; vector-effect: non-scaling-stroke; }
.schem-q .q-mode {
  fill: none;
  stroke: var(--second);
  stroke-width: 1.2;
  vector-effect: non-scaling-stroke;
  opacity: var(--mo, 1);
  transform-box: view-box;
  transform-origin: 50% 50%;
  animation: q-grow 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--bd, 0s);
}
.schem-q .qm-sec { --mo: 0.45; }
.schem-q .qm-acc { stroke: var(--accent); stroke-width: 2; --mo: 1; }
.schem-q .qm-s2 { stroke: var(--spec-2); stroke-width: 1.5; --mo: 0.8; }
.schem-q .qm-s3 { stroke: var(--spec-3); stroke-width: 1.5; --mo: 0.75; }
.schem-q .qm-st { stroke-dasharray: 2 5; --mo: 0.3; animation-name: q-fade; }

@keyframes q-grow {
  0%       { transform: scaleY(0.05); opacity: calc(var(--mo, 1) * 0.35); }
  55%, 78% { transform: scaleY(1); opacity: var(--mo, 1); }
  100%     { transform: scaleY(0.05); opacity: calc(var(--mo, 1) * 0.35); }
}
@keyframes q-fade {
  0%       { transform: scaleY(1); opacity: 0.3; }
  55%, 78% { transform: scaleY(0.3); opacity: 0.12; }
  100%     { transform: scaleY(1); opacity: 0.3; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-q .q-mode { animation: none; }
}

/* Variant O — "Abrikosov lattice". Flux vortices in a type-II superconductor
   frozen into their triangular ground state: 43 single-flux-quantum cores,
   each wrapped by a circulating supercurrent ring and bonded to six
   neighbours, with a field wave sweeping along a lattice diagonal.

   The rings carry TWO animations at once — the dash crawl and the wave — with
   two delays in one animation-delay list. The crawl's delay is fixed at 0s so
   every ring circulates in step; only the wave is staggered by --bd. Splitting
   them into separate rules is not possible: they are one shorthand. */
.schem-o {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-o {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-o .o-bond { fill: none; stroke: var(--second); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: 0.18; }
.schem-o .o-ring {
  fill: none;
  stroke: var(--second);
  stroke-width: 1.2;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1 1;
  opacity: 0.3;
  animation: o-crawl 5s linear infinite, o-wave 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: 0s, var(--bd, 0s);
}
/* Each ring takes the spectrum class of the core it wraps — the candidate drew
   all 43 in --second and only the cores in colour, which made the supercurrent
   read as scaffolding rather than as the thing carrying the flux. The pairing
   is by coordinate, not by document order, so the two groups can be
   regenerated independently without silently mismatching. */
.schem-o .o-ring.s1 { stroke: var(--spec-1); }
.schem-o .o-ring.s2 { stroke: var(--spec-2); }
.schem-o .o-ring.s3 { stroke: var(--spec-3); }
/* Last, so the seed ring stays --text whatever spectrum class it carries. */
.schem-o .o-ring.o-center { stroke: var(--text); }
.schem-o .o-core {
  animation: o-core 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--bd, 0s);
}
.schem-o .o-core.s1 { fill: var(--spec-1); }
.schem-o .o-core.s2 { fill: var(--spec-2); }
.schem-o .o-core.s3 { fill: var(--spec-3); }

/* -12 is six dash periods on a 1-1 dasharray, so the crawl wraps exactly at 5s
   rather than jumping on repeat. */
@keyframes o-crawl { to { stroke-dashoffset: -12; } }
@keyframes o-wave {
  0%, 100% { opacity: 0.24; }
  7%       { opacity: 0.7; }
  22%      { opacity: 0.24; }
}
@keyframes o-core {
  0%, 100% { opacity: 0.35; }
  7%       { opacity: 1; }
  22%      { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-o .o-ring,
  .schem-o .o-core { animation: none; }
}

/* Variant R — "caustic envelope". The coffee-cup caustic: 24 parallel rays
   reflect once inside a circular mirror, every reflected chord is tangent to
   the same envelope, and the nephroid condenses out of ray density alone
   rather than being drawn as a curve the rays are fitted to.

   The --bd delays are optical path length, not a cosmetic stagger — the pulse
   order down the rays IS the wavefront. The caustic's dash crawl is quantised
   the same way O's is: pathLength 12 against a 1.5 dash period is eight whole
   periods per loop.

   Its viewBox is 325 60 635 640, not the 1280x720 the other eight use — the
   composition is a circle, so on the shared canvas it sat inside ~86% of the
   height with 300 units of dead space either side. The box is now the content's
   own bounds plus ~10 units of margin, which is the largest the drawing can be
   without distorting it: `meet` will not stretch a near-square viewBox into a
   10/3 one. This does NOT change how the block is sized — the ratio still comes
   from --media-ratio on .media — only how much of that box the ink reaches. */
.schem-r {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-r {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-r .r-mirror { fill: none; stroke: var(--text); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: 0.4; }
/* .r-front — the dashed vertical wavefront at x=336 — is deliberately gone,
   element and rule both. It labelled where the parallel rays enter, but at this
   scale it read as a stray rule down the left of the drawing rather than as
   optics, and the ray staggers already carry the wavefront. */
.schem-r .r-in {
  fill: none;
  stroke: var(--second);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  opacity: 0.22;
  animation: r-in 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--bd, 0s);
}
.schem-r .r-out {
  fill: none;
  stroke: var(--second);
  stroke-width: 1.1;
  vector-effect: non-scaling-stroke;
  opacity: 0.5;
  animation: r-out 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--bd, 0s);
}
.schem-r .r-caustic {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 0.8 0.7;
  stroke-linecap: round;
  opacity: 0.95;
  animation: r-crawl 5s linear infinite;
}
.schem-r .r-cusp { animation: r-cusp 5s cubic-bezier(0.45, 0, 0.55, 1) infinite; animation-delay: var(--bd, 0s); }
.schem-r .r-cusp.s1 { fill: var(--spec-1); }
.schem-r .r-cusp.s2 { fill: var(--spec-2); }
.schem-r .r-cusp.s3 { fill: var(--spec-3); }

@keyframes r-in {
  0%   { opacity: 0.22; }
  5%   { opacity: 0.65; }
  16%  { opacity: 0.22; }
  100% { opacity: 0.22; }
}
@keyframes r-out {
  0%, 8% { opacity: 0.5; }
  15%    { opacity: 0.9; }
  28%    { opacity: 0.5; }
  100%   { opacity: 0.5; }
}
@keyframes r-crawl {
  from { stroke-dashoffset: 0; }
  to   { stroke-dashoffset: -12; }
}
@keyframes r-cusp {
  0%, 18% { opacity: 0.5; }
  30%     { opacity: 1; }
  48%     { opacity: 0.5; }
  100%    { opacity: 0.5; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-r .r-in,
  .schem-r .r-out,
  .schem-r .r-caustic,
  .schem-r .r-cusp { animation: none; }
}

/* ── The fourth round: S, U ─────────────────────────────────────────────────
   S ships as authored. U's loop and tier opacities were retuned after review —
   see its block — so docs/candidate-u.css is the source received, not what
   renders; its SVG is still verbatim. */

/* Variant S — "Belousov-Zhabotinsky". Excitable-medium waves in a dish: three
   Archimedean spirals (r = pitch * phi / 2pi) rotate rigidly about their own
   cores, one full turn per loop, so each wavefront marches outward and the
   cycle closes exactly. Two target sources pulse their rings in radial order.

   Each spiral's transform-origin is INLINE on the element, as a percentage,
   because the three cores sit at three different points and the shared rule
   can only carry one origin. transform-box: view-box is what makes those
   percentages resolve against the canvas rather than each path's own box —
   without it the spirals precess about their bounding boxes and drift. */
.schem-s {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-s {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-s .s-rim { fill: none; stroke: var(--text); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: 0.4; }
.schem-s .s-spiral {
  fill: none;
  stroke: var(--second);
  stroke-width: 1.4;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
  opacity: 0.75;
  transform-box: view-box;
  animation: s-rot 5s linear infinite;
}
.schem-s .s-spiral.s-ccw { animation-name: s-rot-r; }
.schem-s .ss-acc { stroke: var(--accent); stroke-width: 2; opacity: 0.95; }
.schem-s .ss-s3 { stroke: var(--spec-3); stroke-width: 1.6; opacity: 0.85; }
.schem-s .s-ring {
  fill: none;
  stroke: var(--second);
  stroke-width: 1.2;
  vector-effect: non-scaling-stroke;
  opacity: 0.25;
  animation: s-ring 5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--bd, 0s);
}
.schem-s .st-s2 { stroke: var(--spec-2); }
.schem-s .s-dot { opacity: 0.9; }
.schem-s .sd-acc { fill: var(--spec-1); }
.schem-s .sd-s2 { fill: var(--spec-2); }
.schem-s .sd-s3 { fill: var(--spec-3); }
.schem-s .sd-sec { fill: var(--second); }

@keyframes s-rot { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes s-rot-r { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } }
@keyframes s-ring {
  0%   { opacity: 0.25; }
  10%  { opacity: 0.9; }
  30%  { opacity: 0.25; }
  100% { opacity: 0.25; }
}

@media (prefers-reduced-motion: reduce) {
  .schem-s .s-spiral,
  .schem-s .s-ring { animation: none; }
}

/* Variant U — "electron diffraction". A quasicrystal's tenfold Bragg pattern:
   every spot is a sum over the five star vectors and its radius is the true
   intensity rule, so the 310 spots are computed rather than placed. A shimmer
   travels outward from the central beam, ordered by |k|.

   310 opacity animations is the highest element count of any variant here
   except D, and like D it has NOT been measured on a throttled device. Nothing
   animates position or paint, only opacity, so it should composite — that is a
   reasoned expectation, not a measurement.

   --uo is each tier's resting opacity, and the keyframes multiply it rather
   than replacing it, which is what keeps the four tiers distinguishable at the
   bottom of the pulse instead of collapsing to one value.

   Retuned after review, twice over the candidate's values:

   1. 1.8s, not 5s. The --bd stagger spans 1.46s, which is what sets the
      shortest usable period — under about 1.5s the delays wrap past each other
      and the shimmer stops reading as one wave travelling outward and starts
      reading as noise. 1.8s keeps the order intact with the sweep occupying
      most of the period, so the pattern is almost always in motion somewhere.
      The --bd values are NOT rescaled; they are absolute seconds and the whole
      point of the number above is that they still fit.
   2. The three chromatic tiers sit near full opacity and the --second tier was
      pulled back below its authored 0.5, so the colour leads the pattern
      instead of sharing it with 110 grey spots. The intensity hierarchy is
      unchanged in ORDER — t1 > t2 > t3 > t4 still holds, the gaps are just
      redistributed. */
.schem-u {
  display: block; width: 100%; height: 100%;
  --spec-1: var(--accent);
  --spec-2: var(--accent);
  --spec-3: var(--accent);
}
@supports (color: oklch(from red l c h)) {
  .schem-u {
    --spec-2: oklch(from var(--accent) l c calc(h + 130));
    --spec-3: oklch(from var(--accent) l c calc(h + 260));
  }
}

.schem-u .u-beam { fill: var(--text); opacity: 0.9; }
.schem-u .u-halo { fill: none; stroke: var(--text); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: 0.3; }
.schem-u .u-dot {
  opacity: var(--uo, 0.8);
  animation: u-pulse 1.8s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: var(--bd, 0s);
}
.schem-u .u-dot.t1 { fill: var(--spec-1); --uo: 1; }
.schem-u .u-dot.t2 { fill: var(--spec-2); --uo: 0.95; }
.schem-u .u-dot.t3 { fill: var(--spec-3); --uo: 0.88; }
.schem-u .u-dot.t4 { fill: var(--second); --uo: 0.4; }

@keyframes u-pulse {
  0%   { opacity: calc(var(--uo, 0.8) * 0.5); }
  9%   { opacity: var(--uo, 0.8); }
  30%  { opacity: calc(var(--uo, 0.8) * 0.5); }
  100% { opacity: calc(var(--uo, 0.8) * 0.5); }
}

@media (prefers-reduced-motion: reduce) {
  .schem-u .u-dot { animation: none; }
}

/* Row structure is featured_video's, reused rather than re-authored. The two
   variants are the editor's width control; the height comes off --media-ratio on
   .media, set per row, the same mechanism doois_media() uses everywhere else. */
.hero-schematic__row { display: grid; grid-template-columns: var(--rail) minmax(0, 1fr); gap: var(--gap); align-items: start; }
.hero-schematic__row--full { grid-template-columns: minmax(0, 1fr); }
.hero-schematic__row--half { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.hero-schematic__row--half .hero-schematic__text { max-width: min(46ch, 100%); }
.hero-schematic__text { margin: 20px 0 0; text-wrap: pretty; }
/* The section never exceeds three quarters of the fold, its own padding counted.
   The ratio decides the height wherever it fits, which is everywhere but a short
   viewport; there the box caps and the drawing scales down inside it. It never
   crops — preserveAspectRatio is `meet`, and the background is transparent, so
   the slack is section padding rather than a visible letterbox. */
.hero-schematic__media .media { max-height: calc(75svh - var(--pad-y-dark) * 2); }
/* Width `bleed` — the drawing edge to edge, no gutter, no vertical padding, no
   ratio-driven slack. The partial emits the media outside .container for this
   width; what is left to remove is the section's own padding-block, which
   .is-dark sets to 120px and which is the gap the editor sees above and below.

   The cap is dropped with it. 75svh minus padding is what keeps a drawing from
   running past a fold when it sits INSIDE the page rhythm; a bleed band is a
   full-width image, and capping it would reintroduce exactly the slack this
   width exists to remove. The ratio alone decides the height.

   Only thicket is drawn to survive this. Every other variant is `meet` on a
   transparent background, so at a box ratio away from its canvas it centres and
   leaves the surplus as empty fill — a bleed band with margins painted into it.
   The field allows the combination rather than policing it: the editor sees the
   result immediately, and hard-coding which variant may bleed would put the
   drawing list in two places. */
.hero-schematic--bleed { padding-block: 0; }
.hero-schematic--bleed .hero-schematic__media .media { max-height: none; }
.hero-schematic--bleed .hero-schematic__caption { padding-block: var(--pad-y-dark) 32px; }
/* Below 1024 the rail is 100% anyway, so every width collapses to one column. */
@media (max-width: 1023px) { .hero-schematic__row, .hero-schematic__row--half { grid-template-columns: minmax(0, 1fr); } }

/* schematic_text — the same drawing on text_media_block's grid.

   .tmb supplies the two columns, the 80px gutter, the centred alignment and the
   below-1024 stack, so none of that is repeated here. Three declarations are all
   this block adds:

   1. The media column is a schematic, not a 4:3 video, so the ratio comes off
      --media-ratio per row exactly as it does for hero_schematic.
   2. The fold cap is hero_schematic's, restated for this grid rather than
      inherited — the drawing is half as wide here, so it is already well short
      of the cap at every ratio, but a future ratio that is not must behave the
      same way in both blocks.
   3. The prose column reads at a measure. .tmb has no max-width of its own
      because its text column is already half of a 1440px container; at the
      widths this block ships at that is ~52ch, which is the measure, so this is
      a floor for wide viewports rather than a change to the common case. */
.section--schematic-text .tmb__media--schematic .media {
    max-height: calc(75svh - var(--pad-y-dark) * 2);
}
.section--schematic-text .tmb__body > .prose { max-width: min(62ch, 100%); }
/* The numeral sits above the heading rather than in a gutter — this grid has no
   rail to put it in. Only ever visible in a run, which is not the shape the
   block is for; it is here so a run does not render the numeral flush against
   the H2. */
.section--schematic-text .tmb__body > .numeral { display: block; margin-bottom: 20px; }

/* ---------------------------------------------------------------- 09 motion */
/* One easing curve, nothing longer than 400ms. Written, not decorative. */
[data-reveal] {
    opacity: 0;
    transform: translateY(12px);
    transition: opacity 400ms var(--ease), transform 400ms var(--ease);
    transition-delay: var(--reveal-delay, 0ms);
}
[data-reveal].is-revealed { opacity: 1; transform: none; }

/* ------------------------------------------------------------------ 10 a11y */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Everything honours prefers-reduced-motion by resolving to its END STATE
   instantly — not by being disabled halfway. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
    [data-reveal] { opacity: 1; transform: none; }
    .accordion__item[data-open="true"] .accordion__answer { grid-template-rows: 1fr; }
    /* The poster is the whole treatment; main.js never mounts the video. */
    .media__poster { opacity: 1 !important; }
}

/* ═════════════════════════════════════════════════════════════════════════════
   Stage 3 templates — A2 archive · A3 single · A4 term headers · A5 search + 404
   ═════════════════════════════════════════════════════════════════════════════ */

/* A2 — the case index. "Row gap is twice the gutter so rows read as rows and the
   grid does not become a field of tiles." */
.grid--cards {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: var(--gutter);
    row-gap: 64px;
}
/* "Final row holds one card at 1/3 width, left-aligned, never stretched." A single
   result widened to the container reads as a hero and misrepresents the term's depth,
   so the columns are fixed rather than auto-fit. */
@media (max-width: 1023px) { .grid--cards { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 48px; } }
@media (max-width: 767px)  { .grid--cards { grid-template-columns: minmax(0, 1fr); column-gap: 0; row-gap: 40px; } }

.archive-head { margin-bottom: 40px; }

/* A2 filter bar — term LINKS, not a JS filter, so these are anchors that look like
   controls without pretending to be state. */
.filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 20px;
    padding: 18px 0;
    margin-bottom: 48px;
    border-block: 1px solid var(--rule);
}
.filter-bar__term { color: var(--second); transition: color 160ms var(--ease); }
.filter-bar__term:hover { color: var(--text); }
.filter-bar__term.is-current { color: var(--text); text-decoration: underline; text-underline-offset: 0.3em; }

/* A4 term header. "When the description is absent the H1 sits directly above the
   hairline and the count carries the context" — so the hairline is on the header, not
   on the description, and nothing is padded to fill the gap. */
.term-header { padding-bottom: 40px; border-bottom: 1px solid var(--rule); }
.term-header__count { margin-top: 12px; color: var(--second); }
.term-header__description { margin-top: 20px; max-width: min(56ch, 100%); }
.site-main > .term-header + .section { padding-top: var(--pad-y); }

/* A3 long-form. The measure is 680px in a 296px + rest split, and the article never
   uses the full 1280. */
.article__body { max-width: min(680px, 100%); font-size: 19px; line-height: 1.62; margin-top: 32px; }
.article__media { margin-top: 32px; max-width: min(680px, 100%); }
.article__meta { margin-top: 14px; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--second); display: grid; gap: 4px; }
.article__back { display: inline-block; margin-top: 28px; color: var(--accent); }
.article__back:hover { text-decoration: underline; text-underline-offset: 0.3em; }
@media (max-width: 767px) { .article__body { font-size: 18px; } }

/* THE rule that makes editor content survive: heading spacing is owned by the
   heading's OWN top margin, never by the previous element's bottom margin. An h4
   after an h2, an h3 with no h2 above it, two h2s in a row — each still spaces
   correctly, because no rule here depends on what came before. Do not replace these
   with a sibling combinator. */
/* h1 is included deliberately. Stage 3 A3 assumed "the editor cannot reach H1" —
   true of the block list, false of the clipboard: one post's body carries a pasted
   <h1> and production renders it (RECONCILIATION.md §4.3b, FIXES.md F-10). It is
   reproduced rather than stripped, so it has to be styled, or it renders at the
   browser default — LARGER than the page title it sits beneath, which is the visual
   form of the same hierarchy error. Treated as an h2 throughout. */
.prose :where(h1, h2, h3, h4, h5, h6) { margin-bottom: 0; color: var(--text); }
.prose :where(h1, h2) { margin-top: 56px; font-size: 28px; line-height: 1.2; letter-spacing: -0.012em; }
.prose h3 { margin-top: 40px; font-size: 22px; line-height: 1.25; }
.prose h4 { margin-top: 32px; font-size: 19px; font-weight: 600; line-height: 1.4; }
.prose :where(h1, h2, h3, h4):first-child { margin-top: 0; }
/* The one exception, stated in Stage 3 A3: consecutive headings tighten, because two
   full top margins between an h2 and its immediate h3 reads as a gap, not a pair. */
.prose :where(h1, h2, h3, h4) + :where(h2, h3, h4) { margin-top: 20px; }
.prose :where(ul, ol) { padding-left: 1.25em; }
.prose li + li { margin-top: 0.4em; }
.prose blockquote { margin: 32px 0; padding-left: 24px; border-left: 2px solid var(--accent); color: var(--text); }
.prose img { max-width: 100%; height: auto; }
.prose :where(pre, code) { font-family: var(--font-mono); font-size: 0.9em; }
.prose pre { padding: 20px; background: var(--inset); overflow-x: auto; }


/* A5 search. */
.search-head { padding-bottom: 40px; border-bottom: 1px solid var(--rule); }
.search-head__count { margin-top: 12px; color: var(--second); }
.search-form { margin-top: 28px; max-width: min(560px, 100%); }
.search-form__label { display: block; color: var(--second); }
.search-form__field { display: flex; gap: 12px; margin-top: 10px; }
.search-form__input {
    flex: 1;
    min-width: 0;
    min-height: 44px;
    padding: 12px 16px;
    font: inherit;
    color: var(--text);
    background: var(--fill);
    border: 1px solid var(--rule);
}
.search-form__input:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }

/* The result row. One shape for all three types; the BADGE distinguishes them. */
.row--result { grid-template-columns: 132px minmax(0, 1fr) auto; }
.row__thumb { width: 200px; }
/* "Cases take accent because they are the highest-value result. This is the ONLY
   place in the design where a type is colour-coded, and it is coded once." */
.badge { display: inline-block; color: var(--second); }
.badge--accent { color: var(--accent); }
/* "Matched terms take a muted highlight, never accent and never bold — a long excerpt
   with three bolded matches becomes unreadable." */
.row__standfirst mark { background: var(--inset); color: inherit; font-weight: inherit; padding: 0 2px; }
@media (max-width: 767px) {
    .row--result { grid-template-columns: minmax(0, 1fr); }
    .row__thumb { display: none; }   /* A5: "thumb drops entirely below 768" */
}

/* Empty states — A2/A4/A5. "No illustration, no centred column. An empty archive is a
   routing problem, so it is answered with routes." */
.empty-archive { padding: 48px 0; max-width: min(62ch, 100%); }
.empty-archive__routes { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; margin-top: 28px; }
.search-suggestions { margin-top: 40px; }
.link { color: var(--accent); border-bottom: 1px solid currentColor; padding-bottom: 3px; }
.link:hover { color: var(--text); border-color: var(--text); }

/* 404 — routes from the menu, never strings. */
.error-404__routes { display: grid; gap: 0; margin-top: 36px; border-top: 1px solid var(--rule); max-width: min(560px, 100%); }
.error-404__route {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 24px;
    padding: 18px 0;
    border-bottom: 1px solid var(--rule);
    color: var(--text);
    transition: background-color 200ms var(--ease);
}
.error-404__route:hover { background: var(--inset); }
.error-404__route:hover .error-404__label { color: var(--accent); }
.error-404__label { transition: color 160ms var(--ease); }
.error-404__path { color: var(--second); }
