/* =============================================================================
   otaku — app.css
   Every colour and every typeface the interface uses is a token declared in
   :root below, and so is every size on the documented scale — a theme is a
   list of custom-property overrides and nothing else. What the contract does
   NOT list — title cuts, the tabs, tracking, one-off lengths — is the
   design's own, literal on purpose, and no theme's business.
   Load order:  app.css  →  custom.css   (custom.css is the reader's, and wins)
   The token contract is docs/web_tokens.md.
   ========================================================================== */

/* --- the three faces, bundled ---------------------------------------------
   Literata and IBM Plex Sans cover the scripts Newsreader and Archivo do not:
   only their extended subsets are declared, so Latin never leaves the primary
   face. Literata's size-adjust matches it to Newsreader, by eye. */
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 400 800; font-display: swap; src: url('fonts/Archivo-v25-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 400 800; font-display: swap; src: url('fonts/Archivo-v25-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
/* The prose face's box declared 84% above the baseline and 16% below (it is
   74 and 26): a browser draws its caret over that box, and the face's own
   reaches down to the descenders, a caret hanging under the letters. */
@font-face { font-family: 'Newsreader'; font-style: normal; font-weight: 400 600; font-display: swap; ascent-override: 84%; descent-override: 16%; src: url('fonts/Newsreader-v26-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Newsreader'; font-style: normal; font-weight: 400 600; font-display: swap; ascent-override: 84%; descent-override: 16%; src: url('fonts/Newsreader-v26-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: 'Newsreader'; font-style: italic; font-weight: 400 600; font-display: swap; ascent-override: 84%; descent-override: 16%; src: url('fonts/Newsreader-v26-italic-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Newsreader'; font-style: italic; font-weight: 400 600; font-display: swap; ascent-override: 84%; descent-override: 16%; src: url('fonts/Newsreader-v26-italic-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: 'Literata'; font-style: normal; font-weight: 400 600; font-display: swap; size-adjust: 92%; src: url('fonts/Literata-v40-cyrillic.woff2') format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
@font-face { font-family: 'Literata'; font-style: normal; font-weight: 400 600; font-display: swap; size-adjust: 92%; src: url('fonts/Literata-v40-cyrillic-ext.woff2') format('woff2'); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
@font-face { font-family: 'Literata'; font-style: italic; font-weight: 400 600; font-display: swap; size-adjust: 92%; src: url('fonts/Literata-v40-italic-cyrillic.woff2') format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
@font-face { font-family: 'Literata'; font-style: italic; font-weight: 400 600; font-display: swap; size-adjust: 92%; src: url('fonts/Literata-v40-italic-cyrillic-ext.woff2') format('woff2'); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/IBMPlexSans-v3.201-400.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 500; font-display: swap; src: url('fonts/IBMPlexSans-v3.201-500.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 600; font-display: swap; src: url('fonts/IBMPlexSans-v3.201-600.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/IBMPlexMono-v2.3-400.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('fonts/IBMPlexMono-v2.3-500.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600; font-display: swap; src: url('fonts/IBMPlexMono-v2.3-600.woff2') format('woff2'); }

/* =============================================================================
   TOKENS — the public contract. Override any of these in custom.css.
   ========================================================================== */
:root {
  color-scheme: light;

  /* -- ground ------------------------------------------------------------- */
  --otk-desk:        oklch(0.928 0.005 175);  /* the surface the page lies on */
  --otk-spine-bg:    oklch(0.912 0.005 175);  /* the app's left rail         */
  --otk-paper:       oklch(0.984 0.005 95);   /* the page, panels, dialogs   */
  --otk-well:        oklch(0.955 0.008 95);   /* inside an editable field    */
  --otk-edit-bg:     oklch(0.996 0.003 95);   /* an open field, being typed  */
  --otk-band:        oklch(0.968 0.007 95);   /* footers, rails, index       */

  /* -- ink ---------------------------------------------------------------- */
  --otk-ink:         oklch(0.215 0.008 120);  /* titles, primary button      */
  --otk-body:        oklch(0.27 0.012 90);    /* running prose               */
  --otk-muted:       oklch(0.45 0.01 150);    /* labels, secondary text      */
  --otk-faint:       oklch(0.68 0.008 150);   /* read-only, hints, absent    */

  /* -- rules -------------------------------------------------------------- */
  --otk-line:        oklch(0.865 0.006 165);  /* panel edges, hard rules     */
  --otk-line-soft:   oklch(0.90 0.012 95);    /* rules inside paper          */

  /* -- accent and status -------------------------------------------------- */
  --otk-accent:      oklch(0.55 0.13 55);     /* rules, marks, selection     */
  --otk-accent-ink:  oklch(0.51 0.125 50);    /* accent used as text         */
  --otk-accent-tint: oklch(0.55 0.13 55 / 0.08);
  --otk-dialogue:    oklch(0.47 0.13 244);    /* spoken lines                */
  --otk-dialogue-weight: inherit;             /* their weight, if not the prose's */
  --otk-em:          inherit;                 /* the model's *emphasis*: italic, no colour of its own */
  --otk-strong:      inherit;                 /* its **weight** mark, likewise */
  --otk-strong-weight: 600;                   /* a real cut, not a synthesised 700 */
  --otk-ok:          oklch(0.58 0.11 150);    /* connected, resident         */
  --otk-danger:      oklch(0.5 0.14 30);      /* delete, stop, and failures  */
  --otk-chat-story:  var(--otk-ok);           /* the story's rule, chat layout */
  --otk-chat-you:    var(--otk-accent);       /* the reader's rule, chat layout */

  /* -- three voices, one job each ----------------------------------------- */
  --otk-font-prose:   'Newsreader', 'Literata', Georgia, serif;
  --otk-font-display: 'Archivo', 'IBM Plex Sans', system-ui, sans-serif;
  --otk-font-mono:    'IBM Plex Mono', ui-monospace, monospace;

  /* -- type scale --------------------------------------------------------- */
  --otk-size-display: 31px;  /* a reading column's own title                */
  --otk-size-title:   26px;  /* panel subject, display face, 800            */
  --otk-size-head:    19px;  /* group head, display face, 800               */
  --otk-size-page:    19px;  /* the story, prose face                       */
  --otk-size-lead:    18px;  /* panel ledes, margin prose, the composer     */
  --otk-size-read:    17px;  /* long prose in a panel                       */
  --otk-size-row:     17px;  /* a list row, prose face                      */
  --otk-size-note:    16px;  /* notes and captions, prose face              */
  --otk-size-ui:      14px;  /* every button, display face                  */
  --otk-size-mono:    14px;  /* fields, filters, model names                */
  --otk-size-mono-sm: 13px;  /* facts, urls                                 */
  --otk-size-small:   12px;  /* counts                                      */
  --otk-size-label:   11px;  /* section eyebrow                             */
  --otk-size-foot:    10px;  /* panel footer                                */
  --otk-size-tag:      9px;  /* kind tags, turn rubric                      */

  --otk-leading-page:  1.68;
  --otk-leading-read:  1.72;
  --otk-leading-row:   1.35;
  --otk-row-h:         29px;    /* one list row, whatever face fills it     */

  --otk-track-eyebrow: 0.18em;  /* panel title                              */
  --otk-track-label:   0.22em;  /* section head                             */
  --otk-track-foot:    0.12em;  /* footers                                  */
  --otk-track-tag:     0.20em;

  /* -- metrics ------------------------------------------------------------ */
  --otk-radius:      0px;      /* print discipline: nothing is rounded      */
  --otk-hair:        1px;
  --otk-mark:        2px;      /* the selection rule on a row               */
  --otk-rule-w:      26px;     /* the accent rule of a panel title          */

  --otk-btn-pad:     8px 16px; /* ONE box for every button                  */
  --otk-field-pad:   7px 11px;
  --otk-panel-pad:   26px;
  --otk-page-pad:    72px;
  --otk-dialog-pad:  26px 28px;

  /* -- shell -------------------------------------------------------------- */
  --otk-spine-w:     64px;
  --otk-rail-w:      250px;
  --otk-page-max:    820px;

  /* -- scrollbars --------------------------------------------------------- */
  --otk-scrollbar-w:  10px;    /* the app styles its own scrollbars        */
  --otk-scrollbar-thumb: oklch(0.60 0.011 165);  /* ink-family: it reads as drawn */
  --otk-scrollbar-track: oklch(0.945 0.006 165); /* the channel it runs in   */

  /* -- panel ladder ------------------------------------------------------- */
  --otk-col-lamp:     10px;    /* the resident/connected lamp gutter      */
  --otk-col-size:     66px;    /* a model's size column                   */
  --otk-col-count:    46px;    /* a count column                          */
  --otk-col-ago:      30px;    /* a "2h ago" column                       */
  --otk-reader-w:     400px;   /* the right pane, identical in all lenses  */
  --otk-measure:      860px;   /* a reading line, wherever prose is set    */
  --otk-premise-w:    700px;   /* the premise measure                      */
  --otk-panel-wide:  1600px;   /* models: list plus detail                  */
  --otk-panel-story: 1620px;   /* stories and a story share one box         */
  --otk-panel-read:  1100px;   /* context: one reading column               */
  --otk-dialog-w:    440px;    /* every dialog                             */
  --otk-docket-w:    488px;    /* a slip: usage, settings                   */
  --otk-docket-h:    390px;    /* a slip torn to a set length: balance      */

  /* -- depth -------------------------------------------------------------- */
  --otk-shadow-page:  0 12px 40px oklch(0.25 0.02 170 / 0.10);
  --otk-shadow-panel: 0 24px 80px oklch(0.22 0.02 70 / 0.35);
  --otk-shadow-slip:  0 8px 24px oklch(0.22 0.02 70 / 0.16);
  --otk-scrim:        oklch(0.928 0.005 175 / 0.82);
  --otk-scrim-blur:   4px;
}

/* A dark theme is this list and nothing more. The page follows the OS;
   `data-theme` on <html> pins it either way, and custom.css outranks both. */
[data-theme="dark"] {
  color-scheme: dark;
  --otk-desk:        oklch(0.22 0.006 175);
  --otk-spine-bg:    oklch(0.19 0.006 175);
  --otk-paper:       oklch(0.26 0.008 90);
  --otk-well:        oklch(0.23 0.008 90);
  --otk-edit-bg:     oklch(0.30 0.008 90);
  --otk-band:        oklch(0.24 0.008 90);
  --otk-ink:         oklch(0.94 0.006 95);
  --otk-body:        oklch(0.88 0.008 95);
  --otk-muted:       oklch(0.70 0.01 150);
  --otk-faint:       oklch(0.55 0.008 150);
  --otk-line:        oklch(0.38 0.008 165);
  --otk-line-soft:   oklch(0.33 0.008 95);
  --otk-accent:      oklch(0.68 0.13 55);
  --otk-accent-ink:  oklch(0.74 0.12 55);
  --otk-accent-tint: oklch(0.68 0.13 55 / 0.14);
  --otk-dialogue:    oklch(0.76 0.12 244);
  --otk-scrollbar-thumb: oklch(0.50 0.01 165);
  --otk-scrollbar-track: oklch(0.30 0.008 90);
  --otk-shadow-page:  none;
  --otk-shadow-panel: 0 24px 80px oklch(0 0 0 / 0.5);
  --otk-shadow-slip:  0 8px 24px oklch(0 0 0 / 0.4);
  --otk-scrim:        oklch(0.22 0.006 175 / 0.84);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --otk-desk:        oklch(0.22 0.006 175);
    --otk-spine-bg:    oklch(0.19 0.006 175);
    --otk-paper:       oklch(0.26 0.008 90);
    --otk-well:        oklch(0.23 0.008 90);
    --otk-edit-bg:     oklch(0.30 0.008 90);
    --otk-band:        oklch(0.24 0.008 90);
    --otk-ink:         oklch(0.94 0.006 95);
    --otk-body:        oklch(0.88 0.008 95);
    --otk-muted:       oklch(0.70 0.01 150);
    --otk-faint:       oklch(0.55 0.008 150);
    --otk-line:        oklch(0.38 0.008 165);
    --otk-line-soft:   oklch(0.33 0.008 95);
    --otk-accent:      oklch(0.68 0.13 55);
    --otk-accent-ink:  oklch(0.74 0.12 55);
    --otk-accent-tint: oklch(0.68 0.13 55 / 0.14);
    --otk-dialogue:    oklch(0.76 0.12 244);
    --otk-scrollbar-thumb: oklch(0.50 0.01 165);
    --otk-scrollbar-track: oklch(0.30 0.008 90);
    --otk-shadow-page:  none;
    --otk-shadow-panel: 0 24px 80px oklch(0 0 0 / 0.5);
    --otk-shadow-slip:  0 8px 24px oklch(0 0 0 / 0.4);
    --otk-scrim:        oklch(0.22 0.006 175 / 0.84);
  }
}

/* =============================================================================
   RESET
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
/* unseen until the page knows whether it must ask for a password first;
   opacity rather than visibility, so the shell can still take focus */
html[data-booting] .otk-app { opacity: 0; }
body {
  background: var(--otk-desk);
  color: var(--otk-body);
  font-family: var(--otk-font-display);
  -webkit-font-smoothing: antialiased;
}
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; text-align: left; }
button:disabled { cursor: default; }
a { color: var(--otk-accent-ink); text-decoration: none; }
a:hover { color: var(--otk-accent); }
::selection { background: var(--otk-accent-tint); }
/* No focus ring anywhere, by decision: a dialog opening lands focus on
   its first button and Safari would draw one there. Fields show focus by
   their accent border, rows by their selection tint; buttons and links
   show none. */
:focus, :focus-visible { outline: none; }

/* The standard properties are unconditional; only an engine without them
   falls through to the ::-webkit- pseudo-elements. Do not invert the test —
   querying the webkit SELECTOR instead sends Safari down the legacy branch
   and leaves it unstyled. */
* { scrollbar-width: thin; scrollbar-color: var(--otk-scrollbar-thumb) var(--otk-scrollbar-track); }
@supports not (scrollbar-color: auto) {
  *::-webkit-scrollbar { width: var(--otk-scrollbar-w); height: var(--otk-scrollbar-w); background: var(--otk-scrollbar-track); }
  *::-webkit-scrollbar-track { background: var(--otk-scrollbar-track); }
  *::-webkit-scrollbar-thumb { background: var(--otk-scrollbar-thumb); border: 2px solid var(--otk-scrollbar-track); }
  *::-webkit-scrollbar-thumb:hover { background: var(--otk-muted); }
  *::-webkit-scrollbar-thumb:active { background: var(--otk-ink); }
  *::-webkit-scrollbar-corner { background: var(--otk-scrollbar-track); }
}
/* every scrolling column reserves the gutter, so columns stay aligned */
.otk-col__scroll, .otk-detail__scroll, .otk-index__list, .otk-reading, .otk-margin,
.otk-transcript, .otk-docket__body, .otk-premise__body, .otk-context { scrollbar-gutter: stable; }
[hidden] { display: none !important; }

/* =============================================================================
   ATOMS
   ========================================================================== */

/* the title of a panel: mono caps behind an accent rule */
.otk-eyebrow {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--otk-font-mono); font-size: var(--otk-size-mono);
  letter-spacing: var(--otk-track-eyebrow); text-transform: uppercase;
  color: var(--otk-accent-ink);
}
.otk-eyebrow::before {
  content: ""; width: var(--otk-rule-w); height: var(--otk-hair);
  background: var(--otk-accent); flex: none;
}

/* a section inside a panel: same voice, smaller, no rule */
.otk-label {
  font-family: var(--otk-font-mono); font-size: var(--otk-size-label);
  letter-spacing: var(--otk-track-label); text-transform: uppercase;
  color: var(--otk-muted);
}
.otk-label--ink { color: var(--otk-ink); font-weight: 500; }

/* a section inside a docket: it outranks the names beneath it, so it is the
   display face at reading size over a rule — not a smaller mono caption */
.otk-section {
  display: flex; align-items: baseline; gap: 10px;
  margin-top: 4px; padding-bottom: 7px;
  border-bottom: var(--otk-hair) solid var(--otk-ink);
  font-family: var(--otk-font-display); font-size: 17px; font-weight: 700;
  letter-spacing: -0.01em; color: var(--otk-ink);
}
.otk-section__count { margin-left: auto; font-family: var(--otk-font-mono); font-size: var(--otk-size-small); font-weight: 400; letter-spacing: 0.04em; color: var(--otk-faint); }

/* Editing happens in place: the field IS the text, inheriting whatever face
   the element it carries sets, so nothing reflows when it opens. Declared
   above every component, so a component's own type outranks this reset. */
.otk-editable {
  display: block; margin: 0; padding: 0;
  font: inherit; color: inherit; line-height: inherit; letter-spacing: inherit;
  background: none; border: 0;
  border-radius: 0; resize: none; field-sizing: content; overflow: hidden;
  /* a form control has no content-based minimum, so a flex column would
     squeeze it to a pixel */
  flex: none;
}
/* Element-qualified, so the BOX outranks the component's where the type above
   must not: a field wearing `.otk-reading__lede` is the text, not a boxed
   copy of it, and must not bring that class's rule and padding. */
textarea.otk-editable { margin: 0; padding: 0; border: 0; }
/* What stands in for the field while it is closed: same box, same newlines,
   so opening moves nothing. `.otk-reader` holds both, one visible at a time. */
.otk-typeset { white-space: pre-wrap; }
.otk-reader { display: flex; flex-direction: column; }
/* Open for typing: a white ground, and not one word moves. The ground is a
   spread shadow rather than padding — padding would eat the measure and
   rewrap the prose under the caret, where a shadow costs no layout at all.
   4px is what a passage leaves between its rubric and its text; further would
   print over the label above. */
.otk-editable:focus {
  outline: none;
  background: var(--otk-edit-bg);
  box-shadow: 0 0 0 4px var(--otk-edit-bg);
}
/* the value sizes to its own text, so an empty one would be a field nobody
   can see: it keeps a floor */
.otk-leader .otk-editable { min-width: 9ch; text-align: right; }
.otk-editable.is-out { color: var(--otk-danger); }   /* a value outside its bounds, while typed */
/* An empty field stands at its default, and a default is an absent value —
   said in words, in the face `.otk-absent` says every other one in. */
.otk-leader .otk-editable::placeholder { font-family: var(--otk-font-prose); font-style: italic; color: var(--otk-faint); opacity: 1; }
/* a parameter the provider in use does not read: in its place, closed — the
   label struck, the field shut */
.otk-leader--closed > span:first-child { text-decoration: line-through; color: var(--otk-faint); }
.otk-leader .otk-editable:disabled { color: var(--otk-faint); cursor: default; }

/* The label line under an editable value: one row at a fixed height in every
   state, so opening a field moves nothing above or below it. */
.otk-edit { display: flex; flex-direction: column; gap: 6px; max-width: var(--otk-measure); }
.otk-edit__hint {
  display: flex; align-items: baseline; gap: 16px; height: 20px;
  font-family: var(--otk-font-mono); font-size: var(--otk-size-label);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--otk-faint);
}
/* anchored right, so swapping one verb for two moves neither */
.otk-edit__verbs { margin-left: auto; display: flex; align-items: baseline; gap: 16px; }
/* A verb is a button, and the UA stylesheet resets casing and tracking on
   form controls — taken back from the line, so label and verbs stay one row. */
.otk-edit__verb {
  color: var(--otk-faint); cursor: pointer;
  text-transform: inherit; letter-spacing: inherit;
}
.otk-edit__verb:hover { color: var(--otk-ink); }
/* save is the one that commits, so it is the one that is emphasised */
.otk-edit__verb--save { color: var(--otk-accent-ink); }
/* At rest only the way in exists, and it comes out under the pointer. Faded
   rather than hidden: a hidden verb is not tabbable, and while the field is
   closed it is out of the tab order, so this is the keyboard's only way in. */
.otk-edit__verb--edit { opacity: 0; }
.otk-edit:hover .otk-edit__verb--edit,
.otk-edit__verb--edit:focus-visible { opacity: 1; }
.otk-edit__verb--save, .otk-edit__verb--cancel { display: none; }
.otk-edit[data-editing] .otk-edit__verb--edit { display: none; }
.otk-edit[data-editing] .otk-edit__verb--save,
.otk-edit[data-editing] .otk-edit__verb--cancel { display: inline; }
/* two modes, two elements in one box: a paragraph to read, a field to type in */
.otk-edit:not([data-editing]) .otk-reader > textarea { display: none; }
.otk-edit[data-editing] .otk-reader > .otk-typeset { display: none; }
/* The paragraph wears the field's own class, so it must be flattened the same
   way — or a component with padding of its own shifts the text on open. */
.otk-reader > .otk-typeset { margin: 0; padding: 0; border: 0; }

/* The wrapper takes the heading's margins, so the label line sits tight under
   the title. Sized to CONTENT so the verbs rest at the end of the title
   rather than out at the measure, with max-content as the floor. */
.otk-edit--title { margin: 10px 0 6px; width: fit-content; max-width: var(--otk-measure); }
.otk-edit--title .otk-reading__title { margin: 0; }
.otk-edit--title .otk-edit__hint { width: 100%; min-width: max-content; }

/* one box for every button */
.otk-btn {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--otk-font-display); font-size: var(--otk-size-ui); font-weight: 500;
  color: var(--otk-body); background: var(--otk-paper);
  border: var(--otk-hair) solid var(--otk-line); border-radius: var(--otk-radius);
  padding: var(--otk-btn-pad);
}
.otk-btn:hover:not(:disabled):not([aria-disabled="true"]) { border-color: var(--otk-ink); }
.otk-btn--primary {                     /* ink, not accent: a stamp */
  font-weight: 600; color: var(--otk-paper);
  background: var(--otk-ink); border-color: var(--otk-ink);
}
.otk-btn--danger { color: var(--otk-danger); border-color: var(--otk-danger); }
.otk-btn--quiet {                       /* Regen, Undo: the only smaller chip */
  font-size: var(--otk-size-small); color: var(--otk-muted);
  border-color: var(--otk-line-soft); padding: 6px 11px;
}
.otk-btn--stop { font-weight: 600; color: var(--otk-paper); background: var(--otk-danger); border-color: var(--otk-danger); }
.otk-btn__square { width: 9px; height: 9px; background: currentColor; flex: none; }
.otk-btn[aria-disabled="true"], .otk-btn:disabled { color: var(--otk-faint); border-color: var(--otk-line-soft); background: var(--otk-paper); pointer-events: none; }
.otk-btn__key {                         /* the typed keyboard hint inside a button */
  font-family: var(--otk-font-mono); font-size: var(--otk-size-foot);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--otk-faint);
}
/* a touch screen has no key to name, as with the composer's hints */
@media (pointer: coarse) { .otk-btn__key { display: none; } }

.otk-field {
  display: block; width: 100%;
  font-family: var(--otk-font-prose); font-size: var(--otk-size-note);
  line-height: 1.5; color: var(--otk-ink);
  background: var(--otk-well); border: var(--otk-hair) solid var(--otk-line-soft);
  border-radius: var(--otk-radius); padding: var(--otk-field-pad);
}
.otk-field--mono { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono); }
.otk-field:focus-within, .otk-field:focus,
.otk-filter:focus-within, .otk-filter:focus { border-color: var(--otk-accent); outline: none; }

/* a derived value: never editable, and says so without relying on colour */
.otk-derived {
  margin: 0;
  border-left: var(--otk-hair) solid var(--otk-line-soft); padding-left: 11px;
  font-family: var(--otk-font-prose); font-size: var(--otk-size-note);
  font-style: italic; line-height: 1.55; color: var(--otk-muted);
}
.otk-derived__flag {
  display: block; font-family: var(--otk-font-mono); font-size: var(--otk-size-tag);
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--otk-faint);
}

.otk-pending {
  border: var(--otk-hair) dashed var(--otk-line-soft); padding: 6px 9px;
  font-family: var(--otk-font-prose); font-size: var(--otk-size-note);
  font-style: italic; color: var(--otk-faint);
}

/* a character's card archive: hand-kept source, not a derived quotation —
   plain face, and its newlines are structure */
.otk-card {
  margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-note);
  line-height: 1.5; color: var(--otk-body);
  white-space: pre-wrap; overflow-wrap: anywhere;
}

.otk-note {
  margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-note);
  font-style: italic; line-height: 1.55; color: var(--otk-faint);
}

/* a dotted leader: name left, value right */
.otk-leader { display: flex; align-items: baseline; gap: 6px; padding: 3px 0; font-family: var(--otk-font-mono); font-size: var(--otk-size-mono); color: var(--otk-muted); }
.otk-leader::after { content: ""; flex: 1; order: 1; border-bottom: var(--otk-hair) dotted var(--otk-line); translate: 0 -4px; }
.otk-leader > :last-child { order: 2; color: var(--otk-body); font-variant-numeric: tabular-nums; }

.otk-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--otk-ok); flex: none; }
.otk-dot--off { background: none; border: var(--otk-hair) solid var(--otk-line); }
.otk-dot--sm { width: 6px; height: 6px; }

.otk-tag {
  flex: none;
  font-family: var(--otk-font-mono); font-size: var(--otk-size-tag); font-weight: 400;
  letter-spacing: var(--otk-track-foot); text-transform: uppercase;
  color: var(--otk-faint); border: var(--otk-hair) solid var(--otk-line-soft);
  padding: 1px 5px;
}
/* what a model is to the session, in the models list: the story's, extraction's */
.otk-tag--role { font-weight: 600; }

/* =============================================================================
   SHELL
   ========================================================================== */
.otk-app { display: grid; grid-template-columns: var(--otk-spine-w) var(--otk-rail-w) minmax(0, 1fr); height: 100dvh; overflow: hidden; }

/* The name takes what the mark above and the switch and dots below leave,
   and sits centred in it — clipped on a short screen, never under them. */
.otk-spine { display: flex; flex-direction: column; align-items: center; padding: 22px 0; background: var(--otk-spine-bg); border-right: var(--otk-hair) solid var(--otk-line); }
.otk-spine__mark { width: 12px; height: 12px; background: var(--otk-accent); flex: none; }
.otk-app.is-offline .otk-spine__mark { background: var(--otk-faint); }
/* the name stands 10px clear of the mark and the theme switch at least: a
   margin, outside the box its overflow is clipped at, where padding would
   still let a long name run up to them */
.otk-spine__name { flex: 1; min-height: 0; margin: 10px 0; overflow: hidden; display: flex; align-items: center; justify-content: center; }
/* the spine's two words — the name, and the contents toggle once the rail
   folds — are one line of type, read the same way: bottom to top */
.otk-spine__name > span, .otk-spine__toggle { writing-mode: vertical-rl; transform: rotate(180deg); white-space: nowrap; font-family: var(--otk-font-mono); font-size: var(--otk-size-label); letter-spacing: 0.34em; text-transform: uppercase; color: var(--otk-muted); }
.otk-spine__toggle { flex: none; margin: 14px 0 22px; display: none; padding: 8px 0; } /* the foot below it is the theme switch: room between the word and the sun */
.otk-spine__dots { display: flex; flex-direction: column; gap: 14px; align-items: center; padding-top: 16px; }
.otk-spine__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--otk-faint); }
.otk-spine__dot[aria-current="true"] { background: var(--otk-ink); }

/* the theme switch lives in the spine's foot, above the story dots — app
   chrome with the app chrome. The spine is vertical, so the switch is too:
   sun above, moon below, the knob sliding between them. */
.otk-theme { flex: none; display: flex; flex-direction: column; align-items: center; gap: 8px; padding-bottom: 18px; }
.otk-theme__word { display: flex; line-height: 1; color: var(--otk-faint); }
.otk-switch { position: relative; width: 18px; height: 36px; flex: none; padding: 0; background: var(--otk-well); border: var(--otk-hair) solid var(--otk-line); border-radius: 999px; cursor: pointer; }
.otk-switch__knob { position: absolute; left: 3px; top: 3px; width: 10px; height: 10px; border-radius: 50%; background: var(--otk-faint); transition: top 120ms ease, background 120ms ease; }
.otk-switch[aria-checked="true"] .otk-switch__knob { top: 21px; }
/* full screen, beside the theme switch, drawn in the sun and moon's ink */
.otk-fullscreen { flex: none; display: flex; padding: 6px 6px 6px 0; line-height: 1; color: var(--otk-faint); cursor: pointer; }
.otk-switch:hover .otk-switch__knob { background: var(--otk-accent); }
/* laid on its side for a form. Declared after the base rules, so the
   horizontal travel wins over the vertical one at equal specificity. */
.otk-switch--h { width: 34px; height: 18px; }
.otk-switch--h .otk-switch__knob { top: 3px; left: 3px; transition: left 120ms ease, background 120ms ease; }
.otk-switch--h[aria-checked="true"] .otk-switch__knob { top: 3px; left: 19px; background: var(--otk-accent); }

.otk-rail { display: flex; flex-direction: column; padding: 30px 0 22px; overflow: hidden; }
.otk-rail__head { display: flex; flex-direction: column; gap: 8px; padding: 0 30px 22px; }
.otk-rail__head::after { content: ""; height: var(--otk-hair); background: var(--otk-line); }
.otk-toc { flex: 1; display: flex; flex-direction: column; gap: 2px; padding: 0 30px; overflow-y: auto; counter-reset: otk-folio; }
.otk-toc__row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; width: 100%; padding: 5px 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-note); color: var(--otk-muted); counter-increment: otk-folio; }
.otk-toc__row:hover:not(:disabled) { color: var(--otk-ink); }
.otk-toc__row--lead { color: var(--otk-ink); font-weight: 600; }
/* the folio is counted, not typed, so a new row never renumbers the rest */
.otk-toc__row::after { content: counter(otk-folio, lower-roman); font-family: var(--otk-font-mono); font-size: var(--otk-size-foot); color: var(--otk-faint); }
.otk-toc__gap { flex: none; min-height: 14px; }
.otk-rail__foot { flex: none; display: flex; flex-direction: column; gap: 3px; margin: 0 30px; padding-top: 14px; border-top: var(--otk-hair) solid var(--otk-line); font-family: var(--otk-font-mono); font-size: var(--otk-size-small); color: var(--otk-muted); }
/* a long model name is cut, never wrapped: the foot keeps its two lines */
.otk-rail__foot > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.otk-rail__model { letter-spacing: 0.08em; text-transform: uppercase; color: var(--otk-accent-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* =============================================================================
   THE PAGE
   ========================================================================== */
.otk-main { display: flex; flex-direction: column; align-items: center; min-width: 0; min-height: 0; padding: 30px 40px 0; }
.otk-page {
  width: 100%; max-width: var(--otk-page-max); flex: 1; min-height: 0;
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--otk-paper);
  border: var(--otk-hair) solid var(--otk-line); border-bottom: 0;
  box-shadow: var(--otk-shadow-page);
}
.otk-runhead { flex: none; display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 22px var(--otk-page-pad) 10px; font-family: var(--otk-font-mono); font-size: var(--otk-size-foot); letter-spacing: var(--otk-track-label); text-transform: uppercase; color: var(--otk-faint); }
.otk-runhead__title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.otk-runhead__folio { flex: none; margin-left: auto; white-space: nowrap; color: var(--otk-accent); }

/* Turns stand the prose's own blank line apart — one prose line, what a
   paragraph break measures — and what a turn holds sits closer than that
   (`--otk-block-gap`), so a turn reads as one. */
.otk-transcript {
  flex: 1; min-height: 0; overflow-y: auto;
  overflow-anchor: none;             /* the flow holds its own space after an undo */
  --otk-turn-gap: calc(var(--otk-size-page) * var(--otk-leading-page));
  display: flex; flex-direction: column; gap: var(--otk-turn-gap);
  padding: 22px var(--otk-page-pad) 30px;
}
.otk-prose { margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-page); line-height: var(--otk-leading-page); color: var(--otk-body); text-wrap: pretty; overflow-wrap: anywhere; }
/* The model's own marks take no colour of their own: they read in whatever
   they stand in — the body's ink in narration, the spoken colour in a quote. */
.otk-prose em { font-style: italic; color: var(--otk-em); }
.otk-prose strong { font-weight: var(--otk-strong-weight); color: var(--otk-strong); }
/* Spoken lines take a colour of their own, and stay UPRIGHT: the quotation
   marks already mark them, and italic belongs to the model's `*emphasis*`. */
.otk-quote, .otk-prose--dialogue { color: var(--otk-dialogue); font-weight: var(--otk-dialogue-weight); }

/* ONE gap between a reply's blocks — thinking, notes, a question, a
   failure, and the prose they stand off — in whatever order they come:
   tighter than the blank line between turns (`.otk-transcript`), so what a
   turn holds reads as its own. The prose's own paragraphs keep their blank
   line, the text's. The stylesheet's own variable, not a token. */
.otk-reply, .otk-typeset, .otk-tool { --otk-block-gap: 24px; }
.otk-reply { display: flex; flex-direction: column; gap: var(--otk-block-gap); }
/* who wrote a reply, above its words: on every reply, read in the chat
   layout alone (below) */
.otk-reply__rubric { display: none; }

/* a played turn reads as an interjection, not a speech bubble */
.otk-turn { display: flex; flex-direction: column; align-items: center; gap: 8px; }
/* a tool's turn: nobody's line, a block among the flow — the notes block's
   look inside it, the tool's label over its text */
.otk-tool { display: flex; flex-direction: column; gap: 8px; }
/* and a block of the reply it stands in, the call's and the one the model
   goes on with: their one gap off them, not a turn's */
.otk-transcript:not([data-transcript="canvas"]) > .otk-reply + .otk-tool,
.otk-transcript:not([data-transcript="canvas"]) > .otk-tool + .otk-reply {
  margin-top: calc(var(--otk-block-gap) - var(--otk-turn-gap));
}
.otk-turn__rubric { font-family: var(--otk-font-mono); font-size: var(--otk-size-tag); letter-spacing: 0.26em; text-transform: uppercase; color: var(--otk-accent); }
/* the manuscript's mark before a played line's rubric, decoration alone;
   the chat's rubrics go without it */
.otk-transcript[data-transcript="manuscript"] .otk-turn__rubric::before { content: "◆ "; content: "◆ " / ""; }
.otk-turn__body { margin: 0; max-width: 430px; text-align: center; font-family: var(--otk-font-prose); font-size: var(--otk-size-page); font-style: italic; font-weight: 500; line-height: 1.6; color: var(--otk-ink); overflow-wrap: anywhere; }
/* a command in a played line: the line's own face, the colour alone apart */
.otk-slash { color: var(--otk-accent-ink); }
/* a line's words with the model, not story — all of a /you or /ooc line
   after its token, whatever follows a /ooc or /cue (`transcript.lineNodes`) */
.otk-ooc { color: var(--otk-faint); }
/* the record's own dim line — the dice a /roll rolled: the stats line's
   family, centred because the turn is */
.otk-turn__note { margin: 0; font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); color: var(--otk-faint); font-variant-numeric: tabular-nums; }

/* A stored turn is the dossier's editor in the flow (`.otk-edit`). Its label
   line would add a row under every turn, so it hangs in the gap below the
   turn instead, where it costs no layout. Its words stand the dossier's 6px
   clear of the field, so the open field's ground does not reach them, and
   6 + 18 sits inside the gap between turns. */
.otk-reply, .otk-turn, .otk-tool { position: relative; }
.otk-edit--turn { max-width: none; }
.otk-edit--turn .otk-edit__hint { position: absolute; top: 100%; left: 0; right: 0; height: 24px; padding-top: 6px; }
/* A double click on the text opens a turn, so its edit verb is never shown —
   kept, because it is what the double click takes. Save and cancel still
   come out while the field is open; closed, the line holds nothing, and
   goes — on the canvas it would hang over the text after the turn and take
   its clicks, the typing place's included. */
.otk-edit--turn .otk-edit__verb--edit { display: none; }
.otk-edit--turn:not([data-editing]) > .otk-edit__hint { display: none; }
/* a played line keeps its own centred measure, block and field alike; its
   verbs hang off the turn, as a reply's do, so they stand in one place */
.otk-turn .otk-edit--turn { width: 100%; max-width: 430px; }

/* =============================================================================
   TRANSCRIPT LAYOUTS — the same turns, read three ways. Only the flow's
   `data-transcript` changes (`layout.js`); no markup moves, and every turn
   keeps its rubric whether or not the layout shows it. The manuscript is
   the rules above; the two below are laid over them. The question, notes
   and thinking blocks keep their one look in all three until each layout's
   own is decided.
   ========================================================================== */

/* -- CANVAS. A text being completed, not a conversation: nothing marks who
      wrote what, because in this mode nothing should. One face, one size, one
      ink, one measure, one alignment — the caret is the only thing that moves.
      The transcript IS the text: block flow rather than a column of blocks,
      every turn INLINE in it, so a turn that continues the one before runs
      on in the same paragraph. The seam between two turns is data
      (`data-join`, the backend's `get_join_type`): a paragraph join stands a blank
      line before the turn, an inline one nothing — the turn's own text
      carries its space or its break. */
.otk-transcript[data-transcript="canvas"] { display: block; }
/* The flow's own line boxes are the prose's: the articles take its face
   and leading, so a line the text does not fill — the gap before a
   paragraph, a blank line a row carries — measures one prose line, and
   a field standing in for a paragraph (below) measures the same lines. */
.otk-transcript[data-transcript="canvas"] > .otk-turn,
.otk-transcript[data-transcript="canvas"] > .otk-reply {
  font-family: var(--otk-font-prose); font-size: var(--otk-size-page); line-height: var(--otk-leading-page);
  --otk-block-gap: calc(var(--otk-size-page) * var(--otk-leading-page));
}
/* one text: its gaps are the prose's own line, inside a row as between */
.otk-transcript[data-transcript="canvas"] .otk-typeset { --otk-block-gap: calc(var(--otk-size-page) * var(--otk-leading-page)); }
/* Inline while READ; an open editor is a block again (its base rules), so
   the field spans the paragraph and the verbs hang off a block box — an
   absolute child of an inline box lands elsewhere in every browser. */
.otk-transcript[data-transcript="canvas"] > .otk-turn,
.otk-transcript[data-transcript="canvas"] > .otk-reply,
.otk-transcript[data-transcript="canvas"] .otk-turn > .otk-edit:not([data-editing]),
.otk-transcript[data-transcript="canvas"] .otk-reply > .otk-edit:not([data-editing]),
.otk-transcript[data-transcript="canvas"] .otk-edit:not([data-editing]) > .otk-reader,
.otk-transcript[data-transcript="canvas"] .otk-edit:not([data-editing]) > .otk-reader > .otk-typeset { display: inline; }
.otk-transcript[data-transcript="canvas"] .otk-turn__rubric { display: none; }
/* A text-wire row's own seam — the space or break it opens with, drawn as
   `.otk-seam` — reads only here, where turns flow as one text; the other
   layouts draw each turn as a block of its own and hide it. */
.otk-transcript:not([data-transcript="canvas"]) .otk-seam { display: none; }
/* The seam: one prose line's gap before a paragraph join, a BLOCK rather
   than line breaks — a block measures its height wherever it lands, after
   a line of text or after the field a paragraph became, where two breaks
   would make two blank lines. The first turn opens the text. */
.otk-transcript[data-transcript="canvas"] > [data-join="paragraph"]:not(:first-child)::before { content: ""; display: block; height: var(--otk-block-gap); }
/* The canvas is typed in where the story is: its end editable — the TAIL
   (`transcript.showTail`) — in the text's own face, the composer's box
   standing disabled below with its verbs. */
.otk-tail { display: none; }
.otk-transcript[data-transcript="canvas"] > .otk-tail {
  display: inline; white-space: pre-wrap; outline: none;
  font-family: var(--otk-font-prose); font-size: var(--otk-size-page); line-height: var(--otk-leading-page);
  color: var(--otk-ink);
  /* the gap it opens where the line will start a paragraph (`data-join`,
     the backend's `next_join`), as a row joined so does */
  --otk-block-gap: calc(var(--otk-size-page) * var(--otk-leading-page));
}
/* opening a paragraph it is one, a block: an inline split by its gap would
   leave the browser's caret on the text's last line, beside the drawn one */
.otk-transcript[data-transcript="canvas"] > .otk-tail[data-join="paragraph"] { display: block; }
/* a paragraph opened in the tail: spaced as the story's are — one prose line — and
   a line tall while empty, so the caret has somewhere to stand */
.otk-transcript[data-transcript="canvas"] > .otk-tail > .otk-tail__p {
  display: block;
  margin-top: calc(var(--otk-size-page) * var(--otk-leading-page));
  min-height: calc(var(--otk-size-page) * var(--otk-leading-page));
}
/* the seam's space, outside the editable tail so the caret stands after it */
.otk-tail-seam { display: none; }
.otk-transcript[data-transcript="canvas"] > .otk-tail-seam:not([hidden]) {
  display: inline; white-space: pre-wrap;
  font-family: var(--otk-font-prose); font-size: var(--otk-size-page); line-height: var(--otk-leading-page);
}
/* An empty tail says where the line goes by the caret a reply streams
   behind (`.otk-caret`), drawn while the tail has the focus — the browser's
   own thin one hidden meanwhile — and the browser's once a word is typed. */
.otk-transcript[data-transcript="canvas"] > .otk-tail[data-blank] { caret-color: transparent; }
.otk-transcript[data-transcript="canvas"] > .otk-tail[data-blank]:focus::after {
  content: ""; display: inline-block; width: 8px; height: 0.95em;
  background: var(--otk-accent); vertical-align: text-bottom;
  animation: otk-blink 1.1s steps(1, end) infinite;
}
/* the box stands, disabled, its placeholder saying where the turn is typed
   (`shell.showFacts`) — looking as any placeholder does */
.otk-composer__input textarea:disabled { opacity: 1; cursor: default; }
.otk-composer__input textarea:disabled::placeholder { color: var(--otk-faint); opacity: 1; }
/* holding a refusal of the line typed on the canvas, it reads as the error
   line above the box does (`status.complain`) */
.otk-composer__input textarea:disabled[data-refused]::placeholder { color: var(--otk-danger); }
/* the keys as the tail answers them: Enter a paragraph, no history to walk */
.otk-hints > [data-hint-canvas],
.otk-app[data-layout="canvas"] .otk-hints > [data-hint-box] { display: none; }
.otk-app[data-layout="canvas"] .otk-hints > [data-hint-canvas] { display: inline; }
/* No block in the one text, as the terminal draws none in story mode: a
   tool's turn (kept, hidden — positions count it), the notes, a question
   the story went past; the status row says a call while it is made. The
   live question stays: it is the reader's to answer. Where a block stood
   between two runs of prose, the break it stood for is drawn instead
   (`transcript.blockGap`). */
.otk-transcript[data-transcript="canvas"] > .otk-tool,
.otk-transcript[data-transcript="canvas"] .otk-ask--notes,
.otk-transcript[data-transcript="canvas"] .otk-ask--past { display: none; }
.otk-blockgap { display: none; }
.otk-transcript[data-transcript="canvas"] .otk-blockgap { display: inline; }
/* the played line reads as the prose does */
.otk-transcript[data-transcript="canvas"] .otk-turn__body {
  max-width: none; text-align: start;
  font-style: normal; font-weight: 400;
  line-height: var(--otk-leading-page); color: var(--otk-body);
}
/* The reply arriving is part of the text too: its block inline, so it
   joins the line it continues from the first word and nothing moves when
   it lands. Its status row and the stats line stand under the text with
   no pull-up — the column's negative margins have no gap to eat here —
   the row taking one prose line in all, which the typing place takes over
   when the reply lands: on the text's last line, the flow is a line
   shorter and the space is held (`transcript.holdSpace`); wrapped onto a
   line of its own, it takes exactly the row's room. Either way nothing
   moves. The row a landed reply keeps goes altogether: kept, it would
   split the paragraph the next continuation joins. */
.otk-transcript[data-transcript="canvas"] .otk-generating > .otk-typeset { display: inline; }
.otk-transcript[data-transcript="canvas"] .otk-generating__status { margin-top: calc(var(--otk-block-gap) - 18px); }
.otk-transcript[data-transcript="canvas"] .otk-generating--idle .otk-generating__status { display: none; }
/* The room that row keeps in the other layouts is held all the same, so the
   flow ends where it does there and a layout switch at the end moves
   nothing: by the article itself, as a block after its text, for as long
   as it is the last — an article that follows takes it away, so a
   continuation runs on in the paragraph unsplit. The room is the row's
   18px under its 10px pull-up (`.otk-generating__status`). */
.otk-transcript[data-transcript="canvas"] .otk-generating--idle:last-of-type::after { content: ""; display: block; height: calc(10px + 18px); }
/* the tail goes on from the reply's text, as the played line will */
.otk-transcript[data-transcript="canvas"]:has(> .otk-tail) .otk-generating--idle:last-of-type::after { content: none; }
/* the live question is a block of the reply's, spaced by the article's gap in
   the other layouts; the canvas has no flex gap, so the block's own margins
   stand the one gap off the text on either side */
.otk-transcript[data-transcript="canvas"] .otk-reply > .otk-ask { margin: var(--otk-block-gap) 0; }
/* A block that OPENS a reply (`.otk-ask--first`) leaves the spacing to what
   stands before the reply: the transcript's gap in the other layouts, the
   paragraph gap (`::before`) here. A reply joined inline has neither, so its
   opening block stands its own gap off the text — as the live question does. */
.otk-transcript[data-transcript="canvas"] [data-join="inline"] .otk-typeset > .otk-ask--first { margin-top: var(--otk-block-gap); }
/* No thinking in the one text: it would split the line the reply goes on
   from, and leave a hole when a redraw takes it away (it is never stored).
   The status row says the model is reasoning. */
.otk-transcript[data-transcript="canvas"] .otk-reasoning { display: none; }
/* nor a stats line a reply landed with in another layout: the canvas keeps
   the stats as the reply's hover hint (`transcript.done`) */
.otk-transcript[data-transcript="canvas"] .otk-verbose { display: none; }
/* nor the dice a /roll rolled, under its line: the one text has no line of
   the page's own (the terminal's story mode prints none either) */
.otk-transcript[data-transcript="canvas"] .otk-turn__note { display: none; }
/* Editing in the canvas: the field takes the paragraph's place, the same
   text in the same metrics as the reading it stands in for, so the
   paragraph keeps its size and nothing moves. A paragraph of several
   rows opens as ONE field holding all their text (`transcript.frame`):
   the other rows step out of the flow — the first keeping the blank line
   that stands before the paragraph — the field's own text is transparent
   and the reading stays behind it as a mirror, the edited row's text in
   ink and the other rows' dimmed, the open field's ground on the mirror. */
/* weighted as the inline rule above is, so this one wins by standing later */
.otk-transcript[data-transcript="canvas"] .otk-turn[data-framed] > .otk-edit,
.otk-transcript[data-transcript="canvas"] .otk-reply[data-framed] > .otk-edit { display: none; }
.otk-transcript[data-transcript="canvas"] .otk-edit[data-editing] { position: relative; }
.otk-transcript[data-transcript="canvas"] .otk-edit[data-editing] > .otk-reader { position: relative; }
.otk-transcript[data-transcript="canvas"] .otk-edit[data-framing] > .otk-reader > .otk-typeset {
  display: block; position: absolute; inset: 0; pointer-events: none; overflow: hidden;
  background: var(--otk-edit-bg); box-shadow: 0 0 0 4px var(--otk-edit-bg);
}
/* while a paragraph is open, the tail's space would stand on a line of its
   own under the field — the last paragraph open, the flow a line longer */
.otk-transcript[data-transcript="canvas"]:has(.otk-edit[data-editing]) > .otk-tail-seam { display: none; }
/* nor while a question stands between the text and the typing place: the
   answer is typed at the panel's left edge, under it, and runs on in the
   text once it lands and the panel goes */
.otk-transcript[data-transcript="canvas"]:has(.otk-ask--live) > .otk-tail-seam { display: none; }
/* the field held to the paragraph's height scrolls, and the mirror with it
   (`transcript.holdHeight`); its bar stands outside the text's measure
   (`hangScrollbar`), so both wrap at one width */
.otk-transcript[data-transcript="canvas"] .otk-edit[data-framing] > .otk-reader > textarea { position: relative; color: transparent; caret-color: var(--otk-ink); background: none; box-shadow: none; }
.otk-transcript[data-transcript="canvas"] .otk-framed { color: var(--otk-faint); }
.otk-transcript[data-transcript="canvas"] .otk-edit--turn { max-width: none; }
/* a turn's pictures: centred on a row of their own, one paragraph gap off
   the text on either side — so the paragraph after them opens no second */
.otk-transcript[data-transcript="canvas"] .otk-thumbs { margin: 0 auto; padding: var(--otk-block-gap) 0; }
.otk-transcript[data-transcript="canvas"] > :has(> .otk-thumbs) + [data-join="paragraph"]::before { content: none; }

/* -- CHAT. Alternating sides, aligned like a messenger, without bubbles: a
      filled, bordered box would be the only one in the design. The story sits
      on the left behind a 1px rule in the story's colour on its outer edge;
      the reader's turn shrinks to its text, sits against the right edge and
      carries a 1px accent rule on its outer edge. Text inside both is always
      left-aligned, so a long turn keeps a clean left edge. */
/* The rule runs 2px past the text at either end, and is DRAWN rather than a
   border: a border would carry those 2px into the box, the flow would end
   2px under the last line here and flush with it in the other layouts, and
   the page would sit 2px off at the end when the layout switches. So the box
   is the text's, the rule overshoots on its own, and the gap between turns
   is the text's from text, the transcript's one prose line. */
.otk-transcript[data-transcript="chat"] .otk-reply,
.otk-transcript[data-transcript="chat"] .otk-tool { max-width: 75%; margin-right: auto; padding-left: 22px; }
.otk-transcript[data-transcript="chat"] .otk-turn {
  width: fit-content; max-width: 75%; margin-left: auto; align-items: stretch; gap: 7px;
  padding-right: 22px;
}
/* a phone's column is too narrow to give a quarter of it away: the two
   sides keep apart by their edges and rules, at nearly the full width */
@media (max-width: 600px) {
  .otk-transcript[data-transcript="chat"] .otk-reply,
  .otk-transcript[data-transcript="chat"] .otk-tool,
  .otk-transcript[data-transcript="chat"] .otk-turn { max-width: 90%; }
}
.otk-transcript[data-transcript="chat"] .otk-reply::before,
.otk-transcript[data-transcript="chat"] .otk-tool::before,
.otk-transcript[data-transcript="chat"] .otk-turn::before { content: ""; position: absolute; top: -2px; bottom: -2px; width: 1px; }
.otk-transcript[data-transcript="chat"] .otk-reply::before,
.otk-transcript[data-transcript="chat"] .otk-tool::before { left: 0; background: var(--otk-chat-story); }
.otk-transcript[data-transcript="chat"] .otk-turn::before { right: 0; background: var(--otk-chat-you); }
/* who spoke, quietly, above the words, left-aligned on both sides. The
   reply's rubric stands 7px off its first block, as the turn's does, without
   touching the reply's own rhythm: its blocks keep the one gap
   (`--otk-block-gap`). */
.otk-transcript[data-transcript="chat"] .otk-reply__rubric { display: block; text-align: start; letter-spacing: 0.14em; color: var(--otk-muted); }
/* the pull-up is against a reply's gap; a tool's turn keeps its own 8px */
.otk-transcript[data-transcript="chat"] .otk-reply > .otk-reply__rubric { margin-bottom: calc(7px - var(--otk-block-gap)); }
.otk-transcript[data-transcript="chat"] .otk-turn > .otk-turn__rubric { text-align: start; letter-spacing: 0.14em; }
/* one measure, one alignment: the reader's turn is ink and the story is body
   ink — the distinction the manuscript draws, without italic or centring */
.otk-transcript[data-transcript="chat"] .otk-turn__body {
  max-width: none; text-align: start;
  font-style: normal; font-weight: 400;
  line-height: var(--otk-leading-page); color: var(--otk-ink);
}
/* The text's box is the text's own width, against the rule: a short line
   stands at the right whatever else the turn holds, and a long one fills
   the turn and keeps its clean left edge. */
.otk-transcript[data-transcript="chat"] .otk-turn .otk-edit--turn { width: fit-content; max-width: 100%; align-self: flex-end; }
/* The tiles are the manuscript's size and centred under the line. Sized
   outright: a turn shrinks to its content here, and a tile cut as a share of
   the row would be sized off its own picture, wide as the turn allows. */
.otk-transcript[data-transcript="chat"] .otk-thumbs { width: auto; max-width: none; justify-content: center; }
.otk-transcript[data-transcript="chat"] .otk-thumbs .otk-thumb { width: 138px; }
/* the verbs keep to the right edge of the text, as in every layout: the
   turn's padding lies between the text and its rule */
.otk-transcript[data-transcript="chat"] .otk-turn .otk-edit--turn .otk-edit__hint { right: 22px; }
/* the blocks a reply holds — a failure, a question, the notes, the thinking,
   a tool's turn (the notes block) — drop their own rule here: the reply's is
   the one mark on the story's side. They keep a small inset, so they still
   read as set apart from the prose. */
.otk-transcript[data-transcript="chat"] .otk-error,
.otk-transcript[data-transcript="chat"] .otk-ask,
.otk-transcript[data-transcript="chat"] .otk-reasoning { border-left: 0; padding-left: 3px; }
/* A landed reply keeps its status row's slot so nothing moves (below); here
   the slot would show as the rule running on under the text. So the row goes
   and the article keeps the same room as a margin, outside the rule, for as
   long as the row would have stood: until the next line takes its place. The
   room is the row's 18px under its 10px pull-up (`.otk-generating__status`). */
.otk-transcript[data-transcript="chat"] .otk-generating--idle .otk-generating__status { display: none; }
.otk-transcript[data-transcript="chat"] .otk-generating--idle:has(.otk-generating__status) { margin-bottom: calc(10px + 18px); }

/* =============================================================================
   GENERATING, AND THE OTHER TWO KINDS OF LINE

   Waiting and streaming are ONE state: a blinking caret holds the answer's
   place and text grows in front of it. Nothing else on the page moves.
   ========================================================================== */
@keyframes otk-blink { 0%, 45% { opacity: 1; } 55%, 100% { opacity: 0; } }
@keyframes otk-creep { 0% { background-position: 0 0; } 100% { background-position: 22px 0; } }

/* the caret: an accent block at the end of what has arrived */
.otk-caret {
  display: inline-block; width: 8px; height: 0.95em; margin-left: 3px;
  background: var(--otk-accent); vertical-align: text-bottom;
  animation: otk-blink 1.1s steps(1, end) infinite;
}

/* The reply as it arrives: its paragraphs, and the status beneath them. The
   block IS the reply (the article carries `.otk-reply` too), so it sets no gap
   of its own — a paragraph must not sit closer while it streams than it will
   once the story is drawn again from the store. Whatever sits nearer than that
   rhythm (`--otk-block-gap`) pulls up against it: the status row here, verbose
   below. */
.otk-generating { display: flex; flex-direction: column; }
.otk-generating__status {
  position: relative; display: flex; align-items: center; gap: 0;
  flex: none; height: 18px; margin-top: calc(10px - var(--otk-block-gap));   /* the gap less 10 */
  font-family: var(--otk-font-mono); font-size: var(--otk-size-label);
  letter-spacing: var(--otk-track-foot); text-transform: uppercase; color: var(--otk-muted);
}
/* A reply that failed before a word arrived loses its empty paragraph when
   the turn settles: with nothing above it, the pull would reach into the
   turn before this one. */
.otk-generating__status:first-child { margin-top: 0; }
/* the words sit on paper over the rule, so the creep never appears out of
   nowhere at an edge */
.otk-generating__text { position: relative; padding-right: 14px; background: var(--otk-paper); }
.otk-generating__text:last-of-type { padding-right: 18px; }
.otk-generating__rule {
  position: absolute; inset: 50% 0 auto 0; height: 1px;
  background: repeating-linear-gradient(90deg, var(--otk-accent) 0 6px, transparent 6px 22px);
  animation: otk-creep 1.4s linear infinite;
}
/* idle: the slot keeps its height, so nothing shifts when a reply lands */
.otk-generating--idle .otk-generating__status { visibility: hidden; }
.otk-generating--idle .otk-caret { display: none; }
.otk-generating__elapsed { font-variant-numeric: tabular-nums; color: var(--otk-body); }

/* the model's own reasoning, while it streams: not the story, dressed as the
   notes block is (`.otk-ask--notes` and its body: the mark's width of border,
   the same inset, prose at the note's size, the model's line breaks kept) —
   the text dimmed, and the border in the rules' colour where a note's is the
   faint text's */
.otk-reasoning {
  margin: 0; padding: 2px 0 2px 22px;
  border-left: var(--otk-mark) solid var(--otk-line);
  font-family: var(--otk-font-prose); font-size: 15px; line-height: var(--otk-leading-page);
  color: var(--otk-faint); white-space: pre-wrap; text-wrap: pretty; overflow-wrap: anywhere;
}

/* Verbose: what the terminal prints after a reply. Machine, so mono — and
   ONE LINE always, ellipsised rather than wrapped, so a long stats line
   cannot push the turn below it out of rhythm. It reports on the reply above
   it and is pulled up against it: the block gap, less what leaves 7. */
.otk-verbose {
  flex: none;                  /* a shrinkable item beside overflow: hidden
                                  is a line that can be squashed */
  margin: calc(7px - var(--otk-block-gap)) 0 0; max-width: var(--otk-measure);
  font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm);
  line-height: 1.45; color: var(--otk-faint); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* an error is not a turn: it never reads as the story. Its leading is its
   own (see `.otk-ask`), so its rows measure the same wherever it stands. */
.otk-error {
  display: flex; flex-direction: column; gap: 6px; line-height: normal;
  border-left: var(--otk-mark) solid var(--otk-danger); padding: 2px 0 2px 22px;
}
.otk-error__label {
  font-family: var(--otk-font-mono); font-size: var(--otk-size-label);
  letter-spacing: var(--otk-track-label); text-transform: uppercase; color: var(--otk-danger);
}
.otk-error__body { margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-read); line-height: 1.55; color: var(--otk-body); }
.otk-error__actions { display: flex; gap: 8px; padding-top: 4px; }

/* the space the flow holds open so the page does not move under the reader */
.otk-gap { flex: none; width: 100%; }

/* which layout the transcript is read in — three glyphs above the status
   line, the one in force pressed (`layout.js`) */
.otk-modes {
  flex: none; display: flex; align-items: center; gap: 4px;
  margin: 0 30px; padding: 10px 0 8px;
  border-top: var(--otk-hair) solid var(--otk-line);
}
.otk-modes__mark { flex: none; width: 7px; margin-right: 5px; font-family: var(--otk-font-mono); font-size: 13px; font-weight: 600; line-height: 1; color: var(--otk-muted); }
.otk-modes__label { flex: none; font-family: var(--otk-font-mono); font-size: var(--otk-size-label); letter-spacing: 0.06em; color: var(--otk-muted); }
.otk-modes__group { flex: 1; display: flex; align-items: center; justify-content: center; gap: 4px; }
.otk-mode {
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 26px; padding: 0; line-height: 0;
  color: var(--otk-faint);
}
.otk-mode:hover { color: var(--otk-muted); }
.otk-mode[aria-pressed="true"] { color: var(--otk-ink); }
/* with no story there is nothing to keep a choice for, and while a reply is
   arriving the flow is not reflowed under it: the glyphs are inert */
.otk-mode[aria-disabled="true"],
.otk-app[data-playing] .otk-mode { pointer-events: none; }

/* =============================================================================
   STATUS — one reserved line at the foot of the contents, never a bar across
   the page. It holds its height whether or not it has anything to say.
   ========================================================================== */
.otk-status {
  flex: none; display: flex; align-items: center; gap: 9px;
  min-height: 34px; margin: 0 30px; padding: 8px 0;
  border-top: var(--otk-hair) solid var(--otk-line);
  font-family: var(--otk-font-mono); font-size: var(--otk-size-label);
  letter-spacing: 0.06em; color: var(--otk-muted);
}
.otk-status__lamp { width: 7px; height: 7px; border-radius: 50%; background: var(--otk-ok); flex: none; }
.otk-status__text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.otk-status__action { flex: none; margin-left: auto; color: var(--otk-accent-ink); }
.otk-status--working .otk-status__lamp { background: var(--otk-accent); animation: otk-blink 1.1s steps(1, end) infinite; }
.otk-status--offline { color: var(--otk-danger); }
/* A lamp that is OUT: grey, and still a lamp — the same grey the mark in
   the spine and the tab icon take when otaku stops. The sentence beside
   it carries the alarm. */
.otk-status--offline .otk-status__lamp { background: var(--otk-faint); }
.otk-status--idle .otk-status__lamp { background: var(--otk-line); }
/* the line carries the backend's sentence; the lamp says what the session
   is doing */
.otk-status--said { color: var(--otk-body); }
.otk-status--error { color: var(--otk-danger); }

/* =============================================================================
   COMPOSER
   ========================================================================== */
.otk-composer { flex: none; position: relative; display: flex; flex-direction: column; gap: 12px; padding: 16px 28px 20px; background: var(--otk-band); border-top: var(--otk-hair) solid var(--otk-line-soft); }
.otk-composer__row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; }
/* what refused the line: one row above the box, in the status line's voice
   and the refusal's colour, there only while something is wrong */
.otk-composer__error { margin: 0; font-family: var(--otk-font-mono); font-size: var(--otk-size-label); letter-spacing: 0.06em; color: var(--otk-danger); overflow-wrap: anywhere; }
.otk-composer__input { flex: 1 1 300px; min-width: 280px; display: flex; align-items: flex-start; gap: 10px; }
.otk-composer__input textarea {
  flex: 1 1 auto; min-width: 0; min-height: 30px; max-height: 180px;
  field-sizing: content; resize: none; border: 0; outline: none; background: none;
  font-family: var(--otk-font-prose); font-size: var(--otk-size-lead); line-height: var(--otk-leading-row); color: var(--otk-ink);
  overflow-y: auto;
}
.otk-composer__input textarea::placeholder { color: var(--otk-faint); }
/* The pictures door: a clip at Send's left, among the verbs, in Send's own
   ink — the two are one gesture — and the accent when pointed at. `hidden`
   — no vision — wins over it, so the rule is written for the shown state. It
   takes the row's height rather than one of its own: it comes and goes with
   the mode, and the composer must not grow when it does. */
.otk-attach-btn:not([hidden]) { flex: none; align-self: stretch; display: inline-flex; align-items: center; justify-content: center; width: 36px; padding: 0; color: var(--otk-ink); cursor: pointer; }
.otk-attach-btn:hover, .otk-attach-btn:focus-visible { color: var(--otk-accent); }
.otk-composer__verbs { flex: 0 1 auto; margin-left: auto; display: flex; align-items: center; gap: 7px; }
.otk-hints { margin: 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 16px; font-family: var(--otk-font-mono); font-size: var(--otk-size-tag); letter-spacing: var(--otk-track-foot); text-transform: uppercase; color: var(--otk-faint); }
.otk-hints > * { white-space: nowrap; }
/* A touch screen has no keys to hint at, and on a phone the row costs lines
   the story needs; the menu still opens on a typed `/`. */
@media (pointer: coarse) { .otk-hints { display: none; } }
/* A hint that is a button reads as one of the row, not as a control in it:
   the UA stylesheet resets casing and tracking on a form control, so they
   are taken back from the line. */
.otk-hints > button { text-transform: inherit; letter-spacing: inherit; }

/* =============================================================================
   PICTURES — squared like everything else, and always tied to a turn

   Staged under the prefixes, before Send: a count and small tiles, each with
   its cross. Landed, under the turn they rode and in the dossier's reader:
   tiles that open the picture at full size (`pictures.js` draws both). A
   tile is paper ground until the picture lands in it, which
   is also what a browser that cannot draw a HEIC shows.
   ========================================================================== */
.otk-attach { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 6px; }
.otk-attach__label { margin-right: 4px; font-family: var(--otk-font-mono); font-size: var(--otk-size-tag); letter-spacing: var(--otk-track-foot); text-transform: uppercase; color: var(--otk-faint); }
.otk-thumb { position: relative; display: block; flex: none; width: 68px; height: 68px; padding: 0; overflow: hidden; background: var(--otk-well); border: var(--otk-hair) solid var(--otk-line); }
.otk-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.otk-thumb--sm { width: 48px; height: 48px; }
.otk-thumb__drop { position: absolute; top: 0; right: 0; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; padding: 0; background: var(--otk-paper); border: 0; border-left: var(--otk-hair) solid var(--otk-line); border-bottom: var(--otk-hair) solid var(--otk-line); font-family: var(--otk-font-mono); font-size: 13px; line-height: 1; color: var(--otk-muted); cursor: pointer; }
.otk-thumb__drop:hover { color: var(--otk-danger); }
/* under a played turn, centred with it and held to its measure: three tiles
   across the body's 430px with the gap between them, so the strip's edges
   are the text's — the size is that division, never a number of its own,
   and on a narrow column the same division makes three still fit. A tile
   there is a way in, and says so. */
.otk-thumbs { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; width: 100%; max-width: 430px; padding-top: 2px; }
.otk-thumbs .otk-thumb { width: calc((100% - 16px) / 3); height: auto; aspect-ratio: 1; cursor: zoom-in; }
.otk-thumbs .otk-thumb:hover, .otk-thumbs .otk-thumb:focus-visible { border-color: var(--otk-accent); }
/* over the text wire no picture rides the request, so the flow shows none —
   the tiles alone; the dossier still lists what the story holds */
.otk-app[data-completion="text"] .otk-transcript .otk-thumbs { display: none; }
/* the same tiles in the dossier's reader, set flush with its text */
.otk-detail__section > .otk-thumbs { justify-content: flex-start; }
/* full size: the picture on the desk, held to the window, its measure beneath */
.otk-lightbox { display: flex; flex-direction: column; max-width: calc(100vw - 96px); max-height: calc(100dvh - 96px); background: var(--otk-paper); border: var(--otk-hair) solid var(--otk-line); box-shadow: var(--otk-shadow-panel); }
.otk-lightbox__frame { position: relative; flex: 1 1 auto; min-height: 0; width: min(64vw, 820px); display: flex; align-items: center; justify-content: center; background: var(--otk-well); }
/* the way to a turn's other pictures: a chip on either side, paper on the
   picture, absent when the turn has one; at an end the arrow stays and fades */
.otk-lightbox__arrow:not([hidden]) { position: absolute; top: 50%; transform: translateY(-50%); display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; background: var(--otk-paper); border: var(--otk-hair) solid var(--otk-line); color: var(--otk-ink); cursor: pointer; }
.otk-lightbox__arrow--prev { left: 10px; }
.otk-lightbox__arrow--next { right: 10px; }
.otk-lightbox__arrow:hover, .otk-lightbox__arrow:focus-visible { border-color: var(--otk-ink); }
.otk-lightbox__arrow[aria-disabled="true"] { opacity: 0.35; cursor: default; }
.otk-lightbox__count { color: var(--otk-body); font-variant-numeric: tabular-nums; }
/* At its own size on the well, downsized to the frame when it is too big,
   never upsized to fill it. Its own size is the row's measure, written on
   the element as `width` and `height` before the bytes are asked for
   (`pictures.openPicture`): the width holds as written — no `width` rule
   here, so the attribute stands — the height follows the ratio the two
   declare, and the box is right from the first frame; a picture too tall
   is contained in it. */
.otk-lightbox img { display: block; height: auto; max-width: 100%; max-height: calc(100dvh - 160px); object-fit: contain; }
.otk-lightbox__foot { flex: none; display: flex; align-items: center; gap: 16px; padding: 12px var(--otk-panel-pad); border-top: var(--otk-hair) solid var(--otk-line-soft); background: var(--otk-band); font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); color: var(--otk-body); }
.otk-lightbox__meta { color: var(--otk-faint); }
.otk-lightbox__foot .otk-btn { margin-left: auto; }
@media (max-width: 600px) {
  .otk-lightbox { max-width: calc(100vw - 32px); }
  .otk-lightbox__frame { width: calc(100vw - 32px); }
}

/* the prefix menu: the story's own typed openers, grouped by what they reach */
.otk-prefixes { position: absolute; left: 14px; bottom: 100%; margin-bottom: 8px; width: 428px; max-width: calc(100% - 28px); display: flex; flex-direction: column; padding: 4px 0 6px; background: var(--otk-paper); border: var(--otk-hair) solid var(--otk-line); box-shadow: var(--otk-shadow-slip); }
.otk-prefixes__group { display: flex; align-items: baseline; gap: 9px; padding: 7px 16px 3px; }
.otk-prefixes__group::after { content: ""; flex: 1; height: var(--otk-hair); background: var(--otk-line-soft); }
.otk-prefix { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 10px; align-items: baseline; width: 100%; padding: 3px 16px; }
.otk-prefix__token { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); color: var(--otk-accent-ink); }
.otk-prefix__desc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--otk-font-prose); font-size: var(--otk-size-note); line-height: var(--otk-leading-row); color: var(--otk-body); }
.otk-prefixes--cast { width: 320px; max-height: 200px; overflow-y: auto; }
.otk-prefix--name { grid-template-columns: minmax(0, 1fr); }
.otk-prefix--name .otk-prefix__token { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.otk-prefix[aria-selected="true"] { background: var(--otk-accent-tint); }
.otk-prefix[aria-selected="true"] .otk-prefix__desc { color: var(--otk-ink); }

/* =============================================================================
   PANELS — an auxiliary tool the reader steps out to, never a page.
   Every panel, docket and dialog is a native <dialog>; the scrim is its
   backdrop, so stacking and focus are the platform's.
   ========================================================================== */
/* `overflow` goes with the rest of the UA's dialog box: a docket carries its
   shadow on the section inside, which the dialog shrink-wraps exactly, so
   clipping cuts the shadow off. A panel sets its own `overflow: hidden`. */
dialog { margin: auto; padding: 0; border: 0; background: none; color: inherit; overflow: visible; max-width: none; max-height: none; }
/* the panel classes set display, which would beat the UA's `:not([open])` */
dialog:not([open]) { display: none; }
dialog::backdrop { background: var(--otk-scrim); backdrop-filter: blur(var(--otk-scrim-blur)); }

.otk-panel {
  display: flex; flex-direction: column; overflow: hidden;
  width: min(var(--otk-panel-story), calc(100vw - 48px));
  height: min(920px, calc(100dvh - 48px));
  background: var(--otk-paper); border: var(--otk-hair) solid var(--otk-line);
  box-shadow: var(--otk-shadow-panel);
}
.otk-panel--wide { width: min(var(--otk-panel-wide), calc(100vw - 48px)); }
/* the wire is the longest thing in the app: it gets the whole desk */
.otk-panel--read { width: min(var(--otk-panel-read), calc(100vw - 48px)); height: calc(100dvh - 48px); }

.otk-panel__head { flex: none; display: flex; align-items: center; gap: 14px; padding: 14px var(--otk-panel-pad) 10px; }
/* the FIRST button takes the auto margin and the rest sit beside it, so a
   header's controls read as one group at the right */
.otk-panel__head .otk-btn:first-of-type { margin-left: auto; }
/* A head whose verbs sit at the line's centre: three columns, the eyebrow
   left, the verbs in the middle, the close at the right. */
.otk-panel__head--centered { display: grid; grid-template-columns: 1fr auto 1fr; }
.otk-panel__head--centered .otk-close { justify-self: end; }
.otk-panel__middle { display: flex; gap: 14px; }
.otk-panel__middle .otk-btn { margin-left: 0; }
.otk-panel__head > .otk-push ~ .otk-btn { margin-left: 0; }
.otk-panel__head .otk-eyebrow { min-width: 0; overflow: hidden; white-space: nowrap; }
/* on a narrow screen the title gives way, never a button's words */
.otk-panel__head .otk-btn { flex: none; }
.otk-panel__head > .otk-note { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* on a phone the subtitle gives way before the title does: the title is the
   panel's name, and neither would fit beside the buttons; the title's accent
   rule goes too, and the gaps close up, so the name holds whole beside
   three buttons */
@media (max-width: 600px) {
  .otk-panel__head { gap: 10px; }
  .otk-panel__head > .otk-note { display: none; }
  .otk-panel__head .otk-eyebrow { display: block; text-overflow: ellipsis; }
  .otk-panel__head .otk-eyebrow::before { display: none; }
}

.otk-tabs { flex: none; display: flex; align-items: center; gap: 36px; padding: 0 var(--otk-panel-pad); border-bottom: var(--otk-hair) solid var(--otk-line-soft); }
.otk-tab { font-family: var(--otk-font-display); font-size: 16px; font-weight: 600; letter-spacing: -0.01em; padding-bottom: 14px; color: var(--otk-muted); border-bottom: var(--otk-mark) solid transparent; }
.otk-tab[aria-selected="true"] { color: var(--otk-ink); border-bottom-color: var(--otk-accent); }
.otk-tabs__aside { margin-left: auto; padding-bottom: 14px; font-family: var(--otk-font-mono); font-size: var(--otk-size-small); color: var(--otk-faint); }
/* five tabs and the aside outrun a phone's width: the strip swipes sideways
   instead of clipping, the gaps drawn in */
@media (max-width: 600px) {
  .otk-tabs { gap: 22px; overflow-x: auto; scrollbar-width: none; }
  .otk-tabs::-webkit-scrollbar { display: none; }
  .otk-tab, .otk-tabs__aside { flex: none; white-space: nowrap; }
}

.otk-panel__body { flex: 1; min-height: 0; display: grid; }
.otk-panel__body--split   { grid-template-columns: 1.1fr 1fr; }
.otk-panel__body--models  { grid-template-columns: 2.15fr 1fr; }
.otk-panel__body--dossier { display: flex; overflow-x: auto; }

/* a column that both scrolls and pins something under itself */
.otk-col { min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
.otk-col--divided { border-right: var(--otk-hair) solid var(--otk-line-soft); }
.otk-col__scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-block: 8px; }
.otk-col__scroll:focus { outline: none; }
.otk-col__pinned { flex: 0 0 auto; display: flex; align-items: center; padding: 11px var(--otk-panel-pad); border-top: var(--otk-hair) solid var(--otk-line-soft); }

/* the filter belongs to the list it filters, and sits below it */
.otk-filter { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: var(--otk-font-prose); font-size: var(--otk-size-note); color: var(--otk-ink); background: var(--otk-paper); border: var(--otk-hair) solid var(--otk-line-soft); padding: var(--otk-field-pad); }
.otk-filter::placeholder { color: var(--otk-faint); opacity: 1; }
.otk-filter:focus { border-color: var(--otk-accent); outline: none; }

/* a group of rows under a provider or a scene */
.otk-group { display: flex; align-items: center; gap: 11px; padding: 26px var(--otk-panel-pad) 13px; margin: 0; }
.otk-group__name { font-family: var(--otk-font-display); font-size: var(--otk-size-head); font-weight: 800; letter-spacing: -0.02em; color: var(--otk-ink); }
.otk-group__count { margin-left: auto; font-family: var(--otk-font-mono); font-size: var(--otk-size-small); color: var(--otk-faint); }
.otk-group + .otk-rule { height: var(--otk-hair); background: var(--otk-ink); margin: 0 var(--otk-panel-pad) 7px; }

/* A row: selection is an accent rule plus a tint, never colour alone. Its
   height is DECLARED — every cell states the same line-height and the row a
   floor — or a list of mixed scripts ripples, Cyrillic standing taller than
   Latin. */
.otk-row { display: flex; align-items: baseline; gap: 14px; width: 100%; min-height: var(--otk-row-h); padding: 4.5px var(--otk-panel-pad) 1.5px; border-left: var(--otk-mark) solid transparent; }
.otk-row--indent { padding-left: 40px; }
.otk-row[aria-selected="true"] { border-left-color: var(--otk-accent); background: var(--otk-accent-tint); }
.otk-row__title { flex: 1 1 0%; min-width: 132px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--otk-font-prose); font-size: var(--otk-size-row); line-height: var(--otk-leading-row); color: var(--otk-body); }
/* a title with a note right after its words: the words give way, the note
   never does. The note is centred on the words' CAPITALS — the band from
   the cap line to the baseline — not on their line: the line's descender
   room lies below, so a box centred on it (or set on the baseline) hangs
   under the words. The band's centre sits a tenth of the prose size above
   the line's, hence the bottom margin of twice that. Words that are none
   take no room, so the note starts where they would. */
.otk-row__title--noted { display: flex; align-items: center; gap: 10px; }
.otk-row__title--noted > .otk-tag { margin-bottom: calc(var(--otk-size-row) * 0.2); }
.otk-row__text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.otk-row__text:empty { display: none; }
.otk-row__id { width: 26px; flex: none; text-align: right; font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); line-height: var(--otk-leading-row); color: var(--otk-faint); font-variant-numeric: tabular-nums; }
.otk-row__who { width: 58px; flex: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--otk-font-mono); font-size: var(--otk-size-label); line-height: var(--otk-leading-row); letter-spacing: 0.14em; text-transform: uppercase; color: var(--otk-faint); }
.otk-row__who--you { color: var(--otk-accent-ink); }
.otk-row__num { flex: none; text-align: right; font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); line-height: var(--otk-leading-row); color: var(--otk-muted); font-variant-numeric: tabular-nums; }
.otk-row--mono .otk-row__title { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono); }
.otk-row[aria-selected="true"] .otk-row__title { color: var(--otk-ink); }
/* loaded is bold, unloaded is dim — only where loading is a thing that happens */
.otk-row.is-loaded .otk-row__title { font-weight: 600; color: var(--otk-ink); }
.otk-row.is-dim .otk-row__title { color: var(--otk-muted); }
.otk-row__lamp { width: var(--otk-col-lamp); flex: none; display: flex; align-items: center; }
/* a two-line row, a name over a sub-line: even padding, so the row's centre
   is its content's; the lamp as tall as the name's line and set at the top,
   so the dot centres on the name rather than hanging off the baseline; the
   caption centred on the whole row */
.otk-row--stack { padding-top: 3px; padding-bottom: 3px; }
.otk-row--stack .otk-row__lamp { align-self: flex-start; height: calc(var(--otk-size-note) * var(--otk-leading-row)); }
.otk-row--stack .otk-row__num { align-self: center; }
.otk-stack > .otk-choice__name { line-height: var(--otk-leading-row); }
.otk-row__num--size  { width: var(--otk-col-size); }
.otk-row__num--count { width: var(--otk-col-count); }
.otk-row__num--ago   { width: var(--otk-col-ago); }
/* a phone's row: the indent and the gaps close up, and a hosted model's
   empty size column gives its width to the name — the rows of one group
   still line up, every hosted model lacking the same column; a local one's
   empty count keeps its width so its size stays in line */
@media (max-width: 600px) {
  .otk-row { gap: 10px; }
  .otk-row--indent { padding-left: 24px; }
  .otk-row__num--size:empty { display: none; }
}

/* the index rail of a story: scenes, or cast */
.otk-index { flex: 0 0 236px; min-width: 150px; min-height: 0; overflow: hidden; display: flex; flex-direction: column; padding: 14px 0; background: var(--otk-band); border-right: var(--otk-hair) solid var(--otk-line); }
.otk-index__head { flex: 0 0 auto; display: flex; align-items: baseline; gap: 9px; padding: 0 16px 9px; }
.otk-index__list { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; padding: 0 8px; }
.otk-index__item { display: flex; flex-direction: column; gap: 2px; width: 100%; padding: 9px 8px; border-left: var(--otk-mark) solid transparent; }
.otk-index__item[aria-selected="true"] { border-left-color: var(--otk-accent); background: var(--otk-paper); }
.otk-index__item--pending { border: var(--otk-hair) dashed var(--otk-line-soft); margin-top: 4px; }
.otk-index__title { font-family: var(--otk-font-prose); font-size: var(--otk-size-row); line-height: var(--otk-leading-row); color: var(--otk-body); }
.otk-index__item[aria-selected="true"] .otk-index__title { color: var(--otk-ink); }
.otk-index__sub { font-family: var(--otk-font-mono); font-size: var(--otk-size-label); line-height: var(--otk-leading-row); color: var(--otk-faint); }
.otk-index__note { flex: 0 0 auto; margin: 12px 16px 0; padding-top: 11px; border-top: var(--otk-hair) solid var(--otk-line-soft); }

/* the extraction pass: a background job, so it needs a state and not a button */
.otk-extract { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-start; gap: 7px; margin: 12px 16px 0; padding-top: 11px; border-top: var(--otk-hair) solid var(--otk-line-soft); }
.otk-extract__line { display: flex; align-items: center; gap: 8px; }
.otk-extract__state { font-family: var(--otk-font-mono); font-size: var(--otk-size-label); letter-spacing: 0.06em; color: var(--otk-muted); }
.otk-extract .otk-btn { align-self: stretch; justify-content: center; }

/* ASK. A tool-call question, posed in the transcript where a reply would
   land. Accent rule, not danger — the reader is being invited in, not
   warned. Clicking an option IS the answer and sends it; each option can
   instead be copied to the composer to edit first; and any typed line
   answers it too, so typing takes the panel down. */
/* The block's leading is its own, never the place's: it stands among the
   prose (a past question, the notes), beside it (the live one) and in the
   canvas's articles, which each carry a leading of their own, and its label,
   numbers and foot must measure the same rows in every one of them. */
.otk-ask { display: flex; flex-direction: column; gap: 12px; line-height: normal; border-left: var(--otk-mark) solid var(--otk-accent); padding: 2px 0 2px 22px; }
.otk-ask__label { font-family: var(--otk-font-mono); font-size: var(--otk-size-label); letter-spacing: var(--otk-track-label); text-transform: uppercase; color: var(--otk-accent-ink); }
.otk-ask__body { margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-page); line-height: var(--otk-leading-page); color: var(--otk-ink); text-wrap: pretty; }
.otk-ask__options { display: flex; flex-direction: column; }
/* a numbered line: digit in the accent gutter, text sends, edit verb trails */
.otk-ask__option { display: flex; align-items: baseline; gap: 14px; padding: 9px 0; border-top: var(--otk-hair) solid var(--otk-line-soft); }
.otk-ask__option:last-of-type { border-bottom: var(--otk-hair) solid var(--otk-line-soft); }
.otk-ask__n { flex: none; font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); color: var(--otk-accent-ink); }
.otk-ask__pick { flex: 1; min-width: 0; text-align: start; font-family: var(--otk-font-prose); font-size: var(--otk-size-read); line-height: 1.5; color: var(--otk-body); text-wrap: pretty; }
.otk-ask__option:hover .otk-ask__pick { color: var(--otk-ink); }
.otk-ask__copy { flex: none; font-family: var(--otk-font-mono); font-size: var(--otk-size-label); letter-spacing: 0.06em; text-transform: uppercase; color: var(--otk-faint); white-space: nowrap; }
.otk-ask__copy:hover { color: var(--otk-accent-ink); }
.otk-ask__foot { font-family: var(--otk-font-prose); font-size: var(--otk-size-note); font-style: italic; color: var(--otk-faint); }
/* the live one sits under the reply's last line, inside its article */
.otk-ask--live { margin-top: 0; }  /* a block of the reply's: its gap is the article's */
/* the model's notes: the question's shape, its text a size down, and the
   line breaks the notes' own */
.otk-ask--notes { border-left-color: var(--otk-faint); }
/* a block among the reply's text stands the one gap from whatever is on either
   side of it; one that opens or closes the reply (`replyNodes` marks which)
   leaves the article's own gap to do the spacing */
.otk-typeset > .otk-ask { margin: var(--otk-block-gap) 0; }
.otk-typeset > .otk-ask--first { margin-top: 0; }
.otk-typeset > .otk-ask--last { margin-bottom: 0; }
.otk-ask--notes .otk-ask__body { font-size: 15px; white-space: pre-wrap; }
/* a question already answered: the same shape, quieter, and no verbs */
.otk-ask--past { border-left-color: var(--otk-line); }
.otk-ask--past .otk-ask__label, .otk-ask--past .otk-ask__n { color: var(--otk-faint); }
.otk-ask--past .otk-ask__pick { color: var(--otk-muted); cursor: default; }

/* the Tools pane: one capped column that scrolls, like the premise */
.otk-panel__body--tools { display: block; overflow-y: auto; scrollbar-gutter: stable; padding: 26px 40px 20px; }
.otk-tools { width: 100%; max-width: 940px; margin-inline: auto; }
.otk-tools__head { display: flex; flex-direction: column; border-bottom: var(--otk-hair) solid var(--otk-line); }
/* one feature: a switch, what it does, and the one control it governs */
.otk-features { display: flex; flex-direction: column; }
/* the tools, a rule, then the reminders: the group break is a firmer line than the rows' own */
.otk-features__rule { border: 0; border-top: var(--otk-hair) solid var(--otk-line); margin: 8px 0; width: 100%; }
.otk-features__rule + .otk-feature { border-top: 0; }
.otk-feature { display: flex; flex-direction: column; gap: 12px; padding: 18px 0; border-top: var(--otk-hair) solid var(--otk-line-soft); }
.otk-feature:first-child { border-top: 0; }
.otk-feature__head { display: flex; align-items: flex-start; gap: 14px; }
.otk-feature__text { display: flex; flex-direction: column; gap: 11px; min-width: 0; }
.otk-feature__name { font-family: var(--otk-font-display); font-size: var(--otk-size-note); font-weight: 600; color: var(--otk-ink); }
.otk-feature__note { margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-note); font-style: italic; line-height: 1.5; color: var(--otk-faint); }
.otk-feature__control { flex: none; margin-left: auto; display: flex; align-items: center; gap: 10px; }
.otk-feature__knobs { display: inline-flex; align-items: center; gap: 10px; }
/* the name's line: the name, and a second switch the row may carry after it */
.otk-feature__title { display: flex; align-items: center; gap: 24px; }
.otk-feature__shown { display: inline-flex; align-items: center; gap: 14px; }
/* the second switch's knob takes the notes block's own rule colour, so
   the switch and what it shows read as one thing */
.otk-feature__shown .otk-switch--h[aria-checked="true"] .otk-switch__knob,
.otk-feature__shown .otk-switch:hover .otk-switch__knob { background: var(--otk-faint); }
/* off, the label steps back with it */
.otk-feature__shown .otk-switch[aria-checked="false"] + .otk-feature__shown-label { color: var(--otk-faint); }
/* the row's own face for the name, as the feature's name wears it */
.otk-feature__shown-label { font-family: var(--otk-font-display); font-size: var(--otk-size-ui); font-weight: 500; color: var(--otk-ink); }
.otk-feature__body { padding-left: 48px; }
/* both reminders hold the same box whatever is written in them, so the
   two rows are the same height and the verbs sit on one line; the box
   takes the column's measure, as the premise's does */
.otk-feature__body .otk-edit { max-width: none; }
.otk-feature__body .otk-editable { min-height: 93px; max-height: 150px; overflow-y: auto; }
/* the verbs close the row, so the row's own padding comes off under them */
.otk-feature:has(.otk-feature__body) { padding-bottom: 10px; }
/* off: what the switch governs steps back. The switch is the unambiguous
   statement of which way it is set, so dimming here is a second signal. */
.otk-feature[data-on="false"] .otk-feature__name,
.otk-feature[data-on="false"] .otk-feature__note { color: var(--otk-faint); }
.otk-feature[data-on="false"] .otk-feature__control,
.otk-feature[data-on="false"] .otk-feature__shown,
.otk-feature[data-on="false"] .otk-feature__body { opacity: 0.45; pointer-events: none; }
/* the depth: a figure in a right-hand column, not a button to look at — a
   hairline sets it off, a small head names it, and every row's value lines
   up down the page at one width. Off, it keeps its figure in faint ink. */
.otk-depth {
  flex: none; width: 96px; align-self: stretch;
  display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 3px;
  margin-left: 4px; padding: 0 0 0 16px; border: 0; border-left: var(--otk-hair) solid var(--otk-line-soft);
  background: none; cursor: pointer; text-align: right;
}
.otk-depth__head { font-family: var(--otk-font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--otk-faint); }
.otk-depth__value { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono); color: var(--otk-body); white-space: nowrap; border-bottom: var(--otk-hair) dotted var(--otk-line); }
.otk-depth:hover .otk-depth__value { color: var(--otk-ink); border-bottom-color: var(--otk-ink); }
.otk-feature[data-on="false"] .otk-depth__value { color: var(--otk-faint); }
/* a phone has no room for three columns: the switch and the words share the
   first line, the controls take a line of their own under them, indented to
   the words; a reminder's text runs the full width */
@media (max-width: 600px) {
  .otk-feature__head { flex-wrap: wrap; }
  .otk-feature__text { flex: 1 1 0; }
  .otk-feature__title { flex-wrap: wrap; gap: 10px 24px; }
  .otk-feature__control { flex: 0 0 100%; margin-left: 0; padding-left: 48px; }
  /* a button keeps its words on one line, and the depth takes a line of
     its own under the buttons, as it does where it stands alone */
  .otk-feature__knobs { flex-wrap: wrap; row-gap: 10px; }
  .otk-feature__knobs .otk-btn { white-space: nowrap; }
  .otk-feature__knobs > .otk-depth { flex: 0 0 100%; margin-left: 0; padding-left: 0; border-left: 0; align-items: flex-start; text-align: left; }
  /* the depth's hairline parts it from a knob before it; first on its line,
     there is nothing to part it from */
  .otk-feature__knobs > .otk-depth:first-child { margin-left: 0; padding-left: 0; border-left: 0; align-items: flex-start; text-align: left; }
  .otk-feature__body { padding-left: 0; }
}

/* The depth ruler, in the Injection depths dialog: one row per setting, one
   column per place — the system message, then the reader's messages counted
   from the end, and the newest last, where nothing may go. The page sets
   `--otk-ruler-slots` (the columns after the system one) from the list the
   backend offers, so the grid and the track line follow it. A place a row
   may not take shows a dash. */
.otk-ruler { display: flex; flex-direction: column; gap: 6px; }
.otk-ruler__row { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 12px; align-items: center; }
.otk-ruler__name { display: flex; align-items: baseline; gap: 8px; font-family: var(--otk-font-display); font-size: var(--otk-size-ui); font-weight: 600; color: var(--otk-ink); }
.otk-ruler__off { display: none; font-family: var(--otk-font-mono); font-size: var(--otk-size-foot); font-weight: 400; letter-spacing: 0.08em; text-transform: uppercase; color: var(--otk-faint); }
.otk-ruler__track { --otk-ruler-gap: 10px; position: relative; height: 28px; display: grid; grid-template-columns: 1fr var(--otk-ruler-gap) repeat(var(--otk-ruler-slots), 1fr); align-items: center; }
.otk-ruler__track > :nth-child(2) { grid-column-start: 3; }
/* the track line runs from the middle of the first column to the middle of
   the last: half a column in from either edge, the gap set aside */
.otk-ruler__row:not(.otk-ruler__row--head) .otk-ruler__track::before { content: ""; position: absolute; left: calc((100% - var(--otk-ruler-gap)) / (2 * (var(--otk-ruler-slots) + 1))); right: calc((100% - var(--otk-ruler-gap)) / (2 * (var(--otk-ruler-slots) + 1))); top: 50%; border-top: var(--otk-hair) solid var(--otk-line); }
.otk-ruler__row.otk-ruler__row--nosys:not(.otk-ruler__row--head) .otk-ruler__track::before { left: calc((100% - var(--otk-ruler-gap)) * 3 / (2 * (var(--otk-ruler-slots) + 1)) + var(--otk-ruler-gap)); }
.otk-ruler__row--head .otk-ruler__track { height: auto; font-family: var(--otk-font-mono); font-size: var(--otk-size-label); letter-spacing: 0.06em; text-transform: uppercase; color: var(--otk-muted); text-align: center; }
.otk-ruler__last { color: var(--otk-faint); }
.otk-ruler__cell { position: relative; justify-self: center; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; }
.otk-ruler__cell--no { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); color: var(--otk-line); }
.otk-ruler__mark { width: 6px; height: 6px; border-radius: 50%; background: var(--otk-line); }
button.otk-ruler__cell:not(:disabled):hover .otk-ruler__mark { background: var(--otk-muted); }
.otk-ruler__cell[aria-pressed="true"] .otk-ruler__mark { width: 14px; height: 14px; background: var(--otk-accent); }
/* a phone has no room for a name beside ten columns: each setting's name
   stands over its track, the scale over the first, and the tracks take the
   dialog's whole width */
@media (max-width: 600px) {
  .otk-ruler { gap: 12px; }
  .otk-ruler__row { grid-template-columns: 1fr; gap: 2px; }
  .otk-ruler__row--head > :first-child { display: none; }
}
/* the marker is dragged along its track, snapping to the places it may take */
.otk-ruler__row:not(.otk-ruler__row--head) .otk-ruler__track { touch-action: none; cursor: pointer; user-select: none; }
.otk-ruler__cell[aria-pressed="true"] { cursor: grab; }
.otk-ruler__row[data-dragging] .otk-ruler__track, .otk-ruler__row[data-dragging] .otk-ruler__cell { cursor: grabbing; }
.otk-ruler__row[data-dragging] .otk-ruler__cell[aria-pressed="true"] .otk-ruler__mark { box-shadow: 0 0 0 4px var(--otk-accent-tint); }
/* an off setting: dashed track, hollow marker, the word "off" — and not
   moved from here, so nothing on its row invites a hand */
.otk-ruler__row[data-on="false"] .otk-ruler__track, .otk-ruler__row[data-on="false"] .otk-ruler__cell { cursor: default; }
.otk-ruler__row[data-on="false"] .otk-ruler__name { color: var(--otk-faint); }
.otk-ruler__row[data-on="false"] .otk-ruler__off { display: inline; }
.otk-ruler__row[data-on="false"] .otk-ruler__track::before { border-top-style: dashed; }
.otk-ruler__row[data-on="false"] .otk-ruler__cell[aria-pressed="true"] .otk-ruler__mark { background: var(--otk-paper); border: 1.5px solid var(--otk-faint); }
.otk-field--tall { min-height: 378px; resize: vertical; line-height: 1.55; }

/* the reading column, and the apparatus margin beside it */
.otk-reading { flex: 1 1 0%; min-width: 300px; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; padding: 20px 30px; }
.otk-reading__title { margin: 10px 0 14px; font-family: var(--otk-font-display); font-size: 29px; font-weight: 800; letter-spacing: -0.025em; line-height: 1.1; color: var(--otk-ink); }
.otk-reading__title--lg { font-size: var(--otk-size-display); }
.otk-reading__lede { margin: 0 0 4px; font-family: var(--otk-font-prose); font-size: var(--otk-size-lead); line-height: 1.68; color: var(--otk-body); border-bottom: var(--otk-hair) dotted var(--otk-line); padding-bottom: 8px; text-wrap: pretty; }
.otk-passages { display: flex; flex-direction: column; gap: 20px; border-top: var(--otk-hair) solid var(--otk-line); padding-top: 14px; }
.otk-passage { display: flex; flex-direction: column; gap: 4px; min-width: 0; max-width: var(--otk-measure); }
.otk-passage .otk-note { font-size: var(--otk-size-lead); line-height: 1.6; }
.otk-passage__rubric { font-family: var(--otk-font-mono); font-size: var(--otk-size-label); letter-spacing: var(--otk-track-tag); text-transform: uppercase; color: var(--otk-accent-ink); }
.otk-passage__body { margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-read); line-height: 1.66; color: var(--otk-body); overflow-wrap: anywhere; }
.otk-margin { flex: 0 1 var(--otk-reader-w); min-width: 180px; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; padding: 20px 18px; border-left: var(--otk-hair) solid var(--otk-line); }
.otk-margin__fact { display: flex; flex-direction: column; gap: 3px; }
/* Some values are paragraphs (one wraps, and `present` carries a button per
   name), so the class owns its box: left alone, the UA's `1em 0` stands every
   fact a whole line off its key. */
.otk-margin__value { margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-lead); line-height: 1.5; color: var(--otk-body); }
.otk-margin__value--mono { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono); }
.otk-margin__key { font-family: var(--otk-font-mono); font-size: var(--otk-size-label); letter-spacing: 0.1em; text-transform: uppercase; color: var(--otk-faint); }
.otk-margin__block { padding-top: 10px; border-top: var(--otk-hair) solid var(--otk-line-soft); }
/* A derived value stands off its key where a plain fact sits tight under it:
   the ruled inset is a quotation from the extractor and takes a little air to
   read as one — a little: the column's own gap does the separating. Scoped
   here — `.otk-derived` is also a row in the message detail and a block in a
   report, and neither wants the space. */
.otk-margin__fact > .otk-derived { margin: 8px 0; }
/* what you do TO the character, pinned under what is known about it */
.otk-manage { flex: none; display: flex; flex-direction: column; gap: 8px; padding-top: 14px; border-top: var(--otk-hair) solid var(--otk-line-soft); }
.otk-manage__row { display: flex; flex-wrap: wrap; gap: 8px; }

/* the detail pane of a panel: a body that scrolls, and verbs that do not */
.otk-detail { min-height: 0; overflow: hidden; display: flex; flex-direction: column; padding: 22px var(--otk-panel-pad) 14px; }
.otk-detail--reader { flex: 0 1 var(--otk-reader-w); min-width: 300px; }
.otk-detail__scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; }
.otk-detail__head { display: flex; align-items: baseline; gap: 10px; }
.otk-detail__title { margin: 12px 0 10px; font-family: var(--otk-font-display); font-size: var(--otk-size-title); font-weight: 800; letter-spacing: -0.02em; color: var(--otk-ink); overflow-wrap: anywhere; }
.otk-detail__section { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; padding-top: 14px; border-top: var(--otk-hair) solid var(--otk-line-soft); }
.otk-fact { display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr); gap: 12px; padding: 7px 0; border-bottom: var(--otk-hair) solid var(--otk-line-soft); font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); margin: 0; }
.otk-fact:last-child { border-bottom: 0; }
.otk-fact > :first-child { color: var(--otk-muted); }
.otk-fact > :last-child { color: var(--otk-body); overflow-wrap: anywhere; text-align: right; }
.otk-fact--clip { gap: 36px; }   /* a clipped value stands clear of its key */
.otk-fact--clip > :last-child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; overflow-wrap: normal; }
/* pinned to the foot of its pane: a taller or shorter body above never moves
   the buttons */
.otk-actions { flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: auto; padding-top: 14px; border-top: var(--otk-hair) solid var(--otk-line-soft); }
.otk-actions__note { flex-basis: 100%; margin: 0; }

/* one footer, identical in every panel */
.otk-panel__foot { flex: none; display: flex; align-items: center; gap: 20px; padding: 12px var(--otk-panel-pad); background: var(--otk-band); border-top: var(--otk-hair) solid var(--otk-line-soft); font-family: var(--otk-font-mono); font-size: var(--otk-size-foot); letter-spacing: var(--otk-track-foot); text-transform: uppercase; color: var(--otk-faint); }
.otk-panel__foot > span + span { margin-left: auto; text-align: right; }
.otk-panel__foot > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* the premise: no index, one measure */
.otk-panel__body--premise { display: flex; justify-content: center; padding: 26px 40px 20px; }
.otk-premise { width: var(--otk-premise-w); max-width: 100%; display: flex; flex-direction: column; }
.otk-premise__lede { margin-bottom: 18px; }
.otk-premise__body {
  flex: 1; min-height: 0; width: 100%; resize: none; overflow-y: auto;
  border: 0; border-radius: 0; outline: none; background: none; padding: 0; margin: 0;
  font-family: var(--otk-font-prose); font-size: var(--otk-size-lead); line-height: 1.74; color: var(--otk-ink);
}
/* the field fills the pane the way the bare textarea did, and the edit
   pair inside it takes the premise measure, not the reading one. The
   rule under the lede is the field's box's, at the level the tools
   pane draws its own. */
.otk-premise__field { flex: 1; min-height: 0; display: flex; flex-direction: column; border-top: var(--otk-hair) solid var(--otk-line); padding-top: 18px; }
.otk-premise__field .otk-edit { flex: 1; min-height: 0; max-width: none; }
.otk-premise__field .otk-reader { flex: 1; min-height: 0; }

/* context: the request as a document, its stages as live figures */
.otk-context { display: flex; flex-direction: column; gap: 22px; flex: 1; min-height: 0; overflow-y: auto; padding: 2px 30px 26px; }
.otk-context__summary { display: flex; align-items: baseline; gap: 20px; flex-wrap: wrap; }
.otk-context__lead { display: inline-flex; align-items: baseline; gap: 10px; }
.otk-context__lead .otk-context__figure { font-size: 30px; }
.otk-context__stages { display: flex; border: var(--otk-hair) solid var(--otk-ink); background: var(--otk-band); }
/* the injections, bracketed between the stages and an ink rule: a section
   head with the total, then fixed depth and token columns so every row lines up */
.otk-context__injected { display: flex; flex-direction: column; gap: 6px; padding-bottom: 14px; border-bottom: var(--otk-hair) solid var(--otk-ink); margin-bottom: 6px; }
.otk-context__injrow { display: grid; grid-template-columns: auto minmax(24px, 1fr) 84px 118px; column-gap: 8px; align-items: baseline; padding: 3px 0; }
/* the head is a section head without its rule: the rows follow it at the
   head's 8px margin plus the column's 6px gap, and stand 14px of padding over
   the closing rule, each plus the row's own 3px */
.otk-context__injhead { padding: 0; margin-bottom: 8px; border-bottom: 0; }
.otk-context__injname { font-family: var(--otk-font-prose); font-size: var(--otk-size-read); color: var(--otk-body); }
.otk-context__injdots { border-bottom: var(--otk-hair) dotted var(--otk-line); translate: 0 -4px; }
.otk-context__injat, .otk-context__injtok { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono); font-weight: 400; letter-spacing: 0; color: var(--otk-muted); font-variant-numeric: tabular-nums; }
.otk-context__injtok { text-align: right; }
.otk-context__injhead .otk-context__injtok { color: var(--otk-body); }
.otk-context__cell { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 5px; padding: 18px 14px; background: var(--otk-paper); }
.otk-context__cell + .otk-context__cell { border-left: var(--otk-hair) solid var(--otk-line); }
.otk-context__cell--idle { background: none; }
.otk-context__figure { font-family: var(--otk-font-display); font-size: 28px; font-weight: 800; letter-spacing: -0.02em; color: var(--otk-ink); font-variant-numeric: tabular-nums; }
.otk-context__cell--idle .otk-context__figure { font-size: 24px; color: var(--otk-faint); }
.otk-context__tok { font-family: var(--otk-font-mono); font-size: var(--otk-size-label); color: var(--otk-accent-ink); }
.otk-context__cell--idle .otk-context__tok { color: var(--otk-faint); }
/* a phone: the four stages in two rows of two, the injected rows' fixed
   columns narrowed so a name holds one line */
@media (max-width: 600px) {
  .otk-context__stages { flex-wrap: wrap; }
  .otk-context__cell { flex: 1 1 50%; }
  .otk-context__cell + .otk-context__cell { border-left: 0; }
  .otk-context__cell:nth-child(2n) { border-left: var(--otk-hair) solid var(--otk-line); }
  .otk-context__cell:nth-child(n+3) { border-top: var(--otk-hair) solid var(--otk-line); }
  .otk-context__injrow { grid-template-columns: auto minmax(12px, 1fr) auto auto; column-gap: 8px; }
  .otk-context__injname { font-size: var(--otk-size-ui); white-space: nowrap; }
  .otk-context__injat, .otk-context__injtok { white-space: nowrap; font-size: 13px; }
}
.otk-context__wire { display: flex; flex-direction: column; gap: 20px; padding-top: 6px; }

/* =============================================================================
   DOCKET — a torn slip for every small report
   ========================================================================== */
/* a slip is torn to length: as tall as what it has to say, bounded by the desk */
.otk-docket { display: flex; flex-direction: column; width: var(--otk-docket-w); max-height: calc(100dvh - 80px); max-width: calc(100vw - 32px); background: var(--otk-paper); box-shadow: var(--otk-shadow-panel); }
.otk-docket--narrow { --otk-docket-w: 416px; }
.otk-docket--wide   { --otk-docket-w: 568px; }
/* …except balance, whose figures arrive one per provider after the slip is
   open: a slip that grows under the pointer has moved what was clicked */
.otk-docket--fixed  { height: min(var(--otk-docket-h), calc(100dvh - 80px)); }
/* A slip whose leaders clip: the label keeps its width, the value never
   wraps and is cut with an ellipsis where the slip is too narrow for it
   — as the picker's detail clips its facts. */
.otk-docket--clip .otk-leader > :first-child { flex: none; }
.otk-docket--clip .otk-leader::after { min-width: 36px; }   /* a run of dots always stands between key and value */
.otk-docket--clip .otk-leader > :last-child { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* between the click and the figures: the scrim answers, and the slip arrives
   whole rather than opening small and growing */
.otk-docket.is-waiting { visibility: hidden; }
.otk-docket__tear { flex: 0 0 auto; height: 9px; background: radial-gradient(circle at 6px 0, transparent 0 5px, var(--otk-paper) 5.5px) 0 0/12px 9px repeat-x; }
.otk-docket__tear--bottom { background: radial-gradient(circle at 6px 9px, transparent 0 5px, var(--otk-paper) 5.5px) 0 0/12px 9px repeat-x; }
.otk-docket__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 17px; padding: 8px 34px 16px; }
.otk-docket__body:focus { outline: none; }
.otk-docket__foot { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; margin: 0 34px; padding: 14px 0; border-top: var(--otk-hair) solid var(--otk-line); }
.otk-docket__foot .otk-btn:last-child { margin-left: auto; }
/* two buttons stand together at the right: the first takes the push */
.otk-docket__foot .otk-btn:nth-last-child(2) { margin-left: auto; }
.otk-docket__foot .otk-btn + .otk-btn { margin-left: 0; }
/* a multi-line field: same box as .otk-field, sized in lines, grows by hand */
.otk-field--area { min-height: 140px; max-height: 320px; resize: vertical; line-height: 1.5; font-family: var(--otk-font-prose); font-size: var(--otk-size-read); }
.otk-docket__note { font-family: var(--otk-font-mono); font-size: var(--otk-size-foot); letter-spacing: var(--otk-track-foot); text-transform: uppercase; color: var(--otk-faint); }
.otk-docket__row { display: flex; align-items: baseline; gap: 10px; }
.otk-docket__name { flex: 1; min-width: 0; overflow-wrap: anywhere; font-family: var(--otk-font-display); font-size: var(--otk-size-note); font-weight: 600; color: var(--otk-ink); }
.otk-docket__title { font-family: var(--otk-font-display); font-size: 24px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.15; color: var(--otk-ink); overflow-wrap: anywhere; }
.otk-docket__story { font-family: var(--otk-font-prose); font-size: 21px; line-height: 1.3; color: var(--otk-ink); }
.otk-tabs--docket { gap: 22px; padding: 0; border-bottom: var(--otk-hair) solid var(--otk-line); }
.otk-rule { height: var(--otk-hair); background: var(--otk-line); }
.otk-rule--double { display: flex; flex-direction: column; gap: 3px; margin-top: 8px; }
.otk-rule--double::before, .otk-rule--double::after { content: ""; height: var(--otk-hair); background: var(--otk-ink); }
.otk-total { display: flex; align-items: baseline; gap: 12px; }
.otk-total__figure { margin-left: auto; font-family: var(--otk-font-display); font-size: 33px; font-weight: 800; letter-spacing: -0.02em; color: var(--otk-ink); font-variant-numeric: tabular-nums; }

/* =============================================================================
   DIALOG — one decision, 440px, always dismissible
   ========================================================================== */
.otk-dialog { width: min(var(--otk-dialog-w), calc(100vw - 32px)); display: flex; flex-direction: column; gap: 16px; padding: var(--otk-dialog-pad); background: var(--otk-paper); border: var(--otk-hair) solid var(--otk-line); box-shadow: var(--otk-shadow-panel); }
/* an instruction is paragraphs: the dialog widens. After the base rule,
   which would otherwise win at equal specificity. */
.otk-dialog--wide { width: min(930px, calc(100vw - 48px)); }
.otk-dialog__subject { display: flex; flex-direction: column; gap: 3px; }
.otk-dialog__name { font-family: var(--otk-font-display); font-size: 21px; font-weight: 800; letter-spacing: -0.02em; color: var(--otk-ink); overflow-wrap: anywhere; }
.otk-dialog__body { margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-note); line-height: 1.6; color: var(--otk-body); text-wrap: pretty; }
.otk-dialog__foot { display: flex; align-items: center; gap: 8px; padding-top: 14px; border-top: var(--otk-hair) solid var(--otk-line-soft); }
.otk-dialog__foot .otk-btn:first-of-type { margin-left: auto; }
.otk-dialog__escape { font-family: var(--otk-font-mono); font-size: var(--otk-size-foot); letter-spacing: var(--otk-track-foot); text-transform: uppercase; color: var(--otk-faint); }
/* the dialog that stands in FRONT of the page rather than over it: the
   desk flat behind it, because what it covers cannot be used yet */
.otk-dialog--bare::backdrop { background: var(--otk-desk); backdrop-filter: none; }
/* a label over its field, and under it a refusal when there is one */
.otk-field-stack { display: flex; flex-direction: column; gap: 6px; }
.otk-field-error { margin: 0; color: var(--otk-danger); }
/* a squared checkbox — nothing on this page is round but the dots */
.otk-check-row { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.otk-check { appearance: none; position: relative; width: 15px; height: 15px; margin: 0; flex: none; background: var(--otk-paper); border: var(--otk-hair) solid var(--otk-line); cursor: pointer; }
.otk-check::before, .otk-check::after { content: ""; position: absolute; left: 1px; top: 6px; width: 11px; height: 2px; background: var(--otk-ink); opacity: 0; }
.otk-check::before { transform: rotate(45deg); }
.otk-check::after { transform: rotate(-45deg); }
.otk-check:checked { border-color: var(--otk-ink); }
.otk-check:checked::before, .otk-check:checked::after { opacity: 1; }
/* a choice between two outcomes is two boxes, not two buttons */
.otk-choices { display: flex; flex-direction: column; gap: 8px; }
.otk-choice { display: flex; flex-direction: column; gap: 2px; border: var(--otk-hair) solid var(--otk-line); padding: 11px 14px; }
.otk-choice[aria-checked="true"] { border-color: var(--otk-accent); background: var(--otk-accent-tint); }
.otk-choice__name { font-family: var(--otk-font-display); font-size: var(--otk-size-note); font-weight: 600; color: var(--otk-ink); }
.otk-choice__desc { font-family: var(--otk-font-prose); font-size: var(--otk-size-note); color: var(--otk-muted); }
.otk-dropzone { display: flex; flex-direction: column; align-items: center; gap: 7px; text-align: center; padding: 34px 20px; background: var(--otk-well); border: var(--otk-hair) dashed var(--otk-line); cursor: pointer; }
.otk-dropzone__label { font-family: var(--otk-font-prose); font-size: var(--otk-size-lead); color: var(--otk-body); }
.otk-dropzone__file { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono); color: var(--otk-ink); overflow-wrap: anywhere; }
.otk-dropzone.is-dragover { border-color: var(--otk-accent); background: var(--otk-accent-tint); }
.otk-dropzone.is-filled { border-style: solid; }
.otk-dialog__field { display: flex; align-items: center; gap: 14px; }
.otk-dialog__field > .otk-label { flex: none; }
.otk-outcomes { display: flex; flex-direction: column; gap: 6px; }
.otk-outcome { margin: 0; font-family: var(--otk-font-prose); font-size: var(--otk-size-note); line-height: 1.5; color: var(--otk-body); border-left: var(--otk-hair) solid var(--otk-line); padding-left: 11px; }

/* =============================================================================
   THE UTILITY LAYER — small, named, and closed: every one of these exists
   because two or more components need it, and no markup carries a style
   attribute.
   ========================================================================== */

/* a vertical stack on a four-step scale */
.otk-v { display: flex; flex-direction: column; }
.otk-v--xs { gap: 3px; }
.otk-v--sm { gap: 6px; }
.otk-v--md { gap: 10px; }
.otk-v--lg { gap: 14px; }

/* ink and emphasis */
.otk-faint { color: var(--otk-faint); }
.otk-accent-ink { color: var(--otk-accent-ink); }
.otk-label--accent { color: var(--otk-accent-ink); }
.otk-code { font-family: var(--otk-font-mono); font-style: normal; }
.otk-hr { height: var(--otk-hair); background: var(--otk-line); }
.otk-push { margin-left: auto; }
/* a wiring hook that must not be a box */
.otk-slot { display: contents; }

/* a value that is absent, default, or not yet written — always said in words */
.otk-absent { font-family: var(--otk-font-prose); font-style: italic; color: var(--otk-faint); }

/* a typed key, and a row of them against its meaning */
.otk-key { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); color: var(--otk-ink); border: var(--otk-hair) solid var(--otk-line); padding: 3px 9px; }
.otk-keys { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
/* The think ladder: the keys' row, allowed to wrap onto more when the
   leader is too narrow for every level — flush right, where the value is. */
.otk-ladder { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 5px; min-width: 0; }
/* left only: it stands off the key box it follows, and stays with the key
   or the argument shape it introduces */
.otk-keys__plus { padding-left: 6px; font-family: var(--otk-font-mono); font-size: var(--otk-size-label); color: var(--otk-faint); }
.otk-keys-grid { display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: 11px 30px; align-items: baseline; }
.otk-keys__meaning { font-family: var(--otk-font-prose); font-size: var(--otk-size-read); color: var(--otk-muted); }
/* A key box sits on its row's baseline and hangs below it, so the help
   sheet's last row needs room of its own above the docket's foot. */
[data-help-body] > :last-child { margin-bottom: 16px; }

/* inside a docket */
.otk-leader__aside { align-self: flex-end; font-family: var(--otk-font-mono); font-size: var(--otk-size-small); color: var(--otk-faint); }
/* one step of a ladder written into an aside — the value it would set */
.otk-toggle:hover { color: var(--otk-accent-ink); }
.otk-toggle[aria-checked="true"] { color: var(--otk-ink); font-weight: 500; }
.otk-toggle { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono); color: var(--otk-faint); }

/* prose variants */
.otk-prose--read { font-size: var(--otk-size-read); line-height: var(--otk-leading-read); }
.otk-prose--lead { font-size: var(--otk-size-lead); line-height: 1.55; color: var(--otk-ink); }
.otk-prose--margin { font-size: var(--otk-size-lead); line-height: 1.6; }
.otk-prose--wire { max-width: none; white-space: pre-wrap; }
.otk-prose--said { max-width: none; font-style: italic; color: var(--otk-ink); white-space: pre-wrap; }
.otk-note--push { margin-top: auto; }
.otk-meta { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); color: var(--otk-muted); }
.otk-stack { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }

/* the models header's memory reading */
.otk-memory { margin-left: auto; display: inline-flex; align-items: baseline; gap: 8px; }
.otk-memory__value { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); color: var(--otk-body); }
/* the readout has no room on a phone between the title and the close */
@media (max-width: 600px) {
  .otk-memory { display: none; }
}

.otk-col--messages { flex: 1 1 0%; min-width: 300px; padding: 12px 22px; }
.otk-col--messages .otk-row { padding-left: 14px; padding-right: 14px; }
.otk-col--messages .otk-col__pinned { padding: 11px 0 0; margin-left: 14px; border-top: 0; }

/* =============================================================================
   FIELD ROWS AND EMPTY STATES — a name against its value, and what a list
   says when it holds nothing.
   ========================================================================== */

/* a name and its value on one grid row: the message detail's derived
   rows, and the card dialog's fields */
.otk-field-row { display: grid; grid-template-columns: 148px minmax(0, 1fr); gap: 12px; align-items: baseline; width: 100%; padding: 6px 0; border-bottom: var(--otk-hair) solid var(--otk-line-soft); }
.otk-field-row:last-child { border-bottom: 0; }
.otk-field-row--card { grid-template-columns: 104px minmax(0, 1fr); gap: 12px 14px; align-items: stretch; border-bottom: 0; }
.otk-field-row--card > .otk-label { display: flex; align-items: center; }
/* in the apparatus margin the row is too narrow for two columns: the key
   stacks over its value, the way the margin's own facts stack */
.otk-margin .otk-field-row { grid-template-columns: minmax(0, 1fr); gap: 2px; }

/* an empty list says what it would hold, and how to reach it */
.otk-empty { display: flex; flex-direction: column; align-items: center; gap: 7px; text-align: center; padding: 44px 30px; }
.otk-empty__line { font-family: var(--otk-font-prose); font-size: var(--otk-size-lead); color: var(--otk-muted); }
.otk-empty__hint { font-family: var(--otk-font-mono); font-size: var(--otk-size-mono-sm); letter-spacing: 0.04em; color: var(--otk-faint); }

/* =============================================================================
   RESPONSIVE — the shell folds before the page does

   A phone turned on its side is wider than either width below, and still a
   phone: a touch screen too short to be anything else takes both layouts,
   so turning it changes nothing.
   ========================================================================== */
@media (max-width: 1100px), (pointer: coarse) and (max-height: 540px) {
  .otk-app { grid-template-columns: var(--otk-spine-w) minmax(0, 1fr); }
  .otk-rail { display: none; }
  .otk-spine__toggle { display: block; }
  .otk-rail[data-open="true"] { display: flex; position: fixed; left: var(--otk-spine-w); top: 0; bottom: 0; z-index: 10; width: var(--otk-rail-w); background: var(--otk-desk); border-right: var(--otk-hair) solid var(--otk-line); }
  .otk-panel__body { min-height: 0; overflow-y: auto; }
  /* a list-and-detail pane stacks with the desk's own mechanics turned on
     end: the list takes what it needs up to half the body and scrolls
     beyond, its filter pinned under it; the detail takes the rest, its
     verbs pinned at the foot. Laid out flat instead, a thousand hosted
     models would put the picked one's "use for this story" a thousand rows
     down. */
  .otk-panel__body--split, .otk-panel__body--models { display: flex; flex-direction: column; overflow: hidden; }
  .otk-panel__body--split > .otk-col, .otk-panel__body--models > .otk-col { flex: 0 1 auto; max-height: 50%; min-height: 0; overflow: hidden; border-right: 0; border-bottom: var(--otk-hair) solid var(--otk-line-soft); }
  .otk-panel__body--split > .otk-detail, .otk-panel__body--models > .otk-detail { flex: 1 1 0; min-height: 0; overflow: hidden; }
  .otk-panel__body--split .otk-col__scroll, .otk-panel__body--models .otk-col__scroll,
  .otk-panel__body--split .otk-detail__scroll, .otk-panel__body--models .otk-detail__scroll { flex: 1 1 auto; overflow-y: auto; }
  .otk-panel__body--dossier { flex-direction: column; overflow-x: visible; }
  /* the index rail dissolves into the column, its band carried by its
     head and list, so that the pass's block at its foot — the state, Import
     card, Extract now — can go LAST: it sticks to the screen's bottom edge
     while the rest scrolls under it, and stands at the foot when there is
     nothing to scroll; its two verbs side by side, to cost the screen one
     row */
  .otk-panel__body--dossier > .otk-index { display: contents; }
  .otk-index__head { flex: none; padding: 14px 16px 9px; background: var(--otk-band); }
  .otk-index__list { flex: none; max-height: 40%; overflow-y: auto; padding: 0 8px 14px; background: var(--otk-band); border-bottom: var(--otk-hair) solid var(--otk-line); }
  .otk-index__note { flex: none; margin: 0; padding: 11px 16px 14px; border-top: 0; background: var(--otk-band); border-bottom: var(--otk-hair) solid var(--otk-line); }
  .otk-extract { order: 99; position: sticky; bottom: 0; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; margin: auto 0 0; padding: 11px 16px 14px; background: var(--otk-paper); border-top: var(--otk-hair) solid var(--otk-line); }
  .otk-extract__line { flex: 0 0 100%; }
  .otk-extract .otk-btn { flex: 1 1 0; align-self: auto; }
  /* the index and the messages list each take what they hold up to a share
     of the body and scroll beyond it, the filter pinned under the messages:
     laid out flat, thirty scenes and three hundred turns would stand the
     reading a story's length down */
  .otk-panel__body--dossier > .otk-col--messages { flex: none; max-height: 50%; min-width: 0; overflow: hidden; padding: 12px 8px; border-right: 0; border-bottom: var(--otk-hair) solid var(--otk-line-soft); }
  .otk-panel__body--dossier > .otk-col--messages > .otk-col__scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  .otk-col, .otk-col--messages, .otk-reading, .otk-detail, .otk-detail--reader { flex: 0 0 auto; min-height: 0; overflow: visible; }
  .otk-col__scroll, .otk-detail__scroll { flex: 0 0 auto; overflow: visible; }
  .otk-margin { display: none; }
  /* the facts stay off a narrow screen; what is done TO a character does
     not: its buttons stand under the reading */
  .otk-margin:has(.otk-manage) { display: flex; flex: 0 0 auto; min-width: 0; overflow: visible; padding: 0 30px 20px; border-left: 0; }
  .otk-margin:has(.otk-manage) > :not(.otk-manage) { display: none; }
}
@media (max-width: 720px), (pointer: coarse) and (max-height: 540px) {
  .otk-spine { display: flex; flex-direction: row; align-items: center; gap: 14px; height: 48px; padding: 0 14px; border-right: 0; border-bottom: var(--otk-hair) solid var(--otk-line); }
  .otk-spine__name, .otk-spine__dots { display: none; }
  .otk-theme { flex-direction: row; gap: 8px; margin: 0 0 0 auto; padding: 0; }
  /* the bar is horizontal, so the switch lies down too: sun left, moon right,
     the knob sliding across instead of down */
  .otk-switch { width: 36px; height: 18px; }
  .otk-switch__knob { transition: left 120ms ease, background 120ms ease; }
  .otk-switch[aria-checked="true"] .otk-switch__knob { top: 3px; left: 21px; }
  .otk-spine__toggle { writing-mode: horizontal-tb; transform: none; margin: 0; }
  :root { --otk-page-pad: 24px; --otk-panel-pad: 16px; }
  .otk-app { grid-template-columns: minmax(0, 1fr); grid-template-rows: 48px minmax(0, 1fr); }
  .otk-spine { grid-column: 1; }
  .otk-rail[data-open="true"] { left: 0; top: 48px; }
  .otk-main { padding: 0; }
  .otk-panel, .otk-panel--wide, .otk-panel--read { width: 100vw; height: 100dvh; border: 0; }
  .otk-docket { max-width: 100vw; }
  /* wrapped under the input, the verbs stand centred on their own line;
     beside it, the input takes the room and they stay at its end */
  .otk-composer__verbs { margin: 0 auto; }
  /* the story's name and count above the transcript cost a phone a line of
     story; the story browser still says both */
  .otk-runhead { display: none; }
}
@media print {
  .otk-spine, .otk-rail, .otk-composer, dialog { display: none !important; }
  .otk-app { display: block; height: auto; }
  .otk-page { max-width: none; border: 0; box-shadow: none; }
  .otk-transcript { overflow: visible; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
