/* pipe — static site styles.
 *
 * The palette is sampled from the connector render (brand/, rendered out of
 * pipe_logo.blend): platinum highlight, platinum mid, platinum deep, and the
 * ruby of the glass core. Everything else stays the terminal that pipe
 * actually is — black, monospace, no decoration that a plain-text reader
 * would lose meaning without.
 *
 * The connector is hvac cypherpunk now: a platinum body with brass only on
 * the knurling — the part you grip. The palette follows that split exactly.
 * Platinum carries the type and the structure; brass is left for the one
 * thing on the page you actually take hold of, the flux drop. Spending it
 * anywhere else is what made the old site read as gold rather than as metal.
 */

:root {
    --bg: #000;
    --fg: #e8e8e8;
    --dim: #8a8a8a;
    --hi: #fff;
    --line: #2a2a2a;

    /* The body: type, links, rules. Sampled off the barrel, the worn
       midtone, and the shadowed knurl roots. */
    --platinum: #d6d6da;
    --platinum-mid: #9a9aa2;
    --platinum-deep: #4e4e56;

    /* The knurling. An accent, not a ramp — one element, one use. The press
       flash and focus pool keep their own hardcoded copper; those were never
       part of this ramp and the warmth still belongs there. */
    --brass: #ccac62;

    /* The glass core. Used only for live/verified signal. */
    --core: #d9a8ab;

    /* ── elevation ────────────────────────────────────────────────────
       Two systems, no more: static containers rest on one --surface halo (a
       faint directionless whisper of light); interactive surfaces carry the
       pipe glow instead — nothing at rest, a golden flame on hover/press.
       --seam is the edge you feel rather than see; --hairline is structural. */
    --seam: #0a0a0a;
    --hairline: #151515;
    --surface:
        0 0 1px 0 rgba(255, 252, 245, 0.1),
        0 0 5px 0 rgba(255, 255, 255, 0.06);
    /* An input is a dark slot that warms with a breath of copper when it takes
       focus. It used to also carry a lit bottom edge, which read as an
       underline; the pool alone is the signal now, and it is a stronger one
       than a 1px line ever was. */
    --well: inset 0 0 6px -2px rgba(196, 120, 70, 0);
    --well-focus: inset 0 0 9px -2px rgba(196, 120, 70, 0.45);

    /* ── motion ───────────────────────────────────────────────────────
       The site's movements, named once and reused everywhere. Two curves:
       settle (fast out of the gate, long soft tail) and glide (symmetric).
       Five moves: snap for color and small state, raise for elevation,
       press for the instant down, swap for the rotor word, arrive for
       content entering a page. */
    --ease-settle: cubic-bezier(0.22, 0.61, 0.36, 1);
    --ease-glide: cubic-bezier(0.4, 0, 0.2, 1);
    --motion-snap: 0.12s var(--ease-glide);
    --motion-raise: 0.2s var(--ease-settle);
    --motion-press: 0.05s var(--ease-glide);
    --motion-swap: 0.24s var(--ease-settle);
    --motion-arrive: 0.55s var(--ease-settle);

    /* ── surface ────────────────────────────────────────────────────────
       Surfaces are transparent at rest and fill to opaque black on hover.
       The --ar shape system is retired — no more shade-by-aspect-ratio. */

    /* Two more moves, for the one fold: fold is the box opening/closing,
       veil is its contents leaving/arriving. Contents move on their own
       clock so they never smear against the moving edge. */
    --motion-fold: 0.45s var(--ease-settle);
    --motion-veil: 0.16s var(--ease-glide);

    /* The page's vertical rhythm: one beat between sections. */
    --beat-section: clamp(56px, 10vh, 104px);

    /* ── app geometry ─────────────────────────────────────────────────
       The app shell (body.app) is the only place these apply. There is
       exactly ONE app breakpoint, 760px — media queries can't read custom
       properties, so it lives as a literal; grep "760px". */
    --shell-max: 1600px;
    --gap: 20px;
    --pane-min: 280px;

    /* Clearance a glowing surface needs around itself. The pipe glow is a
       box-shadow reaching ~12px past the border and blooming further on
       hover, so a glowable component sitting flush against its container has
       its outer bands cut off. Components spend this themselves so pages
       don't have to know the halo exists. */
    --glow-room: 14px;

    /* Lets height animate to auto — the folds glide open. */
    interpolate-size: allow-keywords;

    --mono:
        ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas,
        "DejaVu Sans Mono", monospace;
}

/* The pipe glow's nine band alphas plus two motion channels, registered so
   they interpolate/animate (bare custom properties would snap). */
@property --glow-white { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --glow-cream { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --glow-yellow { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --glow-gold { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --glow-amber { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --glow-copper { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --glow-flare { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --glow-cyan { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --glow-blue { syntax: "<number>"; inherits: false; initial-value: 0; }
/* Perpendicular flicker — an intensity multiplier the bands pulse by (rests
   at 1). Reads as the flame breathing toward and away from the surface. */
@property --glow-flicker { syntax: "<number>"; inherits: false; initial-value: 1; }
/* Parallel shimmer — a hair of in-plane offset the whole halo drifts along,
   so light appears to slide across the surface. */
@property --glow-sx { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --glow-sy { syntax: "<length>"; inherits: false; initial-value: 0px; }

/* Membership in the glow bucket, as a value the stylesheet itself declares —
   chrome.ts reads it to find a pressed surface, so the selector list lives in
   exactly one place (CSS) instead of being hand-copied into TS. */
@property --glowable { syntax: "<integer>"; inherits: false; initial-value: 0; }

/* Perpendicular: a deep, erratic intensity flicker — a live, guttering flame. */
@keyframes glow-flicker {
    0% { --glow-flicker: 1; }
    13% { --glow-flicker: 0.91; }
    27% { --glow-flicker: 1.05; }
    41% { --glow-flicker: 0.88; }
    56% { --glow-flicker: 1.07; }
    69% { --glow-flicker: 0.94; }
    84% { --glow-flicker: 1.02; }
    100% { --glow-flicker: 1; }
}

/* Parallel: the halo traces an in-plane orbit — light sliding around. */
@keyframes glow-shimmer {
    0% { --glow-sx: 0.6px; --glow-sy: 0.2px; }
    21% { --glow-sx: -0.2px; --glow-sy: 0.6px; }
    39% { --glow-sx: -0.6px; --glow-sy: -0.15px; }
    56% { --glow-sx: 0.15px; --glow-sy: -0.55px; }
    74% { --glow-sx: 0.5px; --glow-sy: 0.4px; }
    100% { --glow-sx: 0.6px; --glow-sy: 0.2px; }
}

* {
    box-sizing: border-box;
}

html,
body {
    margin: 0;
    /* A sparse dot grid, like a bound notebook's page — grey on black,
       felt more than seen. One dot per 32px cell, fixed to the viewport
       so it reads as the room's paper rather than scrolling with the
       content sitting on top of it. */
    background-color: var(--bg);
    background-image: radial-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px);
    background-size: 32px 32px;
    background-attachment: fixed;
    color: var(--fg);
}

body {
    font-family: var(--mono);
    font-size: 15px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    padding: 0 20px;
}

/* Links carry no rule under them. Platinum is already the site's interactive
   colour, so a lit word inside dim prose reads as a link on its own, and
   the underline was never worth the seven `border-bottom: none` overrides it
   accumulated (nav, footer, .btn, .post-text, .pane-back, and two more that
   only escaped by accident, via the surface family's border shorthand). */
a {
    color: var(--platinum);
    text-decoration: none;
    transition: color var(--motion-snap);
}

a:hover {
    color: var(--hi);
}

/* The `hidden` attribute must always win. The auth page keeps signed-in-only
   sections (me, deactivated, cli-code) in the DOM and toggles them via
   `hidden`; without this a stray display rule could leak that content to a
   signed-out (preauth) visitor. JS shows a section by clearing the attribute. */
[hidden] {
    display: none !important;
}

/* ── component index ───────────────────────────────────────────────────
   The site is built from one small, reused set of components. There are two
   surface systems and no others: INTERACTIVE surfaces carry the pipe glow
   (nothing at rest, a golden flame on hover/press); STATIC surfaces carry the
   --surface halo. Every component is one of:

     .wrap · nav · footer    page shell + chrome (chrome.ts injects nav/footer)
     .btn                    the one control — size/emphasis via modifier
     .well                   the one input slot
     .surface                the one static container (card / stat / pane / …)
     details.disclosure      the one fold
     table.q · .kv           key-value + data tables
     .convo-row · .thread    list links, size variants
     .lede · .sub · .tag · .rule    the type + divider set

   Interactive components are members of the glow bucket (search "the pipe
   glow"); static containers use --surface. Older one-off names are being
   RENAMED in markup rather than aliased: a component carries the primitive
   plus a modifier (`class="surface stat"`), the primitive owns the shared
   behaviour, and the modifier owns only its own padding and inner type. An
   alias list is a second source of truth that has to be kept in lockstep by
   hand — including into chrome.ts's SURFACE_SELECTOR — and it drifted. */

/* One layout for every page: a two-pane grid. Left rail is 40%, right
   content is 60%. Nav spans both across the top, footer spans both across
   the bottom. Both panes scroll independently. On phone they stack.

   Written as 2fr/3fr rather than a fraction against 1fr: `0.3fr 1fr` reads
   like 30/70 but resolves to 23/77, because the shares are taken over their
   own sum. 2:3 is exactly 40:60. */
.wrap {
    display: grid;
    grid-template-columns: minmax(220px, 2fr) 3fr;
    grid-template-rows: auto 1fr auto;
    height: 100vh;
    height: 100dvh;
    max-width: var(--shell-max);
    margin: 0 auto;
    width: 100%;
    gap: var(--gap);
    padding: clamp(14px, 3vh, 28px) var(--gap) max(10px, env(safe-area-inset-bottom));
}

.wrap > nav { grid-column: 1 / -1; }
.wrap > footer { grid-column: 1 / -1; }

.pane-left, .pane-right {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    display: flex;
    flex-direction: column;
    /* Clearance for the halo. These are scrollers, and a scroller clips
       box-shadow at its edge — so a glowable child sitting flush against the
       pane (a fold summary, a button, an install box) had the outer bands of
       its glow sheared off, which reads as a hard-edged rim rather than a
       flame. Every pane spends it, so no page has to. */
    padding: var(--glow-room);
}

.pane-left  { grid-column: 1; }
.pane-right { grid-column: 2; }

/* A page that is one wide thing rather than a rail plus content. Spans both
   columns like the nav and footer do. Pricing uses it: three tiers read as
   one row across the full width, and nothing about them belongs in a rail.
   It also keeps the tiers out of a pane's `overflow-y: auto`, which would
   clip their halos at the pane edge. */
.pane-full {
    grid-column: 1 / -1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Same halo clearance as the two-pane scrollers. */
    padding: var(--glow-room);
    /* Centred on the pane's middle line, the same way the about rail centres
       its mark. `safe` is what makes it usable in a scroller: it centres
       while there is room and falls back to flush-start once the content
       outgrows the box, instead of overflowing equally in both directions
       and putting the top out of reach. */
    display: flex;
    flex-direction: column;
    justify-content: safe center;
}

/* A page with no left-rail content just keeps the pane empty. */

/* One panel, on the centre line — the same shape as the landing, for a page
   that has content in only one of its two panes. The auth page wears it
   while signed out: a sign-in card in the 40% rail with an empty 60% beside
   it reads as a layout that failed to load.

   Done as a modifier on the wrap rather than by moving the section into
   `.pane-full`, because the state is not fixed at build time — the same
   markup is one panel signed out and two panes signed in, and a class on the
   grid is the smallest thing that can change between them. */
.wrap.solo > .pane-right { display: none; }

.wrap.solo > .pane-left {
    grid-column: 1 / -1;
    /* `safe` for the same reason .pane-full uses it: centre while there is
       room, fall back to flush-start once the content outgrows the pane,
       instead of putting the top out of reach on a short viewport. */
    justify-content: safe center;
    align-items: center;
    text-align: center;
}

/* ── nav ─────────────────────────────────────────────────────────────── */

/* No separators and no rule below — spacing groups, the brand anchors the
   left edge, the account entry holds the right. The current page is marked
   by weight and color alone — no dot. */
.nav {
    color: var(--dim);
    font-size: 13px;
    margin: 0 0 14px;
    letter-spacing: 0.02em;
    display: flex;
    gap: 18px;
    flex-wrap: wrap;
    align-items: baseline;
}

.nav b {
    color: var(--fg);
    font-weight: 600;
}

.nav a {
    color: var(--dim);
    transition: color var(--motion-snap);
}

.nav a:hover {
    color: var(--platinum);
}

.nav .brand {
    color: var(--platinum);
    font-weight: 600;
}

.nav .acct {
    margin-left: auto;
}

/* Top-corner account cluster (signed in): the flux droplet anchors the right
   edge, the nick sits next to it, sign out holds the corner. */
.nav .flux {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    color: var(--fg);
    font-weight: 600;
}
.nav .flux .drop {
    width: 8px;
    height: 8px;
    background: var(--brass);
    border-radius: 0 50% 50% 50%;
    transform: rotate(45deg);
}
.nav .signout {
    color: var(--dim);
}
.nav .signout:hover {
    color: var(--platinum);
}

/* Shelved surface: shown so people know it's planned, dimmed so it reads
   as not-yet-live, and static — nothing to expand. */
/* ── hero ────────────────────────────────────────────────────────────── */

/* The connector is the brand mark. It replaced the ASCII wordmark, so it
   carries the identity alone and is allowed the space to do it. The <video>
   is a pre-rendered turntable with an alpha channel; the <img> underneath is
   both the poster and the no-video fallback, so nothing is lost when motion
   is unavailable or unwanted. */
.hero {
    margin: 40px 0 28px;
    position: relative;
}

/* One image, always. The video/poster pair this used to swap between is gone
   (#198): a frame sequence has no separate still to fall back to, because the
   resting frame IS the markup's src. Motion stays opt-out via the OS setting,
   but the script honours it now rather than a display rule — with no JS, or
   under reduced motion, the seated frame is simply what is on the page. */
.hero img {
    display: block;
    width: 100%;
    max-width: 560px;
    height: auto;
    margin: 0 auto;
}

h1 {
    font-size: 26px;
    line-height: 1.3;
    font-weight: 600;
    color: var(--hi);
    margin: 0 0 12px;
    letter-spacing: -0.01em;
}

.lede {
    color: var(--fg);
    font-size: 16px;
    line-height: 1.8;
    max-width: 40em;
    margin: 0 0 clamp(28px, 6vh, 48px);
}

.lede b {
    color: var(--platinum);
    font-weight: 600;
}

.sub {
    color: var(--dim);
    margin: 0 0 22px;
    font-size: 14px;
    line-height: 1.75;
    max-width: 46em;
}

.sub b {
    color: var(--fg);
    font-weight: 600;
}

.sub code,
.tag code,
p code,
li code {
    color: var(--platinum);
}

/* ── install ─────────────────────────────────────────────────────────── */

.install {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    max-width: 560px;
    border-radius: 4px;
    padding: 20px 22px;
    margin: 28px 0 16px;
    cursor: pointer;
    /* The copper pool layers OVER the surface shade — image, not shorthand,
       so it doesn't wipe the fill the surface family sets. */
    background-image: radial-gradient(
        ellipse at center,
        rgba(192, 144, 96, 0.05),
        transparent 72%
    );
}

.install code {
    color: var(--hi);
    font-size: 14px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.install code .p {
    color: var(--platinum-mid);
}

.copy {
    color: var(--dim);
    font-size: 12px;
    white-space: nowrap;
    padding: 4px 8px;
    user-select: none;
    transition: color var(--motion-snap);
}

.install:hover .copy {
    color: var(--platinum);
}

.tag {
    color: var(--dim);
    font-size: 13px;
    margin: 0 0 8px;
}

/* ── section rules ───────────────────────────────────────────────────── */

.rule {
    color: var(--hairline);
    margin: clamp(28px, 6vh, 46px) 0 16px;
    font-size: 13px;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 12px;
}

.rule b {
    color: var(--platinum-mid);
    font-weight: normal;
    white-space: nowrap;
}

/* Not a drawn line — a fade of platinum light trailing off the label. */
.rule::after {
    content: "";
    flex: 1;
    height: 1px;
    background: linear-gradient(90deg, rgba(192, 144, 96, 0.28), transparent);
}

/* ── code blocks ─────────────────────────────────────────────────────── */

pre.script {
    margin: 0 0 10px;
    padding: 16px;
    border-radius: 4px;
    font-size: 13px;
    line-height: 1.7;
    color: var(--fg);
    overflow-x: auto;
}

pre.script .c {
    color: var(--platinum-deep);
}

pre.script .k {
    color: var(--platinum);
}

/* ── tables ──────────────────────────────────────────────────────────── */

table.q {
    border-collapse: collapse;
    width: 100%;
    font-size: 13px;
}

table.q td {
    padding: 11px 18px 11px 0;
    vertical-align: top;
    line-height: 1.55;
}

td.cmd {
    color: var(--platinum);
    white-space: nowrap;
}

td.desc {
    color: var(--dim);
}

/* The trust ladder is ordered weakest to strongest and the colour has to
   carry that order, so it runs dim → core rather than using hue to mean
   category. */
td.level {
    white-space: nowrap;
    color: var(--dim);
}

td.level.t4 {
    color: var(--core);
}
td.level.t3 {
    color: var(--platinum);
}
td.level.t2 {
    color: var(--platinum-mid);
}
td.level.t1 {
    color: var(--dim);
}

/* Below this width the two-column layout gets too cramped to read, so each
   row stacks: command, then its description. */
@media (max-width: 560px) {
    table.q,
    table.q tbody,
    table.q tr {
        display: block;
        width: 100%;
    }

    table.q td {
        display: block;
        width: 100%;
        white-space: normal;
    }

    td.cmd,
    td.level {
        padding-bottom: 1px;
    }

    td.desc {
        padding: 0 0 12px;
    }
}

.dim {
    color: var(--dim);
}

footer {
    color: var(--dim);
    font-size: 12px;
    /* Stuck to the viewport bottom: .wrap is a full-height flex column, so
       auto top margin pins the footer on short pages and flows a beat after
       the content on long ones. No rule above — the gap is the divider. */
    margin-top: auto;
    padding-top: 72px;
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 8px;
}

/* Footer links stay as quiet as the footer text was — no underline,
   warming on hover like the nav. */
footer a {
    color: var(--dim);
    transition: color var(--motion-snap);
}

footer a:hover {
    color: var(--platinum);
}

/* The rig's meter: three true numbers. The on-air count is the one lamp —
   core means live, here as everywhere. */
footer .pulse {
    color: var(--dim);
    font-variant-numeric: tabular-nums;
}

footer .pulse b {
    color: var(--core);
    font-weight: 600;
}

/* ── scroll reveal ───────────────────────────────────────────────────── */

/* Hidden only once reveal.js has confirmed it can reveal them again — the
   class lands on <html> from the script, so no-JS and parse-failure both
   leave the page fully visible. */
.js-reveal .reveal {
    opacity: 0;
}

/* Content is simply there — a near-instant fade, no travel. */
.js-reveal .reveal {
    transition: opacity var(--motion-snap);
}

.js-reveal .reveal.in {
    opacity: 1;
}

/* ── the landing ─────────────────────────────────────────────────────── */

/* The landing is one screen and one pane: mark, sentence, button, stacked on
   the centre line. `.pane-full` already centres with `safe`, so nothing goes
   out of reach on a short viewport; the mark is sized against the viewport
   height so the whole stack stays on one screen. */
.landing {
    align-items: center;
    text-align: center;
}

.landing .hero.mark {
    /* The mark needs air under it. At 8px the sentence crowded the connector
       and the two read as one block; the mark is the thing you look at first
       and it wants a beat before the words start. 70px is that beat plus two
       blank lines (body line-height is 15px x 1.6 = 24px). */
    margin: 0 0 70px;
    width: 100%;
}

.landing .hero.mark img {
    max-width: min(460px, 52vh);
}

.landing h1 {
    margin: 0 0 14px;
    font-size: 30px;
}

.landing .lede {
    margin: 0;
    max-width: 34em;
}

/* The rotating word: platinum, easing through the swap with a small dip.
   Width is measured and eased by index.ts so the sentence breathes to the
   next word instead of snapping. */
#rotor {
    color: var(--platinum);
    display: inline-block;
    white-space: nowrap;
    overflow: hidden;
    vertical-align: bottom;
    transition:
        opacity var(--motion-swap),
        transform var(--motion-swap),
        width var(--motion-swap);
}

#rotor.out {
    opacity: 0;
    transform: translateY(6px);
}

/* ── about ───────────────────────────────────────────────────────────── */

h2 {
    font-size: 13px;
    font-weight: normal;
    color: var(--platinum-mid);
    letter-spacing: 0.08em;
    text-transform: lowercase;
    margin: 0 0 clamp(24px, 5vh, 44px);
}

/* One rhythm for the whole document. The about page is a single scroller in
   the app shell, and it carries its rhythm as one row-gap with every block
   margin zeroed: one beat between every block, no doubles, nothing to
   balance by hand. (A flex column never collapses margins — they stack —
   which is why the gap does the work instead of the margins.) */
.about-doc {
    display: flex;
    flex-direction: column;
    row-gap: var(--beat-section);
}

.about-doc > *,
.about-doc .hero.mark,
.about-doc .disclosure {
    margin: 0;
}

.about-doc h2 {
    margin: 0 0 18px;
}

/* Folds stacked inside one group sit close — they are one group. */
.about-doc .disclosure + .disclosure {
    margin-top: 14px;
}

/* ── about: the circuit stood on end in the rail ──────────────────────── */

/* The circuit asset is wide (1608×536). In a 30% rail it would shrink to a
   stripe, so it takes a quarter turn and reads as a tall column instead.
   A rotation doesn't change layout, so the wrapper reserves the box the
   turned image actually occupies — width and height swap. */
/* The rail is oversized art, not a fitted figure: 2.5x the pane, grounded.
   Its foot sits exactly on the browser's bottom edge and the rest runs out
   of frame past the top of the pane, so what the frame cuts reads as the
   piece continuing beyond it — a piece that ends mid-pane with a gap below
   reads as cropped by mistake instead.

   Reaching the true viewport edge takes three escapes, because three things
   sit between the pane and the browser bottom: the pane's own glow padding,
   the footer row, and the shell's bottom padding. The rail spans the footer
   row (the footer moves to the right column on this page alone), zeroes its
   bottom padding, and pulls through the shell padding with a negative
   margin. `overflow: hidden` then crops at the pane's edges — which now ARE
   the viewport's, on the side that matters. */
.about-rail {
    grid-row: 2 / 4;
    justify-content: flex-end;
    align-items: center;
    overflow: hidden;
    padding-bottom: 0;
    margin-bottom: calc(-1 * max(10px, env(safe-area-inset-bottom)));
}

/* Only the about page trades its full-width footer for the grounded rail;
   `:has` scopes it without a per-page body class. */
.wrap:has(.about-rail) > footer {
    grid-column: 2;
}

/* Selector strength is load-bearing here, and it is the actual #314 fix.
   The rail's markup is `.hero.mark.about-turn`, and the generic mid-page
   mark rules — `.hero.mark img { max-width: 460px }` and `.hero.mark
   { margin }` — outrank a bare `.about-turn` (two classes beat one), so they
   silently clamped the rail's long edge to 460px at every width. That clamp,
   not the sizing model, is why the rail stayed small through a fix that was
   correct about everything else. Scoping through the pane wins the cascade
   without touching the mark rules other pages use. */
.pane-left.about-rail .about-turn {
    margin: 0;
    flex: 0 0 auto;
    height: 250%;
    /* 2.5x the pane on purpose: height is the stated size, the aspect ratio
       derives the width, and the pane's overflow crop takes what does not
       fit. Do not "fix" this back to flex: 1 — filling the pane exactly is
       the small-picture-in-a-big-frame look this replaced. */
    width: auto;
    aspect-ratio: 536 / 1608;
    display: grid;
    place-items: center;
}

.pane-left.about-rail .about-turn video,
.pane-left.about-rail .about-turn img {
    /* Lay the asset out along its own long axis, then turn the whole thing.
       `1608/536` keeps the aspect honest before the transform.

       Anti-clockwise, so the bulb stands at the top of the column and the
       cube sits at its foot. The frames render horizontally with the cube on
       the left, so a clockwise turn stands the assembly on its head — which
       is what the rail shipped as, and what #203 reported. */
    /* Against the container, not the viewport: the container is already the
       turned box, so `100%` of it is exactly the long edge the image needs,
       and the relationship survives whatever ends up sizing that box. */
    width: calc(100% * (1608 / 536));
    max-width: none;
    aspect-ratio: 1608 / 536;
    height: auto;
    transform: rotate(-90deg);
    transform-origin: center;
}

/* On a phone the rail is a pane of its own, one tab away, with the whole
   screen to itself (#261) — so the quarter turn stays and the column runs the
   full height. It used to lie back down here, because a turned image in a
   40vh strip read worse than a flat one; that strip is gone.

   No size override any more: the base rule is height-driven at every width,
   which is what this breakpoint used to have to say for itself. */

section {
    margin: 0 0 var(--beat-section);
}

section:first-of-type {
    margin-top: clamp(32px, 6vh, 72px);
}

/* The install-script fold: read the script before you pipe it to a shell. */
details.script-fold summary {
    display: inline-block;
    cursor: pointer;
    color: var(--platinum-mid);
    font-size: 12px;
    letter-spacing: 0.06em;
    line-height: 1.75;
    user-select: none;
    list-style: none;
    padding: 8px 12px;
    border-radius: 4px;
}
details.script-fold summary::-webkit-details-marker { display: none; }
details.script-fold summary::before { content: "+ "; color: var(--dim); }
details.script-fold[open] summary::before { content: "− "; }
details.script-fold summary:hover { color: var(--platinum); }
details.script-fold[open] summary { margin-bottom: 14px; }
details.script-fold { margin-top: 22px; }
table.q { margin: 6px 0 24px; }
.relay-wrap { margin-top: 6px; }
.tag { margin-top: 18px; }
.hero.mark { margin: var(--beat-section) 0; }

/* ── pricing ─────────────────────────────────────────────────────────── */

/* Three tiers, one matrix: every card lists the same rows in the same
   order, and the lamp dot says what's lit at that tier. Cards glow on
   hover like everything else, but don't press — the CTA inside is the
   pressable thing. The section centers itself in the viewport. */
body.pricing section {
    margin: auto 0;
}

.tiers {
    display: grid;
    /* Capped, not stretched. On a full-width pane `1fr` gave each card a
       third of the viewport, so a twelve-word row sat in a column built for
       a paragraph. The cards take the width their text wants and the row
       centres in whatever is left. */
    grid-template-columns: repeat(auto-fit, minmax(180px, 240px));
    justify-content: center;
    gap: clamp(20px, 3vw, 40px);
    align-items: stretch;
    /* Room for the halo. The glow is a box-shadow reaching ~12px past the
       border, so without an inset margin the outermost bands are clipped by
       whatever contains the grid — and the effect reads as a thin rim
       instead of a flame. Carried by the component, not by each page. */
    padding: var(--glow-room);
}

/* A pricing tier is an interactive panel — the pipe glow (via the `.tier`
   membership in the glow bucket) is its whole elevation.
   Compact on the inside, generous on the outside: the card holds its content
   tightly and the space around it belongs to the halo. */
.tier {
    border-radius: 4px;
    padding: 14px 15px;
    display: flex;
    flex-direction: column;
    /* So a lone `.tier` outside a `.tiers` grid still clears its own glow —
       page authors shouldn't have to remember this. */
    margin: var(--glow-room);
}

/* Inside a grid the gap already separates them; the container's padding
   holds the outer edge. Doubling both would push the columns apart. */
.tiers > .tier {
    margin: 0;
}

.tier h3 {
    margin: 0 0 6px;
    font-size: 13px;
    font-weight: normal;
    color: var(--platinum-mid);
    letter-spacing: 0.08em;
    text-transform: lowercase;
}

.tier-price {
    font-size: 24px;
    color: var(--hi);
    margin: 0 0 8px;
    font-variant-numeric: tabular-nums;
}

.tier-price span {
    font-size: 12px;
    color: var(--dim);
    margin-left: 6px;
}

/* Lists anchor to the bottom of the card: every panel's bullets sit on
   one shared grid, and free's anonymous group juts up above it. */
.tier-list {
    list-style: none;
    padding: 0;
    /* Type stays at its normal size; the rhythm is what tightens. At 2.5
       a twelve-item list ran taller than the viewport on its own — that
       was the height, not the font. 1.7 sits the rows close without
       cramping the words themselves. */
    margin: auto 0 0;
    font-size: 13px;
    line-height: 1.7;
}

/* The lamps: lit platinum for what you get, a dark ring for what you don't. */
.tier-list li {
    color: #4e4e4e;
    display: flex;
    align-items: baseline;
    gap: 9px;
}

.tier-list li::before {
    content: "";
    flex: 0 0 auto;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    border: 1px solid #2e2e2e;
    align-self: center;
}

.tier-list li.got {
    color: var(--dim);
}

/* Opening a fold glides on the settle curve; closing glides back.
   Browsers without ::details-content interpolation simply snap. */
details.disclosure::details-content,
details.script-fold::details-content {
    height: 0;
    opacity: 0;
    overflow: clip;
    will-change: height;
    transition:
        height 0.4s var(--ease-settle),
        opacity 0.3s var(--ease-glide),
        content-visibility 0.4s allow-discrete;
}

details.disclosure[open]::details-content,
details.script-fold[open]::details-content {
    height: auto;
    opacity: 1;
    /* Only the settled-open state gets the clip margin (it lets child
       glows paint past the edges). During a close it drops instantly, so
       nothing lingers outside the collapsing box — the fade covers the
       clip. */
    overflow-clip-margin: 18px;
}

/* Group labels inside a card's list — no lamp, just a quiet heading. */
.tier-list li.tier-group {
    color: var(--platinum-mid);
    font-size: 11px;
    letter-spacing: 0.08em;
    margin-top: 12px;
    height: 28px;
}

.tier-list li.tier-group::before {
    display: none;
}

.tier-list li.got::before {
    border-color: transparent;
    background: var(--platinum);
    box-shadow: 0 0 5px rgba(192, 144, 96, 0.55);
}

.tier-cta {
    margin: 0;
}

/* Enterprise's short list centers itself in the card. */
.tier:last-child .tier-list {
    margin: auto 0;
}

.tier-cta .btn {
    width: 100%;
    font-size: 13px;
}

/* The flux droplets drift behind the cards, and the cards are transparent at
   rest — so the lamp reads *through* the panels you aren't pointing at.
   Hovering fills a tier opaque black, so the one you're reading is the one
   that hides the drops. That's the whole effect, and it needs nothing but
   the stacking order: canvas under the shell, content over it.

   (This used to be inverted — canvas above the content — with invisible
   occluder planes in the scene, positioned per card each frame, to fake the
   panels blocking the drops. Doing it in the stacking order instead deletes
   that machinery and gets the transparency for free.) */
#scene-pricing {
    position: fixed;
    inset: 0;
    z-index: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

body:has(#scene-pricing) .wrap {
    position: relative;
    z-index: 1;
}

/* ── auth ────────────────────────────────────────────────────────────── */

/* Sign-in is a narrow centered card: small buttons, no pitch. */
.auth-view {
    max-width: 400px;
    margin: clamp(48px, 14vh, 120px) auto 0;
    width: 100%;
}

.auth-view h2 {
    text-align: center;
}

/* Nicks list on the me page: oldest first, oldest tagged as display name. */
.nick-list {
    list-style: none;
    margin: 10px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.nick-list li {
    display: flex;
    align-items: baseline;
    gap: 10px;
}
.nick-list .tag {
    font-size: 11px;
    color: var(--platinum);
    border: 1px solid rgba(192, 144, 96, 0.35);
    border-radius: 4px;
    padding: 1px 6px;
    letter-spacing: 0.02em;
}

/* ── .btn — the one control ──────────────────────────────────────────────
   Every button and CTA carries .btn, so there is one button in the whole site.
   Transparent and rounded, it shrinks to its text and lights to platinum on hover
   — the pipe glow carries the elevation. Size and emphasis are modifiers
   (`class="btn mini"`), never a new button. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 8px 16px;
    border-radius: 5px;
    font: inherit;
    font-size: 14px;
    color: var(--fg);
    cursor: pointer;
    text-align: center;
}
.btn:hover:not(:disabled) {
    color: var(--platinum);
}
.btn:disabled {
    opacity: 0.5;
    cursor: default;
}
/* size / shape modifiers */
.mini {
    padding: 3px 10px;
    font-size: 12px;
    color: var(--dim);
}
.compose-open {
    display: block;
    width: 100%;
    text-align: left;
    padding: 12px 14px;
    color: var(--dim);
}

/* Providers stack vertically, compact and centered. */
.providers {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
}
.providers .provider {
    padding: 7px 22px;
    font-size: 13px;
}

.btn-row {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    margin: 16px 0 0;
}

/* Per-conversation actions sit right under the thread head, so they take the
   tighter top margin the generic row's 16px would turn into a gap. */
.btn-row.thread-actions {
    margin: 6px 0 10px;
}

.btn-row .btn {
    flex: 0 0 auto;
    font-size: 13px;
    padding: 7px 14px;
}

/* "or" — a label between two fades, not a drawn rule. */
.auth-div {
    display: flex;
    align-items: center;
    gap: 12px;
    color: var(--dim);
    font-size: 12px;
    margin: 20px 0;
    user-select: none;
}

.auth-div::before,
.auth-div::after {
    content: "";
    flex: 1;
    height: 1px;
}

.auth-div::before {
    background: linear-gradient(90deg, transparent, rgba(192, 144, 96, 0.22));
}

.auth-div::after {
    background: linear-gradient(90deg, rgba(192, 144, 96, 0.22), transparent);
}

/* A generous gap sets the reserved-nick path apart from the providers; nick +
   password share one line, the connect button waits below. */
.pw {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin-top: clamp(44px, 13vh, 104px);
}

.pw-fields {
    display: flex;
    gap: 12px;
}

/* Connect is styled ready-to-go but stays invisible (and unclickable) until
   both fields are filled — then auth.ts adds .ready and it fades in. */
#pw-btn {
    align-self: center;
    padding: 7px 22px;
    font-size: 13px;
    color: var(--platinum);
    border: 1px solid var(--platinum);
    background: rgba(192, 144, 96, 0.12);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--motion-arrive);
}
#pw-btn.ready {
    opacity: 1;
    pointer-events: auto;
}

/* Layout only: .well owns how an input looks. */
.pw .well {
    flex: 1;
    min-width: 0;
}

.auth-hint {
    margin-top: 24px;
    font-size: 12px;
    text-align: center;
}

/* The me page: one responsive stack, centered in the viewport, spaced
   by a single gap instead of ad-hoc margins. */
#me,
#deactivated,
#cli-code {
    display: flex;
    flex-direction: column;
    gap: clamp(18px, 3.5vh, 34px);
    max-width: 560px;
    margin: auto 0;
    width: 100%;
}

#me > *,
#deactivated > *,
#cli-code > * {
    margin: 0;
}

#me table.q {
    margin: 0;
}

#me .btn-row,
#deactivated .btn-row {
    margin: 0;
}

/* Inner groups keep their own tight rhythm. */
#claim-box,
#pw-set {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.status {
    font-size: 13px;
    margin: 0 0 14px;
    min-height: 1.3em;
    max-width: 22em;
}

.status.err {
    color: #d17a5b;
}

.status.ok {
    color: var(--core);
}

.install#submit-btn {
    max-width: 22em;
}

/* ── disclosure ──────────────────────────────────────────────────────── */

.disclosure {
    margin: 24px 0 0;
}

.disclosure > summary {
    color: var(--platinum-mid);
    font-size: 12px;
    letter-spacing: 0.06em;
    cursor: pointer;
    user-select: none;
    list-style: none;
    display: inline-block;
    padding: 8px 12px;
    border-radius: 4px;
}

/* Fold content indents to the label text and gets air under the head. */
.disclosure > :not(summary),
details.script-fold > :not(summary) {
    margin-left: 14px;
}

.disclosure > summary::-webkit-details-marker {
    display: none;
}

.disclosure > summary::before {
    content: "+ ";
    color: var(--dim);
}

.disclosure[open] > summary::before {
    content: "− ";
}

.disclosure[open] > summary {
    margin-bottom: 18px;
}

/* ── bbs board ───────────────────────────────────────────────────────── */

.bbs {
    font-size: 14px;
}

/* A post is a surface like every other container on the site — it takes its
   fill from the shape rule and its edge from the seam, rather than being the
   one thing on the board that floats bare on the page. */
.post {
    padding: 12px 14px;
    border-radius: 4px;
    margin: 0 0 8px;
}

/* Flare (#195): a paid nick's emoji and colour. The wrapper keeps the pair
   together as one item in the flex rows, and the nick element inside it keeps
   whatever the surface already gave it — the colour arrives inline, so a nick
   with no flare is byte-for-byte the same markup it always was. */
.nick-flare {
    display: inline-flex;
    align-items: baseline;
    gap: 0;
}

.flare-emoji {
    font-style: normal;
    /* Emoji ignore the text colour; this keeps one from out-shouting the
       nick it belongs to. */
    opacity: 0.9;
}

.post-meta {
    color: var(--dim);
    font-size: 12px;
    margin-bottom: 4px;
}

.post-meta b {
    color: var(--platinum);
    font-weight: 600;
}

.post-text {
    display: block;
    color: var(--fg);
    white-space: pre-wrap;
    word-break: break-word;
}

.post-text:hover {
    color: var(--platinum);
}

/* ── admin dashboard ─────────────────────────────────────────────────── */

/* Only the requests sparkline is admin's own — everything else on the
   dashboard is the shared set: disclosure folds, table.q, .well, .btn. */
.spark { display: flex; align-items: flex-end; gap: 2px; height: 48px; margin: 14px 0 4px; }
.spark i { flex: 1; min-width: 3px; background: var(--platinum-deep); border-radius: 1px 1px 0 0; }
.spark i.hot { background: var(--platinum); }
.spark-label { font-size: 12px; color: var(--dim); margin-bottom: 4px; }

/* ── small shared controls ───────────────────────────────────────────── */

/* .mini is a size modifier on .btn: a quiet inline row action. */

/* Board tabs: the public board and your cohort boards, side by side. The one
   you are on is a `b` rather than a link, so it is not a place to go. */
.board-tabs {
    margin: 0 0 14px;
}

.board-tabs .here {
    color: var(--platinum);
}

/* ── board composer, bumps, flux chip ────────────────────────────────── */

/* The collapsed composer: a quiet one-line affordance where the composer
   will appear, so reading stays the default and writing is one click away.
   Signed out, the same slot carries the sign-in line. */
.compose-slot { margin: 0 0 26px; }
.compose-slot .composer { margin: 0; }
.compose-slot .sub { margin: 0; padding: 12px 14px; }

.composer { border-radius: 4px; padding: 14px; margin: 0 0 26px; }
.composer input, .composer textarea {
    width: 100%; box-sizing: border-box; background: transparent;
    border-radius: 4px; color: var(--fg);
    padding: 9px 11px; font: inherit; margin: 0 0 8px;
}
.composer textarea { min-height: 76px; resize: vertical; }
.composer input:focus, .composer textarea:focus { outline: none; }
.composer-foot { display: flex; align-items: center; gap: 12px; }
.composer .note { font-size: 12px; color: var(--dim); }
.composer .err { color: var(--core); }
/* The upvote is a control, so it comes from the button family (it joins the
   `.mini` selector lists) and carries the flame like every other one. Only
   its size and the "already voted" state are its own. */
.upvote { padding: 1px 7px; border-radius: 3px; font-size: 12px; }
.upvote.done { color: var(--platinum); }
.flux-chip {
    display: inline-block; border-radius: 3px;
    background: rgba(192, 144, 96, 0.1);
    color: var(--platinum); font-size: 12px; padding: 2px 9px; margin-left: 10px;
    vertical-align: 2px; white-space: nowrap;
}
.board-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px; }
.lobby-meta { font-size: 12px; color: var(--dim); margin: 0 0 10px; }
.lobby-say { display: flex; gap: 8px; }
.lobby-say input {
    flex: 1; min-width: 0; background: transparent;
    border-radius: 4px; color: var(--fg); padding: 8px 11px; font: inherit;
}
.lobby-say input:focus { outline: none; }

/* ── thread lobby lines (board + testing live rooms) ─────────────────── */

.lobby-lines {
    max-height: 420px; overflow-y: auto; font-size: 14px;
    margin: 0 0 12px; overscroll-behavior: contain;
}
.lobby-line { padding: 2px 0; overflow-wrap: anywhere; }
.lobby-line b { color: var(--platinum); font-weight: 600; }
.lobby-line .t { color: var(--dim); font-size: 11px; margin-left: 6px; }

/* ── contacts ────────────────────────────────────────────────────────── */

.contact-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; flex-wrap: wrap; }
.contact-row code { color: var(--platinum); }
.contact-row .live { color: var(--core); font-size: 12px; }
.contact-row .off { color: var(--dim); font-size: 12px; }
.contact-row .mutual { color: var(--dim); font-size: 12px; }
.contact-row .msg-link { font-size: 12px; }
.add { display: flex; gap: 8px; margin: 12px 0 4px; }
.add input {
    flex: 1; min-width: 0; background: transparent;
    border-radius: 4px; color: var(--fg); padding: 8px 11px; font: inherit;
}
.add input:focus { outline: none; }

/* Cohorts (contacts page): section heading + the expanded roster box. */
.reveal h3 { margin: 28px 0 6px; font-size: 15px; color: var(--platinum); }
.cohort-detail {
    margin: 2px 0 14px 14px; padding-left: 12px; border-left: 1px solid var(--seam);
    background: linear-gradient(90deg, rgba(96, 72, 24, 0.18), transparent 18px);
}
.cohort-detail .mini { margin-top: 8px; }

/* ── token reveal (auth: the one-time agent credential box) ──────────── */

.token-reveal {
    border-radius: 4px; padding: 12px 14px;
    margin: 10px 0; font-size: 13px; overflow-wrap: anywhere;
}
.token-reveal code { color: var(--hi); }

/* ── cockpit (/app/) ─────────────────────────────────────────────────── */

.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; margin: 0 0 20px; }
.card { border-radius: 4px; padding: 14px; }
.card h3 { margin: 0 0 10px; }
.kv { display: flex; justify-content: space-between; gap: 10px; padding: 3px 0; font-size: 14px; }
.kv .k { color: var(--dim); }
.kv .v { overflow-wrap: anywhere; text-align: right; }
.kv .v.ok { color: var(--platinum); }
.kv .v.bad { color: var(--core); }
/* Card actions keep only their layout tweak; the rest is the .btn base. */
.card .btn {
    font-size: 13px;
    margin: 8px 8px 0 0;
}
.note-line { font-size: 12px; color: var(--dim); margin: 8px 0 0; }
.note-line.err { color: var(--core); }
.user-code {
    font-size: 22px; letter-spacing: 3px; color: var(--platinum);
    border: 1px dashed var(--platinum-deep); border-radius: 4px;
    padding: 8px 14px; display: inline-block; margin: 8px 0;
}
.approval { border-radius: 4px; padding: 9px 11px; margin: 0 0 8px; font-size: 13px; }
.approval b { color: var(--platinum); }
.ticker {
    max-height: 340px; overflow-y: auto; font-size: 13px;
    overscroll-behavior: contain;
}
.tick { padding: 2px 0; overflow-wrap: anywhere; }
.tick .tag { color: var(--platinum); font-weight: 600; margin-right: 6px; }
.tick .t { color: var(--dim); font-size: 11px; margin-left: 6px; }
.rooms-list { font-size: 13px; color: var(--dim); }

/* ── inbox (relay /inbox/ + cockpit /app/inbox/) ─────────────────────── */

.convo-row {
    display: block; padding: 10px 12px;
    border-radius: 4px; margin: 0 0 8px; cursor: pointer; text-decoration: none;
}
/* Faintly warm seam so the open conversation is findable while scrolled. */
.convo-row.current { border-color: #171310; }
.convo-row b { color: var(--platinum); }
.convo-row .preview {
    color: var(--dim); font-size: 13px; margin-top: 3px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.badge {
    float: right; border-radius: 10px;
    background: rgba(192, 144, 96, 0.1);
    color: var(--platinum); font-size: 11px; padding: 0 7px;
}
.badge.mention { background: rgba(159, 179, 189, 0.12); color: var(--core); }
.msgs {
    max-height: 460px; overflow-y: auto; font-size: 14px;
    margin: 0 0 12px; overscroll-behavior: contain;
}
.msg { padding: 3px 0; overflow-wrap: anywhere; }
.msg b { color: var(--platinum); font-weight: 600; }
.msg.mine b { color: var(--fg); }
.msg .t { color: var(--dim); font-size: 11px; margin-left: 6px; }
.msg .via { color: var(--dim); font-size: 11px; margin-left: 4px; }
.sealed-note { color: var(--dim); font-size: 12px; margin: 0 0 10px; }
.thread-head { display: flex; align-items: baseline; gap: 12px; }
.thread-head .act {
    color: var(--dim); font-size: 12px; cursor: pointer;
    background: none; border: none; font-family: inherit; padding: 0;
}
.thread-head .act:hover { color: var(--core); }
.dm-say { display: flex; gap: 8px; }
.dm-say input {
    flex: 1; min-width: 0; background: transparent;
    border-radius: 4px; color: var(--fg); padding: 8px 11px; font: inherit;
}
.dm-say input:focus { outline: none; }
.dm-new { display: flex; gap: 8px; margin: 0 0 12px; }
.dm-new input {
    flex: 1; min-width: 0; background: transparent;
    border-radius: 4px; color: var(--fg); padding: 7px 10px; font: inherit; font-size: 13px;
}
.dm-new input:focus { outline: none; }

/* ── misc ────────────────────────────────────────────────────────────── */

.more {
    margin: 18px 0 0;
    font-size: 13px;
}

/* ── relay matrix ────────────────────────────────────────────────────── */

/* A three-column truth table rather than prose: what the relay can read,
   and what it can see about a message it cannot read. Colour carries the
   verdict so the eye finds the "nothing" rows without reading. */
table.relay th {
    text-align: left;
    color: var(--dim);
    font-weight: normal;
    font-size: 12px;
    letter-spacing: 0.04em;
    padding: 0 16px 10px 0;
}

table.relay td {
    color: var(--fg);
}

table.relay td.cmd {
    color: var(--platinum);
}

/* Green-ish for "relay sees plaintext", core-blue for "sees nothing" — the
   good outcome is the private one, so the calm colour marks the nothings. */
table.relay td.yes {
    color: var(--platinum-mid);
}

table.relay td.no {
    color: var(--core);
}

/* The relay matrix is a real grid — stacking its cells like the other
   tables would scramble the content/metadata columns. Let it scroll instead
   on narrow screens, and opt it out of the generic stacking rule. */
.relay-wrap {
    overflow-x: auto;
}

@media (max-width: 560px) {
    table.relay,
    table.relay tbody,
    table.relay thead,
    table.relay tr {
        display: table;
        width: auto;
    }
    table.relay tr {
        display: table-row;
    }
    table.relay td,
    table.relay th {
        display: table-cell;
        width: auto;
        white-space: nowrap;
        padding: 8px 16px 8px 0;
    }
}

/* The connector as a mid-page mark on the about page — smaller than the
   landing hero, with air above and below so it reads as a breather between
   the argument and the code. */
.hero.mark {
    margin: 8px 0;
}

.hero.mark img {
    max-width: 460px;
}



/* ── install CTA (landing / 404) ─────────────────────────────────────── */

.cta {
    margin: 30px 0 0;
    font-size: 15px;
}

/* ── board: thread + lobby split ─────────────────────────────────────── */

/* Thread list: a live front page. Each story leads with its headline and
   the opening of its text; the contrast between headline, body, and dim
   meta does the separating, so there are no rules between stories. */
/* The front page: the lead spans the sheet with two-column body text,
   features run two-up, briefs file three-up and dense. Everything
   collapses to one column on a phone. */
/* A single column, in both panes. This was a six-column magazine grid with
   the newest post spanning full width, the next few at half, the rest at a
   third — which turned every post into a differently-shaped box and cut long
   text into ragged pieces. Length decides the pane now (short in the rail,
   long in the wide one), so the cards themselves are all one shape. */
.threads {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* Length picks the pane, so the cards are one shape. The rail's posts are
   short by definition and the wide pane's are long; nothing needs a size
   variant on top of that. (This replaced .lead/.story/.brief, which existed
   to fill a six-column grid that no longer exists.) */

/* In the rail the excerpt is a hint, not the post — three lines and out. */
.pane-left .thread-excerpt {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.thread {
    display: block;
    padding: 16px 18px;
    border-radius: 4px;
    color: var(--fg);
}
.thread:hover .thread-subject { color: var(--platinum); }
.thread-subject {
    color: var(--hi);
    font-weight: 600;
    font-size: 17px;
    line-height: 1.35;
    letter-spacing: -0.01em;
}
.thread-excerpt {
    color: var(--fg);
    font-size: 14px;
    line-height: 1.6;
    margin-top: 4px;
    max-width: 62em;
    overflow-wrap: anywhere;
}
.thread-meta { color: var(--dim); font-size: 12px; margin-top: 5px; }
.thread-meta b { color: var(--platinum); font-weight: 600; }

/* The split: replies left, lobby right. Both panes collapse to a rail. */
.split {
    display: flex;
    gap: 20px;
    align-items: flex-start;
}
.pane {
    flex: 1 1 0;
    min-width: 0;
    border-radius: 4px;
}
.pane.collapsed { flex: 0 0 auto; }

.pane-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 14px 8px;
    cursor: pointer;
    color: var(--platinum-mid);
    font-size: 13px;
    letter-spacing: 0.06em;
    user-select: none;
}
.pane-head:hover { color: var(--platinum); }
.pane-head .caret { color: var(--dim); width: 1ch; }
/* The fold. Closing, the contents leave first and quickly, and the box
   follows a beat behind — nothing is caught mid-air by a moving edge.
   Opening, the box leads and the contents arrive once there is room for
   them. The shade comes along on its own: collapsed to a rail the pane is
   elongated, so --ar climbs and the surface greys through the move. */
.pane-body {
    padding: 6px 14px 14px;
    height: auto;
    overflow: clip;
    transition:
        height var(--motion-fold),
        padding var(--motion-fold);
}

.pane-body > * {
    opacity: 1;
    transform: none;
    transition:
        opacity var(--motion-veil) 0.22s,
        transform 0.3s var(--ease-settle) 0.22s;
}

.pane.collapsed .pane-body {
    height: 0;
    padding-top: 0;
    padding-bottom: 0;
    transition-delay: 0.08s;
}

.pane.collapsed .pane-body > * {
    opacity: 0;
    transform: translateY(4px);
    transition-delay: 0s;
    transition-duration: 0.12s;
}

@media (prefers-reduced-motion: reduce) {
    .pane-body,
    .pane-body > * { transition: none; }
}

/* Replies pane rows reuse .post styling; the root is marked and reads a
   touch larger — it's the piece everyone came for. */
.post.root .post-text { color: var(--fg); font-size: 15px; line-height: 1.75; }
.post.root .post-meta b { color: var(--core); }

/* Moderation: quiet until hovered, like everything else. */
.post-meta .del { margin-left: 10px; }

/* Lobby pane: live chat preview. */
.lobby-note {
    color: var(--dim);
    font-size: 13px;
    line-height: 1.7;
}
.lobby-note b { color: var(--platinum); font-weight: 600; }

/* ── error pages ─────────────────────────────────────────────────────── */

/* The physics scene (or its still fallback) paints behind the text layer;
   the code itself stays 2D — big, bold, platinum. */
.err-page .wrap {
    position: relative;
    z-index: 1;
}

/* The error copy centres in the shell's scroller. This replaces the old
   top-down beat the error page used to borrow from the landing dialect,
   back when both shared a body class. */
.err-stage {
    /* No min-height: as the shell's section it already carries `flex: 1`,
       and the shell's own rule outranks anything set here anyway. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: clamp(16px, 4vh, 32px);
}

.err-stage > * {
    margin: 0;
}

.err-page h1.err-code {
    font-size: clamp(72px, 18vw, 160px);
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1;
    color: var(--platinum);
    text-shadow: 0 12px 40px rgba(96, 72, 24, 0.5);
}

#scene404 {
    position: fixed;
    inset: 0;
    z-index: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    transition: opacity 0.7s var(--ease-settle);
}

.err-still {
    position: fixed;
    right: -5vw;
    bottom: -4vh;
    width: min(46vw, 480px);
    z-index: 0;
    opacity: 0.35;
    pointer-events: none;
}

/* ── the app shell ─────────────────────────────────────────────────────
   Every page uses the two-pane grid (see .wrap above). The body never
   scrolls — panes scroll independently. This replaces the old body.app
   and the 900px ribbon. */

body {
    overflow: hidden;
    padding: 0 max(20px, env(safe-area-inset-left)) 0
        max(20px, env(safe-area-inset-right));
}

/* In a fixed frame the footer is a status strip, not a page ending. */
footer {
    margin-top: 0;
    padding-top: 6px;
    font-size: 11px;
    flex-wrap: nowrap;
    gap: 14px;
    white-space: nowrap;
    overflow: hidden;
}

/* ── panes ─────────────────────────────────────────────────────────────
   Panes live inside the left and right grid columns. They stack vertically,
   each with its own head + scrolling body. The grid handles the 30/70 split;
   panes no longer carry a `major` ratio. */

.pane {
    display: flex;
    flex-direction: column;
    min-height: 0;
    flex: 1;
}

.pane-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 6px 14px 14px;
}

/* A pane that ends in a composer scrolls its LIST, not its body. */
.pane-body:has(.msgs, .lobby-lines) {
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

:is(.msgs, .lobby-lines, .ticker) {
    max-height: none;
    flex: 1;
    min-height: 40px;
}

.pane.collapsed .pane-body { flex: 0 0 0; height: 0; padding-top: 0; padding-bottom: 0; }

/* Phone only: the way back from a detail pane to its list. */
.pane-back {
    display: none;
    font-size: 12px;
    color: var(--dim);
}
.pane-back:hover { color: var(--platinum); }

/* Phone only, likewise: the tab that reaches the pane a `swap` wrap is not
   showing. Above the breakpoint there is room for both panes, so it means
   nothing and the breakpoint block below turns it back on. */
.pane-tab { display: none; }

/* ── the one app breakpoint ────────────────────────────────────────────
   760px, and there is no other. Below it the two-pane grid stacks: left
   rail on top, right content fills the rest — unless the wrap says `swap`,
   in which case the phone shows ONE pane and a tab for the other (#261). */
@media (max-width: 760px) {
    body { padding: 0 12px; }
    .wrap {
        grid-template-columns: 1fr;
        grid-template-rows: auto auto 1fr auto;
        padding-top: max(10px, env(safe-area-inset-top));
    }
    .pane-left  { grid-column: 1; grid-row: 2; max-height: 40vh; }
    .pane-right { grid-column: 1; grid-row: 3; }

    /* A solo wrap is one pane by definition, so the rail cap above is not
       for it — it left a card stranded in the top 40% with nothing under it. */
    .wrap.solo > .pane-left { max-height: none; grid-row: 2 / 4; }

    .pane { min-width: 0; width: 100%; }

    /* The nav becomes a register tape: one row, scrolled, fading out at the
       right edge so it reads as continuing rather than cut. No hamburger —
       the links are the instrument, hiding them behind a menu isn't. */
    .nav {
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
        gap: 16px;
        -webkit-mask: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
        mask: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
    }
    .nav::-webkit-scrollbar { display: none; }
    .nav > * { flex: 0 0 auto; }

    /* Two things a phone browser insists on: 16px in a field (anything
       smaller and iOS zooms the page on focus) and a 44px target. */
    .well {
        font-size: 16px;
    }
    /* .upvote is deliberately absent: it's a count you tap beside a byline,
       not a primary action, and 44px would make it the loudest thing in a
       post's meta line. */
    .btn:not(.upvote) {
        min-height: 44px;
    }
    .mini { padding: 6px 10px; }
    .pane-head { min-height: 44px; }

    /* ── one pane, and a tab for the other (#261) ──────────────────────
       Stacking two panes on a phone gives each of them half a screen and
       neither of them enough: /about spent the top 40vh on a decorative
       rail and letterboxed the copy under it, and the inbox showed a list
       and a thread at once with a back link that had nowhere to go.

       So a `swap` wrap shows exactly one pane below the breakpoint. Which
       one is `show-detail` on the wrap — set by whatever already routes
       the page, because every page that wants this has that state anyway
       (the inbox has had the toggle since the shell landed; only these
       rules were missing).

       A modifier on the wrap rather than a markup move, for the reason
       `.wrap.solo` gives above: the state is not fixed at build time, and
       a class on the grid is the smallest thing that can change. */
    .wrap.swap > .pane-left,
    .wrap.swap > .pane-right {
        grid-row: 2 / 4;
        max-height: none;
    }
    .wrap.swap:not(.show-detail) > .pane-right,
    .wrap.swap.show-detail > .pane-left { display: none; }

    /* The tab is the pane you are not looking at, docked to the bottom
       where a thumb is. It is always there, not just once you have
       navigated somewhere — on /about the hidden pane is decoration rather
       than a place you came from, so an affordance that only appears after
       the fact would never be found. */
    .pane-tab {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
        position: sticky;
        bottom: 0;
        margin-top: auto;
        padding: 10px 14px;
        min-height: 44px;
        font-size: 12px;
        color: var(--dim);
        background: #000;
        border-radius: 4px 4px 0 0;
        cursor: pointer;
        /* Sticky inside a scroller, so it rides above the copy rather than
           scrolling away with it. */
        z-index: 1;
    }
    .pane-tab:hover { color: var(--platinum); }

    /* The inbox's own way back, which has been in the markup and invisible
       since the grid rewrite dropped the rules that showed it. */
    .pane-back { display: inline; }
}

/* ── elevation ───────────────────────────────────────────────────────── */

/* The surface system. Five levels: z0 is the page itself (prose floats free,
   no surface); z1 resting containers; z2 interactive at rest; z3 the same
   element raised by hover/focus; z4 overlays and reveals — the only level
   whose glow may carry the glass core. A surface stays black like the page;
   its altitude is a directionless halo: a whisper at rest, clearer under
   the pointer, and a copper bloom on press. One rule of the system:
   nothing elevated may live inside an overflow container — the glow clips.
   This section stays last in the file so the buckets win the cascade. */

/* The surface family — transparent at rest so the dot matrix shows through.
   Interactive members fill to opaque black on hover/focus/active so the
   pipe glow has a solid backdrop. */
:is(.surface, .pane, .tier, .thread, .install, .btn) {
    background-color: transparent;
    border: 1px solid var(--seam);
}

/* Interactive surfaces fill opaque black when engaged. */
:is(.install, .tier, .convo-row, .btn, .thread,
    details.script-fold summary, .disclosure > summary):is(:hover, :focus-visible):not(:disabled),
.convo-row.current,
.glow-press {
    background-color: #000;
}

/* z1 — resting containers carry the halo. */

/* ── the pipe glow ─────────────────────────────────────────────────────
   The brand's signature, cheap enough to put on everything: nine CONSECUTIVE
   colour bands (each layer's spread = the cumulative extent of the bands inside
   it, so it begins where the previous ends) out to 12px — a golden-rainbow
   flame that is nothing at rest, a warm golden bloom under the pointer, and the
   full flame to its blue edge on press. Two motion channels ride the same
   box-shadow: FLICKER (--glow-flicker) multiplies every band's alpha, a
   perpendicular breath toward/away; SHIMMER (--glow-sx/--glow-sy) drifts the
   whole halo in-plane, light sliding along the surface. Pure math — no filters,
   no script; the whole thing composites on the GPU. */
:is(.install, .tier, .convo-row, .btn, .thread,
    details.script-fold summary, .disclosure > summary) {
    /* Membership, declared where the members are. chrome.ts walks up from a
       pointerdown looking for this instead of carrying a copy of the list. */
    --glowable: 1;
    --glow-white: 0;
    --glow-cream: 0;
    --glow-yellow: 0;
    --glow-gold: 0;
    --glow-amber: 0;
    --glow-copper: 0;
    --glow-flare: 0;
    --glow-cyan: 0;
    --glow-blue: 0;
    box-shadow:
        var(--glow-sx) var(--glow-sy) 0 0.5px rgba(255, 253, 250, calc(var(--glow-white) * var(--glow-flicker))),
        var(--glow-sx) var(--glow-sy) 0 1px rgba(255, 244, 214, calc(var(--glow-cream) * var(--glow-flicker))),
        var(--glow-sx) var(--glow-sy) 0 1.5px rgba(240, 214, 130, calc(var(--glow-yellow) * var(--glow-flicker))),
        var(--glow-sx) var(--glow-sy) 0 2.5px rgba(214, 166, 86, calc(var(--glow-gold) * var(--glow-flicker))),
        var(--glow-sx) var(--glow-sy) 0 3px rgba(200, 132, 66, calc(var(--glow-amber) * var(--glow-flicker))),
        var(--glow-sx) var(--glow-sy) 0 4px rgba(180, 104, 58, calc(var(--glow-copper) * var(--glow-flicker))),
        var(--glow-sx) var(--glow-sy) 0 4.5px rgba(255, 248, 232, calc(var(--glow-flare) * var(--glow-flicker))),
        var(--glow-sx) var(--glow-sy) 0 5px rgba(150, 190, 216, calc(var(--glow-cyan) * var(--glow-flicker))),
        var(--glow-sx) var(--glow-sy) 0 6px rgba(120, 150, 214, calc(var(--glow-blue) * var(--glow-flicker)));
    /* One shared transition, both directions: bands rise and fall in an outward
       wave, inner first, the blue edge last. */
    transition:
        --glow-white 0.15s var(--ease-glide),
        --glow-cream 0.15s var(--ease-glide) 0.02s,
        --glow-yellow 0.15s var(--ease-glide) 0.04s,
        --glow-gold 0.16s var(--ease-glide) 0.06s,
        --glow-amber 0.16s var(--ease-glide) 0.08s,
        --glow-copper 0.17s var(--ease-glide) 0.1s,
        --glow-flare 0.17s var(--ease-glide) 0.12s,
        --glow-cyan 0.18s var(--ease-glide) 0.14s,
        --glow-blue 0.18s var(--ease-glide) 0.16s,
        --ar var(--motion-raise),
        color var(--motion-snap);
}

/* Under the pointer — the warm golden bands bloom; no cool edge yet. Both
   motions start: shimmer slow, flicker gentle. */
:is(.install, .tier, .convo-row, .btn, .thread,
    details.script-fold summary, .disclosure > summary):is(:hover, :focus-visible):not(:disabled),
.convo-row.current {
    --glow-white: 0.2;
    --glow-cream: 0.18;
    --glow-yellow: 0.2;
    --glow-gold: 0.23;
    --glow-amber: 0.19;
    --glow-copper: 0.14;
    animation:
        glow-shimmer 3.1s var(--ease-glide) infinite,
        glow-flicker 1.9s var(--ease-glide) infinite;
}

/* Pressed — the full flame to its blue edge; both motions triple. `.glow-press`
   (added on click by chrome.ts) holds this briefly so a single quick click
   still assembles the whole flame and rounds back down. */
:is(.install, .tier, .convo-row, .btn, .thread,
    details.script-fold summary, .disclosure > summary):active:not(:disabled),
.glow-press {
    --glow-white: 0.36;
    --glow-cream: 0.3;
    --glow-yellow: 0.32;
    --glow-gold: 0.36;
    --glow-amber: 0.3;
    --glow-copper: 0.28;
    --glow-flare: 0.22;
    --glow-cyan: 0.3;
    --glow-blue: 0.34;
    animation:
        glow-shimmer 1.3s var(--ease-glide) infinite,
        glow-flicker 0.62s var(--ease-glide) infinite;
}

@media (prefers-reduced-motion: reduce) {
    :is(.install, .tier, .convo-row, .btn, .thread,
        details.script-fold summary, .disclosure > summary) {
        animation: none;
    }
}

/* Inert reads as resting — disabled controls drop back to z1. */
.btn:disabled {
    box-shadow: var(--surface);
}

/* z4 — overlays and one-time reveals. .user-code keeps its dashed border:
   that's "transcribe this" iconography, not elevation. */
.token-reveal {
    border: 1px solid var(--seam);
}

:is(.token-reveal, .user-code) {
    box-shadow: var(--surface);
}

/* Inputs are wells — sunk into the page, the inverse of a raised button
   under the same light. Focus brightens the pooled light instead of
   recoloring a border. */
.well {
    background: transparent;
    /* --hairline, not --seam: with the lit bottom edge gone, the border is the
       only thing saying "there is a slot here", and a seam (#0a0a0a) is dimmer
       than the background dots. A slot you cannot find is worse than the
       underline we just removed. */
    border: 1px solid var(--hairline);
    border-radius: 4px;
    color: var(--fg);
    padding: 8px 11px;
    font: inherit;
    box-shadow: var(--well);
    transition:
        box-shadow var(--motion-raise),
        border-color var(--motion-snap);
}

.well:focus {
    outline: none;
    border-color: var(--platinum-deep);
    box-shadow: var(--well-focus);
}

/* Glow alone is a weak focus indicator on black; keyboard users get a ring.
   Inputs are excluded — their well-focus glow is the indicator. */
:is(a, button, summary, [tabindex]):focus-visible {
    outline: 1px solid var(--platinum-deep);
    outline-offset: 2px;
}

/* ── flatland ──────────────────────────────────────────────────────────
   Overflow clips at the padding box, so a halo or a flame inside a scroller
   gets sheared off at the edge. The rule the whole system rests on: inside a
   scroller, nothing is elevated. Altitude belongs to the pane itself; the
   rows within it hover flat — a lift in shade and platinum on the text, no
   flame.

   A .pane-body is not in this list on purpose: its 14px padding is wider
   than the flame's 6px reach, so rows inside it keep their glow. Any new
   scroller with less padding than that belongs here. */
:is(.lobby-lines, .msgs, .ticker)
    :is(.install, .tier, .convo-row, .btn, .thread) {
    box-shadow: none;
    animation: none;
}

:is(.lobby-lines, .msgs, .ticker)
    :is(.convo-row, .thread):hover {
    background-color: color-mix(in oklab, var(--surface-wide), white 4%);
}

/* Reduced motion: surfaces still have altitude, they just don't animate
   between levels. */
@media (prefers-reduced-motion: reduce) {
    :is(.install, .tier, .convo-row, .btn, .well) {
        transition: none;
    }
}
