/* THE SHELL'S OWN BASE STYLESHEET. ORDER IV 319 B2, B10.
 *
 * The Architect, 2026-11-10: "Single definitions for things like the header,
 * rather than re-defining it in multiple places." B2: "What every page shares
 * lives in the shell, defined ONCE: the header, the band and their icons; the
 * base stylesheet and the base template; the gate; the router's shell routes."
 *
 * ========================= WHY THIS FILE EXISTS ==========================
 *
 * MEASURED at IV 319, in Firefox, through the router
 * (`performance.getEntriesByType('resource')`): every page the shell serves
 * fetched SEVEN files from the Database app before this file existed --
 * `style.css`, `light.css`, `print.css`, `htmx.min.js`, `table.js`,
 * `grid-select.js` and `catalogue.js`. `/analytics` fetched all seven. So a
 * server running the shell and Analytics and nothing else had an unstyled
 * Analytics page, and the VPS only worked because `ops/build-release.sh`
 * COPIES those files out of the Database repository at release time.
 *
 * This is the shell's own copy of the small part of that which belongs to
 * every page: the reset, the page itself, the one scrolling model, links,
 * the flash block that `base.html` renders on every render, and the two
 * button classes that block uses. Nothing here is specific to any app.
 *
 * ========================= WHAT IS *NOT* HERE, AND THE COUNT =============
 *
 * B10: "If moving the shared rules out of Database's style.css is bigger than
 * this order, do Analytics' independence and the header in full. Plan the
 * rest of the move with a file and rule count, and say so."
 *
 * It is bigger. MEASURED: `Database/app/static/style.css` is **161,090 bytes,
 * 2,789 lines, ~696 declaration blocks**, and the other eight files the
 * shell's pages or the Database app's pages link come to 310,540 bytes more.
 * Almost all of it is that application's own -- the grid, the editors, the
 * pricelists, the specification tables, Print View -- and the shell's
 * remaining pages (Settings, Users, the sign-in doors, the Dashboards) still
 * use its forms, cards and tables. The receipt carries the plan for moving
 * what is genuinely shared; that is the next order's work, not this one's.
 *
 * ========================= THE DUPLICATE, AND WHICH WINS =================
 *
 * A page that links BOTH this file and `style.css` has `body`, `main`, `a`,
 * `.flash*` and `.btn*` declared twice at equal specificity. IV 317's
 * correction 4 is the standing trap, so the rule is stated rather than left
 * to chance: **`base.html` links this file FIRST and `style.css` after it**,
 * so on any page that still carries both, the Database app's declaration
 * wins and that page renders exactly as it did before this order -- which is
 * asserted by before/after screenshots of four of them.
 *
 * `/analytics` links this file and NOT `style.css`, so there is no duplicate
 * there at all.
 */

/* ===== THE TOKENS ======================================================
 * The same values as `style.css`'s `:root`, for the properties the shell's
 * own markup reads. They are NOT re-derived or improved: a token with a
 * different value in two files is the same fault as a rule declared twice.
 */
:root {
  color-scheme: dark;
  --bg: #141414;
  --panel: #1f232a;
  --ink: #e6e8eb;
  --muted: #9aa1ac;
  --line: #333944;
  --accent: #5b8def;
  --accent-ink: #0d1117;
  --ok: #1f7a3d;
  --warn: #a15c00;
  --danger: #f2685e;
  --iv-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}

* { box-sizing: border-box; }

/* ===== THE PAGE, AND IV'S ONE SCROLLING MODEL =========================
 *
 * B4: "IV's header and band stay frozen, as on every other page ... the page
 * uses IV's one scrolling model, not its own." That model is this: the body
 * is the window, it does not scroll, and `main` carries the scrollbar. The
 * header sits above `main` as a flex item of height auto, so it is frozen by
 * the layout and not by `position: fixed` -- which is why no page has ever
 * had to reserve space for it.
 *
 * IV 318 inverted this for the Analytics page alone (`body.an-page`) so the
 * header would scroll away, which is what that order's boards drew. B4
 * reverses it: one model, every page.
 */
html, body { margin: 0; }
body {
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
}
main {
  max-width: 960px;
  margin: 0 auto;
  padding: 1.5rem 1rem 3rem;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  overflow: auto;
}
a { color: var(--accent); }

/* ===== ONE SCROLLBAR, EVERYWHERE -- ORDER IV 320 B2 ====================
 *
 * The Architect, 2026-11-10: "make all scrollbars consistent. If that
 * singularly defined and consistent style needs amending, we'll do that in
 * one spot."
 *
 * THIS IS THAT SPOT. Every scrolling surface on every IV page -- the shell's,
 * the Database app's, Design's, Analytics' -- the page, `main`, a table, a
 * panel, the pop-up, a menu. No app defines its own.
 *
 * IV 319 HELD THIS BACK ON PURPOSE AND SAID SO. Putting it in a base
 * stylesheet moved the Settings page about 5px -- a thin scrollbar is
 * narrower than the browser's, so the content box grows -- and "a base
 * stylesheet may not change how a page looks on the day it arrives". It is
 * ruled on now, so it changes on purpose, and every page it moves is
 * measured, boxed and counted in the IV 320 receipt.
 *
 * WHAT IT REPLACES. SIX copies across the three repositories:
 *     Shell/shell/templates/home.html        its own <style>
 *     Shell/shell/templates/dashboard.html   its own <style>
 *     Shell/shell/templates/_iv/header.html  scoped to .iv-header
 *     Database/app/static/specs.css          twice, for two scrollers
 *     Database/app/static/editor.css         .se-groups, the group strip
 *     Database/app/static/light.css          .light-doc .menu-values, x5
 * All six are gone, each with a line saying where the rule lives now.
 *
 * MY GREP FOUND FOUR; THE SUITE FOUND SIX. I searched for the word
 * `scrollbar` in the files I expected to hold it and reported four. The test
 * `test_the_scrollbar_has_one_home` walks every .css file in all three
 * repositories and names the last two. The count above is the suite's, not
 * mine, and this note stays because the lesson is the method: a negative
 * claim needs an enumeration, and the enumeration has to be the machine's.
 *
 * BOTH FORMS, AND THAT IS STILL ONE DEFINITION (D3). `scrollbar-width` and
 * `scrollbar-color` are the standard properties and Firefox honours them;
 * `::-webkit-scrollbar` is what Chromium and Safari read. They are two
 * spellings of one decision and they read the same seven tokens, so
 * amending the colour is one edit and not two.
 *
 * A LIGHT SURFACE SETS THE VALUES; IT DOES NOT WRITE A SECOND DEFINITION.
 * IV is dark and these defaults are IV's. The one exception measured in
 * this order is the Database app's printable light document, whose column
 * menu scrolls on white paper: a #1a1a1a track there would be a black bar
 * down a sheet of paper. That surface sets these seven properties on
 * `.light-doc .menu-values` (`Database/app/static/light.css`) and the rules
 * themselves stay here, which is what "amended in one spot" means.
 */
:root {
  --iv-sb-thumb: #4a4a4a;
  --iv-sb-track: #1a1a1a;
  --iv-sb-size: 8px;
  --iv-sb-size-h: 6px;
  --iv-sb-radius: 4px;
  --iv-sb-pad: 0;
  --iv-sb-thumb-hover: var(--iv-sb-thumb);
}
html, body, * {
  scrollbar-color: var(--iv-sb-thumb) var(--iv-sb-track);
  scrollbar-width: thin;
}
*::-webkit-scrollbar {
  width: var(--iv-sb-size); height: var(--iv-sb-size-h);
}
*::-webkit-scrollbar-track { background: var(--iv-sb-track); }
*::-webkit-scrollbar-thumb {
  background: var(--iv-sb-thumb); border-radius: var(--iv-sb-radius);
  border: var(--iv-sb-pad) solid var(--iv-sb-track);
}
*::-webkit-scrollbar-thumb:hover { background: var(--iv-sb-thumb-hover); }
/* ===== THE ELEMENTS EVERY IV PAGE RELIES ON ===========================
 *
 * ORDER IV 319 B2. These were in `Database/app/static/style.css`, which
 * every shell page linked, and they are not that application's: they are
 * what a heading, a form control and a button look like in IV.
 *
 * MEASURED, and this is why they are here rather than left behind. With the
 * default inverted so a shell page links nothing of another app, Home came
 * out 8,935 pixels different — and reading every computed property of all
 * 617 elements on it, twice, named the cause exactly:
 *
 *     H2.iv-sec__head   font-size 22.5px -> 17.6px     (21 elements)
 *     BUTTON            font-family sans-serif -> system-ui  (11 elements)
 *     BUTTON            font-size 13.3333px -> 16px
 *     SPAN.iv-btn__ico  width 43px -> 43.75px          (16 elements)
 *
 * The first is `style.css:170`, which puts `h1` and `h2` on ONE LINE --
 * `h1 { font-size: 1.4rem; } h2 { font-size: 1.1rem; margin-top: 1.8rem; }`
 * -- so without it an `<h2>` falls back to the browser's `1.5em`, which at
 * IV's 15px body is 22.5px. The second is `button { font: inherit }`: a
 * button does NOT inherit its font, so without that rule every button in
 * the header reverted to the browser's UI font. The third follows from the
 * first: Home's tile icons are sized in CONTAINER QUERY units, so a
 * heading three pixels taller makes every icon three quarters of a pixel
 * wider.
 *
 * DUPLICATED WITH `style.css` ON A PAGE THAT STILL LINKS BOTH, deliberately
 * and in the safe direction: this file is linked FIRST, the values are
 * identical, and the app's declaration wins. Proved by before-and-after
 * screenshots of every reachable shell page.
 */
h1 { font-size: 1.4rem; }
h2 { font-size: 1.1rem; margin-top: 1.8rem; }
label { font-weight: 600; }
button { font: inherit; cursor: pointer; }
/* THE (0,2,1) RULE THAT HAS TRIPPED FOUR ORDERS, carried across unchanged
 * rather than improved: `min-width: 12rem` on every text input is what
 * IV 313, 314 and 315 each spent a measurement on, and a base stylesheet
 * that quietly dropped it would move every form in the product. */
input:not([type=checkbox]):not([type=radio]), select, output {
  font: inherit; padding: .4rem .5rem; border: 1px solid var(--line);
  border-radius: 6px; background: var(--panel); color: var(--ink);
  min-width: 12rem;
}
fieldset {
  border: 1px solid var(--line); border-radius: 8px; margin: 0 0 1rem;
  padding: .8rem 1rem;
}
legend { font-weight: 700; padding: 0 .4rem; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
       font-size: .92em; }

/* ===== THE SIGN-IN DOORS -- ORDER IV 320 B1, D1 ========================
 *
 * "The five doors ... render from the shell's own stylesheet(s) and
 * scripts." The doors are the VPS's first day: `/login`, `/access`,
 * `/login/link/<token>`, `/break-glass/<token>` and `/recovery` are what
 * the Architect and Max Perry will see before anything else exists.
 *
 * MEASURED, NOT CHOSEN. Two passes over each door: every rule in the
 * Database app's four stylesheets asked against the page's own DOM
 * (`scratch/IV-320/sweep.py`, 25 candidate rules), and then every one of
 * the 89 properties `style.css` sets read off every element with those
 * stylesheets present and removed (`missing.py`). The first list is what
 * COULD apply; the second is what actually changes, and these are the
 * second.
 *
 * Seventeen of the twenty-five candidates were already here -- the tokens,
 * the reset, `body`, `main`, `a`, `h1`, `h2`, `button`, `label`, `code` and
 * the form-control rule. These are the eight that were not.
 *
 * `label` AND `input` NEEDED NOTHING. They came out `display: inline` and
 * `inline-block` without `style.css` and `block` with it, which looked like
 * a missing rule and is not: `.stack` makes the form a flex column and a
 * flex item is blockified. One rule explains four changes, which is why the
 * second pass is run before anything is copied.
 */
.login { max-width: 360px; margin: 3rem auto; }
.stack {
  display: flex; flex-direction: column; gap: .6rem; align-items: flex-start;
}
.stack.narrow { max-width: 320px; }
.stack.full { max-width: none; align-self: stretch; }
.stack.full .hint { max-width: 72ch; }
.stack.full .check { white-space: nowrap; }
.hint { color: var(--muted); font-weight: 400; }
.note { color: var(--muted); font-size: .92em; }
.ctl-row { display: flex; align-items: center; gap: .5rem; }
.ctl-row > :last-child { flex: 1 1 auto; min-width: 0; }
/* ===== THE BUTTONS, IN ONE PLACE AND IN STYLE.CSS'S OWN ORDER =========
 *
 * `.btn` is the shape and `.btn-commit` / `.btn-minor` are the kinds --
 * "every button in this codebase carries exactly one kind"
 * (test_stage102).
 *
 * THE ORDER IS LOAD-BEARING AND I GOT IT WRONG FIRST. `.btn` and
 * `.btn-commit` are both (0,1,0), so the later one wins every property
 * they share. I added `.btn-commit` here and left `.btn` further down
 * beside the flash block, which meant `.btn`'s `border: 1px solid
 * transparent` and `font: inherit` overrode the committing button's blue
 * border and 700 weight -- MEASURED: with `style.css` removed the Sign in
 * button came out `rgba(0,0,0,0)` bordered and `font-weight: 400`.
 * `style.css` declares `.btn` first and its kinds after, and so does this.
 *
 * IV 317's correction 4 is the standing trap: a duplicate at equal
 * specificity. This is the same trap one step along -- not a duplicate,
 * but two rules that share properties, written in the wrong order.
 */
.btn {
  font: inherit; cursor: pointer; white-space: nowrap; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center;
  gap: .5rem; border: 1px solid transparent; border-radius: 6px;
}
.btn-commit {
  background: var(--accent); color: var(--accent-ink);
  border-color: var(--accent); font-weight: 700; padding: .45rem 1rem;
  font-size: .95rem;
}
.btn-commit:hover { filter: brightness(1.07); }
.btn-minor {
  padding: .35rem .8rem; font-size: .92rem; background: transparent;
  border-color: var(--line); color: var(--ink);
}
.btn-minor:hover { border-color: var(--accent); color: var(--accent); }

/* ===== THE POP-UP: ONE MECHANISM AND ONE STYLING -- IV 320 B3 =========
 *
 * The Architect, 2026-11-10: "The very pop-up mechanism and styling should
 * also be singularly defined (shell) then used everywhere consistently."
 *
 * THE MECHANISM IS `shell/static/iv-popup.js`; THIS IS THE STYLING. IV 319
 * built the mechanism here and left the look in `analytics.css`, which is
 * half a single home. Both halves are the shell's now, and Analytics uses
 * these classes rather than a copy of them.
 *
 * THE MARKUP CONTRACT, unchanged from IV 319:
 *
 *     <div class="iv-pop-host">           anything; it is the anchor
 *       ... the thing you click ...
 *       <details class="iv-pop-d" data-iv-pop>
 *         <summary aria-label="..."></summary>     a sheet over the host
 *         <div class="iv-pop">
 *           <button data-iv-pop-close aria-label="Close">x</button>
 *
 * CLICK OR TAP ONLY. No hover-to-open anywhere in IV (B3, and IV 319's
 * question 6 answered): a panel that opens on hover cannot be reached from
 * a keyboard, cannot be read on a phone, and closes while the pointer
 * crosses the gap to it. The HOST still takes a hover treatment, which is
 * the hint that there is something to open.
 *
 * COLOURS COME FROM THE PAGE, NOT FROM HERE. The shape, the shadow, the
 * close button's geometry and the phone sheet are the pattern; a page that
 * wants its own palette sets `--iv-pop-bg`, `--iv-pop-edge`, `--iv-pop-ink`
 * and `--iv-pop-mute` on its own root. Defaults are the shell's panel.
 */
:root {
  --iv-pop-bg: var(--panel);
  --iv-pop-edge: var(--line);
  --iv-pop-ink: var(--ink);
  --iv-pop-mute: var(--muted);
  --iv-pop-w: 540px;
  --iv-pop-hint: var(--accent);
}
.iv-pop-host { position: relative; }
.iv-pop-d > summary {
  position: absolute; inset: 0; z-index: 2; list-style: none; cursor: pointer;
  border-radius: inherit; border: 1px solid transparent;
}
.iv-pop-d > summary::-webkit-details-marker { display: none; }
.iv-pop-d > summary::marker { content: ""; }
.iv-pop-d > summary:hover,
.iv-pop-d[open] > summary {
  border-color: var(--iv-pop-hint);
  box-shadow: 0 0 0 1px var(--iv-pop-hint);
}
.iv-pop-d > summary:focus-visible {
  outline: 2px solid var(--iv-pop-hint); outline-offset: 2px;
}
.iv-pop {
  position: absolute; z-index: 25; left: 0; top: calc(100% + 8px);
  width: var(--iv-pop-w); max-width: calc(100vw - 48px);
  background: var(--iv-pop-bg); border: 1px solid var(--iv-pop-edge);
  border-radius: 12px; box-shadow: 0 18px 50px rgba(0, 0, 0, .55);
  padding: 18px 20px; font-size: 13px; cursor: default;
  color: var(--iv-pop-ink);
}
/* A HOST IN THE RIGHT HALF OF ITS ROW OPENS LEFTWARD, so a 540px panel does
 * not hang off the window. Set from the markup, which knows where the host
 * is; CSS cannot ask. */
.iv-pop-host[data-iv-pop-side="right"] .iv-pop { left: auto; right: 0; }
.iv-pop h4 { margin: 0 0 4px; font-size: 16px; font-weight: 600;
             padding-right: 40px; }
.iv-pop p { margin: 0; }
.iv-pop .iv-pop-k {
  font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--iv-pop-mute); font-weight: 600; margin: 12px 0 6px;
}
.iv-pop ul { margin: 0; padding: 0; list-style: none; display: flex;
             flex-direction: column; gap: 6px; }
.iv-pop li {
  background: var(--iv-pop-bg); border: 1px solid var(--iv-pop-edge);
  border-radius: 8px; padding: 8px 10px; display: flex;
  justify-content: space-between; gap: 10px;
}
.iv-pop li span:last-child {
  color: var(--iv-pop-mute); font-family: var(--iv-mono); font-size: 11px;
  white-space: nowrap;
}
.iv-pop-x {
  position: absolute; right: 14px; top: 12px; width: 30px; height: 30px;
  border-radius: 8px; display: grid; place-items: center; cursor: pointer;
  background: var(--iv-pop-bg); border: 1px solid var(--iv-pop-edge);
  color: var(--iv-pop-mute); font: inherit; font-size: 18px; line-height: 1;
  padding: 0;
}
.iv-pop-x:hover { color: var(--iv-pop-ink); }
.iv-pop-x:focus-visible {
  outline: 2px solid var(--iv-pop-hint); outline-offset: 2px;
}
/* ===== A SHEET AT ANY WIDTH, AND AN ORDINARY SUMMARY -- IV 321 B4 ====
 *
 * The Architect, 2026-11-10: "Do not shift page contents or redirect the
 * user. Context is king, which is only preserved when shit isn't moving."
 *
 * THE POP-UP ABOVE IS A 540px PANEL ANCHORED TO A TILE, and three things
 * on IV's own pages cannot be that shape: a Create-a-role form, a table of
 * retired roles, and the permission grid for one role in one app -- which
 * MEASURED 18,230px tall when it opened in the flow. A 540px panel for
 * that is a column with its own scrollbar over a page nobody can see.
 *
 * SO: THE SAME MECHANISM, A DIFFERENT SIZE. `data-iv-pop` still carries
 * Esc, the x, a click outside and one-open-at-a-time; `position: fixed`
 * still means nothing underneath moves. What changes is that the panel is
 * a SHEET over the page, inset a little, scrolling inside itself.
 *
 * AND THE SUMMARY STAYS IN THE FLOW. `.iv-pop-d > summary` above is an
 * invisible sheet over a whole tile, because there the tile's face is what
 * you click. Here the summary IS the control -- a line of text that says
 * "Create a role" -- so it is an ordinary clickable line and the host does
 * not need to be positioned.
 */
.iv-pop-d--inline > summary {
  position: static; inset: auto; z-index: auto; border: 0;
  border-radius: 0; box-shadow: none; cursor: pointer; font-weight: 600;
  padding: .3rem 0; list-style: none; display: list-item;
}
.iv-pop-d--inline > summary::-webkit-details-marker { display: none; }
.iv-pop-d--inline > summary::marker { content: ""; }
.iv-pop-d--inline > summary::before {
  content: "\25B8"; display: inline-block; width: 1em; color: var(--muted);
  transition: transform .12s;
}
.iv-pop-d--inline[open] > summary::before { transform: rotate(90deg); }
.iv-pop-d--inline > summary:hover { color: var(--iv-pop-hint); }
.iv-pop-d--inline > summary:focus-visible {
  outline: 2px solid var(--iv-pop-hint); outline-offset: 2px;
}
/* A CENTRED DIALOG, THE THIRD SIZE -- IV 321 B4.
 *
 * WHY A THIRD AND NOT A SECOND USE OF ONE OF THE TWO. The 540px panel is
 * ANCHORED to its host with `position: absolute`, which is right on a tile
 * and wrong inside a scroller: `.st-main` is `overflow: auto`, so a panel
 * belonging to a cell near its bottom edge would be CLIPPED by it. The
 * sheet is the whole window, which is heavy for three inputs.
 *
 * SO: fixed (never clipped, nothing underneath moves), centred (no script
 * has to place it), and sized to its content.
 *
 * IT IS STILL IN THE SAME FORM. `position: fixed` moves where a box is
 * PAINTED and not where it sits in the document, so the three inputs
 * inside one of these still post with the card's one Save -- which is what
 * `.st-person` needs and the reason this is not a `<dialog>`.
 */
.iv-pop.iv-pop--dialog {
  position: fixed; inset: auto; left: 50%; top: 12vh; right: auto;
  transform: translateX(-50%);
  width: min(520px, 92vw); max-width: none; max-height: 74vh;
  overflow-y: auto; z-index: 30;
}
@media (max-width: 820px) {
  .iv-pop.iv-pop--dialog {
    inset: 0; left: 0; top: 0; transform: none; width: auto;
    max-height: none;
  }
}

.iv-pop.iv-pop--sheet {
  position: fixed; inset: 3vh 3vw; left: 3vw; top: 3vh; width: auto;
  max-width: 1100px; margin: 0 auto; z-index: 30; overflow-y: auto;
  /* THE SHEET IS TALLER THAN THE PANEL AND STILL BOUNDED. `inset` fixes
   * all four edges, so the box is the window minus the margin and the
   * content scrolls INSIDE it -- which is the whole point: the page
   * underneath has not moved and cannot be scrolled by this. */
  padding: 20px 24px 28px;
}
@media (max-width: 820px) {
  /* ON A PHONE IT IS THE FULL SHEET ALREADY (the block below), and the
   * margin would only make it smaller. */
  .iv-pop.iv-pop--sheet { inset: 0; left: 0; top: 0; max-width: none; }
}

/* ===== THE PHONE SHEET. 820px is the header's own breakpoint. =========
 * "On a phone it opens as a sheet over the page, again with nothing
 * moving." `position: fixed` takes it out of the flow entirely, so the page
 * underneath cannot move. `z-index: 30` is above a frozen bar (20) and a
 * frozen section header (15): a sheet with a bar on top of it would put an
 * unreachable control over the x. */
@media (max-width: 820px) {
  .iv-pop {
    position: fixed; inset: 0; width: auto; max-width: none; left: 0;
    top: 0; border-radius: 0; overflow-y: auto; padding: 20px 16px 40px;
    z-index: 30;
  }
  .iv-pop-host[data-iv-pop-side="right"] .iv-pop { right: 0; }
  .iv-pop-x { position: fixed; right: 12px; top: 12px; }
}

/* ===== THE FLASH BLOCK =================================================
 * `base.html` renders this on EVERY page of every shell surface, so it
 * belongs to the base and not to an app. Build 214 A3's rules are carried
 * with it: one reason per line, bounded, dismissible.
 */
.flash-wrap { position: relative; margin: 0 0 1rem; }
.flash {
  list-style: none; margin: 0 0 1rem; padding: .6rem .9rem;
  background: var(--panel); border: 1px solid var(--accent);
  border-radius: 8px;
}
.flash-reasons { margin: .35rem 0 0; padding-left: 1.1rem; }
.flash-close {
  position: absolute; top: .3rem; right: .4rem; z-index: 1;
  background: transparent; border: 1px solid var(--accent); color: inherit;
  border-radius: 6px; cursor: pointer; line-height: 1; padding: .15rem .4rem;
}

/* ===== THE SHARED FURNITURE -- ORDER IV 321 B1/B3 =====================
 *
 * Ten rules and one token out of `Database/app/static/style.css`, chosen
 * by measurement and not by reading: `scratch/IV-321/split.py` asked every
 * rule in that application's four stylesheets, against each of the four
 * Settings pages' live DOM, which elements it matches and whether each one
 * is inside the Database app's card (`[id^="body-database-"]`). These
 * matched OUTSIDE it -- they are the SHELL's chrome.
 *
 * WHY THEY ARE HERE AND NOT IN `settings.css`. IV 320 B10 says "iv.css
 * holds what every page shares; Settings' own rules go to a shell-owned
 * settings stylesheet". These are not Settings' own: `table.grid` is the
 * Database app's grid tables, `main.wide` is its EDITOR layout, `details`
 * and `summary` are elements, `.inline` is a utility. Both applications
 * use them and both link `/iv.css` first, so one home serves both. The one
 * rule only Settings uses -- `.settings-sub` -- went to `settings.css`.
 *
 * THEY ARE MOVED, NOT COPIED. `style.css` carries a note in each place,
 * and IV 320 B11's test refuses a declaration that appears in both.
 *
 * `--doc-gutter` CAME WITH `main.wide`, because that rule reads it as
 * `--editor-side`: without the token, `padding: 0 var(--editor-side)
 * var(--editor-margin)` is invalid and the whole declaration is dropped.
 */
:root { --doc-gutter: 10px; }

.inline { display: inline; }
/* grids */
table.grid { width: 100%; border-collapse: collapse; margin: .5rem 0 1rem; }
table.grid th, table.grid td { border: 1px solid var(--line); padding: .45rem .6rem; text-align: left; vertical-align: top; }
table.grid thead th { background: var(--bg); }
details { margin: .4rem 0; }
summary { cursor: pointer; font-weight: 600; padding: .3rem 0; }
/* ---- Stage 8a: full-width editor, gridlines, content-sized cols, pin offsets ---- */
main.wide { max-width: none; }
/* ==== Stage 8b ==== */
/* item 6: fixed header area, only the grid scrolls */
/* body: MOVED TO THE SHELL'S `iv.css` AT ORDER IV 320 B11.
   display: flex; flex-direction: column; height: 100vh; overflow: hidden
   This page links `/iv.css` first, so the rule still applies; a
   copy here would be a second place to amend and the one that
   won. */

/* min-width:0 as well as min-height:0: main is a flex item of the flex body, and
   a flex item's default min-width:auto refuses to shrink below its content. The
   11k-px grid would otherwise blow main out to full table width (overflowing the
   viewport) instead of letting main stay viewport-wide and the grid scroll inside it. */
/* main: MOVED TO THE SHELL'S `iv.css` AT ORDER IV 320 B11.
   flex: 1 1 auto; min-width: 0; min-height: 0; overflow: auto
   This page links `/iv.css` first, so the rule still applies; a
   copy here would be a second place to amend and the one that
   won. */
   /* normal pages scroll in main */
/* width:100% pins main to the body's width (the viewport). Without it, main —
   a flex item in a column whose align-items is `normal`, not `stretch` — auto-
   sizes to its widest content, and the 11k-px grid drags the whole page wide
   instead of scrolling inside main. This is the fix for the "grid won't scroll". */
main.wide { width: 100%; overflow: hidden; display: flex; flex-direction: column; padding: .8rem 1rem; }
/* ==== THE PROFILE EDITORS GET THE SURROUND TOO (Stage 89 Part C) ==========
   Adam's specification, from his own screenshot: the gradient shows everything
   ABOVE THE STATUS KEY, plus a thin margin left, right and bottom. The grid
   becomes a floating document exactly as the priced catalogue is, with much
   less surround than light mode uses.

   The Status Key is the first thing inside .grid-wrap (Stage 58 put the legend
   inside the scroll region so it scrolls away), so "above the status key" and
   "above the document" are the same line — which is what makes this the same
   furniture as pricing mode rather than a second arrangement.

   THE MARGIN IS DELIBERATELY THIN and it is a real cost: every pixel of
   surround is a pixel of grid. It is stated once here as a variable so it can
   be changed in one place after he has looked at it.

   THE ACCENTS ARE UNTOUCHED. Deep grey for panels and deep blue for controllers
   live on the TABLE (--ctrl-bg and friends), which is the document. The
   gradient is the room the document sits in; a mode identity painted on the
   surround would be painting it on the wrong thing. */
main.wide:not(.pricing-mode) {
  --editor-margin: 10px;
  /* NINETY PERCENT OF THE WINDOW (Stage 98 Part D). Horizontal only, and it is
     a separate variable for a reason a maintainer would otherwise hit the hard
     way: a PERCENTAGE padding resolves against the containing block's WIDTH on
     every side, so folding this into --editor-margin would have made the
     bottom surround 96px instead of 10 and spent the vertical space Stages 58
     and 63 were about reclaiming. The vertical margin stays 10px. */
  /* STAGE 99 PART A: back to a thin gutter, with the width now set by
     --spec-table-max on .grid-wrap so this surface measures the same as its
     viewer.
     Stage 98's 5% is gone — it made the editors 1728px while the pricelists
     stayed 1840, which is the mismatch Part A exists to remove. */
  --editor-side: var(--doc-gutter);
  /* LIGHTER THAN THE OTHER TWO SURFACES (Stage 91), on Adam's request: at
     #23262b the top 38% was flat near-black against a near-black page and the
     gradient barely read as one. This is a deliberate divergence from the
     family — pricing mode and light mode keep #23262b — so if the three are
     ever meant to match again, this is the line to change back. */
  /* ---------------------------------------------------- ORDER IV 314 B15 ----
     NO GRADIENT, AND THEREFORE NO BANDS. "Remove the thin gradient bands at
     the far left and right of the page."

     WHAT THE BANDS WERE, MEASURED rather than guessed: this rule painted
     `linear-gradient(to top, #b9bfc6 0%, #3a4049 62%)` across the FULL page
     width -- `MAIN.wide`, rect [0, 1920] at 1920 -- and the document inside it
     is narrower, so what a person SAW of it was two vertical strips at the
     edges, pale at the bottom and dark at the top. The gradient was the
     "surround" the document floated on; the strips were all that was left of
     it once the table filled the middle.

     IT WAS NOT ONE RULE IN ONE STYLESHEET. There are three: this one, the
     pricing-mode one below, and `light.css`'s `main.light-doc`, which is the
     PRICELISTS and is not one of the three pages this order is about. That
     one is left exactly as it is -- it is the red-into-grey surround the
     printed document deliberately sits on, and its reasoning is written into
     light.css where it was made.

     ONE FLAT GROUND, SHARED WITH SETTINGS. `#1b1f26` is the colour
     `settings.css` gives `.st-main`, so the three pages the Architect
     reviewed now stand on one background and no edge shows a different
     colour. The two variables stay defined: `--surround-top` and
     `--surround-bot` are read by `.grid-status` and the pricing panels'
     shadows below, and by light mode. */
  --surround-top: #3a4049;
  --surround-bot: #b9bfc6;      /* light grey, as pricing mode — not the red */
  background: #1b1f26;
  padding: 0 var(--editor-side) var(--editor-margin);
}

/* ===== THE PAGE AN ABSENT APPLICATION ANSWERS WITH -- IV 322 B19 ======
 *
 * "Its route answers with the shell's placeholder page (IV's header and
 * band, the app's name and icon, 'Not installed yet'), never a 502, a
 * blank page or a redirect."
 *
 * IN `iv.css` AND NOT IN THE TEMPLATE'S OWN BLOCK, because B19 asks for
 * "the shell's ONE style for that state" and the badge on Home and in the
 * band is the other half of it. A page that carried its own copy would be
 * a second answer to what "not installed" looks like.
 *
 * THE AMBER IS THE SAME TWO VALUES as `.iv-soon--absent` and
 * `.iv-header__soon.is-absent`: #d7a53a on #7a5e1c. Not red -- nothing is
 * wrong -- and not Soon's grey, or the two states read as one.
 */
.iv-absent {
  max-width: 38rem; margin: 4rem auto; text-align: center;
}
.iv-absent__icon {
  width: 72px; height: 72px; margin: 0 auto 1.2rem;
  display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 18px;
  background: var(--panel); opacity: .72;
}
.iv-absent__icon svg { width: 40px; height: 40px; }
.iv-absent h1 { margin: 0 0 .4rem; }
.iv-absent__state {
  display: inline-block; margin: 0 0 1.2rem; padding: 2px 9px;
  font-size: 12px; font-weight: 600; letter-spacing: .02em;
  color: #d7a53a; border: 1px solid #7a5e1c; border-radius: 4px;
}
.iv-absent .hint { margin: 0 0 1.2rem; }
.iv-absent .note { margin: 0 0 1.8rem; }
.iv-absent__back { margin: 0 0 2rem; }
.iv-absent__others {
  color: var(--muted); font-size: .92em; border-top: 1px solid var(--line);
  padding-top: 1.2rem; margin: 0;
}

/* ===== A DOOR SOMEBODY IS ALREADY THROUGH -- ORDER IV 322 B18 =========
 *
 * "The door shows, in its own place and style: 'Signed in as <name>',
 * with Home and Sign out."
 *
 * IN `iv.css` BECAUSE FIVE TEMPLATES DRAW IT. The four doors plus the
 * break-glass page include one partial, and a style carried by each of
 * them would be five answers to what the panel looks like.
 */
.iv-door-signed-in { margin: 1.2rem 0 0; }
.iv-door-signed-in__who { margin: 0 0 .4rem; font-size: 1.05rem; }
.iv-door-signed-in .hint { margin: 0 0 1.2rem; }
.iv-door-signed-in .ctl-row { gap: .8rem; }
.iv-door-signed-in form.inline { display: inline; }

/* ===== A FIELD WHOSE VALUE IS HIDDEN AS IT IS TYPED -- IV 322 B17 =====
 *
 * "The field is hidden as it is typed, every character shown as a dot, with
 * an eye icon at its right end."
 *
 * ONE DEFINITION, HERE, because B17 asks for "a 'hidden field' component in
 * the shell (markup, style, script, the eye as an SVG)" and any page may
 * need one. The markup is `_iv/hidden_field.html`; the behaviour is
 * `/iv-hidden-field.js`.
 *
 * THE INPUT IS `type="text"`, WHICH IS THE WHOLE POINT (B17: "no password
 * manager offers to save it or fills it"). So the dots are drawn, not
 * given: `-webkit-text-security` where the browser has it, and a mask span
 * over a transparent input where it does not. Both are set by the script,
 * which is the only thing that knows which browser this is.
 */
/* `align-items: flex-start` AND IT IS LOAD-BEARING. Without it a column
 * flex container STRETCHES its children, so `.iv-hidden__box` -- which is
 * `inline-flex` and should shrink to its input -- spanned the whole row
 * and the eye, positioned against the box's right edge, floated away from
 * the field. Measured in a screenshot: the eye 300px from the input it
 * belongs to. */
.iv-hidden { display: flex; flex-direction: column; gap: .3rem;
             align-items: flex-start; }
.iv-hidden__box { position: relative; display: inline-flex; align-items: center; }
.iv-hidden__input {
  /* THE EYE SITS INSIDE THE FIELD, so the text must stop before it. 2.4rem
   * is the button's width plus its own padding, measured rather than
   * guessed: a value that was too small put the caret under the eye. */
  padding-right: 2.4rem;
}
/* THE MASK. In the input's own box, in the input's own font, so one dot
 * occupies one character's width and the row does not change length when
 * the eye is pressed. `pointer-events: none` so a click lands on the
 * input underneath and the caret goes where the person pointed. */
.iv-hidden__mask {
  position: absolute; left: 0; top: 0; bottom: 0;
  display: flex; align-items: center;
  padding: .4rem .5rem; padding-right: 2.4rem;
  font: inherit; letter-spacing: .08em; color: var(--ink);
  pointer-events: none; white-space: nowrap; overflow: hidden;
  max-width: 100%;
}
.iv-hidden__eye {
  position: absolute; right: .35rem; top: 50%; transform: translateY(-50%);
  width: 1.7rem; height: 1.7rem; padding: 0;
  display: grid; place-items: center;
  background: transparent; border: 0; border-radius: 5px;
  color: var(--muted); cursor: pointer;
}
.iv-hidden__eye:hover { color: var(--ink); }
.iv-hidden__eye:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.iv-hidden__eye svg { width: 1.05rem; height: 1.05rem; }
/* AND THE SLASH, drawn in CSS so one SVG serves both states (there is no
 * second icon file to keep in step). It appears when the value is SHOWN,
 * because that is when pressing the eye hides it again -- the control shows
 * what it will do, which is how a toggle reads. */
.iv-hidden[data-iv-hidden="shown"] .iv-hidden__eye { color: var(--ink); }
.iv-hidden[data-iv-hidden="shown"] .iv-hidden__eye::after {
  content: ""; position: absolute; left: 10%; right: 10%; top: 50%;
  height: 1.5px; background: currentColor; transform: rotate(-35deg);
  border-radius: 1px;
}
.iv-hidden__hint { margin: 0; }
