/* Storyweave.
 *
 * Sized for a 1366x768 Chromebook, which is the real constraint. Everything
 * below is measured against that: the editor and the play panel side by side
 * with the variable panel underneath, and no vertical scrolling of the page
 * itself at that height.
 */

:root {
    --ink: #1c1e21;
    --ink-soft: #5b6169;
    --paper: #ffffff;
    --panel: #f4f5f7;
    --line: #d9dce1;
    --accent: #1f5fa9;
    --accent-soft: #e8f0fa;
    --wrong: #c0392b;
    --wrong-soft: #fdecea;
    --check: #9a6a00;
    --check-soft: #fdf4e0;
    --ending: #3f7a4b;
    --ending-soft: #e4f0e6;
    /* "does not exist" was grey, which is what a plain scene already looks
       like. A hue of its own is the only way it reads as a fourth thing. */
    --gone: #6b4fa8;
    --gone-soft: #efeaf9;
    --playing: #fff6d4;
    --playing-edge: #e8c95a;

    --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-code: "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;

    /* Wherever a story is shown: the read page, and the panel beside the
       editor. Not the editor, the toolbar or the graph, which are the
       tool rather than the work.

       A paper, an ink and a way of setting type, so that a student can
       tell at a glance whether they are looking at their story or at the
       thing they wrote it in, without a word being said about it.

       Five colours and the type. These are the default -- `paper` -- and
       every theme below is the same list with different values. Nothing
       outside this block names a colour for a story; if a rule further
       down has a hex code in it, it is not themeable and probably
       should be. */
    --font-read: Georgia, "Noto Serif", Tinos, "Times New Roman", serif;
    --read-paper: #fbf9f5;
    --read-ink: #221f1c;
    --read-soft: #6b6459;
    --read-rule: #e3ddd2;
    --read-tint: #f2ede2;
    /* Faces disagree about how big a given pixel size looks. Georgia at
       19px and Cascadia Mono at 19px have different x-heights, so the
       same scene in `screen` came out a size larger than in `paper` for
       no reason anybody chose, and the measure came out shorter with it.
       This is each face measured against Georgia and put back, so that
       picking a theme changes how a story looks and not how big it is.
       Text size is the button for how big it is. */
    --read-scale: 1;
    /* Tracking on the title, and only on the title. Six covers that
       differed in colour and face were still laid out identically, and a
       title is the one line on a cover with room to be set rather than
       just typed. Body text is left alone: tracking prose is how a page
       stops being readable. */
    --read-title-track: 0;
    /* The edge of the page, on a screen wide enough to have room for one.
       A whole border rather than a colour: a book, a terminal and a
       blackboard do not have the same edge, and a hairline that only
       changed hue across six themes would not have earned the rule. */
    --read-edge: 1px solid var(--read-rule);
    /* What the read page is lying on. The same as the paper until there
       is room for the two to differ -- see "the page, on a desk" below --
       so that on a narrow screen nothing has changed at all. */
    --read-ground: var(--read-paper);
    /* Set by the Text size button. docs/UI_SPEC.md asks for one and is
       right that students will use it. */
    --text-scale: 1;
    /* The two multiplied once here rather than at every call site.
       Substitution happens at use, so a theme setting --read-scale
       further down still lands in this. */
    --read-size: calc(var(--text-scale) * var(--read-scale));

    --toolbar-height: 46px;
    --notice-height: 30px;
    --vars-height: 150px;
}

/* -- the themes ----------------------------------------------------------- */

/* What a story is dressed in, chosen by whoever wrote it with
   `theme="night"` on their `Story(...)` line. `js/main.js` puts the name
   on the root element and these five values are the whole of a theme.
   The list of names is `THEMES` in engine/storyweave/errors.py, which is
   also what the suggestion list offers; a name here that is not in that
   list can never be reached, and one there that is not here silently
   falls back to paper. `test_web.py` checks the two agree.

   Each is a paper, an ink and a typeface *together*, because the typeface
   is what actually changes how a story reads. Every pair here clears 7:1
   contrast, which matters more than usual: the author picks and the
   reader is stuck with it.

   No theme touches the editor, the toolbar or the graph. Those are the
   tool, they are used by somebody who is working rather than reading, and
   a student who set their story to `screen` should not lose their syntax
   colours with it. */

/* Dark, for a story that happens after everyone has gone home. Blue-black
   rather than pure black, and the ink slightly warm, because white on
   black at this size buzzes. */
:root[data-story-theme="night"] {
    --font-read: Georgia, "Noto Serif", Tinos, "Times New Roman", serif;
    --read-paper: #14171c;
    --read-ink: #e8e4dc;
    --read-soft: #99a1ad;
    --read-rule: #2c323b;
    --read-tint: #1e232a;
    --read-scale: 1;      /* the same face as paper */
    --read-title-track: 0.01em;
    --read-edge: 1px solid var(--read-rule);
}

/* A computer screen, for anything with a machine in it. Monospaced and
   green, and a costume rather than a comfort, which is the reason the
   list has costumes on it at all. */
:root[data-story-theme="screen"] {
    --font-read: var(--font-code);
    --read-paper: #0b0f0b;
    --read-ink: #86e59a;
    --read-soft: #4f9a63;
    --read-rule: #1c2a1e;
    --read-tint: #131c14;
    --read-scale: 0.92;   /* mono carries the biggest x-height on the list */
    --read-title-track: 0.06em;   /* a banner, not a book title */
    --read-edge: 2px solid var(--read-rule);   /* a window, with a frame */
}

/* Violet and gold, for dragons, maps and long journeys. The genre this
   class writes most and the one nothing on the list was dressed for.
   Parchment on ink rather than the other way round, because a saturated
   paper is what actually reads as colour, and a saturated *light* paper
   with dark ink is the arrangement that ends up unreadable.

   The only theme whose typeface is not already here. `paper` and `night`
   are Georgia, which a Chromebook does not have and resolves to Noto
   Serif; a sixth serif in that same stack would have been one face in
   different colours. Palatino on Windows and a Mac, Caladea on Chrome OS,
   and a machine with neither lands on Noto Serif, where quest is a
   palette rather than a face. Less than intended, never broken. */
:root[data-story-theme="quest"] {
    --font-read: "Palatino Linotype", "Book Antiqua", Palatino, Caladea, Cambria, "Noto Serif", serif;
    --read-paper: #1b1430;
    --read-ink: #f2e3b8;   /* 14.0:1 */
    --read-soft: #c7b083;  /*  8.5:1 */
    --read-rule: #3a2d5c;
    --read-tint: #241a3d;
    --read-scale: 1.03;   /* Palatino and Caladea both sit under Georgia */
    --read-title-track: 0.03em;
    --read-edge: 3px double var(--read-rule);  /* a ruled border, as drawn */
}

/* A blackboard, in white on green. The one that looks like school, which
   for a story set in one is the point. */
:root[data-story-theme="chalk"] {
    --font-read: var(--font-ui);
    --read-paper: #2b3a32;
    --read-ink: #f2f4ef;
    --read-soft: #a9bdb0;
    --read-rule: #40534a;
    --read-tint: #35473d;
    --read-scale: 0.95;   /* Segoe UI and Roboto both run over Georgia */
    --read-title-track: 0.04em;   /* written up, not printed */
    --read-edge: 3px solid var(--read-rule);   /* a board has a frame */
}

/* Black on white, as clear as it gets. Here because the author chooses
   the theme and the reader cannot argue: a story written for somebody who
   needs contrast has to have something to be set to. */
:root[data-story-theme="bright"] {
    --font-read: var(--font-ui);
    --read-paper: #ffffff;
    --read-ink: #000000;
    --read-soft: #4a4a4a;
    --read-rule: #cfcfcf;
    --read-tint: #f0f0f0;
    /* The same correction as `chalk`, which is the same face. This theme
       is the one for somebody who needs contrast, and contrast is what it
       gives them; Text size is where size is asked for, and a theme that
       quietly ran a size large would make that button lie. */
    --read-scale: 0.95;
    --read-title-track: 0;   /* nothing between a reader and the words */
    --read-edge: 1px solid var(--read-rule);
}

/* Pink and cream, for a story that is sweet on purpose. Strawberry milk
   rather than hot pink, because the saturated version is the black on
   purple the theme list exists to prevent; the paper stays pale and the
   ink goes deep mulberry, 11.6:1, with the soft ink at 5.7:1.

   The second theme after `quest` to carry its own typeface, and the same
   trade: a round, hand-lettered face where a machine has one -- Comic
   Sans on Windows and a Mac, Chalkboard on an iPad -- and a Chromebook,
   which has neither, lands in the UI face, where candy is a palette
   rather than a face. Less than intended, never broken. */
:root[data-story-theme="candy"] {
    --font-read: "Comic Sans MS", "Chalkboard SE", "Comic Neue", var(--font-ui);
    --read-paper: #fff1f6;
    --read-ink: #54213b;   /* 11.6:1 */
    --read-soft: #8f4a68;  /*  5.7:1 */
    --read-rule: #f4c9dc;
    --read-tint: #ffe4ef;
    --read-scale: 0.93;   /* Comic Sans runs even larger than the UI faces */
    --read-title-track: 0.02em;
    --read-edge: 3px dotted var(--read-rule);   /* a sticker sheet's cut line */
}

/* The two things a browser draws on a story surface that a theme does not
   otherwise reach, both in the story's own values rather than the
   browser's. Neither is decoration: the default selection blue over
   `night` is the app's only remaining bright rectangle, and the default
   scrollbar next to `screen` is a white stripe down the side of a black
   page. Both surfaces, because the panel wears the theme too. */
.read::selection, .read ::selection,
.play::selection, .play ::selection {
    background: var(--read-soft);
    color: var(--read-paper);
}

.read, .play {
    scrollbar-color: var(--read-soft) var(--read-paper);
}

* { box-sizing: border-box; }

/* `hidden` is a browser default rule, and any author rule that sets
   `display` beats it. Several things here are hidden *and* carry a class
   that sets display: the workspace is a grid, the banner is a flex row.
   Without this they stay on screen with `hidden` set, which quietly
   defeats the whole point of the chooser: hard rule 6 says the editor
   cannot be reachable until a story has somewhere to live. */
[hidden] { display: none !important; }

html, body {
    height: 100%;
    margin: 0;
    overflow: hidden;
    font-family: var(--font-ui);
    color: var(--ink);
    background: var(--panel);
}

/* -- toolbar ------------------------------------------------------------- */

.toolbar {
    height: var(--toolbar-height);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 0 14px;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
}

.toolbar-group {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* The action buttons are their icon and nothing else.
 *
 * With the words, eight of them came to 1390px on the 1366px screen this
 * whole layout is measured against, so the row wrapped and took a chunk
 * out of the editor. Without them the same eight take less than half of
 * it, and the row reads as a row of tools rather than a paragraph of
 * verbs.
 *
 * **The word does not go away, it stops being drawn.** Every one keeps a
 * `.button-label` clipped out of sight, and that is what a screen reader
 * announces and what `setLabel` in js/main.js rewrites when Play becomes
 * Restart. The same text goes on the `title`, so resting on one says what
 * it is. Take the span out and these become eight unlabelled buttons.
 *
 * Text size and Progress keep their words: one of them says what size the
 * text currently is and the other says how the story is doing, and
 * neither of those is a picture. */
/* Square, and big enough to read. Sixteen pixels was sized for an icon
   sitting beside a word, where the word did the explaining; on its own an
   icon has to carry the whole meaning and 16px of two-pixel stroke does
   not. The row still comes to less than half the screen. */
.toolbar .button:has(.icon) {
    padding: 6px 9px;
    gap: 0;
}

.toolbar .icon {
    width: 20px;
    height: 20px;
}

.toolbar .button .button-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.wordmark {
    font-weight: 700;
    letter-spacing: 0.01em;
}

/* Capped, and cut with an ellipsis rather than allowed to grow.
 *
 * A student's own story fills this row to within fifteen pixels at
 * 1366px, and the filename is the one part of it they choose: a story
 * called `the_one_where_everything_goes_wrong_A-Rivera.py` would push
 * Finish off the end of the screen. The whole name is on the element's
 * `title`, and the save indicator beside it is the thing being glanced
 * at anyway. */
.story-name {
    font-family: var(--font-code);
    font-size: 13px;
    color: var(--ink-soft);
    max-width: 24ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.field {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: var(--ink-soft);
}

.field select {
    font: inherit;
    color: var(--ink);
    padding: 4px 6px;
    border: 1px solid var(--line);
    border-radius: 5px;
    background: var(--paper);
}

/* The sprite itself is never seen. `display: none` would stop the symbols
   resolving in some browsers, so it is taken out of the layout instead. */
.sprite {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

.button {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font: inherit;
    font-size: 14px;
    padding: 6px 14px;
    border: 1px solid var(--line);
    border-radius: 5px;
    background: var(--paper);
    color: var(--ink);
    cursor: pointer;
}

/* Drawn in `currentColor`, so an icon is whatever colour its button's
   text is and the primary, hover and disabled states need no rules of
   their own. Stroked rather than filled, at a weight that holds up at
   16px on a Chromebook panel.

   The word stays beside it. These are read by twelve-year-olds who have
   not used this before, and an icon on its own is a thing you have to be
   taught; the icon is what makes the row scannable once they have been. */
.icon {
    width: 16px;
    height: 16px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Two that are shapes rather than outlines. A hollow triangle reads as a
   cursor and a hollow square reads as a box; solid, they read as the
   two things every media player has taught everybody they are.

   Scoped to the class rather than to the button, because the same button
   becomes Restart, and a filled arc is a blob rather than an arrow. */
#play-button.is-play .icon { fill: currentColor; stroke-width: 1.4; }
#stop-button .icon { fill: currentColor; }

.button:hover:not(:disabled) { background: var(--panel); }

.button.primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    font-weight: 600;
}

.button.primary:hover:not(:disabled) { background: #17497f; }

.button:disabled {
    opacity: 0.45;
    cursor: default;
}

.notice {
    height: var(--notice-height);
    margin: 0;
    display: flex;
    align-items: center;
    padding: 0 14px;
    font-size: 12.5px;
    color: var(--check);
    background: var(--check-soft);
    border-bottom: 1px solid var(--line);
}

.toolbar-open {
    display: flex;
    align-items: baseline;
    gap: 10px;
}

.save-state {
    font-size: 12px;
    color: var(--ink-soft);
}

.save-state.is-wrong {
    color: var(--wrong);
    font-weight: 600;
}

/* -- the chooser --------------------------------------------------------- */
/* The whole app until a session exists, because hard rule 6 says unsaved
   work must have nowhere to live. */

.chooser {
    height: calc(100% - var(--toolbar-height));
    display: flex;
    align-items: flex-start;
    justify-content: center;
    overflow-y: auto;
    padding: 6vh 20px 20px;
}

.chooser-card {
    width: 100%;
    max-width: 520px;
    padding: 28px 30px 30px;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 10px;
}

.chooser-title {
    margin: 0 0 6px;
    font-size: 26px;
}

.chooser-subtitle {
    margin: 0 0 14px;
    font-size: 18px;
}

.chooser-lead {
    margin: 0 0 20px;
    font-size: 15px;
    line-height: 1.6;
    color: var(--ink-soft);
}

.chooser-aside {
    margin: 16px 0 0;
    font-size: 13px;
    line-height: 1.55;
    color: var(--ink-soft);
}

.chooser-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 18px;
}

.button.big {
    padding: 11px 20px;
    font-size: 15px;
}

.stacked {
    display: block;
    margin-bottom: 14px;
}

.stacked span {
    display: block;
    margin-bottom: 5px;
    font-size: 13px;
    font-weight: 600;
}

.stacked input {
    width: 100%;
    font: inherit;
    font-size: 15px;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--paper);
    color: var(--ink);
}

.stacked input:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
}

.session-message {
    margin: 18px 0 0;
    padding: 10px 12px;
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--check);
    background: var(--check-soft);
    border-radius: 6px;
}

/* -- layout -------------------------------------------------------------- */

.workspace {
    --editor-width: 50%;
    height: calc(100% - var(--toolbar-height));
    display: grid;
    grid-template-columns: var(--editor-width) 7px 1fr;
}

/* The banner only appears sometimes, so the height it takes is claimed
   only then. The checklist used to need the same treatment and three
   times as many rules; it lives in a dialog now and takes no height at
   all, which is the tidiest thing about moving it. */
body:has(.notice:not([hidden])) .workspace,
body:has(.notice:not([hidden])) .chooser {
    height: calc(100% - var(--toolbar-height) - var(--notice-height));
}

.editor .CodeMirror.is-read-only {
    background: var(--panel);
}

.workspace.is-dragging { cursor: col-resize; user-select: none; }

.pane {
    min-width: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.splitter {
    background: var(--line);
    cursor: col-resize;
    touch-action: none;
}

.splitter:hover { background: var(--accent); }

/* -- the find bar -------------------------------------------------------- */

.find {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    background: var(--panel);
    border-bottom: 1px solid var(--line);
    font-size: calc(13px * var(--text-scale));
}

.find-field {
    display: flex;
    align-items: center;
    gap: 6px;
    flex: 0 1 260px;
    min-width: 0;
}

.find-label { color: var(--ink-soft); }

.find-field input {
    flex: 1;
    min-width: 0;
    padding: 4px 8px;
    font: inherit;
    color: var(--ink);
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 5px;
}

.find-field input:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
}

.find-count {
    flex: 0 0 auto;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}

/* The scene buttons take whatever room is left and give it back rather
   than pushing Back and Next off a 1366px screen. */
.find-scenes {
    display: flex;
    flex: 1 1 auto;
    gap: 5px;
    min-width: 0;
    overflow-x: auto;
}

.find-scene {
    flex: 0 0 auto;
    padding: 3px 9px;
    font-family: var(--font-code);
    font-size: calc(12px * var(--text-scale));
    color: var(--accent);
    background: var(--accent-soft);
    border: 1px solid transparent;
    border-radius: 999px;
    cursor: pointer;
}

.find-scene.is-ending {
    color: var(--ending);
    background: var(--ending-soft);
}

.find-scene:hover { border-color: currentColor; }

.find-scene:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
}

/* -- editor -------------------------------------------------------------- */

.editor {
    flex: 1;
    min-height: 0;
    overflow: hidden;
    background: var(--paper);
}

.editor .CodeMirror {
    height: 100%;
    font-family: var(--font-code);
    font-size: calc(13.5px * var(--text-scale));
    line-height: 1.55;
}

/* The dots in the margin. Every line with something wrong on it gets one,
   so the second and third mistakes in a story are reachable rather than
   only the first, which is all the summary line under the editor can show.
   Content flags never get one: docs/CLASSROOM.md wants those quiet, and a
   dot in the margin is still there when somebody leans over. */
.CodeMirror-gutter.sw-gutter {
    width: 14px;
    background: transparent;
}

.sw-dot {
    display: block;
    width: 8px;
    height: 8px;
    margin: 4px auto 0;
    padding: 0;
    border: 1px solid;
    border-radius: 50%;
    cursor: pointer;
}

.sw-dot.is-wrong { background: var(--wrong); border-color: var(--wrong); }

/* Hollow rather than filled: a broken link is not as urgent as a missing
   quote, and one of them you can still play. Same distinction the two
   squiggles make, made a second time in a shape rather than a colour. */
.sw-dot.is-check { background: var(--paper); border-color: var(--check); }

.sw-dot:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* The suggestion list. show-hint.min.css draws the box; this makes the
   rows two columns, because what a name *is* does as much work as the
   name: "scene on line 42" is the answer to a question a student was
   about to scroll for. */
.CodeMirror-hints {
    z-index: 30;
    padding: 3px 0;
    font-family: var(--font-code);
    font-size: calc(12.5px * var(--text-scale));
    border: 1px solid var(--line);
    border-radius: 6px;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16);
}

.CodeMirror-hint {
    display: flex;
    gap: 14px;
    justify-content: space-between;
    align-items: baseline;
    padding: 3px 10px;
    color: var(--ink);
    border-radius: 0;
}

li.CodeMirror-hint-active {
    color: var(--paper);
    background: var(--accent);
}

.sw-hint-name { font-weight: 600; }

.sw-hint-detail {
    font-family: var(--font-ui);
    font-size: calc(11.5px * var(--text-scale));
    color: var(--ink-soft);
}

li.CodeMirror-hint-active .sw-hint-detail { color: var(--accent-soft); }

/* -- the story language's own words --------------------------------------- */

/* `Story`, `Scene`, `choice`, `goto`: the words that are the tool rather
   than the story. Python's mode cannot pick these out, because to it
   `Scene(` is a call to something and `goto=` is an argument with a name.
   Which words these are comes from `complete.vocabulary()`; see the foot
   of js/editor.js.

   Three roles and three treatments, because three is what a reader has to
   tell apart:

     command   the things you make      Story Start Scene Ending
     method    the things you call      choice text then only_if
     keyword   the settings you name    goto needs locked by about

   Weight as well as hue in every case. A student who cannot tell the
   first two colours apart still sees that one is bold and the other is
   not, which is the same rule the graph legend follows.

   `!important` is not laziness here. codemirror.min.css is appended to the
   head *after* this file, and its `.cm-s-default .cm-variable` and
   `.cm-builtin` rules are the same specificity as these, so without it
   the later sheet wins and every one of these comes out plain. The
   matching-bracket rule a few hundred lines up learned this the hard way
   and solved it with a longer selector; there is no longer selector
   available for a class the overlay puts on. */

.editor .CodeMirror .cm-sw-word { font-style: normal; }

/* The four that make something. The strongest of the three, because a
   story is a list of these and everything else hangs off them. */
.editor .CodeMirror .cm-sw-command {
    color: #7a3ea8 !important;
    font-weight: 700;
}

/* The things you call on one. */
.editor .CodeMirror .cm-sw-method {
    color: var(--accent) !important;
    font-weight: 600;
}

/* The settings, which are always `name=`. Quietest of the three: there
   are ten of them and they are on nearly every line, so anything louder
   turns a choice into a row of colours. */
.editor .CodeMirror .cm-sw-keyword {
    color: #0f7b6c !important;
}

/* Every place the find bar matched, and the one being looked at. */
.CodeMirror .sw-hit { background: #ffe9a8; }

.CodeMirror .sw-hit.is-here {
    background: var(--accent);
    color: var(--paper);
}

/* The line the caret is on. Faint on purpose: it answers "where am I"
   without competing with the scene being played, which is the highlight
   that matters. The line number carries most of it, because the question
   this is really answering is the one the error line asks by number. */
.editor .CodeMirror-activeline-background { background: var(--panel); }

.editor .CodeMirror-activeline-gutter .CodeMirror-linenumber {
    color: var(--ink);
    font-weight: 700;
}

/* Which bracket goes with the one under the caret.
 *
 * The long selector is doing work. codemirror.min.css is appended to the
 * head after this file, and its own rule is
 * `div.CodeMirror span.CodeMirror-matchingbracket`, so anything shorter
 * loses the colour and keeps the background: the first version of this
 * produced CodeMirror's green on our blue, which is worse than either. */
.editor div.CodeMirror span.CodeMirror-matchingbracket {
    color: var(--accent);
    background: var(--accent-soft);
    font-weight: 700;
}

.editor div.CodeMirror span.CodeMirror-nonmatchingbracket {
    color: var(--wrong);
    background: var(--wrong-soft);
    font-weight: 700;
}

/* The scene being played. docs/BUILD_PLAN.md calls this the highest-value
   comprehension feature in the tool, so it is loud enough to notice from the
   back of a classroom and quiet enough to read through. Declared after the
   active line so that it wins where both land on the same line. */
.CodeMirror .sw-playing { background: var(--playing); }

.CodeMirror .sw-playing-gutter {
    background: var(--playing);
    box-shadow: inset 3px 0 0 var(--playing-edge);
}

/* Where something else just sent the reader.
 *
 * An example is read-only, which is `nocursor`, so the caret is not drawn
 * and cannot point at anything. Clicking a graph node scrolled the file
 * and said nothing about which line it had gone to. This says it, briefly,
 * and then gets out of the way. */
.CodeMirror .sw-found {
    animation: found 1.4s ease-out forwards;
}

@keyframes found {
    0%   { background: var(--accent-soft); }
    70%  { background: var(--accent-soft); }
    100% { background: transparent; }
}

@media (prefers-reduced-motion: reduce) {
    /* Still says which line, without the fade. Nothing about this needs
       to move to do its job. */
    .CodeMirror .sw-found {
        animation: none;
        background: var(--accent-soft);
    }
}

/* Not text-decoration: underline, which a proportional squiggle sits badly
   under in a monospace editor. */
.CodeMirror .sw-wrong {
    text-decoration: underline wavy var(--wrong);
    text-underline-offset: 3px;
}

.CodeMirror .sw-check {
    text-decoration: underline wavy var(--check);
    text-underline-offset: 3px;
}

.error-line {
    margin: 0;
    padding: 8px 12px;
    font-size: 13px;
    line-height: 1.4;
    border-top: 1px solid var(--line);
    cursor: pointer;
}

.error-line.is-wrong { background: var(--wrong-soft); color: var(--wrong); }
.error-line.is-check { background: var(--check-soft); color: var(--check); }

/* Content flags. A squiggle and nothing else: no tooltip, no message, no
   count, because there is nothing to say that would not turn the flag
   into a puzzle. docs/CLASSROOM.md wants this quiet, and quiet on the
   student's own screen with no sound and no box is a correction nobody
   else sees. */
/* -- the progress dialog ------------------------------------------------- */

/* This was a strip along the bottom of the window until it was nine items
   and six pixels from wrapping. Behind a button it is something you go
   and look at rather than a score sitting on screen to be farmed. Nothing
   was lost from the structural half of it: a broken link, an orphan and a
   dead end each still have a dot in the editor's gutter and a sentence
   under it, both always on.

   The nine items are now three and six. Three that can be crossed, and
   six that are facts with no verdict on them, because the six were the
   rubric and the rubric is not the same for two students sitting next to
   each other. See "Progress" in docs/UI_SPEC.md. */

.progress {
    width: min(560px, calc(100vw - 32px));
    max-height: calc(100vh - 64px);
    padding: 24px 30px;
    color: var(--ink);
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 10px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.22);
}

/* `[open]`, and that bracket is the whole of a bug worth remembering.
 *
 * A `<dialog>` disappears because the browser's own rule says
 * `dialog:not([open]) { display: none }`, and any author rule that sets
 * `display` beats it. Written as a plain `.progress { display: flex }`
 * -- which is what a two-list column needs -- the dialog went on being
 * painted after it had closed: `close()` ran, `open` went false, the
 * backdrop and the modality went away, and the box stayed on screen
 * looking exactly like a dialog that had refused to close. Pressing
 * Close again did nothing, because there was nothing left to close.
 *
 * The same trap as the `[hidden]` note in index.html, from the other
 * direction, and the reason both are written down. */
.progress[open] {
    display: flex;
    flex-direction: column;
}

.progress::backdrop { background: rgba(28, 30, 33, 0.45); }

.progress-title {
    margin: 0 0 4px;
    font-size: calc(17px * var(--text-scale));
}

/* One scroller around both lists rather than one each. Two independently
   scrolling boxes in a dialog this size is two scrollbars and no way to
   read from the top of one to the bottom of the other. */
.progress-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    /* Not decoration: without it the scrollbar sits against the words on
       the right and the dots against the dialog's edge on the left, and
       the whole thing reads as though pushed into the corner. */
    padding: 0 12px 0 2px;
}

.progress-heading {
    margin: 18px 0 2px;
    font-size: calc(11px * var(--text-scale));
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.progress-scroll > .progress-heading:first-child { margin-top: 10px; }

.checklist,
.stats {
    margin: 0;
    padding: 0;
    list-style: none;
}

.check {
    padding: 11px 0;
    border-top: 1px solid var(--line);
}

.check:first-child { border-top: none; }

.check-label {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: calc(13.5px * var(--text-scale));
    font-weight: 600;
    color: var(--ink-soft);
}

/* Drawn rather than typed, so a screen reader gets the label and the
   state from aria-label and not a stray character. */
.check-label::before {
    content: "";
    width: 13px;
    height: 13px;
    flex: none;
    border-radius: 50%;
    border: 1.5px solid currentColor;
}

.check.is-ok .check-label { color: var(--ending); }

.check.is-ok .check-label::before {
    background: var(--ending);
    border-color: var(--ending);
    box-shadow: inset 0 0 0 2px var(--paper), inset 0 0 0 3px var(--ending);
}

.check-detail {
    margin: 4px 0 0 21px;
    font-size: calc(12.5px * var(--text-scale));
    line-height: 1.5;
    color: var(--ink-soft);
}

.check-jump {
    margin: 6px 0 0 21px;
    padding: 2px 9px;
    font: inherit;
    font-size: calc(12px * var(--text-scale));
    color: var(--accent);
    background: var(--accent-soft);
    border: 1px solid transparent;
    border-radius: 20px;
    cursor: pointer;
}

.check-jump:hover { border-color: var(--accent); }

/* -- the stats ----------------------------------------------------------- */

/* A fact and its number, and deliberately nothing else. No dot, no tick,
   no cross, no jump button: every one of those would be a verdict, and
   the whole reason these left the checklist is that this tool is in no
   position to pass one. It has not been told what the assignment was.

   The values sit right, and the gap between is left empty. A dotted
   leader ran across it for one draft, on the theory that it would carry
   the eye. It did the opposite: nine rows of dots is nine lines of noise
   through the middle of the list, and the labels stop being the thing you
   see first. The rows are close enough together to read across without
   help. */

.stat {
    padding: 7px 0;
    border-top: 1px solid var(--line);
}

.stat:first-child { border-top: none; }

.stat-row {
    display: flex;
    align-items: baseline;
    gap: 10px;
    font-size: calc(13.5px * var(--text-scale));
}

.stat-label { color: var(--ink-soft); }

.stat-value {
    margin-left: auto;
    padding-left: 16px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.stat-detail {
    margin: 3px 0 0;
    font-size: calc(12px * var(--text-scale));
    line-height: 1.5;
    color: var(--ink-soft);
}

/* The one sentence in the tool whose job is to refuse to be the
   definition of done. Only there when everything else passes. */
.progress-note {
    margin: 16px 0 0;
    padding: 12px 14px;
    font-size: calc(13px * var(--text-scale));
    line-height: 1.55;
    color: var(--ending);
    background: var(--ending-soft);
    border-radius: 8px;
}

/* Said once, at the moment of pressing Download, when a check has failed.

   Amber and not red, and the same amber as the Progress button's hollow
   state and the hollow dot in the editor's gutter, because this is a
   `problem` and not an `error`. Red here would say refused, and it is not
   refusing: the button underneath it still works and still says so. */
.progress-warning {
    margin: 16px 0 0;
    padding: 12px 14px;
    font-size: calc(13px * var(--text-scale));
    line-height: 1.55;
    color: var(--check);
    background: var(--check-soft);
    border-radius: 8px;
}

/* Which of the two downloads is which. Quiet, in the tool's own ink: it
   is not a warning and must not read as one, it is the difference
   between the two buttons under it. */
.progress-choose {
    margin: 16px 0 0;
    font-size: calc(12.5px * var(--text-scale));
    line-height: 1.6;
    color: var(--ink-soft);
}

/* -- the link ------------------------------------------------------------ */

.share-lead {
    margin: 0 0 16px;
    font-size: calc(14px * var(--text-scale));
    line-height: 1.55;
    color: var(--ink-soft);
}

.share-label {
    font-size: calc(12px * var(--text-scale));
    color: var(--ink-soft);
}

/* Monospace and small, because this is not prose and nobody reads it: it
   is a thing to select and copy. It wraps rather than scrolling sideways
   so that the whole of it is visible and a student can see that the long
   thing they are about to send is one link and not three. */
.share-link {
    width: 100%;
    padding: 10px 12px;
    font-family: var(--font-code);
    font-size: calc(12px * var(--text-scale));
    line-height: 1.5;
    color: var(--ink);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: 8px;
}

.share-note {
    margin: 12px 0 0;
    padding: 12px 14px;
    font-size: calc(13px * var(--text-scale));
    line-height: 1.55;
    color: var(--check);
    background: var(--check-soft);
    border-radius: 8px;
}

.share-copied {
    margin: 10px 0 0;
    font-size: calc(13px * var(--text-scale));
    color: var(--ending);
}

.progress-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 18px;
}

/* The toolbar button. It reads `Progress` and never a count.
 *
 * `Progress 6/9` was a score, and six of those nine were the rubric: it
 * told a student they were three short of a number nobody had shown
 * them, and told the student beside them, who had been given a different
 * target, exactly the same thing. What is left is one question with the
 * same answer for everybody -- is my story broken -- so it is worth a
 * teacher reading across the room, which is the job the count had.
 *
 * The mark before the word is the reason the count could go. Filled for
 * done and hollow for not, so the two states differ in shape as well as
 * in hue: docs/UI_SPEC.md forbids meaning in colour alone, and with no
 * count left the colour would otherwise be saying all of it. The button
 * also carries the engine's sentence as its title and aria-label.
 *
 * Amber rather than red because a broken link is a `problem` and not an
 * `error`: it is the same tier as the hollow dot in the editor's gutter,
 * and it is the same amber, on purpose. */
/* Flex rather than an inline-block with a vertical-align. An inline mark
   sits on the text's baseline and hangs below it, which grew the button
   to 52px inside a 46px toolbar and pushed the word out of the bar. Same
   arrangement the checklist's own dots use, for the same reason. */
#progress-button {
    display: inline-flex;
    align-items: center;
}

#progress-button::before {
    content: "";
    width: 9px;
    height: 9px;
    flex: none;
    margin-right: 7px;
    border: 2px solid currentColor;
    border-radius: 50%;
}

#progress-button.is-done {
    color: var(--ending);
    border-color: var(--ending);
}

#progress-button.is-done::before { background: currentColor; }

#progress-button.is-not-done {
    color: var(--check);
    border-color: var(--check);
}

/* -- right column -------------------------------------------------------- */

.pane-right {
    background: var(--paper);
    border-left: 1px solid var(--line);
}

.tabs {
    display: flex;
    gap: 2px;
    padding: 0 10px;
    background: var(--panel);
    border-bottom: 1px solid var(--line);
}

.tab {
    padding: 8px 14px;
    font-size: 13px;
    font-weight: 600;
    color: var(--ink-soft);
    border-bottom: 2px solid transparent;
}

.tab.is-active {
    color: var(--accent);
    border-bottom-color: var(--accent);
    background: var(--paper);
}

/* The panel wears the story's theme too, which is the whole reason the
   colours above are variables rather than values.
 *
 * A student picks `theme="night"` and sees it while they are writing,
 * rather than having to open the read page to find out what they chose.
 * It also does something the theme was not asked to do: the panel stops
 * looking like part of the editor beside it, so which half is the tool
 * and which half is the work stops needing to be explained.
 *
 * The graph shares this column and does not inherit any of it. A graph is
 * a picture of the machinery, and its five node colours mean things. */
.play {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 20px 24px 24px;
    font-family: var(--font-read);
    color: var(--read-ink);
    background: var(--read-paper);
}

/* -- the graph ----------------------------------------------------------- */

.graph {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.graph-tools {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 8px 12px;
    border-bottom: 1px solid var(--line);
}

.button.small {
    padding: 3px 12px;
    font-size: 12.5px;
}

.graph-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 12px;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: 11.5px;
    color: var(--ink-soft);
}

.graph-legend li {
    display: flex;
    align-items: center;
    gap: 5px;
}

/* The keys are the same <rect class="graph-box"> the graph draws, in a
   tiny <svg>, so one set of rules paints both and the legend cannot end
   up describing a picture it does not match. */
.graph-legend .key {
    width: 30px;
    height: 17px;
    flex: none;
    overflow: visible;
}

.graph-canvas {
    flex: 1;
    min-height: 0;
    overflow: hidden;
    background:
        radial-gradient(circle at 1px 1px, #e3e6ea 1px, transparent 0) 0 0 / 18px 18px;
    cursor: grab;
    touch-action: none;
}

.graph-canvas.is-panning { cursor: grabbing; }

.graph-svg {
    width: 100%;
    height: 100%;
    display: block;
}

.graph-hint {
    margin: 0;
    padding: 7px 12px;
    font-size: 11.5px;
    line-height: 1.5;
    color: var(--ink-soft);
    border-top: 1px solid var(--line);
    background: var(--panel);
}

/* Nodes. Every status differs in outline as well as fill, so none of the
   meaning is carried by colour alone. */

.graph-node { cursor: pointer; }

.graph-box {
    fill: var(--paper);
    stroke: #9aa3ad;
    stroke-width: 1.5;
    transition: stroke 0.12s ease;
}

/* Only the plain scene darkens on hover; the coloured statuses are
   already saying something with their stroke and keep saying it. */
.graph-node.is-ok:hover .graph-box { stroke: #79828e; }

.graph-label {
    font-family: var(--font-code);
    font-size: 12px;
    fill: var(--ink);
    text-anchor: middle;
    dominant-baseline: central;
    pointer-events: none;
}

.is-start .graph-box {
    fill: var(--accent-soft);
    stroke: var(--accent);
    stroke-width: 3;
}

.is-ending .graph-box {
    fill: var(--ending-soft);
    stroke: var(--ending);
    stroke-width: 3;
}

/* The three that mean something is wrong.
 *
 * Each differs from the other two in hue AND in dash pattern, so neither
 * a small swatch nor colour blindness can collapse any pair together.
 * They used to be 5-3, 1.5-3 and 3-3 dashes in amber, red and grey, and
 * at node size that is three faintly different dotted rectangles, with
 * the grey one looking like an ordinary scene into the bargain. */

.is-orphan .graph-box {
    fill: var(--check-soft);
    stroke: var(--check);
    stroke-width: 2.5;
    stroke-dasharray: 10 6; /* long gaps: cut off from the rest */
}

.is-deadend .graph-box {
    fill: var(--wrong-soft);
    stroke: var(--wrong);
    stroke-width: 3.5;
    stroke-dasharray: 0 7; /* round dots: it stops here */
    stroke-linecap: round;
}

.is-missing .graph-box {
    fill: var(--gone-soft);
    stroke: var(--gone);
    stroke-width: 2.5;
    stroke-dasharray: 11 3 2 3; /* dash-dot: not really there at all */
}

.is-missing .graph-label {
    fill: var(--gone);
    font-style: italic;
}

.graph-node.is-visited .graph-box { stroke-width: 3; }

.graph-node:focus-visible { outline: none; }
.graph-node:focus-visible .graph-box {
    stroke: var(--accent);
    stroke-width: 3.5;
}

/* Where the player is standing. */
.graph-node.is-current .graph-box {
    stroke: var(--accent);
    stroke-width: 3.5;
    animation: sw-pulse 1.6s ease-in-out infinite;
}

@keyframes sw-pulse {
    0%, 100% { stroke-opacity: 1; }
    50% { stroke-opacity: 0.35; }
}

/* Edges.
 *
 * The wiring sits a step behind the boxes: an edge is drawn lighter than
 * a node's outline so the shape of the story reads first and the routing
 * second. Each edge is the same path three times over: a casing in
 * paper, so that where two edges cross, the one painted later breaks the
 * one beneath cleanly instead of forming an X of equal greys; the line
 * itself; and an invisible band wide enough to catch the pointer,
 * because the line is under two pixels and the hover holds the choice's
 * words. */

.graph-edge-casing {
    fill: none;
    stroke: var(--paper);
    stroke-width: 4.6;
}

.graph-edge.is-walked .graph-edge-casing { stroke-width: 6; }

.graph-edge-line {
    fill: none;
    stroke: #b4bbc4;
    stroke-width: 1.7;
    transition: stroke 0.12s ease;
}

.graph-edge-hit {
    fill: none;
    stroke: transparent;
    stroke-width: 12;
}

.graph-edge:hover .graph-edge-line { stroke: #79828e; }

/* A choice that has to be earned, so conditional structure is visible at
   a glance rather than by reading every line. Dashes long enough to
   survive a big story being scaled down to fit. */
.graph-edge.is-gated .graph-edge-line { stroke-dasharray: 9 6; }

.graph-edge.is-broken .graph-edge-line,
.graph-edge.is-broken:hover .graph-edge-line {
    stroke: var(--wrong);
    stroke-dasharray: 2.5 4;
}

.graph-edge.is-walked .graph-edge-line,
.graph-edge.is-walked:hover .graph-edge-line {
    stroke: var(--accent);
    stroke-width: 3;
}

.arrow-plain { fill: #9aa3ad; }
.arrow-walked { fill: var(--accent); }
.arrow-broken { fill: var(--wrong); }

/* -- the title page ------------------------------------------------------ */

/* What `Story(...)` said, shown when somebody presses Play. Centred and
   given room, because the point of it is that a story looks like a piece
   of work with a front rather than a file that starts mid-sentence. */
.cover {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    min-height: 100%;
    padding: 24px 16px;
    text-align: center;
}

.cover-title {
    margin: 0;
    font-size: calc(30px * var(--read-size));
    line-height: 1.15;
    letter-spacing: var(--read-title-track);
    max-width: 20ch;
}

.cover-subtitle {
    margin: 0;
    font-size: calc(16px * var(--read-size));
    color: var(--read-soft);
    max-width: 34ch;
}

.cover-by {
    margin: 10px 0 0;
    font-size: calc(14px * var(--read-size));
    color: var(--read-soft);
}

.cover-about {
    margin: 20px 0 0;
    font-size: calc(16px * var(--read-size));
    line-height: 1.6;
    max-width: 44ch;
}

/* Worked out rather than written: the counts come from the engine, so
   this number moves while a student is still adding scenes. Watching it
   go up is doing the same quiet work the checklist strip does. */
.cover-facts {
    margin: 22px 0 0;
    font-size: calc(13px * var(--read-size));
    color: var(--read-soft);
}

/* Non-breaking spaces, because the ordinary ones either side of a middot
   collapse against the text node next to them and it comes out as
   "4 endings ·September 2026". */
.cover-footnote::before {
    content: "\00a0\00b7\00a0";
}

.cover-begin { margin-top: 18px; }

.cover-thanks {
    margin: 22px 0 0;
    font-size: calc(12.5px * var(--read-size));
    color: var(--read-soft);
    max-width: 40ch;
}

/* -- the story ----------------------------------------------------------- */

/* `pre-line`, because a new line in the story text has to be a new line on
   the screen. The engine keeps them: a blank line in the source becomes a
   new paragraph and a single `\n` stays inside one, which is what `\n` does
   everywhere else in Python. HTML collapses it to a space unless told not
   to, so without this rule the one bit of formatting students reach for
   first silently does nothing. Runs of spaces still collapse, which is
   right: `to_paragraphs` has already stripped the edges of every line. */
.scene .prose {
    margin: 0 0 14px;
    font-size: calc(16px * var(--read-size));
    line-height: 1.65;
    max-width: 62ch;
    white-space: pre-line;
}

/* The scene's own heading, from `title=` on the scene. Small and quiet:
   it says where you are, and then the story talks. */
.scene-heading {
    margin: 0 0 12px;
    font-family: var(--font-ui);
    font-size: calc(11.5px * var(--read-size));
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--read-soft);
}

/* `drop=True` on Story: every scene opens with a big first letter, in
   the story's own face. The renderer marks the first paragraph, because
   CSS cannot find "the first element with this class" on its own.

   The float block is the fallback. A floated cap is sized by hand and
   lands wherever the typeface's metrics drop it, which is never quite
   on the line; `initial-letter` below is the browser doing the font
   math itself, and it wins wherever it exists, which includes every
   Chromebook this runs on. */
:root[data-story-drop="true"] .prose-first::first-letter {
    float: left;
    font-size: 3.15em;
    line-height: 0.9;
    padding: 0.03em 0.07em 0 0;
    /* The soft ink, not a new colour: a coloured initial is the
       tradition, and at this size the softer shade is what keeps it
       from shouting. Soft-on-paper already clears 7:1 in every theme,
       because soft ink is a text colour, and it is what the scene
       heading wears, so the two ornaments speak in one voice. */
    color: var(--read-soft);
}

/* A versal: sunk two lines and risen a little, the illuminated-
   manuscript proportion. `initial-letter`'s own raised form -- a size
   larger than the sink, like `2.6 2` -- is the spec's way to say this,
   and in the browsers this runs on it lets the bottom slide off the
   baseline, which is the one job the property had. What does work:
   `initial-letter: 2` anchors the baseline to the second line and,
   measured rather than assumed, an explicit font-size then scales the
   glyph around that anchor. So the sink pins the foot and the font-size
   raises the top, in ems of the paragraph so both surfaces keep the
   same proportion. Engines that refuse the override get a plain
   two-line drop: smaller, never misaligned. */
@supports (initial-letter: 2) or (-webkit-initial-letter: 2) {
    :root[data-story-drop="true"] .prose-first::first-letter {
        float: none;
        font-size: 3.6em;
        line-height: inherit;
        padding: 0;
        margin-right: 0.12em;
        -webkit-initial-letter: 2;
        initial-letter: 2;
    }
}

/* -- how one scene becomes the next ---------------------------------------
 *
 * `transition=` on Story, worn as an attribute on the root the same way
 * the theme is. The renderer marks the stage `did-move` only when the
 * player actually went somewhere, so an edit re-rendering the panel and
 * the scrubber being dragged stay still. `cut` is the absence of a rule.
 * The `type` transition is JS (js/play.js): it has moving parts. */

/* Slow enough to read as a fade. At a fifth of a second it was a blink
   nobody could tell from `cut`; a fade earns its name by taking a
   moment. The choices wait a beat behind the words, which is most of
   what makes it feel like a fade rather than a flicker. */
:root[data-story-transition="fade"] .did-move .scene {
    animation: sw-arrive-fade 450ms ease both;
}

:root[data-story-transition="fade"] .did-move .choices,
:root[data-story-transition="fade"] .did-move .again-row {
    animation: sw-arrive-fade 450ms ease 180ms both;
}

@keyframes sw-arrive-fade {
    from { opacity: 0; }
}

:root[data-story-transition="page"] .did-move .scene,
:root[data-story-transition="page"] .did-move .choices,
:root[data-story-transition="page"] .did-move .again-row {
    animation: sw-arrive-page 260ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}

@keyframes sw-arrive-page {
    from {
        opacity: 0;
        transform: translateY(14px);
    }
}

/* While `type` is typing, everything under the words holds its breath. */
.is-typing .choices,
.is-typing .again-row,
.is-typing .stuck {
    visibility: hidden;
}

/* Less motion means less motion, not less everything: a plain opacity
   fade is the standard stand-in for movement, so `fade` stays. `page`
   moves, so it goes; `type` skips itself in js/play.js. */
@media (prefers-reduced-motion: reduce) {
    :root[data-story-transition="page"] .did-move .scene,
    :root[data-story-transition="page"] .did-move .choices,
    :root[data-story-transition="page"] .did-move .again-row {
        animation: none;
    }
}

/* An ending has to look different from a scene without anyone explaining
   the difference. That is how the distinction gets taught -- and it is
   the same tinted box in the panel and on the read page, so the signal a
   student learned while writing is the one their family sees. */
.scene.is-ending {
    padding: 16px 20px;
    border-radius: 8px;
    background: var(--read-tint);
    border: 1px solid var(--read-rule);
}

.ending-flag {
    margin: 0 0 10px;
    font-size: calc(12px * var(--read-size));
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--read-soft);
}

/* The colophon at the back, to go with the title page at the front. The
   rule above it is what makes it read as a plate rather than as another
   paragraph of the story. */
.end-plate {
    margin-top: 18px;
    padding-top: 14px;
    border-top: 1px solid var(--read-rule);
}

.end-plate-by {
    margin: 0;
    font-size: calc(13px * var(--read-size));
    font-weight: 600;
    color: var(--read-soft);
}

.end-plate-tally {
    margin: 4px 0 0;
    font-size: calc(12.5px * var(--read-size));
    color: var(--read-soft);
}

/* Shown when a reload could not keep the player exactly where they were. */
/* The tool talking, not the story. A coloured box would be the app's
   amber sitting on whatever paper the author chose, which on `night` or
   `screen` is a lamp in the middle of the page. Quiet, in the story's own
   ink, set apart by a rule down the side. */
.notice-line {
    margin: 0 0 14px;
    padding: 2px 0 2px 12px;
    font-size: 13px;
    color: var(--read-soft);
    border-left: 2px solid var(--read-rule);
}

.stuck {
    margin: 14px 0 0;
    padding: 2px 0 2px 12px;
    font-size: 14px;
    color: var(--read-soft);
    border-left: 2px solid var(--read-rule);
}

/* A stack of rounded boxes, in the panel and on the read page alike. The
   panel is a preview of the page, and two drawings of the same choice
   would be two things to learn. A ruled list was tried in both places
   and looked the part, but a box reads as a thing to press from across
   the room, and set in the story's own face it stays part of the story
   rather than becoming a menu at the end of it. */
.choices {
    margin-top: 22px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.choice {
    display: block;
    width: 100%;
    text-align: left;
    font: inherit;
    font-size: calc(15px * var(--read-size));
    line-height: 1.4;
    padding: 11px 14px;
    border: 1px solid var(--read-rule);
    border-radius: 7px;
    background: var(--read-paper);
    color: var(--read-ink);
    cursor: pointer;
}

.choice:hover { border-color: var(--read-ink); background: var(--read-tint); }

.choice:focus-visible {
    outline: 3px solid var(--read-ink);
    outline-offset: 1px;
}

/* A locked choice looks like any other until it is pressed. Only then does
   it grey out and say why, so a player finds out by trying rather than by
   being told. It stays reachable by keyboard throughout. */
.choice[aria-disabled="true"] {
    color: var(--read-soft);
    background: var(--read-tint);
    border-style: dashed;
    cursor: default;
}

.choice[aria-disabled="true"]:hover {
    border-color: var(--read-rule);
    background: var(--read-tint);
}

.reason {
    margin: 4px 0 0 14px;
    font-size: calc(13px * var(--read-size));
    color: var(--read-soft);
}

.idle {
    color: var(--read-soft);
    padding-top: 8px;
}

.idle-main { margin: 0 0 6px; font-size: 15px; font-weight: 600; }
.idle-detail { margin: 0; font-size: 14px; line-height: 1.5; max-width: 48ch; }

/* -- the way back -------------------------------------------------------- */

.trail {
    border-top: 1px solid var(--line);
    background: var(--panel);
    padding: 6px 12px 7px;
}

.trail-scrub {
    display: flex;
    align-items: center;
    gap: 10px;
}

#scrubber {
    flex: 1;
    min-width: 0;
    height: 18px;
    accent-color: var(--accent);
    cursor: pointer;
}

.trail-turn {
    font-size: 11.5px;
    color: var(--ink-soft);
    white-space: nowrap;
}

.breadcrumbs {
    display: flex;
    align-items: center;
    gap: 3px;
    margin: 4px 0 0;
    padding: 0 0 2px;
    list-style: none;
    overflow-x: auto;
    scrollbar-width: thin;
}

.crumb { display: flex; align-items: center; flex: none; }

/* A separator drawn rather than typed, so it is never read out. */
.crumb + .crumb::before {
    content: "";
    width: 5px;
    height: 5px;
    margin: 0 4px 0 1px;
    border-top: 1.5px solid var(--line);
    border-right: 1.5px solid var(--line);
    transform: rotate(45deg);
}

.crumb-button {
    font: inherit;
    font-family: var(--font-code);
    font-size: 11.5px;
    padding: 2px 7px;
    border: 1px solid transparent;
    border-radius: 4px;
    background: none;
    color: var(--ink-soft);
    white-space: nowrap;
    cursor: pointer;
}

.crumb-button:hover {
    background: var(--paper);
    border-color: var(--line);
    color: var(--ink);
}

.crumb.is-here .crumb-button {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent);
    font-weight: 700;
}

/* -- variables ----------------------------------------------------------- */

.vars {
    height: var(--vars-height);
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--line);
    background: var(--panel);
}

.vars-heading {
    margin: 0;
    padding: 8px 14px 6px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.vars-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 0 14px 12px;
}

.vars-empty {
    margin: 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--ink-soft);
    max-width: 46ch;
}

.vars-table {
    border-collapse: collapse;
    font-family: var(--font-code);
    font-size: calc(13px * var(--text-scale));
}

.vars-table td {
    padding: 3px 18px 3px 0;
    vertical-align: top;
}

.vars-name { color: var(--ink-soft); }
.vars-value { font-weight: 600; }

/* What moved on this turn.
 *
 * docs/UI_SPEC.md: brief, not distracting. This is what makes an abstract
 * idea concrete, so it has to be noticeable, and it has to be over before
 * anybody has to wait for it. The engine only reports a variable as
 * changed when its value actually moved, so this never fires at nothing.
 */
.vars-table tr.is-changed .vars-value {
    animation: sw-changed 1.4s ease-out;
    border-radius: 3px;
}

@keyframes sw-changed {
    0% { background: var(--playing-edge); color: var(--ink); }
    35% { background: var(--playing); }
    100% { background: transparent; }
}

.vars-table tr.is-changed .vars-name::after {
    content: "";
    display: inline-block;
    width: 5px;
    height: 5px;
    margin-left: 5px;
    border-radius: 50%;
    background: var(--playing-edge);
    animation: sw-changed-dot 1.4s ease-out forwards;
}

@keyframes sw-changed-dot {
    0%, 60% { opacity: 1; }
    100% { opacity: 0; }
}

/* -- the story on its own ------------------------------------------------ */

/* Everything below dresses elements that js/play.js built. The read page is
   the same renderer pointed at a different element, so there is nothing
   here to keep in step with the panel beside the editor except how it
   looks -- which is the entire point of the feature. See js/read.js.

   Over the app rather than instead of it: the editor underneath keeps its
   layout, so CodeMirror goes on scrolling to the scene being read and the
   page comes down without a reflow. */

/* `flex-start` rather than the default stretch, with the height asked for
   below instead. Stretch pins the column to exactly one screenful, and a
   title page taller than that -- which Text size at 160% makes of any of
   them -- then centres itself inside a box it does not fit in and puts its
   own first line above the top of the scroll, where nothing can reach it. */
.read {
    position: fixed;
    inset: 0;
    z-index: 40;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    overflow-y: auto;
    font-family: var(--font-read);
    color: var(--read-ink);
    background: var(--read-ground);
}

/* A column measured in the reading face rather than in pixels, so that the
   Text size button makes the words bigger without also making the lines
   longer. Thirty-something characters wider than that and a reader loses
   their place coming back to the left margin. */
.read-stage {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 38em;
    min-height: 100%;
    /* The foot matches the head rather than being the deeper margin a book
       would have. A scene that only just fits should not invent a
       scrollbar for the sake of white space nobody asked to scroll to. */
    padding: 70px 24px 70px;
    font-size: calc(19px * var(--text-scale));
}

/* -- the page, on a desk -- */

/* On a wide screen this was a column of words in the middle of a field of
   exactly the same colour, with nothing anywhere saying where the page
   was. It read as text floating rather than as a thing somebody made.

   So the ground goes to the story's tint and the column takes the story's
   paper and an edge of its own, which is the whole difference between a
   page and a paragraph that happens to be narrow. Every value is one the
   theme already carried; --read-tint in particular was a colour six
   themes declared and almost nothing used.

   Above 880px only, which is the width the rest of this file already
   calls wide. Below it the column *is* the screen, and an edge there is a
   rectangle drawn round the inside of the bezel. */
@media (min-width: 881px) {
    :root { --read-ground: var(--read-tint); }

    .read-stage {
        /* Never edge to edge, whatever Text size is set to. At 160% the
           measure is wider than some of the screens this runs on, and a
           page with no desk beside it is the thing being fixed here. */
        max-width: min(38em, calc(100% - 96px));
        margin: 40px 0;
        min-height: calc(100% - 80px);
        background: var(--read-paper);
        border: var(--read-edge);
    }

}

/* The way out. Small, in the interface's own face rather than the
   story's, and out of the way at the top -- but never faint enough to be
   hard to read, because it is the only exit and a page with no visible
   way off it is where a twelve-year-old decides the tool has broken.

   Fixed, so a long scene cannot scroll it off. The button carries its
   own paper; the frame paints nothing. There used to be a fade across
   the whole width here keeping the button legible over prose sliding
   underneath, and it read as a band of weather laid over the top of
   every page. One small pill does the same job standing still. */
.read-frame {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 11px 16px;
    pointer-events: none;
}

.read-quiet {
    pointer-events: auto;
    font: inherit;
    font-family: var(--font-ui);
    font-size: 12.5px;
    padding: 4px 12px;
    color: var(--read-soft);
    background: var(--read-paper);
    border: 1px solid var(--read-rule);
    border-radius: 999px;
    cursor: pointer;
}

.read-quiet:hover {
    color: var(--read-ink);
    background: var(--read-tint);
}

.read-quiet:focus-visible {
    outline: 3px solid var(--read-ink);
    outline-offset: 1px;
}

/* Close is the only thing up here now, so it sits right on its own. */
.read-close { margin-left: auto; }

/* -- the reader's two arrows -- */

/* Under the choices, where somebody who has just taken the wrong door is
   already looking, rather than in a corner they would have to go and
   find. Arrows and no words: everything else on this page is the story's
   words, and these are not the story's.

   Side by side, as a pair. They sat at opposite ends of the column for
   one draft, on the theory that each belonged on the side it points to;
   apart they read as two unrelated controls, and somebody reaching for
   one wants to see where the other is. The row is only here at all when
   the author wrote `back=True`. */
/* No rule of its own. The choice list already ends with one and the end
   plate has its own, so a border here drew a second line a few
   millimetres under the first with nothing between them. */
.read-nav {
    display: flex;
    gap: 4px;
    margin-top: 1.1em;
}

.read-step {
    display: inline-flex;
    align-items: center;
    padding: 0.5em 0.7em;
    color: var(--read-soft);
    background: none;
    border: 1px solid transparent;
    border-radius: 999px;
    cursor: pointer;
}

.read-step:hover {
    color: var(--read-ink);
    background: var(--read-tint);
}

.read-step:focus-visible {
    outline: 3px solid var(--read-ink);
    outline-offset: 1px;
}

.read-step .icon {
    width: 22px;
    height: 22px;
}

/* The words are still here, clipped, the same as the toolbar's. */
.read-step .button-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* A page turn. Short, and it only ever runs on elements the renderer has
   just made, so nothing has to remember to retrigger it. The
   reduced-motion rule at the foot of this file switches it off, and an
   animation that is off simply leaves the words where they are. */
.read .cover,
.read .scene,
.read .choices,
.read .again-row {
    animation: read-in 260ms ease-out both;
}

.read .choices,
.read .again-row { animation-delay: 90ms; }

@keyframes read-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: none; }
}

/* -- the title page, on paper -- */

/* `margin: auto 0` in a flex column is what centres a cover in whatever
   room is left and stops centring the moment there is none. */
.read .cover {
    margin: auto 0;
    min-height: 0;
    padding: 0;
    gap: 0;
}

/* The stage above holds the page's size; --read-scale is multiplied in
   here rather than there, so it lands on the things actually set in the
   story's face and not on the handful set in the interface's. Scaling
   `screen` down for its mono body would otherwise have taken the choice
   list down with it, and the choice list is not in mono. The panel needs
   none of this: everything in it is in the story's face already. */
.read .cover-title {
    font-size: calc(2.05em * var(--read-scale));
    line-height: 1.12;
    max-width: 16ch;
}

.read .cover-subtitle {
    margin-top: 0.7em;
    font-size: calc(0.92em * var(--read-scale));
    max-width: 30ch;
    color: var(--read-soft);
}

.read .cover-by,
.read .cover-facts,
.read .cover-thanks {
    font-family: var(--font-ui);
    color: var(--read-soft);
}

.read .cover-by {
    margin-top: 1.7em;
    font-size: 0.72em;
    letter-spacing: 0.13em;
    text-transform: uppercase;
}

/* A short rule between what the story is called and everything else the
   title page has to say. Two things at once: it is the only structure a
   cover had -- six of them differing in colour and face were still the
   same page laid out six times -- and it is the only place --read-rule
   reaches a cover at all, a value every theme carried and no cover used.

   Hung off the `by` line rather than given an element of its own, so the
   renderer goes on being a renderer. A story with nobody's name on it
   loses the rule with it, which is a barer cover either way. */
.read .cover-by::before {
    content: "";
    display: block;
    width: 5em;
    height: 1px;
    margin: 0 auto 1.6em;
    background: var(--read-rule);
}

.read .cover-about {
    margin-top: 1.8em;
    font-size: calc(1em * var(--read-scale));
    line-height: 1.7;
    max-width: 32ch;
}

.read .cover-facts {
    margin-top: 2.2em;
    font-size: 0.66em;
    letter-spacing: 0.07em;
    text-transform: uppercase;
}

.read .cover-thanks {
    margin-top: 2.4em;
    font-size: 0.68em;
}

.read .cover-begin { margin-top: 1.9em; }

/* Ink on paper, on both surfaces a story is shown on. The toolbar's blue
   is the tool's colour: on the read page the tool is the thing being got
   rid of, and the panel's cover is still the story's front, so a blue
   Begin there -- bluer still on hover -- was the tool's hand reaching
   into the page. */
.read .button.primary,
.play .button.primary {
    font-family: var(--font-ui);
    color: var(--read-paper);
    background: var(--read-ink);
    border-color: var(--read-ink);
}

/* The background again, not just the opacity: the toolbar's own hover
   (`.button.primary:hover`) outranks the plain surface rules above and
   was repainting Begin blue at the exact moment of pressing it. */
.read .button.primary:hover:not(:disabled),
.play .button.primary:hover:not(:disabled) {
    background: var(--read-ink);
    opacity: 0.82;
}

.read .button:focus-visible,
.play .button:focus-visible {
    outline: 3px solid var(--read-ink);
    outline-offset: 2px;
}

/* -- the scene -- */

/* The column already holds the measure, so the paragraph gives its own up.
   `white-space` is inherited from the rule above rather than restated: a
   new line in the story is still a new line on the screen here, and that
   is the one bit of formatting students reach for first. */
.read .scene .prose {
    margin: 0 0 1.15em;
    font-size: calc(1em * var(--read-scale));
    line-height: 1.75;
    max-width: none;
}

/* The page's own size for the heading; everything else it is comes from
   the shared rule. */
.read .scene-heading {
    margin: 0 0 1.1em;
    font-size: 0.62em;
    letter-spacing: 0.2em;
}

/* Both of these are the tool talking rather than the story, so they are
   set in the tool's face and kept small. Neither is worth a coloured box
   on a page whose job is to have nothing on it. */
.read .notice-line,
.read .stuck {
    margin: 0 0 1.4em;
    padding: 0.15em 0 0.15em 0.9em;
    font-family: var(--font-ui);
    font-size: 0.72em;
    line-height: 1.55;
    color: var(--read-soft);
    background: none;
    border-left: 2px solid var(--read-rule);
    border-radius: 0;
}

.read .stuck { margin: 1.6em 0 0; }

/* -- the choices -- */

/* The same boxes the panel shows, at the same size. Only the space above
   them is the page's own: a page has more room to breathe than a panel
   somebody is working next to. */
.read .choices { margin-top: 2.6em; }

/* The way round again, in the one place the toolbar's Restart is not on
   screen to do it. */
.read .again-row {
    display: flex;
    justify-content: center;
    margin-top: 2.8em;
}

.read .again { padding: 10px 22px; }

/* -- narrow screens ------------------------------------------------------ */

@media (max-width: 880px) {
    .workspace {
        grid-template-columns: 1fr;
        grid-template-rows: 1fr 7px 1fr;
        overflow-y: auto;
    }

    .splitter { cursor: row-resize; }
    .pane-right { border-left: none; border-top: 1px solid var(--line); }
}

@media (prefers-reduced-motion: reduce) {
    * { animation: none !important; transition: none !important; }
}
