/* Blood on the Clocktower information tracker.
 *
 * Two themes off one token set: a candlelit Grimoire at night and aged paper
 * by day, chosen in Setup or left to the device. Every colour is defined once,
 * on :root, and only overridden in the day block -- no rule below the palette
 * may name a literal colour, or the two themes drift apart.
 *
 * Which one is in force reaches this file as data-theme on the root, written
 * by theme.js before the first paint and rewritten by app.js when the choice
 * changes. It is an attribute rather than the prefers-color-scheme query this
 * used to be, because the day palette has to apply in two cases at once -- the
 * device asking for it, and Setup pinning it -- and no single CSS rule can be
 * selected by a media condition and an attribute both. Resolving it in script
 * and stamping the answer is what keeps the day palette a single block instead
 * of two copies drifting apart.
 *
 * The Content-Security-Policy in vercel.json is style-src 'self' with no
 * 'unsafe-inline', so this file is the only place styling may live: no <style>
 * blocks and no style="" attributes in the HTML. Custom properties set from JS
 * through el.style.setProperty() are unaffected -- CSP governs the parsing of
 * style attributes, not writes through the CSSOM. */

:root {
    /* Tells the UA to render number spinners, checkboxes and scrollbars in the
       matching scheme. Without it a dark dialog gets light native controls. */
    color-scheme: light dark;

    /* ------------------------------------------------ night: the Grimoire */

    /* The room, by candlelight. Warm-shifted rather than neutral grey, so the
       brass and parchment below read as materials and not as tinted UI. */
    --bg: #13101a;
    --panel: #1e1826;
    --panel-hi: #2a2233;
    --line: #453c4f;
    --text: #f2ece1;
    --muted: #a89db2;

    /* Covers the busiest part of the circle, so it has to be near-opaque. */
    --scrim: rgba(19, 16, 26, 0.94);

    /* The table itself. The felt runs to the seat ring, the apron past it
       under the name plates, and the wood closes the edge. */
    --felt: #1c1629;
    --felt-hi: #241c33;
    --felt-edge: #150f20;
    --wood: #3b2a20;
    --wood-hi: #5a4230;

    /* The objects on it. Parchment is the token face, bone the vote chip, tan
       the reminder scrap -- three materials that stay apart at 12px. */
    --parchment: #e9ddc2;
    --parchment-dim: #c8b795;
    --brass: #c9a227;
    --brass-dim: #7f6621;
    --ink-on-brass: #1a1305;

    /* The Community Created Content badge in the foot of the window. An image
       among the colours, and here rather than beside the rule that paints it
       for the reason the rest of this block exists: it comes in two variants,
       one per theme, and every other thing that does is settled here. Kept a
       custom property rather than a second [data-theme] rule further down so
       the pair cannot drift apart from the palette they were cut to match.

       Only the variant that wins is ever fetched -- a value a custom property
       never resolves into is never a request -- so a night load never pulls
       the parchment down. Both files are trimmed to the same 256x102 box, so
       the element's aspect-ratio is one number whichever is in force. */
    --ccc-badge: url("resources/community/ccc-sleeve.png");

    /* Text on a control that has been filled with a tracker tint, an alignment
       or the shroud. It has to flip with the theme: the tints are light on a
       dark ground and dark on a light one, so a fixed near-black reads at 2:1
       against the day palette's own accents. */
    --on-tint: #17130a;

    /* The shroud banner is near-black in both themes, so unlike --on-tint this
       one does not flip: it is light either way. */
    --on-shroud: #ece8f2;

    /* Warm vs cool with a luminance gap, so the two interaction types stay
       distinguishable under the common red-green colour vision deficiencies. */
    --nom: #e8a33d;
    --brd: #57cfe0;

    /* Separated from the other two by hue and by lightness, so the three stay
       apart under the common red-green colour vision deficiencies -- where
       warm and cool already read as yellow and blue, this reads as neither.
       It names the vote *arrow* only; a spent dead vote is the medallion on
       the shroud banner. */
    --vote: #e585b4;

    /* Assigning characters is not something you record, so it takes its own
       hue rather than borrowing from any tracker. */
    --role: #c194dd;

    /* The ground the top bar takes while privacy is on. A veil drawn over the
       one strip of the app that is always on screen, rather than a wash over
       the table -- privacy is being made legible precisely because you could
       not tell a change had landed, and dimming the circle would argue with
       the fix.

       Night can afford the hue the eye button wears, because this palette is
       already violet-shifted to its roots and the bar reads as related to the
       control that tinted it rather than as a colour from somewhere else. The
       day block below cannot, and says why.

       --veil-ink is the muted ink for the small-caps label standing on it. A
       veil that moves the ground has to move what is written on it too. */
    --veil: #221a2e;
    --veil-ink: #a89db2;

    /* The shroud banner: dark cloth, its sheen, the damask woven into it and
       the braid along its edge. Near-identical in both themes on purpose -- it
       is an object lying on a parchment token, and a real one does not change
       colour with the light in the room. */
    --shroud: #221f2a;
    --shroud-sheen: #322e3d;
    --shroud-motif: #4a4459;
    --shroud-braid: #8f86a0;

    /* The centre stripe of an executed player's banner, and the same red
       wherever the interface has to name an execution. */
    --shroud-blood: #b3181f;
    --executed: #e05656;

    /* A reminder token tucked against the seat. */
    --reminder: #d9c398;
    --reminder-hi: #f0e0bc;
    --reminder-line: #8a7448;
    --reminder-ink: #6b5730;

    /* Taken from the two art variants, so a ring and the token it rings agree.

       The red is lifted a little off the art's own, which sat at a luminance
       where neither ink could reach it: the alignment button fills with this
       and takes --on-tint, and at #d0433f that was 4.02:1 while the light ink
       the day palette would have used came to 4.24:1. A colour that fails both
       of its inks has to move rather than have its ink swapped, and up rather
       than down -- the same button's ring is a 3:1 graphic against a near-black
       token, which only gets harder as the red darkens. Good needed none of
       this; it was already bright enough for a dark ink at 6.8:1. */
    --good: #5aa3dd;
    --evil: #dc5954;

    /* The name plate: a card resting on the felt. Opaque on purpose -- the
       halo it replaces punched soft holes in every arrow passing behind it. */
    --plate-bg: rgba(28, 22, 41, 0.92);
    --plate-line: #5c4b33;
    --plate-text: #ece3d2;

    /* Laid under every chord so one crossing another still reads as two lines.
       It is the felt colour: the casing is the table showing through. */
    --arrow-casing: #1c1629;

    --shadow-token: 0 2px 7px rgba(0, 0, 0, 0.6);
    --shadow-plate: 0 2px 6px rgba(0, 0, 0, 0.5);
    --shadow-table: 0 26px 60px -22px rgba(0, 0, 0, 0.85);

    /* Set by JS from the selected tracker, or the assign colour when none is. */
    --accent: var(--role);

    /* Set by JS: how many seats are around the circle. */
    --seat-count: 12;

    /* ------------------------------------------------------------- type */

    /* System faces only: the CSP declares no font-src, so it falls back to
       default-src 'self' and no remote font can load. */
    --font-display: ui-serif, Georgia, "Iowan Old Style", "Palatino Linotype",
        "Book Antiqua", "Times New Roman", serif;
    --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;

    /* --------------------------------------------------------- geometry */

    /* The circle is square, so one size drives both axes. dvh rather than vh:
       a mobile browser's collapsing toolbar makes vh overshoot the space the
       page actually has. Sized to leave room outside for the name plates.

       The colophon comes off the height term, and off that term only. On the
       phone this app is mostly used on it is the width that binds -- 72vw is
       the smaller of the two at any window taller than about 1.39 times its
       width -- so this subtraction costs a portrait phone nothing at all. It
       engages on the short, wide window where the height term wins instead,
       which is the one shape where the sum actually ran over: the table, the
       day controls and the footer together left the entry list nothing, and
       the day controls do not shrink, so what got pushed under the footer was
       the list itself. */
    --circle-size: min(72vw, calc(52dvh - var(--footer-h)));

    /* Sized so tokens never collide: available arc is ~2.32 x size / count, so
       1.95 fills roughly five sixths of it and leaves a gap.

       Every bound here is relative to the circle, because everything drawn in
       the SVG -- the ring, the arrows, their heads -- scales with the viewBox.
       A fixed pixel ceiling froze the tokens while the circle kept growing,
       stranding the character art as specks on a wide window. The upper bound
       exists only for low seat counts, where five sixths of the arc would be a
       token wider than the table is empty. The lower bound is a legibility
       floor, not a layout one: pushed any higher it wins over the arc term on
       a landscape phone and the tokens overlap. */
    --token: clamp(26px,
            calc(1.95 * var(--circle-size) / var(--seat-count)),
            calc(var(--circle-size) * 0.17));

    /* How far the felt reaches past the token ring, carrying the name plates.
       Painted with box-shadow spread, which is ink rather than layout, so it
       can exceed the viewport without ever creating a scrollbar. */
    --apron: calc(var(--token) * 0.66);

    /* What the colophon at the foot of the window costs, as a number the
       landscape rule can take off the circle. On a page pinned to 100dvh that
       row's height is not free: it comes out of the table and the entry list,
       and a landscape phone is the one window with none to spare.

       A stated number rather than a measured one, because a circle sized in
       calc() cannot ask an element how tall it turned out. The footer is held
       to it from below by min-height and above it only by its contents, which
       are one sentence set to fit on one line -- 11px of text on a 1.35 line,
       the padding either side of it and the hairline above. */
    --footer-h: calc(28px + env(safe-area-inset-bottom));
}

/* ------------------------------------------------------- day: parchment */

/* Selected by the attribute theme.js stamps rather than by a media query, for
   the reason at the top of this file. The braces below are the whole of the
   day theme: every token it does not name it inherits from the night palette
   above. */
:root[data-theme="day"] {
    /* Narrowed from the "light dark" on :root now that the page has committed
       to one, so the number spinners, checkboxes and scrollbars the browser
       draws for itself follow a theme chosen in Setup and not only one chosen
       by the device. */
    color-scheme: light;

    --bg: #e7ddc8;
    --panel: #f4ecdb;
    --panel-hi: #e3d6ba;
    --line: #c0ae8b;
    --text: #2c2118;
    --muted: #6b5e4b;
    --scrim: rgba(244, 236, 219, 0.94);

    /* Darker than the page, or the table does not read as a table: the first
       light pass had felt and background within a few percent of each other
       and the circle dissolved into the paper. */
    --felt: #ccbd97;
    --felt-hi: #d9cbaa;
    --felt-edge: #ab9a72;
    --wood: #8a6b4f;
    --wood-hi: #a98661;

    /* The token face still has to lift off the table, so parchment goes
       lighter than the page rather than darker. */
    --parchment: #fdf8ec;
    --parchment-dim: #e6d8b8;
    --brass: #8a6a1e;
    --brass-dim: #b09a5e;
    --ink-on-brass: #fdf8ec;
    --on-tint: #fdf8ec;

    /* The parchment cut of the badge, which is the same art on aged paper
       rather than on dark sleeve stock. */
    --ccc-badge: url("resources/community/ccc-parchment.png");

    /* Darkened for a light ground; the hue and luminance ordering that keeps
       the three apart under colour vision deficiency is preserved. */
    --nom: #9c5f14;
    --brd: #0d6d7c;
    --vote: #ad3a6e;
    --role: #6b3d94;

    /* Shaded parchment, not the violet night takes. This palette is warm to
       its roots -- brass, wood, felt, parchment -- and violet is the one hue in
       it that stands for no material, so spread across a whole bar it read as a
       fault rather than as a mode. Against the orange of a pressed toggle and
       the cream of the icon buttons it read as a clash besides.

       So day draws the veil as what the word means: a cloth over the bar. The
       hairline stays the privacy accent and the eye keeps its slash, which is
       what says which mode -- the ground only has to say that the bar is not in
       its ordinary state, and a shade of its own material says that best.

       Darker than the page by enough to be unmistakable, which is what costs
       the ink below: --muted rides at 4.7:1 on the open bar and would fall to
       3.4:1 on this, so the label standing on the veil is darkened to hold the
       contrast the bar had before it was drawn. */
    --veil: #c9bda8;
    --veil-ink: #574a38;

    --shroud: #2a2735;
    --shroud-sheen: #3b3749;
    --shroud-motif: #565064;
    --shroud-braid: #9a91ab;
    --shroud-blood: #9c1116;
    --executed: #a32a2a;


    --reminder: #d3b47c;
    --reminder-hi: #f0dfb6;
    --reminder-line: #7d6535;
    --reminder-ink: #5c4a26;

    --good: #1b5f9e;
    --evil: #9c1f1e;

    --plate-bg: rgba(252, 247, 235, 0.94);
    --plate-line: #b09a5e;
    --plate-text: #2c2118;

    --arrow-casing: #ccbd97;

    --shadow-token: 0 2px 6px rgba(60, 44, 24, 0.32);
    --shadow-plate: 0 2px 5px rgba(60, 44, 24, 0.26);
    --shadow-table: 0 22px 50px -24px rgba(60, 44, 24, 0.5);
}

/* The night palette is already on :root, so this only has to narrow the
   colour-scheme the way the day block does. */
:root[data-theme="night"] {
    color-scheme: dark;
}

* {
    box-sizing: border-box;
}

html,
body {
    margin: 0;
    padding: 0;
}

body {
    /* The Grimoire holds its place and the record scrolls under it. At the
       table you are looking at the circle constantly and reaching for the list
       occasionally, so the list is what gives up its share of the screen.
       A fixed height rather than a minimum, because the page itself must not
       scroll -- the one scroller is the entry list. */
    height: 100dvh;
    display: flex;
    flex-direction: column;
    background-color: var(--bg);

    /* Candlelight: the page falls off toward its corners so the table reads as
       lit rather than as a flat sheet. Fixed, so it does not swim on scroll. */
    background-image: radial-gradient(120% 80% at 50% 22%,
            var(--panel) 0%, transparent 62%);
    background-attachment: fixed;
    color: var(--text);
    font-family: var(--font-ui);
    -webkit-text-size-adjust: 100%;

    /* The felt apron and the name plates are painted past the circle's box on
       purpose; this guarantees neither can ever scroll the page. */
    overflow: hidden;
}

/* ---------------------------------------------------------------- icons */

/* Inline SVG rather than emoji: a glyph like the eye or the gear renders as a
   colour emoji on Windows and Android, which ignores currentColor and defeats
   both themes. Stroked by default, so one path serves either theme. */
.icon {
    display: block;
    width: 20px;
    height: 20px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
    pointer-events: none;
}

.icon--solid {
    fill: currentColor;
    stroke: none;
}

/* Six round caps on zero-length segments: a grip made of dots without needing
   six elements. */
.icon--grip {
    stroke-width: 2.6;
}

/* ---------------------------------------------------------------- top bar */

/* Two stacked rows: the controls, then the script readout under them. A
   column rather than one row with the name squeezed in beside the toggles --
   at 360px the toggles and the two icon buttons already fill the width, so an
   inline name would have had to vanish on exactly the screen it is most wanted
   on, a phone held at the table. */
.topbar {
    flex: none;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 10px 12px;
    padding-top: calc(10px + env(safe-area-inset-top));
    padding-left: calc(12px + env(safe-area-inset-left));
    padding-right: calc(12px + env(safe-area-inset-right));
    border-bottom: 1px solid var(--line);

    /* A hairline of brass under the bar, the way a Grimoire lid is edged. */
    box-shadow: 0 1px 0 var(--brass-dim);
}

/* Ground and hairline together. Neither alone is enough: a 1px line is exactly
   the kind of thing this exists because people do not notice, and a wash with
   the brass still under it reads as a theme gone wrong rather than a mode
   that is on.

   Nothing below the bar moves, dims or recolours. The buttons keep their own
   --panel ground and sit on the veil as objects on a cloth, which is the right
   way round -- the bar is what has changed, not the controls. */
body[data-privacy="true"] .topbar {
    background-color: var(--veil);
    box-shadow: 0 1px 0 var(--role);

    /* Redefined on the bar rather than restated at each label: everything on
       this strip that is set in muted ink -- the small-caps Script, the name
       when no script is loaded, the line that stands in for the toggles when
       nothing is tracked -- is a descendant, so moving the token here moves all
       three and cannot leave one of them behind on the old ground. */
    --muted: var(--veil-ink);
}

.topbar-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* A caption, not a heading: it is the one line on screen that never changes
   during a game, so it states what is loaded and then gets out of the way of
   everything that does.

   Set in the two voices this stylesheet already keeps apart -- the small-caps
   display serif every field label uses, then the name itself in plain ink.
   Run together as one muted string it read as a debug line; splitting the pair
   is most of what makes it read as a caption instead. */
.script-badge {
    display: flex;
    align-items: center;
    gap: 7px;
    min-width: 0;
    margin: 0;
}

/* A rule running out of the name toward the edge of the bar, the way a title
   page carries one under its heading. It earns its place twice: on a wide
   screen it is also what stops the name floating alone at the far left.

   Faded out rather than ruled flat, and painted rather than bordered. A solid
   hairline would have been a second brass line twenty pixels above the bar's
   own edging, reading as a doubled border rather than an ornament; and a 1px
   border of this bronze on this ground rounds to a sub-pixel the screen barely
   resolves. A gradient dissolves instead of ending, which is the difference
   between an ornament and a rule that ran out of room.

   Free to collapse to nothing -- when a long name has taken the width there is
   no rule to draw, and none is wanted. */
.script-badge::after {
    content: "";
    flex: 1;
    min-width: 0;
    height: 1px;
    background-image: linear-gradient(to right, var(--brass-dim), transparent 92%);
}

.script-badge-label {
    flex: none;
    color: var(--muted);
    font-family: var(--font-display);
    font-size: 12px;
    font-variant-caps: small-caps;
    letter-spacing: 0.08em;
}

/* Kept to a single line -- a long homebrew name would otherwise wrap and push
   the circle down by a row it only needs once. */
.script-badge-name {
    min-width: 0;
    overflow: hidden;
    color: var(--text);
    font-family: var(--font-display);
    font-size: 13px;
    line-height: 1.3;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* Nothing loaded is the absence of a name rather than one of them, so it drops
   the label it would otherwise be introducing and sets what is left in the
   muted ink the label was using. */
.script-badge[data-empty="true"] .script-badge-label {
    display: none;
}

.script-badge[data-empty="true"] .script-badge-name {
    color: var(--muted);
    font-size: 12px;
    font-style: italic;
}

.mode-toggle {
    display: flex;
    flex: 1;
    min-width: 0;
    gap: 6px;
}

/* One rule for every tracker: the button carries its own --tint, so adding a
   kind to the catalogue needs no stylesheet change beyond its colour. */
.mode-button {
    flex: 1;

    /* Flex items floor at min-width:auto, which is their min-content width.
       Left at that, three tracker labels plus the two icon buttons need about
       470px and the whole page side-scrolls on a 360px phone. */
    min-width: 0;
    min-height: 44px;
    padding: 8px 8px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 8px;
    background-color: transparent;
    color: var(--muted);
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: bold;
    letter-spacing: 0.02em;
    white-space: nowrap;
    text-overflow: ellipsis;
    cursor: pointer;
    transition: background-color 0.15s, color 0.15s, border-color 0.15s;

    /* Queried by its own labels below. A viewport media query cannot do this
       job: whether the full word fits depends on how many trackers are turned
       on, not on how wide the screen is -- two toggles have room at 390px
       where three do not. Only the button knows its own width. */
    container-type: inline-size;
}

.mode-button[aria-pressed="true"] {
    border-color: var(--tint);
    background-color: var(--tint);
    color: var(--on-tint);
    box-shadow: 0 0 12px -2px var(--tint);
}

/* The corner of the square a round table leaves over. Anchored to the panel
   rather than to the circle, so it travels with the table through all four
   layouts without a breakpoint of its own -- and so it lands out on the
   diagonal, past the apron and the wood, where no seat or name plate reaches.

   It is the one control that sits on the table, which is why it takes the
   panel ground the other icon buttons take rather than going transparent: an
   outline alone on felt is not a button. */
.shroud-toggle {
    position: absolute;
    right: 0;
    bottom: 0;

    /* Above the seat layer, which claims a z-index and would otherwise paint
       over a sibling that only had DOM order on its side. */
    z-index: 2;
    padding: 0;
}

/* Armed. The cloth for the ground and the braid for the edge and the glow,
   which is the pairing the banner itself uses -- and the glow because this one
   is off on its own at the corner, where a quiet border is easy to miss while
   every tap on the table is being read as a shroud.

   --on-tint cannot serve as the ink: it flips with the theme and the braid is
   a mid grey in both, so on day it lands near-white on light grey at 2.8:1.
   The shroud is dark under either palette, which is why --on-shroud never has
   to flip. */
/* Armed, in the Grimoire's own metal rather than in the cloth it draws.

   The cloth was the obvious choice and the wrong one, because the thing it has
   to stand out from is not the same thing in both palettes. On night the cloth
   and the panel beneath it are both near-black -- 1.07:1, no difference at all
   -- and the glyph is near-white either way, so nothing about the button
   changed but a hairline. On day the cloth reads perfectly well and is simply
   wrong for the room: a cold near-black plate is the one thing on a page of
   parchment, brass and warm brown that belongs to no other part of it.

   Brass answers both, and is not a new idea here -- it is already what the
   theme row fills with, for the same reason, and already what the rim and the
   seat plates are drawn in. 7.2:1 against the night panel and 4.3:1 against the
   day one, with --ink-on-brass flipping to stay legible on it either way.

   The braid stays out of it. On a gold ground a grey rope only muddies the
   edge, and the fill no longer needs the help. */
.shroud-toggle[aria-pressed="true"] {
    border-color: var(--brass);
    background-color: var(--brass);
    color: var(--ink-on-brass);
    box-shadow: 0 0 12px -2px var(--brass);
}

/* The catalogue already carries an abbreviation for every tracker, so a narrow
   bar swaps to it rather than truncating the full word to nothing. The long
   label is the default, which is also what a browser without container query
   support will keep -- ellipsised rather than swapped, but never overflowing. */
.mode-short {
    display: none;
    text-transform: uppercase;
}

@container (max-width: 96px) {
    .mode-long {
        display: none;
    }

    .mode-short {
        display: inline;
    }
}

.mode-empty {
    flex: 1;
    min-width: 0;
    margin: 0;
    align-self: center;
    color: var(--muted);
    font-size: 13px;
    line-height: 1.35;
}

.icon-button,
.text-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-width: 44px;
    min-height: 44px;
    flex: none;
    padding: 8px 12px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background-color: var(--panel);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 15px;
    white-space: nowrap;
    cursor: pointer;
}

/* Both, not just the icon one: a text button that cannot act needs to say so
   as plainly, and Clear all names spends most of its life unable to. */
.icon-button:disabled,
.text-button:disabled {
    opacity: 0.35;
    cursor: default;
}

/* The eye gains a slash when privacy is on, so the state reads as "hidden"
   rather than only as "this button is pressed". The slash is drawn twice --
   once in the button's own ground, once in the ink -- so it cuts the eye
   rather than merely crossing it. --icon-casing tracks the ground through
   hover, or the cut would show as a stripe of the wrong colour. */
.privacy-button {
    --icon-casing: var(--panel);

    /* A hold is a gesture on this button, not a selection or a double-tap
       zoom -- without these a long press drags out the text cursor on a
       desktop and raises the callout on iOS. */
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    touch-action: manipulation;
}

.privacy-button .icon-slash {
    display: none;
}

.privacy-button[aria-pressed="true"] {
    border-color: var(--role);
    background-color: var(--panel-hi);
    --icon-casing: var(--panel-hi);
}

.privacy-button[aria-pressed="true"] .icon-slash {
    display: inline;
}

/* The eye opens for as long as it is held. Carries one more compound than the
   rule above it, so it wins on specificity without an !important -- and the
   veil on the bar stays put, because the setting has not changed and a bar
   that went plain on release would say it had. */
body[data-peek="true"] .privacy-button[aria-pressed="true"] .icon-slash {
    display: none;
}

.icon-slash-cut {
    stroke: var(--icon-casing);
    stroke-width: 4.5;
}

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

.layout {
    /* Takes the rest of the window and refuses to grow past it. min-height:0
       is what lets its children shrink below their content, which is what
       makes the entry list a scroller instead of pushing the page taller. */
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
}

/* Reserves the apron. It is drawn with box-shadow spread, so it costs no
   layout on its own -- which is what keeps it from ever forcing a scrollbar,
   and equally what let it paint straight over the entry list beside it on a
   wide window. The padding buys back exactly the room it occupies. */
.circle-panel {
    flex: none;

    /* Opts out of the row layouts' stretch: the table is square and sizes
       itself, where the side panel beside it must take the full height so its
       list has something to scroll inside.

       center rather than flex-start, because this one property serves two
       layouts with perpendicular cross axes. In the row layouts it reads as
       vertical and either value would do. In the stacked ones it reads as
       horizontal, and flex-start meant hard left -- so the table hung off the
       left edge while the entry list under it stayed centred, and the two
       disagreed by seventy pixels on a portrait tablet.

       It cannot overflow upward: the circle is capped at 70dvh and the apron
       adds at most 0.112 of it either side, which still clears the top bar and
       this row's padding at the shortest window this layout ever sees. */
    align-self: center;
    padding: calc(var(--apron) + 6px);

    /* The containing block for the shroud toggle in its corner. The padding
       above is what gives that corner its clearance from the table. */
    position: relative;
}

/* A column of its own: the day controls hold still at the top and the list
   below them takes whatever height is left. The bottom padding moves onto the
   scroller, so the last row clears the edge of the screen when you reach it
   rather than reserving that space permanently. */
.side-panel {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
    max-width: 520px;
    padding: 0 12px;
}

/* ----------------------------------------------------------------- circle */

/* The table: felt to the seat ring, an apron past it carrying the name plates,
   then the wood edge. The apron and the edge are box-shadow spread rather than
   real boxes -- shadows are ink, not layout, so the table can overflow the
   viewport without ever creating a scrollbar. */
.circle {
    position: relative;
    width: var(--circle-size);
    aspect-ratio: 1;
    border-radius: 50%;
    /* The stops stop short of 100%: a radial-gradient defaults to
       farthest-corner, so on a square box the visible edge of the disc is only
       about seven tenths of the way along the ramp. Left at 100% the rim never
       reached --felt-edge and the apron read as a lighter ring around a darker
       table instead of continuing it. */
    background-image:
        radial-gradient(circle at 50% 38%, var(--felt-hi) 0%, var(--felt) 42%, var(--felt-edge) 72%);
    box-shadow:
        0 0 0 var(--apron) var(--felt-edge),
        0 0 0 calc(var(--apron) + 4px) var(--wood),
        0 0 0 calc(var(--apron) + 5px) var(--wood-hi),
        var(--shadow-table);
}

/* Drawn through the seat centres rather than around the outside, so the tokens
   read as chairs at a table instead of dots floating inside a ring. */
.table {
    fill: none;
    stroke: var(--brass-dim);
    stroke-width: 0.3;
    transition: stroke 0.15s;
}

body[data-pending="true"] .table {
    stroke: var(--accent);
    stroke-width: 0.45;
}

/* A 0-100 viewBox over a square box means one computed number is both a CSS
   percentage for the seat tokens and an SVG user unit for the arrows. */
.arrows {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
}

/* Laid under every coloured chord in a separate pass, so where two arrows
   cross the upper one still reads as a line rather than as a junction. It is
   painted in the felt colour: the casing is the table showing through. */
.arrow-casing {
    fill: none;
    stroke: var(--arrow-casing);
    stroke-width: 1.9;
    stroke-linecap: round;
}

/* Fine rather than bold. These are annotations over the art, and at the
   old weight a dozen of them read as the loudest thing on the table. The
   arrowhead is sized in stroke-widths, so it thins with them. */
.arrow {
    fill: none;
    stroke-width: 0.9;
    stroke-linecap: round;
}

/* A crowd tracker runs lighter and underneath. On a contested day votes
   outnumber nominations about five to one, and at equal weight the handful of
   arrows carrying the shape of the day disappears into them. Light enough to
   read as a wash, solid enough to follow one strand when you look for it. */
.arrow--crowd {
    stroke-width: 0.65;
    opacity: 0.5;
}

/* Out of focus. Faded rather than hidden: the rest of the day stays on the
   table as context, so you can see where the moment you picked sits among
   everything else, but it stops competing for the eye. */
.arrow--muted {
    opacity: 0.1;
}

.arrow--crowd.arrow--muted {
    opacity: 0.07;
}

/* Earlier days differ in kind, not only in strength: thin, dashed and without
   a head. With "All days" on, three days of nominations drawn as solid arrows
   at reduced opacity is unreadable spaghetti -- dashes stay countable. */
.arrow--past {
    stroke-width: 0.55;
    stroke-dasharray: 1.5 1.7;
    opacity: 0.55;
}

/* Above the arrow layer, so a chord crossing a seat never buries it.

   pointer-events matter as much as the z-index here. This is a transparent box
   covering the whole circle, and a positive z-index puts it above every
   position:absolute sibling with z-index:auto no matter what the DOM order
   says -- so it was painting over the hub and swallowing every click in the
   middle of the circle. Nothing here is a target except the tokens, which turn
   pointers back on for themselves. */
.seats {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
}

/* A wooden disc rimmed in brass. The unassigned face is dark, so a bare seat
   number reads; an assigned one turns to parchment below. */
.seat {
    position: absolute;
    left: var(--x);
    top: var(--y);
    width: var(--token);
    height: var(--token);
    transform: translate(-50%, -50%);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 2px solid var(--brass-dim);
    border-radius: 50%;
    background-color: var(--panel);
    background-image: radial-gradient(circle at 50% 32%,
            var(--panel-hi) 0%, var(--panel) 70%, var(--felt-edge) 100%);
    color: var(--text);
    font-family: var(--font-display);
    font-size: calc(var(--token) * 0.4);
    font-weight: bold;
    /* lining as well as tabular, everywhere a number is set in the display
       serif: it defaults to old-style figures, which sit at mixed heights and
       drop below the baseline like lowercase letters. Right for prose, wrong
       for a seat number or a day count. */
    font-variant-numeric: lining-nums tabular-nums;
    line-height: 1;
    cursor: pointer;

    /* The layer above opts out of hit testing; the tokens on it are the only
       thing in the circle that should catch a tap. */
    pointer-events: auto;
    box-shadow: var(--shadow-token);
    transition: transform 0.12s, border-color 0.12s, background-color 0.12s;
}

/* Character art is transparent line work drawn for a pale token face, so an
   assigned seat gets parchment rather than the dark wood of an empty one.
   Declared above the pick rules so a highlighted seat still wins. */
.seat[data-assigned] {
    padding: 3px;
    border-color: var(--brass);
    background-color: var(--parchment);
    background-image: radial-gradient(circle at 50% 30%,
            var(--parchment) 0%, var(--parchment-dim) 100%);
}

/* The number on a bare seat -- no character believed for it and nothing laid
   over it. Once either is true the number moves out to the brass plate
   instead, so this never has to compete with art or cloth. */
.seat-index {
    position: relative;
    z-index: 2;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}

/* flex: none, or the automatic minimum size of a 400px source image wins over
   the percentage and the art bursts out of the token. */
.seat-art {
    width: 100%;
    height: 100%;
    flex: none;
    object-fit: contain;

    /* Only ever travels on the way out -- see the fading rule below. Art that
       has just been drawn is a new element already at full, and a transition
       needs a value to change to run at all, so this costs the ordinary case
       nothing. */
    transition: opacity 0.32s ease-in;
}

/* The last third of a receipt: the character dissolves rather than being
   whipped off the token, and the redraw that takes the whole thing back lands
   on art that has already reached nothing.

   A transition and not an animation, deliberately. The reduced-motion rule at
   the foot of this file cuts every animation to a hundredth of a millisecond,
   which would have left the readers most in need of the confirmation with one
   that flashed past unread. Cut to nothing this instead ends the art a third
   of a second early, and the receipt is still on screen for the whole of the
   time app.js holds it. */
.seat[data-fading="true"] .seat-art {
    opacity: 0;
}

/* The seat number survives as an engraved brass plate: the entry list and the
   arrows both refer to players by it, so neither the art nor the shroud may
   swallow it.

   It sits at the foot of the disc, the way a Grimoire token carries its name
   across the bottom -- and in the same place on every seat, rather than
   swinging around the ring with the geometry the way a radial offset does.

   Anchored by its own bottom edge rather than its centre: translateY(-100%)
   against a top of 92% keeps the plate inside the rim at any token size,
   where a fixed offset from the centre would push it out through the bottom
   of a twenty-seat token. */
.seat-number {
    position: absolute;
    left: 50%;
    /* Centred on the rim, half on the token and half hanging below it, the way
       a tag clipped to the edge of a disc sits. The +2px clears the brass
       border so the centre lands on the token's outer edge rather than on its
       padding box, and translateY(-50%) is what makes it a centre rather than
       an edge. Only the plate's top half is over the art, and that half sits
       in the narrow band at the foot of the disc where the icons are clear. */
    top: calc(100% + 2px);

    /* Above the shroud. The entry list and the arrows both address players by
       number, so it is the one mark on a token that a death may not hide. */
    z-index: 2;
    transform: translate(-50%, -50%);

    /* Clamped rather than a straight fraction of the token: the badge is an
       annotation on the art, so past a point it should stop growing with it
       instead of competing. Trimmed from the size it carried while it sat
       radially inward -- the foot of the disc is where the art tends to be
       busiest, so the plate has to spend less of the token to sit there. */
    min-width: clamp(15px, calc(var(--token) * 0.32), 24px);
    padding: 0 3px;
    border: 1px solid var(--wood);
    border-radius: 999px;
    background-image: linear-gradient(var(--brass), var(--brass-dim));
    color: var(--ink-on-brass);
    font-family: var(--font-ui);
    font-size: clamp(9px, calc(var(--token) * 0.235), 14px);
    font-weight: bold;
    font-variant-numeric: lining-nums tabular-nums;
    line-height: 1.5;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

/* A slip of paper tucked against the seat, the way a reminder sits under a
   token on a real Grimoire. Warm and matte where the dead vote beside it is
   cold and glossy -- two small marks on one token have to differ in material
   as well as in hue. Its shape is drawn in app.js; what is here is where it
   sits and how big it is.

   The lane is fixed in screen space, not radial. The shroud owns the top of
   the token and the number plate the foot of it, both in screen space, which
   leaves the two flanks free at every seat and every size -- where a mark that
   swung with the seat's angle had to cross one lane or the other eventually,
   and did.

   Left, a little above the midline. Every value below is a compromise between
   four things the slip has to stay out of, and they do not bind at the same
   size: the shroud and the brass plate are tightest on the smallest token, the
   name plates on the largest. Nudging any one of these is not a free hand --
   it spends clearance somewhere else. */
.seat-note {
    position: absolute;
    left: 50%;
    top: 50%;

    /* Above the character art, level with the shroud. It no longer needs to
       beat the shroud -- it is nowhere near it now -- but the art is what it
       lies on, and that has no z-index of its own. */
    z-index: 1;

    /* Floored well below the twelve pixels this carried while it was square.
       That floor was the largest single reason the mark used to collide with
       everything: at the twenty-seat token it was 46% of the disc, on a token
       already carrying a brass plate most of its width and a shroud down its
       middle. Only the floor gives that much up -- at a twelve-seat token the
       slip is about twelve wide and fifteen tall, so through the sizes anyone
       actually plays at it covers more than the old square did, not less. */
    --note-w: clamp(7px, calc(var(--token) * 0.26), 19px);
    width: var(--note-w);
    height: calc(var(--note-w) * 1.3);

    /* Half the slip's own footprint, which puts its outer edge exactly on the
       rim at every size: subtracting the mark's own half-width is what makes
       the edge, rather than the centre, the thing being placed. Sitting flush
       rather than straddling is what keeps it off the name plate, which starts
       four pixels outside the rim and is the one neighbour that does swing
       round the ring with the seat.

       0.586 is that half-width as a fraction: rotation widens a rectangle's
       upright footprint by 0.5 * (w * cos + h * sin), which at eight degrees
       and a 1.3 ratio is 0.586 of the width -- against 0.68 at the sixteen
       degrees this used to carry. The pixel that buys back is spent on the
       shroud at the smallest token, where the margin is thinnest. Eight
       degrees still reads as laid down by hand rather than set square.

       The lift is small and it earns its keep: the brass plate's top edge is
       proportionally much higher on a small token than a large one, so the
       clearance underneath has to be bought where it is scarce. Larger looks
       free at twelve seats and is not -- past about 0.04 the slip starts
       reaching the name plates of the seats on the upper left. */
    --reach: calc(var(--token) * 0.5 - var(--note-w) * 0.586);
    --lift: calc(var(--token) * 0.03);
    transform: translate(calc(-50% - var(--reach)), calc(-50% - var(--lift)))
        rotate(-8deg);

    /* drop-shadow, not box-shadow: the slip is not a rectangle, and a
       box-shadow follows the element's box -- it would lay a rectangular
       shadow behind a shape with its corner turned down. The shroud casts its
       own the same way and for the same reason. */
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
}

.note-page {
    fill: var(--reminder);
    stroke: var(--reminder-line);
    stroke-width: 1;
}

.note-fold {
    fill: var(--reminder-hi);
    stroke: var(--reminder-line);
    stroke-width: 1;
    stroke-linejoin: round;
}

.note-rule {
    fill: none;
    stroke: var(--reminder-ink);
    stroke-width: 1;
    stroke-linecap: round;
    opacity: 0.7;
}

/* Marks a call you actually made about which side someone is on. Declared
   before the pick rules so a pending pick still wins the border. */
.seat[data-alignment="good"] {
    border-color: var(--good);
}

.seat[data-alignment="evil"] {
    border-color: var(--evil);
}

/* The shroud: a dark banner draped over the token from behind, standing proud
   of the rim and hanging past the middle of the art, its tail cut into a
   swallowtail. Built in app.js as SVG over a 0-100 viewBox mapped onto the
   token. overflow stays visible, because the top of the banner is deliberately
   outside it.

   Above the character art, below the number plate. */
.seat-shroud {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 1;
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
    filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.5));
}

.shroud-cloth {
    fill: var(--shroud);
}

.shroud-sheen {
    fill: var(--shroud-sheen);
}

/* Execution, and the whole of it: the banner is identical to a night death's,
   as it is on a real Grimoire, and only the stripe down its centre turns to
   blood. A shape of its own would have to survive a twenty-seat token; a
   colour reads at any size. */
.seat[data-dead="executed"] .shroud-sheen {
    fill: var(--shroud-blood);
}

.shroud-motif {
    fill: var(--shroud-motif);
}

/* The braid. It is decoration on a real banner and load-bearing here: on an
   unassigned seat the token face is dark too, and this line is what keeps the
   cloth's silhouette from disappearing into it. */
.shroud-braid {
    fill: none;
    stroke: var(--shroud-braid);
    stroke-width: 2;
    stroke-linejoin: round;
}

/* A dead vote already spent. Its absence is what says the vote is still in
   hand, which is how a Grimoire marks it.

   Struck from the braid through, rather than a braid ring around a sheen face.
   The face was the exact colour of the thing it was drawn on: the medallion
   sits low on the centre fold, and the fold is the sheen, so fill and ground
   were the same value and met at 1:1 -- the ring was carrying the whole mark by
   itself at 2.4 units wide on a token that can be forty pixels across.

   Solid, it reads at 4.7:1 against the cloth and 3.8:1 against the fold. On an
   executed banner the fold has turned to blood and the middle of the disc drops
   to 2:1 against it, but the disc is twice the stripe's width, so its edges
   still land on cloth and the silhouette holds. */
.shroud-medallion {
    fill: var(--shroud-braid);
    stroke: var(--shroud-braid);
    stroke-width: 2.4;
}


/* The seat the half-made pick is about: the actor in a mode that records
   pairs, the player being answered in one that records a crowd. */
.seat[data-pick="picked"] {
    border-color: var(--accent);
    background-color: var(--accent);
    background-image: none;
    color: var(--on-tint);
    transform: translate(-50%, -50%) scale(1.12);
    box-shadow: 0 0 16px -2px var(--accent), var(--shadow-token);
}

/* While a pick is pending every other seat is one a tap would act on. */
body[data-pending="true"] .seat[data-pick="candidate"] {
    border-color: var(--accent);
}

/* Already counted in this round. A wash of the tint over the whole token
   rather than another border: a sweep round the table is read at a glance as
   a tally, and at twelve seats a rim differing only in weight from the
   candidates' is not the difference the eye finds.

   Laid over the art but under the shroud and the brass plate, so a dead
   player who spent their vote still reads as dead first. */
.seat[data-cast="true"]::after {
    content: "";
    position: absolute;
    inset: -2px;
    z-index: 0;
    border-radius: 50%;
    background-color: var(--accent);
    opacity: 0.34;
    pointer-events: none;
}

/* The held player answering themselves. Their token is already filled solid
   in the tint, so the wash above lands invisibly on it and the vote needs a
   mark that reads against the fill instead of over it. */
.seat[data-pick="picked"][data-cast="true"] {
    box-shadow: inset 0 0 0 3px var(--on-tint), 0 0 16px -2px var(--accent),
        var(--shadow-token);
}

/* A name card resting on the felt apron. Opaque on purpose: the text-shadow
   halo this replaces punched a soft hole in every arrow that passed behind a
   name, which was a large part of the arrows reading as broken.

   Absolutely positioned, so it shrink-wraps its text and stays centred on the
   radial point without needing to know which side of the circle it is on. */
.seat-label {
    position: absolute;
    left: var(--x);
    top: var(--y);
    /* Pushed radially outward past the token edge, so a label never drifts into
       the neighbouring seat the way a straight-down offset does.

       The second term is the plate's own half-width, not a constant: a
       percentage inside translate() resolves against the element's own border
       box. Multiplied by the outward unit vector it swings the anchor from the
       plate's centre (at the top and bottom of the circle, where there is room
       either side) to its inner edge (at the left and right, where there is
       not) -- so a wide name at three o'clock stops at the token instead of
       lying across it and burying the seat number.

       The last two terms buy clearance for the two things that deliberately
       hang outside the token's rim: the shroud, which stands above it, and the
       number plate, which straddles the bottom of it. Each is scaled by how
       far the seat faces that way -- uy is -1 at twelve o'clock, +1 at six and
       0 at the sides -- and max() floors each at zero over the half of the
       ring it does not apply to. Spending the clearance only where it is
       vertical is what keeps it from re-opening the horizontal clipping this
       reach was tuned to avoid. */
    --reach: calc(var(--token) * 0.5 + 4px
            + max(0px, var(--uy) * -9px)
            + max(0px, var(--uy) * 12px));
    transform: translate(calc(-50% + var(--ux) * var(--reach) + var(--ux) * 50%),
            calc(-50% + var(--uy) * var(--reach) + var(--uy) * 50%));

    /* Two lines rather than one: capped at a single token width with an
       ellipsis, nearly every real name rendered as three characters and a dot
       on a phone.

       width:max-content is not decoration. An absolutely positioned box
       shrink-wraps to the space left between its offset and the edge of its
       containing block, so a plate on a seat at left:93% had seven percent of
       the circle to lay out in and max-width never got a say -- long names on
       the right-hand seats broke mid-word into two clipped fragments. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    width: max-content;

    /* How much room a plate has depends on where its seat sits: at twelve and
       six o'clock the page is wide open either side, at three and nine the
       viewport edge is close. ux squared is 1 on the horizontal extremes and 0
       on the vertical ones, so this widens the plates that can afford it
       without pushing the tight ones off screen -- and it needs no new data,
       since the outward unit vector is already on the element.

       The floor tracks the viewport too. As a flat pixel count it overshot on
       a 320px phone, where the token has shrunk but the constant had not, and
       clipped the three and nine o'clock plates against the screen edge. */
    max-width: max(calc(var(--token) * (2.1 - 0.8 * var(--ux) * var(--ux))),
            clamp(44px, 13vw, 90px));
    overflow: hidden;
    padding: 2px 6px;
    border: 1px solid var(--plate-line);
    border-radius: 6px;
    background-color: var(--plate-bg);
    color: var(--plate-text);
    font-family: var(--font-display);
    /* Tracks the token rather than stopping at 13px, or the name shrinks into
       a caption under a token several times its height. */
    font-size: clamp(9.5px, calc(var(--token) * 0.22), 16px);
    line-height: 1.25;
    text-align: center;

    /* Hyphenation where the browser has a dictionary for the page language,
       breaking mid-word where it does not. A name too long for the plate reads
       better as "Konstan-tin" than with a bare "n" stranded on line two, but
       the dictionary is a component download, so it cannot be relied on.

       break-word, not anywhere. `anywhere` opens a break opportunity between
       every pair of characters, so the line filler always finds one and never
       reaches for a hyphenation point -- the two together silently cancel. */
    hyphens: auto;
    overflow-wrap: break-word;
    box-shadow: var(--shadow-plate);
    pointer-events: none;
}

/* -------------------------------------------------------------------- hub */

/* Explicitly above the seat layer rather than relying on coming after it in
   the DOM, which counts for nothing against that layer's z-index. */
.hub {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 2;
    transform: translate(-50%, -50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    max-width: 46%;
    text-align: center;
}

.hub[data-state="idle"] {
    pointer-events: none;
}

/* A shade wider than a pair pick, which is a trade of width for height. This
   is the one state that stands open the whole time you are tapping the tokens
   around it, and a wrapped tally costs it a row of text -- on a phone that row
   is a seat you cannot see. Sized so the widest count a full table can reach
   still sets on one line. */
.hub[data-round="crowd"] {
    max-width: 64%;
    gap: 6px;
}

/* Sits in the busiest part of the circle, so it needs to cover the chords
   crossing behind it rather than compete with them. It only ever appears while
   a pick is half-made; the idle hint retires as soon as anything is recorded,
   because the centre is exactly where every long chord crosses. */
.hub[data-state="pending"] {
    padding: 10px 12px;
    border: 1px solid var(--brass-dim);
    border-radius: 12px;
    background-color: var(--scrim);
}

/* The panel's gap is meant to be the whole of the spacing between these, so
   the browser's own paragraph margins have to go: left in, it is they that
   set the panel's height rather than the design, and that height comes out of
   the middle of the circle -- which a counting round now sits in for as long
   as it takes to go round the table, not just the moment between two taps. */
.hub-hint,
.hub-pending,
.hub-count {
    margin: 0;
}

.hub-hint {
    color: var(--muted);
    font-size: 13px;
    line-height: 1.35;

    /* Readable even when an all-days overlay draws chords behind it. */
    text-shadow: 0 0 3px var(--bg), 0 0 3px var(--bg);
}

.hub-pending {
    color: var(--accent);
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: bold;
    line-height: 1.3;
    word-break: break-word;
}

/* The running count, set under the name it belongs to and in the muted ink so
   the pair reads as one piece of information rather than two headings. */
.hub-count {
    color: var(--muted);
    font-size: 13px;
    /* Tabular, so the count does not shuffle its own label sideways as it
       climbs from one digit to two. */
    font-variant-numeric: lining-nums tabular-nums;
    line-height: 1.3;

    /* Never wrapped. The rule above buys the width for it, and if a table ever
       outgrows that, a line poking a few pixels wide of the panel costs less
       than one that has quietly grown a second row all round. */
    white-space: nowrap;
}

/* Enough hands to put them on the block. Not a verdict -- a tie still has to
   be broken and the Storyteller has the last word -- just the moment in the
   count worth looking up for. */
.hub-count[data-reached="true"] {
    color: var(--accent);
    font-weight: bold;
}

.hub-actions {
    display: flex;
    gap: 6px;
}

.hub-actions button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 40px;
    padding: 8px 12px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background-color: var(--panel);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 14px;
    cursor: pointer;
}

/* ------------------------------------------------------------------- days */

.daybar {
    flex: none;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 0;

    /* Wraps rather than overflowing: the row totals about 362px unwrapped,
       which is already past a 360px phone. */
    flex-wrap: wrap;
}

.day-label {
    min-width: 0;
    padding: 0 2px;
    font-family: var(--font-display);
    font-size: 18px;
    font-weight: bold;
    font-variant-numeric: lining-nums tabular-nums;
    letter-spacing: 0.02em;
    text-align: center;
    white-space: nowrap;
}

/* The box itself is 18px; the label around it is the real tap target, so it
   carries the touch minimum rather than the input. */
.checkbox {
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    margin-left: auto;
    color: var(--muted);
    font-size: 14px;
    white-space: nowrap;
    cursor: pointer;
}

.checkbox input {
    width: 18px;
    height: 18px;
    accent-color: var(--accent);
}

/* ---------------------------------------------------------------- entries */

/* The one thing on the page that scrolls. overscroll-behavior keeps a flick
   past the end of the list from being handed to the page behind it, which on
   a phone shows up as the whole view rubber-banding.

   The safe area is no longer part of the bottom padding: the colophon sits
   under this list now and is what actually meets the edge of the screen, so it
   is the row that carries the inset. Left here as well it was reserved twice
   and the last entry stopped an unexplained notch short of the rule. */
.entries {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    margin: 0;
    padding: 0 0 16px;
    list-style: none;
}

.entry {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 8px 6px 10px;
    border: 1px solid var(--line);
    border-left: 4px solid var(--line);
    border-radius: 6px;
    margin-bottom: 6px;
    background-color: var(--panel);
    font-size: 15px;
}

/* A row can be tapped to pick its moment out of the circle, so it has to look
   tappable. */
.entry {
    cursor: pointer;
}

/* The whole group lights, not just the row that was tapped -- a nomination and
   every vote answering it -- so the list says the same thing the circle does. */
.entry[data-focus="on"] {
    border-color: var(--brass);
    background-color: var(--panel-hi);
}

.entry[data-focus="off"] {
    opacity: 0.4;
}

.entry-text {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.entry-type {
    flex: none;
    color: var(--muted);
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

/* 44px: this is the undo for a misclick, so it is the one control in the list
   that has to be reachable first time on a phone. */
.entry-delete {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    flex: none;
    border: none;
    border-radius: 6px;
    background-color: transparent;
    color: var(--muted);
    cursor: pointer;
}

.empty {
    padding: 14px 4px;
    color: var(--muted);
    font-size: 14px;
}

/* --------------------------------------------------------------- colophon */

/* The credit line, and one line is all of it. It is read once and then sits
   under a table for the rest of the game, so it is set as quietly as anything
   on this page gets set: muted ink, the smallest type in the file, and a
   hairline of --line above it rather than the brass the top bar is edged with
   -- brass is what marks the lid of the Grimoire, and this is not part of it.

   What is not here is the badge. It stood beside this sentence at 26px, which
   is a size the words inside it cannot be read at, and it doubled the height
   of the row to do it. On the table it has room to be read and costs the entry
   list nothing, so it went there and this row went back to one line. */
.colophon {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: var(--footer-h);
    padding: 5px 12px;
    padding-bottom: calc(5px + env(safe-area-inset-bottom));
    padding-left: calc(12px + env(safe-area-inset-left));
    padding-right: calc(12px + env(safe-area-inset-right));
    border-top: 1px solid var(--line);
}

/* The UI face, not the display serif. Every small-caps line in this file is a
   label naming a control you are about to use, and this names nothing -- set
   in that voice it would have read as one more part of the interface. */
.colophon-text {
    margin: 0;
    color: var(--muted);
    font-size: 11px;
    line-height: 1.35;
    text-align: center;
}

/* The Community Created Content badge, in the corner of the table opposite the
   shroud toggle. Absolute against .circle-panel, which is the containing block
   both of them share and which the panel's own apron padding is what clears.

   Sized off the circle, because the corner it has to fit in is a fraction of
   the circle and nothing else. A round table in a square panel leaves a corner
   whose free room is proportional to the table's radius, and a badge of this
   shape clears the wood edge up to about a quarter of the circle's width --
   0.246 of it at twenty seats, which is the tightest the apron ever gets.
   0.23 is that with a little kept back.

   It carried a flat 80px floor first, which is what a legibility floor looks
   like when it is written without the geometry: on a landscape phone the
   circle is small, the corner with it, and 80px put the badge through the wood
   edge and onto a name plate. A corner cannot be given room it does not have.
   The badge is as large as the corner allows and no larger, which on a wide
   window is the ceiling below and on a phone is a good deal more than the 26px
   it had in the footer.

   contain rather than cover, so a badge that ever came back at a different
   ratio would letterbox rather than have its edges cropped off. */
.ccc-badge {
    position: absolute;
    left: 0;
    bottom: 0;

    /* Below the seat layer, and deliberately the other way round from the
       shroud toggle in the opposite corner. That is a control and has to be
       reachable, so it takes a 2 and sits above everything. This is a credit,
       and the only thing it can ever meet out here is a name plate -- a long
       name at the right angle overflows the apron by a few pixels, which is
       true of the plate against the window edge too and was true before this
       was here. When they do meet, the name is the one that matters, so the
       badge is the one that goes under. A 0 still clears the felt: .seats
       claims a 1 and .circle sets up no stacking context to trap it in. */
    z-index: 0;
    width: min(calc(var(--circle-size) * 0.23), 148px);
    aspect-ratio: 256 / 102;
    background-image: var(--ccc-badge);
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;

    /* It is a credit, not a control, and it sits on the busiest edge of the
       page. Held back a little so the table keeps the eye and this is found
       when looked for rather than competing with the seats above it. */
    opacity: 0.85;
}

/* ------------------------------------------------------------------ setup */

.setup {
    width: min(92vw, 440px);
    max-height: 90dvh;
    padding: 0;
    border: 1px solid var(--brass-dim);
    border-radius: 12px;
    background-color: var(--panel);
    color: var(--text);
    box-shadow: var(--shadow-table);
}

.setup::backdrop,
.picker::backdrop {
    background-color: rgba(0, 0, 0, 0.68);
}

/* A column bounded by the dialog, so the name list is the one thing that
   scrolls. Left to itself the dialog grows past the viewport, Chrome's own
   dialog rule caps it and adds a second scrollbar, and Done ends up below the
   fold of a scroller nested inside another one.

   The bound lives on the dialog alone rather than being repeated here: the
   dialog's max-height covers its border, an identical one on the form would
   not, and the form would overflow it by exactly that much. */
.setup[open] {
    display: flex;
    flex-direction: column;
}

/* The vertical rhythm above the name list is deliberately tight. Everything
   here is flex:none in effect, so each pixel spent on padding is a pixel taken
   off the one scroller in the dialog -- and past a point it is taken off the
   Done button's place on screen instead. */
.setup-form {
    display: flex;
    flex-direction: column;
    min-height: 0;

    /* No bottom padding: a sticky footer parks above its scroll container's
       bottom padding, so the last rows of the name list kept showing in the
       strip underneath it. The actions row carries that padding instead. */
    padding: 16px 18px 0;

    /* A last resort, not the normal case. On a tall phone the name list is the
       only thing that scrolls and this never engages; on a short one the fixed
       content alone outgrows the dialog, and without it the form simply spills
       past the bottom edge. The sticky actions below are what keep that
       survivable. */
    overflow-y: auto;
    overscroll-behavior: contain;
}

.setup h2,
.picker-head h2 {
    font-family: var(--font-display);
    letter-spacing: 0.01em;
}

.setup h2 {
    margin: 0 0 10px;
    font-size: 20px;
}

/* --------------------------------------------------------- setup sections */

/* Each of the dialog's four parts, folded away until wanted. The box is the
   one the tracker fieldset used to carry, now that every section needs it. */
.setup-section {
    margin: 0 0 8px;
    border: 1px solid var(--line);
    border-radius: 8px;
}

/* The section heading, in the small-caps display serif every label in this
   dialog is set in. A full 44px tall: it is the control that opens the
   section, so it is one of the things most often tapped here. */
.setup-section > summary {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 0 12px;
    border-radius: 8px;
    color: var(--muted);
    font-family: var(--font-display);
    font-size: 13px;
    font-variant-caps: small-caps;
    letter-spacing: 0.08em;
    cursor: pointer;

    /* Both are needed to be rid of the browser's own triangle: the standard
       property, and WebKit's pseudo-element for the versions that predate it.
       The chevron in the markup replaces it, so that it can be the same one
       the day arrows use rather than a second disclosure idiom. */
    list-style: none;
}

.setup-section > summary::-webkit-details-marker {
    display: none;
}

.setup-section[open] > summary {
    color: var(--text);

    /* Only once open, so a closed section is a plain bar and an open one reads
       as a heading over its contents. */
    border-bottom: 1px solid var(--line);
    border-radius: 8px 8px 0 0;
}

/* A quarter turn, from pointing at the heading to pointing down into what it
   opened. */
.icon--chevron {
    width: 16px;
    height: 16px;
    transition: transform 0.15s;
}

.setup-section[open] > summary .icon--chevron {
    transform: rotate(90deg);
}

.setup-body {
    padding: 10px 12px 2px;
}

/* Two lists of the same shape in the same section: what to record, and what a
   held eye brings back. One rule rather than two near-copies, because they sit
   one under the other -- where a 4px difference in gap or a shade of ink
   between them reads as a fault rather than as two groups. */
.tracker-fields,
.peek-fields {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.tracker-option,
.peek-option {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 2px 4px;
    border-radius: 6px;
    font-size: 15px;
    cursor: pointer;
}

.tracker-option input,
.peek-option input {
    width: 18px;
    height: 18px;
    flex: none;
    accent-color: var(--accent);
}

/* The colour the toggle and its arrows will take, shown before the choice is
   made rather than discovered after it. */
.tracker-swatch {
    width: 12px;
    height: 12px;
    flex: none;
    border-radius: 50%;
}

/* ----------------------------------------------------------------- script */

.script-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 12px;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--line);
}

.script-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    font-size: 14px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.file-button {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 10px 14px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background-color: var(--panel-hi);
    font-size: 14px;
    white-space: nowrap;
    cursor: pointer;
}

.file-button input {
    display: none;
}

.base-scripts {
    margin-bottom: 12px;
}

.base-script-buttons {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.base-script {
    flex: 1 1 auto;
    min-height: 44px;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background-color: transparent;
    color: var(--muted);
    font-family: var(--font-display);
    font-size: 14px;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color 0.15s, color 0.15s, border-color 0.15s;
}

/* Doubles as a readout of which script is loaded. */
.base-script[aria-pressed="true"] {
    border-color: var(--brass);
    color: var(--text);
    font-weight: bold;
}

.script-error {
    margin: 0 0 12px;
    color: var(--executed);
    font-size: 13px;
    line-height: 1.4;
}

/* Remote icons are a tracking surface, so the consent sits where the script is
   loaded and names the hosts rather than hiding behind a generic wording. */
.remote-notice {
    margin-bottom: 12px;
    padding: 10px;
    border: 1px solid var(--brass-dim);
    border-radius: 8px;
    background-color: var(--panel-hi);
}

.remote-hosts {
    margin: 0 0 8px;
    color: var(--text);
    font-size: 13px;
    line-height: 1.4;
}

/* The daybar copy pushes itself right; here it is a plain row. */
.remote-notice .checkbox {
    margin-left: 0;
    color: var(--text);
}

.field {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
    font-size: 15px;
}

/* Typed narrowly on purpose. The shared input rule below sets width:100% and
   matches with the same specificity, so a plain `.field input` loses the tie
   on source order and the seat box stretches the full dialog, wrapping its own
   label onto a second line. */
.field input[type="number"] {
    width: 92px;
    flex: none;
}

.hint {
    margin: 0 0 10px;
    color: var(--muted);
    font-size: 13px;
    line-height: 1.45;
}

/* The same grip the rows carry, at text size, so the hint points at the
   control rather than at a glyph that only resembles it. */
.hint-grip {
    color: var(--text);
}

.hint-grip .icon {
    display: inline-block;
    width: 1.15em;
    height: 1.15em;
    vertical-align: -0.25em;
}

/* One column, not a grid: seating is a sequence, and reading order in a
   multi-column grid does not match it. It also keeps dragging one-dimensional.
   position:relative makes this the offsetParent the drag maths measures from. */
.name-fields {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 8px;

    /* Takes whatever height the rest of the form leaves. min-height lets it
       shrink below its content, which is what makes it the scroller, and sets
       the floor it may not shrink past.

       That floor has to track the viewport, not a fixed pixel count. The rest
       of the form is a fixed height, so on a tall phone a generous floor still
       leaves Done inside the dialog, while on a short one the same number
       pushes it out and the dialog starts scrolling too -- a scroller inside a
       scroller, which is the thing this layout exists to avoid. */
    flex: 1 1 auto;
    min-height: clamp(96px, 15dvh, 240px);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-right: 4px;
}

.name-fields.reordering {
    user-select: none;
}

/* Set apart from the last row it would otherwise look like a part of. */
.clear-names {
    margin-top: 10px;
}

.name-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px;
    border-radius: 8px;
    background-color: var(--panel);
}

/* Only the rows making way animate; the dragged row must track the finger
   exactly, so it never gets a transition. */
.reordering .name-row:not(.dragging) {
    transition: transform 0.16s ease;
}

.name-row.dragging {
    position: relative;
    z-index: 2;
    background-color: var(--panel-hi);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.55);
}

.name-row-number {
    width: 22px;
    flex: none;
    color: var(--muted);
    font-size: 13px;
    font-variant-numeric: lining-nums tabular-nums;
    text-align: right;
}

.drag-handle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    flex: none;
    border: none;
    border-radius: 6px;
    background-color: transparent;
    color: var(--muted);
    cursor: grab;

    /* Without this the browser claims the gesture to scroll the dialog. */
    touch-action: none;
}

.name-row.dragging .drag-handle {
    cursor: grabbing;
    color: var(--text);
}

input[type="text"],
input[type="search"],
input[type="number"] {
    min-height: 44px;
    width: 100%;
    min-width: 0;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background-color: var(--bg);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 16px;
}

/* summary is focusable in its own right and is not a button, so without it
   named here the section headings fell back to the browser's own ring -- a
   heavy white box that is the first thing the dialog shows, since opening it
   focuses the first heading. */
input:focus-visible,
button:focus-visible,
summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Pinned to the foot of the form. Where the content fits this changes nothing;
   where it does not, Done and New Game stay on screen instead of ending up
   below the fold of a scroller the user has no reason to expect. */
.setup-actions {
    position: sticky;
    bottom: 0;
    z-index: 1;
    display: flex;
    flex: none;
    justify-content: space-between;
    gap: 10px;
    margin-top: 14px;
    padding: 12px 0 16px;
    border-top: 1px solid var(--line);
    background-color: var(--panel);
}

.primary-button,
.danger-button {
    min-height: 44px;
    padding: 10px 18px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background-color: var(--panel-hi);
    color: var(--text);
    font-family: var(--font-display);
    font-size: 15px;
    cursor: pointer;
}

.danger-button {
    border-color: var(--shroud-blood);
    color: var(--executed);
}

.primary-button {
    border-color: var(--brass-dim);
    font-weight: bold;
}

/* ----------------------------------------------------------------- picker */

.picker {
    width: min(94vw, 620px);
    max-height: 86dvh;

    /* See .setup-form: the sticky actions row owns the bottom padding, or the
       character grid scrolls visibly underneath it. */
    padding: 16px 16px 0;
    border: 1px solid var(--brass-dim);
    border-radius: 12px;
    background-color: var(--panel);
    color: var(--text);
    box-shadow: var(--shadow-table);
}

/* Same column as the setup dialog, and for the same reason: the note box made
   the content taller than the cap, so the grid has to be the one thing that
   scrolls or "Clear character" ends up below the fold of a nested scroller. */
.picker[open] {
    display: flex;
    flex-direction: column;
}

.picker-head {
    display: flex;
    flex: none;
    align-items: center;
    gap: 10px;
}

.picker-head h2 {
    flex: 1;
    min-width: 0;
    margin: 0;
    overflow: hidden;
    font-size: 19px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The name sits with the rest of what a player sheet can change. */
.player-name {
    flex: none;
    margin: 12px 0 0;
}

.name-field {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* min-width:0 or the input keeps its intrinsic width and pushes the button
   off the end of a narrow dialog. */
.name-field input {
    flex: 1;
    min-width: 0;
}

/* Locked. Recessed rather than greyed: the name still has to be the easiest
   thing on the sheet to read, which is what you want from it far more often
   than editing it. */
.name-field input:read-only {
    background-color: var(--panel-hi);
    cursor: default;

    /* Inert as well as recessed. Left clickable it took a tap, a caret and a
       selection and then accepted no key, which reads as a broken text box
       rather than a locked one. Nothing here is worth selecting either: the
       name is two words and it is written in the box above the pencil that
       edits it. */
    pointer-events: none;
    user-select: none;
}

/* One button, two jobs. The pencil unlocks the field and only then does it
   become the cross that empties it, so a name takes two deliberate taps to
   lose rather than one stray one. */
.icon-erase,
.icon-pencil {
    display: none;
}

#name-edit[data-editing="false"] .icon-pencil,
#name-edit[data-editing="true"] .icon-erase {
    display: block;
}

.status-row {
    display: flex;
    gap: 6px;
}

/* A direct child of the picker column now that the status row above it is
   gone, so it carries its own flex and margin the way its neighbours do. */
#alignment-row {
    flex: none;
    margin: 12px 0 0;
}

.status-button {
    flex: 1;
    min-width: 0;
    min-height: 44px;
    padding: 8px 6px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background-color: transparent;
    color: var(--muted);
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: bold;
    cursor: pointer;
    transition: background-color 0.15s, color 0.15s, border-color 0.15s;
}

/* A receipt letting go, rather than a press being answered. The row above
   already fades its three colours; this is the same fade run long, matched to
   the seat art's 0.32s so the two confirmations privacy hands back leave at the
   same speed. Only the alignment row is ever given the attribute -- it is the
   only row with anything to hand back, the rest being facts the table saw.

   Duplicated from REVEAL_FADE in app.js the way .seat-art's already is: the
   timer decides when the row stops showing the call, and this decides how the
   colour gets there, so they have to agree without being able to ask. */
.status-row[data-fading="true"] .status-button {
    transition-duration: 0.32s;
}

/* The braid rather than the cloth: the banner is near-black, and a near-black
   border on a dark panel is no border at all. */
.status-button[aria-pressed="true"] {
    border-color: var(--shroud-braid);
    color: var(--text);
}

/* Outlined, like every other pressed status button on the sheet. A recorded
   cause is a fact the sheet is holding, not a warning it is raising, and a
   filled red plate said it louder than the alignment row says a call that
   matters at least as much.

   Which leaves "died" with nothing to declare at all: the rule above already
   gives it the braid edge, so it simply stops overriding and takes it. Only the
   red is worth keeping, and only on the edge -- it is what parts the pair at a
   glance, and it answers the stripe inside the banner. */
.status-button[data-cause="executed"][aria-pressed="true"] {
    border-color: var(--executed);
}

/* Outlined rather than filled, so "Default" reads as the neutral answer and
   not as a decision. There is no unset option in the cause row beside it:
   a death has a cause or there is no death. */
.status-button[data-alignment=""][aria-pressed="true"] {
    border-color: var(--role);
    color: var(--text);
}

/* The Grimoire's own metal. The rows above take their pressed colour from the
   thing they describe -- the shroud, the two alignments -- and what this one
   describes is the book itself, so it is filled the way the seat numbers are
   engraved. All three alike: unlike the rows above there is no unset option
   here, only three answers of equal standing. */
.status-button[data-theme][aria-pressed="true"] {
    border-color: var(--brass);
    background-color: var(--brass);
    color: var(--ink-on-brass);
}

/* The same two colours the rings and the art use. */
.status-button[data-alignment="good"][aria-pressed="true"] {
    border-color: var(--good);
    background-color: var(--good);
    color: var(--on-tint);
}

.status-button[data-alignment="evil"][aria-pressed="true"] {
    border-color: var(--evil);
    background-color: var(--evil);
    color: var(--on-tint);
}

/* The three facts of a death, on one line under the alignment row -- and only
   on screen once there is a death, since the switch that makes one now lives up
   in the head. A box of its own again rather than the display:contents it was
   while a wrapper held it and the shroud together: there is nothing left to
   share a line with, so it can simply be the line.

   It wraps, but only as a floor. The three controls want about 250px of the
   326 a 390px phone has, so wrapping is what happens on something narrower
   still rather than what is planned for here. */
.death-detail {
    display: flex;
    flex: none;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    margin: 12px 0 0;
    font-size: 14px;
}

/* An author display rule outranks the UA's [hidden] { display: none }, so the
   attribute does nothing here without being restated. */
.death-detail[hidden] {
    display: none;
}

/* A pair of labelled halves that share the line, rather than the two bare
   squares this was briefly. They went back to carrying words because the line
   read as empty without them -- three small marks adrift in a row the alignment
   buttons above had already filled edge to edge. */
#cause-row {
    flex: 1 1 200px;
    gap: 6px;
}

/* Flex rather than the block a .status-button normally is, only so the banner
   and the word sit on one baseline with a gap between them. The flex:1 the
   shared rule gives every status button is left alone, so these two go back to
   halving whatever the line can spare -- which is what they did before either
   of them had an icon. */
#cause-row .status-button {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 8px 10px;
}

/* The blood stripe down an executed banner, at icon scale. Drawn a touch
   heavier than the outline it sits inside so it reads as a band and not as a
   fold, and stopped short of the notch so the banner keeps its shape.

   In its own red rather than in the ink, which it now keeps in every state:
   pressed used to fill the plate red underneath it, and a red stripe on red is
   no stripe, so the ink had to take over. Nothing fills red any more, so the
   stripe can simply stay the colour the token paints it. */
.icon-stripe {
    stroke: var(--executed);
    stroke-width: 3;
}

/* The one .vote-button that is not a square: it carries a word now, like the
   two beside it, so it takes the width that word needs.

   No margin-left:auto any more. It was there to send the medallion to the far
   end and stop a half-empty line reading as unfinished beside the full-width
   alignment row above -- and with three labelled controls the line reaches the
   end on its own. Left in, it would have done real harm the moment the line
   wrapped on a narrow phone: the medallion would have been thrown alone onto a
   second row and pinned to the right of it.

   Set in the same display face as its neighbours. Three controls on one line in
   two different voices is the kind of thing that reads as a mistake without
   anyone being able to say why. */
#ghost-vote {
    width: auto;
    gap: 6px;
    padding: 8px 12px;
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: bold;
}

.death-day {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--muted);
    white-space: nowrap;
}

/* A day is one digit, or two in a game nobody wants to be in. */
.death-day input {
    width: 60px;
}

/* The dead vote, marked the way the seat marks it: the medallion is there once
   the vote is gone and absent while it is still in hand, which is how a
   Grimoire says it. Filled and unfilled rather than present and absent, since a
   button that vanished would take its own way back with it. */
.vote-button {
    display: flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 44px;
    min-height: 44px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 8px;
    background-color: transparent;
    color: var(--muted);
    cursor: pointer;
    transition: background-color 0.15s, color 0.15s, border-color 0.15s;
}

/* Outlined, like every pressed control on the sheet now. It used to lay the
   shroud's own cloth down as a plate, which was defensible while the cause
   buttons beside it did the same -- and became the one dark slab on a line of
   outlines the moment they stopped. On day it was worse than out of place: a
   cold near-black rectangle is the one thing on a page of parchment and warm
   brown that belongs to nothing else on it.

   Nothing is lost by dropping it, because the plate was never what said
   "spent". The mark inside is. */
.vote-button[aria-pressed="true"] {
    border-color: var(--shroud-braid);
    color: var(--text);
}

/* Solid rather than drawn, which is the whole of what says spent -- and now the
   whole of what says it, with the plate gone from behind.

   In the ink rather than the braid it took while there was a plate. The braid
   was chosen to read against near-black cloth; against the panel that cloth has
   been dropped for it comes to 2.6:1 on day, which is under the floor for a
   mark carrying a state on its own. currentColor is already the pressed ink,
   and lands at 13:1 either way.

   Serves the shroud switch in the head as well, being the same shape saying the
   same kind of thing: outlined, the shroud this would hang; filled, the one
   already hanging. */
.vote-button[aria-pressed="true"] .icon {
    fill: currentColor;
}

.player-note {
    flex: none;
    margin: 12px 0;
}

.field-label {
    display: block;
    margin-bottom: 6px;
    color: var(--muted);
    font-family: var(--font-display);
    font-size: 13px;
    font-variant-caps: small-caps;
    letter-spacing: 0.08em;
}

.note-input {
    width: 100%;
    min-height: 72px;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background-color: var(--bg);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 16px;
    line-height: 1.4;

    /* Vertical only: a wider box would break out of the dialog. */
    resize: vertical;
}

.picker-filter {
    flex: none;
    margin: 0 0 12px;
}

/* Same bargain as the name list in Setup, and the same reason the floor is
   viewport-relative: it is the one scroller in the dialog, and everything
   above it is a fixed height, so a pixel floor that leaves room on a tall
   phone pushes "Clear character" out of reach on a short one. */
.picker-grid {
    flex: 1 1 auto;
    min-height: clamp(90px, 20dvh, 240px);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.picker-team {
    margin: 12px 0 6px;
    color: var(--muted);
    font-family: var(--font-display);
    font-size: 14px;
    font-variant-caps: small-caps;
    letter-spacing: 0.08em;
}

.picker-row {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
    gap: 8px;
}

.pick {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 6px 4px;
    border: 2px solid transparent;
    border-radius: 10px;
    background-color: var(--bg);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 12px;
    cursor: pointer;
}

.pick-art {
    width: 54px;
    height: 54px;
    flex: none;
    object-fit: contain;
}

.pick-name {
    line-height: 1.2;
    text-align: center;

    /* break-word rather than anywhere, which is the stronger rule and was the
       reason "Washerwoman" came out as "Washerwom / an": anywhere lets the line
       break at whatever character fills the row best, and it will take that
       over a hyphenation point every time. break-word only breaks a word that
       cannot fit a line by itself, so the dictionary gets asked first and the
       break lands where a reader expects it.

       The two behave differently for min-content as well, but not here -- the
       tile grid sizes its tracks from the container, so neither can widen a
       column. The page is lang="en", which is what makes the dictionary
       available at all. */
    hyphens: auto;
    overflow-wrap: break-word;
}

.pick[data-current] {
    border-color: var(--brass);
}

/* Dimmed, not disabled: the Drunk, the Lunatic and the Philosopher all make a
   duplicate guess legitimate, so this marks rather than blocks.

   The art and the caption fade, not the tile. opacity on the tile applies to
   the whole subtree, badge included -- and the badge is the one part of a
   taken tile carrying something you cannot see anywhere else. */
.pick[data-taken] .pick-art,
.pick[data-taken] .pick-name {
    opacity: 0.45;
}

.pick-taken {
    position: absolute;
    top: 4px;
    right: 4px;
    min-width: 18px;

    /* Grows with the seat list. Bounded by the tile, so a character believed
       of half the table cannot push its badge over the neighbouring one. */
    max-width: calc(100% - 8px);
    padding: 0 4px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 999px;
    background-color: var(--panel-hi);
    color: var(--text);
    font-size: 11px;
    font-variant-numeric: lining-nums tabular-nums;
    line-height: 16px;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* Same pinning as the setup dialog's, for the same reason: on a short phone
   the status rows, the note box and the filter together outgrow the cap, and
   Clear character would otherwise sit below the fold. */
.picker-actions {
    position: sticky;
    bottom: 0;
    z-index: 1;
    display: flex;
    flex: none;
    gap: 10px;
    justify-content: space-between;
    margin-top: 12px;
    padding: 10px 0 16px;
    border-top: 1px solid var(--line);
    background-color: var(--panel);
}

.picker-empty {
    padding: 20px 4px;
    color: var(--muted);
    font-size: 14px;
    line-height: 1.45;
}

/* ------------------------------------------------------------------ hover */

/* Guarded: on a touch screen a :hover style latches after a tap and stays
   there until something else is tapped, which reads as a stuck selection. */
@media (hover: hover) {
    .icon-button:not(:disabled):hover,
    .text-button:hover {
        background-color: var(--panel-hi);
    }

    /* The armed toggle keeps its metal under the pointer. The generic rule
       above outranks the armed one -- three classes to two, and later in the
       file besides -- so without this the one button that is armed is the one
       that stops looking it the moment you reach for it.

       Brightened rather than repainted, the way an assigned token answers the
       pointer: brass is a colour the palette flips, and any second value named
       here would have to be flipped alongside it and kept in step. */
    .shroud-toggle[aria-pressed="true"]:hover {
        background-color: var(--brass);
        filter: brightness(1.08);
    }

    .seat:hover {
        background-color: var(--panel-hi);
    }

    .seat[data-assigned]:hover {
        border-color: var(--brass);
        filter: brightness(1.06);
    }

    .hub-actions button:hover {
        background-color: var(--panel-hi);
    }

    .entry-delete:hover {
        background-color: var(--shroud-blood);
        color: var(--text);
    }

    .tracker-option:hover,
    .peek-option:hover {
        background-color: var(--panel-hi);
    }

    .file-button:hover {
        background-color: var(--line);
    }

    .base-script:hover {
        background-color: var(--panel-hi);
        color: var(--text);
    }

    .drag-handle:hover {
        background-color: var(--panel-hi);
        color: var(--text);
    }

    .danger-button:hover {
        background-color: var(--shroud-blood);
        color: var(--text);
    }

    .status-button:hover,
    .vote-button:hover {
        background-color: var(--panel-hi);
    }

    .pick:hover {
        background-color: var(--panel-hi);
    }
}

/* ------------------------------------------------------------- responsive */

/* The player sheet on a phone. Everything above the character grid is a fixed
   height and the grid takes what is left, so on a 390px screen the one thing
   the sheet is opened to do had been squeezed to about two rows of a scroller
   while the chrome above it kept margins sized for a desk.

   Nothing below is a new idea about how the sheet works -- it is the same sheet
   with the air let out of it, and every pixel that frees goes to the grid,
   which is the only child that grows. Bounded at 639px so it stops exactly
   where the tablet rules start.

   Kept as its own block rather than folded into the base, mobile-first though
   the rest of the file is: these are compromises for a screen that has run out
   of room, and a 1400px window should not be made to take them. */
@media (max-width: 639px) {
    .picker {
        /* Near enough the whole screen. At 86dvh a strip of table showed above
           and below that was too thin to read anything on and too tall to
           spend on a sheet this crowded.

           max-width restated because the browser's own default for a dialog is
           calc(100% - 6px - 2em), which on a 390px phone was quietly holding
           the sheet at 352px however wide it asked to be. Those 22px are worth
           having by themselves: the tile grid fills on a 78px minimum, and 350
           of content is where a fourth column starts fitting. */
        width: 96vw;
        max-width: none;
        max-height: 94dvh;
        padding: 12px 12px 0;
    }

    /* The seat number is already on the sheet three times over -- here, as the
       name field's placeholder, and in the note field's own prompt. It can
       afford to be said smaller in the one place that says it largest. */
    .picker-head h2 {
        font-size: 17px;
    }

    /* Still there, and still what names the field to a screen reader. Only
       drawn closer to the thing it names. */
    .field-label {
        margin-bottom: 3px;
        font-size: 12px;
    }

    .player-name,
    #alignment-row,
    .death-detail {
        margin-top: 8px;
    }

    .player-note {
        margin: 8px 0;
    }

    .picker-filter {
        margin-bottom: 8px;
    }

    /* Two lines rather than three. height and not min-height: the rows="3" on
       the element is what actually sizes a textarea, and a floor underneath it
       never came into play -- the box stayed three lines tall however low the
       floor was set.

       What this gives up is a third empty line standing open before anything is
       written, on every sheet, read or not. The box still scrolls as it is
       typed into and can still be dragged taller, and resize is left alone so
       that stays true. */
    .note-input {
        height: 52px;
        min-height: 52px;
    }

    .picker-actions {
        margin-top: 8px;
        padding: 8px 0 12px;
    }

    /* One line, which takes a trim from every part of it. Nothing is dropped --
       the banners stay, the words stay -- they are set closer together and a
       size down, the way the top bar's toggles already give up their full
       labels when the bar runs out of room.

       The basis is the part that actually decides it. A wrapping flex line does
       not shrink its items below flex-basis, it wraps them, so the cause row
       asking for 200px was what pushed the medallion onto a second row however
       small the paddings got. Sized from its own content instead, the row asks
       for what its two words need and no more. */
    .death-detail {
        gap: 6px;
    }

    #cause-row {
        flex: 1 1 auto;
        gap: 4px;
    }

    /* Grown from their own words rather than halved between them. The shared
       status-button rule gives every one flex:1 -- grow, shrink, and a zero
       basis -- which had "Executed" and "Died" claiming the same width and
       clipped the longer of the two the moment the row ran short.

       Basis auto keeps each one as wide as its own word needs; grow hands them
       whatever the line has spare, split evenly, so the row reaches the end
       instead of stopping short of it with the slack piled up after the last
       button. */
    #cause-row .status-button {
        flex: 1 1 auto;
    }

    #cause-row .status-button,
    #ghost-vote {
        gap: 4px;
        padding: 8px 6px;
        font-size: 13px;
    }

    .death-day {
        gap: 4px;
        font-size: 13px;
    }

    /* Two digits is every day this game will ever reach. The 16px on the field
       itself is left alone -- below that, iOS zooms the page in the moment it
       takes focus. */
    .death-day input {
        width: 38px;
    }

    /* A size down, so the sentence stays on the one line it is budgeted for.
       At 11px it runs about 326px, which a 360px phone has and a 320px one
       does not -- and wrapped it would be twice the height --footer-h promises
       the circle, on exactly the screens with the least to give. Nothing is
       dropped; it is set smaller, which is the same bargain the death line
       below makes. */
    .colophon-text {
        font-size: 10px;
    }

    /* The last two millimetres, and taken from the drawing rather than the
       writing. At 360px the line came out one pixel over its container, which
       is no margin at all -- a font that renders a hair wider, or a longer word
       in another language, and it wraps again. Two pixels off each of the three
       marks buys about ten, and a banner reads as well at 18 as at 20 where a
       13px word does not read as well at 12. */
    .death-detail .icon {
        width: 18px;
        height: 18px;
    }
}

/* Narrower still. A 320px phone leaves the death line 281px to live in, where
   the step above it is tuned for the 320 that a 360px phone has, so the same
   three dials get one more notch: a size off the type, two pixels off each
   padding, two off each banner.

   One more notch and not a different layout, because what would have to go
   otherwise is the words -- and the words are the whole reason this line
   stopped reading as empty. The field keeps its 16px for the same reason it
   does above: below that, iOS zooms the page when it takes focus. */
@media (max-width: 359px) {
    /* A size down again, for the reason the step above took one: the credit
       line has to hold the single line --footer-h is written against, and at
       10px it needs about 300px where a 320px phone has 296 between its
       paddings. The closest thing to a rounding error, and what it buys if it
       is lost is the row doubling in height on the shortest screen there is. */
    .colophon-text {
        font-size: 9px;
    }

    .death-detail {
        gap: 4px;
    }

    #cause-row {
        gap: 3px;
    }

    #cause-row .status-button,
    #ghost-vote {
        gap: 3px;
        padding: 8px 4px;
        font-size: 12px;
    }

    .death-detail .icon {
        width: 16px;
        height: 16px;
    }

    .death-day {
        gap: 3px;
        font-size: 12px;
    }

    .death-day input {
        width: 34px;
    }
}

/* A landscape phone: the circle is bounded by height, which leaves the width
   free for the entry list beside it rather than below the fold. */
@media (orientation: landscape) and (max-height: 560px) {
    /* The height fraction has to leave room for the top bar and for the apron
       above and below the circle, or the bottom name plate lands under the
       fold on every load.

       The subtraction is the top bar's second row -- the script line under the
       toggles. The fraction alone was tuned against a single-row bar, and a
       landscape phone is where that tuning had the least to spare: at 320px
       tall the top name plate had begun to cross into the bar. Taken off as a
       flat height rather than off the fraction, because that is what the row
       costs -- scaling it would have shrunk the circle most on the tall
       landscape phones that never had the problem.

       The colophon comes off the same end for the same reason, and is taken as
       a token rather than folded into the 30px: the bar's second row is a fixed
       cost, where the footer carries a safe-area inset that only some devices
       have. Two terms that move independently, so they are subtracted
       independently. */
    :root {
        --circle-size: min(46vw, calc(64dvh - 30px - var(--footer-h)));
    }

    .layout {
        flex-direction: row;
        align-items: stretch;
        justify-content: center;
        gap: 16px;
    }

    .side-panel {
        width: 46vw;
        max-width: 460px;
        padding-top: 6px;
    }
}

/* A tablet in portrait: still stacked, but the circle no longer has to give up
   space to margins it does not need. Its share of the height is smaller than
   the width alone would allow, because the circle now holds its place and the
   list scrolls beneath it -- at the older size the list was left about two
   rows tall on a 900px-high tablet.

   The height bound is what keeps this from also matching a landscape phone,
   which is 844 wide and therefore satisfies every width condition here. The
   two blocks overlap otherwise, and being the later one this would win and
   size the circle off the viewport's short side twice over. */
@media (min-width: 640px) and (max-width: 899px) and (min-height: 561px) {
    :root {
        --circle-size: min(66vw, calc(52dvh - var(--footer-h)));
    }
}

/* From tablet width up the toggles stop being the whole bar. Left to stretch,
   two trackers became a pair of 350px buttons across a landscape phone. */
@media (min-width: 640px) {
    .mode-toggle {
        max-width: 460px;
    }

    /* On the first icon, not the second: pushing only the gear right stranded
       the eye against the toggles with a gap between the two. */
    #privacy {
        margin-left: auto;
    }
}

@media (min-width: 900px) {
    :root {
        /* The colophon comes off the height term here for the same reason it
           does on a landscape phone: the page does not scroll, so a row at the
           foot of it is height the table no longer has. A wide window looks
           like it has room to spare and does not -- at 900px tall the fraction
           was already within fifteen pixels of the apron, and the footer is
           more than that, so the wood edge met the footer's hairline. Every
           laptop height is in that band; only a very tall monitor was clear of
           it. Subtracted as the token rather than folded into the fraction,
           because it is a flat cost that does not scale with the viewport. */
        --circle-size: min(50vw, calc(70dvh - var(--footer-h)));

        /* The band the whole app occupies: the table and its apron, the gap,
           the entry list, and the row's own padding. Every term is the width
           of a thing actually in the row, so the band is exactly as wide as
           its contents at any window size.

           Computed rather than the flat 1200px this used to carry. That number
           was meant to track this same sum, but the sum moves with the circle
           -- which is a fraction of the viewport -- so the two agreed at one
           window width and nowhere else. At 1920 the row ran 137px past the
           top bar at each end; below about 1500 the list stretched to fill
           instead, and the row was not centred at all. */
        --band: calc(var(--circle-size) + 2 * (var(--apron) + 6px)
                + 24px + 380px + 24px);
    }

    /* Both of these get the same cap and the same auto margins, which is what
       puts the top bar and the row beneath it on one axis: the toggles start
       where the table's apron does and the gear lands over the list's right
       edge, at every width. */
    .topbar {
        width: 100%;
        max-width: var(--band);
        margin: 0 auto;
    }

    .layout {
        flex-direction: row;
        align-items: stretch;
        justify-content: center;
        gap: 24px;
        padding: 12px;
        width: 100%;
        max-width: var(--band);
        margin: 0 auto;
    }

    /* Left free to grow, which now costs nothing: past the cap there is no
       spare room in the band to grow into, and under it the list is shrinking
       rather than stretching. */
    .side-panel {
        width: 380px;
        padding-top: 6px;
    }

    /* The third row on the same axis as the other two. Its hairline is the
       bookend to the top bar's, and on a 1920px window a rule running the full
       width under a band that stops 137px short of each edge reads as a piece
       of some other page. */
    .colophon {
        width: 100%;
        max-width: var(--band);
        margin: 0 auto;
    }
}

/* ------------------------------------------------------------ preferences */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
    }

    /* The scale is the picked seat's only cue besides colour, so it stays --
       it just arrives without a transition, which the rule above handles. */
}

@media (prefers-contrast: more) {
    .seat {
        border-width: 3px;
    }

    .seat-label {
        border-width: 2px;
    }

    .arrow {
        stroke-width: 1.3;
    }

    .arrow-casing {
        stroke-width: 2.6;
    }

    .arrow--past {
        opacity: 0.8;
    }

    .mode-button,
    .status-button,
    .vote-button {
        border-width: 2px;
    }
}
