/* ============================================================================
   SAMPLE HOMEPAGE — review only. Everything is namespaced `sh-` so it can never
   collide with the live site, and the whole /sample/ folder is deletable without
   touching a single production file.

   Interaction reference: https://sdsa.ai/ — one terminal column, a statement low
   in the first screen, the menu in the BOTTOM LEFT, and every answer stacking
   downward beneath it with the prompt always last.

   This is NOT a restyle. It sets rhythm only. Every colour, radius, shadow,
   weight and font comes from the design-system tokens already declared in
   css/auth.css (--ink, --muted, --card, --divider, --radius-*, --shadow-*,
   --fw-*), so the page flips light/dark with the live site's own toggle.
   ============================================================================ */

body.sh-body { overflow-x: hidden; overflow-y: auto; }

/* THE GUTTER — one value for the bar and the column, so the wordmark and the
   slogan share a left edge. Full-bleed, not the live site's centred 1200 box:
   the draft sets the type close to the page edge. */
:root {
    --sh-gutter: clamp(24px, 4vw, 58px);
    --sh-foot: clamp(26px, 7vh, 68px);      /* the menu's breathing room off the floor */
    --sh-bar: 80px;                          /* measured onto :root by js/home.js */
    /* THE COLUMN. Half the screen, capped — a full-bleed page would set prose at
       1200px and no measure survives that. The cap is what keeps a sentence to a
       readable line once the screen gets wide. */
    --sh-col: min(50%, 620px);
    /* THE STACK RHYTHM — the gap between two blocks inside a panel.
       It used to be written as `2.2em` on each block, which quietly meant two different
       things: .sh-out inherits the stage's clamp(17px,1.5vw,22px) and resolved to 48px,
       while .sh-wmount RESETS itself to --text-base (15px) and resolved to 33px. Same
       declaration, different answer, so a widget sat closer to what followed it than
       prose did — visible the moment a widget and a child list ended up adjacent.
       Stated absolutely (2.2 × the stage font) it cannot drift with whatever a block
       decides its own type should be. */
    --sh-stack: clamp(37.4px, 3.3vw, 48.4px);
}

/* ── TOP BAR — the live .auth-topbar, restored. Only the brand mark is new; the
   sign-in field, the globe, the light/dark toggle and the CTA are the production
   components with their production classes. ────────────────────────────────── */
/* the compound selector beats auth.css's own narrow-screen padding rule, so the
   wordmark keeps the column's gutter at every width */
.auth-topbar.sh-topbar {
    animation: none;                            /* no entrance on a review page */
    max-width: none; padding: 24px var(--sh-gutter) 16px;
}
/* the wordmark as TEXT (Host Grotesk), not the SVG logotype — assets/logotype.svg
   draws "DEJIMASOLUTION", which is a different mark */
.sh-mark {
    font-family: var(--font-sans);
    font-size: clamp(26px, 2.8vw, 40px); font-weight: var(--fw-bold);
    letter-spacing: normal; line-height: 1;
    color: var(--ink); text-decoration: none; white-space: nowrap;
}
.sh-mark span { color: var(--muted); }
/* the app's ⚡ sign-in field. Its own recipe carries the look; only the width is
   eased so the placeholder fits without truncating. */
.sh-ask { width: 232px; }
.sh-ask input { font-family: var(--font-sans); }

/* ── THE COLUMN — Host Grotesk throughout, aligned to the bar's gutter ─────── */
.sh-stage {
    position: relative; z-index: 1;
    padding: 0 var(--sh-gutter);
    font-family: var(--font-sans);
    font-size: clamp(17px, 1.5vw, 22px); line-height: 1.5;
    /* no negative tracking anywhere on this page — the house sets display type at
       --tracking-display (0em) and body text at normal */
    letter-spacing: normal; color: var(--ink);
}
/* THE MENU STAYS ON THE FLOOR. The first screen is exactly one viewport tall and
   bottom-aligned, so the slogan and the menu sit in the bottom-left corner.
   min-height — NOT flex-grow — is what fixes it: leftover-space layouts collapse
   the moment output is printed and yank the slogan up the page. This one never
   moves; the answers simply stack below it and the page scrolls. */
.sh-first {
    width: var(--sh-col);
    min-height: 100vh; min-height: 100svh;
    display: flex; flex-direction: column; justify-content: flex-end;
    padding: calc(var(--sh-bar) + 24px) 0 var(--sh-foot);
}
/* ── OPEN ──────────────────────────────────────────────────────────────────────
   As soon as anything is expanded, the slogan steps aside and the menu rises to the
   top of the column — the panel opens into the screen instead of below the fold.
   The wordmark closes everything and gives the opening screen back. */
.sh-open .sh-slogan { display: none; }
.sh-open .sh-first {
    min-height: 0;
    padding: calc(var(--sh-bar) + 26px) 0 var(--sh-foot);
}

/* NO veil behind this column — the glass-ribbon scene reads through it. Contrast
   is carried by WEIGHT instead: menu entries medium, open entry semibold ink,
   prose regular muted. */
/* The <br>s set the lines — no max-width, or the measure would add a wrap of its own.
   TRACKING IS NOT TIGHTENED: the house token for display type is --tracking-display,
   which is 0em. Big type here is set at the same tracking as the live site's .site-h1
   and .tier-amount, not squeezed. */
.sh-slogan {
    /* the menu sits close under the last line — one block, not two */
    margin: 0 0 0.5em;
    font-size: clamp(26px, 2.9vw, 40px); font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-display); line-height: 1.16; color: var(--ink);
}

/* THE STAGGER is driven from sample-home.js with the Web Animations API, not from
   here — a line that must not get stuck invisible needs an animation object that
   can be finished on demand, which a CSS transition cannot give. Only opacity and
   a 7px lift move, and every line reserves its space the instant its panel opens,
   so the scroll target is final and nothing reflows mid-reveal. */

/* ── THE PANEL — what an entry expands to show ────────────────────────────────
   Indented to the depth of the sub-entries it sits among, so an entry's content and
   its children read as the same branch. No box and no fill: the indent and the rail
   carry the nesting, exactly as they do for the entries themselves. */
.sh-panel { padding: 0 0 0 18px; margin: 10px 0 6px; position: relative; }
.sh-panel[hidden] { display: none; }
/* A PANEL ON ITS WAY OUT IS INVISIBLE BUT STILL THERE. The collapse animates opacity
   to 0 with fill:forwards and only sets [hidden] when it finishes, so for those few
   hundred milliseconds the panel still occupies layout AND still answers to the
   pointer — a click aimed at the entry below it could land on nothing at all. It stops
   taking clicks the moment it starts leaving. */
.sh-panel.is-leaving { pointer-events: none; }
/* the rail continues down the side of the content, so a long panel — prose, child
   entries and a mounted widget together — stays tied to the entry that opened it */
.sh-panel::before {
    content: ''; position: absolute; left: 3px; top: 0; bottom: 6px;
    border-left: 1px solid var(--divider-strong);
}
/* A CHILD LIST INSIDE A PANEL is already indented by the panel, and the panel already
   draws the upright — so the list adds neither. It keeps the one thing that matters:
   its tick, which is what separates "these are entries" from "this is prose".
   ────────────────────────────────────────────────────────────────────────────────
   THE INDENT IS THE BRANCH, LENGTHENED — not the branch moved. Pushing the whole list
   right would carry the upright with it and start a second trunk 18px in from the
   first, which reads as a new tree rather than a deeper one. Instead the trunk stays
   exactly where the panel drew it and the HORIZONTAL run gets longer, with the entry's
   own padding making room for it. The rail stays put; the reach grows. */
.sh-panel > .sh-sub { padding-left: 0; margin: 0 0 2px; }
.sh-panel > .sh-sub::before { display: none; }
.sh-panel > .sh-sub > li { padding-left: 18px; }
.sh-panel > .sh-sub > li::before { left: -15px; width: 27px; }

/* prose inside a panel — muted, held to one comfortable measure */
.sh-out {
    margin: 0 0 var(--sh-stack); max-width: 52ch;
    color: var(--muted); font-weight: var(--fw-regular); line-height: 1.55;
}
.sh-out p { margin: 0 0 1.05em; }
.sh-out p:last-child { margin-bottom: 0; }
.sh-out .sh-strong { color: var(--ink); font-weight: var(--fw-semibold); }

/* ── THE MARK ─────────────────────────────────────────────────────────────────
   設計 and 會計 drawn as one character — the concept of the whole page in a single
   glyph, so it closes About the way a chop closes a document rather than illustrating
   anything above it. Sized by HEIGHT: it is a piece of writing, and writing is set by
   how tall the character is, not how wide the column happens to be.

   The PNG is black ink on transparency, so on the dark theme it would be black on
   black — inverted there, which is exactly right for a monochrome brush mark and
   leaves the alpha untouched. */
.sh-chop { margin: 2.4em 0 0; padding: 0; }
.sh-chop img {
    display: block; width: auto; height: clamp(112px, 15vh, 156px);
    opacity: 0.88;
}
html.demo-dark .sh-chop img { filter: invert(1); opacity: 0.82; }
.sh-chop figcaption {
    margin-top: 0.9em;
    font-size: 11px; font-weight: var(--fw-medium);
    letter-spacing: var(--tracking-wide);
    color: var(--muted); opacity: 0.7;
}
/* LINKS TAKE THE COLOUR OF THE PARAGRAPH THEY SIT IN. They used to be the studio's
   document accent (#00A2FF, the blue the PDF engine uses for *accent* runs), which put
   the loudest colour on the page inside the quietest text — a link three words into a
   muted paragraph pulled the eye before the sentence did. The underline is doing that
   job already, and it is the mark everybody reads as "this does something". Half-strength
   at rest so it stays a texture, full strength on hover so the state change still lands. */
/* currentColor, not a mixed value: one declaration, nothing computed to go stale
   across a theme flip, and the underline dims and lifts WITH the word instead of
   needing a second colour of its own — the same reasoning as the menu entries. */
.sh-out a {
    color: inherit; text-decoration: underline; text-decoration-color: currentColor;
    text-underline-offset: 4px; text-decoration-thickness: 1.5px;
    transition: color 0.14s ease;
}
.sh-out a:hover, .sh-out a:focus-visible { color: var(--ink); outline: none; }

/* ── THE MENU ──────────────────────────────────────────────────────────────────
   AFFORDANCE FIRST. A quiet grey word is indistinguishable from prose, so the
   clickable text carries a standing underline: the one mark everybody already
   reads as "this does something". It sits a step below ink at rest and takes the
   full step — ink, solid rule, and a marker that slides out of the word — on
   hover, so the state change is unmistakable without adding a button. */
.sh-ls { list-style: none; margin: 0; padding: 0; }
.sh-ls li { margin: 0; line-height: 1.42; }

/* ── THE TREE ──────────────────────────────────────────────────────────────────
   Rails are BORDERS, not ├─ box-drawing glyphs: those are metrically monospace and
   would drift a pixel a row down a proportional column. Drawn this way they are
   exact at any size and flip with the theme for free. */
.sh-sub { list-style: none; margin: 2px 0 2px; padding: 0 0 0 18px; position: relative; }
/* the upright — stops level with the last child's tick, giving the └ turn */
.sh-sub::before {
    content: ''; position: absolute; left: 3px; top: 0.05em; bottom: 0.70em;
    border-left: 1px solid var(--divider-strong);
}
.sh-sub > li { position: relative; }
/* the tick out to each child */
.sh-sub > li::before {
    content: ''; position: absolute; left: -15px; top: 0.72em;
    width: 9px; border-top: 1px solid var(--divider-strong);
}

/* WHAT IS OPEN. Full ink and semibold — the entry is acting as the heading of the
   panel below it, so it reads as one, and the underline comes off because a second
   click closes rather than goes anywhere new. */
.sh-ls li > button[aria-expanded="true"] { opacity: 1; font-weight: var(--fw-semibold); text-decoration: none; }
/* the marker turns down when the entry is open, and points along when it is not */
.sh-ls li > button[aria-expanded="true"]::after { content: '↓'; opacity: 0.45; transform: none; }
/* ink at reduced opacity rather than a mixed colour: one declaration, no
   computed-value caching to go stale, and it tracks --ink through the theme
   toggle for free. The underline inherits currentColor, so it dims and lifts
   with the word instead of needing a second colour of its own. */
/* SCOPED TO ENTRIES, NOT DESCENDANTS. A menu entry's button is always the direct
   child of its <li>. Everything else under .sh-ls is now widget territory — panels
   hold the real components, and a descendant selector here would repaint their
   controls: the waitlist chips, the Request-beta submit, every button inside intake,
   quoting and the ledger. */
.sh-ls > li > button, .sh-sub > li > button {
    position: relative;
    display: inline-block; padding: 0; border: 0; background: transparent;
    font: inherit; line-height: inherit; letter-spacing: inherit;
    font-weight: var(--fw-medium);
    color: var(--ink); opacity: 0.72;
    cursor: pointer; text-align: left;
    text-decoration: underline;
    text-underline-offset: 6px; text-decoration-thickness: 1.5px;
    transition: opacity 0.14s ease;
}
.sh-ls > li > button:hover, .sh-ls > li > button:focus-visible,
.sh-sub > li > button:hover, .sh-sub > li > button:focus-visible { opacity: 1; outline: none; }
/* the marker never takes part in layout — absolutely placed past the word's end, so
   nothing shifts as it appears */
.sh-ls > li > button::after, .sh-sub > li > button::after {
    content: '→'; position: absolute; left: 100%; top: 0;
    margin-left: 0.34em; text-decoration: none;
    opacity: 0; transform: translateX(-5px);
    transition: opacity 0.16s ease, transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
    pointer-events: none;
}
.sh-ls > li > button:hover::after, .sh-ls > li > button:focus-visible::after,
.sh-sub > li > button:hover::after, .sh-sub > li > button:focus-visible::after {
    opacity: 0.55; transform: translateX(0);
}

/* ── THE LEGAL RAIL ────────────────────────────────────────────────────────────
   Set on its side up the right edge, where it costs the column nothing. writing-mode
   turns the whole box, so the inline axis runs top-to-bottom: the three items sit in
   source order down the edge, `text-align: end` drops the group to the floor, and
   margin-bottom is what spaces them. */
.sh-rail {
    position: fixed; top: 0; right: 0; height: 100vh; height: 100svh; z-index: 2;
    box-sizing: border-box;
    writing-mode: vertical-rl;
    /* the rail sits closer to the floor than the menu does. --sh-foot is the MENU's
       breathing room off the bottom (up to 68px), and borrowing it here left the credit
       ending well short of the corner with a conspicuous empty run beneath it — the rail
       is a margin note, so it wants the page edge, not the menu's stage. */
    padding: 0 14px 20px 0;
    text-align: end;
    font-family: var(--font-sans); font-size: 11px; font-weight: var(--fw-medium);
    letter-spacing: 0.055em; line-height: 1;
    color: var(--muted);
    pointer-events: none;                       /* the empty strip stays click-through */
}
.sh-rail > * { pointer-events: auto; }
/* the links sit at exactly the credit's weight — the rail reads as one quiet line,
   and the underline alone says which parts are clickable */
.sh-rail a, .sh-rail-credit { opacity: 0.62; }
.sh-rail a {
    color: inherit; text-decoration: underline;
    text-underline-offset: 3px; text-decoration-thickness: 1px;
    margin-bottom: 2.6em;                       /* inline axis is vertical — this is the gap */
    cursor: pointer; transition: opacity 0.14s ease;
}
.sh-rail a:hover, .sh-rail a:focus-visible { opacity: 1; outline: none; }

/* ── THE LEGAL MODAL — the site's own overlay, holding the real page content ─── */
.sh-legal .site-modal {
    max-width: 720px; max-height: min(82vh, 900px);
    display: flex; flex-direction: column;
    padding: var(--space-8) 34px var(--space-6);
}
.sh-legal-body { overflow-y: auto; text-align: left; }
.sh-legal-body .legal-hero, .sh-legal-body .site-hero { padding: 0; margin: 0 0 var(--space-6); text-align: left; }
/* the pages set their headline at display scale for a full page; in a dialog it wants
   to sit one step down */
.sh-legal-body .site-h1 { font-size: clamp(26px, 3vw, 34px); margin-bottom: var(--space-4); }
.sh-legal-body .site-lead { font-size: var(--text-base); color: var(--muted); }
.sh-legal-body .site-stack { display: grid; row-gap: var(--space-5); }
.sh-legal-wait { color: var(--muted); }

/* ── THE FLUID CARD — the app's own holographic AI surface ────────────────────
   .quick-add-panel + .qa-fluid are the production classes; js/glass/card-fluid.js
   paints the three.js shader over the CSS gradient. Only the chrome that belongs to
   the Quick-Add hub is neutralised here. */
/* parked at the card's FINAL size, so the shader is rendered at the right resolution
   from the first frame rather than waiting on a resize */
.sh-fluid-park { position: fixed; left: -99999px; top: 0; width: 408px; height: 600px; }
/* NO PADDING HERE — .board-header and .qa-card-inner carry the app's own, exactly as
   they do in views/overview.php. A portrait card: narrow enough to read as an object
   in the column rather than a panel filling it.

   600px is PHASE 3'S HEIGHT, not a round number. The card is one box that three beats
   take turns in, so the box has to be the tallest of them — the fullest phase 3 runs
   579px — or the card would grow as the read finishes and snap back as it starts over,
   which turns the loop into a bounce. */
.sh-fluid {
    width: 100%; max-width: 408px; min-height: 600px;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-3);
    display: flex; flex-direction: column;
}
/* everything rides above the fluid, which is absolutely placed at z-index 0 */
.sh-fluid > .board-header, .sh-fluid > .qa-card-inner { position: relative; z-index: 1; }
.sh-fluid .qa-header { flex-shrink: 0; }
/* the header's right side says which card this is, in the board-title's own register */
.sh-qa-tag {
    font-size: 11px; font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    color: rgba(27, 27, 34, 0.5);
}

/* ── THE SHOWCASE LOOP — invite → reading → read, then round again ──────────────
   The card plays the flow instead of describing it. Each beat occupies the same box,
   pushed to the bottom the way the real CTA sits, so nothing jumps between them. */
.sh-fluid .qa-card-inner { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; }
.sh-qa-step[hidden] { display: none; }
.sh-qa-step { width: 100%; text-align: left; }
/* the reading beat borrows the CTA's own type, so only the words change */
.sh-fluid div.qa-cta { cursor: default; }
.sh-qa-bar {
    display: block; margin-top: 18px; height: 2px; border-radius: 2px;
    background: rgba(17, 19, 28, 0.12); overflow: hidden;
}
.sh-qa-bar > span {
    display: block; height: 100%; width: 100%; background: var(--ink);
    transform-origin: left center; animation: shQaFill 1s linear both;
}
@keyframes shQaFill { from { transform: scaleX(0); } to { transform: scaleX(1); } }

.sh-qa-done .ai-sec:first-child { margin: 0; }
.sh-qa-done .ai-sec { margin-top: 16px; }
.sh-qa-done .ai-row { justify-content: space-between; gap: 10px; }
/* the second caption in a section head — a quiet aside, not a status pill */
.sh-qa-note {
    margin-left: auto;
    font-size: 10px; font-weight: var(--fw-medium);
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    color: var(--text-muted); opacity: 0.8;
}
/* Dr / Cr — the two sides, as a fixed-width gutter so the pair reads as a pair.
   A step darker than --text-muted: at 10.5px on the row's own fill the muted token
   measured 4.4:1, just under AA, and these two are the labels that carry the meaning
   of the block. */
.sh-dc {
    flex: 0 0 20px;
    font-size: 10.5px; font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    color: #5B5B63;
}
/* the account the AI would file an unmatched line under — a suggestion, mid-row */
.sh-qa-sug {
    flex: 0 0 auto;
    font-size: 10.5px; font-weight: var(--fw-medium);
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    color: var(--text-muted);
}
/* THE OPEN QUESTION. The one thing the model is not sure about, handed back rather
   than guessed — which is the whole argument for the confirm step underneath it. */
.sh-qa-ask {
    display: flex; align-items: flex-start; gap: 7px;
    margin: 14px 0 0; padding: 9px 11px;
    border-radius: var(--radius-md);
    background: rgba(164, 97, 10, 0.08);
    font-size: 11.5px; line-height: 1.45; color: #8a5209;
}
.sh-qa-ask .material-icons { flex: 0 0 auto; font-size: 15px; margin-top: 0.5px; }
/* the draft's own total — ruled off from the lines it foots */
.sh-pr-sum { margin-top: 2px; border-top: 1px solid var(--divider); }
.sh-pr-sum .ai-row-desc { font-weight: var(--fw-semibold); }
.sh-qa-pr .sh-pr-sum .ai-row-amt { font-size: 13.5px; }

/* ── THE CROSS-CHECK ──────────────────────────────────────────────────────────
   Each statement line arrives UNRESOLVED — a quiet ellipsis where the verdict will
   be — and settles a beat later into matched or not. Showing the resolve is the
   whole feature; a table that arrives already ticked is just a table. */
.sh-qa-xc .ai-row { font-size: 12.5px; }
.sh-qa-xc .ai-row-date { flex: 0 0 auto; color: var(--text-muted); }
.sh-qa-xc .ai-row-desc { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sh-xc-mark {
    flex: 0 0 16px; text-align: right;
    font-size: 12px; font-weight: var(--fw-bold); line-height: 1;
    color: var(--text-muted);
    transition: color 0.2s ease;
}
.sh-xc-mark::after { content: '\00B7\00B7\00B7'; }
.sh-xc.is-checked[data-verdict="good"] .sh-xc-mark { color: #067a44; }
.sh-xc.is-checked[data-verdict="good"] .sh-xc-mark::after { content: '\2713'; }
.sh-xc.is-checked[data-verdict="warn"] .sh-xc-mark { color: #a4610a; }
.sh-xc.is-checked[data-verdict="warn"] .sh-xc-mark::after { content: '\0021'; }
/* the unmatched line is the one you are meant to notice */
.sh-xc.is-checked[data-verdict="warn"] .ai-row-desc,
.sh-xc.is-checked[data-verdict="warn"] .ai-row-amt { color: #a4610a; }
.sh-xc-tally[hidden] { display: none; }

/* ── THE PRICE RESOLVE (Smart quoting) ────────────────────────────────────────
   Same mechanism as the cross-check, opposite emphasis. The LINE lands first with
   no number; the price then arrives together with the past quote it was taken from.
   That order is the product rule made visible: the model writes the words, never
   the number. */
.sh-qa-pr .ai-row { gap: 10px; align-items: baseline; }
.sh-qa-pr .ai-row-desc { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; }
.sh-pr-src, .sh-qa-pr .ai-row-amt {
    opacity: 0; transform: translateY(3px);
    transition: opacity 0.26s ease, transform 0.26s cubic-bezier(0.16, 1, 0.3, 1);
}
.sh-pr.is-checked .sh-pr-src, .sh-pr.is-checked .ai-row-amt { opacity: 1; transform: none; }
.sh-pr-src {
    flex: 0 0 auto; max-width: 40%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 10.5px; font-weight: var(--fw-medium);
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    color: var(--text-muted);
}
/* the line with no comparable is the one you have to price yourself */
.sh-pr[data-verdict="warn"].is-checked .ai-row-amt,
.sh-pr[data-verdict="warn"].is-checked .sh-pr-src { color: #a4610a; }
.sh-pr[data-verdict="warn"] .ai-row-amt { font-weight: var(--fw-medium); }

/* ── DOCUMENTS — a deck of A4 sheets, flipping ────────────────────────────────
   EVERY SHEET IS FICTIONAL. No studio identity, no contact block, no signature and
   no chop is rendered anywhere on this page. */
/* a small object in the column, not a page to read — the sheet is drawn at a true
   210mm and scaled down bodily, type and all */
/* 302 − 2×16 padding − 2×1 border = the same 268px stack the A4 fit is measured
   against, so taking the panel changed the surface and nothing else */
.sh-docwrap { max-width: 302px; padding: 16px; }
/* A4, and nothing else. The box is 210 : 297 and js/home.js scales each sheet to fit
   inside it — never the other way round, and it writes no height, so this ratio is
   the only thing that decides the shape. Every sheet in DOC_SHEETS is written to fit
   one page; the height term in that fit is the backstop if one ever stops fitting. */
.sh-docstack { position: relative; aspect-ratio: 210 / 297; }
.sh-doc {
    position: absolute; inset: 0;
    overflow: hidden;
    background: #fff;
    border-radius: 3px;
    box-shadow: var(--shadow-3);
    transform-origin: 50% 100%;
    transition: transform 0.46s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.46s ease;
}
.sh-doc-scale { transform-origin: top left; width: 210mm; }
/* the deck: the front sheet square on, the ones behind stepped back and dimmed */
.sh-doc[data-pos="0"] { transform: none; opacity: 1; }
.sh-doc[data-pos="1"] { transform: translateY(-11px) scale(0.965); opacity: 0.62; }
.sh-doc[data-pos="2"] { transform: translateY(-21px) scale(0.93); opacity: 0.3; }
.sh-doc[data-pos="3"], .sh-doc[data-pos="4"] { transform: translateY(-28px) scale(0.91); opacity: 0; }
/* and the front sheet lifts away to reveal the next */
.sh-doc.is-leaving { transform: translateY(-46px) rotate(-2.5deg); opacity: 0; }

.sh-doc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.sh-doc-mark { font-size: 11px; font-weight: var(--fw-bold); letter-spacing: 0.16em; }
.sh-doc-kind { font-size: 11px; font-weight: var(--fw-medium); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: #71717A; }
.sh-doc-sub { margin: 4px 0 20px; font-size: 10.5px; color: #71717A; }
.sh-doc-rows { flex: 1; display: flex; flex-direction: column; gap: 0; }
.sh-doc-row {
    display: flex; justify-content: space-between; gap: 12px;
    padding: 7px 0; font-size: 11.5px;
    border-bottom: 1px solid #EFEFF1;
}
.sh-doc-row span:last-child { font-weight: var(--fw-semibold); white-space: nowrap; }
/* the accountant's rule under a total */
.sh-doc-row.is-rule { border-bottom: 0; font-weight: var(--fw-semibold); }
.sh-doc-row.is-rule span:last-child { border-bottom: 3px double #18181B; padding-bottom: 2px; }
.sh-doc-foot { margin-top: 14px; font-size: 9px; letter-spacing: 0.04em; color: #A1A1AA; }

.sh-doc-cap { display: flex; align-items: baseline; gap: 10px; margin: 18px 0 0; flex-wrap: wrap; }
.sh-doc-now { font-size: var(--text-base); font-weight: var(--fw-semibold); color: var(--ink); }
.sh-doc-of { font-size: var(--text-sm); color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
    .sh-doc { transition: none; }
}
.sh-qa-confirm {
    display: flex; align-items: center; justify-content: space-between; gap: 14px;
    margin-top: 18px; flex-wrap: wrap;
}
.sh-qa-hold { font-size: var(--text-sm); color: var(--text-muted); }
/* a span, not a button — this is a picture of the human pass, not the pass itself */
.sh-qa-btn { cursor: default; pointer-events: none; }

/* the fade between the last beat and the first */
.sh-qa-out { opacity: 0; transition: opacity 0.4s ease; }
.sh-qa-inner { transition: opacity 0.28s ease; }
@media (prefers-reduced-motion: reduce) {
    .sh-qa-bar > span { animation: none; }
    .sh-qa-out, .sh-qa-inner { transition: none; opacity: 1; }
}

/* ── THE RETRO READOUT SWAP — the app's own, borrowed ─────────────────────────
   qaRetroSwap is declared in css/app.css (2865) and is what the signed-in card does
   when the top-right slider flips: the copy dims out, glitch-flickers, then resolves.
   The homepage card has no slider, but it has the same event — a beat arriving in a
   box that was showing something else — so it gets the same readout.

   TWO PLACES ONLY. On the card's FIRST appearance in a panel (the widget resolving
   into existence, which is what the reader is watching for), and on invite → reading,
   which is the moment the machine takes over. reading → read is NOT flashed: that beat
   fills itself in row by row, and a flicker on top would fight the fill. */
.sh-fluid.qa-retro .qa-card-inner,
.sh-fluid.qa-retro .board-title { animation: qaRetroSwap 0.52s cubic-bezier(0.4, 0, 0.2, 1) both; }
.sh-fluid.qa-retro .board-title { animation-delay: 0.05s; }   /* the title trails the body */
@media (prefers-reduced-motion: reduce) {
    .sh-fluid.qa-retro .qa-card-inner,
    .sh-fluid.qa-retro .board-title { animation: none; }
}
/* THE FLUID IS A LIGHT SURFACE IN BOTH THEMES — the live app's card is too, which is
   why card-fluid.js floors the palette to a bright silver base ("no dark holes"). So
   inside it the widget keeps LIGHT-theme tokens even while the page is dark, or its
   white ink would sit on a white-ish gradient and vanish. Both families are re-declared
   because the landing (css/auth.css) uses --ink/--muted and the app widgets
   (css/app.css) use --text-main/--text-muted. */
html.demo-dark .sh-fluid {
    --ink: #18181B; --muted: #71717A;
    --text-main: #18181B; --text-muted: #71717A;
    --card: #FFFFFF; --paper: #F4F4F5;
    --bg-card: #FFFFFF; --bg-input: #F4F4F5; --bg-hover: #E4E4E7; --bg-main: #F4F4F5;
    --bg-glass: rgba(255, 255, 255, 0.85); --bg-glass-panel: rgba(255, 255, 255, 0.94);
    --divider: #E4E4E7; --divider-strong: #D4D4D8;
    --shadow-tint: 17, 19, 28;
    /* THE SECOND HALF OF THE FLIP, and the half that was missing. The list above
       covers surfaces and ink; these are the ACTION tokens, and leaving them on their
       dark values put white-on-white inside a light card — "Confirm & post" measured
       1.1:1, i.e. invisible. Taken from html[data-theme='light'] verbatim: every token
       that differs between the two blocks and is reachable from inside this card is
       now here, so the card is a complete light context rather than a mostly-light one. */
    --theme-color: #000000; --theme-color-hover: #333333; --theme-text: #000000;
    --border-ink: #1F1F23; --active-theme-bg: rgba(0, 0, 0, 0.05);
    --danger-red: #ff0e16;
    --neon-green-glow: rgba(1, 235, 114, 0.22); --neon-red-glow: rgba(255, 14, 22, 0.22);
    color: #18181B;
}

/* ── THE STATUS PILL, ON A BRIGHT SURFACE ─────────────────────────────────────
   NOT a dark-mode bug — this one was wrong in both themes. .pj-status--good paints
   its text in --neon-green (#01eb72), which is drawn to sit on the app's near-black
   card. The fluid is a bright silver field in EVERY theme, so the pill was neon green
   on white at 1.6:1 all along.

   The house already has the answer: --quote-ink and the #b8860b of --invoicing are
   both "readable text tone next to a neon dot". So the dot keeps the neon and the
   text takes the ink green this page already uses for a matched cross-check line. */
.sh-fluid .pj-status--good { color: #067a44; }
.sh-fluid .pj-status--risk { color: #b3121a; }

/* ── THE READABILITY WASH (dark only) ─────────────────────────────────────────
   The card is identical in both themes — it is a canvas, and CSS cannot touch it. What
   changes is the EYE: against a black page the surround adaptation makes the same
   bright holographic field read as glare, and the mid-tone captions on it lose apparent
   contrast even though the measured ratio has not moved.

   So the correction belongs on the background, not the text. A thin white veil lifts
   the base, and saturate() on the backdrop calms the blooms that were competing with
   the type. It sits BETWEEN the shader and the content — .qa-fluid is z-index 0 and the
   content is z-index 1, and a ::after paints after every child at the same level — so
   the ink above it is untouched and only the field behind it softens. */
html.demo-dark .sh-fluid::after {
    content: ''; position: absolute; inset: 0; z-index: 0;
    border-radius: inherit; pointer-events: none;
    background: rgba(255, 255, 255, 0.28);
    -webkit-backdrop-filter: saturate(0.85);
    backdrop-filter: saturate(0.85);
}

/* the trust tile carries its own frosted surface and padding; it only needs a width
   in this column, and the .ta block's margin-top:auto has no flex parent to push
   against here */
.sh-trust { max-width: 520px; }
.sh-trust .ta { margin-top: var(--space-6); }

/* ── THE TRIAL BALANCE, BEING POSTED ──────────────────────────────────────────
   The line naming the entry currently on the books, above the columns it is landing
   in. It swaps with the app's own readout flicker so the change registers as an EVENT
   rather than as text quietly becoming different text. */
.sh-ta-post {
    display: flex; align-items: baseline; gap: 9px;
    margin: 0 0 10px; min-height: 17px;
}
.sh-ta-cap {
    flex: 0 0 auto;
    font-size: 10px; font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    color: var(--muted); opacity: 0.75;
}
.sh-ta-name { font-size: 12.5px; font-weight: var(--fw-medium); color: var(--ink); }
.sh-ta-name.is-swap { animation: qaRetroSwap 0.52s cubic-bezier(0.4, 0, 0.2, 1) both; }

/* THE TWO LEGS. Both sides of the entry light at once — which is the point being
   made, so they are the only thing on the tile that is ever highlighted. */
.sh-ta .ta-row { transition: background-color 0.3s ease; border-radius: 4px; margin: 0 -6px; padding-left: 6px; padding-right: 6px; }
.sh-ta .ta-row.is-post { background: rgba(0, 162, 255, 0.10); }
.sh-ta .ta-row span:nth-child(n+2) { transition: color 0.25s ease; }
.sh-ta .ta-row.is-post span:nth-child(n+2) { color: #0076bd; }
html.demo-dark .sh-ta .ta-row.is-post { background: rgba(0, 162, 255, 0.16); }
html.demo-dark .sh-ta .ta-row.is-post span:nth-child(n+2) { color: #63c8ff; }

/* the accountant's double rule belongs under a FIGURE — an empty cell must not carry
   a floating rule while the balance is still being built */
.sh-ta .ta-total span:empty { border-bottom-color: transparent; }
.sh-ta .ta-total span:nth-child(n+2) { transition: border-color 0.3s ease; }

/* the pill ticks after every posting, because it is true after every posting */
.sh-ta-badge.is-tick { animation: shTaTick 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; }
@keyframes shTaTick {
    0%   { transform: scale(1); }
    30%  { transform: scale(1.07); }
    100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
    .sh-ta-name.is-swap, .sh-ta-badge.is-tick { animation: none; }
    .sh-ta .ta-row, .sh-ta .ta-row span { transition: none; }
}

/* ══════════════════════════ THE WIDGET PANEL — one recipe, shared ═══════════
   Compare plans proved it: a widget with no surface is a set of figures floating on
   the artwork, and on the dark theme that is barely an object at all. The components
   that DID carry a surface were carrying their own and disagreeing — .trust-tile and
   .tier both dropped to rgba(255,255,255,0.05) on dark, a lift so faint the widget
   dissolved into the page while the compare table beside it read as a solid card.

   So the values live HERE, once, and every widget on this page takes the same ones:
   the app's own --bg-glass-panel over a blur, which is the same object as every
   .board in the product. Each widget keeps its own padding and width — only the
   SURFACE is shared.

   NOT the AI cards. They carry the three.js shader, which IS their surface; a panel
   behind a panel would just be a frame around it.

   NO SOLID FILL. --bg-glass-panel is 94% white (92% ink on dark), which is a painted
   block, not glass — it sealed the artwork off behind every widget and the page read
   as cards laid ON the scene rather than cut FROM it. The surface is now the blur
   alone: backdrop-filter frosts whatever is behind, so the ribbon still moves through
   the panel and the type still has a settled ground to sit on. The border and shadow
   are what make it an object; they do the work the fill was doing.

   The frost is tuned DOWN from the old saturate(1.4), not up. With a fill behind it,
   saturation was decoration on an opaque surface; with nothing behind it, the ribbon's
   magenta bloom lands directly under the paragraph and eats the muted grey. Real
   frosted glass desaturates and lifts what it covers, so that is what this does —
   a wider blur, saturation pulled below 1, a touch of brightness. No fill returns. */
.sh-cmp, .sh-docwrap, .sh-trust, .sh-plans, .sh-beta, .sh-rm {
    background: transparent;
    -webkit-backdrop-filter: blur(30px) saturate(0.72) brightness(1.1);
    backdrop-filter: blur(30px) saturate(0.72) brightness(1.1);
    border: 1px solid var(--divider);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
}
/* css/auth.css sets these two at html.demo-dark specificity (0,2,1), which the list
   above cannot reach — restated here rather than weakened there, because both are
   live components and the landing still uses them at their own values */
html.demo-dark .sh-trust,
html.demo-dark .sh-plans {
    background: transparent;
    border-color: var(--divider);
}
/* Dark flips which way the frost has to push. The ribbon is the BRIGHT object on this
   theme, so the brightness(1.1) that lifts a light page into legibility drives the
   bloom straight up into the muted grey body text instead. Same blur, same
   desaturation, opposite exposure. */
html.demo-dark .sh-cmp,
html.demo-dark .sh-docwrap,
html.demo-dark .sh-trust,
html.demo-dark .sh-plans,
html.demo-dark .sh-beta,
html.demo-dark .sh-rm {
    -webkit-backdrop-filter: blur(30px) saturate(0.72) brightness(0.42);
    backdrop-filter: blur(30px) saturate(0.72) brightness(0.42);
}
/* the plan cards give up their own surface — inside the panel it would be a card on
   a card, and the flip would read as two objects moving instead of one */
.sh-plans .tier,
html.demo-dark .sh-plans .tier {
    background: transparent; border-color: transparent; box-shadow: none;
}

/* ── MOUNTED WIDGETS — the real components, standing inside their panel ───── */
/* widgets fill the column rather than setting their own width — the column is the
   measure now, and a widget wider than it would break the left edge everything shares */
.sh-wmount { margin: 0 0 var(--sh-stack); max-width: 100%; font-size: var(--text-base); font-weight: var(--fw-regular); line-height: var(--leading-normal); letter-spacing: normal; }
/* the app widgets sit on their own card surface so their recessed fields read */
.sh-wmount--card { background: var(--card); border-radius: var(--radius-lg); padding: 8px; box-shadow: var(--shadow-3); }
/* the A4 sheet scales into the column; the sheet itself stays white — it is paper */
.sh-paper { overflow: hidden; border-radius: var(--radius-doc); box-shadow: var(--shadow-3); }
.sh-paper-scale { transform-origin: top left; }
/* ── COMPARE PLANS — the four tiers as a matrix, read live out of pricing.php.
   Hairline rows, no fills except the popular column: a table in the column's own
   voice, not a pricing page lifted whole.

   IT IS THE ONE WIDGET THAT LEAVES THE COLUMN. A comparison is a wide object — five
   columns at the column's own measure would set the plan headings in 60px and defeat
   the purpose of building it. So it keeps the left edge everything else shares and
   extends right, stopping well clear of the legal rail. */
.sh-cmp {
    width: max(100%, min(820px, 74vw));
    overflow-x: auto;                    /* below its min-width the matrix scrolls, not the page */
    padding: 20px 22px 18px;
}
.sh-cmp-grid { min-width: 560px; }
.sh-cmp-r {
    display: grid;
    grid-template-columns: minmax(148px, 1.5fr) repeat(var(--cmp-n, 4), minmax(78px, 1fr));
    column-gap: 12px; align-items: center;
    padding: 11px 0;
    border-top: 1px solid var(--divider);
    font-size: 0.8em; line-height: 1.4;
}
.sh-cmp-r:last-child { border-bottom: 1px solid var(--divider); }
.sh-cmp-lab { color: var(--muted); }

/* THE HEAD — the plan, its price and who it is for, stacked. Bottom-aligned so the
   names sit on one line however many lines a tag runs to. */
.sh-cmp-r--head { border-top: 0; align-items: end; padding: 0 0 14px; }
.sh-cmp-h { position: relative; }
.sh-cmp-c { text-align: center; }
.sh-cmp-name {
    display: block;
    font-size: 11px; font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ink);
}
.sh-cmp-amt {
    display: block; margin-top: 6px;
    font-size: 21px; font-weight: var(--fw-bold); line-height: 1;
    letter-spacing: var(--tracking-display); color: var(--ink);
    font-variant-numeric: tabular-nums;
}
.sh-cmp-per { display: block; margin-top: 3px; font-size: 10.5px; color: var(--muted); }
.sh-cmp-tag { display: block; margin-top: 8px; font-size: 10px; line-height: 1.35; color: var(--muted); }
/* NO RECOMMENDED COLUMN. The cap and the tint that used to single Studio out are
   gone: the matrix now states the four plans and lets the figures do the arguing. */

/* the caption that says what the rows below it are */
.sh-cmp-sec {
    padding: 20px 0 6px;
    font-size: 10px; font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    color: var(--muted); opacity: 0.75;
}
/* ✓ / – : the staircase. Included is ink, not a colour — four tinted columns of green
   would read as decoration and the shape is the message. */
.sh-cmp-c.is-yes { color: var(--ink); font-weight: var(--fw-bold); }
.sh-cmp-c.is-no { color: var(--ink); opacity: 0.22; }
.sh-cmp-foot { margin: 14px 0 0; font-size: 0.74em; color: var(--muted); }

/* ── PRICING — the four real .tier cards, dealt one at a time ────────────────
   Absolutely stacked so they occupy one box and cross-fade in place; the box takes
   the height of the tallest, set by js/home.js, so the column never jumps as the
   loop comes round to a plan with one more feature line. */
/* 436 − 2×18 = the same 400px card. The top padding also clears the featured badge,
   which straddles the card's own top edge at -11px. */
.sh-plans { max-width: 436px; padding: 18px; }
.sh-planstack { position: relative; }
.sh-plan {
    position: absolute; top: 0; left: 0; width: 100%;
    opacity: 0; transform: translateY(10px);
    transition: opacity 0.42s ease, transform 0.42s cubic-bezier(0.16, 1, 0.3, 1);
    pointer-events: none;
}
.sh-plan.is-on { opacity: 1; transform: none; pointer-events: auto; }
/* the outgoing card leaves upward, so the two never look like a crossfade of the
   same object — one plan gives way to the next */
.sh-plan.is-out { opacity: 0; transform: translateY(-10px); }
/* THE EMOJI IS NOW A GOOGLE ICON. .tier-emoji's 34px and drop-shadow carry over —
   only the glyph source changes, from whatever the platform draws for 🌱 to the
   Material set the rest of the app already uses. */
.sh-tier-ic { display: block; font-size: 34px !important; line-height: 1; color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
    .sh-plan { transition: none; }
}
/* the waitlist component, out of its overlay: it is a panel in the column now, not a
   floating dialog, so the modal-scale shadow and the dialog entrance come off. The
   card surface, the field, the chips and the success state are untouched. */
.sh-beta { animation: none; max-width: 480px; }   /* surface comes from the shared panel */

/* ── THE BETA FORM ─────────────────────────────────────────────────────────────
   Built out to the reference's level of detail — name, email, what you do, volume,
   country, the CTA and the terms line. The FIELD IDIOM is the reference's too: a
   caption sitting on a ruled line rather than a boxed input, which is what lets six
   controls stack without the panel turning into a wall of chrome.

   The colours are not the reference's. The resting rule is --divider and the focused
   one is --ink, so the form belongs to this design system rather than borrowing a
   blue that appears nowhere else on the page. */
.sh-beta-form { display: grid; row-gap: 2px; margin-top: var(--space-5); }
.sh-fld {
    position: relative; display: block; padding: 14px 0 10px;
    border-bottom: 1px solid var(--divider);
    transition: border-color 0.16s ease;
}
.sh-fld:focus-within { border-bottom-color: var(--ink); }
.sh-fld-cap {
    display: block; margin-bottom: 6px;
    font-size: 11px; font-weight: var(--fw-medium);
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    color: var(--muted);
}
.sh-fld input, .sh-fld select {
    width: 100%; margin: 0; padding: 0; border: 0; background: transparent;
    box-shadow: none; border-radius: 0;
    font-family: var(--font-sans); font-size: var(--text-base); font-weight: var(--fw-regular);
    color: var(--ink); outline: none;
}
.sh-fld input::placeholder { color: var(--muted); opacity: 0.75; }
/* the native select keeps its own arrow off — the caret below is drawn to the house */
.sh-fld select { -webkit-appearance: none; appearance: none; padding-right: 22px; cursor: pointer; }
.sh-fld--select::after {
    content: ''; position: absolute; right: 4px; bottom: 18px;
    width: 7px; height: 7px; pointer-events: none;
    border-right: 1.6px solid var(--muted); border-bottom: 1.6px solid var(--muted);
    transform: rotate(45deg);
}
/* the submit is the full-width house CTA, set apart from the last rule */
.sh-beta-go { width: 100%; margin-top: var(--space-6); }
.sh-beta-fine {
    margin: 14px 0 0 !important;
    font-size: var(--text-sm); line-height: 1.5; color: var(--muted); text-align: center;
}
.sh-beta-fine a { color: inherit; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.sh-beta-fine a:hover { color: var(--ink); }
.sh-beta .wl-roles-label { margin-top: var(--space-6); }
/* A LIVE BUG, reproduced here and patched only for the sample. css/auth.css:395 sets
   the waitlist placeholder to rgba(24,24,27,.36) with no html.demo-dark override — so
   it is near-invisible on the dark card, in the real #waitlist-modal as well as here.
   .bar-ask (838) and .ob-input (1633) both have that override; this one was missed.
   Left for you to apply upstream: this sample changes no production file. */
html.demo-dark .sh-beta .wl-field input[type=email]::placeholder { color: rgba(255, 255, 255, 0.45); }
.sh-note { font-size: var(--text-sm); color: var(--muted); margin-top: 14px !important; }
/* same rule as prose links — the note's own colour, never the accent blue */
.sh-note a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
.sh-note a:hover { color: var(--ink); }
/* the Quick-Add card chrome smart-quote.js checks for before it renders. Inert: the
   module does classList.remove('hidden') on the overlay, and display:none here means
   that can never put anything on screen. */
.sh-sq-stub { display: none !important; }

/* SPA-shell chrome inside views/projects.php that has no driver on this page */
.sh-wmount .live-clock, .sh-wmount .seg-tabs,
.sh-wmount .view-controls .toggle-group, .sh-wmount .view-controls .date-nav { display: none; }
.sh-wmount .view-controls { margin-bottom: 0 !important; }

/* ── RESPONSIVE ──────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
    /* the sign-in field is the first thing to go — the CTA and the toggles carry
       the bar, and login.php is one tap away from the demo's own page */
    .sh-ask { display: none; }
}
@media (max-width: 1000px) {
    /* below this the screen IS the column — half of it would be a gutter, not a measure */
    :root { --sh-col: 100%; }
}
@media (max-width: 760px) {
    /* the slogan's own line breaks are short enough to hold at phone widths now,
       so nothing is overridden here */
    .sh-slogan { max-width: none; }
    /* narrower rows wrap, so phase 3 needs MORE height here, not less. No padding
       either — .board-header and .qa-card-inner carry the app's own, and a second
       helping on the card doubled it at exactly the width that could least afford it. */
    .sh-fluid { min-height: 640px; }

    /* the rail stands back up: a plain line at the foot of the page rather than a
       strip down an edge the column now needs */
    .sh-rail {
        position: static; height: auto; writing-mode: horizontal-tb;
        display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1.6em;
        padding: 2.4em var(--sh-gutter) var(--sh-foot);
        text-align: start;
    }
    .sh-rail a { margin-bottom: 0; }
    .sh-rail-credit { flex: 1 0 100%; margin-top: 1em; }
}

/* ── THE ROADMAP — the tree's own grammar, reused ─────────────────────────────
   No image and no new visual language. The rail is the one the panel already draws;
   only its LINE QUALITY changes, and that is the whole legend:

     live       solid, thick     what runs today
     building   solid, thin      being built now
     next       dashed           next
     direction  dotted, fading   direction, not promise

   border-style does the work a mask had to do in the artboard, which is why this
   version themes, reflows and translates while a picture of it could not. */
/* IT TAKES THE SHARED PANEL SURFACE. Built from the tree's grammar, yes — but the
   ribbon crosses this column, and a block this long laid straight on the artwork loses
   its middle stage to a bright bloom. Frosted ground, same as every other widget. */
.sh-rm { margin: 0; padding: var(--space-6) var(--space-6) var(--space-5); }

.sh-rm-stage { position: relative; padding: 0 0 var(--space-6) 18px; }
.sh-rm-stage:last-of-type { padding-bottom: var(--space-5); }
.sh-rm-stage::before {
    content: ''; position: absolute; left: 0; top: 0; bottom: 0;
}
.sh-rm-stage[data-stage="live"]::before      { border-left: 2.5px solid var(--ink); }
.sh-rm-stage[data-stage="building"]::before  { border-left: 1.5px solid var(--ink); }
.sh-rm-stage[data-stage="next"]::before      { border-left: 1.5px dashed var(--muted); }
/* the last rail gives out rather than stopping — a hard end would read as a deadline */
.sh-rm-stage[data-stage="direction"]::before {
    border-left: 1.5px dotted var(--muted);
    -webkit-mask-image: linear-gradient(to bottom, #000 46%, transparent 100%);
            mask-image: linear-gradient(to bottom, #000 46%, transparent 100%);
}

/* the stage name, in the small-caps voice the page uses for labels */
.sh-rm-lbl {
    margin: 0 0 10px; font-size: 11px; font-weight: var(--fw-semibold);
    letter-spacing: 0.17em; text-transform: uppercase; color: var(--ink);
}
/* THE ONE ACCENT ON THE PAGE, used once. Same ochre as the AI-confirm note. */
.sh-rm-stage[data-stage="live"] .sh-rm-lbl { color: #8A5209; }
.sh-rm-stage[data-stage="live"] .sh-rm-lbl::after {
    content: ''; display: block; width: 26px; height: 2px;
    margin-top: 5px; background: #8A5209;
}

.sh-rm-list { list-style: none; margin: 0; padding: 0; }
.sh-rm-list li {
    position: relative; padding: 0 0 0 18px; margin: 0 0 7px;
    font-size: var(--text-base); line-height: 1.5; color: var(--ink);
}
.sh-rm-list li:last-child { margin-bottom: 0; }
/* the node repeats the rail's line quality at item scale: filled, open, dashed, dotted */
.sh-rm-list li::before {
    content: ''; position: absolute; left: 0; top: 0.5em;
    width: 9px; height: 9px; border-radius: 50%; box-sizing: border-box;
}
.sh-rm-stage[data-stage="live"] li::before      { background: var(--ink); width: 7px; height: 7px; top: 0.6em; left: 1px; }
.sh-rm-stage[data-stage="building"] li::before  { border: 1.5px solid var(--ink); }
.sh-rm-stage[data-stage="next"] li::before      { border: 1.5px dashed var(--muted); }
.sh-rm-stage[data-stage="direction"] li::before { border: 1.5px dotted var(--muted); }
/* later stages are less certain, and the type says so without a second colour */
.sh-rm-stage[data-stage="next"] li,
.sh-rm-stage[data-stage="direction"] li { color: var(--muted); }

.sh-rm-foot {
    margin: var(--space-5) 0 0; max-width: 46ch;
    font-size: 12.5px; line-height: 1.5; color: var(--muted);
}
