/* ==========================================================================
   Maslow Secure Room — Phase 1 visual system
   Ported from docs/dataroom-handoff/source/Maslow Data Room.dc.html
   Palette per D11: ink #1B2454 · pink #EE7BE4 · lavender-white. No dark mode.
   Type: Figtree (UI) · Newsreader italic (accents only) · JetBrains Mono (labels/ids)
   ========================================================================== */

:root {
  --ink:        #1B2454;   /* primary text, headings, borders */
  --ink-2:      #2A3572;
  --ink-3:      #3A4070;
  --muted:      #5B6190;   /* secondary text */
  /* #8B90B5 -> #63699B and #686D9C -> #646999, 11 Aug 2026. MEASURED, not adjusted by eye.
     --muted-2 was 2.72:1 against --bg-2 and 3.11:1 against --surface, well under the 4.5:1 WCAG
     AA floor for normal-size text — and it is the room's secondary text: row metadata, card
     subtitles, the palette's document titles. --muted-3 was 4.32:1, marginal, and it is the
     quietest text in the product: mono microlabels, folder counts, placeholders.

     Both darkened along their own hue rather than replaced, so the palette keeps its character —
     the same lavender-grey, one step down. Solved against the WORST ground each is set on
     (--bg-2), not an average, because an average passes while the real page fails.

     Found by computing the ratio in tools/tests/test-room-a11y.php. Three earlier contrast
     failures in this codebase came from a token chosen by its name; a name never tells you. */
  --muted-2:    #63699B;
  /* Darkened from #9CA0C2 (2.55:1 on white — fails AA) to 4.93:1, keeping the cool violet-grey
     of the D11 palette. Every use is on a light surface — placeholders, meta lines, table
     headers, breadcrumb separators — so nothing needed the lighter value. It carried the
     search box and the footer, which investors read. */
  --muted-3:    #646999;   /* placeholders, meta — AA on white */
  --hairline:   #E4E5F3;   /* every border in the room */
  --hairline-2: #B9BDDC;
  --bg:         #F3F3FB;   /* page */
  --bg-2:       #EFEFF8;
  --bg-3:       #F6F6FB;
  --surface:    #FFFFFF;
  /* ── SURFACES THAT WERE HARDCODED IN THEIR RULES ──────────────────────────────────────────
     Added 13 Aug 2026 with the dark theme. These four were literal colours inside the rules that
     use them — rgba(255,255,255,.92) on the topbar and so on — which is why the header stayed
     white on a dark page while every tokenised surface switched correctly.
     Tokenised rather than overridden: an override is a second rule that has to be kept in step
     with the first, and this stylesheet has already been bitten once by two blocks disagreeing.
     Light values below are EXACTLY what the rules held before, so this change is a no-op in the
     light theme by construction. */
  --topbar-bg:     rgba(255,255,255,.92);   /* frosted; alpha is load-bearing, backdrop-filter */
  --topbar-sub-bg: rgba(255,255,255,.75);
  --field-bg:      #ffffff;                 /* legacy-shell inputs, selects, textareas */
  --drift-bg:      rgba(255,255,255,.55);
  --nav-active-bg: #ECECF7;         /* the current nav item's pill */
  /* ── INVERTED SURFACES ────────────────────────────────────────────────────────────────────
     Added 13 Aug 2026, found by the contrast scanner on the INVESTOR HOME — the first screen an
     investor sees. A family of components deliberately inverts: a dark panel with white text, for
     the "continue reading" card, the primary button, the selected filter chip. They were written
     as `background: var(--ink); color: #fff`, which is correct while --ink is dark and becomes
     white-on-near-white the moment the token flips. Measured 1.2:1.
     So the inverted pair does NOT follow the theme: it is a surface that is dark in both, one step
     lighter in dark mode so it still reads as raised rather than as a hole. #FFFFFF on #2A3160 is
     12.3:1, and the pink accent used inside these panels is 5.03:1 — checked, because that accent
     is the one thing in there that is not white. */
  --invert-bg: #1B2454;
  --invert-fg: #FFFFFF;
  /* Secondary text INSIDE an inverted panel. Not --muted or --hairline-2: those are chosen
     against the PAGE, and an inverted panel is not the page. The card's own subtext was
     --hairline-2, which reads 7.97:1 on the light panel and 1.33:1 on the dark one — fixing
     the surface exposed the child, which is the ordinary way this goes. 6.4:1 and 7.7:1. */
  --invert-muted: #B4BADA;
  --chip-blue-bg:   #E9EDFB;        /* VIEW ONLY and friends */
  --chip-blue-line: #C9D4F4;
  --on-pink:        var(--ink);     /* text ON a --pink fill; see the note in the dark block */
  --pink:       #EE7BE4;   /* brand accent */
  /* ACCENT TEXT, AND IT DID NOT MEET THE THRESHOLD ITS OWN COMMENT CLAIMED. #D14EC6 measures
     3.74:1 on white, 3.39:1 on the page background and 3.17:1 inside a --pink-soft chip, against
     the 4.5:1 normal text requires. It is used as text in 23 places including a:hover — every link
     hover in the room — and .eyebrow, which is 11px uppercase and therefore normal text rather than
     large.

     Measured 13 Aug 2026, after the same fault was found in the registry: a colour chosen to be
     filled with, used where contrast is required. --pink stays exactly as it is for fills, chips
     and the brand; only the token whose stated job is TEXT was changed.

     #A6359C: 5.85:1 on white, 5.27:1 on the page, 4.95:1 on a soft-pink chip — clear of the floor
     on every surface it is actually used against, rather than on the one somebody measured. */
  --pink-deep:  #A6359C;   /* accent TEXT — measured on white, page and chip */
  --pink-soft:  #FBE6F9;   /* avatar / chip fills */
  --pink-2:     #F49BEC;
  --green:      #17864E;   /* live / success — A FILL, not a text colour. See below. */
  --green-soft: #E1F3E9;
  /* GREEN TEXT, split out 14 Aug 2026 after a browser measurement.
     --green as chip text on --green-soft reads 3.99:1, and the trust chips on the investor's home
     page — NDA SIGNED, FULL ACCESS — are exactly that pairing at 10.5px. It is the only place in
     this sheet where a semantic hue was doing both jobs: --pink-deep and --amber were each chosen
     AS text and their comments say so, and --green never was.

     #0F5C36 was already in this file at .flash.ok, so this promotes an existing value rather than
     inventing a hue. 6.99:1 on the chip, 8.06 on white, 7.30 on the page ground.
     --green stays exactly as it was, so every FILL (live-dot, the done tick, the diligence box)
     is untouched.

     ── DARK MUST ALIAS THIS BACK. Read the two dark scopes below before touching it. ──────────
     The first draft of this comment said "dark rendering is not in question — this room has one
     theme". That was wrong, and it was wrong in the one direction that hides a defect: the room
     has THREE palette scopes, and a token defined only here silently keeps its light value in the
     other two. #0F5C36 on the dark --green-soft (#12321F) is 1.73:1 — dark green on dark green,
     effectively invisible, against the 7.44:1 the base --green was already giving. So the fix for
     a 3.99 in light would have shipped a 1.73 in dark.

     Caught because a browser pass measured light and reported zero, and the registry's equivalent
     test asserts its dark theme aliases the -text variants. Both dark scopes now do the same, so
     switching a rule to --green-text provably cannot alter dark rendering, and
     test-room-tone-tokens-as-text.php holds it there. */
  --green-text: #0F5C36;
  /* Warning. The fourth semantic colour the style overview calls for, and the only one that was
     missing — the legacy sheet already carried these two values inline at .legacy .badge.warn,
     so this promotes them to tokens rather than inventing a hue. Used for a part-read document
     ("12 min left") and nothing else so far. */
  --amber:      #8A5A00;
  --amber-soft: #FFF6E3;
  --blue:       #3B5BDB;

  --r-sm: 7px;  --r:    10px;  --r-lg: 12px;
  --r-xl: 18px; --r-pill: 999px;

  --shadow:    0 1px 2px rgba(28,36,85,.05);
  --shadow-lg: 0 12px 40px rgba(28,36,85,.14);

  --shell: 1320px;
  --gutter: 28px;

  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --serif: 'Newsreader', Georgia, serif;
  --sans: 'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* --- reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: var(--bg); }
body {
  font-family: var(--sans);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
a { color: var(--ink); text-decoration: none; }
a:hover { color: var(--pink-deep); }
input, textarea, select, button { font-family: var(--sans); font-size: inherit; }
::placeholder { color: var(--muted-3); }
img { max-width: 100%; display: block; }

/* --- motion --------------------------------------------------------------- */
@keyframes pulse   { 0%{box-shadow:0 0 0 0 rgba(23,134,78,.45);} 70%{box-shadow:0 0 0 7px rgba(23,134,78,0);} 100%{box-shadow:0 0 0 0 rgba(23,134,78,0);} }
@keyframes drawerIn{ from{transform:translateX(24px);opacity:0;} to{transform:none;opacity:1;} }
@keyframes fadeUp  { from{transform:translateY(10px);opacity:0;} to{transform:none;opacity:1;} }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}

/* --- typographic primitives ---------------------------------------------- */
.mono      { font-family: var(--mono); font-size: 11px; letter-spacing: .18em; text-transform: uppercase; }
.mono-sm   { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.086em; }
.mono-xs   { font-family: var(--mono); font-size: 10.5px;  letter-spacing: 0.060em; font-weight: 600; }
.eyebrow   { font-family: var(--mono); font-size: 11px; letter-spacing: .18em; color: var(--pink-deep); text-transform: uppercase; }
.accent    { font-family: var(--serif); font-style: italic; font-weight: 400; color: var(--muted); }

/* Numerals inside an italic-serif accent. Newsreader italic's zero is close enough to a
   lowercase o at heading size that "0 envelopes" read as "o envelopes". Lining, tabular figures
   in the sans face make a count unmistakably a count. */
.accent-num {
  font-family: var(--sans); font-style: normal; font-weight: 600;
  font-variant-numeric: lining-nums tabular-nums;
}

/* The NDA template preview renders the published markup, so it needs the same basic block
   rhythm the investor-facing nda-body has, or headings and lists collapse together. */
.nda-preview h3 { font-size: 15px; font-weight: 700; margin: 0 0 8px; color: var(--ink); }
.nda-preview p  { margin: 0 0 10px; }
.nda-preview ul, .nda-preview ol { margin: 0 0 10px; padding-left: 20px; }
.nda-preview li { margin: 0 0 6px; }
.nda-preview strong { font-weight: 700; color: var(--ink); }

h1, .h1 { font-size: 38px; font-weight: 800; letter-spacing: -.02em; margin: 0; line-height: 1.15; }
h2, .h2 { font-size: 17px; font-weight: 800; margin: 0; }
h3, .h3 { font-size: 15px; font-weight: 700; margin: 0; }
.sub    { color: var(--muted); font-size: 14px; line-height: 1.6; }
.meta   { color: var(--muted-3); font-size: 12.5px; }

/* --- the wordmark --------------------------------------------------------- */
/* The wordmark is the SVG artwork, sized by height. It was previously the word "maslow" set in
   the body font at weight 800 with a pink "." bolted on in ::after — which is a different logo
   wearing the brand colours, because the real 'w' is a custom letterform Figtree has no weight
   for. The dot lives inside the SVG, so nothing is added here. */
.wordmark { height: 22px; width: auto; display: block; }
.wordmark-lg { height: 28px; }

/* --- app shell ------------------------------------------------------------ */
.topbar {
  position: sticky; top: 0; z-index: 40;
  background: var(--topbar-bg);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--hairline);
}
.topbar-inner {
  max-width: var(--shell); margin: 0 auto; padding: 0 var(--gutter);
  height: 62px; display: flex; align-items: center; gap: 22px;
}
/* flex: none. Everything in this bar shrank because flex items default to shrinking, and the
   brand shrinking clips the DATA ROOM tag into "DA" — the wordmark is a fixed-width image, so the
   only thing that can give is the tag beside it. The logo is not a thing that yields. */
.brand { display: flex; align-items: center; gap: 10px; cursor: pointer; flex: none; }
.room-tag {
  border: 1.5px solid var(--ink); border-radius: var(--r-sm); padding: 2px 8px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em; white-space: nowrap;
}
/* The nav is the ONLY thing that gives. min-width:0 lets it actually shrink (a flex item's
   default min-width is auto, which is why the bar overflowed instead of compressing), and it
   scrolls rather than wrapping — a nav that wraps to two rows changes the height of the header
   and pushes the whole page down. */
.nav {
  flex: 1 1 auto; min-width: 0; display: flex; gap: 4px;
  overflow-x: auto; scrollbar-width: none;
  /* SAFE centre, and the fallback matters. Plain `justify-content: center` on a container that
     overflows clips the LEADING edge and makes it unreachable — no amount of scrolling brings it
     back, because the overflow is distributed both ways. The first version of this fix did exactly
     that and silently ate the Home link at 1000px: the bar looked tidy and had lost a nav item,
     which is worse than the crush it replaced.
     `safe center` centres while it fits and falls back to flex-start the moment it does not. The
     flex-start line before it is what browsers without `safe` get, which is the right default:
     everything reachable, merely not centred. */
  justify-content: flex-start;
  justify-content: safe center;
}
.nav::-webkit-scrollbar { display: none; }
.nav a { flex: none; white-space: nowrap; }
.nav a {
  padding: 8px 15px; border-radius: var(--r); font-size: 14.5px; font-weight: 600;
  color: var(--muted); cursor: pointer;
}
.nav a:hover { background: var(--bg); color: var(--ink); }
/* --nav-active-bg, not #ECECF7. Measured on the live dark page 13 Aug 2026: the pill kept its
   light lavender while the label took the dark theme's near-white --ink, giving
   rgb(236,236,247) behind rgb(233,234,246) — a contrast ratio of about 1:1. The current section
   of the navigation was, precisely, invisible. Not a mismatch of taste: the one nav item that
   tells you where you are was the one item nobody could read. */
.nav a.is-active { font-weight: 800; color: var(--ink); background: var(--nav-active-bg); }

.icon-btn {
  position: relative; width: 38px; height: 38px; border: 1.5px solid var(--hairline);
  border-radius: var(--r); background: var(--surface);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.icon-btn:hover { border-color: var(--hairline-2); }
.badge {
  position: absolute; top: -5px; right: -5px; background: var(--pink); color: #fff;
  font-size: 10.5px; font-weight: 800; border-radius: 9px; padding: 1px 5px; border: 2px solid #fff;
}
.searchbox {
  display: flex; align-items: center; gap: 8px; border: 1.5px solid var(--hairline);
  border-radius: var(--r); padding: 7px 12px; color: var(--muted-3); font-size: 13px;
  cursor: pointer; background: var(--surface);
  /* nowrap and no shrink: "Search documents" was wrapping to two lines inside its own box, which
     made the whole bar taller and read as a broken control rather than a narrow one. */
  flex: none; white-space: nowrap;
}
.searchbox:hover { border-color: var(--hairline-2); }
/* Desktop shows the words; the glyph is for the widths where the words are gone. */
.sb-ico { display: none; flex: none; }
@media (max-width: 1080px) { .sb-ico { display: block; } }
.searchbox kbd {
  font-family: var(--mono); font-size: 10.5px; border: 1px solid var(--hairline);
  border-radius: 5px; padding: 1px 5px;
}
.who { display: flex; align-items: center; gap: 9px; flex: none; }
.avatar {
  width: 34px; height: 34px; border-radius: 50%; background: var(--pink-soft);
  color: var(--pink-deep); font-weight: 800; font-size: 13px;
  display: flex; align-items: center; justify-content: center; flex: none;
}
.who-name { font-size: 14px; font-weight: 600; white-space: nowrap; }

/* Account / Sign out. The `who` block was two plain spans, so there was no way out of the
   new shell at all — no sign-out anywhere, and no route to account settings. */
.who-link {
  font-size: 12.5px; font-weight: 600; color: var(--muted); cursor: pointer;
  padding: 5px 9px; border-radius: var(--r-sm); white-space: nowrap;
}
.who-link:hover { background: var(--bg); color: var(--ink); }

/* Admin sub-nav. Seven of the eight admin screens had no link anywhere.
   Superseded by .adminbar below — the strip markup is gone from room-ui.php, but these rules stay
   because admin/sh-requests.php and any page still rendering the legacy shell reference them, and
   removing CSS whose last consumer you have not checked is how a page ends up unstyled. */
.topbar-sub { border-top: 1px solid var(--hairline); background: var(--topbar-sub-bg); }
.topbar-sub .topbar-inner { height: 46px; }
.nav-sub { justify-content: flex-start; gap: 2px; }
.nav-sub a { padding: 6px 12px; font-size: 13.5px; font-weight: 600; }

/* ── the header sheds things in priority order ────────────────────────────────────────────────
   There was one breakpoint, at 720px, and above it everything simply compressed: at ~900px the
   brand tag clipped to "DA", "Search documents" wrapped inside its box and "Kane Jackson" wrapped
   under itself. A single breakpoint means every element is fine until they are all broken at once.

   So they go one at a time, least useful first. The search box goes first because Documents is
   still in the nav and reaches the same place. The name goes next — the avatar still identifies
   who is signed in. The room tag goes last of the three, because on a shared laptop "DATA ROOM"
   versus "PARTNER" is the fastest way to see which room you are in. Account and Sign out never
   go: no route out of the shell is the bug this block already fixed once. */
@media (max-width: 1080px) {
  /* SEARCH COLLAPSES TO AN ICON — IT DOES NOT GO. It used to be hidden outright here, and the
     reasoning above was sound at the time: the box was a link to documents.php, and Documents was
     still in the nav, so nothing was lost. That stopped being true when the command palette was
     built. The box now opens a search across every document the reader can see, which the nav
     cannot do, so hiding it removes a capability rather than a duplicate.
     The mobile audit of 11 Aug 2026 found the consequence: no search at all at phone width, on the
     page type where finding a document matters most. */
  /* height matches .icon-btn (38x38), its sibling in this same row. Collapsing the padding
     to make an icon target also collapsed the HEIGHT to the glyph — 38x18 — measured at
     375px on 14 Aug. WCAG 2.2 AA 2.5.8 wants 24x24, and the inline-link exception does not
     apply to a discrete control. It matters here more than on a wider screen: the comment
     above records that this box is the ONLY route to search on a phone, so there is no
     equivalent-control exception either. Width was already right; only height was missing. */
  .searchbox { padding: 0; width: 38px; height: 38px; justify-content: center; }
  .sb-label { display: none; }
}
@media (max-width: 880px) {
  .who-name { display: none; }
}
@media (max-width: 720px) {
  /* The tag is the last of the three to go — on a shared laptop "DATA ROOM" versus "PARTNER" is
     the fastest way to see which room you are in. The wrap itself is handled at 820px, where the
     nav would otherwise have been hidden entirely. */
  .room-tag { display: none; }
  .nav-sub { overflow-x: auto; }
}

/* --- page ----------------------------------------------------------------- */
.page { max-width: var(--shell); margin: 0 auto; padding: 40px var(--gutter) 90px; }

/* --- admin sidebar -------------------------------------------------------- */
/* STYLE-OVERVIEW, "Layout": admin uses a sticky left sidebar instead of tabs. Fifteen entries in
   a horizontal strip was one label from overflowing at 1440px and wrapped on a 1280px laptop.

   SECOND PASS, 9 Aug 2026. The first version made the headings ink/700 and put a rule under each,
   which fixed the contrast and created a new problem: four horizontal rules turned the column into
   something that read like a table, with headings and links at similar visual weight — the
   operator's words, "ugly and hard to distinguish between headings and content".

   The fix is to separate them by KIND rather than by decoration. A heading is small, dark,
   wide-tracked mono; a link is larger, lighter, sans. Those two things cannot be confused for each
   other at a glance, and no rules are needed to say so. Group spacing does the grouping. */
.admin-page { display: grid; grid-template-columns: 208px minmax(0, 1fr); gap: 34px; align-items: start; }
.admin-main { min-width: 0; }          /* without this a wide table stretches the grid column */
.adminbar { position: sticky; top: 88px; display: grid; gap: 26px; }
.adminbar-group { display: grid; gap: 1px; }

.adminbar-head {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em; text-transform: uppercase;
  font-weight: 700; color: var(--ink);
  padding: 0 12px 9px;
}

/* Links are deliberately lighter and larger than the label above them. Weight 500 rather than
   600: at 600 the link was competing with a 700 heading four pixels smaller, which is exactly the
   collision that made the two tiers hard to tell apart. */
.adminbar-link {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 12px; border-radius: var(--r);
  font-size: 13.5px; font-weight: 500; color: var(--muted); text-decoration: none;
  border-left: 2px solid transparent;
  transition: background .15s, color .15s, border-color .15s;
}

/* A FIXED-WIDTH COLUMN, not an inline character. The glyphs are different widths — § is narrow,
   ▤ is wide — so set inline they would each push their own label to a different x, and sixteen
   labels starting at sixteen positions is worse than no icons at all. One width, centred inside
   it, and every label in the sidebar starts on the same vertical line.

   Slightly smaller and one step lighter than the label it sits beside: the word is the thing
   being read, and a glyph at full weight competes with it. flex:none so a long label can never
   squeeze the column shut. */
/* The icon slot. Sized to the artwork rather than to a font size — these were text glyphs until
   13 Aug 2026 and `font-size:12px` was doing the sizing; an inline SVG ignores it and would have
   rendered at whatever the <span> inherited. */
.adminbar-ic {
  flex: none; width: 16px; height: 16px; display: flex;
  align-items: center; justify-content: center;
  color: var(--muted-3); transition: color .15s;
}
.adminbar-ic svg { width: 16px; height: 16px; display: block; }
.adminbar-link:hover .adminbar-ic,
.adminbar-link.is-active .adminbar-ic { color: var(--pink-deep); }
.adminbar-link:hover { background: var(--bg-3); color: var(--ink); }

/* The active item is marked by the accent on its leading edge, not by a floating white pill.
   One pink mark, on the one item that is current — which is the discipline the style overview
   asks for, and it survives being scanned quickly in a way a border-only pill does not. */
.adminbar-link.is-active {
  background: var(--surface); color: var(--ink); font-weight: 700;
  border-left-color: var(--pink);
}
.adminbar-link:focus-visible { outline: 2px solid var(--pink-deep); outline-offset: 2px; }

/* The maker's mark closes the sidebar, above a hairline that separates it from the last group.
   The rule is the divider; the stamp is not a nav item and should not read as one. */
.adminbar-foot {
  border-top: 1px solid var(--hairline);
  padding-top: 18px; margin-top: 4px;
  display: flex; justify-content: center;
}
/* .45/.75, the same pair as .room-stamp — this is the same mark on the same background, and two
   different opacities for it would be a difference nobody chose. */
.adminbar-stamp { width: 104px; height: auto; opacity: .45; transition: opacity .15s; }
.adminbar-stamp:hover { opacity: .75; }

/* Below the two-column breakpoint the sidebar stops being a sidebar and becomes a scrolling row
   above the content — the same information, no horizontal page scroll, and nothing hidden. */
@media (max-width: 900px) {
  .admin-page { grid-template-columns: minmax(0, 1fr); gap: 20px; }
  .adminbar { position: static; grid-auto-flow: column; grid-auto-columns: max-content;
    gap: 18px; overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 4px; }
  .adminbar-group { grid-auto-flow: column; grid-auto-columns: max-content; align-items: center; gap: 2px; }
  /* Horizontal: the heading keeps its own space rather than a rule, matching the column. */
  .adminbar-head { padding: 0 12px 0 0; margin: 0; }
  .adminbar-link { border-left: 0; border-bottom: 2px solid transparent; }
  .adminbar-link.is-active { border-left: 0; border-bottom-color: var(--pink); }
  /* Horizontal mode: the sidebar is a scrolling row of links, and a footer has nowhere to be in
     one. Hidden rather than restyled — room_foot() shows the page-footer stamp whenever the
     sidebar one is absent, so nothing is lost, and it is not display:none on an element the
     footer test would then look for. */
  .adminbar-foot { display: none; }
}
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.live-chip {
  display: flex; align-items: center; gap: 8px; background: var(--surface);
  border: 1px solid var(--hairline); border-radius: var(--r-lg);
  padding: 9px 14px; font-size: 13px; color: var(--muted);
}
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }

.cols { margin-top: 24px; display: grid; grid-template-columns: 1fr 360px; gap: 20px; align-items: start; }
.stack { display: grid; gap: 20px; }

/* --- card ----------------------------------------------------------------- */
/* Borders, not shadows — STYLE-OVERVIEW, "Core principles" 2. A card is defined by a 1px
   hairline on white; shadow is reserved for things that genuinely float (overlays, drawers, the
   document page in wm-preview, which keeps one). A shadow on a static card is what made our
   screens read softer than the design's next to each other. */
.card {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: var(--r-xl); padding: 24px;
}
.card-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.card + .card { margin-top: 0; }
.card-tight { padding: 18px; }

/* --- lists ---------------------------------------------------------------- */
.rows { margin-top: 14px; display: grid; gap: 4px; }
.row {
  display: flex; align-items: center; gap: 12px; padding: 11px 12px;
  border-radius: var(--r); cursor: pointer;
}
.row:hover { background: var(--bg-3); }
.row-title { font-weight: 600; font-size: 14.5px; }
.row-meta { margin-left: auto; color: var(--muted-3); font-size: 12.5px; white-space: nowrap; }

/* --- chips / pills -------------------------------------------------------- */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em; font-weight: 600;
  border: 1.5px solid var(--hairline); border-radius: var(--r-sm);
  padding: 4px 9px; color: var(--muted); background: var(--surface);
}
.chip-new  { color: var(--pink-deep); background: var(--pink-soft); border-color: #F2C7EE; }
.chip-live { color: var(--green-text); background: var(--green-soft); border-color: #C6E6D4; }
/* The fourth read state. DESIGN-SPEC §4.3 names four — Read ✓ (green), N min left (amber),
   New · unread (pink), Unread (grey) — and we had three, so a part-read document was
   indistinguishable from an unopened one. Amber is the existing warning colour; no fifth hue. */
.chip-part { color: var(--amber);     background: var(--amber-soft); border-color: #F0DCAE; }

/* --- the folder list (Documents) ------------------------------------------ */
/* ONE BOX, DENSE ROWS. Operator's choice, 9 Aug 2026, after eleven folders as eleven cards —
   .card at 24px padding with a 20px gap each — took ~1210px to present about forty characters
   per row. The room is the container; the folders are its contents. All eleven now sit inside a
   single card in ~590px, which is the difference between a list you read and a page you scroll.
   The style overview asks for exactly this in its own words: "dense data, roomy containers".

   THE DISCLOSURE IS ON THE LEFT, and that is the load-bearing part rather than a preference. A
   right-hand chevron is the iOS list convention for GO TO ANOTHER PAGE; these rows open in
   place. With it on the right the operator read the rows as single documents — "they appear
   almost as one doc". Left, and rotating, is what every file tree and every accordion does. */
.folds { display: grid; }
.fold { border-radius: var(--r); }
.fold + .fold { border-top: 1px solid var(--hairline); }

.fold > summary {
  list-style: none; cursor: pointer;
  display: grid; grid-template-columns: 18px 38px minmax(0, 1fr) 104px 150px 44px;
  gap: 13px; align-items: center;
  padding: 11px 12px; border-radius: var(--r);
  transition: background .15s;
}
.fold > summary::-webkit-details-marker { display: none; }   /* Safari ignores list-style alone */
.fold > summary:hover { background: var(--bg-3); }
.fold > summary:focus-visible { outline: 2px solid var(--pink-deep); outline-offset: -2px; }
/* The open row is tinted so you can see which group you are inside without counting indents. */
.fold[open] > summary { background: var(--bg-3); }

.fold-chev { flex: none; color: var(--muted-2); transition: transform .18s ease; }
.fold[open] > summary .fold-chev { transform: rotate(90deg); color: var(--pink-deep); }
@media (prefers-reduced-motion: reduce) { .fold-chev { transition: none; } }

.fold-name { font-weight: 600; font-size: 14.5px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The count reads as CONTENTS. It was 11px --muted-3 mono buried in a sub-line; "9 documents"
   next to a triangle says there are nine things in here before anyone has clicked anything. */
.fold-count { font-family: var(--mono); font-size: 11px; color: var(--muted-3);
  white-space: nowrap; font-variant-numeric: tabular-nums; }
.fold-pct { justify-self: end; font-family: var(--mono); font-size: 11.5px;
  color: var(--muted-3); font-variant-numeric: tabular-nums; }
.fold-bar { width: 100%; }

/* Children inset behind a rule, so containment is drawn rather than inferred. */
/* ── THE PROGRESS RING, SWEPT ON FIRST PAINT ────────────────────────────────────────────────
   The rings were called a signature asset by the 10 Aug audit and were completely static. A
   conic-gradient cannot be transitioned — the browser has no way to interpolate between two
   gradient strings — but a REGISTERED custom property can be, and the gradient recomputes as it
   moves. That is what @property buys here, and it is the only way to do this without replacing
   the rings with SVG.

   Registered as a <percentage> with an initial value of 0%, so the keyframe has somewhere to
   start. folder_ring() writes the real value inline as --ring-pct; the animation runs from zero
   to whatever that is.

   Unregistered, the whole thing is inert and the ring simply renders at its final value — which
   is today's behaviour, and correct. Reduced motion zeroes the duration by the global rule, so
   there too the ring appears finished rather than never arriving. */
@property --ring-pct { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
@keyframes ringSweep { from { --ring-pct: 0%; } }
.ring { animation: ringSweep .9s cubic-bezier(.2,.8,.2,1) both; }

/* ── THE EXPAND, ANIMATED ────────────────────────────────────────────────────────────────────
   The 10 Aug UI audit called the folder rows "the single most dated interaction in the product"
   and asked for four things. Three of them were already here and had been for weeks: the whole
   row toggles (it is a native <summary>, so that is free), the chevron rotates 90° over 180ms,
   and reduced motion is honoured. Checking before building saved rebuilding them.

   The one real complaint is this: <details> pops open instantly, because a browser cannot
   animate to `height: auto` and the content has no fixed height to animate to.

   `interpolate-size: allow-keywords` is what changed that — it lets a transition interpolate to
   a keyword size, so `block-size: auto` becomes animatable. `transition-behavior: allow-discrete`
   carries `content-visibility` along with it, without which the content vanishes at the start of
   the close rather than fading with it.

   Where any of that is unsupported the whole block is ignored and the details opens instantly —
   exactly today's behaviour. Nothing degrades to broken. And the global reduced-motion rule at
   the top of this file zeroes the duration, so a reader who asked for stillness gets the
   instant pop back, which for them is the correct answer rather than a lesser one. */
:root { interpolate-size: allow-keywords; }
.fold::details-content {
  block-size: 0; overflow: clip;
  transition: block-size .26s cubic-bezier(.2,.8,.2,1), content-visibility .26s allow-discrete;
}
.fold[open]::details-content { block-size: auto; }

/* The rows arrive in sequence rather than all at once — 30ms apart, which is enough to read as
   a cascade and short enough that a nine-document folder is settled in under a third of a
   second. Six steps only: past that the delay is longer than the animation and the last rows
   read as lag rather than as choreography. */
.fold[open] .doc-row { animation: docIn .22s cubic-bezier(.2,.8,.2,1) both; }
.fold[open] .doc-row:nth-child(1) { animation-delay: 0ms; }
.fold[open] .doc-row:nth-child(2) { animation-delay: 30ms; }
.fold[open] .doc-row:nth-child(3) { animation-delay: 60ms; }
.fold[open] .doc-row:nth-child(4) { animation-delay: 90ms; }
.fold[open] .doc-row:nth-child(5) { animation-delay: 120ms; }
.fold[open] .doc-row:nth-child(n+6) { animation-delay: 150ms; }
@keyframes docIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.fold-kids { margin: 2px 12px 12px 41px; padding-left: 16px;
  border-left: 2px solid var(--hairline-2); display: grid; gap: 2px; }

/* Below the breakpoint the row also gets the stacked-paper edge from treatment 3 — a 13px
   triangle is not a thumb target, so the metaphor carries the affordance where the icon can't. */
@media (max-width: 720px) {
  .fold > summary { grid-template-columns: 16px 36px minmax(0, 1fr) 44px; row-gap: 9px; }
  .fold-count { grid-column: 3; font-size: 10.5px; }
  .fold-bar   { grid-column: 2 / span 3; }
  .fold-kids  { margin-left: 18px; padding-left: 12px; }
  .fold { position: relative; margin-bottom: 9px; }
  .fold::after { content: ""; position: absolute; left: 10px; right: 10px; bottom: -5px; height: 5px;
    border: 1px solid var(--hairline); border-top: 0; border-radius: 0 0 10px 10px;
    background: var(--surface); }
  .fold[open]::after { display: none; }
}
/* --- reading coverage, per folder ----------------------------------------- */
/* Four columns so every bar shares a left and a right edge. The name column is the only elastic
   one; the chip column holds its width whether or not there is a chip in it, so a folder with
   nothing new does not shunt its bar left of the row above. */
.cov { display: grid; gap: 2px; }
.cov-row {
  display: grid; align-items: center;
  grid-template-columns: minmax(0, 1fr) 64px 190px 44px;
  gap: 12px; padding: 9px 12px; border-radius: var(--r); text-decoration: none;
  color: inherit; transition: background .15s;
}
.cov-row:hover { background: var(--bg-3); }
.cov-row:focus-visible { outline: 2px solid var(--pink-deep); outline-offset: -2px; }
.cov-name {
  font-weight: 600; font-size: 14.5px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cov-chip { justify-self: start; }
.cov-bar  { width: 100%; }
.cov-pct  {
  justify-self: end; font-family: var(--mono); font-size: 11.5px;
  color: var(--muted-3); font-variant-numeric: tabular-nums;
}
/* Narrow: the bar keeps its own row rather than squeezing the name to nothing. */
@media (max-width: 620px) {
  .cov-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 8px; }
  .cov-chip { grid-column: 2; justify-self: end; }
  .cov-bar  { grid-column: 1; }
  .cov-pct  { grid-column: 2; }
}

/* Page-level controls, in the heading's own row. .fold-all is retained for admin-signatures,
   which still renders its buttons above its box. */
.page-head-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.fold-all { display: flex; justify-content: flex-end; gap: 8px; margin-top: -6px; }
.fold-chev { flex: none; color: var(--muted-2); transition: transform .18s ease; }
.fold[open] > summary .fold-chev { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .fold-chev { transition: none; } }

/* Document rows sit on ONE line: title takes the slack, everything else is intrinsic width.
   At the 472px the app was capped to, these wrapped — titles broke over two lines and the
   status chip split mid-phrase. min-width:0 is what lets the title ellipsis instead of
   forcing the flex line wider than its container. */
.doc-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px;
  border-radius: var(--r); transition: background .15s; }
.doc-row:hover { background: var(--bg-3); }
.doc-row .doc-name { flex: 1 1 auto; min-width: 0; font-weight: 600; font-size: 14.5px;
  color: var(--ink); text-decoration: none; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.doc-row .doc-name:hover { color: var(--pink-deep); }
.doc-row .chip, .doc-row .btn { flex: none; }
/* THE REGISTER INDEX, ahead of the type chip. Fixed width so 1.1 and 4.1A leave the type chips and
   every title starting on the same vertical line — a ragged left edge here would be worse than the
   numbers-inside-the-title it replaces, because the eye uses that edge to scan the list.
   tabular-nums so 1.1 and 1.9 are the same width in a proportional face. */
.doc-row .doc-ix { flex: none; min-width: 34px; text-align: right; color: var(--muted-3);
  font-variant-numeric: tabular-nums; letter-spacing: .02em; }
@media (max-width: 720px) {
  .doc-row { flex-wrap: wrap; }
  /* The name takes its own line below; the index and type chip stay together on the first one.
     Measured at 375px — without this the index wraps alone and reads as a stray number. */
  .doc-row .doc-name { flex-basis: 100%; white-space: normal; }
  .doc-row .doc-ix { min-width: 0; }
}

/* --- progress ------------------------------------------------------------- */
/* display:block is load-bearing. `height` does not apply to an inline element, and this class
   is used on both <div> and <span>. On a bare <span> the 6px was ignored and the inner <i>,
   being display:block at height:100%, ballooned into a giant pink pill that covered the
   document title next to it. home.php already patched around this with an inline
   style="display:block"; fixing it here means the element choice cannot matter. */
.bar { display: block; height: 6px; border-radius: var(--r-pill); background: var(--bg-2); overflow: hidden; }
.bar > i { display: block; height: 100%; border-radius: var(--r-pill); background: var(--pink); }

/* --- buttons -------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  border: 1.5px solid var(--invert-bg); background: var(--invert-bg); color: var(--invert-fg);
  border-radius: var(--r); padding: 10px 18px; font-size: 14px; font-weight: 700;
  cursor: pointer; transition: background .12s ease, border-color .12s ease;
}
.btn:hover { background: var(--ink-2); border-color: var(--ink-2); color: var(--invert-fg); }
.btn-ghost { background: var(--surface); color: var(--ink); border-color: var(--hairline); }
.btn-ghost:hover { background: var(--bg-3); border-color: var(--hairline-2); color: var(--ink); }
/* Ink on pink, not white on pink.
   White on #EE7BE4 measures 2.45:1 — it fails WCAG AA for normal text by a wide margin, and these
   are the room's primary action buttons. The prototype never did this: it puts ink (#1B2454) on the
   pink and reserves white for the notification dot. .btn-pink simply inherited white from .btn, so
   the accessible value was in the design all along and was lost in the port. Ink on pink is 6.02:1.
   The hover lightens to --pink-2 (7.53:1) rather than darkening to --pink-deep, which would have
   dropped ink to 3.94:1 — a hover state that fails is still a failure, and it is the state a
   pointer user sees at the moment they commit. Changed on the operator's instruction, 4 Aug 2026. */
.btn-pink { background: var(--pink); border-color: var(--pink); color: var(--on-pink); }
.btn-pink:hover { background: var(--pink-2); border-color: var(--pink-2); color: var(--on-pink); }
.btn-sm { padding: 7px 13px; font-size: 13px; }

/* --- forms ---------------------------------------------------------------- */
.field { display: block; width: 100%; border: 1.5px solid var(--hairline); border-radius: var(--r);
         padding: 11px 13px; font-size: 14.5px; color: var(--ink); background: var(--surface); }
/* THE FOCUS RING WAS THE BRAND FILL AT 2.45:1, and `outline: none` removed the browser's own.
   WCAG 2.2 SC 1.4.11 asks 3:1 for a focus indicator; --pink is a colour to fill a chip with, not
   one to see an outline in. Same fault as the registry's, found the same way — by measuring rather
   than by looking, since a pale ring reads as "styled" rather than as "missing".
   --pink-deep is 5.82:1 on white and is now the accent TEXT colour, so it is also the one visible
   as a ring. The soft glow stays: it is decoration on top of an indicator that now carries itself. */
.field:focus { outline: 2px solid var(--pink-deep); outline-offset: 1px;
               border-color: var(--pink-deep); box-shadow: 0 0 0 3px rgba(238,123,228,.16); }
.label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; }

/* --- legacy pages inside the new shell ------------------------------------------------------
   The nine admin/* screens still use bare <label> and <input>, which app.css styles and this
   file does not. Rather than retype the markup of the pages that create users, upload documents
   and publish NDA versions — putting real write logic at risk for a cosmetic change — the chrome
   wraps them in .legacy and the same treatment is given to the raw elements here.

   Scoped to .legacy so it cannot reach the Phase 1 pages, which use the classes directly. */
.legacy label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--ink); }

/* A consent checkbox and its sentence are ONE line, not two.
   The rule above makes every bare <label> display:block, which is right for a form field's caption
   and wrong for a checkbox: it pushed the box onto its own line ABOVE the paragraph it governs.
   On the NDA acceptance screen that is not cosmetic. A tick box that does not visibly belong to
   the sentence beside it is a tick box whose meaning is ambiguous, and meaning is the entire point
   of that screen — it is the record of what somebody agreed to. Reported by the operator on
   4 Aug 2026 after signing the agreement himself: "the boxes should be next to each paragraph,
   above them is confusing". */
.legacy .agreeline { display: flex; gap: 10px; align-items: flex-start; margin: 18px 0; }
.legacy .agreeline label { display: inline; font-size: 14px; font-weight: 400; margin: 0;
                           text-transform: none; letter-spacing: 0; }
.legacy .agreeline input[type="checkbox"] { width: 20px; height: 20px; margin-top: 2px;
                           flex: 0 0 auto; accent-color: var(--navy); }
.legacy input[type="text"], .legacy input[type="email"], .legacy input[type="password"],
.legacy input[type="number"], .legacy input[type="date"], .legacy input[type="search"],
.legacy input[type="url"], .legacy select, .legacy textarea {
  display: block; width: 100%; box-sizing: border-box;
  border: 1.5px solid var(--hairline); border-radius: var(--r);
  padding: 10px 12px; font: inherit; font-size: 14px; color: var(--ink); background: var(--field-bg);
}
.legacy input:focus, .legacy select:focus, .legacy textarea:focus {
  outline: 2px solid var(--pink-deep); outline-offset: 1px;
  border-color: var(--pink-deep); box-shadow: 0 0 0 3px rgba(238,123,228,.16);
}
.legacy input[type="checkbox"], .legacy input[type="radio"] { width: auto; display: inline-block; }
/* The old chrome's own containers, so a legacy page does not sit flat on the new background. */
.legacy .panel { background: var(--surface); border: 1px solid var(--hairline);
                 border-radius: var(--r-lg); padding: 20px; margin-bottom: 18px; }
.legacy h1 { font-size: 24px; font-weight: 800; color: var(--ink); margin: 0 0 14px; }
.legacy h2, .legacy h3 { color: var(--ink); }
.legacy table { width: 100%; border-collapse: collapse; }
.legacy th { text-align: left; font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
             color: var(--muted-3); padding: 8px 10px; border-bottom: 1px solid var(--hairline); }
.legacy td { padding: 10px; border-bottom: 1px solid var(--hairline); font-size: 14px; }
/* :not(.btn) because .btn.small is a SIZE, not a piece of muted body text. Without it this rule
   (two classes) outranked room.css's bare .btn (one class) and painted every small button's label
   --muted — which on the ink-filled .btn background is #5B6190 on #1B2454, a contrast ratio of
   2.49:1. The four action buttons under the divider on admin/users.php were all but unreadable. */
.legacy .muted, .legacy .small:not(.btn), .legacy .help { color: var(--muted); font-size: 13px; }
/* Same correction for the legacy pages' spelling of the class. */
.legacy .btn.pink { background: var(--pink); border-color: var(--pink); color: var(--on-pink); }
.legacy .btn.pink:hover { background: var(--pink-2); border-color: var(--pink-2); color: var(--on-pink); }

/* --- legacy pages: the rest of app.css's vocabulary ------------------------------------------
   The block above backfills form controls, panels, headings and tables. It stops there, and the
   nine legacy screens use a good deal more than that — a flex utility, three grid utilities, two
   button variants, status pills, a breadcrumb, a folder grid, a dropzone.

   None of it was reaching them. Under room_shell='new' page_head() delegates to room_head(), which
   links room.css and ONLY room.css; app.css is not loaded on any page that renders through the new
   shell. So every one of those classes was inert in production, and the markup fell back to raw
   block layout: .rowflex rows of buttons stacked flush with no gap, .grid2/.grid3 field pairs
   collapsed to one column per row.

   Worse than inert, in one case. .badge is defined in this file as the topbar's notification dot —
   position:absolute, top:-5px, right:-5px, faithfully ported from the prototype. The legacy pages
   use .badge as an inline status pill, so ADMIN / STAFF / Pending / Full access / DISABLED /
   EXPIRED were being pulled out of flow and stacked in a single pile off the top-right corner of
   <body>, invisible. On admin/users.php that emptied the entire Access column: an administrator
   auditing who can see the room was shown nothing at all. The .legacy scope re-anchors them
   without touching the topbar dot, which lives outside the wrapper.

   Everything here is a port of the app.css rule into this file's tokens, not a new design: same
   geometry, room palette. Scoped to .legacy throughout so the Phase 1 pages cannot be affected. */

/* Layout utilities. */
.legacy .rowflex { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.legacy .toolbar { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; flex-wrap: wrap; }
.legacy .right   { margin-left: auto; }
.legacy .mt      { margin-top: 18px; }
.legacy .mb      { margin-bottom: 18px; }
.legacy .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.legacy .grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.legacy .grid4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 820px) {
  .legacy .grid2, .legacy .grid3, .legacy .grid4 { grid-template-columns: 1fr; }
}

/* Why the grids collapsing to one column was not enough.
   admin/settings.php and admin/esign.php still ran ~6px wide at 375px after the columns folded. A
   1fr track does not shrink below its content's MIN-CONTENT width, and a bare <input> carries an
   intrinsic width of roughly twenty characters plus padding — so the track was being held open
   from the inside by the field, not by the column count. min-width:0 releases the track;
   border-box stops padding adding to a declared 100%. Scoped to .legacy so no Phase 1 page moves.*/
.legacy .grid2 > *, .legacy .grid3 > *, .legacy .grid4 > * { min-width: 0; }
.legacy input, .legacy select, .legacy textarea { max-width: 100%; box-sizing: border-box; }
/* A long unbroken value — an SMTP host, an access key, a signed URL — wraps rather than pushing
   the page sideways. */
.legacy input, .legacy textarea, .legacy .mono { overflow-wrap: anywhere; }

/* Button variants. This file names them .btn-ghost / .btn-sm; the legacy pages say .btn.ghost /
   .btn.small, so neither reached them and every ghost button rendered as a solid ink block. */
.legacy .btn.ghost { background: var(--surface); color: var(--ink); border-color: var(--hairline); }
.legacy .btn.ghost:hover { background: var(--bg-3); border-color: var(--hairline-2); color: var(--ink); }
.legacy .btn.small { padding: 7px 13px; font-size: 13px; }
.legacy .btn.danger { background: var(--surface); border-color: #F5C7CF; color: #8A1B33; }
.legacy .btn.danger:hover { background: #FDEBEE; border-color: #E9A9B6; color: #8A1B33; }
.legacy .btn[disabled] { opacity: .45; cursor: not-allowed; }

/* Status pills — see the note above on why these were absolutely positioned. Built from the same
   vocabulary as this file's .chip so a legacy pill and a Phase 1 chip read as one system. */
.legacy .badge {
  position: static; top: auto; right: auto;
  display: inline-block; white-space: nowrap; vertical-align: baseline;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em; font-weight: 600;
  text-transform: uppercase; padding: 4px 9px;
  border: 1.5px solid var(--hairline); border-radius: var(--r-sm);
  background: var(--surface); color: var(--muted);
}
.legacy .badge.ok   { color: var(--green-text); background: var(--green-soft); border-color: #C6E6D4; }
.legacy .badge.warn { color: #8A5A00;          background: #FFF6E3;           border-color: #F0DCB0; }
.legacy .badge.err  { color: #8A1B33;          background: #FDEBEE;           border-color: #F5C7CF; }
.legacy .badge.pink { color: var(--pink-deep); background: var(--pink-soft);  border-color: #F2C7EE; }

/* Table helpers. .legacy table/th/td are handled above; these are the per-cell modifiers. */
.legacy table.list { font-size: 14.5px; }
.legacy .num { text-align: right; font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.legacy .file-meta { font-family: var(--mono); font-size: 10.5px; letter-spacing: .06em;
                     color: var(--muted-3); white-space: nowrap; }

/* Right-aligned action clusters in legacy tables.
   The cell used to carry `white-space:nowrap`, which by definition cannot wrap — and on Requests
   that cell holds three forms INCLUDING a free-text reason field. `.legacy input[type=text]` is
   `display:block; width:100%`, and a percentage width inside an auto-width table cell is circular:
   the cell grows to fit, the input grows to the cell, and the table pushes past its card to the
   viewport edge. Found by the data room audit, 12 Aug 2026.
   A wrapping flex row keeps the buttons clustered and right-aligned and lets them fold onto a
   second line instead of widening the table. The fix is the layout, not a narrower input — the
   inline `max-width:130px` those inputs carried was a symptom being held down by hand. */
.legacy td.row-actions { white-space: normal; }
.legacy .row-actions-in { display: flex; flex-wrap: wrap; gap: 6px;
                          justify-content: flex-end; align-items: center; }
.legacy .row-actions-in form { display: flex; gap: 6px; align-items: center; margin: 0; }
.legacy .row-actions-in input[type="text"] { width: 130px; min-width: 90px; }

/* ── FILE DROP-ZONES (room_dropzone) ──────────────────────────────────────────────────────────
   The no-JS state comes first and is the one that must always work: a bordered box containing a
   visible label and a real, visible file input. `.dz-input` is hidden ONLY under `.dz-js`, and that
   class is added by dropzone.js — so a blocked or broken script leaves a working control rather
   than an empty rectangle. Written in this order deliberately; the other order fails silently and
   costs somebody an upload. */
.dz {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 26px 20px; text-align: center;
  border: 1.5px dashed var(--hairline-2); border-radius: var(--r-lg);
  background: var(--bg-3); transition: border-color .16s, background .16s;
}
.dz-ico   { color: var(--muted-3); line-height: 0; transition: color .16s, transform .16s; }
.dz-label { font-size: 14.5px; font-weight: 600; color: var(--ink); cursor: pointer; margin: 0; }
.dz-hint  { font-size: 12.5px; color: var(--muted-3); }
.dz-file  { font-family: var(--mono); font-size: 11.5px; letter-spacing: .04em;
            color: var(--pink-deep); }
.dz-file:empty { display: none; }

/* Enhanced. The whole box becomes the target, so it needs the affordance and the focus ring the
   input used to carry — `:focus-within` because the element actually focused is the hidden input,
   and a keyboard user who cannot see where they are is worse off than before the redesign. */
.dz-js            { cursor: pointer; }
/* Visually hidden, still announced. Extracted from .dz-js .dz-input on 13 Aug 2026 when a second
   caller needed it — the move control's destination label, which must have a name a screen reader
   reads and no visible label, because the sentence beside it already says "Move ticked to".

   A shared utility rather than a second copy of the declaration: two copies of a visually-hidden
   rule is two chances for one of them to become display:none, which removes the element from the
   accessibility tree entirely and turns an accessible control into an unlabelled one. */
.sr-only, .dz-js .dz-input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.dz-js:hover      { border-color: var(--pink-deep); background: var(--surface); }
.dz-js:hover .dz-ico { color: var(--pink-deep); }
.dz-js:focus-within { outline: 2px solid var(--pink-deep); outline-offset: 2px;
                      border-color: var(--pink-deep); }
.dz-js.is-over    { border-color: var(--pink-deep); border-style: solid;
                    background: var(--pink-soft); }
.dz-js.is-over .dz-ico { color: var(--pink-deep); transform: translateY(-2px); }
.dz-js.has-file   { border-style: solid; border-color: var(--pink-deep); background: var(--surface); }

/* The lift on drag-over is the only motion here, and it is the first thing to go. */
@media (prefers-reduced-motion: reduce) {
  .dz, .dz-ico { transition: none; }
  .dz-js.is-over .dz-ico { transform: none; }
}

/* ── SORTABLE, STICKY COLUMN HEADINGS (admin-people.php) ──────────────────────────────────────
   Audit, 12 Aug 2026: "People is a strong table but passive: no sort, no sticky header".

   `top: 88px` is NOT a fresh measurement — it is the value `.adminbar` already uses on these same
   admin pages, three rules above. The topbar is 62px plus a border plus the sub-bar, and rather
   than re-derive that and risk a second number that drifts from the first, this reuses the one
   that is already correct on every admin screen. If the header height ever changes, both move
   together because they say the same thing.

   The background is opaque and z-index sits BELOW the topbar's 40, so rows scroll under the
   heading and the heading scrolls under the topbar, which is the order a reader expects. */
.ppl-th { position: sticky; top: 88px; z-index: 3; background: var(--surface); }
.ppl-th a:hover { color: var(--pink-deep); }
.ppl-th a:focus-visible { outline: 2px solid var(--pink-deep); outline-offset: 3px; border-radius: 3px; }

/* On a phone the sidebar is a horizontal rail and the offset above is wrong — there is no 88px of
   chrome to clear. Sticky is dropped rather than re-offset: the table is already a horizontal
   scroller at this width, and a sticky row inside a two-axis scroller is where this gets fiddly
   for no gain on a screen showing four rows. */
@media (max-width: 900px) {
  .ppl-th { position: static; }
}

/* The reading heatmap's cells (admin-analytics.php). Zero used to render as rgba(pink, 0.00) with
   an empty label — an invisible box among invisible boxes, which is why a sparse heatmap read as
   broken rather than as empty. It has a drawn state of its own now: a tint floor, a dashed edge and
   a centred middot, so "nobody has read this yet" is something the grid says rather than something
   it fails to say. Tabular figures because the numbers sit in a column and are compared down it. */
.heat-cell { height: 34px; border-radius: 6px; display: flex; align-items: center;
             justify-content: center; font-size: 12px; font-weight: 600;
             font-variant-numeric: tabular-nums; }
.heat-cell.is-none { background: var(--bg-3); border: 1px dashed var(--hairline);
                     color: var(--muted-3); font-weight: 500; }

/* A floor under any table whose content genuinely cannot wrap: it scrolls inside its own card
   rather than widening the page. Applied per table, not globally — `overflow-x` opens a block
   formatting context, and applying that to all 16 legacy panels at once would clip anything
   positioned outside them. */
.legacy .table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Breadcrumbs and the folder grid (browse.php, admin/files.php). */
.legacy .crumbs { font-size: 13px; margin-bottom: 16px; color: var(--muted); }
.legacy .crumbs a { color: var(--muted); }
.legacy .crumbs a:hover { color: var(--pink-deep); }
.legacy .crumbs .sep { margin: 0 7px; color: var(--muted-3); }
.legacy .folder-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
                       gap: 12px; margin-bottom: 22px; }
.legacy .folder-card { display: flex; align-items: center; gap: 12px; background: var(--surface);
                       border: 1px solid var(--hairline); border-radius: var(--r);
                       padding: 15px 16px; color: var(--ink); font-weight: 600; font-size: 14.5px; }
.legacy .folder-card:hover { border-color: var(--pink); color: var(--ink); }
.legacy .folder-card svg { flex: none; }
/* --fc is set inline per file type by file_icon() in lib/ui.php. */
.legacy .fico { display: inline-block; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em;
                border: 1px solid var(--fc, var(--muted-3)); color: var(--fc, var(--muted-3));
                border-radius: 4px; padding: 2px 6px; min-width: 34px; text-align: center; }
.legacy .file-row .title { font-weight: 600; font-size: 14.5px; }

/* Stat cards (admin/index.php, admin/analytics.php). */
.legacy .statcard { background: var(--surface); border: 1px solid var(--hairline);
                    border-radius: var(--r-lg); padding: 18px; }
.legacy .statcard .big { font-size: 30px; font-weight: 700; line-height: 1; margin-top: 8px;
                         letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.legacy .statcard .sub { font-size: 12.5px; color: var(--muted); margin-top: 6px; }

/* Q&A (admin/qa.php, qa.php). */
.legacy .qitem { border: 1px solid var(--hairline); border-radius: var(--r); padding: 16px 18px;
                 margin-bottom: 12px; background: var(--surface); }
.legacy .qq { font-weight: 600; }
.legacy .qmeta { font-size: 12px; color: var(--muted-3); margin-top: 8px; }
.legacy .qa-ans { margin-top: 10px; padding: 12px 14px; background: var(--bg-3);
                  border-left: 3px solid var(--pink); border-radius: 0 var(--r) var(--r) 0;
                  color: var(--ink-3); }
.legacy .serif { font-family: var(--serif); font-style: italic; color: var(--pink-deep); }

/* Upload dropzone (admin/files.php). */
.legacy .dropzone { border: 2px dashed var(--hairline); border-radius: var(--r-lg); padding: 26px 18px;
                    text-align: center; cursor: pointer; outline: none;
                    transition: border-color .15s, background .15s; }
.legacy .dropzone:hover, .legacy .dropzone:focus-visible { border-color: var(--hairline-2); }
.legacy .dropzone.drag { border-color: var(--pink); background: var(--pink-soft); }
.legacy .dropzone.busy { cursor: progress; opacity: .85; }
.legacy .dz-label { margin-top: 8px; font-weight: 600; }
.legacy .dz-status { margin-top: 6px; min-height: 18px; }

/* Collapsed detail blocks (admin/esign.php). */
.legacy details.acc { border: 1px solid var(--hairline); border-radius: var(--r); padding: 12px 16px;
                      margin-bottom: 10px; background: var(--surface); }
.legacy details.acc summary { cursor: pointer; font-weight: 600; }

/* Flash variants — NOT scoped to .legacy, deliberately.
   flash_html() in lib/ui.php emits class="flash ok" / class="flash err", and room_head() calls it
   on every page in the app, Phase 1 included. This file only ever defined .flash-ok / .flash-err,
   a naming the shared renderer has never used, so `border: 1px solid` on bare .flash fell through
   to currentColor and every flash in the room rendered as the same colourless ink-outlined box:
   "Approved — full access granted" and "Valid email required." were indistinguishable at a glance.
   Same colours as the hyphenated pair above, which stay for the pages that do write them. */
.flash.ok  { background: var(--green-soft); border-color: #C6E6D4; color: var(--green-text); }
.flash.err { background: #FDEBEE; border-color: #F5C7CF; color: #8A1B33; }

/* --- login (split screen) -------------------------------------------------- */
.login { display: flex; min-height: 100vh; }
.login-brand {
  flex: 0 0 46%; background: var(--ink); color: var(--bg);
  display: flex; flex-direction: column; justify-content: space-between;
  padding: 56px 64px; position: relative; overflow: hidden;
}
.login-brand .mono { color: var(--pink); }
.login-h1 { font-size: 52px; font-weight: 800; line-height: 1.08; letter-spacing: -.02em; text-wrap: pretty; }
.login-h1 .accent { color: var(--pink); font-size: .82em; }
.login-sub { margin-top: 22px; font-size: 16px; color: var(--hairline-2); max-width: 44ch; line-height: 1.6; }
.login-foot { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.086em; color: #6A70A0; }
.login-orb {
  position: absolute; right: -120px; top: -120px; width: 380px; height: 380px; border-radius: 50%;
  background: radial-gradient(circle, rgba(238,123,228,.16), transparent 70%);
}
.login-form { flex: 1; display: flex; align-items: center; justify-content: center; padding: 40px; }
.login-form-inner { width: 400px; }

/* --- flash ---------------------------------------------------------------- */
.flash { border-radius: var(--r); padding: 12px 14px; font-size: 14px; margin-bottom: 16px; border: 1px solid; }
.flash-ok  { background: var(--green-soft); border-color: #C6E6D4; color: var(--green-text); }
.flash-err { background: #FDEBEE; border-color: #F5C7CF; color: #8A1B33; }

/* --- responsive ------------------------------------------------------------ */
@media (max-width: 1080px) {
  .cols { grid-template-columns: 1fr; }
}
@media (max-width: 820px) {
  :root { --gutter: 16px; }

  /* `.nav { display: none }` used to live here, and it meant the data room had NO primary
     navigation on any screen under 820px — no Home, no Documents, no Q&A — with no hamburger or
     anything else in its place. Just the wordmark and Sign out. The comment below shows the admin
     sub-nav had already been rescued from this same rule; the main nav never was.
     That is about to matter: ~50 investors are being invited by email, and an email is opened on a
     phone. So instead of hiding it, the bar wraps and the nav takes its own row — see the
     max-width: 720px block above, which now applies from here down. */
  .topbar-inner {
    flex-wrap: wrap; height: auto; gap: 10px 12px;
    padding-top: 10px; padding-bottom: 10px;
  }
  .nav {
    order: 3; flex-basis: 100%; justify-content: flex-start;
    margin: 0 calc(-1 * var(--gutter)); padding: 0 var(--gutter);
  }
  .nav a { padding: 7px 12px; font-size: 14px; }

  /* The admin sub-nav must survive this breakpoint. It is markup'd as class="nav nav-sub" to
     inherit the link styling, which means `.nav { display: none }` above was hiding it — quietly
     undoing the whole point of adding it, which was that seven of the eight admin screens had no
     link anywhere. On a narrow window they became unreachable again. Scrolls sideways instead. */
  .nav-sub { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .topbar-sub .topbar-inner { height: auto; padding-top: 6px; padding-bottom: 6px; }

  /* The note that used to be here said the badge was gone because nothing implemented the
     shortcut, and hid the whole affordance to avoid leaving an empty clickable box. Both halves
     are now out of date: the command palette exists, the badge is back, and the box is no longer
     empty — it carries a drawn magnifier. So it stays as a 38px icon target rather than
     disappearing. Keeping it costs 38px of a row that has already given up the name and the room
     tag; losing it cost the only route to search on a phone. */
  /* height matches .icon-btn (38x38), its sibling in this same row. Collapsing the padding
     to make an icon target also collapsed the HEIGHT to the glyph — 38x18 — measured at
     375px on 14 Aug. WCAG 2.2 AA 2.5.8 wants 24x24, and the inline-link exception does not
     apply to a discrete control. It matters here more than on a wider screen: the comment
     above records that this box is the ONLY route to search on a phone, so there is no
     equivalent-control exception either. Width was already right; only height was missing. */
  .searchbox { padding: 0; width: 38px; height: 38px; justify-content: center; }
  .sb-label { display: none; }
  .who-name { display: none; }
  h1, .h1 { font-size: 28px; }
  .page { padding: 24px var(--gutter) 72px; }
  .login { flex-direction: column; }
  .login-brand { flex: none; padding: 32px 24px; }
  .login-h1 { font-size: 34px; }
  .login-form-inner { width: 100%; }
}

@media (max-width: 820px) {
  /* Let the document rows reflow on a phone instead of pushing the page sideways.
     .stack and .row are grid/flex children, and a grid/flex child defaults to min-width:auto —
     it will not shrink below its contents' min-content width. .row-meta and .chip are both
     white-space:nowrap, which pinned .stack's minimum at 536px inside a 343px column, so
     documents.php scrolled horizontally on every phone: measured 551px of content in a 375px
     viewport, with the Preview and Download buttons off the right edge of the screen.

     min-width:0 removes the artificial floor and flex-wrap lets the actions drop to a second
     line. Verified to be inert above this breakpoint — the rows measure 779x55 at 1280px with
     and without these rules, and the page's scroll width is identical either way. */
  .cols > *, .stack > * { min-width: 0; }
  .row { flex-wrap: wrap; min-width: 0; }
  .row-title { min-width: 0; overflow-wrap: anywhere; }
  .row-meta { white-space: normal; }

  /* A wide data table scrolls in its own box instead of pushing the whole page sideways.
     The admin screens carry tables of eight to eleven columns — the shareholder request queue
     measures 1064px — and with the table in normal flow that width became the page's width, so
     the header, the nav and the footer all scrolled off to the left along with it. Neither
     stylesheet ever handled this; app.css only shrank the font, which does not help at 375px.

     display:block is what makes the element scrollable. Checked rather than assumed, because
     it is a real change to how the table box is generated: the column positions are identical
     with and without this rule (header and body cells at 37/168/410/527px on the request queue,
     matching to the pixel), so the columns still line up — the table simply scrolls. */
  .page table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }

  /* ── SCROLL AFFORDANCE ON A PHONE — and a deliberate split, because the audit of 12 Aug asked
     for one treatment and the two cases want opposite ones.

     It reported: *"the sub-nav becomes a horizontal rail (right pattern — but with a visible grey
     scrollbar; hide it)"* and separately *"People reflows to cards but clips its role chips and
     Manage button at the right edge — half-visible pills signal broken, not dense."*

     Two corrections from reading the code.

     FIRST, the rail is not the sub-nav. `.nav-sub` is markup'd `class="nav nav-sub"` and inherits
     `.nav`'s `scrollbar-width: none` and `::-webkit-scrollbar { display: none }` — hidden since it
     was built. The bar that was seen belongs to `.adminbar`, which turns into a horizontal rail at
     900px and never had either rule. Hiding the sub-nav's bar would have been a fix to something
     already fixed, shipped next to the thing still broken.

     SECOND, People does not reflow to cards. There is no card layout for it in this file or in
     admin-people.php; it is a table, and at this breakpoint it becomes a horizontal scroller. The
     clipped chips and the half-visible Manage button ARE the scroll boundary. So "un-clip them" is
     not available — the columns do not fit in 390px and no amount of styling makes them.

     What is available is making the cut legible, and that is why these two get opposite treatment:

       A NAV RAIL hides its bar. Every item is a peer, the row is short, and swiping is the
       expected gesture — the bar is chrome over navigation.

       A DATA TABLE keeps its bar, styled slim. Content is cut off mid-row, and the bar is the only
       thing on screen saying so. Hiding it is exactly what makes a clipped table read as broken
       rather than as scrollable — it would have made the second finding worse while appearing to
       address it. */
  .adminbar { scrollbar-width: none; scroll-snap-type: x proximity; scroll-padding-left: 2px; }
  .adminbar::-webkit-scrollbar { display: none; }
  .adminbar-group { scroll-snap-align: start; }

  .page table,
  .legacy .table-scroll { scrollbar-width: thin; scrollbar-color: var(--hairline-2) transparent; }
  .page table::-webkit-scrollbar,
  .legacy .table-scroll::-webkit-scrollbar { height: 8px; }
  .page table::-webkit-scrollbar-track,
  .legacy .table-scroll::-webkit-scrollbar-track { background: transparent; }
  .page table::-webkit-scrollbar-thumb,
  .legacy .table-scroll::-webkit-scrollbar-thumb {
    background: var(--hairline-2); border-radius: 4px;
  }
  /* The last column stops short of the bezel, so a chip or a button at the end of a row is not
     sitting in the edge-swipe gutter where a tap gets taken for a back gesture.

     `!important` HERE IS LOAD-BEARING, and it is overriding inline styles rather than other CSS.
     The admin tables were built with `<td style="padding:12px 14px">` — 22 such cells across
     admin-people, admin-content, admin-nda and admin-analytics — and the `padding` shorthand sets
     padding-right too. An inline declaration beats any selector, so without this the rule applies
     to nothing on precisely the four tables the audit complained about, and works only on
     admin-funders, which has no inline padding and which nobody complained about.

     SHIPPED WITHOUT IT ON 13 Aug, and measured after the fact rather than before: 14px against a
     rule reading 16px. Present in the file, byte-verified on the server, applying to nothing. The
     websites workstream reported the identical shape the same day — a rule of theirs losing 0,1,0
     to an existing 0,2,0 — which is what prompted the measurement.

     The alternative is deleting the inline padding from 22 cells and moving it to a class. That is
     the better end state and it is a bigger, riskier change than one declaration inside a phone-only
     media query; it belongs with the People table work, not here. */
  .page table tr > *:last-child,
  .legacy .table-scroll tr > *:last-child { padding-right: 16px !important; }
}

@media (max-width: 720px) {
  /* The room tag goes first on a phone, as it did in the old chrome.
     .brand is a flex item with the default `flex: 0 1 auto`, so at 375px it is shrunk to 147px —
     but .room-tag inside it keeps its intrinsic width and simply spills out of the shrunken box,
     ending up underneath the avatar. Measured on an iPhone-width viewport: tag 145→230px, avatar
     175→209px, drawn one on top of the other. app.css hid the tag at exactly this breakpoint and
     this file never carried the rule across, so the collision arrived with the new shell and is on
     every page in the room — the investor-facing ones included. */
  .room-tag { display: none; }
  .brand { flex: none; }
  .topbar-inner { gap: 10px; }
}

/* --- print ---------------------------------------------------------------- */
@media print {
  .topbar, .nav, .icon-btn, .searchbox, .btn { display: none !important; }
  body { background: #fff; }
  .card { border: 1px solid #ccc; box-shadow: none; }
}

/* --- Q&A (DR-SPEC §4.5) ---------------------------------------------------- */
.chip.qa-open { color:#8A5A00; background:#FFF6E3; border-color:#F0DCB0; }
.chip.qa-pub  { color:var(--blue); background:var(--chip-blue-bg); border-color:var(--chip-blue-line); }
.qa-answer {
  margin-top: 14px; padding: 12px 16px;
  border-left: 3px solid var(--pink);
  background: var(--bg-3); border-radius: 0 var(--r) var(--r) 0;
  font-size: 14.5px;
}
textarea.field { resize: vertical; min-height: 96px; line-height: 1.55; }
select.field { appearance: none; background-image: none; }
.chip.qa-late { color:#A01020; background:#FFF1F3; border-color:#F0B8C4; }

/* --- RTL -------------------------------------------------------------------------------------
   Only the rules that are genuinely directional. Most of the layout is flexbox and grid, which
   mirror themselves once <html dir> is set, so this is short by design rather than by omission.
   Arabic is in the locale list from the start so this is exercised in development rather than
   discovered by a partner in the Gulf. */
[dir="rtl"] { text-align: right; }
[dir="rtl"] .nav, [dir="rtl"] .who { flex-direction: row-reverse; }
[dir="rtl"] .card-head { flex-direction: row-reverse; }
[dir="rtl"] ul, [dir="rtl"] ol { padding-right: 18px; padding-left: 0; }
[dir="rtl"] .bar { transform: scaleX(-1); }
/* Latin-script identifiers stay LTR inside RTL text — an email address or an ISO currency code
   reversed is not a translation, it is a corruption. */
[dir="rtl"] .mono-xs, [dir="rtl"] code, [dir="rtl"] .field[type="email"] { direction: ltr; unicode-bidi: embed; }

/* ── Cohort band ──────────────────────────────────────────────────────────────────────────────
   The hero at the top of the partner screens: the reach numbers, the bearing map, and the next
   date that matters. Deliberately restrained for a hero — this sits above a working funnel an
   operator uses every day, and a band that shouts drowns the screen it introduces. */
.hero-band {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(330px, 540px);
  gap: 28px; align-items: center;
  padding: 26px 28px; border-radius: var(--r); border: 1px solid var(--hairline);
  background: linear-gradient(135deg, var(--pink-soft) 0%, var(--surface) 58%);
}
@media (max-width: 900px) { .hero-band { grid-template-columns: 1fr; gap: 18px; } }

.reach { display: flex; flex-wrap: wrap; gap: 26px 38px; margin-top: 18px; }
.reach-n {
  font-size: 34px; font-weight: 700; line-height: 1; letter-spacing: -0.02em; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.reach-l { font-family: var(--mono); font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
           color: var(--muted-2); margin-top: 7px; }
.reach-part { font-size: 12px; color: var(--muted-3); margin-top: 3px; }

/* The map. Colours come from the stage scale so a dot means the same thing here as in the list. */
.cohort-map svg { width: 100%; height: auto; overflow: visible; }
.cohort-map figcaption { font-size: 11.5px; color: var(--muted-3); margin-top: 8px; text-align: center; }
.cm-ring        { fill: none; stroke: var(--hairline); stroke-width: 1; }
.cm-ring-label  { font-family: var(--mono); font-size: 10.5px; fill: var(--muted-3); letter-spacing: .02em; }
.cm-compass     { font-family: var(--mono); font-size: 10.5px; fill: var(--muted-2); letter-spacing: 0.060em; }
.cm-spoke       { stroke: var(--muted-3); stroke-width: 1; opacity: .38; }
.cm-spoke.cm-demo { stroke-dasharray: 3 3; opacity: .3; }
.cm-hub         { fill: var(--ink); }
.cm-hub-label   { font-family: var(--mono); font-size: 10.5px; fill: var(--ink); letter-spacing: 0.067em;
                  text-transform: uppercase; font-weight: 600; }
.cm-dot         { fill: var(--muted-2); stroke: var(--surface); stroke-width: 2; }
.cm-label       { font-size: 12.5px; font-weight: 600; fill: var(--ink); }
.cm-sub         { font-family: var(--mono); font-size: 10.5px; fill: var(--muted-3); letter-spacing: 0.060em; }
/* Stage 1 → 6, cool to warm to settled: the same progression the pips use. */
.cm-s1 { fill: #B9BEDC; } .cm-s2 { fill: #8B90B5; } .cm-s3 { fill: var(--pink-2); }
.cm-s4 { fill: var(--pink); } .cm-s5 { fill: var(--pink-deep); } .cm-s6 { fill: var(--green); }

.cd-chip { display: flex; align-items: center; gap: 13px; }
.cd-n { font-size: 40px; font-weight: 700; line-height: 1; color: var(--pink-deep);
        font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.cd-l { font-size: 12.5px; color: var(--muted); line-height: 1.45; }
.cd-d { font-family: var(--mono); font-size: 10.5px; color: var(--muted-3); letter-spacing: .06em; }

/* ── Relationship: owner card and touchpoint timeline ──────────────────────────────────────── */
.owner-card { display: flex; gap: 14px; align-items: flex-start; }
.owner-av {
  width: 46px; height: 46px; border-radius: 50%; flex: 0 0 46px;
  background: var(--pink-soft); color: var(--pink-deep);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-size: 14px; font-weight: 700; letter-spacing: .04em;
}
.tl { list-style: none; margin: 14px 0 0; padding: 0 0 0 22px; position: relative; }
.tl::before { content: ""; position: absolute; left: 5px; top: 6px; bottom: 6px; width: 1px; background: var(--hairline); }
.tl li { position: relative; padding: 0 0 15px; }
.tl li:last-child { padding-bottom: 0; }
.tl li::before {
  content: ""; position: absolute; left: -20px; top: 6px; width: 7px; height: 7px;
  border-radius: 50%; background: var(--pink); box-shadow: 0 0 0 3px var(--surface);
}
/* Observed events are drawn hollow. An operator scanning this needs to tell at a glance what the
   room noticed from what a person actually did — they carry very different weight. */
.tl li.tl-auto::before { background: var(--surface); border: 1.5px solid var(--muted-3); }
.tl-when { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em; text-transform: uppercase;
           color: var(--muted-3); }
.tl-kind { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em; text-transform: uppercase;
           color: var(--pink-deep); margin-left: 8px; }
.tl-body { font-size: 13.5px; color: var(--ink-3); line-height: 1.55; margin-top: 3px; }
.tl li.tl-auto .tl-body { color: var(--muted); }
.cd-unit { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.067em; text-transform: uppercase;
           color: var(--muted-3); margin-right: 5px; }
.cm-leader { fill: none; stroke: var(--muted-3); stroke-width: .8; opacity: .5; }
/* The viewer's own institution, on a map where everyone else is a country. */
.cm-dot.cm-mine   { stroke: var(--pink-deep); stroke-width: 3; }
.cm-label-mine    { fill: var(--pink-deep); font-weight: 700; }

/* ── Watermark tuning ─────────────────────────────────────────────────────────────────────────
   Sliders beside a live, real stamped page. The preview is deliberately given as much room as the
   controls: the operator is judging legibility, and a thumbnail cannot answer that question. */
.wm-tune {
  margin-top: 16px; display: grid; gap: 26px;
  grid-template-columns: minmax(0, 1fr) minmax(240px, 340px); align-items: start;
}
@media (max-width: 820px) { .wm-tune { grid-template-columns: 1fr; } }

.wm-slider + .wm-slider { margin-top: 16px; }
.wm-slider .label { display: flex; justify-content: space-between; align-items: baseline; }
.wm-val {
  font-family: var(--mono); font-size: 11px; letter-spacing: .06em;
  color: var(--pink-deep); text-transform: none; font-weight: 600;
}
.wm-slider input[type="range"] {
  width: 100%; margin-top: 7px; accent-color: var(--pink-deep);
  height: 22px; cursor: pointer; background: transparent;
}
.wm-slider input[type="range"]:focus-visible { outline: 2px solid var(--pink-deep); outline-offset: 3px; }

.wm-preview { margin: 0; }
.wm-preview iframe {
  width: 100%; aspect-ratio: 210 / 297; border: 1px solid var(--hairline);
  border-radius: var(--r); background: var(--surface); display: block; box-shadow: var(--shadow);
}
.wm-preview figcaption { margin-top: 8px; text-align: center; }

/* ── Commercial gate ──────────────────────────────────────────────────────────────────────────
   The two roadmap gates (B1 cohort, B2 binding agreements) plus the two operational counts, at
   the top of Partners. These are the numbers the programme is judged on, so they lead. */
.gate {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px;
  background: var(--hairline); border: 1px solid var(--hairline);
  border-radius: var(--r); overflow: hidden; margin-bottom: 22px;
}
@media (max-width: 900px) { .gate { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.gate-item { background: var(--surface); padding: 18px 20px 20px; }
.gate-l { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.095em; text-transform: uppercase; color: var(--muted-3); }
.gate-n {
  margin-top: 9px; font-size: 32px; font-weight: 700; line-height: 1; color: var(--ink);
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
}
.gate-n.is-met  { color: var(--green-text); }
.gate-n.is-warn { color: var(--pink-deep); }
.gate-of { font-size: 17px; font-weight: 600; color: var(--muted-3); letter-spacing: 0; }
.gate-bar { margin-top: 12px; height: 5px; border-radius: var(--r-pill); background: var(--bg-2); overflow: hidden; }
.gate-bar i { display: block; height: 100%; border-radius: var(--r-pill); background: var(--pink-deep); }
.gate-bar.is-b2 i { background: var(--green); }
.gate-sub { margin-top: 9px; font-size: 11.5px; color: var(--muted-3); line-height: 1.45; }
.gate-demo {
  grid-column: 1 / -1; background: var(--bg-3); padding: 9px 20px;
  font-size: 11.5px; color: var(--muted-2);
}

/* Add institution — collapsed, at the top of the page. */
.addinst { margin-bottom: 18px; }
.addinst > summary {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 13px; font-weight: 600; color: var(--muted);
  border: 1px solid var(--hairline); background: var(--surface);
  padding: 8px 14px; border-radius: var(--r-pill);
}
.addinst > summary:hover { color: var(--pink-deep); border-color: var(--pink-2); }
.addinst-plus { font-size: 15px; line-height: 1; }
.addinst-form {
  margin-top: 14px; padding: 18px 20px; background: var(--surface);
  border: 1px solid var(--hairline); border-radius: var(--r);
  display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap;
}


/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   SIGN-IN PAGES — design workstream handback, 4 August 2026
   Scoped to body.signin so nothing here can reach the room, the admin screens or the .legacy
   wrapper. The collaborate delta is scoped again to body.collab: the two doors share one template,
   so an unscoped rule written for one silently moves the other.
   Every colour is an existing token — the pack introduced none.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */

/* Rules are written against the app’s existing tokens — same names as room.css.
   Everything here is ADDITIVE or replaces the login-only rules; nothing touches .legacy.
   Demo-bar (.hud) rules already stripped. Bump DR_VERSION when shipping. */

/* THE TOKEN BLOCK THAT USED TO SIT HERE IS GONE — 13 Aug 2026.

   The sign-in handback of 4 August arrived with a full copy of the token set, and the header
   above it says: "Scoped to body.signin so nothing here can reach the room" and "Every colour is
   an existing token — the pack introduced none."

   Both sentences describe an intent the CSS did not implement. `:root` is the html element; it is
   not scoped to anything. Sitting 1,100 lines below the real declarations, this copy won on
   ordering and pinned all 27 tokens at their 4 August values across the entire room.

   What that actually cost, measured before removing it — three colours had been corrected since
   and none of the corrections was reaching a screen:

     --pink-deep   authored #A6359C, rendering #D14EC6   (3.74:1 on white — the contrast
                                                          fix made and shipped on 13 Aug,
                                                          live in the file, dead in the cascade)
     --muted-2     authored #63699B, rendering #8B90B5
     --muted-3     authored #646999, rendering #686D9C

   The block declared nothing the file did not already declare — 27 redeclarations, 0 new tokens —
   so deleting it changes only which of two values wins, and the winner is now the authored one.

   This is the same shape as the exclusion-list reasoning in the root CLAUDE.md: a comment
   describing what a rule was meant to reach is not a statement about what it can reach. The test
   that now enforces this is tools/tests/test-token-cascade.php. */

*,*::before,*::after{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg)}
body{font-family:var(--sans);color:var(--ink);-webkit-font-smoothing:antialiased}
a{color:var(--ink);text-decoration:none}
a:hover{color:var(--pink-deep)}
input,button{font-family:var(--sans);font-size:inherit}
::placeholder{color:var(--muted-3)}
.mono{font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase}
.mono-xs{font-family:var(--mono);font-size: 10.5px;letter-spacing: 0.060em;font-weight:600}
@keyframes fadeUp{from{transform:translateY(12px);opacity:0}to{transform:none;opacity:1}}
@keyframes rings{0%{transform:scale(1);opacity:.55}100%{transform:scale(1.75);opacity:0}}
@keyframes stripe{to{background-position:16px 0}}
@keyframes nudge{0%,100%{transform:none}30%{transform:translateX(-4px)}60%{transform:translateX(3px)}}
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}}
.login{display:flex;min-height:100vh}
/* 50/50, and it stays 50/50. The panel was 46% under a 760px cap, so past ~1650px the cap bound and
   every further pixel of window went to the pale side — the ink half stopped growing and the split
   drifted. Operator's instruction, 4 Aug 2026: the halves are equal at every desktop width. */
.brandpanel{flex:0 0 50%;background:var(--ink);color:var(--bg);display:flex;flex-direction:column;justify-content:space-between;gap:28px;padding:52px 60px 44px;position:relative;overflow:hidden}
.brandpanel>*{position:relative;z-index:1}
.orb{position:absolute;right:-130px;top:-130px;width:420px;height:420px;border-radius:50%;background:radial-gradient(circle,rgba(238,123,228,.17),transparent 70%);z-index:0}
.orb2{position:absolute;left:-160px;bottom:-180px;width:460px;height:460px;border-radius:50%;background:radial-gradient(circle,rgba(59,91,219,.14),transparent 70%);z-index:0}
.b-eyebrow{color:var(--pink);animation:fadeUp .6s ease both}
.b-h1{font-size:clamp(32px,3.4vw,50px);font-weight:800;line-height:1.08;letter-spacing:-.02em;text-wrap:pretty;margin:0;animation:fadeUp .6s .07s ease both}
.b-h1 .accent{font-family:var(--serif);font-style:italic;font-weight:400;color:var(--pink);font-size:.84em}
.b-sub{margin:20px 0 0;font-size:15.5px;color:var(--hairline-2);max-width:44ch;line-height:1.65;animation:fadeUp .6s .14s ease both}
.schematic{margin:38px 0 0;animation:fadeUp .6s .22s ease both}
.schematic svg{width:100%;max-width:560px;height:auto;display:block;overflow:visible}
.sysline{margin-top:14px;font-family:var(--mono);font-size: 10.5px;letter-spacing: 0.086em;color:var(--muted-2);display:flex;align-items:center;gap:9px}
.sysline::before{content:"";width:6px;height:6px;border-radius:2px;background:var(--pink);flex:none}
.b-foot{font-family:var(--mono);font-size: 10.5px;letter-spacing: 0.086em;color:#6A70A0}
.formside{position:relative;overflow:hidden;flex:1;display:flex;align-items:center;justify-content:center;padding:44px 36px}
/* SCOPED TO .formside, and it must stay scoped. Written for the sign-in redesign as a bare
   `.card`, this sat after the app-wide `.card` at line 193 and therefore won every page in the
   room — carrying the sign-in card's 472px width into an 884px main column, so 412px of every
   page was dead space, document rows wrapped, and titles broke over two lines. Measured
   9 Aug 2026 against the design handoff: main-column cards rendered at 472px where the design
   has them at 884px. It also imposed --shadow-lg and a fade-up entrance on every card in the
   app, both of which the style overview rules out ("borders, not shadows"; "no entrance
   animations on content"). One unscoped selector, four screens and fifteen admin pages. */
.formside .card{position:relative;z-index:1;width:min(472px,100%);background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-xl);padding:34px 38px 30px;box-shadow:var(--shadow-lg);animation:fadeUp .55s .1s ease both}
.card-top{display:flex;align-items:center;gap:10px}
.wordmark{height:26px;width:auto;display:block}
.room-tag{border:1.5px solid var(--ink);border-radius:var(--r-sm);padding:2px 8px;font-family:var(--mono);font-size: 10.5px;letter-spacing: 0.060em;white-space:nowrap}
.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:26px 0 4px;padding:0;list-style:none}
.steps li{display:grid;grid-template-rows:34px auto;gap:6px;align-content:start}
.s-top{display:flex;align-items:flex-start;gap:7px}
.s-num{width:21px;height:21px;margin-top:-1px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size: 10.5px;font-weight:600;background:var(--bg-2);color:var(--muted-3);transition:background .3s,color .3s}
.s-lab{font-size:12px;font-weight:500;color:var(--muted-3);line-height:1.25}
.s-flag{font-family:var(--mono);font-size:10.5px;letter-spacing:.02em;font-weight:600;color:var(--pink-deep);background:var(--pink-soft);border-radius:4px;padding:2px 5px;margin-left:auto}
.s-track{display:block;height:3px;border-radius:99px;background:var(--bg-2);overflow:hidden}
.s-track b{display:block;height:100%;width:0;border-radius:99px;background:var(--pink);transition:width .6s ease}
.steps li.on .s-num{background:var(--ink);color:#fff}
.steps li.on .s-lab{color:var(--ink);font-weight:700}
.steps li.on .s-track b{width:42%}
.steps li.wait .s-track b{width:100%;background:repeating-linear-gradient(90deg,var(--pink) 0 8px,var(--pink-2) 8px 16px);animation:stripe .9s linear infinite}
.intro{margin-top:16px;font-size:14.5px;color:var(--muted);line-height:1.62}
.flashwrap{margin-top:16px}
/* THE GRID IS FOR FLASHES THAT HAVE AN ICON, AND ONLY THOSE.
   This was a bare `.flash{display:grid;grid-template-columns:30px 1fr}` — two columns, an icon and
   a body. enter.php builds that structure; NOTHING ELSE IN THE APP DOES. flash_html() returns
   `<div class="flash flash-ok">text</div>`, one text node, and a bare text node inside a grid
   container becomes an anonymous grid item — placed in column one, which is 30px wide. So every
   flash outside the sign-in page rendered one word per line down the page, including the one an
   investor sees immediately after signing the confidentiality agreement.

   :has() rather than a modifier class on the caller, deliberately. A modifier has to be remembered
   by every future flash; this way the layout follows the markup that is actually present, which is
   the assumption that broke. Where :has() is unsupported the icon flash degrades to readable text
   with an inline "!", which is the right direction to fail in. */
.flash{border-radius:var(--r-lg);padding:14px 16px;border:1px solid;animation:fadeUp .4s ease both}
.flash:has(.f-ic){display:grid;grid-template-columns:30px 1fr;gap:12px;align-items:start}
/* enter.php writes `flash err`; flash_html() writes `flash flash-err`. This rule matched only the
   first, so a session flash on this shell drew `border: 1px solid currentColor` and no background
   — the border-colour half of the same bug the comment at the top of this file describes. */
.flash.err,.flash.flash-err{background:#FDEBEE;border-color:#F5C7CF;color:#8A1B33}
.flash.ok,.flash.flash-ok{background:var(--green-soft);border-color:#C6E6D4;color:#0F5C36}
.flash .f-ic{width:30px;height:30px;border-radius:8px;display:flex;align-items:center;justify-content:center;background:rgba(138,27,51,.1)}
.flash .f-t{font-weight:700;font-size:14px;line-height:1.3}
.flash .f-b{font-size:13px;line-height:1.55;margin-top:3px}
.flash .f-tag{font-family:var(--mono);font-size: 10.5px;letter-spacing: 0.060em;margin-top:8px;opacity:.85}
.label{display:block;font-size:13px;font-weight:600;margin:0 0 6px}
.field{display:block;width:100%;border:1.5px solid var(--hairline);border-radius:var(--r);padding:12px 13px;font-size:14.5px;color:var(--ink);background:var(--surface);transition:border-color .15s,box-shadow .15s}
.field:focus{outline:2px solid var(--pink-deep);outline-offset:1px;border-color:var(--pink-deep);box-shadow:0 0 0 3px rgba(238,123,228,.16)}
.field.bad{border-color:#D3455E;box-shadow:0 0 0 3px rgba(211,69,94,.12);animation:nudge .3s ease}
.f-hint{display:none;font-size:12.5px;color:#8A1B33;margin-top:6px}
.f-hint.show{display:block}
/* THE SAME BUG AS .card ABOVE, found 9 Aug 2026 while checking the fix for it.
   These are the sign-in page's button rules, written unscoped. Sitting ~560 lines after the
   app-wide button rules, this one re-declared `background: var(--ink)` and, being equal
   specificity but later, beat the pink primary-button variant everywhere in the room.

   The consequence was not cosmetic. That variant is the design's ONE primary action per screen —
   "Resume at page 4" on the investor home, "Ask a question", "Send question". Every one of them
   was rendering navy, identical to the secondary buttons beside it, so no screen had a visible
   primary action at all. The accent discipline the whole style system rests on ("at most one
   pink button per view — it marks the primary moment") was switched off in CSS.

   Scoped to .login, which wraps both halves of the sign-in page. The sweep animation is
   sign-in-only too and goes with it.

   (The class name is spelled out above only in prose form deliberately: test-contrast.php
   scans this file with a regex that does not strip comments, so writing the literal selector in
   a comment before an unrelated `color:#fff` rule makes that test fail on the comment. Worth
   fixing in the test — noted, not done here, because a test and the code it guards should not
   change in the same commit.) */
.login .btn{position:relative;overflow:hidden;display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1.5px solid var(--ink);background:var(--ink);color:#fff;border-radius:var(--r);padding:12px 18px;font-size:14.5px;font-weight:700;cursor:pointer;transition:background .12s,border-color .12s,transform .08s}
.login .btn:hover{background:var(--ink-2);border-color:var(--ink-2)}
.btn:active{transform:scale(.99)}
.btn .sweep{position:absolute;left:0;top:0;bottom:0;width:0;background:linear-gradient(90deg,rgba(238,123,228,.35),rgba(238,123,228,.15));pointer-events:none}
.login .btn-ghost{background:var(--surface);color:var(--ink);border:1.5px solid var(--hairline);font-weight:600}
.login .btn-ghost:hover{background:var(--bg-3);border-color:var(--hairline-2);color:var(--ink)}
.w100{width:100%}
details.pw{margin-top:16px}
details.pw summary{cursor:pointer;font-size:13.5px;color:var(--muted);list-style:none;display:flex;align-items:center;gap:7px;width:fit-content}
details.pw summary::-webkit-details-marker{display:none}
details.pw summary .chev{transition:transform .2s;font-size: 10.5px;color:var(--muted-3)}
details.pw[open] summary .chev{transform:rotate(90deg)}
details.pw summary:hover{color:var(--pink-deep)}
details.pw .pw-body{margin-top:12px;animation:fadeUp .3s ease both}
.caps{display:none;font-family:var(--mono);font-size:10.5px;letter-spacing:.03em;font-weight:600;color:#8A5A00;background:#FFF6E3;border:1px solid #F0DCB0;border-radius:4px;padding:2px 6px;margin-left:8px;vertical-align:2px}
.caps.show{display:inline-block}
.divider{display:flex;align-items:center;gap:12px;margin:20px 0}
.divider span:first-child,.divider span:last-child{flex:1;height:1px;background:var(--hairline)}
.divider .mono-xs{color:var(--muted-3)}
.oauth a+a{margin-top:10px}
body.minimal .oauth,body.minimal .divider{display:none}
.assure{display:flex;gap:11px;align-items:flex-start;margin-top:26px;padding-top:17px;border-top:1px solid var(--hairline)}
.assure svg{flex:none;margin-top:1px}
.assure p{margin:0;color:var(--muted-3);font-size:12.5px;line-height:1.6}
.sent{text-align:left;animation:fadeUp .45s ease both}
.beacon{position:relative;width:64px;height:64px;margin:18px 0 18px}
.beacon .core{position:absolute;inset:0;border-radius:50%;background:var(--pink-soft);display:flex;align-items:center;justify-content:center}
.beacon .ring{position:absolute;inset:0;border-radius:50%;border:1.5px solid var(--pink);animation:rings 2.4s ease-out infinite}
.beacon .ring.r2{animation-delay:1.2s}
.sent h2{font-size:24px;font-weight:800;letter-spacing:-.01em;margin:0}
.sent .s-body{margin:10px 0 0;font-size:14.5px;color:var(--muted);line-height:1.65}
.addr{font-family:var(--mono);font-size:12px;background:var(--bg-3);border:1px solid var(--hairline);border-radius:var(--r-sm);padding:2px 7px;color:var(--ink);white-space:nowrap}
.sent-meta{margin-top:14px;font-family:var(--mono);font-size: 10.5px;letter-spacing: 0.060em;color:var(--muted-3);text-transform:uppercase}
.mailapps{display:flex;gap:10px;margin-top:18px}
.mailapps a{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:7px;border:1.5px solid var(--hairline);border-radius:var(--r);padding:9px 12px;font-size:13px;font-weight:600;color:var(--ink);background:var(--surface)}
.mailapps a:hover{background:var(--bg-3);border-color:var(--hairline-2);color:var(--ink)}
.spam{margin:20px 0 0;font-size:13px;color:var(--muted);line-height:1.6}
.spam a{color:var(--pink-deep);font-weight:600;cursor:pointer}
.spam .cool{font-family:var(--mono);font-size:11px;color:var(--muted-3);margin-left:4px}
.backlink{display:inline-block;margin-top:16px;font-size:13px;font-weight:600;color:var(--muted);cursor:pointer}
.backlink:hover{color:var(--pink-deep)}
.driftw{position:absolute;z-index:0;pointer-events:none}
.drift{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;color:var(--muted-2);background:var(--drift-bg);border:1px solid var(--hairline);border-radius:6px;padding:5px 9px;animation:bob 6s ease-in-out infinite alternate;white-space:nowrap}
.drift::before{content:"";width:5px;height:5px;border-radius:1.5px;background:var(--pink)}
@keyframes bob{from{transform:translateY(-4px)}to{transform:translateY(4px)}}
@keyframes stepping{0%{box-shadow:0 0 0 0 rgba(238,123,228,.5)}100%{box-shadow:0 0 0 10px rgba(238,123,228,0)}}
.s-num.ping{animation:stepping .9s ease-out}
.steps li.on.primed .s-track b{width:70%}
#sendBtn{transition:box-shadow .18s,transform .12s}
#sendBtn:hover{transform:translateY(-1px);box-shadow:0 8px 20px rgba(238,123,228,.28)}
#sendBtn.primed{box-shadow:0 0 0 3px rgba(238,123,228,.22)}
.mote{position:fixed;z-index:60;width:9px;height:9px;border-radius:50%;background:#EE7BE4;box-shadow:0 0 12px 3px rgba(238,123,228,.55);pointer-events:none}
@keyframes orbdrift{from{transform:translate(0,0) scale(1)}to{transform:translate(-22px,16px) scale(1.08)}}
.orb{animation:orbdrift 9s ease-in-out infinite alternate}
.orb2{animation:orbdrift 12s ease-in-out infinite alternate-reverse}
@media (max-width:1100px){.driftw{display:none}}
/* Tighter padding as it narrows — but the basis stays 50%, it is not a proportion that shrinks. */
@media (max-width:1080px){.brandpanel{padding:40px 36px 36px}}
@media (max-width:860px){.login{flex-direction:column}.brandpanel{flex:none;max-width:none;padding:30px 24px 26px}.b-sub{margin-top:12px;font-size:14px}.schematic{margin-top:22px}.b-foot{display:none}/* 90px of bottom padding here was clearance for the fixed badge; it is in the
   flow now, so the gap is the badge's own margin instead of dead space above it. */
.formside{padding:28px 16px 22px}.card{padding:26px 22px 24px}.s-lab{font-size:11px}.b-h1{font-size:29px}}


/* ---- collaborate door: mirror, dotfield, build cells ---------------------------------------- */
/* collaborate.php — shares room.css with enter.php. Delta vs the investor login:
   .login{flex-direction:row-reverse} (mirror), #dotfield canvas rules, .driftw/.drift chips,
   its own schematic SVG, build-cell + ripple JS. Everything else is identical to enter.php.
   Demo-bar (.hud) rules stripped. */
body.collab .login{display:flex;flex-direction:row-reverse;min-height:100vh}
/* The mirror puts the ink panel under the badge's corner, so the ACN line runs beneath it — on the
   live page the last digits of the ACN sat behind the plate. The investor door has the pale form
   side there and needs no inset. Hidden below 860px, so this rule only ever applies on desktop. */
body.collab .b-foot{padding-right:96px}
body.collab #dotfield{position:absolute;inset:0;z-index:0;pointer-events:none}
body.collab .driftw{position:absolute;z-index:0;pointer-events:none;will-change:transform}
body.collab .drift{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;color:var(--muted-2);background:var(--drift-bg);border:1px solid var(--hairline);border-radius:6px;padding:5px 9px;animation:bob 6s ease-in-out infinite alternate}

/* Mobile: the mirror must collapse like the investor door.
   Scoping the delta to body.collab raised its specificity above the `.login` rule inside the
   860px media query, so row-reverse survived the breakpoint and the stacked layout rendered as an
   empty brand panel with the card off-screen. The responsive rule is therefore scoped to match. */
@media (max-width:860px){body.collab .login{flex-direction:column}}

/* ---- Built by Maslow stamp (all sign-in pages) ----------------------------------------------
   ONE block. There were three, in three places, and the last one silently won for the investor
   door while `body.collab .stamp` out-specified the responsive rules and won for the other — so
   the collaborate badge kept its desktop offsets at every width. Same specificity accident as the
   mirrored .login rule twenty lines up. Consolidated so there is one place to be wrong.

   DESKTOP: fixed in the viewport corner, and the <img> is a direct child of <body> because any
   transformed ancestor would re-anchor it and it would drift off the corner.

   MOBILE: NOT fixed. Below 860px the panels stack, the page grows taller than the viewport, and a
   fixed badge then floats over whatever is beneath it — which at the top of the page is the email
   field, and on the collaborate door the submit button. Confirmed on the live page, not reasoned
   about: it was sitting across the input on both doors. So it leaves the viewport and takes its
   place in the flow after the card, which is where it reads correctly anyway.

   The plate flips with it. Choose by the ground under the badge, never by the app's theme: stacked,
   the last thing on the page is the pale form side on BOTH doors, so both use navy there even
   though the collaborate door is off-white on desktop. */
.stamp{position:fixed;right:24px;bottom:24px;width:120px;height:auto;z-index:40;pointer-events:none;opacity:.9;filter:drop-shadow(0 2px 6px rgba(28,36,85,.12))}
body.collab .stamp{filter:drop-shadow(0 4px 12px rgba(4,8,24,.5))}
/* WHICH PLATE SHOWS IS DECIDED HERE AND NOWHERE ELSE. The rules below set position, margin and
   filter — never `display`. The first draft of this block set `display:block` in the
   `body.collab .stamp` rule, which out-specifies `.stamp-wide{display:none}` and put BOTH plates on
   the collaborate door at once. That is the third time this stylesheet has been bitten by a
   body.collab rule beating a single-class one; keeping the two concerns in separate rules is what
   stops it happening a fourth. */
.stamp-narrow{display:none}
@media (max-width:860px){
  .stamp{position:static;width:120px;margin:0 16px 26px auto;opacity:.85;filter:drop-shadow(0 2px 6px rgba(28,36,85,.12))}
  body.collab .stamp{position:static;margin:0 16px 26px auto;filter:drop-shadow(0 2px 6px rgba(28,36,85,.12))}
  .stamp-wide{display:none}
  .stamp-narrow{display:block}
}
@media (max-width:520px){.stamp{width:104px}}

/* ---- schematic: the envelope travelling the connector ---------------------------------------
   HANDBACK.md line 41 says the schematics are "inline SVG + SMIL — no JS to animate". The
   delivered .svg files contain no animation elements at all, so both doors shipped static.

   Adding the SMIL did not work either, and the way it failed is worth recording. The animation was
   correct — seeking the timeline by hand (svg.setCurrentTime(2)) moved the envelope exactly as
   intended — but the SVG's own clock never advanced from 0, with animationsPaused() false. Frozen
   at t=0 the opacity track held the envelope at opacity:0, so the SMIL version was WORSE than the
   static one the pack delivered: not still, but invisible.

   CSS keyframes have no separate timeline to freeze. Reduced motion is already handled by the
   blanket rule at the top of this file, so this needs no guard of its own.

   Both doors' main connector is a horizontal run at y=118 (investor "M56 118 H448", collaborate
   "M40 118 H455"), so a translate reproduces the delivered path exactly rather than approximating
   it. The travellers are authored at the origin, which is why the start offset is the path's own
   start point. Collaborate's curved feeders (#f1/#f3) are NOT animated — a straight translate
   would cut the corner, and inventing a curve is the design workstream's call, not mine. */
@keyframes envFwd{0%{opacity:0}10%{opacity:1}88%{opacity:1}100%{opacity:0}}
.schematic #gFwd,.schematic #gRev{animation:envFwd 4.2s linear infinite}
.schematic #gFwd{animation-name:envFwd,travelFwd}
.schematic #gRev{animation-name:envFwd,travelRev}
@keyframes travelFwd{from{transform:translate(56px,118px)}to{transform:translate(448px,118px)}}
@keyframes travelRev{from{transform:translate(448px,118px)}to{transform:translate(56px,118px)}}
body.collab .schematic #gFwd{animation-name:envFwd,travelFwdC}
body.collab .schematic #gRev{animation-name:envFwd,travelRevC}
@keyframes travelFwdC{from{transform:translate(40px,118px)}to{transform:translate(455px,118px)}}
@keyframes travelRevC{from{transform:translate(455px,118px)}to{transform:translate(40px,118px)}}

/* ---- Built by Maslow, room page footer ------------------------------------------------------
   In the document flow at the foot of every signed-in page, right-aligned beside the confidential
   line. Not fixed: content pages scroll far further than the sign-in doors, and a fixed badge
   would sit permanently over a document row. Stacks below the line on narrow screens so the ACN
   never runs underneath it — the collision the collaborate door had to be fixed for. */
.room-footrow{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;
  border-top:1px solid var(--hairline);padding-top:18px}
.room-stamp{flex:none;width:104px;height:auto;opacity:.45;transition:opacity .15s}
.room-stamp:hover{opacity:.75}
@media (max-width:720px){
  .room-footrow{flex-direction:column;align-items:flex-start;gap:14px}
  .room-stamp{align-self:flex-end}
}

/* --- deal progress stepper (DESIGN-SPEC §4.2) ----------------------------- */
/* RENAMED to .dprog-*, 10 Aug 2026, after shipping it as .step*. `.steps` already existed for
   the sign-in page's three-step gate — `grid-template-columns: repeat(3,1fr)` — and my rule did
   not set that property, so five stages laid out three-across. The same unscoped-duplicate-class
   failure this session spent its first hours removing from .card and .btn, reintroduced by me
   under a new name. A prefix nothing else uses is the fix; test-room-layout.php now asserts it.

   Small circles joined by a 2px line: completed filled, current outlined in the accent, future
   plain. The style overview names this pattern and uses it identically for deal stages,
   onboarding journeys and signature flows — so it is built once, here, and reused. */
.dprog { display: grid; gap: 0; margin-top: 14px; }
.dprog-step { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 12px; }
.dprog-rail { display: flex; flex-direction: column; align-items: center; }
.dprog-dot {
  flex: none; width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid var(--hairline-2); background: var(--surface);
  display: flex; align-items: center; justify-content: center;
  font-size: 10.5px; font-weight: 800; color: #fff;
}
.dprog-dot.is-done { background: var(--green); border-color: var(--green); }
.dprog-dot.is-now  { border-color: var(--pink); border-width: 3px; }
.dprog-dot.is-now::after { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--pink); }
/* The connector stops at the last step rather than trailing into nothing. */
.dprog-line { flex: 1; width: 2px; min-height: 16px; background: var(--hairline); }
.dprog-step:last-child .dprog-line { display: none; }
.dprog-line.is-done { background: var(--green); }
.dprog-body { padding-bottom: 14px; }
.dprog-step:last-child .dprog-body { padding-bottom: 0; }
.dprog-name { font-size: 13.5px; font-weight: 700; color: var(--ink); }
.dprog-step.is-future .dprog-name { color: var(--muted-2); font-weight: 600; }
.dprog-note { font-size: 12px; color: var(--muted-3); margin-top: 2px; }

/* ── DILIGENCE CHECKLIST ────────────────────────────────────────────────────────────────────
   Rows under the stepper, sharing its rhythm deliberately: it is the same question ("where am I
   in this?") answered at a finer grain, and a second visual language would make it read as a
   different feature.

   The two box styles are the whole point and must stay visibly different. .dg-box is a button —
   the investor's claim, pressable. .dg-box-room is a span with no affordance — the room's
   measurement, which nobody types. If those ever converge, a tick stops carrying its own
   provenance and the list becomes decorative. */
.dg-row { display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 10px; margin-top: 12px; }
.dg-box {
  flex: none; width: 18px; height: 18px; border-radius: 5px; padding: 0;
  border: 2px solid var(--hairline-2); background: var(--surface);
  font-size: 10.5px; font-weight: 800; color: #fff; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; transition: border-color .15s, background .15s;
}
.dg-box:hover { border-color: var(--pink); }
.dg-box:focus-visible { outline: 2px solid var(--pink); outline-offset: 2px; }
/* The room's own boxes are not pressable and do not pretend to be. */
.dg-box-room { cursor: default; border-radius: 50%; }
.dg-row.is-done .dg-box { background: var(--green); border-color: var(--green); }
/* min-width:0 is load-bearing, not decoration: a grid track defaults to min-content width, so a
   long unbroken title would push the row wider than the rail instead of wrapping inside it. */
.dg-body { min-width: 0; }
.dg-title { font-size: 13px; font-weight: 700; color: var(--ink); }
.dg-row.is-done .dg-title { color: var(--muted-2); }
.dg-detail { font-size: 12px; color: var(--muted-3); margin-top: 2px; }
/* Provenance, in the row rather than in a legend at the bottom that nobody reads. */
.dg-how { font-size: 11px; color: var(--muted-3); margin-top: 3px; font-style: italic; }

/* ── CROSS-DOCUMENT VIEW TRANSITIONS ────────────────────────────────────────────────────────
   The single highest-rated item in the 10 Aug UI audit, and the cheapest: the room is server
   -rendered PHP with full page loads, which is fine, but the white FLASH between them is what
   makes it feel like a set of pages rather than an application. This is the whole fix, in CSS,
   with no framework and no build step.

   The header carries a name, so it is treated as the SAME ELEMENT across the navigation and
   stays put while the content beneath it crossfades. Without that the header fades out and
   back in with everything else, which is a worse effect than no transition at all — the one
   fixed thing on the screen blinking is what the eye notices.

   Only the header is named. Naming the content too would ask the browser to morph one document
   list into another, and where the lists differ that produces a shuffle rather than a fade.
   One anchor, everything else crossfades around it.

   `prefers-reduced-motion` is handled by the rule at the top of this file, which already zeroes
   animation-duration globally — a view transition is an animation and obeys it. Browsers
   without support ignore the at-rule entirely and navigate exactly as they do today. */
@view-transition { navigation: auto; }
.topbar { view-transition-name: room-topbar; }

/* The crossfade itself: shorter than the browser default, on the platform curve, so it reads as
   responsiveness rather than as an effect being performed at you. */
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 180ms; animation-timing-function: cubic-bezier(.2,.8,.2,1); }

/* ── COMMAND PALETTE ────────────────────────────────────────────────────────────────────────
   A native <dialog>, which is why there is no focus-trap code and no scroll-lock code: the
   platform does both, plus Esc, plus inertness of everything behind it. That is a large amount
   of accessibility that would otherwise be hand-written and subtly wrong. */
#palette {
  width: min(620px, calc(100vw - 32px)); max-height: min(70vh, 560px);
  padding: 0; border: 1px solid var(--hairline); border-radius: var(--r-lg);
  background: var(--surface); color: var(--ink);
  box-shadow: 0 1px 2px rgb(16 24 40 / .06), 0 24px 60px -18px rgb(16 24 40 / .30);
  margin: 12vh auto auto; overflow: hidden;
}
#palette::backdrop { background: rgb(20 26 46 / .34); backdrop-filter: blur(2px); }
.pal-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px;
  border-bottom: 1px solid var(--hairline); color: var(--muted-2); }
#pal-q { flex: 1 1 auto; min-width: 0; border: 0; background: none; outline: none;
  font-family: var(--body); font-size: 15.5px; color: var(--ink); }
#pal-q::placeholder { color: var(--muted-3); }
.pal-esc, .pal-foot kbd, .sb-kbd {
  font-family: var(--mono); font-size: 10.5px; color: var(--muted-2);
  border: 1px solid var(--hairline); border-radius: 5px; padding: 2px 6px; background: var(--bg-2);
}
#pal-list { list-style: none; margin: 0; padding: 6px; overflow-y: auto; max-height: 46vh; }
.pal-group { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em; text-transform: uppercase;
  color: var(--muted-3); padding: 12px 10px 6px; }
.pal-item { display: flex; align-items: center; gap: 10px; padding: 9px 10px;
  border-radius: var(--r-sm); cursor: pointer; }
/* Selection is one class driven by the keyboard, and :hover is deliberately NOT styled the same
   way — with both, moving the mouse while arrowing shows two selected rows and neither is
   obviously the one Enter will open. */
.pal-item.is-sel { background: var(--bg-3); }
.pal-item.is-sel .pal-t { color: var(--ink); font-weight: 600; }
.pal-ext { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em; color: var(--muted-2);
  border: 1px solid var(--hairline); border-radius: 4px; padding: 2px 5px; min-width: 40px;
  text-align: center; flex: none; }
.pal-t { font-size: 14px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pal-empty { padding: 22px 12px; font-size: 14px; color: var(--muted-2); text-align: center; }
.pal-empty a { color: var(--pink-deep); font-weight: 600; }
.pal-foot { display: flex; align-items: center; gap: 16px; padding: 9px 14px;
  border-top: 1px solid var(--hairline); background: var(--bg-2);
  font-size: 11.5px; color: var(--muted-3); }
.pal-foot .pal-count { margin-left: auto; font-family: var(--mono); font-size: 10.5px; }
.sb-kbd { margin-left: 10px; flex: none; }
/* The hint is the first thing to go when the bar tightens — it is an affordance, not content,
   and on a touch device there is no ⌘ to press. */
@media (max-width: 1180px), (pointer: coarse) { .sb-kbd { display: none; } }

/* ── THE DEMOTED "SINCE YOUR LAST VISIT" ────────────────────────────────────────────────────
   One line where a full card used to be, on the visits where nothing changed. It keeps the same
   dateline the card had, because "nothing new" without a since-when is a weaker statement than
   the card it replaced — the reader should still be able to see which window is being reported
   on. Quiet, but not vague. */
.delta-quiet {
  display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap;
  margin: 0 0 4px; padding: 2px 2px 10px;
  border-bottom: 1px solid var(--hairline);
  font-size: 13.5px; color: var(--muted-2);
}

/* A chip that goes somewhere has to look like it does, or it is a link nobody presses. The
   static chip keeps its exact appearance — only the linked variant gains affordance. */
.live-chip.is-link { text-decoration: none; transition: background .15s, border-color .15s, color .15s; }
.live-chip.is-link:hover { background: var(--bg-3); border-color: var(--pink); color: var(--ink); }
.live-chip.is-link:focus-visible { outline: 2px solid var(--pink-deep); outline-offset: 2px; }

/* ── Q&A empty state ────────────────────────────────────────────────────────────────────────
   The one screen that is guaranteed to be empty on an investor's first visit, since nobody has
   asked anything yet. Newsreader italic for the second clause, matching the editorial voice the
   room uses for its asides — the same pattern as "Good afternoon, Kane. Here's what changed." */
.qa-empty { text-align: center; padding: 30px 24px; }
.qa-empty-line { margin: 0; font-size: 17px; color: var(--ink); }
.qa-empty-line em { font-family: var(--serif); font-style: italic; color: var(--muted); }
.qa-empty .sub { max-width: 52ch; margin-left: auto; margin-right: auto; }
/* The composer grows with the question rather than scrolling a four-row box. */
#question { resize: vertical; min-height: 96px; transition: height .12s ease; }
@media (prefers-reduced-motion: reduce) { #question { transition: none; } }

/* ── FOCUS, EVERYWHERE ──────────────────────────────────────────────────────────────────────
   The room had six :focus-visible rules, each written for one component as it was built. Every
   button, every document link, every form field and the whole command palette had none — so a
   keyboard reader following a tab order through the documents list had no idea where they were.

   The 10 Aug UI audit lists this under polish. It is not: it is the difference between the room
   being operable without a mouse and not. An analyst reading 74 documents is exactly the person
   who works from the keyboard, and this is the one accessibility failure the product could not
   argue was somebody else's problem.

   :focus-visible rather than :focus, so a mouse click does not paint a ring — that is the whole
   reason the pseudo-class exists, and it is why this can be applied broadly without the visual
   noise that made everyone stop doing it in the :focus era.

   Specific components keep their own rules below this one where the geometry needs it (an inset
   ring inside a summary row, a tighter offset on a chip). This is the floor, not a replacement. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[role="option"]:focus-visible {
  outline: 2px solid var(--pink-deep);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
/* Fields already carry their own border and radius; a ring offset outside it reads as a halo. */
input:focus-visible, select:focus-visible, textarea:focus-visible { outline-offset: 1px; }

/* ── WATERMARKING, SAID ONCE AND MEANT ──────────────────────────────────────────────────────
   The audit asks for this to become "the product's luxury signature". The chips do that, but the
   reason they are worth having is narrower and more useful: they are the shortest true statement
   of what this room does that an investor can read without opening anything.

   Three words, each of which the product can actually back: the copy is marked, the delivery is
   recorded, and the mark identifies the account it was issued to. Nothing here claims the room
   can stop redistribution — it cannot, and saying so would be the kind of security theatre that
   costs credibility with exactly the audience that would notice. */
.wm-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.wm-chip {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.060em; text-transform: uppercase;
  color: var(--muted-2); background: var(--bg-2);
  border: 1px solid var(--hairline); border-radius: var(--r-pill);
  padding: 5px 10px; cursor: help;
}
.wm-chip:hover { color: var(--ink); border-color: var(--pink); }

/* ── TYPOGRAPHIC HYGIENE ────────────────────────────────────────────────────────────────────
   The audit: right-rail body copy is set small and tight. Opening the leading and capping the
   measure costs nothing and is the difference between reference text being read and skipped.
   text-wrap: pretty stops the widow on the last line, which is most visible in exactly these
   short paragraphs. */
.card-tight .meta, .card-tight .sub { line-height: 1.55; max-width: 60ch; text-wrap: pretty; }
.sub, .meta { text-wrap: pretty; }

/* ── THE GATE ───────────────────────────────────────────────────────────────────────────────
   The pre-NDA screen: the first thing a prospective investor sees, before they have committed
   to anything, and the one screen whose only job is to make signing feel both inevitable and
   worth it. It was a bulleted list with padlock emoji.

   GATE AND POST-SIGN ARE ONE LAYOUT IN TWO STATES, not two screens. `.is-open` is the second
   state, so signing returns to the same place and the locks lift rather than the page being
   replaced by a different page. That is the whole reason the choreography below is possible. */
.ndagate {
  border: 1px solid var(--hairline); border-radius: var(--r-xl);
  background: linear-gradient(180deg, var(--surface), var(--bg-2));
  padding: 26px 26px 24px; margin-bottom: 22px;
  box-shadow: 0 1px 2px rgb(16 24 40 / .04), 0 12px 32px -14px rgb(16 24 40 / .12);
}
.ndagate-head { border-left: 3px solid var(--pink); padding-left: 14px; }
.ndagate-line { margin: 6px 0 0; font-size: clamp(21px, 2.4vw, 27px); font-weight: 700;
  line-height: 1.25; color: var(--ink); letter-spacing: -.01em; }

/* ── OPEN TO YOU NOW ────────────────────────────────────────────────────────────────────────
   The introduction pack, as live links at full opacity. The old card TOLD the reader it existed
   and showed nothing, so the contrast between open and sealed had to be taken on trust. Shown,
   the sealed cards below mean something. */
.ndagate-open { margin-top: 20px; }
.ndagate-open-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 9px; }
.ndagate-open-doc {
  display: flex; align-items: center; gap: 9px; text-decoration: none;
  background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--r);
  padding: 8px 12px 8px 8px;
  transition: border-color .15s var(--ease, cubic-bezier(.2,.8,.2,1)), transform .15s;
}
.ndagate-open-doc:hover { border-color: var(--pink); transform: translateY(-1px); }
.ndagate-open-name { font-size: 13.5px; font-weight: 600; color: var(--ink); }

/* ── THE SEALED GRID ────────────────────────────────────────────────────────────────────────
   auto-fit, not a fixed column count: the folder tree decides how many cards there are. The
   brief said 3x3 because the tree it was written against had nine folders — building that in
   would have been designing for one snapshot of the content. */
.ndagate-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  gap: 10px; margin-top: 20px;
}
.ndagate-card {
  background: var(--bg-2); border: 1px solid var(--hairline); border-radius: var(--r);
  padding: 13px 14px 12px; opacity: .72;
  transition: opacity .22s var(--ease, cubic-bezier(.2,.8,.2,1)),
              transform .22s var(--ease, cubic-bezier(.2,.8,.2,1)),
              border-color .22s, background .22s;
}
/* Locked, but alive. A sealed card that does nothing on hover reads as disabled rather than as
   something waiting to be opened. */
.ndagate-card:hover { opacity: .92; transform: translateY(-2px); border-color: var(--muted-3); }
.ndagate-card-top { display: flex; align-items: center; justify-content: space-between; }
.ndagate-no   { color: var(--muted-3); letter-spacing: .16em; }
.ndagate-lock { color: var(--muted-3); flex: none; }
.ndagate-card-name  { margin-top: 9px; font-size: 13.5px; font-weight: 600; color: var(--ink);
  line-height: 1.3; }
.ndagate-card-count { margin-top: 4px; font-family: var(--mono); font-size: 10.5px;
  letter-spacing: 0.060em; color: var(--muted-3); font-variant-numeric: tabular-nums; }

/* ── THE UNLOCK ─────────────────────────────────────────────────────────────────────────────
   The one moment on this screen worth engineering. Signing returns here with .is-open: the locks
   fade, the cards rise to full opacity in sequence, and the kicker above has already changed to
   "YOURS TO READ". Six steps of stagger, then flat — beyond that the delay outlasts the
   animation and the last cards read as lag.

   The global prefers-reduced-motion rule at the top of this file zeroes all of it, which leaves
   the correct end state rather than a lesser one. */
.ndagate.is-open .ndagate-card { opacity: 1; animation: ndagateOpen .5s cubic-bezier(.2,.8,.2,1) both; }
.ndagate.is-open .ndagate-lock { opacity: 0; transition: opacity .4s ease; }
.ndagate.is-open .ndagate-card:nth-child(1) { animation-delay: 0ms; }
.ndagate.is-open .ndagate-card:nth-child(2) { animation-delay: 60ms; }
.ndagate.is-open .ndagate-card:nth-child(3) { animation-delay: 120ms; }
.ndagate.is-open .ndagate-card:nth-child(4) { animation-delay: 180ms; }
.ndagate.is-open .ndagate-card:nth-child(5) { animation-delay: 240ms; }
.ndagate.is-open .ndagate-card:nth-child(n+6) { animation-delay: 300ms; }
@keyframes ndagateOpen {
  from { opacity: .72; transform: translateY(7px) scale(.985); }
  to   { opacity: 1;   transform: none; }
}

.ndagate-cta { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 22px; }
.ndagate-assure { color: var(--muted-3); letter-spacing: .1em; }

/* ── THE SIGN-IN DOORS ON A PHONE ───────────────────────────────────────────────────────────
   Measured by the website workstream at 375×812 on the live pages, and the operator's benchmark
   is the registry's sign-in screen, where the card starts halfway down:

       registry        card at 0.50 screens · smallest text 8px
       /dataroom/      card at 0.98 screens · smallest text 6.5px
       collaborate     card at 1.02 screens · smallest text 6px    <- below the fold on load

   On collaborate.php an invited collaborator opened the link and had no way to sign in without
   scrolling first. Nobody would report that; it reads as a page that has not loaded properly.

   THE CAUSE IS THE SCHEMATIC. Below 860px .login stacks to a column and .brandpanel comes first,
   so the hero and a horizontal flow diagram consume the whole first screen before the card
   begins. The diagram is scaled to fit 375px and its labels scale with it — 6.5px, roughly half
   the smallest size anyone would call readable. It is illegible AND it is occupying the space
   the sign-in card needs.

   So it goes below 620px. That is not a judgement call about its value: the element is already
   marked aria-hidden="true" in the markup, which is the page declaring it decorative. Nothing a
   screen reader could reach is lost, because a screen reader was never given it.

   The sysline and the sub go with it for the same reason — atmosphere, not instruction — and the
   headline drops a step. What is left above the card is the eyebrow, the wordmark and one line
   of headline, which is the shape of the registry page the operator is pointing at. */
@media (max-width: 620px) {
  .schematic { display: none; }
  .sysline   { display: none; }
  .b-sub     { display: none; }
  .brandpanel { padding: 22px 20px 18px; }
  .b-h1      { font-size: 25px; line-height: 1.15; }
  .b-eyebrow { font-size: 10.5px; }
  /* The card's own top padding is the gap now, so the panel does not need to add one. */
  .formside  { padding: 18px 16px 24px; }
}

/* Below 380px — the smallest phones still in use — the headline takes one more step down rather
   than wrapping to four lines and pushing the card back down again. */
@media (max-width: 380px) {
  .b-h1 { font-size: 22px; }
  .formside .card { padding: 22px 18px 20px; }
}

/* ══ THE SIGN-IN DOORS IN REGISTRY DRESS, BELOW 620px ═══════════════════════════════════════════
   Operator's instruction, 11 Aug 2026: "make them look the same as the registry mobile page. the
   same design. i only want them different on the desktop." Confirmed after: the stepper survives,
   and it applies to BOTH doors — enter.php serves the investor and the collaborate pathway from
   one file, so `body.signin` covers both and `body.collab` needs nothing of its own here.

   THIS KNOWINGLY DEPARTS FROM D11, which assigns the room a light lavender/ink/pink palette and
   says not to unify the palettes across apps. The operator authored D11 and directed this himself;
   it is recorded here rather than left as an unexplained contradiction for whoever reads the file
   next and assumes it was a mistake.

   NOT A REWRITE, WHICH IS WHY IT IS SAFE TO DO DURING THE RAISE. The two doors already shared 23
   class names and the room's door already stacked hero-above-card on a phone — the registry's exact
   structure. So this restyles classes that already exist rather than restructuring the page. The
   form, its names, its CSRF field and every server touchpoint are untouched.

   620px, matching the registry's own mobile breakpoint, and ABOVE the 860px stack point on purpose:
   between 620 and 860 the door keeps the existing light design, which is what the maker's mark and
   the navy wordmark are drawn for.

   Specificity, not source order, decides every override below: the rules being overridden are
   `.login .btn`-shaped (0,2,0), so anything competing with one is written `body.signin .login …`
   (0,3,0). A file this long gets appended to, and a rule that wins only because it is last stops
   winning the moment somebody appends after it. */

/* ══ THE ROOM'S OWN FACES, SELF-HOSTED ═══════════════════════════════════════════════════════
   Until 11 Aug 2026 every page of the data room fetched these from fonts.googleapis.com. The
   registry's sign-in door did not — it has self-hosted since it was built — which is how the
   website workstream found it: measuring from outside, the ONLY third-party origin on the room's
   sign-in path was Google.

   That is a live third-party request on the screen fifty investors meet before they have signed
   in, so an unauthenticated visitor's IP and User-Agent reach Google merely for opening the
   invitation. It is also a dependency on the critical path: if that request is slow, first paint
   waits on it.

   THREE FILES, BECAUSE TWO OF THEM ARE VARIABLE. Figtree ships one file covering 400–800, and
   Newsreader one italic file covering 400–500 — so the five Figtree weights and two Newsreader
   weights the room asks for cost one download each, not seven. Provenance: fetched from
   fonts.gstatic.com on 11 Aug 2026, byte-for-byte the files the browser was already downloading.
   Both families are OFL, which permits redistribution; JetBrains Mono likewise.

   FIGTREE ITALIC IS DELIBERATELY ABSENT. The old <link> requested it on every page and nothing in
   this stylesheet ever rendered it — italics here are Newsreader. Vendoring it would have carried
   a file the room has never used across to a new home. */
@font-face{font-family:'Figtree';src:url('fonts/figtree-400-800-latin.woff2') format('woff2');font-weight:400 800;font-style:normal;font-display:swap}
@font-face{font-family:'Newsreader';src:url('fonts/newsreader-400-600-italic-latin.woff2') format('woff2');font-weight:400 600;font-style:italic;font-display:swap}
@font-face{font-family:'JetBrains Mono';src:url('fonts/jetbrains-mono-600-latin.woff2') format('woff2');font-weight:600;font-style:normal;font-display:swap}

/* Already on disk — vendored with the icon set and byte-identical to the registry's copies
   (sha256-verified, 11 Aug 2026). Declared at the top level but USED only inside the media block,
   so a desktop visitor fetches none of them: a browser downloads a face when a matching rule
   actually needs it, not when it is declared. */
@font-face{font-family:'Bricolage Grotesque';src:url('fonts/bricolage-grotesque-600-latin.woff2') format('woff2');font-weight:600;font-display:swap}
@font-face{font-family:'Outfit';src:url('fonts/outfit-400-latin.woff2') format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'Outfit';src:url('fonts/outfit-500-latin.woff2') format('woff2');font-weight:500;font-display:swap}
@font-face{font-family:'Outfit';src:url('fonts/outfit-500-latin.woff2') format('woff2');font-weight:600;font-display:swap}
@font-face{font-family:'Instrument Serif';src:url('fonts/instrument-serif-400-italic-latin.woff2') format('woff2');font-style:italic;font-weight:400;font-display:swap}
@font-face{font-family:'JetBrains Mono';src:url('fonts/jetbrains-mono-400-latin.woff2') format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'JetBrains Mono';src:url('fonts/jetbrains-mono-500-latin.woff2') format('woff2');font-weight:500;font-display:swap}

/* EVERY ELEMENT ADDED FOR THE MOBILE DRESS IS HIDDEN BY DEFAULT, AND THIS LINE IS LOAD-BEARING.
   All four are emitted at every width and revealed only inside the breakpoint — the same
   both-plates-and-let-CSS-choose pattern the stamp already used, and for the same reason: a door
   that fell between two PHP branches would render no mark at all.

   The aurora and the canvas were briefly left out of this line, with their `position:fixed` living
   only inside the media block. Above 620px that made them ordinary flow content: the canvas keeps
   the intrinsic size room-sky.js last gave it, so it stood 1624px tall and pushed the entire
   desktop door down to y=1444 with a horizontal scrollbar under it. The page still passed every
   assertion in the suite, because nothing there looks at the desktop layout. Caught by resizing
   the browser and looking at it. */
.wm-hero,.stamp-dark,.pbg-aurora,#sky{display:none}

@media (max-width: 620px) {

  /* ---- tokens, scoped so not one of them can leak into the room ---------------------------- */
  body.signin{
    --sd-pink:#ED99ED; --sd-aqua:#6EEDD1; --sd-off:#F4F4FB; --sd-body:#C9CFE8;
    --sd-mut:#98A2C8;  --sd-faint:#7C87B4;
    --sd-glass:rgba(14,23,54,.72); --sd-line:rgba(244,244,251,.14);
    --sd-disp:'Bricolage Grotesque',system-ui,sans-serif;
    --sd-bodyf:'Outfit',system-ui,sans-serif;
    --sd-ser:'Instrument Serif',Georgia,serif;

    background:#0a1430; color:var(--sd-off);
    font:400 14.5px/1.55 var(--sd-bodyf);
    -webkit-font-smoothing:antialiased;
    /* 56px, matching the registry door exactly. Was 0, which put the maker's mark hard against
       the bottom edge of the screen with nothing under it. */
    padding-bottom:56px;
    /* WITHOUT THIS THE PAGE ENDS IN A BAND OF LAVENDER. <html> carries the room's #F3F3FB, and a
       background set on <body> only propagates to the whole canvas when <html> has none of its own
       — so the body's navy stopped at the last element and 117px of the light room showed beneath
       the maker's mark. Measured, not guessed: body 695px in an 812px viewport.
       min-height rather than repainting <html>, because <html> is shared with every other page in
       the app and this rule must not be able to reach one of them. */
    min-height:100vh;
  }
  /* The gradient field. Pure CSS and z-index -2, so it sits under the aurora and the star canvas
     without any of the three needing to know about the others. */
  body.signin::before{
    content:"";position:fixed;inset:0;pointer-events:none;z-index:-2;
    background:
      radial-gradient(1200px 700px at 50% -14%,rgba(20,72,196,.62),transparent 72%),
      radial-gradient(820px 560px at 8% 104%,rgba(237,153,237,.16),transparent 68%),
      radial-gradient(880px 580px at 94% 106%,rgba(110,237,209,.14),transparent 68%),
      radial-gradient(560px 380px at 78% 8%,rgba(110,237,209,.07),transparent 70%);
  }
  body.signin .pbg-aurora{display:block;position:fixed;inset:0;pointer-events:none;overflow:hidden;z-index:-3}
  body.signin .pbg-aurora span{position:absolute;width:56vmax;height:56vmax;border-radius:50%;filter:blur(90px);opacity:.55}
  body.signin .pbg-aurora .a1{background:radial-gradient(circle,rgba(20,72,196,.55),transparent 62%);top:-24vmax;left:8vmax}
  body.signin .pbg-aurora .a2{background:radial-gradient(circle,rgba(110,237,209,.16),transparent 62%);bottom:-26vmax;right:-12vmax}
  body.signin .pbg-aurora .a3{background:radial-gradient(circle,rgba(237,153,237,.16),transparent 62%);bottom:-28vmax;left:-14vmax}
  body.signin #sky{display:block;position:fixed;inset:0;width:100%;height:100%;pointer-events:none;opacity:.9;z-index:-1}

  /* ---- one centred column ------------------------------------------------------------------ */
  body.signin .login{display:block;max-width:460px;margin:0 auto;padding:40px 18px 0;min-height:0}

  /* ---- the hero: what was the dark brand band becomes the registry's centred head ----------- */
  body.signin .brandpanel{
    background:none;color:var(--sd-off);display:block;flex:none;max-width:none;
    padding:0;margin:0 0 24px;overflow:visible;text-align:center;gap:0;
  }
  body.signin .orb,body.signin .orb2,body.signin .b-foot,
  body.signin .schematic,body.signin .sysline,body.signin .card-top{display:none}
  body.signin .wm-hero{display:block;height:26px;width:auto;margin:0 auto 20px}
  /* 9px and tighter tracking, where the registry uses 10px/.26em — because the registry's eyebrow
     reads "SHAREHOLDER PORTAL" and this one reads "MASLOW × HAPPI — INVESTOR DATA ROOM". At the
     registry's own values it measured 339px inside 339px of available width: not overflowing, but
     touching both edges with nothing to spare, and one longer pathway label away from wrapping
     badly. The collaborate door's eyebrow is longer still.
     Deliberately allowed to wrap rather than shrunk further — two tidy lines beat 7px type. */
  body.signin .b-eyebrow{
    font-family:var(--mono);font-size: 10.5px;letter-spacing: 0.060em;
    color:var(--sd-aqua);text-transform:uppercase;
    line-height:1.5;text-wrap:balance;
  }
  body.signin .b-h1{
    font-family:var(--sd-disp);font-weight:600;font-size:clamp(27px,7.4vw,32px);
    line-height:1.14;letter-spacing:-.01em;margin:14px 0 10px;color:var(--sd-off);
  }
  body.signin .b-h1 .accent{font-family:var(--sd-ser);font-style:italic;font-weight:400;color:var(--sd-pink);font-size:.88em}
  body.signin .b-sub{
    margin:0 auto;max-width:46ch;font-size:14.5px;font-weight:400;
    color:var(--sd-body);line-height:1.65;
  }

  /* ---- the card ---------------------------------------------------------------------------- */
  body.signin .formside{display:block;padding:0;overflow:visible}
  body.signin .driftw{display:none}
  /* The collaborate door's own dot canvas. The star field replaces it here; two canvases layered
     over one another would be neither effect. */
  body.signin #dotfield{display:none}
  body.signin .formside .card{
    width:100%;text-align:left;
    background:var(--sd-glass);border:1px solid var(--sd-line);border-radius:18px;
    padding:24px 20px 20px;
    backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
    box-shadow:0 24px 70px rgba(4,8,24,.55),inset 0 1px 0 rgba(255,255,255,.07);
  }

  /* ---- the stepper, kept on the operator's instruction and dressed to match ----------------- */
  /* The registry has no equivalent, so there is nothing to copy and this is the one block here
     that is an interpretation rather than a port. Aqua for the reached step, because aqua is the
     registry's "true/confirmed" colour throughout its portal. */
  body.signin .steps{margin:0 0 4px;gap:10px}
  body.signin .s-num{background:rgba(255,255,255,.06);color:var(--sd-faint)}
  body.signin .s-lab{color:var(--sd-faint)}
  body.signin .s-track{background:rgba(244,244,251,.1)}
  body.signin .s-track b{background:var(--sd-aqua)}
  body.signin .steps li.on .s-num{background:var(--sd-aqua);color:#08131f}
  body.signin .steps li.on .s-lab{color:var(--sd-off);font-weight:600}
  body.signin .steps li.wait .s-track b{background:repeating-linear-gradient(90deg,var(--sd-aqua) 0 8px,rgba(110,237,209,.35) 8px 16px)}
  body.signin .s-flag{color:#08131f;background:var(--sd-aqua)}

  /* ---- messages ---------------------------------------------------------------------------- */
  body.signin .flash.err,body.signin .flash.flash-err{
    background:rgba(192,57,43,.18);border-color:rgba(255,122,110,.38);color:#FFD9D4;
  }
  body.signin .flash.ok,body.signin .flash.flash-ok{
    background:rgba(110,237,209,.1);border-color:rgba(110,237,209,.34);color:#C4F7EA;
  }
  body.signin .flash .f-ic{background:rgba(255,122,110,.16)}
  body.signin .flash .f-b{color:#F2BCB5}

  /* ---- form -------------------------------------------------------------------------------- */
  body.signin .intro{font-size:13.5px;color:var(--sd-body);line-height:1.65}
  body.signin .label{color:var(--sd-body);font-weight:500;font-size:12.5px}
  body.signin .field{
    background:rgba(7,12,30,.6);border:1px solid var(--sd-line);border-radius:10px;
    color:var(--sd-off);padding:12px 13px;font:400 14.5px var(--sd-bodyf);
  }
  body.signin .field::placeholder{color:var(--sd-faint)}
  body.signin .field:focus{border-color:rgba(110,237,209,.75);box-shadow:0 0 0 3px rgba(110,237,209,.14)}
  body.signin .field.bad{border-color:rgba(255,122,110,.7);box-shadow:0 0 0 3px rgba(255,122,110,.14)}
  /* Chrome paints its own autofill background over any of the above; the inset shadow is the only
     way to put it back, and without it the field goes opaque white with white text on it. */
  body.signin .field:-webkit-autofill{-webkit-box-shadow:0 0 0 40px #101a3c inset;-webkit-text-fill-color:var(--sd-off)}
  body.signin .f-hint{color:#FFB3AB}
  body.signin .caps{color:#FFDFA1;background:rgba(255,190,80,.14);border-color:rgba(255,190,80,.35)}

  /* The primary action, and the one thing on this page that has to be unmistakable. */
  body.signin .login .btn{
    background:var(--sd-pink);border:0;color:#3a1440;border-radius:10px;
    padding:13px 16px;font:600 15px var(--sd-bodyf);
  }
  body.signin .login .btn:hover{background:#e88ae8;border:0}
  body.signin .login .btn .sweep{background:rgba(58,20,64,.18)}
  body.signin .login .btn-ghost{
    background:rgba(255,255,255,.04);border:1px solid var(--sd-line);color:#E6E9F8;
    font-weight:500;font-size:13.5px;border-radius:10px;
  }
  body.signin .login .btn-ghost:hover{background:rgba(255,255,255,.09);border-color:rgba(244,244,251,.26);color:#fff}

  body.signin .divider{margin:20px 0 12px}
  body.signin .divider span:first-child,body.signin .divider span:last-child{background:var(--sd-line)}
  body.signin .divider .mono-xs{color:var(--sd-faint);letter-spacing:.09em;font-size:10.5px}
  body.signin .oauth a+a,body.signin .oauth button+a,body.signin .oauth a+button{margin-top:10px}

  body.signin details.pw summary{color:var(--sd-mut);font-size:12.5px}
  body.signin details.pw summary:hover{color:var(--sd-off)}
  body.signin details.pw summary .chev{color:var(--sd-faint)}

  body.signin .assure{border-top-color:var(--sd-line)}
  body.signin .assure p{color:var(--sd-mut);font-size:12px}
  body.signin .assure svg{stroke:var(--sd-faint)}
  body.signin .meta{color:var(--sd-mut)}

  /* ---- the sent panel ---------------------------------------------------------------------- */
  body.signin .sent h2{font-family:var(--sd-disp);font-weight:600;font-size:22px;color:var(--sd-off);letter-spacing:0}
  body.signin .sent .s-body{font-size:13.5px;color:var(--sd-body)}
  body.signin .addr{background:rgba(255,255,255,.06);border-color:var(--sd-line);color:var(--sd-off);font-size:11.5px}
  body.signin .sent-meta{color:var(--sd-faint);font-size: 10.5px;letter-spacing: 0.060em}
  body.signin .spam{color:var(--sd-mut);font-size:12.5px}
  body.signin .spam a{color:var(--sd-aqua)}
  body.signin .spam .cool{color:var(--sd-faint)}
  body.signin .backlink{color:var(--sd-mut);font-size:12.5px}
  body.signin .backlink:hover{color:var(--sd-off)}
  body.signin .mailapps a{
    background:rgba(255,255,255,.04);border:1px solid var(--sd-line);color:#E6E9F8;border-radius:10px;
  }
  body.signin .mailapps a:hover{background:rgba(255,255,255,.09);border-color:rgba(244,244,251,.26);color:#fff}
  /* The beacon's envelope carries stroke="#D14EC6" inline, which no stylesheet can reach — and it
     does not need to: deep pink reads correctly on navy. Only the ring and disc move. */
  body.signin .beacon .core{background:rgba(237,153,237,.12);border:1px solid rgba(237,153,237,.3)}
  body.signin .beacon .ring{border-color:rgba(237,153,237,.7)}

  /* ---- the maker's mark -------------------------------------------------------------------- */
  /* The navy plate is invisible on navy. The offwhite one is shown instead, and the navy one is
     hidden rather than left underneath — two stacked plates would double the drop shadow. */
  body.signin .stamp-narrow{display:none}
  body.signin .stamp-dark{
    display:block;position:static;width:104px;height:auto;
    /* No bottom margin — the clearance below comes from body's padding-bottom, so both apps put
       the same 56px under the badge and it is set in one place per app. */
    margin:40px auto 0;opacity:.85;right:auto;bottom:auto;
    filter:drop-shadow(0 4px 12px rgba(4,8,24,.5));
  }
}

/* ══ THE STACKED DOOR ON A SHORT WINDOW ═════════════════════════════════════════════════════════
   Measured by the website workstream and reproduced here: at 800x660 neither the email field nor
   the sign-in button was on screen at load — the button's bottom edge sat 202px past the fold.

   WIDTH ALONE NEVER FINDS THIS. Between 620 and 860 the door stacks: hero band on top, card below.
   Nothing in that layout depends on viewport height, so the button's bottom edge lands at a nearly
   fixed ~865px down the document — which means it is off-screen on ANY window shorter than that,
   not merely on very short ones. At 900px tall it passes at 0.91 and looks perfectly healthy, two
   hundredths from failing, which is why every check anyone had run said fine.

   800x660 is not contrived. It is a laptop browser that is not full-screen, or a window split
   beside a PDF of the Offer — which is exactly what an investor reading the memorandum and opening
   the invitation link on the other half of the screen is looking at.

   THE FIX IS THE SCHEMATIC, because it is 226px of a 464px hero and it is decoration: aria-hidden,
   carrying no information the page does not state in words. It is already hidden below 620px for
   the same reason. Removing it plus the padding trim recovers ~250px, which puts the button above
   the fold for every window down to roughly 615px tall.

   900px, not 760px, is the threshold on purpose — the failure runs all the way up to ~865px, so a
   lower number would have fixed the width nobody uses and left the ones people do. Above 900 the
   schematic stays: there is room for it, and it is part of the design when there is. */
/* min-width 621 IS LOAD-BEARING. Without it this block also matches every phone — 375px is inside
   max-width:860 — and being later in the file its padding wins over the mobile dress's padding:0
   at equal specificity. Deployed for four minutes without it and the phone fold went 0.53 → 0.75
   screens: not broken, but it quietly undid the work the dress was built for. Caught by measuring
   the live page at 375 after deploying rather than assuming the band was isolated. */
@media (min-width: 621px) and (max-width: 860px) and (max-height: 900px) {
  body.signin .schematic { display: none; }
  body.signin .brandpanel { padding: 22px 24px 18px; gap: 16px; }
  body.signin .b-sub { margin-top: 12px; }
  body.signin .formside { padding: 26px 24px; }
}

/* ══ THE FOLDER ROW ON A PHONE ═══════════════════════════════════════════════════════════════
   Mobile audit, 11 Aug 2026: "Folder rows say the same number three times." Measured and correct —
   documents.php renders folder_ring($pct) at :194, a .fold-bar filled to $pct% at :206, and a
   .fold-pct text label at :207. Three encodings of one value in a tall ragged row, while the folder
   NAME — the only part that identifies which folder it is — truncated to "01 The Current Rou…".

   That is exactly backwards. The ring already states the number, in the position the eye lands on
   first, and it is the encoding that carries the 100%-turns-green rule. So the ring stays and the
   other two go, which buys the name the room to wrap.

   720px keeps the stacked-paper edge and the 44px triangle target from the rule above; this is a
   tighter breakpoint on top of it, because a two-line folder name is fine on a tablet and the
   duplication is only painful once the row is properly narrow. */
@media (max-width: 640px) {
  .fold > summary { grid-template-columns: 16px 36px minmax(0, 1fr); }
  /* THE NAME WRAPS. It is the one thing on the row that must never be abbreviated: a truncated
     folder name is a folder you cannot identify, and there are two in the tree whose first fifteen
     characters are identical. */
  .fold-name  { white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.3; }
  .fold-count { grid-column: 3; }
  /* The ring says it once. */
  .fold-bar, .fold-pct { display: none; }
}

/* ── THE OPENING VIDEO CARD (documents.php) ───────────────────────────────────────────────────
   A link to the viewer, not a player. Playing inline here would be a second issue path with no
   per-viewer mark, no delivery record and no watch tracking — the hole pdfstream.php exists to
   close for PDFs, reopened for video. So this is a door, and it looks like one. */
.opening-video {
  display: flex; align-items: center; gap: 16px; margin: 0 0 20px;
  padding: 16px 18px; border-radius: var(--r-lg);
  background: linear-gradient(100deg, var(--ink) 0%, #2A3572 62%, #46307a 100%);
  color: #fff; text-decoration: none;
  box-shadow: var(--shadow-lg); border: 1px solid rgba(255,255,255,.10);
  transition: transform .16s ease, box-shadow .16s ease;
}
.opening-video:hover { transform: translateY(-1px); color: #fff; }
.opening-video:focus-visible { outline: 2px solid var(--pink); outline-offset: 3px; }
.ov-play {
  flex: none; width: 46px; height: 46px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.28);
  color: #fff; padding-left: 3px;                 /* optical: a triangle sits left of centre */
}
.opening-video:hover .ov-play { background: var(--pink); border-color: var(--pink); color: var(--ink); }
.ov-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ov-eyebrow { font-family: var(--mono); font-size: 10.5px; letter-spacing: .16em;
              color: var(--pink-2); font-weight: 600; }
.ov-title { font-size: 17px; font-weight: 700; line-height: 1.25;
            overflow: hidden; text-overflow: ellipsis; }
.ov-sub { font-size: 13px; color: rgba(255,255,255,.72); }

@media (prefers-reduced-motion: reduce) { .opening-video { transition: none; }
                                          .opening-video:hover { transform: none; } }
@media (max-width: 720px) { .opening-video { padding: 14px; gap: 13px; }
                            .ov-play { width: 40px; height: 40px; } }

/* ── UPLOAD PROGRESS (admin/files.php) ────────────────────────────────────────────────────────
   The status line used to read "Uploading 1 file(s)…" and then say nothing until the page
   reloaded. On a large file that is minutes of a sentence that cannot distinguish a slow upload
   from a dead one — which is why this needed XMLHttpRequest rather than fetch, the only API that
   reports bytes sent. */
.up-bar {
  display: block; height: 6px; margin: 10px auto 6px; max-width: 340px;
  background: var(--bg-2); border-radius: 3px; overflow: hidden;
}
.up-fill {
  display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, var(--pink) 0%, var(--pink-deep) 100%);
  border-radius: 3px; transition: width .18s linear;
}
.up-pct {
  display: block; font-family: var(--mono); font-size: 11px; letter-spacing: .04em;
  color: var(--muted-3); font-variant-numeric: tabular-nums;
}
/* The width transition is the only motion, and it is the first thing to go — a progress bar that
   animates for somebody with reduced-motion set still needs to move, so the transition goes and
   the position does not. */
@media (prefers-reduced-motion: reduce) { .up-fill { transition: none; } }

/* ── FOLDER DELETE (admin/files.php) ───────────────────────────────────────────────────────────
   The grid item becomes the positioning context and the button sits in its corner — OUTSIDE the
   card's anchor, because a destructive control nested inside a link is one mis-tap from being
   pressed by somebody who meant to open the folder.

   Visible on hover and on keyboard focus, never hover-only: a control that only exists for a
   pointer does not exist on a phone or for anybody tabbing. */
.folder-grid { position: relative; }
.folder-grid > .folder-card { position: relative; }
.folder-del { position: absolute; margin: 0; }
.folder-card + .folder-del { margin-top: -46px; margin-left: auto; right: 0; }
.folder-del-btn {
  width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid var(--hairline); background: var(--surface); color: var(--muted-3);
  font-size: 15px; line-height: 1; cursor: pointer; padding: 0;
  opacity: 0; transition: opacity .14s, color .14s, border-color .14s;
}
.folder-card:hover + .folder-del .folder-del-btn,
.folder-del:hover .folder-del-btn,
.folder-del-btn:focus-visible { opacity: 1; }
.folder-del-btn:hover { color: #8A1B33; border-color: #F5C7CF; background: #FDEBEE; }
.folder-del-btn:focus-visible { outline: 2px solid var(--pink-deep); outline-offset: 2px; opacity: 1; }

/* On a touch screen there is no hover, so it is simply always there. */
@media (pointer: coarse) { .folder-del-btn { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .folder-del-btn { transition: none; } }

/* ── DOCUMENT ORDER (admin-content.php) ────────────────────────────────────────────────────────
   Drag to reorder within a folder. The arrows are not a fallback — they are the primary control
   for anyone not using a mouse, and they are always visible for that reason. A drag-only
   interaction is one that does not exist on a keyboard or a phone. */
.doc-sort .doc-row { cursor: default; }
.doc-sort .doc-row.is-dragging { opacity: .4; }
.doc-sort .doc-row.drop-above { box-shadow: inset 0 2px 0 0 var(--pink-deep); }
.doc-sort .doc-row.drop-below { box-shadow: inset 0 -2px 0 0 var(--pink-deep); }

.drag-grip {
  display: inline-block; color: var(--muted-2); cursor: grab;
  font-size: 13px; line-height: 1; letter-spacing: -1px; margin-right: 2px;
}
.doc-row:active .drag-grip { cursor: grabbing; }

.ord-btn {
  width: 20px; height: 18px; padding: 0; margin-left: 1px;
  border: 1px solid var(--hairline); border-radius: 4px;
  background: var(--surface); color: var(--muted-3);
  /* 10.5px, the floor. It was 10px and the type ratchet caught it on the same run it was
     written — which is the ratchet doing exactly its job, on me, an hour after I set it to
     zero and called it a rule. The arrow is a glyph in a 20px box, so the extra half-pixel
     costs nothing. */
  font-size: 10.5px; line-height: 1; cursor: pointer;
}
.ord-btn:hover { border-color: var(--pink-deep); color: var(--pink-deep); }
.ord-btn:focus-visible { outline: 2px solid var(--pink-deep); outline-offset: 1px; }
.ord-btn[disabled] { opacity: .3; cursor: default; }

/* Saved / failed, said in place rather than by reloading the page — a reload after every drag
   would lose the scroll position in a table of 78 documents. */
.ord-note { font-family: var(--mono); font-size: 10.5px; letter-spacing: .04em;
            color: var(--muted-3); margin-left: 8px; }
.ord-note.is-err { color: #8A1B33; }

@media (prefers-reduced-motion: reduce) { .doc-sort .doc-row { transition: none; } }

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   DARK MODE — added 13 August 2026 under D11a.

   D11 said the room has NO dark mode. The operator overturned it in those words and asked for it
   "in line with the registry dark mode". D11a records the amendment; this is the implementation,
   and the distinction it turns on is worth restating where somebody editing colours will see it:

     "IN LINE WITH THE REGISTRY" IS AN INSTRUCTION ABOUT THE MECHANISM, NOT THE COLOURS.

   Same machinery as the console: a `theme_pref` on the account, a three-state system/light/dark
   choice, and a data attribute that a media query defers to. DIFFERENT PAINT: this ground is
   built from the room's own ink (#1B2454) — a deep indigo that keeps the violet cast — and NOT
   from the registry portal's cosmic navy #070D26. Copying that value across would be the
   "unify the palettes" move the rest of D11 still forbids, and it would erase the divergence the
   whole decision exists to protect.

   ── KEYED ON [data-theme="dark"], AND ON [data-theme="system"] UNDER THE MEDIA QUERY ─────────
   NOT :not([data-theme]). The shell always emits the attribute — "system" is a value, not an
   absence — so a :not() selector would match nothing, ever. The registry learned that the
   expensive way: its palette got it right and an inline hover block did not, so on every page one
   rule silently never applied. Same trap, written down rather than rediscovered.

   ── EVERY PAIR MEASURED, NOT ADJUSTED BY EYE ─────────────────────────────────────────────────
   Worst ratio in the set is --muted-3 on --pink-soft at 5.04:1, against the 4.5:1 AA floor for
   normal text — and the mono microlabels are 10.5px, which is normal text, not large. First pass
   put that pair at 4.56, which passes and sits one careless tweak from failing; it was lightened
   for margin rather than left at the line. The light theme has twice been fixed for a colour
   chosen by its name, so nothing here was chosen by its name.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */

:root[data-theme="dark"] {
  --ink:        #E9EAF6;   /* was the darkest; on dark it is the lightest */
  --ink-2:      #D3D6EB;
  --ink-3:      #C0C4DF;
  --muted:      #A2A8CB;
  --muted-2:    #949AC0;
  --muted-3:    #9399C0;   /* 5.04:1 on --pink-soft, the worst ground it is set on */

  --hairline:   #2B3158;   /* every border in the room */
  --hairline-2: #3C4474;

  --bg:         #141830;   /* the room's ink, taken down — NOT the registry's #070D26 */
  --bg-2:       #10142A;
  --bg-3:       #191E3A;
  --surface:    #1C2140;

  /* --pink is the BRAND and does not move: it is filled with, not read. --pink-deep is the token
     whose stated job is TEXT, so on a dark ground it inverts — a dark magenta on near-black would
     be unreadable, which is the same fault the light theme carried until 13 Aug. */
  --pink:       #EE7BE4;
  --pink-deep:  #F3A6EC;   /* accent TEXT — 7.65:1 on the darkest ground it is used on */
  --pink-soft:  #3B2340;   /* avatar / chip fills */
  --pink-2:     #F49BEC;

  /* Status colours invert too. #17864E green and #8A5A00 amber are chosen to be read on white;
     on #141830 they are close to invisible, which would make a STAMPED chip and a warning chip
     equally unreadable — and those two carry meaning rather than decoration. */
  --green:      #4FD494;
  --green-soft: #12321F;
  /* Aliased, not re-picked: --green-text must follow --green here or it keeps its light value
     (#0F5C36) and reads 1.73:1 on the dark --green-soft. Aliasing is what makes switching any
     rule to --green-text a light-only change, which is how the sweep was safe without a dark
     re-measure. Same shape as the registry's --neg-text/--pos-text/--warn-text aliases. */
  --green-text: var(--green);
  --amber:      #F2B44C;
  --amber-soft: #33280F;
  --blue:       #93A8F7;

  /* Shadows on a dark ground need to be black rather than ink-tinted, or they read as a haze. */
  --topbar-bg:     rgba(28,33,64,.92);
  --topbar-sub-bg: rgba(28,33,64,.75);
  --field-bg:      #171B36;
  --drift-bg:      rgba(28,33,64,.55);
  --nav-active-bg: #2C3358;
  --invert-bg: #2A3160;   /* stays dark; one step up from --surface so it still reads as raised */
  --invert-fg: #FFFFFF;
  --invert-muted: #B4BADA;
  --chip-blue-bg:   #1E2748;
  --chip-blue-line: #33406E;
  /* ON A PINK FILL THE TEXT STAYS DARK IN BOTH THEMES. --pink #EE7BE4 does not move between
     themes — it is the brand — so text on it must not follow --ink, which flips to near-white
     and gave 2.05:1 on the primary button. The fill is light in both themes; therefore so is
     the rule for what sits on it. Measured: #1B2454 on #EE7BE4 = 6.5:1. */
  --on-pink:        #1B2454;

  --shadow:     0 1px 2px rgba(0,0,0,.40);
  --shadow-lg:  0 12px 40px rgba(0,0,0,.55);

  color-scheme: dark;
}

/* Following the OS when the reader has not chosen. data-theme="system" is a VALUE the shell
   emits, so this keys on it explicitly — see the note above. */
@media (prefers-color-scheme: dark) {
  :root[data-theme="system"] {
    --ink: #E9EAF6; --ink-2: #D3D6EB; --ink-3: #C0C4DF;
    --muted: #A2A8CB; --muted-2: #949AC0; --muted-3: #9399C0;
    --hairline: #2B3158; --hairline-2: #3C4474;
    --bg: #141830; --bg-2: #10142A; --bg-3: #191E3A; --surface: #1C2140;
    --pink: #EE7BE4; --pink-deep: #F3A6EC; --pink-soft: #3B2340; --pink-2: #F49BEC;
    --green: #4FD494; --green-soft: #12321F; --amber: #F2B44C; --amber-soft: #33280F;
    --green-text: var(--green);   /* alias — see the note in [data-theme="dark"] above */
    --blue: #93A8F7;
    --topbar-bg: rgba(28,33,64,.92); --topbar-sub-bg: rgba(28,33,64,.75);
    --field-bg: #171B36; --drift-bg: rgba(28,33,64,.55); --nav-active-bg: #2C3358;
    --chip-blue-bg: #1E2748; --chip-blue-line: #33406E; --on-pink: #1B2454;
    --invert-bg: #2A3160; --invert-fg: #FFFFFF; --invert-muted: #B4BADA;
    --shadow: 0 1px 2px rgba(0,0,0,.40); --shadow-lg: 0 12px 40px rgba(0,0,0,.55);
    color-scheme: dark;
  }
}

/* Brand plate swap — D11a. Two <img> elements, one hidden. CSS chooses rather than PHP, so a
   reader whose OS flips to dark mid-session gets the right plate without a page load.
   Keyed on [data-theme="system"] for the OS case, never :not([data-theme]) — the attribute is
   always present, so a :not() would match nothing. */
.stamp-on-dark { display: none; }
:root[data-theme="dark"] .stamp-on-light { display: none; }
:root[data-theme="dark"] .stamp-on-dark  { display: block; }
@media (prefers-color-scheme: dark) {
  :root[data-theme="system"] .stamp-on-light { display: none; }
  :root[data-theme="system"] .stamp-on-dark  { display: block; }
}
