/* styleyourai — vaporwave-hacker skin. no external fonts, no deps. */
:root {
  /* The page paints its own colours, but anything the UA draws does not follow
     them: on a light-preference OS the native <select> popup for "export for",
     the colour-picker dialog, autofill and the caret/selection colours all came
     up light against a near-black app. */
  color-scheme: dark;
  --bg: #0a0a12;
  --panel: #11111c;
  --panel-2: #16162440;
  --line: #585892;
  --line-hot: #58588D;
  --ink: #d7d7ea;
  --ink-dim: #8686a8;
  --ink-faint: #7d7da1; /* ~4.7:1 on the panels — the old #5b5b78 (~3:1) failed AA for essential small text */
  --magenta: #ff2e97;
  --cyan: #00e5ff;
  --purple: #9d7bff;
  --green: #38f9a8;
  --amber: #ffcc55;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
  --radius: 10px;
}

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
  background:
    radial-gradient(1200px 600px at 80% -10%, #1a1030 0%, transparent 60%),
    radial-gradient(900px 500px at -10% 110%, #06212b 0%, transparent 55%),
    var(--bg);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 13.5px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
body::before {
  content: "";
  position: fixed; inset: 0;
  background-image:
    linear-gradient(var(--line-hot) 1px, transparent 1px),
    linear-gradient(90deg, var(--line-hot) 1px, transparent 1px);
  background-size: 44px 44px;
  opacity: 0.04;
  pointer-events: none;
  z-index: 0;
}

#app {
  position: relative;
  /* NO z-index here. It made #app a stacking context, which capped every
     z-index inside it (#sidebar 40, .ed-more-panel 75) below any body-level
     surface — so the assistant dock at 70 painted OVER the persona drawer and
     over the "⋯ more" menu, and taps in the drawer hit the dock. Positioned
     and later in tree order than body::before, so it still paints above the
     grid backdrop without one. */
  --sb: 264px;   /* sidebar column: 264 open, 0 collapsed */
  --pv: 38px;    /* preview column: thin rail collapsed, full panel when open */
  display: grid;
  grid-template-columns: var(--sb) minmax(340px, 1fr) var(--pv);
  /* dvh, not vh: on iOS Safari 100vh is the URL-bar-RETRACTED height, so while
     the bar is showing #app is ~60-90px taller than the visible area — and
     since overflow:clip below there is no scroll escape. What sits at the
     bottom of the sidebar is backup/restore and the assistant pill. The vh
     declaration stays first as the fallback for browsers without dvh. */
  height: 100vh;
  height: 100dvh;
  /* `clip`, NOT `hidden`. They paint identically, but `overflow:hidden` makes
     the box a real scroll container — scrollTop is settable and the browser
     treats it as a scroll ANCESTOR. #app carries ~2400px of hidden overflow, so
     anything that scrolls a descendant into view (native Tab focus onto the 1px
     influence checkboxes, scrollIntoView, find-in-page) drags the entire app
     above the fold — measured at scrollTop 968 after 22 Tabs from the name
     field — and with no scrollbar there is nothing to scroll back with. Wheel,
     click and Escape are all inert; only a reload recovers.
     That is the "black screen" that cost the 2026-08-13 demo. preventScroll on
     the two focus() calls patched two callers; `clip` removes the mechanism —
     #app is no longer a scroll container, so no caller can scroll it. */
  overflow: clip;
  transition: grid-template-columns .22s ease;
}
/* No transition until initNav() has applied the first layout — otherwise the
   shell animates itself together on every cold load. See initNav() in app.js. */
#app:not(.nav-ready) { transition: none; }
#app:not(.nav-open) { --sb: 0px; }
#app.preview-open { --pv: clamp(320px, 40vw, 380px); }

/* ── sidebar ─────────────────────────────────────────────── */
#sidebar {
  background: linear-gradient(180deg, #0d0d18, #0a0a12);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  padding: 16px 12px;
  gap: 12px;
  overflow: hidden;
}
.brand { display: flex; align-items: center; gap: 10px; padding: 4px 6px; }
.brand-mark {
  font-size: 22px; color: var(--magenta);
  text-shadow: 0 0 12px var(--magenta);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.1; }
/* An <h1> — the app had no headings at all, and this is its only document-level
   one. font/margin reset so the element change is invisible. */
.brand-name { font-weight: 700; letter-spacing: 1px; color: var(--ink); font-size: inherit; margin: 0; }
.brand-sub { font-size: 10px; color: var(--ink-faint); letter-spacing: 2px; text-transform: uppercase; }

.persona-list { flex: 1; overflow-y: auto; margin: 0 -4px; padding: 0 4px; }
.pl-group { margin-bottom: 6px; }
.pl-item {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 9px 10px; border-radius: var(--radius);
  border: 1px solid transparent;
  cursor: pointer; margin-bottom: 3px;
  transition: background .12s, border-color .12s;
}
.pl-item:hover { background: var(--panel-2); }
.pl-item.active { background: #191926; border-color: var(--line-hot); }
/* --ink-faint is tuned for the panels (~4.7:1); the active row is lighter, and
   the tagline came out at 4.4 against it — the app's one remaining AA miss. */
.pl-item.active .pl-tag { color: var(--ink-dim); }
.pl-item.variant { margin-left: 16px; }
.pl-item.variant::before {
  content: "└"; color: var(--ink-faint); margin-left: -14px; margin-right: 2px;
}
.pl-emoji { font-size: 15px; line-height: 1.3; }
.pl-meta { min-width: 0; flex: 1; }
.pl-name { color: var(--ink); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl-tag { color: var(--ink-faint); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Pure decoration — the persona's accent colour, nothing more. It carries no
   state (visibility is the .pl-vis pill, which has text and a title), so it is
   aria-hidden in personaItem rather than a nameless node in the a11y tree.
   The border is what keeps it visible under forced-colors, where a background
   colour is discarded and the dot went black-on-black. */
.pl-dot {
  width: 7px; height: 7px; border-radius: 50%; margin-top: 6px; flex: none;
  border: 1px solid transparent;
}
/* unsaved-work marker: only on the row being edited, only while it is dirty. */
.pl-dirty { display: none; }
#app.dirty .pl-item.active .pl-dirty {
  display: block; color: var(--magenta); font-size: 10px; line-height: 1.6; flex: none;
}

/* ── buttons ─────────────────────────────────────────────── */
.btn {
  font-family: var(--mono); font-size: 12px;
  background: #16161f; color: var(--ink);
  border: 1px solid var(--line-hot); border-radius: 8px;
  padding: 6px 12px; cursor: pointer;
  /* Standard control height — shared with .accent-field so mixed rows (swatch +
     buttons) sit on one line. Was 34px, which is under the 44px touch minimum
     on the tablet this is demoed on: save, delete, copy and every "⋯ more" row
     were a 34px target with an 8px gap. See .btn-mini (28px + overhang). */
  min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  transition: border-color .12s, background .12s, color .12s, box-shadow .12s;
}
.btn:hover { border-color: var(--purple); }
/* No .btn had ANY focus style. The confirm dialog opens with focus already on
   its danger button, so an armed "delete" was pixel-identical to an idle one
   and the next Space destroyed the persona. :focus-visible, so a mouse click
   doesn't paint a ring. */
.btn:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.btn-danger:focus-visible, .btn-accent:focus-visible { outline-color: #ff9bad; }
.btn:active { transform: translateY(1px); }
.btn-block { width: 100%; }
.btn-ghost { background: transparent; border-style: dashed; color: var(--ink-dim); }
.btn-ghost:hover { color: var(--magenta); border-color: var(--magenta); }
.btn-accent { background: var(--magenta); color: #12000a; border-color: var(--magenta); font-weight: 700; }
.btn-accent:hover { box-shadow: 0 0 16px #ff2e9766; }
.btn-cyan { border-color: #164a5a; color: var(--cyan); }
.btn-cyan:hover { border-color: var(--cyan); box-shadow: 0 0 14px #00e5ff44; }
.btn-danger { border-color: #4a1620; color: #ff6b8a; }
.btn-danger:hover { border-color: #ff6b8a; }
/* Small by design, but not small to hit. The overhang is a transparent
   ::after — the visual stays 28px, the target is 44px tall. Used everywhere a
   control has to stay visually quiet: backup/restore (the only data-recovery
   controls in a build with no server), raw/copy, the per-field export copies. */
.btn-mini { padding: 3px 10px; font-size: 11px; min-height: 28px; position: relative; }
.btn-mini::after { content: ""; position: absolute; inset: -8px -4px; }
/* A disabled button used to compute IDENTICALLY to a live one — same colour,
   same border, opacity 1, cursor:pointer, hover glow and all — so an editor
   with its assistant switched off advertised two dead primaries on every
   persona, over a composer that looked typeable. Nothing said "this is off"
   until you pressed it and nothing happened. Every .btn, in both builds. */
.btn:disabled, .btn[disabled] { opacity: .45; cursor: not-allowed; box-shadow: none; }
.btn:disabled:hover, .btn[disabled]:hover { border-color: var(--line-hot); box-shadow: none; }
.btn:disabled:active, .btn[disabled]:active { transform: none; }
/* .btn-off: live but declining — a control that refuses with a reason instead
   of a dead button, which says nothing at all on a touch screen. */
.btn-off { opacity: .5; box-shadow: none; }
textarea:disabled, .text-input:disabled, input:disabled {
  color: var(--ink-faint); background: #0d0d15; cursor: not-allowed;
}

/* ── editor ──────────────────────────────────────────────── */
.editor { overflow-y: auto; border-right: 1px solid var(--line); }
.editor-empty {
  height: 100%; display: flex; flex-direction: column; gap: 12px;
  align-items: center; justify-content: center; color: var(--ink-faint);
}
/* Not --line-hot: at #2e2e4a on --bg this measured 1.51:1, half the 3:1 a
   non-text graphic needs. It is the only mark on the empty editor, and on a
   projector it disappeared entirely. #5c5c85 clears 3:1 and still reads dim. */
.editor-empty .ghost { font-size: 60px; color: #5c5c85; text-shadow: 0 0 30px #9d7bff33; }
.empty-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
/* Above the overlay breakpoint the sidebar is already an in-flow column, so
   "browse personas" would point at a list that is right there. */
@media (min-width: 1081px) { #app.nav-open #empty-browse { display: none; } }
/* margin-inline:auto, not just max-width: left-flush in a 1618px column put
   898px of dead black on a 1920 projector — 47% of the screen. Centring the
   measure costs nothing at laptop width and gives the column back at 1080p. */
.editor-body { padding: 20px 24px 60px; max-width: 720px; margin-inline: auto; }
.hidden { display: none !important; }

.ed-head { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 6px; }
.emoji-field {
  width: 52px; height: 52px; text-align: center; font-size: 26px;
  background: var(--panel); border: 1px solid var(--line-hot); border-radius: 12px;
  color: var(--ink); flex: none;
}
.ed-head-main { flex: 1; min-width: 0; }
.name-field {
  width: 100%; font-size: 22px; font-weight: 700; letter-spacing: .5px;
  background: transparent; border: none; color: var(--ink); padding: 8px 0;
  border-bottom: 1px solid transparent;
}
.name-field:focus { outline: none; border-bottom-color: var(--magenta); }
.tag-field {
  width: 100%; background: transparent; border: none; color: var(--ink-dim);
  font-size: 12.5px; padding: 12px 0; line-height: 1.4;
  border-bottom: 1px solid transparent; /* reserved so focus can't shift layout */
}
/* The tagline's only focus signal used to be its text getting brighter —
   colour alone, directly under .name-field, which shows a magenta underline.
   Two adjacent fields behaving differently, and neither readable on a
   projector. Mirror the name field; keep the colour change as a second cue. */
.tag-field:focus { outline: none; color: var(--ink); }
.tag-field:focus-visible { border-bottom-color: var(--cyan); }
.parent-chip {
  display: inline-flex; align-items: center; gap: 5px; margin-top: 6px;
  font-size: 11px; color: var(--purple); background: #17122a;
  border: 1px solid #2a2145; border-radius: 20px; padding: 2px 9px;
}

/* Wrapped rows align to a shared baseline instead of stretching per-line, so a
   two-row action bar reads as a grid, not a ragged stack. */
.ed-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 14px 0 22px; }
.accent-field {
  -webkit-appearance: none; appearance: none; width: 44px; height: 44px; flex: none;
  border: 1px solid var(--line-hot); border-radius: 8px; background: none; cursor: pointer; padding: 0;
}
.accent-field::-webkit-color-swatch-wrapper { padding: 3px; }
.accent-field::-webkit-color-swatch { border: none; border-radius: 5px; }

.field { margin-bottom: 20px; }
.field-label {
  display: flex; align-items: center; gap: 8px;
  font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--ink-dim); margin-bottom: 8px;
}
.field-label .lbl-accent { color: var(--cyan); }
.field-hint { color: var(--ink-faint); font-size: 11px; text-transform: none; letter-spacing: 0; }
textarea, .text-input {
  width: 100%; background: var(--panel); color: var(--ink);
  border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px;
  font-family: var(--mono); font-size: 13px; resize: vertical;
}
textarea:focus, .text-input:focus { outline: none; border-color: var(--purple); box-shadow: 0 0 0 3px #9d7bff22; }
textarea { min-height: 70px; line-height: 1.55; }
/* Auto-grown fields size to their content (JS sets height); cap so one long
   field can't dominate the editor, then it scrolls internally. */
textarea.autogrow { resize: none; overflow-y: auto; max-height: 60vh; max-height: 60dvh; }

/* ── dials ───────────────────────────────────────────────── */
.dials { display: grid; gap: 16px; }
.dial-row {}
.dial-top { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 5px; }
.dial-label { display: flex; align-items: center; gap: 8px; min-width: 0; }
.dial-name { color: var(--ink); font-weight: 600; font-size: 12.5px; flex: none; }
.dial-val { color: var(--cyan); font-size: 11px; font-variant-numeric: tabular-nums; flex: none; }
.dial-ends { display: flex; justify-content: space-between; font-size: 10px; color: var(--ink-faint); margin-bottom: 4px; }
/* The element's own box used to BE the 4px track, while the 16px thumb was
   painted by a pseudo-element that overflowed it. So the product's headline
   control looked grabbable and had a ~12px hit region: measured, a press 8px
   off centre did nothing at all — no move, no feedback. The box is 44px now and
   transparent; the track is painted by the track pseudo-element inside it, and
   negative margin keeps the row's layout the height it always was. */
input[type=range] {
  -webkit-appearance: none; appearance: none; width: 100%;
  height: 44px; margin: -14px 0; padding: 0;
  background: transparent; cursor: pointer;
}
input[type=range]::-webkit-slider-runnable-track {
  height: 4px; border-radius: 4px;
  background: linear-gradient(90deg, var(--purple), var(--magenta));
}
input[type=range]::-moz-range-track {
  height: 4px; border-radius: 4px;
  background: linear-gradient(90deg, var(--purple), var(--magenta));
}
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 16px; height: 16px;
  border-radius: 50%; background: var(--ink); border: 2px solid var(--magenta);
  box-shadow: 0 0 10px #ff2e9788; cursor: pointer;
  margin-top: -6px; /* re-centre on the 4px track */
}
input[type=range]::-moz-range-thumb {
  width: 16px; height: 16px; box-sizing: border-box;
  border-radius: 50%; background: var(--ink); border: 2px solid var(--magenta);
  box-shadow: 0 0 10px #ff2e9788; cursor: pointer;
}
/* The dials carried `outline: none` and nothing in its place: Tab landed on one
   and the screen did not change, so arrow keys moved a value the user could not
   locate. Inset because the box is 44px and the visible track is 4px. */
input[type=range]:focus-visible { outline: 2px solid var(--cyan); outline-offset: -16px; }
/* current-position readout: a chip beside the trait name — deliberately unlike
   the plain Cold/Warm end labels and body text, so it never reads as overflow. */
.dial-prose {
  color: var(--ink-dim); font-size: 9.5px; letter-spacing: .6px; text-transform: uppercase;
  padding: 2px 8px; border: 1px solid var(--line-hot); border-radius: 999px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto;
}

/* Respect a reduced-motion preference: the pulsing dot and blinking cursor are
   decoration, and for a vestibular-sensitive reader they are the opposite of
   helpful. Transitions stay — they are short and convey state. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; }
}

/* Windows High Contrast / forced-colors discards background images and colours.
   Measured before this block: the eleven dials rendered as lone circles floating
   in empty space (the track is a gradient), the persona dots went black on
   black, and `save` — the primary action — became visually identical to every
   secondary button, its signal being fill colour alone. Everything here restores
   a NON-COLOUR difference, which is the only kind that survives. */
@media (forced-colors: active) {
  /* forced-color-adjust:none, then repaint in SYSTEM colours: a border on the
     track pseudo-element is discarded outright, so the only way to keep a track
     under the thumb is to opt the control out and draw it in CanvasText. */
  input[type=range] { forced-color-adjust: none; background: transparent; }
  input[type=range]::-webkit-slider-runnable-track { background: CanvasText; }
  input[type=range]::-moz-range-track { background: CanvasText; }
  input[type=range]::-webkit-slider-thumb {
    background: Canvas; border-color: CanvasText; box-shadow: none;
  }
  input[type=range]::-moz-range-thumb {
    background: Canvas; border-color: CanvasText; box-shadow: none;
  }
  .pl-dot { border-color: CanvasText; }
  /* the primary action, signalled by something other than fill colour */
  .btn-accent { border-width: 3px; }
  .btn-accent::before { content: "▸ "; }
}

/* ── influences ──────────────────────────────────────────── */
.infl-toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
.infl-find {
  flex: 1 1 160px; min-width: 0; padding: 14px 9px;
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--line-hot); border-radius: 7px;
  font-family: inherit; font-size: 11px;
}
.infl-find:focus { outline: none; border-color: var(--cyan); }
.infl-find::placeholder { color: var(--ink-faint); }
.infl-empty { font-size: 11px; color: var(--ink-faint); font-style: italic; padding: 6px 2px; }
.infl-cat { margin-bottom: 14px; }
.infl-cat-name {
  font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--purple); margin-bottom: 7px;
}
.infl-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
/* One column on a narrow screen. Two columns at ~120px each turned the blurb
   into five wrapped lines and pushed everything below the picker off-screen —
   and a tablet is the surface this gets browsed on most. */
@media (max-width: 760px) { .infl-grid { grid-template-columns: 1fr; } }
.infl-chip {
  border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
  background: var(--panel); transition: border-color .12s, background .12s;
}
.infl-chip:hover { border-color: var(--line-hot); }
.infl-chip.on { border-color: var(--magenta); background: #1a1020; box-shadow: inset 0 0 14px #ff2e9711; }
.infl-chip.gate-restricted.on { border-color: #ff6b3d; box-shadow: inset 0 0 14px #ff6b3d18; }
.infl-chip.gate-faith.on { border-color: var(--amber); box-shadow: inset 0 0 14px #ffcc5518; }
.infl-main {
  display: flex; gap: 8px; align-items: flex-start; padding: 8px 10px; cursor: pointer;
  /* Positioned ANCESTOR for the visually-hidden checkbox below. Without it the
     absolute input resolves against #app, and 48 of them stacked ~3,400px of
     phantom scrollable overflow onto it — the mechanism behind the black screen
     (see the note on #app's `overflow: clip`). Containing them here means #app
     cannot scroll no matter what overflow value it carries. */
  position: relative;
}
/* Visually hidden, NOT display:none — the latter drops the checkbox out of the
   tab order and the accessibility tree at once, which left all 48 influence
   chips unreachable by keyboard and invisible to a screen reader. The chip is
   the visible control; this stays the real one. */
.infl-main input {
  position: absolute; width: 1px; height: 1px; margin: 0;
  opacity: 0; pointer-events: none;
}
.infl-main input:focus-visible + .infl-check {
  outline: 2px solid var(--cyan); outline-offset: 3px;
}
.infl-check {
  width: 15px; height: 15px; border-radius: 4px; border: 1px solid var(--line-hot);
  flex: none; margin-top: 1px; display: grid; place-items: center; font-size: 10px; color: transparent;
}
.infl-chip.on .infl-check { background: var(--magenta); border-color: var(--magenta); color: #12000a; }
.infl-chip.gate-restricted.on .infl-check { background: #ff6b3d; border-color: #ff6b3d; }
.infl-chip.gate-faith.on .infl-check { background: var(--amber); border-color: var(--amber); }
.infl-weight {
  display: flex; align-items: center; gap: 8px; padding: 8px 10px;
  border-top: 1px solid var(--line-hot);
}
.infl-weight-lbl { font-size: 9px; letter-spacing: 1px; text-transform: uppercase; color: var(--ink-faint); flex: none; }
/* Same defect as the dials, worse: a 3px box. 44px of hit area, with the
   overhang held INSIDE the row's own padding so it can never reach the chip's
   checkbox label above it. */
.infl-weight input[type=range] { height: 44px; margin: -8px 0; }
.infl-weight input[type=range]:focus-visible { outline-offset: -16px; }
.infl-weight-val { font-size: 10px; color: var(--cyan); font-variant-numeric: tabular-nums; flex: none; width: 32px; text-align: right; }
.infl-body { min-width: 0; }
.infl-name { color: var(--ink); font-weight: 600; font-size: 12px; }
.infl-blurb { color: var(--ink-faint); font-size: 10.5px; }
.tag-gate { font-size: 9px; letter-spacing: 1px; border-radius: 4px; padding: 0 4px; margin-left: 6px; }
.tag-restricted { color: #ff6b3d; border: 1px solid #4a2416; }
.tag-faith { color: var(--amber); border: 1px solid #4a3c16; }
.gate-toggle { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
.gate-toggle input { accent-color: var(--magenta); cursor: pointer; }

/* ── list editors ────────────────────────────────────────── */
.list-editor { display: flex; flex-direction: column; gap: 6px; }
.list-row { display: flex; gap: 6px; align-items: center; }
.list-row .text-input { flex: 1; }
.list-del {
  flex: none; width: 30px; height: 34px; border-radius: 7px; cursor: pointer;
  background: transparent; border: 1px solid var(--line); color: var(--ink-faint);
  position: relative;
}
.list-del::after { content: ""; position: absolute; inset: -5px -7px; } /* 44x44 target */
.list-del:hover { border-color: #ff6b8a; color: #ff6b8a; }
.list-del:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.list-add { align-self: flex-start; }

/* ── preview ─────────────────────────────────────────────── */
/* In-flow right column: a thin clickable rail when collapsed, a full panel when
   open — so it can stay open beside the editor and update live while you edit. */
.preview { background: #0c0c14; display: flex; flex-direction: column; overflow: hidden; border-left: 1px solid var(--line); }
.preview-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 14px 16px; border-bottom: 1px solid var(--line);
}
.preview-title { font-size: 11px; letter-spacing: 2px; text-transform: uppercase; color: var(--ink-dim); }
/* 12px, not 8: raw/copy and the ✕ each carry a transparent 44px overhang, and
   at 8px the overhangs met inside the gap. */
.preview-actions { display: flex; gap: 12px; align-items: center; }
.preview-body { flex: 1; overflow-y: auto; padding: 18px 20px; }

/* export target bar — top of the editor, the first choice you make */
.export-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 20px; padding: 11px 14px;
  background: linear-gradient(90deg, #17102a, #12121c);
  border: 1px solid var(--line-hot); border-left: 3px solid var(--cyan);
  border-radius: 10px;
}
.export-bar-k {
  font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--cyan); font-weight: 700; flex: none;
}
.export-select {
  font-family: var(--mono); font-size: 13px; color: var(--ink);
  background: #10101a; border: 1px solid var(--line-hot); border-radius: 8px;
  padding: 12px 30px 12px 12px; cursor: pointer;
  /* Cannot exceed its column: at 320px the always-on 38px export rail left the
     editor 282px while this rendered 312px, slicing off its value and chevron. */
  max-width: 100%; min-width: 0;
  appearance: none; -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--cyan) 50%),
                    linear-gradient(135deg, var(--cyan) 50%, transparent 50%);
  background-position: calc(100% - 15px) 20px, calc(100% - 10px) 20px;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}
.export-select:hover { border-color: var(--cyan); }
.export-select:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 0 3px #00e5ff22; }
.export-bar-hint { font-size: 11px; color: var(--ink-faint); font-style: italic; flex: 1; min-width: 120px; }

.target-guidance {
  padding: 10px 16px; font-size: 11px; line-height: 1.5; color: var(--ink-dim);
  background: #0e0e18; border-bottom: 1px solid var(--line);
}

/* export field cards (platform-specific exports) */
.xf-card {
  border: 1px solid var(--line); border-radius: 10px; margin-bottom: 14px;
  overflow: hidden; background: var(--panel);
}
.xf-head {
  display: flex; align-items: center; gap: 8px; padding: 9px 12px;
  border-bottom: 1px solid var(--line); background: #12121c;
}
.xf-label { font-size: 12px; font-weight: 600; color: var(--ink); flex: 1; min-width: 0; }
.xf-meter {
  font-size: 10.5px; color: var(--ink-faint); font-variant-numeric: tabular-nums;
  flex: none; white-space: nowrap;
}
.xf-meter.over { color: #ff6b8a; font-weight: 700; }
/* between a platform's two limits: legal on the paid plan, truncated on the
   free one. Not red — red means "this truncates wherever you paste it". */
.xf-meter.warn { color: var(--amber); }
.xf-copy { flex: none; }
.xf-paste { padding: 7px 12px 0; font-size: 10.5px; color: var(--ink-faint); font-style: italic; }
.xf-value {
  margin: 8px 12px 12px; padding: 10px 12px; max-height: 320px; overflow: auto;
  background: #0b0b13; border: 1px solid var(--line); border-radius: 7px;
  font-family: var(--mono); font-size: 11.5px; line-height: 1.55; color: var(--ink-dim);
  white-space: pre-wrap; word-break: break-word;
}

/* collapsed rail — fills the narrow column, and is the affordance to expand */
/* The rail is the door to the payoff (watch the prompt build, copy it out),
   and at 10px ink-dim it read as furniture — testers on narrow windows never
   found it. It is an interaction affordance, so it gets the interaction
   colour and a standing glow, not a whisper. */
.preview-rail {
  flex: 1; width: 100%; cursor: pointer;
  border: none; border-left: 1px solid var(--line-hot);
  background: #0c1216; box-shadow: inset 0 0 18px #00e5ff12;
  writing-mode: vertical-rl; text-orientation: mixed;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  color: var(--cyan); font-family: var(--mono); font-weight: 700;
  font-size: 11px; letter-spacing: 2px; text-transform: uppercase;
  transition: color .12s, background .12s, box-shadow .12s, border-color .12s;
}
.preview-rail .rail-glyph { font-size: 13px; letter-spacing: 0; }
.preview-rail:hover {
  background: #0e161b; border-left-color: var(--cyan);
  box-shadow: inset 0 0 22px #00e5ff22; text-shadow: 0 0 10px #00e5ff66;
}
.preview-rail:focus-visible { outline: 2px solid var(--cyan); outline-offset: -2px; }
#app:not(.preview-open) .preview-head,
#app:not(.preview-open) .target-guidance,
#app:not(.preview-open) .preview-body { display: none; }
#app.preview-open .preview-rail { display: none; }
.md-raw { white-space: pre-wrap; font-size: 12.5px; line-height: 1.6; color: var(--ink-dim); }
.md h1 { font-size: 17px; color: var(--magenta); margin: 0 0 4px; text-shadow: 0 0 14px #ff2e9733; }
.md h2 {
  font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--cyan); margin: 20px 0 8px; padding-bottom: 4px; border-bottom: 1px solid var(--line);
}
.md blockquote { margin: 8px 0; padding-left: 12px; border-left: 2px solid var(--purple); color: var(--ink-dim); font-style: italic; }
.md p { margin: 6px 0; color: var(--ink); }
.md ul, .md ol { margin: 6px 0; padding-left: 20px; color: var(--ink); }
.md li { margin: 3px 0; }
.md h3, .md h4, .md h5, .md h6 {
  font-size: 12px; color: var(--ink); margin: 12px 0 4px; letter-spacing: .5px;
}
.md strong { color: var(--ink); font-weight: 700; }
.md em { color: var(--ink-dim); font-style: italic; }
.md code {
  font-family: var(--mono); font-size: 11.5px; color: var(--cyan);
  background: #0b0b13; border: 1px solid var(--line); border-radius: 4px; padding: 0 4px;
}
.md pre {
  margin: 8px 0; padding: 10px 12px; overflow-x: auto;
  background: #0b0b13; border: 1px solid var(--line); border-radius: 7px;
}
.md pre code { border: none; background: none; padding: 0; color: var(--ink-dim); white-space: pre; }
.md hr { border: none; border-top: 1px dashed var(--line-hot); margin: 16px 0; }

/* ── toast ───────────────────────────────────────────────── */
.toast {
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  background: #17121f; border: 1px solid var(--magenta); color: var(--ink);
  padding: 10px 18px; border-radius: 10px; font-size: 12px; z-index: 90;
  box-shadow: 0 8px 30px #00000088, 0 0 20px #ff2e9722;
  /* A notification must never be a target. Without this the toast is a live
     z-90 surface for its whole 2.6s: press save, reach for a dial, dead click.
     Measured landing on "Spice" at 720p and "Humor" at 1080p — i.e. exactly on
     the controls a demo touches right after saving. */
  pointer-events: none;
  /* Bounded so it can never run under the fixed ☰ once it moves to the top. */
  max-width: min(480px, calc(100vw - 96px));
}
.toast.err { border-color: #ff6b8a; }
/* The assistant dock (bottom-left, up to 560px wide) and the toast (bottom
   centre) shared the same corner: every "prompt copied" / "saved" landed on the
   composer and its send button for 2.6s, and on a phone covered them outright.
   z-order was never the problem. The dock sits at body level and carries
   .hidden while closed, so :has answers "is it open?" with no class to keep in
   sync from two different files. The server build's own dock adds its half of
   this rule in its own stylesheet. */
body:has(.bot-dock:not(.hidden)) .toast {
  bottom: auto; top: 20px;
}

::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-thumb { background: var(--line-hot); border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background: var(--purple); }

/* ── collapsible drawers + responsive ─────────────────────── */
#sidebar { transition: transform .22s ease; }

/* floating "open" affordance for the sidebar; hidden once the drawer is open */
.sidebar-toggle {
  position: fixed; top: 12px; left: 12px; z-index: 60;
  width: 34px; height: 34px; border-radius: 8px;
  background: #14141f; border: 1px solid var(--line-hot); color: var(--magenta);
  font-size: 15px; line-height: 1; cursor: pointer;
  display: grid; place-items: center;
  transition: border-color .12s, box-shadow .12s, color .12s;
}
.sidebar-toggle:hover { border-color: var(--magenta); box-shadow: 0 0 12px #ff2e9755; }
.sidebar-toggle:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.sidebar-toggle::after { content: ""; position: absolute; inset: -5px; } /* 44x44 */
#app.nav-open .sidebar-toggle { display: none; }
/* Collapsed sidebar shows the fixed ☰; give the editor top clearance so it can't
   overlap the export bar's top-left corner. (Overlay mode pads via its own rule.) */
#app:not(.nav-open) .editor-body,
#app:not(.nav-open) .editor-empty { padding-top: 54px; }

/* in-drawer close controls (sidebar header ‹, preview header ›) — never
   overlap the content they sit beside */
.drawer-x {
  flex: none; width: 26px; height: 26px; border-radius: 7px; cursor: pointer;
  background: transparent; border: 1px solid var(--line); color: var(--ink-faint);
  font-size: 16px; line-height: 1; display: grid; place-items: center;
  transition: border-color .12s, color .12s;
  position: relative;
}
/* On a phone the ✕ is the ONLY exit from a full-screen panel. 26x26 was the
   whole target; the overhang makes it 44x44 without changing the visual. */
.drawer-x::after { content: ""; position: absolute; inset: -9px; }
.drawer-x:hover { border-color: var(--magenta); color: var(--magenta); }
.drawer-x:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
#nav-collapse { margin-left: auto; }

.backdrop {
  position: fixed; inset: 0; background: #00000099; z-index: 35;
  opacity: 0; pointer-events: none; transition: opacity .2s ease;
}

/* desktop (>1080): sidebar is an in-flow column; slides out when collapsed */
@media (min-width: 1081px) {
  #app:not(.nav-open) #sidebar { transform: translateX(-110%); }
}

/* iPad and below (≤1080): the sidebar becomes a slide-over overlay (transient
   persona picker), but the preview stays an in-flow column — so you can edit
   and watch the system prompt update live at the same time. iPad is the
   minimum supported size. */
@media (max-width: 1080px) {
  #app { grid-template-columns: minmax(0, 1fr) var(--pv); }
  .editor { border-right: none; }
  #sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; width: 264px; max-width: 84vw;
    z-index: 40; transform: translateX(-110%);
  }
  #app.nav-open #sidebar { transform: translateX(0); box-shadow: 10px 0 50px #000c; }
  #app.nav-open .backdrop { opacity: 1; pointer-events: auto; }
  .editor-body { padding-top: 54px; }
  .editor-empty { padding-top: 54px; }
  .sidebar-toggle { top: 10px; left: 10px; }
}

/* phone (≤640): a 320px preview column beside a 1fr editor crushes the editor
   to a sliver. Below this width the open preview becomes a full-screen overlay
   instead of a push column, so the editor keeps its full width underneath. */
@media (max-width: 640px) {
  #app.preview-open { --pv: 38px; } /* keep the grid to the rail; overlay covers */
  #app.preview-open .preview {
    position: fixed; inset: 0; z-index: 45; border-left: none;
    box-shadow: 0 0 60px #000d;
  }
  /* The ☰ used to be display:none here. That removed the only affordance that
     opens the sidebar — which is itself translated off-screen — so while the
     export panel was open there was no way to switch persona, forge a new one,
     back up, restore or reach the assistant, and the sole exit was a 26px ›.
     Copying an export is the most common reason to open this panel, so the app's
     primary action led to a dead end. It is z-index 60 against the overlay's 45,
     so it simply sits on top; move it out of the panel header's way. */
  #app.preview-open .sidebar-toggle { top: auto; bottom: 14px; }
  /* …but the drawer it opens is z-40 and the backdrop z-35, both UNDER the
     z-45 panel. Tapping ☰ therefore slid the sidebar to x=0 behind the panel,
     removed the ☰ itself (#app.nav-open .sidebar-toggle{display:none}), cleared
     the sidebar's inert — six invisible tab stops — and left the screen
     unchanged, with the backdrop unclickable underneath. Strictly worse than
     the display:none it replaced: the control was absent before, now it is
     present and does nothing. Lift both above the panel when it is open. */
  #app.preview-open.nav-open #sidebar { z-index: 50; }
  #app.preview-open.nav-open #backdrop { z-index: 47; }
}

/* ≤400px: stop reserving a hard 38px grid column for the collapsed rail. At
   320px it left the editor 282px for a 312px-wide export select — the first
   choice the UI asks you to make, with its value and chevron sliced off, and a
   horizontal scrollbar as the only way to see them. Float the rail instead. */
@media (max-width: 400px) {
  #app { grid-template-columns: minmax(0, 1fr); }
  #app:not(.preview-open) .preview {
    position: fixed; right: 0; top: 0; bottom: 0; width: 38px; z-index: 30;
  }
  #app:not(.preview-open) .editor-body,
  #app:not(.preview-open) .editor-empty { padding-right: 46px; }
}

/* preview / boot error states */
.preview-error {
  color: #ff9bad; font-size: 12px; background: #1a0f14;
  border: 1px solid #4a1620; border-radius: 8px; padding: 10px 12px;
  /* column so the retry sits under the sentence rather than beside it */
  display: flex; flex-direction: column; align-items: flex-start; gap: 9px;
}
.boot-error-detail { color: var(--ink-faint); font-size: 11px; max-width: 420px; }
.boot-error-detail summary { cursor: pointer; }
.boot-error-detail div { margin-top: 6px; word-break: break-word; }

/* ── boot state (browser-only builds) ─────────────────────────
   #app carries data-booting from first paint until bootApp resolves. The shell
   otherwise paints a finished-looking EMPTY app while a 4.4MB engine is still
   downloading, and every control in it is dead. */
.boot-only { display: none; }
#app[data-booting] .boot-only { display: block; }
#app[data-booting] .ready-only { display: none; }
.pl-skeleton { display: flex; flex-direction: column; gap: 3px; padding: 2px 0; }
.pl-skeleton span {
  height: 42px; border-radius: var(--radius);
  background: linear-gradient(90deg, #12121c 25%, #1a1a28 50%, #12121c 75%);
  background-size: 300% 100%; animation: pl-shimmer 1.4s linear infinite;
}
@keyframes pl-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .pl-skeleton span { animation: none; } }

/* An unreadable store is announced where it happened — persistently, not in a
   toast: a silent empty state is indistinguishable from a first visit, which is
   how the unread bytes used to get overwritten by the next ordinary save. */
.store-problem {
  max-width: 460px; text-align: center; font-size: 11.5px; color: #ffcf9b;
  background: #1d1408; border: 1px solid #5a3a16; border-radius: 8px; padding: 12px 14px;
}
.store-problem p { margin: 0 0 8px; }

/* ── static build: sidebar-foot extras ────────────────────── */
.local-note {
  color: var(--ink-faint); font-size: 10px; font-style: italic;
  line-height: 1.5; margin-bottom: 8px;
}
.local-tools { display: flex; gap: 6px; margin-bottom: 8px; }
.made-by {
  display: block; color: var(--ink-dim); font-size: 11px;
  text-decoration: none; transition: color .12s;
}
.made-by:hover { color: var(--cyan); }

/* ── autobot (public build) ───────────────────────────────────
   The client-side assistant panel. Deliberately its own small set of classes
   rather than sharing the server build's dock: that one carries tabs, undo chips
   and structured-edit chrome this panel has no use for, and visual duplication
   is obvious the moment it breaks. What must NOT be duplicated — the system
   prompt and the tool schema — comes from the WASM engine instead. */
.bot-dock {
  position: fixed; left: 14px; bottom: 14px; z-index: 70;
  width: min(560px, calc(100vw - 28px));
  border: 1px solid var(--line-hot); border-radius: 14px; overflow: hidden;
  background: linear-gradient(180deg, #14101f, #100c18);
  box-shadow: 0 24px 70px #000d, 0 0 30px #00e5ff18;
}
@media (min-width: 1081px) { #app.nav-open ~ .bot-dock { left: 278px; } }
/* At overlay widths the sidebar IS an overlay, and the dock sat over it: the
   persona drawer opened underneath, its bottom half covered, taps landing on the
   dock. Drop below the backdrop (35) and the drawer (40) whenever the drawer is
   open, so the dock is dimmed with everything else instead of fighting it. */
@media (max-width: 1080px) { #app.nav-open ~ .bot-dock { z-index: 30; } }
/* Phone: an honest bottom sheet. A rounded card floating over 79% of the
   viewport hid every field, dial and influence — the persona you are supposed
   to be watching change. Anchored to the bottom edge with a capped transcript,
   the editor keeps the top of the screen. */
@media (max-width: 640px) {
  .bot-dock { left: 0; right: 0; bottom: 0; width: auto; border-radius: 14px 14px 0 0; }
  .bot-thread { max-height: 28vh; max-height: 28dvh; }
}
/* tabs — forge (shape it) vs try (talk to it), one panel, one corner */
.bot-tabs {
  display: flex; gap: 2px; padding: 0 8px;
  border-bottom: 1px solid var(--line-hot); background: #0e0e18;
}
.bot-tab {
  flex: 1; padding: 14px 10px; font-size: 11px; letter-spacing: 1px;
  text-transform: uppercase; cursor: pointer; font-family: inherit;
  background: transparent; border: none; border-bottom: 2px solid transparent;
  color: var(--ink-faint);
}
.bot-tab:hover { color: var(--ink-dim); }
.bot-tab.on { color: var(--cyan); border-bottom-color: var(--cyan); }
.bot-tab:focus-visible { outline: 2px solid var(--cyan); outline-offset: -3px; }
.bot-tab.tab-try.on { color: var(--magenta); border-bottom-color: var(--magenta); }
.bot-msg.cursor::after {
  content: "▍"; margin-left: 1px; color: var(--magenta); animation: botpulse 1s infinite;
}
.bot-msg.cursor.pending::after { color: var(--ink-faint); }
.bot-head {
  display: flex; align-items: center; gap: 8px; padding: 11px 14px;
  border-bottom: 1px solid var(--line); font-size: 11px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--ink-dim);
}
.bot-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-faint); }
.bot-head.on .bot-dot { background: var(--cyan); box-shadow: 0 0 8px var(--cyan); animation: botpulse 2s infinite; }
@keyframes botpulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } }
.bot-head .bot-spacer { margin-left: auto; }
.bot-body { padding: 12px 14px; }
.bot-thread {
  display: flex; flex-direction: column; gap: 10px;
  max-height: 40vh; max-height: 40dvh; overflow-y: auto; margin-bottom: 12px;
}
.bot-msg { font-size: 12px; border-radius: 9px; padding: 9px 12px; max-width: 92%; }
.bot-msg.user { align-self: flex-end; background: #1a1226; border: 1px solid #2a2145; color: var(--ink); white-space: pre-wrap; }
.bot-msg.bot { align-self: flex-start; background: #0c0c14; border: 1px solid var(--line); color: var(--ink-dim); }
.bot-msg.err { border-color: #4a1620; color: #ff9bad; }
.bot-msg.pending { color: var(--ink-faint); font-style: italic; }
.bot-empty { color: var(--ink-faint); font-size: 11.5px; font-style: italic; padding: 6px 2px 2px; }
.bot-applied {
  display: flex; align-items: center; gap: 8px; margin-top: 8px; padding-top: 7px;
  border-top: 1px dashed var(--line-hot); font-size: 10.5px; color: var(--green);
}
.bot-applied.err { color: #ff9bad; border-top-color: #4a1620; }
.bot-retry { margin-left: 8px; }
.bot-row { display: flex; gap: 8px; align-items: flex-end; }
.bot-row textarea { flex: 1; min-height: 58px; max-height: 200px; margin: 0; }
.bot-presets { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
/* 22px tall was the smallest target in the app. Grown by padding rather than a
   transparent overhang: these wrap into rows 8px apart, so overhangs big enough
   to matter would have overlapped each other. */
.bot-chip {
  font-family: var(--mono); font-size: 10.5px; background: #12121c; color: var(--cyan);
  border: 1px solid #164a5a; border-radius: 20px; padding: 9px 13px; cursor: pointer;
}
.bot-chip:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.bot-chip:hover { border-color: var(--cyan); box-shadow: 0 0 10px #00e5ff33; }
.bot-chip:disabled { opacity: .4; cursor: not-allowed; box-shadow: none; }
.bot-pill {
  display: block; width: 100%; text-align: center; font-size: 11px; letter-spacing: 1px;
  padding: 15px 6px; border-radius: 6px; color: var(--ink-faint);
  border: 1px solid var(--line); background: transparent; font-family: var(--mono); cursor: pointer;
  margin-top: 8px;
}
.bot-pill:hover { border-color: var(--cyan); color: var(--cyan); }
.bot-pill.on { color: var(--cyan); border-color: #12384a; }
/* key setup — the security note is part of the control, not a footnote */
.bot-key { display: flex; flex-direction: column; gap: 8px; }
.bot-key input {
  width: 100%; padding: 7px 9px; background: var(--panel); color: var(--ink);
  border: 1px solid var(--line-hot); border-radius: 7px; font-family: var(--mono); font-size: 11px;
}
.bot-key input:focus { outline: none; border-color: var(--cyan); }
.bot-note { font-size: 10.5px; color: var(--ink-faint); line-height: 1.5; }
.bot-note strong { color: var(--amber); font-weight: 600; }

/* overflow menu for rare + irreversible editor actions */
.ed-more { position: relative; }
.ed-more > summary { list-style: none; cursor: pointer; user-select: none; }
.ed-more > summary::-webkit-details-marker { display: none; }
.ed-more[open] > summary { border-color: var(--cyan); color: var(--cyan); }
.ed-more-panel {
  position: absolute; right: 0; top: calc(100% + 6px);
  /* 75, over the assistant dock (70): this menu holds delete/transfer and used
     to render with only its first row visible on a short viewport, the rest
     behind the dock. Still under the modal backdrop (85). */
  z-index: 75;
  display: flex; flex-direction: column; gap: 6px; padding: 8px;
  min-width: 190px;
  background: #12121c; border: 1px solid var(--line-hot); border-radius: 10px;
  box-shadow: 0 18px 50px #000c;
}
.ed-more-panel .btn { width: 100%; text-align: left; justify-content: flex-start; }
/* the destructive one sits apart, below a rule — never adjacent to a routine
   action it could be mistaken for. 12px, not 4: this is the row a mis-tap
   deletes a persona with.
   The rule is drawn in the GAP, not on the button: recolouring the button's own
   border-top gave the most dangerous control in the app a grey lid on three red
   sides, and the padding-top that went with it rode the label 3px low. */
.ed-more-panel .btn-danger { margin-top: 18px; position: relative; }
.ed-more-panel .btn-danger::before {
  content: ""; position: absolute; left: 0; right: 0; top: -10px;
  border-top: 1px solid var(--line);
}
/* The old rule here flipped the panel to `left: 0` below 760px viewport width.
   The constraining box is the EDITOR COLUMN, not the viewport — ~360px narrower
   whenever the export panel is open — so between 641 and 760 the 190px panel ran
   off the column end and was clipped with nothing to scroll to ("delete" read as
   "delet"), and at 390 it ended 48px past the viewport under the export rail.
   Anchor it to the action ROW instead: it then spans the editor column exactly,
   and cannot leave it from either side at any width. */
@media (max-width: 760px) {
  .ed-actions { position: relative; }
  .ed-more { position: static; }
  .ed-more-panel { left: 0; right: 0; min-width: 0; }
}

/* ── modals ───────────────────────────────────────────────────
   Replaces window.prompt/confirm. Sits over the dock (70) AND over the profile
   gate (80) — the gate's own panels open confirms ("lock all to private"), and
   at the old z-index 78 those rendered behind the surface that opened them.
   Toasts (90) still win, so a result message is never buried by its dialog. */
.modal-back {
  position: fixed; inset: 0; z-index: 85;
  background: #0a0a12dd;
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  display: grid; place-items: center; padding: 20px; overflow-y: auto;
}
.modal-card {
  width: min(440px, 100%);
  background: linear-gradient(180deg, #12121c, #0d0d16);
  border: 1px solid var(--line-hot); border-radius: 14px;
  padding: 20px 22px 16px;
  box-shadow: 0 20px 70px #000c;
}
.modal-title { font-size: 14px; font-weight: 600; color: var(--ink); }
.modal-hint { font-size: 11.5px; color: var(--ink-dim); margin-top: 6px; line-height: 1.5; }
.modal-body { margin: 14px 0 4px; }
.modal-input {
  width: 100%; padding: 9px 11px; font-family: inherit; font-size: 13px;
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--line-hot); border-radius: 8px;
}
.modal-input:focus { outline: none; border-color: var(--cyan); }
.modal-list { display: flex; flex-direction: column; gap: 4px; max-height: 46vh; overflow-y: auto; }
.modal-pick {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: 9px; cursor: pointer;
  background: var(--panel);
}
.modal-pick:hover { border-color: var(--line-hot); }
.modal-pick:focus-within { border-color: var(--cyan); }
.modal-pick-emoji {
  width: 26px; height: 26px; display: grid; place-items: center; flex: none;
  border: 1px solid var(--line-hot); border-radius: 7px; font-size: 13px;
}
.modal-pick-name { font-size: 12.5px; color: var(--ink); }
/* Two 69x34 buttons 8px apart, one of which deletes a persona: a mis-tap
   between "cancel" and "delete" was a ~15px error. */
.modal-row { display: flex; justify-content: flex-end; gap: 16px; margin-top: 16px; }
.modal-row .btn { min-height: 44px; padding-inline: 18px; }
/* Why the dialog refused to close, said inside the dialog — the toast that used
   to carry this arrived after the picker was already gone. */
.modal-err { margin-top: 10px; font-size: 11.5px; color: #ff9bad; }

/* A/B badge: the dial positions a reply came from, plus a re-ask. Only appears
   once the draft has moved, so it reads as a diff rather than chrome. */
.bot-ab, .assist-ab {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 8px; padding-top: 7px; border-top: 1px dashed var(--line-hot);
  font-size: 10.5px; color: var(--cyan);
}
.bot-ab .btn, .assist-ab .btn { flex: none; }
.assist-ab-label { overflow: hidden; text-overflow: ellipsis; }

/* ── concept hints ───────────────────────────────────────────────────────
   Tap-to-reveal, one-line plain explanations of "dial"/"influence", hung off
   the section headings. A real button (not title=, which is dead on touch).
   Ships in both builds. The button is a flex child of .field-label, so its
   spacing comes from that row's gap. */
.concept-q {
  width: 18px; height: 18px; padding: 0; flex: none;
  display: inline-grid; place-items: center;
  border: 1px solid var(--line-hot); border-radius: 50%;
  background: transparent; color: var(--ink-faint);
  font-family: var(--mono); font-size: 11px; font-weight: 700; line-height: 1;
  cursor: pointer; transition: color .12s, border-color .12s, background .12s;
}
.concept-q:hover, .concept-q.on { color: var(--cyan); border-color: var(--cyan); }
.concept-q.on { background: #0d1a1e; }
.concept-q:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.concept-note {
  margin: 8px 0 4px; padding: 9px 12px;
  background: #12121f; border: 1px solid var(--line); border-left: 2px solid var(--cyan);
  border-radius: 8px; color: var(--ink-dim); font-size: 12px; line-height: 1.55;
  text-transform: none; letter-spacing: 0; font-weight: 400;
}

/* ── first-run welcome (public build only) ───────────────────────────────
   A newcomer used to land in the editor with no orientation at all. */
.welcome-back {
  position: fixed; inset: 0; z-index: 100;
  display: grid; place-items: center; padding: 20px;
  background: #05050bcc; -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  animation: welcome-fade .18s ease-out;
}
@keyframes welcome-fade { from { opacity: 0; } to { opacity: 1; } }
.welcome-card {
  max-width: 440px; width: 100%;
  background: linear-gradient(180deg, #14121f, #0f0f18);
  border: 1px solid var(--line-hot); border-radius: 16px;
  padding: 26px 26px 22px; box-shadow: 0 24px 70px #000a, 0 0 0 1px #ffffff08;
}
.welcome-title { margin: 0 0 6px; font-size: 20px; letter-spacing: .3px; color: var(--ink); }
.welcome-sub { margin: 0 0 16px; color: var(--ink-dim); font-size: 13px; }
.welcome-steps { list-style: none; margin: 0 0 20px; padding: 0; display: grid; gap: 12px; }
.welcome-steps li {
  display: grid; grid-template-columns: 26px 1fr; gap: 12px; align-items: start;
  font-size: 13.5px; color: var(--ink-dim); line-height: 1.5;
}
.welcome-steps b { color: var(--ink); font-weight: 600; }
.ws-n {
  width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center;
  background: var(--purple); color: var(--bg); font-weight: 700; font-size: 12px;
}
.welcome-row { display: flex; justify-content: flex-end; }
@media (prefers-reduced-motion: reduce) { .welcome-back { animation: none; } }
