﻿/* ==========================================================================
   UAAI Solutions -- public site design system
   Rewritten 2026-08-12. Instrument vernacular.

   PHASE 1 (2026-08-12): TYPOGRAPHY MADE REAL.

   Until this pass the stylesheet asked for Inter and JetBrains Mono, neither
   of which was installed and neither of which was ever loaded. Every visitor
   saw Segoe UI and Cascadia Mono on Windows, and something else again on Mac
   or iOS. The design described here had never actually rendered.

   Fonts are now SELF-HOSTED from static/fonts. Deliberately not loaded from a
   third-party CDN: this site exists so that a senior executive can verify us
   after a warm introduction, and (a) leaking that visitor's IP to an ad-funded
   font host is the wrong posture for a firm selling data governance, (b)
   enterprise networks frequently block those CDNs, which is exactly where our
   buyer sits.

   Both faces are SIL Open Font License 1.1. The licences ship alongside them
   in static/fonts as OFL requires. Do not delete those files.

   WHY THIS LOOKS THE WAY IT DOES

   The first pass was near-black plus one accent plus rounded cards on a grid.
   That is the house style of every developer-tool site since 2021 and it
   reads as generic to exactly the buyers this site is for. Worse, it was
   decoration standing where the product should be.

   This version borrows from the visual grammar the buyers already read all
   day: instrument panels, well logs, nautical charts, one-line diagrams.

   RULES
     - No border-radius above 2px. Instruments have square corners.
     - Every displayed number carries a unit and a confidence.
     - Low confidence renders in amber. It is never hidden or rounded up.
     - No drop shadows, no gradients as decoration, no card hover lift.
     - Type sizes come from the SCALE below. Do not invent one-off sizes.
     - ASCII only in this file.
   ========================================================================== */

/* --- fonts, self-hosted ------------------------------------------------- */

@font-face {
    font-family: "InterVar";
    src: url("../fonts/InterVariable.woff2") format("woff2");
    font-weight: 100 900;   /* variable: one file covers every weight */
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "JetBrains Mono";
    src: url("../fonts/JetBrainsMono-Regular.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "JetBrains Mono";
    src: url("../fonts/JetBrainsMono-Medium.woff2") format("woff2");
    font-weight: 500;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "JetBrains Mono";
    src: url("../fonts/JetBrainsMono-Bold.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

:root {
    --ground:        #080b10;   /* page */
    --panel:         #0c1117;   /* instrument face */
    --panel-raised:  #111823;
    --rule:          #1c2632;   /* hairline */
    --rule-bright:   #2c3a4a;
    --grid:          rgba(120, 160, 200, 0.045);

    --ink:           #dfe7f0;
    --ink-mid:       #9fb0c2;
    --ink-dim:       #7a8a9c;   /* PHASE 2: was #6d7d8f, which measured 4.49
                                   on --panel and 4.23 on --panel-raised -
                                   both below AA 4.5. Now 5.58 / 5.04. */
    --conf-off:      #566a7e;   /* PHASE 2: unfilled confidence segments.
                                   Previously --rule-bright at 1.63 against
                                   the panel, which made the empty segments
                                   near-invisible - so the bar read SHORTER
                                   than the value it was reporting. A
                                   confidence indicator that misreports
                                   confidence is the one graphic here that
                                   cannot be allowed to be wrong. */
    --focus:         #dfe7f0;   /* Neutral by design. Amber already means
                                   "low confidence" in this language; reusing
                                   it for focus would overload the signal. */

    --signal:        #d29922;   /* amber - attention, low confidence */
    --instrument:    #58a6ff;   /* cyan - interactive, derived */
    --live:          #3fb950;
    --measured:      #58a6ff;
    --parameter:     #a371f7;

    --measure: 66ch;
    --pad-page: 2rem;
    --tick: 26px;

    --font-display: "InterVar", "Inter", "Segoe UI", system-ui, -apple-system,
                    sans-serif;
    --font-mono: "JetBrains Mono", "Cascadia Mono", "SFMono-Regular",
                 Consolas, monospace;

    /* --- TYPE SCALE -----------------------------------------------------
       One scale, used everywhere. Before this pass the stylesheet carried
       roughly twenty ad-hoc mono sizes between 0.66rem and 1.75rem, which is
       not a system, it is an accumulation.

       FLOOR IS 0.75rem / 12px. The old floor was 0.66rem, about 10.5px. This
       site is read by senior executives; a meaningful share of that audience
       has presbyopia, and 10.5px amber-on-near-black is the worst legibility
       combination available.
       -------------------------------------------------------------------- */
    --t-micro:  0.75rem;   /* 12px  - rail keys, roles, tick labels. FLOOR. */
    --t-small:  0.8125rem; /* 13px  - nav, status, captions, panel meta      */
    --t-base:   0.9375rem; /* 15px  - mono body, tag names, proof lines      */
    --t-body:   1.0625rem; /* 17px  - prose                                  */
    --t-lead:   1.1875rem; /* 19px  - lede paragraphs                        */
    --t-h3:     1.25rem;   /* 20px                                           */
    --t-h2:     1.5rem;    /* 24px                                           */
    --t-read:   1.75rem;   /* 28px  - large instrument readouts              */
    --t-h1:     2.25rem;   /* 36px  - interior page titles                   */

    /* Tracking. Mono needs positive tracking at small sizes to stay legible;
       display type needs negative tracking as it gets larger. */
    --track-mono:  0.06em;
    --track-label: 0.14em;
    --track-tight: -0.02em;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

/* --- accessibility: focus, motion, skip target --------------------------
   PHASE 2. Before this pass the stylesheet contained ZERO :focus rules, so a
   keyboard user got whatever the browser defaulted to - frequently invisible
   against a near-black ground.

   The progressive pattern below gives a visible ring to keyboard users while
   not drawing one for mouse users, and still works in browsers that do not
   support :focus-visible.
   ------------------------------------------------------------------------ */

:focus {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
}
:focus:not(:focus-visible) { outline: none; }
:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
}

/* Skip link. Off-screen until focused, then pinned top-left. Without this,
   a screen reader or keyboard user tabs the whole nav on every page. */
.skip {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 100;
    background: var(--signal);
    color: var(--ground);
    padding: 0.7rem 1.1rem;
    font-family: var(--font-mono);
    font-size: var(--t-small);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: var(--track-label);
}
.skip:focus {
    left: 0;
    text-decoration: none;
}

/* Respect an operating-system request for less motion. Covers the smooth
   scroll above and any transition in the sheet. twin.js reads the same
   preference and stops auto-refreshing the panel. */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

body {
    margin: 0;
    background: var(--ground);
    color: var(--ink);
    font-family: var(--font-display);
    font-size: var(--t-body);
    line-height: 1.62;
    -webkit-font-smoothing: antialiased;
    /* Inter is variable, so real weights exist across 100-900. Disable
       synthesis so the browser never fakes a weight it already has. */
    font-synthesis-weight: none;
    font-optical-sizing: auto;
    /* Short pages (platforms, contact) were leaving a void under the footer.
       Footer is pushed to the bottom rather than floating mid-viewport. */
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}
body > footer { margin-top: auto; }

/* TABULAR FIGURES on anything that is a reading.
   Proportional digits change width as a value updates, so a live panel
   twitches sideways every poll. Instruments do not do that. */
.tag-val, .figure .n, .conf, .rail-row .v, .proof {
    font-variant-numeric: tabular-nums;
}

a { color: var(--instrument); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3 {
    font-weight: 620;
    letter-spacing: var(--track-tight);
    /* 620 is a real weight now that Inter is loaded as a variable font. It
       was previously rounded to whatever the fallback face had. */
}

.wrap { max-width: 1220px; margin: 0 auto; padding: 0 var(--pad-page); }

/* .wrap.narrow used to centre a 820px column, which left interior page body
   copy starting 270px to the right of the hero headline above it. Two
   different left edges on one page reads as broken. Content now runs in a
   grid that keeps the SAME left edge as the hero, with the leftover width
   used for an annotation rail instead of being dead space. */
.wrap.narrow { max-width: 1220px; }

.layout {
    display: grid;
    grid-template-columns: minmax(0, 68ch) minmax(200px, 300px);
    gap: 4rem;
    align-items: start;
}
.layout > .col-main { min-width: 0; }

/* --- annotation rail --------------------------------------------------- */
/* Reads as the title block on a drawing: what this is, where the data comes
   from, what state it is in. Mono, hairline, no prose. */

.rail {
    position: sticky; top: 78px;
    border-top: 1px solid var(--rule-bright);
    font-family: var(--font-mono); font-size: var(--t-micro);
}
.rail-title {
    text-transform: uppercase; letter-spacing: 0.14em;
    color: var(--ink-dim); padding: 0.7rem 0;
    border-bottom: 1px solid var(--rule);
}
.rail-row {
    display: grid; grid-template-columns: 1fr auto; gap: 0.6rem;
    padding: 0.65rem 0;
    border-bottom: 1px solid var(--rule);
    color: var(--ink-dim);
}
.rail-row .v { color: var(--ink-mid); text-align: right; }
.rail-row .v.ok { color: var(--live); }
.rail-row .v.warn { color: var(--signal); }
.rail-note {
    padding: 0.8rem 0; color: var(--ink-dim);
    line-height: 1.7; letter-spacing: 0.02em;
}

/* --- measurement grid, used behind the hero only ----------------------- */

.gridfield {
    background-image:
        repeating-linear-gradient(to right, var(--grid) 0 1px, transparent 1px var(--tick)),
        repeating-linear-gradient(to bottom, var(--grid) 0 1px, transparent 1px var(--tick));
    background-position: center top;
}

/* --- header ------------------------------------------------------------ */

.site-head {
    border-bottom: 1px solid var(--rule);
    position: sticky; top: 0; z-index: 20;
    background: rgba(8, 11, 16, 0.94);
    backdrop-filter: blur(6px);
}
.site-head .wrap {
    display: flex; align-items: center; justify-content: space-between;
    height: 58px;
}
.brand {
    font-family: var(--font-mono);
    font-weight: 700; font-size: var(--t-base);
    letter-spacing: 0.06em; color: var(--ink);
}
.brand em { font-style: normal; color: var(--signal); }
.nav a {
    /* PHASE 2: inline-flex with a minimum height so the tap target reaches a
       usable size on touch. Previously a 13px inline text run, well under the
       24x24 minimum and awkward on a phone. */
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    font-family: var(--font-mono);
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: var(--track-label);
    color: var(--ink-dim);
    margin-left: 1.9rem;
    border-bottom: 2px solid transparent;
}
.nav a:hover {
    color: var(--ink);
    text-decoration: none;
    border-bottom-color: var(--signal);
}

/* --- hero -------------------------------------------------------------- */

.hero {
    border-bottom: 1px solid var(--rule);
    padding: 4.5rem 0 4rem;
}
.hero .wrap {
    display: grid;
    grid-template-columns: 1.02fr 0.98fr;
    gap: 3.5rem;
    align-items: start;
}
.hero h1 {
    font-size: clamp(2.4rem, 5.6vw, 4rem);
    line-height: 1.04; margin: 0 0 1.4rem; max-width: 14ch;
}
.hero .lede {
    font-size: var(--t-body); color: var(--ink-mid);
    max-width: 50ch; margin: 0 0 2.2rem;
}
.hero-compact { padding: 3.2rem 0 2.4rem; }
.hero-compact .wrap { display: block; }

.eyebrow {
    font-family: var(--font-mono); font-size: var(--t-micro);
    text-transform: uppercase; letter-spacing: 0.18em;
    color: var(--signal); margin-bottom: 1.1rem;
    display: flex; align-items: center; gap: 0.7rem;
}
.eyebrow::after {
    content: ""; flex: 1; height: 1px; background: var(--rule);
    max-width: 120px;
}

/* --- instrument panel: the twin readout -------------------------------- */

.panel {
    border: 1px solid var(--rule-bright);
    background: var(--panel);
}
.panel-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; flex-wrap: wrap;
    padding: 0.7rem 1rem;
    border-bottom: 1px solid var(--rule);
    background: var(--panel-raised);
    font-family: var(--font-mono); font-size: var(--t-micro);
    letter-spacing: 0.08em; color: var(--ink-dim);
}
.panel-head .urn { color: var(--ink-mid); word-break: break-all; }
.panel-body { padding: 0.35rem 0 0.2rem; }
.panel-foot {
    border-top: 1px solid var(--rule);
    padding: 0.7rem 1rem;
    font-family: var(--font-mono); font-size: var(--t-micro);
    color: var(--ink-dim); line-height: 1.65;
}

/* feed status lamp */
.lamp {
    display: inline-flex; align-items: center; gap: 0.45rem;
    text-transform: uppercase; letter-spacing: 0.12em;
}
.lamp::before {
    content: ""; width: 6px; height: 6px; background: currentColor;
}
.lamp-nominal  { color: var(--live); }
.lamp-partial  { color: var(--signal); }
.lamp-degraded { color: var(--signal); }
.lamp-none     { color: var(--ink-dim); }

/* one tag row */
.tag {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.15rem 1rem;
    padding: 0.62rem 1rem;
    border-bottom: 1px solid var(--rule);
}
.tag:last-child { border-bottom: 0; }
.tag-name {
    font-family: var(--font-mono); font-size: var(--t-small);
    color: var(--ink-dim); word-break: break-all;
}
.tag-val {
    font-family: var(--font-mono);
    font-size: var(--t-h3); line-height: 1.2;
    color: var(--ink); text-align: right; white-space: nowrap;
    grid-row: span 2;
    align-self: center;
}
.tag-val .unit {
    font-size: var(--t-micro); color: var(--ink-dim); margin-left: 0.35rem;
}
.tag-meta {
    display: flex; align-items: center; gap: 0.6rem;
    font-family: var(--font-mono); font-size: var(--t-micro);
    letter-spacing: 0.07em; color: var(--ink-dim);
}
.tag.is-low .tag-val { color: var(--signal); }

.role {
    text-transform: uppercase; letter-spacing: 0.1em;
    padding: 0 0.3rem; border: 1px solid var(--rule-bright);
}
.role-derived   { color: var(--instrument); }
.role-measured  { color: var(--measured); }
.role-parameter { color: var(--parameter); }

/* confidence: ten segments, filled. amber below 50 percent. */
.conf { display: inline-flex; align-items: center; gap: 0.45rem; }
.conf-bar { display: inline-flex; gap: 2px; }
.conf-seg {
    width: 5px; height: 8px;
    background: var(--conf-off);
}
.conf-seg.on { background: var(--live); }
.is-low .conf-seg.on { background: var(--signal); }

/* --- big derived figures ----------------------------------------------- */

.figures {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    border-top: 1px solid var(--rule);
    border-left: 1px solid var(--rule);
}
.figure {
    border-right: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
    padding: 1.15rem 1.2rem;
}
.figure .n {
    font-family: var(--font-mono); font-size: var(--t-read);
    letter-spacing: -0.02em; color: var(--ink); display: block;
    line-height: 1.15;
}
.figure .n .unit { font-size: var(--t-small); color: var(--ink-dim); }
.figure .k {
    font-family: var(--font-mono); font-size: var(--t-micro);
    text-transform: uppercase; letter-spacing: 0.12em;
    color: var(--ink-dim); margin-top: 0.4rem; display: block;
}

/* --- sections ---------------------------------------------------------- */

section { padding: 4.2rem 0; border-bottom: 1px solid var(--rule); }
section h2 { font-size: var(--t-read); margin: 0 0 0.8rem; }
section h2:first-child { margin-top: 0; }
section p.sub { color: var(--ink-mid); max-width: var(--measure); margin: 0 0 2rem; }

/* section index label, reads like a drawing callout */
.slabel {
    font-family: var(--font-mono); font-size: var(--t-micro);
    text-transform: uppercase; letter-spacing: 0.16em;
    color: var(--ink-dim); margin-bottom: 1.5rem;
    display: flex; align-items: center; gap: 0.9rem;
}
.slabel .idx { color: var(--signal); }
.slabel::after {
    content: ""; flex: 1; height: 1px;
    background: repeating-linear-gradient(to right,
        var(--rule) 0 6px, transparent 6px 12px);
}

/* --- mimic board: the platform list ------------------------------------ */
/* Rows, not cards. A mimic board is how a control room lists plant items,
   and it scales to any number of verticals without a ragged grid. */

.mimic { border-top: 1px solid var(--rule); }
.mimic-row {
    display: grid;
    grid-template-columns: 3.2rem minmax(0, 15rem) minmax(0, 1fr) 7.5rem;
    gap: 1.5rem; align-items: baseline;
    padding: 1.15rem 0.9rem 1.15rem 0;
    border-bottom: 1px solid var(--rule);
    border-left: 2px solid transparent;
    color: inherit;
}
a.mimic-row:hover {
    text-decoration: none;
    background: var(--panel);
    border-left-color: var(--signal);
    padding-left: 0.9rem;
}
.mimic-idx {
    font-family: var(--font-mono); font-size: var(--t-small);
    color: var(--ink-dim); padding-left: 0.9rem;
}
a.mimic-row:hover .mimic-idx { padding-left: 0; }
.mimic-name { font-size: var(--t-body); font-weight: 600; letter-spacing: -0.01em; }
.mimic-desc { color: var(--ink-mid); font-size: var(--t-base); }
.mimic-state { justify-self: end; }

.status {
    display: inline-flex; align-items: center; gap: 0.45rem;
    font-family: var(--font-mono); font-size: var(--t-micro);
    text-transform: uppercase; letter-spacing: 0.12em;
    color: var(--live);
}
.status::before { content: ""; width: 6px; height: 6px; background: currentColor; }
.status-review { color: var(--signal); }

/* --- body copy and detail pages ---------------------------------------- */

.body-copy {
    font-size: var(--t-body); color: var(--ink-mid);
    max-width: var(--measure); margin: 0 0 1.5rem;
}

/* proof line: reads as an annotated measurement, not a pull quote */
.proof {
    font-family: var(--font-mono); font-size: var(--t-base); line-height: 1.7;
    color: var(--signal);
    border-left: 2px solid var(--signal);
    background: var(--panel);
    padding: 0.9rem 1.1rem; margin: 1.5rem 0;
    max-width: var(--measure);
}
.proof .k {
    display: block; color: var(--ink-dim); font-size: var(--t-micro);
    text-transform: uppercase; letter-spacing: 0.14em; margin-bottom: 0.35rem;
}

/* Visible marker so uncleared copy cannot ship by accident. */
.placeholder {
    display: inline-block;
    background: #2a1d00; color: var(--signal);
    border: 1px dashed var(--signal);
    font-family: var(--font-mono); font-size: var(--t-micro);
    letter-spacing: 0.06em;
    padding: 0.25rem 0.55rem;
}

/* points list, styled as a spec schedule */
.points { list-style: none; padding: 0; margin: 2rem 0; max-width: var(--measure); }
.points li {
    display: grid; grid-template-columns: 2.4rem 1fr; gap: 1rem;
    padding: 0.7rem 0;
    border-bottom: 1px solid var(--rule);
    color: var(--ink-mid); font-size: var(--t-base);
}
.points li::before {
    content: counter(pt, decimal-leading-zero);
    counter-increment: pt;
    font-family: var(--font-mono); font-size: var(--t-micro); color: var(--ink-dim);
}
.points { counter-reset: pt; }

/* --- buttons ----------------------------------------------------------- */

.btn {
    display: inline-block; padding: 0.62rem 1.25rem;
    font-family: var(--font-mono);
    font-size: var(--t-small); text-transform: uppercase; letter-spacing: 0.12em;
    border: 1px solid var(--rule-bright); border-radius: 0;
    color: var(--ink);
}
.btn:hover { text-decoration: none; border-color: var(--signal); color: var(--signal); }
.btn-primary { border-color: var(--signal); color: var(--signal); }
.btn-primary:hover { background: var(--signal); color: var(--ground); }
.btn-ghost { margin-left: 0.6rem; color: var(--ink-mid); }

/* --- footer ------------------------------------------------------------ */

.site-foot {
    padding: 2.5rem 0; color: var(--ink-dim);
    font-family: var(--font-mono); font-size: var(--t-micro); letter-spacing: 0.06em;
}
.site-foot .wrap { display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }

/* --- schematic ---------------------------------------------------------- */
/* Inline SVG only. Drawn, not generated - a generated "architecture" image
   with invented boxes would be the same mistake as a fake dashboard. */

.schematic {
    border: 1px solid var(--rule-bright);
    background: var(--panel);
    padding: 1.6rem 1.2rem;
    margin: 2rem 0;
    overflow-x: auto;
}
.schematic svg { display: block; width: 100%; height: auto; min-width: 640px; }
.sc-box { fill: var(--panel-raised); stroke: var(--rule-bright); stroke-width: 1; }
.sc-box-key { fill: #16202c; stroke: var(--signal); stroke-width: 1; }
.sc-label {
    fill: var(--ink); font-family: var(--font-mono); font-size: 11px;
    letter-spacing: 0.06em;
}
.sc-sub {
    fill: var(--ink-dim); font-family: var(--font-mono); font-size: 9px;
    letter-spacing: 0.05em;
}
.sc-cap {
    fill: var(--signal); font-family: var(--font-mono); font-size: 9px;
    letter-spacing: 0.14em;
}
.sc-flow { stroke: var(--rule-bright); stroke-width: 1; fill: none; }
.sc-flow-live { stroke: var(--signal); stroke-width: 1; fill: none; }
.sc-tick { stroke: var(--rule); stroke-width: 1; }
.sc-seam { stroke: var(--signal); stroke-width: 1; stroke-dasharray: 3 4; }

/* --- responsive -------------------------------------------------------- */

@media (max-width: 980px) {
    .hero .wrap { grid-template-columns: 1fr; gap: 2.5rem; }
    .hero h1 { max-width: 18ch; }
    .layout { grid-template-columns: 1fr; gap: 2.5rem; }
    .rail { position: static; }
    .mimic-row { grid-template-columns: 2.4rem 1fr; gap: 0.4rem 1rem; }
    .mimic-desc { grid-column: 2; }
    .mimic-state { grid-column: 2; justify-self: start; margin-top: 0.3rem; }
}

@media (max-width: 640px) {
    :root { --pad-page: 1.15rem; }
    .nav a { margin-left: 1rem; font-size: var(--t-micro); letter-spacing: 0.08em; }
    .hero { padding: 2.8rem 0 2.4rem; }
    .tag-val { font-size: var(--t-h3); }
    .figure .n { font-size: var(--t-h2); }
}

/* ==========================================================================
   PHASE 3a -- PAPER SURFACE (2026-08-12)

   A SECOND FULL PALETTE, not a background swap. Measured: every dark accent
   collapses on a light ground -

       signal     #d29922   2.33 : 1   on paper
       instrument #58a6ff   2.33 : 1
       live       #3fb950   2.35 : 1
       parameter  #a371f7   3.10 : 1

   All well under AA. So the light surface needs its own accents, darkened
   until they pass on white, and every value below has been measured rather
   than picked by eye.

   STATUS: PREVIEW, NOT SHIPPED. Marketing Plan 10.1 asks for an engineering
   standards body or a specialist instrument maker, and those identities are
   overwhelmingly light and paper-like. Dark-on-near-black is the developer
   tool convention this site set out to avoid, so it is worth looking at the
   alternative honestly before committing.

   Preview with ?surface=paper on any page. This is a comparison mechanism for
   a decision, NOT a user-facing theme toggle - shipping a toggle would give
   two different brand impressions and put the choice on the visitor.
   ========================================================================== */

html[data-surface="paper"] {
    --ground:        #f5f6f7;
    --panel:         #ffffff;
    --panel-raised:  #eef1f4;
    --rule:          #d5dae0;
    --rule-bright:   #7a8794;
    --grid:          rgba(40, 70, 100, 0.055);

    --ink:           #11181f;   /* 16.53 on paper */
    --ink-mid:       #3c4a58;   /*  8.39 */
    --ink-dim:       #5b6a79;   /*  5.13 */
    --conf-off:      #7a8794;   /*  3.39 - clears the 3:1 non-text threshold */
    --focus:         #11181f;

    --signal:        #8a5d00;   /*  5.32 - amber darkened until it passes */
    --instrument:    #0b5cad;   /*  6.16 */
    --live:          #1a7f37;   /*  4.69 */
    --measured:      #0b5cad;
    --parameter:     #6335c4;   /*  6.84 */
}

/* The two places that hard-code a colour outside the token set. */
html[data-surface="paper"] .placeholder { background: #fdf4e0; }
html[data-surface="paper"] .sc-box-key  { fill: #fdf4e0; }
html[data-surface="paper"] .skip        { color: #ffffff; }


/* ==========================================================================
   PHASE 3b -- PRINT

   Executives print things and forward PDFs. Printed dark, this site is either
   a black slab that empties a toner cartridge, or - because most browsers
   drop backgrounds by default - light text on white, i.e. invisible. Either
   outcome destroys the credibility the screen version earned.

   Print therefore forces the paper palette regardless of the screen surface,
   and strips anything that is navigation rather than content.
   ========================================================================== */

@media print {
    :root {
        --ground:       #ffffff;
        --panel:        #ffffff;
        --panel-raised: #ffffff;
        --rule:         #b8bfc7;
        --rule-bright:  #6b7783;
        --grid:         transparent;
        --ink:          #000000;
        --ink-mid:      #1a1a1a;
        --ink-dim:      #3a3a3a;
        --conf-off:     #b0b0b0;
        --signal:       #7a5200;
        --instrument:   #0a4f96;
        --live:         #15682d;
        --parameter:    #522ba3;
        --measured:     #0a4f96;
    }

    @page { margin: 18mm 16mm; }

    body {
        background: #ffffff;
        color: #000000;
        font-size: 10.5pt;
        line-height: 1.45;
    }

    /* Navigation is not content. */
    .site-head, .skip, .btn { display: none !important; }

    /* Backgrounds and rules that only made sense on a dark ground. */
    .gridfield { background-image: none; }
    .panel, .schematic, .proof, .figures, .figure,
    .rail, .mimic, .mimic-row, .tag {
        background: #ffffff;
        border-color: #b8bfc7;
    }

    /* Let the content use the full page - the annotation rail stacks under
       the body column rather than fighting it for width. */
    .layout { display: block; }
    .rail { position: static; margin-top: 8mm; border-top: 1pt solid #6b7783; }
    .wrap { max-width: none; padding: 0; }
    .hero { padding: 0 0 6mm; }

    /* Keep readings and their labels together across a page break. */
    .tag, .figure, .rail-row, .proof, .mimic-row { break-inside: avoid; }
    .panel, .schematic { break-inside: avoid; }
    h1, h2, h3 { break-after: avoid; }
    section { break-inside: auto; padding: 6mm 0; border-bottom: 0; }

    /* A printed page loses the href, so put it back - external links only.
       Internal ones would add noise without adding information. */
    a[href^="http"]::after {
        content: " <" attr(href) ">";
        font-family: var(--font-mono);
        font-size: 8pt;
        color: #3a3a3a;
        word-break: break-all;
    }

    /* Uncleared copy must remain unmistakable on paper too. */
    .placeholder {
        background: #ffffff;
        border: 1pt dashed #7a5200;
        color: #7a5200;
    }
}

/* ==========================================================================
   PHASE 4 -- SYSTEMATISED (2026-08-12)

   SPACING SCALE. Colour and type were tokenised; spacing was not, so margins
   were being invented per use site - 0.9rem here, 2.75rem there - and eight
   of them had leaked into template markup as inline styles.

   Same rule as the type scale: take a value from the scale. If nothing fits,
   the scale is wrong and should be changed here, not worked around inline.

   ALSO A WARNING FOR FUTURE AUDITS. Several class names in this sheet are
   COMPOSED AT RUNTIME and will not appear literally in any template:

       .lamp-nominal .lamp-partial .lamp-degraded   from lamp-{{ feed_status }}
       .role-derived .role-measured .role-parameter from role-{{ r.role }}
       .status-review                               from a Jinja conditional
       .ok .warn                                    from the rail tone field

   A naive "unused selector" sweep reports all of these as dead. Deleting them
   silently breaks the confidence roles and the feed lamp - the two things on
   this site that must never lie.
   ========================================================================== */

:root {
    --sp-1: 0.25rem;
    --sp-2: 0.5rem;
    --sp-3: 0.75rem;
    --sp-4: 1rem;
    --sp-5: 1.5rem;
    --sp-6: 2rem;
    --sp-7: 3rem;
    --sp-8: 4rem;
}

/* A seam block: prose column plus its annotation rail. */
.seam { margin-bottom: var(--sp-7); }
.seam-lede {
    font-size: var(--t-h3);
    margin: 0 0 var(--sp-2);
}

/* A proof-domain group: label, blurb, then the mimic rows. */
.domain-group { margin-bottom: var(--sp-7); }
.domain-group > .slabel { margin-bottom: var(--sp-3); }
.domain-group > .sub { margin-bottom: var(--sp-4); }

/* A sub-heading inside a body column - smaller than a section h2 and
   spaced to sit with the paragraph it introduces. */
.h-inline {
    font-size: var(--t-h3);
    margin: var(--sp-5) 0 var(--sp-1);
}

/* A figure whose value is a hash or identifier rather than a quantity, so it
   needs to sit at reading size instead of readout size. */
.figure .n-id { font-size: var(--t-body); }
/* ==========================================================================
   PHASE 5 -- PER-DOMAIN CHARACTER (2026-08-12)

   Flagged in the first redesign and not delivered until now: every vertical
   page was visually identical, which quietly says the domains are
   interchangeable. They are not, and the people who work in them read very
   different instruments.

   Each domain hero gets ONE motif drawn from its own vernacular. Deliberately
   restrained - pure CSS gradients, no images, low alpha, sitting behind the
   measurement grid rather than replacing it. This is orientation, not
   decoration: it should register without being noticed.

   Applied as data-domain on the hero. Unknown or missing values simply fall
   back to the plain gridfield, so a new vertical never breaks the page.
   ========================================================================== */

/* Energy - a one-line diagram. Heavier horizontal rules at intervals, the
   way a single-line electrical drawing reads. */
.hero[data-domain="energy"] {
    background-image:
        repeating-linear-gradient(to bottom,
            rgba(210, 153, 34, 0.055) 0 1px, transparent 1px 78px),
        repeating-linear-gradient(to right, var(--grid) 0 1px, transparent 1px var(--tick)),
        repeating-linear-gradient(to bottom, var(--grid) 0 1px, transparent 1px var(--tick));
}

/* Life sciences - a plate. Regular square cells, larger than the base grid. */
.hero[data-domain="life-sciences"] {
    background-image:
        repeating-linear-gradient(to right,
            rgba(88, 166, 255, 0.05) 0 1px, transparent 1px 52px),
        repeating-linear-gradient(to bottom,
            rgba(88, 166, 255, 0.05) 0 1px, transparent 1px 52px),
        repeating-linear-gradient(to right, var(--grid) 0 1px, transparent 1px var(--tick)),
        repeating-linear-gradient(to bottom, var(--grid) 0 1px, transparent 1px var(--tick));
}

/* Built environment - section hatching, as on a construction drawing. */
.hero[data-domain="built-environment"] {
    background-image:
        repeating-linear-gradient(45deg,
            rgba(163, 113, 247, 0.045) 0 1px, transparent 1px 14px),
        repeating-linear-gradient(to right, var(--grid) 0 1px, transparent 1px var(--tick)),
        repeating-linear-gradient(to bottom, var(--grid) 0 1px, transparent 1px var(--tick));
}

/* Open demonstration - a chart. Crossed bearing lines, as a nautical chart
   carries compass roses and rhumb lines. */
.hero[data-domain="demonstration"] {
    background-image:
        repeating-linear-gradient(30deg,
            rgba(63, 185, 80, 0.04) 0 1px, transparent 1px 34px),
        repeating-linear-gradient(-30deg,
            rgba(63, 185, 80, 0.04) 0 1px, transparent 1px 34px),
        repeating-linear-gradient(to right, var(--grid) 0 1px, transparent 1px var(--tick)),
        repeating-linear-gradient(to bottom, var(--grid) 0 1px, transparent 1px var(--tick));
}


/* ==========================================================================
   PHASE 5 -- MOTION DISCIPLINE

   The only motion on this site marks a STATE CHANGE: when the twin panel
   polls and a value actually changes, that value flashes once so the eye is
   told where to look. Nothing moves for decoration.

   The reduced-motion block earlier in this sheet neutralises the transition,
   and twin.js stops polling entirely under the same preference - so this
   degrades to a static, correct panel rather than to a broken one.
   ========================================================================== */

.tag-val { transition: color 140ms linear, background-color 140ms linear; }

.tag-val.changed {
    color: var(--ground);
    background-color: var(--signal);
    box-shadow: 0 0 0 3px var(--signal);
}

/* ==========================================================================
   PHASE 5b -- ICONS, SECTION HEADS, AND PAGE TEXTURE (2026-08-12)

   The screen read as: hero panel, then prose in a void. Every section carried
   identical visual weight, nothing anchored the eye below the fold, and the
   measurement grid - the one motif the whole identity rests on - stopped at
   the hero. Three fixes, none of which add an image file.
   ========================================================================== */

/* --- icons -------------------------------------------------------------- */
/* Inherit colour from whatever they sit in, so there are no icon variants to
   maintain across the two surfaces or in print. */

.ico {
    display: inline-block;
    vertical-align: -0.15em;
    flex: 0 0 auto;
    color: inherit;
}

/* An icon set into a bordered square - used where an icon is the anchor for
   a block rather than an inline mark. */
.ico-frame {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    border: 1px solid var(--rule-bright);
    background: var(--panel);
    color: var(--signal);
    flex: 0 0 auto;
}

/* --- section head ------------------------------------------------------- */
/* .slabel gains an optional leading icon without changing its markup
   contract - the dashed rule still runs to the right edge. */

.slabel .ico { color: var(--signal); margin-right: 0.1rem; }

/* --- swap list ---------------------------------------------------------- */
/* The four replacements on the landing page. Was a plain numbered list; now
   an icon column, the claim, and the thing it replaces - which is the actual
   argument, so it should be legible as three columns rather than a sentence. */

.swaps { list-style: none; padding: 0; margin: var(--sp-5) 0; }
.swaps li {
    display: grid;
    grid-template-columns: 42px 1fr;
    gap: var(--sp-4);
    align-items: center;
    padding: var(--sp-4) 0;
    border-bottom: 1px solid var(--rule);
}
.swaps .claim { font-weight: 600; color: var(--ink); }
.swaps .was {
    display: block;
    font-family: var(--font-mono);
    font-size: var(--t-small);
    color: var(--ink-dim);
    letter-spacing: var(--track-mono);
    margin-top: 0.15rem;
}

/* --- credibility blocks ------------------------------------------------- */

.cred { display: grid; grid-template-columns: 42px 1fr; gap: var(--sp-4);
        margin: var(--sp-5) 0; align-items: start; }
.cred h3 { margin: 0 0 var(--sp-1); font-size: var(--t-h3); }
.cred p { margin: 0; }

/* --- page texture ------------------------------------------------------- */
/* The grid continues below the hero at a much lower alpha, alternating so it
   reads as a rhythm rather than wallpaper. Sections keep their own ground so
   the panel and rail still sit clearly on top. */

section.ruled {
    background-image:
        repeating-linear-gradient(to right,
            rgba(120, 160, 200, 0.022) 0 1px, transparent 1px 52px);
    background-position: center top;
}

/* A depth-scale rule down the left edge of a ruled section - the vertical
   tick strip a well log or a chart carries. */
section.ruled > .wrap { position: relative; }
section.ruled > .wrap::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 1px;
    background: repeating-linear-gradient(to bottom,
        var(--rule) 0 6px, transparent 6px 18px);
}

@media (max-width: 980px) {
    section.ruled > .wrap::before { display: none; }
    .swaps li, .cred { grid-template-columns: 32px 1fr; gap: var(--sp-3); }
    .ico-frame { width: 32px; height: 32px; }
}

@media print {
    .ico-frame { border-color: #6b7783; background: #ffffff; color: #000000; }
    section.ruled { background-image: none; }
    section.ruled > .wrap::before { display: none; }
}

/* ==========================================================================
   PHASE 6 -- HERO ART (2026-08-12)

   Interior hero right columns were empty: a headline top-left and several
   hundred pixels of void beside it on six of eight routes. The landing page
   is NOT in this set and must not be - its hero right column carries the twin
   panel, and decoration standing where the product stands is the failure this
   site argues against.

   WHY IT CANNOT MUDDY THE PAGE. mix-blend-mode: screen means every pixel of
   the art lightens the ground or does nothing at all - pure black contributes
   exactly zero. The generated assets are 4 to 37 mean luminance out of 255, so
   what actually survives the blend is the drawn line-work and nothing else.
   No box, no edge, no rectangle of slightly-different-black.

   THE MASK IS NOT DECORATION. It is what keeps the headline on a clean ground:
   the art is fully transparent through the left 38 percent of the hero, where
   the h1 and lede sit, and only reaches full strength out in the void.

   The measurement grid stays the dominant motif - it is painted on .hero
   itself, this sits in ::after above it but under the content, which carries
   its own stacking context via .hero > .wrap.

   Keyed off data-art on <body>, set by inject_hero_art() in app.py.
   HERO_ART_ENABLED = False there removes it with no change to this file.
   Regenerating an asset changes its seed suffix, so the filenames below must
   be updated to match. That is the one maintenance cost of this block.
   ========================================================================== */

.hero { position: relative; overflow: hidden; }

/* Content sits above the art. Without this the h1 would be composited under
   a screen-blended layer and lose contrast at the point it matters most. */
.hero > .wrap { position: relative; z-index: 1; }

.hero::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: var(--hero-art, none);
    background-size: cover;
    background-position: right center;
    background-repeat: no-repeat;
    /* NORMAL, not screen. Screen was correct while these were abstract
       line-work on near-black. They are photographs now, and a photograph has
       no black to drop - under screen it lifts the whole frame to grey fog.
       Changed 2026-08-12 after looking at the rendered page rather than at
       HTTP status codes. */
    mix-blend-mode: normal;
    opacity: 0.55;
    pointer-events: none;
    z-index: 0;
    /* Fully clear under the headline, full strength out in the void. A
       photograph carries far more detail than line-work, so it has to be
       gone there rather than merely faint. */
    -webkit-mask-image: linear-gradient(to right,
        transparent 0%, transparent 30%, rgba(0,0,0,0.85) 64%, #000 100%);
    mask-image: linear-gradient(to right,
        transparent 0%, transparent 30%, rgba(0,0,0,0.85) 64%, #000 100%);
}

/* Night aerials, one per navbar route, matched to the four domain images:
   same altitude, same hour, mean luminance 17 to 29. Subject chosen to mean
   something on its page - see the prompts in scripts/gen_site_art.py.
   Filenames carry seed suffixes; reroll one and update it here. */
body[data-art="platforms"] {
    --hero-art: url("../img/generated/og_page_platforms_114255833.png");
}
body[data-art="federation"] {
    --hero-art: url("../img/generated/og_page_federation_2053010819.png");
}
body[data-art="technology"] {
    --hero-art: url("../img/generated/og_page_technology_1222193014.png");
}
body[data-art="education"] {
    --hero-art: url("../img/generated/og_page_education_1069748257.png");
}
body[data-art="about"] {
    --hero-art: url("../img/generated/og_page_about_260843499.png");
}
body[data-art="contact"] {
    --hero-art: url("../img/generated/og_page_contact_1550220493.png");
}

/* Narrow: the hero collapses to one column, so the art can no longer sit
   BESIDE the headline - but there is still room for it behind and below. Only
   genuinely small screens drop it entirely.

   980 WAS THE WRONG NUMBER AND IT COST AN AFTERNOON. A half-width window on a
   4K display at 200% scaling is 960 CSS px, so the hero media vanished on an
   ordinary desktop window and looked exactly like a broken video - it played
   on localhost in a maximised window and "failed" through a tunnel in a
   snapped one. The layout breakpoint and the "is there room for a picture"
   breakpoint are different questions and must not share a number. */
@media (max-width: 980px) and (min-width: 641px) {
    .hero::after {
        -webkit-mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.5) 45%, #000 100%);
        mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.5) 45%, #000 100%);
    }
}
@media (max-width: 640px) {
    .hero::after {
        object-position: center center;
        -webkit-mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.45) 40%, #000 100%);
        mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.45) 40%, #000 100%);
    }
}

/* Screen blend against a light ground lightens toward white - the art would
   either vanish or fog the page. Not shown on the paper surface. */
html[data-surface="paper"] .hero::after { display: none; }

@media print {
    .hero::after { display: none !important; }
}

/* ==========================================================================
   PHASE 6b -- PER-DOMAIN PHOTOGRAPHY (2026-08-12)

   The six vertical pages were sharing one image, which says the domains are
   interchangeable - the fault the Phase 5 CSS motifs were built to fix and
   which Phase 6 reintroduced. Each of the four proof domains now carries its
   own night aerial, keyed off data-domain, which platform_detail.html already
   sets on its hero.

   WHY THIS OVERRIDES THE BLEND MODE. Phase 6 uses mix-blend-mode: screen,
   which is correct for abstract line-work on near-black: black contributes
   nothing, so only the traces show. A PHOTOGRAPH has no black to drop. Screen
   would lift the entire frame and turn the hero into grey fog. These four
   composite normally instead, at reduced opacity, over the ground.

   The four assets are matched, not merely collected: all night aerials at one
   altitude, mean luminance 19.8 to 29.0, and every one dark through the left
   third where the mask discards it. og_domain_built_environment was crushed to
   bp 16 to bring it down to the other three - see BLACK_POINTS in
   scripts/post_art.py.

   THE PHOTOGRAPHS ARE GENERATED AND ILLUSTRATIVE. They are not photographs of
   any asset, corridor or facility that UAAI models. On a site selling
   provenance that distinction matters, so it belongs in content_clearance.csv
   before launch rather than in a reader's assumption.

   Filenames carry seed suffixes. Reroll an asset and this block must be
   updated or that domain silently loses its hero.
   ========================================================================== */

.hero[data-domain]::after {
    /* Normal, not screen - see the note above. */
    mix-blend-mode: normal;
    opacity: 0.55;
    background-size: cover;
    background-position: right center;
    /* Wider clear run than Phase 6: a photograph carries far more detail than
       line-work, so it needs to be fully gone under the headline rather than
       merely faint. */
    -webkit-mask-image: linear-gradient(to right,
        transparent 0%, transparent 30%, rgba(0,0,0,0.85) 64%, #000 100%);
    mask-image: linear-gradient(to right,
        transparent 0%, transparent 30%, rgba(0,0,0,0.85) 64%, #000 100%);
}

.hero[data-domain="energy"]::after {
    background-image: url("../img/generated/og_domain_energy_1537021338.png");
}
.hero[data-domain="life-sciences"]::after {
    background-image: url("../img/generated/og_domain_life_sciences_1190890649.png");
}
.hero[data-domain="built-environment"]::after {
    background-image: url("../img/generated/og_domain_built_environment_663508679.png");
}
.hero[data-domain="demonstration"]::after {
    background-image: url("../img/generated/og_domain_demonstration_1288071539.png");
}

/* The Phase 6 suppressions - narrow screens, paper surface, print - all key
   off .hero::after and carry no competing display declaration, so they still
   apply here. Verified rather than assumed. */

/* ==========================================================================
   PHASE 7 -- CARDS AND CALLOUTS (2026-08-12)

   Body sections below the hero were unbroken prose. These give the eye
   something to land on without inventing new content.

   TWO TREATMENTS, ONE MARKUP. The default is INSTRUMENT: hairline border,
   2px radius, no shadow, numbered like a spec schedule - the house rules at
   the top of this sheet. ?panels=glass previews the frosted variant on the
   same markup, so the two can be compared on a real page rather than argued
   about. Whichever loses should be deleted along with inject_panel_style().

   READ THIS BEFORE JUDGING THE GLASS VARIANT. backdrop-filter samples and
   blurs whatever sits BEHIND the element. The body ground here is a flat
   #080b10, and blurring a flat colour returns that same flat colour - so on
   a body section the glass panel is optically just a lighter rounded box
   with a blur cost attached. It does real work only where something varied
   sits behind it, which on this site means the hero photography. If the glass
   variant wins, the honest way to ship it is over the heroes, not over prose.
   ========================================================================== */

.cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--sp-4);
    margin: var(--sp-5) 0 var(--sp-6);
    max-width: var(--measure);
}

.card {
    position: relative;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    padding: var(--sp-4) var(--sp-4) var(--sp-4);
}

/* Numbered like a spec schedule, which is how the .points list already
   reads. Consistent with the drawing vernacular rather than a new idea. */
.card-idx {
    display: block;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--track-label);
    color: var(--signal);
    margin-bottom: var(--sp-2);
}
.card-title {
    font-size: var(--t-h3);
    margin: 0 0 var(--sp-2);
    letter-spacing: var(--track-tight);
}
.card-body {
    margin: 0;
    color: var(--ink-mid);
    font-size: var(--t-base);
    line-height: 1.62;
}

/* A callout is an emphasis block, not a quote. Accent edge on the left, the
   same device .proof already uses, so the page has one idiom for "this bit
   matters" instead of two. */
.callout {
    border-left: 2px solid var(--instrument);
    border-top: 1px solid var(--rule);
    border-right: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
    border-radius: 0 2px 2px 0;
    background: var(--panel);
    padding: var(--sp-4) var(--sp-5);
    margin: var(--sp-4) 0 var(--sp-5);
    max-width: var(--measure);
}
.callout .body-copy:last-child { margin-bottom: 0; }

/* --- GLASS VARIANT, preview only ---------------------------------------- */

html[data-panels="glass"] .card,
html[data-panels="glass"] .callout,
html[data-panels="glass"] .panel,
html[data-panels="glass"] .rail {
    background: rgba(28, 38, 50, 0.42);
    -webkit-backdrop-filter: blur(14px) saturate(120%);
    backdrop-filter: blur(14px) saturate(120%);
    border-color: rgba(160, 190, 220, 0.14);
    border-radius: 10px;
}

/* The top highlight is what actually sells glass - a lit edge where the
   panel catches light. Without it the blur alone reads as fog. */
html[data-panels="glass"] .card::before,
html[data-panels="glass"] .callout::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 1px;
    background: linear-gradient(to right,
        transparent, rgba(210, 230, 255, 0.30), transparent);
}
html[data-panels="glass"] .callout { position: relative; }
html[data-panels="glass"] .card { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35); }

/* Glass over a light ground needs the opposite tint or it turns to milk. */
html[data-surface="paper"][data-panels="glass"] .card,
html[data-surface="paper"][data-panels="glass"] .callout,
html[data-surface="paper"][data-panels="glass"] .panel,
html[data-surface="paper"][data-panels="glass"] .rail {
    background: rgba(255, 255, 255, 0.55);
    border-color: rgba(20, 40, 60, 0.12);
}

/* Print flattens both variants - a blur cannot print and a shadow wastes
   toner. Same reasoning as the PHASE 3b block. */
@media print {
    .card, .callout {
        background: #ffffff;
        border-color: #b8bfc7;
        border-radius: 0;
        box-shadow: none;
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
        break-inside: avoid;
    }
    .card::before, .callout::before { display: none; }
    .cards { display: block; }
}

/* ==========================================================================
   PHASE 7b -- DOMAIN BANDS (2026-08-12)

   The proof-domain groups on /platforms were a label, two lines of prose and
   a couple of thin rows, repeated four times down a very tall page. Nothing
   held the eye and the four domains read as one undifferentiated list.

   Each group now carries its own domain photograph - the SAME four night
   aerials used on the vertical pages, so a reader who clicks through from a
   band to a detail page meets the same image. That is the point: the band is
   a preview of the page it leads to, not decoration chosen at random.

   The scrim is the load-bearing part. Text sits on a near-opaque left edge
   that ramps to clear on the right, so the prose keeps its contrast against
   the photograph rather than depending on the photograph being dark enough.
   Measured: the four assets run 8.3 to 18.2 mean luminance through their left
   thirds, and the scrim adds an --ground floor on top of that.

   The mimic rows below are deliberately UNCHANGED. Rows are how a control
   room lists plant items and they scale where a card grid goes ragged - a
   domain with one twin would be one lonely card. The band gives the group
   weight; the rows stay a schedule.
   ========================================================================== */

.domain-band {
    position: relative;
    overflow: hidden;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background: var(--panel);
    margin-bottom: var(--sp-5);
    min-height: 132px;
}

.domain-band::after {
    content: "";
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: right center;
    background-repeat: no-repeat;
    opacity: 0.62;
    z-index: 0;
    pointer-events: none;
    /* Clear through the text column, full strength on the right. */
    -webkit-mask-image: linear-gradient(to right,
        transparent 0%, transparent 26%, rgba(0,0,0,0.9) 62%, #000 100%);
    mask-image: linear-gradient(to right,
        transparent 0%, transparent 26%, rgba(0,0,0,0.9) 62%, #000 100%);
}

.domain-band-inner {
    position: relative;
    z-index: 1;
    padding: var(--sp-5) var(--sp-5) var(--sp-4);
}
.domain-band .slabel { margin-bottom: var(--sp-3); }
.domain-band .sub {
    margin: 0;
    max-width: 54ch;
}

/* Reads as an instrument count, not a badge. */
.domain-count {
    display: inline-block;
    margin-top: var(--sp-3);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    text-transform: uppercase;
    letter-spacing: var(--track-label);
    color: var(--ink-dim);
    border-top: 1px solid var(--rule-bright);
    padding-top: var(--sp-2);
}

.domain-band[data-domain="energy"]::after {
    background-image: url("../img/generated/og_domain_energy_1537021338.png");
}
.domain-band[data-domain="life-sciences"]::after {
    background-image: url("../img/generated/og_domain_life_sciences_1190890649.png");
}
.domain-band[data-domain="built-environment"]::after {
    background-image: url("../img/generated/og_domain_built_environment_663508679.png");
}
.domain-band[data-domain="demonstration"]::after {
    background-image: url("../img/generated/og_domain_demonstration_1288071539.png");
}

/* Narrow: the band becomes a header with no room beside the text, so the
   photograph would sit under the prose. Dropped, exactly as the hero art is. */
@media (max-width: 980px) {
    .domain-band::after { display: none; }
    .domain-band-inner { padding: var(--sp-4); }
}

html[data-surface="paper"] .domain-band::after { display: none; }

@media print {
    .domain-band::after { display: none !important; }
    .domain-band { border-color: #b8bfc7; background: #ffffff; }
}

/* ==========================================================================
   PHASE 7c -- OFFER BLOCKS (2026-08-12)

   What a client can buy, per proof domain. Sits under the twin schedule
   because the order of the argument is: here is the domain, here are the
   systems running in it, here is what you can buy.

   DELIBERATELY NOT CARDS. The .card treatment is on /contact for the
   engagement steps, which are a SEQUENCE. These are alternatives, so they get
   a column rule and a hanging indent - the way a schedule of rates reads on a
   drawing - rather than four floating boxes. Same reasoning that keeps the
   mimic rows as rows.

   NO PRICE SLOT EXISTS IN THIS COMPONENT, on purpose. Pricing is an open
   decision (E-1, E-3) and a CSS affordance for a number invites someone to
   invent one. If prices are ever cleared, add the slot then.
   ========================================================================== */

.offers {
    margin: var(--sp-6) 0 var(--sp-4);
    border-top: 1px solid var(--rule-bright);
    padding-top: var(--sp-4);
}

.offers-head {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    text-transform: uppercase;
    letter-spacing: var(--track-label);
    color: var(--signal);
    margin-bottom: var(--sp-4);
}

.offer-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--sp-5) var(--sp-6);
}

.offer {
    border-left: 1px solid var(--rule-bright);
    padding-left: var(--sp-4);
}
.offer-name {
    font-size: var(--t-body);
    font-weight: 600;
    letter-spacing: var(--track-tight);
    margin: 0 0 var(--sp-2);
    color: var(--ink);
}
.offer-body {
    margin: 0;
    color: var(--ink-mid);
    font-size: var(--t-base);
    line-height: 1.62;
}

/* The engagement terms are part of the offer, not a footnote to it - the
   Marketing Plan asks us to lead with how the buyer buys. */
.offer-foot {
    margin: var(--sp-5) 0 0;
    padding-top: var(--sp-3);
    border-top: 1px solid var(--rule);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--track-mono);
    color: var(--ink-dim);
}

@media (max-width: 640px) {
    .offer { border-left: 0; padding-left: 0; border-top: 1px solid var(--rule); padding-top: var(--sp-3); }
}

@media print {
    .offer { break-inside: avoid; }
}

/* ==========================================================================
   PHASE 7d -- DOMAIN CARDS (2026-08-12)

   Each proof domain is now ONE enclosure: photographic header, twin schedule,
   offers, engagement terms. Previously three stacked elements that happened
   to sit near each other, which is why the page read as a list of fragments.

   THIS CHANGES A HOUSE RULE. The rule at the top of this sheet is "No
   border-radius above 2px. Instruments have square corners." These cards use
   --radius-card. That is a deliberate, recorded exception, not drift:

     - it is ONE token, not a value sprinkled across components. Change it in
       :root and every card follows. Set it to 2px and the house rule is
       restored with no other edit.
     - 6px is chosen to read as an enclosure at arm's length while still
       looking machined rather than soft. Past about 10px it starts to read
       as a consumer app, which is the register 10.1 rules out.
     - the instrument components - .panel, .tag, .figures, .rail, .schematic -
       are UNTOUCHED and stay square. The readouts are the part that must look
       like an instrument; a container around a section is not a readout.

   Logged as CLR-0038 for a decision. If it is rejected, set the token to 2px.

   NESTING. The band supplies its own border and radius when it stands alone;
   inside a card it becomes the card's header instead, so its border is
   removed and the image runs flush to the card edge. overflow:hidden on the
   card is what clips the photograph to the rounded corner - without it the
   image squares off the top two corners.
   ========================================================================== */

:root {
    --radius-card: 6px;
}

.domain-card,
.section-card {
    border: 1px solid var(--rule-bright);
    border-radius: var(--radius-card);
    background: var(--panel);
    overflow: hidden;
    margin-bottom: var(--sp-4);
}

/* Band becomes the card header: no border of its own, no radius, flush. */
.domain-card > .domain-band {
    border: 0;
    border-radius: 0;
    border-bottom: 1px solid var(--rule);
    margin-bottom: 0;
    background: transparent;
}

.domain-card-body,
.section-card-body {
    padding: var(--sp-5) var(--sp-5) var(--sp-4);
}

/* The schedule sits inside the card now, so it no longer needs a top rule of
   its own - the band's bottom border already separates them. */
.domain-card-body > .mimic {
    border-top: 0;
}
.domain-card-body > .mimic .mimic-row:last-child {
    border-bottom: 0;
}
.domain-card-body > .mimic .mimic-idx { padding-left: 0; }
.domain-card-body > .mimic a.mimic-row:hover { padding-left: 0; }
.domain-card-body > .mimic a.mimic-row:hover .mimic-idx { padding-left: 0; }

/* Offers already carry their own top rule, which now reads as the divider
   between the schedule and what you can buy. */
.domain-card-body > .offers { margin-top: var(--sp-5); }
.domain-card-body > .offers .offer-foot { margin-bottom: 0; }

@media (max-width: 640px) {
    .domain-card-body { padding: var(--sp-4) var(--sp-4) var(--sp-3); }
}

/* Print: flatten. A radius and a panel fill are screen affordances. */
@media print {
    .domain-card {
        border-radius: 0;
        border-color: #b8bfc7;
        background: #ffffff;
        break-inside: avoid;
    }
    .domain-card-body { padding: 0; }
}

/* --- PHASE 7d: the domain title is a card TITLE, not a section callout ----
   .slabel is sized for a callout floating above prose - 12px, uppercase,
   --ink-dim. Inside a domain card it is the heading of the whole enclosure
   and was the quietest thing in it, sitting under a photograph while the
   body copy below it read louder.

   Promoted to --t-h3 from the SCALE (no invented size) and to full --ink.
   Tracking comes down from 0.16em to 0.09em because letter-spacing that wide
   is legible at 12px and starts to fall apart at 20px.

   Scoped to the card so every other .slabel on the site is untouched. */

.domain-card > .domain-band .slabel {
    font-size: var(--t-h3);
    color: var(--ink);
    font-weight: 500;
    letter-spacing: 0.09em;
    gap: 0.7rem;
    margin-bottom: var(--sp-4);
}

/* The index keeps the amber but needs to hold its own at the larger size. */
.domain-card > .domain-band .slabel .idx {
    color: var(--signal);
    font-weight: 700;
}

/* The dashed rule was sized against 12px type; at 20px it reads as a hairline
   floating mid-cap. Lifted to sit on the optical centre of the larger text. */
.domain-card > .domain-band .slabel::after {
    opacity: 0.65;
}

/* Blurb steps down so the title clearly outranks it. */
.domain-card > .domain-band .sub {
    font-size: var(--t-body);
    color: var(--ink-mid);
}

@media (max-width: 640px) {
    .domain-card > .domain-band .slabel {
        font-size: var(--t-h2);
        letter-spacing: 0.06em;
    }
}

/* ==========================================================================
   PHASE 8 -- PLATFORMS NAV MENU (2026-08-12)

   The six systems were invisible until a visitor landed on /platforms. They
   are the evidence this site exists to show, so they belong in the navigation.

   NO JAVASCRIPT. Opens on :hover AND :focus-within, so a keyboard user tabs
   straight into the menu and out again in DOM order. twin.js is progressive
   enhancement only and navigation must never depend on it.

   The header is position:sticky with z-index 20; the menu needs to sit above
   page content but inside that stacking context, hence z-index on .nav-menu
   rather than on the header.

   HOVER GAP. The menu is flush to the bottom of the 58px header row - no
   margin - because a gap between trigger and panel makes the menu close as
   the pointer crosses it. Padding inside the panel does the visual spacing.
   ========================================================================== */

.nav-item { position: relative; display: inline-flex; }
.nav-item > a { position: relative; z-index: 2; }

.nav-menu {
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%) translateY(-4px);
    width: min(920px, calc(100vw - 4rem));
    background: var(--panel);
    border: 1px solid var(--rule-bright);
    border-radius: var(--radius-card);
    padding: var(--sp-5) var(--sp-5) var(--sp-4);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 120ms linear, transform 120ms linear;
    z-index: 30;
}

.nav-item.has-menu:hover .nav-menu,
.nav-item.has-menu:focus-within .nav-menu {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: translateX(-50%) translateY(0);
}

.nav-menu-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: var(--sp-5) var(--sp-6);
}

.nav-col-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    text-transform: uppercase;
    letter-spacing: var(--track-label);
    color: var(--signal);
    padding-bottom: var(--sp-2);
    margin-bottom: var(--sp-2);
    border-bottom: 1px solid var(--rule);
}
.nav-col-head .ico { color: var(--signal); }

/* Each twin is a two-line target: name, then what it is. Reset the inherited
   .nav a rules - those are sized for a horizontal bar, not a list. */
.nav-menu a.nav-twin {
    display: block;
    min-height: 0;
    margin-left: 0;
    padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-3);
    border-left: 2px solid transparent;
    border-bottom: 0;
    text-transform: none;
    letter-spacing: normal;
    font-family: var(--font-display);
}
.nav-menu a.nav-twin:hover,
.nav-menu a.nav-twin:focus-visible {
    background: var(--panel-raised);
    border-left-color: var(--signal);
    text-decoration: none;
}
.nav-twin-name {
    display: block;
    font-size: var(--t-base);
    font-weight: 600;
    color: var(--ink);
    letter-spacing: var(--track-tight);
}
.nav-twin-sub {
    display: block;
    font-size: var(--t-micro);
    color: var(--ink-dim);
    line-height: 1.5;
    margin-top: 0.1rem;
}

.nav-menu-foot {
    display: flex;
    gap: var(--sp-5);
    margin-top: var(--sp-4);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--rule);
}
.nav-menu a:not(.nav-twin) {
    min-height: 0;
    margin-left: 0;
    border-bottom: 0;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--instrument);
}

/* Below the desktop breakpoint the header is already tight and a 920px panel
   has nowhere to go. The Platforms link still works - it goes to the page
   that lists exactly the same thing. */
@media (max-width: 980px) {
    .nav-menu { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .nav-menu { transition: none; }
}

@media print {
    .nav-menu { display: none !important; }
}

/* ==========================================================================
   PHASE 8b -- NAV MENU: GLASS PANEL, CARDED COLUMNS (2026-08-12)

   GLASS IS JUSTIFIED HERE and nowhere else so far. backdrop-filter samples
   what sits BEHIND an element; a body section has flat --ground behind it, so
   blurring returns the same flat colour (see the PHASE 7 note). This panel
   floats over hero photography, headlines and rules - real texture - so the
   blur does actual work and the panel reads as glass rather than as a lighter
   box. This is NOT the CLR-0036 decision, which is about panels over prose.

   OPACITY FLOOR. The fill sits at 0.93. A navigation menu must be readable
   over WHATEVER happens to be behind it, and behind it can be a bright night
   aerial - /about has the brightest right-hand image on the site. Glass that
   looks good over one page and fails over another is a bug, not a style. The
   blur still does the work; the fill just stops the page reading through it.
   Raised from 0.82 on 2026-08-12 - too see-through in use.

   COLUMNS ARE CARDS. Each domain gets its own enclosure so the four groups
   read as four things - previously only a hairline under the heading divided
   them, and the third heading wraps to four lines, which broke the alignment
   that hairline was providing.
   ========================================================================== */

.nav-menu {
    background: rgba(16, 22, 30, 0.93);
    -webkit-backdrop-filter: blur(18px) saturate(130%);
    backdrop-filter: blur(18px) saturate(130%);
    border-color: rgba(160, 190, 220, 0.16);
    box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
}

/* The lit top edge is what actually reads as glass - blur alone reads as fog. */
.nav-menu::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 1px;
    border-radius: var(--radius-card) var(--radius-card) 0 0;
    background: linear-gradient(to right,
        transparent, rgba(210, 230, 255, 0.34), transparent);
}

.nav-menu-grid { gap: var(--sp-4); }

.nav-col {
    border: 1px solid var(--rule);
    border-radius: var(--radius-card);
    background: rgba(34, 45, 59, 0.62);
    padding: var(--sp-4) var(--sp-3) var(--sp-3);
    transition: background-color 120ms linear, border-color 120ms linear,
                transform 120ms linear;
}

/* Popped out on hover. transform is 1px - enough to register as a lift,
   small enough that four columns do not appear to wobble as the pointer
   crosses them. */
.nav-col:hover,
.nav-col:focus-within {
    background: rgba(48, 63, 82, 0.78);
    border-color: var(--rule-bright);
    transform: translateY(-1px);
}

/* Heading: icon pinned to the first line. The built-environment name wraps to
   four lines and a centred icon floated to the middle of the block. */
.nav-col-head {
    align-items: flex-start;
    gap: 0.45rem;
    min-height: 2.6em;
    border-bottom: 1px solid var(--rule);
}
.nav-col-head .ico { margin-top: 0.1em; }

.nav-menu a.nav-twin {
    border-radius: 2px;
    margin: 0 calc(var(--sp-1) * -1);
    padding-left: var(--sp-2);
}

/* Paper surface: same construction, opposite tint, or the panel turns to milk. */
html[data-surface="paper"] .nav-menu {
    background: rgba(255, 255, 255, 0.95);
    border-color: rgba(20, 40, 60, 0.16);
    box-shadow: 0 18px 48px rgba(20, 40, 60, 0.18);
}
html[data-surface="paper"] .nav-col {
    background: rgba(238, 241, 244, 0.85);
}
html[data-surface="paper"] .nav-col:hover,
html[data-surface="paper"] .nav-col:focus-within {
    background: rgba(255, 255, 255, 0.95);
}

/* A backdrop-filter cannot animate meaningfully, but the lift can. */
@media (prefers-reduced-motion: reduce) {
    .nav-col { transition: none; }
    .nav-col:hover, .nav-col:focus-within { transform: none; }
}

/* ==========================================================================
   PHASE 8c -- SCHEMATIC LEGIBILITY (2026-08-12)

   Three faults, all found by reading the drawing rather than the markup:

   1. The 04 PUBLISHED cap and its sub-caption sat 8px apart at 9px type and
      overlapped. The caption is now a note under the stack it describes.
   2. The flow paths had no heads, so the diagram did not read left to right
      and the seam could have been an inbound boundary. Direction is the most
      important thing this drawing says.
   3. Thirty pixels of viewBox under the baseline did nothing. The baseline
      now labels which side of the seam you are on, which IS the argument.

   Still drawn, still no generated architecture. The page says "Drawn, not
   generated" two lines above it and that has to stay true.
   ========================================================================== */

.sc-arrow      { fill: var(--rule-bright); }
.sc-arrow-live { fill: var(--signal); }

.sc-flow-live { stroke-width: 1.4; }

.sc-zone {
    fill: var(--ink-dim);
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: 0.14em;
}
.sc-zone-live { fill: var(--signal); }

.sc-seam { stroke-width: 1.4; }

@media print {
    .sc-arrow      { fill: #6b7783; }
    .sc-arrow-live { fill: #7a5200; }
    .sc-zone       { fill: #3a3a3a; }
    .sc-zone-live  { fill: #7a5200; }
}

/* ==========================================================================
   PHASE 8d -- SCHEMATIC: STOPPED PATH AND FLOW MOTION (2026-08-12)

   A DOCUMENTED EXCEPTION TO THE MOTION RULE. The rule in PHASE 5 is that the
   only motion on this site marks a STATE CHANGE and nothing moves for
   decoration. A travelling dash is not a state change, so this needs a
   justification rather than a shrug.

   The justification: the drawing's whole argument is DIRECTIONAL - aggregate
   state crosses the seam, record-level data does not. The motion carries that
   argument. It runs only on the paths that cross, it stops dead at the seam,
   and the one path that terminates at the boundary never moves at all. A
   reader watching for two seconds learns what the caption spends a sentence
   saying. That is illustration, not ornament.

   IT IS ALSO ENTIRELY REMOVABLE. The animated stroke is a SEPARATE path drawn
   over a static one. Delete every .sc-pulse element and the drawing is exactly
   what it was - no layout shift, no missing line. Under prefers-reduced-motion
   and in print the pulse is hidden and the static path is what remains.

   SPEED. 2.6s per cycle. Fast enough to read as flow, slow enough that it does
   not pull the eye away from copy elsewhere on the page. Anything under about
   1.5s starts to feel like a loading spinner, which reads as "waiting", which
   is the opposite of what a running system should look like.
   ========================================================================== */

/* The path that dies at the seam. No marker, no continuation - the drawing
   shows the boundary rather than asserting it. Dashed because it is the one
   flow that never becomes a delivery. */
.sc-flow-stop {
    stroke: var(--ink-dim);
    stroke-width: 1;
    stroke-dasharray: 2 3;
    fill: none;
}
/* Heavier than anything it meets. A stop bar that reads as just another
   hairline does not say stop. */
.sc-stopbar {
    stroke: var(--ink-dim);
    stroke-width: 2.2;
    stroke-linecap: square;
}
.sc-stop-label { fill: var(--ink-dim); }

/* Travelling dash over the live path. */
.sc-pulse {
    fill: none;
    stroke: var(--signal);
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-dasharray: 5 95;
    stroke-dashoffset: 100;
    animation: sc-travel 2.6s linear infinite;
    opacity: 0.9;
}
/* Staggered so the three published surfaces do not fire in lockstep, which
   reads as a single wide bar rather than three separate deliveries. */
.sc-pulse:nth-of-type(2) { animation-delay: 0.35s; }
.sc-pulse:nth-of-type(3) { animation-delay: 0.7s; }
.sc-pulse:nth-of-type(4) { animation-delay: 1.05s; }

@keyframes sc-travel {
    from { stroke-dashoffset: 100; }
    to   { stroke-dashoffset: 0; }
}

/* The static path underneath is the drawing. This is the layer that goes. */
@media (prefers-reduced-motion: reduce) {
    .sc-pulse { display: none; }
}

@media print {
    .sc-pulse { display: none !important; }
    .sc-flow-stop { stroke: #6b7783; }
    .sc-stopbar   { stroke: #6b7783; }
    .sc-stop-label { fill: #3a3a3a; }
}

/* PHASE 8e -- the seam note reads as an annotation on the boundary, not as a
   caption on the boxes it happens to sit near. Amber ties it to the SEAM cap
   and to the PUBLISHED zone label, so the three things that describe the
   boundary now share one colour as well as one location. */
.sc-seam-note { fill: var(--signal); opacity: 0.85; }

@media print {
    .sc-seam-note { fill: #7a5200; opacity: 1; }
}

/* ==========================================================================
   PHASE 8f -- CONFIDENCE FIGURE (2026-08-12)

   The rail on this page claims "Every value carries: unit + confidence". That
   was an assertion sitting next to prose. It is now a drawing: one mark per
   published tag, placed by its own confidence, read from the same snapshot
   the hero panel reads.

   THE AMBER THRESHOLD IS THE SAME 0.50 USED EVERYWHERE. A tag that renders
   amber in the twin panel renders amber here. If those two ever disagree the
   site is lying somewhere, so they share the constant rather than each
   carrying their own.

   NOT A CHART LIBRARY, and deliberately not one. Positions come from the
   snapshot through Jinja; there is no client-side plotting, nothing to
   hydrate, and the figure is complete in the HTML source. It also prints.
   ========================================================================== */

.sc-threshold {
    stroke: var(--signal);
    stroke-width: 1;
    stroke-dasharray: 3 4;
    opacity: 0.75;
}

/* Row baseline the marks sit on. Fainter than the axis so the marks read as
   the subject and the rule as the scaffold. */
.sc-rowrule {
    stroke: var(--rule);
    stroke-width: 1;
}

/* One tag. A tick, not a dot - a dot reads as a data point in a scatter and
   invites the reader to look for a trend across two axes. There is only one
   axis here. */
.sc-mark {
    stroke: var(--instrument);
    stroke-width: 2;
    stroke-linecap: butt;
    opacity: 0.9;
}
/* Below 0.50. Amber means low confidence on every surface of this site. */
.sc-mark-low { stroke: var(--signal); }

.sc-row-label { fill: var(--ink); }
.sc-row-count { fill: var(--ink-dim); }

@media print {
    .sc-threshold { stroke: #7a5200; }
    .sc-rowrule   { stroke: #b8bfc7; }
    .sc-mark      { stroke: #0a4f96; }
    .sc-mark-low  { stroke: #7a5200; }
    .sc-row-label { fill: #000000; }
    .sc-row-count { fill: #3a3a3a; }
}

/* PHASE 8g -- stacked confidence marks. A mark is now a small block rather
   than a tall tick, because blocks stack into a legible column and ticks do
   not. Column height is the tag count at that confidence, so the shape is
   data rather than styling. */
.sc-mark { stroke: none; fill: var(--instrument); opacity: 0.92; }
.sc-mark-low { fill: var(--signal); }

/* The count above a stack, so the reader does not have to measure the column
   against a scale that is not there. Only drawn where n > 1. */
.sc-stack-n {
    fill: var(--ink-dim);
    text-anchor: middle;
    letter-spacing: 0.04em;
}

@media print {
    .sc-mark     { fill: #0a4f96; }
    .sc-mark-low { fill: #7a5200; }
    .sc-stack-n  { fill: #3a3a3a; }
}

/* ==========================================================================
   PHASE 9 -- HERO VIDEO (2026-08-13)

   Six page heroes play a 5s loop generated from the still they already used.
   The still remains the poster, so the two are the same image and the swap is
   invisible: first paint shows the poster, the video takes over when decoded,
   and a failed load leaves the poster in place forever.

   IT INHERITS THE STILL'S TREATMENT EXACTLY - same 0.55 opacity, same
   left-to-right mask, same right-centre framing. The point is that the hero
   looks identical and then starts moving, not that video gets a new look.

   .hero::after IS SUPPRESSED when a video is present, via data-heromedia on
   the body. Two layers at 0.55 would composite to roughly 0.80 and the hero
   would visibly darken and brighten as the video decoded.

   NOT ON THE VERTICAL PAGES. There are six page videos and no domain videos,
   so /platforms/<slug> keeps its photograph. The rule keys off the presence
   of the element, so adding og_domain_*.mp4 later needs a scan change in
   app.py, not a change here.
   ========================================================================== */

.hero-video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: right center;
    opacity: 0.55;
    z-index: 0;
    pointer-events: none;
    -webkit-mask-image: linear-gradient(to right,
        transparent 0%, transparent 30%, rgba(0,0,0,0.85) 64%, #000 100%);
    mask-image: linear-gradient(to right,
        transparent 0%, transparent 30%, rgba(0,0,0,0.85) 64%, #000 100%);
}

/* One layer only. */
body[data-heromedia="video"] .hero::after { display: none; }

/* REDUCED MOTION: the hero video KEEPS PLAYING. Deliberate product decision,
   2026-08-13, overriding the earlier rule that hid it.

   Be clear about what this trades. prefers-reduced-motion is a real
   accessibility signal - some people set it because motion makes them ill.
   The video is a full-frame moving background, which is squarely the kind of
   thing that setting exists to suppress, and the site honours the preference
   everywhere else: .sc-pulse still stops, nav transitions still stop, the
   global rule still clamps animation durations.

   What makes this defensible rather than reckless is the CONTENT of the clip:
   a slow aerial drift with no cuts, no flashing, no parallax and no motion
   tied to scroll. It is closer to a slowly changing photograph than to the
   swooping backgrounds the preference was written for.

   To restore the accessible behaviour, delete this comment block and put back:
       @media (prefers-reduced-motion: reduce) {
           .hero-video { display: none; }
           body[data-heromedia="video"] .hero::after { display: block; }
       }
   and the matching .hero-playlist rule below, and restore the check in
   heroloop.js. */

/* Single column: the mask flips from left-to-right to bottom-up, so the video
   sits behind and below the headline instead of beside it. Only true phone
   widths drop it - see the note on the .hero::after breakpoint above. */
@media (max-width: 980px) and (min-width: 641px) {
    .hero-video {
        object-position: center bottom;
        opacity: 0.42;
        -webkit-mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.5) 45%, #000 100%);
        mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.5) 45%, #000 100%);
    }
}
/* PHONE. The media stays - it just stops pretending it has a right-hand void
   to live in.

   THREE CHANGES, each for a reason:
     object-position centre - these clips are 1.9:1 and a portrait screen crops
       hard. Framed right-of-centre the subject falls off the edge entirely,
       so the interesting part has to be pulled back to the middle.
     mask flipped to bottom-up - single column means the headline is ON the
       image, not beside it, so the top of the frame has to be clear.
     opacity down - a phone is held closer and usually in worse light than a
       desktop monitor, and body copy over moving footage is the first thing
       to become unreadable. */
@media (max-width: 640px) {
    .hero-video {
        object-position: center center;
        opacity: 0.38;
        -webkit-mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.45) 40%, #000 100%);
        mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.45) 40%, #000 100%);
    }
}

html[data-surface="paper"] .hero-video { display: none; }

@media print {
    .hero-video { display: none !important; }
}

/* ==========================================================================
   PHASE 9b -- LANDING HERO PLAYLIST (2026-08-13)

   The landing hero shows all six page videos in sequence, then starts again.

   IT IS TREATED DIFFERENTLY FROM THE SIX PAGE HEROES, on purpose. Those use a
   left-to-right mask because their headline sits left and the void sits right.
   The landing hero has content in BOTH columns - headline left, twin panel
   right - so a directional mask would put the picture underneath one of them.
   This one is full bleed at a lower opacity instead, with a bottom fade so it
   resolves into the section boundary rather than stopping at a hard edge.

   0.30, NOT 0.55. The twin panel is the product and must stay the loudest
   thing in the hero. --panel is #0c1117 and fully opaque, so the panel itself
   is safe at any opacity - the constraint is the headline and lede, which sit
   directly on the moving image.
   ========================================================================== */

.hero-playlist {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center center;
    opacity: 0.30;
    z-index: 0;
    pointer-events: none;
    -webkit-mask-image: linear-gradient(to bottom,
        #000 0%, #000 62%, rgba(0,0,0,0.55) 86%, transparent 100%);
    mask-image: linear-gradient(to bottom,
        #000 0%, #000 62%, rgba(0,0,0,0.55) 86%, transparent 100%);
}

/* heroloop.js checks the same width condition and will not fetch the rest of
   the playlist below it. Reduced motion no longer suppresses either - see the
   note in PHASE 9. */
@media (max-width: 980px) and (min-width: 641px) {
    .hero-playlist { opacity: 0.24; }
}
/* PHONE. Plays, but as a SINGLE looping clip rather than the six-clip
   sequence - heroloop.js stops sequencing below 640. The full playlist is
   7.1 MB and pushing that down a phone connection to decorate a hero is not a
   reasonable thing to do to someone on a metered plan. One clip is ~0.9 MB
   and looks identical until the first cut. */
@media (max-width: 640px) {
    .hero-playlist {
        object-position: center center;
        opacity: 0.26;
        -webkit-mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.45) 40%, #000 100%);
        mask-image: linear-gradient(to bottom,
            transparent 0%, rgba(0,0,0,0.45) 40%, #000 100%);
    }
}

html[data-surface="paper"] .hero-playlist { display: none; }

@media print {
    .hero-playlist { display: none !important; }
}

/* ==========================================================================
   PHASE 10 -- MOBILE NAVIGATION (2026-08-13)

   Six uppercase mono links plus the brand do not fit a 390px viewport. The
   previous answer was to shrink them to 10px, which made every tap target
   marginal and still overflowed on the narrowest phones.

   THE COLLAPSED STATE IS SCOPED TO html.js. Without scripting the nav stays
   visible and simply wraps onto two rows - cramped, but every link reachable.
   Nothing is hidden unless something is present to reveal it. navinit.js sets
   that class render-blocking so the panel never flashes open on load.

   THE MEGA-MENU FLATTENS rather than disappears. On desktop the six systems
   hang off Platforms on hover; on a phone hover does not exist, so the whole
   thing renders inline as an indented list. The systems are the evidence this
   site exists to show and they should not be desktop-only.

   820px, not 980. The nav runs out of room before the hero layout does, so it
   gets its own breakpoint - the mistake that cost an afternoon on the hero
   video was making one number answer two questions.
   ========================================================================== */

.nav-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    background: transparent;
    border: 1px solid var(--rule);
    border-radius: 2px;
    color: var(--ink);
    cursor: pointer;
}
.nav-toggle:hover { border-color: var(--rule-bright); }
.nav-toggle .nt-line {
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: square;
}
/* Open state: the bars become a cross, drawn with transforms so there is no
   second icon to keep in sync. */
.nav-toggle[aria-expanded="true"] .nt-line:nth-child(1) {
    transform: translateY(5px) rotate(45deg);
    transform-origin: center;
}
.nav-toggle[aria-expanded="true"] .nt-line:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nt-line:nth-child(3) {
    transform: translateY(-5px) rotate(-45deg);
    transform-origin: center;
}

@media (max-width: 820px) {
    .site-head .wrap { position: relative; }

    /* No scripting: wrap onto a second row rather than overflow. */
    .nav { flex-wrap: wrap; justify-content: flex-end; }

    html.js .nav-toggle { display: inline-flex; }

    html.js .nav {
        display: none;
        position: absolute;
        top: 58px;
        left: calc(var(--pad-page) * -1);
        right: calc(var(--pad-page) * -1);
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        padding: var(--sp-3) var(--pad-page) var(--sp-4);
        background: var(--panel);
        border-bottom: 1px solid var(--rule-bright);
        box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5);
        max-height: calc(100vh - 58px);
        overflow-y: auto;
    }
    html.js .nav.is-open { display: flex; }

    /* Full-width rows with a real separator, so each link is an obvious,
       comfortably sized target rather than a word in a line of words. */
    .nav a {
        margin-left: 0;
        width: 100%;
        min-height: 48px;
        font-size: var(--t-small);
        border-bottom: 1px solid var(--rule);
        border-left: 2px solid transparent;
        padding-left: var(--sp-3);
    }
    .nav a:hover, .nav a:focus-visible {
        border-left-color: var(--signal);
        background: var(--panel-raised);
    }

    .nav-item, .nav-item.has-menu { display: block; width: 100%; }

    /* The panel becomes part of the page instead of an overlay: no absolute
       positioning, no blur, no shadow, always visible. */
    .nav-menu,
    .nav-item.has-menu:hover .nav-menu,
    .nav-item.has-menu:focus-within .nav-menu {
        position: static;
        transform: none;
        width: auto;
        opacity: 1;
        visibility: visible;
        pointer-events: auto;
        background: transparent;
        border: 0;
        border-radius: 0;
        box-shadow: none;
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
        padding: 0 0 var(--sp-2) var(--sp-4);
    }
    .nav-menu::before { display: none; }
    .nav-menu-grid { grid-template-columns: 1fr; gap: var(--sp-3); }
    .nav-col {
        background: transparent;
        border: 0;
        border-radius: 0;
        padding: 0;
    }
    .nav-col:hover, .nav-col:focus-within {
        background: transparent;
        transform: none;
        border-color: transparent;
    }
    .nav-menu a.nav-twin {
        border-bottom: 1px solid var(--rule);
        min-height: 44px;
        padding-top: var(--sp-2);
        padding-bottom: var(--sp-2);
    }
    .nav-menu-foot { flex-direction: column; gap: 0; }
    .nav-menu a:not(.nav-twin) { min-height: 40px; }
}

@media print {
    .nav-toggle { display: none !important; }
}

/* PHASE 10b -- the domain card heading is an h2 now (it was a div, which made
   /platforms jump h1 -> h3 and hid the domain names from anyone navigating by
   heading). h2 brings its own margin and font-size from the base rule, so
   they are reset here - the .slabel styling is what should apply, and the
   change is meant to be structural only, with no visual difference. */
.domain-card > .domain-band h2.slabel {
    margin-top: 0;
    font-family: var(--font-mono);
    text-transform: uppercase;
}

/* PHASE 10c -- visible active state, driven by aria-current.
   The attribute is the single source of truth: the highlight cannot show on a
   page the attribute does not mark, and a screen reader cannot be told
   something the eye is not. .nav a already carries a transparent bottom
   border, so this colours an existing edge rather than adding one and
   shifting the row. */
.nav a[aria-current="page"] {
    color: var(--ink);
    border-bottom-color: var(--signal);
}
@media (max-width: 820px) {
    html.js .nav a[aria-current="page"] {
        border-bottom-color: var(--rule);
        border-left-color: var(--signal);
        background: var(--panel-raised);
    }
}

/* ==========================================================================
   PHASE 11 -- BRAND MARKS (2026-08-13)

   The header was a typographic stand-in: the letters UAAI plus a full stop,
   set in the display face. The real wordmark replaces it.

   HEIGHT IS FIXED, WIDTH IS AUTO. The asset is a raster extracted from a
   PowerPoint and it will be replaced when a vector original turns up, almost
   certainly at a different pixel size. Constraining height and letting width
   follow means that swap cannot distort the mark or shift the header.

   The <img> carries width and height attributes matching the file so the
   browser reserves the right box before the image decodes - without them the
   header reflows on every cold load, which is the kind of small nastiness
   that reads as an unfinished site.
   ========================================================================== */

.brand {
    display: inline-flex;
    align-items: center;
    border-bottom: 0;
}
.brand-mark {
    display: block;
    height: 22px;
    width: auto;
    /* The mark is light-on-transparent, drawn for a dark ground. On the paper
       surface it would vanish, so it is inverted there rather than shipped
       twice - see the paper rule below. */
}
.brand:hover { border-bottom: 0; }

html[data-surface="paper"] .brand-mark {
    filter: invert(1) hue-rotate(180deg);
}

@media (max-width: 820px) {
    .brand-mark { height: 20px; }
}

@media print {
    .brand-mark { filter: invert(1) hue-rotate(180deg); }
}

/* ==========================================================================
   PHASE 11b -- REAL BRAND MARKS (2026-08-15)

   Header carries the shield (parent-glyph-02), which appears on 24 of the 27
   slides in the logo deck and is therefore the primary company mark. It is a
   RASTER, and that is fine: at 24px it renders perfectly. The vector trace of
   it fails - the interior is colour on a shaded render, not shape - but a
   website does not need the vector, only a filing does.

   /platforms shows each system's segment mark beside its name. The grid gains
   a column for it; the cell is always rendered even when empty, so the four
   systems with artwork and the two without stay aligned instead of the names
   jumping left and right down the schedule.
   ========================================================================== */

/* The whole lockup. Height fixed, width auto, so the 2.97:1 ratio holds and a
   future replacement at different pixel dimensions cannot distort it or shift
   the header. 26px keeps the roundel - which is only about a quarter of the
   mark's height - large enough to read as a star rather than a dot. */
.brand-mark {
    height: 26px;
    width: auto;
    display: block;
}
@media (max-width: 820px) {
    .brand-mark { height: 22px; }
}.brand { display: inline-flex; align-items: center; border-bottom: 0; }
.brand:hover { border-bottom: 0; }

/* Column added between the index and the name. Fixed width so an empty cell
   holds the same space as a filled one. */
.mimic-row {
    grid-template-columns: 3.2rem 2rem minmax(0, 15rem) minmax(0, 1fr) 7.5rem;
}
.mimic-mark {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 1.6rem;
}
.mimic-mark img {
    max-height: 1.6rem;
    max-width: 2rem;
    object-fit: contain;
    display: block;
}

/* The marks are drawn for light slides. On the dark surface the mid-greys of
   the chrome renders sit close to the ground, so a small lift keeps them
   legible without making them louder than the system name. */
.mimic-mark img { filter: brightness(1.12); }

@media (max-width: 900px) {
    .mimic-row { grid-template-columns: 2.6rem 1.7rem minmax(0, 1fr) 6rem; }
    .mimic-mark img { max-width: 1.7rem; }
}

html[data-surface="paper"] .mimic-mark img { filter: none; }

/* PHASE 11c -- segment lockups on /platforms.
   The mark beside each system is now the full UAAi wordmark with that
   segment's glyph set between the two A's, not a bare icon. At roughly 3.6:1
   it needs a real column: 2rem held an icon, a lockup at that width would be
   6px tall and illegible. Height is capped and width follows, so the four
   lockups - which differ slightly in ratio because their glyphs do - all sit
   on the same baseline without stretching. */
.mimic-row {
    grid-template-columns: 3.2rem 7.5rem minmax(0, 13rem) minmax(0, 1fr) 7.5rem;
}
.mimic-mark { height: auto; justify-content: flex-start; }
.mimic-mark img {
    max-height: 1.5rem;
    max-width: 7rem;
    width: auto;
    object-fit: contain;
}

@media (max-width: 1100px) {
    .mimic-row { grid-template-columns: 2.6rem 6rem minmax(0, 1fr) 6rem; }
    .mimic-mark img { max-width: 5.6rem; max-height: 1.3rem; }
}

/* PHASE 11d -- segment lockups now sit ~2.4:1, not 3.8:1, because the glyph
   rises above the caps instead of widening the word. Column narrows and the
   height cap rises to suit. */
.mimic-row {
    grid-template-columns: 3.2rem 5.5rem minmax(0, 13rem) minmax(0, 1fr) 7.5rem;
}
.mimic-mark img { max-height: 2rem; max-width: 5rem; }

@media (max-width: 1100px) {
    .mimic-row { grid-template-columns: 2.6rem 4.4rem minmax(0, 1fr) 6rem; }
    .mimic-mark img { max-width: 4rem; max-height: 1.7rem; }
}

/* ==========================================================================
   PHASE 12 -- THE CONTROL LOOP FIGURE (2026-08-15)

   Section 01 listed four things you buy. They are not a list: state is
   measured, a command is issued, the result is verified, the decision is
   recorded, and that record is what the next measurement is read against.
   Drawn as a closed loop because that is the argument the page is making -
   a dashboard is the thing that does NOT close.

   The first node carries a live tag from the twin snapshot, so "measured
   state" is evidenced rather than asserted. Same discipline as the hero
   panel: the site shows its own instrument rather than describing one.
   ========================================================================== */

.loopfig {
    margin: var(--sp-5) 0 var(--sp-4);
    padding: 0;
}
.loopfig svg { width: 100%; height: auto; display: block; }

.lp-box {
    fill: var(--panel);
    stroke: var(--rule-bright);
    stroke-width: 1;
}
.lp-num {
    font-family: var(--font-mono);
    font-size: 12px;
    fill: var(--signal);
    letter-spacing: 0.08em;
}
.lp-cap {
    font-family: var(--font-mono);
    font-size: 11px;
    fill: var(--ink);
    letter-spacing: 0.14em;
}
.lp-tag {
    font-family: var(--font-mono);
    font-size: 11.5px;
    fill: var(--ink-dim);
}
.lp-val {
    font-family: var(--font-mono);
    font-size: 19px;
    fill: var(--ink);
}
.lp-unit { font-size: 11px; fill: var(--ink-dim); }
.lp-conf {
    font-family: var(--font-mono);
    font-size: 11px;
    fill: var(--measured);
}
.lp-note {
    font-family: var(--font-mono);
    font-size: 11px;
    fill: var(--ink-dim);
    text-anchor: middle;
}
.lp-line {
    fill: none;
    stroke: var(--rule-bright);
    stroke-width: 1.2;
}
.lp-head { fill: var(--rule-bright); }
.lp-return { stroke: var(--rule); }

/* The pulse is the one piece of motion here, and it earns its place: the
   direction of travel IS the point. Static line underneath, so nothing is
   lost when it is switched off. */
.lp-pulse {
    fill: none;
    stroke: var(--signal);
    stroke-width: 1.6;
    stroke-dasharray: 14 150;
    stroke-dashoffset: 164;
    animation: lp-run 3.2s linear infinite;
}
@keyframes lp-run { to { stroke-dashoffset: 0; } }

@media (prefers-reduced-motion: reduce) {
    .lp-pulse { display: none; }
}
@media print {
    .lp-pulse { display: none; }
    .lp-box { fill: none; }
}
/* A 760px rule here stepped .lp-cap DOWN to 9.5px, .lp-tag to 10px and
   .lp-val to 15px. REMOVED 2026-09-14, PHASE 35. Those are SVG user units in
   a 1040-unit viewBox rendered at width:100%, so the type was ALREADY
   shrinking in proportion to the viewport - stepping it down again took the
   caption to about 3px on a phone. Below 900px the figure is replaced by the
   HTML fallback instead. */

/* PHASE 12b -- the loop figure spans the section, and the stages carry the
   labels the list used to. Sized against the section rather than a column,
   which is what stopped the tag name overrunning its box. */
.loopfig {
    margin: var(--sp-6) 0 0;
    padding: var(--sp-4) 0 0;
    border-top: 1px solid var(--rule);
}
.lp-was {
    font-family: var(--font-mono);
    font-size: 12.5px;
    fill: var(--ink-dim);
}
.lp-tag { font-size: 12px; }
.lp-val { font-size: 26px; }
.lp-conf { font-size: 11.5px; }
.lp-cap { font-size: 12px; letter-spacing: 0.12em; }
.lp-num { font-size: 12px; }
.lp-note { font-size: 11.5px; }

/* Same fault as the 760px block above, same removal. PHASE 35 replaces the
   drawing below 900px rather than shrinking its type further. */

/* PHASE 12c -- the figure no longer quotes a live tag.
   Node 01 briefly showed a reading from the maritime twin, which tied a claim
   about EVERY engagement to one shipping system. The caption states what the
   loop carries in general instead; the live instrument stays in the hero,
   where it is explicitly one of ours rather than a stand-in for all of them. */
.lp-caption {
    font-family: var(--font-mono);
    font-size: var(--t-small);
    color: var(--ink-dim);
    line-height: 1.7;
    max-width: 62ch;
    margin: var(--sp-3) 0 0;
}

/* ==========================================================================
   PHASE 13 -- COVERAGE PANEL (2026-08-15)

   Replaces the live twin readout in the hero. Same panel chrome as the
   instrument it displaced, so the hero keeps its shape, but the content is a
   claim about breadth rather than a measurement.

   The lockups are the argument here: each one is the company mark carrying a
   segment glyph, which says "the same layer, a different process" in a way a
   list of industry names cannot.
   ========================================================================== */

.coverage { padding: 0; }

.cov-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--rule-bright);
}
.cov-title {
    font-family: var(--font-mono);
    font-size: var(--t-small);
    color: var(--ink);
    letter-spacing: 0.06em;
}
.cov-count {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--ink-dim);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    white-space: nowrap;
}

.cov-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1px;
    background: var(--rule);
}
.cov-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-2);
    background: var(--panel);
    min-height: 92px;
    text-align: center;
}
/* 1px gaps over a rule-coloured ground give hairline separators without
   twelve individual borders to keep aligned. */
.cov-cell img {
    max-height: 30px;
    max-width: 100%;
    width: auto;
    display: block;
}
.cov-name {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--ink-dim);
    letter-spacing: 0.04em;
}

.cov-foot {
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--rule-bright);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    line-height: 1.75;
    color: var(--ink-dim);
}

@media (max-width: 980px) {
    .cov-grid { grid-template-columns: repeat(2, 1fr); }
}

/* PHASE 13b -- the bridge line under the section headline. Sized between the
   headline and body copy so the eye goes headline -> bridge -> loop, which is
   the order the argument is made in. */
.lede-sub {
    /* --t-lead, NOT --t-lede. Corrected 2026-08-16 - see the note at the end
       of PHASE 22. The typo made this declaration invalid at computed-value
       time, so the line silently inherited body size for as long as it
       existed. */
    font-size: var(--t-lead);
    line-height: 1.55;
    color: var(--ink-mid);
    max-width: 54ch;
    margin: var(--sp-3) 0 var(--sp-4);
}

/* ==========================================================================
   PHASE 14 -- FOOTER LEGAL (2026-08-16)

   Copyright notice and trademark marking. Counsel's engagement ladder places
   marketing materials at Stage 0 and requires both; the footer carried
   neither.

   Set QUIETLY on purpose. A legal line that shouts undermines the tone of the
   rest of the site, and nobody reads it for pleasure - it needs to be present
   and findable, not prominent.
   ========================================================================== */

.foot-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-3);
}
.foot-legal {
    color: var(--ink-dim);
    max-width: 64ch;
}
/* .foot-note IS CURRENTLY UNUSED. The aggregate-only disclosure line it
   styled was removed from base.html on 2026-08-16 (CLR-0064). Retained
   rather than deleted because that removal is pending review with Jeff
   Streets alongside CLR-0051, and restoring the line should be a one-line
   template edit rather than a template edit plus a CSS rebuild. If the
   removal is confirmed, delete this block with it. */
.foot-note {
    margin-top: var(--sp-3);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--rule);
    color: var(--ink-dim);
}
/* The TM sits as a superscript against the company name without changing the
   line height of the row it is in. */
.tm {
    font-size: 0.62em;
    vertical-align: super;
    letter-spacing: 0.04em;
    margin-left: 0.15em;
    color: var(--ink-dim);
}

@media (max-width: 760px) {
    .foot-row { flex-direction: column; gap: var(--sp-2); }
}

/* ==========================================================================
   PHASE 15 -- BUILD RECORD (2026-08-16)

   The schedule rendered a 104,000-line platform and an empty folder as two
   equal rows. That flatters the folder, robs the platform, and hides the one
   fact that is hardest for a competitor to answer: the rate.

   THREE TIERS, HONESTLY LABELLED. The committed tier exists precisely so the
   empty ones can be shown without pretending. A roadmap that admits what is
   not built is more credible than one that does not, and this audience -
   procurement-driven institutions checking a small vendor - reads for exactly
   that kind of tell.
   ========================================================================== */

.tiers {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1px;
    background: var(--rule);
    border: 1px solid var(--rule-bright);
}
.tier {
    background: var(--panel);
    padding: var(--sp-4);
}
.tier-head {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
}
.tier-n {
    font-family: var(--font-mono);
    font-size: 2.4rem;
    line-height: 1;
    color: var(--ink);
}
.tier-label {
    font-family: var(--font-mono);
    font-size: var(--t-small);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-mid);
}
.tier-dates {
    margin-top: var(--sp-2);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--ink-dim);
    letter-spacing: 0.04em;
}
.tier-note {
    margin: var(--sp-3) 0 0;
    font-size: var(--t-small);
    line-height: 1.65;
    color: var(--ink-dim);
    max-width: 42ch;
}
/* Only the running tier gets the live colour. Building and committed are
   deliberately quiet - they are a promise, and a promise should not be
   coloured like a fact. */
.tier-running .tier-n { color: var(--live); }
.tier-building .tier-n { color: var(--signal); }
.tier-committed .tier-n { color: var(--ink-dim); }

.tier-foot {
    margin: var(--sp-4) 0 0;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    line-height: 1.8;
    color: var(--ink-dim);
    max-width: 80ch;
}

/* Row badges, same three states. */
.status-running   { color: var(--live); }
.status-building  { color: var(--signal); }
.status-committed { color: var(--ink-dim); }
.status .since {
    margin-left: 0.5rem;
    color: var(--ink-dim);
    letter-spacing: 0.04em;
}

@media (max-width: 900px) {
    .tiers { grid-template-columns: 1fr; }
    .tier-n { font-size: 2rem; }
}

/* Footer links. The IP page is diligence material - looked for deliberately,
   not browsed to - so it sits here rather than taking a seventh slot in a
   navigation that is already tight at six. */
.foot-links a {
    color: var(--ink-mid);
    border-bottom: 1px solid var(--rule-bright);
}
.foot-links a:hover { color: var(--ink); border-bottom-color: var(--signal); }

/* ==========================================================================
   PHASE 16 -- THE INSTRUMENT LADDER (2026-08-16)

   Counsel's engagement sequence, published. Drawn as a numbered ladder rather
   than prose because the reader's question is "how far does this commit me",
   and a sequence answers that at a glance where a paragraph does not.

   The rung number is the loud element and the document type is the quiet one,
   because the reassurance is in the ORDER: a one-page non-binding letter comes
   before anything a general counsel has to argue about.
   ========================================================================== */

.ladder {
    list-style: none;
    margin: var(--sp-4) 0 var(--sp-5);
    padding: 0;
    border-top: 1px solid var(--rule);
}
.rung {
    display: grid;
    grid-template-columns: 3rem 1fr;
    gap: var(--sp-3);
    padding: var(--sp-3) 0;
    border-bottom: 1px solid var(--rule);
}
.rung-n {
    font-family: var(--font-mono);
    font-size: 1.5rem;
    line-height: 1.1;
    color: var(--signal);
}
.rung-body { min-width: 0; }
.rung-name {
    display: inline;
    font-weight: 600;
    color: var(--ink);
    margin-right: var(--sp-2);
}
.rung-doc {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--ink-dim);
    white-space: nowrap;
}
.rung-body p {
    margin: var(--sp-2) 0 0;
    color: var(--ink-mid);
    max-width: 62ch;
}
/* Step zero is where the reader is standing right now, so it is marked. */
.rung:first-child { border-left: 2px solid var(--signal); padding-left: var(--sp-3); }
.rung:first-child .rung-n { color: var(--live); }

@media (max-width: 700px) {
    .rung { grid-template-columns: 2.2rem 1fr; gap: var(--sp-2); }
    .rung-n { font-size: 1.2rem; }
}

/* ==========================================================================
   PHASE 13c -- COVERAGE PANEL, SHORT LAST ROW (2026-08-16)

   Ten systems in a three-across grid leaves two cells empty, and because the
   separators are 1px gaps over a rule-coloured ground, the hole rendered as a
   solid slab. Reflowing to five across would have fixed ten and broken eleven.

   The remainder is filled by a cell that spans it and does a job: the three
   tier counts, linking to the build record. So the panel stops being a
   dead-end display and gains the one thing it lacked - somewhere to go.
   ========================================================================== */

.cov-span-1 { grid-column: span 1; }
.cov-span-2 { grid-column: span 2; }

.cov-cta {
    align-items: flex-start;
    justify-content: center;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    text-align: left;
    border-bottom: 0;
    text-decoration: none;
}
.cov-cta:hover { background: var(--panel-raised); }
.cov-cta:hover .cov-cta-link { border-bottom-color: var(--signal); color: var(--ink); }

.cov-cta-tiers {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-3);
}
.cov-tier {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.06em;
    color: var(--ink-dim);
    white-space: nowrap;
}
/* Same colour logic as the build record band: only what runs gets the live
   colour, a promise is not coloured like a fact. */
.cov-tier-running::before,
.cov-tier-building::before,
.cov-tier-committed::before {
    content: "";
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-right: 0.45rem;
    vertical-align: 0.05em;
    background: currentColor;
}
.cov-tier-running { color: var(--live); }
.cov-tier-building { color: var(--signal); }
.cov-tier-committed { color: var(--ink-dim); }

.cov-cta-link {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-mid);
    border-bottom: 1px solid var(--rule-bright);
    align-self: flex-start;
}

/* At two columns the computed remainder no longer matches the layout, so the
   cell simply spans the row rather than guessing. */
@media (max-width: 980px) {
    .cov-span-1, .cov-span-2 { grid-column: 1 / -1; }
}

/* ==========================================================================
   PHASE 17 -- RADIAL COVERAGE (2026-08-16)

   NOT RENDERED since 2026-09-16. The landing hero is now the PHASE 36
   honeycomb. Retained with PHASE 19 in case the radial returns.

   Replaces the coverage grid. Ten lockups at thumbnail size read as one logo
   repeated: the constant part dominated and the distinguishing glyph was about
   twenty pixels. Here the company mark sits once at the hub and only the
   glyphs radiate, so every element on the ring is the part that differs.

   TIER COLOUR IS CARRIED BY THE SPOKE, not the node. The glyph should read as
   itself; colouring its disc would fight the artwork. The line into the centre
   is the right place to say how real the coupling is - and it keeps the same
   logic used everywhere else on the site: only what runs gets the live colour.
   ========================================================================== */

.radial { padding: 0; }
.radial-svg {
    display: block;
    width: 100%;
    height: auto;
    padding: var(--sp-3) var(--sp-2);
}

.ring {
    fill: none;
    stroke: var(--rule);
    stroke-width: 1;
    stroke-dasharray: 2 6;
}
.hub {
    fill: var(--panel-raised);
    stroke: var(--rule-bright);
    stroke-width: 1;
}

.spoke { stroke-width: 1.2; }
.spoke-running { stroke: var(--live); opacity: 0.55; }
.spoke-building { stroke: var(--signal); opacity: 0.5; }
/* Committed spokes are dashed: the coupling is intended, not built. The
   drawing should not claim more than the build record does. */
.spoke-committed {
    stroke: var(--ink-dim);
    opacity: 0.4;
    stroke-dasharray: 3 5;
}

.node-disc {
    fill: var(--panel);
    stroke: var(--rule-bright);
    stroke-width: 1;
}
.node-running .node-disc { stroke: var(--live); }
.node-building .node-disc { stroke: var(--signal); }
.node-committed .node-disc { stroke: var(--rule); }

.node-label {
    font-family: var(--font-mono);
    font-size: 15px;
    fill: var(--ink-mid);
    letter-spacing: 0.02em;
}
.node-committed .node-label { fill: var(--ink-dim); }

html[data-surface="paper"] .hub { fill: var(--panel-raised); }

@media (max-width: 980px) {
    .node-label { font-size: 17px; }
}

/* Section 01 runs full width now that the cost rail beside it has gone. The
   copy still needs a measure - 90ch of body text is unreadable however much
   room there is. */
.body-wide { max-width: 78ch; }

/* ==========================================================================
   PHASE 18 -- BACKGROUND GRID (2026-08-16)

   Three faults, compounding:

   1. PITCH VS DEVICE PIXELS. The grid was 26px with 1px lines. On a display at
      1.35 device pixel ratio - which is what this machine runs - each line
      lands on a different subpixel boundary, so some render as one crisp pixel
      and others smear across two. The result shimmers and looks dirty rather
      than drawn. A wider pitch means fewer lines and far less visible variance.

   2. TWO GRIDS THAT DISAGREED. The hero ticked at 26px and section.ruled at
      52px, so the rhythm changed halfway down the page for no reason. One
      pitch now, shared.

   3. IT RAN HARD TO THE EDGES and straight through body copy. A drawing grid
      should sit under the content, not stripe it. The hero grid now fades out
      before it reaches the text, and the vertical stripe over body sections is
      gone entirely - the dashed left rule on .wrap already gives that
      engineering-drawing cue without crossing a paragraph.
   ========================================================================== */

:root {
    --tick: 52px;
    --grid: rgba(120, 160, 200, 0.030);
}

/* Fade the field out towards the middle and bottom, so lines never terminate
   abruptly against a heading. */
.gridfield {
    background-image:
        repeating-linear-gradient(to right, var(--grid) 0 1px, transparent 1px var(--tick)),
        repeating-linear-gradient(to bottom, var(--grid) 0 1px, transparent 1px var(--tick));
    background-position: center top;
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 42%, rgba(0,0,0,0.35) 78%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0%, #000 42%, rgba(0,0,0,0.35) 78%, transparent 100%);
}

/* The 52px vertical stripe over body sections is removed. It was the one
   crossing paragraphs, and it is not load-bearing. */
section.ruled { background-image: none; }

html[data-surface="paper"] .gridfield {
    -webkit-mask-image: none;
    mask-image: none;
}

/* The radial panel lost its header and footer bars 2026-08-16. Without them
   the panel is a frame around a drawing, so the internal padding has to do
   the work the bars were doing - otherwise the figure sits hard against the
   border. */
.panel.radial { padding: var(--sp-4) var(--sp-3); }

/* ==========================================================================
   PHASE 18b -- GRID MASK MOVED OFF THE ELEMENT (2026-08-16)

   THE FADE WAS EATING THE CONTENT. Phase 18 put mask-image on .gridfield to
   stop the grid lines terminating hard against a heading. But a CSS mask
   applies to the WHOLE element and everything inside it, not to its
   background - so the hero's buttons and the bottom of the right-hand panel
   were being faded out along with the lines.

   The grid now lives on a pseudo-element, which is the only thing the mask
   touches. Content sits above it untouched. This is the standard reason to
   reach for ::before rather than a background on the element itself, and it
   is worth remembering the next time a "background" needs a mask.
   ========================================================================== */

.gridfield {
    position: relative;
    background-image: none;
    -webkit-mask-image: none;
    mask-image: none;
}
.gridfield::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 0;
    background-image:
        repeating-linear-gradient(to right, var(--grid) 0 1px, transparent 1px var(--tick)),
        repeating-linear-gradient(to bottom, var(--grid) 0 1px, transparent 1px var(--tick));
    background-position: center top;
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 46%, rgba(0,0,0,0.4) 80%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0%, #000 46%, rgba(0,0,0,0.4) 80%, transparent 100%);
}
/* Content above the grid. The hero video already carries z-index 0, so the
   grid sits behind it as well, which is what we want. */
.gridfield > .wrap { position: relative; z-index: 1; }

html[data-surface="paper"] .gridfield::before {
    -webkit-mask-image: none;
    mask-image: none;
}
@media print {
    .gridfield::before { display: none; }
}

/* ==========================================================================
   PHASE 19 -- GLASS TREATMENT ON THE RADIAL PANEL (2026-08-16)

   The panel sits over the hero video, so there is something behind it worth
   sampling. A blurred, part-transparent surface reads as depth rather than as
   a box pasted on top - and it lets the footage through without the footage
   competing with the drawing.

   THE LIT TOP EDGE IS WHAT SELLS IT, not the blur. A uniform translucent
   rectangle reads as a weak panel; the thin bright line along the top edge is
   what the eye interprets as a pane catching light. Same technique as the
   nav menu in Phase 8b.

   RADIUS IS AN EXCEPTION to the house rule of no radius above 2px, alongside
   --radius-card. Recorded rather than quietly introduced - see CLR-0060.

   FALLBACK MATTERS. Without backdrop-filter the panel would be 55 percent
   transparent over moving video, which is unreadable. The @supports block
   gives non-supporting browsers a solid surface instead.
   ========================================================================== */

.panel.radial {
    position: relative;
    border-radius: 10px;
    background: rgba(12, 17, 23, 0.55);
    border: 1px solid rgba(160, 190, 220, 0.10);
    -webkit-backdrop-filter: blur(20px) saturate(125%);
    backdrop-filter: blur(20px) saturate(125%);
    box-shadow:
        0 18px 50px rgba(0, 0, 0, 0.45),
        inset 0 1px 0 rgba(255, 255, 255, 0.04);
    overflow: hidden;
}

/* The lit edge. A 1px gradient along the top, brightest in the middle, which
   is what a pane of glass does under a light source. */
.panel.radial::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 1px;
    background: linear-gradient(to right,
        transparent 0%,
        rgba(255, 255, 255, 0.16) 30%,
        rgba(255, 255, 255, 0.22) 50%,
        rgba(255, 255, 255, 0.16) 70%,
        transparent 100%);
    pointer-events: none;
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .panel.radial { background: var(--panel); }
}

html[data-surface="paper"] .panel.radial {
    background: rgba(255, 255, 255, 0.72);
    border-color: var(--rule);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}

@media print {
    .panel.radial {
        background: none;
        box-shadow: none;
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }
}

/* ==========================================================================
   PHASE 20 -- NAVIGATION MENU (2026-08-16)

   Four faults, in order of how much they mattered:

   1. IT LEAKED INTERNAL SCAFFOLDING. Two entries rendered "NEEDS PROOF POINT -
      not yet supplied or cleared" as their description, in the public
      navigation, on every page of the site. That is the same fault as the
      SITE STATE rail: instrumentation for us, shown to a stranger. The tagline
      is now suppressed when it is a placeholder and the tier label shown
      instead.

   2. FIVE DOMAINS IN AN AUTO-FIT GRID left the fifth orphaned on its own row,
      half the width of the panel and visually detached. Five explicit columns
      at full width, dropping to three then two as the viewport narrows.

   3. NO SIGNAL. The menu listed ten systems as equals while /platforms
      distinguishes what runs from what is planned. A dot per entry carries
      that through, so the reader knows before they click.

   4. THE HEADS WERE RAGGED. Domain names run to three lines, which pushed
      each column's first entry to a different height. A fixed head height
      lines them up.
   ========================================================================== */

.nav-menu {
    width: min(1120px, calc(100vw - 3rem));
    padding: var(--sp-4);
}

.nav-menu-grid {
    grid-template-columns: repeat(5, 1fr);
    gap: var(--sp-3);
    align-items: start;
}

.nav-col {
    padding: var(--sp-3) var(--sp-2) var(--sp-2);
    background: rgba(34, 45, 59, 0.45);
}
.nav-col:hover, .nav-col:focus-within {
    background: rgba(48, 63, 82, 0.72);
}

.nav-col-head {
    display: grid;
    grid-template-columns: 16px 1fr auto;
    align-items: start;
    gap: 0.5rem;
    min-height: 3.4em;
    line-height: 1.35;
    padding-bottom: var(--sp-2);
    margin-bottom: var(--sp-2);
}
.nav-col-name { min-width: 0; }
/* The count sits quietly against the heading rather than competing with it. */
.nav-col-n {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--ink-dim);
    letter-spacing: 0;
}

.nav-twin-name {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    font-size: var(--t-small);
}
/* One dot, three states - the same colour logic as the build record and the
   radial spokes. A promise is not coloured like a fact. */
.nav-dot {
    flex: 0 0 auto;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: currentColor;
}
.nav-twin-running  .nav-dot { color: var(--live); }
.nav-twin-building .nav-dot { color: var(--signal); }
.nav-twin-committed .nav-dot { color: var(--ink-dim); }

.nav-twin-sub { padding-left: calc(5px + 0.5rem); }
.nav-twin-soon {
    font-family: var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--ink-dim);
    opacity: 0.75;
}

@media (max-width: 1180px) {
    .nav-menu-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 900px) {
    .nav-menu-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ==========================================================================
   PHASE 20b -- OPAQUE MENU, GLYPHS INSTEAD OF DOTS (2026-08-16)

   TRANSPARENCY WAS WRONG HERE. Glass works on the hero panel because there is
   one thing behind it and the panel is decorative. This menu sits over dense
   body copy and its job is to be READ - the headline showing through the
   columns made both harder to read, and the panel is where a first-time
   visitor navigates from. Opaque.

   THE DOTS ARE REPLACED BY THE SEGMENT GLYPHS. A dot encoded tier only, and
   the tier is already spelled out underneath for anything not running. The
   glyph identifies the system, which is the more useful thing to carry, and it
   is the same mark used on the radial and on /platforms.
   ========================================================================== */

.nav-menu {
    background: #10161e;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-color: var(--rule-bright);
    box-shadow: 0 22px 60px rgba(0, 0, 0, 0.72);
}
.nav-col {
    background: var(--panel);
    border-color: var(--rule);
}
.nav-col:hover, .nav-col:focus-within {
    background: var(--panel-raised);
    border-color: var(--rule-bright);
}

.nav-glyph {
    flex: 0 0 18px;
    width: 18px;
    height: 18px;
    object-fit: contain;
    display: block;
    /* The glyphs are drawn for a dark ground but vary in weight; a small lift
       keeps the fainter ones legible at 18px without making them shout. */
    filter: brightness(1.1);
    align-self: center;
}
.nav-glyph-none { visibility: hidden; }

.nav-twin-name {
    display: flex;
    align-items: center;
    gap: 0.55rem;
}
.nav-twin-label { min-width: 0; }

/* The description lines up under the label, not under the glyph. */
.nav-twin-sub { padding-left: calc(18px + 0.55rem); }

html[data-surface="paper"] .nav-menu { background: var(--panel); }
html[data-surface="paper"] .nav-glyph { filter: none; }

/* ==========================================================================
   PHASE 20c -- MENU CENTRED ON THE PAGE, NOT ON ITS TRIGGER (2026-08-16)

   The panel is 1120px wide and was running off the right edge of the
   viewport, cutting the fifth domain column in half.

   THE CAUSE WAS NOT THE WIDTH. left:50% plus translateX(-50%) centres an
   absolutely positioned element on its CONTAINING BLOCK, and PHASE 8 set
   .nav-item to position:relative - so the containing block was the Platforms
   LINK, about 90px wide and sitting right of centre in the header. The menu
   was correctly centred on a small element near the right edge. It only
   looked like a width problem.

   THE FIX IS THE CONTAINING BLOCK. .nav-item.has-menu goes back to static and
   the header .wrap becomes the reference instead. .wrap is max-width 1220px
   with margin:0 auto, so its centre IS the page centre at every viewport
   width - which makes left:50% do what it was always meant to do. Nothing
   about the width, the transform or the open/close transition changes.

   THE 10px BRIDGE IS NOT PADDING, AND IT IS LOAD-BEARING. .nav a is 44px
   tall inside a 58px header row, so the link's bottom edge sits about 7px
   above the header's. Anchored to the link, top:100% put the panel flush
   under the link - which is what the PHASE 8 note about the hover gap was
   protecting. Anchored to the wrap, top:100% resolves to the bottom of the
   58px row instead, opening that 7px as dead space. A pointer crossing dead
   space drops :hover and the menu closes on the way to it. The ::after
   extends the menu's own hit area upward to cover the gap; because it is a
   descendant of .nav-item, hovering it keeps .nav-item:hover true.

   IT IS INERT WHILE CLOSED. The panel carries pointer-events:none until it
   opens, so the bridge cannot catch a pointer that is merely passing along
   the header.
   ========================================================================== */

.site-head .wrap { position: relative; }

.nav-item.has-menu { position: static; }

/* Now resolves against the header wrap: bottom of the 58px row. */
.nav-menu { top: 100%; }

.nav-menu::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: -10px;
    height: 10px;
}

/* Below the desktop breakpoint the menu is static and inline, so the bridge
   would be an orphaned 10px box resolving against the wrap. Removed, exactly
   as ::before already is. */
@media (max-width: 820px) {
    .nav-menu::after { display: none; }
}

@media print {
    .nav-menu::after { display: none !important; }
}

/* ==========================================================================
   PHASE 21 -- GROUND DARKENED (2026-08-16)

   The page ground and the instrument faces sat almost on top of each other.
   Measured, before this change:

       panel        #0c1117  on ground  1.032 : 1
       panel-raised #111823  on ground  1.121 : 1
       nav menu     #10161e  on ground  1.099 : 1

   A panel edge separating at 1.03 is a hairline border doing all the work,
   and when that border is --rule the enclosure reads as a slightly different
   patch of the same black rather than as a thing sitting ON the page.

   THE GROUND MOVES, THE PANELS DO NOT. Darkening the ground raises every one
   of those separations without touching a single surface the content sits on,
   so no text contrast anywhere is reduced - every ratio against the ground
   goes UP. --ink-dim, the tightest text on this site, measures 5.58 : 1 before
   and 5.74 : 1 after against the ground, so the AA floor moves further away
   rather than closer. After:

       panel        #0c1117  on ground  1.062 : 1   (+92% separation)
       panel-raised #111823  on ground  1.153 : 1
       nav menu     #10161e  on ground  1.131 : 1

   NOT PURE BLACK, deliberately. #000 drops the blue cast the whole palette is
   built on, and full black behind near-black panels reads as a hole rather
   than as depth. #04060a keeps the hue and roughly halves the luminance.

   --ground-rgb IS THE SOURCE OF TRUTH AND --ground IS DERIVED FROM IT. That
   is not tidiness, it is a bug fix. The header needs the ground colour with
   an alpha for its blur, and rgba() cannot take a hex custom property - so
   the previous code hand-copied the literal rgba(8, 11, 16, 0.94). It then
   drifted twice over: it would have kept the OLD ground after this change,
   and it was never overridden on the paper surface, so the light surface has
   been rendering a near-black header this whole time. Both faults come from
   the same copied literal. Derived, they cannot recur.
   ========================================================================== */

:root {
    --ground-rgb: 4 6 10;
    --ground: rgb(var(--ground-rgb));
}

/* Same colour as the page, at the alpha the sticky blur needs. */
.site-head {
    background: rgb(var(--ground-rgb) / 0.94);
}

/* PAPER SURFACE HEADER FIX. The paper block overrides --ground but could not
   reach the hand-written rgba above, so the light surface carried a dark
   header. It follows the ground now. */
html[data-surface="paper"] {
    --ground-rgb: 245 246 247;
}

/* PHASE 21 -- PRINT GROUND RE-ASSERTED.
   CAUGHT BEFORE SHIPPING, and worth recording because it is a trap this
   stylesheet will hit again. The PHASE 3b print block sets --ground: #ffffff
   on :root. A media query adds NO specificity, so :root inside @media print
   and :root at the top of the file are equally specific and SOURCE ORDER
   decides. The PHASE 21 :root above is later in the file than the print
   block, so it was overriding white with the new dark ground for print.

   Impact would have been small - print sets body, .panel and the rest to
   #ffffff explicitly, and the remaining var(--ground) users are hidden or
   transient on paper - but "small" is not "correct", and the next token
   redefined at the end of this file might not be so lucky.

   RULE FOR FUTURE PHASES: any :root added at the end of this stylesheet must
   re-assert the print and paper values it shadows, or be written with a
   selector specific enough not to shadow them. */
@media print {
    :root {
        --ground-rgb: 255 255 255;
        --ground: #ffffff;
    }
}

/* ==========================================================================
   PHASE 22 -- SECTION CARD (2026-08-16)

   Section 01 on the landing page was a label, a headline, two paragraphs and
   a large figure, all sitting directly on the ground. On /platforms the same
   kind of content is enclosed - band header, divider, body - and reads as one
   object. Here it read as four things that happened to be near each other,
   and the loop figure in particular floated with nothing holding it.

   SAME MATERIALS, NOT A NEW COMPONENT. .section-card is added to the EXISTING
   .domain-card rule rather than given its own copy of the border, radius,
   background and overflow. One rule, two names. Copying those five
   declarations is exactly how the header background drifted out of step with
   --ground in PHASE 21, and the fix for that was the same move: derive, never
   duplicate. Change --radius-card and both enclosures follow.

   THE HEAD IS THE ANALOGUE OF .domain-band-inner, with the same padding and
   the same --rule bottom border the band carries inside a card. It holds the
   framing - section label, headline, bridge line - and the body holds the
   argument and the figure, which mirrors how a domain card puts the name and
   blurb above the schedule.

   NO PHOTOGRAPH, deliberately. The domain band carries one because it is a
   preview of the page it links to. This card leads nowhere and an image here
   would be decoration standing where the drawing should be - the same
   reasoning that keeps the landing hero free of generated art.

   .loopfig ALREADY CARRIES ITS OWN TOP RULE, so inside the body that rule
   becomes the divider between the closing paragraph and the figure. Same
   behaviour as .offers inside a domain card; nothing needed adding.
   ========================================================================== */

.section-card-head {
    padding: var(--sp-5) var(--sp-5) var(--sp-4);
    border-bottom: 1px solid var(--rule);
}

/* Tightened to match the band, where the label sits closer to the name it
   introduces than a free-standing section callout does. */
.section-card-head .slabel { margin-bottom: var(--sp-3); }
.section-card-head > *:last-child { margin-bottom: 0; }
.section-card-body > *:last-child { margin-bottom: 0; }

/* The figure is the last thing in the card and its caption already provides
   the closing space, so the figure's own bottom margin would double it. */
.section-card-body > .loopfig { margin-bottom: 0; }

@media (max-width: 640px) {
    .section-card-head { padding: var(--sp-4) var(--sp-4) var(--sp-3); }
    .section-card-body { padding: var(--sp-4) var(--sp-4) var(--sp-3); }
}

/* Flatten on paper and in print, exactly as the domain card does - a radius
   and a panel fill are screen affordances. */
@media print {
    .section-card {
        border-radius: 0;
        border-color: #b8bfc7;
        background: #ffffff;
        break-inside: avoid;
    }
    .section-card-head { padding: 0 0 4mm; border-bottom-color: #b8bfc7; }
    .section-card-body { padding: 4mm 0 0; }
}

/* --- BUG FIX: .lede-sub had no font size ---------------------------------
   PHASE 13b set `font-size: var(--t-lede)`. THAT TOKEN HAS NEVER EXISTED -
   the scale defines --t-lead. An undefined custom property does not fall back
   to the previous declaration; it makes the whole declaration INVALID AT
   COMPUTED-VALUE TIME, which for an inherited property like font-size means
   the element inherits instead. So the bridge line has been rendering at body
   size, and PHASE 13b's stated intent - "sized between the headline and body
   copy" - has never once been true on the page.

   This is the failure mode of custom properties worth remembering: a typo in
   a var() name is silent. No parse error, no console warning, and the rule
   looks correct in the source. It only shows up by measuring the rendered
   text against the token it claims to use.

   FIXED AT SOURCE in the PHASE 13b block rather than overridden from here, so
   there is exactly one .lede-sub font-size declaration in this file and no
   dead broken rule left above for someone to trip over later. */

/* ==========================================================================
   PHASE 22b -- SECTION 01 HEAD PHOTOGRAPH (2026-08-16)

   Supersedes the "NO PHOTOGRAPH, deliberately" note in PHASE 22 at Daniel's
   direction. What makes it defensible rather than merely overruled: the
   subject is a CLOSED LOOP - a circular interchange with vehicle light trails
   that come back on themselves - so the image carries the same argument the
   figure below it makes. A generic aerial here would have been decoration.

   OPACITY IS 0.48, NOT THE BAND'S 0.62, AND THAT IS MEASURED. Full-resolution
   mean luminance, Rec.601, across the shipped set:

       og_page_education            17.5   <- dimmest shipped
       og_domain_built_environment  29.2   <- brightest shipped
       og_section_control_loop      32.4   <- this asset

   It is brighter than every other asset on the site, and its left third
   measures 19.6 against a shipped range of 6.6 to 17.4 - the brightest left
   third in the set, which is exactly the third the headline sits on. At the
   band's 0.62 it would read visibly hotter than every domain card.

   0.62 * 25.6 (the demonstration band) = 15.9 effective. To land the same
   weight: 15.9 / 32.4 = 0.49. Rounded to 0.48.

   THE HONEST FIX IS A CRUSH, NOT AN OPACITY. post_art.py exists for exactly
   this and would bring the file itself into the band, which is what keeps the
   SET matched rather than papering over one member in CSS. It could not be
   run: art_post.bat probes ComfyUI's venv for PIL and numpy, and no such
   interpreter exists on this machine any more. Recorded as a follow-up.

   THE MASK RAMPS LATER THAN THE BAND'S. The domain band clears to 26 percent
   and reaches strength at 62. This head carries a 28px headline running to
   roughly 56 percent of the content width, over a brighter asset, so the
   clear run extends to 38 percent and strength arrives at 70. Numbers chosen
   against the rendered text, and worth re-checking by LOOKING at the page.

   CHILDREN ARE POSITIONED, and that is load-bearing. A positioned ::after at
   z-index 0 paints ABOVE the inline content of non-positioned siblings - the
   text would have gone under the photograph. The band solves this with a
   .domain-band-inner wrapper; here the children are lifted directly so no
   markup changes.
   ========================================================================== */

.section-card-head {
    position: relative;
    overflow: hidden;
}

/* Above the photograph. See the note on paint order above. */
.section-card-head > * {
    position: relative;
    z-index: 1;
}

/* SCOPED TO THE LANDING PAGE'S SECTION 01 ONLY. Corrected 2026-08-17,
   CLR-0085.

   This selector was written for one card and applied to EVERY .section-card
   on the site. The control-loop interchange photograph was therefore
   rendering behind all three seam cards on the landing page and behind the
   exclusivity card on /partners - a picture of a road junction sitting
   behind a paragraph about partnership terms.

   It survived review because it looks deliberate: a dark photograph under a
   dark panel reads as texture rather than as the wrong image. The clue was
   that the same frame appeared four times on one page.

   .home-loop-card is applied by index.html to that one card. Any future card
   wanting art has to ask for it by name, which is the behaviour a background
   this specific should always have had. */
.home-loop-card .section-card-head::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: url("../img/generated/og_section_control_loop_575278925553576.png");
    background-size: cover;
    background-position: right center;
    background-repeat: no-repeat;
    opacity: 0.48;
    z-index: 0;
    pointer-events: none;
    -webkit-mask-image: linear-gradient(to right,
        transparent 0%, transparent 38%, rgba(0,0,0,0.9) 70%, #000 100%);
    mask-image: linear-gradient(to right,
        transparent 0%, transparent 38%, rgba(0,0,0,0.9) 70%, #000 100%);
}

/* Same three suppressions the band and the hero art carry: no room beside the
   text at narrow widths, and a dark night aerial cannot work on paper or in
   print. */
@media (max-width: 980px) {
    .home-loop-card .section-card-head::after { display: none; }
}

html[data-surface="paper"] .home-loop-card .section-card-head::after { display: none; }

@media print {
    .home-loop-card .section-card-head::after { display: none !important; }
}

/* ==========================================================================
   PHASE 23 -- THE STACK TABLE (2026-08-16)

   Five rows of the Purdue model with one of them marked as ours. The design
   problem is that the argument lives in the ROWS WE DO NOT OWN: four rows
   saying "your control vendors" are what makes the fifth row credible. So
   the four must be legible and calm rather than greyed into decoration, and
   the fifth must read as different in kind rather than merely highlighted.

   NOT A <table>. The content is five records with a name, a detail line and
   an owner - it reads as a stack, and at narrow widths a real table either
   scrolls sideways or collapses into something worse. Grid rows reflow.

   THE OURS ROW IS MARKED THREE WAYS - amber left edge, raised panel, amber
   level label - because on a near-black ground a single cue at this contrast
   is easy to miss, and this is the one row a reader must not miss.
   ========================================================================== */

.stack-table {
    margin-top: var(--sp-5);
    border: 1px solid var(--rule);
    border-radius: var(--radius-card);
    overflow: hidden;
}

.stack-row {
    display: grid;
    grid-template-columns: 130px minmax(0, 1fr) 190px;
    gap: var(--sp-4);
    align-items: baseline;
    padding: var(--sp-4) var(--sp-4);
    border-bottom: 1px solid var(--rule);
    border-left: 3px solid transparent;
}

.stack-row:last-child { border-bottom: 0; }

.stack-level {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-dim);
}

.stack-name { color: var(--ink); }

.stack-detail {
    font-size: var(--t-small);
    color: var(--ink-dim);
    margin-top: 0.2rem;
}

/* Right-aligned so the eye can run down the ownership column on its own and
   see four names that are not us before reaching the one that is. */
.stack-owner {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--ink-dim);
    text-align: right;
}

.stack-row-ours {
    background: var(--panel-raised);
    border-left-color: var(--signal);
}

/* --signal is the section-marker amber used by .slabel .idx, not the
   low-confidence amber of the instrument readouts. Same token, and the
   surrounding context is what disambiguates it. */
.stack-row-ours .stack-level { color: var(--signal); }
.stack-row-ours .stack-name { color: var(--ink); }
.stack-row-ours .stack-owner { color: var(--ink-mid); }

@media (max-width: 820px) {
    /* One column. The owner line keeps its own row rather than being dropped:
       it IS the argument, so losing it on a phone loses the point. */
    .stack-row {
        grid-template-columns: 1fr;
        gap: var(--sp-2);
    }
    .stack-owner { text-align: left; }
}

@media print {
    .stack-table { border-color: #b8bfc7; break-inside: avoid; }
    .stack-row { border-bottom-color: #b8bfc7; }
    .stack-row-ours { background: transparent; border-left-color: #333333; }
}

/* ==========================================================================
   PHASE 24 -- CSP FIX: PULSE DELAYS AND CARD SPACING (2026-08-16)

   THESE WERE INLINE style ATTRIBUTES AND THEY WERE NOT WORKING.

   app.py sets style-src 'self' with no 'unsafe-inline', and its own comment
   claims "zero inline style attributes across every route, so 'self' with no
   'unsafe-inline' holds". That claim was false: index.html carried two,
   both setting animation-delay on the loop-figure pulses. A CSP-enforcing
   browser blocked them, so all four stage pulses fired SIMULTANEOUSLY
   instead of chasing round the loop, and the return pulse fired with them.

   The figure still animated, which is why it survived review - it just
   animated wrongly, and silently, with the console error being the only
   evidence. The stagger is the entire point of the animation: a loop is
   meant to look like something travelling round it.

   Delays are 0.5s apart, matching the arithmetic the template used before
   (outer.index0 * 0.5). Four stages plus the return leg at 1.6s.
   ========================================================================== */

.lp-delay-0 { animation-delay: 0s; }
.lp-delay-1 { animation-delay: 0.5s; }
.lp-delay-2 { animation-delay: 1s; }
.lp-delay-3 { animation-delay: 1.5s; }
.lp-delay-return { animation-delay: 1.6s; }

/* Was style="margin-top: 2rem" on /partners. Same CSP problem, caught before
   it shipped. */
.cards-spaced { margin-top: var(--sp-5); }

/* ==========================================================================
   PHASE 25 -- CONNECTOR MATRIX STATUS (2026-08-17)

   Reuses .stack-table. The only new thing is the status colour, and it is
   deliberately restrained: green on the built rows would turn a technical
   matrix into a scorecard, and the one row that says "Not yet" is the row
   this reader will trust the page for.

   So built rows get --live on the status only, and the gap row is left at
   --ink-dim rather than reddened. A gap stated plainly is not a failure
   state and should not be painted as one.
   ========================================================================== */

.conn-built .stack-owner { color: var(--live); }

/* ==========================================================================
   PHASE 26 -- BUILD TIMELINE (2026-08-17)  *** UNUSED AS OF 2026-08-17 ***

   The figure was removed from /platforms the same day it was added - see
   CLR-0087 and the note above build_timeline() in app.py. Nothing on the
   site carries .tlfig or any .tl- class now.

   RETAINED, NOT DELETED, because the styling is not what failed: the figure
   was positioned by true date and near-day clusters collided, which is a
   geometry problem. If the timeline is rebuilt with an ordinal axis or
   clustered marks, these rules still apply unchanged.

   MARKED SO A FUTURE UNUSED-SELECTOR SWEEP KNOWS IT IS INTENTIONAL. Delete
   this block and the TL/build_timeline code in app.py together, or neither.

   Original design note follows.

   Ten repositories against a date axis. The persuasive content is the SHAPE:
   a stack of four on 10 July, then a run of singles through August. So the
   drawing is styled to keep the shape legible and nothing else - no gradient
   fills, no glow, no animation. It is evidence, and evidence that looks
   sold is worth less.

   TIER IS THE ONLY COLOUR, and it follows the meanings already fixed in the
   token set rather than inventing a third scale: --live for what runs,
   --instrument for what is being built, --ink-dim for what is committed with
   little or no code. A reader who has seen the status pills on the rows
   below already knows what these mean.

   THE NAME SITS BESIDE THE MARK, not under it. Under the mark, four stacked
   labels collide at 21px spacing; beside it they read as a list and the
   column still reads as a column.
   ========================================================================== */

.tlfig {
    margin: var(--sp-5) 0 var(--sp-4);
}

.tlfig svg {
    width: 100%;
    height: auto;
    display: block;
    overflow: visible;
}

.tl-axis { stroke: var(--rule-bright); stroke-width: 1; }
.tl-tick { stroke: var(--rule-bright); stroke-width: 1; }

/* Dashed, so it reads as a tie-line rather than as a measured quantity. A
   solid stem would look like a bar chart, and height here is a count of
   systems on one day - not a magnitude. */
.tl-stem {
    stroke: var(--rule);
    stroke-width: 1;
    stroke-dasharray: 2 3;
}

.tl-date {
    fill: var(--ink-dim);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-anchor: middle;
}

.tl-name {
    fill: var(--ink-mid);
    font-family: var(--font-mono);
    font-size: 11.5px;
}

.tl-mark { stroke: none; }
.tl-running   { fill: var(--live); }
.tl-building  { fill: var(--instrument); }
.tl-committed { fill: var(--conf-off); }

.tl-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-4);
    margin-top: var(--sp-3);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-dim);
}

/* The swatch is a pseudo-element so the legend stays plain text in the
   markup and in a screen reader. */
.tl-key::before {
    content: "";
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
    margin-right: 0.5rem;
    vertical-align: baseline;
    background: currentColor;
}
.tl-legend .tl-running   { color: var(--live); }
.tl-legend .tl-building  { color: var(--instrument); }
.tl-legend .tl-committed { color: var(--conf-off); }

@media (max-width: 720px) {
    /* Ten labels in mono do not survive a phone width. The figure keeps its
       shape and loses its names - the dates and the legend still carry the
       argument, and the rows below name every system anyway. */
    .tl-name { display: none; }
}

@media print {
    .tl-axis, .tl-tick { stroke: #666666; }
    .tl-stem { stroke: #b8bfc7; }
    .tl-date, .tl-name { fill: #333333; }
    .tl-mark { fill: #333333; }
    .tlfig { break-inside: avoid; }
}

/* ==========================================================================
   PHASE 27 -- THE STACK, DRAWN (/partners only)  2026-08-17

   The landing page keeps the .stack-table. On /partners that table was the
   fourth stacked-row block on one page, and the most important argument was
   the hardest to tell apart from the three around it.

   THE OWNED LAYERS ARE SOLID AND OURS IS OPEN. Four bands are filled panels
   because somebody owns them; the hand-off is a dashed outline with no fill,
   because the claim is that it is structurally unowned. The drawing states
   the argument rather than illustrating it - which is the difference between
   a diagram and a picture.

   THE FOUR ARE NOT GREYED OUT. It is tempting to dim them so ours pops, but
   the persuasive content IS the four - a reader relaxes because four layers
   are visibly not contested. Dimming them would throw away the point to make
   a highlight look better.
   ========================================================================== */

.sfig { margin: var(--sp-5) 0 0; }

.sfig svg {
    width: 100%;
    height: auto;
    display: block;
}

.sf-band {
    fill: var(--panel);
    stroke: var(--rule-bright);
    stroke-width: 1;
}

/* No fill, dashed edge: an opening between the owned layers rather than
   another block sitting among them. */
.sf-ours {
    fill: none;
    stroke: var(--signal);
    stroke-width: 1;
    stroke-dasharray: 5 4;
}

.sf-edge { fill: var(--signal); }

.sf-level {
    fill: var(--ink-dim);
    font-family: var(--font-mono);
    font-size: 11.5px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.sf-level-ours { fill: var(--signal); }

.sf-name { fill: var(--ink); font-size: 15px; }
.sf-name-ours { fill: var(--ink); font-weight: 600; }

.sf-detail {
    fill: var(--ink-dim);
    font-family: var(--font-mono);
    font-size: 11.5px;
}

.sf-owner {
    fill: var(--ink-mid);
    font-family: var(--font-mono);
    font-size: 11.5px;
    text-anchor: end;
}

/* A 760px rule here DELETED .sf-owner and .sf-detail on phones, on the
   grounds that the 900-unit viewBox scaled the mono text below legibility.
   The diagnosis was right and the treatment was wrong: the owner column is
   the entire argument of this figure - four layers that are not ours - and
   dropping it left a phone reader with five bands whose names were rendering
   at about 4px anyway. REMOVED 2026-09-14, PHASE 35. Below 900px the whole
   drawing is replaced by the same five rows as HTML, owner column intact. */

@media print {
    .sf-band { fill: #ffffff; stroke: #b8bfc7; }
    .sf-ours { fill: none; stroke: #333333; }
    .sf-edge { fill: #333333; }
    .sf-level, .sf-detail, .sf-owner { fill: #444444; }
    .sf-name { fill: #000000; }
    .sfig { break-inside: avoid; }
}

/* ==========================================================================
   PHASE 28 -- CAPABILITY TABLE (/partners)  2026-08-17

   FIXES A REAL LAYOUT BUG. This block reused .stack-table, which is a THREE
   column grid - level, body, owner - and supplied only TWO children. Grid
   places children in order, so the name and description landed in the 130px
   LEVEL column and wrapped every two or three words down a narrow ribbon,
   the term sat in the 1fr middle, and the 190px right column stood empty.

   The lesson is that .stack-row is not a generic row: its column widths ARE
   its contract, and a caller that supplies the wrong number of children gets
   a silently wrong layout rather than an error. Anything with a different
   shape gets its own grid, which is what this is.

   TWO COLUMNS. The prose needs the width - these are full sentences aimed at
   a commercial reader, not labels - and the term is a short mono tag that
   should sit out of the way on the right.
   ========================================================================== */

.cap-table {
    margin-top: var(--sp-5);
    border-top: 1px solid var(--rule);
}

.cap-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 210px;
    gap: var(--sp-4);
    align-items: start;
    padding: var(--sp-4) 0;
    border-bottom: 1px solid var(--rule);
}

.cap-name {
    color: var(--ink);
    /* --t-body, 17px. An earlier draft invented --t-read-sm and relied on an
       inline fallback to the same number - which works, but a var() naming a
       token that does not exist is the exact silent failure that left
       .lede-sub sized wrong for weeks. Use the real token. */
    font-size: var(--t-body);
    font-weight: 600;
    letter-spacing: -0.005em;
}

.cap-desc {
    color: var(--ink-mid);
    margin-top: 0.35rem;
    max-width: 62ch;
}

/* Mono and dim: present for the engineer this gets forwarded to, and
   deliberately quiet for the person reading it first. */
.cap-term {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--ink-dim);
    text-align: right;
    padding-top: 0.3rem;
}

@media (max-width: 720px) {
    .cap-row { grid-template-columns: 1fr; gap: var(--sp-2); }
    .cap-term { text-align: left; padding-top: 0; }
}

/* ==========================================================================
   PHASE 29 -- THREE-ACROSS CARD ROW  2026-08-17

   .cards is auto-fit minmax(260px, 1fr) capped at --measure, and --measure is
   a READING width. Three cards inside it therefore wrap to two-plus-one, and
   an orphan card on its own row reads as a mistake rather than a set - which
   is what the three boundary statements on /partners were doing.

   The cap is right for prose cards sitting in a text column. It is wrong for
   a row of three short assertions that belong together, so this modifier
   lifts the cap and fixes the count at three. It does NOT change .cards.

   Below 900px it falls back to the normal auto-fit behaviour, where a
   single column is correct and there is no orphan to worry about.
   ========================================================================== */

.cards-3 {
    max-width: none;
    grid-template-columns: repeat(3, minmax(0, 1fr));
}

@media (max-width: 900px) {
    .cards-3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
}

/* ==========================================================================
   PHASE 30 -- CONNECTOR NAME LINE (/partners)  2026-08-17

   Replaces a six-row spec table with one line. The table moved to
   /technology where its reader is; here the names only have to be PRESENT -
   available to someone who wants them, ignorable by someone who does not.

   MONO, DIM, AND SMALL ON PURPOSE. Set at body weight it competes with the
   claim above it, which is the sentence that actually has to land. Set as a
   table it becomes the section, which is what just went wrong.

   THE GAP SITS ON THE SAME LINE as the tested connectors rather than in a
   caveat underneath. A gap on its own line below the good news is how fine
   print looks; a gap inline is a statement of fact.
   ========================================================================== */

.conn-line {
    font-family: var(--font-mono);
    font-size: var(--t-small);
    color: var(--ink-mid);
    letter-spacing: 0.02em;
    margin: var(--sp-4) 0 var(--sp-5);
    line-height: 1.9;
}

.conn-line-ok,
.conn-line-gap {
    font-size: var(--t-micro);
    letter-spacing: 0.09em;
    text-transform: uppercase;
    margin-left: 0.45rem;
}

.conn-line-ok { color: var(--live); }

/* Not red. A stated gap is not a failure state, and colouring it as one
   would punish the honesty this line exists to demonstrate. */
.conn-line-gap { color: var(--ink-dim); }

/* ==========================================================================
   PHASE 31 -- SEAM ANSWER  2026-08-17

   Each seam card states a problem then states what we do about it, and the
   two were rendering as identical grey paragraphs. The one that matters was
   second and unmarked, which is the wrong way round for a reader who skims.

   MARKED WITH A RULE AND A GLYPH, NOT A PANEL. A nested panel inside a card
   inside a section is three levels of enclosure for one sentence. A left
   rule and a lead-in mark cost nothing and survive a narrow window.

   BRIGHTER THAN THE BODY, not coloured. Amber here would read as a warning
   and green as a status pill - both wrong for a plain statement of what the
   product does. It just needs to be the lightest text in the card.
   ========================================================================== */

.seam-answer {
    position: relative;
    margin: var(--sp-4) 0 0;
    padding-left: var(--sp-4);
    border-left: 2px solid var(--signal);
    color: var(--ink);
    font-size: var(--t-lead);
    line-height: 1.55;
    max-width: 62ch;
}

/* The glyph sits in the padding, so it cannot reflow into the text or
   change the measure. */
.seam-answer::before {
    content: "->";
    position: absolute;
    left: calc(var(--sp-4) * -1 + 0.35rem);
    top: 0;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--signal);
    line-height: 2.2;
}

@media (max-width: 640px) {
    .seam-answer { font-size: var(--t-body); }
}

@media print {
    .seam-answer { border-left-color: #333333; color: #000000; }
    .seam-answer::before { color: #333333; }
}

/* ==========================================================================
   PHASE 32 -- PLAIN-LANGUAGE CLOSER  2026-08-18

   One line at the foot of each landing section for a reader without the
   vocabulary. The whole design problem is TONE: the audience is chief
   executives, and a line that announces itself as the simplified version
   reads as condescension. So it is set as an ASIDE, not as a callout.

   QUIETER THAN THE BODY, NOT LOUDER. No panel, no amber, no icon. A tinted
   key, a hairline above it, and text at body size. Someone who does not need
   it should be able to skip it without it having shouted; someone who does
   need it should find it exactly where the section ends.

   THE KEY IS INLINE, not stacked above. Stacked it becomes a heading and the
   section appears to have a fifth part. Inline it reads as a lead-in, which
   is what it is.
   ========================================================================== */

.plainly {
    margin: var(--sp-5) 0 0;
    padding-top: var(--sp-3);
    border-top: 1px solid var(--rule);
    max-width: var(--measure);
    color: var(--ink-mid);
    font-size: var(--t-body);
    line-height: 1.6;
}

.plainly-key {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-dim);
    margin-right: 0.7rem;
    white-space: nowrap;
}

@media print {
    .plainly { border-top-color: #b8bfc7; color: #333333; }
    .plainly-key { color: #666666; }
}

/* ==========================================================================
   PHASE 33 -- PAGE AIM LINE  2026-08-18

   One line in the hero naming who a page is written for. Only /technology
   uses it: 1,039 words at roughly double the jargon density of anything else
   on the site, with nothing telling an executive that was deliberate.

   SET AS A NOTE, NOT A WARNING. No amber, no border box, no icon. This is
   not an apology for the page being technical - it is a signpost, and a
   signpost styled as a caution reads as "sorry about this". Depth stated
   plainly is confidence; depth flagged nervously is not.

   Sits under the lede rather than above the headline, so the headline still
   lands first for the reader who does belong here.
   ========================================================================== */

.page-aim {
    margin: var(--sp-4) 0 0;
    max-width: 58ch;
    color: var(--ink-dim);
    font-size: var(--t-small);
    line-height: 1.65;
}

.page-aim-key {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-mid);
    margin-right: 0.6rem;
}

.page-aim a { color: var(--ink-mid); }

@media print {
    .page-aim { color: #444444; }
}

/* ==========================================================================
   PHASE 34 -- PRODUCT SCREENSHOTS  2026-08-22
   REVISED SAME DAY: thumbnail grid, not a stack of full-width images.

   The first version ran each shot at the full width of the card body. With
   five of them on the energy card that turned one domain into a scroll of
   screenshots and buried the offers underneath - the images stopped being
   evidence for the systems list and became the section.

   NOW A GRID OF THUMBNAILS. Each one is a link that opens the full asset, so
   nothing is lost; what changes is that the default state is a glance rather
   than a study. This is the marketing register - show that it exists, let
   the reader choose to look closely.

   THE THUMBNAIL IS DELIBERATELY TOP-CROPPED, not letterboxed. These are
   1235x952 operator screens; scaled whole into a wide thumbnail they become
   an illegible grey rectangle. object-position: top keeps the title bar and
   the first panel row, which is the part that reads as a real application at
   any size.

   NO SHADOW, NO TILT, NO BROWSER CHROME. The argument is "this exists and
   here it is" - a stylised device frame says "here is a rendering of a
   product concept", which is the opposite claim.

   The caption stays required. Every shot states what is live and what is
   modelled, the same rule the site applies to numbers.
   ========================================================================== */

.shots {
    margin-top: var(--sp-5);
    padding-top: var(--sp-4);
    border-top: 1px solid var(--rule);
}

.shots-head {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-dim);
    margin-bottom: var(--sp-4);
}

/* auto-fill rather than auto-fit: with a single screenshot in a domain,
   auto-fit would stretch it back to full width, which is the thing being
   fixed. auto-fill holds the track size and leaves the row short. */
.shots-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 300px));
    gap: var(--sp-4);
}

.shot { margin: 0; }

.shot a {
    display: block;
    border: 1px solid var(--rule-bright);
    border-radius: var(--radius-card);
    overflow: hidden;
    background: var(--panel);
}

.shot img {
    display: block;
    width: 100%;
    height: 168px;
    object-fit: cover;
    object-position: top;
}

.shot a:hover { border-color: var(--ink-dim); }

.shot figcaption {
    margin-top: var(--sp-2);
    color: var(--ink-dim);
    font-size: var(--t-micro);
    line-height: 1.5;
}

@media (max-width: 620px) {
    .shots-grid { grid-template-columns: 1fr; }
    .shot img { height: 200px; }
}

@media print {
    /* A dark operator screen prints as a black rectangle and wastes a page. */
    .shots { display: none; }
}

/* ==========================================================================
   PHASE 35 -- MOBILE FIGURE FALLBACKS  2026-09-14

   MEASURED, NOT EYEBALLED. SVG text is sized in USER UNITS, so it scales with
   the viewBox. Every drawn figure here is authored 700 to 1040 units wide and
   rendered at width:100%, which means its type shrinks in direct proportion
   to the viewport. Rendered size of the SMALLEST type in each figure, on a
   390px phone with --pad-page at 1.15rem:

       figure                viewBox    authored    rendered
       loop      (landing)   1040 x 300   11.5 px     3.5 px
       stack     (/partners)  900 x 347   11.5 px     4.2 px
       radial    (landing)    700.7x640   15.0 px     8.6 px
       schematic (/technology)1000 x 330   9.0 px     5.8 px

   The 12px floor set in PHASE 1 exists because this audience is senior and a
   meaningful share of it has presbyopia. Three and a half pixels is not small
   type, it is a grey smear.

   THE TWO EXISTING MOBILE RULES MADE IT WORSE, and both are removed with this
   phase - see the notes left at PHASE 12 and PHASE 27. One stepped .lp-cap
   DOWN to 9.5px at 760, the other DELETED the .sf-owner column at 760. Both
   were written as though a user unit were a CSS pixel.

   WHY A FALLBACK AND NOT A BIGGER FONT. The geometry of both figures is
   computed in app.py against the authored size - _wrap() budgets characters
   from SF["name_px"], and radial_nodes() derives the viewBox width from
   char_px. Raising the CSS font size alone overruns boxes the Python still
   believes are wide enough. A horizontal scroll pane is the other option and
   it makes a phone reader drag a diagram around to read a five-row table.

   NOTHING NEW IS CLAIMED. Every string in a fallback comes from the same
   loop_stages / stackfig / radial data the drawing renders. No clearance row
   changes, because no copy changes.

   PRINT TAKES THE FALLBACK TOO, deliberately. A print media query is
   evaluated against the PAPER width - about 816px for US Letter at 96dpi -
   so these drawings were already printing below the floor.
   ========================================================================== */

.fig-alt { display: none; }

@media (max-width: 900px) {
    .loopfig > svg, .sfig > svg { display: none; }
    .loopfig > .fig-alt, .sfig > .fig-alt { display: block; }
}

@media print {
    .loopfig > svg, .sfig > svg { display: none; }
    .loopfig > .fig-alt, .sfig > .fig-alt { display: block; }
}

/* --- the control loop, stacked ------------------------------------------ */
/* Numbered rows rather than a bulleted list: the sequence IS the argument,
   and the return line below restates what the drawn arrow says. Dropping it
   would leave four steps and no loop. */

.loop-steps {
    list-style: none;
    margin: 0;
    padding: 0;
    border-top: 1px solid var(--rule);
}

.loop-step {
    display: grid;
    grid-template-columns: 2.2rem minmax(0, 1fr);
    gap: 0.35rem var(--sp-3);
    padding: var(--sp-3) 0;
    border-bottom: 1px solid var(--rule);
}

.loop-step-n {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.08em;
    color: var(--signal);
}

.loop-step-cap {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink);
}

.loop-step-body {
    grid-column: 2;
    color: var(--ink-mid);
    font-size: var(--t-small);
    line-height: 1.6;
}

.loop-return {
    margin: var(--sp-3) 0 0;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    line-height: 1.6;
    color: var(--ink-dim);
}

/* --- the coverage ring, named -------------------------------------------- */
/* The ring itself survives on a phone: ten glyphs around one hub still says
   one layer, many processes. Only the LABELS fail, at 8.6px, so only the
   labels are replaced. Tier colour follows the spoke, which is where the
   drawing already carries it. */

@media (max-width: 640px) {
    .radial .node-label { display: none; }
    .radial > .fig-alt { display: block; }
}

.radial-list {
    list-style: none;
    margin: var(--sp-3) 0 0;
    padding: var(--sp-3) 0 0;
    border-top: 1px solid var(--rule);
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--sp-2) var(--sp-3);
}

.radial-item {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--ink-mid);
}

.radial-item::before {
    content: "";
    width: 6px;
    height: 6px;
    flex: none;
    background: var(--conf-off);
}

.radial-running::before { background: var(--live); }
.radial-building::before { background: var(--signal); }
.radial-committed::before { background: var(--ink-dim); }

/* --- schematic, held at authored size ------------------------------------ */
/* The schematic is the one figure that keeps its scroll pane: it is a drawn
   data flow rather than a table, an engineer is its reader, and there is no
   list form of it that says the same thing. But min-width 640 against a
   1000-unit viewBox meant it was scrolling AND shrinking - 9px sub-labels at
   5.8px. Holding it at the authored width means the scroll buys legibility
   instead of only movement. */

@media (max-width: 900px) {
    .schematic svg { min-width: 1000px; }
}

/* ==========================================================================
   PHASE 36 -- HONEYCOMB HERO  2026-09-16

   Replaces the PHASE 17 / 19 radial in the landing hero at Daniel's request.
   Modelled on Cliff Pyles, "Interlocking Hexagon Grid Gallery (CSS-Only)",
   codepen.io/cliffpyles/pen/azmwMwE, itself after Temani Afif's responsive
   hexagon grid on CSS-Tricks. The grid maths below is the pen's.

   THE PHASE 17 / 19 .radial RULES ARE NOW NOT RENDERED. The .radial-list and
   .radial-item rules in PHASE 35 ARE still live - the honeycomb reuses them
   for its phone and print name list.

   WHAT WAS PORTED AS-IS: the interlocking offset-row layout, the hexagon
   clip-path, the edge vignette, the caption that rises in on hover, the lift and
   drop shadow, and the hexagonal focus ring.

   WHAT WAS NOT, AND WHY:
   1. FONTS. The pen imports Playfair Display and Montserrat from Google
      Fonts. font-src and style-src are 'self' in the CSP, and self-hosting
      rather than using a font CDN is decision 9.2. Captions use the site's
      own Inter and JetBrains Mono from the type scale.
   2. PHOTOS. The pen fills each cell with a picsum stock photo. Blocked by
      img-src 'self' and forbidden by Marketing Plan 10.1 in any case.
      PHASE 36c, 2026-09-16: every cell now takes the hub's plain dark face
      at Daniel's request. The per-domain aerials were tried first and
      removed - with ten photographs the comb read as a gallery and the
      glyphs, which are the part that differs, lost the contrast.
   3. SIZE. The pen is a full-page gallery with 220px cells. Here the comb
      has to fit the hero's right column, so it is 4-3-4 with the wordmark in
      the centre cell, and the cell width is DERIVED from the column with a
      container query rather than fixed per breakpoint.

   HOUSE-RULE EXCEPTIONS, RECORDED NOT DRIFTED: the stylesheet header forbids
   drop shadows and card hover lift. Both are the point of the pen, so both
   are allowed here and ONLY on .hex-link and the hub cell, along with the
   PHASE 36b bloom. Same treatment as --radius-card in CLR-0060. See CLR-0098.

   GRID MATHS. For a pointy-top hexagon of width w the height is
   w * 2 / sqrt(3). Rows overlap by a quarter of that height, so each cell
   carries a negative vertical margin, and the grid carries the same amount
   back as padding so the first and last rows are not clipped. The 5th cell
   (7n + 5) starts an offset row of three, which is what makes it interlock.
   ========================================================================== */

.honeycomb {
    container-type: inline-size;
    --hx-gap: 10px;
    --hx-w: min(144px, calc(100cqi / 4 - var(--hx-gap) - 1px));
    --hx-h: calc(var(--hx-w) * 1.1547);
    --hx-mx: calc(var(--hx-gap) / 2);
    --hx-my: calc((0.43301 * var(--hx-gap)) - (0.125 * var(--hx-h)));
    /* PHASE 36d, 2026-09-16: 35 percent transparent at Daniel's request so
       the hero video reads through the comb (first set at 20 percent, then
       raised). Same color as the old #111823. Glyphs, lamp and captions stay
       fully opaque. */
    --hx-face: rgb(17 24 35 / 0.65);
    --hx-ease: cubic-bezier(0.25, 1, 0.5, 1);

    /* PHASE 36b BLOOM, 2026-09-16. A soft light behind every cell so the
       comb separates from the hero video, which is the same near-black as
       the cell faces and was swallowing their edges.

       DONE WITH drop-shadow, NOT A BLURRED PSEUDO-ELEMENT. drop-shadow
       follows the ALPHA of what it is applied to, and the only opaque thing
       inside .hex-link is the clipped hexagon - so the glow is hexagonal for
       free. A ::before with blur() would need its own clip-path and would
       collide with the focus ring, which already uses ::before.

       Two layers: a tight one that defines the edge and a wide one that
       reads as bloom. Cool blue from the --instrument family, uniform across
       tiers - the lamp already carries tier, and a green bloom round six
       cells would shout. Committed cells glow at roughly half strength so
       the drawing still does not claim more than BUILD_RECORD does.

       Filter lists on rest and hover have the SAME three functions in the
       same order. If they did not, the browser could not interpolate them
       and the hover would snap instead of easing. */
    --hx-glow-rgb: 120 180 255;
    --hx-bloom-near: rgb(var(--hx-glow-rgb) / 0.30);
    --hx-bloom-far: rgb(var(--hx-glow-rgb) / 0.18);
    --hx-bloom-near-hot: rgb(var(--hx-glow-rgb) / 0.50);
    --hx-bloom-far-hot: rgb(var(--hx-glow-rgb) / 0.30);
}

.hex-committed {
    --hx-bloom-near: rgb(var(--hx-glow-rgb) / 0.16);
    --hx-bloom-far: rgb(var(--hx-glow-rgb) / 0.09);
}

/* A light surface has no darkness for a bloom to stand out against; it just
   greys the page. The depth shadow stays. */
html[data-surface="paper"] .honeycomb,
html[data-surface="paper"] .hex-committed {
    --hx-bloom-near: transparent;
    --hx-bloom-far: transparent;
    --hx-bloom-near-hot: transparent;
    --hx-bloom-far-hot: transparent;
}

@supports not (width: 1cqi) {
    .honeycomb { --hx-w: 120px; }
}

@media (min-width: 981px) {
    .hero-home .wrap {
        grid-template-columns: 0.94fr 1.06fr;
        align-items: center;
    }
}

.hex-grid {
    list-style: none;
    margin: 0 auto;
    display: flex;
    flex-wrap: wrap;
    width: calc(4 * (var(--hx-w) + var(--hx-gap)) + 1px);
    padding: calc(var(--hx-my) * -1) 0;
    perspective: 1000px;
}

.hex {
    width: var(--hx-w);
    height: var(--hx-h);
    margin: var(--hx-my) var(--hx-mx);
    position: relative;
}

.hex:nth-child(7n + 5) {
    margin-left: calc((var(--hx-w) / 2) + var(--hx-gap));
}

/* --- the cell ----------------------------------------------------------- */

.hex-link {
    display: block;
    width: 100%;
    height: 100%;
    position: relative;
    color: var(--ink);
    text-decoration: none;
    filter:
        drop-shadow(0 0 5px var(--hx-bloom-near))
        drop-shadow(0 0 20px var(--hx-bloom-far))
        drop-shadow(0 4px 8px rgba(0, 0, 0, 0.4));
    transform: translateY(0);
    transition: filter 0.5s var(--hx-ease), transform 0.5s var(--hx-ease);
}
.hex-link:hover { text-decoration: none; }

.hex-shape {
    display: block;
    width: 100%;
    height: 100%;
    position: relative;
    overflow: hidden;
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
    background-color: var(--hx-face);
}

/* Edge vignette. Every cell rests at the hub's strength so the whole comb
   reads as one surface, and it lifts on hover as part of the lit state. */
.hex-shape::after {
    opacity: 0.6;
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at 50% 50%,
        transparent 40%, rgba(0, 0, 0, 0.8) 120%);
    pointer-events: none;
    z-index: 1;
    transition: opacity 0.5s ease;
}

/* Hexagonal focus ring. The global :focus-visible outline is a rectangle,
   which around a hexagon reads as a selection box rather than focus. Neutral
   --focus, never amber, as PHASE 2 requires. */
.hex-link:focus-visible { outline: none; }
.hex-link::before {
    content: "";
    position: absolute;
    inset: 0;
    background-color: var(--focus);
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
    z-index: -1;
    opacity: 0;
    transform: scale(1);
    transition: opacity 0.4s ease, transform 0.4s var(--hx-ease);
}
.hex-link:focus-visible::before {
    opacity: 1;
    transform: scale(1.06);
}

.hex-link:hover,
.hex-link:focus-visible {
    z-index: 10;
    transform: translateY(-6px);
    filter:
        drop-shadow(0 0 8px var(--hx-bloom-near-hot))
        drop-shadow(0 0 30px var(--hx-bloom-far-hot))
        drop-shadow(0 20px 25px rgba(0, 0, 0, 0.7));
}
.hex-link:hover .hex-shape::after,
.hex-link:focus-visible .hex-shape::after { opacity: 0; }

/* --- glyph, lamp, wordmark ---------------------------------------------- */

.hex-glyph {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 34%;
    height: auto;
    z-index: 2;
    transform: translate(-50%, -50%);
    transition: opacity 0.35s ease, transform 0.4s var(--hx-ease);
}
.hex-committed .hex-glyph { opacity: 0.55; }
.hex-link:hover .hex-glyph,
.hex-link:focus-visible .hex-glyph {
    opacity: 0;
    transform: translate(-50%, -70%);
}

/* Tier lamp. Same three colours as the radial-list dots and the /platforms
   pills: only what runs gets the live colour. */
.hex-lamp {
    position: absolute;
    top: 15%;
    left: 50%;
    width: 6px;
    height: 6px;
    z-index: 3;
    transform: translateX(-50%);
    background: var(--conf-off);
}
.hex-running .hex-lamp { background: var(--live); }
.hex-building .hex-lamp { background: var(--signal); }
.hex-committed .hex-lamp { background: var(--ink-dim); }

.hex-hub {
    filter:
        drop-shadow(0 0 5px var(--hx-bloom-near))
        drop-shadow(0 0 20px var(--hx-bloom-far))
        drop-shadow(0 4px 8px rgba(0, 0, 0, 0.4));
}
.hex-hub .hex-shape {
    background-color: var(--hx-face);
    display: flex;
    align-items: center;
    justify-content: center;
}
.hex-wordmark {
    position: relative;
    z-index: 2;
    width: 72%;
    height: auto;
}

/* --- caption ------------------------------------------------------------ */
/* Sits in the band where the hexagon is full width. Padding is derived from
   the cell HEIGHT because a percentage padding would resolve against width. */

.hex-caption {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    align-items: center;
    gap: var(--sp-1);
    padding: 0 8% calc(var(--hx-h) * 0.27);
    text-align: center;
    background: linear-gradient(to top,
        rgba(0, 0, 0, 0.95) 0%,
        rgba(0, 0, 0, 0.4) 50%,
        rgba(0, 0, 0, 0) 100%);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.4s ease;
}

.hex-name {
    display: block;
    font-family: var(--font-display);
    font-size: clamp(0.75rem, calc(var(--hx-w) * 0.096), 0.875rem);
    font-weight: 600;
    line-height: 1.2;
    color: #ffffff;
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
    transform: translateY(15px);
    transition: transform 0.4s var(--hx-ease);
}

.hex-tier {
    display: block;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-mid);
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
    transform: translateY(15px);
    transition: transform 0.4s var(--hx-ease);
}
.hex-running .hex-tier { color: var(--live); }
.hex-building .hex-tier { color: var(--signal); }

.hex-link:hover .hex-caption,
.hex-link:focus-visible .hex-caption { opacity: 1; }
.hex-link:hover .hex-name,
.hex-link:focus-visible .hex-name,
.hex-link:hover .hex-tier,
.hex-link:focus-visible .hex-tier { transform: translateY(0); }
.hex-link:hover .hex-tier,
.hex-link:focus-visible .hex-tier { transition-delay: 0.05s; }

/* --- surfaces, touch, phone, print, motion ------------------------------ */

/* Paper keeps the cells dark: they carry light glyph
   artwork, the same reason the hero video and domain bands never go light.
   The live and amber tier text needs the SLATE values on that dark face,
   because the paper palette darkens them for a white ground. */
html[data-surface="paper"] .hex-running .hex-tier { color: #3fb950; }
html[data-surface="paper"] .hex-building .hex-tier { color: #d29922; }
html[data-surface="paper"] .hex-tier { color: #9fb0c2; }

/* THE NAME LIST WAS SHOWING ON DESKTOP. PHASE 35 hides .fig-alt at (0,1,0)
   and then .radial-list sets display:grid at the SAME specificity further
   down the file, so source order won and the "phone only" list rendered at
   every width under the radial from 2026-09-14. Scoped here at (0,2,0) so
   the hide actually holds, and shown as grid where it is wanted. */
.honeycomb > .fig-alt { display: none; }

/* No hover on a touchscreen, so the names would never appear. The cells
   still link; the list underneath names them. */
@media (hover: none) {
    .honeycomb > .fig-alt { display: grid; }
}

@media (max-width: 640px) {
    .honeycomb { --hx-gap: 6px; }
    .hex-caption { display: none; }
    .honeycomb > .fig-alt { display: grid; }
    .hex-link:hover .hex-glyph,
    .hex-link:focus-visible .hex-glyph {
        opacity: 1;
        transform: translate(-50%, -50%);
    }
}

@media (prefers-reduced-motion: reduce) {
    .hex-link,
    .hex-link::before,
    .hex-shape::after,
    .hex-glyph,
    .hex-caption,
    .hex-name,
    .hex-tier { transition: none; }
    .hex-link:hover,
    .hex-link:focus-visible { transform: none; }
}

@media print {
    .hex-grid { display: none; }
    .honeycomb > .fig-alt { display: grid; }
}

/* ------------------------------------------------------------------
   DIANE WEB CONTENT (2026-10-04). Sections built from Diane Brown's deck.
   Her logos are silver on transparent, made for black slides, so they
   sit directly on --ground. Built only from existing tokens.
   ------------------------------------------------------------------ */
.diane .wrap { padding-top: 2.5rem; padding-bottom: 2.5rem; }
.diane-logo { display: block; width: 100%; max-width: 420px; height: auto; margin: 0 0 1.5rem; }
.diane-logo-center { margin-left: auto; margin-right: auto; }
.diane-wordmark { display: block; width: 100%; max-width: 520px; height: auto; margin: 0 0 1rem; }
.diane-lede { font-size: 1.15rem; line-height: 1.55; color: var(--ink); max-width: var(--measure); }
.diane-body { color: var(--ink-mid); line-height: 1.65; max-width: var(--measure); }
.diane-tagline { font-size: 1.35rem; font-weight: 600; color: var(--ink); margin: 1.5rem 0 0.75rem; }
.diane-center { text-align: center; margin-left: auto; margin-right: auto; }
.diane-science { display: flex; align-items: center; justify-content: center; gap: 0.75rem; flex-wrap: wrap; }
.diane-science img { width: 160px; height: auto; }
.diane-formula { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.75rem; margin: 2rem 0 1rem; }
.diane-term { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; width: 120px;
              text-align: center; font-size: var(--t-micro); color: var(--ink-mid); }
.diane-term img { width: 88px; height: 88px; object-fit: contain; }
.diane-result { width: 230px; }
.diane-result img { width: 160px; height: 72px; }
.diane-op { font-size: 2rem; color: var(--ink-dim); }
.diane-quote { margin: 0 0 1.5rem; padding: 0; font-size: 1.15rem; line-height: 1.55; color: var(--ink); font-style: italic; }
.diane-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2.5rem; }
.diane-lines p { margin: 0.2rem 0; color: var(--ink-mid); }
.diane-lines h2 { margin-top: 0; }
.diane-center .diane-lines p { text-align: center; }
.diane-sectors { list-style: none; padding: 0; margin: 1rem 0 1.5rem; display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem;
                 font-size: var(--t-micro); letter-spacing: 0.08em; color: var(--ink); }
.diane-portfolio li { margin-bottom: 0.5rem; }


/* Fixes after visual check (2026-10-04): the computational-intelligence icon is
   dark navy line art that disappears on --ground; brightened, keeping its hue.
   The formula tagline is centered to match Diane's slide. */
.diane-term img[src*="formula_computational"] { filter: brightness(3.2) saturate(1.15); }
.diane-formula + .diane-tagline { text-align: center; }

/* ------------------------------------------------------------------
   CONTACT FORM (2026-10-04). Same instrument language as the rest of the
   site: square corners, mono labels, panel and rule tokens. Errors use
   --signal (amber), which the site already reserves for attention.
   ------------------------------------------------------------------ */
.contact-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 3rem; align-items: start; }
@media (max-width: 860px) { .contact-grid { grid-template-columns: 1fr; gap: 2rem; } }
.contact-card { background: var(--panel); border: 1px solid var(--rule); padding: 1.75rem; }
.form-title { margin: 0 0 1.25rem; }
.field { margin-bottom: 1.1rem; }
.field label { display: block; margin-bottom: 0.35rem; font-family: var(--font-mono); font-size: var(--t-micro);
               text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-mid); }
.field .req { color: var(--signal); }
.field .opt { text-transform: none; letter-spacing: 0; color: var(--ink-dim); }
.field input[type="text"], .field input[type="email"], .field input[type="tel"], .field select, .field textarea {
    width: 100%; box-sizing: border-box; padding: 0.65rem 0.75rem; border-radius: 0;
    background: var(--ground); color: var(--ink); border: 1px solid var(--rule-bright);
    font: inherit; font-size: 1rem; }
.field textarea { resize: vertical; min-height: 8rem; }
.field input:focus, .field select:focus, .field textarea:focus { outline: 2px solid var(--focus); outline-offset: 1px; border-color: var(--focus); }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 520px) { .field-row { grid-template-columns: 1fr; } }
.field-check { display: grid; grid-template-columns: auto 1fr; gap: 0.6rem; align-items: start; }
.field-check input { width: 1.1rem; height: 1.1rem; margin-top: 0.15rem; accent-color: var(--signal); }
.field-check label { margin: 0; font-family: var(--font-display); font-size: 0.95rem; text-transform: none; letter-spacing: 0; color: var(--ink); }
.field-check .field-error { grid-column: 1 / -1; }
.has-error input, .has-error select, .has-error textarea { border-color: var(--signal); }
.field-error { margin: 0.35rem 0 0; font-size: 0.9rem; color: var(--signal); }
.form-alert { margin: 0 0 1rem; padding: 0.6rem 0.8rem; border: 1px solid var(--signal); color: var(--signal); }
.form-privacy { font-size: 0.85rem; color: var(--ink-dim); margin: 0.25rem 0 1.25rem; }
.form-sent h2 { margin-top: 0; }
.form-sent .btn-ghost { margin-left: 0; }
.hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }

/* ------------------------------------------------------------------
   CONTROL LOOP, RING VERSION (2026-10-04). See templates/_loop_ring.html.
   One amber pulse per 8 s lap; stage k lights at 2k s. Circumference of
   r=170 is 1068.14; the 90-unit dash starts centred on the top node.
   ------------------------------------------------------------------ */
.ring-card { padding: 2.25rem 2.25rem 1.75rem; }
.ring-grid { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.35fr); gap: 2.5rem; align-items: center; }
@media (max-width: 900px) { .ring-grid { grid-template-columns: 1fr; gap: 1.5rem; } }
.ring-text h2 { font-size: clamp(1.75rem, 3vw, 2.35rem); line-height: 1.15; margin: 0.9rem 0 1rem; }
.ring-plain { font-size: 1.15rem; line-height: 1.55; color: var(--ink-mid); max-width: 30ch; margin: 0; }
.ring-fig { margin: 0; }
.ring-svg { display: block; width: 100%; height: auto; overflow: visible; }
.ring-track { fill: none; stroke: var(--rule-bright); stroke-width: 1.5; }
.ring-pulse { fill: none; stroke: var(--signal); stroke-width: 3; stroke-linecap: round;
              stroke-dasharray: 90 978.14; stroke-dashoffset: 45;
              filter: drop-shadow(0 0 6px var(--signal)); animation: ring-run 8s linear infinite; }
@keyframes ring-run { from { stroke-dashoffset: 45; } to { stroke-dashoffset: -1023.14; } }
.ring-node { fill: var(--panel); stroke: var(--ink-dim); stroke-width: 1.5;
             animation: ring-node 8s linear infinite; animation-delay: calc(var(--k) * 2s); }
@keyframes ring-node { 0% { fill: var(--signal); stroke: var(--signal); } 18% { fill: var(--signal); stroke: var(--signal); }
                       32%, 100% { fill: var(--panel); stroke: var(--ink-dim); } }
.ring-node-halo { fill: var(--signal); opacity: 0; transform-box: fill-box; transform-origin: center;
                  animation: ring-halo 8s ease-out infinite; animation-delay: calc(var(--k) * 2s); }
@keyframes ring-halo { 0% { opacity: 0.4; transform: scale(0.45); } 28% { opacity: 0; transform: scale(1.7); } 100% { opacity: 0; } }
.ring-idx { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.14em; fill: var(--signal); }
.ring-word { font-family: var(--font-display); font-size: 21px; font-weight: 600; fill: var(--ink);
             animation: ring-word 8s linear infinite; animation-delay: calc(var(--k) * 2s); }
@keyframes ring-word { 0%, 18% { fill: var(--signal); } 32%, 100% { fill: var(--ink); } }
.ring-cap { font-family: var(--font-mono); font-size: 12.5px; fill: var(--ink-mid); }
.ring-note { font-family: var(--font-mono); font-size: 9.2px; letter-spacing: 0.03em; fill: var(--ink-dim); }
.ring-center { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.24em; text-transform: uppercase; fill: var(--ink-dim); }
.ring-text .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@media (prefers-reduced-motion: reduce) {
  .ring-pulse { display: none; }
  .ring-node, .ring-node-halo, .ring-word { animation: none; }
  .ring-node { fill: var(--signal); stroke: var(--signal); }
}

/* Science-of-UAAi animated background (2026-10-04). Decorative canvas behind
   the formula; edges fade so the text stays clear. Script: js/science_tunnel.js */
.diane-science-sec { position: relative; overflow: hidden; }
.science-bg { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none;
              -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 18%, #000 82%, transparent 100%);
                      mask-image: linear-gradient(to bottom, transparent 0%, #000 18%, #000 82%, transparent 100%); }
.diane-science-sec .wrap { position: relative; z-index: 1; }
.diane-science-sec .diane-term, .diane-science-sec .diane-body { text-shadow: 0 0 12px var(--ground), 0 0 4px var(--ground); }


/* ------------------------------------------------------------------
   DIANE SECTIONS RESTYLED (2026-10-04): mission, quote, team/support.
   Wording is Diane's; only the presentation changed. Site tokens only.
   ------------------------------------------------------------------ */
.dx-sec .wrap { padding-top: 4.5rem; padding-bottom: 4.5rem; }
.dx-split { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 3.5rem; align-items: start; }
.dx-split-even { grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr); align-items: center; margin-top: 3.5rem; }
@media (max-width: 900px) { .dx-split, .dx-split-even { grid-template-columns: 1fr; gap: 2rem; } }
.dx-label { display: block; margin-bottom: 1rem; font-family: var(--font-mono); font-size: var(--t-micro); font-weight: 400;
            letter-spacing: 0.22em; text-transform: uppercase; color: var(--signal); }
.dx-statement { margin: 0; font-size: clamp(1.55rem, 2.7vw, 2.35rem); line-height: 1.22; font-weight: 600; letter-spacing: -0.015em; color: var(--ink); }
.dx-panel { padding: 1.75rem 1.9rem; border: 1px solid var(--rule); background: linear-gradient(170deg, var(--panel-raised), var(--panel) 70%); }
.dx-body { margin: 0; font-size: 1.06rem; line-height: 1.78; color: var(--ink-mid); }
.dx-kicker { margin: 1.1rem 0 0; font-size: 1.08rem; font-weight: 600; color: var(--ink); }
.dx-banner { margin: 4.5rem 0 1.5rem; max-width: 22ch; font-size: clamp(1.9rem, 4vw, 3.3rem); line-height: 1.08; letter-spacing: -0.025em;
             background: linear-gradient(95deg, var(--ink) 15%, var(--instrument) 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }
.dx-sell { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 0.65rem; margin: 0; font-size: 1.08rem; color: var(--ink-mid); }
.dx-chip { padding: 0.5rem 0.85rem; border: 1px solid var(--rule-bright); background: var(--panel); color: var(--ink);
           font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase;
           transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease; }
.dx-chip:hover { border-color: var(--signal); color: var(--signal); background: var(--panel-raised); }

.dx-quote { position: relative; max-width: 54rem; margin: 0 auto; padding-top: 3rem; text-align: center; }
.dx-quote::before { content: "\201C"; position: absolute; top: -2.6rem; left: 50%; transform: translateX(-50%);
                    font-family: Georgia, "Times New Roman", serif; font-size: 9rem; line-height: 1; color: var(--signal); opacity: 0.28; }
.dx-quote blockquote { margin: 0; }
.dx-quote p { margin: 0 0 1.2rem; font-size: clamp(1.45rem, 2.8vw, 2.25rem); line-height: 1.32; font-weight: 500; letter-spacing: -0.015em; color: var(--ink); }
.dx-quote .dx-quote-2 { font-size: clamp(1.1rem, 1.9vw, 1.45rem); font-weight: 400; color: var(--ink-mid); }
.dx-best { font-weight: 800; background: linear-gradient(100deg, #f5d27a 0%, var(--signal) 70%); -webkit-background-clip: text; background-clip: text; color: transparent; }
.dx-keys { margin: 0; font-size: clamp(1.45rem, 2.5vw, 2.05rem); line-height: 1.3; font-weight: 600; letter-spacing: -0.015em; color: var(--ink-dim); }
.dx-keys span { color: var(--ink); }

.dx-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.5rem; }
.dx-card { position: relative; overflow: hidden; padding: 2.1rem 2.1rem 2.3rem; border: 1px solid var(--rule);
           background: linear-gradient(165deg, var(--panel-raised) 0%, var(--panel) 55%);
           transition: transform 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease; }
.dx-card::after { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity 0.3s ease;
                  background: radial-gradient(520px circle at var(--mx, 50%) var(--my, 0%), rgba(88, 166, 255, 0.11), transparent 45%); }
.dx-card:hover { transform: translateY(-4px); border-color: var(--rule-bright); box-shadow: 0 18px 40px -24px rgba(0, 0, 0, 0.9); }
.dx-card:hover::after { opacity: 1; }
.dx-card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.4rem; }
.dx-icon { display: grid; place-items: center; width: 46px; height: 46px; border: 1px solid var(--rule-bright); color: var(--instrument); background: var(--ground); }
.dx-num { font-family: var(--font-mono); font-size: var(--t-micro); letter-spacing: 0.2em; color: var(--signal); }
.dx-card h2 { margin: 0 0 1.1rem; font-size: clamp(1.6rem, 2.4vw, 2rem); letter-spacing: -0.015em; }
.dx-lines p { margin: 0.32rem 0; font-size: 1.02rem; line-height: 1.55; color: var(--ink-mid); }
.dx-hl { font-weight: 600; color: var(--ink); }

.dx-anim .dx-reveal { opacity: 0; transform: translateY(22px); transition: opacity 0.8s ease, transform 0.8s cubic-bezier(.2,.7,.2,1); }
.dx-anim .dx-reveal.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .dx-card, .dx-chip { transition: none; } .dx-card:hover { transform: none; } }
/* refinements after first render: commas kept in the text but hidden in the tags;
   balanced wrapping so big lines do not leave a word alone */
.dx-comma { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.dx-statement, .dx-banner, .dx-quote p, .dx-keys { text-wrap: balance; }


/* ------------------------------------------------------------------
   FOOTER v2 (2026-10-04). Brand, navigation, a call to action and a
   bottom legal bar. Legal wording unchanged (see base.html comment).
   ------------------------------------------------------------------ */
.site-foot-v2 { position: relative; padding: 0; color: var(--ink-dim); border-top: 1px solid var(--rule);
                background: radial-gradient(ellipse 60% 120% at 50% 0%, rgba(88, 166, 255, 0.07), transparent 70%), var(--panel); }
.site-foot-v2::before { content: ""; position: absolute; top: -1px; left: 50%; transform: translateX(-50%); width: min(560px, 80%); height: 1px;
                        background: linear-gradient(90deg, transparent, var(--instrument), transparent); opacity: 0.55; }
.site-foot-v2 .wrap { display: block; }
.foot-top { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 0.6fr) minmax(0, 1fr); gap: 2.5rem; padding: 3.5rem 0 2.75rem; }
@media (max-width: 860px) { .foot-top { grid-template-columns: 1fr; gap: 2rem; padding: 2.75rem 0 2rem; } }
.foot-logo img { display: block; width: 200px; max-width: 100%; height: auto; }
.foot-id { margin: 1rem 0 0; font-family: var(--font-mono); font-size: var(--t-small); color: var(--ink-mid); }
.foot-id .tm { font-size: 0.7em; vertical-align: super; margin-left: 0.1em; color: var(--ink-dim); }
.foot-head { display: block; margin-bottom: 1rem; font-family: var(--font-mono); font-size: var(--t-micro); letter-spacing: 0.2em;
             text-transform: uppercase; color: var(--signal); }
.foot-nav { display: flex; flex-direction: column; gap: 0.65rem; }
.foot-nav a { color: var(--ink-mid); font-size: 0.98rem; width: fit-content; transition: color 0.2s ease; }
.foot-nav a:hover { color: var(--ink); text-decoration: none; }
.foot-cta p { margin: 0 0 1.25rem; font-size: 1rem; line-height: 1.6; color: var(--ink-mid); max-width: 34ch; }
.foot-bottom { display: flex; justify-content: space-between; align-items: center; gap: 1.5rem; flex-wrap: wrap;
               padding: 1.4rem 0 1.8rem; border-top: 1px solid var(--rule); }
.foot-legal { margin: 0; max-width: 80ch; font-family: var(--font-mono); font-size: var(--t-micro); line-height: 1.7; color: var(--ink-dim); }
.foot-top-link { font-family: var(--font-mono); font-size: var(--t-micro); letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-mid); }
.foot-top-link:hover { color: var(--signal); text-decoration: none; }
/* footer refinements after first render: body copy and links in the display face;
   mono kept for labels and the legal bar */
.site-foot-v2 { font-family: var(--font-display); }
.foot-nav a, .foot-cta p { font-family: var(--font-display); }
/* prose variant for rewritten card copy */
.dx-prose p { margin: 0 0 1rem; font-size: 1.06rem; line-height: 1.75; color: var(--ink-mid); }
.dx-prose p:last-child { margin-bottom: 0; }


/* ------------------------------------------------------------------
   MOBILE PASS (2026-10-04), from a Playwright audit at 390 x 844.
   No sideways scroll was found; these fix the formula layout, tap
   targets below ~44 px, and over-generous section padding on phones.
   ------------------------------------------------------------------ */
/* Tap targets at every width (footer links, back to top). */
.foot-nav { gap: 0; }
.foot-nav a { display: block; padding: 0.6rem 0; }
.foot-top-link { display: inline-block; padding: 0.85rem 0; }
@media (max-width: 640px) {
  /* Formula: a 2 x 2 grid of the four intelligences, then "=" and the result,
     instead of a ragged wrap with dangling brackets and plus signs. */
  .diane-formula { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem 0.75rem; justify-items: center; margin: 1.5rem 0 1rem; }
  .diane-formula .diane-op { display: none; }
  .diane-formula .diane-op:nth-last-child(2) { display: block; grid-column: 1 / -1; line-height: 1; }
  .diane-formula .diane-term { width: auto; max-width: 150px; }
  .diane-formula .diane-result { grid-column: 1 / -1; }
  .diane-science { gap: 0.5rem; }
  .diane-science img { width: 130px; }
  /* The converging tunnel is built for the one-row formula; off on phones. */
  .science-bg { display: none; }
  /* Tighter rhythm between sections. */
  .dx-sec .wrap, .diane .wrap { padding-top: 2.75rem; padding-bottom: 2.75rem; }
  .dx-banner { margin-top: 2.75rem; }
  .dx-quote { padding-top: 2.25rem; }
  .dx-quote::before { font-size: 6.5rem; top: -1.9rem; }
  .dx-card { padding: 1.6rem 1.4rem 1.8rem; }
  .dx-split-even { margin-top: 2.25rem; }
  .contact-card { padding: 1.35rem; }
  /* Header logo link gets a full-size tap area. */
  a.brand { display: inline-flex; align-items: center; min-height: 44px; }
}

/* Platforms rows on phones (2026-10-04). The 640px rule near line 636 collapsed
   these rows, but the logo rules added later (four fixed columns at every width)
   override it, leaving about 80px for the description. Placed last so it wins:
   logo and name side by side, the description full width, then the status. */
@media (max-width: 700px) {
  .mimic-row, .domain-card-body > .mimic .mimic-row {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "mark name" "desc desc" "state state";
    gap: 0.55rem 0.9rem; align-items: center; }
  .mimic-row .mimic-idx { display: none; }
  .mimic-row .mimic-mark { grid-area: mark; }
  .mimic-row .mimic-name { grid-area: name; }
  .mimic-row .mimic-desc { grid-area: desc; grid-column: auto; }
  .mimic-row .mimic-state { grid-area: state; grid-column: auto; justify-self: start; margin-top: 0.1rem; }
}
/* Phone fix 2: the logo cell was taking most of the row, so names were clipped.
   Cap the logo cell; the name takes the rest. */
@media (max-width: 700px) {
  .mimic-row, .domain-card-body > .mimic .mimic-row { grid-template-columns: 4.6rem minmax(0, 1fr); }
  .mimic-row .mimic-mark { width: auto; min-width: 0; max-width: 4.6rem; justify-content: flex-start; }
  .mimic-row .mimic-mark img { max-width: 4.4rem; max-height: 1.9rem; width: auto; height: auto; }
  .mimic-row .mimic-name { min-width: 0; overflow-wrap: anywhere; }
}
/* Phone fix 3: explicit grid positions (the named areas were being reset by a
   'grid-column: auto' and the description fell into the narrow logo column). */
@media (max-width: 700px) {
  .mimic-row .mimic-mark  { grid-area: auto; grid-column: 1; grid-row: 1; }
  .mimic-row .mimic-name  { grid-area: auto; grid-column: 2; grid-row: 1; }
  .mimic-row .mimic-desc  { grid-area: auto; grid-column: 1 / -1; grid-row: 2; }
  .mimic-row .mimic-state { grid-area: auto; grid-column: 1 / -1; grid-row: 3; }
}

/* Intro growing-network background (2026-10-04). Right side of the section,
   behind the text, dissolving at its edges. Off below 900px, where the text
   takes the full width. Script: js/intro_network.js */
.diane-intro-sec { position: relative; overflow: hidden; }
.intro-net { position: absolute; top: 0; right: 0; width: 42%; height: 100%; z-index: 0; pointer-events: none;
             -webkit-mask-image: radial-gradient(ellipse 62% 60% at 56% 50%, #000 45%, transparent 100%);
                     mask-image: radial-gradient(ellipse 62% 60% at 56% 50%, #000 45%, transparent 100%); }
.diane-intro-sec .wrap { position: relative; z-index: 1; }
@media (max-width: 900px) { .intro-net { display: none; } }

/* Intro text and network each get their own space on wide screens (2026-10-04):
   text within the left 55%, network in the right 40%. */
@media (min-width: 901px) {
  .diane-intro-sec .diane-lede, .diane-intro-sec .diane-body { max-width: min(var(--measure), 55%); }
  .diane-intro-sec .intro-net { width: 40%; }
}

/* PROGRAMS list and static honeycomb cells (2026-10-04, compliance pass).
   Names and a status label only. Colors reuse the tier tokens: live for the
   featured program, signal for under construction, dim for planned. A static
   cell is a span, not a link, while the Platforms page is held. */
.hex-static { cursor: default; }
/* Hero paragraph with a list in the middle (2026-10-05, team wording). The
   lead paragraph drops its bottom margin so the list sits under it. */
.hero .lede-lead { margin-bottom: 0.7rem; }
.hero-points { list-style: none; margin: 0 0 1rem; padding: 0; max-width: 50ch;
               font-size: var(--t-body); color: var(--ink-mid); }
.hero-points li { position: relative; padding-left: 1.15rem; margin: 0.3rem 0; }
.hero-points li::before { content: ""; position: absolute; left: 0; top: 0.62em;
                          width: 6px; height: 6px; background: var(--signal); }
/* "UAAI offers ..." lead-in takes its own line above the service chips. */
.dx-sell-lead { flex-basis: 100%; }
/* Blank cell for a held program (2026-10-05). Same face as the hub, dimmed,
   with nothing on it, so the 4-3-4 honeycomb keeps its shape. */
.hex-blank .hex-shape { background-color: var(--hx-face); opacity: 0.4; }
.programs-sec .diane-tagline { margin-top: 0; }
.program-list { list-style: none; margin: 1.5rem 0 0; padding: 0; display: grid;
                grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.6rem; }
.program { display: flex; flex-direction: column; gap: 0.35rem;
           padding: 0.75rem 0.9rem; border: 1px solid var(--rule); background: var(--panel); }
.program-name { color: var(--ink); font-weight: 600; }
.program-state { font-family: var(--font-mono); font-size: var(--t-micro); letter-spacing: 0.1em;
                 text-transform: uppercase; color: var(--ink-dim); white-space: nowrap; }
.program-running { border-color: var(--live); }
.program-running .program-state { color: var(--live); }
.program-building .program-state { color: var(--signal); }
@media (max-width: 640px) { .program-list { grid-template-columns: 1fr; } }

/* WHAT UAAI OFFERS section, added 2026-10-13 (UAAI word mark proof of use, CLR-0134).
   Reuses the .dx-card styles from Our Team / Support; only the call-to-action line is new. */
.dx-offer .dx-banner { margin-top: 0; max-width: none; }
.dx-offer-cta { margin: 2rem 0 0; }
.dx-offer-cta a { color: inherit; text-decoration: underline; text-underline-offset: 0.2em; }
