﻿/* Scene Board.
 *
 * Ported from docs/storyboard-handoff/public/scene-board.css. Everything is
 * namespaced under .sb- so it cannot reach the rest of the dashboard.
 *
 * Two deliberate changes from the handoff:
 *
 *   - Archivo is gone. The board uses Cabin Condensed, the site font.
 *   - The accent was #ec3013. Remapped to the site gold, with the shared
 *     danger red kept for errors only.
 *
 * .sb-host is new and load-bearing. .page is display:block with no height
 * (css/dashboard.css), so .sb-board's height:100% flex column collapses to
 * nothing and the board renders blank without it.
 */

/* Sized off --header-height (css/global.css) rather than a measured constant,
   so the board follows the site header instead of drifting when it changes.
   Same shape as the .content rule in global.css:283.
   Note the editor sets --header-height to 0 (editor/css/pr-editor.css), which
   is correct there: it runs full screen with no site header. */
.sb-host {
    display: flex;
    height: calc(100vh - var(--header-height) - 20px);
    min-height: 420px;
}

/* For a board embedded in a container that ALREADY HAS A DEFINITE HEIGHT --
   a panel on an idea page, say.

   Not a general replacement for the rule above. A percentage height resolves
   against the parent's height, and on the dashboard there is none to resolve
   against: .content is `flex: 1` with no height and .page is a plain block with
   only padding, so this collapses to zero and the board renders blank. That was
   the original bug this file was written to fix.

   Use it only where you control the parent's height, and check it in place.

     <div class="sb-host sb-host-fill"></div>
*/
.sb-host-fill {
    height: 100%;
    min-height: 0;
}

/* The Scene Board pane inside the editor (pages/editscript.php).
 *
 * Same shape as #tutorialDiv in editor/css/tutorial.css: a full-height pane of
 * .editorMain, which has margins but no height of its own. --header-height is
 * already 0px in the editor (editor/css/pr-editor.css), so this resolves to the
 * viewport and needs nothing measured. The root inside carries .sb-host-fill,
 * the variant written for exactly this -- a parent whose height is definite.
 *
 * No position or z-index here on purpose. The reason a pane in this stack would
 * want them is .editorMain > .fixed-wrapper, which is position:fixed across the
 * viewport at z-index 100 and shows the script header and footer through
 * anything merely later in the document. showSceneBoard() hides that wrapper
 * instead, which removes the cause rather than out-stacking it -- otherwise an
 * invisible fixed layer stays over the board, and anything added to it later
 * bleeds through again. */
#sceneBoardDiv {
    margin-top: 7px;
    height: calc(100vh - var(--header-height) - 8px);
}

/* Overlay variant: covers the parent completely, for a board that sits on top
   of the script or title-page editor rather than beside it. */
.sb-host-overlay {
    position: absolute;
    inset: 0;
    height: auto;
    min-height: 0;
    z-index: 20;
    background: var(--sb-bg, #f3f2f2);
}

/* One palette, both roots.
 *
 * .sb-list is not inside .sb-board -- a board and the listing are alternative
 * renders of the same mount, siblings rather than parent and child. So anything
 * shared between them, .sb-readonly most of all, found these undefined on the
 * list: var() with no fallback makes the whole declaration invalid, which cost
 * that tag its colour and its border entirely. */
.sb-board,
.sb-list {
    --sb-ink: #201e1d;
    --sb-bg: #f3f2f2;
    --sb-surface: #fff;
    --sb-rule: #201e1d;
    --sb-line: #c9c5c3;
    --sb-muted: #6b6664;
    --sb-accent: #c9a84c;
    --sb-accent-dark: #ad8f39;
    --sb-accent-tint: #f7f0dc;
    --sb-danger: #e85347;
}

.sb-board {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    width: 100%;
    background: var(--sb-bg);
    color: var(--sb-ink);
    font-family: 'Cabin Condensed', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.sb-board *,
.sb-board *::before,
.sb-board *::after {
    box-sizing: border-box;
}

.sb-loading {
    padding: 40px;
    font-size: 14px;
    color: #6b6664;
}

/* header
 *
 * One row on a laptop. Two things keep it there: the pickers stack their label
 * above the select rather than beside it, and the title truncates instead of
 * pushing. nowrap is deliberate -- letting this wrap is what doubled its height.
 */
.sb-head {
    display: flex;
    flex-wrap: nowrap;
    align-items: flex-end;
    gap: 8px;
    padding: 10px 20px;
    border-bottom: 2px solid #201e1d;
    background: #fff;
    flex: none;
}

/* Takes the slack, and gives it back first. min-width:0 is what allows a flex
   item to shrink below its content -- without it a long title cannot ellipsis
   and shoves everything else off the row.
   Stacked in two lines to match the label-above-select pickers beside it, so
   the gap matches theirs exactly. */
.sb-brand {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
}

/* Title and its pencil on one line. min-width:0 so the title can still shrink
   past its content and ellipsis -- without it a long name pushes the pencil
   off the row instead of truncating. */
.sb-title-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    max-width: 100%;
    min-width: 0;
}

.sb-title {
    max-width: 100%;
    min-width: 0;
    font-size: 18px;
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The title of a script-built board doubles as the way into the script. No
   underline at rest -- it is the page's heading first and a link second, and a
   permanently underlined 18px title reads as a mistake. */
.sb-title-link {
    color: inherit;
    text-decoration: none;
    cursor: pointer;
    transition: color .12s ease;
}

.sb-title-link:hover,
.sb-title-link:focus-visible {
    color: var(--sb-accent-dark);
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* Quiet, like the plus in a beat header: it sits beside the title permanently,
   and at full weight it would compete with the one thing the header is for. */
.sb-rename {
    flex: none;
    align-self: center;
    padding: 0;
    border: 0;
    background: none;
    color: var(--sb-line);
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    transition: color .12s ease;
}

/* Approaching the title brings it up, the way a beat brings up its plus.
   Scoped to the title row rather than the whole header, so crossing the header
   to reach a picker does not light it.

   The second rule is not redundant: an ancestor-hover selector outweighs the
   plain :hover below it, so without it the pencil would stay muted at the one
   moment it is actually being pointed at. Same shape as .sb-beat:hover .sb-add. */
.sb-title-row:hover .sb-rename {
    color: var(--sb-muted);
}

.sb-title-row:hover .sb-rename:hover,
.sb-rename:hover,
.sb-rename:focus-visible {
    color: var(--sb-accent-dark);
}

/* Delete. Same quiet-until-approached treatment as the pencil, but it resolves
   to the danger red rather than the accent -- the one control here that cannot
   be undone should not look like the ones that can. */
.sb-delete-board {
    flex: none;
    align-self: center;
    padding: 0;
    border: 0;
    background: none;
    color: var(--sb-line);
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    transition: color .12s ease;
}

.sb-title-row:hover .sb-delete-board {
    color: var(--sb-muted);
}

/* Ancestor-hover outranks a plain :hover, so the combined selector is what lets
   it turn red at the moment it is actually pointed at. */
.sb-title-row:hover .sb-delete-board:hover,
.sb-delete-board:hover,
.sb-delete-board:focus-visible {
    color: var(--sb-danger);
}

.sb-brand-sub {
    display: flex;
    align-items: center;
    gap: 8px;
    max-width: 100%;
}

/* Same size and tracking as .sb-field-label, so the second line of the brand
   and the labels on the pickers read as one row of small caps. */
.sb-kicker {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--sb-accent-dark);
    line-height: 1;
    white-space: nowrap;
}

.sb-readonly {
    flex: none;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--sb-muted);
    border: 1px solid var(--sb-line);
    padding: 1px 6px;
    line-height: 1.3;
}

/* Label above control. */
.sb-field {
    flex: none;
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.sb-field-label {
    font-size: 12px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--sb-muted, #6b6664);
    line-height: 1;
}

.sb-select {
    font: 500 13px 'Cabin Condensed', sans-serif;
    padding: 5px 8px;
    min-width: 0;
    width: 190px;
    max-width: 100%;
    border: 1px solid var(--sb-rule);
    border-radius: 0;
    background: var(--sb-surface);
    color: var(--sb-ink);
}

/* The structure picker carries the long framework names; these two are short. */
.sb-select-layout {
    width: 130px;
}

.sb-select-density {
    width: 120px;
}

/* Below a laptop there is not enough width for one row however tight the
   controls are, so let it wrap rather than crush the selects. */
@media (max-width: 900px) {
    .sb-head {
        flex-wrap: wrap;
    }

    .sb-brand {
        flex: 1 0 100%;
    }

    /* The captions above the pickers go.
     *
     * This is where the header has just wrapped, so it is already two rows tall
     * -- and a caption sitting over each select makes the second of those rows
     * twice the height it needs to be. What the pickers are is legible from
     * what they are showing: a select reading "Three Act" or "Full cards" does
     * not need the word Structure or Cards written above it.
     *
     * Each select carries an aria-label, so the name is still there for anyone
     * not reading the screen -- the <label> wrapping it would otherwise be
     * empty and name nothing at all. */
    .sb-head .sb-field-label {
        display: none;
    }
}

/* Sits on the brand's second line, so it matches the kicker rather than
   competing with it. */
.sb-count {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--sb-muted);
    line-height: 1;
    white-space: nowrap;
}

.sb-count b {
    font-size: 11px;
    letter-spacing: 0;
    color: var(--sb-ink);
}

/* Bullet as generated content: it belongs to the join between the two, not to
   either of them, and nothing stray ends up in the DOM. */
.sb-brand-sub .sb-count::before {
    content: '\2022';
    margin-right: 7px;
    font-weight: 400;
    color: var(--sb-line);
}

/* Paired with .solid-btn (css/global.css, loaded on every page -- it used to be
   .blog-new-btn in css/blog.css, which is not loaded in the editor). This adds
   only what .solid-btn leaves out: it was written for an <a>, so a <button>
   still brings its own border and cursor. */
.sb-nav-btn {
    flex: none;
    align-self: flex-end;
    border: 0;
    cursor: pointer;
    font-family: 'Cabin Condensed', sans-serif;
}

/* Secondary: same shape, outlined. Two filled buttons side by side both read as
   the main action, which leaves neither of them reading as one. */
/* The words on the header buttons, hidden when the board's name needs the room.
 *
 * The header is one row of fixed-width things -- three pickers at 190, 130 and
 * 120, two buttons at roughly 130 each, plus gaps and padding -- and one thing
 * that gives: the board's name. So as the window narrows, everything else holds
 * its size and the name is eaten a letter at a time until it is an ellipsis.
 *
 * Dropping the two labels returns about 140px, which is the difference between
 * a name you can read and a name you can tell is there. The icons stay, and both
 * controls carry an aria-label so losing the words costs nothing to a screen
 * reader.
 *
 * 1200px, because that is roughly where it starts to hurt: with the dashboard's
 * sidebar taking 165px, a 1280px window leaves the name about 305px and a
 * 1100px window about 125px. Viewport width is a proxy for the board's own --
 * the same board in the editor has a panel beside it and is tighter than this
 * measure knows -- but there is no container query here to ask properly. */
@media (max-width: 1200px) {

    .sb-head .sb-nav-btn-label {
        display: none;
    }

    /* Square-ish once there is only a glyph inside: the 15px side padding was
       sized for text sitting next to it. */
    .sb-head .sb-nav-btn {
        padding: 7px 10px;
    }
}

.sb-nav-btn-alt {
    background: transparent;
    color: var(--primary, #6576ff);
    box-shadow: inset 0 0 0 1px var(--primary, #6576ff);
}

.sb-nav-btn-alt:hover {
    background: var(--primary, #6576ff);
    color: #fff;
    filter: none;
}

/* The handoff's own square button style is gone -- every button in the widget
   now wears the site's, via .solid-btn + .sb-nav-btn above. */
.sb-board :focus-visible {
    outline: 2px solid var(--sb-accent-dark);
    outline-offset: 2px;
}

/* No focus ring on the pickers.
 *
 * A select matches :focus-visible on a plain click, not only from the keyboard,
 * so the board's ring above fired every time one was opened -- and a dropdown
 * that has just opened is not ambiguous about where the focus went. The control
 * says so itself.
 *
 * This does mean a keyboard user tabbing through the header gets no mark on
 * these three. Judged an acceptable trade for a control that announces itself
 * the moment it is used. */
.sb-board .sb-select:focus,
.sb-board .sb-select:focus-visible {
    outline: none;
}

/* The body holds the holding group and the structure side by side. Which way
   round, and which axis, is the layout's business. */
.sb-body {
    flex: 1;
    min-height: 0;
    display: flex;
}

/* Undo, in the header beside the pickers.
 *
 * Always present rather than appearing when there is something to take back: a
 * control that comes and goes is one you have to notice arriving, and its
 * disabled state is itself worth reading -- it says whether the drag you just
 * made registered. Sized like the pickers it stands next to rather than like the
 * quiet icons on the title row, because this one is reached for on purpose. */
.sb-undo {
    flex: none;
    align-self: flex-end;
    margin-bottom: 1px;
    padding: 5px 9px;
    border: 1px solid var(--sb-rule);
    border-radius: 5px;
    background: var(--sb-surface);
    color: var(--sb-ink);
    font-size: 13px;
    line-height: 1.2;
    cursor: pointer;
    transition: color .12s ease, border-color .12s ease, opacity .12s ease;
}

.sb-undo:not(:disabled):hover,
.sb-undo:not(:disabled):focus-visible {
    border-color: var(--sb-accent);
    color: var(--sb-accent-dark);
}

/* Dimmed rather than hidden, and still readable: it is the difference between
   "there is nothing to undo" and "undo has gone away". */
.sb-undo:disabled {
    opacity: .35;
    cursor: default;
    color: var(--sb-muted);
}

/* columns */
.sb-cols {
    flex: 1;
    min-height: 0;
    display: flex;
    overflow-x: auto;
    overflow-y: hidden;
}

/* 285px is what an act column would like to be, and whether it is allowed to
 * settle for less depends on how many columns there are.
 *
 * Squeezing is only worth doing when it *avoids* a scrollbar. A three-act board
 * narrowed to 234px columns still shows the whole story at once, which is the
 * shape it most needs to show. A seven-column television structure squeezed the
 * same way shows seven cramped columns and scrolls anyway -- every column made
 * worse and nothing gained, because you were never going to read seven at once.
 *
 * So: four or fewer share the width, five or more hold 285 and scroll. Nobody
 * needs more than three or four columns on screen at a time; past that, the
 * useful thing is for the ones you can see to be comfortable.
 *
 * An act column rarely gets the whole window. On the dashboard the sidebar takes
 * 165px, in the editor 145px plus whatever panel is open, and the Unassigned
 * rail another 232px on top -- so the space the columns divide is a good deal
 * less than the screen.
 *
 *   THREE ACTS          1366px window -> 3 columns at 323
 *                       1100px        -> 3 at 234
 *                        900px        -> 2 at 257, third scrolls
 *
 *   SEVEN COLUMNS       any width     -> 285 each, scrolling past four
 *
 * max-width keeps a board with few columns from spreading a card across half a
 * monitor. 400px is a little under a card's worth of comfortable line length --
 * a slugline and a six-line synopsis stop being easier to read past about that,
 * and start being a long way for the eye to travel back. On a wide screen the
 * leftover space is simply left over, which reads better than columns stretched
 * to fill it. */
.sb-col {
    flex: 1 0 285px;
    min-width: 285px;
    max-width: 400px;
    display: flex;
    flex-direction: column;
    min-height: 0;
    border-right: 2px solid var(--sb-rule);
}

/* Four or fewer: let them give width up together rather than scroll.
 *
 * :first-child:nth-last-child(-n+4) is true only when the first column is also
 * among the last four -- which is another way of saying there are at most four
 * of them. The sibling combinator then catches the rest. It works here because
 * .sb-cols holds nothing but columns; the Unassigned rail is a sibling of that
 * container, not a child of it.
 *
 * 200px is the real floor, and it is about reading rather than arithmetic:
 * below it a slugline stops being legible and a column becomes a list you
 * scroll past rather than one you read down.
 *
 * Columns only, and said so in the selector.
 *
 * The sibling half of this reads .sb-col:first-child:...~.sb-col -- two
 * classes and two pseudo-classes, which outweighs .sb-board.is-lanes .sb-col
 * and outranked the layout it was written to leave alone. So every lane after
 * the first took flex-shrink: 1 back from here, and lanes stack down the page:
 * shrink on a column axis is vertical, .sb-col carries min-height: 0, and the
 * lanes squashed under their own contents. What sat inside them went on being
 * as tall as it was, over the top of the lane below, and the scroll height was
 * measured from boxes that had given up their height.
 *
 * :not() twice because 'columns' is the default and has no class of its own. */
.sb-board:not(.is-lanes):not(.is-flow) .sb-col:first-child:nth-last-child(-n+4),
.sb-board:not(.is-lanes):not(.is-flow) .sb-col:first-child:nth-last-child(-n+4)~.sb-col {
    flex-shrink: 1;
    min-width: 200px;
}

.sb-act {
    flex: none;
    background: var(--sb-ink);
    color: var(--sb-bg);
    padding: 12px 16px 14px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.sb-act-row {
    display: flex;
    align-items: baseline;
    gap: 10px;
}

.sb-act-name {
    font-size: 17px;
    font-weight: 700;
    letter-spacing: -0.01em;
}

.sb-act-meta {
    margin-left: auto;
    font-size: 11px;
    letter-spacing: .1em;
    text-transform: uppercase;
    opacity: .65;
}

.sb-act-bar {
    height: 4px;
    background: rgba(243, 242, 242, .25);
}

.sb-act-bar span {
    display: block;
    height: 100%;
    background: var(--sb-accent);
}

.sb-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 14px;
    display: flex;
    flex-direction: column;
    gap: 14px;
}

/* beats */
.sb-beat {
    background: var(--sb-surface);
    border: 2px solid var(--sb-rule);
}

/* One row, always: chevron, then the name and its counts, then the plus hard
   against the right edge. Nothing here wraps -- the wrapping happens inside
   .sb-beat-titles, which is the whole reason that box exists.

   align-items:baseline rather than centre so the chevron and the plus sit on
   the name's baseline whether the counts are beside it or beneath it. A flex
   container's baseline is its first line's, so the two stay put when the middle
   grows to two lines. */
.sb-beat-head {
    display: flex;
    flex-wrap: nowrap;
    align-items: baseline;
    gap: 8px;
    padding: 6px 8px;
    background: #faf4e2;
    border-bottom: 2px solid var(--sb-rule);
}

/* The name and the counts, and the only thing on this header that wraps.
 *
 * Takes the width the chevron and the plus do not want, and shares it: the
 * counts sit at the right of the name while both fit, and drop to a second line
 * inside this box when they stop fitting. The chevron and the plus never move,
 * because they are outside it.
 *
 * min-width:0 so this can shrink below its content -- without it a flex item
 * refuses to go under its natural width and the whole header would push the
 * column wider instead. */
/* space-between is what gives the counts two different alignments without
   anything having to know whether they wrapped.
 *
 * Sharing a line with the name, they are the last item of two and go to the far
 * edge -- which is where a count belongs when it is sitting beside a title.
 * Alone on the second line they are the only item on it, and the spec makes
 * space-between behave as flex-start for a single item, so they land under the
 * name's first letter instead.
 *
 * margin-left:auto would have done the first of those and not the second: it
 * pushes to the right whether or not there is anything to be pushed away from,
 * so a wrapped line kept the counts stranded at the far edge. */
.sb-beat-titles {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    /* Nothing between the two lines. The name is uppercase and the counts are
       small and grey, so they are already told apart by weight and colour --
       adding space as well only made the header taller for a distinction that
       had already been made. */
    row-gap: 0;
}

/* Both of these carry align-self:center, written when this header was always
   one line tall and centring meant "beside the name". Now that the middle can
   be two lines, centring means beside the gap between them -- which is how the
   chevron ended up floating in the middle of a taller header. On the baseline
   they sit on the name's line whatever the counts are doing below it.

   Scoped to the beat header: the holding column's plus lives in a header that
   is still one line, and the centring is right for it. */
.sb-beat-head .sb-beat-collapse,
.sb-beat-head .sb-add {
    align-self: baseline;
}

/* The name gives way first. Four things share this row at 11px -- chevron,
   name, meta, plus -- and the meta is the one that must not wrap, because a
   page total broken across two lines reads as two numbers. */
/* A floor, where there was none.
 *
 * This is what the ellipsis problem actually was: the name carried min-width:0
 * and the default flex-shrink, while the meta beside it was flex:none. So the
 * name was the only thing that ever gave way, and it gave way all the way --
 * "Bad Guys Close In" became "Bad G..." to protect a scene count that had not
 * been asked to yield a pixel.
 *
 * 120px is about fifteen characters at 12px, which is enough to tell one beat
 * from another rather than merely enough to prove something is written there.
 * It is also what fits: once the chevron, the plus and the gaps are paid for,
 * the name has 141px to work with at the 200px column floor and 131px at the
 * 190px lanes floor. Asking for much more than that would push the header wider
 * than its column.
 *
 * A floor, not a width -- a 400px column gives the name 341px and it takes
 * them. */
.sb-beat-name {
    min-width: 120px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--sb-accent-dark);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Scene count and page total, e.g. "4 scenes · 3 5/8 pgs." */
/* Follows the name rather than being pushed to the far edge. Left-packed, the
   counts read as a note belonging to the name -- and when they drop to the
   second line they land directly under its first letter, which is where the eye
   is already looking. Pinned right, they were a column of their own that
   happened to sit near the name. */
.sb-beat-meta {
    flex: none;
    font-size: 11px;
    font-weight: 500;
    color: var(--sb-muted);
    white-space: nowrap;
}

/* Fold a beat away. Sits left of the name so the chevrons point the way the
   beat moves, matching the holding rail's own collapse control. */
.sb-beat-collapse {
    flex: none;
    align-self: center;
    padding: 0;
    border: 0;
    background: none;
    color: var(--sb-muted);
    font-size: 11px;
    line-height: 1;
    width: 12px;
    text-align: left;
    cursor: pointer;
    transition: color .12s ease;
}

.sb-beat-collapse:hover,
.sb-beat-collapse:focus-visible {
    color: var(--sb-accent-dark);
}

/* ---------------------------------------------------------------------------
 * Collapsed beat
 *
 * Same bargain as the collapsed holding rail: the cards go, the .sb-drop stays.
 * storyboard.js does not build the cards at all when a beat is shut -- which is
 * what actually shortens the column -- but it still emits the drop container
 * and its tail marker, so the beat remains a drop target. A folded beat you
 * cannot file a scene into would be useless precisely when it is most wanted,
 * which is when everything except the target has been folded away.
 *
 * The strip keeps enough height to be an easy target and still tints on hover.
 * ------------------------------------------------------------------------- */

.sb-beat.is-collapsed .sb-beat-head {
    border-bottom: 0;
}

.sb-beat.is-collapsed .sb-drop {
    min-height: 0;
    padding: 6px 10px 8px;
}

/* Tighter than the box in an expanded beat. At its usual 14px padding a folded
   beat costs ~50px, and thirteen of them would give back barely more height
   than they took -- which is the whole reason for folding. This lands near
   30px: unmistakably a target, still a fraction of the cards it replaced. */
.sb-beat.is-collapsed .sb-empty {
    padding: 7px 10px;
    font-size: 11px;
}

/* Add a scene straight into this group. Quiet until wanted -- it sits in every
   beat header, so at full colour it would out-shout the beat names it labels. */
.sb-add {
    flex: none;
    align-self: center;
    margin-left: 2px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--sb-line);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    transition: color .12s ease, transform .12s ease;
}

.sb-add:hover,
.sb-add:focus-visible {
    color: var(--sb-accent-dark);
    transform: scale(1.15);
}

/* The whole beat brightens its plus on hover, so the affordance is findable
   without hunting for a 15px target. */
.sb-beat:hover .sb-add,
.sb-unassigned:hover .sb-add {
    color: var(--sb-muted);
}

.sb-beat:hover .sb-add:hover,
.sb-unassigned:hover .sb-add:hover {
    color: var(--sb-accent-dark);
}

.sb-drop {
    padding: 10px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: 56px;
    transition: background .12s ease;
}

.sb-drop.is-over {
    background: var(--sb-accent-tint);
}

.sb-empty {
    border: 2px dashed var(--sb-line);
    padding: 14px 12px;
    font-size: 12px;
    color: var(--sb-muted);
}

/* cards */
.sb-slot {
    display: flex;
    flex-direction: column;
}

.sb-card {
    border: 2px solid var(--sb-line);
    border-radius: 5px;
    background: var(--sb-surface);
    padding: 10px 12px;
    display: flex;
    flex-direction: column;
    gap: 5px;
    /* The -webkit- prefix is the one that matters, and it is not decoration:
       Safari only understood plain `user-select` from version 17, so on any
       iPhone older than that the card's text stayed selectable -- and iOS turns
       a drag that starts on selectable text into a *selection* drag, which does
       not scroll. That is why a quick finger down a column highlighted a
       synopsis instead of moving the column, and why it happened even starting
       on the grip. */
    -webkit-user-select: none;
    user-select: none;
    /* And no long-press callout or magnifier, which is the same gesture arriving
       by a different route. */
    -webkit-touch-callout: none;
    /* Panning and pinching still belong to the browser -- that is how the column
       scrolls -- but double-tap-to-zoom does not. Two taps on a card mean open
       it, and a card is not something anybody wants to zoom into. See
       armTapGesture(), which supplies the gesture this rules out. */
    touch-action: manipulation;
    /* No grey flash on tap. Cards respond by opening or moving; the flash only
       says "something was pressed", which is never in doubt. */
    -webkit-tap-highlight-color: transparent;
    transition: border-color .12s ease, box-shadow .12s ease;
}

/* The bars above and beside the cards, for the same reason as the card itself:
   a finger reaching for a column often lands on its header first, and a header
   whose text can be selected answers that by highlighting a beat name instead of
   scrolling. Nothing on this board is text anybody wants to select -- it is a
   surface for moving things, and the words on it are labels. */
.sb-beat-head,
.sb-unassigned-head,
.sb-act-bar {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
}

/* Only cards you can actually move advertise themselves as draggable.

   No touch-action here any more, deliberately. It used to be `none`, which is
   what lets Pointer Events see a drag rather than a scroll -- but it says that
   about the whole card, and cards fill the column, so on a tablet there was
   almost nowhere left to put a finger that would scroll. A touch gesture is
   claimed as it begins, so this could not be softened at drag time; it had to
   stop being said about the card at all.

   Dragging by mouse is untouched by the change: touch-action has no bearing on
   a mouse. What replaces it for fingers is .sb-card-grip below. */
.sb-card.is-draggable {
    cursor: grab;
}

/* A card nobody may drag can still be opened, so it should not look inert.

   A reader gets a pointer where an editor gets a grab hand, which is the
   honest pair: one of them moves the card and the other does not. The class
   is put on by the same branch that decides the drag, so the two cannot
   disagree. */
.sb-card.is-openable {
    cursor: pointer;
}

/* Only where hovering is a thing somebody can do.

   A touch screen has no hover, but it fakes one: iOS applies :hover on tap and
   leaves it applied until something else is tapped -- and since it is left on
   *each* card that was tapped, a few taps around the holding column left a trail
   of cards wearing the accent border, looking selected without being selected.
   Selection has its own mark (.sb-card.is-selected, a tinted background and a
   bar down the left edge) and the two were impossible to tell apart.

   Follows the @media (hover: none) blocks further down, which solve the same
   problem for the delete icons from the other direction. */
@media (hover: hover) {
    .sb-card.is-draggable:hover {
        border-color: var(--sb-accent);
        box-shadow: 0 1px 0 rgba(32, 30, 29, .18);
    }

    /* The same lift for a reader, so a card responds to the pointer that is
       already over it. Inside the same (hover: hover) block as the rule
       above, and for the same reason it is: iOS leaves :hover applied after
       a tap, so without the guard a few taps down a column would leave a
       trail of cards looking picked out. */
    .sb-card.is-openable:hover {
        border-color: var(--sb-accent);
        background: var(--sb-tint, #fbf8f1);
    }

    /* And the thing a tint cannot say: that it opens, and on two clicks.

       A tint alone reads as "this is under the pointer", which every
       hoverable thing says. Double-click is not a gesture anybody guesses
       at, so the card has to name it -- the whole reason a reader wants the
       card at all is the part of the synopsis it had to cut, and they have
       no way of knowing there is more to be had. */
    .sb-card.is-openable .sb-open-hint {
        opacity: 0;
        transition: opacity .12s ease;
    }

    .sb-card.is-openable:hover .sb-open-hint,
    .sb-card.is-openable:focus-within .sb-open-hint {
        opacity: 1;
    }
}

/* Where hovering is not a thing -- a touch screen -- the hint is simply
   always there. Two taps is even less guessable than two clicks. */
.sb-open-hint {
    align-self: flex-end;
    margin-top: 2px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .04em;
    color: var(--sb-muted, #6b6664);
    pointer-events: none;
}

.sb-open-hint i {
    margin-right: 4px;
    font-size: 10px;
}

/* The handle a finger drags by, and the only part of a card that refuses to
   scroll. Absent on a mouse: there the whole card drags, so a grip would be
   clutter advertising something already true everywhere. */
.sb-card-grip {
    display: none;
}

/* pointer:coarse is the primary pointer being a finger -- a tablet or a phone,
   not a desktop, and not a laptop whose touchscreen is the secondary input.
   That last case keeps the desktop behaviour it already had: the whole card
   drags by mouse, and a finger scrolls. */
@media (pointer: coarse) {

    .sb-card.is-draggable .sb-card-grip {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: none;
        /* Grown well past the glyph, and pulled back out again with negative
           margins, so the target a finger has to hit is bigger than the mark it
           aims at without changing the card's layout. */
        min-width: 34px;
        min-height: 34px;
        margin: -9px 0 -9px -8px;
        align-self: center;
        color: var(--sb-muted, #8d8781);
        font-size: 13px;
        cursor: grab;
        /* The point of all this: the one place a touch is ours rather than the
           scroller's. */
        touch-action: none;
    }

    .sb-card.is-draggable .sb-card-grip:active {
        color: var(--sb-accent);
        cursor: grabbing;
    }
}

.sb-card.is-dragging {
    opacity: .4;
    border-style: dashed;
    border-color: var(--sb-accent);
}

/* A just-saved card, flagged long enough to find after the board rebuilds. */
.sb-card.is-new {
    border-color: var(--sb-accent);
    animation: sb-new-card 1.8s ease-out;
}

@keyframes sb-new-card {

    0%,
    40% {
        background: var(--sb-accent-tint);
    }

    100% {
        background: var(--sb-surface);
    }
}

@media (prefers-reduced-motion: reduce) {
    .sb-card.is-new {
        animation: none;
        background: var(--sb-accent-tint);
    }
}

/* Selected cards, holding column only. A filled border rather than a tint, so a
   selection still reads at a glance against the .is-over drop highlight. */
.sb-card.is-selected {
    border-color: var(--sb-accent-dark);
    background: var(--sb-accent-tint);
    box-shadow: inset 3px 0 0 var(--sb-accent-dark);
}

/* Count on the drag ghost when a block is moving. The ghost's own rule is
   further down, beside the note about pointer-events -- this one said only
   `position: fixed`, which that rule repeats, so it was doing nothing. */
.sb-ghost-count {
    position: absolute;
    top: -9px;
    right: -9px;
    min-width: 22px;
    height: 22px;
    padding: 0 6px;
    border: 2px solid var(--sb-rule, #201e1d);
    background: #c9a84c;
    color: #201e1d;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
}

.sb-card-top {
    display: flex;
    align-items: baseline;
    gap: 8px;
}

/* Remove this card. Hidden until the card is hovered or focused, so a column of
   cards is not a column of trash cans -- but always present for keyboard and
   touch, where there is no hover to reveal it. */
.sb-del {
    flex: none;
    align-self: flex-start;
    margin-left: auto;
    padding: 0;
    border: 0;
    background: none;
    color: var(--sb-line);
    font-size: 12px;
    line-height: 1.2;
    cursor: pointer;
    opacity: 0;
    transition: opacity .12s ease, color .12s ease;
}

.sb-card:hover .sb-del,
.sb-card:focus-within .sb-del,
.sb-del:focus-visible {
    opacity: 1;
}

.sb-del:hover,
.sb-del:focus-visible {
    color: var(--sb-danger);
}

/* No hover on touch, so the icon has to stay put. */
@media (hover: none) {
    .sb-del {
        opacity: .55;
    }
}

/* Sized and weighted with the slugline it sits beside rather than shrunk away
   from it, and in the accent so it reads as the script's own numbering.

   No min-width: it was reserving 22px on every card to line the numbers up in a
   column, which only pays off when every card has one -- and most boards have
   none until the script is numbered. No letter-spacing either; that was
   compensating for 10px type. */
.sb-num {
    font-size: 14px;
    font-weight: 600;
    color: var(--sb-accent-dark);
}

/* No text-transform here on purpose. Headings are stored in caps by the save
   and import paths, so the card shows what is actually in the row -- a card
   reading in mixed case is a signal that something wrote around them. */
.sb-slug {
    font-size: 14px;
    font-weight: 600;
    line-height: 1.2;
}

/* No left indent. It used to line up under the slugline, past the scene number,
   which cost 30px on every card for an alignment nobody reads -- and hurt worst
   in the Unassigned rail, which is the narrowest column on the board. */
/* A synopsis is now a run of opening Action paragraphs rather than one, so the
   block is capped as well as spaced. Both matter: without the gap the
   paragraphs read as one undifferentiated wall, and without the cap a scene
   that opens on three long paragraphs makes a card taller than the beat holding
   it, which is the opposite of an index card.

   Six lines here, five in the narrower holding rail below. The full text is
   always a double-click away in the modal. */
.sb-synopsis {
    font-size: 13px;
    line-height: 1.2;
    color: #4a4644;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 6;
    line-clamp: 6;
    overflow: hidden;
}

/* A synopsis arrives as one <p> per paragraph. Browser defaults would put a
   full blank line between them -- as much height as a line of text -- so the
   margin is reset and a deliberate small gap used instead: enough to separate
   two paragraphs, not enough to cost a line. */
.sb-synopsis p {
    margin: 0;
}

.sb-synopsis p+p {
    margin-top: 5px;
}

/* The card shows the writer's formatting now, so these are reachable inside it.
   Only the tags a browser would lay out at a size of its own choosing are said
   here: the card is clamped to six lines, and a list indented at the usual 40px
   on a 200px card wraps every line it holds. The marks themselves -- bold,
   italic, underline, strikethrough, a colour -- are left alone, which is the
   whole point of showing them.

   Nothing here has to hold a font size or family off the card. The payload
   never carries either; see storyboardTree(). */
.sb-synopsis ul,
.sb-synopsis ol {
    margin: 0;
    padding-left: 18px;
}

/* The holding column is narrower and holds the most cards, so its synopses are
   a size down and capped at five lines. Cards there are for scanning and
   placing; the full text is a double-click away in the modal. */
/* One step down from a board card in the narrow holding rail: 14/13 rather
   than 15/14. */
.sb-unassigned .sb-slug {
    font-size: 13px;
    line-height: 1.1;
}

/* One line tighter than a board card. The clamping machinery itself now lives
   on .sb-synopsis, so this only says what differs -- the rail is the narrowest
   column on the board and holds the most cards.

   And the line-height is not among the differences any more. It was 1.1 here
   against 1.2 on a board card, which at 12px is a 13.2px line box -- shorter
   than the type wants, so overflow:hidden on the -webkit-box clipped the tails
   off j, g, p and y. Worst on a one-line synopsis, where the clipped line is
   also the only one.

   Deleted rather than restated as 1.2: that is what .sb-synopsis already says,
   and a copy of a value is a second place for it to drift. The five lines cost
   6px more than they did, which is the price of legible descenders. */
.sb-unassigned .sb-synopsis {
    -webkit-line-clamp: 5;
    line-clamp: 5;
    font-size: 12px;
}

/* Same step down as the synopsis above, for the same reason. */
.sb-unassigned .sb-chars {
    font-size: 11px;
    line-height: 1.25;
}

/* Who is in the scene. Three claims, three colours: the script says this person
   speaks, the script says they are present, the writer says they are present.
   Run together in one colour they would read as a single flat list and the
   distinction the scene report draws would be lost.

   Wrapping rather than clamped: a cast is a handful of short words and reads
   fine over two or three lines, unlike the synopsis it sits under. The comma
   separators are drawn in CSS so the names stay separate text nodes -- the card
   is built with textContent throughout and joining them into one string would
   put member-authored text back through innerHTML. */
.sb-chars {
    font-size: 12px;
    line-height: 1.35;
    letter-spacing: .02em;
}

.sb-chars span+span::before {
    content: ", ";
    color: var(--sb-muted);
}

/* Speaking is the strongest claim, so it gets the ink. */
.sb-char-speak {
    color: var(--sb-ink);
    font-weight: 600;
}

/* Present but silent -- named in the action, no dialogue. Muted, as the scene
   report prints it. */
.sb-char-silent {
    color: var(--sb-muted);
}

/* The writer's own, which the script does not say. Given the board's accent so
   it reads as an annotation rather than as something extracted. */
.sb-char-added {
    color: var(--sb-accent-dark);
    font-style: italic;
}

/* Neither density lists the cast. It is the longest thing on a card and the
   least scannable -- a row of names reads as texture rather than information
   when you are looking down a column for a scene. */
.sb-board.is-compact .sb-chars,
.sb-board.is-minimal .sb-chars {
    display: none;
}

/* ---- the cast section in the scene modal ------------------------------- */

/* Not inside .sb-board, so these cannot lean on the board's palette variables
   -- the modal is mounted at the document root. Same reason the list page has
   its own copy of them. */
/* One row for all three kinds -- speaking, silent, then the writer's own --
   wrapping as a single list. They were two containers, which put the added ones
   on a line of their own however few there were. */
/* No :empty rule to hide it when the script named nobody: the container is
   never truly empty -- PHP leaves whitespace in it -- and it does not need one,
   because whitespace generates no flex items and the row collapses to nothing
   on its own. */
.sb-chars-group .sb-cast-list {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

/* Not .sb-chip: that name is already taken further down by the structure tag on
   the board list, which is a 10px uppercase pill and nothing like this. */
.sb-cast-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 10px;
    border-radius: 13px;
    font-size: 14px;
    line-height: 1.4;
    border: 1px solid transparent;
    background: #f3f2f2;
    color: #201e1d;
}

/* The three claims, told apart by fill rather than by ink. A row of chips is
   read as shapes before it is read as words, so the difference has to be in the
   thing the eye lands on first -- three shades of text on one background reads
   as one group with some emphasis in it, which is the opposite of the point. */

/* Speaking: solid, the strongest claim. */
.sb-cast-chip.is-speaking {
    background: #201e1d;
    border-color: #201e1d;
    color: #fff;
    font-weight: 600;
}

/* Present but silent: the same neutral family, filled in rather than solid. */
.sb-cast-chip.is-silent {
    background: #e2dedb;
    border-color: #cfc9c5;
    color: #4a4644;
}

/* The leading icon: a head speaking, or an eye for someone only seen. Quieter
   than the name it marks -- it is there to be scanned past, not read. */
.sb-cast-chip>i {
    font-size: 12px;
    opacity: .75;
}

/* The writer's own, in the board's accent, so it reads as an annotation rather
   than as something the script said. */
.sb-cast-chip.is-added {
    background: #f7f0dc;
    border-color: #c9a84c;
    color: #8a6f21;
}

/* Only the writer's own carry a remove control; the script's are read-only.

   One class is enough now. This used to be three deep to outrank a blanket
   `.modal360-content .dialogReset button` rule that padded every button in a
   dialog like a form field; that rule is gone, replaced by the opt-in
   `.dialog-field` in css/modal.css. */
.sb-cast-chip-x {
    border: 0;
    background: none;
    padding: 0;
    margin: 0 -3px 0 0;
    cursor: pointer;
    color: inherit;
    opacity: .6;
    font-size: 13px;
    line-height: 1;
}

.sb-cast-chip-x:hover {
    opacity: 1;
}

/* Sized to the names it holds rather than to the dialog. A cast list is a few
   short words, and a full-width select over one of them reads as a form field
   for something much bigger than it is. */
.sb-cast-pick {
    margin-top: 10px;
}

/* Three parts to the selector, not two. `.sb-cast-pick select` is (0,1,1), which
   only ties `.modal360-content select` in css/modal.css -- and modal.css is
   loaded after storyboard.css, so the tie went to it and neither the padding nor
   the size below applied. (0,2,1) settles it. */
.sb-chars-group .sb-cast-pick select {
    width: auto;
    min-width: 180px;
    max-width: 100%;
    /* .form-control sizes for a text field the width of the dialog; this holds
       one short name. */
    padding: .3rem;
    font-size: 15px;
}

/* Sits between the label and the chips, so it is read before the row it
   describes rather than after the control at the bottom. */
.sb-cast-note {
    margin: 2px 0 8px;
    font-size: 12px;
    line-height: 1.35;
    color: #6b6664;
}

/* Page and length, from the last sync. Quiet -- it is reference, not content. */
.sb-card-meta {
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: 11px;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: #8a8482;
}

/* margin-left rather than justify-content: space-between, so the length still
   sits hard right on a card that has a length but no page number. */
.sb-meta-len {
    margin-left: auto;
}

/* The page and length line stays at every density.
 *
 * It used to be hidden with the synopsis and the cast, on the grounds that a
 * dense card should carry as little as possible. But it is the one line that
 * only exists when the card is tied to a screenplay -- renderCard() draws it
 * only when there is a page or a length to draw -- so hiding it took away the
 * thing that says which cards the script accounts for, exactly when the board
 * is dense enough that you cannot tell by looking. It is also two short numbers
 * on one line, which is what the density is for. */

.sb-marker {
    height: 3px;
    margin-bottom: 2px;
    background: transparent;
}

.sb-marker.is-active {
    background: var(--sb-accent);
}

/* The card that follows the pointer. pointer-events:none matters -- without it
   the clone sits under the cursor and elementFromPoint only ever finds itself,
   so no drop target is ever resolved. */
.sb-ghost {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 4000;
    pointer-events: none;
    opacity: .95;
    border-color: #c9a84c;
    box-shadow: 0 6px 18px rgba(32, 30, 29, .28);
    background: #fff;
}

body.sb-dragging {
    cursor: grabbing;
}

body.sb-dragging .sb-card.is-draggable {
    cursor: grabbing;
}

/* ---------------------------------------------------------------------------
 * Unassigned -- the holding group
 *
 * Scenes that exist but have not been placed. Narrower than an act column,
 * because it is a staging area rather than part of the story's shape.
 * ------------------------------------------------------------------------- */

.sb-unassigned {
    flex: 0 0 230px;
    display: flex;
    flex-direction: column;
    min-height: 0;
    min-width: 0;
    border-right: 2px solid var(--sb-rule);
    background: #ecebea;
}

.sb-unassigned-head {
    flex: none;
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 12px 12px 10px;
    border-bottom: 2px solid var(--sb-rule);
}

.sb-unassigned-name {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .12em;
    text-transform: uppercase;
}

.sb-unassigned-count {
    margin-left: auto;
    font-size: 11px;
    color: var(--sb-muted);
}

.sb-unassigned-drop {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    align-content: flex-start;
}

/* The collapse toggle. Louder than the plus beside it -- this one has to be
   findable when the rail is a 34px strip and nothing else is legible. */
.sb-collapse {
    flex: none;
    align-self: center;
    padding: 0;
    border: 0;
    background: none;
    color: var(--sb-muted);
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    transition: color .12s ease;
}

.sb-collapse:hover,
.sb-collapse:focus-visible {
    color: var(--sb-accent-dark);
}

/* ---------------------------------------------------------------------------
 * Collapsed holding column
 *
 * A narrow strip, not display:none. That is what keeps it a drop target: the
 * drag hit-tests with elementFromPoint and closest('.sb-drop'), so a rail that
 * is merely thin still accepts a card dragged out of the board. Hidden
 * outright, there would be nowhere to drop and the rail would have to expand on
 * hover mid-drag to compensate.
 *
 * The cards go; the .sb-drop stays, keeps its full height, and still tints on
 * hover and shows the insert marker.
 * ------------------------------------------------------------------------- */

.sb-unassigned.is-collapsed {
    flex: 0 0 34px;
    min-width: 34px;
}

.sb-unassigned.is-collapsed .sb-unassigned-head {
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 10px 2px;
    border-bottom: 0;
}

/* Turned on their side so the label and count read up the strip. */
.sb-unassigned.is-collapsed .sb-unassigned-name,
.sb-unassigned.is-collapsed .sb-unassigned-count {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    margin-left: 0;
    letter-spacing: .14em;
}

.sb-unassigned.is-collapsed .sb-unassigned-count {
    font-weight: 700;
    color: var(--sb-ink);
    letter-spacing: 0;
}

.sb-unassigned.is-collapsed .sb-unassigned-drop {
    padding: 4px 2px;
}

.sb-unassigned.is-collapsed .sb-slot,
.sb-unassigned.is-collapsed .sb-empty {
    display: none;
}

/* ---------------------------------------------------------------------------
 * Layouts (design handoff options 1a / 1b / 1d)
 *
 * All three render the same DOM -- acts > beats > scenes is invariant, which is
 * what the design deck meant by "four directions, same data model underneath".
 * So these are flex-direction changes, not repositioning: no absolute
 * positioning, no transforms, no coordinate maths.
 *
 * Drag is unaffected. targetAt() in storyboard.js hit-tests with
 * elementFromPoint + closest('.sb-drop'), so it reads whatever the browser has
 * laid out and never assumes an axis.
 *
 * 1a "columns" is the default above and needs no class.
 * ------------------------------------------------------------------------- */

/* 1b -- acts as swimlanes. Acts stack down the page, each a full-width lane
   with its title on a rail at the left and its beats running left to right.
   Reads like a timeline; suits teaser/act/tag television shapes. */
.sb-board.is-lanes .sb-cols {
    flex-direction: column;
    overflow-x: hidden;
    overflow-y: auto;
}

.sb-board.is-lanes .sb-col {
    flex: none;
    flex-direction: row;
    min-width: 0;
    max-width: none;
    border-right: 0;
    border-bottom: 2px solid var(--sb-rule);
}

/* The act header becomes the rail. It is a flex column already, so it only
   needs a width and to stop stretching. */
.sb-board.is-lanes .sb-act {
    flex: 0 0 190px;
    justify-content: flex-start;
    padding: 14px 14px 16px;
}

.sb-board.is-lanes .sb-act-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
}

.sb-board.is-lanes .sb-act-meta {
    margin-left: 0;
}

.sb-board.is-lanes .sb-scroll {
    flex-direction: row;
    overflow-x: auto;
    overflow-y: hidden;
    align-items: flex-start;
}

/* Shares and then scrolls, the same way .sb-col does -- 230 is what a beat
   would like, 190 is where it stops being readable. Lanes put every beat of an
   act side by side rather than three acts, so there are more of them competing
   and they reach the floor sooner; that is the layout's nature, not a fault. */
.sb-board.is-lanes .sb-beat {
    flex: 1 1 230px;
    min-width: 190px;
    align-self: stretch;
}

/* The beat header used to be told to stack here and nowhere else, on the
   grounds that swimlane beats were the narrow ones. .sb-beat-head wraps
   everywhere now and decides for itself, so there is nothing left to say here --
   this layout gets the second line by being narrow, which is the reason it ever
   had one. */

/* Nothing here either. The name's min-width and the meta's order do the same
   work for every layout, and flex-basis:100% -- which forced the meta onto its
   own line whether or not it needed one -- was the thing making a wide swimlane
   spend a line it could have kept. */

/* No width override for a folded beat here, deliberately.
 *
 * It had one, and every candidate number was wrong. Narrow enough to be worth
 * having (170px) ellipsed the beat name to a few characters and pushed the plus
 * onto its own line; wide enough to stay readable (225px) differed from the
 * 230px above by five pixels, so folding twitched the column for no reason.
 *
 * A folded beat is therefore exactly as wide as an open one, and 230px is
 * stated once, in .sb-board.is-lanes .sb-beat above. What folding gives back in
 * this layout is height: a lane is as tall as its tallest beat, so folding one
 * changes little and folding all of them collapses the act to a band -- which
 * is the gesture that matters here anyway, putting an act you are not working
 * on out of the way. */

/* Unassigned needs no override in swimlanes.
 *
 * It was a capped grid band across the top, on the reasoning that a vertical
 * rail fights an axis already running downward. In practice the holding column
 * is the one thing that should not move between layouts: it is where you go to
 * find an unplaced scene, and hunting for it in a different place on each board
 * costs more than the tidier axis buys. .sb-body stays a row, so the rail sits
 * to the left of the stacked lanes exactly as it does in the other two.
 *
 * Nothing in the drag path cares. targetAt() hit-tests with elementFromPoint
 * and closest('.sb-drop'), so it reads whatever the browser laid out and never
 * assumes a direction.
 */

/* 1d -- continuous script order. One vertical document: acts are black bands,
   beats are ruled bands under them, and cards wrap left to right in reading
   order. Structure becomes annotation rather than architecture. */
.sb-board.is-flow .sb-cols {
    flex-direction: column;
    overflow-x: hidden;
    overflow-y: auto;
}

.sb-board.is-flow .sb-col {
    flex: none;
    flex-direction: column;
    min-width: 0;
    max-width: none;
    border-right: 0;
}

.sb-board.is-flow .sb-scroll {
    overflow: visible;
    padding: 0;
    gap: 0;
}

/* Beats stop being boxes and become ruled bands. */
.sb-board.is-flow .sb-beat {
    border-width: 0 0 1px 0;
    border-bottom-style: dashed;
    border-bottom-color: var(--sb-line);
}

.sb-board.is-flow .sb-beat-head {
    border-bottom: 0;
    padding: 10px 14px 9px;
}

/* Bigger here, because the beat is not a column any more.
 *
 * Elsewhere a beat header is 200-400px wide and its type is sized to survive
 * that -- small enough that a name still fits when the column is at its floor.
 * In script order the same header runs the whole width of the board, and type
 * chosen to fit a narrow column reads as a caption on a headline's worth of
 * space. Nothing is competing for the room, so the name can have the size it
 * would have been given if this were the only layout. */
.sb-board.is-flow .sb-beat-name {
    font-size: 16px;
}

.sb-board.is-flow .sb-beat-meta {
    font-size: 14px;
}

/* Scoped to .sb-cols throughout: the Unassigned rail sits beside this, stacks
   its cards vertically, and must keep the default treatment. */
.sb-board.is-flow .sb-cols .sb-drop {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
    padding: 8px 14px 14px;
}

/* Out-specifies the wrapping-row padding above, which would otherwise keep a
   folded band taller than it needs to be. The dashed box inside is already
   flex: 1 0 100% by the rule below, so it spans the band rather than sitting
   in it like a card. */
.sb-board.is-flow .sb-cols .sb-beat.is-collapsed .sb-drop {
    padding: 6px 14px 8px;
}

.sb-board.is-flow .sb-cols .sb-slot {
    flex: 0 0 220px;
    flex-direction: row;
    align-items: stretch;
}

.sb-board.is-flow .sb-cols .sb-slot .sb-card {
    flex: 1;
}

.sb-board.is-flow .sb-cols .sb-empty {
    flex: 1 0 100%;
}

/* The insert marker is a 3px horizontal rule in the stacked layouts. In a
   wrapping row it has to turn 90 degrees, or it points along the wrong axis and
   the drop position reads as ambiguous. */
.sb-board.is-flow .sb-cols .sb-marker {
    width: 3px;
    height: auto;
    align-self: stretch;
    margin-bottom: 0;
    margin-right: 2px;
}

/* compact density */
/* Two densities below Full, and nearly all of it is shared.
 *
 * Compact keeps two lines of synopsis; Minimal keeps none. Everything else --
 * the tighter card, the tighter column, the slightly larger slugline -- is the
 * same in both, so it is written once. What separates them is how much of the
 * scene you can still read, not how the cards are spaced. */
.sb-board.is-compact .sb-card,
.sb-board.is-minimal .sb-card {
    padding: 7px 10px;
    gap: 0;
}

/* 13px, not 12. Cabin Condensed runs small, and 12 was hard to read at a glance -- which
   defeats the point, where you are scanning a lot of cards. */
.sb-board.is-compact .sb-slug,
.sb-board.is-minimal .sb-slug {
    font-size: 13px;
}

/* Two lines: enough to say what the scene is, not enough to read it. Six is
   Full's clamp, and the difference between them is the whole point of this
   setting. */
.sb-board.is-compact .sb-synopsis {
    -webkit-line-clamp: 2;
    line-clamp: 2;
    margin-top: 3px;
}

.sb-board.is-minimal .sb-synopsis {
    display: none;
}

.sb-board.is-compact .sb-drop,
.sb-board.is-minimal .sb-drop {
    padding: 8px;
    gap: 5px;
}

/* footer */
.sb-foot {
    flex: none;
    display: flex;
    align-items: center;
    gap: 18px;
    padding: 10px 24px;
    border-top: 2px solid var(--sb-rule);
    background: var(--sb-surface);
    font-size: 12px;
    color: var(--sb-muted);
}

.sb-foot .sb-status {
    margin-left: auto;
}

.sb-foot .sb-status.is-error {
    color: var(--sb-danger);
    font-weight: 600;
}

/* ---------------------------------------------------------------------------
 * Scene modal (popup/storyboardScene.php inside #modal360)
 *
 * The width is set here on the dialog rather than on .modal360-content for two
 * reasons: resizeModal() writes an inline `width: fit-content`, which would win
 * over any width in modal.css, and .modal360-content is shared by every modal
 * on the site, so widening it would widen Contact Us and the rest too. Sizing
 * the dialog instead means fit-content resolves to 800px, and the existing
 * `max-width: 90%` on .modal360-content (css/modal.css) still caps it on
 * narrow screens.
 * ------------------------------------------------------------------------- */

.sb-scene-dialog {
    width: 800px;
    max-width: 100%;
}

/* Narrower than the scene dialog: one text field, and 800px of it would leave
   the input looking like a mistake. Same reasoning about resizeModal()'s inline
   fit-content applies -- the width goes on the dialog, not on .modal360-content,
   which every modal on the site shares. */
.sb-board-dialog {
    width: 460px;
    max-width: 100%;
}

/* The stacked label, the stretched field and the space beneath the pair are
   `.dialogReset .form-group` in css/modal.css now -- one definition for every
   dialog, instead of the identical copy these two both carried. */
.sb-board-dialog .form-group input {
    width: 100%;
}

/* The heading's three parts on one line. The location takes what is left after
   the two qualifiers, which are sized to their content -- "INT./EXT." and
   "MOMENTS LATER" are the widest realistic values. */
.sb-scene-dialog .sb-slug-row {
    display: flex;
    align-items: stretch;
    gap: 8px;
    width: 100%;
}

.sb-scene-dialog .sb-slug-field {
    display: flex;
}

.sb-scene-dialog .sb-slug-field input {
    width: 100%;
}

.sb-scene-dialog .sb-slug-intext {
    flex: 0 0 110px;
}

.sb-scene-dialog .sb-slug-location {
    flex: 1 1 auto;
    min-width: 0;
}

.sb-scene-dialog .sb-slug-daynight {
    flex: 0 0 150px;
}

/* Typing shows caps immediately. The stored value is uppercased server-side in
   saveStoryboardScene.php, so this is feedback rather than the mechanism --
   what you see really is what is in the row. */
.sb-scene-dialog .sb-slug-row input {
    text-transform: uppercase;
    font-weight: 600;
    letter-spacing: .01em;
}

.sb-scene-dialog .sb-slug-row input::placeholder {
    text-transform: none;
    font-weight: 400;
    opacity: .55;
}

/* A heading the screenplay owns. Muted and not-allowed rather than hidden or
   removed -- the writer still needs to read it, and often to copy it, on the way
   to changing it in the script. */
.sb-scene-dialog .sb-slug-row.is-locked input {
    background: #f3f2f2;
    color: #6b6664;
    cursor: not-allowed;
}

.sb-scene-dialog .sb-locked-note {
    display: flex;
    align-items: baseline;
    gap: 7px;
    margin: 8px 0 0;
    font-size: 12px;
    line-height: 1.35;
    color: #6b6664;
}

.sb-scene-dialog .sb-locked-note i {
    flex: none;
    font-size: 11px;
    color: #ad8f39;
}

@media (max-width: 560px) {

    /* Three fields cannot share a line on a phone; the qualifiers pair up and
       the location takes its own row. */
    .sb-scene-dialog .sb-slug-row {
        flex-wrap: wrap;
    }

    .sb-scene-dialog .sb-slug-location {
        flex: 1 1 100%;
        order: -1;
    }

    /* ---- the board on a phone ----

       Everything below is the same board at a smaller scale rather than a
       different one: the columns keep their proportions, the cards keep their
       shape, and only the numbers come down. A phone showing one and a bit
       columns is still a board you can drag across; a phone showing one column
       and a sliver is a list with a scrollbar. */

    /* The three pickers share the line instead of each holding a fixed width.
       Their captions sit above them, so the widths were what pushed the last one
       onto a second row -- 190 + 130 + 120 plus gaps never fit 390px of iPhone.
       Sharing what there is puts them on one line at any width.

       Only the padding is narrowed here now. The gap used to be tightened as
       well, back when the header's own was 16px; at 8px there is nothing left
       for a phone to reclaim, and saying it again would only be a second number
       to keep in step with the first. */
    .sb-head {
        padding: 8px 12px;
    }

    /* A floor rather than flex-basis:0, because the header also carries a
       "Create script" button and the back link. With a basis of zero the fields
       would surrender their width to those and the three selects would be
       crushed to a few characters each; with a floor they hold their line and
       the buttons wrap below instead -- which is the right way round, since the
       pickers are what you reach for while working and the buttons are what you
       reach for once.

       92 x 3 plus two 10px gaps is 296px, inside the 374px an iPhone SE leaves
       after the header's own padding, so all three fit on the narrowest phone
       worth supporting. */
    .sb-head .sb-field {
        flex: 1 1 92px;
        min-width: 92px;
    }

    .sb-head .sb-select,
    .sb-head .sb-select-layout,
    .sb-head .sb-select-density {
        width: 100%;
        font-size: 12px;
        padding: 4px 6px;
    }

    /* No column overrides here.
       .sb-col and the lanes .sb-beat already shrink toward their floors and then
       let the row scroll, which is the same thing a phone needs -- just reached
       sooner. Repeating them here would only be a chance for the two to drift,
       and the first draft of this block did drift: it set flex-shrink to 0 and
       would have switched the sharing off on exactly the screens that need it
       most. */

    /* The holding rail is a staging area, not somewhere to read: it can lose
       more width than the board it feeds, and it collapses to 34px when it is
       in the way. */
    .sb-unassigned {
        flex: 0 0 165px;
    }

    /* A notch, not a step. The slugline stays the loudest thing on the card and
       the synopsis stays quieter than it -- taking a point off each keeps that
       relationship while buying back a word or two per line at this width. */
    .sb-slug {
        font-size: 13px;
    }

    .sb-synopsis {
        font-size: 12px;
    }
}

/* board picker */
.sb-list {
    display: flex;
    flex-direction: column;
    width: 100%;
    /* min-height:0 so the scrolling body below can actually shrink -- a flex
       item defaults to min-height:auto and refuses to go under its content,
       which would push the page instead of scrolling inside the list. */
    min-height: 0;
    background: var(--sb-bg);
    color: var(--sb-ink);
    font-family: 'Cabin Condensed', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

/* Scrolling lives here rather than on .sb-cards, so a section heading scrolls
   away with its own boards. With it on each grid, every section scrolled inside
   its own box and the headings stayed pinned to nothing. */
.sb-list-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 16px 24px 24px;
}

.sb-list-section+.sb-list-section {
    margin-top: 26px;
}

/* The dashboard's block-head carries its own bottom margin; this only adds what
   a count on the right of it needs. */
.sb-section-count {
    flex: none;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--sb-muted, #6b6664);
    white-space: nowrap;
}

.sb-list *,
.sb-list *::before,
.sb-list *::after {
    box-sizing: border-box;
}

.sb-list .sb-head {
    border-bottom: 2px solid #201e1d;
}

.sb-empty-state {
    padding: 30px 20px;
    font-size: 14px;
    color: #6b6664;
}

.sb-empty-state p {
    margin: 0 0 8px;
    font-size: 18px;
}

/* Padding and scrolling moved to .sb-list-body -- the grid is now one section's
   worth of cards, not the whole page. */
.sb-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 14px;
}

/* A div now, not an anchor: the card holds a link of its own (the script name),
   and nesting anchors is invalid. cursor:pointer is what it loses by not being
   one, since the whole card is still clickable. */
.sb-boardcard {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 14px 16px;
    border: 1px solid #201e1d;
    border-radius: 5px;
    background: #fff;
    text-decoration: none;
    color: inherit;
    cursor: pointer;
    transition: border-color .12s ease, box-shadow .12s ease;
}

.sb-boardcard:hover {
    border-color: #c9a84c;
    box-shadow: 0 1px 0 rgba(32, 30, 29, .18);
}

/* The card's real link. Undecorated, because the whole card behaves as one
   target anyway -- this exists so the board is reachable by keyboard,
   middle-click and copy-link, not to look like a link inside a link. */
.sb-boardcard-title {
    font-size: 15px;
    font-weight: 700;
    color: inherit;
    text-decoration: none;
}

.sb-boardcard-meta {
    font-size: 12px;
    color: #6b6664;
}

/* This one does look like a link, because it goes somewhere else entirely --
   the script's own page, not the board. Without the distinction it would read
   as part of the card's subtitle and nobody would click it. */
.sb-boardcard-script {
    color: var(--sb-accent-dark, #ad8f39);
    font-weight: 600;
    text-decoration: none;
}

.sb-boardcard-script:hover,
.sb-boardcard-script:focus-visible {
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* Whose script this is. Quieter than the meta line above it -- it is
   attribution, not a property of the board -- and it sits directly under the
   script name it refers to. */
.sb-boardcard-shared {
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-size: 11px;
    color: var(--sb-muted);
}

.sb-boardcard-shared i {
    flex: none;
    font-size: 10px;
    color: var(--sb-line);
}

/* Structure and, when it applies, read-only. margin-top:auto pins them to the
   bottom so cards of differing title lengths still line their tags up. */
.sb-boardcard-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-top: auto;
    padding-top: 2px;
}

/* Pushed to the far end of the tag row, and hidden until the card is
   approached -- a grid of boards should not read as a grid of trash cans. Kept
   present for keyboard and touch, where there is no hover to reveal it. Same
   bargain as .sb-del on a scene card. */
.sb-boardcard-del {
    flex: none;
    margin-left: auto;
    padding: 0;
    border: 0;
    background: none;
    color: var(--sb-line);
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    opacity: 0;
    transition: opacity .12s ease, color .12s ease;
}

.sb-boardcard:hover .sb-boardcard-del,
.sb-boardcard:focus-within .sb-boardcard-del,
.sb-boardcard-del:focus-visible {
    opacity: 1;
}

.sb-boardcard-del:hover,
.sb-boardcard-del:focus-visible {
    color: var(--sb-danger);
}

@media (hover: none) {
    .sb-boardcard-del {
        opacity: .55;
    }
}

/* The board's framework. Tinted rather than outlined -- it is a fact about the
   board, not a warning, and .sb-readonly beside it is the one that should read
   as a caveat. */
.sb-chip {
    flex: none;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--sb-accent-dark, #ad8f39);
    background: var(--sb-accent-tint, #f7f0dc);
    border-radius: 3px;
    padding: 2px 7px;
    line-height: 1.4;
    white-space: nowrap;
}

/* .sb-readonly is shared with the board header. It reads --sb-muted and
   --sb-line, which is why the palette above is declared on .sb-list as well as
   .sb-board. */

@media (max-width: 700px) {

    /* No height override here any more. It hardcoded 150px to compensate for
       the old measured constant; --header-height now governs at every width.
       No .sb-col override either: it narrowed the column from 380 to 300, and
       the base is 285 now -- so it had become a rule that made columns *wider*
       on the smallest screens. Nothing this narrow fits two columns beside the
       sidebar and the rail at any of these widths, so the width is the only
       thing it was deciding. */

    .sb-head {
        padding: 12px 14px;
    }

    .sb-foot {
        padding: 10px 14px;
    }
}
/* ------------------------------------------------ a scene card, read only

   What somebody invited to read a board gets when they open a card.

   It exists because .sb-synopsis clamps to six lines on a board card and five
   in the holding rail. A reader can see the text is cut and, until this, had
   nowhere to go with that. So this is not the form with its fields disabled --
   a greyed-out textarea is a worse way to read a paragraph than the card was.
   See the !$canOpen branch in popup/storyboardScene.php.

   Nothing here clamps. The whole point is the part the card could not show. */

.sb-scene-dialog--view {
    width: min(560px, 92vw);
}

/* The slugline, in the shape a script writes it. */
.sb-view-slug {
    margin: 0 0 10px;
    font-size: 15px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--sb-ink, #201e1d);
}

.sb-view-synopsis {
    margin: 0;
    font-size: 15px;
    line-height: 1.5;
    color: var(--sb-ink, #201e1d);
}

/* One paragraph per block, spaced to be read. The card packs its lines tight
   because it is an index card; this is the panel somebody opened BECAUSE the
   card had to cut the text short. */
.sb-view-synopsis p {
    margin: 0 0 10px;
}

.sb-view-synopsis p:last-child {
    margin-bottom: 0;
}

.sb-view-head {
    margin: 18px 0 6px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--sb-muted, #6b6664);
}

/* The cast as chips rather than a bulleted column: they are a set of names,
   and a list marker down the side implies an order they do not have. */
.sb-view-cast {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.sb-view-cast li {
    padding: 2px 10px;
    border: 1px solid var(--border);
    border-radius: 11px;
    font-size: 13px;
    font-weight: 600;
    color: var(--sb-ink, #201e1d);
}

/* The AI's offer to lay the board out ---------------------------------------

   An icon button on the holding rail's header, beside Add a scene. Coloured
   rather than grey because it is the only control on this board that spends
   money, and a wand that looks like every other icon is one somebody presses
   to find out what it does. */
.sb-ai-place {
    color: var(--sb-accent);
}

.sb-unassigned-head:hover .sb-ai-place,
.sb-ai-place:hover,
.sb-ai-place:focus-visible {
    color: var(--sb-accent-dark);
}
