/* ============================================================
   Writersbox: one calm place for every project.

   The type system is real and travels inside the app:
     Inter          — the interface. Precise, professional.
     Space Grotesk  — titles and numbers. The marquee voice.
     Courier Prime  — the script page. Courier drawn for
                      screenwriters, not for typewriters.
     Source Serif 4 — quotes and reading, in italic.
   All four are open-licensed and bundled, so the app looks the
   same on every machine and never phones home for a font.
   ============================================================ */
@import url('/assets/fonts.css');

/* ============================================================
   THE TOKENS.

   Every measurement the interface is allowed to have. If a rule
   needs a number that is not here, either the number is wrong or
   the scale is missing a rung — add the rung, do not improvise at
   the call site. The exceptions are named and justified where they
   occur: the script page, the deck page and the map overlay carry
   raw pixel sizes because they mirror a printed page or a canvas
   at a fixed scale, and must not move when the text-size control
   moves the interface.
   ============================================================ */
:root {
  /* Text size. --fs scales the whole interface, --write-fs scales the
     writing surfaces on top of that, for readers who want big prose but a
     compact UI. Both are set from localStorage at boot. */
  --fs: 1;
  --write-fs: 1;

  --font-ui: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
  --font-display: "Space Grotesk", "Inter", "Segoe UI", system-ui, sans-serif;
  --font-read: "Source Serif 4", Georgia, "Iowan Old Style", "Palatino Linotype", serif;
  --font-mono: "Courier Prime", "Courier New", monospace;

  /* ---- tracking: four rungs ---------------------------------------------
   * Letter-spacing was improvised twenty-three different ways, nine of them
   * inside a hundredth of an em of each other — differences no eye can see
   * and no hand can maintain. Four rungs, each doing a different job.
   */
  --track-tight: -.015em;  /* display type set large: close it up */
  --track-fine:  .02em;    /* small text that needs a breath of air */
  --track-label: .06em;    /* a label with capitals in it */
  --track:       .12em;    /* the uppercase, letterspaced whisper */
  --track-wide:  .3em;     /* spaced right out: a PIN, a divider, a stamp */

  /* ---- type: eleven roles, and only eleven -----------------------------
   * Every one is built on --fs, so the text-size control moves the whole
   * interface at once. Ask for the role, not the pixel.
   *
   * There were twenty-six of these written out as calc() at the call site,
   * nine of them inside a single 4px band — 9, 9.5, 10, 10.5, 11, 11.5, 12,
   * 12.5, 13 — which is eight distinctions no eye can see and no hand can
   * keep straight. Worse, forty-four more rules wrote a raw pixel size with
   * no --fs at all, so the text-size control silently did nothing to them.
   *
   * An app this dense genuinely needs the bottom of the scale finely graded
   * and the top of it coarse, which is why the rungs are not a fixed ratio.
   * The rule is simply: if your size is not on this list, you are guessing.
   */
  --fz-display: calc(30px * var(--fs));    /* the marquee: a greeting, a hero */
  --fz-title:   calc(24px * var(--fs));    /* a page's own name */
  --fz-stat:    calc(20px * var(--fs));    /* a number the page exists to show */
  --fz-heading: calc(18px * var(--fs));    /* a dialog, a section that owns a screen */
  --fz-subhead: calc(17px * var(--fs));    /* a card's own title, set in the serif */
  --fz-lead:    calc(15.5px * var(--fs));  /* reading matter: prose, quotes, blurbs */
  --fz-body:    calc(14px * var(--fs));    /* the default sentence */
  --fz-ui:      calc(13px * var(--fs));    /* controls, rows, chrome */
  --fz-small:   calc(12.5px * var(--fs));  /* a caption, a secondary line in a row */
  --fz-tiny:    calc(11.5px * var(--fs));  /* a count, a timestamp, a unit */
  --fz-micro:   calc(10.5px * var(--fs));  /* the tracked uppercase whisper */

  /* ---- glyph sizes: the documented exception ---------------------------
   * A mark inside a fixed hit target — the ✕ on a still, the star on a
   * photo, the chevron on a rail toggle. These deliberately do NOT scale
   * with --fs, because the box around them is a 20px hit target and a glyph
   * that grew while its button did not would simply spill out of it. They
   * are named so that they read as a decision rather than an accident.
   *
   * The other places that hold a fixed pixel size on purpose, and should be
   * left alone: the script page and the document page (a printed page at a
   * known scale), the deck page (a slide that must match its PDF), the SVG
   * chart labels (drawn inside a fixed viewBox), and the map and living-world
   * overlays (drawn over a canvas at its own scale).
   */
  --glyph-sm: 11px;
  --glyph:    15px;
  --glyph-lg: 20px;

  /* ---- space ----------------------------------------------------------
   * A gap is one of these. The rungs are close together at the bottom
   * because that is where interface spacing actually lives.
   */
  --sp-1: 2px;
  --sp-2: 4px;
  --sp-3: 6px;
  --sp-4: 8px;
  --sp-5: 10px;
  --sp-6: 12px;
  --sp-7: 16px;
  --sp-8: 22px;
  --sp-9: 30px;
  --sp-10: 44px;

  /* ---- shape -----------------------------------------------------------
   * Six rungs. There were 156 hardcoded radii across twelve values against
   * 71 uses of the tokens, which is a stylesheet with no shape language at
   * all. The rungs sit where the app actually needed them: a 2px bar and a
   * 16px dialog are different things, 7px and 8px are not.
   */
  --r-2xs: 2px;         /* a rule, a bar, a progress fill: barely a corner */
  --r-xs: 4px;
  --r-sm: 6px;
  --r-md: 8px;          /* the button, the row, the input */
  --r: 10px;
  --r-lg: 16px;
  --r-pill: 999px;      /* a stadium, whatever the height */
  --r-circle: 50%;

  /* ---- elevation -------------------------------------------------------
   * Five levels. A component picks one; it does not write a shadow.
   */
  --elev-0: none;
  --elev-1: 0 1px 2px rgb(0 0 0 / .18);
  --elev-2: 0 6px 24px -8px rgb(0 0 0 / .35);
  --elev-3: 0 24px 60px -16px rgb(0 0 0 / .45);
  --elev-4: 0 40px 96px -24px rgb(0 0 0 / .55);
  /* The names the app already speaks, kept so nothing has to change at once. */
  --shadow-1: var(--elev-1);
  --shadow-2: var(--elev-2);
  --shadow-3: var(--elev-3);

  /* ---- rings ------------------------------------------------------------
   * A ring is a STATE — focused, selected, current, lifted clear of the page
   * — and it is not an elevation. Twenty rules wrote `0 0 0 Npx` by hand in
   * five different widths, so "selected" looked slightly different on every
   * surface it appeared on.
   */
  --ring-focus: 0 0 0 3px var(--accent-soft);
  --ring-on:    0 0 0 2px var(--accent);
  --ring-hair:  0 0 0 1px var(--border);
  --ring-punch: 0 0 0 4px var(--bg-deep);   /* cut a mark out of what is behind it */
  /* A ring in whatever colour the thing itself carries — a character's own
     accent, a family's own colour. The component sets --ring-tint-colour and
     gets the same weight of ring as everything else. Five surfaces used to
     write this by hand at 22%, 25%, 30%, 45% and 55%, so "this one is yours"
     was said five different strengths in five places. */
  --ring-tint: 0 0 0 3px color-mix(in srgb, var(--ring-tint-colour, var(--accent)) 45%, transparent);

  /* ---- glow -------------------------------------------------------------
   * Coloured light under a control that means something. Two weights: at
   * rest, and under the hand. Both derive from the theme, so a studio built
   * from somebody's own photograph glows in its own colour.
   */
  --glow:      0 2px 10px color-mix(in srgb, var(--accent) 34%, transparent);
  --glow-lift: 0 4px 18px color-mix(in srgb, var(--accent) 48%, transparent);
  --glow-danger:      0 2px 10px var(--danger-glow);
  --glow-danger-lift: 0 4px 18px color-mix(in srgb, var(--danger) 48%, transparent);

  /* ---- motion: four speeds, two curves ---------------------------------
   * There were twenty durations, five of them inside a 40ms band (.12, .13,
   * .14, .15, .16) — differences nobody can perceive and nobody can keep
   * consistent. Four, each with a job. Every transition also names a curve;
   * six of them used to omit --ease and fall back to the browser's `ease`,
   * which is a different motion from everything beside it.
   */
  --dur-0: .08s;        /* the press of a button: as near instant as motion gets */
  --dur-1: .12s;        /* answering the cursor: colour, opacity */
  --dur-2: .2s;         /* arriving or leaving: dialogs, toasts, panels */
  --dur-3: .38s;        /* a considered reveal: cards dealing onto a grid */
  --ease: cubic-bezier(.2, .7, .3, 1);
  --ease-soft: cubic-bezier(.32, .72, 0, 1);   /* long, settling; for entrances */

  /* ---- depth -----------------------------------------------------------
   * Nothing writes a raw z-index. If two things fight, they are on the
   * same rung and one of them is wrong.
   *
   * The two --z-local rungs are the exception, and a real one: a badge over
   * its own card, a sticky first column in its own table. Those never meet
   * the app's furniture, because they live inside a stacking context of
   * their own, so putting them on the global ladder would be a lie.
   */
  --z-floor: 0;      /* the scenery behind everything */
  --z-local: 2;
  --z-local-2: 3;
  --z-base: 1;
  --z-raised: 5;
  --z-sticky: 20;
  --z-rail: 30;
  --z-float: 40;
  --z-drawer: 60;
  --z-veil: 80;
  --z-modal: 200;
  --z-pop: 300;
  --z-toast: 400;
  --z-top: 500;

  /* ---- measure ---------------------------------------------------------
   * How wide a line of text may get before the eye loses its way back.
   * Nine different hand-picked measures used to be scattered through the
   * file (44ch, 52ch, 62ch, 70ch, 76ch…), which is nine slightly different
   * reading rhythms on nine screens of the same app. Three rungs.
   */
  --measure-short: 46ch;  /* a centred caption, an empty state: read at a glance */
  --measure-line: 54ch;   /* the single dramatic line under a hero's name */
  --measure: 68ch;        /* prose you sit and read */
  --measure-wide: 88ch;   /* a hint, a subtitle: a line or two, so it may run on */
  --content-max: 1340px;  /* the widest the made page ever gets */

  /* ---- icons -----------------------------------------------------------
   * The sprite is drawn on a 24x24 grid; these are the sizes it is set at.
   */
  --icon-sm: 13px;
  --icon: 15px;
  --icon-lg: 18px;

  /*
   * *** THE RAIL HAS TO GROW WITH THE TYPE IN IT. ***
   *
   * Logan: "When I change the text size the whole site messes up and looks
   * really bad and not well made, this is to make things easy to read and not
   * mess the whole site up."
   *
   * The text-size control goes to 200%, and this was a flat 262px. Everything
   * inside the rail scales with --fs and the rail did not, so at 200% the
   * "+ New project" button and the text-size row broke straight out of it and
   * sat on top of the page - photographed on the shelf and on a project page,
   * with the A+ button off the edge of the rail entirely and "A-" printed over
   * "200%". Nothing was clipped, which is why no overflow check ever caught it:
   * the content escaped rather than being cut.
   *
   * Growing it PROPORTIONALLY was the wrong shape, and laptop-test caught it:
   * at 130% the rail took 70px it did not need and the script's scene rail lost
   * them, slicing every "EXT - NIGHT" at 1280. The rail was not broken at 130%;
   * it broke somewhere above 150%. So the width holds until the type has
   * outgrown it and only then follows, and it is capped at a third of the
   * window - past that the ⟨ button, which has always worked, is the better
   * answer than a rail that keeps eating the page.
   *
   *   70%  232    100%  232    130%  232    150%  232    175%  290    200%  348
   */
  --sidebar-w: min(max(262px, calc(262px * (var(--fs) - 0.5))), 34vw);

  /* Derived from whatever the theme sets, so a theme only ever states its
     own colours. A theme may override --accent-2 to pick its own second
     stop; the fallback leans the accent toward violet. */
  --accent-2: color-mix(in srgb, var(--accent) 55%, #b06cff);
  --danger-glow: color-mix(in srgb, var(--danger) 34%, transparent);
}

/* ============================================================
   THE LADDER.

   Five rungs, and every theme must show a real step between each
   one, because the whole interface is built on them:

     --bg-deep   the floor: the rail and the sidebar
     --bg        the room
     --surface   a card sitting in it
     --raised    a control sitting on the card
     --raised-2  that control, engaged

   ONE RULE, IN ALL SEVEN THEMES: each rung is LIGHTER than the rung
   below it. Nearer the viewer is nearer the light. A hover therefore
   brightens everywhere, an engaged control is the brightest thing on
   its card everywhere, and a writer who switches theme does not have
   to relearn what depth looks like.

   Two faults were fixed to get here. Light and Glass set --surface
   and --raised both to #ffffff, so a button on a card was the card
   and every hover state collapsed onto one plane. Correcting that by
   stepping the light themes DOWN into grey fixed the flatness but
   left the direction inverted: five themes brightened on hover and
   two dimmed. The real fault was spending pure white on step one.
   The light themes now start on warm or cool paper and climb, and
   #ffffff is where the ladder ENDS rather than where it begins.

   Verified numerically, not by eye: every adjacent pair differs in
   relative luminance, and every step is positive, in all seven.
   ============================================================ */

html[data-theme="dark"] {
  --bg:        #111216;
  --bg-deep:   #0a0b0e;
  --surface:   #272a31;
  --raised:    #373c45;
  --raised-2:  #464d5a;
  --border:    #2b2f38;
  --border-soft: #21242c;
  --text:      #eceef4;
  --text-2:    #b8bcc8;
  /* The third step down. Seven surfaces asked for it and never got it, so
     every one of them was rendering at full brightness. */
  --text-3:    #a3aac1;
  --muted:     #adb5ca;
  --faint:     #9ea6bf;
  --accent:    #7d90fb;
  /* Dark ink, because this accent is a light periwinkle: white on it measured
     2.5:1 on the primary button, in the theme most people never leave. Dark
     reads 7.3:1, and it is what four of the seven themes already do. */
  --accent-ink:#0f1424;
  --accent-2:  #b57dfb;
  --accent-soft: #7d90fb22;
  --film:      #f0a868;
  --tv:        #5fc9c0;
  --danger:    #ff7a7a;
  --danger-ink:#2b0d10;
  --ok:        #6fd39b;
  --page-bg:   #1b1d23;
  --page-ink:  #e6e7ee;
}

/*
 * Light: warm paper, and it climbs.
 *
 * This theme used to set --surface and --raised both to #ffffff, so a button
 * on a card WAS the card, and every hover state on the page was the same
 * plane as the thing it was hovering. The first fix stepped the controls DOWN
 * into grey, on the argument that white is the ceiling — but that made light
 * the only pair of themes where coming toward the viewer got darker, so a
 * hover brightened in five themes and dimmed in two.
 *
 * The ceiling is not the problem; starting at the ceiling is. The room is
 * warm paper, the card is lighter than the room, the control is lighter than
 * the card, and the engaged control is the white that used to be spent on
 * step one. Every rung climbs, in every theme, and a hover always brightens.
 */
html[data-theme="light"] {
  --bg:        #e8e6e0;
  --bg-deep:   #dddbd4;
  --surface:   #f5f3ee;
  --raised:    #fffffc;
  --raised-2:  #ffffff;
  --border:    #d5d2ca;
  --border-soft: #e2dfd8;
  --text:      #1c1d21;
  --text-2:    #43454d;
  --text-3:    #61636a;
  /* Nudged one step darker: the card it sits on is no longer pure white, and
     a secondary line still has to clear 4.5:1 on it. */
  --muted:     #595b65;
  --faint:     #64666e;
  --accent:    #4d5edb;
  --accent-ink:#ffffff;
  --accent-2:  #8d4ddb;
  --accent-soft: #4d5edb14;
  --film:      #b8722a;
  --tv:        #17817a;
  --danger:    #c8342f;
  --danger-ink:#ffffff;
  --ok:        #1d8a52;
  --page-bg:   #ffffff;
  --page-ink:  #1a1b1f;
}

/*
 * Riso — the printshop. The default, and the one people should be able to
 * recognise across a room.
 *
 * A risograph prints a handful of flat spot inks onto warm paper, one pass
 * each, slightly out of register. Everything below follows from that and the
 * rules are worth stating, because half of them are things this stylesheet
 * does everywhere else:
 *
 *   Flat inks. No gradient anywhere, including the gradient-clipped numerals
 *     and the primary button. A gradient is the one thing a riso cannot do.
 *   Few inks. An ink navy, one blue, one fluorescent coral. A third spot
 *     colour would turn a print into a swatch library.
 *   Warm paper, never white — except the script page, which stays true white
 *     because a screenplay is printed on white and that is not the app's
 *     decision to make.
 *   Hard shadows. Elevation is an offset block of ink with no blur, the way a
 *     second pass sits beside the first, rather than a soft glow.
 *
 * Every pair below was measured before it was written; the working is in
 * `scratchpad/riso-palette.mjs`, 22 pairs, worst 3.20. The fluorescent coral
 * is the interesting one: it is legible ON paper but you overprint it with
 * the navy rather than reversing white out of it, because white on it is only
 * 3.44 and navy is 4.55. That is also how the real ink behaves.
 */
html[data-theme="riso"] {
  --bg:        #efe9de;
  --bg-deep:   #e4dccc;
  --surface:   #faf6ef;
  --raised:    #fffdf8;
  --raised-2:  #ffffff;
  --border:    #bfb6a4;
  --border-soft: #dad2c1;
  --text:      #16233d;
  --text-2:    #33405c;
  --text-3:    #4b5876;
  --muted:     #55617d;
  --faint:     #5e6a85;
  --accent:    #2440d8;
  --accent-ink:#ffffff;
  --accent-2:  #f5503e;
  --accent-soft: #2440d814;
  --film:      #c2410c;
  --tv:        #0f766e;
  --danger:    #c42a1c;
  --danger-ink:#ffffff;
  --ok:        #15803d;
  --page-bg:   #ffffff;
  --page-ink:  #14161d;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--fz-lead);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }

::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: var(--r-pill);
  border: 3px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--faint); background-clip: content-box; }

::selection { background: var(--accent); color: var(--accent-ink); }

/*
 * Keyboard focus.
 *
 * One ring, everywhere, and only for the keyboard: `:focus-visible` never fires
 * on a mouse click, so nothing flashes when clicking around, but tabbing through
 * the app always shows where you are. Anything with its own focus look — the
 * inputs, the writing surfaces — sets outline itself and wins on specificity.
 */
:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent) 75%, transparent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
[contenteditable]:focus-visible { outline: none; }

/* ---------- layout ---------- */

.app {
  display: grid;
  grid-template-columns: 56px var(--sidebar-w) 1fr;
  height: 100vh;
  overflow: hidden;
  /* Above the scenery layer, which is painted at z-index 0 behind it. */
  position: relative;
  z-index: var(--z-base);
}

/*
 * The rail: the app's spine. Eight doors, one icon each, always present —
 * the silhouette of professional studio software. The project list beside
 * it is now a drawer the rail opens and closes.
 */
.rail {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-2); padding: 12px 0 14px;
  background: var(--bg-deep);
  border-right: 1px solid var(--border-soft);
  z-index: var(--z-raised);
}
.rail-logo {
  width: 36px; height: 36px; border-radius: var(--r); margin-bottom: 12px;
  display: flex; align-items: center; justify-content: center;
}
.rail-logo img { border-radius: var(--r-md); display: block; }
.rail-btn {
  width: 38px; height: 38px; border-radius: var(--r); border: 0;
  background: none; color: var(--muted); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.rail-btn:hover { background: var(--raised); color: var(--text); }
.rail-btn .ic { width: 17px; height: 17px; }
.rail-gap { flex: 1; }
body.faux-fullscreen .rail { display: none; }

/* A laptop screen gives the sidebar less; the list gets its room back by
   collapsing the sidebar entirely (the ⟨ button), which always worked. */
@media (max-width: 1440px) { :root { --sidebar-w: min(max(232px, calc(232px * (var(--fs) - 0.5))), 34vw); } }
@media (max-width: 1200px) { :root { --sidebar-w: min(max(206px, calc(206px * (var(--fs) - 0.5))), 34vw); } }

.sidebar {
  background: var(--bg-deep);
  border-right: 1px solid var(--border-soft);
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.main {
  overflow-y: auto;
  min-width: 0;
  position: relative;
  scroll-behavior: smooth;
}

/*
 * Arriving on a page.
 *
 * Views used to snap in, which reads as a flicker when moving around quickly.
 * A short lift as each one arrives makes navigation feel deliberate. Only the
 * top-level view animates — redraws inside a page replace smaller containers
 * and are untouched — and it obeys both the app's own motion switch and the
 * machine's.
 */
@keyframes view-in { from { opacity: 0; transform: translateY(5px); } }
.main > * { animation: view-in .18s var(--ease); }
body.no-motion .main > * { animation: none; }
@media (prefers-reduced-motion: reduce) { .main > * { animation: none; } }

.loading {
  display: grid;
  place-items: center;
  height: 100%;
  color: var(--muted);
  font-size: var(--fz-body);
  letter-spacing: var(--track-fine);
}

/* ---------- sidebar ---------- */

.sidebar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 14px 10px;
}

.brand { display: flex; align-items: center; gap: var(--sp-5); }

/*
 * Set in the same lower case as the artwork, rather than the shouty uppercase
 * the old two-word name used, so the sidebar reads as the same thing as the
 * icon sitting next to it.
 */
.brand-text {
  font-size: var(--fz-lead);
  line-height: 1.1;
  color: var(--muted);
  letter-spacing: var(--track-tight);
  font-weight: 400;
}
/* The name itself carries the website's gradient ink — in the theme's own
   two colours. The last stop used to be a hardcoded #ff3ca0, so the wordmark
   ended in hot pink in all seven themes. */
.brand-text b {
  font-weight: 650;
  background: linear-gradient(100deg, var(--text) 20%, var(--accent) 75%,
    color-mix(in srgb, var(--accent) 55%, var(--accent-2)) 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}

.icon-btn {
  background: none; border: 1px solid transparent;
  color: var(--muted); cursor: pointer;
  width: 30px; height: 30px; border-radius: var(--r-sm);
  font-size: var(--fz-lead); display: grid; place-items: center;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.icon-btn:hover { background: var(--raised); color: var(--text); }

.search-trigger {
  margin: 4px 12px 12px;
  padding: 8px 11px;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  color: var(--muted);
  font: inherit; font-size: var(--fz-ui);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.search-trigger:hover { border-color: var(--border); color: var(--text-2); }
.search-trigger kbd {
  font: inherit; font-size: var(--fz-micro); letter-spacing: var(--track-label);
  background: var(--raised); border: 1px solid var(--border);
  border-radius: var(--r-xs); padding: 1px 5px; color: var(--faint);
}

.help-strip {
  margin: 0 12px 12px;
  padding: 9px 12px;
  display: block; width: calc(100% - 24px);
  text-align: left;
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: var(--r);
  color: var(--text-2);
  font: inherit; font-size: var(--fz-small);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease);
}
.help-strip:hover { border-color: var(--accent); }
.help-strip b { color: var(--accent); display: block; margin-top: 1px; }

.sidebar-nav { flex: 1; overflow-y: auto; padding: 0 8px 12px; min-height: 0; }

.nav-section { margin-bottom: 14px; }

.nav-label {
  font-size: var(--fz-micro); text-transform: uppercase; letter-spacing: var(--track);
  color: var(--faint); padding: 6px 10px 5px;
  /* Was space-between with two children. A third - the + that starts a film
     right here - would have left the count stranded in the middle. */
  display: flex; align-items: center; gap: var(--sp-3);
}
.nav-label .count { margin-left: auto; font-size: var(--fz-micro); color: var(--faint); }

.nav-item {
  display: flex; align-items: center; gap: var(--sp-5);
  padding: 7px 10px;
  border-radius: var(--r-md);
  color: var(--text-2);
  font-size: var(--fz-ui);
  cursor: pointer;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
  position: relative;
}
.nav-item:hover { background: var(--surface); color: var(--text); }
.nav-item.active { background: var(--accent-soft); color: var(--text); font-weight: 550; }
.nav-item.active::before {
  content: ""; position: absolute; left: 0; top: 7px; bottom: 7px;
  width: 2.5px; border-radius: var(--r-2xs); background: var(--accent);
}
.nav-item .dot {
  width: 7px; height: 7px; border-radius: var(--r-pill); flex: none;
  box-shadow: 0 0 0 2.5px color-mix(in srgb, currentColor 18%, transparent);
}
.nav-item .label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item .meta { font-size: var(--fz-micro); color: var(--faint); flex: none; }

/* The plus beside "Films" and "TV series". Quiet until the group is under the
   pointer, because a rail is a list of what you have, not a row of buttons. */
.nav-label .nav-add {
  width: 18px; height: 18px; line-height: 1;
  border: 0; border-radius: var(--r-sm); background: none; cursor: pointer;
  color: var(--faint); font: inherit; font-size: var(--fz-small);
  display: grid; place-items: center;
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.nav-section:hover .nav-add,
.nav-add:focus-visible { opacity: 1; }
.nav-add:hover { background: var(--surface); color: var(--text); }

.nav-empty { padding: 10px 12px; font-size: var(--fz-small); color: var(--faint); line-height: 1.5; }

.sidebar-foot {
  margin-top: auto; padding: var(--sp-5) var(--sp-6) 14px;
  border-top: 1px solid var(--border-soft);
  display: grid; gap: var(--sp-3);
  /* A grid track is min-content wide by default, so a button with a long label
     in big type pushes the whole column past the rail rather than wrapping
     inside it. This is what let the footer escape. */
  grid-template-columns: minmax(0, 1fr);
}
.sidebar-foot > * { min-width: 0; }
/* "+ New project" over two lines is a fine button. Off the side of the rail,
   on top of the writing, is not. */
#new-project-btn { white-space: normal; }

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

/*
 * ONE definition. A second `.btn` in the instrument layer set the radius and
 * the weight again — 7px against the 8px written here, 550 against 500 — so
 * neither number on this rule was the number that shipped. Both are folded
 * in, and both are now on the scale.
 */
.btn {
  font: inherit; font-size: var(--fz-ui); font-weight: 550;
  padding: 7px 13px;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--raised);
  color: var(--text);
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-4);
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), transform var(--dur-0) var(--ease), color var(--dur-1) var(--ease);
  white-space: nowrap;
}
.btn:hover { background: var(--raised-2); border-color: var(--faint); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .45; cursor: not-allowed; }

/*
 * The main button carries a little light: a gradient through the accent and a
 * soft glow of the same colour, the way the writer's website does its buttons.
 * Built from var(--accent) so every theme — including one somebody made from
 * their own photograph — gets the treatment in its own colour.
 *
 * ONE definition. There were three: this one, a letter-spacing tweak 3,900
 * lines down, and a third that replaced both the gradient and the glow and
 * added the hover lift. Only the last one's gradient ever rendered; the
 * 140deg three-stop written here was dead the day it was written. What is
 * below is what actually ships.
 */
.btn-primary {
  background: linear-gradient(180deg,
    color-mix(in srgb, #fff 6%, var(--accent)), var(--accent));
  border-color: transparent; color: var(--accent-ink);
  letter-spacing: var(--track-fine);
  box-shadow: var(--glow);
  transition: filter var(--dur-1) var(--ease), box-shadow var(--dur-2) var(--ease),
    transform var(--dur-1) var(--ease);
}
.btn-primary:hover {
  filter: brightness(1.07); border-color: transparent;
  box-shadow: var(--glow-lift);
  transform: translateY(-1px);
}
/* Without this the hover lift wins over `.btn:active` and the primary button
   is the only one in the app that does not press down. */
.btn-primary:active { transform: translateY(1px); }

.btn-ghost { background: transparent; border-color: transparent; color: var(--muted); }
.btn-ghost:hover { background: var(--surface); color: var(--text); border-color: transparent; }

/*
 * The destructive language: two weights, one colour, one meaning.
 *
 * .btn-danger is the quiet weight — the ✕ at the end of a row, "Take it off",
 * "Throw it away". Red ink on nothing, so a list of forty of them does not
 * read as a wall of alarms.
 *
 * .btn-destructive is the committed weight — the button you press in a
 * confirmation, where the entire job is to not look like Cancel. It fills
 * with --danger and takes --danger-ink.
 *
 * Both key off --danger, --danger-ink and --danger-glow, which every theme
 * now states. Before this, the confirm button was `.btn-primary` with
 * `background` and `border-color` overridden inline: the accent box-shadow
 * survived, so "Delete for good" was a red button wearing a blue, green or
 * pink halo depending on the theme, and the label inherited --accent-ink,
 * which is near-black in Blossom and Mountains — dark navy type on red.
 * Nothing here uses the accent, on purpose. A delete button should not glow
 * with the colour the app uses to say yes.
 */
.btn-danger { color: var(--danger); border-color: transparent; background: transparent; }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); border-color: transparent; }

.btn-destructive {
  background: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 72%, #000);
  color: var(--danger-ink);
  font-weight: 600;
  box-shadow: var(--glow-danger);
  transition: background var(--dur-1) var(--ease), box-shadow var(--dur-2) var(--ease),
    transform var(--dur-0) var(--ease);
}
.btn-destructive:hover {
  background: color-mix(in srgb, var(--danger) 86%, #000);
  border-color: color-mix(in srgb, var(--danger) 58%, #000);
  box-shadow: var(--glow-danger-lift);
  transform: translateY(-1px);
}
.btn-destructive:active { transform: translateY(1px); }
.btn-destructive:focus-visible { outline-color: var(--danger); }

.btn-sm { font-size: var(--fz-small); padding: 5px 10px; }
.btn-block { width: 100%; justify-content: center; }

/* ---------- page chrome ---------- */

.topbar {
  position: sticky; top: 0; z-index: var(--z-sticky);
  display: flex; align-items: center; gap: var(--sp-6);
  padding: 12px 28px;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border-soft);
}

.crumbs { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fz-ui); color: var(--muted); flex-wrap: wrap; min-width: 0; }
.crumbs a { cursor: pointer; border-radius: var(--r-xs); padding: 1px 3px; }
.crumbs a:hover { color: var(--text); background: var(--surface); }
.crumbs .sep { color: var(--faint); }
.crumbs .here { color: var(--text); font-weight: 550; }

.topbar-actions { margin-left: auto; display: flex; gap: var(--sp-4); align-items: center; }

/*
 * How wide the page gets.
 *
 * The history, because the file used to contradict itself about this: the
 * container was capped at 1180px, which on a 49in ultrawide meant a narrow
 * strip of app with a field of empty either side. The cap was lifted here —
 * and then quietly put back 3,800 lines further down, at 1340px, in a later
 * patch layer. The comment went on claiming there was no cap for a year.
 *
 * What actually ships, and what this rule now says on its own: there IS a
 * cap, at --content-max, and it is centred. 1340px is wide enough that the
 * card grids gain columns rather than stretching, and narrow enough that the
 * app still reads as a made page rather than a wall. A map takes the whole
 * window instead, because a map is not a page.
 *
 * What must never stretch is prose: a line of text 5000px wide is unreadable
 * and your eye loses its place on the way back. So reading matter keeps its
 * own measure (see below) whatever the container does.
 */
.view {
  padding: 28px 38px 90px;
  max-width: var(--content-max);
  margin-inline: auto;
}
.view.wide { max-width: none; }

/* Anything you actually read stays a comfortable line length. */
.page-sub,
.dossier-blurb,
.section-head .sub,
.empty-state p,
.hint,
.rich-edit,
/* Reading matter: the serif, at a width the eye can walk back across.
   Stated once — the face used to be set 800 lines away from the width. */
.rich-read {
  font-family: var(--font-read); font-size: var(--fz-lead); line-height: 1.7;
  max-width: var(--measure-wide);
}

.page-head { margin-bottom: var(--sp-8); }

/*
 * ONE definition of a page's title. There used to be four, spread over 4,000
 * lines and four eras: the base, a display-face override, a clamp, and the
 * accent underline. The clamp won the size, the display face won the family,
 * and the base's own 32px never rendered anywhere.
 */
.page-title {
  position: relative;
  margin: 0 0 6px; padding-bottom: 8px;
  font-family: var(--font-display);
  font-size: clamp(19px, var(--fz-title), 34px);
  font-weight: 700; letter-spacing: var(--track-tight); line-height: 1.18;
  overflow-wrap: break-word;
}
/* A short accent underline, like a title card. */
.page-title::after {
  content: ""; position: absolute; left: 1px; bottom: 0;
  width: 44px; height: 3px; border-radius: var(--r-2xs);
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 10%, transparent));
}
.page-title.editable { cursor: text; border-radius: var(--r-sm); padding: 0 4px 8px; margin-left: -4px; }
.page-title.editable:hover { background: var(--surface); }
.page-sub {
  color: var(--muted); font-size: var(--fz-body); margin: 0;
  max-width: var(--measure); overflow-wrap: break-word;
}

.eyebrow {
  font-size: var(--fz-tiny); text-transform: uppercase; letter-spacing: var(--track);
  color: var(--faint); margin-bottom: 7px; display: flex; align-items: center; gap: var(--sp-4);
}

/* ---------- tabs ---------- */

.tabs {
  display: flex; gap: var(--sp-1); margin: 20px 0 22px;
  border-bottom: 1px solid var(--border-soft);
  overflow-x: auto; scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
/*
 * ONE definition. A second one 3,800 lines down set the type — uppercase,
 * tracked, 11.5px, 600 — and the padding, so the size and padding written
 * here never rendered. Merged: what you read is what you get.
 */
.tab {
  padding: var(--sp-5) var(--sp-6);
  font-size: var(--fz-tiny); font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track);
  color: var(--muted);
  cursor: pointer; border: none; background: none; font-family: inherit;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  white-space: nowrap; display: flex; align-items: center; gap: var(--sp-4);
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.tab:hover { color: var(--text-2); }
.tab.active { color: var(--text); border-bottom-color: var(--accent); font-weight: 550; }
.tab .pill {
  font-size: var(--fz-micro); background: var(--raised); color: var(--muted);
  padding: 0 6px; border-radius: var(--r-pill); min-width: 18px; text-align: center;
}
.tab.active .pill { background: var(--accent-soft); color: var(--accent); }

/* ---------- cards & grids ---------- */

.grid { display: grid; gap: var(--sp-6); }
.grid-2 { grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fill, minmax(238px, 1fr)); }

.card {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  padding: 15px 16px;
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
  position: relative;
}
.card.clickable { cursor: pointer; }
.card.clickable:hover { border-color: var(--border); transform: translateY(-1px); box-shadow: var(--shadow-2); }

.card-title { font-weight: 600; font-size: var(--fz-lead); margin: 0 0 4px; display: flex; align-items: center; gap: var(--sp-4); }
.card-sub { color: var(--muted); font-size: var(--fz-ui); margin: 0; }
.card-foot { margin-top: 12px; display: flex; align-items: center; gap: var(--sp-5); font-size: var(--fz-tiny); color: var(--faint); flex-wrap: wrap; }

/*
 * `.project-card` and its parts — `.stripe`, `.body`, `.pc-row`, `.pc-art`,
 * `.has-art` — used to live here. That was the first of three home pages.
 * The shelf of posters replaced it; the only thing that still built one was
 * `projectCard()` in web/views/dashboard.js, which nothing calls. Each
 * project still glows its own colour, on `.shelf-poster`, which is where
 * `--pc` is read now.
 */

/*
 * Cards arrive one after another, quickly.
 *
 * The website reveals its projects as you reach them; here the grid is at the
 * top of the page, so they simply take their places in order when it opens.
 * Capped after the eighth or a big studio would spend a second dealing cards.
 */
@keyframes card-in { from { opacity: 0; transform: translateY(12px); } }
.grid > .card { animation: card-in .38s var(--ease) backwards; }
.grid > .card:nth-child(2) { animation-delay: .05s; }
.grid > .card:nth-child(3) { animation-delay: .1s; }
.grid > .card:nth-child(4) { animation-delay: .15s; }
.grid > .card:nth-child(5) { animation-delay: .19s; }
.grid > .card:nth-child(6) { animation-delay: .23s; }
.grid > .card:nth-child(7) { animation-delay: .26s; }
.grid > .card:nth-child(n+8) { animation-delay: .29s; }
body.no-motion .grid > .card { animation: none; }
@media (prefers-reduced-motion: reduce) { .grid > .card { animation: none; } }

/*
 * A chip: a fact about the thing beside it, not a button.
 *
 * ONE definition. There were two — this one, and a restatement 3,800 lines
 * down that turned it uppercase and shrank it. Only the second one's type
 * ever rendered, so half of what was written here was dead the day it was
 * written. What is below is what actually ships: the tracked micro-label,
 * in a stadium, in whatever colour the fact deserves.
 */
.chip {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  font-size: var(--fz-micro); font-weight: 650;
  text-transform: uppercase; letter-spacing: var(--track-label);
  padding: 2.5px var(--sp-4); border-radius: var(--r-pill);
  background: var(--raised); color: var(--text-2);
  border: 1px solid var(--border-soft);
  white-space: nowrap;
}
.chip.film { color: var(--film); border-color: color-mix(in srgb, var(--film) 30%, transparent); background: color-mix(in srgb, var(--film) 10%, transparent); }
.chip.tv   { color: var(--tv);   border-color: color-mix(in srgb, var(--tv) 30%, transparent);   background: color-mix(in srgb, var(--tv) 10%, transparent); }
.chip.accent { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 32%, transparent); background: var(--accent-soft); }
.chip.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 30%, transparent); background: color-mix(in srgb, var(--ok) 10%, transparent); }

/* ---------- list rows ---------- */

.rows { display: flex; flex-direction: column; gap: var(--sp-3); }

.row {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: 11px 14px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.row.clickable { cursor: pointer; }
.row.clickable:hover { border-color: var(--border); background: var(--raised); }
.row-main { flex: 1; min-width: 0; }
.row-title { font-weight: 550; font-size: var(--fz-body); display: flex; align-items: center; gap: var(--sp-4); }
.row-sub { color: var(--muted); font-size: var(--fz-small); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/*
 * Always faintly there, not hidden until hover.
 *
 * These used to appear only on hovering a .row, which meant two things: on a
 * card or a franchise person there is no .row to hover, so they never showed
 * at all; and even on a row you had to already suspect they existed. The
 * writer's actual complaint was not being able to find how to delete things.
 */
.row-actions {
  display: flex; gap: var(--sp-2); flex: none;
  opacity: .45; transition: opacity var(--dur-1) var(--ease);
}
.row:hover .row-actions, .row:focus-within .row-actions,
.card:hover .row-actions, .card:focus-within .row-actions,
.fr-person:hover .row-actions, .fr-person:focus-within .row-actions,
.row-actions:hover, .row-actions:focus-within { opacity: 1; }

.row-num {
  width: 40px; flex: none; text-align: center;
  font-family: var(--font-mono); font-size: var(--fz-small); color: var(--faint);
  background: var(--raised); border-radius: var(--r-sm); padding: 3px 0;
}

.avatar {
  overflow: hidden;
  width: 34px; height: 34px; flex: none; border-radius: var(--r-pill);
  display: grid; place-items: center;
  font-size: var(--fz-small); font-weight: 650; color: #fff;
  letter-spacing: var(--track-fine);
}

/*
 * Any photo inside any avatar fills its circle and stops there. Without
 * this, a face in the writing desk's Scene tab rendered at its natural
 * size and burst the panel — reported as "shows a photo and looks bad".
 */
.avatar > img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---------- a character's photo ---------- */

/* When there is a picture it fills the circle; the initials stay underneath
   as the fallback, so a missing file never leaves an empty hole. */
.avatar-shot { overflow: hidden; padding: 0; border: none; position: relative; }
.avatar-shot img {
  width: 100%; height: 100%; object-fit: cover; display: block; border-radius: inherit;
}

.avatar-wrap { position: relative; flex: none; }

button.avatar-shot { cursor: pointer; }

/* The little camera, only while you are pointing at it. */
.avatar-hint {
  position: absolute; inset: 0; display: grid; place-items: center;
  background: rgba(12, 14, 20, .55); border-radius: inherit;
  font-size: var(--glyph-lg); opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
button.avatar-shot:hover .avatar-hint,
button.avatar-shot:focus-visible .avatar-hint { opacity: 1; }

.avatar-clear {
  position: absolute; top: -4px; right: -4px;
  width: 20px; height: 20px; border-radius: var(--r-pill); padding: 0;
  display: grid; place-items: center; cursor: pointer;
  font-size: var(--glyph-sm); line-height: 1;
  background: var(--raised); color: var(--muted);
  border: 1px solid var(--border);
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.avatar-wrap:hover .avatar-clear,
.avatar-clear:focus-visible { opacity: 1; }
.avatar-clear:hover { background: var(--danger); color: #fff; border-color: var(--danger); }

/* ---------- picking roles ---------- */

.role-pick { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: 6px; }

.role-chip {
  font: inherit; font-size: var(--fz-small);
  padding: 5px 11px; border-radius: var(--r-pill); cursor: pointer;
  background: var(--raised); color: var(--muted);
  border: 1px solid var(--border-soft);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.role-chip:hover { color: var(--text); border-color: var(--border); }
.role-chip.on {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 550;
}
.role-chip.add { border-style: dashed; }

/* ---------- empty state ---------- */

/*
 * An empty state is an invitation, so it is drawn like one: a soft disc with
 * the sprite mark inside it, then the question, then the way in. The dashed
 * border stays — it says "this is where something will be" rather than "this
 * is a thing" — but it is a hairline of the accent rather than plain grey.
 */
/*
 * `.empty-state` and not `.empty`.
 *
 * `.empty` was two ideas wearing one name: this box, and the modifier a dozen
 * components put on a field nobody has filled in yet - `.stanza.empty`,
 * `.dossier-field.empty`, `.spec-row.empty`, `.naming-half.empty`,
 * `.sense.empty`, `.life-stage.empty`, `.provenance.empty`. All of those
 * matched this rule, so 64 unanswered prompts on one character page and 28 on
 * a world entry were each drawn as a 44px-padded, centre-aligned, dashed
 * accent box. On the character dossier the item number stayed left while its
 * own label was centred by this rule, one line apart: the single loudest
 * reason that page read as assembled rather than designed.
 */
/*
 * *** NEVER SLICED ACROSS COLUMNS. ***
 *
 * Logan: "the pictures section is really messed up btw" - the icon in the first
 * column, the heading in the second, the sentence in the third and the button
 * in the fourth.
 *
 * The picture strip is `columns: 4 190px` so a wall of stills packs like a
 * pinboard instead of a grid of identical crops. Multicol does not merely
 * ARRANGE what is put in it, it FRAGMENTS it - a block taller than a column is
 * cut and continued in the next one, and that applies to descendants, not only
 * to direct children. One message became four pieces standing side by side.
 *
 * `column-span: all` is ignored outside a multi-column box, so this costs
 * nothing everywhere else and fixes it everywhere it can happen: the corkboard
 * pinboard is `columns` too, and so is anything else that packs.
 */
.empty-state {
  column-span: all;
  break-inside: avoid;

  text-align: center;
  padding: var(--sp-10) var(--sp-8);
  border: 1px dashed color-mix(in srgb, var(--accent) 22%, var(--border));
  border-radius: var(--r-lg);
  color: var(--muted);
  background:
    radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--accent) 5%, transparent), transparent 70%),
    color-mix(in srgb, var(--surface) 45%, transparent);
}
.empty-state .big {
  width: 54px; height: 54px; margin: 0 auto var(--sp-6);
  display: grid; place-items: center;
  font-size: var(--fz-display);          /* the emoji call sites that remain */
  line-height: 1;
  border-radius: var(--r-pill);
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent);
}
/* The drawn mark, at a size that reads as artwork rather than as chrome. */
.empty-state .big .ic.empty-mark { width: 25px; height: 25px; vertical-align: 0; opacity: .9; }
.empty-state h3 {
  margin: 0 0 var(--sp-3); font-size: var(--fz-heading);
  color: var(--text); font-weight: 600;
}
.empty-state p { margin: 0 auto var(--sp-7); font-size: var(--fz-body); max-width: var(--measure-short); line-height: 1.6; }

/* The sentence a confirmation turns on. It used to be set with an inline
   `font-size:14px` from ui.js, which ignored --fs entirely, so the one screen
   where reading carefully matters most was the one the text-size control
   could not reach. */
.confirm-message {
  margin: 0; font-size: var(--fz-lead); line-height: 1.6;
  color: var(--text-2); max-width: var(--measure-short);
}
/* Two sentences that are two thoughts. A dialogue that has to say both what
   happened and where the writing went ran as one block, and the second thought
   is the one that stops the worry. Only ever bites from the second paragraph
   on, so every one-line confirmation is exactly as it was. */
.confirm-message + .confirm-message { margin-top: 10px; }

/* ---------- forms ---------- */

.field { margin-bottom: 14px; }
.field > label {
  display: block; font-size: var(--fz-small); font-weight: 550;
  color: var(--text-2); margin-bottom: 5px; letter-spacing: var(--track-fine);
}
.field .hint { font-size: var(--fz-tiny); color: var(--faint); margin-top: 5px; line-height: 1.5; }

/*
 * A field you type into.
 *
 * It sits BELOW the card it is on rather than above it — a well cut into the
 * surface, which is what a text field physically is — so it reads as somewhere
 * to put something rather than as another button. The focus state is the one
 * ring the whole app uses, plus the accent on the edge, so the eye finds the
 * field it is in without anything moving.
 */
.input, .textarea, .select {
  width: 100%;
  font: inherit; font-size: var(--fz-body);
  padding: 8px 11px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease),
    background var(--dur-1) var(--ease);
}
.input:hover, .textarea:hover, .select:hover { border-color: var(--faint); }
.input:focus, .textarea:focus, .select:focus {
  outline: none; border-color: var(--accent);
  box-shadow: var(--ring-focus);
}
.input:disabled, .textarea:disabled, .select:disabled {
  opacity: .55; cursor: not-allowed; background: var(--surface);
}
.textarea { resize: vertical; min-height: 76px; line-height: 1.6; }

/* ---- the dropdown -----------------------------------------------------
 *
 * A `<select>` is two things, and only one of them was ever ours. The closed
 * control is an ordinary box we can draw. The list that drops out of it is
 * drawn by the operating system: white, square-cornered, in Segoe UI, with
 * the system's own blue highlight — over a dark studio, in an app whose whole
 * point is that it does not look like a form. Nineteen of them across twelve
 * views, and no amount of styling the closed box fixed the open one.
 *
 * `appearance: base-select` hands the picker back to CSS: the popup becomes a
 * real element we can style, with `::picker(select)` and `::picker-icon`. It
 * is guarded by @supports, so on an engine without it the control below is
 * exactly what shipped before and nothing breaks. See the report for the
 * markup-level fallback if the bundled engine turns out to be older.
 */
.select {
  appearance: none;
  cursor: pointer;
  padding-right: 32px;
  /* The chevron, drawn: two hairlines meeting at a point, in the text's own
     colour, so it belongs to the control rather than to the platform. */
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 17px) calc(50% + 1px), calc(100% - 12px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  color: var(--text);
}
.select:focus { background-image:
  linear-gradient(45deg, transparent 50%, var(--accent) 50%),
  linear-gradient(135deg, var(--accent) 50%, transparent 50%); }

@supports (appearance: base-select) {
  .select {
    appearance: base-select;
    background-image: none;
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-4); text-align: left;
  }
  .select::picker-icon {
    content: "";
    width: 9px; height: 9px; flex: none;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: translateY(-2px) rotate(45deg);
    opacity: .65;
    transition: transform var(--dur-1) var(--ease), opacity var(--dur-1) var(--ease);
  }
  .select:hover::picker-icon { opacity: 1; }
  .select:open::picker-icon { transform: translateY(1px) rotate(225deg); opacity: 1; }

  /* The list, at last, in the app's own hand. */
  .select::picker(select) {
    appearance: base-select;
    margin-top: var(--sp-2);
    padding: var(--sp-2);
    min-width: anchor-size(width);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r);
    box-shadow: var(--elev-3);
    font-family: var(--font-ui);
    font-size: var(--fz-body);
    color: var(--text);
    max-height: 46vh;
    overflow-y: auto;
  }
  .select option {
    display: flex; align-items: center; gap: var(--sp-4);
    padding: 7px var(--sp-5);
    border-radius: var(--r-sm);
    background: none; color: var(--text-2);
    cursor: pointer;
    transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
  }
  .select option::checkmark { display: none; }   /* the tick is the accent bar below */
  .select option:hover,
  .select option:focus { background: var(--raised); color: var(--text); outline: none; }
  .select option:checked {
    background: var(--accent-soft); color: var(--text); font-weight: 550;
    box-shadow: inset 2px 0 0 var(--accent);
  }
  /* The dialogs it opens inside are on --z-modal; the picker must clear them. */
  .select::picker(select) { z-index: var(--z-pop); }
}
.input::placeholder, .textarea::placeholder { color: var(--faint); }

.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-6); }

.seg { display: flex; gap: var(--sp-2); background: var(--bg); padding: var(--sp-2); border-radius: var(--r); border: 1px solid var(--border); }
.seg button {
  flex: 1; font: inherit; font-size: var(--fz-ui); padding: 7px 10px;
  background: none; border: none; border-radius: var(--r-sm); color: var(--muted); cursor: pointer;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.seg button:hover { color: var(--text); }
.seg button.on { background: var(--raised-2); color: var(--text); font-weight: 550; box-shadow: var(--shadow-1); }

.swatches { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.swatch {
  width: 26px; height: 26px; border-radius: var(--r-pill); cursor: pointer;
  border: 2px solid transparent; transition: transform var(--dur-1) var(--ease);
}
.swatch:hover { transform: scale(1.12); }
.swatch.on { border-color: var(--text); }

/* The nine and the tenth, on one row: the radiogroup is its own element, so
   they need a parent to sit them side by side. */
.swatch-row { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }

/* The tenth one, which is however many you like.

   A colour wheel behind a circle: the native input is stretched well past the
   swatch and clipped by it, because a colour input draws its own swatch inside
   itself and there is no way to style that away. Wearing the colour it holds,
   so the row reads as ten colours rather than nine and a control. */
.swatch-custom {
  position: relative; overflow: hidden; display: block; padding: 0;
  background:
    conic-gradient(#ff3b30, #ff9500, #ffcc00, #34c759, #00c7be,
                   #007aff, #5856d6, #af52de, #ff2d55, #ff3b30);
}
.swatch-custom input[type="color"] {
  position: absolute; inset: -30%; width: 160%; height: 160%;
  border: 0; padding: 0; background: none; cursor: pointer; opacity: 0;
}
.swatch-custom:focus-within { border-color: var(--text); }

/* Inline editable text — click a value on the page and type. */
/*
 * *** A FIELD HAS TO LOOK LIKE A FIELD BEFORE IT IS TOUCHED. ***
 *
 * This had NOTHING at rest - no edge, no rule, no tint. It showed a background
 * on hover and a ring on focus, which means every editable on a character page,
 * a world entry or a project page was invisible until the pointer happened to
 * cross it. Logan, on the synopsis: "as of now I just see the words and don't
 * see anywhere to write", and again on the world pages: "all the options are far
 * to hard to see or read and its hard to know where to write or edit, there
 * needs to be underlines or something."
 *
 * He is describing the fix. A hairline under the text is what every form on
 * earth uses to say "you may type here", it costs one pixel of ink, and it does
 * not shout - which matters on a page holding thirty of them.
 *
 * Deliberately NOT a full box. Thirty boxed fields on a dossier reads as a tax
 * return, and this app is meant to feel like paper. The line is enough: it draws
 * the eye along the row a writer is meant to fill in and disappears the moment
 * they are reading rather than filling.
 *
 * It strengthens on hover and gives way entirely to the focus ring, so the three
 * states say three different things: you may write here, you are about to, you
 * are.
 */
.inline-edit {
  border-radius: var(--r-sm); padding: 2px 5px; margin: -2px -5px;
  cursor: text; min-height: 1.5em;
  white-space: pre-wrap;
  box-shadow: inset 0 -1px 0 var(--border-soft);
  transition: background var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.inline-edit:hover { background: var(--raised); box-shadow: inset 0 -1px 0 var(--border); }
.inline-edit:focus { outline: none; background: var(--bg); box-shadow: inset 0 0 0 2px var(--accent-soft), var(--ring-on); }
.inline-edit.placeholder::before { content: attr(data-placeholder); color: var(--faint); }

/* ---------- detail panel (key/value blocks) ----------
 *
 * ONE definition. There were three: this one, a padding restatement in the
 * "room to breathe" layer that set exactly the same 18/20 it already had,
 * and a third in the instrument layer that added the lit top edge. Two of
 * them did nothing at all; the third is folded in here, where it can be
 * seen. A panel is the app's quietest container — a surface, a hairline,
 * and a whisper of the accent along the top edge so it reads as lit rather
 * than as a box.
 */
.panel {
  background: var(--surface);
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 4.5%, transparent), transparent 42px);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  padding: 18px 20px;
}
.panel + .panel { margin-top: var(--sp-6); }
.panel h3 {
  margin: 0 0 var(--sp-5); font-size: var(--fz-small); text-transform: uppercase;
  letter-spacing: var(--track); color: var(--faint); font-weight: 600;
}

.kv { display: grid; grid-template-columns: 132px 1fr; gap: var(--sp-5) var(--sp-7); align-items: start; font-size: var(--fz-body); }
.kv dt { color: var(--muted); font-size: var(--fz-small); padding-top: 2px; }
.kv dd { margin: 0; }

.split { display: grid; grid-template-columns: 1fr 320px; gap: var(--sp-7); align-items: start; }
@media (max-width: 1080px) { .split { grid-template-columns: 1fr; } }

/* ---------- modal ---------- */

/*
 * Where a dialog sits.
 *
 * This used to be `place-items: start center` with `padding: 8vh 20px 20px`,
 * which pinned every dialog 8vh from the top whatever its height — so a
 * two-line confirmation floated in the upper third of a big screen with a
 * field of scrim underneath it. `safe center` centres the short ones
 * optically and falls back to the top for a tall one, so a dialog taller than
 * the window never has its head scrolled off above the viewport.
 */
.modal-backdrop {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgb(0 0 0 / .55);
  backdrop-filter: blur(3px);
  display: grid;
  justify-items: center;
  align-items: safe center;
  padding: 24px 20px;
  overflow-y: auto;
  animation: fade var(--dur-2) var(--ease);
}
@keyframes fade { from { opacity: 0; } }
@keyframes rise { from { opacity: 0; transform: translateY(10px) scale(.99); } }

/*
 * One height cap, not two. The dialog is a flex column capped to the window;
 * the body is the part that scrolls. There used to be a second cap of
 * `max-height: 62vh` on `.modal-body` fighting this one, so a dialog on a
 * short window got two scroll regions and a body that stopped short of the
 * space it had.
 */
.modal {
  width: min(560px, 100%);
  max-width: calc(100vw - 24px);
  max-height: calc(100vh - 48px);   /* the backdrop's own padding, doubled */
  display: flex; flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
  animation: rise var(--dur-2) var(--ease);
  overflow: hidden;
}
.modal.wide { width: min(820px, 100%); }
.modal:focus { outline: none; }   /* it takes focus on open; the ring belongs on the controls */
.modal-head { padding: 20px 22px 6px; }
.modal-head h2 { margin: 0; font-size: var(--fz-heading); font-weight: 650; font-family: var(--font-read); }
.modal-head p { margin: 5px 0 0; font-size: var(--fz-ui); color: var(--muted); }
.modal-body { padding: 16px 22px; overflow-y: auto; min-height: 0; }
.modal-foot {
  padding: 14px 22px; display: flex; gap: var(--sp-4); justify-content: flex-end;
  align-items: center; flex-wrap: wrap;
  border-top: 1px solid var(--border-soft); background: var(--bg-deep);
}

/* ---------- command palette / search ---------- */

.palette { width: min(620px, 100%); padding: 0; }
.palette input {
  width: 100%; font: inherit; font-size: var(--fz-lead); padding: 16px 20px;
  background: none; border: none; border-bottom: 1px solid var(--border-soft); color: var(--text);
}
.palette input:focus { outline: none; }
.palette-results { max-height: 56vh; overflow-y: auto; padding: var(--sp-3); }
.palette-item {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: 9px 12px; border-radius: var(--r-md); cursor: pointer;
}
.palette-item:hover, .palette-item.on { background: var(--raised); }
.palette-item .kind {
  font-size: var(--fz-micro); text-transform: uppercase; letter-spacing: var(--track-label);
  color: var(--faint); flex: none; width: 66px;
}
.palette-item .txt { min-width: 0; flex: 1; }
.palette-item .txt b { display: block; font-weight: 550; font-size: var(--fz-body); }
.palette-item .txt span { display: block; font-size: var(--fz-small); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.palette-hint { padding: 26px 20px; text-align: center; color: var(--faint); font-size: var(--fz-ui); }

/* ---------- tooltips ----------
 *
 * The app's own, replacing the operating system's. Built and positioned by
 * `ui.js`, which sets `top`/`left` in viewport coordinates and flips it below
 * the target when there is no room above. `position: fixed` so no
 * `overflow: hidden` ancestor can cut it in half.
 */
.tip {
  position: fixed; z-index: var(--z-top);
  max-width: 260px;
  padding: 6px var(--sp-5);
  background: var(--raised-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  box-shadow: var(--elev-2);
  font-family: var(--font-ui);
  font-size: var(--fz-small);
  line-height: 1.45;
  text-align: center;
  pointer-events: none;               /* never stands between cursor and target */
  opacity: 0;
  transform: translateY(3px);
  transition: opacity var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.tip.on { opacity: 1; transform: none; }
.tip[data-where="below"] { transform: translateY(-3px); }
.tip[data-where="below"].on { transform: none; }
body.no-motion .tip { transition: none; }
@media (prefers-reduced-motion: reduce) { .tip { transition: none; } }

/* ---------- toasts ---------- */

.toast-root {
  position: fixed; bottom: 20px; right: 20px; z-index: var(--z-toast);
  display: flex; flex-direction: column; gap: var(--sp-4); align-items: flex-end;
  pointer-events: none;              /* the gaps between toasts stay clickable */
}
.toast-root > * { pointer-events: auto; }

.toast {
  display: flex; align-items: center; gap: var(--sp-6);
  background: var(--raised-2);
  border: 1px solid var(--border);
  /* Severity is a rule down the left edge, not the colour of the sentence.
     Red body text on a dark chip is the least readable way to say "error" —
     it fails contrast and it fights the one thing you need to read. */
  border-left: 3px solid var(--border);
  padding: 10px 15px 10px 13px;      /* 13 + 3 = the 15 the other sides have */
  color: var(--text);
  border-radius: var(--r);
  font-size: var(--fz-ui);
  box-shadow: var(--shadow-2);
  animation: rise var(--dur-2) var(--ease);
  max-width: 380px;
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
.toast.err { border-left-color: var(--danger); }
.toast.ok  { border-left-color: var(--ok); }

/* The way out. Every toast leaves the same way, including the sticky one,
   which used to simply vanish mid-sentence. */
.toast.leaving { opacity: 0; transform: translateY(6px) scale(.98); }

/* When more arrive than the corner can hold: one line standing in for the
   rest, so thirty failed file drops are one chip and not a wall. */
.toast-more {
  font-size: var(--fz-micro); letter-spacing: var(--track); text-transform: uppercase;
  color: var(--muted); background: var(--surface);
  border: 1px solid var(--border-soft); border-radius: var(--r-pill);
  padding: 3px 11px; box-shadow: var(--shadow-1);
  animation: rise var(--dur-2) var(--ease);
}

.toast-action {
  font: inherit; font-size: var(--fz-small); font-weight: 600;
  padding: 4px 11px; border-radius: var(--r-sm); cursor: pointer;
  background: var(--accent); border: none; color: var(--accent-ink);
  flex: none;
}
.toast-action:hover { filter: brightness(1.1); }
.toast-action:disabled { opacity: .5; }

.spinner {
  width: 13px; height: 13px; flex: none; border-radius: var(--r-pill);
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ============================================================
   The beat sheet editor — a Word-style page with a guide.
   ============================================================ */

.editor-shell { background: var(--bg-deep); min-height: 100%; }

.editor-bar {
  position: sticky; top: 0; z-index: var(--z-rail);
  display: flex; align-items: center; gap: var(--sp-4);
  /* On a narrow window or at a big text size the buttons wrap to a second
     line. Clipping them left whole tools unreachable. */
  flex-wrap: wrap; row-gap: var(--sp-3);
  padding: 9px 20px;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border-soft);
  flex-wrap: wrap;
}
.editor-bar .divider { width: 1px; height: 20px; background: var(--border); margin: 0 3px; }
/* `.spacer` is global now — see the utilities. */

.fmt-btn {
  width: 30px; height: 28px; border-radius: var(--r-sm);
  border: 1px solid transparent; background: none; color: var(--text-2);
  cursor: pointer; font-size: var(--fz-body); display: grid; place-items: center;
  font-family: var(--font-read);
}
.fmt-btn:hover { background: var(--raised); color: var(--text); }
.fmt-btn.on { background: var(--accent-soft); color: var(--accent); }

.save-state { font-size: var(--fz-small); color: var(--faint); display: flex; align-items: center; gap: var(--sp-3); }
.save-state .dot { width: 6px; height: 6px; border-radius: var(--r-pill); background: var(--faint); }
.save-state.dirty .dot { background: var(--film); }
.save-state.saved .dot { background: var(--ok); }

/* the page */
.paper-wrap { padding: 30px 20px 140px; display: flex; justify-content: center; }

/*
 * A beat sheet, shaped like the document it is.
 *
 * Logan: "The beat sheet pages aren't proper sized, I want them the same size as
 * a word doc but they are smaller." The width was already 8.5in. What was
 * missing was the HEIGHT: a card that stopped where the words stopped, so a
 * three-beat sheet was a stub and nothing about it said "page". Eleven inches to
 * begin with, and the same eleven-inch boundaries the outline page draws, for
 * the same reason and with the same caveat - this shows where the pages end, it
 * does not paginate.
 *
 * `border-box` so the boundaries measure from the top of the paper rather than
 * from inside the padding, or the first break lands an inch early.
 */
.paper {
  width: min(8.5in, 100%);
  min-height: 11in;
  box-sizing: border-box;
  background: var(--page-bg);
  color: var(--page-ink);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-xs);
  box-shadow: var(--shadow-3);
  padding: 0.9in 1in 1.1in;
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent calc(11in - 1px),
    var(--border-soft) calc(11in - 1px),
    var(--border-soft) 11in
  );
}
@media print { .paper { background-image: none; min-height: 0; } }

.paper-title {
  font-family: var(--font-read);
  font-size: var(--fz-display); font-weight: 700; text-align: center;
  margin: 0 0 4px; letter-spacing: var(--track-tight);
  outline: none; border-radius: var(--r-xs);
}
.paper-title:focus { box-shadow: var(--ring-focus); }
.paper-meta { text-align: center; color: var(--muted); font-size: var(--fz-small); margin-bottom: 34px; letter-spacing: var(--track-label); }

/*
 * One beat, told apart from the next one.
 *
 * Logan: "there is no separator for the beats other then the headers, its hard
 * to see the separation of the beats, they should look like it does in word."
 * Thirty pixels of air was the only thing between them, and air reads as one
 * long column the moment two beats run to similar lengths.
 */
.beat {
  position: relative;
  margin-bottom: 26px; padding-bottom: 26px;
  border-bottom: 1px solid var(--border-soft);
  /* Clear of the sticky bar plus a little air, whatever the bar's height turns
     out to be today. */
  scroll-margin-top: calc(var(--bar-h, 50px) + 16px);
}
/* No rule under the last one: a line with nothing after it reads as the page
   being cut off rather than as a separator. */
.beat:last-child { border-bottom: 0; padding-bottom: 0; }

.beat-head { display: flex; align-items: baseline; gap: var(--sp-5); margin-bottom: 2px; }

.beat-heading {
  font-family: var(--font-read);
  font-size: var(--fz-stat); font-weight: 700;
  outline: none; border-radius: var(--r-xs); padding: 1px 4px; margin-left: -4px;
  flex: 1; min-width: 0;
}
.beat-heading:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
.beat-heading:focus { box-shadow: var(--ring-focus); background: transparent; }

.beat-page {
  font-family: var(--font-mono); font-size: var(--fz-tiny); color: var(--muted);
  outline: none; border-radius: var(--r-xs); padding: 1px 5px; flex: none; min-width: 42px;
  text-align: right;
}
.beat-page:hover { background: var(--raised); }
.beat-page:empty::before { content: "pg"; color: var(--faint); }

.beat-guide {
  font-family: var(--font-read);
  font-size: var(--fz-ui); font-style: italic; color: var(--muted);
  line-height: 1.6;
  border-left: 2px solid color-mix(in srgb, var(--accent) 42%, transparent);
  padding: 2px 0 2px 12px;
  margin: 6px 0 12px;
  outline: none; border-radius: 0 var(--r-xs) var(--r-xs) 0;
}
.beat-guide:focus { background: var(--raised); }
body.hide-guides .beat-guide { display: none; }

.beat-body {
  font-family: var(--font-read);
  font-size: var(--fz-subhead);
  line-height: 1.75;
  outline: none;
  min-height: 3.4em;
  border-radius: var(--r-xs);
  padding: 4px 6px;
  margin: 0 -6px;
  transition: background var(--dur-1) var(--ease);
}
.beat-body:hover { background: color-mix(in srgb, var(--accent) 3.5%, transparent); }
.beat-body:focus { background: color-mix(in srgb, var(--accent) 5%, transparent); }
.beat-body:empty::before {
  content: "Start writing…";
  color: var(--faint); font-style: italic;
}
.beat-body p { margin: 0 0 .85em; }
.beat-body ul, .beat-body ol { margin: 0 0 .85em; padding-left: 1.5em; }
.beat-body li { margin-bottom: .3em; }

.beat-tools {
  position: absolute; right: -46px; top: 0;
  display: flex; flex-direction: column; gap: var(--sp-1);
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.beat:hover .beat-tools, .beat:focus-within .beat-tools { opacity: 1; }
.beat-tools button {
  width: 28px; height: 26px; border-radius: var(--r-sm); border: 1px solid transparent;
  background: none; color: var(--faint); cursor: pointer; font-size: var(--fz-ui);
  display: grid; place-items: center;
}
.beat-tools button:hover { background: var(--raised); color: var(--text); }
.beat-tools button.danger:hover { color: var(--danger); }

.beat.done .beat-heading { color: var(--ok); }
/* Drawn from the sheet's own tick rather than typed, so a done beat does not
   change shape with whatever emoji font the machine happens to have. */
.beat.done .beat-heading::after {
  content: ''; display: inline-block; vertical-align: -1px;
  width: 13px; height: 13px; margin-left: 5px;
  background-color: currentColor;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}

@media (max-width: 1000px) {
  .beat-tools { position: static; flex-direction: row; opacity: 1; margin-bottom: 6px; }
}

/*
 * *** THE BEAT SHEET'S PAPER IS DRAWN BEHIND IT NOW, NOT BY IT. ***
 *
 * A beta tester: "the beat sheet still doesnt create a new page - atleast foe
 * the blank page". Measured, a sheet of sixteen beats was ONE piece of paper
 * sixty-three inches tall, with a hairline ruled across it every eleven inches
 * to suggest where a page would end. The treatment editor beside it draws
 * fifteen real sheets of eleven inches each.
 *
 * So `.beat-paper` gives up being paper. It keeps its type and its margins and
 * becomes the layer the words flow in; the `.sheet` elements underneath are the
 * pages, which is exactly the arrangement `.doc-paper` already had.
 *
 * The `z-index` is what puts the words in front of the paper rather than under
 * it - `.sheet-layer` is `z-index: 0` and positioned, so an unpositioned
 * sibling after it still loses.
 */
.beat-paper {
  position: relative;
  z-index: 1;
  min-height: 0;
  background: none;
  background-image: none;   /* the ruled hairline every 11in, which lied */
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* Cleared onto the next sheet rather than cut in half by the edge of one. A
   beat is the unit a writer thinks in, so a beat is the unit that moves. */
.beat { margin-top: var(--lead, 0px); }

/* the outline rail beside the page */
.editor-layout { display: grid; grid-template-columns: 232px 1fr; align-items: start; }
@media (max-width: 1180px) { .editor-layout { grid-template-columns: 1fr; } .outline-rail { display: none; } }

.outline-rail {
  /* --bar-h is measured, not guessed - see watchBarHeight in app.js. 47 was
     right at 100% text and wrong at every larger size, which put the top of
     the outline behind the toolbar. */
  position: sticky; top: var(--bar-h, 50px);
  max-height: calc(100vh - var(--bar-h, 50px)); overflow-y: auto;
  padding: 24px 8px 40px 20px;
}
.outline-rail h4 {
  font-size: var(--fz-micro); text-transform: uppercase; letter-spacing: var(--track);
  color: var(--faint); margin: 0 0 10px 10px; font-weight: 600;
}
.outline-item {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: 5px 10px; border-radius: var(--r-md); cursor: pointer;
  font-size: var(--fz-small); color: var(--muted); line-height: 1.35;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.outline-item:hover { background: var(--surface); color: var(--text); }
.outline-item.filled { color: var(--text-2); }
.outline-item .tick { width: 6px; height: 6px; border-radius: var(--r-pill); background: var(--border); flex: none; }
.outline-item.filled .tick { background: var(--accent); }
.outline-item.done .tick { background: var(--ok); }
.outline-item .n { font-family: var(--font-mono); font-size: var(--fz-micro); color: var(--faint); width: 16px; flex: none; }

.progress-bar { height: 4px; background: var(--border-soft); border-radius: var(--r-pill); overflow: hidden; margin: 4px 10px 14px; }
.progress-bar i { display: block; height: 100%; background: var(--accent); border-radius: var(--r-pill); transition: width var(--dur-3) var(--ease); }

/* ---------- file preview ---------- */

.preview-shell { background: var(--page-bg); color: var(--page-ink); border-radius: var(--r); padding: 34px 44px; border: 1px solid var(--border-soft); }
.preview-shell.script { font-family: var(--font-mono); font-size: var(--fz-small); line-height: 1.5; }

.fdx-scene { font-weight: 700; text-transform: uppercase; margin: 1.6em 0 .5em; }
.fdx-action { margin: 0 0 .9em; }
.fdx-character { margin: 1.1em 0 0; padding-left: 2.2in; text-transform: uppercase; }
.fdx-parenthetical { margin: 0; padding-left: 1.6in; }
.fdx-dialogue { margin: 0 0 .5em; padding-left: 1in; padding-right: 1.5in; }
.fdx-transition { text-align: right; text-transform: uppercase; margin: 1em 0; }
.fdx-shot { font-weight: 700; margin: 1.2em 0 .5em; }
.fdx-general { margin: 0 0 .9em; }

.docx-view { font-family: var(--font-read); font-size: var(--fz-lead); line-height: 1.7; }
.docx-view p { margin: 0 0 .8em; }
.docx-view .h { font-weight: 700; font-size: 1.15em; margin: 1.4em 0 .5em; font-family: var(--font-read); }
.docx-view .b { font-weight: 700; }

.plain-view { font-family: var(--font-mono); font-size: var(--fz-ui); white-space: pre-wrap; line-height: 1.6; }

/* ---------- versions ---------- */

.version {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: 9px 12px; border-radius: var(--r-md);
  border: 1px solid var(--border-soft); background: var(--bg);
}
.version + .version { margin-top: 6px; }
.version.latest { border-color: color-mix(in srgb, var(--accent) 42%, transparent); background: var(--accent-soft); }
.vtag {
  font-family: var(--font-mono); font-size: var(--fz-tiny); color: var(--muted);
  background: var(--raised); padding: 3px 7px; border-radius: var(--r-sm); flex: none;
}
.version.latest .vtag { background: var(--accent); color: var(--accent-ink); }

.ext-badge {
  font-size: var(--fz-micro); font-weight: 700; letter-spacing: var(--track-label); text-transform: uppercase;
  padding: 3px 7px; border-radius: var(--r-sm); flex: none;
  background: var(--raised); color: var(--muted); border: 1px solid var(--border-soft);
}
.ext-badge.fdx { color: #6ea8ff; border-color: #6ea8ff44; background: #6ea8ff14; }
.ext-badge.docx, .ext-badge.doc { color: #5b8dee; border-color: #5b8dee44; background: #5b8dee14; }
.ext-badge.pdf { color: #ef6d6d; border-color: #ef6d6d44; background: #ef6d6d14; }

/* ---------- dropzone ---------- */

.dropzone {
  border: 1.5px dashed var(--border);
  border-radius: var(--r);
  padding: 26px 20px;
  text-align: center;
  color: var(--muted);
  font-size: var(--fz-ui);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.dropzone:hover, .dropzone.over {
  border-color: var(--accent); background: var(--accent-soft); color: var(--text);
}
.dropzone .big { font-size: var(--fz-title); display: block; margin-bottom: 6px; opacity: .6; }

body.dragging-file .dropzone { border-color: var(--accent); }

/* ---------- misc ---------- */

.stat-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--sp-5); margin: 26px 0 22px;
}

/*
 * ONE definition of a stat tile. The base said padding 13/15 and nothing
 * about layout; a later layer made it a flex column with padding 14/16 and
 * an icon. Both shipped, half each.
 */
.stat {
  display: flex; flex-direction: column; gap: var(--sp-2);
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--r); padding: 14px 16px;
}
.stat .ic { width: 16px; height: 16px; color: var(--accent); opacity: .9; margin-bottom: 2px; }
/* The numbers get the website's gradient-ink treatment: the accent sliding
   toward its neighbour, clipped to the type. That neighbour is --accent-2,
   which each theme states for itself; it used to be a hardcoded #ff3ca0, so
   Forest's numbers ran sage green to hot pink. */
.stat b {
  display: block; font-size: var(--fz-stat); font-weight: 650;
  font-family: var(--font-read); line-height: 1.2;
  width: fit-content;
  background: linear-gradient(100deg, var(--accent), color-mix(in srgb, var(--accent) 45%, var(--accent-2)));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
/* The whisper under the number: tracked uppercase, the app's micro-label. */
.stat span {
  font-size: var(--fz-micro); color: var(--faint); font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track);
}

.section-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-6); margin: 26px 0 12px;
}
.section-head:first-child { margin-top: 0; }
.section-head h2 { margin: 0; font-size: var(--fz-lead); font-weight: 600; }
.section-head .sub { font-size: var(--fz-small); color: var(--muted); font-weight: 400; margin-left: 8px; }

.tpl-card {
  text-align: left; width: 100%; font: inherit;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--r);
  padding: 13px 15px; cursor: pointer; color: var(--text);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.tpl-card:hover { border-color: var(--accent); background: var(--accent-soft); }
.tpl-card.on { border-color: var(--accent); background: var(--accent-soft); }
.tpl-card b { display: block; font-size: var(--fz-body); font-weight: 600; margin-bottom: 3px; }
.tpl-card span { display: block; font-size: var(--fz-small); color: var(--muted); line-height: 1.5; }
.tpl-card em { font-style: normal; font-size: var(--fz-tiny); color: var(--faint); display: block; margin-top: 5px; }

.tpl-group-label {
  font-size: var(--fz-micro); text-transform: uppercase; letter-spacing: var(--track);
  color: var(--faint); margin: 16px 0 8px; font-weight: 600;
}
.tpl-group-label:first-child { margin-top: 0; }

.muted { color: var(--muted); }
.faint { color: var(--faint); }
.small { font-size: var(--fz-small); }
.mono { font-family: var(--font-mono); }

/*
 * The gap in a row of controls.
 *
 * This was scoped to `.editor-bar .spacer`, so the same markup collapsed to
 * nothing everywhere else — including `.modal-foot`, which is
 * justify-content: flex-end, so the spacer was the only thing holding "1 of 3"
 * away from Next on the welcome tour, and "Remove the lock" away from
 * "Lock now". Both were rendering flush against each other on the app's
 * first screen. One definition, no scope.
 */
.spacer { flex: 1 1 auto; }

/* Same job, said the other way: push me and everything after me to the end. */
.right { margin-left: auto; }
.nowrap { white-space: nowrap; }
.stack { display: flex; flex-direction: column; gap: var(--sp-5); }
.hstack { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.mt-0 { margin-top: 0; } .mt-1 { margin-top: 8px; } .mt-2 { margin-top: 16px; } .mt-3 { margin-top: 24px; }
.mb-0 { margin-bottom: 0; } .mb-1 { margin-bottom: 8px; } .mb-2 { margin-bottom: 16px; }

.rich-read p { margin: 0 0 .8em; }
.rich-read ul, .rich-read ol { padding-left: 1.4em; margin: 0 0 .8em; }

.rich-edit {
  font-family: var(--font-read); font-size: var(--fz-lead); line-height: 1.7;
  min-height: 120px; padding: 12px 14px; outline: none;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-md);
}
.rich-edit:focus { border-color: var(--accent); box-shadow: var(--ring-focus); }
.rich-edit:empty::before { content: attr(data-placeholder); color: var(--faint); font-style: italic; }
.rich-edit p { margin: 0 0 .7em; }
.rich-edit ul, .rich-edit ol { padding-left: 1.4em; margin: 0 0 .7em; }

/* ============================================================
   The importer — browsing your own drive from inside the app
   ============================================================ */

.import-grid { display: grid; grid-template-columns: 220px 1fr; gap: var(--sp-8); align-items: start; }
@media (max-width: 900px) { .import-grid { grid-template-columns: 1fr; } }

.import-side { position: sticky; top: 70px; }
.import-side h4 {
  font-size: var(--fz-micro); text-transform: uppercase; letter-spacing: var(--track);
  color: var(--faint); margin: 0 0 8px; font-weight: 600;
}

.place {
  display: flex; align-items: center; gap: var(--sp-5);
  padding: 7px 10px; border-radius: var(--r-md); cursor: pointer;
  font-size: var(--fz-ui); color: var(--text-2);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.place:hover { background: var(--surface); color: var(--text); }
.place .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.import-main { min-width: 0; }

/*
 * The importer's folder bar: where you are, and the way back up.
 *
 * This and `.folder-chip` below were being overwritten by the world view's
 * category filters, 3,300 lines down, which happen to use the same two class
 * names for a completely different thing. The later rules won, so the
 * importer's breadcrumb bar was rendering with the filter row's wrapping and
 * spacing, and the world's filter pills were sitting inside the importer's
 * bordered card. Each is now scoped to markup it actually has: the world's
 * bar carries `data-folders`, the importer's folders live in `.folder-grid`.
 */
.folder-bar {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: 9px 12px; margin-bottom: var(--sp-6);
  background: var(--surface); border: 1px solid var(--border-soft); border-radius: var(--r);
}
.folder-bar .crumbs { font-size: var(--fz-small); overflow: hidden; }
.folder-bar .crumbs a { cursor: pointer; }

.deep-toggle {
  display: flex; align-items: flex-start; gap: var(--sp-5);
  padding: 11px 14px; border-radius: var(--r);
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 26%, transparent);
  font-size: var(--fz-ui); cursor: pointer; line-height: 1.5;
}
.deep-toggle input { margin-top: 3px; flex: none; }
/* The same affordance, without the accent shout: for a setting that is worth
   offering but not worth drawing the eye away from what the writer came to
   the dialog to do. */
.deep-toggle.quiet {
  background: var(--raised);
  border-color: var(--border-soft);
}

.folder-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: var(--sp-4); }

/* A folder on your own drive: a card you can open, not a filter. */
.folder-grid .folder-chip {
  display: flex; align-items: center; gap: var(--sp-5);
  padding: 9px 12px; border-radius: var(--r); cursor: pointer;
  background: var(--surface); border: 1px solid var(--border-soft);
  font-size: var(--fz-ui);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.folder-grid .folder-chip:hover { border-color: var(--accent); background: var(--accent-soft); }
.folder-grid .folder-chip .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.filter-chip {
  font: inherit; font-size: var(--fz-small);
  padding: 5px 11px; border-radius: var(--r-pill); cursor: pointer;
  background: var(--surface); border: 1px solid var(--border-soft); color: var(--muted);
  display: inline-flex; align-items: center; gap: var(--sp-4);
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.filter-chip:hover { color: var(--text); border-color: var(--border); }
.filter-chip.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 550; }
.filter-chip .n { font-size: var(--fz-tiny); opacity: .75; }

.file-list { border: 1px solid var(--border-soft); border-radius: var(--r); overflow: hidden; }

.file-row {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: 8px 13px; cursor: pointer;
  background: var(--surface);
  border-bottom: 1px solid var(--border-soft);
  font-size: var(--fz-ui);
  transition: background var(--dur-1) var(--ease);
}
.file-row:last-child { border-bottom: none; }
.file-row:hover { background: var(--raised); }
.file-row.on { background: var(--accent-soft); }
.file-row.dupe { opacity: .62; }
.file-row input { flex: none; }
.file-row .fname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-row .fpath {
  font-size: var(--fz-tiny); color: var(--faint); flex: none; max-width: 210px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  background: var(--raised); padding: 1px 7px; border-radius: var(--r-sm);
}

.import-footer {
  position: sticky; bottom: 0; z-index: var(--z-sticky);
  display: flex; align-items: center; gap: var(--sp-7);
  margin-top: 18px; padding: 13px 18px;
  background: color-mix(in srgb, var(--bg-deep) 94%, transparent);
  backdrop-filter: blur(14px);
  border: 1px solid var(--border); border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
}
.import-footer > .right { margin-left: auto; }
.import-footer b { font-size: var(--fz-subhead); font-family: var(--font-read); }

.import-item {
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--r); padding: 13px 15px;
}
.import-sources { margin-top: 9px; padding-left: 2px; }
.import-sources .src {
  display: flex; align-items: center; gap: var(--sp-5);
  padding: 4px 0; font-size: var(--fz-small); color: var(--text-2);
}
.import-sources .src.dupe { opacity: .55; }
.import-sources .fname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- help page ---------- */

.steps { display: grid; gap: var(--sp-5); }

.step {
  display: flex; gap: var(--sp-7); align-items: flex-start;
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--r); padding: 18px 20px;
}
.step-n {
  width: 34px; height: 34px; flex: none; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--accent); color: var(--accent-ink);
  font-weight: 700; font-size: var(--fz-lead); font-family: var(--font-read);
}
.step h3 { margin: 0 0 5px; font-size: var(--fz-lead); font-weight: 650; }
.step p { margin: 0; color: var(--text-2); font-size: var(--fz-body); line-height: 1.65; max-width: var(--measure); }

.faq {
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--r); overflow: hidden;
}
.faq summary {
  padding: 13px 18px; cursor: pointer; font-weight: 550; font-size: var(--fz-body);
  list-style: none; display: flex; align-items: center; gap: var(--sp-5);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::before {
  content: "＋"; color: var(--accent); font-size: var(--fz-lead); flex: none; width: 14px;
}
.faq[open] summary::before { content: "－"; }
.faq summary:hover { background: var(--raised); }
.faq-body {
  padding: 0 18px 16px 42px; color: var(--text-2);
  font-size: var(--fz-body); line-height: 1.7; max-width: var(--measure);
}
.faq-body b { color: var(--text); }

/* ---------- first-run choices ---------- */

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

.start-choice {
  font: inherit; text-align: left; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 20px 20px 18px; color: var(--text);
  display: block;
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.start-choice:hover {
  border-color: var(--accent); transform: translateY(-2px); box-shadow: var(--shadow-2);
}
.start-choice .ico { font-size: var(--fz-title); display: block; margin-bottom: 10px; }
.start-choice b { display: block; font-size: var(--fz-lead); font-weight: 650; margin-bottom: 5px; }
.start-choice span:not(.ico) {
  display: block; font-size: var(--fz-ui); color: var(--muted); line-height: 1.55;
}

/* ============================================================
   Editing a real .fdx / .docx
   ============================================================ */

.script-paper {
  font-family: var(--font-mono);
  font-size: calc(12.5px * var(--fs) * var(--write-fs));
  line-height: 1.55;
  padding: 0.8in 1in 1.2in;
}
/*
 * Outlines and treatments write on the same paper the script does: the
 * writer's own request — "I want it to write like the script section".
 * Same page furniture, same shadow, same room; prose measure instead of
 * Courier columns, because an outline is prose.
 */
/*
 * WHERE THE PAGES END, drawn on one continuous sheet.
 *
 * Logan, on outlines and treatments: "it doesn't make new pages as you write, it
 * only make the page longer." True: `.doc-paper` is one element holding every
 * row, eleven inches tall to begin with and taller after that. A writer working
 * to a page count has no idea where they are.
 *
 * THIS IS NOT PAGINATION AND IS NOT PRETENDING TO BE. Real pages mean measuring
 * every row and pushing the one that would straddle a break onto the next sheet
 * - which is what layoutSheets does for a script, and it is a build rather than
 * a rule. That is written up as its own job.
 *
 * What this does is show the eleven-inch boundaries, the way a word processor in
 * page view does. A line can still straddle one. But "am I near the end of page
 * three" becomes answerable at a glance, which is most of what the complaint is
 * actually about, and it costs one rule instead of a week.
 *
 * `border-box` so the boundaries measure from the top of the paper rather than
 * from inside the padding, or the first break lands an inch early and every one
 * after it drifts.
 */
/*
 * *** AND NOW THEY REALLY ARE SEPARATE PAGES. ***
 *
 * The note above describes the cheap half - drawn boundaries on one long sheet -
 * and says plainly that it is not pagination. Logan, after seeing it: "it does
 * open but it still only makes the page longer and not separate at all."
 *
 * fileeditor.js now measures every paragraph and pushes one that would straddle
 * a break onto the next sheet, and the sheets are drawn behind as separate
 * pieces of paper with the desk showing between them. So this element stops
 * being the paper: it becomes the words, transparent, over the real thing -
 * which is exactly the arrangement the script page has used all along.
 */
.doc-paper {
  font-family: var(--font-read);
  font-size: calc(16px * var(--fs) * var(--write-fs));
  line-height: 1.75;
  padding: 1in 1.1in 1.3in;
  box-sizing: border-box;
}
/* Three classes deep, for the same reason the script page needs it: Glass
   paints `.paper` solid and a two-class selector would lose to it, leaving one
   long sheet over the gutters in exactly one of the seven themes. */
.doc-stack .paper.doc-paper {
  position: relative;
  z-index: 1;
  width: 100%;
  min-height: 0;
  background: none;
  border: 0;
  box-shadow: none;
}
.doc-stack { width: min(8.5in, 100%); }
/* The push that lands a paragraph on the top of the next sheet.

   `.dp-row + .dp-row` sets the space BETWEEN paragraphs and is one selector
   heavier than `.doc-paper .dp-row`, so it won and every push was thrown away -
   the variable was set to 362px and the margin computed to 12px. The space
   between paragraphs is still .75em; the push is added to it. */
.dp-row + .dp-row { margin-top: calc(.75em + var(--lead, 0px)); }
.doc-paper .dp-row:first-child { margin-top: var(--lead, 0px); }
@media print {
  .doc-stack .sheet-layer { display: none; }
  /* Real page breaks on paper; a drawn one would land mid-sheet. */
  .dp-row + .dp-row { margin-top: .75em; }
}
/* Printing puts real page breaks in; a drawn one would land in the middle of a
   sheet and read as a mistake on paper somebody is holding. */
@media print { .doc-paper { background-image: none; } }
.doc-paper .dp-row {
  max-width: 68ch; margin-inline: auto; padding: 1px 0;
  scroll-margin: 30vh 0;
}
.doc-paper .dp-row:focus-within { outline: none; }
/* The line you are on gets the same quiet lift as a script line. */
.doc-paper .dp-row[data-here] {
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  border-radius: var(--r-xs);
}

.sp-row, .dp-row { position: relative; display: block; }

.sp-type {
  position: absolute; left: -158px; top: 0;
  width: 140px; font: inherit; font-family: var(--font-ui);
  font-size: calc(11px * var(--fs) * var(--write-fs));
  padding: 2px 6px; border-radius: var(--r-sm);
  background: var(--surface); color: var(--muted);
  border: 1px solid var(--border-soft);
  opacity: 0; cursor: pointer;
  transition: opacity var(--dur-1) var(--ease);
}
.sp-row:hover .sp-type, .sp-row:focus-within .sp-type { opacity: 1; }
.sp-row:focus-within .sp-type { border-color: var(--accent); color: var(--text); }

.sp-del {
  position: absolute; right: -34px; top: 0;
  width: 24px; height: 22px; border-radius: var(--r-sm);
  border: none; background: none; color: var(--faint);
  cursor: pointer; opacity: 0; font-size: var(--fz-small);
  transition: opacity var(--dur-1) var(--ease);
}
.sp-row:hover .sp-del, .dp-row:hover .sp-del,
.sp-row:focus-within .sp-del, .dp-row:focus-within .sp-del { opacity: 1; }
.sp-del:hover { color: var(--danger); background: var(--raised); }

.sp-text, .dp-text {
  outline: none; border-radius: var(--r-xs); padding: 1px 4px; margin: 0 -4px;
  min-height: 1.5em; white-space: pre-wrap; word-wrap: break-word;
}
.sp-text:focus, .dp-text:focus { background: color-mix(in srgb, var(--accent) 7%, transparent); }
.sp-text:empty::before, .dp-text:empty::before {
  content: attr(data-ph); color: var(--faint);
}

/* Script element layout, roughly Final Draft's US Letter geometry. */
.sp-scene   .sp-text { text-transform: uppercase; font-weight: 700; margin-top: 1.6em; }
.sp-action  .sp-text { margin-top: .9em; }
.sp-character .sp-text { margin-left: 2.0in; text-transform: uppercase; margin-top: 1.1em; }
.sp-paren   .sp-text { margin-left: 1.5in; margin-right: 2.2in; }
.sp-dialogue .sp-text { margin-left: 1.0in; margin-right: 1.5in; }
.sp-transition .sp-text { text-align: right; text-transform: uppercase; margin-top: 1em; }
.sp-shot    .sp-text { text-transform: uppercase; font-weight: 700; margin-top: 1.2em; }
.sp-general .sp-text { margin-top: .9em; }

/* The space between paragraphs moved to line 2190, where it also carries the
   page push. Two rules with the same selector meant the later one won and every
   push was silently discarded - the variable said 362px and the margin said 12. */
.dp-heading .dp-text { font-weight: 700; font-size: 1.18em; margin-top: 1.3em; }

@media (max-width: 1200px) {
  .sp-type { position: static; opacity: 1; width: 130px; margin-bottom: 3px; display: block; }
  .sp-del { right: 0; }
}

/* ---------- full screen / focus mode ---------- */

.editor-shell:fullscreen { background: var(--bg-deep); overflow-y: auto; }
.editor-shell:fullscreen .paper-wrap { padding-top: 26px; }

body.faux-fullscreen .sidebar { display: none; }
body.faux-fullscreen .app { grid-template-columns: 1fr; }

/* ---------- text size control ---------- */

.size-control {
  display: flex; align-items: center; gap: var(--sp-2);
  /* The one row in the app that has to survive its own control being turned up.
     Wrapping is what keeps "A-", "200%" and "A+" from printing on top of each
     other at the top of the range. */
  flex-wrap: wrap;
  margin: 0 12px 8px;
  padding: 6px 8px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
}
/* `--track` is a theme value, .09em in dark and .24em in mountains, and this
   box was sized against the tightest of them. In starry, blossom and
   mountains "TEXT SIZE" wrapped onto a second line and stood the control up
   from 38px to 47px - in the sidebar footer, so on every screen in the app. */
.size-control .lbl {
  font-size: var(--fz-micro); text-transform: uppercase; letter-spacing: var(--track);
  color: var(--faint); flex: 1; white-space: nowrap;
}
/*
 * The three controls hold their size; the label gives way.
 *
 * A flex item shrinks below its declared width unless told not to, so at 200%
 * the label - which is `flex: 1` and never wraps - took the room and squeezed
 * "A-", "200%" and "A+" until they printed on top of each other. Photographed:
 * "A-" sat over the "2" of "200%".
 *
 * `flex: none` on the three of them means the label wraps to its own line
 * instead, which is what `flex-wrap` above is there for. And the boxes scale
 * with the type, or the letter in a 200% A+ is bigger than the button holding
 * it - which is a strange thing for the text-size control itself to get wrong.
 */
.size-control button {
  flex: none;
  width: calc(26px * var(--fs)); height: calc(24px * var(--fs));
  border-radius: var(--r-sm);
  border: 1px solid var(--border); background: var(--raised);
  color: var(--text-2); cursor: pointer; font: inherit; font-size: var(--fz-small);
  display: grid; place-items: center;
}
.size-control button:hover { background: var(--raised-2); color: var(--text); }
.size-control .now {
  flex: none;
  font-size: var(--fz-tiny); color: var(--muted);
  min-width: calc(34px * var(--fs)); text-align: center;
}

/* Who is on a bible's cast page: ticks, because the answer is four out of
   forty and a select is the wrong shape for that. */
.pick-cast { display: grid; gap: 2px; max-height: 52vh; overflow: auto; }
.pick-row {
  display: grid; grid-template-columns: auto auto 1fr; align-items: baseline;
  gap: var(--sp-4); padding: 7px 8px; border-radius: var(--r-md); cursor: pointer;
}
.pick-row:hover { background: var(--surface); }
.pick-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.pdf-frame {
  border: 1px solid var(--border-soft); border-radius: var(--r);
  overflow: hidden; background: var(--page-bg);
}
.pdf-frame iframe { width: 100%; height: 72vh; border: 0; display: block; }

/* ============================================================
   Character dossier
   ============================================================ */

.dossier h3 { display: flex; align-items: center; gap: var(--sp-5); }
.dossier-count {
  font-size: var(--fz-micro); letter-spacing: 0; text-transform: none;
  color: var(--faint); font-weight: 400;
  background: var(--raised); border-radius: var(--r-pill); padding: 1px 8px;
}
.dossier-blurb {
  margin: -6px 0 16px; font-size: var(--fz-small);
  color: var(--muted); max-width: var(--measure); line-height: 1.55;
}
.dossier-top { padding-bottom: 14px; }

.dossier-grid {
  display: grid; gap: var(--sp-7) var(--sp-8);
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
}
/* One column means one column. This used to say minmax(300px, 1fr) inside a
   1030px main, which is three columns and a 38-character measure for a
   paragraph about someone's deepest wound. */
.dossier-grid.one-col { grid-template-columns: minmax(0, 1fr); gap: var(--sp-8); }
.dossier-grid.two-col { grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); }

.dossier-field > label {
  display: block; font-size: var(--fz-micro); font-weight: 600;
  color: var(--text-3); margin-bottom: 5px;
  text-transform: uppercase; letter-spacing: var(--track);
}
/* The writer's own sentence is the largest, warmest thing in the cell. It is
   set in the reading face, because it is reading, not data entry. */
.dossier-value {
  font-family: var(--font-read);
  font-size: var(--fz-lead); line-height: 1.7; min-height: 1.6em;
  color: var(--text);
}
.dossier-field.empty .dossier-value { color: var(--faint); font-family: var(--font-ui); font-size: var(--fz-ui); }
/*
 * The prompt is a margin note, never the loudest thing in the cell. It used to
 * inherit the body — 15px at full brightness — which made the app's suggestion
 * bigger and brighter than the writer's answer in all sixty-four cells. That
 * one missing declaration is most of why the whole app read as a form.
 */
.dossier-field .hint {
  margin-top: 5px; font-family: var(--font-read); font-style: italic;
  font-size: var(--fz-tiny); line-height: 1.55; color: var(--faint);
}
/* Answering a question should settle the page, not shorten it. */
.dossier-field:not(.empty) .hint { opacity: .5; }

/*
 * The dossier is long now, so a summary rides alongside it: the face, the name,
 * and how much of the profile is filled in. The card stays put while the
 * questions scroll, the way the writer's other tool lays a profile out.
 */
.dossier-layout {
  display: grid; grid-template-columns: 1fr 232px;
  gap: 0 var(--sp-7); align-items: start;
}
.dossier-main { min-width: 0; }
.dossier-rail { position: sticky; top: 60px; }

.dossier-card {
  display: flex; flex-direction: column; gap: var(--sp-4); text-align: center;
}
.dossier-face {
  /* Their own colour, ringing their own face. */
  --ring-tint-colour: var(--character-accent, var(--accent));
  box-shadow: var(--ring-tint);
  width: 100%; aspect-ratio: 3 / 4; object-fit: cover;
  border-radius: var(--r-sm); background: var(--raised);
}
.dossier-face.blank {
  display: grid; place-items: center; color: #fff;
  font-size: var(--fz-display); font-weight: 600; letter-spacing: var(--track-label);
}
.dossier-card b { font-size: var(--fz-lead); }
.dossier-card > span { font-size: var(--fz-small); color: var(--muted); }
.dossier-card-roles { display: flex; flex-wrap: wrap; gap: var(--sp-2); justify-content: center; }
.dossier-card em {
  font-style: normal; font-size: var(--fz-tiny); color: var(--faint);
  margin-top: 2px;
}

@media (max-width: 980px) {
  .dossier-layout { display: block; }
  .dossier-rail { display: none; }   /* the header already shows who they are */
}

/*
 * The photo strip: reference pictures, one starred as the face.
 */
.shot-strip { display: flex; flex-wrap: wrap; gap: var(--sp-5); }
.shot {
  position: relative; width: 108px; aspect-ratio: 3 / 4;
  border-radius: var(--r-sm); overflow: hidden;
  border: 2px solid transparent; background: var(--raised);
}
.shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shot.face { border-color: var(--accent); }

.shot-star, .shot-x {
  position: absolute; top: 4px; width: 22px; height: 22px;
  border: 0; border-radius: var(--r-pill); cursor: pointer;
  display: grid; place-items: center; font-size: var(--glyph-sm);
  background: rgb(10 10 14 / .62); color: #fff;
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.shot-star { left: 4px; }
.shot-x { right: 4px; }
.shot:hover .shot-star, .shot:hover .shot-x,
.shot:focus-within .shot-star, .shot:focus-within .shot-x { opacity: 1; }
.shot.face .shot-star { opacity: 1; color: #ffd76a; }

.shot.add {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-2); cursor: pointer; font: inherit;
  border: 1.5px dashed var(--border); background: none; color: var(--muted);
}
.shot.add span { font-size: var(--fz-stat); line-height: 1; }
.shot.add em { font-style: normal; font-size: var(--fz-micro); }
.shot.add:hover { border-color: var(--accent); color: var(--text); }

/* The project gallery gets bigger tiles: these are posters, not thumbnails. */
/*
 * The Pictures tab, as a board rather than a row of stamps.
 *
 * Logan: "When adding photos or videos in this page it should be like a
 * Pinterest board and should be more full, not overwhelmingly full but a good
 * size." It was a wrapping row of 164px thumbnails in a 3:4 crop, which is a
 * contact sheet: every picture the same size and the same shape, and a wide
 * still cropped to a tall box for no reason but the grid.
 *
 * Columns, not rows: the pictures keep their own proportions and stack up the
 * shortest column, which is what a pinboard does and why one looks full without
 * looking cramped. `break-inside` is what stops a picture being sliced in half
 * across a column boundary - the whole trick fails silently without it.
 */
.shot-strip-big {
  display: block;
  columns: 4 190px;
  column-gap: var(--sp-5);
}
.shot-strip-big .shot {
  width: 100%;
  aspect-ratio: auto;          /* the picture's own shape, not a 3:4 crop */
  /* `-webkit-column-break-inside` first, and it is not decoration: WebKit only
     learned the unprefixed `break-inside` in Safari 16, and a Mac two versions
     behind would slice a picture in half down a column edge. Every writer this
     is going in front of is on a Mac and nobody here has one to check on. */
  -webkit-column-break-inside: avoid;
  break-inside: avoid;
  margin-bottom: var(--sp-5);
  cursor: zoom-in;
}
.shot-strip-big .shot img { height: auto; object-fit: contain; }

/* One picture, as big as the window will allow. "When I click on a photo or
   video I should be able to get a fuller look at it like Fullscreen." */
.lightbox {
  position: fixed; inset: 0; z-index: var(--z-modal, 900);
  display: grid; place-items: center;
  background: rgb(8 9 14 / .88);
  padding: clamp(20px, 4vw, 60px);
  cursor: zoom-out;
}
.lightbox img {
  max-width: 100%; max-height: 100%;
  object-fit: contain; cursor: default;
  border-radius: var(--r-xs);
  box-shadow: 0 30px 80px -20px rgb(0 0 0 / .7);
}
.lightbox-x, .lightbox-step {
  position: absolute; border: 0; cursor: pointer;
  width: 42px; height: 42px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: rgb(255 255 255 / .12); color: #fff;
}
.lightbox-x:hover, .lightbox-step:hover { background: rgb(255 255 255 / .24); }
.lightbox-x { top: 18px; right: 18px; }
.lightbox-step.prev { left: 18px; top: 50%; transform: translateY(-50%); }
.lightbox-step.next { right: 18px; top: 50%; transform: translateY(-50%); }
.lightbox-count {
  position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%);
  color: rgb(255 255 255 / .7); font-size: var(--fz-small);
}
.lightbox .ic { width: 20px; height: 20px; }

/* ---- the shape of it ------------------------------------------------------ */

/*
 * Pacing as a picture: one bar per scene. The saggy middle hides in a list of
 * sluglines and stands up immediately in a row of heights.
 */
.pacing-strip {
  display: flex; align-items: flex-end; gap: var(--sp-1);
  height: 64px; margin-bottom: 14px;
  padding: var(--sp-2); border-radius: var(--r-sm); background: var(--surface);
}
.pacing-bar {
  flex: 1; min-width: 3px; border-radius: var(--r-2xs) var(--r-2xs) 0 0;
  background: color-mix(in srgb, var(--accent) 55%, var(--raised-2));
  cursor: pointer; transition: filter var(--dur-1) var(--ease);
}
.pacing-bar:hover { filter: brightness(1.35); }
.pacing-bar.tone-red    { background: #c0504a; }
.pacing-bar.tone-amber  { background: #e0a33e; }
.pacing-bar.tone-green  { background: #5aab72; }
.pacing-bar.tone-blue   { background: #5b8def; }
.pacing-bar.tone-purple { background: #9b6fe0; }
.pacing-bar.tone-pink   { background: #d5568c; }

.web-suggest { max-width: 640px; margin: 18px auto 0; }

/*
 * The line Find is sitting on.
 *
 * `data-found` was set and cleared by the editor from the day Find was written
 * and NOTHING EVER DREW IT. The only thing showing a writer where the match was
 * happened to be the browser's own selection - which is why showFind moved the
 * selection into the page, which is why focus left the find box, which is why
 * typing a second letter into it was impossible. One missing rule, three faults
 * downstream.
 *
 * A tint on the whole line rather than on the matched word: the words on a
 * script page are the work, and a box drawn tightly round six characters of
 * Courier reads as damage to it. The line is enough to find your eye, and it
 * cannot be mistaken for something the script itself is doing.
 */
.script-page .el[data-found] {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-radius: var(--r-sm, 3px);
  /* Wider than the text so short lines - a cue, a transition - still read as a
     band rather than as a smudge behind three words. */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* ---- coloured drafts ------------------------------------------------------ */

.draft-chip {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  font-size: var(--fz-tiny); color: var(--text-2);
  padding: 3px 9px; border-radius: var(--r-pill);
  background: var(--raised); border: 1px solid var(--border-soft);
  white-space: nowrap;
}
.draft-chip i { width: 9px; height: 9px; border-radius: var(--r-pill); display: inline-block; }

/*
 * The production asterisk: this line is not in the last kept copy.
 *
 * On ::after, in the right margin, which is where Final Draft puts it and
 * where the left margin is not. It used to be written as ::before, which is
 * also where the scene number lives, and the asterisk was the more specific
 * of the two - so a scene heading you had revised lost its number, on the
 * only kind of page anybody ever marks up.
 */
.script-page .el[data-changed="1"] { position: relative; }
.script-page .el[data-changed="1"]::after {
  content: '*'; position: absolute; right: -5ch; top: 0; left: auto;
  color: var(--accent); opacity: .7; font-weight: 700;
  font-size: inherit;
}
/* Blank lines never carry the mark, and the hint ::before owns blank lines
   anyway — data-changed is only ever set on lines with words. */

/* ---- what changed ---------------------------------------------------------- */

/* The diff reads like script, because it is script. */
.diff-page {
  font-family: var(--font-mono); font-size: var(--fz-small);
  line-height: 1.5; color: var(--text);
  max-height: 64vh; overflow-y: auto; padding: 6px 2px;
}
.diff-page .el { padding: 1px 8px; border-radius: var(--r-xs); white-space: pre-wrap; }
.diff-page .el[data-type="Scene Heading"] { font-weight: 700; margin-top: 10px; text-transform: uppercase; }
.diff-page .el[data-type="Character"] { margin-left: 14ch; }
.diff-page .el[data-type="Dialogue"] { margin-left: 7ch; max-width: 42ch; }
.diff-page .el[data-type="Parenthetical"] { margin-left: 11ch; }
.diff-add { background: color-mix(in srgb, var(--ok) 16%, transparent); }
.diff-del {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  text-decoration: line-through; opacity: .75;
}
.diff-gap { text-align: center; color: var(--faint); padding: var(--sp-3) 0; letter-spacing: var(--track-wide); }

/*
 * The setup mark in the script margin was drawn here: a small bullseye beside
 * every line marked as a promise. It is gone because a screenplay page does not
 * have marks on it, and this one sat in the margin of every page a writer
 * looked at all day.
 *
 * The `data-setup` attribute is still written and still saved. Nothing styles
 * it, so nothing appears. See the note in web/views/script.js for why the flag
 * is kept rather than stripped.
 */

/* ---- coming from Story Architect ----------------------------------------- */

.starc-panel {
  margin-bottom: 18px;
  border-color: color-mix(in srgb, var(--accent) 30%, var(--border-soft));
}

/* ---- their words --------------------------------------------------------- */

.voice-flow { max-width: var(--measure); }
.voice-script {
  margin: 20px 0 8px; font-size: var(--fz-small); font-weight: 650;
  color: var(--accent); cursor: pointer;
}
.voice-script:first-child { margin-top: 0; }
.voice-script:hover { text-decoration: underline; }
.voice-scene {
  margin: 12px 0 6px;
  font-family: var(--font-mono); font-size: var(--fz-micro);
  text-transform: uppercase; letter-spacing: var(--track-label); color: var(--faint);
}
/* The lines themselves read like type, not like UI. */
.voice-line {
  margin: 0 0 9px; font-family: var(--font-mono);
  font-size: var(--fz-ui); line-height: 1.6; color: var(--text);
}

.voice-pages { display: flex; flex-direction: column; gap: var(--sp-7); align-items: flex-start; }
.voice-page {
  /* Real pages, but reading-sized: the full 8.5in belongs to the editor. */
  --page-w: 7in;
  padding: 2.4em 8ch 2.6em 12ch;
  font-size: calc(12px * var(--write-fs, 1));
}

/* ---- the world ---------------------------------------------------------- */

.world-group {
  margin: 20px 0 10px;
  font-size: var(--fz-tiny); font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track-label);
  color: var(--muted);
}
.world-group:first-child { margin-top: 0; }

/* History: the order of time, one era per row, draggable. */
.world-history { display: flex; flex-direction: column; gap: var(--sp-2); }
.world-era {
  display: grid; grid-template-columns: 130px 1fr; gap: var(--sp-6); align-items: baseline;
  padding: 8px 10px; border-radius: var(--r-sm);
  background: var(--surface); border: 1px solid var(--border-soft);
  cursor: grab;
}
.world-era:active { cursor: grabbing; }
.world-era:hover { border-color: var(--border); }
.world-era.lifting { opacity: .35; }
.world-era-when {
  font-family: var(--font-mono); font-size: var(--fz-micro);
  color: var(--accent); text-align: right;
}
.world-era b { font-size: var(--fz-ui); }
.world-era em {
  display: block; font-style: normal; color: var(--muted);
  font-size: var(--fz-tiny); line-height: 1.4;
}

/* A character's ties to the world, worn as chips. */
.their-world { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.their-world .chip { cursor: pointer; }
.their-world .chip:hover { border-color: var(--accent); color: var(--text); }

/* ---- the welcome -------------------------------------------------------- */

/* `.welcome-row` and `.view-seg` were the second home page's header. Nothing
   renders either one; the marquee hero replaced them. */

/*
 * The quote of the day, set like something worth reading rather than a
 * subtitle. The writer's other tool opens on a quote; this is our answer.
 *
 * ONE definition. There used to be three, from three different eras of the
 * home page, and the last one won: it set Georgia and a decorative ::before
 * quote mark on top of the curly quotes already in the markup. So the app's
 * one ceremonial object was in the wrong face — a system serif, not the
 * Source Serif 4 the stylesheet header promises and bundles — and opened with
 * two quote marks. If this needs to change, change it here.
 */
.daily-quote {
  position: relative;
  margin: 18px 0 0; padding: 0 0 0 16px;
  border-left: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
  max-width: var(--measure);
}
.daily-quote p {
  margin: 0; font-family: var(--font-read); font-style: italic;
  font-size: var(--fz-lead); line-height: 1.6; color: var(--text-2);
}
.daily-quote cite {
  display: block; margin-top: 6px; font-style: normal;
  font-size: var(--fz-micro); letter-spacing: var(--track); text-transform: uppercase;
  color: var(--faint);
}

/* ---- the Shelf ----------------------------------------------------------- */

/*
 * A wall of posters: standing in front of your own filmography. Titles without
 * artwork get a spine in their own colour, so the wall has no holes — only
 * films still waiting for their poster.
 */
/*
 * The grid version of the shelf (`.shelf`, auto-fill columns) is gone: the
 * shelf scrolls sideways now and its container is `.home-shelf`.
 *
 * ONE definition of a poster. There used to be two — this one, with an
 * aspect ratio and no size, and a second 3,000 lines later giving it a flat
 * 200×300 and a different hover shadow. The later one won on half the
 * properties and lost on the rest, which is why the poster's geometry was
 * impossible to reason about. 200×300 is 2:3, the standard one-sheet, so the
 * aspect ratio is stated as the fallback for anything that sets its own width.
 */
.shelf-poster {
  position: relative; cursor: pointer;
  width: 200px; height: 300px; aspect-ratio: 2 / 3;
  border-radius: var(--r-sm); overflow: hidden;
  background: var(--surface); border: 1px solid var(--border-soft);
  transition: transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
  animation: card-in var(--dur-3) var(--ease) backwards;
}
.shelf-poster:nth-child(2) { animation-delay: .04s; }
.shelf-poster:nth-child(3) { animation-delay: .08s; }
.shelf-poster:nth-child(4) { animation-delay: .12s; }
.shelf-poster:nth-child(5) { animation-delay: .16s; }
.shelf-poster:nth-child(n+6) { animation-delay: .2s; }
body.no-motion .shelf-poster { animation: none; }
@media (prefers-reduced-motion: reduce) { .shelf-poster { animation: none; } }

.shelf-poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The screening-room glow: the poster's own colour bleeding onto the wall. */
.shelf-poster:hover {
  transform: translateY(-4px) scale(1.015);
  border-color: color-mix(in srgb, var(--pc) 50%, var(--border));
  box-shadow: 0 22px 55px rgb(0 0 0 / .55),
    0 0 40px color-mix(in srgb, var(--pc, var(--accent)) 22%, transparent);
}

.shelf-spine {
  position: absolute; inset: 0; display: grid; place-items: center; padding: var(--sp-7);
  background: linear-gradient(160deg,
    color-mix(in srgb, var(--pc) 42%, var(--surface)) 0%,
    color-mix(in srgb, var(--pc) 14%, var(--surface)) 100%);
}
.shelf-spine b {
  font-family: var(--font-read); font-size: var(--fz-subhead);
  text-align: center; line-height: 1.3; color: var(--text);
  text-shadow: 0 1px 8px rgb(0 0 0 / .25);
}

.shelf-label {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 26px 10px 9px;
  background: linear-gradient(transparent, rgb(8 9 14 / .88));
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
/*
 * IT NO LONGER APPEARS ON HOVER. Logan: "when you hover over the poster for a
 * project on the main page this ugly white bar shows up, remove it completely
 * please."
 *
 * He is right and the reason is his own work: a poster he made already carries
 * the title, in his type, chosen by him - and this dropped a slab of gradient
 * and system-font white over the bottom third of it every time the pointer
 * crossed. A project with no poster is unaffected: that draws `.shelf-spine`,
 * which is a different thing and still shows the name.
 *
 * The element stays in the page rather than being deleted, and keeps its text.
 * It is the tile's accessible name, so a screen reader still announces which
 * project this is - and taking a name away from somebody who cannot see the
 * artwork is not what "remove the bar" meant.
 */
.shelf-label { pointer-events: none; }
.shelf-label b { display: block; color: #fff; font-size: var(--fz-ui); }
.shelf-label em {
  display: block; font-style: normal; color: rgb(255 255 255 / .75);
  font-size: var(--fz-micro); line-height: 1.4;
}

/* ---- give me a spark ----------------------------------------------------- */

.spark { display: flex; flex-direction: column; gap: var(--sp-6); }
.spark-q > label {
  display: block; font-size: var(--fz-tiny); font-weight: 600;
  color: var(--text-2); margin-bottom: 6px;
}
.spark-opts { display: flex; flex-wrap: wrap; gap: var(--sp-3); }

.spark-out {
  margin-top: 4px; padding: 16px 18px;
  background: var(--bg-deep); border-radius: var(--r);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border-soft));
  font-family: var(--font-read); font-size: var(--fz-lead);
  line-height: 1.7; color: var(--text);
  min-height: 5em;
}

/* The name dealer: a hand of eight, laid out like cards on a table. */
.name-deal {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--sp-4); margin-top: 4px;
}
.name-card {
  padding: 12px 14px; text-align: left; cursor: pointer;
  background: var(--bg-deep); border: 1px solid var(--border-soft);
  border-radius: var(--r); color: var(--text);
  font-family: var(--font-read); font-size: var(--fz-body);
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease),
    box-shadow var(--dur-1) var(--ease);
}
.name-card:hover {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border-soft));
  transform: translateY(-1px);
  box-shadow: var(--glow-lift);
}

/* The quiet milestones line at the top of a project's overview. */
.milestones {
  margin: 2px 2px 12px; font-size: var(--fz-small);
  color: var(--text-3); letter-spacing: var(--track-fine);
}

/* ============================================================
   The lock — an opaque curtain over everything, up before the
   first view paints so nothing shows through even for a frame.
   ============================================================ */
.lock-screen {
  position: fixed; inset: 0; z-index: var(--z-top);   /* above modals and toasts both */
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-deep);
  animation: fade .2s;
}
.lock-screen.opening { animation: fade .25s reverse forwards; }
.lock-card {
  text-align: center; padding: 34px 40px; max-width: 320px;
  background: var(--bg); border: 1px solid var(--border-soft);
  border-radius: calc(var(--r) * 2);
  box-shadow: var(--elev-3);
}
.lock-card h2 { margin: 0 0 4px; font-size: var(--fz-subhead); }
.lock-card p { margin: 0 0 14px; font-size: var(--fz-small); }
.lock-emoji { font-size: var(--fz-display); margin-bottom: 10px; }
.lock-pin {
  text-align: center; letter-spacing: var(--track-wide); font-size: var(--fz-subhead);
  width: 100%;
}
.lock-wrong {
  color: var(--danger, #e5484d); font-size: var(--fz-tiny);
  margin: 10px 0 0 !important;
}
.lock-card.shake { animation: lock-shake .3s; }
@keyframes lock-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-7px); }
  55% { transform: translateX(6px); }
  80% { transform: translateX(-3px); }
}

/* ============================================================
   Character arc — shown as a journey, not a card
   ============================================================ */

.arc-card {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  padding: 20px 22px;
}

.arc-head { display: flex; gap: var(--sp-7); align-items: flex-start; margin-bottom: 18px; }
.arc-title {
  margin: 0 0 4px; font-family: var(--font-read);
  font-size: var(--fz-stat); font-weight: 700;
}
.arc-summary { color: var(--muted); font-size: var(--fz-ui); line-height: 1.55; }

.arc-journey {
  display: grid;
  grid-template-columns: 1fr 60px 1fr;
  gap: var(--sp-5); align-items: stretch;
  margin-bottom: 22px;
}
@media (max-width: 760px) {
  .arc-journey { grid-template-columns: 1fr; }
  .arc-arrow { transform: rotate(90deg); height: 34px; }
}

.arc-end {
  background: var(--bg); border: 1px solid var(--border-soft);
  border-radius: var(--r); padding: 13px 15px;
}
.arc-end > label {
  display: block; font-size: var(--fz-micro); text-transform: uppercase;
  letter-spacing: var(--track); color: var(--faint); margin-bottom: 6px; font-weight: 600;
}
.arc-end:first-child { border-left: 3px solid var(--film); }
.arc-end:last-child { border-left: 3px solid var(--ok); }
.arc-state {
  font-family: var(--font-read); font-size: var(--fz-lead);
  line-height: 1.6; min-height: 1.6em;
}

.arc-arrow { display: flex; align-items: center; justify-content: center; gap: 0; }
.arc-line {
  flex: 1; height: 2px;
  background: linear-gradient(90deg, var(--film), var(--ok));
  border-radius: var(--r-2xs);
}
.arc-tip { color: var(--ok); font-size: var(--fz-tiny); margin-left: -2px; }

.arc-points { border-top: 1px solid var(--border-soft); padding-top: 14px; }
.arc-points-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-6);
  font-size: var(--fz-tiny); text-transform: uppercase;
  letter-spacing: var(--track); color: var(--faint); font-weight: 600;
  margin-bottom: 10px;
}
.arc-empty {
  margin: 0; font-size: var(--fz-ui); color: var(--muted);
  line-height: 1.6; max-width: var(--measure);
}

.arc-timeline { list-style: none; margin: 0; padding: 0 0 0 4px; position: relative; }
.arc-timeline::before {
  content: ""; position: absolute; left: 15px; top: 12px; bottom: 12px;
  width: 2px; background: var(--border);
}

.arc-point {
  display: flex; gap: var(--sp-7); align-items: flex-start;
  padding: 9px 0; position: relative;
}
.arc-dot {
  width: 24px; height: 24px; flex: none; border-radius: var(--r-pill);
  display: grid; place-items: center; position: relative; z-index: var(--z-base);
  background: var(--accent); color: var(--accent-ink);
  font-size: var(--fz-tiny); font-weight: 700;
  box-shadow: 0 0 0 4px var(--surface);   /* punches out of a card, not the page */
}
.arc-point-body { flex: 1; min-width: 0; padding-top: 1px; }
.arc-point-title {
  font-weight: 600; font-size: var(--fz-body); min-height: 1.4em;
}
.arc-point-detail {
  color: var(--muted); font-size: var(--fz-ui);
  line-height: 1.55; margin-top: 2px; min-height: 1.4em;
}
.arc-point-side {
  display: flex; align-items: center; gap: var(--sp-1); flex: none;
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.arc-point:hover .arc-point-side,
.arc-point:focus-within .arc-point-side { opacity: 1; }
/* The episode tag sits outside the hover group — it is information, not a control. */
.arc-point > .chip { flex: none; align-self: center; }

.arc-body {
  margin-top: 16px; padding-top: 14px;
  border-top: 1px solid var(--border-soft);
}

/* ============================================================
   Sorting out a folder of folders
   ============================================================ */

.place-row { display: flex; flex-wrap: wrap; gap: var(--sp-4); }
.place-pill {
  font: inherit; font-size: var(--fz-ui);
  padding: 7px 13px; border-radius: var(--r-pill); cursor: pointer;
  background: var(--bg); border: 1px solid var(--border); color: var(--text-2);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.place-pill:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--text); }

.org-project {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.org-project.on { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

.org-head { display: flex; align-items: center; gap: var(--sp-6); padding: 13px 16px; }
.org-head > input[type="checkbox"] { flex: none; width: 16px; height: 16px; }

.org-kind { display: flex; gap: var(--sp-2); flex: none; }
.org-kind-btn {
  width: 32px; height: 30px; border-radius: var(--r-md); cursor: pointer;
  border: 1px solid var(--border); background: var(--bg);
  font-size: var(--fz-body); opacity: .4;
  transition: opacity var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.org-kind-btn.on { opacity: 1; border-color: var(--accent); background: var(--accent-soft); }
.org-kind-btn:hover { opacity: .85; }

.org-name {
  font-weight: 600; font-size: var(--fz-lead);
  background: none; border: 1px solid transparent; padding: 3px 7px; margin-left: -7px;
}
.org-name:hover { border-color: var(--border-soft); }
.org-name:focus { background: var(--bg); border-color: var(--accent); }

.org-detail {
  border-top: 1px solid var(--border-soft);
  padding: 14px 16px 16px; background: var(--bg-deep);
  border-radius: 0 0 var(--r) var(--r);
  max-height: 460px; overflow-y: auto;
}
.org-group { margin-bottom: 16px; }
.org-group:last-child { margin-bottom: 0; }
.org-group h4 {
  margin: 0 0 8px; font-size: var(--fz-micro);
  text-transform: uppercase; letter-spacing: var(--track); color: var(--faint); font-weight: 600;
}
.org-ep { margin: 10px 0 10px 14px; padding-left: 12px; border-left: 2px solid var(--border); }
.org-ep-head {
  font-size: var(--fz-small); font-weight: 600;
  color: var(--text-2); margin-bottom: 5px;
}
.org-item {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: 3px 0; font-size: var(--fz-small); color: var(--text-2);
}
.org-item-icon { display: grid; place-items: center; width: 18px; flex: none; color: var(--faint); }
.org-item-icon .ic { width: 15px; height: 15px; vertical-align: 0; }
.org-item-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ============================================================
   All my files
   ============================================================ */

.file-table { border: 1px solid var(--border-soft); border-radius: var(--r); overflow: hidden; }

.file-line {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: 10px 14px; cursor: pointer;
  background: var(--surface);
  border-bottom: 1px solid var(--border-soft);
  transition: background var(--dur-1) var(--ease);
}
.file-line:last-child { border-bottom: none; }
.file-line:hover { background: var(--raised); }
.file-line-main { flex: 1; min-width: 0; }
.file-line-title {
  font-size: var(--fz-body); font-weight: 550;
  display: flex; align-items: center; gap: var(--sp-4);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.file-line-where {
  font-size: var(--fz-small); color: var(--muted); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.file-line-where a { color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
.file-line-where a:hover { color: var(--accent); }
.file-line-meta {
  font-size: var(--fz-small); color: var(--faint);
  flex: none; white-space: nowrap; min-width: 66px; text-align: right;
}
.file-line-actions { display: flex; gap: var(--sp-1); flex: none; opacity: 0; transition: opacity var(--dur-1) var(--ease); }
.file-line:hover .file-line-actions { opacity: 1; }

@media (max-width: 780px) {
  .file-line-meta { display: none; }
  .file-line-actions { opacity: 1; }
}

/* ============================================================
   Where things stand
   ============================================================ */

.ov-card {
  display: flex; gap: var(--sp-7); align-items: stretch;
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--r); overflow: hidden;
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.ov-card:hover { border-color: var(--border); transform: translateY(-1px); box-shadow: var(--shadow-2); }
.ov-stripe { width: 4px; flex: none; }
.ov-main { flex: 1; min-width: 0; padding: 15px 0 15px 2px; }
.ov-title {
  font-size: var(--fz-lead); font-weight: 650;
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
}
.ov-logline {
  color: var(--muted); font-size: var(--fz-ui);
  margin-top: 3px; max-width: var(--measure);
}
.ov-counts { font-size: var(--fz-small); color: var(--faint); margin-top: 6px; }
.ov-gaps { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: 10px; }
.ov-gap {
  font-size: var(--fz-tiny); padding: 2px 9px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--film) 12%, transparent);
  color: var(--film);
  border: 1px solid color-mix(in srgb, var(--film) 28%, transparent);
}
.ov-gap.done {
  background: color-mix(in srgb, var(--ok) 12%, transparent);
  color: var(--ok); border-color: color-mix(in srgb, var(--ok) 28%, transparent);
}

.ov-side {
  flex: none; width: 190px; padding: 15px 16px 15px 0;
  display: flex; flex-direction: column; align-items: flex-start;
}
@media (max-width: 720px) { .ov-side { display: none; } }

.ov-ring {
  width: 52px; height: 52px; border-radius: var(--r-pill); flex: none;
  display: grid; place-items: center; margin-bottom: 10px;
  background: conic-gradient(var(--accent) calc(var(--pct) * 1%), var(--border-soft) 0);
}
.ov-ring span {
  width: 40px; height: 40px; border-radius: var(--r-pill);
  background: var(--surface); display: grid; place-items: center;
  font-size: var(--fz-body); font-weight: 700;
}
.ov-ring i { font-style: normal; font-size: var(--fz-micro); color: var(--faint); }

.ov-steps { display: flex; flex-direction: column; gap: var(--sp-1); }
.ov-step { font-size: var(--fz-tiny); color: var(--faint); }
.ov-step.ok { color: var(--ok); }

.share-msg {
  font-family: var(--font-mono);
  font-size: var(--fz-small);
  line-height: 1.6;
  white-space: pre-wrap;
  background: var(--bg);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  padding: 14px 16px;
  color: var(--text-2);
  max-height: 320px;
  overflow-y: auto;
  user-select: text;
}

/* ============================================================
   Searching inside the writing
   ============================================================ */

.search-big {
  font-size: var(--fz-stat);
  padding: 14px 18px;
  font-family: var(--font-read);
}

.hit-list { display: flex; flex-direction: column; gap: var(--sp-4); }

.hit {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  padding: 13px 16px;
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.hit:hover { border-color: var(--border); background: var(--raised); }

.hit-head {
  display: flex; align-items: center; gap: var(--sp-5);
  font-size: var(--fz-body); margin-bottom: 6px;
}
.hit-icon { flex: none; }
.hit-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hit-where {
  color: var(--faint); font-size: var(--fz-small);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
}
.hit-actions { display: flex; gap: var(--sp-1); flex: none; opacity: 0; transition: opacity var(--dur-1) var(--ease); }
.hit:hover .hit-actions { opacity: 1; }

/* The matched line, with the words you typed picked out. */
.hit-snippet {
  font-family: var(--font-read);
  font-size: var(--fz-body);
  line-height: 1.65;
  color: var(--text-2);
  background: var(--bg);
  border-left: 2px solid var(--border);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  padding: 8px 12px;
}
.hit-snippet mark,
.pal-snippet mark {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  color: var(--text);
  border-radius: var(--r-2xs);
  padding: 0 2px;
  font-weight: 600;
}

.pal-snippet {
  display: block;
  font-family: var(--font-read);
  font-size: var(--fz-small);
  color: var(--text-2);
  margin-top: 3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.palette-item.pal-more { border-top: 1px solid var(--border-soft); margin-top: 4px; padding-top: 11px; }
.palette-item.pal-more b { color: var(--accent); }

/* ============================================================
   Scratchpad
   ============================================================ */

.scratch-input {
  width: 100%;
  font-family: var(--font-read);
  font-size: var(--fz-subhead);
  line-height: 1.7;
  padding: 14px 16px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  resize: vertical;
}
.scratch-input:focus {
  outline: none; border-color: var(--accent);
  box-shadow: var(--ring-focus);
}
.scratch-input::placeholder { color: var(--faint); font-style: italic; }

.scrap-list { display: grid; gap: var(--sp-5); grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }

.scrap {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  padding: 14px 16px 10px;
  display: flex; flex-direction: column; gap: var(--sp-4);
  transition: border-color var(--dur-1) var(--ease);
}
.scrap:hover, .scrap:focus-within { border-color: var(--border); }

.scrap-body {
  font-family: var(--font-read);
  font-size: var(--fz-lead);
  line-height: 1.65;
  white-space: pre-wrap;
  outline: none;
  border-radius: var(--r-sm);
  padding: 2px 4px; margin: 0 -4px;
  min-height: 1.6em;
  flex: 1;
}
.scrap-body:focus { background: color-mix(in srgb, var(--accent) 6%, transparent); }

.scrap-foot {
  display: flex; align-items: center; gap: var(--sp-4);
  border-top: 1px solid var(--border-soft);
  padding-top: 7px;
}
.scrap-when { font-size: var(--fz-tiny); color: var(--faint); flex: 1; }
.scrap-actions { display: flex; gap: var(--sp-1); opacity: 0; transition: opacity var(--dur-1) var(--ease); }
.scrap:hover .scrap-actions, .scrap:focus-within .scrap-actions { opacity: 1; }

.scrap-preview {
  font-family: var(--font-read);
  font-size: var(--fz-lead);
  line-height: 1.65;
  white-space: pre-wrap;
  background: var(--bg);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  padding: 12px 14px;
  color: var(--text-2);
  max-height: 200px; overflow-y: auto;
}

/* ============================================================
   The script editor.

   The page is a real US Letter page: 12pt Courier, a 1.5in left
   margin, 60 columns of text. Indents are expressed in ch units
   against that monospace grid, so what you see lines up exactly
   with what the pagination engine counts and the PDF prints.
   ============================================================ */

/*
 * How big the page is.
 *
 * A US Letter page is 85 characters of Courier across, and a character is
 * 0.6 of the type size — so the page is 51 times the font size, and setting
 * the font sets everything.
 *
 * `cqw` is one percent of the writing area's width, so `100cqw / 51` is the
 * largest type that still lets a whole page fit beside the sidebar and the
 * scene list. `min()` takes that or the true size, whichever is smaller: the
 * page shrinks to fit but is never blown up bigger than a real page.
 *
 * Done in the stylesheet rather than by measuring in code, because the
 * browser knows the width before anything has been drawn and cannot miss it.
 */
.script-shell {
  --script-true-fs: calc(12pt * var(--fs) * var(--write-fs));
  --script-fs: min(var(--script-true-fs), calc(100cqw / 51));
  --page-w: calc(var(--script-fs) * 51);
}
/*
 * The scene list sits on the right, opposite the project sidebar.
 *
 * With both panels on the left the page ended up well right of the middle of
 * the screen, which is what you notice for hours while writing. One each side
 * puts the page where your eyes already are.
 */
/*
 * The writing room: the desk on the left, the page in the middle, the scenes
 * on the right. The writer's words for the old layout were "sitting in an
 * empty void all alone" — the margins are now company, and one button clears
 * them again for the days the void is wanted.
 */
/*
 * *** THE RAILS SIT UNDER THE TOOLBARS, NOT BEHIND THEM. ***
 *
 * `.outline-rail` sticks at `top: 47px`, which is the height of ONE bar - and
 * correct everywhere in the app except here. The script stacks two: the editor
 * bar and the row of named tools, both inside `.bar-stack`, which is itself
 * sticky at `top: 0`. So the rail pinned forty-seven pixels down while the bars
 * above it were taller than that, and the top of whatever the rail was showing
 * slid underneath them and was clipped away. Open the beat sheet, scroll the
 * script, and the first beats are simply gone.
 *
 * MEASURED, NOT COUNTED. The stack's height is not a number anybody can write
 * here: it changes when the named row is hidden, when the writer turns the text
 * size up, and when the row wraps on a narrow laptop. script.js watches it with
 * a ResizeObserver and writes the answer into `--stack-h`. The 47px fallback is
 * only what applies for the frame before the first measurement lands.
 */
.script-layout .outline-rail {
  top: var(--stack-h, 47px);
  max-height: calc(100vh - var(--stack-h, 47px));
}

.script-layout { grid-template-columns: 250px minmax(0, 1fr) 240px; }
.script-layout .desk-rail { grid-column: 1; padding: 24px 8px 40px 20px; }
.script-layout .script-wrap { grid-column: 2; }
.script-layout .script-rail {
  grid-column: 3;
  padding: 24px 20px 40px 8px;
}

/*
 * The rails on a laptop: narrow first, vanish only when they must.
 *
 * The page already shrinks itself to fit (the cqw rule above), so the rails
 * can stay far longer than the old breakpoints assumed — losing the scene
 * list at 1400px threw away the writing room on the most common laptop
 * screen there is. The page always wins; the company goes quietly, one side
 * at a time.
 */
@media (max-width: 1520px) {
  .script-layout { grid-template-columns: 222px minmax(0, 1fr) 212px; }
}
@media (max-width: 1240px) {
  .script-layout { grid-template-columns: 222px minmax(0, 1fr); }
  .script-layout .script-rail { display: none; }
  /* The generic editor rule hides every rail at 1180; the desk stays. */
  .script-layout .desk-rail { display: block; }
}
@media (max-width: 1000px) {
  .script-layout { grid-template-columns: minmax(0, 1fr); }
  .script-layout .desk-rail { display: none; }
  .script-layout .script-wrap { grid-column: 1; }
}

/* The bare page, one button away, remembered. */
.script-shell.room-off .desk-rail,
.script-shell.room-off .script-rail,
.script-shell.room-off .write-status { display: none; }
.script-shell.room-off .script-layout { grid-template-columns: minmax(0, 1fr); }
.script-shell.room-off .script-wrap { grid-column: 1; }
.editor-bar [data-act="room"].on { color: var(--accent); }

/* ---- the desk ----------------------------------------------------------- */

/*
 * *** HOW FAR DOWN THE RAIL PINS, AND WHY IT IS NOT A NUMBER. ***
 *
 * The same offset was written by hand in THREE places: `.outline-rail` says
 * 47px, this says 46px, and the `max-width: 1664px` block says 46px again in
 * two identical copies. Which one won depended on how wide the window was,
 * which is why the rails looked slightly different on different screens and
 * nobody could say why.
 *
 * All of them were wrong on the script. `.bar-stack` there holds two bars - the
 * editor bar and the row of named tools - and is 95px tall at the normal text
 * size, 99px with the text turned up. So the rail pinned itself roughly half
 * way up behind the toolbars, and whatever it was showing had its top clipped
 * off by them. Logan, having scrolled a script with the beat sheet open: "the
 * top of the beat sheet gets cut off."
 *
 * It cannot be a constant. The stack changes height when the named row is
 * hidden, when the text size goes up, and when the row wraps on a narrow
 * laptop. script.js measures it with a ResizeObserver and writes `--stack-h`;
 * the literal here is only what applies for the frame before the first
 * measurement lands.
 */
.desk-rail {
  position: sticky; top: var(--stack-h, 46px);
  max-height: calc(100vh - var(--stack-h, 46px)); overflow-y: auto;
}

.desk-scene-head {
  font-family: var(--font-mono); font-size: var(--fz-tiny);
  color: var(--text-2); text-transform: uppercase; letter-spacing: var(--track-fine);
  margin-bottom: 10px; line-height: 1.4;
}

.desk-cast { display: flex; flex-direction: column; gap: var(--sp-2); }
.desk-person {
  display: grid; grid-template-columns: auto 1fr auto; gap: var(--sp-5); align-items: center;
  padding: 5px 7px; border-radius: var(--r-md);
}
/* Quiet, not hidden.

   This was opacity 0 until the row was hovered, which reads well and cannot be
   found: the whole complaint was that there was nowhere to do this, and a
   control you have to already know about does not answer that. Faint is enough
   to keep a rail of them from reading as a list of things left undone. */
.desk-person-add { opacity: .5; transition: opacity .12s; }
.desk-person:hover .desk-person-add,
.desk-person-add:hover,
.desk-person-add:focus-visible { opacity: 1; }
.desk-person.clickable { cursor: pointer; }
.desk-person.clickable:hover { background: var(--surface); }
.desk-person-words { min-width: 0; }
.desk-person-words b { display: block; font-size: var(--fz-small); }
.desk-person-words em {
  font-style: normal; display: block; color: var(--faint);
  font-size: var(--fz-micro);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.desk-note {
  margin-top: 10px; color: var(--faint); font-size: var(--fz-tiny); line-height: 1.5;
}

.desk-pick { width: 100%; margin-bottom: 10px; }

.desk-beats { display: flex; flex-direction: column; gap: var(--sp-1); margin-bottom: 10px; }
.desk-beat {
  display: grid; grid-template-columns: 14px 1fr; gap: var(--sp-3);
  padding: 5px 6px; border-radius: var(--r-sm); font-size: var(--fz-small);
}
.desk-beat span { color: var(--faint); }
.beat-tick { display: grid; place-items: center; width: 15px; flex: none; margin-top: 2px; }
.beat-tick .ic { width: 12px; height: 12px; vertical-align: 0; }
.desk-beat.done span { color: var(--ok); }
.desk-beat b { font-weight: 550; }
.desk-beat em {
  display: block; font-style: normal; color: var(--muted);
  font-size: var(--fz-tiny); line-height: 1.45;
}
.desk-beat.done b { color: var(--muted); }

.desk-doc { margin-bottom: 10px; }
.desk-para {
  margin: 0 0 8px; font-size: var(--fz-small); line-height: 1.55;
  color: var(--text-2);
}
.desk-para.head { font-weight: 650; color: var(--text); }

.desk-scratch-box { margin-bottom: 8px; }
.desk-scratch-list { margin-top: 12px; display: flex; flex-direction: column; gap: var(--sp-3); }
.desk-scratch-item {
  background: var(--surface); border-radius: var(--r-md); padding: 7px 9px;
  font-size: var(--fz-small); line-height: 1.5; color: var(--text-2);
}

/* ---- the status bar ------------------------------------------------------ */

/*
 * The cockpit line: always there, never loud. Half of what a toolbar-heavy
 * editor gives people is quiet, constant proof that the rest of the script
 * exists and the day is going somewhere. This is that, in one row.
 */
.write-status {
  position: sticky; bottom: 0; z-index: var(--z-sticky);
  display: flex; align-items: center; justify-content: center; gap: var(--sp-7);
  padding: 6px 20px;
  background: color-mix(in srgb, var(--bg) 90%, transparent);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--border-soft);
  font-size: var(--fz-tiny); color: var(--muted);
  flex-wrap: wrap;
}
.write-status b { color: var(--text-2); font-weight: 600; }
.write-status i { width: 3px; height: 3px; border-radius: var(--r-pill); background: var(--faint); }

/* .paper-wrap is a row flex built for a single child. This one has three —
   the title, the page, the footer — so it has to stack them, or they compete
   for the width and the page ends up narrower than 8.5 inches. */
.script-wrap {
  padding: 24px 20px 60vh;
  flex-direction: column;
  align-items: center;
  container-type: inline-size;    /* so the page can size itself against this */
}

/*
 * THE NAME ABOVE THE PAPER IS A LABEL, NOT A TITLE PAGE.
 *
 * It was set at title size, bold and centred, sitting directly over the first
 * sheet - which is exactly how the title of a screenplay looks. So it read as
 * part of the script, and a writer who had not asked for it there had no way to
 * make it go. Logan: "the name of the script and project name fall off the page
 * and Flot above the page and look really bad. The scripts name and projects
 * name should be something the writer adds themselves, not something they are
 * forced to have on their script."
 *
 * A screenplay's title belongs on its title page, which this app has, and
 * nowhere else. So this stops competing with it: small, quiet, and the colour
 * of the other chrome. It is still there and still editable, because it is how
 * a script gets renamed - it just no longer pretends to be on the page.
 */
.script-head { width: min(var(--page-w, 8.5in), 100%); margin: 0 0 10px; }
.script-title {
  font-family: var(--font-ui);
  font-size: var(--fz-small); font-weight: 500; text-align: center;
  color: var(--text-3);
  outline: none; border-radius: var(--r-xs); padding: 2px 6px;
}
.script-title:focus { box-shadow: var(--ring-focus); }

/*
 * The page, measured in characters rather than inches.
 *
 * A US Letter page is 85 characters wide and 66 lines tall in 12pt Courier,
 * so expressing it that way means the whole page — margins and all — scales
 * exactly with the type. Fixing the box in inches while the type scaled left
 * the text sixty-four columns wide inside a page meant for sixty, which is
 * the one measurement that has to be right.
 */
.script-page {
  width: min(var(--page-w, 8.5in), 100%);
  flex: none;
  background: var(--page-bg);
  color: var(--page-ink);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-xs);
  box-shadow: var(--shadow-3);
  padding: 6em 10ch 6em 15ch;         /* 1in top and bottom, 1.5in / 1in sides */
  font-family: var(--font-mono);
  font-size: var(--script-fs);
  line-height: 1;                     /* six lines to the inch, as on paper */
  outline: none;
  min-height: 66em;                   /* eleven inches of lines */
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

/* One element. Widths are in characters so they match the engine exactly. */
.script-page .el {
  outline: none;
  max-width: 60ch;
  min-height: 1em;
  scroll-margin: 30vh 0;
  position: relative;
}

.el[data-type="Scene Heading"] { text-transform: uppercase; font-weight: 700; margin-top: 1em; }
.el[data-type="Action"]        { margin-top: 1em; }
.el[data-type="Shot"]          { text-transform: uppercase; margin-top: 1em; }
.el[data-type="General"]       { margin-top: 1em; }
.el[data-type="Character"] {
  text-transform: uppercase; margin-top: 1em;
  margin-left: 22ch; max-width: 38ch;
}
.el[data-type="Parenthetical"] { margin-left: 16ch; max-width: 26ch; }
.el[data-type="Dialogue"]      { margin-left: 10ch; max-width: 35ch; }
.el[data-type="Transition"]    { text-transform: uppercase; margin-top: 1em; text-align: right; }

/* The hint for what goes on a line, shown on the line the caret is actually
   in and nowhere else. Showing it on every blank line put a grey instruction
   under your scene heading for as long as you were writing, which reads as
   something stuck rather than something helpful. */
.script-page:focus .el[data-blank][data-here]::before {
  color: var(--faint);
  text-transform: none; font-weight: 400;
  position: absolute; pointer-events: none; white-space: nowrap;
}
.script-page:focus .el[data-type="Scene Heading"][data-blank][data-here]::before { content: "INT. SOMEWHERE - DAY"; }
.script-page:focus .el[data-type="Action"][data-blank][data-here]::before        { content: "What we see."; }
.script-page:focus .el[data-type="Character"][data-blank][data-here]::before     { content: "WHO SPEAKS"; }
.script-page:focus .el[data-type="Dialogue"][data-blank][data-here]::before      { content: "What they say."; }
.script-page:focus .el[data-type="Parenthetical"][data-blank][data-here]::before { content: "(how)"; }
.script-page:focus .el[data-type="Transition"][data-blank][data-here]::before    { content: "CUT TO:"; }
.script-page:focus .el[data-type="Shot"][data-blank][data-here]::before          { content: "ANGLE ON"; }

/*
 * Hints off, for people who find them distracting rather than helpful.
 * Toggled from the script toolbar and remembered. The scene number and page
 * break markers are not hints and stay put.
 */
body.hide-script-hints .script-page .el[data-blank][data-here]:not([data-scene])::before { content: none; }

/* The scene number, out in the margin where production puts it. */
.el[data-scene]::before {
  content: attr(data-scene);
  position: absolute; left: -1.2in; width: 1in; text-align: right;
  color: var(--muted); font-weight: 400;
}

/* ------------------------------------------------------------
   Dual dialogue: two speeches sharing the width, in columns.
   The figures are the engine's DUAL table in ch: left 0-28,
   right 32-60, gutter of 4.

   Only the left speech floats. Floating both was tried and
   cannot work: a float may never rise above the top of an
   earlier float (CSS float rule 5), so the right cue always
   landed a line low. In normal flow the right speech ignores
   the out-of-flow left column entirely and hangs level with
   it, and the caret behaves like it does anywhere else.
   ------------------------------------------------------------ */
.script-page .el[data-dualside="left"] { float: left; clear: left; }
.script-page .el[data-dualside="left"][data-type="Character"]     { width: 20ch; margin-left: 8ch; }
.script-page .el[data-dualside="left"][data-type="Parenthetical"] { width: 21ch; margin-left: 5ch; }
.script-page .el[data-dualside="left"][data-type="Dialogue"]      { width: 28ch; margin-left: 0; }
.script-page .el[data-dualside="right"][data-type="Character"]     { width: 20ch; margin-left: 40ch; }
.script-page .el[data-dualside="right"][data-type="Parenthetical"] { width: 21ch; margin-left: 37ch; }
.script-page .el[data-dualside="right"][data-type="Dialogue"]      { width: 28ch; margin-left: 32ch; }
/* Whatever follows the pair starts below both columns. */
.script-page .el[data-dualside="right"] + .el:not([data-dualside]) { clear: both; }
/* And so does the next pair, when two sit back to back. */
.script-page .el[data-dualstart] { clear: both; }
/* A pair at the very end must not spill out of the page. */
.script-page::after { content: ""; display: block; clear: both; }

/* ------------------------------------------------------------
   The table read: the line being spoken right now.
   ------------------------------------------------------------ */
.el[data-reading="1"] {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-radius: var(--r-xs);
  transition: background var(--dur-2) var(--ease);
}

.read-panel { display: flex; flex-direction: column; gap: var(--sp-5); }
.read-honest {
  margin: 0; font-size: var(--fz-tiny); line-height: 1.5;
  color: var(--text-3);
}
.read-line {
  display: flex; align-items: center; gap: var(--sp-4);
  font-size: var(--fz-small); color: var(--text-2); cursor: pointer;
}
.read-state { font-size: var(--fz-tiny); color: var(--muted); align-self: center; }
.read-panel h4 {
  margin: 6px 0 2px; font-size: var(--fz-tiny);
  text-transform: uppercase; letter-spacing: var(--track-label); color: var(--text-3);
}
.read-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); padding: 3px 0;
}
.read-name {
  font-family: var(--font-mono); font-size: var(--fz-tiny);
  color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.read-voice { max-width: 46%; }

/* Where a page ends. Drawn above the element that starts the next one. */
.el[data-pagebreak="1"] {
  border-top: 1px dashed color-mix(in srgb, var(--page-ink) 28%, transparent);
  margin-top: 2.4em; padding-top: 1.6em;
}
.el[data-pagebreak="1"]::after {
  content: "page break";
  position: absolute; top: -0.75em; right: 0;
  font-family: var(--font-ui); font-size: var(--fz-micro);
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--muted); background: var(--page-bg); padding: 0 6px;
}

.script-foot {
  width: min(var(--page-w, 8.5in), 100%); margin: 12px 0 0; text-align: center;
  font-size: var(--fz-tiny); color: var(--faint);
}
.script-foot b { color: var(--muted); font-family: var(--font-mono); }

/* toolbar bits */
.el-picker {
  background: var(--raised); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 4px 8px; font-size: var(--fz-small);
  font-family: var(--font-ui); cursor: pointer;
}
.pagecount {
  font-family: var(--font-mono); font-size: var(--fz-small);
  color: var(--text-2); padding: 3px 9px; border-radius: var(--r-pill);
  background: var(--raised); white-space: nowrap;
}

/* SmartType */
.smart-pop {
  position: fixed; z-index: var(--z-drawer);
  background: var(--raised); border: 1px solid var(--border);
  border-radius: var(--r); box-shadow: var(--shadow-3);
  padding: var(--sp-2); min-width: 190px; max-height: 260px; overflow-y: auto;
}
.smart-item {
  padding: 5px 10px; border-radius: var(--r-sm); cursor: pointer;
  font-family: var(--font-mono); font-size: var(--fz-small); color: var(--text-2);
}
.smart-item.on { background: var(--accent); color: var(--accent-ink); }
.smart-item:hover { background: var(--raised-2); color: var(--text); }

/* the rail */
.script-rail { padding-right: 14px; }
.rail-tabs { display: flex; gap: var(--sp-1); margin-bottom: 12px; }
.rail-tab {
  flex: 1; padding: 5px 4px; border: none; background: none; cursor: pointer;
  color: var(--faint); font-size: var(--fz-tiny); font-family: var(--font-ui);
  border-bottom: 2px solid transparent;
}
.rail-tab.on { color: var(--text); border-bottom-color: var(--accent); }

.scene-row {
  display: grid; grid-template-columns: 22px 1fr auto; gap: var(--sp-4); align-items: baseline;
  padding: 5px 8px; border-radius: var(--r-md); cursor: pointer;
  font-size: var(--fz-small); color: var(--muted); line-height: 1.35;
}
.scene-row:hover { background: var(--surface); color: var(--text); }
.scene-n { font-family: var(--font-mono); font-size: var(--fz-micro); color: var(--faint); }
.scene-h { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.scene-p { font-family: var(--font-mono); font-size: var(--fz-micro); color: var(--faint); }

.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); margin-bottom: 18px; }
.stat-grid > div {
  background: var(--surface); border-radius: var(--r-sm); padding: 8px 10px;
  display: flex; flex-direction: column;
}
.stat-grid b {
  font-size: var(--fz-heading); font-family: var(--font-read);
  width: fit-content;
  background: linear-gradient(100deg, var(--accent), color-mix(in srgb, var(--accent) 45%, var(--accent-2)));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
.stat-grid span { font-size: var(--fz-micro); color: var(--faint); text-transform: uppercase; letter-spacing: var(--track-label); }

.bar-row {
  display: grid; grid-template-columns: 1fr 60px 22px; gap: var(--sp-3); align-items: center;
  font-size: var(--fz-tiny); color: var(--text-2); padding: 3px 8px;
}
.bar-row.plain { grid-template-columns: 1fr 22px; }
.bar-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-row i { height: 5px; border-radius: var(--r-pill); background: var(--accent); display: block; min-width: 2px; }
.bar-row b { text-align: right; color: var(--faint); font-weight: 500; font-size: var(--fz-micro); }

/* A kept copy, in a rail about two hundred pixels wide.

   This was one line: a label and a date sharing that width with two buttons, so
   the words were cut off before they were read. Logan: "all of the words and
   data are cut off, I cant see when they where made or what time." Stacked, the
   full width belongs to each thing in turn. */
.draft-row {
  display: grid; gap: 2px;
  padding: 8px; border-radius: var(--r-md);
}
.draft-row:hover { background: var(--surface); }
.draft-row b {
  font-size: var(--fz-small); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.draft-when { font-size: var(--fz-small); color: var(--muted); }
.draft-acts { display: flex; gap: 2px; flex-wrap: wrap; margin-top: 3px; }

.export-grid { display: grid; gap: var(--sp-4); }
.export-card {
  text-align: left; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r); padding: 11px 14px; cursor: pointer; color: var(--text);
  display: flex; flex-direction: column; gap: var(--sp-2); font-family: var(--font-ui);
}
.export-card:hover { border-color: var(--accent); background: var(--raised); }
.export-card b { font-size: var(--fz-body); }
.export-card span { font-size: var(--fz-small); color: var(--muted); line-height: 1.45; }

.script-side .btn-block { margin-bottom: var(--sp-5); }

/* Full screen: the page and nothing else. */
.script-shell:fullscreen .script-rail { display: none; }
.script-shell:fullscreen .script-layout { grid-template-columns: 1fr; }

.panel-gap { height: 34px; }

/* ============================================================
   The corkboard.
   ============================================================ */

.board-shell { background: var(--bg-deep); min-height: 100%; }
.board-wrap { padding: 26px 26px 40vh; }

.board-head { text-align: center; margin-bottom: 22px; }
.board-head h1 {
  font-family: var(--font-read); font-size: var(--fz-title);
  margin: 0 0 3px; font-weight: 700;
}
.board-head p { color: var(--muted); font-size: var(--fz-small); margin: 0; letter-spacing: var(--track-label); }

.board {
  display: grid;
  gap: var(--sp-7);
  align-items: start;
  grid-template-columns: repeat(auto-fill, minmax(212px, 1fr));
}
.board[data-size="small"]  { grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: var(--sp-5); }
.board[data-size="large"]  { grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: var(--sp-7); }

.board .card {
  background: var(--page-bg);
  color: var(--page-ink);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-xs);
  box-shadow: var(--shadow-2);
  padding: 10px 12px 8px;
  cursor: grab;
  display: flex; flex-direction: column; gap: var(--sp-3);
  min-height: 132px;
  touch-action: none;              /* pointer events handle the drag */
  transition: box-shadow var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
  border-left: 4px solid var(--border);
}
.board .card:hover { box-shadow: var(--shadow-3); transform: translateY(-2px); }
.board .card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.board .card.dragging {
  position: fixed; z-index: var(--z-veil); margin: 0; cursor: grabbing;
  transform: rotate(1.5deg) scale(1.02);
  box-shadow: var(--shadow-3);
  pointer-events: none;
}
.card-slot {
  border: 1px dashed color-mix(in srgb, var(--accent) 55%, transparent);
  border-radius: var(--r-xs);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}

.board .card header {
  display: flex; justify-content: space-between; align-items: baseline;
  font-family: var(--font-mono); font-size: var(--fz-micro);
  color: var(--muted);
}
.card-n { font-weight: 700; color: var(--text-2); }

.board .card h4 {
  font-family: var(--font-mono);
  font-size: var(--fz-tiny);
  font-weight: 700; text-transform: uppercase; line-height: 1.35;
  margin: 0; letter-spacing: var(--track-fine);
}
.card-body {
  font-family: var(--font-read);
  font-size: var(--fz-small); line-height: 1.5;
  margin: 0; flex: 1;
  display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden;
}
.board[data-size="small"] .card-body { -webkit-line-clamp: 3; }
.card-hint { color: var(--faint); font-style: italic; }

.board .card footer {
  display: flex; justify-content: space-between; gap: var(--sp-4);
  font-size: var(--fz-micro); color: var(--faint);
  border-top: 1px solid var(--border-soft); padding-top: 5px;
  font-family: var(--font-mono);
}
.card-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.card-img {
  margin: -2px -4px 0; border-radius: var(--r-xs); overflow: hidden;
  aspect-ratio: 16 / 9; background: var(--raised);
}
.card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-img.big { aspect-ratio: 16 / 8; margin: 0 0 10px; max-height: 220px; }

/* Colour down the left edge, the way a coloured card reads on a wall. */
.board .card.c-red    { border-left-color: #e5674f; }
.board .card.c-amber  { border-left-color: #e0a33e; }
.board .card.c-green  { border-left-color: #5aab72; }
.board .card.c-blue   { border-left-color: #5b8def; }
.board .card.c-purple { border-left-color: #9b6fe0; }
.board .card.c-grey   { border-left-color: #8d93a3; opacity: .62; }

.board-foot {
  text-align: center; margin-top: 26px;
  /* muted, not faint: this line explains the controls, and on the pale themes
     faint measured under 2.2 to 1, which is decoration rather than writing */
  font-size: var(--fz-small); color: var(--muted);
}

/*
 * Picking what a scene card is: a dot and a word, not just a colour.
 *
 * These used to share the class name `.swatch` with the plain colour circles
 * on the character page. Two rules, one selector: the older one pinned the box
 * to 26x26 and this one added padding for the label, leaving about five pixels
 * of room for the words — which then spilled straight across the neighbouring
 * circles. Separate names, separate designs, and neither can break the other.
 */
.colour-chips { display: flex; flex-wrap: wrap; gap: var(--sp-4); }
.colour-chip {
  border: 1px solid var(--border); border-radius: var(--r-pill);
  padding: 5px 12px 5px 9px; cursor: pointer; background: var(--surface);
  color: var(--text-2); font-size: var(--fz-small); font-family: var(--font-ui);
  display: inline-flex; align-items: center; gap: var(--sp-4);
  white-space: nowrap;
}
.colour-chip::before {
  content: ""; width: 10px; height: 10px; border-radius: var(--r-pill);
  background: var(--border); flex: none;
}
.colour-chip.on { border-color: var(--accent); color: var(--text); background: var(--raised); }
.colour-chip.c-red::before    { background: #e5674f; }
.colour-chip.c-amber::before  { background: #e0a33e; }
.colour-chip.c-green::before  { background: #5aab72; }
.colour-chip.c-blue::before   { background: #5b8def; }
.colour-chip.c-purple::before { background: #9b6fe0; }
.colour-chip.c-grey::before   { background: #8d93a3; }

/* Just the dot, for the corner of a card and the colour key. */
.colour-dot {
  width: 16px; height: 16px; border-radius: var(--r-pill); flex: none;
  display: inline-block; background: var(--border);
}
.colour-dot.c-red    { background: #e5674f; }
.colour-dot.c-amber  { background: #e0a33e; }
.colour-dot.c-green  { background: #5aab72; }
.colour-dot.c-blue   { background: #5b8def; }
.colour-dot.c-purple { background: #9b6fe0; }
.colour-dot.c-grey   { background: #8d93a3; }

.board-shell:fullscreen { overflow-y: auto; background: var(--bg-deep); }

/* ============================================================
   The arc grid — arcs down, episodes across.
   ============================================================ */

.grid-shell { background: var(--bg-deep); min-height: 100%; }
.grid-wrap { padding: 26px 20px 30vh; overflow-x: auto; }

.arcgrid {
  display: grid;
  grid-template-columns: 236px repeat(var(--cols), minmax(122px, 1fr)) 150px;
  gap: var(--sp-1);
  background: var(--border-soft);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  overflow: hidden;
  min-width: max-content;
}

.ag-head {
  background: var(--surface);
  padding: 9px 11px;
  font-size: var(--fz-tiny);
  position: sticky; top: 47px; z-index: var(--z-local-2);
  display: flex; flex-direction: column; gap: var(--sp-1);
  cursor: pointer;
}
.ag-head b { font-family: var(--font-mono); color: var(--text); font-size: var(--fz-tiny); }
.ag-head span {
  color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ag-ep:hover { background: var(--raised); }
.ag-corner {
  cursor: default;
  text-transform: uppercase; letter-spacing: var(--track); color: var(--faint);
  justify-content: flex-end;
  position: sticky; left: 0; z-index: var(--z-local-2);
}
.ag-head.ag-unplaced { cursor: default; color: var(--faint); }

.ag-row-head {
  background: var(--surface);
  padding: 10px 11px;
  display: flex; flex-direction: column; gap: var(--sp-1);
  position: sticky; left: 0; z-index: var(--z-local);
  cursor: pointer;
}
.ag-row-head:hover { background: var(--raised); }
.ag-row-head b { font-size: var(--fz-small); line-height: 1.3; }
.ag-kind {
  font-size: var(--fz-micro); color: var(--faint);
  text-transform: uppercase; letter-spacing: var(--track-label);
}
.ag-states {
  font-size: var(--fz-tiny); color: var(--muted); line-height: 1.4; margin-top: 3px;
}
.ag-states i { color: var(--accent); font-style: normal; }

.ag-cell {
  background: var(--bg);
  min-height: 60px;
  padding: var(--sp-3);
  display: flex; flex-direction: column; gap: var(--sp-2);
  cursor: pointer;
  transition: background var(--dur-1) var(--ease);
}
.ag-cell:hover { background: var(--surface); }
.ag-cell.has { background: var(--surface); }
.ag-cell.over {
  background: color-mix(in srgb, var(--accent) 18%, var(--surface));
  box-shadow: inset var(--ring-on);
}
.ag-cell.ag-unplaced { background: color-mix(in srgb, var(--film) 6%, var(--bg)); }

.moment {
  background: var(--raised);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-xs);
  padding: 5px 7px;
  font-size: var(--fz-tiny);
  line-height: 1.35;
  cursor: grab;
  color: var(--text-2);
}
.moment:hover { background: var(--raised-2); color: var(--text); }
.moment.lifting { opacity: .4; }

.grid-shell:fullscreen { overflow: auto; background: var(--bg-deep); }

/* A moment that lives in an episode outside this grid — visible, but clearly
   not one of these columns. */
.moment.elsewhere { border-left-color: var(--film); opacity: .85; }
.moment-where {
  display: block; font-size: var(--fz-micro);
  color: var(--film); letter-spacing: var(--track-label); margin-top: 2px;
  font-family: var(--font-mono);
}

/* ============================================================
   The relationship web.
   ============================================================ */

.web-shell { background: var(--bg-deep); min-height: 100%; }
.web-wrap { padding: 22px 20px 60px; }
.web-stage {
  /* A map wants all the room it can get, especially on a wide screen. */
  max-width: none; margin: 0 auto;
  background: var(--bg);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  overflow: hidden;
}
/*
 * No text selection anywhere on the map.
 *
 * Dragging a person used to light the whole board up blue, because the browser
 * treats a drag across text as a selection. The pointerdown handler calls
 * preventDefault as well; this covers the rest.
 */
.web-svg {
  display: block; width: 100%; height: auto; touch-action: none;
  user-select: none; -webkit-user-select: none;
}
.web-svg text { user-select: none; -webkit-user-select: none; }

.web-stage.panning { cursor: grabbing; }
.web-stage:not(.panning) .web-svg { cursor: grab; }

/* The photo sits inside the circle; this draws the edge back over it. */
.web-photo { pointer-events: none; }
.web-ring { stroke: var(--border); stroke-width: 1.5; }
.web-node:hover .web-ring { stroke: var(--accent); stroke-width: 3; }

.web-node { cursor: grab; }
.web-node circle {
  stroke: var(--bg); stroke-width: 3;
  transition: stroke var(--dur-1) var(--ease);
}
.web-node:hover circle { stroke: var(--accent); }
.web-node.holding { cursor: grabbing; }
.web-node.holding circle { stroke: var(--accent); stroke-width: 4; }
.web-node circle.lonely { fill-opacity: .35; stroke-dasharray: 3 3; }

.web-initials {
  fill: #fff; font-family: var(--font-ui); font-size: 13px; font-weight: 700;
  pointer-events: none; letter-spacing: var(--track-fine);
}
.web-name {
  fill: var(--text); font-family: var(--font-ui); font-size: 12px; font-weight: 600;
  pointer-events: none;
  paint-order: stroke; stroke: var(--bg-deep); stroke-width: 3.5px; stroke-linejoin: round;
}
.web-role {
  fill: var(--faint); font-family: var(--font-ui); font-size: 9.5px;
  pointer-events: none; text-transform: uppercase; letter-spacing: var(--track-label);
  paint-order: stroke; stroke: var(--bg-deep); stroke-width: 3px; stroke-linejoin: round;
}

.web-edge { cursor: pointer; }
.web-edge:hover line:first-child { stroke-opacity: 1; }
.web-edge-label {
  font-family: var(--font-ui); font-size: 10.5px; font-weight: 600;
  pointer-events: none; paint-order: stroke;
  stroke: var(--bg); stroke-width: 3.5px; stroke-linejoin: round;
}

.rel-row {
  display: grid; grid-template-columns: 10px auto 1fr; gap: var(--sp-5); align-items: baseline;
  padding: 5px 8px; border-radius: var(--r-md); cursor: pointer;
  font-size: var(--fz-small);
}
.rel-row:hover { background: var(--surface); }
.rel-dot { width: 9px; height: 9px; border-radius: var(--r-pill); }
.rel-row span:last-child { color: var(--muted); }

.web-shell:fullscreen { overflow-y: auto; background: var(--bg-deep); }

/* ============================================================
   The timeline — story chronology, not telling order.
   ============================================================ */

.time-shell { background: var(--bg-deep); min-height: 100%; }
.time-wrap { padding: 24px 20px 30vh; max-width: min(100%, 1200px); margin: 0 auto; }

.timeline { list-style: none; margin: 0; padding: 0 0 0 8px; position: relative; }
.timeline::before {
  content: ''; position: absolute; left: 96px; top: 8px; bottom: 8px;
  width: 2px; background: var(--border);
}

.tl-event {
  display: grid; grid-template-columns: 88px 22px 1fr; gap: 0;
  align-items: start; padding: 9px 0; cursor: grab; position: relative;
}
.tl-event:hover .tl-body { background: var(--surface); }
.tl-event.lifting { opacity: .4; }

.tl-when {
  font-family: var(--font-mono); font-size: var(--fz-tiny);
  color: var(--text-2); text-align: right; padding-top: 9px; padding-right: 10px;
  line-height: 1.35;
}
.tl-dot {
  width: 11px; height: 11px; border-radius: var(--r-pill); margin: 12px auto 0;
  box-shadow: var(--ring-punch); z-index: var(--z-base);
}
.tl-body {
  background: var(--bg); border: 1px solid var(--border-soft);
  border-radius: var(--r); padding: 10px 13px; margin-left: 10px;
  transition: background var(--dur-1) var(--ease);
}
.tl-body b { font-size: var(--fz-body); }
.tl-body p {
  margin: 4px 0 0; color: var(--muted); font-size: var(--fz-small);
  line-height: 1.55; font-family: var(--font-read);
}

.tl-tags { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: 8px; }
.tl-tag {
  font-size: var(--fz-micro); padding: 2px 8px; border-radius: var(--r-pill);
  border: 1px solid var(--border); color: var(--text-2);
}
.tl-tag.reveal { border-color: var(--accent); color: var(--accent); }
.tl-tag.faint { color: var(--faint); border-style: dashed; }

.tl-event.k-backstory .tl-body { border-left: 3px solid #8d93a3; }
.tl-event.k-story .tl-body     { border-left: 3px solid #5b8def; }
.tl-event.k-future .tl-body    { border-left: 3px solid #9b6fe0; }

/*
 * How long between one event and the next, where both labels held a year.
 * It rides the same grid as an event so the words line up with the cards
 * rather than floating, and it is quiet on purpose: it is the spine talking,
 * not another thing that happened.
 */
.tl-gap {
  display: grid; grid-template-columns: 88px 22px 1fr;
  align-items: center; height: 26px; list-style: none;
}
/* While a card is being dragged the captions belong to the old order, so a
   stale "400 years later" would sit between the wrong two events. They come
   back on drop, when the list is redrawn from what was saved. */
.timeline:has(.lifting) .tl-gap { visibility: hidden; }
.tl-gap span {
  grid-column: 3; margin-left: 10px;
  font-size: var(--fz-tiny); font-style: italic; color: var(--faint);
  letter-spacing: .01em;
}

.time-shell:fullscreen { overflow-y: auto; background: var(--bg-deep); }

/* ============================================================
   Bibles, pitch decks and lookbooks.
   The page shapes mirror the PDF so nothing is a surprise.
   ============================================================ */

.deck-shell { background: var(--bg-deep); min-height: 100%; }
.deck-wrap { padding: 24px 20px 30vh; flex: 1; min-width: 0; }

.deck-pages { display: flex; flex-direction: column; gap: var(--sp-8); align-items: center; }

.deck-page { position: relative; width: min(920px, 100%); }
.deck-page.lifting { opacity: .4; }

.dp-num {
  position: absolute; left: -38px; top: 0;
  width: 26px; height: 26px; border-radius: var(--r-pill);
  background: var(--raised); color: var(--muted);
  display: grid; place-items: center; cursor: grab;
  font-size: var(--fz-tiny); font-family: var(--font-mono);
}
.dp-num:active { cursor: grabbing; }

.dp {
  /* Follows the deck's chosen look, defaulting to Clean, so the page on screen
     is the page that comes out of the PDF. */
  background: var(--dp-paper, #fff); color: var(--dp-ink, #1a1a1a);
  border-radius: var(--r-xs); box-shadow: var(--shadow-3);
  overflow: hidden; position: relative;
  display: flex; flex-direction: column;
  padding: 40px 46px;
}
.deck-pages.landscape .dp { aspect-ratio: 11 / 8.5; }
.deck-pages.portrait  .dp { aspect-ratio: 8.5 / 11; }

.dp [contenteditable] { outline: none; }
.dp [contenteditable]:empty::before {
  content: attr(data-placeholder);
  color: #a2a7b5; font-style: italic;
}
.dp [contenteditable]:focus { background: rgba(128,147,255,.07); border-radius: var(--r-xs); }

.dp-heading {
  font-family: var(--font-ui); font-size: 25px; font-weight: 700;
  line-height: 1.2; margin-bottom: 14px; letter-spacing: var(--track-tight);
}
.dp-body {
  font-family: var(--font-read); font-size: 14.5px; line-height: 1.6;
  flex: 1; overflow: hidden; max-width: 62ch;
}
.dp-body p { margin: 0 0 .8em; }

.dp-bleed {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}

/* The title sits in a band across the lower part of the page, which is where
   the PDF puts it. If the two disagree the preview is worse than none. */
.dp-cover { padding: 0; justify-content: flex-end; }
.dp-cover::after {
  content: ''; position: absolute; inset: 58% 0 0 0;
  background: linear-gradient(180deg, rgba(0,0,0,.12), rgba(0,0,0,.78));
  pointer-events: none;
}
.dp-cover-text {
  position: relative; z-index: var(--z-base); padding: 52px 46px; text-align: center;
  display: flex; flex-direction: column; gap: var(--sp-6); align-items: center;
}
.dp-cover:has(.dp-bleed) .dp-cover-text { color: #fff; }
.dp-cover-text b {
  font-family: var(--font-ui); font-size: 38px; font-weight: 800;
  text-transform: uppercase; letter-spacing: var(--track-fine); line-height: 1.1;
}
.dp-cover-text span { font-size: 15px; max-width: 46ch; line-height: 1.5; opacity: .92; }

.dp-quote { justify-content: center; align-items: center; text-align: center; gap: var(--sp-7); }
.dp-quote-text {
  font-family: var(--font-ui); font-size: 27px; font-weight: 700;
  line-height: 1.3; max-width: 30ch;
}
.dp-rule { width: 60px; height: 3px; border-radius: var(--r-2xs); display: block; }

.dp-image { padding: 0; justify-content: flex-end; }
.dp-caption {
  position: relative; z-index: var(--z-base);
  background: rgba(0,0,0,.62); color: #fff;
  padding: 14px 46px; font-size: 13px;
}
.dp-image:not(:has(.dp-bleed)) .dp-caption { background: #eceef3; color: #41465a; }

.dp-split { flex-direction: row; padding: 0; }
.dp-half { flex: 1; position: relative; min-width: 0; }
.dp-words { padding: 40px 40px; display: flex; flex-direction: column; }

.dp-gallery .dp-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-5);
  flex: 1; min-height: 0;
}
.dp-cell { position: relative; background: #f2f3f6; border-radius: var(--r-xs); overflow: hidden; }
.dp-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dp-x {
  position: absolute; top: 5px; right: 5px; width: 20px; height: 20px;
  border: none; border-radius: var(--r-pill); background: rgba(0,0,0,.55); color: #fff;
  cursor: pointer; font-size: 11px; line-height: 1; opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.dp-cell:hover .dp-x { opacity: 1; }

.dp-slot, .deck-slot {
  border: 2px dashed #c9cdd8; background: #f7f8fa; color: #8a90a0;
  border-radius: var(--r-xs); cursor: pointer; font-family: var(--font-ui); font-size: 13px;
  display: grid; place-items: center; width: 100%; height: 100%;
}
.deck-slot.in-grid { min-height: 60px; }
.deck-slot:hover { border-color: var(--accent); color: var(--accent); }
.dp-cover .deck-slot, .dp-image .deck-slot { position: absolute; inset: 0; }

.dp-auto-list { display: flex; flex-direction: column; gap: var(--sp-6); overflow: hidden; }
.dp-auto-list > div { display: grid; grid-template-columns: auto auto 1fr; gap: var(--sp-4); align-items: baseline; }
.dp-auto-list b { font-size: 14px; text-transform: uppercase; letter-spacing: var(--track-fine); }
.dp-auto-list span { font-size: 11px; color: #8a90a0; text-transform: uppercase; letter-spacing: var(--track-label); }
.dp-auto-list p {
  grid-column: 1 / -1; margin: 0; font-family: var(--font-read);
  font-size: 13px; color: #41465a; line-height: 1.5;
}
.dp-empty { color: #a2a7b5; font-style: italic; font-size: 13.5px; }

.dp-tools {
  position: absolute; right: -40px; top: 0;
  display: flex; flex-direction: column; gap: var(--sp-1);
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.deck-page:hover .dp-tools, .deck-page:focus-within .dp-tools { opacity: 1; }
.dp-tools button {
  width: 30px; height: 28px; border-radius: var(--r-md); border: 1px solid transparent;
  background: none; color: var(--faint); cursor: pointer; font-size: 13px;
  display: grid; place-items: center;
}
.dp-tools button:hover { background: var(--raised); color: var(--text); }
.dp-tools button.danger:hover { color: var(--danger); }
.dp-tools button.on { background: var(--raised); color: var(--accent); }
.deck-page.picking .dp-tools { opacity: 1; }

/*
 * The strip of pages down the side.
 *
 * A deck is read by flicking, not scrolling: you want page nine, or you want the
 * tone page before the cast page. Both are one gesture here and two screens of
 * scrolling without it.
 */
.deck-stage { display: flex; align-items: flex-start; }

.slide-rail {
  /* The offset tracks the toolbar above it, whose height scales with the text
     size. A fixed 46px left the strip sliding under the bar at 150% and up. */
  position: sticky; top: calc(10px + 36px * var(--fs)); align-self: flex-start;
  flex: none; width: 158px;
  max-height: calc(100vh - 10px - 36px * var(--fs));
  display: flex; flex-direction: column; gap: var(--sp-4);
  padding: 18px 12px 14px 16px;
  border-right: 1px solid var(--border-soft);
}
.rail-list {
  display: flex; flex-direction: column; gap: var(--sp-4);
  overflow-y: auto; overflow-x: hidden;
  margin: 0 -4px; padding: 0 4px;
}

.rail-slide {
  display: grid; grid-template-columns: 15px 1fr; gap: var(--sp-4); align-items: start;
  background: none; border: 0; padding: var(--sp-2); border-radius: var(--r-md);
  cursor: grab; text-align: left; color: var(--muted); font: inherit;
}
.rail-slide:active { cursor: grabbing; }
.rail-slide:hover { background: var(--raised); }
.rail-slide.on { background: var(--raised); color: var(--text); }
.rail-slide.lifting { opacity: .35; }

.rail-n {
  font-family: var(--font-mono); font-size: var(--fz-micro);
  line-height: 1.7; color: var(--faint); text-align: right;
}
.rail-slide.on .rail-n { color: var(--accent); }

.rail-thumb {
  display: block; border-radius: var(--r-xs); overflow: hidden;
  background: var(--dp-paper, #fff); color: var(--dp-ink, #1a1a1a);
  box-shadow: var(--ring-hair);
}
.slide-rail.landscape .rail-thumb { aspect-ratio: 11 / 8.5; }
.slide-rail.portrait  .rail-thumb { aspect-ratio: 8.5 / 11; }
.rail-slide.on .rail-thumb { box-shadow: var(--ring-on); }

.rail-cap {
  grid-column: 2; font-size: var(--fz-micro); line-height: 1.35;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}

.rail-add {
  flex: none; background: none; border: 1px dashed var(--border);
  border-radius: var(--r-md); padding: 7px 6px; cursor: pointer;
  color: var(--muted); font: inherit; font-size: var(--fz-tiny);
}
.rail-add:hover { border-color: var(--accent); color: var(--text); }

/* The little drawing of a layout. Two shades: picture areas, and lines of type. */
.lg { display: block; width: 100%; height: 100%; }
.lg-img { fill: currentColor; opacity: .17; }
.lg-ln  { fill: currentColor; opacity: .42; }

/*
 * Choosing what kind of page it is, in place.
 *
 * This was a dialog with a dropdown, which asked you to read eight descriptions
 * and guess. Here the shapes stay on screen while your own words rearrange
 * themselves behind them, so you choose by looking.
 */
.dp-layouts {
  display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: flex-start;
  margin-top: 10px; padding: 12px 14px;
  /* Was `var(--panel)`, a token that has never existed in this stylesheet.
     An undefined custom property with no fallback is invalid at computed
     value time, so this box has been rendering with a border and a radius
     around nothing at all. */
  background: var(--surface); border: 1px solid var(--border-soft); border-radius: var(--r);
}
.dp-layout {
  width: 74px; background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--muted); font: inherit; text-align: center;
}
.dp-layout .lg-box {
  display: block; border-radius: var(--r-xs); overflow: hidden; margin-bottom: 4px;
  background: var(--dp-paper, #fff); color: var(--dp-ink, #1a1a1a);
  box-shadow: var(--ring-hair);
}
.dp-layout .lg-box.landscape { aspect-ratio: 11 / 8.5; }
.dp-layout .lg-box.portrait  { aspect-ratio: 8.5 / 11; }
.dp-layout em {
  font-style: normal; font-size: var(--fz-micro); line-height: 1.3;
  display: block;
}
.dp-layout:hover { color: var(--text); }
.dp-layout:hover .lg-box { box-shadow: 0 0 0 1px var(--accent); }
.dp-layout.on { color: var(--text); }
.dp-layout.on .lg-box { box-shadow: var(--ring-on); }
.dp-layouts-note {
  flex: 1 0 100%; margin: 2px 0 0; color: var(--faint);
  font-size: var(--fz-tiny);
}

@media (max-width: 1100px) {
  .dp-num { left: 4px; top: 4px; z-index: var(--z-local); }
  .dp-tools { position: static; flex-direction: row; opacity: 1; margin-top: 6px; justify-content: flex-end; }

  /* No room beside the pages, so the strip lies along the top instead. Not
     sticky here: on a narrow window the toolbar above it wraps to two or three
     rows, and a pinned strip would slide underneath it. */
  .deck-stage { display: block; }
  .slide-rail {
    position: static; flex-direction: row; align-items: center; width: auto;
    max-height: none; border-right: 0; border-bottom: 1px solid var(--border-soft);
    padding: 10px 14px; background: var(--bg);
  }
  .rail-list { flex: 1; min-width: 0; flex-direction: row; overflow-x: auto; overflow-y: hidden; }
  /* flex: none, or the pages squash to slivers instead of the strip scrolling. */
  .rail-slide { flex: none; grid-template-columns: 1fr; width: 62px; gap: var(--sp-1); }
  .rail-n { text-align: left; }
  .rail-cap { display: none; }
  .rail-add { white-space: nowrap; }
}

/* ---- elements the other formats add ---------------------------------- */

.el[data-type="Act"] {
  text-transform: uppercase; font-weight: 700; text-align: center;
  margin-top: 2em; text-decoration: underline;
}
.el[data-type="Scene"] {
  text-transform: uppercase; font-weight: 700; margin-top: 2em;
}
.el[data-type="Section"] {
  text-transform: uppercase; font-weight: 700; margin-top: 2em;
}
.el[data-type="Note"] {
  font-style: italic; margin-left: 4ch; max-width: 52ch; margin-top: 1em;
  color: color-mix(in srgb, var(--page-ink) 62%, transparent);
}
.el[data-type="Sound"], .el[data-type="Music"], .el[data-type="SFX"] {
  text-transform: uppercase; margin-top: 1em; font-weight: 700;
}
.el[data-type="Sound"]::before { content: "SFX: "; }
.el[data-type="Music"]::before { content: "MUSIC: "; }
.el[data-type="SFX"]::before   { content: "SFX: "; margin-left: 0; }
.el[data-type="SFX"] { margin-left: 8ch; max-width: 48ch; }
.el[data-type="Caption"] {
  margin-left: 8ch; max-width: 48ch; margin-top: 1em;
}
.el[data-type="Caption"]::before { content: "CAPTION: "; opacity: .6; }
.el[data-type="Page"] {
  text-transform: uppercase; font-weight: 700; margin-top: 2em; text-decoration: underline;
}
.el[data-type="Panel"] { text-transform: uppercase; font-weight: 700; margin-top: 1em; }
.el[data-type="Stage Direction"] {
  font-style: italic; margin-left: 6ch; max-width: 48ch; margin-top: 1em;
}

/* Two-column A/V: what you see on the left, what you hear on the right. */
.el[data-type="Video"] {
  text-transform: uppercase; max-width: 28ch; margin-top: 1em;
  border-right: 1px solid color-mix(in srgb, var(--page-ink) 18%, transparent);
  padding-right: 2ch;
}
.el[data-type="Audio"] {
  margin-left: 32ch; max-width: 28ch; margin-top: -1em;
}
.el[data-type="Video"] + .el[data-type="Audio"] { margin-top: -1.05em; }

/* Sitcom: dialogue is double-spaced, action is shouted in brackets. */
.script-shell[data-format="sitcom"] .el[data-type="Dialogue"] {
  line-height: 2; max-width: 45ch;
}
.script-shell[data-format="sitcom"] .el[data-type="Action"] {
  text-transform: uppercase;
}
.script-shell[data-format="sitcom"] .el[data-type="Action"]::before { content: "("; }
.script-shell[data-format="sitcom"] .el[data-type="Action"]::after  { content: ")"; }
/* Both slots on this line are spoken for by the brackets, so a changed line
   wears its asterisk against the bracket instead of out in the right margin.
   Wrong place, present; rather than right place, missing. */
.script-shell[data-format="sitcom"] .el[data-type="Action"][data-changed="1"]::after {
  content: ")\00a0*"; position: static; color: inherit; opacity: 1; font-weight: inherit;
}
.script-shell[data-format="sitcom"] .el[data-type="Scene Heading"] { text-decoration: underline; }

/* Stage: the speaker sits hard left, the speech tucked under it. */
.script-shell[data-format="stage"] .el[data-type="Character"] { margin-left: 0; max-width: 60ch; }
.script-shell[data-format="stage"] .el[data-type="Dialogue"] { margin-left: 6ch; max-width: 54ch; }

/* ============================================================
   Franchises — several titles that share a world.
   ============================================================ */

.fr-shell { background: var(--bg); min-height: 100%; }
.fr-wrap { padding-top: 22px; }

.fr-count {
  font-size: var(--fz-tiny); padding: 1px 8px; border-radius: var(--r-pill);
  margin-left: 6px; font-weight: 600; vertical-align: middle;
}
.fr-count.bad { background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); }
.fr-count.ok  { background: color-mix(in srgb, var(--ok) 16%, transparent); color: var(--ok); }

.fr-problems { display: flex; flex-direction: column; gap: var(--sp-5); }
.fr-problem {
  display: grid; grid-template-columns: 20px 1fr; gap: var(--sp-5); align-items: start;
  background: color-mix(in srgb, var(--danger) 7%, transparent);
  border-left: 3px solid var(--danger);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  padding: 10px 12px; font-size: var(--fz-ui); line-height: 1.5;
}
.fr-warn { color: var(--danger); font-size: var(--fz-body); }
.fr-sides { margin-top: 5px; color: var(--text-2); }
.fr-sides b { color: var(--text); }
.fr-where { color: var(--faint); font-size: var(--fz-tiny); }
.fr-vs {
  color: var(--faint); margin: 0 9px; font-size: var(--fz-tiny);
  text-transform: uppercase; letter-spacing: var(--track-label);
}

.fr-cast { display: flex; flex-direction: column; gap: var(--sp-2); }
.fr-person {
  display: grid; grid-template-columns: auto 1fr auto; gap: var(--sp-5); align-items: center;
  padding: 5px 7px; border-radius: var(--r-md);
}
.fr-person:hover { background: var(--surface); }
.fr-person > div { display: flex; flex-direction: column; min-width: 0; }
.fr-person b { font-size: var(--fz-small); }
.fr-person span:last-child { font-size: var(--fz-tiny); color: var(--faint); }
.fr-unlinked {
  width: 17px; height: 17px; border-radius: var(--r-pill); display: grid; place-items: center;
  background: color-mix(in srgb, var(--film) 22%, transparent); color: var(--film);
  font-size: var(--fz-micro); font-weight: 700;
}

/* ---- the saga board -------------------------------------------------- */

/*
 * A shelf, not a graph.
 *
 * The titles sit in a row in order, because the order is the point and a
 * free-floating cloud of cards hides it. Connections arc over the top so no line
 * ever crosses a card, and a trilogy is bracketed underneath.
 */
.saga { padding-top: 14px; }
.saga-head { margin-bottom: 12px; }
.saga-head p { margin: 6px 0 0; }

.saga-modes { display: inline-flex; gap: var(--sp-1); padding: var(--sp-2); border-radius: var(--r-pill); background: var(--surface); }
.saga-mode {
  background: none; border: 0; cursor: pointer; font: inherit;
  font-size: var(--fz-small); padding: 5px 13px; border-radius: var(--r-pill);
  color: var(--muted);
}
.saga-mode:hover { color: var(--text); }
.saga-mode.on { background: var(--raised-2); color: var(--text); font-weight: 550; }

.saga-strip { overflow-x: auto; overflow-y: hidden; margin: 0 -20px; padding: 0 20px; }
.saga-board { position: relative; display: flex; align-items: flex-start; gap: var(--sp-10); min-width: max-content; }
.saga-links { position: absolute; inset: 0; pointer-events: none; overflow: visible; }

.saga-card {
  position: relative; z-index: var(--z-base); flex: none; width: 178px;
  display: flex; flex-direction: column; gap: var(--sp-4);
  padding: var(--sp-5); border-radius: var(--r);
  background: var(--surface); border: 1px solid var(--border-soft);
  cursor: grab; transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.saga-card:hover { border-color: var(--border); transform: translateY(-2px); }
.saga-card:active { cursor: grabbing; }
.saga-card.lifting { opacity: .35; }

.saga-num {
  position: absolute; top: -8px; left: -8px; z-index: var(--z-local);
  width: 22px; height: 22px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--raised-2); color: var(--muted);
  font-family: var(--font-mono); font-size: var(--fz-micro);
  border: 1px solid var(--bg);
}

.saga-art {
  display: block; width: 100%; aspect-ratio: 27 / 40; object-fit: cover;
  border-radius: var(--r-sm); background: var(--raised);
}
/*
 * A title with no poster now gets the same struck mark the home shelf gives
 * it, with its initials under it - the board used to be the one place that
 * answered "no picture" with a flat block of colour.
 *
 * Two rows rather than `place-items: center` on one, or the mark and the
 * letters stack on top of each other. Written as `img.saga-forge` for the
 * same reason `img.shelf-forge` is: `.ic`-style sizing rules elsewhere are
 * (0,1,0) and would otherwise stretch the medal to the full box.
 */
.saga-art.blank {
  display: grid; grid-auto-flow: row; align-content: center; justify-items: center;
  gap: var(--sp-3); color: #fff;
  font-size: var(--fz-stat); font-weight: 600; letter-spacing: var(--track-label);
}
.saga-art.blank img.saga-forge {
  width: 56%; max-width: 92px; height: auto; display: block;
  image-rendering: pixelated;
  filter: drop-shadow(0 2px 10px rgb(0 0 0 / .35));
  transition: transform var(--dur-1) var(--ease);
}
.saga-card:hover .saga-forge { transform: translateY(-3px) scale(1.04); }
.saga-art.blank b { font-weight: inherit; text-shadow: 0 1px 8px rgb(0 0 0 / .3); }
body.no-motion .saga-forge { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .saga-card:hover .saga-forge { transform: none; }
}

.saga-words { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.saga-words b { font-size: var(--fz-ui); line-height: 1.25; }
.saga-part {
  font-size: var(--fz-micro); text-transform: uppercase; letter-spacing: var(--track);
  color: var(--accent);
}
.saga-kind { font-size: var(--fz-micro); color: var(--muted); }
.saga-when { font-size: var(--fz-micro); color: var(--text-2); font-style: italic; }
.saga-follows { font-size: var(--fz-micro); color: var(--faint); margin-top: 3px; }

.saga-card .row-actions { opacity: 0; transition: opacity var(--dur-1) var(--ease); }
.saga-card:hover .row-actions, .saga-card:focus-within .row-actions { opacity: 1; }

/* Three kinds of line: carrying on, going back, and going sideways. */
.saga-arc { fill: none; stroke-width: 1.6; }
.saga-arc.straight { stroke: var(--accent); }
.saga-arc.back { stroke: var(--film); stroke-dasharray: 7 4; }
.saga-arc.aside { stroke: var(--muted); stroke-dasharray: 2 4; }
.saga-head.straight { fill: var(--accent); }
.saga-head.back { fill: var(--film); }
.saga-head.aside { fill: var(--muted); }

.saga-arc-label {
  fill: var(--muted); font-size: 10.5px; font-family: var(--font-ui);
  paint-order: stroke; stroke: var(--bg); stroke-width: 4px; stroke-linejoin: round;
}
.saga-bracket { fill: none; stroke: var(--border); stroke-width: 1.4; }
.saga-bracket-label {
  fill: var(--faint); font-size: 10.5px; font-family: var(--font-ui);
  text-transform: uppercase; letter-spacing: var(--track);
}

.saga-said { margin-top: 16px; display: flex; flex-direction: column; gap: var(--sp-2); }
.saga-said p {
  margin: 0; display: grid; grid-template-columns: 16px 1fr; gap: var(--sp-3);
  font-size: var(--fz-small); color: var(--text-2); line-height: 1.5;
}
.saga-said span { color: var(--film); }

.row.unsettled .row-title { color: var(--film); }
.chip.small { font-size: var(--fz-micro); padding: 1px 6px; }
.chip.warn { background: color-mix(in srgb, var(--film) 18%, transparent); color: var(--film); }
.avatar.sm { width: 26px; height: 26px; font-size: var(--fz-micro); }

/* ============================================================
   Production — breakdown sheets, reports, sides.
   Plain and dense: this is read at arm's length on a location.
   ============================================================ */

.prod-shell { background: var(--bg); min-height: 100%; }
.prod-wrap { padding-top: 22px; }

.prod-summary {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: var(--sp-4); margin-bottom: 18px;
}
.prod-summary > div {
  background: var(--surface); border-radius: var(--r-sm); padding: 9px 11px;
  display: flex; flex-direction: column;
}
.prod-summary b { font-size: var(--fz-stat); font-family: var(--font-read); }
.prod-summary span {
  font-size: var(--fz-micro); color: var(--faint);
  text-transform: uppercase; letter-spacing: var(--track-label);
}

.prod-sheets { display: flex; flex-direction: column; gap: var(--sp-5); }
.prod-sheet {
  display: grid; grid-template-columns: 54px 1fr; gap: var(--sp-6);
  border: 1px solid var(--border-soft); border-radius: var(--r);
  padding: 11px 13px; background: var(--bg);
}
.prod-sheet.picked { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 6%, var(--bg)); }

.prod-pick { display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); cursor: pointer; }
.prod-pick input { width: 15px; height: 15px; cursor: pointer; }
.prod-num {
  font-family: var(--font-mono); font-size: var(--fz-ui);
  font-weight: 700; color: var(--text-2);
}

.prod-head { display: flex; justify-content: space-between; gap: var(--sp-6); align-items: baseline; }
.prod-head b { font-family: var(--font-mono); font-size: var(--fz-small); text-transform: uppercase; }
.prod-meta {
  font-family: var(--font-mono); font-size: var(--fz-micro);
  color: var(--faint); white-space: nowrap;
}
.prod-syn {
  margin: 5px 0 0; font-family: var(--font-read);
  font-size: var(--fz-small); color: var(--muted); line-height: 1.5;
}

.prod-tags { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: 8px; }
.prod-dept {
  display: grid; grid-template-columns: 9px 108px 1fr; gap: var(--sp-4); align-items: baseline;
  font-size: var(--fz-tiny);
}
.prod-dot { width: 9px; height: 9px; border-radius: var(--r-pill); display: inline-block; }
.prod-dept-label { color: var(--faint); text-transform: uppercase; letter-spacing: var(--track-label); font-size: var(--fz-micro); }
.prod-items { color: var(--text-2); }

.prod-table { width: 100%; border-collapse: collapse; font-size: var(--fz-small); }
.prod-table th {
  text-align: left; padding: 7px 10px; color: var(--faint);
  font-size: var(--fz-micro); text-transform: uppercase; letter-spacing: var(--track-label);
  border-bottom: 1px solid var(--border);
}
.prod-table td { padding: 7px 10px; border-bottom: 1px solid var(--border-soft); }
.prod-table tr:hover td { background: var(--surface); }

.prod-group { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin: 4px 0 8px; }
.prod-chip {
  font-family: var(--font-mono); font-size: var(--fz-tiny);
  background: var(--raised); border-radius: var(--r-sm); padding: 3px 8px; cursor: pointer;
  color: var(--text-2);
}
.prod-chip:hover { background: var(--accent); color: var(--accent-ink); }

.export-card .btn { flex: 1; justify-content: center; }

/* ============================================================
   The overview: everyone, and who they connect to.
   ============================================================ */

.ov-list { display: flex; flex-direction: column; gap: var(--sp-1); max-height: 60vh; overflow-y: auto; }

.ov-row {
  display: grid; grid-template-columns: minmax(160px, 220px) 1fr;
  gap: var(--sp-7); align-items: start;
  padding: 9px 10px; border-radius: var(--r-sm);
}
.ov-row:nth-child(odd) { background: var(--raised); }

/* Nobody has given these people a relationship yet, which is usually the
   most useful thing this list can tell you. */
.ov-row.alone { opacity: .62; }

.ov-who { display: flex; gap: var(--sp-5); align-items: center; min-width: 0; }
.ov-who b { display: block; font-size: var(--fz-ui); }

.ov-ties { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; }

.ov-tie {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  font-size: var(--fz-small);
  padding: 3px 9px; border-radius: var(--r-pill);
  background: var(--bg); border: 1px solid var(--border-soft);
}
.ov-dot { width: 7px; height: 7px; border-radius: var(--r-pill); flex: none; }

@media (max-width: 700px) {
  .ov-row { grid-template-columns: 1fr; gap: var(--sp-3); }
}

/* ============================================================
   Themes.

   Each block is the same set of variables the built-in dark and light themes
   define at the top of this file, so nothing else in the stylesheet has to
   know a theme exists. Add a block here and it works everywhere.

   The scenery that some of these put behind the app is further down.
   ============================================================ */

/* Starry night. */
html[data-theme="starry"] {
  --bg:        #0a0f24;
  --bg-deep:   #060a18;
  --surface:   #1d264d;
  --raised:    #2c376b;
  --raised-2:  #3a4884;
  --border:    #2a3462;
  --border-soft: #1e2749;
  --text:      #e8ecff;
  --text-2:    #b7c1e8;
  --text-3:    #97a7e1;
  --muted:     #a4b1e8;
  --faint:     #92a2e4;
  --accent:    #9db4ff;
  --accent-ink:#0a0f24;
  --accent-2:  #c79dff;
  --accent-soft: #9db4ff22;
  --film:      #ffc078;
  --tv:        #7fe0e0;
  --danger:    #ff8b9a;
  --danger-ink:#2a0a12;
  --ok:        #7fdcae;
  --page-bg:   #141b38;
  --page-ink:  #e8ecff;
}

/* Pink Japan. */
html[data-theme="blossom"] {
  --bg:        #241322;
  --bg-deep:   #1b0e1a;
  --surface:   #41253b;
  --raised:    #59334f;
  --raised-2:  #6f4261;
  --border:    #563149;
  --border-soft: #422538;
  --text:      #ffeaf2;
  --text-2:    #e6c0d1;
  --text-3:    #cea4bd;
  --muted:     #dfacc4;
  --faint:     #d39cb7;
  --accent:    #ffb7d0;
  --accent-ink:#2a1420;
  /* Pink leaning to gold: the blossom against the lantern, not against
     more pink. */
  --accent-2:  #ffd9a8;
  --accent-soft: #ffb7d022;
  --film:      #ffcf9b;
  --tv:        #9fdcd4;
  --danger:    #ff8fa0;
  --danger-ink:#2e0d15;
  --ok:        #93d9b0;
  --page-bg:   #33202e;
  --page-ink:  #ffeaf2;
}

/* Forest. */
html[data-theme="forest"] {
  --bg:        #0f1f18;
  --bg-deep:   #0a1710;
  --surface:   #1c3529;
  --raised:    #284837;
  --raised-2:  #335a45;
  --border:    #2c4d3a;
  --border-soft: #1f3a2b;
  --text:      #e7f3ea;
  --text-2:    #bcd6c5;
  --text-3:    #98baa5;
  --muted:     #a3c5af;
  --faint:     #92b7a1;
  --accent:    #7fc9a0;
  --accent-ink:#0b1911;
  /* Sage sliding to the low sun through the canopy. Never to hot pink. */
  --accent-2:  #cbcf85;
  --accent-soft: #7fc9a022;
  --film:      #e3b877;
  --tv:        #79cfc4;
  --danger:    #f0918b;
  --danger-ink:#2b100e;
  --ok:        #8fd9a8;
  --page-bg:   #172c21;
  --page-ink:  #e7f3ea;
}

/* Cold mountains. */
html[data-theme="mountains"] {
  --bg:        #141a25;
  --bg-deep:   #0e131c;
  --surface:   #252f40;
  --raised:    #344157;
  --raised-2:  #42526c;
  --border:    #35415a;
  --border-soft: #263043;
  --text:      #e9eef6;
  --text-2:    #bcc8da;
  --text-3:    #a1b3cb;
  --muted:     #acbdd8;
  --faint:     #9aaed1;
  --accent:    #a8c4e0;
  --accent-ink:#131a25;
  /* Cold light on cold stone: the accent leans violet, never warm. */
  --accent-2:  #c9b8e2;
  --accent-soft: #a8c4e022;
  --film:      #e8b98a;
  --tv:        #8fcfd4;
  --danger:    #f2929a;
  --danger-ink:#2b0e12;
  --ok:        #8ed3b0;
  --page-bg:   #1d2533;
  --page-ink:  #e9eef6;
}

/* ============================================================
   The scenery.

   A fixed layer behind everything, built by web/lib/themes.js. It never
   receives a click and never sits over the writing: the sidebar, the panels and
   every page surface are opaque and painted on top.

   Everything animates transform or opacity only, so none of it touches layout
   and the compositor can carry it while you type.
   ============================================================ */

.scene {
  /* The scenery is the floor: everything, including .app, is painted on it. */
  position: fixed; inset: 0; z-index: var(--z-floor);
  pointer-events: none; overflow: hidden;
  contain: strict;
}
.scene:empty { display: none; }

/* The app sits above the scenery — it says so itself, with the layout. */

/*
 * With scenery on, the app background steps aside so it can be seen. The
 * sidebar and the working area keep a translucent wash, because text over
 * moving petals is unreadable and no amount of prettiness is worth that.
 */
html[data-scenery] body,
html[data-scenery] .app { background: transparent; }

html[data-scenery] .sidebar {
  background: color-mix(in srgb, var(--bg-deep) 84%, transparent);
  backdrop-filter: blur(14px);
}

html[data-scenery] .main {
  background: color-mix(in srgb, var(--bg) 66%, transparent);
}

/*
 * The sky comes into the writing room.
 *
 * The editors used to sit on an opaque slab, which under a scenery theme read
 * as an empty void with a page floating in it. The slab is now a wash the
 * scenery shows through — you write under the starry sky, among the blossom —
 * while the page itself stays exactly as opaque and readable as ever. The
 * scenery rules hold: it is behind the margins, never behind the words.
 */
html[data-scenery] .editor-shell,
html[data-scenery] .deck-shell,
html[data-scenery] .web-shell {
  background: color-mix(in srgb, var(--bg-deep) 45%, transparent);
}
html[data-scenery] .write-status {
  background: color-mix(in srgb, var(--bg) 72%, transparent);
}

/* ---------- shared pieces ---------- */

.sc-sky { position: absolute; inset: 0; }

/* The website's exact dome: navy light overhead falling away to near black. */
.sc-sky-night   { background: radial-gradient(ellipse 120% 80% at 50% -10%, #1a1a3e 0%, #0d0d20 42%, #06060f 100%); }
.sc-sky-blossom { background: radial-gradient(120% 100% at 78% 0%, #6b3149 0%, #3a1c30 45%, #1b0e1a 100%); }
.sc-sky-forest  { background: radial-gradient(110% 90% at 50% -20%, #2a5c40 0%, #14301f 48%, #081410 100%); }
.sc-sky-cold    { background: radial-gradient(120% 100% at 50% 0%, #2b3a52 0%, #1a2333 46%, #0c111a 100%); }

/* ---------- starry night ---------- */

.sc-stars { position: absolute; inset: 0; }
.sc-stars i {
  position: absolute; border-radius: var(--r-pill); background: #fff;
  box-shadow: 0 0 4px rgb(255 255 255 / .7);
  animation-name: sc-twinkle;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}
@keyframes sc-twinkle {
  0%, 100% { opacity: .25; transform: scale(.7); }
  50%      { opacity: 1;   transform: scale(1.15); }
}

/*
 * A shooting star: a bright head with a tapering trail, launched by the scenery
 * code at a random spot every few seconds and thrown away after one crossing.
 * The whole element is rotated so the streak and its travel agree — that is
 * what makes it read as one object moving rather than a line being drawn.
 */
.sc-meteor {
  position: absolute; width: 3px; height: 3px; border-radius: var(--r-pill);
  background: #fff; opacity: 0;
  box-shadow: 0 0 8px 2px rgb(255 255 255 / .9);
  transform: rotate(var(--angle, 18deg));
  animation-name: sc-shoot;
  animation-timing-function: ease-in;
  animation-fill-mode: forwards;
}
.sc-meteor::after {
  content: ""; position: absolute; right: 3px; top: 50%;
  width: var(--trail, 140px); height: 2px;
  transform: translateY(-50%);
  background: linear-gradient(to left, rgb(255 255 255 / .9), transparent);
  border-radius: var(--r-pill);
}
@keyframes sc-shoot {
  0%   { opacity: 0; transform: rotate(var(--angle, 18deg)) translateX(0); }
  8%   { opacity: 1; }
  70%  { opacity: 1; }
  100% { opacity: 0; transform: rotate(var(--angle, 18deg)) translateX(var(--dist, 620px)); }
}

/* ---------- pink Japan ---------- */

.sc-branch {
  position: absolute; top: 0; right: 0;
  width: min(62vw, 720px); height: min(52vh, 460px);
  color: #2b1420;
  opacity: .92;
}
.sc-branch svg { width: 100%; height: 100%; }
.sc-branch .sc-buds { color: #ffc2d8; opacity: .9; }

.sc-petals { position: absolute; inset: -6% 0 0 0; }
.sc-petals i {
  position: absolute; top: -4%;
  width: var(--size); height: calc(var(--size) * .78);
  background: linear-gradient(150deg, #ffd3e2, #ff9dbe);
  border-radius: 100% 0 100% 0;
  opacity: 0;
  animation-name: sc-fall;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
}
@keyframes sc-fall {
  0%   { opacity: 0; transform: translate3d(0, 0, 0) rotate(0deg); }
  8%   { opacity: .95; }
  92%  { opacity: .95; }
  100% { opacity: 0; transform: translate3d(var(--sway), 108vh, 0) rotate(var(--spin)); }
}

/* ---------- forest ---------- */

.sc-trees { position: absolute; inset: 0; }
.sc-trees svg { width: 100%; height: 100%; }
.sc-trees-far  { color: #1c3b28; opacity: .55; }
.sc-trees-mid  { color: #122a1c; opacity: .75; transform: scaleX(1.4); }
.sc-trees-near { color: #08160e; opacity: .92; transform: scaleX(2.2); }

.sc-shafts {
  position: absolute; inset: -20% -10% auto -10%; height: 120%;
  background:
    linear-gradient(102deg, transparent 18%, rgb(190 255 210 / .10) 21%, transparent 26%),
    linear-gradient(97deg,  transparent 42%, rgb(210 255 220 / .08) 45%, transparent 51%),
    linear-gradient(107deg, transparent 66%, rgb(180 250 205 / .09) 69%, transparent 75%);
  filter: blur(3px);
  animation: sc-breathe 14s ease-in-out infinite;
}
@keyframes sc-breathe {
  0%, 100% { opacity: .7; }
  50%      { opacity: 1; }
}

.sc-motes { position: absolute; inset: 0; }
.sc-motes i {
  position: absolute;
  width: var(--size); height: var(--size); border-radius: var(--r-pill);
  background: rgb(220 255 230 / .8);
  box-shadow: 0 0 6px 2px rgb(190 255 215 / .35);
  opacity: 0;
  animation-name: sc-drift;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}
@keyframes sc-drift {
  0%   { opacity: 0; transform: translate3d(0, 0, 0); }
  15%  { opacity: .9; }
  85%  { opacity: .9; }
  100% { opacity: 0; transform: translate3d(var(--drift), -60px, 0); }
}

/* ---------- cold mountains ---------- */

.sc-ridge { position: absolute; left: 0; right: 0; height: 62%; }
.sc-ridge svg { width: 100%; height: 100%; }
.sc-ridge-0 { color: #39485f; opacity: .55; bottom: 26%; }
.sc-ridge-1 { color: #2c3950; opacity: .7;  bottom: 16%; }
.sc-ridge-2 { color: #212c3f; opacity: .85; bottom: 7%; }
.sc-ridge-3 { color: #171f2e; opacity: 1;   bottom: 0; }

.sc-mist {
  position: absolute; left: -10%; right: -10%; bottom: 4%; height: 42%;
  background: linear-gradient(to top, rgb(200 220 245 / .16), transparent 70%);
  filter: blur(10px);
  animation: sc-breathe 18s ease-in-out infinite;
}

.sc-snow { position: absolute; inset: -6% 0 0 0; }
.sc-snow i {
  position: absolute; top: -4%;
  width: var(--size); height: var(--size); border-radius: var(--r-pill);
  background: rgb(255 255 255 / .9);
  opacity: 0;
  animation-name: sc-snowfall;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
}
@keyframes sc-snowfall {
  0%   { opacity: 0; transform: translate3d(0, 0, 0); }
  10%  { opacity: .9; }
  90%  { opacity: .9; }
  100% { opacity: 0; transform: translate3d(var(--sway), 108vh, 0); }
}

/* ============================================================
   Stopping the movement.

   Either because the machine asked — someone who gets motion sick has usually
   set that once, system wide, and should not have to ask twice — or because the
   writer turned it off. The scenery stays; it just holds still.
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  .scene * { animation: none !important; }
  .sc-petals i, .sc-snow i, .sc-motes i { opacity: .5; }
  .sc-meteor { opacity: 0; }
}

body.no-motion .scene * { animation: none !important; }
body.no-motion .sc-petals i,
body.no-motion .sc-snow i,
body.no-motion .sc-motes i,
body.no-motion .sc-meteor { opacity: 0; }

/* ---------- the theme picker ---------- */

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

/* ============================================================
   Articles that feel alive: a banner, a face, a colour.
   ============================================================ */
.article-view { --entry-accent: var(--accent); }
.article-banner {
  position: relative; height: 190px; margin: -28px -38px 20px;
  overflow: hidden;
}
@media (max-width: 1240px) { .article-banner { margin: -22px -24px 18px; } }
.article-banner img { width: 100%; height: 100%; object-fit: cover; display: block; }
.article-banner-veil {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    transparent 30%,
    color-mix(in srgb, var(--entry-accent) 16%, var(--bg)) 96%);
}
.article-view .page-title::after {
  background: linear-gradient(90deg, var(--entry-accent), transparent); width: 64px;
}
.article-view .avatar-shot { --ring-tint-colour: var(--entry-accent); box-shadow: var(--ring-tint); }
.emoji-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.emoji-pick {
  width: 30px; height: 30px; border-radius: var(--r-sm); cursor: pointer;
  background: var(--bg-deep); border: 1px solid var(--border-soft);
  font-size: var(--glyph); line-height: 1; padding: 0;
}
.emoji-pick:hover { border-color: var(--border); transform: scale(1.12); }
.emoji-pick.on { border-color: var(--entry-accent, var(--accent)); background: color-mix(in srgb, var(--accent) 14%, transparent); }
.emoji-own { width: 58px; text-align: center; }
.colour-pick {
  width: 24px; height: 24px; border-radius: var(--r-circle); cursor: pointer; padding: 0;
  border: 2px solid var(--border-soft); color: var(--muted); font-size: var(--glyph-sm);
}
.colour-pick:hover { transform: scale(1.15); }
.colour-pick.on { border-color: var(--text); box-shadow: var(--ring-tint); }

/* ============================================================
   The companion — a beat sheet or outline as wide as the page,
   like two writing apps open side by side.
   ============================================================ */
.script-layout.beside { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.script-layout.beside .desk-rail,
.script-layout.beside .script-rail { display: none; }
.script-layout.beside .script-wrap { grid-column: 1; }
.script-layout.beside .companion-pane { grid-column: 2; }
.companion-pane {
  display: flex; flex-direction: column; min-width: 0;
  border-left: 1px solid var(--border-soft);
  background: color-mix(in srgb, var(--bg-deep) 55%, var(--bg));
  max-height: calc(100vh - 46px); position: sticky; top: 0;
}
/*
 * Hidden means hidden, everywhere.
 *
 * `[hidden]` is only a browser default of `display: none`, so any author rule
 * that sets a display beats it. This file does that constantly, and the
 * consequence was patched three separate times - here, on `.studio-menu` and
 * on `.ending` - without anyone noticing they were three instances of one
 * bug. The ones nobody patched shipped: `.map-empty` is an absolutely
 * positioned overlay, so "Every world deserves a map" was painted across a
 * writer's own map, over their own pins, making every one of them
 * unclickable; the same overlay sat on the connections web reading "No
 * connections yet" over eighty drawn lines. A script's "White pages" chip
 * never went away either.
 *
 * `!important` because the whole point is to beat a more specific rule that
 * has already been written, and every rule that will be written later.
 */
[hidden] { display: none !important; }

.companion-bar {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: 10px 16px; border-bottom: 1px solid var(--border-soft);
}
.companion-bar b { font-size: var(--fz-ui); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.companion-body { flex: 1; overflow-y: auto; padding: 22px 28px 60px; }
.companion-beats { display: flex; flex-direction: column; gap: var(--sp-7); max-width: var(--measure); margin: 0 auto; }
.companion-beat {
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--r); padding: 12px 15px;
}
.companion-beat-head {
  font-weight: 700; font-size: var(--fz-ui);
  text-transform: uppercase; letter-spacing: var(--track-label);
  outline: none; margin-bottom: 6px; min-height: 1.2em;
  border-bottom: 1px dashed transparent;
}
.companion-beat-head:focus { border-bottom-color: var(--accent); }
.companion-beat-body {
  outline: none; min-height: 2.2em; line-height: 1.65;
  font-size: var(--fz-ui); color: var(--text-2);
}
.companion-beat-body:empty::before { content: attr(data-placeholder); color: var(--faint); }
/* A kept copy read beside the live page. Script type, script margins, and no
   caret: this is here to be looked at, not written in. */
.companion-script {
  font: var(--script-fs) / 1 var(--font-mono);
  color: var(--page-ink);
  padding: var(--sp-4) 0;
}
.companion-script .el { white-space: pre-wrap; margin-top: 1em; }
.companion-script .el:first-child { margin-top: 0; }
.companion-script .el[data-type="Scene Heading"] { text-transform: uppercase; font-weight: 700; }
.companion-script .el[data-type="Character"] { margin-left: 13.2em; text-transform: uppercase; }
.companion-script .el[data-type="Parenthetical"] { margin-left: 9.6em; margin-top: 0; }
.companion-script .el[data-type="Dialogue"] { margin-left: 6em; margin-right: 8.4em; margin-top: 0; }
.companion-script .el[data-type="Transition"] { text-align: right; text-transform: uppercase; }

.companion-doc { max-width: var(--measure); margin: 0 auto; font-size: var(--fz-body); line-height: 1.8; }
.companion-doc p { margin: 0 0 0.9em; }
.companion-doc .companion-h { font-weight: 700; margin-top: 1.3em; }

/* ============================================================
   THE INSTRUMENT — the design language of the rebuild.
   Precise type, drawn icons, tracked labels, one accent.
   This layer is deliberate: it restates the identity of the
   core components in one place, late in the file, so the whole
   app speaks with one voice whatever order features arrived in.
   ============================================================ */

/* Drawn icons, everywhere the chrome used to wear emoji. */
.ic {
  width: var(--icon); height: var(--icon); flex: none;
  vertical-align: -2px; display: inline-block;
}
.ic-sm { width: var(--icon-sm); height: var(--icon-sm); }
.ic-lg { width: var(--icon-lg); height: var(--icon-lg); }
.btn .ic { opacity: .85; }
.btn:hover .ic { opacity: 1; }

/* Titles speak in the display face. (`.page-title` says so itself, with the
   rest of its one definition.) */
.map-title, .modal-head h2, .empty-state h3 {
  font-family: var(--font-display);
  letter-spacing: var(--track-tight);
}
.section-head h2 {
  font-family: var(--font-display);
  font-size: var(--fz-lead);
  letter-spacing: var(--track-tight);
}

/* Numbers are set like instruments read: display face, tabular. */
.stat b, .pagecount, .dossier-count {
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
}

/* The professional whisper: tracked uppercase micro-labels. `.tab` states
   this itself now, with the rest of the tabs. */
.dossier-field > label, .tools-pick h4, .world-group,
#studio-menu-btn, .studio-menu .btn {
  letter-spacing: var(--track-label);
}

/* Buttons state their own radius and weight, up with the buttons. */

/* `.chip` joined the label system here, restating type the base definition
   had already set differently. It says it once now, up with the chips. */

/* The sidebar goes full studio: darker than the room, so the
   work glows and the chrome recedes. */
/* The sidebar states its own background, up with the sidebar. */
.sidebar .nav-item { border-radius: var(--r-md); }
.sidebar .nav-item .label { font-weight: 500; }

/* A hint used to vanish entirely once answered, which meant filling the page
   in made it shorter and greyer — no reward for writing. It fades instead;
   the rule that does it lives with the rest of the dossier type. */

/* The reading serif carries the quotes and the cork notes. Both state it
   themselves now — see the one definition of each above. */

/* The marquee eyebrow: the studio's own letterhead over the greeting. */
.marquee-eyebrow { color: var(--muted); margin-bottom: 10px; }
.marquee-dot {
  width: 7px; height: 7px; border-radius: var(--r-circle); flex: none;
  background: var(--accent);
  box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 70%, transparent);
}

/* The deck's cast page: faces, the way a real pitch deck introduces people. */
.dp-cast-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-7) var(--sp-8); margin-top: 6px;
}
.dp-face { display: flex; gap: var(--sp-6); margin: 0; align-items: flex-start; }
.dp-face-pic {
  width: 54px; height: 54px; border-radius: var(--r-md); flex: none; overflow: hidden;
  display: grid; place-items: center; color: #fff;
  font-weight: 700; font-size: 16px; letter-spacing: var(--track-fine);
}
.dp-face-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dp-face figcaption { min-width: 0; }
.dp-face b { display: block; font-size: 14px; }
.dp-face figcaption > span {
  display: block; font-size: 10.5px; text-transform: uppercase;
  letter-spacing: var(--track); color: var(--dp-muted, var(--muted)); margin-top: 1px;
}
.dp-face p { margin: 5px 0 0; font-size: 12.5px; line-height: 1.5; }

/* The colour wheel that sits at the end of every palette. */
.swatch-own, .colour-own {
  position: relative; display: grid; place-items: center;
  cursor: pointer; overflow: hidden;
  border: 1.5px dashed var(--border) !important;
  color: var(--muted); font-size: 13px; font-weight: 700;
}
.swatch-own input, .colour-own input {
  position: absolute; inset: 0; opacity: 0; cursor: pointer; border: 0; padding: 0;
}
.swatch-own:hover, .colour-own:hover { border-color: var(--accent) !important; color: var(--text); }

/* ============================================================
   THE SCREENING ROOM — the home is a marquee, not a dashboard.
   Full-bleed hero with the latest story's own art, shelves of
   posters below, and the numbers last, where they belong.
   ============================================================ */
.home { padding: 0 0 90px; }

.home-hero {
  position: relative; min-height: 46vh; max-height: 560px;
  display: flex; align-items: flex-end;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 75% 10%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 55%),
    var(--bg-deep);
}
.home-hero-art {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center 25%;
}
.home-hero-veil {
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg, rgb(0 0 0 / .18), transparent 35%, color-mix(in srgb, var(--bg) 92%, transparent) 96%),
    linear-gradient(90deg, color-mix(in srgb, var(--bg-deep) 88%, transparent), transparent 55%);
}
.home-hero-body {
  position: relative; z-index: var(--z-base);
  padding: 40px clamp(24px, 5vw, 72px) 44px;
  max-width: 780px;
}
.home-title {
  margin: 6px 0 0;
  font-family: var(--font-display);
  font-size: clamp(30px, 4.6vw, 54px);
  font-weight: 700; line-height: 1.06; letter-spacing: var(--track-tight);
  text-shadow: 0 2px 24px rgb(0 0 0 / .45);
}
.home-logline {
  margin: 12px 0 0; max-width: var(--measure-line);
  font-family: var(--font-read); font-style: italic;
  font-size: clamp(14px, 1.35vw, 17px); line-height: 1.6;
  color: var(--text-2); text-shadow: 0 1px 12px rgb(0 0 0 / .5);
}
.home-hero-actions { display: flex; gap: var(--sp-5); flex-wrap: wrap; margin-top: 20px; }

.home-row { padding: 30px clamp(24px, 5vw, 72px) 4px; }
.home-row-head {
  display: flex; align-items: baseline; gap: var(--sp-6); margin-bottom: 12px;
}
.home-row-head h2 {
  margin: 0;
  font-size: var(--fz-small); font-weight: 700;
  text-transform: uppercase; letter-spacing: var(--track);
  color: var(--text-2);
}
.home-shelf {
  display: flex; gap: var(--sp-7); overflow-x: auto; padding: 6px 2px 18px;
  scroll-snap-type: x proximity; scrollbar-width: thin;
}
/*
 * *** ROOM FOR THE LIFT. ***
 *
 * Logan: "when i hover over any poster on the home page there is this ugly
 * white line that shows up." The white line is a scrollbar.
 *
 * Hovering a title grows its cover by 1.5% - about 1px past the right edge of
 * a row that is `overflow-x: auto`. One pixel of overhang is still overhang,
 * so the browser did the correct thing and offered to scroll to it, on a shelf
 * holding two titles with nothing to scroll to.
 *
 * The padding gives the grown cover somewhere to be; the matching negative
 * margin puts the row back where it was, so nothing moves. Not `overflow:
 * clip`, which would take the sideways scrolling with it - and that scrolling
 * is the shelf.
 */
.home-shelf { padding-inline: 6px; margin-inline: -6px; }
.home-shelf .shelf-poster { flex: none; scroll-snap-align: start; }

.home-band {
  max-width: 1180px; margin: 26px auto 0;
  padding: 0 clamp(24px, 5vw, 72px);
}
.home-band .daily-quote { margin: 26px 0 8px; }

/* The rest of the app centres its column: a made page, not a panel. The cap
   itself is stated once, with `.view`, as --content-max. */

/* ============================================================
   The title card — a project introduced by its own art.
   ============================================================ */
.project-view { --project-accent: var(--accent); padding-top: 0; }
.project-hero {
  position: relative; min-height: 230px; display: flex; align-items: flex-end;
  margin: 0 calc(clamp(22px, 3vw, 38px) * -1) 6px;
  padding: 0; overflow: hidden;
  border-bottom: 1px solid var(--border-soft);
  background:
    radial-gradient(90% 120% at 20% 0%, color-mix(in srgb, var(--project-accent) 16%, transparent), transparent 60%),
    var(--bg-deep);
}
.project-hero-art {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center 28%;
}
.project-hero-veil {
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg, rgb(0 0 0 / .2), transparent 40%, color-mix(in srgb, var(--bg) 94%, transparent) 97%),
    linear-gradient(90deg, color-mix(in srgb, var(--bg-deep) 85%, transparent), transparent 62%);
}
.project-hero-body {
  position: relative; z-index: var(--z-base);
  padding: 30px clamp(22px, 3vw, 38px) 26px; max-width: 760px;
}
.project-hero .page-title {
  font-size: clamp(26px, 3.4vw, 42px);
  text-shadow: 0 2px 20px rgb(0 0 0 / .45);
}
.project-hero .page-title::after { background: linear-gradient(90deg, var(--project-accent), transparent); }
.project-hero .page-sub { text-shadow: 0 1px 12px rgb(0 0 0 / .5); max-width: var(--measure); }
.project-view .tabs { margin-top: 4px; }

/* The editor bar reads as an instrument now: drawn icons, even weight. */
.editor-bar .ic { width: 15px; height: 15px; }
.editor-bar .btn { padding-inline: 9px; }

/* Stat tiles read like a professional dashboard: icon, number, whisper.
   Stated once, with the tile itself. */

/* The shelf poster's size and glow are stated once, with the rest of it.
   `.welcome-head` was the old greeting block; nothing renders it. */

/* ============================================================
   The quiet corner — the sidebar's bottom, uncluttered.
   `.side-duo` (a pair of buttons side by side) lived here and is
   not rendered anywhere.
   ============================================================ */
.studio-menu {
  display: grid; gap: var(--sp-1); padding: var(--sp-3);
  background: var(--bg-deep); border: 1px solid var(--border-soft);
  border-radius: var(--r); animation: fade var(--dur-1);
}
/* `display: grid` beats the browser's own [hidden] rule, so without this the
   drawer is open on every launch and the rail button that closes it does
   nothing at all: eight utility buttons hold 279px of the sidebar permanently,
   and a writer with ten projects sees two of them. The same trap is guarded on
   `.companion-pane` and `.ending`; this one was missed. */
.studio-menu .btn { justify-content: flex-start; text-align: left; }
#studio-menu-btn.open { border-color: var(--accent); color: var(--text); }

/* ============================================================
   Titles that never collide, whatever the size and screen.
   ============================================================ */
/* `.page-title` is stated once, with the rest of the page chrome. */

/* Room to breathe: the single cheapest way to stop feeling cramped.
   `.panel` used to restate its padding and its heading margin here, in
   exactly the values it already had. Both said nothing and both are gone;
   the one definition lives with the rest of the panel. */
@media (max-width: 1240px) { .view { padding: var(--sp-8) 24px 70px; } }

/* ============================================================
   Small screens fold the rails away — but never take the tools.
   The toggles bring either rail back as a drawer over the page.
   ============================================================ */
.rail-toggle { display: none; }
@media (max-width: 1240px) {
  .rail-toggle-rail { display: inline-flex; }
  .script-layout { position: relative; }
  .script-shell.show-rail .script-rail {
    display: block; position: absolute; right: 0; top: 0; bottom: 0;
    width: 280px; z-index: var(--z-drawer); overflow-y: auto;
    background: var(--bg); border-left: 1px solid var(--border-soft);
    box-shadow: -14px 0 34px rgb(0 0 0 / .3);
    padding: 18px 16px 40px;
  }
}
@media (max-width: 1000px) {
  .rail-toggle-desk { display: inline-flex; }
  .script-shell.show-desk .desk-rail {
    display: block; position: absolute; left: 0; top: 0; bottom: 0;
    width: 280px; z-index: var(--z-drawer); overflow-y: auto;
    background: var(--bg); border-right: 1px solid var(--border-soft);
    box-shadow: 14px 0 34px rgb(0 0 0 / .3);
    padding: 18px 16px 40px;
  }
}

/* ============================================================
   The living world — a god-sim window onto the writer's realm.
   ============================================================ */
.world-alive {
  position: relative; height: 300px; margin-bottom: 16px;
  border-radius: var(--r-lg); overflow: hidden;
  border: 1px solid var(--border-soft);
  box-shadow: var(--shadow-2);
}
.world-alive .livingworld-canvas {
  width: 100%; height: 100%; display: block;
  image-rendering: pixelated;
}
.livingworld-overlay { position: absolute; inset: 0; pointer-events: none; }
.lw-banner {
  position: absolute; transform: translate(-50%, -100%);
  pointer-events: auto; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-1);
  background: none; border: 0; padding: var(--sp-1); font: inherit;
}
.lw-flag {
  font-size: 15px; line-height: 1;
  filter: drop-shadow(0 2px 3px rgb(0 0 0 / .6));
  transition: transform var(--dur-1) var(--ease);
}
.lw-banner:hover .lw-flag { transform: scale(1.35); }
.lw-name {
  font-size: 10px; font-weight: 700; letter-spacing: var(--track-label); color: #fff;
  text-shadow: 0 1px 3px rgb(0 0 0 / .9), 0 0 8px rgb(0 0 0 / .6);
  background: rgb(0 0 0 / .35); padding: 1px 6px; border-radius: var(--r-pill);
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.lw-banner:hover .lw-name { opacity: 1; }
.world-alive-hint {
  position: absolute; right: 10px; bottom: 8px; z-index: var(--z-local);
  font-size: var(--fz-micro); letter-spacing: var(--track-label); text-transform: uppercase;
  color: rgb(255 255 255 / .75); text-shadow: 0 1px 3px rgb(0 0 0 / .8);
  pointer-events: none;
}
/* As the app's backdrop, it fills the window and dims behind the panels. */
.scene .livingworld-host { position: absolute; inset: 0; }
.scene .livingworld-host .livingworld-canvas {
  width: 100%; height: 100%; opacity: .5; image-rendering: pixelated;
}

/* ============================================================
   The category tree — folders that filter the world.
   ============================================================ */
/* The world's own filters. Scoped to `data-folders`, which only this bar
   carries, so it stops standing on the importer's breadcrumbs. */
.folder-bar[data-folders] {
  display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-6);
  align-items: center;
  padding: 0; background: none; border: 0; border-radius: 0;
}
.folder-bar[data-folders]:empty { display: none; }
.folder-bar[data-folders] .folder-chip {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) 11px; border-radius: var(--r-pill); cursor: pointer;
  background: var(--surface); border: 1px solid var(--border-soft);
  color: var(--text-2); font: inherit; font-size: var(--fz-small);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}
.folder-bar[data-folders] .folder-chip:hover { border-color: var(--border); color: var(--text); }
.folder-bar[data-folders] .folder-chip.on {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-color: var(--accent); color: var(--text);
}
.folder-bar[data-folders] .folder-chip.ghost { border-style: dashed; color: var(--muted); }

/* ============================================================
   History on a spine — eras divide the ages, events alternate.
   ============================================================ */
.world-history.spine {
  position: relative; display: flex; flex-direction: column; gap: var(--sp-5);
  padding: 8px 0 18px;
}
.world-history.spine::before {
  content: ""; position: absolute; left: 50%; top: 0; bottom: 0; width: 2px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 55%, transparent),
    color-mix(in srgb, var(--accent) 12%, transparent));
}
.spine-card {
  position: relative; width: calc(50% - 30px); z-index: var(--z-base);
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--r); padding: 9px 12px;
}
.spine-card.left { margin-right: auto; }
.spine-card.right { margin-left: auto; }
.spine-card::after {
  content: ""; position: absolute; top: 16px; width: 10px; height: 10px;
  border-radius: var(--r-circle); background: var(--accent);
  box-shadow: var(--ring-tint);
}
.spine-card.left::after { right: -36px; }
.spine-card.right::after { left: -36px; }
.era-divider {
  position: relative; z-index: var(--z-base); text-align: center; margin: 8px 0 2px;
  padding: 7px 16px; align-self: center; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--accent) 14%, var(--bg-deep));
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
}
.era-divider b {
  font-size: var(--fz-small); letter-spacing: var(--track); text-transform: uppercase;
}
.era-divider span { display: block; font-size: var(--fz-micro); color: var(--muted); }
.mini-history { display: flex; flex-direction: column; gap: var(--sp-2); }
.mini-era {
  display: flex; gap: var(--sp-5); align-items: baseline; padding: 4px 6px;
  border-left: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: 0 var(--r-sm) var(--r-sm) 0; cursor: pointer;
}
.mini-era:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.mini-era span { font-size: var(--fz-micro); color: var(--muted); min-width: 64px; }
.mini-era b { font-size: var(--fz-small); }

/* ============================================================
   Webs and trees — people and powers, drawn.
   ============================================================ */
/*
 * `min-height`, or the family tree draws nothing at all. Ever.
 *
 * `flex: 1` is `flex-basis: 0`, and the host these sit in has an auto height,
 * so with nothing else to go on the box resolves to zero. `overflow: auto`
 * then clips the contents rather than growing, and the page is blank.
 *
 * The web of the world escaped it only by accident: `worldweb.js` writes
 * `class="map-stage worldweb-stage"`, and `.map-stage` (line 5537) happens to
 * state a min-height. `familytree.js` writes `class="tree-stage"` on its own
 * and got none - so a character with family measured five nodes, positioned,
 * visible, inside a box 1152 wide and 0 tall. The empty state is the only
 * thing that page has ever shown a writer.
 *
 * Stated here rather than by adding `.map-stage` to the markup, because the
 * two are not the same component - the map pans and hides its overflow, this
 * one scrolls - and borrowing a class for one declaration is how the pair
 * drifted apart in the first place.
 */
.worldweb-stage, .tree-stage {
  position: relative; flex: 1; min-height: 380px;
  overflow: auto; background: var(--bg-deep);
  /*
   * The belt to `preventDefault` in worldweb.js. That stops a drag STARTING a
   * selection; this stops one existing at all, which also covers a
   * double-click - the gesture that selects a word wherever it lands, and on a
   * board of names lands on a name every time.
   *
   * The family tree gets it for the same reason: same board, same hands.
   */
  user-select: none; -webkit-user-select: none;
}
.worldweb-lines { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.worldweb-lines line, .worldweb-lines path {
  stroke: color-mix(in srgb, var(--accent) 38%, var(--border));
  stroke-width: 1.5; fill: none;
}
.worldweb-lines line.tree-couple {
  stroke: color-mix(in srgb, var(--accent) 70%, transparent); stroke-width: 3;
}
.worldweb-lines text {
  fill: var(--muted); font-size: 10px; letter-spacing: var(--track-label);
  paint-order: stroke; stroke: var(--bg-deep); stroke-width: 3px;
}
.worldweb-node {
  position: absolute; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  background: none; border: 0; padding: var(--sp-2); cursor: grab;
  font: inherit; color: var(--text); touch-action: none;
}
.worldweb-face {
  position: relative;
  width: 46px; height: 46px; border-radius: var(--r-circle); overflow: hidden;
  display: flex; align-items: center; justify-content: center; font-size: var(--glyph-lg);
  background: var(--surface); border: 2px solid var(--border);
  box-shadow: var(--shadow-2);
}
.worldweb-face img { width: 100%; height: 100%; object-fit: cover; }
.worldweb-node:hover .worldweb-face { border-color: var(--accent); }
.worldweb-name {
  font-size: var(--fz-tiny); font-weight: 600; max-width: 120px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  text-shadow: 0 1px 4px var(--bg-deep);
}
.tree-node.root .worldweb-face { border-color: var(--accent); box-shadow: var(--ring-tint); }

/* ---- connecting two things, from the board itself ----------------------
 *
 * "It's a whole mess and should be like the character web... And I cant connect
 * them." The only place a link could be made was an entry's own page, so the
 * one screen showing the whole shape of a world was the one screen that could
 * not change it.
 *
 * The ring sits on the rim of the face. It is quiet until the node is under the
 * pointer, because a board of forty nodes each wearing a permanent handle is a
 * board of eighty things. */
.worldweb-tie {
  position: absolute; top: 2px; right: 2px; z-index: var(--z-local);
  width: 15px; height: 15px; border-radius: var(--r-circle);
  border: 2px solid var(--page-bg, #fff); background: var(--accent);
  cursor: crosshair; touch-action: none;
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.worldweb-node:hover .worldweb-tie,
.worldweb-tie:focus-visible { opacity: 1; }
.worldweb-live {
  stroke: var(--accent); stroke-width: 2; stroke-dasharray: 6 5;
  pointer-events: none;
}
/* Not joined to anything yet. Present, because a board you can draw on has to
   show you what there is to draw; quiet, because a world of forty strangers is
   not a web and should not look like one. */
.worldweb-node.loose { opacity: .55; }
.worldweb-node.loose:hover { opacity: 1; }
.worldweb-node.loose .worldweb-face { border-style: dashed; }

.worldweb-stage.tying .worldweb-node { cursor: crosshair; }
.worldweb-node.would-tie .worldweb-face {
  border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* ============================================================
   Corkboards — a wall for thinking out loud.
   ============================================================ */
.cork-stage {
  position: relative; flex: 1; overflow: auto;
  background:
    radial-gradient(circle at 20% 30%, rgb(255 255 255 / .03), transparent 50%),
    color-mix(in srgb, #7a5b3a 22%, var(--bg-deep));
}
/*
 * DOUBLE-CLICKING THE BOARD NO LONGER SELECTS THE WHOLE BOARD.
 *
 * A double click is how a note is added here, and a double click on anything
 * that is not text is also how a browser selects everything around it. So the
 * one gesture the corkboard is built on turned every note on the wall blue at
 * the same time. Logan: "It shouldn't go blue and highlight everything when I
 * double click, I have to double click to add one and when it highlights them
 * all it wrecks it and throws it off for me."
 *
 * The wall refuses selection; the note TEXT takes it back, because a writer
 * still has to be able to select the words they wrote on a card.
 */
.cork-wall {
  position: relative; width: 2600px; height: 1600px;
  user-select: none; -webkit-user-select: none;
}
.cork-note-text { user-select: text; -webkit-user-select: text; }
.cork-pin {
  position: absolute; cursor: grab; touch-action: none;
  transform: rotate(var(--tilt, 0deg));
  box-shadow: 0 6px 16px rgb(0 0 0 / .35);
  border-radius: var(--r-xs);
}
.cork-pin::before {
  content: ""; position: absolute; top: -6px; left: 50%; transform: translateX(-50%);
  width: 11px; height: 11px; border-radius: var(--r-circle); z-index: var(--z-local);
  background: radial-gradient(circle at 35% 30%, #ff8f8f, #b02a2a 70%);
  box-shadow: 0 2px 3px rgb(0 0 0 / .45);
}
.cork-pin.lifted { z-index: var(--z-sticky); box-shadow: 0 14px 30px rgb(0 0 0 / .5); }

/* ---- the red string ----------------------------------------------------
 *
 * "I should be able to string all the red string to whatever pin I want, right
 * now I cant at all, the whole point of this board is to string things
 * together." There was none.
 */

/* Under the pins, where string on a wall is. */
.cork-string { position: absolute; left: 0; top: 0; overflow: visible; z-index: 0; }
.cork-pins { position: relative; z-index: 1; }

.cork-thread {
  fill: none; stroke: #c0392b; stroke-width: 2.5; stroke-linecap: round;
  filter: drop-shadow(0 2px 2px rgb(0 0 0 / .45));
}
.cork-thread.live { stroke-dasharray: 7 5; opacity: .85; }
/* A 2.5px line is impossible to hit. This one is fourteen wide and invisible,
   and it is the one that takes the click that cuts the string. */
.cork-thread-grab {
  fill: none; stroke: transparent; stroke-width: 14; cursor: pointer;
  pointer-events: stroke;
}
.cork-thread-grab:hover + .cork-thread,
.cork-thread-grab:hover { stroke: transparent; }
.cork-thread-grab:hover ~ .cork-thread { stroke: #e74c3c; }

/* The nub IS the drawing pin: it sits exactly over the head the card already
   draws, so there is nothing new on the board to learn - the thing that looks
   like a pin is the thing you pull string from. */
.cork-nub {
  position: absolute; top: -8px; left: 50%; transform: translateX(-50%);
  width: 15px; height: 15px; border-radius: var(--r-circle);
  border: 0; padding: 0; background: none; cursor: crosshair;
  z-index: var(--z-local); touch-action: none;
}
.cork-nub:hover { box-shadow: 0 0 0 3px rgb(192 57 43 / .45); }
.cork-nub:focus-visible { box-shadow: var(--ring-focus); }

/* While a string is being pulled, the pin it would land on says so. */
.cork-wall.tying .cork-pin { cursor: crosshair; }
.cork-pin.would-tie { outline: 2px solid #e74c3c; outline-offset: 3px; }
.cork-note { background: var(--paper, #f7e08b); color: #2c2617; padding: 18px 13px 13px; min-height: 90px; }
.cork-note-text {
  font-family: var(--font-read); font-size: var(--fz-body); line-height: 1.5;
  outline: none; min-height: 60px; white-space: pre-wrap; word-break: break-word;
}
.cork-note-text:empty::before { content: "Write on me."; color: rgb(0 0 0 / .35); }
.cork-photo { background: #fff; padding: 8px 8px 22px; }
.cork-photo img { width: 100%; display: block; }
.cork-card { background: var(--surface); border: 1px solid var(--border); overflow: hidden; }
.cork-card img { width: 100%; height: 90px; object-fit: cover; display: block; }
.cork-card-body { padding: 8px 11px; display: flex; flex-direction: column; gap: var(--sp-1); }
.cork-card-body b { font-size: var(--fz-small); }
.cork-card-body span { font-size: var(--fz-micro); color: var(--muted); }
.cork-x {
  position: absolute; top: 3px; right: 3px; z-index: var(--z-local-2);
  width: 20px; height: 20px; border-radius: var(--r-circle); border: 0; cursor: pointer;
  background: rgb(0 0 0 / .35); color: #fff; line-height: 1;
  display: grid; place-items: center;
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.cork-x .ic { width: 13px; height: 13px; vertical-align: 0; }
.cork-pin:hover .cork-x { opacity: 1; }

/* ============================================================
   The world on the writing desk.
   ============================================================ */
.desk-world-q { width: 100%; margin-bottom: 8px; }
.desk-lore { border-bottom: 1px solid var(--border-soft); }
.desk-lore-head {
  display: flex; align-items: center; gap: var(--sp-4); width: 100%;
  background: none; border: 0; padding: 6px 2px; cursor: pointer;
  font: inherit; color: var(--text); text-align: left;
}
.desk-lore-head:hover .desk-lore-name { color: var(--accent); }
.desk-lore-face {
  width: 26px; height: 26px; border-radius: var(--r-sm); overflow: hidden; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--raised); font-size: var(--glyph);
}
.desk-lore-face img { width: 100%; height: 100%; object-fit: cover; }
.desk-lore-name {
  font-size: var(--fz-small); font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.desk-lore-body { padding: 2px 4px 10px 36px; display: flex; flex-direction: column; gap: var(--sp-3); }
.desk-lore-body em { font-size: var(--fz-tiny); color: var(--text-3); font-style: italic; }
.desk-fact { font-size: var(--fz-tiny); color: var(--text-2); line-height: 1.45; }
.desk-fact b {
  display: inline-block; margin-right: 5px; color: var(--faint);
  font-size: var(--fz-micro); text-transform: uppercase; letter-spacing: var(--track-label);
}

/* ============================================================
   The signature — small consistent touches that make the app
   read as made, not assembled. All of it keys off the theme's
   own accent, so every theme keeps its own character.
   ============================================================ */

/* The page title and its accent underline are stated once, with the page
   chrome. `.welcome-head` — the second home page's greeting block — used to
   restate both here; nothing renders it.
 *
 * `.daily-quote` used to be restated here for a third time, in Georgia, with
 * a second opening quote mark. It lives in exactly one place now — search for
 * "the quote of the day". */

/* Section heads carry a quiet accent tick. */
.section-head h2 { position: relative; padding-left: 13px; }
.section-head h2::before {
  content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 4px; height: 1em; border-radius: var(--r-2xs);
  background: linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 30%, transparent));
}

/* The panel's lit top edge is folded into the one definition of `.panel`,
   with the rest of it. The primary button's glow and lift likewise live with
   the buttons. Both used to be restated here. */

/* Cards answer the cursor with light, not just movement. */
.map-card:hover, .theme-card:hover {
  box-shadow: var(--glow-lift);
}

/*
 * Declutter: one bar at the bottom of the editor, not two. While the room
 * is up, the status bar already says everything; the Tab/Enter school note
 * only returns on the bare page, where there is nothing else to say it.
 */
.script-shell:not(.room-off) .script-foot { display: none; }

/* The home page with the to-do corner put away spreads out. */
.split.solo { grid-template-columns: 1fr; }

/* ============================================================
   @mentions — a name in the writing that is also a door.
   ============================================================ */
a.mention {
  display: inline; padding: 1px 6px 2px; border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  color: color-mix(in srgb, var(--accent) 80%, var(--text));
  text-decoration: none; font-weight: 600; white-space: nowrap;
  cursor: pointer;
}
a.mention:hover {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-color: var(--accent);
}
[contenteditable="true"] a.mention { cursor: text; }

.mention-pop {
  position: fixed; z-index: var(--z-pop); min-width: 220px; max-width: 300px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r); box-shadow: var(--shadow-3);
  padding: var(--sp-2); animation: fade .12s;
  display: flex; flex-direction: column;
}
.mention-item {
  display: flex; align-items: center; gap: var(--sp-4); text-align: left;
  padding: 6px 9px; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--text); font: inherit;
  font-size: var(--fz-small); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mention-item > span:first-child {
  display: grid; place-items: center; width: 18px; flex: none; color: var(--faint);
}
.mention-item .ic { width: 15px; height: 15px; vertical-align: 0; }
.mention-item.on, .mention-item:hover {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

/* ============================================================
   World articles — wide body, quick facts down the side, the
   way an encyclopedia page carries itself.
   ============================================================ */
.article-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) 310px;
  gap: var(--sp-7); align-items: start;
}
.article-main { grid-column: 1; grid-row: 1; min-width: 0; }
.article-side { grid-column: 2; grid-row: 1; min-width: 0; }
.article-kind { width: 100%; }
/* In the sidebar the questions stack single file, like a stat column. */
.article-side .dossier-grid { grid-template-columns: 1fr; }
.article-body { min-height: 260px; font-size: var(--fz-body); line-height: 1.75; }
@media (max-width: 1150px) {
  .article-grid { grid-template-columns: 1fr; }
  .article-main, .article-side { grid-column: 1; grid-row: auto; }
}

/* ============================================================
   The atlas — a picture of somewhere, with the lore pinned on.
   ============================================================ */
/*
 * `height: 100%` needs an ancestor that has one, and this has never had it -
 * the chain above resolves to auto, so the whole column collapsed to its
 * content and the stage inside it got nothing. `min-height` off the viewport
 * asks nobody's permission. These pages sit at the top of the writing area
 * with no bar above them, so a viewport height is the height.
 */
.map-view {
  display: flex; flex-direction: column; height: 100%; min-height: 100vh;
  padding: 0; max-width: none; margin-inline: 0;
}
.map-bar {
  display: flex; align-items: center; gap: var(--sp-5); flex-wrap: wrap;
  padding: 10px 18px; border-bottom: 1px solid var(--border-soft);
  background: color-mix(in srgb, var(--bg) 94%, transparent);
}
.map-title { margin: 0; font-size: var(--fz-lead); }
.map-hint { font-size: var(--fz-tiny); }
.map-stage {
  position: relative; flex: 1; overflow: hidden; min-height: 380px;
  background:
    radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--accent) 6%, transparent), transparent 60%),
    var(--bg-deep);
  cursor: grab; touch-action: none;
}
.map-stage.grabbing { cursor: grabbing; }
.map-canvas { position: absolute; top: 0; left: 0; transform-origin: 0 0; }
.map-img { display: block; user-select: none; border-radius: var(--r-xs); box-shadow: var(--shadow-3); }
.map-pins { position: absolute; inset: 0; }

.map-pin {
  position: absolute; transform: translate(-50%, -92%);
  display: flex; flex-direction: column; align-items: center; gap: 0;
  background: none; border: 0; padding: var(--sp-1); cursor: pointer;
  font: inherit; color: inherit;
}
.map-pin-icon {
  font-size: 22px; line-height: 1;
  filter: drop-shadow(0 2px 3px rgb(0 0 0 / .5));
  transition: transform var(--dur-1) var(--ease);
}
.map-pin:hover .map-pin-icon { transform: scale(1.3); }
.map-pin-label {
  font-size: 11px; font-weight: 600; letter-spacing: var(--track-fine); white-space: nowrap;
  color: #fff; text-shadow: 0 1px 3px rgb(0 0 0 / .85), 0 0 6px rgb(0 0 0 / .6);
  max-width: 160px; overflow: hidden; text-overflow: ellipsis;
}

.map-empty {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: var(--sp-4); text-align: center;
  padding: var(--sp-9); cursor: default;
}
.map-empty .big { font-size: var(--fz-display); }
.map-empty p { max-width: 380px; color: var(--muted); font-size: var(--fz-small); line-height: 1.6; }

.pin-card {
  position: absolute; z-index: var(--z-float); width: 260px; pointer-events: none;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r); box-shadow: var(--shadow-3); overflow: hidden;
  animation: fade .15s;
}
.pin-card img { width: 100%; height: 110px; object-fit: cover; display: block; }
.pin-card-body { padding: 9px 11px; display: flex; flex-direction: column; gap: var(--sp-2); }
.pin-card-body b { font-size: var(--fz-ui); }
.pin-card-body span { font-size: var(--fz-tiny); color: var(--accent); }
.pin-card-body p { margin: 2px 0 0; font-size: var(--fz-tiny); color: var(--muted); line-height: 1.5; }

/* The map shelf on the world tab. */
.map-strip { display: flex; gap: var(--sp-5); flex-wrap: wrap; margin-bottom: 14px; }
.map-strip:empty { display: none; }
.map-card {
  width: 190px; text-align: left; cursor: pointer; padding: 0;
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--r); overflow: hidden; font: inherit; color: var(--text);
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.map-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.map-card img { width: 100%; height: 96px; object-fit: cover; display: block; }
.map-card-blank {
  display: flex; align-items: center; justify-content: center;
  height: 96px; font-size: var(--fz-display); background: var(--bg-deep);
}
.map-card-name {
  display: block; padding: 7px 10px 1px; font-weight: 600;
  font-size: var(--fz-small);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.map-card-pins { display: block; padding: 0 10px 8px; font-size: var(--fz-micro); color: var(--muted); }

/* 🧰 What you see, and the first-open welcome. */
.tools-pick h4 {
  margin: 12px 0 4px; font-size: var(--fz-tiny);
  text-transform: uppercase; letter-spacing: var(--track-label); color: var(--text-3);
}
.tools-pick h4:first-child { margin-top: 0; }
.tool-row {
  display: flex; align-items: center; gap: var(--sp-5); padding: 5px 2px;
  font-size: var(--fz-ui); cursor: pointer; border-radius: var(--r-sm);
}
.tool-row:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
.tour-h { margin: 0 0 10px; font-size: var(--fz-lead); }
.tour-note { margin: 12px 0 0; font-size: var(--fz-tiny); color: var(--text-3); line-height: 1.55; }
.tour-themes { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--sp-4); }
.tour-themes .theme-card { padding: var(--sp-4); gap: var(--sp-3); }
.tour-themes .theme-swatch { height: 34px; }
.tour-count { font-size: var(--fz-tiny); align-self: center; }

.theme-card {
  display: flex; flex-direction: column; gap: var(--sp-4); text-align: left;
  padding: var(--sp-5); border-radius: var(--r);
  background: var(--surface); border: 1.5px solid var(--border-soft);
  cursor: pointer; font: inherit; color: var(--text);
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.theme-card:hover { border-color: var(--border); transform: translateY(-1px); }
.theme-card.on { border-color: var(--accent); }

.theme-swatch {
  height: 46px; border-radius: var(--r-sm);
  border: 1px solid rgb(255 255 255 / .08);
}

.theme-card b { font-size: var(--fz-ui); font-weight: 600; }
.theme-card span { font-size: var(--fz-tiny); color: var(--muted); line-height: 1.45; }
.theme-card em {
  font-style: normal; font-size: var(--fz-micro);
  text-transform: uppercase; letter-spacing: var(--track-label); color: var(--faint);
}

.theme-swatch-make {
  display: grid; place-items: center;
  background: var(--raised); border-style: dashed; border-color: var(--border);
  color: var(--muted); font-size: var(--fz-stat);
}
.make-own:hover .theme-swatch-make { color: var(--accent); border-color: var(--accent); }

/* ---- building a theme of your own ------------------------------------- */

.tint { display: flex; flex-direction: column; gap: var(--sp-7); }

.tint-top { display: grid; grid-template-columns: 132px 1fr; gap: var(--sp-7); align-items: start; }
.tint-shot {
  width: 132px; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--r); border: 1px solid var(--border);
  background: var(--raised);
}
.tint-shot.empty {
  display: grid; place-items: center; border-style: dashed;
  color: var(--faint); font-size: var(--fz-tiny);
}
.tint-intro { display: flex; flex-direction: column; gap: var(--sp-4); }
.tint-intro p { margin: 0; }

/* The colours the picture is actually made of, biggest share first. */
.tint-palette { display: flex; gap: var(--sp-3); margin-top: 2px; }
.tint-chip {
  width: 26px; height: 26px; border-radius: var(--r-sm);
  border: 1px solid rgb(255 255 255 / .12);
}

.tint-warn {
  padding: 9px 12px; border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 34%, transparent);
}
.tint-warn p { margin: 0; font-size: var(--fz-small); color: var(--text-2); line-height: 1.5; }
.tint-warn p + p { margin-top: 4px; }

.tint-list { display: flex; flex-direction: column; gap: var(--sp-2); }

.tint-row {
  display: grid; grid-template-columns: 34px 1fr 86px; gap: var(--sp-5); align-items: center;
  padding: 5px 7px; border-radius: var(--r-sm); cursor: pointer;
}
.tint-row:hover { background: var(--raised); }

/*
 * The colour wheel is the browser's own, hidden behind the swatch.
 *
 * Its native appearance is a small grey button with a stripe in it, which reads
 * as a control rather than as a colour. Making the swatch the whole target and
 * the input invisible on top of it means the thing you click is the colour.
 */
.tint-swatch {
  position: relative; width: 34px; height: 28px;
  border-radius: var(--r-sm); border: 1px solid var(--border);
  overflow: hidden;
}
.tint-swatch input[type="color"] {
  position: absolute; inset: -4px; width: calc(100% + 8px); height: calc(100% + 8px);
  border: 0; padding: 0; background: none; cursor: pointer; opacity: 0;
}

.tint-words { min-width: 0; }
.tint-words b { display: block; font-size: var(--fz-small); font-weight: 600; }
.tint-words em {
  font-style: normal; display: block; color: var(--muted);
  font-size: var(--fz-tiny); line-height: 1.35;
}

.tint-code {
  font-family: var(--font-mono); font-size: var(--fz-tiny);
  padding: 5px 7px; border-radius: var(--r-sm); text-align: center;
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
}
.tint-code:focus { outline: none; border-color: var(--accent); }
.tint-code.wrong { border-color: var(--danger); }

.tint-more {
  align-self: flex-start; background: none; border: 0; padding: 2px 0;
  color: var(--accent); cursor: pointer; font: inherit;
  font-size: var(--fz-small); text-decoration: underline;
}

@media (max-width: 620px) {
  .tint-top { grid-template-columns: 1fr; }
  .tint-shot { width: 100%; }
  .tint-row { grid-template-columns: 34px 1fr; }
  .tint-code { grid-column: 2; justify-self: start; width: 86px; }
}

/* ============================================================
   Liquid glass.

   Lifted from the writer's own site (css/apple.css) and adapted: the aurora
   blooms, the flowing rainbow gradient, the frosted panels with a hairline
   edge. The palette is Apple's light greys so the glass has something bright
   to sit on.

   One deliberate restraint: the rainbow flow is on primary buttons only. Every
   button in the app pulsing through a rainbow would be exhausting inside five
   minutes, and this is a place people work for hours.
   ============================================================ */

/* Glass: the same ladder, in cool light. It had the same two faults as
   Light — surface and raised both pure white, then a reversed direction —
   and it is fixed the same way, so a hover brightens here too. The frosted
   panels state their own translucent white further down; this ladder is what
   everything else in the theme stands on. */
html[data-theme="glass"] {
  --bg:        #e8ebf4;
  --bg-deep:   #dde1ee;
  --surface:   #f7f9fe;
  --raised:    #feffff;
  --raised-2:  #ffffff;
  --border:    #ccd2e3;
  --border-soft: #e0e4ef;
  --text:      #14161d;
  --text-2:    #3d4250;
  --text-3:    #616673;
  --muted:     #585e6f;
  --faint:     #646976;
  --accent:    #2f5fff;
  --accent-ink:#ffffff;
  /* The second stop of its own rainbow. */
  --accent-2:  #9b50ff;
  --accent-soft: #2f5fff14;
  --film:      #b35309;
  --tv:        #0a7d76;
  --danger:    #d70015;
  --danger-ink:#ffffff;
  --ok:        #248a3d;
  --page-bg:   #ffffff;
  --page-ink:  #14161d;
  /* The shape ladder is not stated here. Glass sets its own in `AIR.glass`,
     written onto <html>, which beats anything in this file - four numbers
     sat here saying something different and nothing ever read them. */
}

/* ---------- the backdrop ---------- */

.sc-sky-glass {
  background:
    radial-gradient(120% 90% at 15% 0%, #f7f9ff 0%, #e9edf8 45%, #dfe4f2 100%);
}

/*
 * The aurora. Three coloured blooms, heavily blurred, sliding slowly against
 * each other. This is what the frosted panels blur, and without it glass has
 * nothing to be glass about.
 */
.sc-aurora {
  position: absolute; inset: -20% -10% auto -10%; height: 85%;
  background:
    radial-gradient(50% 60% at 20% 30%, var(--sky-a, rgb(47 95 255 / .18)), transparent 70%),
    radial-gradient(45% 55% at 72% 20%, var(--sky-b, rgb(155 80 255 / .16)), transparent 70%),
    radial-gradient(40% 50% at 90% 60%, var(--sky-c, rgb(0 200 255 / .12)), transparent 70%),
    radial-gradient(50% 60% at 40% 70%, var(--sky-d, rgb(255 60 160 / .10)), transparent 70%);
  filter: blur(40px);
  opacity: .9;
  animation: sc-aurora-flow 24s ease-in-out infinite alternate;
}
@keyframes sc-aurora-flow {
  0%   { transform: translate3d(-4%, -2%, 0) scale(1.05); }
  50%  { transform: translate3d(3%, 3%, 0) scale(1.16); }
  100% { transform: translate3d(-2%, 1%, 0) scale(1.08); }
}

/* Softer and warmer for the light theme, or it looks like a bruise. */
.sc-aurora-soft {
  --sky-a: rgb(47 95 255 / .22);
  --sky-b: rgb(180 110 255 / .20);
  --sky-c: rgb(255 120 190 / .16);
  height: 105%;
  opacity: .7;
}

/* ---------- drifting stars ---------- */

/*
 * Twice the height of the window with the field mirrored top and bottom, so
 * sliding up by half lands on an identical sky and the loop is invisible.
 */
/*
 * One element per layer, every star a box-shadow dot — the website's trick.
 * The dots are twinned 1100px apart, so sliding up by exactly that lands on an
 * identical sky and the loop has no seam.
 */
.sc-field {
  position: absolute; top: 0; left: 0;
  width: 1px; height: 1px; border-radius: var(--r-circle); background: transparent;
}
.sc-field.sc-drift {
  animation-name: sc-skydrift;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes sc-skydrift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -1100px, 0); }
}
/* The third layer holds still and breathes as one, like the website's. */
.sc-field.sc-star-pulse { animation: sc-skybreathe 3.5s ease-in-out infinite alternate; }
@keyframes sc-skybreathe {
  from { opacity: .4; }
  to   { opacity: .95; }
}

/* ============================================================
   Glass surfaces.

   Only for the glass theme. A hairline edge, a bright inset highlight along
   the top, and a saturated blur of whatever is behind — which is the aurora.
   ============================================================ */

/*
 * `.modal-box` used to be in this list and does not exist — the component is
 * `.modal` — so the showcase theme's whole frosted-glass treatment missed
 * every dialog in the app. Dialogs are handled just below rather than here,
 * because a dialog sits over the scrim, not over the bright page.
 */
html[data-theme="glass"] .panel,
html[data-theme="glass"] .card,
html[data-theme="glass"] .empty,
html[data-theme="glass"] .editor-bar,
html[data-theme="glass"] .topbar {
  background: rgb(255 255 255 / .62);
  backdrop-filter: blur(22px) saturate(1.7);
  -webkit-backdrop-filter: blur(22px) saturate(1.7);
  border: 1px solid rgb(255 255 255 / .7);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / .9),
    0 1px 2px rgb(20 30 60 / .06),
    0 12px 40px -18px rgb(20 30 60 / .18);
}

/*
 * Dialogs, on glass.
 *
 * A frosted panel blurs whatever is behind it, and behind a dialog is the
 * scrim. Frosting a white panel over a 55%-black scrim gives grey, not glass,
 * so the scrim goes pale and cool here and the dialog frosts over that. This
 * is the one place the theme lightens the scrim; everywhere else it stays as
 * dark as the other six themes.
 */
html[data-theme="glass"] .modal-backdrop {
  background: rgb(228 231 240 / .62);
  backdrop-filter: blur(10px) saturate(1.3);
  -webkit-backdrop-filter: blur(10px) saturate(1.3);
}
html[data-theme="glass"] .modal,
html[data-theme="glass"] .palette {
  background: rgb(255 255 255 / .82);
  backdrop-filter: blur(26px) saturate(1.7);
  -webkit-backdrop-filter: blur(26px) saturate(1.7);
  border: 1px solid rgb(255 255 255 / .85);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / .95),
    0 30px 80px -24px rgb(20 30 60 / .34);
}
/* The foot is opaque in every theme; here it is a frosted tray instead, or a
   solid bar would cut the dialog in half. */
html[data-theme="glass"] .modal-foot {
  background: rgb(255 255 255 / .34);
  border-top-color: rgb(255 255 255 / .7);
}

html[data-theme="glass"] .sidebar {
  background: rgb(255 255 255 / .55);
  backdrop-filter: blur(26px) saturate(1.6);
  -webkit-backdrop-filter: blur(26px) saturate(1.6);
  border-right: 1px solid rgb(255 255 255 / .6);
}

html[data-theme="glass"] .main {
  background: transparent;
}

/* A card lifting off the glass. */
html[data-theme="glass"] .card:hover,
html[data-theme="glass"] .theme-card:hover {
  background: rgb(255 255 255 / .78);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / .95),
    0 20px 60px -22px rgb(20 30 60 / .26);
}

/*
 * The writing itself stays solid paper. Frosted glass under a screenplay would
 * be unreadable, and the page has to look like a page.
 */
/* `.beat-page` was in this list. It is not a page — it is the little
   page-number box on a beat, three characters wide. The pages are `.paper`
   and `.dp`. */
html[data-theme="glass"] .script-page,
html[data-theme="glass"] .paper,
html[data-theme="glass"] .deck-page {
  background: var(--page-bg);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* ============================================================
   The flowing rainbow.

   Their `prowFlow`: a wide multi-stop gradient slid sideways for ever. On the
   primary button only.
   ============================================================ */

html[data-theme="glass"] .btn-primary {
  background: linear-gradient(100deg,
      #2f5fff 0%, #9b50ff 22%, #ff3ca0 44%,
      #ff9d3c 62%, #00c8ff 82%, #2f5fff 100%);
  background-size: 300% 100%;
  background-position: 0% 50%;
  border-color: transparent;
  color: #fff;
  animation: sc-rainbow 22s linear infinite;
  box-shadow: 0 6px 20px -8px rgb(47 95 255 / .5);
}
/*
 * Hovering does not change the tempo.
 *
 * It used to drop the cycle from 22s to 12s, so the colours visibly accelerated
 * the moment the pointer arrived. That is the "jarring" half of the complaint:
 * a control that speeds up when you look at it. What a hover should say is "you
 * are on it" - a little more light, a little more lift - and it now says only
 * that.
 */
html[data-theme="glass"] .btn-primary:hover {
  box-shadow: 0 10px 30px -8px rgb(100 86 248 / .55);
  filter: brightness(1.06);
}
@keyframes sc-rainbow {
  from { background-position: 0% 50%; }
  to   { background-position: 300% 50%; }
}

/* Held still when movement is off, but keep the colours. */
html[data-theme="glass"] body.no-motion .btn-primary { animation: none; }

@media (prefers-reduced-motion: reduce) {
  html[data-theme="glass"] .btn-primary { animation: none; }
}

/* ============================================================
   The title page, on screen.

   A beta tester filled in the form and had no idea what came out. This puts the
   page beside the boxes at its real proportions, with every line where the PDF
   writer puts it.

   The sizing trick is the same one the script page uses: a page is 85
   characters across and a Courier character is 0.6 of the type size, so the
   font size is the page width over 51 and everything else can be measured in
   ems.
   ============================================================ */

/* The title page stands in the paper column in place of the script, at the size
   it prints at. It was a modal with the page shrunk into a corner of it. */
/* *** THE CONTAINER HAS TO BE THE PARENT, NOT THE PAGE ITSELF. ***

   `.tp-page` declared `container-type: inline-size` and then sized its own type
   with `100cqw`. An element is never its own query container, so those cqw units
   quietly resolved against the next container up - the whole paper column - and
   every margin on the page came out 23% too wide. It measured 1.85in where a
   screenplay's left margin is 1.5in. Nothing looked broken; it was just wrong. */
.title-sheet {
  width: min(var(--page-w, 8.5in), 100%);
  container-type: inline-size;
}

.ts-bar {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-bottom: var(--sp-3);
}

.tp-page {
  position: relative;
  aspect-ratio: 8.5 / 11;
  background: var(--page-bg); color: var(--page-ink);
  border: 1px solid var(--sheet-edge, var(--border));
  border-radius: 2px;
  box-shadow: 0 1px 1px rgb(0 0 0 / .22), 0 10px 22px -14px rgb(0 0 0 / .5);
  font-family: var(--font-mono);
  /* Courier sets ten characters to the inch, so an 8.5in page is 85 columns and
     a column is 0.6em: 51em across. Every offset below is in those ems. */
  font-size: calc(100cqw / 51);
  line-height: 1;
}

/* *** CENTRED BETWEEN THE MARGINS, NOT ON THE PAPER. ***

   The left margin is an inch and a half and the right is an inch, so the middle
   of the type sits at 4.5in and the middle of the sheet at 4.25in. This used to
   be `left: 0; right: 0`, with a comment claiming it matched pdf.js - and it did
   until titleSheet() was corrected, after which the screen showed every title a
   quarter inch left of where the PDF put it. The comment outlived the fact. */
.tp-line {
  position: absolute; left: 9em; right: 6em;
  text-align: center;
  white-space: nowrap;
}

/* The PDF sets the title in capitals, so the page does. The stored value keeps
   whatever casing was typed - this is how it prints, not what it is. */
.tp-title { font-weight: 400; text-transform: uppercase; }

/* Bottom left, at the 1.5in left margin. */
.tp-contact {
  position: absolute; left: 9em; right: 6em;
  white-space: pre-wrap; line-height: 1.2;
}

/* Bottom right, at the 1in right margin. */
.tp-date { position: absolute; right: 6em; left: 9em; text-align: right; white-space: nowrap; }

/*
 * Nothing typed here yet. The hint is the only thing that says a line exists at
 * all - an empty centred line on white paper is invisible, and the whole
 * complaint about this page was not being able to see where to type.
 *
 * THE HINT ON THE "BASED ON" LINE IS NOT A SUGGESTION. Logan: "the Based on the
 * novel by.. should be not there, not every movie is based off a novel." Right,
 * and most are based on nothing at all - so a hint naming a novel reads as the
 * app expecting one. It says "Based on..." now: still visible, still typeable,
 * and it no longer finishes the sentence for a writer whose film is original.
 */
.tp-page [data-tp] { outline: none; }
.tp-page [data-tp]:empty::before {
  content: attr(data-hint);
  color: color-mix(in srgb, var(--page-ink) 30%, transparent);
}
/* The line under the caret, so you can tell which one you are on. Drawn under
   the type rather than around it, because a box on a title page is not paper. */
.tp-page [data-tp]:focus {
  box-shadow: 0 1px 0 color-mix(in srgb, var(--accent) 70%, transparent);
}

/* ============================================================
   Getting the sidebar out of the way.

   Both asked for by a beta tester: it could not be hidden, and the split
   between the project list and the controls under it could not be moved.
   ============================================================ */

/*
 * Hidden. The column collapses to nothing rather than to an icon rail, because
 * the point is to get the whole width back for the writing.
 */
.app.sidebar-hidden { grid-template-columns: 56px 0 1fr; }
.app.sidebar-hidden .sidebar {
  overflow: hidden;
  border-right: none;
  /* Nothing inside can be tabbed to while it is not on screen. */
  visibility: hidden;
}

/* The way back in. Only there while the sidebar is not. */
.sidebar-reopen {
  position: fixed; top: 12px; left: 0; z-index: var(--z-float);
  display: none;
  width: 26px; height: 40px; padding: 0;
  border: 1px solid var(--border); border-left: none;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: var(--surface); color: var(--muted);
  font: inherit; font-size: var(--glyph-lg); line-height: 1; cursor: pointer;
  box-shadow: var(--shadow-1);
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.sidebar-reopen:hover { color: var(--text); background: var(--raised); }
body.sidebar-hidden .sidebar-reopen { display: none; }

/*
 * The movable split. A thin band with a short line in the middle, so it reads
 * as a handle rather than a border. Wider than it looks, because a 1px target
 * is a joke to hit.
 */
.sidebar-split {
  flex: none; height: 9px; margin: 2px 12px 0;
  cursor: row-resize;
  display: grid; place-items: center;
}
.sidebar-split::after {
  content: ""; width: 26px; height: 2px; border-radius: var(--r-2xs);
  background: var(--border);
  transition: background var(--dur-1) var(--ease), width var(--dur-1) var(--ease);
}
.sidebar-split:hover::after { background: var(--faint); width: 40px; }
body.splitting .sidebar-split::after { background: var(--accent); width: 40px; }

/* While dragging, nothing should select or flicker a text cursor. */
body.splitting { cursor: row-resize; user-select: none; }

/*
 * When a height has been chosen the list stops growing to fill and takes
 * exactly that, scrolling inside. Without a choice it behaves as before.
 */
.sidebar-nav[style*="--nav-h"] { flex: 0 0 var(--nav-h); }

/*
 * Once the list has a fixed height there is spare room in the column, and
 * without this it opens as a gap below "New project" — the controls float up
 * and leave the bottom of the sidebar empty, which just looks broken. Pinning
 * the last block to the bottom puts the spare room where it belongs: between
 * the list and the controls, which is the break the writer is moving.
 */

/* The written journey, with the button that turns it into moments. */
.arc-body-wrap { display: flex; flex-direction: column; gap: var(--sp-5); align-items: flex-start; }
.arc-body-wrap .arc-body { align-self: stretch; }

/* ---------- the deck look ---------- */

/*
 * Colours come from the deck's own look, set as variables on the shell, so the
 * page you are editing is the page that gets exported. Defaults are Clean, which
 * is what every deck made before looks existed was.
 */
.dp-muted, .dp .dp-caption, .dp [contenteditable]:empty::before { color: var(--dp-muted, #a2a7b5); }
.dp-rule, .dp-accent { background: var(--dp-accent, #8093ff); }
.dp-heading { color: var(--dp-ink, #1a1a1a); }

/* The typewriter look really is Courier, on screen as well as in the PDF. */
.deck-shell[data-mono="1"] .dp-heading,
.deck-shell[data-mono="1"] .dp-body,
.deck-shell[data-mono="1"] .dp-quote-text,
.deck-shell[data-mono="1"] .dp-cover-text { font-family: var(--font-mono); }

/* Picking one. Same card idea as the theme picker, because the only useful
   preview of a look is the look. */
.look-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: var(--sp-5); }

.look-card {
  padding: 0; overflow: hidden; cursor: pointer; font: inherit;
  border-radius: var(--r); border: 1.5px solid var(--border-soft);
  background: var(--surface); color: var(--text);
  text-align: left; transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.look-card:hover { border-color: var(--border); transform: translateY(-1px); }
.look-card.on { border-color: var(--accent); }

/* A miniature of the cover, drawn in the look's own colours. */
.look-demo {
  aspect-ratio: 11 / 8.5; display: grid; place-items: center; position: relative;
  border-bottom: 1px solid var(--border-soft);
}
.look-demo span {
  font-size: var(--fz-tiny); font-weight: 700; letter-spacing: var(--track-label);
  padding: 0 10px; text-align: center;
}
.look-demo i {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 30%; width: 34px; height: 2px; border-radius: var(--r-2xs);
}
.look-card b { display: block; padding: 9px 11px 0; font-size: var(--fz-ui); }
.look-card em {
  display: block; padding: 2px 11px 11px; font-style: normal;
  font-size: var(--fz-tiny); color: var(--muted); line-height: 1.45;
}

/* ============================================================
   The poster maker.

   The sheet on the right is drawn in HTML at the real proportions, so the
   arrangement matches the PDF without pretending to be typeset by it.
   ============================================================ */

.po-layout {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(220px, 300px);
  gap: var(--sp-8); align-items: start;
}
@media (max-width: 860px) {
  .po-layout { grid-template-columns: 1fr; }
  .po-preview { order: -1; }
}

.po-fields { display: flex; flex-direction: column; gap: var(--sp-1); }

.po-looks { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: 5px; }
.po-look {
  width: 34px; height: 34px; border-radius: var(--r-sm); cursor: pointer;
  border: 2px solid transparent; padding: 0;
  display: grid; place-items: end center;
}
.po-look i { display: block; width: 16px; height: 3px; border-radius: var(--r-2xs); margin-bottom: 5px; }
.po-look.on { outline: 2px solid var(--accent); outline-offset: 1px; }

.po-sheet {
  position: relative; overflow: hidden;
  border-radius: var(--r-xs); border: 1px solid var(--border);
  box-shadow: var(--shadow-3);
  display: flex; flex-direction: column; justify-content: center;
  container-type: inline-size;
  font-family: var(--font-ui);
}
.po-sheet[data-mono="1"] { font-family: var(--font-mono); }

.po-art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.po-scrim {
  position: absolute; inset: 0 0 48% 0;
  background: linear-gradient(to bottom, rgb(0 0 0 / .72), rgb(0 0 0 / .35));
}

.po-words {
  position: relative; z-index: var(--z-base); text-align: center;
  padding: 0 8cqw; margin-top: -6cqw;
  display: flex; flex-direction: column; align-items: center; gap: 2cqw;
}
.po-tag { font-size: 2.6cqw; letter-spacing: var(--track); text-transform: uppercase; opacity: .85; }
.po-title {
  font-size: 12cqw; line-height: .95; font-weight: 800;
  letter-spacing: var(--track-tight); text-transform: uppercase;
}
.po-rule { display: block; width: 22cqw; height: .9cqw; border-radius: var(--r-2xs); }
.po-log { font-size: 2.4cqw; line-height: 1.45; opacity: .9; max-width: 78%; }

.po-foot {
  position: absolute; left: 0; right: 0; bottom: 4cqw; z-index: var(--z-base);
  text-align: center; padding: 0 8cqw;
  display: flex; flex-direction: column; gap: 1.4cqw;
}
.po-foot b { font-size: 2cqw; letter-spacing: var(--track); text-transform: uppercase; }
.po-foot span {
  font-size: 1.5cqw; letter-spacing: var(--track-label); text-transform: uppercase;
  line-height: 1.5; opacity: .85;
}

/* ============================================================
   Out to market: who has the script, and what they said.
   ============================================================ */

.subs { display: flex; flex-direction: column; gap: var(--sp-3); }
.subs-count { color: var(--muted); font-size: var(--fz-small); margin: 0 0 4px; }

.sub-row {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) auto auto;
  gap: var(--sp-6); align-items: center;
  padding: 10px 12px; border-radius: var(--r-sm);
  background: var(--surface); border: 1px solid var(--border-soft);
}
.sub-row:hover { border-color: var(--border); }

.sub-who b { display: block; font-size: var(--fz-ui); }
.sub-who span { font-size: var(--fz-tiny); color: var(--muted); }

.sub-what { display: flex; flex-direction: column; font-size: var(--fz-small); }

/* Where it stands, colour-coded so a whole list reads at a glance. */
.sub-status {
  font-size: var(--fz-tiny); font-weight: 550;
  padding: 3px 10px; border-radius: var(--r-pill); white-space: nowrap;
  background: var(--raised); color: var(--muted);
}
.sub-status.good  { background: color-mix(in srgb, var(--tv) 18%, transparent); color: var(--tv); }
.sub-status.great { background: color-mix(in srgb, var(--ok) 20%, transparent); color: var(--ok); }
.sub-status.bad   { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); }

/* A note runs the full width underneath rather than squeezing the columns. */
.sub-notes {
  grid-column: 1 / -1; margin: 2px 0 0;
  font-size: var(--fz-small); color: var(--text-2);
  white-space: pre-wrap;
}

@media (max-width: 780px) {
  .sub-row { grid-template-columns: 1fr auto; }
  .sub-what { grid-column: 1 / -1; flex-direction: row; gap: var(--sp-5); }
}

/* ==========================================================================
   THE CAST — a character is introduced the way a place is
   A tavern had a full-bleed title card and the protagonist had a 56px circle.
   That asymmetry told the writer the props mattered more than the people.
   ========================================================================== */

.character-hero {
  position: relative; min-height: 300px; display: flex; align-items: flex-end;
  margin: 0 calc(clamp(22px, 3vw, 38px) * -1) 10px;
  overflow: hidden; border-bottom: 1px solid var(--border-soft);
  background:
    radial-gradient(90% 120% at 18% 0%, color-mix(in srgb, var(--character-accent) 22%, transparent), transparent 62%),
    var(--bg-deep);
}
.character-hero-art {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center 20%;
}

/*
 * A portrait, shown whole.
 *
 * `cover` on a 2:3 photograph in a 1100x300 band is a six-times enlargement of
 * whatever happens to be 20% down it. Set by the picture's own proportions in
 * character.js, so a landscape still fills the band and a full-body shot stands
 * beside the name instead of being cropped to a strip of it.
 */
.character-hero.tall-art { min-height: 380px; }
.character-hero.tall-art .character-hero-art {
  object-fit: contain; object-position: right center;
  width: 38%; left: auto; right: clamp(22px, 3vw, 38px);
  filter: drop-shadow(0 8px 26px rgb(0 0 0 / .35));
}
/* The veil exists to hold type off a busy photograph. With the photograph over
   to one side there is nothing to hold it off, and it only greys the page. */
.character-hero.tall-art .character-hero-veil { display: none; }
.character-hero.tall-art .character-hero-body { max-width: 58%; }
/* A forged portrait is 34 squares across. Blow it up with its edges intact,
   and sit it to one side so the type has room to breathe. */
.character-hero-forge {
  image-rendering: pixelated;
  object-fit: contain; object-position: right 8% center;
  width: 46%; left: auto; right: 0;
  filter: drop-shadow(0 6px 22px rgb(0 0 0 / .45));
}
.character-hero-veil {
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg, rgb(0 0 0 / .18), transparent 42%, color-mix(in srgb, var(--bg) 92%, transparent) 98%),
    linear-gradient(90deg, color-mix(in srgb, var(--bg-deep) 88%, transparent) 24%, transparent 70%);
}
.character-hero-body {
  position: relative; z-index: var(--z-base);
  padding: 0 clamp(22px, 3vw, 38px) 26px; max-width: var(--measure);
}
.character-name {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(30px, 3.6vw, 52px); line-height: 1.04;
  letter-spacing: var(--track-tight); margin: 6px 0 0;
  text-shadow: 0 2px 20px rgb(0 0 0 / .5);
}
.character-line {
  font-family: var(--font-read); font-style: italic;
  font-size: var(--fz-subhead); line-height: 1.5;
  color: var(--text-2); margin: 8px 0 0; max-width: var(--measure-line);
}
/* Roles as billing, not as database tags. */
.cast-billing { margin-top: 12px; display: flex; align-items: baseline; gap: var(--sp-5); flex-wrap: wrap; }
.cast-credit {
  font-size: var(--fz-micro); font-weight: 700;
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--character-accent, var(--accent));
}
.cast-also { font-family: var(--font-read); font-style: italic; font-size: var(--fz-ui); color: var(--muted); }
.cast-age { font-size: var(--fz-tiny); color: var(--muted); }
.character-hero-tools {
  margin-top: var(--sp-7); display: flex; gap: var(--sp-4);
  transition: opacity var(--dur-2) var(--ease);
}
.character-hero:not(:hover) .character-hero-tools { opacity: 0; }
.character-hero:focus-within .character-hero-tools { opacity: 1; }

/* Their colour, finally doing something, everywhere they appear. */
.view [style*="--character-accent"] .page-title::after,
.character-hero + * .section-head::before { background: var(--character-accent, var(--accent)); }

@media (max-width: 780px) {
  .character-hero { min-height: 220px; }
  .character-hero-forge { width: 40%; opacity: .55; }
  .character-hero-body { max-width: none; }
}

/* ==========================================================================
   THE SEVEN FAMILIES — a kingdom is not a currency
   Twenty-six kinds of thing used to share one template: a photo circle, a
   dropdown, and a stack of grey fields. Each family now arrives as itself.
   ========================================================================== */

.entry-hero {
  position: relative; min-height: 300px; display: flex; align-items: flex-end;
  margin: 0 calc(clamp(22px, 3vw, 38px) * -1) 10px;
  overflow: hidden; border-bottom: 1px solid var(--border-soft);
  background:
    radial-gradient(88% 118% at 20% 0%, color-mix(in srgb, var(--entry-accent, var(--accent)) 20%, transparent), transparent 62%),
    var(--bg-deep);
}
.entry-hero-art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 26%; }
/* Drawn art is a handful of squares across: blow it up with its edges intact
   and stand it to one side, so the type keeps the left of the frame. */
.entry-hero-forge {
  image-rendering: pixelated;
  object-fit: contain; object-position: right 7% center;
  width: 48%; left: auto; right: 0;
  filter: drop-shadow(0 8px 26px rgb(0 0 0 / .5));
}
.entry-hero-veil {
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg, rgb(0 0 0 / .16), transparent 44%, color-mix(in srgb, var(--bg) 92%, transparent) 98%),
    linear-gradient(90deg, color-mix(in srgb, var(--bg-deep) 90%, transparent) 26%, transparent 72%);
}
.entry-hero-body { position: relative; z-index: var(--z-base); padding: 0 clamp(22px, 3vw, 38px) 26px; max-width: var(--measure); }
.entry-name {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(28px, 3.4vw, 50px); line-height: 1.05;
  letter-spacing: var(--track-tight); margin: 6px 0 0;
  text-shadow: 0 2px 20px rgb(0 0 0 / .5);
}
.entry-line {
  font-family: var(--font-read); font-style: italic;
  font-size: var(--fz-subhead); line-height: 1.5;
  color: var(--text-2); margin: 8px 0 0; max-width: var(--measure-line);
}
/* The family is a whisper, not a badge: it tells you what sort of page this
   is without competing with the name of the thing itself. */
.entry-family {
  font-size: var(--fz-micro); font-weight: 600;
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--entry-accent, var(--accent));
}
.entry-hero-tools { margin-top: 16px; display: flex; gap: var(--sp-4); opacity: 0; transition: opacity var(--dur-2) var(--ease); }
.entry-hero:hover .entry-hero-tools,
.entry-hero:focus-within .entry-hero-tools { opacity: 1; }

/* The world index: every entry wears its own face, at a size you can read. */
.world-card {
  padding: 0; overflow: hidden; display: flex; flex-direction: column;
  border-color: color-mix(in srgb, var(--entry-accent, var(--border)) 34%, var(--border));
}
.world-card-art {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  background:
    radial-gradient(70% 100% at 50% 30%, color-mix(in srgb, var(--entry-accent, var(--accent)) 16%, transparent), transparent 70%),
    var(--bg-deep);
  display: grid; place-items: center;
}
.world-card-art img { width: 100%; height: 100%; object-fit: cover; }
.world-card-art img.forged {
  image-rendering: pixelated; object-fit: contain;
  width: auto; height: 82%; max-width: 86%;
  filter: drop-shadow(0 4px 12px rgb(0 0 0 / .45));
  transition: transform var(--dur-2) var(--ease);
}
.world-card:hover .world-card-art img.forged { transform: translateY(-2px) scale(1.05); }
.world-card-body { padding: 12px 14px 14px; }
.world-card-body .card-title { margin-bottom: 3px; }

/* Each family keeps a different light, so the index reads as a world with
   regions in it rather than a single grey grid. */
.family-grounds { --entry-accent: #4f8a5c; }
.family-powers  { --entry-accent: #a8483c; }
.family-peoples { --entry-accent: #4a7fae; }
.family-relics  { --entry-accent: #b08a34; }
.family-forces  { --entry-accent: #7a56a8; }
.family-epochs  { --entry-accent: #8a7a68; }
.family-customs { --entry-accent: #4d8f92; }
.world-card[style*="--entry-accent"] { border-color: color-mix(in srgb, var(--entry-accent) 40%, var(--border)); }

@media (max-width: 780px) {
  .entry-hero { min-height: 220px; }
  .entry-hero-forge { width: 42%; opacity: .6; }
  .entry-hero-body { max-width: none; }
}



/* ==========================================================================
   THE LADDER, THE INK, AND THE QUIET BUTTON
   Measured, not eyeballed. See the note on each rule.
   ========================================================================== */

/*
 * A light page cannot have a deep ladder: white is a ceiling, and four
 * near-white surfaces physically cannot sit 1.28:1 apart. The dark themes are
 * spaced at 1.30 and carry their depth in fill; the two light themes are
 * spaced at 1.13 and carry it in the border instead, which is what a good
 * light interface has always done. Stating it here so the next person does not
 * "fix" the light ladder by flattening the dark one to match.
 */
html[data-theme="light"] .card,
html[data-theme="light"] .panel,
html[data-theme="glass"] .card,
html[data-theme="glass"] .panel {
  border-color: color-mix(in srgb, var(--border) 88%, var(--text-3));
}
html[data-theme="light"] .card:hover,
html[data-theme="glass"] .card:hover {
  border-color: color-mix(in srgb, var(--border) 60%, var(--text-3));
}

/*
 * `.btn-quiet` was used at eight call sites and defined nowhere — including
 * both Redraw buttons, on the character hero and the world entry hero. They
 * rendered as unstyled inline text on top of artwork.
 *
 * It is the third weight of button: after the filled primary and the outlined
 * default comes the one that should be available without ever asking for
 * attention. It only resolves once you are already looking at it.
 */
.btn.btn-quiet {
  background: color-mix(in srgb, var(--surface) 62%, transparent);
  border-color: color-mix(in srgb, var(--border) 70%, transparent);
  color: var(--text-2);
  font-weight: 500;
  backdrop-filter: blur(6px);
}
.btn.btn-quiet:hover {
  background: color-mix(in srgb, var(--raised) 85%, transparent);
  border-color: var(--border);
  color: var(--text);
}
.btn.btn-quiet:active { background: var(--raised-2); }

/* ==========================================================================
   THE DRAWN DROPDOWN.

   A `<select>` is two controls and only one of them was ever ours. The closed
   box is drawn above; the list that fell out of it was drawn by Windows, in
   Segoe UI, square, white, with the system's own blue highlight — a white
   rectangle opening in the middle of a dark studio, and unreachable by CSS.

   `ui.js` keeps the real select, hides it, and stands a button and a list
   beside it. The button wears the select's own classes, so everything above
   that sizes and skins a dropdown — `.select`, `.select-sm`, `.el-picker`,
   `.sp-type`, `.kin-kind`, `.credit-status`, `.article-kind` — reaches it
   untouched. What is here is only the difference between a button and a
   select, and the list itself.
   ========================================================================== */

/* No box of its own: the button has to be the flex or grid item the select
   was, or every row it sits in would need rewriting. */
.drop { display: contents; }

/*
 * The real control: taken from the eye and from the accessibility tree, and
 * left in the form. Not `display: none` — that hides a thing we still want
 * submitted, and the day it stops being submitted nobody finds out until a
 * writer notices a field that never saves.
 *
 * `left`/`top` are stated because a call site may have positioned the select
 * itself (`.sp-type` sits at `left: -158px`), and the clipped remnant should
 * not go hunting for the edge of the window.
 */
.drop > select.drop-native {
  /* `appearance: none` matters: `.select` above asks for `base-select`, and a
     select in that mode carries a minimum size of its own that a stated width
     does not beat. It was leaving a 24px invisible box behind the button. */
  appearance: none;
  position: absolute;
  left: auto; top: auto;
  width: 1px; height: 1px;
  min-width: 0; min-height: 0;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
}

/* Zero specificity on purpose: this is only here to undo what a browser does
   to a bare button, and any class the call site copied across must win. */
:where(.drop-btn) {
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.drop-btn {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.drop-btn .drop-label { pointer-events: none; white-space: pre; }
.drop-btn.drop-unset { color: var(--faint); }
.drop-btn:disabled { opacity: .55; cursor: not-allowed; }

/*
 * `.select` above is handed to the engine's own picker inside an
 * `@supports (appearance: base-select)` block, which sets `display: flex` and
 * takes the chevron away. The drawn button wears that same class for its size
 * and its skin, so it has to say plainly that it is not that control. The
 * chevron is restated rather than shared because the rule it belongs to is
 * further up the file and this block only ever appends.
 */
/*
 * *** THE PADDING TRAVELS WITH THE CHEVRON, OR IT GETS LEFT BEHIND. ***
 *
 * Logan reported this twice: "the drop down arrow thing cuts into the words and
 * overlaps them", then "you didnt fix the overlaping button".
 *
 * The first fix put 34px of right padding on `.select-sm` - the rule for a
 * real `<select>`. But ui.js hides the real select at 1x1 and draws THIS button
 * over it, and the drawn button was measured at 10px of right padding while its
 * chevron occupies the last 15. So the words ran under the arrow, the fix was
 * real, and it was applied to a control nobody can see.
 *
 * The chevron is restated here rather than shared, and that is exactly how the
 * padding that clears it came to live somewhere else. So it is restated here
 * too, in the same block, where the next person to move one will see the other.
 */
.drop-btn.select {
  appearance: none;
  display: block;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 17px) calc(50% + 1px), calc(100% - 12px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  /* The arrow's own 5px sits 12 in, so it ends 17 from the edge. */
  padding-right: 32px;
}
.drop-btn.select.select-sm {
  background-position: calc(100% - 13px) 50%, calc(100% - 9px) 50%;
  /* Same sum at the small size: the arrow ends 13 in, plus a gap the width of
     a space so the last letter is not touching it. */
  padding-right: 26px;
}
.drop-btn.select:focus-visible,
.drop-btn.select[aria-expanded="true"] {
  background-image:
    linear-gradient(45deg, transparent 50%, var(--accent) 50%),
    linear-gradient(135deg, var(--accent) 50%, transparent 50%);
}

/* Open reads as engaged, in the same ring the whole app uses for it. */
.drop-btn[aria-expanded="true"] {
  border-color: var(--accent);
  box-shadow: var(--ring-focus);
  color: var(--text);
}

/*
 * The list. `fixed` and in the body, like the tooltip and for the same
 * reason: a dialog, a card and a sidebar all clip their overflow, and any one
 * of them would otherwise cut it in half. `ui.js` sets top, left, min-width
 * and max-height in viewport coordinates and flips it above the control when
 * there is no room below.
 */
.drop-pop {
  position: fixed; z-index: var(--z-pop);
  box-sizing: border-box;
  max-width: 92vw;
  padding: var(--sp-2);
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: var(--elev-3);
  font-family: var(--font-ui);
  font-size: var(--fz-body);
  line-height: 1.45;
  color: var(--text);
  overflow-y: auto;
  overscroll-behavior: contain;     /* a list at its end does not scroll the page */
  opacity: 0;
  transform: translateY(-3px);
  transition: opacity var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.drop-pop[data-where="above"] { transform: translateY(3px); }
.drop-pop.on { opacity: 1; transform: none; }

/* `pre`, not `nowrap`: two lists indent their options with spaces to draw a
   tree, and collapsing whitespace would flatten both of them. */
.drop-item {
  padding: var(--sp-3) var(--sp-5);
  border-radius: var(--r-sm);
  color: var(--text-2);
  cursor: pointer;
  white-space: pre;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
/*
 * `.on` is where the keyboard is; `aria-selected` is what the select holds.
 * They are different things, they are often not the same row, and both have to
 * be legible at once — so one is a surface and the other is a mark.
 *
 * The highlight is `--raised-2` with the accent mixed into it rather than
 * `--raised-2` alone. On the five dark themes the rung on its own would have
 * done; in Light and Glass the top two rungs are #fffffc and #ffffff, which is
 * a step of 0.002 in luminance, and a keyboard highlight nobody can see is the
 * one thing this control may not have. The tint is visible on all seven.
 */
.drop-item.on {
  background: color-mix(in srgb, var(--accent) 16%, var(--raised-2));
  color: var(--text);
}
.drop-item[aria-selected="true"] {
  color: var(--text);
  font-weight: 550;
  box-shadow: inset 2px 0 0 var(--accent);
}
.drop-item[aria-disabled="true"] { color: var(--faint); opacity: .6; cursor: default; }
.drop-item[aria-disabled="true"].on { background: none; }

.drop-group {
  padding: var(--sp-4) var(--sp-5) var(--sp-2);
  font-size: var(--fz-micro);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--faint);
}

body.no-motion .drop-pop, body.no-motion .drop-item { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .drop-pop, .drop-item { transition: none; }
}

/* ============================================================
   PAPER.

   Ctrl+P is reflex, and until now the app had no answer to it: the
   browser printed the studio. Rail, sidebar, tab bar, buttons, a
   dark background poured over an entire ink cartridge, and the
   writing itself cropped to whatever happened to be on screen,
   because .app is 100vh with overflow hidden and .main scrolls
   inside it. Somebody who pressed it to check a page count got a
   photograph of a website.

   A script has a better answer than paper and takes the keystroke
   for itself (see setPrintHandler in web/app.js): it hands back the
   same paginated PDF the export writes. Everything else — a
   character, a world article, a beat sheet, an outline — genuinely
   wants paper, and this is what it gets.

   Three jobs, in this order:
     1. Take the app off the page and leave the writing.
     2. Black on white, whatever the room is wearing. Four of the
        seven themes are dark and one is nearly black; printing one
        of those is unreadable and expensive at the same time.
     3. Give the writing the whole sheet, and do not break a card,
        a beat or a heading across two of them.

   The colours are done by moving the tokens rather than by painting
   the components, because the whole interface is built on the
   tokens: change --text once and 400 rules follow. That is most of
   the way and not all of it, so a backstop over the working area
   follows it — the reasoning is written where it sits.

   !important throughout, for one reason: a theme the writer built
   from a photograph is applied as inline custom properties on
   <html>, and several components carry inline colours of their own.
   Nothing without it can outrank an inline style.
   ============================================================ */

/* ============================================================
   THE OTHER THREE FAMILIES.

   Grounds, powers, peoples and relics have had a page shaped
   like the thing they describe for a while. Forces, epochs and
   customs fell through to the plain layout: a stack of
   questions and answers, identical on a plague, a coronation
   and a currency. Those are the pages a worldbuilder spends
   most of their time on.

   Everything here is drawn from answers the writer already
   typed. Nothing stores anything, and nothing invents anything.
   ============================================================ */

/* ---- forces: what it does, against what it costs ---------- */

/*
 * Every force in a story is a bargain. Set as a column these are
 * six paragraphs; set facing each other they are the argument,
 * and the empty side is the one to go and fill. The scales sit
 * between them rather than beside, so the eye reads across.
 */
.cost-ledger { display: flex; flex-direction: column; gap: var(--sp-5); }

.ledger-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: stretch;
  gap: var(--sp-4);
}

.ledger-side {
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  padding: var(--sp-5) var(--sp-5) calc(var(--sp-5) + 2px);
  min-width: 0;
}
.ledger-side h4 {
  margin: 0 0 var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-3);
}
.ledger-side p {
  margin: 0;
  font-family: var(--font-read);
  font-size: calc(15px * var(--fs));
  line-height: 1.6;
  color: var(--text);
  white-space: pre-wrap;
}
/* The two halves are not the same weight in the story. What a thing costs is
   where the drama is, so it carries the accent and the giving side does not. */
.ledger-side.gives { border-left: 3px solid var(--border); }
.ledger-side.takes { border-left: 3px solid var(--accent); }
.ledger-side.blank { background: none; border-style: dashed; }
.ledger-side.blank p { font-family: var(--font-ui); font-size: var(--fz-small); }

.ledger-scales {
  display: grid;
  place-items: center;
  color: var(--text-3);
  opacity: .6;
}
.ledger-scales .ic { width: 20px; height: 20px; vertical-align: 0; }

.ledger-stop {
  display: flex;
  gap: var(--sp-4);
  align-items: flex-start;
  padding: var(--sp-4) var(--sp-5);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  border-radius: var(--r);
}
.ledger-stop > span { color: var(--accent); opacity: .8; flex: none; margin-top: 2px; }
.ledger-stop .ic { width: 16px; height: 16px; vertical-align: 0; }
.ledger-stop h4 {
  margin: 0 0 3px;
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-3);
}
.ledger-stop p {
  margin: 0;
  font-family: var(--font-read);
  font-size: calc(14.5px * var(--fs));
  line-height: 1.55;
  color: var(--text-2);
  white-space: pre-wrap;
}

/* ---- epochs: cause, the thing, and what was different ----- */

/*
 * An event is the only kind of page with a shape built into it, and it is the
 * one shape the plain layout flattened. Read as a chain, the empty box in the
 * middle of it is the most useful blank space in the app.
 */
.cause-chain {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--sp-5);
}

.chain-beat {
  position: relative;
  padding-left: 26px;
}
.chain-mark {
  position: absolute;
  left: 0;
  top: 3px;
  width: 13px;
  height: 13px;
  display: grid;
  place-items: center;
}
.chain-mark i {
  width: 9px; height: 9px;
  border-radius: var(--r-pill);
  background: var(--border);
  display: block;
}
.chain-beat.during .chain-mark i { background: var(--accent); width: 11px; height: 11px; }
.chain-beat.blank .chain-mark i { background: none; border: 1.5px dashed var(--text-3); }

/* The rule joining one beat to the next, drawn from the mark rather than the
   text, so it stays put however long an answer runs. */
.chain-arrow {
  position: absolute;
  left: 6px;
  top: 19px;
  bottom: calc(var(--sp-5) * -1);
  width: 1px;
  background: var(--border);
}

.chain-beat h4 {
  margin: 0 0 var(--sp-2);
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-3);
}
.chain-beat p {
  margin: 0;
  font-family: var(--font-read);
  font-size: calc(15px * var(--fs));
  line-height: 1.62;
  color: var(--text);
  white-space: pre-wrap;
}
.chain-beat.blank p { font-family: var(--font-ui); font-size: var(--fz-small); }

/* ---- epochs: who was on each side ------------------------- */

.sides-board { display: flex; flex-direction: column; gap: var(--sp-5); }

.sides-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-4);
}
.sides-versus {
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-style: italic;
  color: var(--faint);
  white-space: nowrap;
}

.side-card { min-width: 0; }
.side-card button,
.side-unknown {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  width: 100%;
  min-width: 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.side-unknown { cursor: default; border-style: dashed; background: none; }
.side-card button:hover { border-color: var(--accent); background: var(--raised-2); }
.side-card img {
  width: 34px; height: 34px; flex: none;
  border-radius: var(--r-sm);
  object-fit: cover;
  image-rendering: pixelated;      /* the art is drawn a pixel at a time */
}
.side-card b,
.side-unknown b {
  font-size: var(--fz-body);
  font-weight: 550;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.sides-rest {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
}
.sides-rest em {
  font-size: var(--fz-tiny);
  color: var(--faint);
  font-style: italic;
  margin-right: var(--sp-2);
}
/* The third party is usually where the interesting one is, so it is on the
   page rather than dropped, but smaller than the two doing the fighting. */
.sides-rest .side-card { flex: 0 1 auto; min-width: 150px; }
.sides-rest .side-card img { width: 24px; height: 24px; }

/* ---- customs: a loaf, and a day's work -------------------- */

/*
 * The two numbers a currency page already asks for happen to be the whole
 * economy. Dividing one by the other is arithmetic, not cleverness, and it is
 * the difference between two figures and a reason somebody steals.
 */
.purse { display: flex; flex-direction: column; gap: var(--sp-4); }

.purse-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-4);
}
.purse-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: var(--sp-5) var(--sp-4);
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  text-align: center;
  min-width: 0;
}
.purse-mark { color: var(--text-3); opacity: .7; }
.purse-mark .ic { width: 17px; height: 17px; vertical-align: 0; }
.purse-cell b {
  font-family: var(--font-mono);
  font-size: calc(22px * var(--fs));
  font-weight: 600;
  color: var(--text);
  line-height: 1.1;
}
.purse-cell em {
  font-size: var(--fz-tiny);
  font-style: normal;
  color: var(--text-3);
  line-height: 1.4;
}
.purse-rule { width: 1px; align-self: stretch; background: var(--border); }

.purse-reading {
  margin: 0;
  font-family: var(--font-read);
  font-size: calc(14.5px * var(--fs));
  font-style: italic;
  line-height: 1.55;
  color: var(--text-2);
  text-align: center;
}
.purse-reading.faint { color: var(--faint); }

/* ---- customs: when it comes round ------------------------- */

.round-band {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--sp-4);
}
.round-cell {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--sp-4) var(--sp-5);
  background: var(--raised);
  border-radius: var(--r);
  border-left: 3px solid var(--border);
  min-width: 0;
}
.round-cell em {
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-style: normal;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-3);
}
.round-cell b {
  font-family: var(--font-read);
  font-size: calc(15px * var(--fs));
  font-weight: 500;
  line-height: 1.5;
  color: var(--text);
}

/* ---- the bands at the head of the page -------------------- */

.epochs-band .stat-band,
.customs-band .stat-band { flex-wrap: wrap; }

/* ---- narrow screens --------------------------------------- */

/*
 * Facing pairs stop facing below this width: side by side in 300px is two
 * columns of one word each. They stack, and the mark between them turns on
 * its side so it still reads as a relationship rather than two boxes.
 */
@media (max-width: 760px) {
  .ledger-pair,
  .sides-pair,
  .purse-pair { grid-template-columns: minmax(0, 1fr); }
  .ledger-scales,
  .sides-versus { justify-self: center; }
  .purse-rule { width: auto; height: 1px; align-self: auto; }
}

/* ============================================================
   THE PICKERS IN A WORLD ENTRY'S SIDEBAR.

   Two things were wrong and they read as one mess.

   A field label is set in small uppercase with tracking, and the
   quiet explanation that sits beside it is a span INSIDE that
   label - so it was shouted too. "Its emoji" and "the face it
   wears in lists, maps and pins" ran together into one
   unbroken line of capitals, which is why the sidebar looked
   like a form written by somebody in a hurry.

   And the emoji palette was twenty-four buttons, always open.
   After the work to get emoji out of the chrome, a permanent
   3x8 block of them was the cheapest-looking thing on the page.
   The writer picks one, rarely; it does not deserve a quarter of
   the sidebar every time the page is opened.
   ============================================================ */

/* The aside inside a label is an aside, not part of the shout. */
.dossier-field > label .faint,
.dossier-field > label .small {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 450;
  display: block;
  margin-top: 3px;
  color: var(--faint);
}

/* ---- the emoji, folded away until asked for ---------------- */

.emoji-fold { display: flex; flex-direction: column; gap: var(--sp-3); }

/* What is on the page when nothing is being chosen: the mark the
   writer already picked, and one quiet way to change it. */
.emoji-now {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  cursor: pointer;
  width: 100%;
  text-align: left;
  color: var(--text);
  font: inherit;
  transition: border-color var(--dur-1) var(--ease);
}
.emoji-now:hover { border-color: var(--text-3); }
.emoji-now[aria-expanded="true"] { border-color: var(--accent); }

.emoji-now b {
  font-size: calc(20px * var(--fs));
  line-height: 1;
  width: 26px;
  text-align: center;
  flex: none;
}
/* Nothing chosen yet: a drawn mark rather than a stray glyph, so an
   unset field does not look like a broken one. */
.emoji-now .ic { width: 17px; height: 17px; opacity: .55; vertical-align: 0; }
.emoji-now span { font-size: var(--fz-small); color: var(--text-2); }

.emoji-fold .emoji-row { display: none; }
.emoji-fold.open .emoji-row {
  display: flex;
  padding-top: var(--sp-1);
  animation: fade var(--dur-1) var(--ease);
}

/* ==========================================================================
   COMPOSED FOR THE SCREEN A WRITER ACTUALLY OWNS

   The home page and the project page were both composed at desktop width and
   never re-composed for a short viewport. At 1280x720 the result was a strong
   hero, one franchise card, and a hole: the largest single empty rectangle on
   the first screen measured 648 x 328px, 29.8% of everything right of the
   sidebar, and 42.9% of that screen was carrying nothing at all.

   Nothing here changes the visual language. It changes where things sit, how
   tall they are on a short screen, and which of them the writer meets first.
   Every measurement is on the app's own ladders, so the seven themes keep
   restating their own air.
   ========================================================================== */


/* ==========================================================================
   1 - THE FRONT DOOR
   ========================================================================== */

/*
 * A poster sized by the room it is standing in.
 *
 * `min-height: 46vh` reads as viewport-aware and is only half of one: the
 * body inside it held 84px of fixed vertical padding, so on a 720px screen
 * the marquee took 331px - 46% - and left 389px for everything a returning
 * writer came back for. 38vh with a floor keeps the hero a marquee at every
 * height (274px at 720, 410px at 1080) and hands the shelf below it enough
 * room to finish above the fold. The cap goes: max-height fought min-height
 * on tall screens and neither won cleanly.
 */
.home-hero {
  min-height: clamp(268px, 38vh, 500px);
  max-height: none;
}
.home-hero-body { padding-block: clamp(var(--sp-8), 3.2vh, var(--sp-10)); }

/* Day one has no art and a two-line promise, so it is content-bound rather
   than height-bound; the air around it is what has to give. All three doors
   in now stand clear of the fold on a 720px screen instead of being cut in
   half by it. */
.home-first .home-hero { min-height: clamp(232px, 32vh, 430px); }
.start-band { padding-top: clamp(var(--sp-8), 3.4vh, var(--sp-10)); }
.start-head { margin-bottom: clamp(var(--sp-7), 2.4vh, var(--sp-8)); }
.first-proof { padding-top: clamp(var(--sp-9), 4vh, var(--sp-10)); }

/* Both bands were capped and *centred* under a hero that runs the full width
   of the page, so on a 1920px screen the promise and the three doors started
   131px to the right of the title above them: two left edges on the first
   screen anybody sees. Same cap, ranged left. */
.start-band, .first-proof { margin-inline: 0 auto; }

.home-controls { padding-top: clamp(var(--sp-6), 2vh, var(--sp-8)); }


/* ---- the shelves: one bookcase, not three bands ------------------------- */

/*
 * A shelf used to be a band across the whole page whatever stood on it.
 *
 * A studio with one saga and three films therefore spent two full-width bands
 * - 802px of a 720px screen - on four cards, and left a poster-shaped hole
 * beside each. Shelves that do not need the width now stand side by side and
 * share one band, so the writer's whole library arrives on the first screen.
 * A shelf with more titles than fit still claims the whole row and scrolls
 * sideways with its arrows, exactly as before; `--n` is the card count the
 * view hands down, and `min()` is what decides between the two.
 */
.home-work {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  column-gap: var(--sp-10); row-gap: 0;
  padding-inline: clamp(24px, 5vw, 72px);
}
.home-work > .home-row {
  /*
   * A shelf asks for three covers and will take as many as it has.
   *
   * Asking for its full natural width instead is what put a nine-title Films
   * shelf on a line of its own and left a two-card Franchises shelf standing
   * beside 546px of nothing above it. Three covers is what decides whether
   * two shelves share a line; the grow puts the rest of the line back into
   * whichever of them still wants it, and the max stops a shelf stretching
   * past the covers it actually holds.
   */
  flex: 1 1 min(100%, calc(3 * var(--card-w) + 2 * var(--sp-7)));
  max-width: min(100%, calc(var(--n, 6) * var(--card-w) + (var(--n, 6) - 1) * var(--sp-7)));
  min-width: 0;
  padding-inline: 0;
  padding-top: clamp(var(--sp-7), 2.6vh, var(--sp-9));
}
.home-row-head { margin-bottom: var(--sp-5); }

/*
 * A poster is as big as the screen can afford.
 *
 * 176px was chosen against a 1080px-tall window. Held at 176 on a 720px one,
 * a row of covers plus its caption is 311px deep and pushes the next section
 * clean off the screen. The width answers to the shorter of the two axes, so
 * the card grows on a real desktop (227px at 1920x1080) and stands down on a
 * laptop (151px at 1280x720) rather than taking the fold with it.
 */
.home { --card-w: clamp(150px, min(13vw, 21vh), 268px); }
.shelf-item { width: var(--card-w); }
/* The 6px keeps the room the hover lift needs; a shorthand with 0 here would
   quietly undo it on a narrow window. */
.home-shelf { padding: var(--sp-3) 6px var(--sp-6); }
.shelf-strip::before, .shelf-strip::after { bottom: var(--sp-6); }

/* Everything at once wraps rather than scrolls, and its tracks are the same
   cover the shelf view uses, so switching between the two moves the covers
   rather than resizing them. `auto-fill` against a fixed 150px used to hand a
   two-card Franchises shelf five tracks and leave three of them empty. */
.shelf-strip.as-grid .home-shelf {
  grid-template-columns: repeat(auto-fill, minmax(var(--card-w), 1fr));
}


/* ---- the back of the room ---------------------------------------------- */

/*
 * One left edge down the whole page.
 *
 * The band at the foot was capped at 1180px and centred while the shelves
 * above it ran the full width of the page, so at 1920 the numbers began 209px
 * to the right of the posters and the page changed its mind about where it
 * started halfway down. Same padding as everything else, capped at the app's
 * own --content-max, ranged left against the shelves.
 */
.home-band {
  max-width: none;
  margin: clamp(var(--sp-7), 2.4vh, var(--sp-9)) 0 0;
  padding-inline: clamp(24px, 5vw, 72px);
}
/* The quote keeps its own measure; only the two full-width blocks are capped,
   or a line of reading matter would run to 1,340px. */
.home-band .split, .home-band .stat-row { max-width: var(--content-max); }
.home-band .split {
  grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
  gap: var(--sp-9);
}
/* A writer who has put the to-do list away gets one column, and a window
   dragged below the desktop floor gets one column: both rules already existed
   and this one sits after them, so it has to say so itself. */
.home-band .split.solo { grid-template-columns: minmax(0, 1fr); }
@media (max-width: 1080px) {
  .home-band .split { grid-template-columns: minmax(0, 1fr); }
}
.home-band .stat-row { margin-block: clamp(var(--sp-8), 3vh, var(--sp-9)) var(--sp-8); }


/* ==========================================================================
   2 - THE PROJECT PAGE
   ========================================================================== */

/*
 * A title card as tall as its title.
 *
 * `min-height: 230px` against a body that measures 171px, bottom-aligned, put
 * a 59px band of nothing between the breadcrumb and the first word on the
 * page - at every window size, because 230 is a number and not a proportion.
 * The floor is a proportion now and sits under the content rather than over
 * it. Real artwork is the one case that earns the height, so it keeps it.
 */
.project-hero { min-height: clamp(148px, 17vh, 214px); }
.project-hero.has-art { min-height: clamp(196px, 24vh, 300px); }
.project-hero-body { padding-block: clamp(var(--sp-8), 3vh, var(--sp-9)); }

/*
 * The page beside the page.
 *
 * The synopsis reads at a measure and stops well short of the right-hand
 * edge, so a third of the column carried nothing while the three live
 * miniatures and the list of what is left sat below the fold. Reading matter
 * left, the things worth looking at right, the masthead across both.
 *
 * The floor is a floor, not a phone: this is a chromeless desktop window and
 * there is no build below 1280. It exists only so that a window dragged
 * absurdly narrow degrades into one column instead of two thin ones.
 */
.overview {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(248px, 25%, 330px);
  grid-template-areas: "masthead masthead" "main side";
  column-gap: var(--sp-10);
  align-items: start;
}
.overview > .fp-band { grid-area: masthead; }
.overview > .overview-main { grid-area: main; min-width: 0; }
.overview > .overview-side { grid-area: side; min-width: 0; }
@media (max-width: 1080px) {
  .overview {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "masthead" "main" "side";
  }
}

/*
 * The masthead: eight short facts on one line instead of two bands.
 *
 * The frontispiece was centred in a page whose every other line is ranged
 * left, and it and the credits between them spent 244px of a 720px screen.
 * "N people who did not exist before you" keeps its rank - it is still the
 * only italic serif on the line, against 10.5px tracked capitals - it just
 * stops being an island.
 */
.fp-band {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-5) var(--sp-9);
  margin: 0 0 clamp(var(--sp-8), 2.6vh, var(--sp-9));
  padding-bottom: var(--sp-7);
  border-bottom: var(--hair) solid var(--hair-ink);
}
.fp-band .frontispiece {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-3) var(--sp-7);
  text-align: left; margin: 0; padding: 0; border-bottom: 0;
}
.fp-band .fp-day { margin: 0; }
.fp-band .fp-nums { margin: 0; justify-content: flex-start; }
.fp-band .credit-block { margin: 0; padding: 0; border-bottom: 0; }

/*
 * A page to write on, not a field to fill in.
 *
 * The synopsis is the reason this tab exists and an empty one was a 110px
 * box, so a project made five minutes ago ended its left-hand column two
 * fifths of the way down a 1080px screen and left the rest of it blank. A
 * writing surface should look like somewhere to write before there is
 * anything in it.
 */
.overview-main .bible .rich-edit.reading { min-height: clamp(190px, 27vh, 360px); }

/*
 * One tile per row in the companion column.
 *
 * `repeat(auto-fit, minmax(260px, 1fr))` collapses its empty tracks, so a
 * film - which has two of these, not three - was handed two 451px slabs
 * 346px deep across the full width, and at 1920 two of 625px. A tile is a
 * poster of the real thing; it does not need to be half a screen wide.
 */
.overview-side .tool-tiles {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-6);
  margin-top: 0;
}
.overview-side .section-head { margin-top: clamp(var(--sp-8), 2.6vh, var(--sp-9)); }
.overview-side .overview-todo { max-width: none; }

@media print {

  /* ---- the sheet ------------------------------------------------------- */

  @page {
    /* Room for a thumb down the sides, and enough at the head and foot that
       the browser's own header and footer do not sit on the words. */
    margin: 18mm 16mm;
  }

  /* ---- the ink --------------------------------------------------------- */

  html {
    /* The ladder, flattened. Every surface is the paper; the steps between
       them were light, and paper has no light in it. */
    --bg:          #ffffff !important;
    --bg-deep:     #ffffff !important;
    --surface:     #ffffff !important;
    --raised:      #ffffff !important;
    --raised-2:    #ffffff !important;
    --page-bg:     #ffffff !important;

    /* The words. The greys are kept as distinct rungs rather than all set to
       black, because they are read by borders, marks and rules as well as by
       text, and none of them is pale enough to fade: 30% grey is about the
       lightest ink that survives toner. Text inside the working area is taken
       all the way to black further down, by the backstop. */
    --text:        #000000 !important;
    --text-2:      #1a1a1a !important;
    --text-3:      #333333 !important;
    --page-ink:    #000000 !important;
    --muted:       #333333 !important;
    --faint:       #4d4d4d !important;

    /* Lines. Light enough to read as a rule, dark enough to exist. */
    --border:      #999999 !important;
    --border-soft: #cccccc !important;

    /*
     * The colours that mean something.
     *
     * Flattened to black rather than kept, because these are read on screen
     * as "this is the highlight" and on paper as "this is a different, paler
     * shade of grey" — the meaning does not survive the trip, and a colour
     * printer would spend ink saying nothing. The tint behind a highlighted
     * chip goes to nothing at all so the chip prints as its words.
     */
    --accent:      #000000 !important;
    --accent-2:    #000000 !important;
    --accent-soft: transparent !important;
    --accent-ink:  #000000 !important;
    --film:        #000000 !important;
    --tv:          #000000 !important;
    --ok:          #000000 !important;
    --danger:      #000000 !important;
    --danger-ink:  #000000 !important;
    --danger-glow: transparent !important;

    /* Depth is a screen idea. On paper it is a grey smudge. */
    --elev-0: none !important;
    --elev-1: none !important;
    --elev-2: none !important;
    --elev-3: none !important;
    --elev-4: none !important;
    --shadow-1: none !important;
    --shadow-2: none !important;
    --shadow-3: none !important;
    --ring-focus: none !important;
    --ring-on: none !important;
    --ring-hair: none !important;
    --ring-punch: none !important;
    --ring-tint: none !important;
    --glow: none !important;
    --glow-lift: none !important;
    --glow-danger: none !important;
    --glow-danger-lift: none !important;
  }

  html,
  body {
    background: #ffffff !important;
    color: #000000 !important;
    /* Both are height:100% on screen. Left alone, the printout is one sheet
       of whatever happened to be in the window. */
    height: auto !important;
    overflow: visible !important;
  }

  /*
   * Gradients, veils and washes.
   *
   * A hero band is a photograph under a dark gradient with white type on it.
   * Browsers drop background images when printing anyway, which leaves the
   * white type on white paper — so the veils have to go with them, and the
   * type has to come back to black. Everything below inherits its colour from
   * the tokens above once its own background is out of the way.
   */
  *,
  *::before,
  *::after {
    background-image: none !important;
    box-shadow: none !important;
    text-shadow: none !important;
    filter: none !important;
    animation: none !important;
    transition: none !important;
  }

  /*
   * The backstop: inside the working area, every word is black and no box has
   * a fill.
   *
   * Moving the tokens gets most of the way, and it is not enough on its own,
   * because roughly thirty rules write `color: #fff` by hand — a logline over
   * a poster, initials on an avatar, the chip for the role a character is
   * playing. Every one of those sits on a photograph or an accent fill, and a
   * printer drops background images: white type on a dropped photograph is a
   * blank rectangle where the writing was. It is not enough to be nearly
   * right here — one invisible line is a line the writer will not know is
   * missing until somebody asks about it.
   *
   * !important, because a theme built from a photograph applies as inline
   * custom properties and several components carry inline colours of their
   * own. Nothing else outranks those.
   */
  .main,
  .main * {
    color: #000000 !important;
    background-color: transparent !important;
  }

  .project-hero,
  .project-hero-veil,
  .project-hero-body,
  .character-hero,
  .character-hero-veil,
  .character-hero-body,
  .entry-hero,
  .entry-hero-veil,
  .entry-hero-body,
  .home-hero,
  .home-hero-body,
  .home-hero-glow,
  .frontispiece,
  .lead-band,
  .world-band,
  .grounds-band,
  .senses-band,
  .stat-band,
  .band-row {
    background: none !important;
    color: #000000 !important;
    min-height: 0 !important;
  }

  /* ---- take the app off the page --------------------------------------- */

  /*
   * Every name below was read out of web/index.html and the views, not
   * guessed. Grouped by what it is rather than where it lives, because the
   * same kind of furniture turns up on six different pages.
   */

  /* The studio itself: the spine, the project list, the weather behind it. */
  #scene,
  .scene,
  .rail,
  .sidebar,
  .sidebar-reopen,
  .sidebar-split,
  .studio-menu,
  .lock-screen,
  .loading {
    display: none !important;
  }

  /* Things that float above everything: dialogs, toasts, tips, pickers. */
  #modal-root,
  .modal-backdrop,
  .toast-root,
  .tip,
  .smart-pop,
  .mention-pop,
  .el-menu,
  .cmd-sheet,
  .ending {
    display: none !important;
  }

  /* Toolbars and the strips of controls that live in them. */
  .topbar-actions,
  .editor-bar,
  .find-bar,
  .write-status,
  .import-footer,
  .home-controls,
  .home-sort,
  .beat-tools,
  .character-hero-tools,
  .entry-hero-tools,
  .search-trigger,
  .help-strip,
  .size-control {
    display: none !important;
  }

  /* Tab bars. Only the open tab's contents are ever in the document, so the
     bar is a control with nothing left to control. */
  .tabs,
  .lane-tabs,
  .rail-tabs,
  .tab,
  .tab-slider {
    display: none !important;
  }

  /* Side rails that navigate rather than say anything: the scene list, the
     desk, the outline jump list, the slide thumbnails, the companion page. */
  .script-rail,
  .desk-rail,
  .outline-rail,
  .slide-rail,
  .companion-pane,
  .import-side {
    display: none !important;
  }

  /*
   * A character's rail goes too, and not by my judgement: the stylesheet
   * already drops it under 980px, saying "the header already shows who they
   * are". A sheet of paper is narrower than that. Keeping it also cost a
   * whole page of portrait — the face is width:100% of its card, and with the
   * column collapsed the card was the full width of the paper.
   */
  .dossier-rail {
    display: none !important;
  }

  /* Controls. Buttons are not writing.

     Named rather than blanket `button`, deliberately: several surfaces carry
     real content inside a <button> — a fact chip on a world article, a
     colour, a photograph in a strip — and hiding every button would quietly
     delete them from the printout. */
  .btn,
  .icon-btn,
  .rail-btn,
  .fmt-btn,
  .el-pick,
  .seg,
  .seg-toggle,
  .row-actions,
  .shelf-arrow,
  .tool-tiles,
  .tool-tile,
  .tool-mini,
  .shot.add,
  .ref-empty,
  .emoji-pick,
  .colour-pick,
  .dropzone,
  .export-grid,
  .add,
  input[type="search"],
  .progress-bar {
    display: none !important;
  }

  /*
   * The sprite marks stay, which is the opposite of what I first wrote.
   *
   * `.ic` looked like pure decoration, so it was hidden — and then counting
   * what actually survives the rules above showed the mistake: almost every
   * icon in the studio lives inside a button, a tab or a toolbar and has
   * already gone with its parent. What is left is the ones that carry the
   * meaning. The tick against a finished step on "Where things stand" is the
   * only thing distinguishing it from an unfinished one; the four glyphs on a
   * world entry ARE sight, sound, smell and underfoot; and the help page sets
   * a hundred of them inside its sentences, naming the button to press.
   *
   * They inherit black from the backstop above, so they print as line art.
   */

  /* ---- give the writing the paper -------------------------------------- */

  .app {
    display: block !important;
    height: auto !important;
    overflow: visible !important;
  }

  .main {
    display: block !important;
    height: auto !important;
    max-height: none !important;
    overflow: visible !important;
  }

  .view {
    /* The sheet already has margins; the screen's gutters would be a second
       set inside them. */
    padding: 0 !important;
    max-width: none !important;
    margin: 0 !important;
  }

  /*
   * Two columns become one.
   *
   * The rails that were only navigation are gone; these are the splits whose
   * second column is genuinely something to read — a character's presence, a
   * place's facts — so it drops underneath the main text rather than being
   * squeezed into a third of a sheet.
   */
  .dossier-layout,
  .dossier-grid,
  .article-grid,
  .editor-layout,
  .prose-cols,
  .spec-sheet,
  .two-col,
  .split,
  .script-layout,
  .approach-pair,
  .closing-pair,
  .deck-stage {
    display: block !important;
  }

  .article-main,
  .article-side,
  .dossier-main,
  .script-wrap,
  .paper-wrap {
    width: auto !important;
    max-width: none !important;
    padding: 0 !important;
    margin: 0 0 12pt !important;
  }

  /* Nothing sticks to a sheet of paper. */
  .topbar,
  .crumbs,
  .paper,
  .beat-page,
  .world-page,
  .bible-page,
  .sheet-layer {
    position: static !important;
  }

  /* The breadcrumb trail stays: on paper it is the only thing saying which
     project this page came out of. It just stops looking like a toolbar. */
  .topbar {
    background: none !important;
    border: 0 !important;
    padding: 0 !important;
    margin: 0 0 8pt !important;
  }

  /*
   * Fields stop looking like fields, and keep their words.
   *
   * Flattened rather than hidden, and that distinction is the whole reason
   * this list is separate from the one above. A character's status, a world
   * entry's kind, a document's type: all of them are a fact the writer
   * entered, and all of them live in a <select> or an <input>. Hiding the
   * control would print the label with nothing after it, which is a worse lie
   * than printing a box. The box goes; the answer stays.
   */
  .rich-edit,
  .rich-read,
  .inline-edit,
  .input,
  .select,
  .el-picker,
  select,
  /* A tick box is left exactly as it is: on a printed to-do list the tick is
     the whole point, and stripping the control's appearance erases it. */
  input:not([type="checkbox"]):not([type="radio"]),
  textarea,
  [contenteditable] {
    appearance: none !important;
    background: none !important;
    border: 0 !important;
    outline: 0 !important;
    padding: 0 !important;
    min-height: 0 !important;
    height: auto !important;
    max-height: none !important;
    overflow: visible !important;
    resize: none !important;
    color: #000000 !important;
  }

  /*
   * A prompt is not an answer.
   *
   * Empty fields draw their placeholder as generated content — "What is it
   * like here at this hour" sitting in the box in grey italic. On screen that
   * is an invitation. On paper it is indistinguishable from something the
   * writer wrote, and a producer reading a printed world entry would have no
   * way of telling the two apart. Blank stays blank.
   */
  .inline-edit.placeholder::before,
  .rich-edit:empty::before,
  .companion-beat-body:empty::before,
  .dp [contenteditable]:empty::before,
  [contenteditable]:empty::before {
    content: none !important;
  }

  ::placeholder {
    color: transparent !important;
  }

  /* Anything that scrolls on screen has to unroll on paper, or it prints as
     the first inch of itself with the rest missing and nothing to say so. */
  .panel,
  .card,
  .rows,
  .tasks-list,
  .dossier,
  .article-body,
  .beat-body,
  .stanzas {
    max-height: none !important;
    overflow: visible !important;
  }

  /*
   * Photographs earn their ink; keep them on the sheet.
   *
   * Width only. Forcing `height: auto` as well looks tidier and is wrong: a
   * poster on the shelf and a still in a strip are both `width:100%;
   * height:100%` inside a box of a fixed size, and releasing the height pops
   * them out of it at whatever the file happens to measure.
   */
  img,
  canvas {
    max-width: 100% !important;
  }

  /* ---- do not break it in the wrong place ------------------------------ */

  /*
   * A card cut across a page fold is a card you have to hold up to the light
   * to read. These are the app's units of one thought each.
   */
  .card,
  .panel,
  .row,
  .beat,
  .stanza,
  .spec-row,
  .dossier-card,
  .dossier-field,
  .ov-card,
  .shelf-item,
  .character-line,
  .entry-line,
  .cast-credit,
  .credit-item,
  .step,
  .faq,
  .sense,
  .shot,
  figure,
  blockquote,
  tr {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  /*
   * And never leave a heading stranded at the foot of a sheet with its
   * paragraph over the fold. `break-after` is the modern spelling and
   * `page-break-after` is what older engines answer to; both are cheap.
   */
  h1, h2, h3, h4, h5, h6,
  .page-title,
  .page-head,
  .section-head,
  .card-title,
  .beat-heading,
  .beat-head,
  .character-name,
  .entry-name,
  .home-title,
  .paper-title,
  .dossier-label,
  .stanza-label,
  .eyebrow {
    break-after: avoid;
    page-break-after: avoid;
    break-inside: avoid;
    page-break-inside: avoid;
    color: #000000 !important;
  }

  /* A single line of a paragraph alone at the top or bottom of a sheet reads
     as a mistake. Three is the printer's usual answer. */
  p, li, .rich-read, .rich-edit {
    orphans: 3;
    widows: 3;
  }
}

/* ---- from the script-laptop rebuild ---- */
/* ============================================================
   The writing room on a laptop.

   The page is the product. At 1280x720 it was not behaving like
   it: the editor gets 992px once the studio sidebar has had its
   share, two docked rails took 434 of them, and the paper came
   out 518px wide. A US Letter page is 816px. The writer spent
   their working life on a page rendered at 63% of itself, with
   more screen given to its furniture (666px) than to the words.

   Two labels were also being sliced in half, "Todo" and "Read",
   because six words do not fit across a 222px rail at any type
   size and there was nothing else the tabs could be drawn as.

   Nothing here touches the paper. The margins, the 60 and 35
   column measures and the type on the page were measured against
   Final Draft and match it to the column. This is the furniture.

   OVERRIDES, in web/styles.css, all of them still needed for the
   wide screen and none of them right below it:
     .script-layout            the three docked columns
     @media (max-width: 1520px)  narrower docked rails
     @media (max-width: 1240px)  the scene rail vanishes
     @media (max-width: 1000px)  the desk vanishes too
     .rail-tabs / .rail-tab      a row of words that could slice
     .scene-row                  one line, cut wherever it ran out

   This has to merge LAST, after script.css, because every one of
   those lives earlier in the file and several tie on specificity.
   merge-css.py sorts unlisted files after the named ones by name,
   which puts it there already; if the ORDER list ever grows, put
   this at the end of it.

   BEFORE RUNNING merge-css.py: `python stage-laptop.py off`. This
   block is currently appended to styles.css by hand, behind its own
   markers, because a full merge could not be run at the time — an
   unmerged emoji.css was sitting in staging and a block of
   hand-written rules after the END marker would have been lifted
   above the merged block and quietly reordered. The merge will pick
   this file up on its own; taking the hand-applied copy out first
   is what stops it landing twice.
   ============================================================ */

:root {
  /*
   * TWO NEW TOKENS, and the only two numbers this file invents.
   *
   * Neither belongs on the spacing scale and neither should: --sp-10
   * is 44px and the scale is for the gaps between things, not for the
   * width of a column of controls or of the panel it opens. They are
   * named because four rules need to agree about them: the grid
   * column, the aside, the button inside it, and the panel that
   * floats beside it. The button size is derived rather than stated,
   * so the icon stays centred if the rail is ever made wider.
   *
   * The bar is 46 rather than 44 because a 34px target with --sp-3
   * either side of it is the smallest column that does not feel
   * pinched against the edge of the window.
   */
  --rail-icon-w: 46px;
  --rail-panel-w: 300px;
}

/* ============================================================
   1. Tabs that cannot be sliced, at any width or type size.
   ============================================================ */

/*
 * The row wraps rather than overflowing.
 *
 * `flex: 1` with no wrap is what cut "Todo" and "Read" in half: six
 * buttons that will not shrink below their words, in a box narrower
 * than their words, in a rail that clips. Wrapping is the honest
 * answer, because it is the only one that still holds when a reader
 * turns the text size up and the labels grow again.
 */
.rail-tabs {
  flex-wrap: wrap;
  gap: var(--sp-2) 0;
  margin-bottom: var(--sp-7);
}
/*
 * `flex: 1` shared the row out evenly, which is right for one row and wrong
 * for two: it stretched the three tabs that wrapped onto the second line
 * across the whole rail, so a tidy strip became a scatter. They sit at their
 * own width now, packed from the left, and the second row lines up under the
 * first.
 */
.rail-tab {
  flex: 0 0 auto;
  min-width: max-content;              /* a label is never narrower than itself */
  position: relative;
  display: flex; align-items: center; justify-content: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-xs) var(--r-xs) 0 0;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.rail-tab:hover { color: var(--text-2); }
.rail-tab:focus-visible { outline: none; box-shadow: var(--ring-focus); }

/* Wide screens read the words; the icon is for the narrow rail below. */
.rail-tab .ic { display: none; }

/* ============================================================
   2. Below 1664px, the rails are their icons.

   The number is arithmetic, not taste, and it is the sum of what
   the page needs before anything else may have any:

     856  a true 8.5in page (816) inside its wrapper (2 x 20)
     490  two docked rails, at the widths they already have
     318  the studio's own spine (56) and sidebar (262)
     ---
    1664

   At 1664 and above, docking both rails is free: the page is a
   true 8.5 inches either way, so it keeps its company. Below, the
   page starts paying for them, and by 1280 it is down to 518px,
   63% of itself, on the commonest screen there is.

   So below the line the rails become columns of icons, 46px each
   instead of 490 together, and the panel one opens floats over the
   page rather than pushing it. The paper is a true 8.5in from 1280
   upwards, whatever is open beside it, and it never moves when
   something opens.

   Nothing is taken away. Every panel that was docked is one press
   from the same edge it always lived on.
   ============================================================ */

@media (max-width: 1664px) {
  .script-layout {
    grid-template-columns: var(--rail-icon-w) minmax(0, 1fr) var(--rail-icon-w);
  }

  /*
   * The asides, restated whole.
   *
   * Three older rules describe these at this width: one hides them,
   * one brings them back as a 280px drawer over the page, and the
   * generic outline rail gives them a scroll box that would clip the
   * floating panel to 46px. Restating rather than patching means the
   * geometry is readable in one place instead of assembled out of
   * four.
   *
   * The three modes that exist to take the room away are named out
   * of it rather than outranked. This has to be the selector and not
   * the cascade: restating an aside needs three classes of weight,
   * which is exactly what `.script-shell.room-off .desk-rail` has,
   * and being written later would have won it. The bare page came
   * back with a 946px desk down one side of it.
   */
  .script-shell:not(.room-off):not(:fullscreen) .script-layout:not(.beside) .desk-rail,
  .script-shell:not(.room-off):not(:fullscreen) .script-layout:not(.beside) .script-rail {
    display: block;
    /* Measured, not 46. See the note on `.desk-rail`. This block exists TWICE
       under the same media query, so both copies carry the change - editing one
       of them alone does nothing, because the later one wins. */
    position: sticky; top: var(--stack-h, 46px);
    inset-inline: auto; bottom: auto;
    width: auto; max-height: none;
    overflow: visible;                 /* or the floating panel is clipped away */
    background: none; border: 0; box-shadow: none;
    padding: var(--sp-8) var(--sp-3) var(--sp-10);
    z-index: var(--z-base);
  }
  .script-shell .script-layout .desk-rail { grid-column: 1; }
  .script-shell .script-layout .script-rail { grid-column: 3; }

  /* The tab strip is now a column of doors down the edge of the room. */
  .script-layout .rail-tabs {
    flex-direction: column; flex-wrap: nowrap;
    gap: var(--sp-2); margin: 0;
  }
  .script-layout .rail-tab {
    flex: 0 0 auto; min-width: 0;
    width: calc(var(--rail-icon-w) - var(--sp-3) * 2);
    height: calc(var(--rail-icon-w) - var(--sp-3) * 2);
    padding: 0;
    border-bottom: 0; border-radius: var(--r-md);
    color: var(--text-3);
  }
  .script-layout .rail-tab .ic { display: block; width: var(--icon-lg); height: var(--icon-lg); }
  .script-layout .rail-tab:hover { background: var(--surface); color: var(--text); }

  /* The label stays in the button so a screen reader still hears a word
     rather than "button". `data-tip` draws it for everybody else. */
  .script-layout .rail-tab-name {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden; white-space: nowrap;
  }

  /*
   * A lit tab means "this panel is open", so it is only lit when one
   * is. The tab the panel would show is still marked `.on` while the
   * side is shut, and a button glowing at a closed panel is a lie.
   */
  .script-shell.show-desk .desk-rail .rail-tab.on,
  .script-shell.show-rail .script-rail .rail-tab.on {
    background: var(--surface); color: var(--accent);
  }
  /* The accent sits on the edge nearest the page, so it points at what
     it opened rather than floating unattached. */
  .script-shell.show-desk .desk-rail .rail-tab.on::after,
  .script-shell.show-rail .script-rail .rail-tab.on::after {
    content: ''; position: absolute;
    top: var(--sp-3); bottom: var(--sp-3); width: 2px;
    border-radius: var(--r-pill); background: var(--accent);
  }
  .script-shell.show-desk .desk-rail .rail-tab.on::after { right: 0; }
  .script-shell.show-rail .script-rail .rail-tab.on::after { left: 0; }

  /* ---- the panel that floats ------------------------------------------ */

  /* Shut is the resting state. The page is what the editor should be
     showing anybody who has not asked for anything else. */
  .script-layout .desk-rail > [data-desk-panel],
  .script-layout .script-rail > [data-panel] { display: none; }

  .script-shell.show-desk .desk-rail > [data-desk-panel]:not([hidden]),
  .script-shell.show-rail .script-rail > [data-panel]:not([hidden]) {
    display: block;
    position: absolute; top: 0;
    width: var(--rail-panel-w);
    max-height: calc(100vh - 46px - var(--sp-10));
    overflow-y: auto; overscroll-behavior: contain;
    z-index: var(--z-drawer);
    background: var(--bg);
    border: 1px solid var(--border-soft);
    box-shadow: var(--elev-3);
    padding: var(--sp-7) var(--sp-7) var(--sp-9);
  }
  /* It grows out of the rail it belongs to: square against that edge,
     rounded on the three it floats over. */
  .script-shell.show-desk .desk-rail > [data-desk-panel]:not([hidden]) {
    inset-inline-start: 100%;
    border-radius: 0 var(--r-lg) var(--r-lg) 0;
  }
  .script-shell.show-rail .script-rail > [data-panel]:not([hidden]) {
    inset-inline-end: 100%;
    border-radius: var(--r-lg) 0 0 var(--r-lg);
  }

  /* Docked, the tab strip says which panel you are looking at. A column
     of icons does not, so the panel says it itself. */
  .script-shell.show-desk .desk-rail > [data-desk-panel]:not([hidden])::before,
  .script-shell.show-rail .script-rail > [data-panel]:not([hidden])::before {
    content: attr(data-name);
    display: block; margin-bottom: var(--sp-7);
    font-size: var(--fz-micro); font-weight: 600;
    text-transform: uppercase; letter-spacing: var(--track);
    color: var(--faint);
  }

  /*
   * Frosted rather than opaque on glass, where a solid slab over the
   * paper would be the one panel in the app that is not made of light.
   */
  html[data-theme="glass"] .script-shell.show-desk .desk-rail > [data-desk-panel]:not([hidden]),
  html[data-theme="glass"] .script-shell.show-rail .script-rail > [data-panel]:not([hidden]) {
    background: rgb(255 255 255 / .82);
    backdrop-filter: blur(22px) saturate(1.7);
    -webkit-backdrop-filter: blur(22px) saturate(1.7);
    border: 1px solid rgb(255 255 255 / .7);
  }

  /*
   * The two bar buttons that used to fold the rails away have nothing
   * left to do: the rails no longer fold away, and their icons are on
   * screen at all times. Two dead controls out of the writer's eyeline.
   */
  .script-bar .rail-toggle { display: none; }
}

/* ============================================================
   3. A scene list you can navigate by.

   Fourteen rows reading "EXT. ROOKSFER..." is not a scene list.
   The first fourteen characters of a slugline are the least
   distinguishing part of it: every scene in a script starts with
   the same three or four words. What tells one from another is
   where it happens, so the place gets the width, and the INT/EXT
   and the time of day go underneath, where they separate the
   three scenes set on the same quay without competing for room.
   The ellipsis was never the defect. Identical rows were.
   ============================================================ */

.scene-row {
  grid-template-columns: 22px minmax(0, 1fr) auto;
  gap: 0 var(--sp-4);
  align-items: baseline;
  padding: var(--sp-3) var(--sp-4);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.scene-n { grid-column: 1; grid-row: 1; }

/*
 * The place gets the whole width, including the column the page number sits
 * in, because the page number only occupies the line underneath. And it wraps
 * to a second line rather than truncating: "THE VANE HOUSE, KITCHEN" and "THE
 * VANE HOUSE, HER ROOM" differ in their last word, so cutting them at the
 * rail's width is the identical-rows fault all over again in miniature. Two
 * lines is the ceiling; past that a slugline is prose, not a label.
 */
.scene-h {
  grid-column: 2 / -1; grid-row: 1;
  font-weight: 500;
  white-space: normal; text-overflow: clip;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}

/*
 * Where and when, under the where. Micro and tracked, so it reads as a
 * caption to the place rather than a second thing to scan.
 *
 * It wraps rather than truncating, for the same reason the place does and
 * for one more: a reader who turns the text size up to 130% is asking for
 * taller rows, and "INT · CONTINUOUS" at that size does not fit a 240px
 * docked rail on any screen. Cutting it there would answer a request for
 * bigger words by showing fewer of them.
 */
.scene-when {
  grid-column: 2; grid-row: 2;
  font-size: var(--fz-micro); color: var(--faint);
  letter-spacing: var(--track-label); text-transform: uppercase;
  white-space: normal;
}
.scene-p { grid-column: 3; grid-row: 2; }

/* The scene the caret is standing in. The list is redrawn five times a
   second while somebody types, so this is the one thing on it that has
   to be findable without reading. */
.scene-row.on { background: var(--surface); color: var(--text); }
.scene-row.on .scene-n { color: var(--accent); }
/* ---- end script-laptop ---- */

/* ==== MERGED FROM THE REBUILD — do not edit between these markers ==== */

/* ---- from the shell rebuild ---- */
/* ============================================================
   THE SHELL — the first ninety seconds, and everything around
   the work: the front door, the search, the crumb bar, the
   welcome, and the parts of the room that are not a document.

   Territory: web/index.html, app.js, views/{dashboard,search,
   help,trash,files,organise,overview}.js, lib/themes.js.

   Three new tokens are defined below. Everything else is off
   the existing scale; if a number is not on a rung it is
   because it is a photographic scrim or a drawn geometry, and
   it says so where it happens.
   ============================================================ */

:root {
  /* ---- three new rungs, all theme-driven -------------------------------
   * A theme is no longer twenty hex values: lib/themes.js writes a set of
   * existing tokens (radius, elevation, spacing, display face, tracking)
   * onto the root element when the theme changes, so the seven themes are
   * seven rooms rather than one room relit. Three of the knobs it turns had
   * no token yet, so they are declared here with the defaults that match
   * the Dark theme. Every one is read by the components in this file.
   */

  /* How heavy a display title sets. Paper themes want 600, the night
     themes want 700, and Cold mountains wants 500 with wide tracking. */
  --wt-title: 700;
  /* How heavy the small tracked uppercase label sets. */
  --wt-label: 700;
  /* The width of a separating line. Cold mountains draws hairlines; the
     rest draw a pixel. Stated so a theme can thin the whole interface. */
  --hair: 1px;
}

/* ============================================================
   1. NAVIGATION — arriving somewhere without a flicker
   ============================================================ */

/*
 * Waiting.
 *
 * Every route change used to mount `<div class="loading">Loading…</div>`,
 * which is a top-level child of `.main` and therefore ran the view-in
 * animation, then was replaced about ten milliseconds later because the
 * server is on this machine. So every click flashed the word "Loading".
 *
 * The router keeps the old view up until the new one is built. If a page
 * genuinely takes a moment, this thread appears across the top of the work
 * area — the honest signal, at the cost of no layout and no flash.
 */
.route-busy .main::before {
  content: '';
  position: absolute; inset: 0 0 auto; height: 2px;
  z-index: var(--z-sticky);
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: route-thread 1s var(--ease) infinite;
}
@keyframes route-thread {
  from { background-position: -45% 0; }
  to   { background-position: 145% 0; }
}
body.no-motion .route-busy .main::before { animation: none; background-position: 0 0; }
@media (prefers-reduced-motion: reduce) {
  .route-busy .main::before { animation: none; background-position: 0 0; }
}

/*
 * `.main` carries `scroll-behavior: smooth`, which is right for a jump to
 * an anchor and wrong for a route change — the app.js router suspends it
 * for the one assignment that resets the scroll. Left here as a note for
 * whoever owns this rule: it should arguably be scoped to a class rather
 * than living on the scroll container for everything.
 */

/*
 * A page that would not open.
 *
 * Not the dashed `.empty` box: that component draws an *absence*, and a
 * fault is not an absence. This says what broke, says the writing is fine,
 * and offers the only two useful doors.
 */
.fault {
  max-width: 46ch; margin: clamp(40px, 12vh, 120px) auto 0;
  text-align: center;
}
.fault-mark {
  display: inline-grid; place-items: center;
  width: 52px; height: 52px; margin-bottom: var(--sp-7);
  border-radius: var(--r-lg);
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border: var(--hair) solid color-mix(in srgb, var(--danger) 26%, transparent);
}
.fault-mark .ic { width: 22px; height: 22px; }
.fault h2 {
  margin: 0 0 var(--sp-5);
  font-family: var(--font-display); font-weight: var(--wt-title);
  font-size: var(--fz-heading); letter-spacing: -0.01em;
}
.fault-why {
  margin: 0 0 var(--sp-5); color: var(--text-2); font-size: var(--fz-body);
  font-family: var(--font-mono);
  padding: var(--sp-4) var(--sp-6);
  background: var(--surface); border: var(--hair) solid var(--border-soft);
  border-radius: var(--r-sm);
  word-break: break-word;
}
.fault-calm {
  margin: 0 0 var(--sp-8); color: var(--muted); font-size: var(--fz-ui);
  line-height: 1.6;
}

/* ============================================================
   2. THE SIDEBAR SEARCH BUTTON
   ============================================================ */

/*
 * It gained a drawn magnifier, so the three parts need real order: mark,
 * label, shortcut, with the label taking the slack. The label truncates
 * rather than wrapping — at 200% text size on a narrow sidebar it otherwise
 * became a two-line button, and a control that changes height with the text
 * size pushes everything under it about.
 */
.search-trigger { justify-content: flex-start; flex-wrap: nowrap; }
.search-trigger > span {
  flex: 1; min-width: 0; text-align: left;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.search-trigger > .ic { width: 15px; height: 15px; flex: none; opacity: .8; }
.search-trigger > kbd { flex: none; }
.search-trigger:hover > .ic { opacity: 1; }

/* ============================================================
   3. ONE SEARCH

   There were two: a command palette behind the sidebar button and a
   whole separate page behind the rail button wearing the identical
   tooltip, with a different result format, a different result set and
   a second kind-icon map that had already drifted from the first.

   There is one now. `.sr-*` is the shared result renderer; the palette
   and the page are the same list at two densities.
   ============================================================ */

.sr-list { display: flex; flex-direction: column; gap: 2px; }

.sr-hit {
  display: flex; align-items: flex-start; gap: var(--sp-5);
  padding: var(--sp-5) var(--sp-6);
  border-radius: var(--r-sm);
  cursor: pointer;
  border: var(--hair) solid transparent;
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.sr-hit:hover, .sr-hit.on { background: var(--raised); border-color: var(--border-soft); }
.sr-hit.on { border-color: color-mix(in srgb, var(--accent) 40%, transparent); }

/* The kind, drawn. One sprite, one map, in views/search.js. */
.sr-kind {
  flex: none; display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: var(--r-sm);
  background: var(--surface);
  border: var(--hair) solid var(--border-soft);
  color: var(--text-3);
}
.sr-hit:hover .sr-kind, .sr-hit.on .sr-kind { color: var(--accent); }
.sr-kind .ic { width: 15px; height: 15px; }

.sr-body { min-width: 0; flex: 1; }
.sr-line { display: flex; align-items: baseline; gap: var(--sp-4); flex-wrap: wrap; }
.sr-label {
  font-weight: 600; font-size: var(--fz-body); color: var(--text);
  overflow: hidden; text-overflow: ellipsis;
}
/* The word for the kind, whispered. The icon carries it at a glance; this
   is for when two icons look alike at 15px. */
.sr-kind-word {
  font-size: var(--fz-micro); text-transform: uppercase;
  letter-spacing: var(--track); font-weight: var(--wt-label);
  color: var(--faint);
}
.sr-detail { font-size: var(--fz-micro); color: var(--text-3); }
.sr-where {
  font-size: calc(12px * var(--fs)); color: var(--muted);
  margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/*
 * The snippet is the entire point of searching inside the writing: you do
 * not want a filename, you want the half-remembered line with your words
 * lit in it. Set in the reading serif, because it is prose.
 */
.sr-snippet {
  margin-top: var(--sp-3);
  font-family: var(--font-read);
  font-size: calc(13px * var(--fs)); line-height: 1.55;
  color: var(--text-2);
  padding-left: var(--sp-5);
  border-left: 2px solid var(--border);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
.sr-snippet mark {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  color: var(--text); border-radius: 3px; padding: 0 2px;
}

.sr-acts { flex: none; display: flex; gap: 2px; opacity: .5; transition: opacity var(--dur-1) var(--ease); }
.sr-hit:hover .sr-acts, .sr-hit:focus-within .sr-acts { opacity: 1; }
.sr-acts .ic { width: 14px; height: 14px; }

/* The last row: the same words, handed to the full page. */
.sr-more {
  margin-top: var(--sp-3);
  border-top: var(--hair) solid var(--border-soft);
  border-radius: 0 0 var(--r-sm) var(--r-sm);
}
.sr-more .sr-label { color: var(--accent); }
.sr-more .sr-acts { color: var(--accent); opacity: 1; align-self: center; }

.sr-hint { padding: var(--sp-9) var(--sp-7); text-align: center; }
.sr-hint b {
  display: block; font-size: var(--fz-body); font-weight: 600;
  color: var(--text-2); margin-bottom: var(--sp-3);
}
.sr-hint span {
  display: block; font-size: var(--fz-ui); color: var(--faint);
  max-width: 46ch; margin: 0 auto; line-height: 1.6;
}

/* ---- the palette: the search in a hurry --------------------------------- */

.palette { width: min(680px, 100%); padding: 0; }

.palette-field {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: 0 var(--sp-8);
  border-bottom: var(--hair) solid var(--border-soft);
}
.palette-field .ic { width: 17px; height: 17px; flex: none; color: var(--text-3); }
.palette-field input {
  flex: 1; width: 100%;
  font: inherit; font-family: var(--font-ui);
  font-size: calc(16px * var(--fs));
  padding: var(--sp-7) 0;
  background: none; border: none; color: var(--text);
}
.palette-field input:focus { outline: none; }
.palette-field input::placeholder { color: var(--faint); }

.palette-results { max-height: 54vh; overflow-y: auto; padding: var(--sp-3); }

.palette-foot {
  display: flex; gap: var(--sp-7); align-items: center;
  padding: var(--sp-4) var(--sp-8);
  border-top: var(--hair) solid var(--border-soft);
  font-size: var(--fz-micro); color: var(--faint);
}
.palette-foot kbd {
  font: inherit; font-size: var(--fz-micro);
  background: var(--raised); border: var(--hair) solid var(--border);
  border-radius: var(--r-xs); padding: 1px 5px; margin-right: 3px;
}

/* ---- the page: the same search, with room to read ----------------------- */

.search-page .page-head { margin-bottom: var(--sp-7); }

.search-field {
  display: flex; align-items: center; gap: var(--sp-5);
  padding: 0 var(--sp-6) 0 var(--sp-7);
  background: var(--surface);
  border: var(--hair) solid var(--border);
  border-radius: var(--r);
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.search-field:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.search-field > .ic { width: 18px; height: 18px; flex: none; color: var(--text-3); }
.search-field .input {
  flex: 1; border: none; background: none; box-shadow: none;
  font-size: calc(17px * var(--fs));
  padding: var(--sp-6) 0;
}
.search-field .input:focus { outline: none; box-shadow: none; border-color: transparent; }
.search-clear {
  flex: none; display: grid; place-items: center;
  width: 26px; height: 26px; border-radius: var(--r-pill);
  border: 0; background: none; color: var(--faint); cursor: pointer;
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.search-field:focus-within .search-clear,
.search-field:hover .search-clear { opacity: 1; pointer-events: auto; }
.search-clear:hover { color: var(--text); background: var(--raised); }
.search-clear .ic { width: 14px; height: 14px; }

.search-scope {
  margin: var(--sp-5) 0 0;
  font-size: var(--fz-ui); color: var(--muted); min-height: 1.4em;
}

.sr-page { gap: var(--sp-3); }
.sr-page .sr-hit {
  padding: var(--sp-6) var(--sp-7);
  background: var(--surface);
  border-color: var(--border-soft);
  border-radius: var(--r);
}
.sr-page .sr-hit:hover { border-color: var(--border); background: var(--raised); }
.sr-page .sr-kind { width: 34px; height: 34px; background: var(--raised); }

/*
 * Nothing typed, or nothing found. Both describe the reward rather than the
 * void, and neither is a dashed box.
 */
.search-invite {
  text-align: center;
  padding: var(--sp-10) var(--sp-7);
  max-width: 54ch; margin: 0 auto;
}
.search-invite-mark {
  display: inline-grid; place-items: center;
  width: 54px; height: 54px; margin-bottom: var(--sp-7);
  border-radius: var(--r-lg);
  color: var(--accent);
  background: var(--accent-soft);
  border: var(--hair) solid color-mix(in srgb, var(--accent) 24%, transparent);
}
.search-invite-mark .ic { width: 23px; height: 23px; }
.search-invite h3 {
  margin: 0 0 var(--sp-5);
  font-family: var(--font-display); font-weight: var(--wt-title);
  font-size: var(--fz-heading); color: var(--text);
}
.search-invite p {
  margin: 0; color: var(--muted); font-size: var(--fz-body);
  line-height: 1.65;
}

/* Three dots, because "Looking through everything" is a real wait on a
   library of a thousand files and a panel saying so is not. */
.search-working {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-8) var(--sp-7);
  color: var(--muted); font-size: var(--fz-ui);
}
.search-working span {
  width: 6px; height: 6px; border-radius: var(--r-circle);
  background: var(--accent);
  animation: search-pulse 1.1s var(--ease) infinite;
}
.search-working span:nth-child(2) { animation-delay: .14s; }
.search-working span:nth-child(3) { animation-delay: .28s; }
.search-working em { font-style: normal; margin-left: var(--sp-3); }
@keyframes search-pulse { 0%, 100% { opacity: .25; } 50% { opacity: 1; } }
body.no-motion .search-working span { animation: none; opacity: .7; }
@media (prefers-reduced-motion: reduce) {
  .search-working span { animation: none; opacity: .7; }
}

/* ============================================================
   4. THE FRONT DOOR — day one
   ============================================================ */

.home-first .home-hero { min-height: 40vh; }

/*
 * The hero with nothing behind it.
 *
 * `.has-art` was set on the hero and never styled, and the darkening veil
 * was laid down whether or not there was a photograph to darken — so the
 * commonest case, a studio with no poster art, spent its first screen
 * dimming a colour. Art and no-art are two different treatments now, and
 * the no-art one is *drawn*: a bloom in the leading title's own colour,
 * with the forge's struck medal bled off the right edge.
 */
.home-hero.no-art .home-hero-veil { display: none; }

.home-hero-glow {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(70% 120% at 12% 110%,
      color-mix(in srgb, var(--pc, var(--accent)) 26%, transparent), transparent 62%),
    radial-gradient(60% 90% at 88% -10%,
      color-mix(in srgb, var(--accent-2) 20%, transparent), transparent 60%);
}

/* The medal, huge and half off the edge: an emblem, not a picture. */
.home-hero-forge {
  position: absolute; right: clamp(-40px, -2vw, 0px); bottom: -8%;
  width: clamp(200px, 26vw, 380px); height: auto;
  image-rendering: pixelated;
  opacity: .2;
  transform: rotate(-4deg);
  pointer-events: none;
  filter: drop-shadow(0 18px 40px rgb(0 0 0 / .5));
}

/*
 * The hero with real art behind it. The scrim runs bottom-left, because
 * that is where the words are, and it is stated in absolute black rather
 * than a token on purpose: it sits over a photograph the app did not
 * choose, and it has to hold the title legible over a white sky.
 */
.home-hero.has-art .home-hero-veil {
  background:
    linear-gradient(180deg, rgb(0 0 0 / .3), transparent 30%,
      color-mix(in srgb, var(--bg) 94%, transparent) 98%),
    linear-gradient(75deg, rgb(0 0 0 / .78) 0%, rgb(0 0 0 / .42) 42%, transparent 72%);
}
.home-hero.has-art .home-hero-art { object-position: center 28%; }
.home-hero.has-art .home-title,
.home-hero.has-art .home-logline { color: #fff; }
.home-hero.has-art .home-logline { color: rgb(255 255 255 / .82); }

.home-title { font-weight: var(--wt-title); }

/* What the leading title is, and when it was last touched. The hero used
   to state a name and a logline and nothing else, which told a returning
   writer nothing they did not already know. */
.home-hero-tag {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: var(--sp-6);
  font-size: var(--fz-micro); text-transform: uppercase;
  letter-spacing: var(--track); font-weight: var(--wt-label);
  color: var(--text-3);
}
.home-hero.has-art .home-hero-tag { color: rgb(255 255 255 / .72); }
.home-hero-tag .ic { width: 13px; height: 13px; color: var(--pc, var(--accent)); }
.home-hero.has-art .home-hero-tag .ic { color: rgb(255 255 255 / .8); }

/* ---- the shelf controls ------------------------------------------------- */

/*
 * Sort and layout, above the shelves.
 *
 * The shelf came off the database in creation order, had no way to be
 * re-sorted, and the grid/shelf preference was still being written to the
 * server on every change with nothing left in the app that read it. Both
 * controls are real now and both are remembered by the studio.
 */
.home-controls {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: var(--sp-8) clamp(24px, 5vw, 72px) 0;
  max-width: none;
}
.home-controls-count {
  font-size: var(--fz-micro); text-transform: uppercase;
  letter-spacing: var(--track); font-weight: var(--wt-label);
  color: var(--text-3);
}
.home-sort { display: flex; align-items: center; gap: var(--sp-4); }
.home-sort > span {
  font-size: var(--fz-micro); text-transform: uppercase;
  letter-spacing: var(--track); font-weight: var(--wt-label); color: var(--text-3);
}
.select-sm {
  width: auto; font-size: var(--fz-ui);
  /*
   * 34px of clearance on the right, not 28. The arrow is drawn as a background
   * image whose centre sits 13px in, so it occupies roughly the last 20 - and
   * with 28px of padding the longest option ("Recently worked on") ran under it.
   * Logan: "the drop down arrow thing cuts into the words and overlaps them."
   *
   * Padding rather than a narrower arrow: the arrow is the same one every other
   * select in the app draws, and shrinking it here would make this one control
   * quietly different from all of them.
   */
  padding: var(--sp-3) 34px var(--sp-3) var(--sp-5);
  background-position: calc(100% - 13px) 50%, calc(100% - 9px) 50%;
  border-radius: var(--r-sm);
}

/* A pair of drawn buttons, one lit. */
.seg-toggle {
  display: flex; gap: 2px; padding: 2px;
  background: var(--bg); border: var(--hair) solid var(--border);
  border-radius: var(--r-sm);
}
.seg-toggle button {
  display: grid; place-items: center;
  width: 30px; height: 26px; border: 0; border-radius: var(--r-xs);
  background: none; color: var(--text-3); cursor: pointer;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.seg-toggle button:hover { color: var(--text-2); background: var(--raised); }
.seg-toggle button.on { background: var(--raised-2); color: var(--text); }
.seg-toggle .ic { width: 15px; height: 15px; }

/* ---- the shelves -------------------------------------------------------- */

.home-row-head h2 { font-weight: var(--wt-label); }
.home-row-count {
  font-size: var(--fz-micro); color: var(--faint);
  font-variant-numeric: tabular-nums;
}
.home-row-head .spacer { flex: 1; }

/*
 * A strip that says there is more.
 *
 * Twelve titles in a row that scrolls sideways with no chevron and no fade
 * looks finished at the eighth. The arrows only exist while there is
 * somewhere to go — app.js measures on scroll and on resize and sets
 * `.can-left` / `.can-right` — and the fades are on the container so they
 * survive whatever shape the cards are.
 */
.shelf-strip { position: relative; }

.shelf-strip::before,
.shelf-strip::after {
  content: ''; position: absolute; top: 0; bottom: 14px; width: 64px;
  pointer-events: none; z-index: 2;
  opacity: 0; transition: opacity var(--dur-2) var(--ease);
}
.shelf-strip::before {
  left: 0;
  background: linear-gradient(90deg, var(--bg) 10%, transparent);
}
.shelf-strip::after {
  right: 0;
  background: linear-gradient(270deg, var(--bg) 10%, transparent);
}
.shelf-strip.can-left::before, .shelf-strip.can-right::after { opacity: 1; }

.shelf-arrow {
  position: absolute; top: 34%; z-index: 3;
  display: grid; place-items: center;
  width: 36px; height: 36px; border-radius: var(--r-pill);
  background: var(--raised); color: var(--text);
  border: var(--hair) solid var(--border);
  box-shadow: var(--elev-2);
  cursor: pointer;
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-1) var(--ease);
}
.shelf-arrow.left { left: 4px; }
.shelf-arrow.right { right: 4px; }
.shelf-arrow:hover { transform: scale(1.08); border-color: var(--accent); }
.shelf-arrow .ic { width: 17px; height: 17px; }
.shelf-strip.can-left .shelf-arrow.left,
.shelf-strip.can-right .shelf-arrow.right { opacity: 1; pointer-events: auto; }

/* The grid: every title at once, for when the shelf is a search. */
.shelf-strip.as-grid .home-shelf {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--sp-8) var(--sp-7);
  overflow: visible;
}
.shelf-strip.as-grid .shelf-arrow { display: none; }
.shelf-strip.as-grid::before, .shelf-strip.as-grid::after { display: none; }
.shelf-strip.as-grid .shelf-item { width: auto; }
.shelf-strip.as-grid .shelf-poster,
.shelf-strip.as-grid .saga-tile { width: 100%; height: auto; aspect-ratio: 2 / 3; }

/* ---- one title on the shelf --------------------------------------------- */

/*
 * The caption is always there.
 *
 * It used to live inside the poster, revealed on hover, which is fine for a
 * wall of recognisable one-sheets and useless for a shelf of titles that
 * have no artwork yet — you could not read your own filmography without
 * running the mouse along it. Poster above, name below, like a shelf.
 */
.shelf-item {
  flex: none; width: 176px;
  display: flex; flex-direction: column; gap: var(--sp-5);
  cursor: pointer; scroll-snap-align: start;
}
.home-shelf .shelf-item .shelf-poster { width: 100%; height: auto; }

.shelf-cap { min-width: 0; }
.shelf-cap b {
  display: block; font-size: var(--fz-ui); font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: color var(--dur-1) var(--ease);
}
.shelf-item:hover .shelf-cap b { color: var(--pc, var(--accent)); }
.shelf-cap em {
  display: block; font-style: normal; margin-top: 1px;
  font-size: var(--fz-micro); color: var(--faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.shelf-pin {
  position: absolute; top: var(--sp-4); right: var(--sp-4); z-index: 2;
  display: grid; place-items: center;
  width: 22px; height: 22px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--bg-deep) 78%, transparent);
  color: var(--pc, var(--accent));
  backdrop-filter: blur(4px);
}
.shelf-pin .ic { width: 12px; height: 12px; }

/*
 * A title with no artwork.
 *
 * Twelve of these used to be twelve flat coloured rectangles — the app
 * admitting it has nothing to show. The forge draws a struck medal from the
 * title itself, the same mark every time for the same name, so an
 * un-postered shelf reads as a set of things rather than a set of gaps.
 */
/* Scoped past the base `.shelf-spine`, which centres a single title in a
   grid; this one has a mark above the words and must not depend on which
   block the merge happens to put last. */
.shelf-poster > .shelf-spine {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-6); padding: var(--sp-7);
}
/*
 * Written as `img.shelf-forge`, and it has to be.
 *
 * `.shelf-poster img` sets width and height to 100% with object-fit: cover,
 * which is right for a one-sheet filling its frame and wrong for a struck
 * medal that has to stay square. That rule is (0,1,1) and a bare
 * `.shelf-forge` is (0,1,0), so the medal came out stretched to the full
 * height of the card. Matching the element type wins it back.
 */
.shelf-poster img.shelf-forge {
  width: 56%; max-width: 96px; height: auto;
  object-fit: contain;
  image-rendering: pixelated;
  filter: drop-shadow(0 6px 16px rgb(0 0 0 / .45));
  transition: transform var(--dur-2) var(--ease);
}
.shelf-item:hover .shelf-forge { transform: translateY(-3px) scale(1.04); }
.shelf-poster > .shelf-spine b {
  font-family: var(--font-display); font-weight: var(--wt-title);
  font-size: calc(15px * var(--fs)); line-height: 1.25;
  letter-spacing: -0.01em;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The whole item is the hover target now, caption included — the lift used
   to fire only from the poster, so hovering a title's own name did nothing. */
.shelf-item:hover .shelf-poster {
  transform: translateY(-4px) scale(1.015);
  border-color: color-mix(in srgb, var(--pc) 50%, var(--border));
  box-shadow: var(--elev-3), 0 0 40px color-mix(in srgb, var(--pc, var(--accent)) 22%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  .shelf-item:hover .shelf-poster { transform: none; }
  .shelf-item:hover .shelf-forge { transform: none; }
}

/* ---- the franchise shelf ------------------------------------------------ */

/*
 * A franchise is not a poster.
 *
 * The home page did not admit franchises existed — the saga board and the
 * cross-title continuity check, the marquee feature of the app, reachable
 * only from one line in the sidebar. A franchise is several titles standing
 * together, so its card is exactly that: a stack of spines in their own
 * colours with the saga's crest struck on the front.
 */
/*
 * Named `saga-tile`, not `saga-card`.
 *
 * `.saga-card` is the franchise BOARD's card (styles.css, around line 4190):
 * a 178px column you drag. This is the home shelf's tile, a 2:3 poster. Two
 * components, one name, and this file is merged in LATER - so the board
 * inherited `aspect-ratio: 2/3` and every card on it measured 1076x1614px,
 * which pushed the whole saga board off the bottom of the screen.
 *
 * Rename here and nowhere else: an edit to the merged block in styles.css is
 * thrown away the next time `merge-css.py` runs, which is how this came back
 * once already.
 */
.saga-tile {
  position: relative; overflow: hidden;
  width: 100%; aspect-ratio: 2 / 3;
  border-radius: var(--r-sm);
  border: var(--hair) solid var(--border-soft);
  background:
    linear-gradient(165deg,
      color-mix(in srgb, var(--pc) 26%, var(--surface)),
      color-mix(in srgb, var(--pc) 8%, var(--bg-deep)));
  display: flex; align-items: flex-end;
  padding: var(--sp-7) var(--sp-6) var(--sp-6);
  transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease),
    border-color var(--dur-2) var(--ease);
}
.shelf-item:hover .saga-tile {
  transform: translateY(-4px);
  border-color: color-mix(in srgb, var(--pc) 55%, var(--border));
  box-shadow: var(--elev-3), 0 0 40px color-mix(in srgb, var(--pc) 24%, transparent);
}

/* Books stood on end, leaning the way books do. */
.saga-spines {
  display: flex; align-items: flex-end; gap: 5px;
  width: 100%; height: 74%;
}
.saga-spine {
  flex: 1; min-width: 0; height: calc(84% + var(--i) * 3%);
  border-radius: 2px 2px 0 0;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--sc) 88%, #000 0%),
    color-mix(in srgb, var(--sc) 55%, #000));
  box-shadow: inset -2px 0 0 rgb(0 0 0 / .28), inset 1px 0 0 rgb(255 255 255 / .12);
  display: grid; place-items: center;
  overflow: hidden;
  transform-origin: bottom center;
  transform: rotate(calc((var(--i) - 2) * 0.8deg));
  transition: transform var(--dur-2) var(--ease);
}
.shelf-item:hover .saga-spine { transform: rotate(calc((var(--i) - 2) * 1.6deg)) translateY(-2px); }
.saga-spine b {
  writing-mode: vertical-rl; text-orientation: mixed;
  font-family: var(--font-display); font-weight: 600;
  font-size: calc(10.5px * var(--fs));
  letter-spacing: .04em;
  color: rgb(255 255 255 / .92);
  text-shadow: 0 1px 3px rgb(0 0 0 / .5);
  max-height: 92%; overflow: hidden;
  padding: var(--sp-4) 0;
}
.saga-spine.empty-spine {
  background: color-mix(in srgb, var(--pc) 14%, var(--surface));
  border: var(--hair) dashed color-mix(in srgb, var(--pc) 40%, var(--border));
}
.saga-spine.empty-spine b { color: var(--text-3); text-shadow: none; }

/* The crest, struck across the front of the stack. */
.saga-crest {
  position: absolute; left: 50%; bottom: 12%;
  width: 46%; max-width: 78px; height: auto;
  transform: translateX(-50%);
  image-rendering: pixelated;
  filter: drop-shadow(0 8px 20px rgb(0 0 0 / .6));
  transition: transform var(--dur-2) var(--ease);
}
.shelf-item:hover .saga-crest { transform: translateX(-50%) translateY(-4px) scale(1.05); }

body.no-motion .saga-spine,
body.no-motion .saga-crest,
body.no-motion .shelf-forge { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .saga-spine, .saga-crest, .shelf-forge, .saga-tile, .shelf-arrow { transition: none; }
  .shelf-item:hover .saga-spine { transform: rotate(calc((var(--i) - 2) * 0.8deg)); }
}

/* ---- the back of the room ----------------------------------------------- */

/* The recent list wore emoji in its leading column. Same shape, drawn. */
.row-mark {
  width: 34px; height: 34px; flex: none;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  background: var(--raised); color: var(--text-3);
  border: var(--hair) solid var(--border-soft);
}
.row-mark .ic { width: 15px; height: 15px; }
.row.clickable:hover .row-mark { color: var(--accent); }

.todo-panel { display: flex; flex-direction: column; gap: var(--sp-2); }
.todo-line {
  display: flex; align-items: flex-start; gap: var(--sp-5);
  padding: var(--sp-4) var(--sp-3);
  border-radius: var(--r-sm); cursor: pointer;
  font-size: var(--fz-ui); color: var(--text-2);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.todo-line:hover { background: var(--raised); color: var(--text); }
.todo-ring {
  flex: none; width: 13px; height: 13px; margin-top: 3px;
  border-radius: var(--r-circle);
  border: 1.5px solid var(--faint);
  transition: border-color var(--dur-1) var(--ease);
}
.todo-line:hover .todo-ring { border-color: var(--accent); }
.todo-line em {
  display: block; font-style: normal; margin-top: 1px;
  font-size: var(--fz-micro); color: var(--faint);
}

/* ============================================================
   5. DAY ONE — the room with nothing in it

   The old first run was: a grand sentence with no buttons under
   it (the whole actions block sat inside the `featured ?`
   branch), then the word "Nothing" inside `.empty` — the app's
   own drawing of absence, dashed border and all, patched with
   five inline styles — then a row of five gradient-clipped
   zeros. Three beats, every one of them about what you have not
   got. What follows is a room instead.
   ============================================================ */

.start-band {
  padding: var(--sp-10) clamp(24px, 5vw, 72px) 0;
  max-width: var(--content-max); margin-inline: auto;
}
.start-head { margin-bottom: var(--sp-8); max-width: 62ch; }
.start-head h2 {
  margin: 0 0 var(--sp-4);
  font-family: var(--font-display); font-weight: var(--wt-title);
  font-size: var(--fz-title); letter-spacing: -0.015em;
}
.start-head p { margin: 0; color: var(--muted); font-size: var(--fz-lead); line-height: 1.6; }

/*
 * The three doors in. Real cards with a drawn mark, a promise and the verb
 * they perform — not a dashed box with an emoji in it.
 *
 * Scoped to `.start-band` on purpose: `.start-choice` is also the "what shall
 * I make?" card inside a project (web/views/project.js), which is somebody
 * else's territory and must not move underneath them.
 */
.start-band .start-choices { gap: var(--sp-7); }
.start-band .start-choice {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--sp-4);
  padding: var(--sp-8);
  border-color: var(--border-soft);
  transition: transform var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease),
    box-shadow var(--dur-2) var(--ease);
}
.start-band .start-choice::after {
  content: ''; position: absolute; inset: auto -30% -60% auto;
  width: 220px; height: 220px; border-radius: var(--r-circle);
  background: radial-gradient(closest-side, var(--accent-soft), transparent);
  opacity: 0; transition: opacity var(--dur-2) var(--ease);
  pointer-events: none;
}
.start-band .start-choice:hover {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
  box-shadow: var(--elev-3);
}
.start-band .start-choice:hover::after { opacity: 1; }

.start-mark {
  display: grid; place-items: center;
  width: 42px; height: 42px; margin-bottom: var(--sp-3);
  border-radius: var(--r);
  background: var(--accent-soft); color: var(--accent);
  border: var(--hair) solid color-mix(in srgb, var(--accent) 22%, transparent);
}
.start-mark .ic { width: 20px; height: 20px; }

.start-band .start-choice b {
  display: block; font-family: var(--font-display);
  font-size: var(--fz-lead); font-weight: var(--wt-title);
  letter-spacing: -0.01em; margin: 0;
}
.start-band .start-choice > span:not(.start-mark) {
  display: block; color: var(--muted);
  font-size: var(--fz-ui); line-height: 1.6;
}
/* The verb, so the card says what pressing it does. */
.start-band .start-choice em {
  display: flex; align-items: center; gap: var(--sp-3);
  margin-top: auto; padding-top: var(--sp-5);
  font-style: normal; font-size: var(--fz-ui); font-weight: 600;
  color: var(--accent);
}
.start-band .start-choice em .ic {
  width: 14px; height: 14px;
  transition: transform var(--dur-1) var(--ease);
}
.start-band .start-choice:hover em .ic { transform: translateX(3px); }
body.no-motion .start-band .start-choice,
body.no-motion .start-band .start-choice em .ic { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .start-band .start-choice:hover { transform: none; }
  .start-band .start-choice:hover em .ic { transform: none; }
}

/* ---- the proof: an actual page ------------------------------------------ */

/*
 * The first thing the software should be seen doing is setting a scene
 * properly. Nothing in the old first run showed a page at all.
 */
.first-proof {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 460px);
  gap: clamp(24px, 4vw, 64px); align-items: center;
  padding: var(--sp-10) clamp(24px, 5vw, 72px) 0;
  max-width: var(--content-max); margin-inline: auto;
}
.first-proof-words h2 {
  margin: var(--sp-4) 0 var(--sp-5);
  font-family: var(--font-display); font-weight: var(--wt-title);
  font-size: var(--fz-title); letter-spacing: -0.015em; line-height: 1.15;
}
.first-proof-words p {
  margin: 0; color: var(--muted); font-size: var(--fz-lead); line-height: 1.7;
  max-width: 52ch;
}
.first-proof-acts { display: flex; gap: var(--sp-4); flex-wrap: wrap; margin-top: var(--sp-8); }

@media (max-width: 900px) {
  .first-proof { grid-template-columns: minmax(0, 1fr); }
}

/*
 * A screenplay page, drawn to the real geometry: the same monospace the
 * editor sets, the same indents a script actually has. The indents are in
 * `em` off the mono face rather than on the spacing scale, because they are
 * a printed page's measurements and must hold their proportions whatever
 * the interface is doing.
 */
.page-preview {
  position: relative;
  border-radius: var(--r-sm);
  box-shadow: var(--elev-3);
  overflow: hidden;
  background: var(--page-bg);
  border: var(--hair) solid var(--border-soft);
  /* Laid on the desk rather than pinned to the screen. Static: it is a
     photograph of the product, not an animation of one. */
  transform: rotate(-0.7deg);
}
.page-preview.in-tour { transform: none; }

.page-preview-sheet {
  padding: clamp(20px, 4%, 38px) clamp(18px, 7%, 54px);
  font-family: var(--font-mono);
  font-size: calc(11.5px * var(--fs));
  line-height: 1.5;
  color: var(--page-ink);
}
.page-preview-sheet > * { margin-bottom: 1em; }
/* The indents are in `em` off the monospace face, not on the spacing scale:
   they are a printed page's measurements and must hold their proportions
   whatever the text-size control is doing. */
.pp-slug { text-transform: uppercase; font-weight: 700; letter-spacing: .02em; }
.pp-char { margin-left: 7.5em; text-transform: uppercase; margin-bottom: 0; }
.pp-paren { margin-left: 5.8em; margin-bottom: 0; opacity: .82; }
.pp-dial { margin-left: 3.6em; margin-right: 3em; }
.pp-trans { text-align: right; text-transform: uppercase; margin-bottom: 0; }

.page-preview-foot {
  display: flex; align-items: center; gap: var(--sp-6);
  padding: var(--sp-4) var(--sp-7);
  border-top: var(--hair) solid var(--border-soft);
  background: var(--surface);
  font-size: var(--fz-micro); color: var(--faint);
  text-transform: uppercase; letter-spacing: var(--track);
  font-weight: var(--wt-label);
}
.page-preview-foot span:last-child {
  margin-left: auto; font-family: var(--font-mono); text-transform: none; letter-spacing: 0;
}

/* ============================================================
   6. THE WELCOME

   It used to open by asking "How should it look?" — a question
   about decoration, put to somebody who has not yet been shown
   one thing the software does. The first question is now the
   one a colleague would ask; the theme comes second, against a
   real page; and the text-size step is gone, because that
   control is permanently in the sidebar.
   ============================================================ */

/* The tour is the widest thing in the app that is still a dialog. `.modal`
   already caps its own height; this only stops a three-column step from
   growing past a comfortable read. */
.tour .modal-body { max-height: min(560px, 60vh); }
.tour-h {
  margin: 0 0 var(--sp-4);
  font-family: var(--font-display); font-weight: var(--wt-title);
  font-size: var(--fz-heading); letter-spacing: -0.01em;
}
.tour-sub {
  margin: 0 0 var(--sp-8); color: var(--muted);
  font-size: var(--fz-ui); line-height: 1.6; max-width: 62ch;
}
.tour-note {
  margin: var(--sp-8) 0 0; color: var(--faint);
  font-size: var(--fz-ui); line-height: 1.6;
}
.tour-count { font-variant-numeric: tabular-nums; font-size: var(--fz-micro); }

/* Step one: what are you working on. */
.tour-start {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--sp-6);
}
.tour-choice {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-7);
  text-align: left;
  background: var(--surface);
  border: var(--hair) solid var(--border-soft);
  border-radius: var(--r);
  color: var(--text); font: inherit; cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease),
    transform var(--dur-1) var(--ease);
}
.tour-choice:hover { border-color: var(--border); transform: translateY(-2px); }
.tour-choice.on {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
.tour-choice-mark {
  display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: var(--r-sm);
  background: var(--raised); color: var(--accent);
  margin-bottom: var(--sp-2);
}
.tour-choice.on .tour-choice-mark { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.tour-choice-mark .ic { width: 17px; height: 17px; }
.tour-choice b { font-size: var(--fz-body); font-weight: 650; }
.tour-choice > span:not(.tour-choice-mark) {
  font-size: var(--fz-ui); color: var(--muted); line-height: 1.55;
}

/*
 * Step two: the theme, against a page.
 *
 * Picking a theme used to be picking a swatch. The page beside the swatches
 * is a real one and it repaints as each theme goes on, so the choice is
 * made by looking at the thing rather than at a colour chip.
 */
.tour-look {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
  gap: var(--sp-8); align-items: start;
}
.tour-themes { align-content: start; }
.tour-stage {
  position: relative;
  padding: var(--sp-6);
  border-radius: var(--r);
  background:
    radial-gradient(120% 100% at 50% 0%, var(--accent-soft), transparent 62%),
    var(--bg-deep);
  border: var(--hair) solid var(--border-soft);
}
.tour-stage .page-preview { box-shadow: var(--elev-2); }
.tour-stage .page-preview-sheet { font-size: calc(9px * var(--fs)); padding: 16px 22px; }
.tour-stage .page-preview-foot { padding: 5px 12px; }

@media (max-width: 760px) {
  .tour-look { grid-template-columns: minmax(0, 1fr); }
  .tour-stage { display: none; }
}

/* ============================================================
   7. ODDS AND ENDS
   ============================================================ */

/*
 * Two marks that used to be emoji and are now drawn, in rules that sized them
 * with `font-size` because a glyph is text. `.ic` is 15px unless told
 * otherwise, so both need their real size stating.
 */
.lock-emoji {
  display: grid; place-items: center;
  color: var(--accent);
  margin-bottom: var(--sp-5);
}
.lock-emoji .ic { width: 30px; height: 30px; }

.org-kind-btn { display: grid; place-items: center; }
.org-kind-btn .ic { width: 16px; height: 16px; }

/* A chip that names a kind without shouting it. */
.chip.quiet {
  background: transparent; color: var(--text-3);
  border-color: var(--border-soft);
  font-size: var(--fz-micro); text-transform: uppercase;
  letter-spacing: .06em; font-weight: var(--wt-label);
}

/* The facet pills gained drawn marks where they used to carry emoji. */
.filter-chip .ic { width: 13px; height: 13px; margin-right: 5px; vertical-align: -2px; }

/* The bin and the file list state their kind with the sprite, at the same
   size the rest of the app draws a leading mark. */
.file-line-title .chip.quiet { flex: none; }

/* ---- from the craft rebuild ---- */
/* ============================================================
   CRAFT — the shared row vocabulary, and the structure and
   business tools built on it.

   Owned by the components engineer. Everything here is written
   against the existing tokens; the two new ones are declared at
   the top and flagged in the report.

   Sections
     1. New tokens
     2. The lane: rows, marks, drawn figures, empty states
     3. Notes, to-do, tabs
     4. The deck: container queries and eight architectures
     5. Production: paperwork a first AD would carry
     6. The corkboard and the arc grid, as split panes
     7. The beat sheet: page numbers and scene bindings
     8. Odds: file editor, scratchpad, importer, submissions, web
   ============================================================ */

:root {
  /* A hairline that reads on every one of the seven themes. --border-soft is
     already the quietest line the app has, but between rows in a shared
     surface even that is a fence; this is half of it.

     Named --lane-hair rather than --hair because the shell engineer's staging
     file already uses --hair for a border WIDTH. Two different types under one
     name would have broken both on merge. */
  --lane-hair: color-mix(in srgb, var(--border-soft) 62%, transparent);

  /* The width of a drawn figure column in a row. Fixed, so six rows of
     different lengths still line their numbers up — the whole point of
     drawing the figure rather than writing the number. */
  --fig-w: 112px;
}

/* ============================================================
   2. THE LANE

   A list of works, not a table of records. One surface, hairlines
   between rows, and a fourth column that draws what the row knows.
   ============================================================ */

.lane {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  overflow: hidden;
  /* The rows share one surface, so the shadow belongs to the lane. */
  box-shadow: var(--elev-1);
}

.lane-row {
  position: relative;
  display: flex; align-items: center; gap: 14px;
  padding: 12px 16px 12px 15px;
  transition: background var(--dur-1) var(--ease);
}
.lane-row + .lane-row { border-top: 1px solid var(--lane-hair); }
.lane-row.clickable { cursor: pointer; }

/*
 * The accent grows from the left edge rather than the row lighting up whole.
 * A list of nine rows where the hovered one changes colour flickers as the
 * pointer crosses it; a bar that grows reads as "this one" without the flash.
 */
.lane-row::before {
  content: ''; position: absolute; left: 0; top: 6px; bottom: 6px;
  width: 2px; border-radius: 0 2px 2px 0;
  background: var(--accent);
  transform: scaleY(0); transform-origin: center;
  transition: transform var(--dur-1) var(--ease);
}
.lane-row.clickable:hover { background: var(--raised); }
.lane-row.clickable:hover::before,
.lane-row:focus-within::before { transform: scaleY(1); }

/* A file with no file in it yet. Present, but not pretending to be finished. */
.lane-row.waiting .lane-mark { color: var(--film); background: color-mix(in srgb, var(--film) 12%, transparent); }

.lane-mark {
  flex: none; width: 34px; height: 34px; border-radius: var(--r-sm);
  display: grid; place-items: center;
  background: var(--raised); color: var(--text-3);
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.lane-mark .ic { width: 17px; height: 17px; }
.lane-row.clickable:hover .lane-mark { color: var(--accent); background: var(--accent-soft); }

.lane-body { flex: 1; min-width: 0; }
.lane-name {
  display: flex; align-items: center; gap: 8px;
  font-size: calc(14px * var(--fs)); font-weight: 550; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lane-note {
  margin-top: 3px; color: var(--muted); font-size: calc(12.5px * var(--fs));
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lane-quiet { color: var(--faint); font-size: calc(11.5px * var(--fs)); font-style: italic; }

/* ---- the drawn figure ---------------------------------------------------- */

.lane-fig {
  flex: none; width: var(--fig-w);
  display: flex; align-items: center; gap: 9px; justify-content: flex-end;
  color: var(--text-3);
}
.lane-num {
  font-family: var(--font-mono); font-size: calc(12.5px * var(--fs));
  color: var(--text-2); font-weight: 400; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.lane-num span { color: var(--faint); font-size: .82em; margin-left: 1px; }

/* How long it is, against how long this format usually runs. */
.ribbon { width: 56px; height: 20px; flex: none; }
.ribbon-track { fill: currentColor; opacity: .22; }
.ribbon-band  { fill: var(--ok); opacity: .16; }
.ribbon-fill  { fill: var(--text-2); }
.ribbon.onside .ribbon-fill { fill: var(--ok); }
.ribbon.short  .ribbon-fill { fill: var(--text-3); }
.ribbon.over   .ribbon-fill { fill: var(--film); }
.ribbon.over  ~ .lane-num { color: var(--film); }

/* A beat sheet as its beats. */
.ladder { display: flex; align-items: flex-end; gap: 2px; height: 18px; }
.ladder .rung {
  width: 3px; height: 9px; border-radius: 2px;
  background: currentColor; opacity: .24;
}
.ladder .rung.part { height: 13px; opacity: .5; background: var(--accent); }
.ladder .rung.on   { height: 18px; opacity: 1; background: var(--accent); }

/* A deck as the strip of pages it is. */
.strip { display: flex; align-items: center; gap: 2px; }
.strip i {
  width: 5px; height: 14px; border-radius: 1px;
  background: currentColor; opacity: .3;
}
.strip i.first { background: var(--accent); opacity: .8; }
.strip u {
  text-decoration: none; font-family: var(--font-mono);
  font-size: calc(10px * var(--fs)); color: var(--faint); margin-left: 3px;
}

/* A document as the stack of versions behind it. */
.sheaf { position: relative; width: 22px; height: 20px; }
.sheaf i {
  position: absolute; inset: 0 auto 0 0;
  width: 15px; height: 19px; border-radius: 2px;
  border: 1px solid currentColor; opacity: calc(.9 - var(--i) * .2);
  transform: translateX(calc(var(--i) * 2.5px)) translateY(calc(var(--i) * -1px));
  background: var(--surface);
}

/* ---- the head of a list -------------------------------------------------- */

.section-head.lane-head { align-items: baseline; gap: 10px; }
.section-head.lane-head h2 {
  font-family: var(--font-display); font-weight: 500;
  font-size: calc(16px * var(--fs)); letter-spacing: -.005em;
}
.section-head.lane-head .sub { font-family: var(--font-ui); }
.section-head .btn .ic { width: 14px; height: 14px; }

/* ---- the empty state ----------------------------------------------------- */

/*
 * The ghost of what will be there.
 *
 * An empty screen has exactly one job: say what this tab is FOR. A faint
 * drawing of the rows you are about to have does that in less time than the
 * sentence under it takes to read.
 */
.lane-empty {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 34px 24px 30px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
}
.lane-ghost { width: min(260px, 70%); margin-bottom: 20px; }
.lane-ghost svg { display: block; width: 100%; height: auto; }
.lane-ghost rect, .lane-ghost circle { fill: var(--raised); }
.lane-ghost .g-mark { fill: var(--accent); opacity: .3; }
.lane-ghost .g-line { fill: var(--text-3); opacity: .38; }
.lane-ghost .g-line.dim { opacity: .18; }

.lane-empty h3 {
  margin: 0 0 8px; font-family: var(--font-display); font-weight: 500;
  font-size: calc(17px * var(--fs)); color: var(--text); letter-spacing: -.01em;
}
.lane-empty p {
  margin: 0 0 18px; max-width: 46ch; line-height: 1.62;
  font-size: calc(13.5px * var(--fs)); color: var(--muted);
}
.lane-empty-hint {
  margin-top: 12px; font-size: calc(11.5px * var(--fs)); color: var(--faint);
}

/* On a narrow window the figure column is the first thing worth losing:
   the row still says everything, just in words. */
@media (max-width: 780px) {
  .lane-fig { display: none; }
  .lane-row { padding: 11px 12px; gap: 11px; }
}

/* ============================================================
   3. NOTES, TO-DO, TABS
   ============================================================ */

/*
 * A note is a piece of paper, so it gets a paper's proportions and a torn
 * top-left corner where the pin goes. It was a .card, which is the same
 * rounded box every other object in the app uses.
 */
.note-card {
  position: relative;
  display: flex; flex-direction: column; gap: 7px;
  padding: 16px 17px 12px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  border-top: 2px solid var(--border);
  min-height: 132px;
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.note-card.clickable { cursor: pointer; }
.note-card:hover { border-color: var(--border); border-top-color: var(--accent); }
.note-card.pinned { border-top-color: var(--accent); }
.note-pin {
  position: absolute; top: -9px; right: 13px;
  width: 20px; height: 20px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--accent); color: var(--accent-ink, #fff);
}
.note-pin .ic { width: 11px; height: 11px; }

.note-title {
  margin: 0; font-weight: 600; font-size: calc(13.5px * var(--fs)); color: var(--text);
}
.note-body {
  margin: 0; flex: 1;
  font-family: var(--font-ui); font-size: calc(12.5px * var(--fs));
  color: var(--text-2); line-height: 1.55;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
}
.note-foot {
  display: flex; align-items: center; gap: 7px;
  padding-top: 8px; border-top: 1px solid var(--lane-hair);
  font-size: calc(11px * var(--fs)); color: var(--faint);
}
.note-sep { width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: .6; }
.note-foot .row-actions { margin-left: auto; }
.note-card:hover .row-actions, .note-card:focus-within .row-actions { opacity: 1; }

/* ---- to do --------------------------------------------------------------- */

/*
 * Compact used to mean unlabelled: a bare box with a text field in it, sitting
 * in a project sidebar next to three panels that all said what they were. This
 * one always says.
 */
.tasks-panel { padding: 14px 16px 12px; }
.tasks-head {
  display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
}
.tasks-mark { color: var(--accent); display: grid; place-items: center; }
.tasks-mark .ic { width: 15px; height: 15px; }
.tasks-head b {
  font-size: calc(12px * var(--fs)); text-transform: uppercase;
  letter-spacing: var(--track); color: var(--text-2); font-weight: 600;
}
.tasks-count {
  margin-left: auto; font-family: var(--font-mono);
  font-size: calc(11px * var(--fs)); color: var(--faint);
  font-variant-numeric: tabular-nums;
}

.tasks-bar {
  height: 2px; border-radius: 2px; background: var(--raised);
  margin: 0 0 10px; overflow: hidden;
}
.tasks-bar i {
  display: block; height: 100%; background: var(--accent);
  transition: width var(--dur-2) var(--ease);
}

.tasks-list { display: flex; flex-direction: column; }
.tasks-item {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 4px; border-radius: var(--r-sm); cursor: pointer;
}
.tasks-item:hover { background: var(--raised); }
.tasks-item input { width: 15px; height: 15px; flex: none; cursor: pointer; accent-color: var(--accent); }
.tasks-text { flex: 1; font-size: calc(13px * var(--fs)); line-height: 1.4; }
.tasks-item.done .tasks-text { text-decoration: line-through; color: var(--faint); }
.tasks-item .row-actions, .tasks-item .btn { opacity: 0; transition: opacity var(--dur-1) var(--ease); }
.tasks-item:hover .btn { opacity: .6; }
.tasks-item .btn:hover { opacity: 1; }
.tasks-none {
  margin: 2px 4px 8px; color: var(--faint);
  font-size: calc(12.5px * var(--fs)); line-height: 1.5;
}

.tasks-new {
  display: flex; align-items: center; gap: 8px;
  margin-top: 8px; padding-top: 9px; border-top: 1px solid var(--lane-hair);
  color: var(--faint);
}
.tasks-new .ic { width: 14px; height: 14px; flex: none; }
.tasks-new .input {
  flex: 1; border: 0; background: none; padding: 2px 0;
  font-size: calc(13px * var(--fs));
}
.tasks-new .input:focus { outline: none; box-shadow: none; }
/*
 * "Something to do..." is a borderless input on a divider, so `.input:focus`
 * has its outline and ring switched off above - correct, or a plain box would
 * appear round it and the row would jump. But the only thing left saying the
 * caret is in there was the label going accent, which moves a 14px icon and
 * some placeholder text and nothing else. A keyboard user got no answer to
 * "where am I".
 *
 * The divider it already sits on becomes the indicator: accent, and thick
 * enough to see. Nothing moves, because the border is on the box either way.
 * `.search-field` solves the same problem by lighting its own border, which
 * is the pattern this now follows.
 */
.tasks-new:focus-within {
  color: var(--accent);
  border-top-color: var(--accent);
  border-top-width: 2px;
  padding-top: 8px;              /* one less, so the row does not shift */
}

/* ---- tabs ---------------------------------------------------------------- */

/*
 * The underline slides. On a project page with six tabs the jump gave no clue
 * which direction you had moved, and the tab you left is worth a glance.
 */
.lane-tabs { position: relative; }
.lane-tabs .tab { display: inline-flex; align-items: center; gap: 6px; }
.lane-tabs .tab .ic { width: 14px; height: 14px; opacity: .75; }
.lane-tabs .tab.active .ic { opacity: 1; color: var(--accent); }
.lane-tabs .tab-slider {
  position: absolute; bottom: -1px; height: 2px; border-radius: 2px;
  background: var(--accent); pointer-events: none;
  left: var(--tab-x, 0); width: var(--tab-w, 0);
  transition: left var(--dur-2) var(--ease), width var(--dur-2) var(--ease);
}
/* The static underline would double up with the slider. */
.lane-tabs .tab.active { border-bottom-color: transparent; }

@media (prefers-reduced-motion: reduce) {
  .lane-tabs .tab-slider { transition: none; }
  .lane-row::before { transition: none; }
}

/* ============================================================
   4. THE DECK

   Two faults, both structural.

   The page was a container that shrank with the window while its
   type stayed at 25px, 14.5px, 38px, so at a narrow window the
   heading was half the page and the body overflowed. And the
   overflow was `hidden`, silently, so the writer's sentences
   were eaten with no line, no mark and no message.

   Everything inside a page is now sized in cqw against the page
   itself, so the page and its type shrink together and the
   proportions a writer sets at one width are the proportions
   they get at every other. The clipping stays, because a page is
   a page, but it now announces itself.

   And the eight looks are eight looks. They were four hex
   values, a caps flag and a scale: one deck, tinted. Each now
   has its own cover architecture, its own measure, its own
   pairing of faces and its own idea of a heading.
   ============================================================ */

.dp {
  /* Everything below is a multiple of the page's own width. */
  container-type: inline-size;

  /* --dp-m  the margin, and the unit the whole page is spaced by
     --dp-t  one unit of type, carrying the look's scale */
  --dp-m: 5.2cqw;
  --dp-t: calc(1cqw * var(--dp-scale, 1));
  --dp-foot: var(--dp-m);

  padding: calc(var(--dp-m) * .86) var(--dp-m) var(--dp-foot);
  font-family: var(--dp-text, var(--font-ui));
  font-size: calc(1.58 * var(--dp-t));
  line-height: 1.6;
}

.dp-heading {
  font-family: var(--dp-display, var(--font-ui));
  font-weight: 700;
  font-size: calc(2.7 * var(--dp-t));
  line-height: 1.18;
  letter-spacing: -.01em;
  margin-bottom: calc(1.5 * var(--dp-t));
}
.dp-body {
  font-family: var(--dp-text, var(--font-ui));
  font-size: calc(1.58 * var(--dp-t));
  line-height: 1.62;
  max-width: 62ch;
}
.dp-body p { margin: 0 0 .78em; }

.dp-quote-text {
  font-family: var(--dp-quote, var(--dp-display, var(--font-ui)));
  font-size: calc(2.95 * var(--dp-t));
  font-weight: 700; line-height: 1.3; max-width: 26ch;
}
.dp-rule { width: calc(6.5 * var(--dp-t)); height: calc(.32 * var(--dp-t)); border-radius: 2px; }

.dp-caption { padding: calc(1.5 * var(--dp-t)) var(--dp-m); font-size: calc(1.4 * var(--dp-t)); }
.dp-words { padding: calc(var(--dp-m) * .86) var(--dp-m); }
.dp-gallery .dp-grid { gap: calc(1.1 * var(--dp-t)); }
.dp-auto-list { gap: calc(1.2 * var(--dp-t)); }
.dp-auto-list b { font-size: calc(1.5 * var(--dp-t)); }
.dp-auto-list span { font-size: calc(1.2 * var(--dp-t)); }
.dp-auto-list p { font-size: calc(1.4 * var(--dp-t)); }
.dp-empty { font-size: calc(1.45 * var(--dp-t)); }
.dp-cast-grid { gap: calc(1.4 * var(--dp-t)); }
.deck-slot { font-size: calc(1.4 * var(--dp-t)); }
.deck-slot span { display: inline-flex; align-items: center; gap: .55em; }
.deck-slot .ic { width: calc(1.9 * var(--dp-t)); height: calc(1.9 * var(--dp-t)); }

/* ---- the cover, four ways ------------------------------------------------ */

/* The shared gradient belonged to a cover that always had a picture behind
   the words. Three of the four architectures do not. */
.dp-cover::after { content: none; }

/* The old cover was always "picture, words pinned to the bottom", so it hard
   coded flex-end. Three of the four architectures put the words somewhere
   else, so the alignment belongs to each of them, not here. */
.dp-cover { padding: 0; display: flex; justify-content: flex-start; }
.dp-cover-words {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; gap: calc(1.1 * var(--dp-t));
}
.dp-cover-title {
  font-family: var(--dp-display, var(--font-ui));
  font-size: calc(4.1 * var(--dp-t)); font-weight: 700; line-height: 1.08;
  letter-spacing: -.012em;
}
.dp-cover-sub {
  font-family: var(--dp-text, var(--font-ui));
  font-size: calc(1.62 * var(--dp-t)); line-height: 1.5;
  max-width: 44ch; color: var(--dp-muted);
}
.dp-cover-rule { display: none; height: calc(.3 * var(--dp-t)); background: var(--dp-accent); }
.deck-shell[data-caps="1"] .dp-cover-title { text-transform: uppercase; letter-spacing: .012em; }

/* A. Full bleed: the picture reaches all four edges, the words sit on it. */
.dp-cover-bleed { align-items: flex-end; }
.dp-cover-bleed::after {
  content: ''; position: absolute; inset: 42% 0 0 0; pointer-events: none;
  background: linear-gradient(180deg, rgb(0 0 0 / .05), rgb(0 0 0 / .8));
}
.dp-cover-bleed .dp-cover-words { padding: var(--dp-m); width: 100%; color: #fff; }
.dp-cover-bleed .dp-cover-sub { color: rgb(255 255 255 / .84); }
/* No picture yet: the empty slot is the field, so the wash starts lower and
   the words still have something to sit on. */
.dp-cover-bleed:not(:has(.dp-bleed))::after { inset: 55% 0 0 0; }

/* B. Half bleed: a band of picture, and the words on paper beneath it. */
.dp-cover-half { flex-direction: column; }
.dp-cover-half .dp-cover-plate { position: relative; flex: 1 1 56%; min-height: 0; overflow: hidden; }
.dp-cover-half .dp-cover-words { flex: none; padding: calc(var(--dp-m) * .8) var(--dp-m) var(--dp-m); }

/* C. Rule and void: a stack near the top, a great deal of nothing, a strip. */
.dp-cover-void { flex-direction: column; }
.dp-cover-void .dp-cover-words {
  flex: 1; margin: var(--dp-m) var(--dp-m) 0;
  padding: calc(var(--dp-m) * .7) 0 0;
  border-top: calc(.5 * var(--dp-t)) solid var(--dp-accent);
}
.dp-cover-void .dp-cover-band {
  position: relative; flex: none; height: 22%;
  margin: 0 var(--dp-m) var(--dp-m); overflow: hidden;
}

/* D. Typographic: no picture in the field at all, only a plate below. */
.dp-cover-typo { flex-direction: column; }
.dp-cover-typo .dp-cover-words { padding: var(--dp-m) var(--dp-m) 0; }
.dp-cover-typo .dp-cover-plate {
  position: relative; flex: 1; min-height: 0; overflow: hidden;
  margin: calc(var(--dp-m) * .9) var(--dp-m) var(--dp-m);
}

/* ---- the eight looks ----------------------------------------------------- */

/*
 * Clean. White paper, Inter throughout, wide even margins, a hairline under
 * every heading. The one that gets out of the way.
 */
.deck-shell[data-look="clean"] .dp {
  --dp-m: 5.8cqw; --dp-display: var(--font-ui); --dp-text: var(--font-ui);
  --dp-quote: var(--font-read);
}
.deck-shell[data-look="clean"] .dp-heading {
  padding-bottom: calc(.7 * var(--dp-t));
  border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
}
.deck-shell[data-look="clean"] .dp-quote-text { font-style: italic; font-weight: 400; }

/*
 * Noir. Tight margins, Space Grotesk in tracked caps, and a brass rule above
 * each heading rather than under it, so the eye hits the metal first.
 */
.deck-shell[data-look="noir"] .dp {
  --dp-m: 4.1cqw; --dp-display: var(--font-display); --dp-text: var(--font-ui);
}
.deck-shell[data-look="noir"] .dp-heading {
  font-size: calc(2.1 * var(--dp-t)); text-transform: uppercase;
  letter-spacing: .1em; font-weight: 700;
  padding-top: calc(1 * var(--dp-t));
  border-top: calc(.32 * var(--dp-t)) solid var(--dp-accent);
}
.deck-shell[data-look="noir"] .dp-cover-title { letter-spacing: .07em; font-size: calc(3.5 * var(--dp-t)); }
.deck-shell[data-look="noir"] .dp-cover-bleed .dp-cover-words {
  width: auto; align-self: flex-start; margin: var(--dp-m);
  padding: calc(1.4 * var(--dp-t)) calc(1.8 * var(--dp-t));
  border: 1px solid var(--dp-accent);
}

/*
 * Warm paper. Classical proportions, a deeper foot than head the way a book
 * is set, headings in Space Grotesk at reading weight, upper and lower.
 */
.deck-shell[data-look="warm"] .dp {
  --dp-m: 6.2cqw; --dp-foot: 7.4cqw;
  --dp-display: var(--font-display); --dp-text: var(--font-ui);
  --dp-quote: var(--font-read);
}
.deck-shell[data-look="warm"] .dp-heading {
  font-weight: 500; font-size: calc(3 * var(--dp-t));
  letter-spacing: -.015em; margin-bottom: calc(2 * var(--dp-t));
}
.deck-shell[data-look="warm"] .dp-cover-title { font-weight: 500; text-transform: none; letter-spacing: -.015em; }
.deck-shell[data-look="warm"] .dp-quote-text { font-style: italic; font-weight: 400; }

/*
 * Cold. A modular grid: everything hangs off one flush-left rule, and the
 * body is set in Courier, because a procedural should read like evidence.
 */
.deck-shell[data-look="cold"] .dp {
  --dp-m: 5cqw; --dp-display: var(--font-ui); --dp-text: var(--font-mono);
  --dp-quote: var(--font-mono);
}
.deck-shell[data-look="cold"] .dp-body { font-size: calc(1.42 * var(--dp-t)); line-height: 1.66; }
.deck-shell[data-look="cold"] .dp-heading {
  font-weight: 600; font-size: calc(2.4 * var(--dp-t));
  padding-left: calc(1.4 * var(--dp-t));
  border-left: calc(.34 * var(--dp-t)) solid var(--dp-accent);
}
.deck-shell[data-look="cold"] .dp-quote-text { font-weight: 400; font-size: calc(2.3 * var(--dp-t)); }

/*
 * Blood. The tightest margins in the set, and the title straight across the
 * middle of the picture rather than tucked into a corner.
 */
.deck-shell[data-look="blood"] .dp {
  --dp-m: 3.7cqw; --dp-display: var(--font-display); --dp-text: var(--font-ui);
}
.deck-shell[data-look="blood"] .dp-heading {
  padding-left: calc(1.3 * var(--dp-t));
  border-left: calc(.55 * var(--dp-t)) solid var(--dp-accent);
  text-transform: uppercase; letter-spacing: .02em; font-size: calc(2.3 * var(--dp-t));
}
.deck-shell[data-look="blood"] .dp-cover-bleed { align-items: center; }
.deck-shell[data-look="blood"] .dp-cover-bleed::after {
  inset: 0; background: radial-gradient(120% 90% at 50% 50%, rgb(0 0 0 / .15), rgb(0 0 0 / .88));
}
.deck-shell[data-look="blood"] .dp-cover-words { text-align: center; align-items: center; }
.deck-shell[data-look="blood"] .dp-cover-title { font-size: calc(5.4 * var(--dp-t)); letter-spacing: -.02em; }
.deck-shell[data-look="blood"] .dp-cover-rule { display: block; width: calc(7 * var(--dp-t)); }

/*
 * Pastel. The airiest measure here, nothing shouted, no rules at all, and a
 * rounded window for the picture with the words above it rather than over it.
 */
.deck-shell[data-look="pastel"] .dp {
  --dp-m: 7cqw; --dp-display: var(--font-display); --dp-text: var(--font-ui);
  --dp-quote: var(--font-read);
}
.deck-shell[data-look="pastel"] .dp-heading {
  font-weight: 500; letter-spacing: .012em; text-transform: none;
  font-size: calc(2.6 * var(--dp-t)); margin-bottom: calc(1.8 * var(--dp-t));
}
.deck-shell[data-look="pastel"] .dp-body { line-height: 1.75; }
.deck-shell[data-look="pastel"] .dp-cover-half { flex-direction: column-reverse; justify-content: flex-end; }
.deck-shell[data-look="pastel"] .dp-cover-half .dp-cover-plate {
  flex: 1 1 auto; margin: 0 var(--dp-m) var(--dp-m);
  border-radius: calc(2 * var(--dp-t)); overflow: hidden;
}
.deck-shell[data-look="pastel"] .dp-cover-half .dp-cover-words {
  padding: var(--dp-m) var(--dp-m) calc(var(--dp-m) * .7);
}
.deck-shell[data-look="pastel"] .dp-cover-title { font-weight: 500; text-transform: none; letter-spacing: -.01em; }
.deck-shell[data-look="pastel"] .dp-quote-text { font-style: italic; font-weight: 400; }

/*
 * Forest. Centred and classical: a double rule under a tracked cap heading,
 * a deep foot, and the cover's picture reduced to a band along the bottom.
 */
.deck-shell[data-look="forest"] .dp {
  --dp-m: 6.6cqw; --dp-foot: 8cqw;
  --dp-display: var(--font-ui); --dp-text: var(--font-ui);
  --dp-quote: var(--font-read);
}
.deck-shell[data-look="forest"] .dp-heading {
  text-align: center; text-transform: uppercase; letter-spacing: .16em;
  font-weight: 600; font-size: calc(1.75 * var(--dp-t));
  padding-bottom: calc(.9 * var(--dp-t)); margin-bottom: calc(2.2 * var(--dp-t));
  border-bottom: 1px solid currentColor;
  box-shadow: 0 calc(.36 * var(--dp-t)) 0 -1px var(--dp-accent);
}
.deck-shell[data-look="forest"] .dp-body { margin-inline: auto; }
.deck-shell[data-look="forest"] .dp-cover-void .dp-cover-words {
  text-align: center; align-items: center; border-top-width: 1px;
  box-shadow: 0 calc(-.36 * var(--dp-t)) 0 0 var(--dp-accent);
}
.deck-shell[data-look="forest"] .dp-quote-text { font-style: italic; font-weight: 400; }

/*
 * Typewriter. Courier for everything, wide even margins the way a script has
 * them, and headings underlined because that is what a typewriter could do.
 */
.deck-shell[data-look="typewriter"] .dp {
  --dp-m: 6.4cqw; --dp-display: var(--font-mono); --dp-text: var(--font-mono);
  --dp-quote: var(--font-mono);
}
.deck-shell[data-look="typewriter"] .dp-heading {
  font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  font-size: calc(1.85 * var(--dp-t)); text-decoration: underline;
  text-underline-offset: .32em; text-decoration-thickness: 1px;
}
.deck-shell[data-look="typewriter"] .dp-body { font-size: calc(1.45 * var(--dp-t)); line-height: 1.7; }
.deck-shell[data-look="typewriter"] .dp-cover-title { font-size: calc(3 * var(--dp-t)); letter-spacing: .04em; }
.deck-shell[data-look="typewriter"] .dp-quote-text { font-weight: 400; font-size: calc(2.2 * var(--dp-t)); }

/* ---- words that ran off the page ---------------------------------------- */

/*
 * The band only appears when there is something to say, and it says the one
 * thing worth saying: how much is gone, and the fix.
 */
.deck-page.overset .dp { box-shadow: var(--elev-3), inset 0 -2px 0 0 var(--film); }

.dp-overset {
  display: flex; align-items: center; gap: 10px;
  margin-top: 8px; padding: 9px 13px;
  background: color-mix(in srgb, var(--film) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--film) 34%, transparent);
  border-radius: var(--r);
  font-size: calc(12.5px * var(--fs)); color: var(--text-2); line-height: 1.45;
}
.dp-overset-mark { color: var(--film); display: grid; place-items: center; flex: none; }
.dp-overset-mark .ic { width: 16px; height: 16px; }
.dp-overset .btn { margin-left: auto; flex: none; white-space: nowrap; }

/* ---- the rest of the deck chrome ---------------------------------------- */

.rail-add { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
.rail-add .ic { width: 13px; height: 13px; }
.dp-tools button .ic { width: 15px; height: 15px; }
.dp-x { display: grid; place-items: center; }
.dp-x .ic { width: 11px; height: 11px; }

/* The look picker shows the architecture, not just the paint, so the
   difference is visible before it is applied rather than after. */
.look-card .look-demo {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end; gap: 4px;
  padding: 9px 10px; min-height: 62px;
}
.look-card .look-demo span {
  font-size: calc(10px * var(--fs)); font-weight: 700;
  letter-spacing: .06em; line-height: 1.2; position: relative; z-index: 1;
}
.look-card .look-demo i { height: 3px; width: 34px; border-radius: 2px; position: relative; z-index: 1; }

/* ============================================================
   5. PRODUCTION

   The architecture here was already the best decision in the
   app: nothing is stored, every sheet is worked out from the
   script, so the paperwork cannot disagree with the words. It
   was then presented as a page head, five tabs and four raw
   HTML tables, which is a report screen, not paperwork.

   This is the paperwork. Scene numbers down a monospace gutter,
   eighths lining up under each other, hairlines instead of
   boxes, a real breakdown sheet with department boxes, and the
   shooting order drawn as a strip board in the four colours the
   industry has used since long before any of this.
   ============================================================ */

/* ---- the document header ------------------------------------------------- */

.prod-wrap { padding-top: 20px; }

.doc-head {
  display: flex; align-items: flex-start; gap: 28px; flex-wrap: wrap;
  padding-bottom: 16px; margin-bottom: 18px;
  border-bottom: 2px solid var(--text);
}
.doc-head > div:first-child { flex: 1; min-width: 260px; }
.doc-kicker {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: calc(10.5px * var(--fs)); text-transform: uppercase;
  letter-spacing: var(--track); color: var(--faint); font-weight: 600;
}
.doc-kicker .ic { width: 13px; height: 13px; }
.doc-title {
  margin: 7px 0 4px; font-family: var(--font-display); font-weight: 700;
  font-size: calc(28px * var(--fs)); line-height: 1.1; letter-spacing: -.02em;
}
.doc-where { margin: 0; color: var(--muted); font-size: calc(12.5px * var(--fs)); }

/* The stamp in the corner of a call sheet: what this page IS, and the one
   fact about it worth trusting. */
.doc-stamp {
  flex: none; max-width: 300px;
  padding: 10px 0 0 18px; border-left: 1px solid var(--border);
}
.doc-stamp b {
  display: block; font-family: var(--font-mono);
  font-size: calc(12px * var(--fs)); text-transform: uppercase;
  letter-spacing: .1em; color: var(--text-2); margin-bottom: 5px;
}
.doc-stamp span { font-size: calc(11.5px * var(--fs)); color: var(--faint); line-height: 1.5; }

/* ---- the summary band ---------------------------------------------------- */

.prod-summary {
  display: flex; flex-wrap: wrap; align-items: stretch; gap: 0;
  border: 1px solid var(--border-soft); border-radius: var(--r);
  background: var(--surface); overflow: hidden; margin-bottom: 18px;
}
/* The band is one surface with hairlines through it, so the facts inside it
   must give up the card each one used to be. */
.prod-summary .prod-fact {
  flex: 1 1 0; min-width: 96px; padding: 12px 16px;
  display: flex; flex-direction: column; gap: 3px;
  background: none; border-radius: 0;
}
.prod-summary .prod-fact + .prod-fact { border-left: 1px solid var(--lane-hair); }
.prod-summary .prod-fact b {
  font-family: var(--font-mono); font-size: calc(21px * var(--fs));
  font-weight: 400; line-height: 1; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.prod-summary .prod-fact > span {
  font-size: calc(10.5px * var(--fs)); text-transform: uppercase;
  letter-spacing: var(--track); color: var(--faint);
}
.prod-fact-split { flex: 1 1 0; min-width: 138px; justify-content: center; }

/* Day against night is a ratio, and a ratio is a picture. */
.prod-split { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: var(--raised); }
.prod-split i, .prod-split u { display: block; height: 100%; }
.prod-split.daynight i { background: var(--film); }
.prod-split.daynight u { background: color-mix(in srgb, var(--accent) 70%, var(--bg-deep)); }
.prod-split.intext  i { background: var(--text-3); }
.prod-split.intext  u { background: var(--tv); }
.prod-split-key { display: flex; gap: 14px; margin-top: 7px; }
.prod-split-key span {
  display: flex; align-items: baseline; gap: 4px;
  font-size: calc(10.5px * var(--fs)); text-transform: uppercase;
  letter-spacing: .06em; color: var(--faint);
}
.prod-split-key b {
  font-family: var(--font-mono); font-size: calc(13px * var(--fs)) !important;
  color: var(--text-2) !important; font-variant-numeric: tabular-nums;
}

.prod-tabs { margin-bottom: 16px; }
.prod-tabs .tab { display: inline-flex; align-items: center; gap: 6px; }
.prod-tabs .tab .ic { width: 14px; height: 14px; opacity: .7; }
.prod-tabs .tab.active .ic { opacity: 1; color: var(--accent); }

.prod-hint {
  display: flex; gap: 9px; align-items: flex-start;
  margin-bottom: 14px; color: var(--muted);
  font-size: calc(12.5px * var(--fs)); line-height: 1.55;
}
.prod-hint > span { color: var(--faint); flex: none; display: grid; place-items: center; }
.prod-hint .ic { width: 15px; height: 15px; }
.prod-hint p { margin: 0; max-width: 78ch; }
.prod-hint b { color: var(--text-2); }
.prod-empty { color: var(--faint); font-size: calc(13px * var(--fs)); padding: 20px 2px; }

/* ---- the strip board's four colours -------------------------------------- */

/*
 * Not decoration. White interior day, yellow exterior day, blue interior
 * night, green exterior night is the convention every AD reads without being
 * told, and getting it wrong would be worse than having no colour. Held at low
 * alpha over the app's own surface so all seven themes still work.
 */
[data-strip="int-day"]   { --strip: #d8d9dd; }
[data-strip="ext-day"]   { --strip: #e0b94e; }
[data-strip="int-night"] { --strip: #6d93d6; }
[data-strip="ext-night"] { --strip: #5fb383; }

/* ---- the breakdown sheet ------------------------------------------------- */

.bs-sheets { display: flex; flex-direction: column; gap: 10px; }

.bs {
  display: flex; align-items: stretch;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left: 3px solid var(--strip, var(--border));
  border-radius: var(--r);
  overflow: hidden;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.bs:hover { border-color: var(--border); border-left-color: var(--strip, var(--border)); }
.bs.picked {
  background: color-mix(in srgb, var(--accent) 5%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  border-left-color: var(--accent);
}

/* Everything a crew reads down a column lives in the gutter. */
.bs-gutter {
  flex: none; width: 62px;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 12px 0; border-right: 1px solid var(--lane-hair);
  background: color-mix(in srgb, var(--raised) 55%, transparent);
}
.bs-pick { display: grid; place-items: center; cursor: pointer; }
.bs-pick input { width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent); }
.bs-no {
  font-family: var(--font-mono); font-size: calc(15px * var(--fs));
  color: var(--text); font-variant-numeric: tabular-nums; line-height: 1;
}
.bs-eighths {
  font-family: var(--font-mono); font-size: calc(11px * var(--fs));
  color: var(--faint); font-variant-numeric: tabular-nums;
}

.bs-main { flex: 1; min-width: 0; padding: 11px 16px 13px; }

.bs-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding-bottom: 8px; border-bottom: 1px solid var(--lane-hair);
}
.bs-ie {
  font-family: var(--font-mono); font-size: calc(10.5px * var(--fs));
  letter-spacing: .1em; color: var(--text-3);
  border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px;
}
.bs-set {
  margin: 0; flex: 1; min-width: 0;
  font-family: var(--font-mono); font-size: calc(13.5px * var(--fs));
  font-weight: 700; text-transform: uppercase; letter-spacing: .01em;
  color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bs-dn {
  font-family: var(--font-mono); font-size: calc(10.5px * var(--fs));
  text-transform: uppercase; letter-spacing: .1em;
}
.bs-dn.day { color: var(--film); }
.bs-dn.night { color: color-mix(in srgb, var(--accent) 78%, var(--text)); }
.bs-page {
  font-family: var(--font-mono); font-size: calc(11px * var(--fs)); color: var(--faint);
  font-variant-numeric: tabular-nums;
}

.bs-syn {
  margin: 9px 0 0; max-width: 82ch;
  font-family: var(--font-ui); font-size: calc(13px * var(--fs));
  color: var(--text-2); line-height: 1.55;
}
.bs-none {
  margin: 9px 0 0; max-width: 68ch;
  font-size: calc(12px * var(--fs)); color: var(--faint); line-height: 1.5;
}

/* Department boxes, so the art department can find its own line without
   reading everyone else's. */
.bs-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(178px, 1fr));
  gap: 0; margin-top: 11px;
  border-top: 1px solid var(--lane-hair);
}
.bs-dept { padding: 9px 12px 8px 0; border-right: 1px solid var(--lane-hair); }
.bs-dept:last-child { border-right: 0; }
.bs-dept h4 {
  display: flex; align-items: center; gap: 6px;
  margin: 0 0 5px; font-size: calc(10px * var(--fs));
  text-transform: uppercase; letter-spacing: var(--track);
  color: var(--faint); font-weight: 600;
}
.bs-dept h4 em {
  margin-left: auto; font-style: normal; font-family: var(--font-mono);
  font-size: calc(10px * var(--fs)); color: var(--text-3); padding-right: 10px;
}
.bs-dept ul { margin: 0; padding: 0; list-style: none; }
.bs-dept li {
  font-size: calc(12.5px * var(--fs)); color: var(--text-2);
  line-height: 1.5; padding: 1px 0;
}
.bs-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; display: inline-block; }

/* ---- typographic tables -------------------------------------------------- */

.ptable-wrap {
  border: 1px solid var(--border-soft); border-radius: var(--r);
  background: var(--surface); overflow: auto; max-height: 70vh;
}
.ptable {
  width: 100%; border-collapse: collapse;
  font-size: calc(12.5px * var(--fs));
  font-variant-numeric: tabular-nums;
}
.ptable th {
  position: sticky; top: 0; z-index: 1;
  text-align: left; padding: 9px 14px;
  background: var(--surface);
  font-size: calc(10px * var(--fs)); font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track); color: var(--faint);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.ptable td {
  padding: 8px 14px; border-bottom: 1px solid var(--lane-hair);
  color: var(--text-2); vertical-align: baseline;
}
.ptable tbody tr:last-child td { border-bottom: 0; }
.ptable tbody tr:hover td { background: var(--raised); }
.ptable .num { text-align: right; }
.ptable .mono { font-family: var(--font-mono); }
.ptable .wide { width: 30%; }
.ptable-name { color: var(--text); font-weight: 600; }
.ptable-quiet { color: var(--faint); }

.ptable tfoot td {
  position: sticky; bottom: 0;
  background: var(--raised); border-top: 1px solid var(--border); border-bottom: 0;
  font-size: calc(10.5px * var(--fs)); text-transform: uppercase;
  letter-spacing: .06em; color: var(--faint);
  font-family: var(--font-mono);
}

/* Who is carrying the picture, at a glance. */
.load { display: block; width: 74px; height: 5px; border-radius: 3px; background: var(--raised); }
.load i { display: block; height: 100%; border-radius: 3px; background: var(--accent); opacity: .8; }

/* ---- what is needed ------------------------------------------------------ */

.prod-depts { display: flex; flex-direction: column; gap: 12px; }
.prod-dept-card {
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: var(--r); overflow: hidden;
}
.prod-dept-card h3 {
  display: flex; align-items: center; gap: 8px;
  margin: 0; padding: 11px 16px; border-bottom: 1px solid var(--lane-hair);
  font-size: calc(11px * var(--fs)); text-transform: uppercase;
  letter-spacing: var(--track); color: var(--text-2); font-weight: 600;
}
.prod-dept-card h3 em {
  margin-left: auto; font-style: normal;
  font-family: var(--font-mono); color: var(--faint);
}
.prod-item {
  display: flex; align-items: center; gap: 14px;
  padding: 8px 16px;
}
.prod-item + .prod-item { border-top: 1px solid var(--lane-hair); }
.prod-item:hover { background: var(--raised); }
.prod-item > b { flex: 1; min-width: 0; font-size: calc(13px * var(--fs)); font-weight: 550; }
.prod-item-scenes { display: flex; flex-wrap: wrap; gap: 3px; justify-content: flex-end; max-width: 55%; }
.prod-item-scenes u {
  text-decoration: none; font-family: var(--font-mono);
  font-size: calc(10.5px * var(--fs)); color: var(--text-3);
  background: var(--raised); border-radius: 3px; padding: 1px 5px;
}
.prod-item-scenes em { font-style: normal; font-size: calc(10.5px * var(--fs)); color: var(--faint); }
.prod-item .btn { opacity: 0; transition: opacity var(--dur-1) var(--ease); }
.prod-item:hover .btn, .prod-item:focus-within .btn { opacity: 1; }

/* ---- the strip board ----------------------------------------------------- */

.board-key { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 14px; }
.board-key-item {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: calc(11px * var(--fs)); color: var(--muted);
}
.board-key-item i {
  width: 16px; height: 10px; border-radius: 2px; display: block;
  background: var(--strip); border: 1px solid color-mix(in srgb, var(--strip) 65%, black);
}

.stripboard { display: flex; flex-direction: column; gap: 16px; }

.sb-block {
  border: 1px solid var(--border-soft); border-radius: var(--r);
  background: var(--surface); overflow: hidden;
}
.sb-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 10px 15px;
  border-bottom: 1px solid var(--lane-hair);
  border-left: 4px solid var(--strip, var(--border));
}
.sb-head b { font-family: var(--font-mono); font-size: calc(13px * var(--fs)); text-transform: uppercase; }
.sb-dn {
  font-size: calc(10px * var(--fs)); text-transform: uppercase;
  letter-spacing: var(--track); color: var(--faint);
}
.sb-figs {
  margin-left: auto; font-family: var(--font-mono);
  font-size: calc(11px * var(--fs)); color: var(--faint);
}

/* One strip per scene, the way they hang on a board. */
.sb-strips { display: flex; flex-direction: column; }
.sb-strip {
  display: grid; grid-template-columns: 46px 1fr auto 56px;
  align-items: center; gap: 12px;
  width: 100%; text-align: left; cursor: pointer;
  padding: 7px 14px 7px 0;
  background: none; border: 0;
  border-left: 8px solid var(--strip, var(--border));
  color: var(--text-2); font: inherit;
  transition: background var(--dur-1) var(--ease);
}
.sb-strip + .sb-strip { border-top: 1px solid var(--lane-hair); }
.sb-strip:hover { background: var(--raised); color: var(--text); }
.sb-no {
  font-family: var(--font-mono); font-size: calc(12.5px * var(--fs));
  text-align: right; color: var(--text); font-variant-numeric: tabular-nums;
}
.sb-set {
  font-family: var(--font-mono); font-size: calc(12px * var(--fs));
  text-transform: uppercase; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sb-time {
  font-size: calc(10px * var(--fs)); text-transform: uppercase;
  letter-spacing: .08em; color: var(--faint);
}
.sb-len {
  font-family: var(--font-mono); font-size: calc(11.5px * var(--fs));
  text-align: right; color: var(--text-3); font-variant-numeric: tabular-nums;
}
.sb-cast {
  margin: 0; padding: 9px 15px; border-top: 1px solid var(--lane-hair);
  display: flex; align-items: center; gap: 7px;
  font-size: calc(11.5px * var(--fs)); color: var(--faint); line-height: 1.5;
}
.sb-cast .ic { width: 14px; height: 14px; flex: none; }

.export-card { display: flex; flex-direction: column; gap: 3px; }
.export-mark { color: var(--accent); }
.export-mark .ic { width: 18px; height: 18px; }

@media (max-width: 760px) {
  .doc-stamp { border-left: 0; padding-left: 0; border-top: 1px solid var(--border); padding-top: 12px; max-width: none; }
  .bs-grid { grid-template-columns: 1fr 1fr; }
  .sb-strip { grid-template-columns: 40px 1fr auto; }
  .sb-len { display: none; }
}

/* ============================================================
   6. THE CORKBOARD AND THE ARC GRID

   Both are now mountable into a pane beside the script instead
   of only reachable as a route, so restructuring an act stops
   costing a teardown and a rebuild each way. The pane styles
   below are all a host needs: mount into `.companion-body` and
   the board or the grid sizes itself down to fit.
   ============================================================ */

/* The board with nothing on it: the promise, not the void. */
.board-empty {
  grid-column: 1 / -1;
  text-align: center; padding: 48px 24px; color: var(--muted);
}
.board-empty > p:first-child { margin: 0 0 12px; color: var(--faint); }
.board-empty > p:first-child .ic { width: 30px; height: 30px; }
.board-empty h3 {
  margin: 0 0 8px; font-family: var(--font-display); font-weight: 500;
  font-size: calc(17px * var(--fs)); color: var(--text);
}
.board-empty p { margin: 0 auto; max-width: 44ch; font-size: calc(13.5px * var(--fs)); line-height: 1.6; }

/*
 * Beside the page rather than instead of it.
 *
 * The pane is roughly half the width of a window, so the cards drop a size and
 * the board scrolls inside itself. Scrolling inside itself matters more than
 * it sounds: the drag autoscroll asks whichever box is actually scrolling, and
 * a board that made the WINDOW scroll would slide out from under the card.
 */
.companion-body .board {
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--sp-5, 10px);
  overflow: auto;
  max-height: 100%;
  padding: 2px;
}
.companion-body .arcgrid {
  grid-template-columns: 150px repeat(var(--cols), minmax(88px, 1fr)) 104px;
  font-size: calc(12px * var(--fs));
}
.companion-body .arcgrid .ag-head,
.companion-body .arcgrid .ag-row-head { position: static; }
.companion-body .ag-cell { min-height: 44px; }
.companion-body .ag-silence { display: none; }

/* ---- the arc grid: the empty cells, drawn ------------------------------- */

.ag-corner { flex-direction: row !important; align-items: baseline; gap: 8px; }
.ag-corner em {
  margin-left: auto; font-style: normal; font-family: var(--font-mono);
  color: var(--text-3); letter-spacing: 0;
}

/* How much lands in each episode. The column carrying four storylines and the
   one after it carrying none is the single most useful thing this grid knows,
   and it used to be something you counted by eye across nine rows. */
.ag-load {
  display: block; height: 16px; margin-top: 4px;
  display: flex; align-items: flex-end;
}
.ag-load u {
  display: block; width: 100%; min-height: 1px;
  background: var(--accent); opacity: .45; border-radius: 1px;
  text-decoration: none;
  transition: opacity var(--dur-1) var(--ease);
}
.ag-ep:hover .ag-load u { opacity: .8; }
.ag-ep.barren { background: color-mix(in srgb, var(--film) 7%, var(--surface)); }
.ag-ep.barren b { color: var(--film); }
.ag-ep.barren .ag-load { border-bottom: 1px dashed color-mix(in srgb, var(--film) 55%, transparent); }

/*
 * Silence.
 *
 * "The useful information is almost always the empty cells" was the thesis,
 * and an empty cell was blank: technically true, and invisible. A run of two
 * or more now reads as one continuous hatched band with its length written on
 * it, so the character nobody has thought about since episode three is the
 * loudest thing on the screen.
 */
.ag-cell.quiet {
  background: repeating-linear-gradient(
    -45deg,
    transparent 0 6px,
    color-mix(in srgb, var(--film) 9%, transparent) 6px 12px
  ), var(--bg);
}
.ag-cell.quiet:hover { background-color: var(--surface); }
.ag-silence {
  align-self: center; margin: auto 0;
  font-size: calc(10px * var(--fs)); line-height: 1.3;
  color: color-mix(in srgb, var(--film) 78%, var(--text-3));
  text-transform: uppercase; letter-spacing: .06em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.ag-states { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ag-states .ic { width: 12px; height: 12px; color: var(--accent); flex: none; }
/* Not written yet, drawn rather than filled with dots. */
.ag-blank {
  display: inline-block; width: 26px; height: 0;
  border-bottom: 1px dashed var(--faint); vertical-align: middle;
}

/* ============================================================
   7. THE BEAT SHEET

   The page number was a field you typed. It is now worked out
   from the length you are aiming at, with what you type kept as
   an override on top of it.
   ============================================================ */

.paper-meta {
  display: flex; align-items: center; justify-content: center;
  gap: 12px; flex-wrap: wrap;
}
.meta-sep { width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: .5; }

.sheet-target { display: inline-flex; align-items: baseline; gap: 5px; }
.sheet-target b {
  font-family: var(--font-mono); font-weight: 400; color: var(--text-2);
  min-width: 2.4em; text-align: center;
  border-bottom: 1px dashed var(--border); padding: 0 3px;
  outline: none; letter-spacing: 0;
}
.sheet-target b:hover { background: var(--raised); }
.sheet-target b:focus { border-bottom-color: var(--accent); color: var(--text); }
.sheet-target b:empty::before { content: '—'; color: var(--faint); }

/* Empty means "use the derived one", so the derived one is what shows. Typed
   over, it is the writer's; cleared, it comes straight back. */
.beat-page.has-derived:empty::before {
  content: attr(data-derived);
  color: var(--faint); font-style: italic;
}
.beat-page:not(:empty) { color: var(--text-2); }

/* Which scene is sitting on that page today. Derived, so it can never be
   stale, and quiet, so it never nags a sheet written before the script. */
.beat-lands {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  margin-top: 8px; padding-top: 7px;
  border-top: 1px dashed var(--border-soft);
  font-size: calc(11.5px * var(--fs)); color: var(--faint);
}
.beat-lands .ic { width: 12px; height: 12px; flex: none; }
.beat-lands button {
  display: inline-flex; align-items: baseline; gap: 6px;
  background: none; border: 0; padding: 1px 4px; margin: 0 -4px;
  border-radius: var(--r-xs); cursor: pointer;
  color: var(--text-2); font: inherit;
  font-family: var(--font-mono); text-transform: uppercase;
}
.beat-lands button:hover { background: var(--raised); color: var(--accent); }
.beat-lands button b { color: var(--accent); font-weight: 400; }
body.hide-guides .beat-lands { display: none; }

/* ============================================================
   8. ODDS

   Small things in the views this engineer owns that the sprite
   changed the shape of.
   ============================================================ */

/* The dropzone's mark is drawn now, not an arrow character. */
.dropzone .big { display: block; margin-bottom: 6px; }
.dropzone .big .ic { width: 24px; height: 24px; opacity: .55; }
.dropzone:hover .big .ic, .dropzone.over .big .ic { opacity: .9; color: var(--accent); }

/* The scratchpad's keep-it-close star fills when it is on. */
.scrap-actions .btn.on { color: var(--accent); opacity: 1; }
.scrap-actions .btn .ic { width: 14px; height: 14px; }

/* Which way a relationship runs. */
.rel-dir { display: inline-grid; place-items: center; vertical-align: -2px; color: var(--faint); }
.rel-dir .ic { width: 13px; height: 13px; }

/* Buttons that are only an icon should be square, not a slab of padding with
   a mark floating in the middle of it. */
.btn.btn-sm > .ic:only-child { width: 15px; height: 15px; margin: 0 -2px; }

/* The importer's folder chips and the segmented film/TV picker. */
.folder-chip .ic { width: 15px; height: 15px; opacity: .7; }
.seg button { display: inline-flex; align-items: center; gap: 6px; justify-content: center; }
.seg button .ic { width: 14px; height: 14px; }

/* ---- from the cast rebuild ---- */
/* ==========================================================================
   CAST & HUB — the character dossier and the project overview.

   These are the two pages a writer opens most, and until now they were the
   two that most looked like a database had been given a stylesheet. Sixty-four
   fields at one visual weight; a project hub whose three most cinematic
   surfaces were three text buttons in a right rail.

   The whole file works to one idea: *rank what is on the page*. The five
   answers that make a character work get a ground of their own and the reading
   face at full measure. Things you look up get a call sheet. Things you can
   see get drawn.

   Merged into web/styles.css by the main agent. Everything here uses existing
   tokens except the three declared immediately below.
   ========================================================================== */

:root {
  /* NEW TOKEN. A rule you can see but never read: the hairline that separates
     rows of a spec sheet without turning it into a table. */
  --hair-ink: color-mix(in srgb, var(--border) 62%, transparent);

  /* NEW TOKEN. Figures that line up in a column. Named, because five separate
     surfaces want the same thing and one of them will otherwise invent it. */
  --num: "tnum" 1, "lnum" 1;
}
/*
 * NEW TOKEN, and deliberately NOT declared on :root.
 *
 * The ground The Engine sits on is the character's own colour stirred into the
 * deepest surface the theme owns. A custom property has its var() references
 * substituted on the element that declares it, so declaring this at :root
 * would freeze --character-accent to the theme accent before the character
 * page ever gets to set it. Declared on .engine, it resolves against the
 * accent the view actually carries.
 */
.engine { --engine-ground: color-mix(in srgb, var(--character-accent, var(--accent)) 7%, var(--bg-deep)); }

/* ==========================================================================
   1 · THE ENGINE
   Want, need, flaw, the wound, the lie. Five answers, at the top of the page,
   above the photographs, each with the full measure a paragraph needs. They
   used to share a three-column grid with "Their pet".
   ========================================================================== */

.engine {
  position: relative;
  margin: 0 0 14px;
  padding: 26px clamp(20px, 2.4vw, 34px) 28px;
  border-radius: var(--r-lg);
  background:
    radial-gradient(120% 90% at 0% 0%,
      color-mix(in srgb, var(--character-accent, var(--accent)) 10%, transparent), transparent 58%),
    var(--engine-ground);
  border: 1px solid color-mix(in srgb, var(--character-accent, var(--accent)) 16%, var(--border-soft));
  overflow: hidden;
}

/* The rule in their colour. Not a border on all four sides — a single stated
   line along the top, the way a title card is underscored. */
.engine::before {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 3px;
  background: linear-gradient(90deg,
    var(--character-accent, var(--accent)),
    color-mix(in srgb, var(--character-accent, var(--accent)) 20%, transparent) 72%,
    transparent);
}

.engine-head { margin-bottom: 22px; }
.engine-head h2 {
  margin: 0; font-family: var(--font-display); font-weight: 650;
  font-size: calc(19px * var(--fs)); letter-spacing: -.01em; color: var(--text);
}
.engine-head p {
  margin: 5px 0 0; max-width: 56ch;
  font-family: var(--font-read); font-style: italic;
  font-size: calc(13.5px * var(--fs)); color: var(--text-3); line-height: 1.55;
}

/* Five stanzas, not five form rows. */
.stanzas { list-style: none; margin: 0; padding: 0; display: grid; gap: 26px; }

.stanza {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  gap: 0 16px;
  align-items: start;
}

/* The numeral is a margin mark, in the counting face, never louder than the
   answer beside it. */
.stanza-num {
  grid-row: 1 / span 3;
  font-family: var(--font-display); font-variant-numeric: tabular-nums;
  font-size: calc(12px * var(--fs)); font-weight: 600;
  color: color-mix(in srgb, var(--character-accent, var(--accent)) 70%, transparent);
  padding-top: 3px; text-align: right; user-select: none;
}

/* inline-block, not block: the dashed "you may rewrite this" underline should
   run under the words, not across the whole column. */
.stanza-label {
  display: inline-block; margin: 0 0 6px;
  font-size: var(--fz-micro); font-weight: 650;
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--text-3);
}

/* The writer's sentence: the reading face, at the size you would set a book
   in, on a measure the eye can walk back along. */
.stanza-value {
  font-family: var(--font-read);
  font-size: calc(18.5px * var(--fs)); line-height: 1.62;
  color: var(--text); max-width: 62ch; min-height: 1.5em;
  white-space: pre-wrap;
}
.stanza.empty .stanza-value {
  font-family: var(--font-ui); font-size: calc(14px * var(--fs));
  color: var(--faint); font-style: normal;
}

.stanza-hint {
  margin: 7px 0 0; max-width: 58ch;
  font-family: var(--font-read); font-style: italic;
  font-size: calc(12.5px * var(--fs)); line-height: 1.5; color: var(--faint);
}
/* Answering the question settles the page: the prompt steps back rather than
   disappearing, so it can still be reworded. */
.stanza:not(.empty) .stanza-hint { opacity: .42; }
.stanza:not(.empty):hover .stanza-hint,
.stanza:focus-within .stanza-hint { opacity: .8; }

/* The generic inline-edit hover is `--raised`, which on the Engine's deep
   ground reads as a pale patch. Inside the Engine it answers in their colour. */
.engine .inline-edit:hover {
  background: color-mix(in srgb, var(--character-accent, var(--accent)) 13%, transparent);
}
.engine .inline-edit:focus {
  background: color-mix(in srgb, var(--bg-deep) 82%, transparent);
}

@media (max-width: 700px) {
  .stanza { grid-template-columns: minmax(0, 1fr); }
  .stanza-num { display: none; }
  .stanza-value { font-size: calc(17px * var(--fs)); }
}

/* ==========================================================================
   2 · WHO THEY ARE TO EVERYONE ELSE
   The relationships table has held the label, the kind and the strength all
   along. The dossier's answer to the question was a free-text box.
   ========================================================================== */

.kin-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }

.kin-row {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) auto;
  gap: 0 13px; align-items: center;
  padding: 9px 12px 9px 14px;
  border-radius: var(--r-sm);
  /* The rule is the relationship: its colour is the kind, its weight is how
     much of the story hangs on it. */
  border-left: var(--kin-weight, 3px) solid var(--kin-colour, var(--faint));
  background: color-mix(in srgb, var(--kin-colour, var(--faint)) 4%, transparent);
  transition: background var(--dur-1) var(--ease);
}
.kin-row:hover { background: color-mix(in srgb, var(--kin-colour, var(--faint)) 9%, transparent); }

.kin-face {
  width: 44px; aspect-ratio: 3 / 4; object-fit: cover;
  border-radius: var(--r-xs); background: var(--raised);
  cursor: pointer;
}
.kin-face.forged { image-rendering: pixelated; }

.kin-body { min-width: 0; }
.kin-name {
  display: block; font-size: calc(14px * var(--fs)); font-weight: 600;
  color: var(--text); cursor: pointer; line-height: 1.35;
}
.kin-name:hover { color: var(--accent); }

/* The label is the whole point of the row, so it is set to be read. */
.kin-label {
  font-family: var(--font-read); font-style: italic;
  font-size: calc(14.5px * var(--fs)); line-height: 1.45;
  color: var(--text-2); margin-top: 1px;
}
.kin-label.placeholder { font-style: normal; font-family: var(--font-ui); font-size: calc(12.5px * var(--fs)); }

/* A relationship is directional: A's view of B is not B's view of A. Inbound
   rows say so quietly rather than pretending to be symmetrical. */
.kin-dir {
  color: var(--faint); width: 13px; height: 13px;
  vertical-align: -2px; margin-right: 5px;
}
.kin-row.inbound .kin-label { color: var(--text-3); }

.kin-tools { display: flex; align-items: center; gap: 8px; }
.kin-row:not(:hover):not(:focus-within) .kin-tools .btn { opacity: 0; }
.kin-tools .btn { transition: opacity var(--dur-1) var(--ease); }

/* A real <select>, so the keyboard and a screen reader both get the eight
   kinds — but wearing the tracked whisper the rest of the row is set in. */
.kin-kind {
  appearance: none; -webkit-appearance: none;
  font-size: var(--fz-micro); letter-spacing: var(--track); text-transform: uppercase;
  font-weight: 600; color: var(--kin-colour, var(--muted));
  background: none; border: 0; font-family: inherit; cursor: pointer;
  padding: 2px 4px; border-radius: var(--r-xs); text-align: center;
}
.kin-kind option { color: var(--text); background: var(--surface); text-transform: none; letter-spacing: 0; }
.kin-kind:hover { background: color-mix(in srgb, var(--kin-colour, var(--faint)) 14%, transparent); }

/* Three dots: faint, normal, the spine of the story. */
.kin-strength { display: flex; gap: 3px; align-items: center; }
.kin-dot {
  width: 7px; height: 7px; padding: 0; border-radius: var(--r-circle);
  border: 1px solid var(--kin-colour, var(--faint)); background: none; cursor: pointer;
}
.kin-dot.on { background: var(--kin-colour, var(--faint)); }

/* The discarded `mentions`: everywhere in this story whose writing names them.
   It was fetched on every character load and printed nowhere. */
.kin-mentions {
  margin: 16px 0 0; padding-top: 12px;
  border-top: 1px solid var(--hair-ink);
  font-size: calc(12.5px * var(--fs)); color: var(--text-3); line-height: 1.7;
}
.kin-mentions a { color: var(--text-2); border-bottom: 1px solid var(--hair-ink); cursor: pointer; }
.kin-mentions a:hover { color: var(--accent); border-bottom-color: var(--accent); }

.kin-empty {
  margin: 0; font-family: var(--font-read); font-style: italic;
  font-size: calc(14px * var(--fs)); color: var(--text-3); line-height: 1.6; max-width: 58ch;
}

@media (max-width: 640px) {
  .kin-row { grid-template-columns: 36px minmax(0, 1fr); }
  .kin-face { width: 36px; }
  .kin-tools { grid-column: 1 / -1; justify-content: flex-end; margin-top: 6px; }
  .kin-row:not(:hover):not(:focus-within) .kin-tools .btn { opacity: 1; }
}

/* ==========================================================================
   3 · THE PROSE SECTIONS — two columns, never three
   A paragraph about someone's childhood is reading matter. It gets a measure.
   ========================================================================== */

.prose-cols {
  display: grid; gap: 22px 30px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-flow: row dense;
}
/* A field declared LONG is a paragraph, so it takes the whole width and keeps
   its own measure inside it. A one-liner pairs up with its neighbour. */
.prose-cols > .long { grid-column: 1 / -1; }
.prose-cols > .long .dossier-value { max-width: 66ch; }

@media (max-width: 900px) { .prose-cols { grid-template-columns: minmax(0, 1fr); } }

/* ==========================================================================
   4 · THE SPEC SHEET — On paper, What you see
   These sections stopped pretending to be prose. Height, weight, build, eyes:
   this is a call sheet, and a call sheet is a beautiful thing when it commits.
   ========================================================================== */

.spec-sheet { margin: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 34px; }
@media (max-width: 820px) { .spec-sheet { grid-template-columns: minmax(0, 1fr); } }

.spec-row {
  display: grid; grid-template-columns: 13ch minmax(0, 1fr);
  gap: 0 14px; align-items: baseline;
  padding: 7px 0; border-bottom: 1px solid var(--hair-ink);
}
.spec-row:last-child { border-bottom: 0; }

/* justify-self shrinks the term to its own words, so the "you may rewrite
   this" underline sits under the label rather than ruling the whole column. */
.spec-sheet dt {
  justify-self: start;
  font-size: var(--fz-micro); font-weight: 600;
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--text-3); line-height: 1.9;
}
.spec-sheet dd {
  margin: 0; font-size: calc(14px * var(--fs)); color: var(--text);
  /* Ages, heights, weights and dates line up in a column, the way they do on
     a real sheet of paper. */
  font-feature-settings: var(--num);
  font-variant-numeric: tabular-nums lining-nums;
  min-height: 1.5em; line-height: 1.5;
}
.spec-row.empty dd { color: var(--faint); font-size: calc(13px * var(--fs)); }

/* The prompt, where one exists, is a note in the margin of the sheet. */
.spec-note {
  grid-column: 2; margin: 1px 0 0;
  font-family: var(--font-read); font-style: italic;
  font-size: calc(11.5px * var(--fs)); color: var(--faint); line-height: 1.45;
}
.spec-row:not(.empty) .spec-note { display: none; }
.spec-row:focus-within .spec-note { display: block; }

/* ==========================================================================
   5 · YOUR WORDING, AND WHOSE IT IS
   Rewording a heading rewords it for the whole cast. That was true, correct,
   and communicated by a `title=` tooltip and nothing else.
   ========================================================================== */

.wording-note {
  display: flex; align-items: flex-start; gap: 11px;
  margin: 0 0 14px; padding: 11px 14px;
  border: 1px dashed var(--border);
  border-radius: var(--r); background: color-mix(in srgb, var(--surface) 55%, transparent);
  font-size: calc(12.5px * var(--fs)); color: var(--text-2); line-height: 1.6;
}
.wording-note .ic { color: var(--muted); margin-top: 3px; }
.wording-note p { margin: 0; max-width: 74ch; }
.wording-note .btn { margin-left: auto; flex: none; }

/* The affordance itself: any heading you are allowed to rewrite carries a
   dotted underline the moment the cursor is in its neighbourhood. */
.stanza-label, .spec-sheet dt, .dossier-label {
  cursor: text;
  border-bottom: 1px dashed transparent;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.dossier-label { display: inline-block; }
.engine:hover .stanza-label,
.panel:hover .spec-sheet dt,
.panel:hover .dossier-label { border-bottom-color: color-mix(in srgb, var(--faint) 45%, transparent); }
.stanza-label:hover, .spec-sheet dt:hover, .dossier-label:hover {
  color: var(--text-2); border-bottom-color: var(--faint);
}
.stanza-label:focus, .spec-sheet dt:focus, .dossier-label:focus {
  outline: none; color: var(--accent); border-bottom-style: solid; border-bottom-color: var(--accent);
}

/* ==========================================================================
   6 · THE RAIL CARD
   It used to build `hsl(hsl(214 42% 48%) 45% 42%)` — a nested string the
   browser drops — and fall back to --raised, which is #ffffff in Light, under
   a rule setting the initials to #fff. White initials on a white card for
   every character without a photograph. The card no longer draws initials at
   all: pixelforge draws them a face.
   ========================================================================== */

.dossier-presence {
  font-style: normal; font-size: calc(11px * var(--fs));
  color: var(--text-3); line-height: 1.5; margin-top: 4px;
}
.dossier-card .cast-credit { justify-self: center; }
.dossier-face.forged { image-rendering: pixelated; object-fit: contain; background: var(--bg-deep); }

/* The derived line in the hero: what the scripts say about them, which is the
   only honest measure of how much of this person exists. */
.cast-presence {
  margin: 14px 0 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 9px;
  font-size: calc(12px * var(--fs)); color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.cast-presence b { color: var(--text-2); font-weight: 600; }
.cast-presence .slug {
  font-family: var(--font-mono); font-size: calc(11px * var(--fs));
  letter-spacing: .01em; color: var(--text-2);
}
.cast-presence .sep { color: var(--faint); }
.cast-presence.quiet { font-family: var(--font-read); font-style: italic; font-size: calc(13px * var(--fs)); }

/* ==========================================================================
   7 · THE CAST WALL
   The Characters tab was a card grid with 34px circles. A cast is faces.
   ========================================================================== */

.cast-wall {
  display: grid; gap: 16px 14px;
  grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
}

.cast-tile {
  position: relative; display: block; width: 100%; text-align: left;
  padding: 0; border: 0; background: none; font: inherit; color: inherit; cursor: pointer;
}
.cast-tile-art {
  position: relative; aspect-ratio: 3 / 4; overflow: hidden;
  border-radius: var(--r); background: var(--bg-deep);
  box-shadow: var(--elev-1);
  transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.cast-tile-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cast-tile-art img.forged { image-rendering: pixelated; object-fit: contain; }
.cast-tile:hover .cast-tile-art { transform: translateY(-3px); box-shadow: var(--elev-2); }

/* A stripe in their own colour along the foot of the portrait: the cast wall
   and the web and the map all agree about who is what colour. */
.cast-tile-art::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 3px;
  background: var(--tile-colour, var(--accent));
}

.cast-tile-name {
  display: block; margin: 9px 0 0; font-size: calc(14.5px * var(--fs));
  font-weight: 600; color: var(--text); line-height: 1.3;
}
.cast-tile-role {
  display: block; margin-top: 3px;
  font-size: var(--fz-micro); font-weight: 650;
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--tile-colour, var(--muted));
}
.cast-tile-line {
  display: block; margin-top: 5px; max-width: 30ch;
  font-family: var(--font-read); font-style: italic;
  font-size: calc(12.5px * var(--fs)); color: var(--text-3); line-height: 1.5;
}
/* The delete control never sits in the reading matter; it waits on the art. */
.cast-tile-tools {
  position: absolute; top: 7px; right: 7px; z-index: 2;
  opacity: 0; transition: opacity var(--dur-1) var(--ease);
}
.cast-tile:hover .cast-tile-tools,
.cast-tile:focus-within .cast-tile-tools { opacity: 1; }
.cast-tile-tools .btn {
  background: color-mix(in srgb, var(--bg-deep) 74%, transparent);
  backdrop-filter: blur(6px);
}

/* ==========================================================================
   8 · THE FRONTISPIECE
   "N people who did not exist before you" is the best sentence in the app. It
   was rendering at 12px in a middot run-on where "day 142 of this story"
   weighed exactly as much as "Started 3 March".
   ========================================================================== */

.frontispiece {
  text-align: center;
  margin: 4px 0 30px;
  padding: 26px 20px 24px;
  border-bottom: 1px solid var(--hair-ink);
}
.fp-date {
  font-size: var(--fz-micro); font-weight: 650;
  letter-spacing: .22em; text-transform: uppercase;
  color: var(--text-3);
}
.fp-day {
  margin: 10px 0 0;
  font-family: var(--font-read); font-style: italic;
  font-size: calc(22px * var(--fs)); line-height: 1.25;
  color: var(--text);
}
.fp-nums {
  margin: 12px 0 0; display: flex; flex-wrap: wrap;
  justify-content: center; align-items: baseline; gap: 4px 14px;
  font-size: calc(12.5px * var(--fs)); color: var(--text-3);
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: var(--num);
}
.fp-nums b { font-weight: 600; color: var(--text-2); }
.fp-nums i { width: 3px; height: 3px; border-radius: var(--r-circle); background: var(--faint); font-style: normal; }

/* ==========================================================================
   9 · THE CREDIT BLOCK
   "Details" was a <dl> with a 132px label column, five rows deep, in a right
   rail. It is five facts. It is one line.
   ========================================================================== */

.credit-block {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 0 26px; margin: 0 0 26px;
  padding: 0 0 16px; border-bottom: 1px solid var(--hair-ink);
}
.credit-item { display: flex; align-items: baseline; gap: 8px; padding: 5px 0; }
.credit-item dt {
  font-size: var(--fz-micro); font-weight: 650;
  letter-spacing: var(--track); text-transform: uppercase; color: var(--text-3);
}
.credit-item dd {
  margin: 0; font-size: calc(13.5px * var(--fs)); color: var(--text);
  font-variant-numeric: tabular-nums;
}
.credit-item dd.muted { color: var(--text-3); }
/* The status select stops looking like a form control and starts looking like
   a word on a credit, until you reach for it. */
.credit-status {
  font: inherit; font-size: calc(13.5px * var(--fs)); color: var(--text);
  background: none; border: 1px solid transparent; border-radius: var(--r-xs);
  padding: 1px 4px; cursor: pointer;
}
.credit-status:hover, .credit-status:focus { border-color: var(--border); background: var(--raised); }

/* ==========================================================================
   10 · THE FIRST PAGE OF THE BIBLE
   One column of reading matter at a measure, not a CRM with a poster on top.
   ========================================================================== */

.bible { max-width: 80ch; }
/* The to-do list is not part of the bible, but it wants the same measure so
   the page has one left edge and one right edge rather than four. */
.overview-todo { max-width: 80ch; }
.bible-head {
  display: flex; align-items: baseline; gap: 10px; margin: 0 0 10px;
}
.bible-head h3 {
  margin: 0; font-size: var(--fz-micro); font-weight: 650;
  letter-spacing: var(--track); text-transform: uppercase; color: var(--text-3);
}
.bible-head .sub {
  font-family: var(--font-read); font-style: italic;
  font-size: calc(12.5px * var(--fs)); color: var(--faint);
}
.bible-page + .bible-page { margin-top: 34px; }

/* The synopsis is the longest thing a writer reads on this page. It gets the
   reading face and a line height you can live in. */
.bible .rich-edit.reading,
.bible .rich-read.reading {
  font-family: var(--font-read);
  font-size: calc(16.5px * var(--write-fs, 1) * var(--fs));
  line-height: 1.72; max-width: 70ch;
}

/* The logline lives once, in the hero. It just never said it was the logline.
   A tracked whisper appears the moment the cursor is anywhere near it. */
.project-hero .page-sub { position: relative; }
.project-hero .page-sub::before {
  content: "Logline";
  position: absolute; left: 0; top: -15px;
  font-size: var(--fz-micro); font-weight: 650;
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--text-3); opacity: 0;
  transition: opacity var(--dur-1) var(--ease);
  pointer-events: none;
}
.project-hero:hover .page-sub::before,
.project-hero .page-sub:focus::before { opacity: 1; }

/* ==========================================================================
   11 · THE THREE THINGS WORTH LOOKING AT
   The relationship web, the timeline and the arc grid were three text buttons
   in a right rail. Each tile now shows a live miniature of the real thing,
   drawn from this project's own data.
   ========================================================================== */

.tool-tiles {
  display: grid; gap: 14px; margin-top: 38px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}

/*
 * A button does not lay its children out the way a div does.
 *
 * Logan: "The words on the boxes on the right side of the screen that say how
 * is everyone connected and what happened and when, they are almost cut off."
 * Measured, the tile's content ran 9 to 12 pixels past the box at EVERY text
 * size, and `overflow: hidden` ate the difference - so the caption under the
 * name lost its bottom edge and the whole card read as slightly wrong without
 * anything obviously being wrong.
 *
 * The cause is `display: block` on a <button>: the browser wraps the children
 * in an anonymous box of its own and sizes the button from that, which does not
 * account for the last line's descender and the padding under it. A column
 * flexbox lays them out the way the same markup in a <div> would.
 */
.tool-tile {
  display: flex; flex-direction: column; width: 100%; text-align: left; padding: 0; font: inherit;
  color: inherit; cursor: pointer;
  background: var(--surface);
  border: 1px solid var(--border-soft); border-radius: var(--r);
  overflow: hidden;
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease);
}
.tool-tile:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
  transform: translateY(-2px); box-shadow: var(--elev-2);
}

.tool-mini {
  display: block; aspect-ratio: 16 / 9; width: 100%;
  background:
    radial-gradient(90% 120% at 50% 0%, color-mix(in srgb, var(--accent) 7%, transparent), transparent 70%),
    var(--bg-deep);
  border-bottom: 1px solid var(--border-soft);
}
.tool-mini svg { width: 100%; height: 100%; display: block; }
/* While the real data is still being read, the tile is quiet rather than
   jumping. Nothing spins. */
.tool-mini.waiting { opacity: .35; }

.tool-tile-body { padding: 13px 15px 15px; }
.tool-tile-body b {
  display: block; font-size: calc(14px * var(--fs)); font-weight: 600; color: var(--text);
}
.tool-tile-body span {
  display: block; margin-top: 3px;
  font-size: calc(12px * var(--fs)); color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   12 · THE TAB DECK — thirteen tabs, none of them off the edge
   `.tabs { overflow-x: auto; scrollbar-width: none }` meant that on a 1440px
   laptop "Out to market" — where a working screenwriter tracks live
   submissions — was off-screen with no indication it existed. Two semantic
   rows that wrap, and never scroll.
   ========================================================================== */

.tab-deck {
  margin: 20px 0 22px;
  border-bottom: 1px solid var(--border-soft);
}
/* The hero runs to the edge and the deck tucks up under it, exactly as the
   old single strip did (`.project-view .tabs`). */
.project-view .tab-deck { margin-top: 4px; }
.tab-row {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 2px 2px;
  overflow: visible;              /* the whole point: nothing is ever hidden */
}
.tab-row + .tab-row {
  margin-top: 2px; padding-top: 4px;
  border-top: 1px dashed var(--hair-ink);
}
/*
 * The label is a label, not the loudest tab on the row.
 *
 * Logan: "All the options on the top bar when it says 'the story' and lists all
 * the options I have and the 'around it' it's a little to hard to see what to do
 * and its all a bit cluttered."
 *
 * "THE STORY" was set at weight 650 - HEAVIER than the tabs beside it, which
 * are 600 - in the same case, the same tracking and nearly the same size. So
 * the first thing on the busiest row read as the selected tab, and thirteen
 * real tabs read as one undifferentiated line of capitals with two odd ones at
 * the front. The grouping was already right; it just was not visible.
 *
 * Lighter than everything it introduces, and a hairline where the group starts,
 * so the eye can see two groups instead of counting fifteen words.
 */
.tab-row-label {
  flex: none; padding-right: 12px; margin-right: 10px;
  border-right: 1px solid var(--border-soft);
  font-size: var(--fz-micro); font-weight: 500;
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--faint); user-select: none;
}
/* The second row is the quieter one, and says so before you read it. */
.tab-row.secondary .tab { font-size: calc(12.5px * var(--fs)); padding-block: 7px; }
.tab-row.secondary .tab:not(.active) { color: var(--text-3); }

.tab-deck .tab { border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tab-row + .tab-row .tab { margin-bottom: -5px; }

@media (max-width: 720px) {
  .tab-row-label { display: none; }
}

/* ==========================================================================
   13 · SMALL DEBTS PAID
   ========================================================================== */

/* `emptyState` and the dropzone now hand in a drawn sprite rather than an
   emoji, and `.ic` is a fixed 15px, so every one of them was rendering a
   thumbnail-sized icon in a 30px slot. One rule, everywhere a big glyph goes. */
.empty .big .ic,
.dropzone .big .ic { width: calc(30px * var(--fs)); height: calc(30px * var(--fs)); opacity: 1; }
.start-choice .ico .ic { width: calc(26px * var(--fs)); height: calc(26px * var(--fs)); }

/* The small slots that used to hold a glyph and now hold a sprite. */
.shot-star .ic, .shot-x .ic { width: 12px; height: 12px; vertical-align: 0; }
.shot.add span .ic { width: calc(20px * var(--fs)); height: calc(20px * var(--fs)); }
.arc-tip .ic { width: 13px; height: 13px; vertical-align: -1px; }
.row-num .ic { width: 16px; height: 16px; vertical-align: -3px; }
.fr-warn .ic { width: 14px; height: 14px; }

/* Buttons that lead somewhere, with the drawn arrow rather than "→". */
.btn .ic { flex: none; }

/* The dossier no longer counts anything, so the count pill has no callers on
   this page. Left defined in the main stylesheet for the surfaces that do. */

/* ---- from the world rebuild ---- */
/* ==========================================================================
   The world
   --------------------------------------------------------------------------
   Twenty-six kinds of thing grouped into seven families, each family drawn as
   the page it wants rather than as the same page in different words. Almost
   everything here is an instrument: a wheel of the hours, a ribbon of a life,
   a ladder of ranks, a ring of standing, a chain of hands. They are drawn from
   typed answers and typed connections, so no picture here can disagree with
   the page it came from.

   Every colour is a token. The seven family accents already live in the
   stylesheet as .family-* rules setting --entry-accent; everything below
   leans on that one variable so a page, a card, a pin and a bar all agree.

   NEW TOKENS: none. Two local variables are used and both are set inline by
   the markup that needs them:
     --entry-accent   the family accent, or the writer's own colour
     --pred           the colour of one connection's predicate

   RAW COLOURS: seven, all deliberate, all commented where they appear. Four
   are the light of the four hours on the day-wheel and five are the steps of
   the danger gauge (two are shared). They are meaning rather than styling —
   dusk must read as dusk and "a disaster with legs" must read as red in every
   one of the seven themes — so they are literals mixed against --surface
   rather than tokens that a theme could reasonably move. This matches how the
   app already stores relationship and event colours on the server.
   ========================================================================== */

/* ---- the page shell ------------------------------------------------------ */

.world-page { --band-gap: 18px; }

/* A power flies a banner rather than wearing a photograph badly: heraldry is
   drawn tall and centred, not cropped to a landscape strip. */
.entry-hero-forge.is-banner {
  object-fit: contain;
  object-position: center;
  width: 34%;
  left: auto;
  right: clamp(18px, 4vw, 60px);
  filter: drop-shadow(0 10px 26px rgb(0 0 0 / .38));
}

/* ---- the band under the hero --------------------------------------------- */

/*
 * What this thing IS, in figures, before a word of prose. Every family gets
 * its own reading; they share this frame so the eye lands in the same place
 * whichever kind of page it opened.
 */
.world-band {
  margin: 0 auto;
  padding: 20px clamp(14px, 2.4vw, 26px) 4px;
  max-width: 1360px;
  display: flex;
  flex-direction: column;
  gap: var(--band-gap);
}
.world-band .band-row {
  display: flex;
  gap: 20px;
  align-items: stretch;
  flex-wrap: wrap;
}
.world-band .band-holder { flex: 0 0 auto; min-width: 0; }
.band-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font: 600 calc(12px * var(--fs))/1 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.band-head .ic { width: 15px; height: 15px; }

/* ---- readings ------------------------------------------------------------ */

.stat-band {
  flex: 1 1 320px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(126px, 1fr));
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.stat-cell {
  background: var(--surface);
  padding: 14px 16px 13px;
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}
.stat-cell.wide { grid-column: span 2; }
.stat-label {
  font: 600 calc(10.5px * var(--fs))/1.2 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.stat-figure {
  font: 500 calc(23px * var(--fs))/1.05 var(--font-display);
  color: var(--text);
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex-wrap: wrap;
  font-variant-numeric: tabular-nums;
}
.stat-figure em {
  font: 400 calc(11.5px * var(--fs))/1 var(--font-ui);
  font-style: normal;
  color: var(--muted);
}
.stat-sub { font-size: calc(11.5px * var(--fs)); color: var(--muted); }

/* Standing: two figures that mean opposite things, coloured as such. */
.standing-figures { gap: 4px; align-items: baseline; }
.standing-figures b { font-weight: 500; }
.standing-figures .at-war { color: var(--danger); }
.standing-figures .allied { color: var(--ok); }
.standing-figures em + b { margin-left: 8px; }

/* ---- a card for something the page points at ----------------------------- */

/*
 * The reason questions have types. A leader used to be a paragraph, so the app
 * could never draw their face; now it is a person you can click.
 */
.ref-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px 10px 10px;
  min-width: 220px;
  max-width: 340px;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--entry-accent, var(--accent));
  border-radius: var(--r-lg);
  cursor: pointer;
  transition: border-color .16s var(--ease), transform .16s var(--ease), box-shadow .16s var(--ease);
}
.ref-card:hover {
  transform: translateY(-1px);
  border-color: color-mix(in srgb, var(--entry-accent, var(--accent)) 55%, var(--border));
  box-shadow: var(--shadow-2);
}
.ref-card-art {
  width: 46px;
  height: 46px;
  flex: 0 0 auto;
  border-radius: 9px;
  object-fit: cover;
  background: var(--raised);
  image-rendering: pixelated;
}
.ref-card.big { padding: 14px 20px 14px 14px; }
.ref-card.big .ref-card-art { width: 66px; height: 66px; border-radius: 12px; }
.ref-card-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ref-card-body em {
  font: 600 calc(10px * var(--fs))/1 var(--font-ui);
  font-style: normal;
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--entry-accent, var(--accent));
}
.ref-card-body b {
  font: 500 calc(15.5px * var(--fs))/1.25 var(--font-display);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ref-card.big .ref-card-body b { font-size: calc(19px * var(--fs)); }
.ref-card-body > span {
  font-size: calc(12px * var(--fs));
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ref-empty {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  min-width: 220px;
  border: 1px dashed var(--border);
  border-radius: var(--r-lg);
  background: transparent;
  color: var(--muted);
  font-size: calc(13px * var(--fs));
  cursor: pointer;
  transition: border-color .16s var(--ease), color .16s var(--ease);
}
.ref-empty:hover { border-color: var(--entry-accent, var(--accent)); color: var(--text-2); }
.ref-empty.big { padding: 22px 24px; min-width: 250px; }
.ref-empty .ic { width: 16px; height: 16px; }
.ref-change {
  align-self: flex-start;
  margin-top: 6px;
  padding: 0;
  background: none;
  border: 0;
  color: var(--text-3);
  font-size: calc(11.5px * var(--fs));
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.ref-change:hover { color: var(--accent); }

/* ---- where it sits ------------------------------------------------------- */

.ground-chain {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-wrap: wrap;
  font-size: calc(12.5px * var(--fs));
}
.ground-chain-step {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 9px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
}
.ground-chain-step:hover { background: var(--raised); color: var(--text); }
.ground-chain-step .ic { width: 13px; height: 13px; opacity: .6; }
.chain-glyph { font-size: calc(13px * var(--fs)); line-height: 1; }
.ground-chain-sep { color: var(--faint); display: inline-flex; }
.ground-chain-sep .ic { width: 13px; height: 13px; }
.ground-chain-here {
  padding: 4px 10px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--entry-accent, var(--accent)) 15%, transparent);
  color: var(--text);
  font-weight: 600;
}

/* ---- the hours ----------------------------------------------------------- */

/*
 * A tavern at noon and a tavern at three in the morning are two different
 * locations wearing the same name, and no amount of prose in one box says so.
 */
.day-wheel {
  display: flex;
  gap: 26px;
  align-items: center;
  flex-wrap: wrap;
}
.day-wheel-dial { width: 190px; height: 190px; flex: 0 0 auto; }
.dw-rim { fill: none; stroke: var(--border); stroke-width: 1; }
.dw-hub { fill: var(--surface); stroke: var(--border); stroke-width: 1; }
.dw-arc {
  fill: var(--raised);
  stroke: var(--bg);
  stroke-width: 1.5;
  cursor: pointer;
  transition: fill .18s var(--ease), opacity .18s var(--ease);
}
.dw-arc:hover { fill: var(--raised-2); }
/* Lit quadrants carry the light of their own hour: cold at dawn, warm at
   noon, low at dusk, near-dark at night. Read at a glance, in any theme. */
.dw-arc.on.dw-dawn  { fill: color-mix(in srgb, #e0a33e 42%, var(--surface)); }
.dw-arc.on.dw-noon  { fill: color-mix(in srgb, #f2c94c 52%, var(--surface)); }
.dw-arc.on.dw-dusk  { fill: color-mix(in srgb, #c0504a 38%, var(--surface)); }
.dw-arc.on.dw-night { fill: color-mix(in srgb, #3f5aa8 38%, var(--surface)); }
.dw-arc.held { stroke: var(--entry-accent, var(--accent)); stroke-width: 2.5; }
.dw-label {
  font: 600 9.5px var(--font-ui);
  letter-spacing: .1em;
  text-transform: uppercase;
  fill: var(--text-2);
  pointer-events: none;
}
.dw-centre {
  font: 500 13px var(--font-display);
  fill: var(--text-3);
  pointer-events: none;
}
.day-wheel-read { flex: 1 1 260px; min-width: 0; }
.day-wheel-read h4 {
  margin: 0 0 6px;
  font: 500 calc(17px * var(--fs))/1.2 var(--font-display);
  color: var(--text);
}
.day-wheel-read p {
  margin: 0;
  font: 400 calc(15px * var(--fs))/1.65 var(--font-read);
  color: var(--text-2);
  max-width: 46ch;
}
.day-wheel-hint {
  display: inline-block;
  margin-top: 10px;
  font-size: calc(11.5px * var(--fs));
  color: var(--faint);
}
.hours-fields { margin-top: 18px; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }

/* ---- the senses ---------------------------------------------------------- */

/* Across, not down. Down a column they read as four more boxes to fill;
   side by side they read as one place. */
.senses-band {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.sense {
  background: var(--surface);
  padding: 14px 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  min-height: 116px;
}
.sense-glyph { color: var(--entry-accent, var(--accent)); opacity: .85; }
.sense-glyph .ic { width: 17px; height: 17px; }
.sense-label {
  font: 600 calc(10.5px * var(--fs))/1 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.sense-value {
  font: 400 calc(14px * var(--fs))/1.55 var(--font-read);
  color: var(--text-2);
  min-height: 1.55em;
}
.sense.empty { background: color-mix(in srgb, var(--surface) 70%, var(--bg)); }
.sense.empty .sense-glyph { opacity: .3; }

/* ---- a life -------------------------------------------------------------- */

/*
 * Five stages laid end to end with a human life ghosted behind at the same
 * scale. That comparison is the whole point: "three hundred years" means
 * nothing until you can see how much of it is spent being old.
 */
.life-ribbon { display: flex; flex-direction: column; gap: 12px; }
.life-scale { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.life-span {
  font: 500 calc(21px * var(--fs))/1 var(--font-display);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.life-compare { font-size: calc(12px * var(--fs)); color: var(--muted); }
.life-track {
  position: relative;
  height: 46px;
  border-radius: var(--r);
  background: var(--raised);
  overflow: hidden;
}
.life-human {
  position: absolute;
  inset: 0 auto 0 0;
  border-right: 1px dashed var(--text-3);
  background: repeating-linear-gradient(
    135deg, transparent 0 5px, color-mix(in srgb, var(--text-3) 12%, transparent) 5px 10px);
}
.life-human span {
  position: absolute;
  right: 6px;
  bottom: 4px;
  font-size: calc(10px * var(--fs));
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
.life-stages { position: absolute; inset: 0; }
.life-stage {
  position: absolute;
  top: 8px;
  bottom: 8px;
  border-radius: 3px;
  background: var(--entry-accent, var(--accent));
  box-shadow: inset -1px 0 0 var(--bg);
}
/* The arc of a life, lightest at the start and darkest at the end. */
.life-stage.st-infancy { opacity: .32; }
.life-stage.st-coming  { opacity: .5; }
.life-stage.st-prime   { opacity: .95; }
.life-stage.st-decline { opacity: .55; }
.life-stage.st-death   { opacity: .28; }
.life-stage.empty { background: var(--text-3); opacity: .18; }
/* The columns are written inline, from the same shares the bar's segments are
   drawn from, so the words sit under the part of the life they describe. The
   fallback here is only for a ribbon rendered without them. */
.life-words {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px;
}
/* A prime is 46% of a life and an infancy is 6%, so one column is seven times
   another and the narrow ones have to be allowed to break a word rather than
   push the whole row wider than the bar above it. */
.life-word { overflow-wrap: anywhere; }
.life-word { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.life-word-head {
  font: 600 calc(10px * var(--fs))/1.2 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.life-word-body {
  font: 400 calc(13px * var(--fs))/1.5 var(--font-read);
  color: var(--text-2);
}
.life-word.empty .life-word-body::before {
  content: '—';
  color: var(--faint);
}
@media (max-width: 900px) {
  .life-words { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.life-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 16px; }

/* ---- how dangerous ------------------------------------------------------- */

.creature-row { align-items: flex-start; }
.creature-gauges { display: flex; gap: 26px; flex-wrap: wrap; flex: 1 1 300px; }
.gauge { display: flex; flex-direction: column; gap: 8px; }
.danger-dial { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.danger-dial svg { width: 150px; height: 90px; }
.dd-step { fill: var(--raised); stroke: var(--surface); stroke-width: 1.5; }
/* Five steps from calm to catastrophe. The needle says which. */
.dd-step.on.dd-0 { fill: #5aab72; }
.dd-step.on.dd-1 { fill: #9bb04e; }
.dd-step.on.dd-2 { fill: #e0a33e; }
.dd-step.on.dd-3 { fill: #d4703a; }
.dd-step.on.dd-4 { fill: #c0504a; }
.dd-needle { stroke: var(--text); stroke-width: 2.5; stroke-linecap: round; }
.dd-pin { fill: var(--text); }
.danger-word {
  font: 500 calc(14px * var(--fs))/1 var(--font-display);
  color: var(--text);
}
.danger-dial.unset .danger-word { color: var(--faint); font-style: italic; }
.tame-row { display: flex; flex-wrap: wrap; gap: 5px; }
.tame-why { font-size: calc(11.5px * var(--fs)); color: var(--faint); max-width: 32ch; }

/* ---- strength against its peers ------------------------------------------ */

/* A number on its own is a fact; a number beside its peers is a story. */
.strength-bars {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 14px 16px 12px;
  background: var(--surface);
}
.strength-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}
.strength-head h4 {
  margin: 0;
  font: 600 calc(10.5px * var(--fs))/1 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.strength-rank { font-size: calc(12px * var(--fs)); color: var(--muted); }
.strength-row {
  display: grid;
  grid-template-columns: minmax(90px, 150px) 1fr auto;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 4px 6px;
  border: 0;
  border-radius: 7px;
  background: none;
  cursor: pointer;
  text-align: left;
}
.strength-row:hover { background: var(--raised); }
.strength-name {
  font-size: calc(12.5px * var(--fs));
  color: var(--text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.strength-track {
  height: 9px;
  border-radius: 999px;
  background: var(--raised);
  overflow: hidden;
}
.strength-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--text-3);
  transition: width .4s var(--ease);
}
.strength-figure {
  font: 500 calc(12.5px * var(--fs))/1 var(--font-display);
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.strength-figure em { font-style: normal; color: var(--faint); font-size: calc(10.5px * var(--fs)); }
.strength-row.me .strength-name { color: var(--text); font-weight: 600; }
.strength-row.me .strength-fill { background: var(--entry-accent, var(--accent)); }
.strength-row.me .strength-figure { color: var(--text); }

/* ---- the ring ------------------------------------------------------------ */

/* A page of green with one red spoke in it is telling you where the next
   scene is. */
.ring { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; }
.ring-svg { width: 300px; height: 300px; flex: 0 0 auto; }
.ring-orbit { fill: none; stroke: var(--border); stroke-dasharray: 2 6; }
.ring-spoke { stroke-width: 2; opacity: .55; }
.ring-node { cursor: pointer; stroke: var(--bg); stroke-width: 2.5; transition: r .16s var(--ease); }
.ring-node:hover { r: 12; }
.ring-hub { fill: var(--surface); stroke: var(--entry-accent, var(--accent)); stroke-width: 2; }
.ring-hub-text { font: 600 10px var(--font-ui); fill: var(--text); pointer-events: none; }
.ring-key {
  flex: 1 1 220px;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.ring-key button {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: 7px;
  background: none;
  cursor: pointer;
  text-align: left;
}
.ring-key button:hover { background: var(--raised); }
.ring-key i { width: 9px; height: 9px; border-radius: 3px; flex: 0 0 auto; }
.ring-key b {
  font: 500 calc(13px * var(--fs))/1.3 var(--font-ui);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ring-key span { font-size: calc(11.5px * var(--fs)); color: var(--muted); margin-left: auto; white-space: nowrap; }

/* ---- the roster ladder --------------------------------------------------- */

/*
 * The ranks the writer listed become rungs, and everyone linked to this power
 * stands on the rung their connection names. Dragging a face up a rung
 * rewrites the connection, because a diagram that does not write back is a
 * drawing of a lie.
 */
.ladder { display: flex; flex-direction: column; }
.ladder-top {
  display: flex;
  align-items: center;
  gap: 16px;
  padding-bottom: 16px;
  margin-bottom: 4px;
  border-bottom: 2px solid color-mix(in srgb, var(--entry-accent, var(--accent)) 40%, var(--border));
  flex-wrap: wrap;
}
.rung {
  display: grid;
  grid-template-columns: minmax(110px, 170px) 1fr;
  align-items: center;
  gap: 14px;
  padding: 10px 8px;
  border-bottom: 1px solid var(--border);
  border-radius: 6px;
  transition: background .14s var(--ease), box-shadow .14s var(--ease);
}
.rung:last-of-type { border-bottom: 0; }
.rung.over {
  background: color-mix(in srgb, var(--entry-accent, var(--accent)) 12%, transparent);
  box-shadow: inset 0 0 0 1px var(--entry-accent, var(--accent));
}
.rung-name {
  display: flex;
  align-items: center;
  gap: 8px;
  font: 600 calc(11px * var(--fs))/1.3 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-2);
}
.rung-name i {
  display: inline-grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  background: var(--raised);
  color: var(--text-3);
  font: 500 10px/1 var(--font-mono);
  font-style: normal;
}
.rung-faces { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; min-height: 40px; }
.rung-face {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 4px 11px 4px 4px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  cursor: grab;
  transition: border-color .14s var(--ease), transform .14s var(--ease);
}
.rung-face:hover { border-color: var(--entry-accent, var(--accent)); transform: translateY(-1px); }
.rung-face:active { cursor: grabbing; }
.rung-face.lifting { opacity: .35; }
.rung-face img {
  width: 28px;
  height: 28px;
  border-radius: 999px;
  object-fit: cover;
  background: var(--raised);
  image-rendering: pixelated;
}
.rung-face span { font-size: calc(12.5px * var(--fs)); color: var(--text); white-space: nowrap; }
.rung-empty { font-size: calc(12px * var(--fs)); color: var(--faint); font-style: italic; }
.ladder-hint { margin: 12px 0 0; font-size: calc(11.5px * var(--fs)); color: var(--faint); }
.roster-ranks { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }

/* ---- the provenance chain ------------------------------------------------ */

/*
 * A relic is not its description, it is its route. A gap with nothing in it
 * is a scene nobody has written yet, which is the most useful thing this page
 * can say to a writer — so blank gaps are marked, not hidden.
 */
.provenance {
  list-style: none;
  margin: 0;
  padding: 4px 0 8px;
  display: flex;
  align-items: flex-start;
  gap: 0;
  overflow-x: auto;
  scrollbar-width: thin;
}
.provenance.empty {
  display: block;
  padding: 18px 20px;
  border: 1px dashed var(--border);
  border-radius: var(--r-lg);
}
.provenance.empty p { margin: 0; color: var(--muted); font-size: calc(13.5px * var(--fs)); max-width: 60ch; }
.prov-link { display: flex; align-items: center; gap: 0; flex: 0 0 auto; }
.prov-node button,
.prov-unknown {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 104px;
  padding: 10px 6px;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
  cursor: pointer;
  text-align: center;
}
.prov-node button:hover { border-color: var(--entry-accent, var(--accent)); }
.prov-unknown { cursor: default; border-style: dashed; }
.prov-node img,
.prov-unknown i {
  width: 42px;
  height: 42px;
  border-radius: 9px;
  object-fit: cover;
  background: var(--raised);
  image-rendering: pixelated;
  display: grid;
  place-items: center;
  color: var(--faint);
}
.prov-unknown i .ic { width: 20px; height: 20px; }
.prov-node b,
.prov-unknown b {
  font: 500 calc(12px * var(--fs))/1.3 var(--font-ui);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.prov-unknown b { color: var(--faint); font-style: italic; }
.prov-how {
  align-self: flex-start;
  margin: 14px 0 0 -6px;
  font-size: calc(10px * var(--fs));
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
.prov-gap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 74px;
  flex: 0 0 auto;
}
.prov-rule {
  width: 100%;
  height: 2px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--entry-accent, var(--accent)) 50%, var(--border));
}
.prov-gap em {
  font: 400 calc(11px * var(--fs))/1 var(--font-ui);
  font-style: normal;
  color: var(--muted);
  white-space: nowrap;
}
.prov-gap.blank .prov-rule {
  background: repeating-linear-gradient(90deg, var(--border) 0 4px, transparent 4px 8px);
}
.prov-gap.blank em { color: var(--faint); font-style: italic; }
.prov-link.now .prov-node button {
  border-color: var(--entry-accent, var(--accent));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--entry-accent, var(--accent)) 16%, transparent);
}

/* ---- what they call themselves ------------------------------------------- */

/* Endonym and exonym given top billing, facing each other, because the gap
   between the two names is usually the whole relationship. */
.naming {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 22px;
  padding: 18px 22px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
}
.naming-half { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.naming-half:last-child { text-align: right; }
.naming-half span {
  font: 600 calc(10px * var(--fs))/1 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.naming-half b {
  font: 400 calc(24px * var(--fs))/1.2 var(--font-read);
  color: var(--text);
}
.naming-half.empty b { color: var(--faint); }
.naming-rule {
  width: 1px;
  height: 34px;
  background: var(--border);
}
@media (max-width: 700px) {
  .naming { grid-template-columns: 1fr; gap: 12px; }
  .naming-half:last-child { text-align: left; }
  .naming-rule { width: 100%; height: 1px; }
}

/* ---- the three that matter ----------------------------------------------- */

/*
 * The first three questions of every kind are the ones that matter, so they
 * are set large and alone above the page rather than buried in a grid of
 * twenty. Choosing them was the work.
 */
.lead-band {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
  margin-bottom: 18px;
}
.lead-band.one-loud { grid-template-columns: 1fr; }
.lead-band.one-loud .lead-q:not(.loud) { display: none; }
.lead-q > .dossier-field {
  height: 100%;
  padding: 18px 20px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  border-top: 3px solid color-mix(in srgb, var(--entry-accent, var(--accent)) 60%, var(--border));
}
.lead-q > .dossier-field > label {
  font-size: calc(11px * var(--fs));
  color: var(--text-3);
}
.lead-q .fact-plain,
.lead-q .dossier-value {
  font: 400 calc(16px * var(--fs))/1.7 var(--font-read);
  color: var(--text);
}
.lead-q.loud > .dossier-field { padding: 30px 34px; }
.lead-q.loud .fact-plain,
.lead-q.loud .dossier-value {
  font: 400 calc(23px * var(--fs))/1.5 var(--font-read);
  max-width: 44ch;
}
.lead-q .fact-figure b { font-size: calc(38px * var(--fs)); }

/* ---- a typed answer ------------------------------------------------------ */

/*
 * Two nodes, not one: the drawn reading and the plain words underneath it that
 * actually get edited. They swap on a click. Putting the drawing inside the
 * editable box would mean the commas and the unit were read back as part of
 * the writer's own answer next time they touched it.
 */
.dossier-field .fact-shown { cursor: text; }
.dossier-field:not(.editing):not(.empty) .dossier-value.behind { display: none; }
.dossier-field.editing .fact-shown,
.dossier-field.empty .fact-shown { display: none; }

.fact-figure { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.fact-figure b {
  font: 500 calc(30px * var(--fs))/1 var(--font-display);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.fact-figure em {
  font: 400 calc(12.5px * var(--fs))/1 var(--font-ui);
  font-style: normal;
  color: var(--muted);
}
.fact-date { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.fact-date-mark { display: inline-flex; color: var(--entry-accent, var(--accent)); }
.fact-date-mark .ic { width: 15px; height: 15px; }
.fact-date b { font: 500 calc(15.5px * var(--fs))/1.3 var(--font-display); color: var(--text); }
.fact-date em {
  font-style: normal;
  font-size: calc(10.5px * var(--fs));
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
  padding: 2px 7px;
  border: 1px solid var(--border);
  border-radius: 999px;
}
.fact-plain { font: 400 calc(14px * var(--fs))/1.65 var(--font-read); color: var(--text-2); white-space: pre-wrap; }
.fact-plain.long { max-width: 66ch; }

.enum-row, .fact-enum { display: flex; flex-wrap: wrap; gap: 5px; }
.enum-chip {
  padding: 5px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-2);
  font-size: calc(12px * var(--fs));
  cursor: pointer;
  transition: border-color .14s var(--ease), background .14s var(--ease), color .14s var(--ease);
}
.enum-chip:hover { border-color: var(--text-3); color: var(--text); }
.enum-chip.on {
  border-color: var(--entry-accent, var(--accent));
  background: color-mix(in srgb, var(--entry-accent, var(--accent)) 16%, transparent);
  color: var(--text);
  font-weight: 600;
}

.fact-list, .list-rows { margin: 0; padding: 0; list-style: none; counter-reset: rung; }
.fact-list li, .list-rows li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 4px 0;
  font-size: calc(13.5px * var(--fs));
  color: var(--text-2);
}
.fact-list li { counter-increment: rung; }
.fact-list li::before {
  content: counter(rung);
  font: 500 10px/1 var(--font-mono);
  color: var(--faint);
  min-width: 14px;
}
.list-hand {
  font: 500 10px/1 var(--font-mono);
  color: var(--faint);
  min-width: 14px;
}
.list-cell, .table-cell { flex: 1; min-width: 40px; color: var(--text); }
.list-drop {
  border: 0;
  background: none;
  color: var(--faint);
  cursor: pointer;
  padding: 2px;
  border-radius: 5px;
  display: inline-flex;
  opacity: 0;
  transition: opacity .14s var(--ease);
}
.list-rows li:hover .list-drop,
.table-row:hover .list-drop { opacity: 1; }
.list-drop:hover { color: var(--danger); background: var(--raised); }
.list-drop .ic { width: 14px; height: 14px; }

.fact-table-wrap, .table-rows { overflow-x: auto; }
.fact-table { border-collapse: collapse; width: 100%; font-size: calc(13px * var(--fs)); }
.fact-table th {
  text-align: left;
  padding: 6px 12px 6px 0;
  font: 600 calc(10px * var(--fs))/1 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
  border-bottom: 1px solid var(--border);
}
.fact-table td { padding: 7px 12px 7px 0; color: var(--text-2); border-bottom: 1px solid var(--border); }
.table-head, .table-row {
  display: grid;
  grid-template-columns: repeat(var(--cols, 2), minmax(90px, 1fr)) 24px;
  gap: 10px;
  align-items: baseline;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
}
.table-head {
  font: 600 calc(10px * var(--fs))/1 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.table-row { font-size: calc(13px * var(--fs)); }

/* ---- panels a family gets for itself ------------------------------------- */

.approach-pair, .closing-pair, .door-pair, .facing-pair {
  display: grid;
  gap: 20px;
}
.approach-pair, .closing-pair { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/* Deliberately unequal, because getting in and getting out almost never are. */
.door-pair { grid-template-columns: 5fr 7fr; }
.door {
  padding: 16px 18px;
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
}
.door.in { background: color-mix(in srgb, var(--ok) 7%, var(--surface)); }
.door.out { background: color-mix(in srgb, var(--danger) 7%, var(--surface)); }
@media (max-width: 800px) { .door-pair { grid-template-columns: 1fr; } }

/* The story they tell, and the story told about them, facing. */
.facing-pair { grid-template-columns: 1fr 1fr; position: relative; }
.facing { padding: 16px 18px; }
.facing.said { border-right: 1px solid var(--border); }
.facing.heard { color: var(--text-2); }
.facing.heard .dossier-value, .facing.heard .fact-plain { font-style: italic; }
@media (max-width: 800px) {
  .facing-pair { grid-template-columns: 1fr; }
  .facing.said { border-right: 0; border-bottom: 1px solid var(--border); }
}

.closing-panel { border-top: 2px solid color-mix(in srgb, var(--entry-accent, var(--accent)) 35%, var(--border)); }
.closing-panel h3 .ic { width: 16px; height: 16px; vertical-align: -3px; margin-right: 4px; }

.inside-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
}
.inside-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--entry-accent, var(--accent));
  border-radius: var(--r);
  background: var(--surface);
  cursor: pointer;
  text-align: left;
  transition: transform .14s var(--ease), border-color .14s var(--ease);
}
.inside-card:hover { transform: translateY(-2px); border-color: var(--entry-accent, var(--accent)); }
.inside-card img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 6px;
  background: var(--raised);
  image-rendering: pixelated;
}
.inside-card b { font: 500 calc(13.5px * var(--fs))/1.3 var(--font-display); color: var(--text); }
.inside-card span {
  font-size: calc(11.5px * var(--fs));
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.face-row { display: flex; flex-wrap: wrap; gap: 10px; }
.face {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  width: 92px;
  padding: 8px 4px;
  border: 0;
  border-radius: var(--r);
  background: none;
  cursor: pointer;
  text-align: center;
}
.face:hover { background: var(--raised); }
.face img {
  width: 52px;
  height: 52px;
  border-radius: 999px;
  object-fit: cover;
  background: var(--raised);
  image-rendering: pixelated;
}
.face b {
  font: 500 calc(12px * var(--fs))/1.25 var(--font-ui);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.face span { font-size: calc(10.5px * var(--fs)); color: var(--faint); }

/* ---- connections, said the right way round ------------------------------- */

.links-panel .link-group { margin-top: 12px; }
.links-panel .link-group h4 {
  margin: 0 0 4px;
  font: 600 calc(10px * var(--fs))/1 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.link-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 8px;
  border-radius: var(--r);
  border-left: 3px solid var(--pred, var(--border));
  cursor: pointer;
  transition: background .14s var(--ease);
}
.link-row:hover { background: var(--raised); }
.link-face {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--raised-2);
  display: grid;
  place-items: center;
}
.link-face img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
.link-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.link-pred {
  font: 600 calc(9.5px * var(--fs))/1.2 var(--font-ui);
  font-style: normal;
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--pred, var(--muted));
}
.link-main b {
  font: 500 calc(13.5px * var(--fs))/1.3 var(--font-ui);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.link-note { font-size: calc(11.5px * var(--fs)); color: var(--muted); font-style: italic; }
.link-actions { display: flex; gap: 2px; opacity: 0; transition: opacity .14s var(--ease); }
.link-row:hover .link-actions,
.link-row:focus-within .link-actions { opacity: 1; }
.link-actions .ic { width: 14px; height: 14px; }

/* ---- choosing what to add ------------------------------------------------ */

/*
 * This used to be a select with twenty-six options, each a full sentence.
 * Now: seven shelves you can see, then the kinds on one, each drawn.
 */
.kind-chooser .modal-body { padding-top: 4px; }
.family-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
}
.family-tile {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 18px 18px 16px;
  border: 1px solid var(--border);
  border-top: 3px solid var(--entry-accent, var(--accent));
  border-radius: var(--r-lg);
  background: var(--surface);
  cursor: pointer;
  text-align: left;
  transition: transform .16s var(--ease), box-shadow .16s var(--ease), border-color .16s var(--ease);
}
.family-tile:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-2);
  border-color: var(--entry-accent, var(--accent));
}
.family-tile-glyph { color: var(--entry-accent, var(--accent)); }
.family-tile-glyph .ic { width: 24px; height: 24px; }
.family-tile b { font: 500 calc(19px * var(--fs))/1.15 var(--font-display); color: var(--text); }
.family-tile em {
  font-style: normal;
  font-size: calc(12px * var(--fs));
  color: var(--entry-accent, var(--accent));
}
.family-tile p {
  margin: 4px 0 0;
  font-size: calc(12.5px * var(--fs));
  line-height: 1.5;
  color: var(--muted);
}
.family-tile-count {
  margin-top: auto;
  padding-top: 10px;
  font-size: calc(10.5px * var(--fs));
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--faint);
}
.chooser-back {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-bottom: 10px;
  padding: 4px 10px 4px 6px;
  border: 0;
  border-radius: 999px;
  background: var(--raised);
  color: var(--text-2);
  font-size: calc(12px * var(--fs));
  cursor: pointer;
}
.chooser-back:hover { color: var(--text); }
.chooser-back .ic { width: 14px; height: 14px; }
.chooser-head {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 14px;
  font: 500 calc(20px * var(--fs))/1.2 var(--font-display);
  color: var(--text);
}
.chooser-head > span { display: inline-flex; color: var(--entry-accent, var(--accent)); }
.chooser-head .ic { width: 20px; height: 20px; }
.chooser-head em {
  font-style: normal;
  font-size: calc(12.5px * var(--fs));
  font-family: var(--font-ui);
  color: var(--muted);
  margin-left: auto;
}
.kind-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
}
.kind-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  padding: 14px 12px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  cursor: pointer;
  text-align: center;
  transition: transform .16s var(--ease), border-color .16s var(--ease), box-shadow .16s var(--ease);
}
.kind-card:hover {
  transform: translateY(-3px);
  border-color: var(--entry-accent, var(--accent));
  box-shadow: var(--shadow-2);
}
.kind-card img {
  width: 64px;
  height: 64px;
  border-radius: 12px;
  image-rendering: pixelated;
  background: var(--raised);
}
.kind-card b { font: 500 calc(15px * var(--fs))/1.2 var(--font-display); color: var(--text); }
.kind-card span { font-size: calc(11.5px * var(--fs)); line-height: 1.45; color: var(--muted); }

/* ---- picking a page ------------------------------------------------------ */

.ref-picker .ref-search { width: 100%; margin-bottom: 10px; }
.ref-picker .ref-list { max-height: 46vh; overflow-y: auto; scrollbar-width: thin; }
.ref-list h4 {
  margin: 12px 0 4px;
  font: 600 calc(10px * var(--fs))/1 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.ref-list h4:first-child { margin-top: 0; }
.ref-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 8px;
  border: 0;
  border-radius: var(--r);
  background: none;
  cursor: pointer;
  text-align: left;
}
.ref-row:hover { background: var(--raised); }
.ref-row img {
  width: 34px;
  height: 34px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--raised-2);
  image-rendering: pixelated;
}
.ref-row span { display: flex; flex-direction: column; min-width: 0; }
.ref-row b { font: 500 calc(13.5px * var(--fs))/1.3 var(--font-ui); color: var(--text); }
.ref-row em { font-style: normal; font-size: calc(11px * var(--fs)); color: var(--muted); }

/* ---- what a connection says ---------------------------------------------- */

.link-dialog .pred-groups { display: flex; flex-direction: column; gap: 12px; }
.pred-group h4 {
  margin: 0 0 5px;
  font: 600 calc(10px * var(--fs))/1 var(--font-ui);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--text-3);
}
.pred-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.pred-chip {
  padding: 6px 13px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--pred, var(--border));
  border-radius: 999px;
  background: transparent;
  color: var(--text-2);
  font-size: calc(12.5px * var(--fs));
  cursor: pointer;
  transition: background .14s var(--ease), color .14s var(--ease);
}
.pred-chip:hover { color: var(--text); background: var(--raised); }
.pred-chip.on {
  background: color-mix(in srgb, var(--pred, var(--accent)) 20%, transparent);
  color: var(--text);
  font-weight: 600;
  border-color: var(--pred, var(--accent));
}

/* ==========================================================================
   The world tab
   ========================================================================== */

.world-controls {
  display: flex;
  gap: 10px;
  align-items: center;
  margin: 14px 0 10px;
  flex-wrap: wrap;
}
.world-search {
  flex: 1 1 260px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  transition: border-color .16s var(--ease);
}
.world-search:focus-within { border-color: var(--accent); }
.world-search .ic { width: 16px; height: 16px; color: var(--text-3); flex: 0 0 auto; }
.world-search .input {
  flex: 1;
  border: 0;
  background: none;
  padding: 9px 0;
  min-width: 0;
}
.world-search .input:focus { outline: none; box-shadow: none; }
.world-sort .el-picker { min-width: 170px; }

.family-tabs {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
.family-tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  color: var(--text-2);
  font-size: calc(12.5px * var(--fs));
  cursor: pointer;
  transition: background .14s var(--ease), color .14s var(--ease), border-color .14s var(--ease);
}
.family-tab .ic { width: 15px; height: 15px; opacity: .75; }
.family-tab span {
  font: 500 calc(10.5px * var(--fs))/1 var(--font-mono);
  color: var(--faint);
  padding: 2px 5px;
  border-radius: 4px;
  background: var(--raised);
}
.family-tab:hover { background: var(--raised); color: var(--text); }
.family-tab.on {
  border-color: color-mix(in srgb, var(--entry-accent, var(--accent)) 50%, var(--border));
  background: color-mix(in srgb, var(--entry-accent, var(--accent)) 13%, transparent);
  color: var(--text);
  font-weight: 600;
}
.family-tab.on .ic { opacity: 1; color: var(--entry-accent, var(--accent)); }

.world-group .ic { width: 15px; height: 15px; vertical-align: -3px; margin-right: 5px; opacity: .8; }
.world-group.family-grounds .ic,
.world-group.family-powers .ic,
.world-group.family-peoples .ic,
.world-group.family-relics .ic,
.world-group.family-forces .ic,
.world-group.family-epochs .ic,
.world-group.family-customs .ic { color: var(--entry-accent); opacity: 1; }

/* The writer's own emoji, riding the corner of the card it belongs to.
   .world-card-art is already `position: relative`; nothing here re-declares it. */
.world-card-glyph {
  position: absolute;
  right: 7px;
  bottom: 7px;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  font-size: 14px;
  line-height: 1;
  border-radius: 8px;
  background: color-mix(in srgb, var(--bg) 76%, transparent);
  backdrop-filter: blur(3px);
  box-shadow: 0 1px 4px rgb(0 0 0 / .3);
}
.world-card-figure {
  margin: 5px 0 0;
  font-size: calc(11.5px * var(--fs));
  color: var(--muted);
}
.world-card-figure b {
  font: 500 calc(14px * var(--fs))/1 var(--font-display);
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

.history-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.history-head .world-group { margin-bottom: 0; }
.era-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; }
.era-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-style: normal;
  font-size: calc(10.5px * var(--fs));
  color: var(--muted);
}
.era-tag .ic { width: 11px; height: 11px; }
.era-tag.has-page { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
.era-tag.cost { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--border)); }
/* An event with a page of its own carries its own accent on the spine. Only
   the spine cards: an era divider is a centred pill and a stripe down one
   side of it would read as a mistake. */
.world-era.spine-card.src-world { border-left: 3px solid var(--entry-accent, var(--accent)); }

/* ==========================================================================
   The web of the world
   ========================================================================== */

.web-key { display: flex; gap: 12px; flex-wrap: wrap; }
.web-key-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: calc(11px * var(--fs));
  color: var(--muted);
}
.web-key-item i { width: 8px; height: 8px; border-radius: 2px; }
/* The family accent rings the face rather than the button, because
   .worldweb-node is absolutely positioned by the spring layout and must be
   left entirely alone: it is the layout, not decoration. */
.worldweb-node.family-grounds .worldweb-face,
.worldweb-node.family-powers .worldweb-face,
.worldweb-node.family-peoples .worldweb-face,
.worldweb-node.family-relics .worldweb-face,
.worldweb-node.family-forces .worldweb-face,
.worldweb-node.family-epochs .worldweb-face,
.worldweb-node.family-customs .worldweb-face {
  border-color: color-mix(in srgb, var(--entry-accent) 60%, var(--border));
}
.worldweb-face img { image-rendering: pixelated; }
/* The writer's own emoji, on the shoulder of the drawn face. The node is
   already a containing block, so this needs no positioning of its own added
   to the node itself. */
.worldweb-glyph {
  position: absolute;
  right: 2px;
  top: 2px;
  font-size: 12px;
  line-height: 1;
  padding: 2px;
  border-radius: 6px;
  background: var(--surface);
  box-shadow: 0 1px 3px rgb(0 0 0 / .3);
}

/* ==========================================================================
   The chronology
   -------------------------------------------------------------------------- */

/*
 * A page-backed event is told apart by its dot, not by its box.
 *
 * The obvious move was a stripe down the left of the row, and it is wrong:
 * .tl-event is a three-column grid, so a border on some rows and not others
 * gives the whole timeline a ragged left edge. The two halves of one
 * chronology should look like one list, because they are one list.
 */
.tl-event.src-world .tl-dot {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--entry-accent, var(--accent)) 30%, transparent);
}
.tl-glyph { margin-right: 6px; font-size: calc(14px * var(--fs)); }
.tl-tag.page { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
.tl-tag.page .ic { width: 11px; height: 11px; vertical-align: -1px; margin-right: 3px; }
.tl-tag.era { color: var(--text); background: var(--raised-2); }
.tl-tag.cost { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--border)); }
.tl-event.is-era b { font-size: calc(17px * var(--fs)); }

/* ==========================================================================
   The atlas
   -------------------------------------------------------------------------- */

/* A pin now wears the entry's own colour, which was collected and thrown away
   before: a red kingdom pins red. */
.map-pin[style*="--entry-accent"] .map-pin-label {
  border-color: color-mix(in srgb, var(--entry-accent) 55%, var(--border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--entry-accent) 26%, transparent);
}

/* ==========================================================================
   Narrow windows
   ========================================================================== */

@media (max-width: 1000px) {
  .world-band { padding-left: 14px; padding-right: 14px; }
  .day-wheel-dial { width: 150px; height: 150px; }
  .ring-svg { width: 240px; height: 240px; }
  .rung { grid-template-columns: 1fr; gap: 6px; }
  .lead-band.one-loud .lead-q.loud > .dossier-field { padding: 20px 22px; }
  .lead-q.loud .fact-plain,
  .lead-q.loud .dossier-value { font-size: calc(18px * var(--fs)); }
}

/* ---- from the script rebuild ---- */
/* ============================================================
   The writing surface.

   Everything here belongs to web/views/script.js: the paper, the
   chrome around it, and the three quiet ways the app says
   something about a day's work.

   It replaces the old ribbon-and-dashed-rule page. Where a rule
   in the main stylesheet still describes that page, the override
   is marked OVERRIDES. Those old declarations can be deleted
   outright once this is merged, with one caveat: room-test
   asserts `/\.script-page \{[^}]*background/` under the name "the
   page itself stays opaque". The paper is opaque, but it is now
   `.sheet` that carries the colour, so that expectation wants
   moving to `.sheet` before the old declaration goes.
   ============================================================ */

:root {
  /* NEW TOKEN. The edge of a sheet of paper, against the paper itself, so it
     reads as a lifted edge in every theme without hardcoding a grey.
     The gutter between sheets needs no token: it is the shell's own
     --bg-deep, showing through. */
  --sheet-edge: color-mix(in srgb, var(--page-ink) 13%, transparent);
}

/* ============================================================
   1. Sheets, not a ribbon.

   The words stay in one contenteditable — that is what gives us
   the browser's own undo stack, selection across elements and
   spellcheck. The paper is drawn behind them in a layer of its
   own, one absolutely positioned sheet per page, each exactly
   sixty-six lines tall. web/views/script.js measures the flow and
   pushes each page's first line down by exactly the amount that
   lands it on the next sheet, in --lead.
   ============================================================ */

.paper-stack {
  position: relative;
  flex: none;
  width: min(var(--page-w, 8.5in), 100%);
}

/* OVERRIDES .script-page in styles.css: the paper is no longer the editable
   box. It is transparent, and the sheets underneath carry the colour, the
   edge and the shadow.

   Three classes deep on purpose. Glass paints the page solid with
   `html[data-theme="glass"] .script-page`, and a two-class selector would
   lose to it — leaving one long sheet of paper over the gutters in exactly
   one of the seven themes. */
.script-shell .paper-stack .script-page {
  position: relative;          /* the origin every measurement is taken from */
  z-index: 1;
  width: 100%;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* The engine skips the blank line above the first element of a page. So does
   the page: without this, page one starts a line lower than the PDF. */
.script-page .el:first-child { margin-top: 0; }

.sheet-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
}

.sheet {
  position: absolute;
  left: 0;
  right: 0;
  background: var(--page-bg);
  border: 1px solid var(--sheet-edge);
  border-radius: 2px;
  /* Tight, not theatrical. A sheet of paper on a desk casts about this much. */
  box-shadow: 0 1px 1px rgb(0 0 0 / .22), 0 10px 22px -14px rgb(0 0 0 / .5);
  /* Everything drawn on a sheet is measured in lines and columns of the
     script's own type, so the sheet has to be set in it. */
  font: var(--script-fs) / 1 var(--font-mono);
  overflow: hidden;            /* keeps the goal hairline inside the corners */
}

/* The page number, in its true margin position.
   Down: a line and a half above the first line of the body, which is what
   `PAGE_H - TOP + LINE_H * 1.5` works out to in pdf.js.
   Across: its last character lands on the right text margin, one inch in,
   which is what right-aligning a shrink-to-fit box 10ch from the edge does. */
/* Page numbers off, on screen. The same switch adds &pagenumbers=0 to the
   export, so the paper and the PDF never disagree about it. */
.script-shell.no-page-numbers .sheet-no { display: none; }

.sheet-no {
  position: absolute;
  top: 4.5em;
  right: 10ch;
  color: color-mix(in srgb, var(--page-ink) 62%, transparent);
  white-space: nowrap;
}

/* ============================================================
   2. (MORE) and NAME (CONT'D).

   The engine emits both on every block and the editor used to
   throw them away, so a speech was cut with nothing to say so and
   then turned up in the PDF with both. These are set exactly as
   the PDF sets them: Courier 12, at the character cue's indent,
   on the line the engine put them on. They are not editable and
   not selectable, because they are not the writer's words — the
   engine adds them at the break.
   ============================================================ */

.sheet-more,
.sheet-contd {
  position: absolute;
  color: var(--page-ink);
  white-space: nowrap;
  user-select: none;
}

/* OVERRIDES .el[data-pagebreak="1"] in styles.css: the dashed rule and its
   "page break" caption are what the sheets replace. The attribute stays,
   because the flow still needs to know which line opens a page. */
.script-page .el[data-pagebreak="1"] {
  border-top: 0;
  padding-top: 0;
  /* Measured per page in layoutSheets(). The fallback is one bottom margin,
     one gutter and one top margin, which is the right answer for the frame
     before the first measurement lands. */
  margin-top: var(--lead, 13.6em);
}
/*
 * The "page break" caption goes, on every line that opens a page.
 *
 * The `:not([data-setup="1"])` that used to qualify this was protecting the
 * promise marker, which was drawn with the same pseudo-element. That marker is
 * gone - but any line a writer already marked STILL CARRIES the attribute, so
 * leaving the qualifier here would have kept the caption on exactly those
 * lines: a dashed "page break" label reappearing on a handful of pages, in
 * older scripts only, for a reason nothing on screen could explain.
 *
 * Removing a feature is not finished when its own code is gone. It is finished
 * when nothing is still branching on the traces it left behind.
 */
.script-page .el[data-pagebreak="1"]::after { content: none; }

/* A page that opens on a dual pair opens on a float, and a float's margin
   moves the float alone. Its partner is in normal flow and is sent down with
   it, or the two columns come apart at the top of the sheet. */
.script-page .el[data-leadmate="1"] { margin-top: var(--lead, 0px); }

/* ============================================================
   3. Where you are.
   ============================================================ */

.pagecount b { font-weight: 700; color: var(--text); }

/* ============================================================
   4. Typewriter scrolling.

   OVERRIDES .script-page .el in styles.css, which carried
   scroll-margin: 30vh — that was the third-of-a-screen jump on
   Enter. With the typewriter on, nothing calls scrollIntoView for
   the caret at all; with it off, the caret should move the page
   as little as possible.
   ============================================================ */

.script-page .el { scroll-margin: 3em 0 8em; }

/* `.main` smooth-scrolls by stylesheet, which is right for a deliberate jump
   to a scene and seasick under a keystroke: the column is still gliding when
   the next letter lands, so the text swims. There is deliberately no rule
   turning it off here, because that would also flatten the jumps it is right
   for. holdCaret() in script.js scrolls with `behavior: 'instant'`, which the
   scroll-behavior property does not get a vote on.

   `.script-shell.typewriter` is set while the band is on, as a hook for
   anything that needs to know. Nothing here needs it yet. */

/* ============================================================
   5. Letting the chrome go.

   The bar keeps its place in the layout while it is away, so
   nothing underneath it ever moves. It fades; it does not
   collapse.
   ============================================================ */

.script-bar {
  transition: opacity var(--dur-3) var(--ease), transform var(--dur-3) var(--ease);
}
.script-bar.away {
  opacity: 0;
  transform: translateY(-3px);
  pointer-events: none;
}

.cmd-open { margin-left: 2px; }

/* ============================================================
   6. What this line is: a drawn menu, not the operating system's.

   It was a bare <select>, so the list came from Windows: a white
   Segoe UI box over a dark studio. Each row now carries a
   miniature of where the element actually sits on the page —
   sixty columns wide, the indent to scale — so it can be picked
   by shape as well as by name.
   ============================================================ */

.el-pick {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-width: 176px;
  height: 28px;
  padding: 0 7px 0 9px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--raised);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: calc(12px * var(--fs));
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.el-pick:hover { background: var(--raised-2); border-color: var(--text-3); }
.el-pick[aria-expanded="true"] { border-color: var(--accent); }

/* Sixty columns of page, and where this element sits in them. */
.el-pick-rule,
.el-row-mini {
  position: relative;
  height: 3px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--text) 13%, transparent);
}
.el-pick-rule { width: 38px; flex: none; }
.el-pick-rule i,
.el-row-mini i {
  position: absolute;
  top: 0;
  height: 3px;
  border-radius: 2px;
  background: var(--accent);
  transition: left var(--dur-2) var(--ease), width var(--dur-2) var(--ease);
}
.el-pick-name {
  flex: 1;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.el-pick-caret { width: 13px; height: 13px; opacity: .55; }

.el-menu {
  position: fixed;
  z-index: 70;
  min-width: 316px;
  padding: 5px;
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: var(--shadow-3);
}
.el-row {
  display: grid;
  grid-template-columns: 1fr 96px auto;
  gap: 14px;
  align-items: center;
  width: 100%;
  padding: 7px 10px;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--text-2);
  font-family: var(--font-ui);
  font-size: calc(12.5px * var(--fs));
  text-align: left;
  cursor: pointer;
}
.el-row:hover,
.el-row.at { background: var(--raised-2); color: var(--text); }
.el-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.el-row.on { color: var(--accent); }
.el-row.on .el-row-name::after { content: ' ·'; }

/* ============================================================
   7. The command menu.

   Everything the bar used to carry, by name, with the shortcut
   shown beside it so the menu teaches itself out of a job.
   ============================================================ */

.cmd-sheet {
  position: fixed;
  right: 18px;
  z-index: 70;
  width: min(400px, calc(100vw - 36px));
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  animation: cmd-in var(--dur-2) var(--ease);
}
@keyframes cmd-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
.cmd-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border-soft);
}
.cmd-head .ic { width: 15px; height: 15px; color: var(--faint); flex: none; }
.cmd-q {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  outline: none;
  color: var(--text);
  font-family: var(--font-ui);
  font-size: calc(13.5px * var(--fs));
}
.cmd-q::placeholder { color: var(--faint); }

.cmd-list { max-height: min(60vh, 520px); overflow-y: auto; padding: 6px; }
.cmd-row {
  display: grid;
  grid-template-columns: 17px 1fr auto;
  gap: 11px;
  align-items: center;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--text-2);
  font-family: var(--font-ui);
  text-align: left;
  cursor: pointer;
}
.cmd-row:hover,
.cmd-row.at { background: var(--raised-2); color: var(--text); }
.cmd-row .ic { width: 16px; height: 16px; opacity: .8; }
.cmd-words { min-width: 0; }
.cmd-words b {
  display: block;
  font-weight: 600;
  font-size: calc(12.5px * var(--fs));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cmd-words em {
  display: block;
  font-style: normal;
  color: var(--faint);
  font-size: calc(11px * var(--fs));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cmd-none { padding: 18px 12px; color: var(--faint); font-size: calc(12.5px * var(--fs)); }

/* One key cap, used by both menus. */
.el-row kbd,
.cmd-row kbd {
  font-family: var(--font-ui);
  font-size: calc(10.5px * var(--fs));
  letter-spacing: .02em;
  color: var(--text-3);
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 2px 6px;
  white-space: nowrap;
}

/* ============================================================
   8. The element's name, out in the margin.

   Set in the left margin of the page, on the line the pointer is
   over — or on the caret's line when the pointer is elsewhere. It
   answers "what am I standing in?" without making anyone look up
   at the bar, which is the whole point of putting it near the
   words.
   ============================================================ */

.margin-type {
  position: absolute;
  left: calc(var(--script-fs) * .8);
  z-index: 2;
  max-width: calc(var(--script-fs) * 6.6);
  font-family: var(--font-ui);
  font-size: calc(var(--script-fs) * .58);
  line-height: 1.1;
  letter-spacing: var(--track);
  text-transform: lowercase;
  color: color-mix(in srgb, var(--page-ink) 34%, transparent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  transform: translateY(.1em);
  transition: top var(--dur-1) var(--ease), opacity var(--dur-2) var(--ease);
}

/* ============================================================
   9. Something to say about the day.

   A hairline of accent light along the top edge of the sheet
   itself, filling as the session target fills. Not a panel, not a
   badge, nothing to protect: the paper simply gets a little
   brighter along its top edge.
   ============================================================ */

.sheet-goal {
  position: absolute;
  left: 0;
  top: 0;
  height: 2px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 25%, transparent), var(--accent));
  box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 40%, transparent);
  transition: width .6s var(--ease);
}
.sheet-goal.met {
  background: var(--accent);
  box-shadow: 0 0 16px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* A milestone takes the status line for a few seconds and gives it back. */
.write-note {
  font-family: var(--font-read);
  font-size: calc(12.5px * var(--fs));
  color: var(--text-2);
  animation: note-in var(--dur-3) var(--ease);
}
@keyframes note-in {
  from { opacity: 0; transform: translateY(3px); }
  to   { opacity: 1; transform: none; }
}
/* The bare page has no status bar. It borrows one for the moment it has
   something to say, and lets it go again. */
.script-shell.room-off .write-status.has-note { display: flex; }

/* ============================================================
   10. The end of a draft.

   Once per draft, and never again until a copy is kept. The page
   dims and offers three things. No confetti: a screenwriter who
   has just finished a draft is tired, not eleven.
   ============================================================ */

.ending {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  padding: 24px;
  background: color-mix(in srgb, var(--bg-deep) 84%, transparent);
  backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity .55s var(--ease);
}
/* `display: grid` would otherwise beat the browser's own [hidden] rule, and the
   ceremony would sit over the page from the moment the view opened. */
.ending[hidden] { display: none; }
.ending.on { opacity: 1; }

.ending-card {
  width: min(520px, 100%);
  text-align: center;
  padding: 34px 34px 28px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
  animation: ending-in .6s var(--ease);
}
@keyframes ending-in {
  from { opacity: 0; transform: translateY(10px) scale(.99); }
  to   { opacity: 1; transform: none; }
}
.ending-eyebrow {
  margin: 0 0 6px;
  font-size: calc(11.5px * var(--fs));
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--accent);
}
.ending-card h2 {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-size: calc(30px * var(--fs));
  font-weight: 600;
  letter-spacing: -.01em;
}
.ending-count {
  margin: 0 0 16px;
  font-family: var(--font-mono);
  font-size: calc(12.5px * var(--fs));
  color: var(--text-3);
}
.ending-count b { color: var(--text); font-weight: 700; }
.ending-said {
  margin: 0 auto 22px;
  max-width: 40ch;
  font-family: var(--font-read);
  font-size: calc(14.5px * var(--fs));
  line-height: 1.6;
  color: var(--text-2);
}
.ending-acts {
  display: flex;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}
.ending-esc {
  margin: 18px 0 0;
  font-size: calc(11px * var(--fs));
  color: var(--faint);
}

/* ============================================================
   11. Small screens.

   The page shrinks itself against its container, so the sheets
   follow it. Only the chrome needs anything here.
   ============================================================ */

@media (max-width: 720px) {
  .el-pick { min-width: 0; }
  .el-pick-name,
  .el-pick-rule { display: none; }
  .cmd-sheet { right: 10px; left: 10px; width: auto; }
}

/* Someone who has asked for less motion gets the paper and the words, and
   none of the travelling. */
@media (prefers-reduced-motion: reduce) {
  .script-bar,
  .sheet-goal,
  .margin-type,
  .el-pick-rule i,
  .el-row-mini i { transition: none; }
  .cmd-sheet,
  .ending-card,
  .write-note { animation: none; }
  .ending { transition: none; }
}

/* ---- from the account rebuild ---- */
/* ============================================================
   THE ACCOUNT, THE SETTINGS AND THE PAPERWORK.

   Three new pages: web/views/account.js, settings.js, legal.js.

   Drawn for riso, which is the default theme and the one people
   should recognise across a room: flat spot inks, warm paper, hard
   edges, and elevation as an offset block of ink with no blur.
   Nothing here declares a gradient, a glow or a blur radius. What
   depth there is comes from --elev-*, which the air system already
   swaps per theme (see AIR in web/lib/themes.js), so the same rules
   print flat in riso and float in the other six without a single
   theme-specific override for elevation.

   Every size, space and radius is a token, with four stated
   exceptions, all of them fixed art boxes rather than type or
   spacing: the monogram (54px), the badge mark (34px), the theme
   swatch (15x20px) and the checkbox (16px). The scale has no rung
   for "a square the size of a face" and the stylesheet already
   improvises these - `.avatar` is 34px at styles.css:967 and
   `.avatar.sm` is 26px at 4352 - so a fifth private answer here
   would be worse than saying so. The missing rung is in the report.
   Grid track minimums (190px, 230px, 280px) have no rung either and
   follow `.grid-2`'s minmax(330px, 1fr) at styles.css:857.

   RULES OVERRIDDEN, quoted:
     styles.css:1271  `.panel + .panel { margin-top: var(--sp-6); }`
       Zeroed inside `.acct-body` only, which is a flex column with
       its own gap. Left alone everywhere else.
     styles.css:1692  `.stat-row { ... margin: 26px 0 22px; }`
       Tightened inside a panel, where the h3 above it already
       carries the separation.
   ============================================================ */

/* ---- the shared page body ---------------------------------------------- */

/*
 * One column of panels with one gap between them.
 *
 * `.panel + .panel { margin-top: var(--sp-6) }` (styles.css:1271) and a flex
 * gap would both apply and add up, which is how a page ends up with two
 * different rhythms depending on whether a strip happens to sit between two
 * panels. The gap wins here because the connection strip comes and goes.
 */
.acct-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-7);
}
.acct-body .panel + .panel { margin-top: 0; }

/* The tab deck sits directly under the page head, so it loses the top half of
   the stock margin: `.tabs { margin: 20px 0 22px }` (styles.css:826). */
.acct-tabs { margin-top: 0; }

/* ---- who you are -------------------------------------------------------- */

.acct-id {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-7);
}

/*
 * The monogram: a stamped square, not a circle.
 *
 * A fixed art box, the same category as `.avatar` (styles.css:967). The scale
 * has no rung for it, which is stated in the report rather than solved by
 * inventing one. Square because the riso identity is a printed block; the
 * radius is still the theme's, so it rounds off in the softer rooms.
 */
.acct-mono {
  width: 54px;
  height: 54px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r-md);
  background: var(--accent);
  color: var(--accent-ink);
  font-family: var(--font-display);
  font-size: var(--fz-heading);
  font-weight: 700;
  letter-spacing: var(--track-label);
  box-shadow: var(--elev-2);
}
/* Signed out there is no name to stamp, so the block holds a drawn mark and
   steps back to the surface colours: an empty accent square reads as somebody
   whose initials failed to load. */
.acct-mono.out {
  background: var(--raised);
  color: var(--faint);
  border: 1px solid var(--border);
  box-shadow: none;
}
.acct-mono.out .ic {
  width: var(--icon-lg);
  height: var(--icon-lg);
}

.acct-id-main { min-width: 0; }

.acct-who {
  margin: 0 0 var(--sp-2);
  font-family: var(--font-display);
  font-size: var(--fz-heading);
  font-weight: 700;
  letter-spacing: var(--track-tight);
  overflow-wrap: break-word;
}

/* The page's own explaining voice. Reading matter, so it keeps a measure. */
.acct-sub {
  margin: 0;
  color: var(--muted);
  font-size: var(--fz-body);
  line-height: 1.6;
  max-width: var(--measure);
}
.acct-sub b { color: var(--text-2); font-weight: 650; }

.acct-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}

/* ---- no connection ------------------------------------------------------ */

/*
 * Not an error, so not the danger colour.
 *
 * `--film` is the app's warm "worth knowing" ink, already used by `.chip.warn`
 * (styles.css:4351). Danger red here would say something has gone wrong, and
 * nothing has: a writing application that cannot reach the internet is a
 * writing application.
 */
.acct-wire {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-6);
  padding: var(--sp-6) var(--sp-7);
  border: 1px solid color-mix(in srgb, var(--film) 34%, transparent);
  background: color-mix(in srgb, var(--film) 10%, transparent);
  border-radius: var(--r);
}
/*
 * The mark is a span, and so is the sentence beside it.
 *
 * `.acct-wire span` and `.acct-wire-mark` are both one class deep, so the
 * later one wins and the icon inherited the SENTENCE's rules: muted grey
 * rather than the warning ink, display block rather than flex, and a top
 * margin it never asked for. It rendered as a nearly invisible smudge and no
 * measurement caught it, because an 18px svg in the wrong colour is still an
 * 18px svg. Every text rule here is scoped to the text block instead.
 */
.acct-wire-mark { flex: none; color: var(--film); display: flex; padding-top: var(--sp-1); }
.acct-wire-mark .ic { width: var(--icon-lg); height: var(--icon-lg); }
.acct-wire-main b { display: block; font-size: var(--fz-body); font-weight: 650; }
.acct-wire-main span {
  display: block;
  margin-top: var(--sp-2);
  color: var(--text-2);
  font-size: var(--fz-ui);
  line-height: 1.6;
  max-width: var(--measure);
}

/* ---- the plan ----------------------------------------------------------- */

.acct-planhead {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-7);
  flex-wrap: wrap;
  margin-bottom: var(--sp-7);
}

.acct-plan-now {
  font-family: var(--font-display);
  font-size: var(--fz-title);
  font-weight: 700;
  letter-spacing: var(--track-tight);
  line-height: 1.2;
  margin-bottom: var(--sp-2);
}

/*
 * Founder, printed like a serial number, because that is what it is: one of a
 * hundred and fifty, and the number matters to whoever holds it.
 */
.acct-founder {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-6);
  border: 1px solid var(--accent);
  border-radius: var(--r-md);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--fz-small);
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  white-space: nowrap;
  box-shadow: var(--elev-1);
}
.acct-founder .ic { width: var(--icon); height: var(--icon); }
.acct-founder b { font-family: var(--font-mono); font-size: var(--fz-body); letter-spacing: var(--track-fine); }

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

/*
 * A price, not a button.
 *
 * Deliberately not `.card.clickable` and deliberately without a hover lift:
 * nothing here can be pressed, and a tile that answers the cursor is a promise
 * the page cannot keep.
 */
.acct-plan {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-6) var(--sp-7);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  background: var(--bg);
}
.acct-plan b {
  font-family: var(--font-display);
  font-size: var(--fz-subhead);
  font-weight: 700;
  letter-spacing: var(--track-tight);
}
.acct-plan-price {
  font-size: var(--fz-body);
  font-weight: 650;
  color: var(--accent);
}
.acct-plan-blurb {
  font-size: var(--fz-small);
  color: var(--muted);
  line-height: 1.55;
}
.acct-plan em {
  font-style: normal;
  margin-top: var(--sp-3);
  font-size: var(--fz-micro);
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--faint);
}

/* The one you hold: a ring in the app's own "this is selected" weight
   (--ring-on), not a colour wash, so it reads the same in all seven rooms. */
.acct-plan.held {
  border-color: var(--accent);
  box-shadow: var(--ring-on);
  background: var(--surface);
}
.acct-plan-mark {
  position: absolute;
  top: 0;
  right: var(--sp-6);
  transform: translateY(-50%);
  padding: var(--sp-1) var(--sp-4);
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--fz-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  white-space: nowrap;
}

.acct-note { margin: var(--sp-6) 0 0; max-width: var(--measure); line-height: 1.6; }

/* ---- badges ------------------------------------------------------------- */

.acct-badges {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--sp-6);
}
.acct-badge {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-6);
  padding: var(--sp-6);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  background: var(--bg);
}

/*
 * The second ink.
 *
 * `--accent-2` is riso's fluorescent coral and every other theme's violet. It
 * is laid down as a wash with the text colour over it rather than reversed out
 * in white, because that is both what the ink does on real paper and what the
 * numbers say: white on the coral measures 3.44 and the navy measures 4.55
 * (the working is in the riso palette note at styles.css:341).
 */
.acct-badge-mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--accent-2) 22%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent-2) 45%, transparent);
  color: var(--text);
}
.acct-badge-mark .ic { width: var(--icon-lg); height: var(--icon-lg); }
.acct-badge-main { min-width: 0; }
.acct-badge-main b { display: block; font-size: var(--fz-body); font-weight: 650; }
.acct-badge-main span {
  display: block;
  margin-top: var(--sp-2);
  color: var(--muted);
  font-size: var(--fz-small);
  line-height: 1.55;
}
.acct-badge-main em {
  display: block;
  margin-top: var(--sp-3);
  font-style: normal;
  font-size: var(--fz-micro);
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--faint);
}

/* ---- the numbers -------------------------------------------------------- */

/* `.stat-row { margin: 26px 0 22px }` (styles.css:1692) is spacing for a row
   sitting on its own under a page head. Inside a panel the h3 has already done
   that work, so the top margin goes. */
.panel > .stat-row { margin: 0 0 var(--sp-6); }
.panel > .stat-row + .acct-note { margin-top: calc(var(--sp-6) * -1); margin-bottom: var(--sp-7); }

/* The day's three tiles are the ones a writer looks at, so they get the second
   ink under them and the rest of the counts stay quiet. */
.acct-day .stat { border-color: color-mix(in srgb, var(--accent) 30%, transparent); }

/* ---- this copy ---------------------------------------------------------- */

.acct-kv { margin: 0 0 var(--sp-7); }
.acct-path {
  font-family: var(--font-mono);
  font-size: var(--fz-small);
  overflow-wrap: anywhere;
  color: var(--text-2);
}

/* ============================================================
   SETTINGS
   ============================================================ */

.set-lead { margin-bottom: var(--sp-6); }

/*
 * One preference to a line: what it is on the left, the control on the right.
 *
 * A grid rather than flex, so every control in a panel lands on the same
 * vertical line however long its explanation runs. The rule between rows is a
 * hairline rather than a gap, because the rows are a list of related decisions
 * and a printed form separates those with a rule.
 */
.set-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--sp-7);
  padding: var(--sp-6) 0;
}
.set-row + .set-row { border-top: 1px solid var(--border-soft); }
.set-row:first-child { padding-top: 0; }
.set-row:last-child { padding-bottom: 0; }

.set-row-main { min-width: 0; }
.set-row-main b {
  display: block;
  font-size: var(--fz-body);
  font-weight: 650;
}
.set-row-main span {
  display: block;
  margin-top: var(--sp-2);
  color: var(--muted);
  font-size: var(--fz-small);
  line-height: 1.55;
  max-width: var(--measure);
}

.set-row-ctl {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
  justify-content: flex-end;
  flex-wrap: wrap;
}

/* The same yes/no the rest of the studio uses: a native box wearing the
   theme's accent, as at `.tasks-item input` (styles.css:9588). */
.set-check {
  width: 16px;
  height: 16px;
  cursor: pointer;
  accent-color: var(--accent);
  flex: none;
}

/* A fact the page is stating rather than a control: same weight as a label,
   never the colour of something you can press. */
.set-fact {
  font-size: var(--fz-small);
  color: var(--faint);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  font-weight: 650;
  white-space: nowrap;
}

/* A folder, under the sentence that mentions it rather than in the control
   column: a Windows path is longer than any right-hand column, and squeezed
   into one it broke mid-word, which reads as the page being broken. Given the
   measure it wraps where a path should. */
.set-path {
  display: block;
  margin-top: var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--fz-tiny);
  color: var(--text-2);
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-sm);
  padding: var(--sp-3) var(--sp-5);
  overflow-wrap: anywhere;
  max-width: var(--measure);
}

/* The size steppers. One shape, used twice, so the two sizes are visibly the
   same kind of decision. */
.set-step {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-md);
  background: var(--bg);
}
.set-step-now {
  min-width: 5ch;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--fz-small);
  color: var(--text-2);
}

.set-theme-now {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  font-size: var(--fz-body);
  font-weight: 650;
}
/* Two flat squares of the theme's own inks. The picker's card uses a 135deg
   ramp between the same two colours; a ramp is the one thing the default room
   cannot print, so here they sit side by side, out of register by a hairline. */
.set-swatch {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--r-xs);
  overflow: hidden;
}
.set-swatch i { display: block; width: 15px; height: 20px; }

.set-tools {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--sp-7);
  align-items: start;
}
.set-tool-group .tpl-group-label { margin-top: 0; }

/* ============================================================
   THE PAPERWORK

   The one screen in the app whose job is to look unfinished.
   ============================================================ */

.legal-index {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--sp-6);
}
.legal-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-7);
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.legal-card:hover { border-color: var(--accent); box-shadow: var(--elev-2); }
/* Third instance of the same trap: the card's mark is a span, and
   `.legal-card span` was overruling its colour. The mark is excluded by name
   rather than by hoping the order stays put. */
.legal-card-mark { color: var(--accent); display: flex; }
.legal-card-mark .ic { width: var(--icon-lg); height: var(--icon-lg); }
.legal-card b { font-size: var(--fz-subhead); font-weight: 650; }
.legal-card span:not(.legal-card-mark) {
  font-size: var(--fz-small);
  color: var(--muted);
  line-height: 1.55;
}
.legal-card em {
  font-style: normal;
  margin-top: var(--sp-3);
  font-size: var(--fz-micro);
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--danger);
  font-weight: 700;
}

/* A contract is read, so it gets the reading measure and nothing wider. */
.legal-doc { max-width: var(--measure); }

/* The flag sits in the page's eyebrow, above the title, so the first thing
   anybody reads on this screen is that it is not the real wording. */
.legal-eyebrow { gap: var(--sp-5); }

/*
 * The word, set in the danger ink.
 *
 * Red because this is not a caution, it is a stop: a document that reads like
 * a contract and is not one must be impossible to mistake for the finished
 * thing in a screenshot, on a projector or on paper. It is repeated at the top
 * and the bottom of every document for the same reason.
 */
.legal-flag {
  padding: var(--sp-1) var(--sp-4);
  border: 1px solid var(--danger);
  border-radius: var(--r-2xs);
  color: var(--danger);
  font-family: var(--font-ui);
  font-size: var(--fz-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track);
}

.legal-stamp {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-6);
  padding: var(--sp-6) var(--sp-7);
  border: 2px solid var(--danger);
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
  margin: var(--sp-7) 0;
}
/* Scoped to the text block for the same reason as the strip above: the mark
   is a span too, and a bare `.legal-stamp span` was repainting the warning
   triangle in body-text grey. */
.legal-stamp-mark { flex: none; color: var(--danger); display: flex; padding-top: var(--sp-1); }
.legal-stamp-mark .ic { width: var(--icon-lg); height: var(--icon-lg); }
.legal-stamp-main b {
  display: block;
  color: var(--danger);
  font-size: var(--fz-body);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}
.legal-stamp-main span {
  display: block;
  margin-top: var(--sp-2);
  font-size: var(--fz-ui);
  line-height: 1.6;
  color: var(--text-2);
}

.legal-sections {
  margin: 0;
  padding: 0 0 0 var(--sp-8);
  counter-reset: none;
}
.legal-sections li { margin-bottom: var(--sp-8); }
.legal-sections li::marker { color: var(--faint); font-size: var(--fz-small); }
.legal-sections h3 {
  margin: 0 0 var(--sp-3);
  font-size: var(--fz-subhead);
  font-weight: 650;
  letter-spacing: var(--track-tight);
}

/*
 * The body of every clause, and the reason this page is safe to ship.
 *
 * Set in the interface face rather than the reading serif, and marked "To be
 * written", so it reads as an instruction to whoever writes the real wording
 * and never as the wording itself. If a future edit puts real prose in here,
 * this class is the thing to delete first.
 */
.legal-note {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fz-ui);
  line-height: 1.65;
  color: var(--muted);
  border-left: 2px solid var(--border);
  padding-left: var(--sp-6);
}
.legal-note b { color: var(--danger); font-weight: 700; }

.legal-foot {
  margin: 0;
  font-size: var(--fz-small);
  color: var(--faint);
  line-height: 1.6;
}

/*
 * On paper.
 *
 * The app's print stylesheet flattens every surface to white and every ink to
 * black, which is right for a script and wrong for exactly one thing on this
 * page: the banner saying the document is not real has to survive being
 * printed, or a placeholder walks out of the building as a contract. So the
 * two stamps keep a hard black rule and their capitals. !important for the
 * same reason the print sheet uses it throughout: a theme built from a
 * photograph is applied inline and nothing else can outrank it.
 */
@media print {
  .legal-stamp,
  .legal-flag {
    border-color: #000 !important;
    background: #ffffff !important;
  }
  .legal-stamp-main b,
  .legal-stamp-mark,
  .legal-flag,
  .legal-note b {
    color: #000 !important;
  }
  .legal-sections li { break-inside: avoid; }
}

/* ---- from the chrome2 rebuild ---- */
/* ============================================================
   DISCOVERABILITY AND SCALE
   Five faults, all of them the interface knowing something and
   not saying it.

   1  A dialog that hides 282px of itself with no mark at the cut
      and an invisible scrollbar thumb (1.07:1).
   2  A theme picker that previews two colours out of a theme
      that also states its corner, its shadow, its display face
      and its tracking.
   3  `--icon` frozen at 15px while the text-size control moves
      everything around it.
   4  An inline `font-size` in web/views/import.js that stood
      outside the same control.
   5  A selected tab set lighter than the unselected ones, and a
      "quieter" tab row set a point larger than the row above it.

   Territory: web/lib/themes.js (the air data, published for the
   preview) and web/views/import.js (the inline size). Every CSS
   rule that would otherwise have gone into web/styles.css is
   here instead, because five other agents are in that file.

   Every override quotes the line and the text it overrides, at
   web/styles.css as it stood when this was written. Line numbers
   drift; the quoted text is the thing to search for.

   Proof: scratchpad/chrome2-test.py. It fails without this file
   and passes with it (`python chrome2-test.py --staged`).
   themes-test.mjs, radius-test.py and shelf-test.py must stay
   green either way.
   ============================================================ */


/* ==========================================================================
   1 - THE CUT: a scroll region that admits it is one
   ========================================================================== */

/*
 * Measured, at the theme picker, with nothing else open:
 *
 *   1280x720   body 526px, content 808px, 282px below the fold
 *   1366x768   body 574px, content 808px, 234px below the fold
 *   1920x1080  body 808px, nothing hidden
 *
 * The cut lands cleanly after a row of cards, so the list looks finished; the
 * scrollbar is an overlay one and takes no layout width, so nothing widens;
 * and the thumb is drawn in `--border` on `--surface`, which is 1.07:1 in the
 * default theme. Three separate reasons to believe you have seen everything.
 *
 * What is below the fold at both laptop sizes is "Make your own" - build a
 * theme out of your own photograph - so the feature is not discoverable on a
 * laptop at all.
 *
 * Two halves, and both are general: any `.modal-body` in the app that
 * overflows now says so, whatever is in it.
 */

/*
 * Half one: a shade at the cut that is not there when there is nothing to cut.
 *
 * Four background layers, and the trick is which of them scrolls. The two
 * shades are `scroll`, so they stay welded to the top and bottom of the
 * visible box. The two covers are `local`, so they travel with the content and
 * sit at its very start and very end - each cover exactly as tall as the
 * padding on that side. At rest the top cover is parked over the top padding
 * and hides the shade beneath it; scroll a pixel and a pixel of shade appears;
 * reach the end and the bottom cover slides into place and the bottom shade is
 * gone. No script, no scroll listener, and it is right on the first paint.
 *
 * The cover heights are the padding heights on purpose. A cover taller than
 * the padding would be an opaque band sitting over the first line of text.
 *
 * Overrides nothing on web/styles.css:1334
 *   `.modal-body { padding: 16px 22px; overflow-y: auto; min-height: 0; }`
 * - it adds to it. The padding is deliberately NOT restated here: five other
 * agents are in this stylesheet and a second declaration of the same thing at
 * the same specificity, merged later, would silently win over theirs. What
 * this rule does depend on is those two 16px, and `--cut-top` / `--cut-bottom`
 * have to move if they ever do.
 */
.modal-body {
  --cut-top: 16px;
  --cut-bottom: 16px;
  /* Ink, not the theme's text colour: `--text` is near-white in five of the
     seven themes, and a white haze at the bottom edge of a dark dialog reads
     as a rendering fault rather than as content passing under an edge.
     .38 rather than .30 because the cut in the theme picker falls on the gap
     between two rows of cards, where the shade has nothing but flat surface to
     work against: at .30 that measured 2.1 of luminance and it wants to be
     unmistakable in the one case the whole file exists for. */
  --cut-ink: rgb(0 0 0 / .38);
  --cut-ink-mid: rgb(0 0 0 / .14);
  /* Whatever stands behind the body. `--surface` is the dialog's own colour in
     every theme but Clean, which frosts its dialogs and states its own just
     below. */
  --cut-cover: var(--surface);

  background-image:
    linear-gradient(var(--cut-cover), var(--cut-cover)),
    linear-gradient(var(--cut-cover), var(--cut-cover)),
    linear-gradient(to bottom, var(--cut-ink), var(--cut-ink-mid) 42%, rgb(0 0 0 / 0)),
    linear-gradient(to top,    var(--cut-ink), var(--cut-ink-mid) 42%, rgb(0 0 0 / 0));
  background-repeat: no-repeat;
  background-position: left top, left bottom, left top, left bottom;
  background-size:
    100% var(--cut-top), 100% var(--cut-bottom),
    100% var(--cut-top), 100% var(--cut-bottom);
  background-attachment: local, local, scroll, scroll;
}

/*
 * Clean frosts its dialogs, so the cover cannot be an opaque colour or the
 * padding bands would read as two flat strips in a panel that is otherwise
 * blurred glass. It is the panel's own fill instead, which leaves a fifth of
 * the shade showing through at rest - over the padding, where there is nothing
 * to veil.
 *
 * Matches web/styles.css:5900
 *   `html[data-theme="glass"] .modal, html[data-theme="glass"] .palette {
 *      background: rgb(255 255 255 / .82); ... }`
 */
html[data-theme="glass"] .modal-body {
  --cut-cover: rgb(255 255 255 / .82);
  --cut-ink: rgb(24 32 60 / .32);
  --cut-ink-mid: rgb(24 32 60 / .11);
}

/*
 * The one dialog that sets its own top padding keeps its own cover height.
 * Overrides nothing; it states the other half of web/styles.css:12319
 *   `.kind-chooser .modal-body { padding-top: 4px; }`
 */
.kind-chooser .modal-body { --cut-top: 4px; }

/*
 * Half two: a thumb somebody can see.
 *
 * `--border` is the colour of a hairline between two surfaces, which is
 * exactly the wrong brief for a control: measured against the surface it is
 * drawn on it is 1.07:1 in Dark, and no other theme is far from it.
 * Mixed out of the room's own text colour instead, so it lands between 3.4:1
 * and 4.5:1 in every theme there is, in any theme added later, and in one
 * built from somebody's photograph.
 *
 * Overrides web/styles.css:402
 *   `::-webkit-scrollbar-thumb { background: var(--border); border-radius:
 *      var(--r-pill); border: 3px solid transparent; background-clip:
 *      content-box; }`
 * Only the colour changes. The 3px transparent border and the content-box clip
 * are what make the thumb a lozenge inside its track, and they stay.
 */
:root {
  --scroll-thumb: color-mix(in srgb, var(--text) 52%, var(--surface));
}
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); background-clip: content-box; }

/*
 * And in a dialog, the scrollbar takes room rather than floating over the
 * content. An overlay scrollbar is fine on a page you already know is long; it
 * is no use at all as the only sign that a dialog has more in it. `thin` keeps
 * it modest, and `scrollbar-color` is the standard property, so this is the
 * half that works outside Chromium too.
 */
.modal-body {
  scrollbar-width: thin;
  scrollbar-color: var(--scroll-thumb) transparent;
}


/* ==========================================================================
   2 - THE PICKER: a room per card, however many rooms there are
   ========================================================================== */

/*
 * Every theme states its air in web/lib/themes.js - corner ladder, elevation
 * ladder, display face, tracking, spacing - and `airOf()` was exported there
 * with the comment "so a preview can show it honestly" and read by nothing.
 * So the picker offered two colours per theme, and nobody deciding whether to
 * buy this ever saw that Cold mountains squares every corner, that Clean
 * floats everything, or that Light and Forest set their headlines in a serif.
 *
 * WHERE THE VALUES COME FROM. themes.js builds one declaration block per
 * theme - `installAirPreview()` / `airPreviewCss()` - and each block sets six
 * `--tc-*` custom properties on that theme's own card, keyed off the card's
 * `data-theme-pick` (the picker) or `data-pick-theme` (the welcome tour).
 * That is generated rather than written out here on purpose: the theme list is
 * growing, and a hand-written block per theme would mean the list living in
 * two files, one of which nobody would open when adding the next one. Nothing
 * about the LOOK is decided there - only what each room's numbers are.
 *
 * Everything below is theme-agnostic. It never names a theme, it never counts
 * them, and an eighth theme needs nothing here at all.
 *
 * Restraint on purpose. A card shows its corner, its shadow, its display face
 * and its tracking, in situ, because those are the four a card can BE rather
 * than describe. Spacing is the one air value deliberately left out: the card
 * would have to grow to show it, and this dialog is the one already hiding
 * 282px below the fold.
 */

/*
 * The corner and the shadow, on the card itself.
 *
 * Overrides web/styles.css:5627
 *   `.theme-card { display: flex; flex-direction: column; gap: var(--sp-4);
 *      text-align: left; padding: var(--sp-5); border-radius: var(--r);
 *      background: var(--surface); border: 1.5px solid var(--border-soft); ... }`
 * The radius was the current room's, so every card drew the same corner.
 * Everything else on that rule is untouched: same box, same padding, same fill,
 * same border, so the grid does not move.
 *
 * The shadow is new rather than an override - a card had none - and it is the
 * theme's own `--elev-2`, the rung a card sits on everywhere else in the app.
 * `--elev-1` is a hairline in every theme and would have shown nothing.
 */
.theme-card {
  border-radius: var(--tc-r, var(--r));
  box-shadow: var(--tc-elev, var(--elev-2));
}

/*
 * Overrides web/styles.css:5637
 *   `.theme-swatch { height: 46px; border-radius: var(--r-sm);
 *      border: 1px solid rgb(255 255 255 / .08); }`
 * Only the radius. The colours in the swatch are the theme's; the corner it is
 * cut with should be too, and it is the rung below the card's, so the two read
 * as one shape.
 */
.theme-swatch { border-radius: var(--tc-r-sm, var(--r-sm)); }

/*
 * The name of the theme, set in the theme's own headline face and weight.
 * This is the whole of the typographic preview and it costs no height at all:
 * Space Grotesk, Source Serif and Inter are three obviously different things
 * at the same size, and the weight runs 500 in Cold mountains to 700 in Dark.
 *
 * Overrides web/styles.css:5642
 *   `.theme-card b { font-size: var(--fz-ui); font-weight: 600; }`
 * The size stays: a card is a label, not a specimen sheet.
 */
.theme-card b {
  font-family: var(--tc-face, var(--font-display));
  font-weight: var(--tc-weight, 600);
  letter-spacing: var(--track-tight);
}

/*
 * And the tracked whisper - the "moves" mark on the five themes that have
 * scenery - tracks the way its own theme tracks: .09em in Clean against .24em
 * in Cold mountains, which is the widest tracking in the app.
 *
 * Overrides web/styles.css:5644
 *   `.theme-card em { font-style: normal; font-size: var(--fz-micro);
 *      text-transform: uppercase; letter-spacing: var(--track-label);
 *      color: var(--faint); }`
 * Only the tracking. Dark and Light have no scenery and so no `em`; their
 * tracking is the one air value their card cannot show, and inventing a label
 * to carry it would be a demo reel rather than a picker.
 */
.theme-card em { letter-spacing: var(--tc-track, var(--track-label)); }

/*
 * The welcome tour builds the same cards with `data-pick-theme` rather than
 * `data-theme-pick` (web/app.js:751 against web/app.js:838), which is why both
 * spellings are listed above. It therefore gets the whole preview for free,
 * and it is the first screen anybody ever sees.
 *
 * web/styles.css:5623
 *   `.tour-themes .theme-card { padding: var(--sp-4); gap: var(--sp-3); }`
 * is left alone: the tour's card keeps its own smaller box, and only what is
 * drawn inside that box changes.
 */


/* ==========================================================================
   3 - ICONS: the text-size control moves the sprite as well as the type
   ========================================================================== */

/*
 * Overrides web/styles.css:216-218
 *   `--icon-sm: 13px;`
 *   `--icon: 15px;`
 *   `--icon-lg: 18px;`
 *
 * The app offers 70% to 200% text and every size token is built on `--fs`
 * (web/styles.css:64-74). These three were not, so:
 *
 *   70%   a `.btn` stands 30.1px with a 15px sprite in it and 9.1px of type -
 *         the sprite, not the type, is what the control is built around
 *   130%  16.9px of type beside a 15px sprite
 *   200%  26px of type beside a 15px sprite, and every icon looks like a
 *         mistake next to the word it belongs to
 *
 * The numbers are the same at 100%, so nothing moves for anybody who has not
 * touched the control.
 *
 * The documented fixed-size exceptions (web/styles.css:76-91 - the script and
 * document pages, the deck page, the SVG chart labels, the map and
 * living-world overlays) are NOT affected by this, and that was checked rather
 * than assumed: every icon on those surfaces already states its own size and
 * never reads `--icon`. The deck's own icons are `calc(1.9 * var(--dp-t))`
 * (web/styles.css:9709), which is the deck's own scale; the page marks are
 * raw pixels. Nothing on a printed surface changes size here.
 */
:root {
  --icon-sm: calc(13px * var(--fs));
  --icon:    calc(15px * var(--fs));
  --icon-lg: calc(18px * var(--fs));
}

/*
 * The one place the scale had to be held back, found by measuring rather than
 * by reading: below 1664px the script page turns its two docked rails into
 * columns of icons, and that column is a layout constant -
 *   web/styles.css:7940  `--rail-icon-w: 46px;`
 *   web/styles.css:8053  `.script-layout .rail-tab { width: calc(var(--rail-icon-w)
 *                          - var(--sp-3) * 2); height: same; }`
 * - a 34px square, whose width is one term in the width arithmetic that gives
 * the page a true 8.5 inches at 1280 (web/styles.css:7989 onwards). At 200%
 * `--icon-lg` is 36px and eleven rail tabs each had 2px of sprite outside
 * their button.
 *
 * The rail is not type, so it does not answer to the type control. This is the
 * same decision the app's own 56px spine already makes at web/styles.css:463
 *   `.rail-btn .ic { width: 17px; height: 17px; }`
 * and it is written the same way.
 *
 * Overrides web/styles.css:8061 (and its twin at :14863)
 *   `.script-layout .rail-tab .ic { display: block; width: var(--icon-lg);
 *      height: var(--icon-lg); }`
 * `display: block` is what makes the icon appear at all below 1664px and is
 * deliberately not restated, so the media query keeps ownership of when.
 */
.script-layout .rail-tab .ic { width: 18px; height: 18px; }

/*
 * The app's own spine is already fixed at web/styles.css:463
 *   `.rail-btn .ic { width: 17px; height: 17px; }`
 * and stays that way. It is noted here so the two exceptions read as one
 * decision rather than two accidents.
 */


/* ==========================================================================
   4 - A FIELD AND A BUTTON THAT STAY A PAIR
   ========================================================================== */

/*
 * web/views/import.js carried `style="flex:1;font-size:12.5px"` on the "or
 * type a folder" field. 12.5px is the right size and a raw pixel is the wrong
 * way to ask for it: the `Go` button beside it is `.btn-sm`, which is on the
 * scale, so the two came apart the moment anybody touched the text-size
 * control. Measured:
 *
 *    70%   field 12.5px / 37.4px tall   button  8.8px / 25.6px   11.8px apart
 *   100%   field 12.5px / 37.4px tall   button 12.5px / 31.4px    6.0px apart
 *   130%   field 12.5px / 37.4px tall   button 16.3px / 37.2px    0.2px apart
 *   200%   field 12.5px / 37.4px tall   button 25.0px / 50.8px   13.4px apart
 *
 * The inline size is gone from import.js. This is the class it now wears, and
 * it is `.btn-sm` said for an input, on the same rung and the same padding, so
 * the pair is one control at every size.
 *
 * Sits with web/styles.css:1115
 *   `.input, .textarea, .select { width: 100%; font: inherit;
 *      font-size: var(--fz-body); padding: 8px 11px; ... }`
 * and mirrors web/styles.css:728
 *   `.btn-sm { font-size: var(--fz-small); padding: 5px 10px; }`
 *
 * NOTE FOR WHOEVER MERGES: import.js already ships the class. Until this rule
 * lands with it the field falls back to `--fz-body`, which is 14px against the
 * button's 12.5px - still on the scale and still moving with the control, but
 * 1.5px out. These two go in together.
 */
.input-sm, .textarea-sm, .select-sm {
  font-size: var(--fz-small);
  padding: 5px 10px;
}


/* ==========================================================================
   5 - TWO TYPE WEIGHTS THAT SAID THE OPPOSITE OF WHAT THEY MEANT
   ========================================================================== */

/*
 * Overrides web/styles.css:848
 *   `.tab.active { color: var(--text); border-bottom-color: var(--accent);
 *      font-weight: 550; }`
 * against web/styles.css:837-846
 *   `.tab { ... font-size: var(--fz-tiny); font-weight: 600; ... }`
 *
 * So the selected tab was set LIGHTER than the ones beside it, and the colour
 * and the underline were carrying the whole of "you are here" against the
 * weight actively arguing the other way. 650 rather than 700: it is a step up
 * from 600 on the app's own weight ladder, it is what `.tab-row-label` already
 * uses (web/styles.css:11156), and it costs about a pixel of width on one tab
 * rather than three on one tab - which matters, because the deck at 1280 in
 * Cold mountains has less than a pixel of slack (see the tab deck notes at
 * web/styles.css:15168 onwards).
 *
 * Everything else on the rule stands; only the weight is restated.
 */
.tab.active { font-weight: 650; }

/*
 * Overrides web/styles.css:11163
 *   `.tab-row.secondary .tab { font-size: calc(12.5px * var(--fs));
 *      padding-block: 7px; }`
 * under the comment "The second row is the quieter one, and says so before you
 * read it" - and it was set a full rung LARGER than the primary row above it
 * (`--fz-tiny`, 11.5px, web/styles.css:839). Quieter by intent, louder by
 * measurement.
 *
 * Equal, not smaller. The row is already quieter three other ways - `--text-3`
 * rather than `--muted`, a dashed rule above it, and its own label - and the
 * rung below `--fz-tiny` is `--fz-micro`, the tracked-whisper size, which is
 * not a size to set a control anybody has to click.
 *
 * `padding-block: 7px` is left exactly as it is: it is what holds the deck's
 * measured height, which shelf-test.py is pinned to.
 */
.tab-row.secondary .tab { font-size: var(--fz-tiny); }

/* ---- from the compose rebuild ---- */
/* ==========================================================================
   COMPOSED FOR THE SCREEN A WRITER ACTUALLY OWNS

   The home page and the project page were both composed at desktop width and
   never re-composed for a short viewport. At 1280x720 the result was a strong
   hero, one franchise card, and a hole: the largest single empty rectangle on
   the first screen measured 648 x 328px, 29.8% of everything right of the
   sidebar, and 42.9% of that screen was carrying nothing at all.

   Nothing here changes the visual language. It changes where things sit, how
   tall they are on a short screen, and which of them the writer meets first.
   Every measurement is on the app's own ladders, so the seven themes keep
   restating their own air.
   ========================================================================== */


/* ==========================================================================
   1 - THE FRONT DOOR
   ========================================================================== */

/*
 * A poster sized by the room it is standing in.
 *
 * `min-height: 46vh` reads as viewport-aware and is only half of one: the
 * body inside it held 84px of fixed vertical padding, so on a 720px screen
 * the marquee took 331px - 46% - and left 389px for everything a returning
 * writer came back for. 38vh with a floor keeps the hero a marquee at every
 * height (274px at 720, 410px at 1080) and hands the shelf below it enough
 * room to finish above the fold. The cap goes: max-height fought min-height
 * on tall screens and neither won cleanly.
 */
.home-hero {
  min-height: clamp(268px, 38vh, 500px);
  max-height: none;
}
.home-hero-body { padding-block: clamp(var(--sp-8), 3.2vh, var(--sp-10)); }

/* Day one has no art and a two-line promise, so it is content-bound rather
   than height-bound; the air around it is what has to give. All three doors
   in now stand clear of the fold on a 720px screen instead of being cut in
   half by it. */
.home-first .home-hero { min-height: clamp(232px, 32vh, 430px); }
.start-band { padding-top: clamp(var(--sp-8), 3.4vh, var(--sp-10)); }
.start-head { margin-bottom: clamp(var(--sp-7), 2.4vh, var(--sp-8)); }
.first-proof { padding-top: clamp(var(--sp-9), 4vh, var(--sp-10)); }

/* Both bands were capped and *centred* under a hero that runs the full width
   of the page, so on a 1920px screen the promise and the three doors started
   131px to the right of the title above them: two left edges on the first
   screen anybody sees. Same cap, ranged left. */
.start-band, .first-proof { margin-inline: 0 auto; }

.home-controls { padding-top: clamp(var(--sp-6), 2vh, var(--sp-8)); }


/* ---- the shelves: one bookcase, not three bands ------------------------- */

/*
 * A shelf used to be a band across the whole page whatever stood on it.
 *
 * A studio with one saga and three films therefore spent two full-width bands
 * - 802px of a 720px screen - on four cards, and left a poster-shaped hole
 * beside each. Shelves that do not need the width now stand side by side and
 * share one band, so the writer's whole library arrives on the first screen.
 * A shelf with more titles than fit still claims the whole row and scrolls
 * sideways with its arrows, exactly as before; `--n` is the card count the
 * view hands down, and `min()` is what decides between the two.
 */
.home-work {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  column-gap: var(--sp-10); row-gap: 0;
  padding-inline: clamp(24px, 5vw, 72px);
}
.home-work > .home-row {
  /*
   * A shelf asks for three covers and will take as many as it has.
   *
   * Asking for its full natural width instead is what put a nine-title Films
   * shelf on a line of its own and left a two-card Franchises shelf standing
   * beside 546px of nothing above it. Three covers is what decides whether
   * two shelves share a line; the grow puts the rest of the line back into
   * whichever of them still wants it, and the max stops a shelf stretching
   * past the covers it actually holds.
   */
  flex: 1 1 min(100%, calc(3 * var(--card-w) + 2 * var(--sp-7)));
  max-width: min(100%, calc(var(--n, 6) * var(--card-w) + (var(--n, 6) - 1) * var(--sp-7)));
  min-width: 0;
  padding-inline: 0;
  padding-top: clamp(var(--sp-7), 2.6vh, var(--sp-9));
}
.home-row-head { margin-bottom: var(--sp-5); }

/*
 * A poster is as big as the screen can afford.
 *
 * 176px was chosen against a 1080px-tall window. Held at 176 on a 720px one,
 * a row of covers plus its caption is 311px deep and pushes the next section
 * clean off the screen. The width answers to the shorter of the two axes, so
 * the card grows on a real desktop (227px at 1920x1080) and stands down on a
 * laptop (151px at 1280x720) rather than taking the fold with it.
 */
.home { --card-w: clamp(150px, min(13vw, 21vh), 268px); }
.shelf-item { width: var(--card-w); }
/* The 6px keeps the room the hover lift needs; a shorthand with 0 here would
   quietly undo it on a narrow window. */
.home-shelf { padding: var(--sp-3) 6px var(--sp-6); }
.shelf-strip::before, .shelf-strip::after { bottom: var(--sp-6); }

/* Everything at once wraps rather than scrolls, and its tracks are the same
   cover the shelf view uses, so switching between the two moves the covers
   rather than resizing them. `auto-fill` against a fixed 150px used to hand a
   two-card Franchises shelf five tracks and leave three of them empty. */
.shelf-strip.as-grid .home-shelf {
  grid-template-columns: repeat(auto-fill, minmax(var(--card-w), 1fr));
}


/* ---- the back of the room ---------------------------------------------- */

/*
 * One left edge down the whole page.
 *
 * The band at the foot was capped at 1180px and centred while the shelves
 * above it ran the full width of the page, so at 1920 the numbers began 209px
 * to the right of the posters and the page changed its mind about where it
 * started halfway down. Same padding as everything else, capped at the app's
 * own --content-max, ranged left against the shelves.
 */
.home-band {
  max-width: none;
  margin: clamp(var(--sp-7), 2.4vh, var(--sp-9)) 0 0;
  padding-inline: clamp(24px, 5vw, 72px);
}
/* The quote keeps its own measure; only the two full-width blocks are capped,
   or a line of reading matter would run to 1,340px. */
.home-band .split, .home-band .stat-row { max-width: var(--content-max); }
.home-band .split {
  grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
  gap: var(--sp-9);
}
/* A writer who has put the to-do list away gets one column, and a window
   dragged below the desktop floor gets one column: both rules already existed
   and this one sits after them, so it has to say so itself. */
.home-band .split.solo { grid-template-columns: minmax(0, 1fr); }
@media (max-width: 1080px) {
  .home-band .split { grid-template-columns: minmax(0, 1fr); }
}
.home-band .stat-row { margin-block: clamp(var(--sp-8), 3vh, var(--sp-9)) var(--sp-8); }


/* ==========================================================================
   2 - THE PROJECT PAGE
   ========================================================================== */

/*
 * A title card as tall as its title.
 *
 * `min-height: 230px` against a body that measures 171px, bottom-aligned, put
 * a 59px band of nothing between the breadcrumb and the first word on the
 * page - at every window size, because 230 is a number and not a proportion.
 * The floor is a proportion now and sits under the content rather than over
 * it. Real artwork is the one case that earns the height, so it keeps it.
 */
.project-hero { min-height: clamp(148px, 17vh, 214px); }
.project-hero.has-art { min-height: clamp(196px, 24vh, 300px); }
.project-hero-body { padding-block: clamp(var(--sp-8), 3vh, var(--sp-9)); }

/*
 * The page beside the page.
 *
 * The synopsis reads at a measure and stops well short of the right-hand
 * edge, so a third of the column carried nothing while the three live
 * miniatures and the list of what is left sat below the fold. Reading matter
 * left, the things worth looking at right, the masthead across both.
 *
 * The floor is a floor, not a phone: this is a chromeless desktop window and
 * there is no build below 1280. It exists only so that a window dragged
 * absurdly narrow degrades into one column instead of two thin ones.
 */
.overview {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(248px, 25%, 330px);
  grid-template-areas: "masthead masthead" "main side";
  column-gap: var(--sp-10);
  align-items: start;
}
.overview > .fp-band { grid-area: masthead; }
.overview > .overview-main { grid-area: main; min-width: 0; }
.overview > .overview-side { grid-area: side; min-width: 0; }
@media (max-width: 1080px) {
  .overview {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "masthead" "main" "side";
  }
}

/*
 * The masthead: eight short facts on one line instead of two bands.
 *
 * The frontispiece was centred in a page whose every other line is ranged
 * left, and it and the credits between them spent 244px of a 720px screen.
 * "N people who did not exist before you" keeps its rank - it is still the
 * only italic serif on the line, against 10.5px tracked capitals - it just
 * stops being an island.
 */
.fp-band {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-5) var(--sp-9);
  margin: 0 0 clamp(var(--sp-8), 2.6vh, var(--sp-9));
  padding-bottom: var(--sp-7);
  border-bottom: var(--hair) solid var(--hair-ink);
}
.fp-band .frontispiece {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-3) var(--sp-7);
  text-align: left; margin: 0; padding: 0; border-bottom: 0;
}
.fp-band .fp-day { margin: 0; }
.fp-band .fp-nums { margin: 0; justify-content: flex-start; }
.fp-band .credit-block { margin: 0; padding: 0; border-bottom: 0; }

/*
 * A page to write on, not a field to fill in.
 *
 * The synopsis is the reason this tab exists and an empty one was a 110px
 * box, so a project made five minutes ago ended its left-hand column two
 * fifths of the way down a 1080px screen and left the rest of it blank. A
 * writing surface should look like somewhere to write before there is
 * anything in it.
 */
.overview-main .bible .rich-edit.reading { min-height: clamp(190px, 27vh, 360px); }

/*
 * One tile per row in the companion column.
 *
 * `repeat(auto-fit, minmax(260px, 1fr))` collapses its empty tracks, so a
 * film - which has two of these, not three - was handed two 451px slabs
 * 346px deep across the full width, and at 1920 two of 625px. A tile is a
 * poster of the real thing; it does not need to be half a screen wide.
 */
.overview-side .tool-tiles {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-6);
  margin-top: 0;
}
.overview-side .section-head { margin-top: clamp(var(--sp-8), 2.6vh, var(--sp-9)); }
.overview-side .overview-todo { max-width: none; }

/* ---- from the emoji rebuild ---- */
/* ============================================================
   THE PICKERS IN A WORLD ENTRY'S SIDEBAR.

   Two things were wrong and they read as one mess.

   A field label is set in small uppercase with tracking, and the
   quiet explanation that sits beside it is a span INSIDE that
   label - so it was shouted too. "Its emoji" and "the face it
   wears in lists, maps and pins" ran together into one
   unbroken line of capitals, which is why the sidebar looked
   like a form written by somebody in a hurry.

   And the emoji palette was twenty-four buttons, always open.
   After the work to get emoji out of the chrome, a permanent
   3x8 block of them was the cheapest-looking thing on the page.
   The writer picks one, rarely; it does not deserve a quarter of
   the sidebar every time the page is opened.
   ============================================================ */

/* The aside inside a label is an aside, not part of the shout. */
.dossier-field > label .faint,
.dossier-field > label .small {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 450;
  display: block;
  margin-top: 3px;
  color: var(--faint);
}

/* ---- the emoji, folded away until asked for ---------------- */

.emoji-fold { display: flex; flex-direction: column; gap: var(--sp-3); }

/* What is on the page when nothing is being chosen: the mark the
   writer already picked, and one quiet way to change it. */
.emoji-now {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  cursor: pointer;
  width: 100%;
  text-align: left;
  color: var(--text);
  font: inherit;
  transition: border-color var(--dur-1) var(--ease);
}
.emoji-now:hover { border-color: var(--text-3); }
.emoji-now[aria-expanded="true"] { border-color: var(--accent); }

.emoji-now b {
  font-size: calc(20px * var(--fs));
  line-height: 1;
  width: 26px;
  text-align: center;
  flex: none;
}
/* Nothing chosen yet: a drawn mark rather than a stray glyph, so an
   unset field does not look like a broken one. */
.emoji-now .ic { width: 17px; height: 17px; opacity: .55; vertical-align: 0; }
.emoji-now span { font-size: var(--fz-small); color: var(--text-2); }

.emoji-fold .emoji-row { display: none; }
.emoji-fold.open .emoji-row {
  display: flex;
  padding-top: var(--sp-1);
  animation: fade var(--dur-1) var(--ease);
}

/* ---- from the empty rebuild ---- */
/* ============================================================
   ONE EMPTY STATE, DRAWN TWO WAYS, ON TABS THAT TOUCH.

   `emptyState()` draws `.empty`; `emptyLane()` draws `.lane-empty`.
   Both are deliberate. The lane one exists because a faint drawing
   of the rows you are about to have says what a tab is FOR faster
   than the sentence under it can, and because it can carry a quiet
   hint line under the button. That is worth keeping and is left
   alone here.

   What was not deliberate is that everything the two share drifted
   apart. Walking an empty film project, SCRIPTS & DRAFTS to
   CHARACTERS is one step, and across that step the box changes its
   border style, its padding, its background, its title size, its
   title weight, its body size and its body face, for the same kind
   of sentence. Measured at 1440x900 on the default dark theme:

                 .lane-empty              .empty
     padding     34px 24px 30px           44px 22px
     border      1px solid #21242c        1px dashed, accent-tinted
     background  flat #272a31             radial accent wash
     h3          17px / 500 / -0.17px     18px / 600 / -0.27px
     p           13.5px Inter, 391.8px    14px Source Serif 4, 340.7px

   Several of those numbers are off the token scale outright: 34px
   and 24px of padding, 13.5px of body text, 18px and 20px of margin.
   The 46ch measure was the same token in both and still came out
   51px wider on one, because the faces differ.

   `.empty` is the one to converge on. Its frame is argued for where
   it is defined (styles.css:986) - "the dashed border stays, it says
   'this is where something will be' rather than 'this is a thing'" -
   and that argument is just as true of a lane. Its type is on the
   scale from end to end. So the lane keeps its ghost and its hint,
   and takes everything else from `.empty`.

   HOW TO MERGE: these rules replace the shared parts of the
   `.lane-empty` block at styles.css:7603-7627. If they are appended
   rather than swapped in, they must land AFTER line 7627 or the old
   values win on equal specificity.

   OVERRIDES, quoted:

   styles.css:7603  .lane-empty {
   styles.css:7605    padding: 34px 24px 30px;
   styles.css:7606    background: var(--surface);
   styles.css:7607    border: 1px solid var(--border-soft);
   styles.css:7610  .lane-ghost { width: min(260px, 70%); margin-bottom: 20px; }
   styles.css:7617  .lane-empty h3 {
   styles.css:7618    margin: 0 0 8px; font-family: var(--font-display); font-weight: 500;
   styles.css:7619    font-size: calc(17px * var(--fs)); color: var(--text); letter-spacing: -.01em;
   styles.css:7621  .lane-empty p {
   styles.css:7622    margin: 0 0 18px; max-width: 46ch; line-height: 1.62;
   styles.css:7623    font-size: calc(13.5px * var(--fs)); color: var(--muted);
   styles.css:7625  .lane-empty-hint {
   styles.css:7626    margin-top: 12px; font-size: calc(11.5px * var(--fs)); color: var(--faint);

   MATCHED TO, quoted:

   styles.css:992   padding: var(--sp-10) var(--sp-8);
   styles.css:993   border: 1px dashed color-mix(in srgb, var(--accent) 22%, var(--border));
   styles.css:996   background:
   styles.css:997     radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--accent) 5%, transparent), transparent 70%),
   styles.css:998     color-mix(in srgb, var(--surface) 45%, transparent);
   styles.css:1013  .empty h3 {
   styles.css:1014    margin: 0 0 var(--sp-3); font-size: var(--fz-heading);
   styles.css:1015    color: var(--text); font-weight: 600;
   styles.css:1017  .empty p { margin: 0 auto var(--sp-7); font-size: var(--fz-body); max-width: var(--measure-short); line-height: 1.6; }
   styles.css:729   .empty p,   (in the reading-matter group at 725-737, which is
   styles.css:735               where `font-family: var(--font-read)` reaches it)
   styles.css:4855  .map-title, .modal-head h2, .empty h3 {
   styles.css:4856    font-family: var(--font-display);
   styles.css:4857    letter-spacing: var(--track-tight);
   ============================================================ */

/* The frame. Flex column and centring are the lane's own and stay:
   they are what holds the ghost, the sentence and the button in a
   stack rather than a paragraph. */
.lane-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--sp-10) var(--sp-8);
  border: 1px dashed color-mix(in srgb, var(--accent) 22%, var(--border));
  border-radius: var(--r-lg);
  color: var(--muted);
  background:
    radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--accent) 5%, transparent), transparent 70%),
    color-mix(in srgb, var(--surface) 45%, transparent);
}

/* The one thing the lane has that the shared empty state does not.
   The gap under it is a rung up from the 12px under `.empty`'s disc,
   because this drawing is five times as wide and crowds without it. */
.lane-ghost {
  width: min(260px, 70%);
  margin-bottom: var(--sp-7);
}

.lane-empty h3 {
  margin: 0 0 var(--sp-3);
  font-family: var(--font-display);
  font-size: var(--fz-heading);
  font-weight: 600;
  letter-spacing: var(--track-tight);
  color: var(--text);
}

/* The reading face, at the width the eye can walk back across. The
   measure token was already the same on both; only the face was not,
   which is why one line ran 51px longer than the other. */
.lane-empty p {
  margin: 0 auto var(--sp-7);
  max-width: var(--measure-short);
  font-family: var(--font-read);
  font-size: var(--fz-body);
  line-height: 1.6;
  color: var(--muted);
}

/* The lane's other genuine extra: a quiet second thought under the
   button. Same values as before, said in tokens. */
.lane-empty-hint {
  margin-top: var(--sp-6);
  font-size: var(--fz-tiny);
  color: var(--faint);
}

/* ---- from the families rebuild ---- */
/* ============================================================
   THE OTHER THREE FAMILIES.

   Grounds, powers, peoples and relics have had a page shaped
   like the thing they describe for a while. Forces, epochs and
   customs fell through to the plain layout: a stack of
   questions and answers, identical on a plague, a coronation
   and a currency. Those are the pages a worldbuilder spends
   most of their time on.

   Everything here is drawn from answers the writer already
   typed. Nothing stores anything, and nothing invents anything.
   ============================================================ */

/* ---- forces: what it does, against what it costs ---------- */

/*
 * Every force in a story is a bargain. Set as a column these are
 * six paragraphs; set facing each other they are the argument,
 * and the empty side is the one to go and fill. The scales sit
 * between them rather than beside, so the eye reads across.
 */
.cost-ledger { display: flex; flex-direction: column; gap: var(--sp-5); }

.ledger-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: stretch;
  gap: var(--sp-4);
}

.ledger-side {
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  padding: var(--sp-5) var(--sp-5) calc(var(--sp-5) + 2px);
  min-width: 0;
}
.ledger-side h4 {
  margin: 0 0 var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-3);
}
.ledger-side p {
  margin: 0;
  font-family: var(--font-read);
  font-size: calc(15px * var(--fs));
  line-height: 1.6;
  color: var(--text);
  white-space: pre-wrap;
}
/* The two halves are not the same weight in the story. What a thing costs is
   where the drama is, so it carries the accent and the giving side does not. */
.ledger-side.gives { border-left: 3px solid var(--border); }
.ledger-side.takes { border-left: 3px solid var(--accent); }
.ledger-side.blank { background: none; border-style: dashed; }
.ledger-side.blank p { font-family: var(--font-ui); font-size: var(--fz-small); }

.ledger-scales {
  display: grid;
  place-items: center;
  color: var(--text-3);
  opacity: .6;
}
.ledger-scales .ic { width: 20px; height: 20px; vertical-align: 0; }

.ledger-stop {
  display: flex;
  gap: var(--sp-4);
  align-items: flex-start;
  padding: var(--sp-4) var(--sp-5);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  border-radius: var(--r);
}
.ledger-stop > span { color: var(--accent); opacity: .8; flex: none; margin-top: 2px; }
.ledger-stop .ic { width: 16px; height: 16px; vertical-align: 0; }
.ledger-stop h4 {
  margin: 0 0 3px;
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-3);
}
.ledger-stop p {
  margin: 0;
  font-family: var(--font-read);
  font-size: calc(14.5px * var(--fs));
  line-height: 1.55;
  color: var(--text-2);
  white-space: pre-wrap;
}

/* ---- epochs: cause, the thing, and what was different ----- */

/*
 * An event is the only kind of page with a shape built into it, and it is the
 * one shape the plain layout flattened. Read as a chain, the empty box in the
 * middle of it is the most useful blank space in the app.
 */
.cause-chain {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--sp-5);
}

.chain-beat {
  position: relative;
  padding-left: 26px;
}
.chain-mark {
  position: absolute;
  left: 0;
  top: 3px;
  width: 13px;
  height: 13px;
  display: grid;
  place-items: center;
}
.chain-mark i {
  width: 9px; height: 9px;
  border-radius: var(--r-pill);
  background: var(--border);
  display: block;
}
.chain-beat.during .chain-mark i { background: var(--accent); width: 11px; height: 11px; }
.chain-beat.blank .chain-mark i { background: none; border: 1.5px dashed var(--text-3); }

/* The rule joining one beat to the next, drawn from the mark rather than the
   text, so it stays put however long an answer runs. */
.chain-arrow {
  position: absolute;
  left: 6px;
  top: 19px;
  bottom: calc(var(--sp-5) * -1);
  width: 1px;
  background: var(--border);
}

.chain-beat h4 {
  margin: 0 0 var(--sp-2);
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-3);
}
.chain-beat p {
  margin: 0;
  font-family: var(--font-read);
  font-size: calc(15px * var(--fs));
  line-height: 1.62;
  color: var(--text);
  white-space: pre-wrap;
}
.chain-beat.blank p { font-family: var(--font-ui); font-size: var(--fz-small); }

/* ---- epochs: who was on each side ------------------------- */

.sides-board { display: flex; flex-direction: column; gap: var(--sp-5); }

.sides-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-4);
}
.sides-versus {
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-style: italic;
  color: var(--faint);
  white-space: nowrap;
}

.side-card { min-width: 0; }
.side-card button,
.side-unknown {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  width: 100%;
  min-width: 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.side-unknown { cursor: default; border-style: dashed; background: none; }
.side-card button:hover { border-color: var(--accent); background: var(--raised-2); }
.side-card img {
  width: 34px; height: 34px; flex: none;
  border-radius: var(--r-sm);
  object-fit: cover;
  image-rendering: pixelated;      /* the art is drawn a pixel at a time */
}
.side-card b,
.side-unknown b {
  font-size: var(--fz-body);
  font-weight: 550;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.sides-rest {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
}
.sides-rest em {
  font-size: var(--fz-tiny);
  color: var(--faint);
  font-style: italic;
  margin-right: var(--sp-2);
}
/* The third party is usually where the interesting one is, so it is on the
   page rather than dropped, but smaller than the two doing the fighting. */
.sides-rest .side-card { flex: 0 1 auto; min-width: 150px; }
.sides-rest .side-card img { width: 24px; height: 24px; }

/* ---- customs: a loaf, and a day's work -------------------- */

/*
 * The two numbers a currency page already asks for happen to be the whole
 * economy. Dividing one by the other is arithmetic, not cleverness, and it is
 * the difference between two figures and a reason somebody steals.
 */
.purse { display: flex; flex-direction: column; gap: var(--sp-4); }

.purse-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-4);
}
.purse-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: var(--sp-5) var(--sp-4);
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r);
  text-align: center;
  min-width: 0;
}
.purse-mark { color: var(--text-3); opacity: .7; }
.purse-mark .ic { width: 17px; height: 17px; vertical-align: 0; }
.purse-cell b {
  font-family: var(--font-mono);
  font-size: calc(22px * var(--fs));
  font-weight: 600;
  color: var(--text);
  line-height: 1.1;
}
.purse-cell em {
  font-size: var(--fz-tiny);
  font-style: normal;
  color: var(--text-3);
  line-height: 1.4;
}
.purse-rule { width: 1px; align-self: stretch; background: var(--border); }

.purse-reading {
  margin: 0;
  font-family: var(--font-read);
  font-size: calc(14.5px * var(--fs));
  font-style: italic;
  line-height: 1.55;
  color: var(--text-2);
  text-align: center;
}
.purse-reading.faint { color: var(--faint); }

/* ---- customs: when it comes round ------------------------- */

.round-band {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--sp-4);
}
.round-cell {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--sp-4) var(--sp-5);
  background: var(--raised);
  border-radius: var(--r);
  border-left: 3px solid var(--border);
  min-width: 0;
}
.round-cell em {
  font-family: var(--font-ui);
  font-size: var(--fz-tiny);
  font-style: normal;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-3);
}
.round-cell b {
  font-family: var(--font-read);
  font-size: calc(15px * var(--fs));
  font-weight: 500;
  line-height: 1.5;
  color: var(--text);
}

/* ---- the bands at the head of the page -------------------- */

.epochs-band .stat-band,
.customs-band .stat-band { flex-wrap: wrap; }

/* ---- narrow screens --------------------------------------- */

/*
 * Facing pairs stop facing below this width: side by side in 300px is two
 * columns of one word each. They stack, and the mark between them turns on
 * its side so it still reads as a relationship rather than two boxes.
 */
@media (max-width: 760px) {
  .ledger-pair,
  .sides-pair,
  .purse-pair { grid-template-columns: minmax(0, 1fr); }
  .ledger-scales,
  .sides-versus { justify-self: center; }
  .purse-rule { width: auto; height: 1px; align-self: auto; }
}

/* ---- from the monogram rebuild ---- */
/* ============================================================
   THE MONOGRAM — what stands where the generated artwork stood.

   The crests, banners, beasts, struck medals and pixel faces are gone. They
   assumed a genre (a shield is wrong on a cyberpunk city) and they looked
   generated (three films came out as the same spoked wheel in three colours).
   In their place: the thing's initials, in its own ink, printed.

   Everything below is a thing a two-colour press does and a screen effect does
   not. There is no gradient, no blur, no glow and no bevel anywhere in this
   file, and there must not be one added:

     out of register  the letters are struck twice. The second impression is a
                      hard offset down and right in the raw ink, with zero
                      blur. Blur reads as a drop shadow, which is a screen
                      thing; a hard offset in a stronger ink reads as the sheet
                      having moved between passes.
     one ink          a thing the writer coloured prints in that colour. A
                      thing nobody coloured prints in the room's own accent,
                      never in a colour hashed off its name — a shelf of twelve
                      hashed hues is exactly what "generated" looks like.
     crop marks       the larger sizes carry two corner registration marks, top
                      left and bottom right, running with the slip. A printer's
                      mark belongs to no genre and is four flat rectangles.
     elevation        an offset block of ink, never a soft shadow. Same move as
                      `zz-riso.css` makes on the primary button.

   Every ink is stated as a mix against `--text` or `--bg-deep` rather than as
   a value, so all eight themes are handled by the same three lines: the
   letters go pale on a dark page and dark on a light one without this file
   knowing which page it is on.
   ============================================================ */

/* ---- the base ----------------------------------------------------------- */

.mono {
  /* Handed in on the element by `monogram()`. Named here as well so a monogram
     that somehow arrives without one is still printed rather than invisible. */
  --mono-ink: var(--accent);
  --mono-slip: 2px;

  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  overflow: hidden;

  /*
   * The plate is mixed against `--bg-deep` and not `--surface`, and the
   * difference was visible rather than theoretical: a title with no poster sits
   * on `.shelf-spine`, which tints ITSELF with the same colour against
   * `--surface`. Mixed the same way the plate vanished into the spine and only
   * its rule showed, so the printed label read as a wireframe. `--bg-deep` is
   * the one rung the app guarantees is darker than every card and every spine,
   * in all eight themes, so the plate separates wherever it is put down.
   */
  background: color-mix(in srgb, var(--mono-ink) 18%, var(--bg-deep));
  /* Half the ink and half the page's own text colour. The ink alone was
     tried first and a red one measured 4.49:1 against its own plate in
     riso, which is a fail rather than a near miss. Half and half keeps the
     hue plainly readable - a red monogram is still red - and lets the
     letters take the page's contrast rather than the ink's. */
  color: color-mix(in srgb, var(--mono-ink) 50%, var(--text));

  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--mono-fz, 17px);
  line-height: 1;
  /* Tracking puts a gap after the last letter too, so a centred pair sits half
     a gap left of true centre. Half the tracking back is the correction. */
  letter-spacing: .06em;
  text-indent: .03em;
  white-space: nowrap;
  -webkit-user-select: none;
  user-select: none;

  /* The second pass. Hard, never blurred. */
  text-shadow: var(--mono-slip) var(--mono-slip) 0 var(--mono-ink);
}

/* The letter size is deliberately NOT scaled by `--fs`, unlike almost
   everything else in this stylesheet. The box it sits in is a fixed avatar or
   a fixed share of a card, so letters that grew to 200% would simply run out
   of it. The name beside the monogram carries the text-size control, and the
   name is the thing being read. */

/* ---- the plate: rule and crop marks ------------------------------------- */

/*
 * Only worth it above about 90px. Below that the marks are noise and the rule
 * eats the letters, which is why the small avatars further down never ask for
 * them.
 */
.mono-framed::before {
  content: "";
  position: absolute;
  inset: var(--mono-inset, 7px);
  border: 1.5px solid color-mix(in srgb, var(--mono-ink) 34%, transparent);
  pointer-events: none;
}

.mono-framed::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  --mark: color-mix(in srgb, var(--mono-ink) 62%, transparent);
  --len: var(--mono-mark, 13px);
  --edge: var(--mono-mark-edge, 5px);
  background:
    linear-gradient(var(--mark), var(--mark)) no-repeat
      left var(--edge) top var(--edge) / var(--len) 2px,
    linear-gradient(var(--mark), var(--mark)) no-repeat
      left var(--edge) top var(--edge) / 2px var(--len),
    linear-gradient(var(--mark), var(--mark)) no-repeat
      right var(--edge) bottom var(--edge) / var(--len) 2px,
    linear-gradient(var(--mark), var(--mark)) no-repeat
      right var(--edge) bottom var(--edge) / 2px var(--len);
}

/* ---- the home shelf ----------------------------------------------------- */

/*
 * The hero, for a title with no poster.
 *
 * styles.css:8641 `.home-hero-forge` hung the struck medal here at `opacity:
 * .2`, rotated, half off the right edge — an emblem rather than a picture, and
 * that judgement was right. The monogram keeps the position and drops the
 * plate: at this size a filled panel would fight the title, and bare letters
 * slipped against themselves read as an overprint on the page, which is what
 * the space actually wants.
 */
.home-hero-mono {
  position: absolute;
  right: clamp(-24px, -1.2vw, 0px);
  bottom: -8%;
  width: clamp(180px, 24vw, 340px);
  aspect-ratio: 1;
  background: none;
  color: color-mix(in srgb, var(--mono-ink) 38%, transparent);
  text-shadow: 8px 8px 0 color-mix(in srgb, var(--mono-ink) 15%, transparent);
  font-size: clamp(112px, 16vw, 226px);
  transform: rotate(-4deg);
  pointer-events: none;
}

/*
 * A title with no poster, on the shelf.
 *
 * styles.css:8863 `.shelf-poster img.shelf-forge` is written at that
 * specificity because `.shelf-poster img` sets `object-fit: cover` at 100% and
 * would have stretched the medal. A span is not an img, so none of that
 * reaches it and the sizing is stated plainly.
 *
 * No plate here, and this was measured before it was decided. `.shelf-spine`
 * tints ITSELF with the title's colour, so a tinted plate inside it is one
 * colour on top of the same colour: at 124px the field disappeared and only its
 * rule showed, which reads as a wireframe rather than as a label. The spine is
 * already the field, so the mark is simply printed onto it — a rule and two
 * crop marks at cover size, which is what a one-sheet actually looks like.
 */
.shelf-poster .shelf-mono {
  /* Stretched rather than square, so the printed rule is the shape of the
     cover it is printed on. A square frame inside a 2:3 poster reads as a
     sticker somebody put there. */
  flex: 1 1 auto;
  align-self: stretch;
  width: 100%;
  min-height: 0;
  background: none;
  box-shadow: none;
  font-size: 84px;
  --mono-slip: 5px;
  --mono-inset: 0px;
  --mono-mark: 15px;
  --mono-mark-edge: 0px;
  transition: transform var(--dur-2) var(--ease);
}
.shelf-item:hover .shelf-mono { transform: translateY(-3px); }

/*
 * The franchise tile: the saga's mark, struck across the front of the spines.
 * styles.css:8971 `.saga-crest` did the same job with the same geometry.
 */
.shelf-item .saga-mono {
  position: absolute;
  left: 50%;
  bottom: 12%;
  transform: translateX(-50%);
  width: 48%;
  max-width: 86px;
  aspect-ratio: 1;
  border-radius: var(--r-xs);
  font-size: 30px;
  box-shadow: 3px 3px 0 color-mix(in srgb, var(--mono-ink) 34%, transparent);
  transition: transform var(--dur-2) var(--ease);
}
.shelf-item:hover .saga-mono { transform: translateX(-50%) translateY(-4px); }

body.no-motion .shelf-mono,
body.no-motion .saga-mono { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .shelf-mono, .saga-mono { transition: none; }
  .shelf-item:hover .shelf-mono { transform: none; }
  .shelf-item:hover .saga-mono { transform: translateX(-50%); }
}

/* ---- the saga board ----------------------------------------------------- */

/*
 * styles.css:4278 `.saga-art` is `display: block` for an <img>; a span has to
 * be told otherwise, and `.saga-art.mono` is (0,2,0) against its (0,1,0) so it
 * wins without !important. The poster proportion comes from `.saga-art` and is
 * left alone: a card on the board should be the shape of a card.
 *
 * The separate `<b>` of initials that used to sit under the medal
 * (styles.css:4304 `.saga-art.blank b`) is gone from the markup, because the
 * monogram IS the initials and printing them twice was the old fallback
 * apologising for the medal.
 */
.saga-art.mono {
  display: flex;
  border-radius: var(--r-sm);
  font-size: 76px;
  --mono-slip: 5px;
  --mono-inset: 9px;
  --mono-mark: 15px;
  transition: transform var(--dur-1) var(--ease);
}
.saga-card:hover .saga-art.mono { transform: translateY(-2px); }
body.no-motion .saga-art.mono { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .saga-art.mono { transition: none; }
  .saga-card:hover .saga-art.mono { transform: none; }
}

/* ---- the two heroes: a character, and a world entry --------------------- */

/*
 * styles.css:6330 `.character-hero-forge` and styles.css:6400
 * `.entry-hero-forge` both stretched an <img> across `inset: 0` and then
 * pulled it back with `object-fit: contain` and `object-position: right`. A
 * plate does not need that: it is placed where it goes.
 *
 * These are the only two monograms big enough to be looked at rather than
 * glanced at, so they are the two that carry the plate rule and the crop
 * marks, and they sit clear of the type on the left.
 */
.character-hero-mono,
.entry-hero-mono {
  position: absolute;
  top: 50%;
  right: clamp(18px, 4vw, 76px);
  transform: translateY(-50%);
  width: clamp(132px, 19%, 216px);
  aspect-ratio: 3 / 4;
  border-radius: var(--r-sm);
  font-size: clamp(58px, 7vw, 96px);
  --mono-slip: 5px;
  --mono-inset: 10px;
  --mono-mark: 16px;
  --mono-mark-edge: 7px;
  box-shadow: 5px 5px 0 color-mix(in srgb, var(--mono-ink) 22%, transparent);
}

/* A world entry is a wider thing than a person more often than not — a realm,
   an event, a law — so its plate is a landscape rather than a portrait. */
.entry-hero-mono {
  aspect-ratio: 4 / 3;
  width: clamp(160px, 23%, 268px);
}

/* ---- the cast wall, and the world list ---------------------------------- */

/*
 * styles.css:10984 `.cast-tile-art img.forged` and styles.css:6448
 * `.world-card-art img.forged` both sat the drawing inside the card's own
 * backdrop. The monogram fills the frame instead: at card size a plate with a
 * margin round it inside a card with a margin round it is two frames doing one
 * job. Filling it also hides `.world-card-art`'s radial wash, which is a
 * gradient this theme cannot print anyway.
 */
.cast-tile-art .mono,
.world-card-art .mono {
  width: 100%;
  height: 100%;
  border-radius: 0;
  --mono-slip: 3px;
}
.cast-tile-art .mono { font-size: 58px; }
.world-card-art .mono { font-size: 44px; }

/* ---- the small avatars -------------------------------------------------- */

/*
 * Each of these mirrors the `img` rule the monogram is standing in for, so the
 * row it sits in keeps its rhythm. Sizes are quoted from the rules they match:
 *
 *   styles.css:12288  .face img        52px, round
 *   styles.css:12338  .link-face img   fills a 34px box
 *   styles.css:12502  .ref-row img     34px, 8px corners
 *   styles.css:11882  .rung-face img   28px, round
 *   styles.css:11936  .prov-node img   42px, 9px corners
 *   styles.css:6939   .side-card img   34px, --r-sm corners
 *   styles.css:6970   .sides-rest ...  24px
 *   styles.css:11398  .ref-card-art    46px, 9px corners (66px when big)
 *   styles.css:10766  .kin-face        44px wide, 3/4
 *   styles.css:12254  .inside-card img full width, 3/2
 *
 * None of them is framed: below about 90px a plate rule and a crop mark are
 * dirt on the glass rather than detail.
 */
.face > .mono {
  width: 52px; height: 52px; flex: none;
  border-radius: var(--r-pill);
  font-size: 19px;
  --mono-slip: 1px;
}
.link-face > .mono {
  width: 100%; height: 100%;
  border-radius: 0;
  font-size: 13px;
  --mono-slip: 1px;
}
.ref-row > .mono {
  width: 34px; height: 34px; flex: none;
  border-radius: 8px;
  font-size: 13px;
  --mono-slip: 1px;
}
.rung-face > .mono {
  width: 28px; height: 28px; flex: none;
  border-radius: var(--r-pill);
  font-size: 11px;
  --mono-slip: 1px;
}
.prov-node button > .mono {
  width: 42px; height: 42px; flex: none;
  border-radius: 9px;
  font-size: 16px;
  --mono-slip: 1px;
}
.side-card button > .mono {
  width: 34px; height: 34px; flex: none;
  border-radius: var(--r-sm);
  font-size: 13px;
  --mono-slip: 1px;
}
.sides-rest .side-card button > .mono {
  width: 24px; height: 24px;
  font-size: 10px;
  --mono-slip: 0px;
}
.ref-card-art.mono {
  width: 46px; height: 46px; flex: 0 0 auto;
  border-radius: 9px;
  font-size: 17px;
  --mono-slip: 1px;
}
.ref-card.big .ref-card-art.mono {
  width: 66px; height: 66px;
  border-radius: 12px;
  font-size: 24px;
  --mono-slip: 2px;
}
/* styles.css:5362 `.worldweb-face img` filled a 46px round node. Not in the
   brief and found by the app refusing to start: the connections web imported
   the forge's helper by a different door. */
.worldweb-face > .mono {
  width: 100%; height: 100%;
  border-radius: 0;
  font-size: 16px;
  --mono-slip: 1px;
}
.inside-card > .mono {
  width: 100%; aspect-ratio: 3 / 2;
  border-radius: 6px;
  font-size: 26px;
  --mono-slip: 2px;
}
.kin-face.mono {
  width: 44px; height: auto; aspect-ratio: 3 / 4; flex: none;
  border-radius: var(--r-xs);
  font-size: 16px;
  --mono-slip: 1px;
  cursor: pointer;
}

/* ---- picking a kind of thing to add to the world ------------------------ */

/*
 * styles.css:12467 `.kind-card img` showed forged art for each of the
 * twenty-six kinds. A monogram is wrong here and so was the art: these are not
 * things, they are categories, and "Pl" for Places says nothing. They take the
 * drawn glyph of the family they belong to instead — the same seven glyphs the
 * screen immediately before this one already shows (styles.css:12390
 * `.family-tile-glyph`), so the two steps of one dialog now agree.
 */
.kind-card-glyph {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--entry-accent, var(--accent)) 14%, var(--surface));
  color: var(--entry-accent, var(--accent));
  box-shadow: 2px 2px 0 color-mix(in srgb, var(--entry-accent, var(--accent)) 24%, transparent);
}
.kind-card-glyph .ic { width: 24px; height: 24px; }

/* ---- riso, where the paper is warm and the ink is flat ------------------ */

/*
 * The base above is already flat, so riso needs almost nothing. What it does
 * want is a firmer impression: on warm paper a 15% tint is nearly invisible
 * and the plate stops reading as a plate.
 */
html[data-theme="riso"] .mono {
  background: color-mix(in srgb, var(--mono-ink) 24%, var(--bg-deep));
  color: color-mix(in srgb, var(--mono-ink) 56%, var(--text));
}
html[data-theme="riso"] .home-hero-mono {
  background: none;
  color: color-mix(in srgb, var(--mono-ink) 42%, transparent);
}

/* ---- from the nav rebuild ---- */
/* =========================================================================
   The keyboard ring on the project list.

   The rows in `#sidebar-nav` are anchors now (web/app.js, drawSidebar), so
   for the first time they take keyboard focus and the shared ring is asked
   to draw on them. Photographed in all seven themes at 1280x720, it comes
   up short twice on these rows: once for a reason that is the shared rule's
   and once for a reason that is theirs.

   What is being overridden, styles.css:369-373:

       :focus-visible {
         outline: 2px solid color-mix(in srgb, var(--accent) 75%, transparent);
         outline-offset: 2px;
         border-radius: var(--r-xs);
       }

   1. The colour is 25% transparent, which is comfortable on the six dark
      themes and not on the light one. Measured off the rendered frame, ring
      against the pixel it covers:

         dark 4.2:1   glass 3.1:1   starry 5.8:1   blossom 6.8:1
         forest 5.8:1   mountains 6.1:1   light 2.7:1

      On light not one pixel of the ring reaches 3:1, so the row a keyboard
      user is standing on is the hardest one to find. WCAG 1.4.11 asks 3:1 of
      a control boundary. Solid accent instead of 75% of it measures, on the
      same frames:

         dark 6.8:1   glass 4.6:1   starry 9.9:1   blossom 11.5:1
         forest 9.4:1   mountains 9.9:1   light 3.9:1

      Measured with focus-ring.py in the scratchpad, which diffs the frame
      with the row focused against the frame without and reads the ring off
      the difference. The scenery themes wobble in the last decimal.

      Nothing else changes: same width, same colour family, same
      only-on-keyboard rule.

   2. `outline-offset: 2px` puts the ring outside the row. `.sidebar-nav`
      (styles.css:537) is `overflow-y: auto` with `padding: 0 8px 12px`, so
      there is no room above the first row and the scroll box cuts the top
      edge off. Counted along the top edge of "Everything": 8 pixels lit out
      of 215, against 209 along the bottom. Three sides of a ring.

      Turning the offset inwards puts the ring on ground the row owns, where
      nothing can clip it (207 of 215 top and bottom), and stops it crowding
      the rows above and below, which sit flush against it. `.el-row`
      (styles.css:11180) and `.board .card` (styles.css:3426) already do
      exactly this, for the same reason.

   The ring rule's third line is left alone: `border-radius: var(--r-xs)` is
   the same specificity as `.nav-item` (styles.css:548) and loses to it on
   source order anyway, so a focused row measures 8px today and will still
   measure 8px. Restating it here would be a declaration that does nothing.

   Scoped to the list, so the app's one ring stays the app's one ring
   everywhere else. The honest fix for (1) is the shared rule itself, which
   belongs to whoever owns styles.css: every theme would be better for it
   and the light theme needs it on all forty of its tab stops, not eight.
   ========================================================================= */

.sidebar-nav .nav-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* ---- from the paper rebuild ---- */
/* ============================================================
   The floating panel stops at the last character.

   THE FAULT. At 1280x720, opening Scenes or Stats put the panel
   on top of the writing. Measured, with a Range rather than the
   element box (the element box is the whole 816px sheet, which is
   not where the ink is):

     paper   376 - 1192   a true 816, as promised
     ink     520 - 1096   1.5in to 7.5in of it, 60 columns
     panel   934 - 1234   300px, anchored to the rail

   162px of a 60 column line, columns 43 to 60, sat under the
   panel, and a writer could type there without seeing what they
   typed. The left rail does the same thing at the other end:
   its panel runs 334 - 634 against an ink edge at 520, so the
   first 12 columns go under it. Both, at every width from 1280
   to 1600, and worse at the larger text sizes (178px and 128px
   at 200%). Only 1920 was clean, because there the rails dock.

   WHAT THIS DOES NOT DO. It does not touch the paper's width.
   The page holds a true 816px (8.5in at 96dpi) at every width,
   which is the invariant web/views/script.js:3518 states and a
   reviewer verified, and nothing here scales or shrinks it.
   Nor does it move the page when a panel opens: the one shift
   this file makes is applied whether a panel is open or shut, so
   opening one changes nothing but the panel.

   THE ROOM THERE ACTUALLY IS. The paper is 816 but the ink is
   576. Inside the sheet there are 144px of blank left margin
   (1.5in) and 96px of blank right margin (1in), and a panel over
   blank margin costs the writer nothing. Everything outside the
   ink, at 1280:

     186   left of the ink, from the desk rail to column 1
     138   right of the ink, from column 60 to the scene rail
     324   in total, and that total is fixed

   It is fixed because it is (the writing column) minus (the ink),
   and the writing column is the window less the studio spine, the
   sidebar and the two 46px icon rails. Nothing in this file can
   invent more of it. So 300px does not fit at 1280 and cannot be
   made to: the panel takes what is there, and at 1280 that is
   147px. That is a real loss, stated plainly here and in the
   report, and it is still better than a panel over the words.

   TWO CHANGES, in order of how much they buy:

   1. The panel takes the room its own side has, never more. This
      is the whole fix; everything else is refinement.

   2. The ink is centred in the room rather than the paper. The
      page's margins are lopsided (1.5in against 1in), so centring
      the sheet leaves 186px on one side and 138px on the other
      while only one panel can ever be open. Sliding the page left
      by half the difference evens them up, and from 1366 upwards
      both panels are then the same width whichever side they are
      on. It is a constant, so nothing moves when a panel opens,
      and it is capped at the slack that already exists, so it can
      never squeeze the paper below its 816.

   Not done, and why: pinning the page hard left, which the fault
   report suggested first. Measured, it is worth only the centring
   slack, 22px of the 162 needed, and it takes all of it off the
   desk panels, which need it too. Centring the ink is the same
   idea taken to where it stops helping.

   OVERRIDES. Line numbers are against a web/styles.css of 15155
   lines, read after the merge of 1 August. That file now carries
   the script-laptop block twice, once at 7806 outside the merge
   markers and once at 14595 inside them, so every rule named
   below has a stale twin about 6,800 lines above it. Nothing here
   relies on which copy wins: each selector carries four classes
   more than the rule it restates, so it holds from wherever
   merge-css.py drops this file.

     14818  `width: var(--rail-panel-w);`
            in `.script-shell.show-desk .desk-rail >
            [data-desk-panel]:not([hidden])` (14814) and
            `.script-shell.show-rail .script-rail >
            [data-panel]:not([hidden])` (14815). 300px flat,
            whatever is beside it. Restated as a min() against
            the room.

     14825  `padding: var(--sp-7) var(--sp-7) var(--sp-9);`
            in the same rule. Only the inline sides are restated.

     3135   `padding: 24px 20px 60vh;` in `.script-wrap` (3134).
            Not replaced; a margin is added beside it.

     1175   `.seg { display: flex; gap: var(--sp-2); ... }`
            with no wrap, which at these widths posted the third
            button off the edge of the Read panel where it could
            not be clicked at all.

   The three modes that take the rails away (`room-off`, `beside`,
   `:fullscreen`) are named out of every selector here exactly as
   they are at styles.css:14749, because in those the writing
   column is not two rails narrower and the arithmetic below would
   be describing a room that is not there.
   ============================================================ */

@media (max-width: 1664px) {

  /* ---- how much room is beside the ink -------------------------------- */

  /*
   * Measured from the window, because the panel cannot see the page.
   *
   * The panel is positioned inside a 46px rail, so every percentage
   * available to it is a percentage of 46px, and container units
   * resolve against the wrong box outside `.script-wrap`. The one
   * frame the panel and the paper share is the window, so the
   * arithmetic is done there: the writing column is the window less
   * the studio's spine and sidebar and the two icon rails, and the
   * paper is what the stylesheet has already said it will be,
   * `12pt x --fs x --write-fs x 51`, or that column, whichever is
   * smaller.
   *
   * Every term is deliberately a little pessimistic, so the panel
   * can only ever stop short of the ink and never over it:
   *
   *  - 11px comes off the column for the writing area's own
   *    scrollbar (styles.css:349). Headless Chromium draws that as
   *    an overlay and takes no width for it; a build that draws it
   *    in the flow takes 11. `100vw` cannot tell the difference, so
   *    the width is always subtracted and the panel gives up about
   *    5px it may not have needed.
   *  - that same pessimism is what guarantees the paper cannot
   *    shrink: --room-page is at most --room-inner, which leaves
   *    the slide below 11px of headroom it can never eat.
   */
  .script-shell {
    --room-chrome: calc(56px + var(--sidebar-w));   /* styles.css:380 */
    --room-wrap: calc(100vw - var(--room-chrome) - 2 * var(--rail-icon-w) - 11px);
    --room-inner: calc(var(--room-wrap) - 40px);    /* styles.css:3135, 20px a side */
    --room-page: min(calc(var(--script-true-fs) * 51), var(--room-inner));

    /*
     * Half the difference between the page's two margins: 9/51 of
     * the sheet on the left, 6/51 on the right, so 3/102 of it is
     * the shift that puts the writing in the middle of the room.
     * Never more than the slack the page is already floating in,
     * or the sheet would be pushed into the desk rail and the
     * clamp above would start taking it off the paper's width.
     */
    --room-slide: min(
      calc(var(--room-page) / 34),
      calc(max(0px, calc(var(--room-inner) - var(--room-page))) / 2));

    /*
     * From the rail's inner edge to the ink, on each side. 45/51 of
     * the sheet is its left edge to column 60; 42/51 is its right
     * edge back to column 1. The 2px is not a gap for the eye, it
     * is a guard: a panel landing on the exact pixel of the last
     * glyph is one rounding error from covering it.
     */
    --room-right: calc(var(--room-wrap) / 2 + var(--room-page) / 2
                       - var(--room-page) * 45 / 51 + var(--room-slide) - 2px);
    --room-left: calc(var(--room-wrap) / 2 + var(--room-page) / 2
                      - var(--room-page) * 42 / 51 - var(--room-slide) - 2px);
  }

  /* The sidebar folds away (styles.css:5986) and focus mode takes the
     spine with it (styles.css:2019). Both hand the width to the page,
     and the panel should be told rather than left guessing low. */
  .app.sidebar-hidden .script-shell { --room-chrome: 56px; }
  body.faux-fullscreen .script-shell { --room-chrome: 0px; }

  /* ---- the writing sits in the middle of the room --------------------- */

  /*
   * The margin is on the column, not on the sheet, so the title and
   * the footer travel with the page instead of staying behind over
   * its edge. At the larger text sizes the page already fills the
   * column, the slack is nil, and this is nil with it.
   */
  .script-shell:not(.room-off):not(:fullscreen)
    .script-layout:not(.beside) .script-wrap {
    margin-inline-end: calc(var(--room-slide) * 2);
  }

  /* ---- the panel takes what is left ----------------------------------- */

  .script-shell.show-rail:not(.room-off):not(:fullscreen)
    .script-layout:not(.beside) .script-rail > [data-panel]:not([hidden]) {
    width: min(var(--rail-panel-w), max(0px, var(--room-right)));
  }
  .script-shell.show-desk:not(.room-off):not(:fullscreen)
    .script-layout:not(.beside) .desk-rail > [data-desk-panel]:not([hidden]) {
    width: min(var(--rail-panel-w), max(0px, var(--room-left)));
  }

  /*
   * A narrower panel has less to spend on its own edges. 10px still
   * reads as an inset and hands 12px back to the words, which on a
   * 147px panel is a whole word of a scene heading.
   */
  .script-shell.show-rail:not(.room-off):not(:fullscreen)
    .script-layout:not(.beside) .script-rail > [data-panel]:not([hidden]),
  .script-shell.show-desk:not(.room-off):not(:fullscreen)
    .script-layout:not(.beside) .desk-rail > [data-desk-panel]:not([hidden]) {
    padding-inline: var(--sp-5);
  }

  /*
   * Read's speed control is three buttons that will not shrink, in a
   * row that did not wrap: below about 210px "Quicker" was drawn
   * outside the panel and could not be clicked at all. The rail tabs
   * in the same block made the same choice for the same reason
   * (styles.css:14673) - a control that wraps is still a control,
   * one that overflows is gone.
   */
  .script-shell.show-rail:not(.room-off) .script-rail > [data-panel] .seg,
  .script-shell.show-desk:not(.room-off) .desk-rail > [data-desk-panel] .seg {
    flex-wrap: wrap;
  }

  /* ---- and the panel has to survive being the narrow one --------------
   *
   * The rule above buys the writer their last seventeen columns back by
   * stopping the panel at the ink. The bill is paid by the panel: at 1280
   * it comes out 147px wide with 79px of text column inside it, and the
   * scene list was built for the 212px it gets at 1920.
   *
   * Measured there, with the panel open: "CANDLEMARCH SQUARE" wants 98px
   * and was cut off sideways, and "THE VANE HOUSE, KITCHEN" wants three
   * lines and was clamped to two. Both are exactly the identical-rows
   * fault the two-line slugline was introduced to fix - a list of scenes
   * that all read "THE VANE HOUSE..." is not a list.
   *
   * 1280 cannot hold a true 8.5in page and a comfortable panel at the same
   * time; the arithmetic does not allow it and the page is not negotiable.
   * So the panel is the thing that gets cramped - but it must get SMALLER,
   * never cut. A third line, and long words allowed to break rather than
   * run off the edge. Both revert to the shipped two lines the moment the
   * rail has room, which is 1366 up.
   */
  .script-shell.show-rail:not(.room-off) .script-rail > [data-panel] .scene-h,
  .script-shell.show-desk:not(.room-off) .desk-rail > [data-desk-panel] .scene-h {
    /* No clamp at all, rather than a bigger number. Three lines held at
       100%, and failed again at 130% - any fixed count is a guess that a
       writer can invalidate by turning the text up, which is the one thing
       this rail exists to survive. The row gets as tall as the slugline
       needs; the list scrolls, and nothing is lost. */
    display: block;
    -webkit-line-clamp: none;
    overflow-wrap: anywhere;
  }
  .script-shell.show-rail:not(.room-off) .script-rail > [data-panel] .scene-when,
  .script-shell.show-desk:not(.room-off) .desk-rail > [data-desk-panel] .scene-when {
    overflow-wrap: anywhere;
  }
}

/* ---- from the print rebuild ---- */
/* ============================================================
   PAPER.

   Ctrl+P is reflex, and until now the app had no answer to it: the
   browser printed the studio. Rail, sidebar, tab bar, buttons, a
   dark background poured over an entire ink cartridge, and the
   writing itself cropped to whatever happened to be on screen,
   because .app is 100vh with overflow hidden and .main scrolls
   inside it. Somebody who pressed it to check a page count got a
   photograph of a website.

   A script has a better answer than paper and takes the keystroke
   for itself (see setPrintHandler in web/app.js): it hands back the
   same paginated PDF the export writes. Everything else — a
   character, a world article, a beat sheet, an outline — genuinely
   wants paper, and this is what it gets.

   Three jobs, in this order:
     1. Take the app off the page and leave the writing.
     2. Black on white, whatever the room is wearing. Four of the
        seven themes are dark and one is nearly black; printing one
        of those is unreadable and expensive at the same time.
     3. Give the writing the whole sheet, and do not break a card,
        a beat or a heading across two of them.

   The colours are done by moving the tokens rather than by painting
   the components, because the whole interface is built on the
   tokens: change --text once and 400 rules follow. That is most of
   the way and not all of it, so a backstop over the working area
   follows it — the reasoning is written where it sits.

   !important throughout, for one reason: a theme the writer built
   from a photograph is applied as inline custom properties on
   <html>, and several components carry inline colours of their own.
   Nothing without it can outrank an inline style.
   ============================================================ */

@media print {

  /* ---- the sheet ------------------------------------------------------- */

  @page {
    /* Room for a thumb down the sides, and enough at the head and foot that
       the browser's own header and footer do not sit on the words. */
    margin: 18mm 16mm;
  }

  /* ---- the ink --------------------------------------------------------- */

  html {
    /* The ladder, flattened. Every surface is the paper; the steps between
       them were light, and paper has no light in it. */
    --bg:          #ffffff !important;
    --bg-deep:     #ffffff !important;
    --surface:     #ffffff !important;
    --raised:      #ffffff !important;
    --raised-2:    #ffffff !important;
    --page-bg:     #ffffff !important;

    /* The words. The greys are kept as distinct rungs rather than all set to
       black, because they are read by borders, marks and rules as well as by
       text, and none of them is pale enough to fade: 30% grey is about the
       lightest ink that survives toner. Text inside the working area is taken
       all the way to black further down, by the backstop. */
    --text:        #000000 !important;
    --text-2:      #1a1a1a !important;
    --text-3:      #333333 !important;
    --page-ink:    #000000 !important;
    --muted:       #333333 !important;
    --faint:       #4d4d4d !important;

    /* Lines. Light enough to read as a rule, dark enough to exist. */
    --border:      #999999 !important;
    --border-soft: #cccccc !important;

    /*
     * The colours that mean something.
     *
     * Flattened to black rather than kept, because these are read on screen
     * as "this is the highlight" and on paper as "this is a different, paler
     * shade of grey" — the meaning does not survive the trip, and a colour
     * printer would spend ink saying nothing. The tint behind a highlighted
     * chip goes to nothing at all so the chip prints as its words.
     */
    --accent:      #000000 !important;
    --accent-2:    #000000 !important;
    --accent-soft: transparent !important;
    --accent-ink:  #000000 !important;
    --film:        #000000 !important;
    --tv:          #000000 !important;
    --ok:          #000000 !important;
    --danger:      #000000 !important;
    --danger-ink:  #000000 !important;
    --danger-glow: transparent !important;

    /* Depth is a screen idea. On paper it is a grey smudge. */
    --elev-0: none !important;
    --elev-1: none !important;
    --elev-2: none !important;
    --elev-3: none !important;
    --elev-4: none !important;
    --shadow-1: none !important;
    --shadow-2: none !important;
    --shadow-3: none !important;
    --ring-focus: none !important;
    --ring-on: none !important;
    --ring-hair: none !important;
    --ring-punch: none !important;
    --ring-tint: none !important;
    --glow: none !important;
    --glow-lift: none !important;
    --glow-danger: none !important;
    --glow-danger-lift: none !important;
  }

  html,
  body {
    background: #ffffff !important;
    color: #000000 !important;
    /* Both are height:100% on screen. Left alone, the printout is one sheet
       of whatever happened to be in the window. */
    height: auto !important;
    overflow: visible !important;
  }

  /*
   * Gradients, veils and washes.
   *
   * A hero band is a photograph under a dark gradient with white type on it.
   * Browsers drop background images when printing anyway, which leaves the
   * white type on white paper — so the veils have to go with them, and the
   * type has to come back to black. Everything below inherits its colour from
   * the tokens above once its own background is out of the way.
   */
  *,
  *::before,
  *::after {
    background-image: none !important;
    box-shadow: none !important;
    text-shadow: none !important;
    filter: none !important;
    animation: none !important;
    transition: none !important;
  }

  /*
   * The backstop: inside the working area, every word is black and no box has
   * a fill.
   *
   * Moving the tokens gets most of the way, and it is not enough on its own,
   * because roughly thirty rules write `color: #fff` by hand — a logline over
   * a poster, initials on an avatar, the chip for the role a character is
   * playing. Every one of those sits on a photograph or an accent fill, and a
   * printer drops background images: white type on a dropped photograph is a
   * blank rectangle where the writing was. It is not enough to be nearly
   * right here — one invisible line is a line the writer will not know is
   * missing until somebody asks about it.
   *
   * !important, because a theme built from a photograph applies as inline
   * custom properties and several components carry inline colours of their
   * own. Nothing else outranks those.
   */
  .main,
  .main * {
    color: #000000 !important;
    background-color: transparent !important;
  }

  .project-hero,
  .project-hero-veil,
  .project-hero-body,
  .character-hero,
  .character-hero-veil,
  .character-hero-body,
  .entry-hero,
  .entry-hero-veil,
  .entry-hero-body,
  .home-hero,
  .home-hero-body,
  .home-hero-glow,
  .frontispiece,
  .lead-band,
  .world-band,
  .grounds-band,
  .senses-band,
  .stat-band,
  .band-row {
    background: none !important;
    color: #000000 !important;
    min-height: 0 !important;
  }

  /* ---- take the app off the page --------------------------------------- */

  /*
   * Every name below was read out of web/index.html and the views, not
   * guessed. Grouped by what it is rather than where it lives, because the
   * same kind of furniture turns up on six different pages.
   */

  /* The studio itself: the spine, the project list, the weather behind it. */
  #scene,
  .scene,
  .rail,
  .sidebar,
  .sidebar-reopen,
  .sidebar-split,
  .studio-menu,
  .lock-screen,
  .loading {
    display: none !important;
  }

  /* Things that float above everything: dialogs, toasts, tips, pickers. */
  #modal-root,
  .modal-backdrop,
  .toast-root,
  .tip,
  .smart-pop,
  .mention-pop,
  .el-menu,
  .cmd-sheet,
  .ending {
    display: none !important;
  }

  /* Toolbars and the strips of controls that live in them. */
  .topbar-actions,
  .editor-bar,
  .find-bar,
  .write-status,
  .import-footer,
  .home-controls,
  .home-sort,
  .beat-tools,
  .character-hero-tools,
  .entry-hero-tools,
  .search-trigger,
  .help-strip,
  .size-control {
    display: none !important;
  }

  /* Tab bars. Only the open tab's contents are ever in the document, so the
     bar is a control with nothing left to control. */
  .tabs,
  .lane-tabs,
  .rail-tabs,
  .tab,
  .tab-slider {
    display: none !important;
  }

  /* Side rails that navigate rather than say anything: the scene list, the
     desk, the outline jump list, the slide thumbnails, the companion page. */
  .script-rail,
  .desk-rail,
  .outline-rail,
  .slide-rail,
  .companion-pane,
  .import-side {
    display: none !important;
  }

  /*
   * A character's rail goes too, and not by my judgement: the stylesheet
   * already drops it under 980px, saying "the header already shows who they
   * are". A sheet of paper is narrower than that. Keeping it also cost a
   * whole page of portrait — the face is width:100% of its card, and with the
   * column collapsed the card was the full width of the paper.
   */
  .dossier-rail {
    display: none !important;
  }

  /* Controls. Buttons are not writing.

     Named rather than blanket `button`, deliberately: several surfaces carry
     real content inside a <button> — a fact chip on a world article, a
     colour, a photograph in a strip — and hiding every button would quietly
     delete them from the printout. */
  .btn,
  .icon-btn,
  .rail-btn,
  .fmt-btn,
  .el-pick,
  .seg,
  .seg-toggle,
  .row-actions,
  .shelf-arrow,
  .tool-tiles,
  .tool-tile,
  .tool-mini,
  .shot.add,
  .ref-empty,
  .emoji-pick,
  .colour-pick,
  .dropzone,
  .export-grid,
  .add,
  input[type="search"],
  .progress-bar {
    display: none !important;
  }

  /*
   * The sprite marks stay, which is the opposite of what I first wrote.
   *
   * `.ic` looked like pure decoration, so it was hidden — and then counting
   * what actually survives the rules above showed the mistake: almost every
   * icon in the studio lives inside a button, a tab or a toolbar and has
   * already gone with its parent. What is left is the ones that carry the
   * meaning. The tick against a finished step on "Where things stand" is the
   * only thing distinguishing it from an unfinished one; the four glyphs on a
   * world entry ARE sight, sound, smell and underfoot; and the help page sets
   * a hundred of them inside its sentences, naming the button to press.
   *
   * They inherit black from the backstop above, so they print as line art.
   */

  /* ---- give the writing the paper -------------------------------------- */

  .app {
    display: block !important;
    height: auto !important;
    overflow: visible !important;
  }

  .main {
    display: block !important;
    height: auto !important;
    max-height: none !important;
    overflow: visible !important;
  }

  .view {
    /* The sheet already has margins; the screen's gutters would be a second
       set inside them. */
    padding: 0 !important;
    max-width: none !important;
    margin: 0 !important;
  }

  /*
   * Two columns become one.
   *
   * The rails that were only navigation are gone; these are the splits whose
   * second column is genuinely something to read — a character's presence, a
   * place's facts — so it drops underneath the main text rather than being
   * squeezed into a third of a sheet.
   */
  .dossier-layout,
  .dossier-grid,
  .article-grid,
  .editor-layout,
  .prose-cols,
  .spec-sheet,
  .two-col,
  .split,
  .script-layout,
  .approach-pair,
  .closing-pair,
  .deck-stage {
    display: block !important;
  }

  .article-main,
  .article-side,
  .dossier-main,
  .script-wrap,
  .paper-wrap {
    width: auto !important;
    max-width: none !important;
    padding: 0 !important;
    margin: 0 0 12pt !important;
  }

  /* Nothing sticks to a sheet of paper. */
  .topbar,
  .crumbs,
  .paper,
  .beat-page,
  .world-page,
  .bible-page,
  .sheet-layer {
    position: static !important;
  }

  /* The breadcrumb trail stays: on paper it is the only thing saying which
     project this page came out of. It just stops looking like a toolbar. */
  .topbar {
    background: none !important;
    border: 0 !important;
    padding: 0 !important;
    margin: 0 0 8pt !important;
  }

  /*
   * Fields stop looking like fields, and keep their words.
   *
   * Flattened rather than hidden, and that distinction is the whole reason
   * this list is separate from the one above. A character's status, a world
   * entry's kind, a document's type: all of them are a fact the writer
   * entered, and all of them live in a <select> or an <input>. Hiding the
   * control would print the label with nothing after it, which is a worse lie
   * than printing a box. The box goes; the answer stays.
   */
  .rich-edit,
  .rich-read,
  .inline-edit,
  .input,
  .select,
  .el-picker,
  select,
  /* A tick box is left exactly as it is: on a printed to-do list the tick is
     the whole point, and stripping the control's appearance erases it. */
  input:not([type="checkbox"]):not([type="radio"]),
  textarea,
  [contenteditable] {
    appearance: none !important;
    background: none !important;
    border: 0 !important;
    outline: 0 !important;
    padding: 0 !important;
    min-height: 0 !important;
    height: auto !important;
    max-height: none !important;
    overflow: visible !important;
    resize: none !important;
    color: #000000 !important;
  }

  /*
   * A prompt is not an answer.
   *
   * Empty fields draw their placeholder as generated content — "What is it
   * like here at this hour" sitting in the box in grey italic. On screen that
   * is an invitation. On paper it is indistinguishable from something the
   * writer wrote, and a producer reading a printed world entry would have no
   * way of telling the two apart. Blank stays blank.
   */
  .inline-edit.placeholder::before,
  .rich-edit:empty::before,
  .companion-beat-body:empty::before,
  .dp [contenteditable]:empty::before,
  [contenteditable]:empty::before {
    content: none !important;
  }

  ::placeholder {
    color: transparent !important;
  }

  /* Anything that scrolls on screen has to unroll on paper, or it prints as
     the first inch of itself with the rest missing and nothing to say so. */
  .panel,
  .card,
  .rows,
  .tasks-list,
  .dossier,
  .article-body,
  .beat-body,
  .stanzas {
    max-height: none !important;
    overflow: visible !important;
  }

  /*
   * Photographs earn their ink; keep them on the sheet.
   *
   * Width only. Forcing `height: auto` as well looks tidier and is wrong: a
   * poster on the shelf and a still in a strip are both `width:100%;
   * height:100%` inside a box of a fixed size, and releasing the height pops
   * them out of it at whatever the file happens to measure.
   */
  img,
  canvas {
    max-width: 100% !important;
  }

  /* ---- do not break it in the wrong place ------------------------------ */

  /*
   * A card cut across a page fold is a card you have to hold up to the light
   * to read. These are the app's units of one thought each.
   */
  .card,
  .panel,
  .row,
  .beat,
  .stanza,
  .spec-row,
  .dossier-card,
  .dossier-field,
  .ov-card,
  .shelf-item,
  .character-line,
  .entry-line,
  .cast-credit,
  .credit-item,
  .step,
  .faq,
  .sense,
  .shot,
  figure,
  blockquote,
  tr {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  /*
   * And never leave a heading stranded at the foot of a sheet with its
   * paragraph over the fold. `break-after` is the modern spelling and
   * `page-break-after` is what older engines answer to; both are cheap.
   */
  h1, h2, h3, h4, h5, h6,
  .page-title,
  .page-head,
  .section-head,
  .card-title,
  .beat-heading,
  .beat-head,
  .character-name,
  .entry-name,
  .home-title,
  .paper-title,
  .dossier-label,
  .stanza-label,
  .eyebrow {
    break-after: avoid;
    page-break-after: avoid;
    break-inside: avoid;
    page-break-inside: avoid;
    color: #000000 !important;
  }

  /* A single line of a paragraph alone at the top or bottom of a sheet reads
     as a mistake. Three is the printer's usual answer. */
  p, li, .rich-read, .rich-edit {
    orphans: 3;
    widows: 3;
  }
}

/* ---- from the progress rebuild ---- */
/* ============================================================
   HOW FAR ALONG.

   The screen that stands where the living world used to. A spine per script:
   how long it is, where the scenes fall, and where the writer pencilled their
   beats in.

   Drawn flat, because riso is the default room and a progress bar is exactly
   the sort of control that usually arrives wearing a gradient and a glow. Ink
   on paper: a filled block for what is written, hairlines between the scenes,
   a hard tick for each beat.

   No rounded ends either. A rounded bar reads as a toy; a square one reads as
   a measurement, which is what this is.
   ============================================================ */

.prog-script + .prog-script {
  margin-top: var(--sp-8);
  padding-top: var(--sp-7);
  border-top: 1px solid var(--border-soft);
}

.prog-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-5);
  flex-wrap: wrap;
  margin-bottom: var(--sp-4);
}
.prog-head > b { font-size: var(--fz-subhead); }

.prog-facts {
  display: flex;
  gap: var(--sp-5);
  flex-wrap: wrap;
  font-size: var(--fz-small);
  color: var(--muted);
}
.prog-facts b { color: var(--text); font-weight: 600; }

/* ---- the spine ---------------------------------------------------------- */

/*
 * The bar is the WHOLE run - the script, or the plan if the plan is longer.
 * A script that has overrun its target is drawn overrunning it rather than
 * rescaled to look finished, because the second is a lie a writer would act
 * on.
 */
.prog-bar {
  position: relative;
  height: 34px;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--r-xs);
  overflow: hidden;
}

/* What is actually written. One flat ink. */
.prog-written {
  position: absolute;
  inset: 0 auto 0 0;
  background: color-mix(in srgb, var(--accent) 22%, var(--surface));
  border-right: 2px solid var(--accent);
}

/*
 * One hairline per scene, laid over the written part.
 *
 * Deliberately not a block per scene in alternating shades: at 90 scenes that
 * is a barcode, and the eye reads a barcode as noise. A rule at each scene
 * boundary gives the same information - how long the scenes are, where they
 * bunch up - and disappears when there are only four of them.
 */
.prog-scene {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: color-mix(in srgb, var(--accent) 45%, transparent);
  pointer-events: none;
}
/* The first boundary is the start of the bar, which is already an edge. */
.prog-scene:first-of-type { background: none; }

/*
 * A beat the writer planned, at the page they pencilled it in for. Full height
 * and in the second ink, so it reads as a mark ON the script rather than as
 * part of it. Hollow until something is written in that beat, filled after -
 * the same distinction the pips below use.
 */
.prog-beat {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--accent-2);
  opacity: .45;
}
.prog-beat.on { opacity: 1; }

.prog-scale {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-top: var(--sp-3);
  font-size: var(--fz-micro);
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--faint);
}
.prog-target { color: var(--accent-2); }

/* ---- the beats, as a count ---------------------------------------------- */

.prog-beats { display: flex; flex-direction: column; gap: var(--sp-5); }

.prog-sheet {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.prog-sheet > b { font-size: var(--fz-ui); }

.prog-pips { display: flex; gap: 3px; flex-wrap: wrap; }

/*
 * One pip per beat: hollow if nothing is written in it, filled if something
 * is. Square, for the same reason the bar is - and small enough that forty of
 * them still read as one row rather than as a chart.
 */
.prog-pip {
  width: 10px;
  height: 10px;
  border: 1px solid var(--accent);
  border-radius: 1px;
  background: none;
}
.prog-pip.on { background: var(--accent); }

@media (prefers-reduced-motion: no-preference) {
  .prog-written { transition: width var(--dur-2) var(--ease); }
}

/* ---- what the ink means -------------------------------------------------- */

/*
 * A mark nobody can name is a smudge. This is the smallest thing that stops
 * the coral ticks reading as damage, and it only lists the marks that are
 * actually on the bar - a key to something absent is worse than no key.
 */
.prog-key {
  display: flex;
  gap: var(--sp-6);
  flex-wrap: wrap;
  margin-top: var(--sp-3);
  font-size: var(--fz-micro);
  color: var(--faint);
}
.prog-key span { display: inline-flex; align-items: center; gap: var(--sp-2); }

.prog-key-scene,
.prog-key-beat {
  width: 2px;
  height: 11px;
  flex: none;
  background: color-mix(in srgb, var(--accent) 45%, transparent);
}
.prog-key-beat { background: var(--accent-2); opacity: .45; }
.prog-key-beat.on { opacity: 1; }

/* ---- from the script-laptop rebuild ---- */
/* ============================================================
   The writing room on a laptop.

   The page is the product. At 1280x720 it was not behaving like
   it: the editor gets 992px once the studio sidebar has had its
   share, two docked rails took 434 of them, and the paper came
   out 518px wide. A US Letter page is 816px. The writer spent
   their working life on a page rendered at 63% of itself, with
   more screen given to its furniture (666px) than to the words.

   Two labels were also being sliced in half, "Todo" and "Read",
   because six words do not fit across a 222px rail at any type
   size and there was nothing else the tabs could be drawn as.

   Nothing here touches the paper. The margins, the 60 and 35
   column measures and the type on the page were measured against
   Final Draft and match it to the column. This is the furniture.

   OVERRIDES, in web/styles.css, all of them still needed for the
   wide screen and none of them right below it:
     .script-layout            the three docked columns
     @media (max-width: 1520px)  narrower docked rails
     @media (max-width: 1240px)  the scene rail vanishes
     @media (max-width: 1000px)  the desk vanishes too
     .rail-tabs / .rail-tab      a row of words that could slice
     .scene-row                  one line, cut wherever it ran out

   This has to merge LAST, after script.css, because every one of
   those lives earlier in the file and several tie on specificity.
   merge-css.py sorts unlisted files after the named ones by name,
   which puts it there already; if the ORDER list ever grows, put
   this at the end of it.

   BEFORE RUNNING merge-css.py: `python stage-laptop.py off`. This
   block is currently appended to styles.css by hand, behind its own
   markers, because a full merge could not be run at the time — an
   unmerged emoji.css was sitting in staging and a block of
   hand-written rules after the END marker would have been lifted
   above the merged block and quietly reordered. The merge will pick
   this file up on its own; taking the hand-applied copy out first
   is what stops it landing twice.
   ============================================================ */

:root {
  /*
   * TWO NEW TOKENS, and the only two numbers this file invents.
   *
   * Neither belongs on the spacing scale and neither should: --sp-10
   * is 44px and the scale is for the gaps between things, not for the
   * width of a column of controls or of the panel it opens. They are
   * named because four rules need to agree about them: the grid
   * column, the aside, the button inside it, and the panel that
   * floats beside it. The button size is derived rather than stated,
   * so the icon stays centred if the rail is ever made wider.
   *
   * The bar is 46 rather than 44 because a 34px target with --sp-3
   * either side of it is the smallest column that does not feel
   * pinched against the edge of the window.
   */
  --rail-icon-w: 46px;
  --rail-panel-w: 300px;
}

/* ============================================================
   1. Tabs that cannot be sliced, at any width or type size.
   ============================================================ */

/*
 * The row wraps rather than overflowing.
 *
 * `flex: 1` with no wrap is what cut "Todo" and "Read" in half: six
 * buttons that will not shrink below their words, in a box narrower
 * than their words, in a rail that clips. Wrapping is the honest
 * answer, because it is the only one that still holds when a reader
 * turns the text size up and the labels grow again.
 */
.rail-tabs {
  flex-wrap: wrap;
  gap: var(--sp-2) 0;
  margin-bottom: var(--sp-7);
}
/*
 * `flex: 1` shared the row out evenly, which is right for one row and wrong
 * for two: it stretched the three tabs that wrapped onto the second line
 * across the whole rail, so a tidy strip became a scatter. They sit at their
 * own width now, packed from the left, and the second row lines up under the
 * first.
 */
.rail-tab {
  flex: 0 0 auto;
  min-width: max-content;              /* a label is never narrower than itself */
  position: relative;
  display: flex; align-items: center; justify-content: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-xs) var(--r-xs) 0 0;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.rail-tab:hover { color: var(--text-2); }
.rail-tab:focus-visible { outline: none; box-shadow: var(--ring-focus); }

/* Wide screens read the words; the icon is for the narrow rail below. */
.rail-tab .ic { display: none; }

/* ============================================================
   2. Below 1664px, the rails are their icons.

   The number is arithmetic, not taste, and it is the sum of what
   the page needs before anything else may have any:

     856  a true 8.5in page (816) inside its wrapper (2 x 20)
     490  two docked rails, at the widths they already have
     318  the studio's own spine (56) and sidebar (262)
     ---
    1664

   At 1664 and above, docking both rails is free: the page is a
   true 8.5 inches either way, so it keeps its company. Below, the
   page starts paying for them, and by 1280 it is down to 518px,
   63% of itself, on the commonest screen there is.

   So below the line the rails become columns of icons, 46px each
   instead of 490 together, and the panel one opens floats over the
   page rather than pushing it. The paper is a true 8.5in from 1280
   upwards, whatever is open beside it, and it never moves when
   something opens.

   Nothing is taken away. Every panel that was docked is one press
   from the same edge it always lived on.
   ============================================================ */

@media (max-width: 1664px) {
  .script-layout {
    grid-template-columns: var(--rail-icon-w) minmax(0, 1fr) var(--rail-icon-w);
  }

  /*
   * The asides, restated whole.
   *
   * Three older rules describe these at this width: one hides them,
   * one brings them back as a 280px drawer over the page, and the
   * generic outline rail gives them a scroll box that would clip the
   * floating panel to 46px. Restating rather than patching means the
   * geometry is readable in one place instead of assembled out of
   * four.
   *
   * The three modes that exist to take the room away are named out
   * of it rather than outranked. This has to be the selector and not
   * the cascade: restating an aside needs three classes of weight,
   * which is exactly what `.script-shell.room-off .desk-rail` has,
   * and being written later would have won it. The bare page came
   * back with a 946px desk down one side of it.
   */
  .script-shell:not(.room-off):not(:fullscreen) .script-layout:not(.beside) .desk-rail,
  .script-shell:not(.room-off):not(:fullscreen) .script-layout:not(.beside) .script-rail {
    display: block;
    /* Measured, not 46. See the note on `.desk-rail`. This block exists TWICE
       under the same media query, so both copies carry the change - editing one
       of them alone does nothing, because the later one wins. */
    position: sticky; top: var(--stack-h, 46px);
    inset-inline: auto; bottom: auto;
    width: auto; max-height: none;
    overflow: visible;                 /* or the floating panel is clipped away */
    background: none; border: 0; box-shadow: none;
    padding: var(--sp-8) var(--sp-3) var(--sp-10);
    z-index: var(--z-base);
  }
  .script-shell .script-layout .desk-rail { grid-column: 1; }
  .script-shell .script-layout .script-rail { grid-column: 3; }

  /* The tab strip is now a column of doors down the edge of the room. */
  .script-layout .rail-tabs {
    flex-direction: column; flex-wrap: nowrap;
    gap: var(--sp-2); margin: 0;
  }
  .script-layout .rail-tab {
    flex: 0 0 auto; min-width: 0;
    width: calc(var(--rail-icon-w) - var(--sp-3) * 2);
    height: calc(var(--rail-icon-w) - var(--sp-3) * 2);
    padding: 0;
    border-bottom: 0; border-radius: var(--r-md);
    color: var(--text-3);
  }
  /*
   * The sprite is sized off the BUTTON, not off the text.
   *
   * The button above is `--rail-icon-w` minus its padding - a layout
   * constant, because it is what keeps the paper a true 8.5in at 1280, so it
   * does not answer to the text-size control. `--icon-lg` does. At 200% that
   * put a 36px sprite in a 34px box and the icon hung out of its own button.
   *
   * `min()` lets it grow with the words up to the point where the box runs
   * out, and no further. Unchanged at every size that already fitted.
   */
  .script-layout .rail-tab .ic {
    display: block;
    width: min(var(--icon-lg), calc(var(--rail-icon-w) - var(--sp-3) * 2 - 6px));
    height: min(var(--icon-lg), calc(var(--rail-icon-w) - var(--sp-3) * 2 - 6px));
  }
  .script-layout .rail-tab:hover { background: var(--surface); color: var(--text); }

  /* The label stays in the button so a screen reader still hears a word
     rather than "button". `data-tip` draws it for everybody else. */
  .script-layout .rail-tab-name {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden; white-space: nowrap;
  }

  /*
   * A lit tab means "this panel is open", so it is only lit when one
   * is. The tab the panel would show is still marked `.on` while the
   * side is shut, and a button glowing at a closed panel is a lie.
   */
  .script-shell.show-desk .desk-rail .rail-tab.on,
  .script-shell.show-rail .script-rail .rail-tab.on {
    background: var(--surface); color: var(--accent);
  }
  /* The accent sits on the edge nearest the page, so it points at what
     it opened rather than floating unattached. */
  .script-shell.show-desk .desk-rail .rail-tab.on::after,
  .script-shell.show-rail .script-rail .rail-tab.on::after {
    content: ''; position: absolute;
    top: var(--sp-3); bottom: var(--sp-3); width: 2px;
    border-radius: var(--r-pill); background: var(--accent);
  }
  .script-shell.show-desk .desk-rail .rail-tab.on::after { right: 0; }
  .script-shell.show-rail .script-rail .rail-tab.on::after { left: 0; }

  /* ---- the panel that floats ------------------------------------------ */

  /* Shut is the resting state. The page is what the editor should be
     showing anybody who has not asked for anything else. */
  .script-layout .desk-rail > [data-desk-panel],
  .script-layout .script-rail > [data-panel] { display: none; }

  .script-shell.show-desk .desk-rail > [data-desk-panel]:not([hidden]),
  .script-shell.show-rail .script-rail > [data-panel]:not([hidden]) {
    display: block;
    position: absolute; top: 0;
    width: var(--rail-panel-w);
    max-height: calc(100vh - 46px - var(--sp-10));
    overflow-y: auto; overscroll-behavior: contain;
    z-index: var(--z-drawer);
    background: var(--bg);
    border: 1px solid var(--border-soft);
    box-shadow: var(--elev-3);
    padding: var(--sp-7) var(--sp-7) var(--sp-9);
  }
  /* It grows out of the rail it belongs to: square against that edge,
     rounded on the three it floats over. */
  .script-shell.show-desk .desk-rail > [data-desk-panel]:not([hidden]) {
    inset-inline-start: 100%;
    border-radius: 0 var(--r-lg) var(--r-lg) 0;
  }
  .script-shell.show-rail .script-rail > [data-panel]:not([hidden]) {
    inset-inline-end: 100%;
    border-radius: var(--r-lg) 0 0 var(--r-lg);
  }

  /* Docked, the tab strip says which panel you are looking at. A column
     of icons does not, so the panel says it itself. */
  .script-shell.show-desk .desk-rail > [data-desk-panel]:not([hidden])::before,
  .script-shell.show-rail .script-rail > [data-panel]:not([hidden])::before {
    content: attr(data-name);
    display: block; margin-bottom: var(--sp-7);
    font-size: var(--fz-micro); font-weight: 600;
    text-transform: uppercase; letter-spacing: var(--track);
    color: var(--faint);
  }

  /*
   * Frosted rather than opaque on glass, where a solid slab over the
   * paper would be the one panel in the app that is not made of light.
   */
  html[data-theme="glass"] .script-shell.show-desk .desk-rail > [data-desk-panel]:not([hidden]),
  html[data-theme="glass"] .script-shell.show-rail .script-rail > [data-panel]:not([hidden]) {
    background: rgb(255 255 255 / .82);
    backdrop-filter: blur(22px) saturate(1.7);
    -webkit-backdrop-filter: blur(22px) saturate(1.7);
    border: 1px solid rgb(255 255 255 / .7);
  }

  /*
   * The two bar buttons that used to fold the rails away have nothing
   * left to do: the rails no longer fold away, and their icons are on
   * screen at all times. Two dead controls out of the writer's eyeline.
   */
  .script-bar .rail-toggle { display: none; }
}

/* ============================================================
   3. A scene list you can navigate by.

   Fourteen rows reading "EXT. ROOKSFER..." is not a scene list.
   The first fourteen characters of a slugline are the least
   distinguishing part of it: every scene in a script starts with
   the same three or four words. What tells one from another is
   where it happens, so the place gets the width, and the INT/EXT
   and the time of day go underneath, where they separate the
   three scenes set on the same quay without competing for room.
   The ellipsis was never the defect. Identical rows were.
   ============================================================ */

.scene-row {
  grid-template-columns: 22px minmax(0, 1fr) auto;
  gap: 0 var(--sp-4);
  align-items: baseline;
  padding: var(--sp-3) var(--sp-4);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.scene-n { grid-column: 1; grid-row: 1; }

/*
 * The place gets the whole width, including the column the page number sits
 * in, because the page number only occupies the line underneath. And it wraps
 * to a second line rather than truncating: "THE VANE HOUSE, KITCHEN" and "THE
 * VANE HOUSE, HER ROOM" differ in their last word, so cutting them at the
 * rail's width is the identical-rows fault all over again in miniature. Two
 * lines is the ceiling; past that a slugline is prose, not a label.
 */
.scene-h {
  grid-column: 2 / -1; grid-row: 1;
  font-weight: 500;
  white-space: normal; text-overflow: clip;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}

/*
 * Where and when, under the where. Micro and tracked, so it reads as a
 * caption to the place rather than a second thing to scan.
 *
 * It wraps rather than truncating, for the same reason the place does and
 * for one more: a reader who turns the text size up to 130% is asking for
 * taller rows, and "INT · CONTINUOUS" at that size does not fit a 240px
 * docked rail on any screen. Cutting it there would answer a request for
 * bigger words by showing fewer of them.
 */
.scene-when {
  grid-column: 2; grid-row: 2;
  font-size: var(--fz-micro); color: var(--faint);
  letter-spacing: var(--track-label); text-transform: uppercase;
  white-space: normal;
}
.scene-p { grid-column: 3; grid-row: 2; }

/* The scene the caret is standing in. The list is redrawn five times a
   second while somebody types, so this is the one thing on it that has
   to be findable without reading. */
.scene-row.on { background: var(--surface); color: var(--text); }
.scene-row.on .scene-n { color: var(--accent); }

/* ---- from the shelf rebuild ---- */
/* ============================================================
   THE SHELF HEAD AND THE TAB DECK
   Two measured height faults, both of them a box that was sized
   by whatever happened to stand in it rather than by the design.

   Territory: web/views/dashboard.js (the home shelves) and the
   `.tab-deck` block already in web/styles.css (12 - THE TAB
   DECK, line 9267 onwards). Nothing here changes the visual
   language: the two-row deck stays two rows, the tracked caps
   stay tracked, the seven themes keep their own air.

   Every number below is on the app's existing ladders, and
   every override quotes the line it is overriding.

   Proof: scratchpad/shelf-test.py measures all three shelf
   heads and each shelf's first poster at 1280 / 1440 / 1920,
   and the deck across all seven themes at the same widths.
   It fails without this file and passes with it.
   ============================================================ */


/* ==========================================================================
   1 - THE SHELF HEAD: a header is as tall as the design says, not as tall
       as the button that happens to be in it
   ========================================================================== */

/*
 * Measured on one screen: `.home-row-head` stood 19px on Franchises, 31px on
 * Films and 48px on TV series. Three headers, three heights, on the first
 * screen of the app.
 *
 * Two separate causes, both of them the header having no height of its own:
 *
 *   19 vs 31   only the Films and TV heads carry a `+ New` button, and the
 *              button was the tallest thing in a baseline-aligned flex row,
 *              so it set the height. Shelves that do not need the full width
 *              stand side by side and share a band, so the Franchises posters
 *              sat 12px above the Films posters standing right beside them.
 *   31 vs 48   a shelf with one or two titles is one or two covers wide, and
 *              in that narrow column "TV series" broke onto a second line
 *              (19px of heading became 39px). It recovered at 1920 only
 *              because the covers - and so the column - grew.
 *
 * The fix reserves the height rather than adding an invisible button, and
 * stops the heading breaking. dashboard.js now builds all three heads from
 * one helper, with the title and its count in their own baseline-aligned
 * group, so the group can be centred in a header of a stated height and every
 * head is identical whether or not a button stands in it.
 */
:root {
  /*
   * What a `+ New` button needs, said in the button's own terms so it follows
   * the text-size control rather than fighting it:
   *   styles.css:677  `.btn-sm { font-size: var(--fz-small); padding: 5px 10px }`
   *   styles.css:587  `.btn { ... border: 1px solid var(--border);
   *                           display: inline-flex; align-items: center }`
   * so 5px above, 5px below, a 1px border each side, and inside that the
   * taller of the label and the `+` beside it.
   *
   * The `max()` is not defensive: `--icon` is a flat 15px (styles.css:217) and
   * does not answer to `--fs`, so below about 95% the sprite is the taller of
   * the two and it, not the type, is what the button stands on. Reserving
   * against the type alone left the Franchises head 1.4px short at 70%, which
   * is the same fault this file exists to remove, only smaller.
   *
   * Measured against a real button: 27.0px at 70%, 31.4 at 100%, 37.2 at 130%,
   * 50.8 at 200%.
   *
   * If the button's recipe ever changes, the shelves with a button grow past
   * this and shelf-test.py fails on the assertion that caught it the first time.
   */
  --shelf-head-h: calc(max(var(--icon), var(--fz-small) * 1.55) + 12px);
}

/*
 * Overrides styles.css:4982
 *   `.home-row-head { display: flex; align-items: baseline; gap: var(--sp-6);
 *                     margin-bottom: 12px; }`
 * (and the later `.home-row-head { margin-bottom: var(--sp-5) }` at :12224,
 * which is left alone - only the cross axis is restated here).
 *
 * `align-items: baseline` is what let the button dictate the box: a taller
 * item pushes the shared baseline down and the whole row with it. The header
 * now states its own height and centres what stands in it, so an empty head
 * and a head with a button are the same box. The baseline relationship that
 * actually matters - the heading and its count - moves into `.home-row-title`
 * below, where it is between two pieces of type and nothing else.
 */
.home-row-head {
  align-items: center;
  min-height: var(--shelf-head-h);
}

/* The heading and its count are one thing, sat on one baseline. `flex: none`
   because a heading is not the part of a header that gives: let it shrink and
   the nowrap below stops being a wrap and starts being an overlap. */
.home-row-title {
  display: flex;
  align-items: baseline;
  gap: var(--sp-6);
  flex: none;
}

/*
 * A shelf heading is a label, not prose: it gets one line. Extends
 * styles.css:4985 `.home-row-head h2 { ... }` rather than replacing it.
 */
.home-row-head h2 { white-space: nowrap; }

/*
 * Overrides `min-width: 0` in styles.css:12220 (`.home-work > .home-row`).
 *
 * A shelf's column is sized by the covers on it: `max-width: min(100%,
 * var(--n) * var(--card-w) + ...)`, so a studio with one series gets a column
 * exactly one cover wide - 151px at 1280x720. That is where "TV series" was
 * breaking onto a second line, and it is why it "recovered" at 1920: the
 * covers grew, so the column did.
 *
 * The column was never too narrow for the heading alone. It is too narrow for
 * the heading *and* the `+ New` button: about 171px of header in Dark and
 * 208px in Cold mountains, in 151px of column. `min-width: 0` told the layout
 * to ignore that, so something had to give, and it was the type.
 *
 * `min-content` is the honest statement: a shelf is at least as wide as the
 * header standing on it. It costs nothing on a shelf with three or more
 * covers - those columns are already wider than any header - and it scales by
 * itself with the theme's tracking and with the text-size control, which no
 * written-out number would.
 */
.home-work > .home-row { min-width: min-content; }

/*
 * ...and the covers are not part of that minimum.
 *
 * `.home-shelf` scrolling sideways (styles.css:4991 `overflow-x: auto`) stops
 * it *overflowing*, but a scroller's min-content size is still its contents:
 * asked for `min-content` above, a nine-cover Films shelf answered 1489px at
 * a 1280px window and took the whole home page with it.
 *
 * Size containment in the inline axis is the exact statement wanted - the
 * strip is measured as if it were empty - and it is safe here because the
 * strip is a scroller that already never wants to be wider than what it is
 * given. Both arrows are `position: absolute` (styles.css:6882) so nothing
 * else in it has a width to contribute. The block axis is untouched, so the
 * covers still set the shelf's height.
 */
.home-row .shelf-strip { contain: inline-size; }


/* ==========================================================================
   2 - THE TAB DECK: thirteen tabs that fit at 1280 in all seven themes
   ========================================================================== */

/*
 * The deck is a deliberate two-row wrapping design and stays one. The fault is
 * that it was sized against the tightest theme.
 *
 * `--track` is a per-theme value (themes.js AIR): .09em in Clean, .12em in
 * Dark, .24em in Cold mountains. Measured at 1280x720 on a project page, deck
 * width 916px, the primary row needs 858px in Dark and 971px in mountains, and
 * the secondary row needs 879px in Dark, 915px in Pink Japan - eight tenths of
 * one pixel of slack - 938px in Starry night and 1002px in mountains.
 *
 * So this is not a taller line box: `.tab { white-space: nowrap }` at
 * styles.css:793 means a tab never breaks internally. It is horizontal
 * overflow, and the row wraps onto extra visual lines. 69px of deck in four
 * themes, 97px in two, 134px in mountains, with "THE WORLD 9" and "OUT TO
 * MARKET" each orphaned onto a line of their own. 65px of chrome, on the app's
 * busiest page, at its stated minimum width.
 *
 * The tracking is not frozen: every theme keeps its own value and its place in
 * the order. What changes is how much of it the deck spends, and where the
 * rest goes.
 */

/*
 * The deck spends 72% of the room's tracking on thirteen tabs.
 *
 * Tracking is the one thing here that costs width per character, and thirteen
 * tabs is about 126 characters per screen. Mountains still tracks the widest
 * of the seven (.173em against Dark's .086em and Clean's .065em), in exactly
 * the same proportion; the deck simply cannot afford all of it and say
 * thirteen things at 1280. Scoped to `.tab-deck`, so every other tab strip in
 * the app - `.lane-tabs`, `.prod-tabs`, the plain `.tabs` - is untouched.
 */
.tab-deck {
  --deck-track: calc(var(--track) * .72);
}

/*
 * Overrides the inline half of styles.css:787 `.tab { padding: var(--sp-5)
 * var(--sp-6); ... }`. The block padding is deliberately left alone: it is
 * what makes the deck 69px tall, and that height is not in question.
 *
 * Tracked capitals already carry their air between the letters, so the air
 * around them is doing less work the further the theme tracks - and the
 * trailing side is worse still, because letter-spacing is added after the last
 * character too, so a tracked tab ships one whole letter-space of padding it
 * never asked for. Both come back out of the padding, which means the padding
 * now shrinks in exactly the themes where the type is widest, and the tab's
 * total width barely moves between rooms.
 *
 * `max()` is the floor: at 200% text size the tracking outruns `--sp-6`, and a
 * tab with negative padding would run into its neighbour.
 */
.tab-deck .tab {
  letter-spacing: var(--deck-track);
  padding-inline:
    max(var(--sp-3), calc(var(--sp-6) - var(--deck-track)))
    max(var(--sp-3), calc(var(--sp-6) - var(--deck-track) * 2));
}

/*
 * The row's own label is tracked type in the same row and pays the same way.
 * Overrides the letter-spacing and the right padding of styles.css:9291
 *   `.tab-row-label { flex: none; padding-right: 12px; margin-right: 2px;
 *                     font-size: var(--fz-micro); font-weight: 650;
 *                     letter-spacing: var(--track); ... }`
 * `flex: none` stays: the label must never be the thing that shrinks.
 */
.tab-deck .tab-row-label {
  letter-spacing: var(--deck-track);
  padding-right: max(var(--sp-4), calc(12px - var(--deck-track)));
}

/* ---- from the worldside rebuild ---- */
/* ==========================================================================
   STAGED CSS: the world entry sidebar, and the glass theme's primary button.

   Nothing here has been written into web/styles.css. Every rule below is
   complete and self-contained, and every rule that overrides an existing one
   quotes the line number and the exact text it replaces, so the merge can be
   checked line by line.

   LINE NUMBERS RE-READ AFTER the nav/empty/shelf merge, against a
   web/styles.css of 14,4xx lines in which `.empty` had become `.empty-state`.
   Every rule body quoted below was re-read from the file at that point and is
   verbatim. If the file has moved again since, grep the selector rather than
   trusting the number.

   Two kinds of rule live here:
     REPLACES  - drop the quoted declaration(s) from the original rule and use
                 this instead. Same selector, same specificity.
     ADDS      - a new rule. Place it after the original it sits beside.
   ========================================================================== */


/* ==========================================================================
   1 - THE GLASS THEME'S PRIMARY BUTTON: A RAINBOW WHITE COULD NOT SURVIVE
   ==========================================================================

   REPLACES web/styles.css:5885-5895

     5885  html[data-theme="glass"] .btn-primary {
     5886    background: linear-gradient(100deg,
     5887        #2f5fff 0%, #9b50ff 22%, #ff3ca0 44%,
     5888        #ff9d3c 62%, #00c8ff 82%, #2f5fff 100%);
     5889    background-size: 300% 100%;
     5890    background-position: 0% 50%;
     5891    border-color: transparent;
     5892    color: #fff;
     5893    animation: sc-rainbow 22s linear infinite;
     5894    box-shadow: 0 6px 20px -8px rgb(47 95 255 / .5);
     5895  }

   Only the six colours change. The size, the position, the border, the ink,
   the animation and the shadow are restated exactly as they stand, and
   `@keyframes sc-rainbow`, the `:hover` speed-up and both no-motion guards
   (5896 onward) are untouched.

   The earlier contrast pass measured --accent-ink on --accent and recorded
   glass as 5.00:1, passing. This button never touches --accent. White on the
   six declared stops measured:

     stop    0%    22%    44%    62%    82%    100%
     ratio   5.00  4.21   3.28   2.07   1.96   5.00

   and because `background-size: 300%` slides a third of the gradient across
   the plate at a time, the pixels actually painted under the label were worse
   still. Sampled from the screen at 89 phases of the 22s cycle, the worst
   colour under the letters of "+ New project" was #01c8ff at 1.96:1, and the
   BEST phase in the whole cycle only reached 4.28:1. At 13px/550 the bar is
   4.5:1, so this control never passed, at any moment, in either half.

   The fix keeps the rainbow and levels it. Every stop is pulled down to the
   luminance the theme's own blue already had, holding its hue and its
   saturation, so the sweep is the same six colours in the same order and the
   button simply stops flashing between a highlighter and an ink.

   Why levelling the stops is enough to prove the whole sweep: relative
   luminance is a convex function of the sRGB channel values, and a
   linear-gradient interpolates in sRGB, so no colour painted between two
   stops can be lighter than the lighter of the two. Cap every stop and the
   whole plate is capped. Each stop below is at or under L = 0.166, which is
   4.86:1 against white, leaving real headroom over the 4.5 bar for dither
   and antialiasing. Measured back off the painted pixels afterwards, at 179
   phases of the cycle: worst 4.85:1, over the whole plate and over the
   letters alike.

   Levelling also fixes something nobody filed: the old sweep swung from
   L = .16 to L = .49, so the button visibly pulsed light and dark once every
   22 seconds. It now changes hue only, which is what "flow through a
   rainbow" was always supposed to mean.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   THE MUD BETWEEN THE COLOURS.

   Logan, on this theme: "when I hover over the buttons they mesh the colors
   into an ugly mix and its jarring when I wanted it to all feel smooth and
   apple like on the clean theme."

   Photographed, the button at rest was olive-green sliding into blue, and under
   the pointer it was magenta sliding through burnt orange into olive. Neither
   colour is in the declared list. They are what a linear-gradient PAINTS
   BETWEEN two distant hues: sRGB interpolation runs a straight line through the
   colour cube, and the line from magenta #dd0072 to burnt orange #c04a00 to
   teal #0b7a99 passes through the middle of it, which is grey. Sampled along
   the whole sweep, the least saturated colour painted was 14% - a wet
   cardboard the palette never asked for and nobody could have predicted from
   reading the six stops.

   The levelling pass above made this worse without knowing: pulling six wildly
   different hues to one luminance is exactly what puts their midpoints near the
   neutral axis.

   So the sweep stays inside one family. Blue to indigo to violet to orchid and
   back, which is the arc Apple's own gradients live in - and because every stop
   is in that arc, so is every colour between them. Measured over the whole
   painted plate at 200 samples a segment: least saturated point 65%, and the
   worst contrast against the white label is 4.93:1, which is BETTER than the
   4.86:1 the six-hue version managed. Nothing was traded for this.

   The luminance proof from the levelling pass still stands unchanged: relative
   luminance is convex in the sRGB channels and the gradient interpolates in
   sRGB, so no colour between two stops is lighter than the lighter stop. Every
   stop below sits at L = 0.161 to 0.163, or 4.94 to 5.00 against white.
   -------------------------------------------------------------------------- */
html[data-theme="glass"] .btn-primary {
  background: linear-gradient(100deg,
      #2f5fff 0%, #6456f8 17%, #864ceb 34%, #a33bdf 50%,
      #864ceb 66%, #6456f8 83%, #2f5fff 100%);
  background-size: 300% 100%;
  background-position: 0% 50%;
  border-color: transparent;
  color: #fff;
  animation: sc-rainbow 22s linear infinite;
  box-shadow: 0 6px 20px -8px rgb(47 95 255 / .5);
}


/* ==========================================================================
   2 - BUTTONS THAT WERE SPEAKING ARIAL
   ==========================================================================

   A button element does not inherit the page's font; it takes the platform
   UI face unless something says otherwise, and this stylesheet has no blanket
   `button { font: inherit }`. Every button class in the app declares it
   individually, and four in the world section forgot. Swept live at 1280x720
   and 1440x900 across every route in web/app.js and every tab on the project
   page: 38 buttons paint words in Arial, in four classes, and nothing else
   in the app does.
   -------------------------------------------------------------------------- */

/* ADDS beside web/styles.css:11299-11311 (`.ref-empty`), which declares
   `font-size: calc(13px * var(--fs))` on line 11309 but never the face, so
   "Who leads them?" and "point this at somebody" rendered in Arial next to
   an Inter page. The size is restated on the app's own rung, --fz-ui, which
   is the same calc it already had. */
.ref-empty {
  font: inherit;
  font-size: var(--fz-ui);
}

/* ADDS beside web/styles.css:4780-4783 (`.colour-pick`), which declares
   `font-size: var(--glyph-sm)` on line 4782 and no face. Found by the sweep,
   not by eye: the "no colour" swatch paints a real glyph, the crossed circle,
   and it was being drawn from Arial's ring rather than Inter's. */
.colour-pick {
  font: inherit;
  font-size: var(--glyph-sm);
}

/* REPLACES web/styles.css:11984-11993
 *
 *   11984  .enum-chip {
 *   11985    padding: 5px 11px;
 *   11986    border: 1px solid var(--border);
 *   11987    border-radius: 999px;
 *   11988    background: transparent;
 *   11989    color: var(--text-2);
 *   11990    font-size: calc(12px * var(--fs));
 *   11991    cursor: pointer;
 *   11992    transition: border-color .14s var(--ease), background .14s var(--ease), color .14s var(--ease);
 *   11993  }
 *
 * Four faults in ten lines, all of them the same fault: a number written at
 * the call site instead of asked for by name.
 *   - no face at all, so "Free", "Licensed", "Forbidden" set in Arial;
 *   - a raw 999px where --r-pill says exactly that and means it;
 *   - 12px, which is not a rung: --fz-small is 12.5px and is what a chip of
 *     secondary words is for;
 *   - .14s, one of the five durations inside a 40ms band that --dur-1 was
 *     created to end.
 * Padding and the 1px border are left exactly as they were: those are the
 * chip's shape, and changing them would move every enum row in the world.
 */
.enum-chip {
  padding: 5px 11px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--text-2);
  font: inherit;
  font-size: var(--fz-small);
  cursor: pointer;
  transition:
    border-color var(--dur-1) var(--ease),
    background var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

/* ADDS beside web/styles.css:11590-11602 (`.strength-row`), the row of the
   "How it stands" bar chart on a faction or a realm. It sets no font at all,
   and a <button> resets to the platform face, so every peer's NAME was in
   Arial. `.strength-name` (11605) sets a size but not a family, and a span
   inherits its family from the button above it, which is where the Arial was
   coming from.

   `font: inherit` alone is enough and is safe: `.strength-name` (12.5px),
   `.strength-figure` (12.5px) and `.strength-figure em` (10.5px) each state
   their own size, so nothing here moves except the shape of the letters.

   This one only appears once a writer has answered a number question, which
   is why no sweep of an empty studio has ever seen it. worldside-test.py now
   seeds a faction with its questions filled in. */
.strength-row {
  font: inherit;
}

/* REPLACES web/styles.css:12452-12464
 *
 *   12452  .family-tab {
 *   12453    display: inline-flex;
 *   12454    align-items: center;
 *   12455    gap: 7px;
 *   12456    padding: 7px 13px;
 *   12457    border: 1px solid transparent;
 *   12458    border-radius: 999px;
 *   12459    background: transparent;
 *   12460    color: var(--text-2);
 *   12461    font-size: calc(12.5px * var(--fs));
 *   12462    cursor: pointer;
 *   12463    transition: background .14s var(--ease), color .14s var(--ease), border-color .14s var(--ease);
 *   12464  }
 *
 * The same four faults as `.enum-chip`, in the same words, on the row of
 * tabs across the top of the world list: Everything, Grounds, Powers,
 * Peoples, Forces, Customs, History. Those seven words are bare text nodes
 * inside the button (world.js:1590 and 1593), so all seven were set in
 * Arial, on the screen a writer uses to navigate the whole world section.
 *
 * This one was NOT found by the first sweep, and the reason is worth
 * recording: the sweep was walking routes like `#/project/1/world`, which is
 * not in the route table in web/app.js. The router sends an unknown hash
 * quietly back to `#/`, so ten screens of sweeping were ten sweeps of the
 * home page, all passing. worldside-test.py now asserts that the hash it
 * asked for is the hash it got, and clicks through every tab on a page
 * before it reads it.
 *
 * 12.5px is exactly --fz-small, so the size does not move; only the name for
 * it does.
 */
.family-tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--text-2);
  font: inherit;
  font-size: var(--fz-small);
  cursor: pointer;
  transition:
    background var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease);
}


/* ==========================================================================
   3 - THE WORLD ENTRY SIDEBAR
   ==========================================================================

   `.world-side` is a new class, added in web/views/world.js `sideHtml()` to
   the panel that already carried `panel dossier-top`. It exists because
   `.dossier-top` is also worn by a panel in web/views/character.js, which
   lays its fields out in a grid and must not be touched by any of this.
   -------------------------------------------------------------------------- */

/* ADDS. The rhythm of the column, taken off the five inline `margin-top`
   declarations that used to carry it in world.js (four `margin-top:12px`,
   one `margin-top:14px` that was not on any scale and made the last gap 2px
   deeper than the four above it for no reason anybody could name).
   --sp-6 is 12px, so four of the five gaps are unchanged and the fifth
   joins them. */
.world-side .dossier-field + .dossier-field {
  margin-top: var(--sp-6);
}

/* ADDS. Was `style="padding:0"` inline on the era checkbox's label in
   world.js. The value is right; writing it at the call site was not. */
.world-side .dossier-field > label.tool-row {
  padding: 0;
}

/* OVERRIDES web/styles.css:2104
 *
 *   2104  .dossier-field:not(.empty) .hint { opacity: .5; }
 *
 * That rule is correct where it was written: a fact cell's prompt should
 * fade once the writer has answered it, and `.empty` is what says whether
 * they have. The sidebar's four fields are settings, not questions. They
 * never carry `.empty` and never will, so every hint in the column sat
 * permanently at half strength - which is most of why the two Inter asides
 * looked like the brighter, more important text.
 *
 * It was also unreadable. Measured on the world entry sidebar, hint against
 * the panel it sits on, in all seven themes:
 *
 *   theme       at .5      at 1
 *   dark        2.58:1     5.92:1
 *   light       2.03:1     5.16:1
 *   glass       1.95:1     4.61:1
 *   starry      2.57:1     5.92:1
 *   blossom     2.61:1     5.93:1
 *   forest      2.63:1     5.98:1
 *   mountains   2.63:1     6.00:1
 *
 * At full strength the hint is --faint, which is what the app's other hint
 * component (.field .hint, line 1053) has always used, and which clears
 * 4.5:1 in every theme with very little to spare. There is no room to fade
 * it at all and stay legible.
 *
 * NOTE FOR WHOEVER MERGES THIS: the override is scoped to `.world-side` on
 * purpose, because line 2104 is a deliberate design decision about answered
 * fact cells and those are not this agent's to change. But the numbers above
 * are the numbers for every answered fact cell in the world and character
 * dossiers too. That is a separate, larger finding.
 */
.world-side .dossier-field .hint {
  opacity: 1;
}

/* ADDS. The tenth colour swatch, given a name.
 *
 * Nine 24px circles with --sp-2 between them already overrun the 250px
 * column, so the custom-colour control wrapped onto a line by itself and sat
 * there as an unexplained `+`. It still wraps, because nine circles and a
 * tenth control still do not fit; it now takes that line deliberately, as a
 * dashed pill that says what it opens. The `<input type="color">` is still
 * the whole hit area, laid over the pill at zero opacity, so the browser's
 * own picker opens on a click anywhere in it - the same trick
 * `.swatch-own, .colour-own` (line 4938) uses, kept for the same reason.
 *
 * It stays INSIDE `.emoji-row` rather than becoming a sibling of it, because
 * `.dossier-field > label` (line 2080) is uppercase, tracked and
 * `display: block` - a label that is a direct child of a field is that
 * field's title, and this one is a control. Sitting inside the row it is
 * only a control, and the row's own wrap puts it on its own line for free.
 *
 * The old markup wore `.colour-pick .colour-own`. `.colour-pick` is left
 * exactly as it is, because the nine swatches beside this still wear it.
 * `.colour-own` now has no user at all: world.js was the only one, and
 * line 4938 reads `.swatch-own, .colour-own {`. Dropping the second half of
 * that selector pair (and of line 4944 and line 4947) is a safe tidy for
 * whoever merges this, but it is a deletion in a file this agent does not
 * own, so it is left flagged rather than done.
 */
.colour-any {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-1);
  padding: var(--sp-2) var(--sp-5) var(--sp-2) var(--sp-2);
  border: 1px dashed var(--border);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--text-2);
  font-size: var(--fz-small);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.colour-any:hover {
  border-color: var(--accent);
  color: var(--text);
}
.colour-any:focus-within {
  border-color: var(--accent);
  box-shadow: var(--ring-focus);
}
/* The whole pill is the hit target, and getting that exactly right takes
   both halves of this:
   - `inset: -1px` rather than 0, because an absolutely positioned child is
     laid out against its parent's PADDING box, so `inset: 0` stops a pixel
     short of the dashed rim on all four sides;
   - an explicit width and height, because `input[type="color"]` is a
     replaced element: with `width: auto` Chromium ignores the right and
     bottom insets and paints it at its intrinsic 50x27, which left most of
     the pill dead to the click. Measured: 50x27 inside a 152x29 label.
   `.colour-own` (line 4944) has the same `width: auto` bug; there it does
   not show, because that control is a 24px circle and the swatch is smaller
   than the input. */
.colour-any input {
  position: absolute;
  inset: -1px;
  width: calc(100% + 2px);
  height: calc(100% + 2px);
  opacity: 0;
  border: 0;
  padding: 0;
  cursor: pointer;
}
/* The current colour, shown rather than described. Transparent when nothing
   has been picked, which is exactly what the row of swatches shows too. */
.colour-any-dot {
  flex: none;
  width: 18px;
  height: 18px;
  border: 1px solid var(--border-soft);
  border-radius: var(--r-circle);
}

/* ADDS beside web/styles.css:7555-7557 AND 14344-14346, the two print blocks that already hides
   `.ref-empty`, `.emoji-pick` and `.colour-pick`. A colour picker is not
   something a printed page can offer, so the new control goes with them.
   Cleanest merge is to add `.colour-any,` to that same selector list. */
@media print {
  .colour-any { display: none !important; }
}

/* ---- from the zz-licence rebuild ---- */
/* ============================================================
   THE LICENCE DOOR.

   The screen a copy shows when it has not been unlocked. It borrows
   `.lock-screen` and `.lock-card` from the PIN screen deliberately: the two
   are the same shape of moment, and a second full-screen treatment would be
   a second thing to keep in step with the seven themes.

   What is added here is only what the PIN screen has no use for - a machine
   name meant to be read aloud down a phone or copied into an email, and a
   box big enough to paste 144 characters into without it looking like a
   mistake.

   Named `zz-licence` so it merges after `zz-riso`, since the riso rules for
   `.btn-primary` and `.panel` are the ones this should inherit rather than
   fight.
   ============================================================ */

.licence-screen .lock-card {
  max-width: 460px;
  text-align: left;
}

.licence-screen .lock-lead {
  color: var(--text-2);
  font-size: var(--fz-lead);
  margin: 0 0 var(--sp-7);
}

/* ---- the sign-in form, which is now the way in -------------------------- */

/*
 * Two fields and a button, and nothing between them competing for the eye.
 * This is the first thing a paying customer ever sees of the software, and it
 * has one job: take an email and a password.
 */
.licence-signin { margin-bottom: var(--sp-5); }
.licence-field { display: block; margin-bottom: var(--sp-5); }
.licence-field > span {
  display: block;
  margin-bottom: var(--sp-3);
  font-size: var(--fz-micro);
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--faint);
}
.licence-field input { width: 100%; }

.licence-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: baseline;
  margin: var(--sp-5) 0 var(--sp-7);
  font-size: var(--fz-small);
}
.licence-links a { color: var(--text-2); text-decoration: underline; text-underline-offset: 2px; }
.licence-links a:hover { color: var(--text); }
.licence-links span { color: var(--faint); }

/*
 * What comes after a refusal somebody can act on: the resend button for an
 * unconfirmed address, and the list of computers when this would be the
 * fourth. Set apart from the form, because it is an answer to something that
 * just happened rather than another thing to fill in.
 */
.licence-next[hidden] { display: none; }
.licence-aside {
  margin: var(--sp-5) 0 0;
  padding: var(--sp-5);
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
.licence-aside > p {
  margin: 0 0 var(--sp-4);
  font-size: var(--fz-small);
  color: var(--text-2);
  line-height: 1.6;
}
.licence-machine-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--border);
}
.licence-machine-row:first-child { border-top: 0; }
.licence-machine-row b { display: block; font-size: var(--fz-small); }
.licence-machine-row span { display: block; font-size: var(--fz-tiny); }

/*
 * The divider, and what is under it.
 *
 * Three people in the world hold a key minted by hand, and every one of them
 * was told where to find this. It stays on the screen rather than folding away
 * behind a disclosure, because a reader who was sent a key by email should not
 * have to guess that there is something to click, and the two fields above it
 * are visibly the main way in without needing to be the only thing there.
 */
.licence-or {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  margin: var(--sp-7) 0 var(--sp-6);
  font-size: var(--fz-small);
  color: var(--faint);
}
.licence-or::before,
.licence-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/*
 * A card that can now be taller than a laptop screen.
 *
 * Two fields, a divider, a machine name and a paste box is a lot of card, and
 * `.lock-screen` centres its child with no way to reach anything that falls
 * off the top. On a 13 inch laptop that would put the Sign in button below the
 * fold with nothing to scroll, which is not a cosmetic fault: it is a copy
 * nobody can unlock.
 */
.licence-screen { overflow-y: auto; padding: var(--sp-7) var(--sp-5); align-items: flex-start; }
.licence-screen .lock-card { margin: auto; }

/* ---- the read-only strip ------------------------------------------------ */

/*
 * A plan that has run out, said once, at the top, for as long as it is true.
 *
 * It takes a ROW of the app grid rather than floating over anything. `.app` is
 * a three-column grid with no rows declared, so its children sit in one
 * implicit row; adding this one and asking for `auto 1fr` puts the strip above
 * the rail, the sidebar and the studio, all three of which keep the height
 * that is left. Nothing is covered on any page, including the script editor,
 * which is the one surface where an overlay would be unforgivable.
 *
 * The class is only ever added when the strip is, so an ordinary copy - and
 * every browser suite - is laid out exactly as it was.
 */
.app.has-notice {
  grid-template-rows: auto 1fr;
}
.readonly-strip {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-5);
  background: var(--raised);
  border-bottom: 1px solid var(--border);
  font-size: var(--fz-small);
  color: var(--text-2);
}
/*
 * Deliberately not the warning ink. This is information about a plan, not a
 * fault and not a danger, and a red bar across the top of somebody's studio
 * every day for a month reads as an accusation. The icon is the calm one for
 * the same reason.
 */
.readonly-strip .ic {
  flex: none;
  color: var(--faint);
}
.readonly-said {
  flex: 1;
  min-width: 0;
  line-height: 1.5;
}
/* The two controls keep their size on a narrow window; the sentence wraps. */
.readonly-strip .btn { flex: none; }

@media (max-width: 720px) {
  .readonly-strip { flex-wrap: wrap; }
  .readonly-said { flex: 1 0 100%; }
}

/* Nothing about a plan belongs on a printed page. */
@media print {
  .readonly-strip { display: none !important; }
}

/* ---- the machine name -------------------------------------------------- */

/*
 * Set in the mono face and given room, because its whole job is to be
 * transcribed by a person who did not choose to be doing this. Twelve
 * characters, tracked, so a 0 and an O are told apart at a glance.
 */
.licence-machine {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "label label" "code button";
  gap: var(--sp-3) var(--sp-4);
  align-items: center;
  margin-bottom: var(--sp-5);
}
.licence-machine > span {
  grid-area: label;
  font-size: var(--fz-micro);
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--faint);
}
.licence-machine code {
  grid-area: code;
  font-family: var(--font-mono);
  font-size: var(--fz-subhead);
  letter-spacing: .08em;
  padding: var(--sp-4) var(--sp-5);
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  user-select: all;                 /* one click selects the whole thing */
}
.licence-machine [data-copy] { grid-area: button; }

.licence-screen .lock-note {
  font-size: var(--fz-small);
  color: var(--muted);
  line-height: 1.6;
  margin: 0 0 var(--sp-8);
}

/* ---- the key ----------------------------------------------------------- */

.licence-key { display: block; margin-bottom: var(--sp-6); }
.licence-key > span {
  display: block;
  margin-bottom: var(--sp-3);
  font-size: var(--fz-micro);
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--faint);
}
/*
 * Mono and wrapping, because a licence is 144 characters of base64url and a
 * proportional face turns that into a smear the writer cannot check against
 * the email it came from. `anywhere` rather than `break-word`: there are no
 * spaces in it to break at.
 */
.licence-key textarea {
  width: 100%;
  font-family: var(--font-mono);
  font-size: var(--fz-small);
  line-height: 1.5;
  overflow-wrap: anywhere;
  resize: vertical;
}

.licence-screen .lock-actions { display: flex; gap: var(--sp-4); }
.licence-screen .lock-actions .btn { flex: 1; }

/* Empty until there is something to say, so the card does not reserve a gap
   for a message that is usually absent. */
.licence-screen .lock-why {
  margin: var(--sp-5) 0 0;
  font-size: var(--fz-small);
  color: var(--danger);
  min-height: 0;
}
.licence-screen .lock-why:empty { display: none; }

/* ---- from the zz-riso rebuild ---- */
/* ============================================================
   RISO — flattening the things this stylesheet gradients everywhere else.

   Named `zz-riso` so it merges last and wins on source order. The palette
   itself is not here: it lives with the other themes at the top of
   styles.css, outside the merged block. This file is only the structural
   half - the places where the app reaches for a gradient, a glow or a
   translucent wash, none of which a risograph can print.

   A riso lays down flat spot inks, one pass each, slightly out of register.
   There is no such thing as a soft edge or a colour ramp. So every rule
   below replaces a ramp with a single ink, and the ones that were carrying
   meaning (a primary button reading as the important one) keep that meaning
   by other means - weight, ink, and a hard offset shadow.

   Seven gradients were found painting on the home screen alone, measured in
   the browser rather than grepped: `scratchpad/riso-look.py`.
   ============================================================ */

/* ---- the primary button ------------------------------------------------
   styles.css: `.btn-primary` is a gradient in the base sheet and a 6-stop
   animated rainbow in glass. Here it is one ink, and the shadow is an offset
   block rather than a blur - the second pass sitting beside the first. */
html[data-theme="riso"] .btn-primary,
html[data-theme="riso"] .btn-primary:hover {
  background: var(--accent);
  background-image: none;
  animation: none;
  border: 1px solid var(--accent);
  box-shadow: 2px 2px 0 color-mix(in srgb, var(--text) 26%, transparent);
}
html[data-theme="riso"] .btn-primary:hover {
  background: color-mix(in srgb, var(--accent) 88%, var(--text));
}
/* Pressed, the sheet meets the block: the offset closes rather than the
   button dimming, which is what a stamp does. */
html[data-theme="riso"] .btn-primary:active {
  box-shadow: 0 0 0 color-mix(in srgb, var(--text) 26%, transparent);
  transform: translate(2px, 2px);
}

/* ---- gradient-clipped numerals ----------------------------------------
   styles.css `.stat b`, `.stat-grid b` and the page-title treatment clip a
   linear-gradient to the type. Clipped to text, a ramp is invisible at this
   size anyway and only costs legibility, so these become one ink. */
html[data-theme="riso"] .stat b,
html[data-theme="riso"] .stat-grid b,
html[data-theme="riso"] .page-title-ink {
  background: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  -webkit-text-fill-color: currentColor;
  color: var(--accent);
}

/* ---- the home hero ----------------------------------------------------
   Two radial washes of the accent, plus a separate glow element. Riso gets
   a flat panel and a hard rule under it: the hero is still clearly the top
   of the page, but by edge rather than by haze. */
html[data-theme="riso"] .home-hero,
html[data-theme="riso"] .home-hero.no-art {
  background: var(--bg-deep);
  background-image: none;
  border-bottom: 2px solid var(--text);
}
html[data-theme="riso"] .home-hero-glow { display: none; }

/* The forged medal in the hero is the one place a second ink earns its keep:
   flat coral behind the artwork, printed slightly off the corner. */
html[data-theme="riso"] .home-hero-forge {
  opacity: .28;
  filter: none;
}

/* ---- a title with no poster -------------------------------------------
   `.shelf-spine` ramps the project's colour into the surface. One flat ink,
   and the title sits on it in the paper colour rather than in white, which
   is what an overprint looks like. */
html[data-theme="riso"] .shelf-spine {
  background: color-mix(in srgb, var(--pc, var(--accent)) 22%, var(--surface));
  background-image: none;
}

/* ---- the dropdown -----------------------------------------------------
   `.drop-btn` carries a subtle vertical ramp to look recessed. On paper a
   control is a box with a rule round it. */
html[data-theme="riso"] .drop-btn,
html[data-theme="riso"] .select,
html[data-theme="riso"] .input {
  background-image: none;
  background-color: var(--raised);
}

/* ---- edges instead of shadows -----------------------------------------
   The air already makes elevation a hard offset, but a few surfaces draw
   their own soft shadow inline. A panel on paper is a panel with a rule. */
html[data-theme="riso"] .panel,
html[data-theme="riso"] .card,
html[data-theme="riso"] .modal {
  border: 1px solid var(--border);
}

/* ---- the writing surface ----------------------------------------------
   Left alone deliberately. The script page is true white with black ink
   because that is what a screenplay is printed on, and no theme gets to
   have an opinion about it. Stated here so nobody adds one later. */

/* ---- the two that got away --------------------------------------------
   Found by the agent building the account page, and they are on every page
   in the app rather than on one screen: `.panel` (styles.css:1263) washes an
   accent gradient down its first 42px, and `.page-title::after` draws a
   gradient rule under every heading. A riso cannot print either. The panel
   keeps its edge, which is what was doing the work anyway; the title rule
   becomes one flat ink. */
html[data-theme="riso"] .panel {
  background-image: none;
}
html[data-theme="riso"] .page-title::after {
  background: var(--accent-2);
  background-image: none;
}

/* ---- from the zz-toolbar rebuild ---- */
/* ============================================================
   THE NAMED ROW.

   The second bar above the page, and the whole reason it earns its height is
   that it has WORDS on it. A row of bare icons helps somebody who already
   knows the app; a word under each one is what teaches somebody who does not,
   and that was Logan's stated reason for asking - "it helps a wide amount of
   people".

   The arithmetic it has to survive, measured rather than assumed: at 1280 the
   editor column is 992px, so this row has about 952px to work in. Eleven
   buttons at their longest label, ten gaps and three dividers come to roughly
   823px. It must not wrap - a wrapped row costs a whole second row silently,
   and the page loses another four lines without anybody deciding to spend
   them. `zz-toolbar` so it merges after zz-riso and can rely on the riso rules
   already being in place.
   ============================================================ */

/*
 * The two bars pin together.
 *
 * Each `.editor-bar` sets `position: sticky; top: 0` for itself, which is right
 * when there is one of them and wrong the instant there are two - both land on
 * zero and the second disappears under the first. The wrapper takes the pin and
 * the bars inside it go static, so the named row sits at whatever height the
 * top bar happens to be: bigger with the text size up, taller again when the
 * top bar wraps to two lines. No number is written down anywhere, which is the
 * only way it stays true.
 */
.bar-stack {
  position: sticky;
  top: 0;
  z-index: var(--z-rail);
  /*
   * *** THE PAGE MUST NOT SHOW THROUGH THE TOOLBAR. ***
   *
   * `.editor-bar` is `color-mix(var(--bg) 92%, transparent)` with a blur behind
   * it - a good effect over a photograph and the wrong one over a script. What
   * scrolls under this bar is twelve-point Courier on white paper, and at 92%
   * it ghosts through: the writer sees their own words smeared behind the
   * buttons. Logan: "when writing the page goes through the tool bar at the top
   * and looks bad and unfinished, it should just disappear like it does in final
   * draft."
   *
   * Solid, on the stack rather than on either bar, so the named row is covered
   * too - it has no background of its own and was the clearer offender of the
   * two. The blur stays underneath and now does nothing, which costs nothing
   * and leaves the effect in place for anybody who makes this translucent again
   * on purpose.
   */
  background: var(--bg);
}
.bar-stack > .editor-bar { position: static; }

.bar-named {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* Never wrap. If it will not fit, it scrolls - which is visible and
     recoverable, where a silent extra row is neither. */
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  padding: 5px var(--sp-7);
  min-height: 44px;
  border-bottom: 1px solid var(--border-soft);
  background: var(--bg);
}
.bar-named::-webkit-scrollbar { display: none; }

/* The switch in the command menu hides it outright. Off restores exactly the
   screen that was here before this row existed. */
.script-shell.no-named-bar .bar-named { display: none; }

/*
 * Icon over word. The icon is what a returning writer finds; the word is what
 * a new one reads. Both, in 44px, because either alone fails one of them.
 */
.bar-named-btn {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 2px var(--sp-3);
  min-width: 0;
  height: 34px;
  line-height: 1;
  color: var(--text-2);
}
.bar-named-btn .ic {
  width: calc(15px * var(--fs));
  height: calc(15px * var(--fs));
  flex: none;
}
.bar-named-btn span {
  font-size: var(--fz-micro);
  white-space: nowrap;      /* a label that wraps makes the row taller */
  letter-spacing: 0;
  text-transform: none;
}
.bar-named-btn:hover { color: var(--text); background: var(--surface); }

/* On, when the thing it opens is open. The same accent underline the tabs
   use, so "this is showing" means one thing across the app. */
.bar-named-btn.on {
  color: var(--accent);
  box-shadow: inset 0 -2px 0 var(--accent);
}

.bar-named .divider {
  flex: none;
  width: 1px;
  height: 22px;
  background: var(--border-soft);
  margin: 0 var(--sp-2);
}

/*
 * The eleven controls come to 732px at 1280 and 802px with the text turned up
 * a step, against 952px of row, so there is real slack at the floor width and
 * no breakpoint is needed. If a control is ever added here, measure again -
 * `toolbar-test.py` asserts the no-wrap, which is the whole reason the row is
 * affordable.
 */
/* ==== END MERGED ==== */
