/* =====================================
   THE PALETTE — Steampunk Horror
   ============================================
   Brass, soot, hardwood and verdigris. Named here and used everywhere, so a
   new panel does not arrive in whatever colors its author reached for - which
   is how the battle map turned up with a cornflower-blue party and a mint
   green ally in a room otherwise lit by gaslight.

   Four families and what each is FOR:

     soot      the room. Backgrounds, from the page to a raised panel.
     wood      the furniture. Borders, rules, the edges of things.
     brass     the instrument. Anything that reads as machined: labels,
               headings, the active turn, a value worth looking at.
     verdigris oxidised copper. The friendly signal - your own party, hit
               points you still have.
     oxblood   the horror half. Enemies, damage, refusal.

   Two rules worth keeping. Nothing decorative is pure black or pure white:
   the darkest is soot and the lightest is parchment, because a lantern-lit
   room has neither. And a faction color is used for that faction ONLY, so a
   green border always means the same thing wherever it appears - on an
   initiative card, on a token, on a roster row.
*/
:root {
    /* Borders, with contrast (2026-10-01: "more contrast on borders"): the
       wood line pulled toward the text color, so it darkens by day and
       lightens by night instead of fading into the page in both. */
    --line: color-mix(in srgb, var(--wood-700) 55%, var(--parchment));
    /* The type: two faces, seven sizes, two weights, no italics. */
    --font-display: "Cinzel", "Source Serif 4", Georgia, serif;
    --font-body: "Source Serif 4", Georgia, serif;
    --text-2xs: 0.6rem;
    --text-xs: 0.7rem;
    --text-sm: 0.8rem;
    --text-md: 0.95rem;
    --text-lg: 1.2rem;
    --text-xl: 1.6rem;
    --text-2xl: 2.2rem;
    /* The shell's geometry, named because three rules already have to agree
       about it: the body's top padding, the war room's `calc(100vh - 7rem)`,
       and now the inspector rail's top edge. Seven rem was never a number -
       it is the header plus the gutter under the page, and writing it as one
       made the rail a fourth place to get it wrong. */
    --header-height: 5rem;
    --page-gutter: 2rem;
    --inspector-width: 22rem;

    /* CORNERS, NAMED ONCE. There were ninety-two border-radius declarations
       across nine different values - 2, 3, 4, 5, 6px, 0.6rem, a 50%, three
       zeroes and one lopsided `0 0 0 4px` - which is not a design, it is
       whatever each rule's author reached for. Two now, and the split is by
       how big the thing is rather than by what it is: a 12px corner on a
       22px-tall chip is a lozenge, so small controls get half of it and
       stay rectangles with soft corners.

       Circles keep `50%`, which is not a radius but a shape. Three rules keep
       a square corner and each says why where it sits: the battle map's
       cells, which have to tile; the hit point fill, which is clipped by the
       rounded track around it; and the inside edges of a joined pair of
       buttons, where the corners belong to the group. */
    /* Rounder (2026-10-01: "more fun, more rounded"). */
    --radius: 18px;
    --radius-small: 10px;
    /* One screenful, below the header and above the gutter. The war room has
       always been exactly this tall; naming it is what lets the stacked
       layout hand the same measurement to one panel at a time. */
    --screen-height: calc(100vh - var(--header-height) - var(--page-gutter));

    /* Tell the browser this is a dark application, so the controls it draws
       itself - checkboxes, scrollbars, spinners, the default page ground -
       come in dark chrome instead of arriving as white squares in an unlit
       room. One line, and it reaches every native control in the app.

       Nine of the ten themes are dark and this is right for them. Vellum is
       not, so theme.py emits a `color-scheme` of its own in the block it
       writes into the page head, which lands after this file and wins. This
       line is what a page with no theme gets, which is Gaslight. */
    color-scheme: dark;

    /* soot — the room */
    --soot-900: #0c0c0c;
    --soot-800: #141414;
    --soot-700: #181818;
    --soot-600: #1b1b1b;
    --soot-500: #1e1e1e;

    /* hardwood — the furniture */
    --wood-900: #241d12;
    --wood-800: #2a2418;
    --wood-700: #4a3c22;
    --wood-600: #5c4a2a;
    --wood-500: #6b4d33;

    /* brass — the instrument */
    --brass-700: #79673f;
    --brass-600: #78684d;
    --brass-500: #958463;
    --brass-400: #b8873a;
    --brass-300: #c9a84c;
    --brass-200: #d4af37;

    /* parchment — the ink on it */
    --parchment: #d4c9a8;
    --parchment-bright: #e8dcc0;
    --parchment-pale: #f0e6cc;

    /* verdigris — oxidised copper */
    --verdigris-700: #2f5d4f;
    --verdigris-600: #3e7a68;
    --verdigris-500: #5f9e86;
    --verdigris-400: #7fc4a8;

    /* oxblood — the horror half */
    --oxblood-700: #6b3a2a;
    --oxblood-600: #8a3b3b;
    --oxblood-500: #9e3a3a;
    --oxblood-400: #c96b5a;

    /* What the four families MEAN, so a rule reads as its purpose rather than
       as a color. A faction is one color everywhere it appears. */
    /* The four sides, the way a table already reads them: party blue, ally
       green, neutral yellow, enemy red - muted to sit in the palette.

       These are DEFAULTS, and the distinction is the point. A theme is
       allowed to recolor a faction and wins when it does: each name below
       reads `--theme-faction-*` first and falls back to the default only
       while no theme has set one. So a theme block sets the four it cares
       about, no rule further down this file changes, and a theme that sets
       none still gets these. Blue-green-yellow-red is the table's habit, not
       a law - a theme owes only four things a person can tell apart across a
       table, not these four hues. */
    --faction-party:   var(--theme-faction-party,   #4f7fb3);
    --faction-ally:    var(--theme-faction-ally,    var(--verdigris-500));
    --faction-neutral: var(--theme-faction-neutral, var(--brass-300));
    --faction-enemy:   var(--theme-faction-enemy,   var(--oxblood-500));

    /* Interface, not faction. A button is not a party member and a hit point
       bar is not an ally - they shared a hex, not a meaning. */
    --accent:        var(--verdigris-600);
    --accent-bright: var(--verdigris-500);
    --temp-hp:       var(--brass-400);
    /* Refusal, and a creature at nothing. Both read a theme slot first, for
       the same reason the four factions do and with one more wrinkle: the
       sixth slot of a theme is SIGNAL, not danger. Gaslight's signal happens
       to be oxblood, but Grimoire's is a violet and Sanctum's a blue, and a
       theme that recolors the oxblood family would otherwise make a refused
       action violet and a dying character pale blue. A theme emits
       `--theme-danger` as a red at its own register; one that genuinely wants
       violet refusal still can, but it has to say so. */
    --danger:        var(--theme-danger, var(--oxblood-500));

    --hp-good:     var(--verdigris-500);
    --hp-bloodied: var(--brass-300);
    --hp-down:     var(--theme-danger, var(--oxblood-500));
    /* One color per condition, so each of the fifteen reads on its own.
       Chosen to sit inside the palette's register - tarnished, never neon -
       and, where a condition has a color in the imagination, to be it:
       poison green, petrified stone, the yellow of a paralysing shock. */
    --cond-blinded:       #8a8f94;
    --cond-charmed:       #c46a8a;
    --cond-deafened:      #8f7fa8;
    --cond-exhausted:     #967b5b;
    --cond-frightened:    #8870bd;
    --cond-grappled:      #bd6a2c;
    --cond-incapacitated: #ce584b;
    --cond-invisible:     #7fb0c4;
    --cond-paralyzed:     #d4c33a;
    --cond-petrified:     #9a9a86;
    --cond-poisoned:      #6fa83a;
    --cond-prone:         #c9a06a;
    --cond-restrained:    #61809d;
    --cond-stunned:       #e39b2c;
    --cond-unconscious:   #7579c4;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    background-color: var(--soot-900);
    background-image: radial-gradient(ellipse at 50% -10%, var(--soot-900) 0%, var(--soot-900) 65%);
    background-attachment: fixed;
    color: var(--parchment);
    font-family: var(--font-body);
    padding: var(--header-height) var(--page-gutter) var(--page-gutter);
}

.site-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;
    background-color: var(--soot-900);
    border-bottom: 1px solid var(--line);
    padding: 0.75rem 2rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

/* The name in the top-left corner, and on the sign-in card.

   Its own class rather than .campaign-name, which the campaign shelf claims
   further down this file - a single-class rule later in the cascade, so the
   shelf was quietly repainting the header and the login card parchment. Two
   different things wanted the same name; the one that is Rollies' own name kept
   a name of its own.

   A link in the header and an <h1> on the card, styled the same either way,
   because it is the same word doing the same job. */
.site-id {
    font-family: var(--font-body);
    /* Bigger, on a second look at the header (2026-10-01): the mark is the
       brand, and it was the size of a menu item. */
    font-size: 1.9rem;
    color: var(--brass-300);
    letter-spacing: 0.12em;
    font-variant: small-caps;
    text-shadow: 0 0 12px rgba(201, 168, 76, 0.25);
    text-decoration: none;
}

a.site-id:hover { color: var(--brass-200); }

/* TWO TYPEFACES, served from here (2026-10-01). A cold look at the war room
   found "too many different fonts", and it was worse than it looked: Cinzel
   was named in this file and never loaded, so every title fell back to
   whatever serif the machine had. Now there are two, both in static/fonts
   under the SIL Open Font License, so a table with no internet sees the same
   page: Cinzel for titles and buttons - capitals, the book's voice - and
   Source Serif 4 for everything else, numbers included, in its tabular
   figures. The name in the header is not type at all; see below. */
@font-face { font-family: "Cinzel"; src: url("fonts/cinzel-latin-400-normal.woff2") format("woff2"); font-weight: normal; font-display: swap; }
@font-face { font-family: "Cinzel"; src: url("fonts/cinzel-latin-600-normal.woff2") format("woff2"); font-weight: 600; font-display: swap; }
@font-face { font-family: "Source Serif 4"; src: url("fonts/source-serif-4-latin-400-normal.woff2") format("woff2"); font-weight: normal; font-display: swap; }
@font-face { font-family: "Source Serif 4"; src: url("fonts/source-serif-4-latin-600-normal.woff2") format("woff2"); font-weight: 600; font-display: swap; }

/* THE MARK AND THE NAME. Six seats around a spark, then "Rollies" in Aidan's
   own hand (2026-10-08): drawn in Notes with the ribbon pen, vectorized to a
   single path, one color - so every theme can paint it. The diner-sign
   script it replaces (Yellowtail, a font shipped in static/fonts) was
   somebody else's letters, and a brand should be its owner's. Both parts
   are the link's own color, so a theme recolors the brand without knowing
   it is there: each is a mask over a block painted `currentColor` rather
   than a picture, because a picture would stay black on every palette. A
   mask rather than the path inlined in base.html because the file is then
   fetched and cached once, where the path is 19 KB on every page.

   The name's height matches the script it replaces. Yellowtail's "Rollies"
   stood 0.73em from baseline to ascender at 1.35em of the link's size, and
   the drawing's capital stands about 0.82 of its box, so a box of 1.2em
   lands the R where it was. The width follows from the drawing's own
   proportions (2149 by 1055 in the file). The max-width is for a phone: the
   mask's `contain` shrinks the word inside its box rather than pushing the
   menu off the bar. */
/* 0.2em, not 0.4 (2026-10-22): the drawn wordmark carries air of its own on
   the left, and the mark sat a word's width from the word (Aidan, after
   session 31). */
.site-id { display: inline-flex; align-items: center; gap: 0.2em; }

.site-mark {
    width: 1.45em;
    height: 1.45em;
    flex: 0 0 auto;
    color: inherit;
    /* The rekindle after a lost connection (.is-offline, below). */
    transition: color 0.9s ease;
}
/* Inline SVG (templates/_site_mark.html), not a mask: a mask paints the
   whole drawing at once, and Spark has to move apart from the table. */
.site-mark svg { display: block; width: 100%; height: 100%; fill: currentColor; overflow: visible; }
/* Both paths turn and scale about the drawing's center, (256, 256) in the
   viewBox, and the keyframes carry that center themselves: translate there,
   turn, translate back. Not transform-box: fill-box with transform-origin:
   center, which Chrome honors and WebKit does not - in Safari and the
   DuckDuckGo browser the table turned about the drawing's corner and swung
   out of the header (Aidan, 2026-10-23). */
.site-mark .mark-table, .site-mark .mark-spark { transform-box: view-box; transform-origin: 0 0; }
/* Spark thinks: the table turns slowly and Spark breathes, from the press
   that asks until the answer's page arrives (base.html adds .is-thinking on
   the question form's submit). Under reduced motion Spark only dims, the
   color-only rule the whole scale keeps. One motion at a time: a natural 20
   landing mid-question wins, and the thinking stops. */
.site-mark.is-thinking .mark-spark { opacity: 0.55; }

.rollies {
    height: 1.2em;
    aspect-ratio: 2149 / 1055;
    max-width: 38vw;
    flex: 0 0 auto;
    background-color: currentColor;
    -webkit-mask: url("rollies.svg") center / contain no-repeat;
    mask: url("rollies.svg") center / contain no-repeat;
}

.site-id .version-badge { align-self: flex-start; margin-left: 0; }

/* THE MARK ROLLS. One turn under the pointer, settling past true and back
   the way a die does. The burst below is the light a leap throws (the
   party's twenty; SPARK MOVES, further down), in the link's own color like
   the mark itself, so every theme cheers in its own palette. Only for
   someone who has not asked their system for less motion - the atlas's
   pattern - and the page's own reduced-motion rule covers anything that
   slips past. */
.site-id { position: relative; }
@media (prefers-reduced-motion: no-preference) {
    .site-id:hover .site-mark { animation: mark-roll 0.7s cubic-bezier(0.34, 1.56, 0.64, 1); }
    .site-burst {
        position: absolute;
        left: 0;
        top: 50%;
        width: 1.45em;
        height: 1.45em;
        margin-top: -0.725em;
        border-radius: 50%;
        background: radial-gradient(circle, currentColor 0%, transparent 70%);
        pointer-events: none;
        animation: mark-burst 1.3s ease-out forwards;
    }
}
@keyframes mark-roll {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}
@keyframes mark-burst {
    0% { transform: scale(0.4); opacity: 0.9; }
    100% { transform: scale(5); opacity: 0; }
}
@media (prefers-reduced-motion: no-preference) {
    .site-mark.is-thinking .mark-spark { opacity: 1; animation: spark-breathe 1.4s ease-in-out infinite; }
    .site-mark.is-thinking .mark-table { animation: mark-turn 4s linear infinite; }
    .site-mark.is-thinking.is-leap .mark-spark, .site-mark.is-thinking.is-leap .mark-table { animation: none; }
}
@keyframes mark-turn {
    from { transform: translate(256px, 256px) rotate(0deg) translate(-256px, -256px); }
    to { transform: translate(256px, 256px) rotate(360deg) translate(-256px, -256px); }
}
@keyframes spark-breathe {
    0%, 100% { transform: translate(256px, 256px) scale(0.8) translate(-256px, -256px); opacity: 0.55; }
    50% { transform: translate(256px, 256px) scale(1.3) translate(-256px, -256px); opacity: 1; }
}

.version-badge {
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--text-2xs);
    color: var(--brass-700);
    letter-spacing: 0.06em;
    font-variant: normal;
    text-shadow: none;
    vertical-align: super;
    margin-left: 0.4rem;
}

.header-center {
    font-size: var(--text-sm);
    color: var(--brass-500);
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.header-right {
    font-size: var(--text-sm);
    color: var(--brass-500);
    font-style: normal;
}

.character-card {
    background-color: var(--soot-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 1.6rem;
    position: relative;
}

.card-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 0.2rem;
}

.character-card h2 {
    font-size: var(--text-lg);
    color: var(--brass-300);
    font-variant: small-caps;
    letter-spacing: 0.06em;
}

.player-name {
    font-size: var(--text-sm);
    color: var(--brass-500);
    font-style: normal;
}

.subtitle {
    font-size: var(--text-sm);
    color: var(--brass-500);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    margin-bottom: 0.75rem;
    /* The class line and its controls on one row while they fit; the controls
       drop as a group when they do not, and everything shares one center
       line, so a button never sits half a step below the words beside it. */
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 0.6rem;
    row-gap: 0.3rem;
}
.subtitle-actions {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    white-space: nowrap;
    margin-left: auto;
}

/* The way into the rules panel, under the six scores it is made of. Spaced
   off them, because it is a door rather than a seventh number. */
.card-athletics { margin: 0.6rem 0 0.2rem; }

.ability-scores {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 0.4rem;
    margin-bottom: 0.75rem;
}

.ability {
    position: relative;
    text-align: center;
    background-color: var(--soot-900);
    border: 1px solid var(--soot-500);
    border-radius: var(--radius);
    padding: 0.35rem;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.ab-label {
    font-size: var(--text-2xs);
    color: var(--brass-500);
    text-transform: uppercase;
}

.ab-score {
    font-size: var(--text-md);
    color: var(--parchment);
}

.ab-mod {
    font-size: var(--text-xs);
    color: var(--brass-300);
}


.ac-label {
    font-size: var(--text-2xs);
    color: var(--brass-500);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}


.hp-temp {
    background-color: var(--temp-hp);
}

.hp-bar {
    height: 100%;
    /* Square on purpose, and one of only two in the file. This is the FILL,
       not the track: its width moves with the hit points, so a corner here
       would round whichever edge the damage happened to leave it at. The
       track around it is rounded and clips it. */
    border-radius: 0;
    transition: width 0.3s ease;
}

.hp-high { background-color: var(--hp-good); }
.hp-mid  { background-color: var(--brass-300); }
.hp-low  { background-color: var(--hp-down); }

.hp-values {
    font-size: var(--text-sm);
    color: var(--parchment);
    white-space: nowrap;
}

.temp-hp-value {
    color: var(--temp-hp);
}


.combat-controls {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.4rem;
    width: 100%;
    /* Boxed like the armor class below it: the two groups of controls on the
       card look like two groups. */
    padding: 0.4rem 0.5rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

.damage-form,
.temp-hp-form,
.conditions-form {
    display: flex;
    gap: 0.4rem;
    width: 100%;
    min-width: 0;
}

.damage-form input,
.temp-hp-form input {
    width: 0;
    flex: 1;
    min-width: 0;
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.3rem 0.5rem;
    font-size: var(--text-sm);
}

.damage-form button {
    background-color: transparent;
    /* Calm at rest (a cold look: "a lot of color"): the word carries the
       meaning, the border is everybody's, the color fills in on hover. */
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.3rem 0.6rem;
    cursor: pointer;
    font-size: var(--text-sm);
    white-space: nowrap;
    min-width: 52px;
    text-align: center;
}

.damage-form button { color: color-mix(in srgb, var(--faction-enemy) 55%, var(--parchment)); }
.damage-form button:hover { background-color: color-mix(in srgb, var(--faction-enemy) 18%, transparent); border-color: var(--faction-enemy); }



.temp-hp-form button {
    background-color: transparent;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.3rem 0.6rem;
    cursor: pointer;
    font-size: var(--text-sm);
    white-space: nowrap;
}

.temp-hp-form button:hover { background-color: color-mix(in srgb, var(--brass-300) 15%, transparent); }

.conditions-dropdown {
    width: 100%;
}

.conditions-toggle {
    display: block;
    width: 100%;
    background-color: var(--soot-500);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--brass-300);
    padding: 0.3rem 0.6rem;
    cursor: pointer;
    font-size: var(--text-sm);
    user-select: none;
    box-sizing: border-box;
}

.conditions-panel {
    display: none;
    position: absolute;
    z-index: 10;
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.5rem;
    margin-top: 0.25rem;
    /* The card's width and no wider: its rows ran past the card's edge and
       the column scrolled sideways (2026-10-01). */
    left: 0;
    right: 0;
    width: auto;
    box-sizing: border-box;
}

.conditions-panel.open {
    display: block;
}

.conditions-panel table {
    width: 100%;
    font-size: var(--text-sm);
    color: var(--parchment);
}

.conditions-panel td {
    padding: 0.15rem 0.2rem;
}
.conditions-panel table { width: 100%; table-layout: auto; }
.conditions-panel select { max-width: 5.2rem; }

.conditions-panel input[type="number"] {
    width: 2.6rem;
    background-color: var(--soot-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.2rem;
}

.conditions-panel select {
    background-color: var(--soot-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.2rem;
    font-size: var(--text-xs);
}

.conditions-panel button {
    margin-top: 0.4rem;
    background-color: var(--soot-500);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--brass-300);
    padding: 0.3rem 0.6rem;
    cursor: pointer;
    font-size: var(--text-sm);
}

.active-conditions {
    margin-top: 0.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}

.condition-badge {
    font-size: var(--text-sm);
    color: var(--danger);
    font-style: normal;
}


.note-delete-form button {
    background: none;
    border: none;
    color: var(--danger);
    cursor: pointer;
    font-size: var(--text-xs);
    padding: 0 0.2rem;
}

/* ============================================
   TURN INDICATOR — round/turn/current actor banner
   Sits above the initiative strip, states who's acting right now
   ============================================ */
.turn-indicator {
    padding: 0.6rem 1rem;
    background-color: var(--soot-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    text-align: center;
    font-size: var(--text-md);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--brass-300);
}

/* ============================================
   INITIATIVE STRIP — the live combat order, one chip per combatant
   ============================================ */
/* One row that scrolls sideways, not a block that wraps downwards.
   Wrapping cost a band of vertical space per extra combatant - eight of them
   made two rows of tall chips and pushed the battle map, which is the middle
   of the dashboard and the thing everyone is looking at, off the bottom of the
   screen. Sideways is the axis there is spare room on. */
.initiative-strip {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 0.4rem;
    padding-bottom: 0.3rem;
    align-items: stretch;
}

/* A chip must not be squeezed into illegibility by the one next to it; past
   the width of the strip they scroll instead. */
.initiative-strip > * { flex: 0 0 auto; }

.initiative-entry {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.2rem;
    width: 4.6rem;
    background-color: var(--soot-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.35rem 0.3rem;
}

/* THE CARD. Two faces on one piece of card, rotated about its own vertical
   axis - which is why both sides are in the DOM at once and neither is toggled
   away. Hiding one and showing the other would work and would jump; this turns
   over, and turning over is what tells you it is the same combatant.

   preserve-3d on the container, backface-visibility on the faces: without the
   second, the far side shows through the near one, mirrored. */
.chip-card {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 5;
    transform-style: preserve-3d;
    transition: transform 0.35s ease;
    cursor: pointer;
}

.initiative-entry.is-flipped .chip-card { transform: rotateY(180deg); }

.chip-face {
    position: absolute;
    inset: 0;
    border-radius: var(--radius);
    overflow: hidden;
    border: 2px solid var(--line);
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

.chip-front { background-color: var(--soot-600); }

.chip-front img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Portraits are framed for a card, not a thumbnail, so a plain center crop
       takes the chin and leaves the forehead. Bias upward to the face. */
    object-position: 50% 20%;
}


/* The wound. Deep red, laid over the portrait and driven by hit points, so
   how hurt somebody is reads across a room without anyone reading a number. */
.chip-wound {
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at 50% 40%,
                rgba(150, 20, 20, 0.35), rgba(120, 8, 8, 0.85));
    pointer-events: none;
}

/* The number, on its own line above the card - see the template for why it
   is not on the picture. An input, because SET ALL still writes to it, but
   drawn as the number it is rather than as a form field: it grows a box only
   when you are actually typing in it. */
.chip-init {
    position: absolute;
    left: 0.15rem;
    bottom: 0.15rem;
    z-index: 3;
    width: 2rem;
    padding: 0.05rem 0.1rem;
    background-color: rgba(8, 9, 12, 0.72);
    border: 1px solid transparent;
    border-radius: var(--radius-small);
    color: var(--parchment-pale);
    font-size: var(--text-md);
    font-weight: 600;
    text-align: center;
    text-shadow: 0 1px 2px var(--soot-900);
}

.chip-init:hover { border-color: var(--brass-600); }
.chip-init:focus {
    border-color: var(--brass-300);
    background-color: var(--soot-900);
    outline: none;
}

.chip-init:disabled { opacity: 0.85; }

/* The back: the numbers you want on somebody's turn and not before. */
.chip-back {
    transform: rotateY(180deg);
    background-color: var(--soot-800);
    flex-direction: column;
    justify-content: center;
    gap: 0.15rem;
    padding: 0.25rem;
    text-align: center;
}

.chip-back-hp {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--hp-good);
    line-height: 1;
}

.chip-back-hp.is-bloodied { color: var(--brass-300); }
.chip-back-hp.is-down { color: var(--hp-down); }
.chip-back-untracked { color: var(--brass-600); font-weight: normal; }
.chip-back-slash { color: var(--brass-600); margin: 0 0.05rem; }

.chip-back-track {
    width: 80%;
    height: 3px;
    border-radius: var(--radius-small);
    background-color: var(--soot-500);
    overflow: hidden;
}

.chip-back-fill { display: block; height: 100%; background-color: var(--hp-good); }

.chip-back-ac {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    color: var(--brass-500);
}

.chip-back-conditions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.1rem;
    justify-content: center;
}

.chip-back-condition {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    padding: 0.05rem 0.15rem;
    border: 1px solid var(--oxblood-700);
    border-radius: var(--radius-small);
    color: var(--oxblood-400);
}

/* The same colors the map tokens use, so a face in the strip and a token on
   the board are recognisably the same combatant. */
.faction-party   .chip-face { border-color: var(--faction-party); }
.faction-ally    .chip-face { border-color: var(--faction-ally); }
.faction-neutral .chip-face { border-color: var(--faction-neutral); }
.faction-enemy   .chip-face { border-color: var(--faction-enemy); }

.initiative-entry.active-turn .chip-face {
    border-color: var(--brass-300);
    box-shadow: 0 0 0 2px rgba(201, 168, 76, 0.35);
}

/* Removal is a rare, destructive click, so it lives on the back - reachable
   when you have deliberately turned a card over, not while scanning faces. */
.initiative-remove-btn {
    position: absolute;
    top: 0;
    right: 0;
    padding: 0 0.25rem;
    font-size: var(--text-xs);
    line-height: 1.2;
    border: 0;
    border-radius: 0 0 0 var(--radius);
    background-color: rgba(20, 10, 10, 0.85);
    color: var(--danger);
    cursor: pointer;
}

/* The stat block on an enemy chip is the tall part, and it is only wanted for
   the combatant being acted on. It opens on hover or on keyboard focus, so
   nothing is lost and nothing is permanently in the way. The chip whose turn
   it is keeps it open, because that is the one about to be used. */
.initiative-entry .chip-stats {
    display: none;
}

.initiative-entry:hover .chip-stats,
.initiative-entry:focus-within .chip-stats,
.initiative-entry.active-turn .chip-stats {
    display: flex;
}

/* An open stat block must not shove the chips beside it sideways, or the strip
   reflows every time the mouse crosses it. It lifts out over the board. */
.initiative-entry:hover,
.initiative-entry:focus-within,
.initiative-entry.active-turn {
    position: relative;
    z-index: 3;
}

.initiative-entry .chip-stats {
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    min-width: 11rem;
    margin-top: 0.2rem;
    padding: 0.4rem;
    background-color: var(--soot-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

/* The stat block on an enemy chip is the tall part, and it is only wanted for
   the combatant being acted on. It opens on hover or on keyboard focus, so
   nothing is lost and nothing is permanently in the way. The chip whose turn
   it is keeps it open, because that is the one about to be used. */
.initiative-entry .chip-stats {
    display: none;
}

.initiative-entry:hover .chip-stats,
.initiative-entry:focus-within .chip-stats,
.initiative-entry.active-turn .chip-stats {
    display: flex;
}

/* whoever's currently acting gets a warm gold highlight */
.initiative-entry.active-turn {
    border-color: var(--brass-300);
    background-color: var(--wood-800);
}



/* ============================================
   INITIATIVE CONTROLS — Start Combat / Add enemy / Next Turn row
   ============================================ */

.initiative-add-form {
    display: flex;
    gap: 0.4rem;
    flex: 1;
    flex-wrap: wrap;
    /* Four fields plus a button cannot honestly fit a phone in one line; below
       this width the form wraps instead of crushing every input to nothing. */
    min-width: 16rem;
}

.initiative-add-form input {
    flex: 0 1 7rem;
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.4rem 0.6rem;
    font-size: var(--text-sm);
}

.initiative-add-form button {
    background-color: var(--accent);
    border: none;
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.4rem 0.8rem;
    cursor: pointer;
    font-size: var(--text-md);
    font-weight: 600;
}

/* ============================================
   PREP CONTROLS — the "add enemy to a future combat" bar
   ============================================ */
.prep-controls {
    max-width: 1400px;
    margin: 0 auto 0.5rem;
}

.prep-add-form {
    display: flex;
    gap: 0.4rem;
}

.prep-add-form input {
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.4rem 0.6rem;
    font-size: var(--text-sm);
}

.prep-add-form input[name="label"] { flex: 2; }
.prep-add-form input[name="name"] { flex: 1; }
.prep-add-form input[name="initiative"] { width: 8rem; }

.prep-add-form button {
    background-color: var(--soot-500);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--brass-300);
    padding: 0.4rem 0.8rem;
    cursor: pointer;
    font-size: var(--text-sm);
    white-space: nowrap;
}

/* ============================================
   PREP LIST — saved encounter rosters, each with a LOAD button
   ============================================ */
.prep-list {
    max-width: 1400px;
    margin: 0 auto 1.5rem;
}

.prep-entry {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0;
    border-bottom: 1px solid var(--soot-500);
    font-size: var(--text-sm);
    color: var(--parchment);
}

.prep-entry strong {
    color: var(--brass-300);
}

.prep-load-form button {
    background-color: var(--soot-500);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--brass-300);
    padding: 0.2rem 0.6rem;
    cursor: pointer;
    font-size: var(--text-xs);
    margin-left: auto;
}


.initiative-entry input[type="number"] {
    width: 2.8rem;
    font-weight: 600;
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.2rem 0.3rem;
    font-size: var(--text-sm);
    text-align: center;
}

.initiative-remove-btn {
    background: none;
    border: none;
    color: var(--danger);
    cursor: pointer;
    font-size: var(--text-sm);
    padding: 0 0.2rem;
}

.initiative-setall-btn {
    background-color: var(--soot-500);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--brass-300);
    padding: 0.4rem 0.8rem;
    cursor: pointer;
    font-size: var(--text-sm);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* visible number-input spinner arrows (WebKit/Safari only) */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
    opacity: 1;
    background-color: var(--soot-500);
    border-left: 1px solid var(--line);
    cursor: pointer;
}

.prep-add-form textarea {
    flex: 1;
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 0.4rem 0.6rem;
    font-size: var(--text-sm);
    font-family: inherit;
    resize: vertical;
}

/* ============================================
   WAR ROOM — the four-panel grid: top strip, left/center/right below
   ============================================ */
.war-room {
    display: grid;
    /* The middle column is the battle map, and it is the thing everyone is
       looking at, so the two side panels are given what they need rather than
       what they would take. Every pixel taken off them is a pixel of board. */
    grid-template-columns: 300px 1fr 300px;
    grid-template-rows: minmax(0, 1fr);
    grid-template-areas:
        "left   center right";
    gap: 1.5rem;
    max-width: 1800px;
    margin: 0 auto;
    /* Less whatever sits above it - a one-line strip, a flash - measured by
       the script after the opening tag. The height assumed the war room began
       under the header, so every strip pushed the action bar off the screen. */
    height: calc(var(--screen-height) - var(--above-war-room, 0px));
    align-items: start;
}

/* Half a monitor: the three columns become one, and the board goes first.

   Two 300px side panels and two 1.5rem gaps are 648px that CANNOT shrink,
   because a fixed grid column does not. What is left over is the board's, and
   what is left over at 900px - which is the width Rollies is actually for,
   sitting beside Discord - measured 184 pixels. The thing everyone at the
   table is looking at was the narrowest of the three columns, and the two
   panels flanking it were at their full desktop width.

   1250px, and the number came from measuring rather than from the arithmetic
   above, because the arithmetic gives the wrong answer. The board does not run
   out of room when the columns overlap; it runs out well before, and for a
   reason the column widths do not show: as the center column narrows, the
   initiative strip and the action bar WRAP, and every row they gain is a row
   taken off the board. At 1101px the two of them were 534px of a 788px column
   and the board was left with its 160px floor.

   Visible board height in the three-column layout, one campaign mid-fight:

       1700px - 347    1400px - 280    1200px - 178
       1500px - 312    1300px - 280    1150px - 160

   Stacked, the same board gets a flat 299px at every one of those widths,
   because a full-width column does not wrap its own chrome. So below about
   1250 the stacked layout is simply the better one, and above it the three
   columns are worth keeping for what they show AT ONCE - the roster, the board
   and the log together, which 19 pixels of board does not buy back.

   This is deliberately not the rail's 1100px further down. That number is the
   width at which a fixed rail and a fixed grid collide; this one is the width
   at which the board stops being readable. Two constraints, two numbers, and
   the file says which is which.

   The board leads the stack. The old rule at 820px put it LAST, on the
   argument that a narrow screen is the exception and the map is the least of
   it. The map is the heart of the page now, and a tablet is a case this
   release is designed for rather than an exception, so there is one order and
   the board leads it.

   The heights are the half that is easy to get wrong. On the desktop grid every
   panel is bounded by the war room's own height and scrolls inside itself; let
   that go and the combat log - which is two thousand events in a campaign that
   has been played - sets the height of the page instead. An earlier attempt at
   exactly this made the war room 26,000 pixels tall. So each stacked region
   that owns a scroll region is given a screenful of its own, and the page
   scrolls by two of them rather than by two hundred. */
/* THE ROOM'S WIDTH, NOT THE WINDOW'S (2026-10-27). Opening the rail takes
   352px plus its gap off the war room without changing the window, so a 1500px
   window with the rail open kept three columns in 1150px and squeezed the
   board into a tall, narrow strip (Aidan's screenshot, notes and Renku's sheet
   up). The page's own width - the body's content box, which the rail's
   padding already excludes - is what decides now. 1186px is the old 1250px
   window less the two 2rem gutters, so with the rail shut nothing moves. */
body:has(.war-room) { container: page / inline-size; }

/* The jump bar: nothing while the room is three columns, frozen under the
   header while it is stacked (2026-10-27). Its top is the header's measured
   height (--jump-top), never 0 - the sticky-header trap. */
.room-jump { display: none; }
@container page (max-width: 1186px) {
    .room-jump {
        display: flex; gap: 0.35rem; flex-wrap: wrap; justify-content: center;
        position: sticky; top: var(--jump-top, var(--header-height)); z-index: 50;
        margin: 0 calc(-1 * var(--page-gutter)) 0.75rem; padding: 0.4rem var(--page-gutter);
        background: var(--soot-900); border-bottom: 1px solid var(--line);
    }
    .room-jump-link {
        padding: 0.2rem 0.75rem; border: 1px solid var(--line); border-radius: 999px;
        font-size: var(--text-xs); letter-spacing: 0.1em; text-transform: uppercase;
        color: var(--brass-400); text-decoration: none;
    }
    .room-jump-link:hover, .room-jump-link:focus-visible { color: var(--brass-300); border-color: var(--brass-500); }
    body #the-table, body #open-card, body #room-roster, body #room-log, body #room-notes, body #room-inventory {
        scroll-margin-top: calc(var(--jump-top, var(--header-height)) + 3rem);
    }

    .war-room {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: auto;
        grid-template-areas:
            "center"
            "left"
            "right";
        gap: 1rem;
        /* The page owns the scrolling now; each panel below still owns its
           own, which is why this is safe. */
        height: auto;
    }

    /* A screenful minus a hair, so the heading of the panel underneath is
       visible at the bottom of the first screen. A board that fills the
       viewport exactly is a board that looks like the whole page. */
    .panel-center {
        align-self: auto;
        height: calc(var(--screen-height) - 3rem);
    }

    /* Three cards in equal thirds, each scrolling its own body - the same
       shape as the desktop column, given the height it needs to keep it. */
    .panel-right {
        align-self: auto;
        height: var(--screen-height);
    }

    /* The left panel is the exception, and deliberately: a roster is six rows
       and a sheet is a page. Neither grows without bound, so it is as tall as
       it is and nothing inside it needs to scroll. */
    .panel-left {
        align-self: auto;
        height: auto;
    }
}

/* Only the dialogs live here now; they position themselves when open and
   take no cell of the grid when closed. */
.panel-top { display: contents; }
.panel-left { grid-area: left; }
/* The board's column stretches like the two beside it. It did not, so it was
   only ever as tall as its content - which is why the action bar could end
   forty pixels above the inventory and the last character card with nothing
   under it. `fitBoard` measures this column to decide what the board and the
   bar may have, and a column that is the size of its contents can only ever
   tell it what it already knew. */
.panel-center {
    grid-area: center;
    align-self: stretch;
    display: flex;
    flex-direction: column;
    min-height: 0;
}
.panel-right { grid-area: right; }

/* placeholder for panels with no real content yet (battle map, notes/inventory) */
.panel-placeholder {
    background-color: var(--soot-800);
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    padding: 2rem;
    text-align: center;
    color: var(--brass-500);
    font-style: normal;
    font-size: var(--text-md);
    min-height: 200px;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* faction accent stripe — left border only, doesn't compete with active-turn's full gold border */
.initiative-entry.faction-party   { border-left: 4px solid var(--faction-party); }
.initiative-entry.faction-ally    { border-left: 4px solid var(--faction-ally); }
.initiative-entry.faction-neutral { border-left: 4px solid var(--brass-500); }
.initiative-entry.faction-enemy   { border-left: 4px solid var(--faction-enemy); }

/* ============================================
   PORTRAITS — fixed-size frame on each character card
   ============================================ */


.portrait-placeholder {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-2xl);
    color: var(--wood-700);
    font-family: var(--font-body);
    background: var(--soot-700);
}

/* ---- the blank face --------------------------------------------------------
   A head and shoulders in the side's color, wherever a portrait would be and
   none has been chosen. On a card it is the whole placeholder; on a chip and
   a token it sits behind the initials, which stay - they are what tells
   Kobold 2 from Cultist 3 across the table. */
.silhouette { display: block; fill: currentColor; }
.silhouette.faction-party   { color: var(--faction-party); }
.silhouette.faction-ally    { color: var(--faction-ally); }
.silhouette.faction-neutral { color: var(--faction-neutral); }
.silhouette.faction-enemy   { color: var(--faction-enemy); }
.portrait-placeholder .silhouette { width: 78%; height: 78%; margin-top: 12%; opacity: 0.85; }
.chip-front .silhouette, .map-token .silhouette {
    position: absolute; left: 18%; right: 18%; top: 10%; bottom: 0;
    width: auto; height: auto; opacity: 0.55;
}
.map-token-initial { position: relative; z-index: 1; }

.portrait-upload-form {
    display: flex;
    gap: 0.4rem;
    margin-bottom: 0.75rem;
}

.portrait-upload-form input[type="file"] {
    flex: 1;
    font-size: var(--text-xs);
    color: var(--brass-500);
}

.portrait-upload-form button {
    background-color: var(--soot-500);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--brass-300);
    padding: 0.3rem 0.6rem;
    cursor: pointer;
    font-size: var(--text-xs);
}
/* ============================================
   LEFT PANEL — compact roster (auto) + detail pane (remainder)
   ============================================ */
.panel-left {
    align-self: stretch;
    display: grid;
    /* minmax(0, 1fr) rather than the `auto` a grid gives you for free. An auto
       column is allowed to be as wide as its content's minimum, which means one
       widget with a wide minimum - a text input, say, whose intrinsic size is
       twenty characters - silently makes the whole panel wider than the column
       it was given, and the war room grows a sideways scrollbar nobody can
       trace back. The panel is 300px because the board wants the rest. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    gap: 1rem;
}

/* --- compact roster --- */
.party-roster {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.roster-row-form {
    display: block;
}

.roster-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    height: 52px;
    padding: 0 0.6rem;
    background-color: var(--soot-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.roster-row:hover {
    border-color: var(--brass-500);
}

.roster-selected {
    border-color: var(--brass-300);
}

.roster-portrait {
    width: 36px;
    height: 36px;
    flex-shrink: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    background-color: var(--soot-900);
    display: block;
}

.roster-portrait img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.roster-portrait .portrait-placeholder {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-lg);
    color: var(--wood-700);
}

.roster-name {
    flex: 0 0 88px;
    font-size: var(--text-sm);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.roster-hp-track {
    flex: 1;
    display: flex;
    height: 8px;
    background-color: var(--soot-900);
    border-radius: var(--radius);
    overflow: hidden;
}

.roster-hp-track .hp-bar {
    display: block;
}

.roster-hp-values {
    font-size: var(--text-sm);
    white-space: nowrap;
}

.roster-ac {
    flex-shrink: 0;
    font-size: var(--text-xs);
    color: var(--parchment);
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 2px 6px;
    white-space: nowrap;
}

/* --- detail pane --- */
.detail-card {
    height: auto;
}

/* The open card takes whatever the column has left over, so the column ends
   where the two beside it end.

   The party is a scrolling list of cards in a box as tall as the war room,
   and a party that does not fill it left the bottom of the column empty while
   the action card and the inventory ran on another two hundred pixels - which
   reads as the column being cut off rather than as the list being short.

   The slack goes to the OPEN card rather than being shared out. Spread across
   the gaps it would be forty pixels between every row at a tall window, which
   disconnects the list; spread across the rows it doubles their height and
   they are meant to be compact. The open card is the one thing on this column
   somebody is reading, it is the one with sections worth separating, and it
   is the only one whose size is already exceptional. When the list is long
   enough to scroll there is no free space and none of this applies. */
.party-roster > .detail-card {
    /* The card's BOX takes the leftover height so the column ends where the
       two beside it end - but the card's CONTENTS stay stacked at the top.
       Spreading them was the first attempt and it was wrong: `space-between`
       over a short party put forty pixels between every section, so a sheet
       somebody reads at a glance became a sheet they scan. The whitespace
       collects once, under the last row, where it reads as room left over
       rather than as a card that has come apart. */
    flex: 1 1 auto;
}

/* The bars on the open card, which are the compressed row's bars: the card
   stands in that row's place, so it says what the row said. */
.card-vitals {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    margin-bottom: 0.5rem;
}

/* The open card, in the list, keeping the place the compressed row held. A
   left edge in brass rather than a box of its own: the rows around it already
   carry borders, and a second frame inside the first reads as two things. */
.party-roster .detail-card {
    margin: 0.15rem 0;
    border-left: 2px solid var(--brass-400);
    scroll-margin: 0.5rem;
}

/* ============================================
   STEAMPUNK HORROR THEME PASS — brass frames, interior gloom, lamplight
   Layered on top of the base rules above; keep these last so they win.
   ============================================ */

/* Victorian panel framing: brass hairline inside the border + shadowed interior */
.character-card,
.turn-indicator,
.panel-placeholder,
.roster-row,
.conditions-panel {
    box-shadow:
        inset 0 0 0 1px rgba(201, 168, 76, 0.07),
        inset 0 0 18px rgba(0, 0, 0, 0.55);
}

/* selected roster row glows like a lit lamp, not just a border change */
.roster-selected {
    background-color: var(--wood-900);
    box-shadow:
        0 0 10px rgba(201, 168, 76, 0.22),
        inset 0 0 0 1px rgba(201, 168, 76, 0.18),
        inset 0 0 14px rgba(0, 0, 0, 0.45);
}

/* active turn chip gets the same lamplight treatment */
.initiative-entry.active-turn {
    box-shadow:
        0 0 8px rgba(201, 168, 76, 0.2),
        inset 0 0 12px rgba(0, 0, 0, 0.4);
}

/* section-voice: turn indicator reads like an engraved plate */
.turn-indicator {
    font-variant: small-caps;
    letter-spacing: 0.12em;
    text-shadow: 0 0 10px rgba(201, 168, 76, 0.18);
}

/* roster names in small caps to match the reference's character list */
.roster-name {
    font-variant: small-caps;
    letter-spacing: 0.06em;
    font-size: var(--text-md);
}

/* The column's scrollbar: dark channel, brass thumb (WebKit/Safari/Chrome).
   It used to dress the detail pane, which was the only box in this column
   that scrolled. The card is a row in the roster now, so the roster scrolls
   scrolls and the roster is what these dress - which it should have had
   anyway: it has carried `scrollbar-width: thin` for Firefox all along, with
   nothing beside it for the browser the table actually uses. */
.party-roster::-webkit-scrollbar {
    width: 8px;
}
.party-roster::-webkit-scrollbar-track {
    background: var(--soot-900);
}
.party-roster::-webkit-scrollbar-thumb {
    background: var(--wood-700);
    border-radius: var(--radius);
}
.party-roster::-webkit-scrollbar-thumb:hover {
    background: var(--brass-700);
}
/* ---- Right panel: tabs ----
   .war-room sets align-items: start, so a panel is only as tall as its content
   unless it opts out. .panel-left already does this with align-self: stretch
   plus minmax(0, 1fr); the right panel needs the same so the tab body can own
   the leftover height and scroll inside it rather than stretching the page. */
.panel-right {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    min-height: 0;
}


.tab-body {
    flex: 1 1 auto;
    overflow-y: auto;
    min-height: 0;
}

/* ---- Timeline ---- */
.turn-block {
    margin-bottom: 0.75rem;
    padding-left: 0.5rem;
    border-left: 3px solid var(--wood-800);
}
/* The side that took the turn, in the board's colors. */
.turn-block.side-party   { border-left-color: var(--faction-party); }
.turn-block.side-ally    { border-left-color: var(--faction-ally); }
.turn-block.side-neutral { border-left-color: var(--faction-neutral); }
.turn-block.side-enemy   { border-left-color: var(--faction-enemy); }
.turn-block .turn-line { padding-left: 0.6rem; }
.turn-time { margin-left: 0.3rem; color: var(--brass-600); }

/* The newest block is marked by the template rather than matched with
   :first-child, because a combat divider can now sit in that slot and the
   highlight would silently move to nothing. */
.turn-block.is-latest { background: color-mix(in srgb, var(--brass-300) 6%, transparent); }

/* Blocks read newest-first, so consecutive fights would otherwise run together
   as one long scramble of rounds. This is the seam between them. */
.combat-divider {
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brass-300);
    border-top: 1px solid var(--line);
    padding-top: 0.4rem;
    margin: 1rem 0 0.5rem;
}

.combat-divider:first-child {
    border-top: none;
    padding-top: 0;
    margin-top: 0;
}

.turn-heading {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.5rem;
    margin-bottom: 0.25rem;
}

.turn-where {
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--text-2xs);
    color: var(--brass-700);
    letter-spacing: 0.06em;
    white-space: nowrap;
}

.turn-actor {
    font-family: var(--font-body);
    font-size: var(--text-xs);
    color: var(--brass-300);
    font-variant: small-caps;
    letter-spacing: 0.06em;
    text-align: right;
}

.turn-line {
    font-size: var(--text-xs);
    color: var(--brass-500);
    line-height: 1.45;
    padding: 0.1rem 0;
}

/* ---- Notes tab ---- */
.notes-for {
    font-family: var(--font-body);
    font-size: var(--text-xs);
    color: var(--brass-700);
    font-variant: small-caps;
    letter-spacing: 0.12em;
    margin-bottom: 0.4rem;
}

.note-add-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.3rem;
    margin-bottom: 0.6rem;
}

.note-add-form input[type="text"] {
    flex: 1 1 9rem;
    min-width: 0;
    background-color: var(--soot-700);
    border: 1px solid var(--wood-800); border-radius: var(--radius-small);
    color: var(--parchment);
    font-size: var(--text-xs);
    padding: 0.3rem 0.4rem;
}

.note-entry {
    display: flex;
    align-items: flex-start;
    gap: 0.4rem;
    padding: 0.25rem 0;
    border-bottom: 1px solid var(--wood-900);
}

.note-text {
    flex: 1;
    font-size: var(--text-xs);
    color: var(--brass-500);
    line-height: 1.4;
}

.note-delete-form button {
    background: none;
    border: none;
    color: var(--wood-700);
    cursor: pointer;
    font-size: var(--text-xs);
    padding: 0 0.2rem;
}

.note-delete-form button:hover { color: var(--oxblood-600); }
/* Who, when, and whether the table may read it - above the line, small.
   A private note is dimmer and marked with the word, never hue alone. */
.note-body { flex: 1; display: flex; flex-direction: column; gap: 0.05rem; min-width: 0; }
.note-by { font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-700); }
.note-entry.is-dm .note-by { color: var(--brass-500); }
.note-entry.is-private .note-text { color: var(--brass-600); }
.note-entry.is-private { border-left: 2px solid var(--wood-800); padding-left: 0.4rem; }
.note-reach { display: inline-flex; align-items: center; gap: 0.2rem; font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-600); white-space: nowrap; }
.note-reach input { margin: 0; accent-color: var(--brass-400); }
/* The same notes under a page of lore, at reading size. */
.lore-notes { display: grid; gap: 0.4rem; }
.lore-notes .note-entry { padding: 0.35rem 0; }
.lore-notes .note-text { font-size: var(--text-sm); color: var(--parchment); }
.lore-notes .note-add-form input[type="text"] { font-size: var(--text-sm); padding: 0.4rem 0.5rem; }
.lore-notes-title { margin: 0 0 0.2rem; font-size: var(--text-xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
.inspect-lore .note-entry { padding: 0.25rem 0; }

/* ---- Enemy HP on initiative chips ---- */


.add-hp-input {
    width: 52px;
}

/* ---- Chip layout: identity on top, stat block beneath ---- */
.initiative-entry {
    flex-direction: column;
    align-items: stretch;
    gap: 0.3rem;
}


.chip-stats {
    display: flex;
    align-items: center;
    gap: 0.35rem;
}


/* ---- Conditions on a chip ---- */


.chip-condition-badge {
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--text-2xs);
    color: var(--brass-400);
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    padding: 0.05rem 0.25rem;
    letter-spacing: 0.02em;
}


/* ============================================
   NARROW SCREENS — stack the war room into one column
   ============================================

   The desktop layout is a fixed three-column grid: 360px + 1fr + 320px, plus
   two 1.5rem gaps and 2rem of body padding on each side. That has a hard floor
   around 740px before the center column gets a single pixel, so on a 390px
   phone the page is roughly twice the width of the screen and you swipe
   sideways to find the half that is missing.

   That was true before the viewport meta too - it was just hidden, because the
   browser scaled the whole 740px down to fit and everything was legible only in
   the sense that it was on screen. Declaring the viewport did not cause this;
   it stopped the browser papering over it.

   Scoped to width, not `pointer: coarse`. The question here is genuinely "does
   the layout fit", which is about how many pixels there are - unlike the touch
   targets below, which are about what you are pointing WITH. A narrow desktop
   window should stack too.

   820px, because that is where 360 + 320 + gaps + padding stops leaving the
   center column a usable width - the breakpoint is derived from the layout
   rather than from a table of device sizes. */
@media (max-width: 820px) {
    body {
        /* 2rem each side is 10% of a phone screen spent on nothing. */
        padding: 4.5rem 0.75rem 2rem;
    }

    .site-header {
        padding: 0.6rem 0.75rem;
        gap: 0.5rem;
    }

    /* "Spirefall - Session 24" is the first thing to go: the campaign is not a
       fact you forget mid-fight, and the header has room for two items. */
    .header-center { display: none; }

    /* The sideways scroll is set up above, unconditionally. All a phone needs
       here is for the links to sit closer together so fewer of them are off the
       end of it to begin with. */
    .site-nav { gap: 0.6rem; }
    .site-utils { margin-left: 0.6rem; padding-left: 0.6rem; gap: 0.55rem; }

    .site-id { font-size: var(--text-lg); }
    .header-right { font-size: var(--text-xs); }

    /* The stacking itself happens at 1250px, up beside the war room's own
       rule, and the board leads it. What is left for a phone is what a phone
       actually needs: controls that are not three inputs wide. */
    .panel-left {
        display: flex;
        flex-direction: column;
        min-height: 0;
    }

    /* A phone is not a screenful of board and then a screenful of log - it is
       a narrow strip of everything, and thirds of a short viewport are three
       windows too small to read. The right panel goes back to its content's
       height here, with the cards' own bodies no longer scrolling inside it. */
    .panel-right {
        height: auto;
        min-height: 0;
        grid-template-rows: auto;
    }

    .side-card .tab-body { max-height: 60vh; }

    /* What used to be here: `.initiative-entry { flex-wrap: wrap; width: 100% }`,
       with a comment about a chip being "a name, HP, AC, conditions and two
       buttons ... a desktop row and a phone paragraph". That WAS true of the
       row this class named when the rule was written - a list of combatants
       down the left panel, with DMG and HEAL on the end of each one.

       The class means something else now. `.initiative-entry` is a card in the
       initiative strip, 4.6rem wide in a row that scrolls sideways, and a
       stale `width: 100%` made each card as wide as the strip - which, being a
       stretched column, made it as TALL as the strip too. On an iPad held
       upright the war room opened on one combatant's card, a thousand pixels
       of it, with two letters in the middle and the board somewhere below.

       Nothing replaces it. The strip has scrolled sideways at every width
       since the day it was built, for the reason its own comment gives: the
       axis with room to spare is the horizontal one. */

    /* Damage and heal side by side needs about 300px of form. Below that the
       inputs collapse to a few pixels each, so the pair stacks. */
    .combat-controls { grid-template-columns: 1fr; }

    /* Six ability scores across a phone gives each one about 55px including
       borders - two rows of three is readable, one row of six is not. */
    .ability-scores { grid-template-columns: repeat(3, 1fr); }

    .prep-controls,
    .prep-list { max-width: none; }
}


/* ---- Touch targets ----
   Everything above is sized for a mouse: the damage and heal buttons are about
   16px square, which a cursor hits precisely and a thumb does not. Apple's HIG
   asks for 44pt, Material for 48dp.

   Scoped to `pointer: coarse` rather than a width breakpoint, because the thing
   that matters is what you are pointing WITH, not how wide the screen is. A
   touchscreen laptop needs the big targets; a narrow desktop window does not.
   The desktop layout is untouched. */
@media (pointer: coarse) {
    .initiative-remove-btn {
        min-width: 44px;
        min-height: 44px;
        font-size: var(--text-md);
        padding: 0.5rem;
    }

    .initiative-entry input[type="number"] {
        min-height: 44px;
        width: 56px;
        font-size: var(--text-md);
    }

    .chip-stats {
        gap: 0.5rem;
        flex-wrap: wrap;
    }

    /* Two arrows that step a creature up and down a tower, on the tablet the
       board is drawn for. */
    .map-height-btn {
        min-width: 44px;
        min-height: 44px;
        font-size: var(--text-md);
    }


}

/* --- Sign-in ---------------------------------------------------------- */
/* The login page has no war room grid to sit in, so it opts out of the
   body padding the dashboard needs and centers one card instead. */
.login-page {
    padding: 2rem 1rem;
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
}

.login-card {
    width: 100%;
    max-width: 22rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background-color: rgba(13, 14, 19, 0.85);
    padding: 2rem 1.75rem;
    text-align: center;
}

.login-tagline {
    font-size: var(--text-sm);
    font-style: normal;
    color: var(--brass-700);
    margin: 0.4rem 0 1.75rem;
}

.login-form {
    display: flex;
    flex-direction: column;
    text-align: left;
}

.login-label {
    font-size: var(--text-xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brass-700);
    margin-bottom: 0.35rem;
}

.login-input {
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    color: var(--parchment);
    font-family: var(--font-body);
    font-size: var(--text-md);
    /* 44px of height. Below that a thumb misses on glass, and this is the one
       form in the app a player meets before they are invested. */
    padding: 0.7rem 0.6rem;
    margin-bottom: 1.1rem;
}

.login-input:focus {
    outline: none;
    border-color: var(--brass-300);
}

.login-button {
    background-color: var(--wood-700);
    border: 1px solid var(--brass-300);
    border-radius: var(--radius-small);
    color: var(--parchment);
    font-family: var(--font-body);
    font-variant: small-caps;
    letter-spacing: 0.12em;
    font-size: var(--text-md);
    padding: 0.7rem;
    cursor: pointer;
    margin-top: 0.25rem;
}

.login-button:hover {
    background-color: var(--wood-600);
}

.login-error {
    color: var(--oxblood-400);
    font-size: var(--text-sm);
    margin-bottom: 0.9rem;
}

.login-empty {
    text-align: left;
    font-size: var(--text-sm);
    line-height: 1.5;
}

.login-empty p {
    margin-bottom: 0.75rem;
}

.login-command {
    display: block;
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--text-xs);
    color: var(--brass-300);
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    padding: 0.6rem;
    margin-bottom: 0.9rem;
    overflow-x: auto;
}

.terms { text-align: left; font-size: var(--text-sm); line-height: 1.55; }
.terms-title { font-size: var(--text-md); margin: 0 0 0.6rem; }
.terms p { margin: 0 0 0.7rem; }
.login-note {
    color: var(--brass-700);
    font-size: var(--text-xs);
    font-style: normal;
}

/* The signed-in indicator in the war room header.

   Grouped with the adventurer count in one flex child rather than added as a
   fourth, because .site-header is space-between: a fourth child would push the
   centerd session label off center. */
.header-right-group {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    /* The header can never become two rows. It is position:fixed and the body's
       top padding is sized to one row of it, so a header that wraps hides the
       first thing on the page behind itself - the same trap the sticky tally
       fell into. Everything inside is nowrap, and the group scrolls sideways
       when the links outrun the width. Not in a media query: the links need
       about 1060px and Rollies is built to run beside Discord on half of one, so
       the squeeze starts well above any phone breakpoint. */
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
}

.header-right-group::-webkit-scrollbar { display: none; }

/* Everything in the group keeps its size and lets the scroll happen instead of
   squashing. Written as a child selector rather than a list of the three things
   that are in there today, because `header_right` takes whatever the page puts
   in it - and the first thing it was given, the war room's "5 adventurers",
   promptly shrank to "5 / adventurers" over two lines and sat on top of the
   session label at 900px. A page should not have to know this rule exists. */
.header-title { flex: 0 0 auto; }
.header-right-group > * { flex: 0 0 auto; }
.header-right { white-space: nowrap; }

.header-user {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    margin-left: 0.75rem;
}

.header-user-name {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    color: var(--brass-700);
    font-variant: small-caps;
    text-decoration: none;
}
.header-user-name:hover,
.header-user-name:focus-visible {
    text-decoration: underline;
    text-decoration-color: currentColor;
}
.password-form {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    max-width: 22rem;
}
.password-form .login-label {
    margin-top: 0.6rem;
}
.password-form .accounts-button {
    align-self: flex-start;
    margin-top: 0.9rem;
}

.logout-button {
    background: none;
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    color: var(--brass-700);
    font-family: var(--font-body);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    font-variant: small-caps;
    padding: 0.25rem 0.5rem;
    cursor: pointer;
}

.logout-button:hover {
    border-color: var(--brass-300);
    color: var(--brass-300);
}

.login-hint {
    font-size: var(--text-xs);
    color: var(--brass-700);
    margin: -0.7rem 0 1rem;
}

/* --- Accounts page ---------------------------------------------------- */
.header-link {
    font-size: var(--text-xs);
    color: var(--brass-500);
    text-decoration: none;
    font-variant: small-caps;
    letter-spacing: 0.06em;
}

.header-link:hover {
    color: var(--brass-300);
}

/* The two groups in the header, and the mark for the page you are on.

   Sections are what Rollies holds - the war room, the chronicle, the numbers, the
   monsters. Utilities help you use Rollies or change it. They were one row of
   nine at one weight, which made every link cost the same glance and made the
   four that matter no easier to find than Accounts. The utilities are smaller,
   dimmer, and sit behind a hairline.

   The current page is marked with a color AND a line under the word. `ux.md`
   section 6 settles this for the faction colors and the reason carries: never
   hue alone, because roughly one man in twelve cannot use the hue. The mark
   hangs off aria-current, which is also what a screen reader reads, so the
   thing seen and the thing announced cannot drift apart. */
.site-nav,
.site-utils {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    flex-wrap: nowrap;
}

.site-utils {
    gap: 0.7rem;
    margin-left: 0.85rem;
    padding-left: 0.85rem;
    border-left: 1px solid var(--wood-800);
}

.header-util {
    font-size: var(--text-xs);
    color: var(--brass-700);
}

.header-link[aria-current="page"] {
    color: var(--brass-300);
    border-bottom: 1px solid var(--brass-400);
    padding-bottom: 1px;
}

.accounts-page {
    max-width: 46rem;
    margin: 0 auto;
}

.accounts-panel {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background-color: var(--soot-800);
    padding: 1.25rem;
    margin-bottom: 1.25rem;
}

.accounts-heading {
    font-size: var(--text-md);
    color: var(--brass-300);
    font-variant: small-caps;
    letter-spacing: 0.12em;
    font-weight: normal;
    margin-bottom: 1rem;
}

.accounts-table {
    width: 100%;
    border-collapse: collapse;
}

.accounts-table th {
    text-align: left;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--brass-700);
    font-weight: normal;
    padding-bottom: 0.5rem;
}

.accounts-table td {
    padding: 0.5rem 0.5rem 0.5rem 0;
    border-top: 1px solid var(--wood-900);
    vertical-align: middle;
}

.accounts-name {
    font-size: var(--text-md);
}

.accounts-you {
    font-size: var(--text-2xs);
    color: var(--brass-700);
    font-variant: small-caps;
    letter-spacing: 0.12em;
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    padding: 0.05rem 0.3rem;
    margin-left: 0.4rem;
}

.accounts-role {
    font-size: var(--text-xs);
    color: var(--brass-500);
    font-variant: small-caps;
    letter-spacing: 0.06em;
}

.accounts-inline-form,
.accounts-add-form {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
}

.accounts-input {
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    color: var(--parchment);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    /* 44px, same reasoning as the login form: this gets used from a phone. */
    padding: 0.6rem 0.5rem;
    min-width: 8rem;
}

.accounts-input:focus {
    outline: none;
    border-color: var(--brass-300);
}

.accounts-button {
    background-color: var(--wood-700);
    border: 1px solid var(--brass-300);
    border-radius: var(--radius-small);
    color: var(--parchment);
    font-family: var(--font-body);
    font-variant: small-caps;
    letter-spacing: 0.12em;
    font-size: var(--text-sm);
    padding: 0.6rem 0.9rem;
    cursor: pointer;
}

.accounts-button:hover {
    background-color: var(--wood-600);
}

/* Quiet until you reach for it: a delete on every row, as loud as SET, made
   the dangerous press the most visible one on the page. */
.accounts-danger {
    background-color: transparent;
    border-color: transparent;
    color: var(--brass-600);
}

.accounts-danger:hover {
    border-color: var(--oxblood-700);
    color: var(--oxblood-400);
}

.accounts-note {
    font-size: var(--text-sm);
    color: var(--brass-700);
    line-height: 1.5;
    margin-top: 0.9rem;
}

.flash-area {
    max-width: 1800px;
    margin: 0 auto 1rem;
}

.flash {
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    padding: 0.7rem 0.9rem;
    margin-bottom: 1rem;
    font-size: var(--text-sm);
}

.flash-notice {
    color: var(--hp-good);
    border-color: var(--verdigris-700);
}

.flash-error {
    color: var(--oxblood-400);
    border-color: var(--oxblood-700);
}

@media (max-width: 700px) {
    .accounts-table,
    .accounts-table tr,
    .accounts-table td {
        display: block;
        width: 100%;
    }

    /* The header row is meaningless once the cells stack under each other. */
    .accounts-table th { display: none; }

    .accounts-table td { border-top: none; padding: 0.25rem 0; }

    .accounts-table tr {
        border-top: 1px solid var(--wood-900);
        padding: 0.75rem 0;
    }
}

.login-link {
    color: var(--brass-300);
}


/* ============================================
   BATTLE MAP — an image, and a grid drawn over it in percentages
   ============================================ */

/* The board is a positioning context and nothing else: the image sets the
   size, and everything laid over it - the grid now, tokens next - is absolutely
   positioned against these edges. That is what lets the overlay stay aligned at
   any panel width without knowing the image's pixel dimensions. */
/* The frame is the board plus its gutters. Unlabeled it is a single cell and
   the board fills it; labeled it grows a row of letters above and a column of
   numbers beside, both sized in the same fractions as the grid itself, which is
   what keeps a heading over its own column at any width. */
.map-frame {
    display: grid;
    grid-template-columns: 1fr;
    /* The frame is now as wide as its board rather than as wide as its
       container, because it is the thing that scrolls. `1fr` would stretch the
       label gutters past the squares they name. */
    width: max-content;
    margin-inline: auto;
}

.map-frame.is-labeled {
    /* A FIXED gutter, not `auto`. The corner names the square under the
       pointer, and "F2" and "F21" are different widths - under `auto` the
       gutter followed the text and the whole board slid sideways as the mouse
       moved. 3rem holds the longest name a board makes, "AB40".
       The row is fixed for the same reason, one axis over: the corner is empty
       until the pointer is on a square, and at `auto` the heading row grew by
       the corner's line the moment "D8" appeared, pushing the board down. */
    grid-template-columns: 3rem max-content;
    grid-template-rows: 1.1rem max-content;
    column-gap: 4px;
    row-gap: 2px;
}

.map-colheads,
.map-rowheads {
    display: grid;
    font-size: var(--text-2xs);
    letter-spacing: 0.02em;
    color: var(--brass-500);
    text-align: center;
    line-height: 1;
}

/* repeat() over the same custom properties the grid uses, so a heading cannot
   drift from the column it names - there is one source for the count. */
.map-colheads { grid-template-columns: repeat(var(--map-cols), 1fr); }
.map-rowheads { grid-template-rows: repeat(var(--map-rows), 1fr); }

.map-colheads span,
.map-rowheads span {
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.map-rowheads span { min-width: 1.4rem; }

/* The square under the pointer, named in its own two gutters. A glow rather
   than a filled block: the heads sit in a strip a few pixels wide, and a lit
   block there reads as something the mouse SELECTED rather than as a label
   answering where the mouse is. */
.map-colheads span.is-lit,
.map-rowheads span.is-lit {
    color: var(--brass-200);
    text-shadow: 0 0 10px rgba(201, 168, 76, 0.55);
}

.map-corner {
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    text-align: right;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--brass-300);
    align-self: end;
    line-height: 1;
    padding-right: 0.2rem;
    white-space: nowrap;
    overflow: hidden;
}

/* The headings stay on screen while the board scrolls under them. A map taller
   than the panel scrolled its column numbers away with its first row, and the
   one thing a heading is for - "that's column 8" - went with them exactly when
   the map was big enough to need it. Numbers pin to the top, letters to the
   left, the corner to both.

   Sticky against `.map-viewport`, which is the box that scrolls - not the page,
   so the fixed site header (the sticky-header trap) never covers them. Opaque
   in the page's own color, because the board scrolling behind a transparent
   heading is two things written on top of each other. Above the tokens (2). */
.map-frame.is-labeled .map-colheads,
.map-frame.is-labeled .map-rowheads,
.map-frame.is-labeled .map-corner {
    position: sticky;
    background: var(--soot-900);
}
/* The shadows fill the grid's gaps (2px under the numbers, 4px beside the
   letters), which would otherwise be slivers of map scrolling past. */
.map-frame.is-labeled .map-colheads { top: 0; z-index: 4; box-shadow: 0 2px 0 var(--soot-900); }
.map-frame.is-labeled .map-rowheads { left: 0; z-index: 4; box-shadow: 4px 0 0 var(--soot-900); }
.map-frame.is-labeled .map-corner { top: 0; left: 0; z-index: 5; }

/* The window over the board: the rounded corners and the outline belong to
   what you can SEE, not to the map. The board's own border-radius is the map's
   corners, so once the map scrolled they scrolled away with its edges and the
   visible cut was square. This stays put - sticky, like the headings - at the
   size fitBoard measures, draws the outline, and paints the page's color
   outside its own rounded corners with a spread shadow. Never clicked.

   Placed in the board's own grid cell, because a sticky grid item moves within
   its grid area and the board's area is exactly the range it may travel.
   Above the tokens (2), below the headings (4). Without the measurements it is
   the board's size and sits where the board is, which is how it looked before. */
.map-window {
    grid-column: 1;
    grid-row: 1;
    align-self: start;
    justify-self: start;
    position: sticky;
    top: var(--window-top, 0px);
    left: var(--window-left, 0px);
    width: var(--window-w, 100%);
    height: var(--window-h, 100%);
    z-index: 3;
    pointer-events: none;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: 0 0 0 100vmax var(--soot-900);
}
/* Every piece of the frame in a named cell. The window shares the board's
   cell, and an item placed explicitly is placed FIRST - so a board left to
   auto-placement would find its cell taken and drop into a new row. */
.map-frame.is-labeled > .map-corner { grid-column: 1; grid-row: 1; }
.map-frame.is-labeled > .map-colheads { grid-column: 2; grid-row: 1; }
.map-frame.is-labeled > .map-rowheads { grid-column: 1; grid-row: 2; }
.map-frame.is-labeled > .map-board,
.map-frame.is-labeled > .map-window { grid-column: 2; grid-row: 2; }

/* The board takes the GRID's proportions, not the image's, and that is what
   makes a square square. Cell width is board-width/cols and cell height is
   board-height/rows; with the board at cols:rows those are the same number, at
   any panel width, whatever the image happens to be.

   Before this the board was the image, so a 3000x1339 map under a 60x40 grid
   drew every cell at 50 by 33 pixels and nothing on screen said so. It was
   found by a grid that looked wrong at a table.

   No max-height on purpose. Clamping the height with a definite width would
   break the ratio and quietly bring the rectangles back - the honest fix for a
   board that is too tall is a grid that matches the map, which MATCH IMAGE
   sets in one press. */
.map-board {
    position: relative;
    /* Was `aspect-ratio: cols / rows` with the width set by the panel. The
       ratio is still exactly that - cols*cell by rows*cell - so squares stay
       square; what changed is that the multiplier is now a constant instead of
       whatever the panel had spare. */
    width: calc(var(--map-cols) * var(--map-cell));
    height: calc(var(--map-rows) * var(--map-cell));
    /* Placed, not auto-placed: `.map-window` shares this cell (see it). */
    grid-column: 1;
    grid-row: 1;
    flex: 0 0 auto;
    line-height: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    background-color: var(--soot-800);
}

/* contain rather than cover or fill: a battle map is never cropped, because
   the edge of the map is a wall someone is standing against, and never
   stretched, because it usually has its own printed grid to line up with. The
   bars that appear when the counts are wrong are the point - they say so. */
.map-image {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* Cells are fractions rather than pixels, so the grid the DM typed - 20 by 15 -
   is the grid drawn, whatever the image resolution or the panel width. */
.map-grid {
    position: absolute;
    inset: 0;
    display: grid;
    grid-template-columns: repeat(var(--map-cols), 1fr);
    grid-template-rows: repeat(var(--map-rows), 1fr);
    pointer-events: none;
}

/* A square is a button so a click has somewhere to land, which means undoing
   every global button style this sheet applies. */
.map-cell {
    border: 0;
    border-right: 1px solid rgba(212, 175, 55, 0.16);
    border-bottom: 1px solid rgba(212, 175, 55, 0.16);
    /* Square on purpose: these tile into a grid. */
    border-radius: 0;
    padding: 0;
    margin: 0;
    background: transparent;
    cursor: crosshair;
    pointer-events: auto;
}

.map-cell:hover { background-color: rgba(212, 175, 55, 0.12); }

/* Tokens sit above the squares and take their own clicks; the layer between
   them stays transparent to the mouse so a square is still reachable. */
.map-tokens {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.map-token {
    position: absolute;
    width: calc(100% / var(--map-cols));
    height: calc(100% / var(--map-rows));
    transform: translate(-50%, -50%);
    border-radius: 50%;
    border: 2px solid var(--brass-500);
    background-color: var(--soot-700);
    pointer-events: auto;
}

/* THE CLIP MOVED OFF THE TOKEN AND ONTO ITS BODY, so that something can be
   drawn outside the circle - the height badge - while everything inside it is
   still cut to the circle. Taking `overflow: hidden` off the token itself was
   tried first and let the blank-face silhouette out: that SVG is sized by its
   own intrinsic width, has always been bigger than the token, and the clip was
   what made it read as a face filling the circle rather than a shape sitting
   over the board. */
.map-token-body {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}

.map-token img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.map-token-initial {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--parchment-bright);
    text-transform: uppercase;
}

.map-token.is-movable { cursor: grab; }
.map-token.is-active-turn { box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.75); }

/* The one piece of feedback the whole interaction rests on: you have picked
   something up, and the next click puts it down. */
.map-token.is-selected,
.map-tray-chip.is-selected {
    outline: 2px solid var(--brass-200);
    outline-offset: 2px;
}

/* Factions color the token the same way they color an initiative chip, so
   the board and the strip read as the same fight. */
.map-token.faction-party   { border-color: var(--faction-party); }
.map-token.faction-ally    { border-color: var(--faction-ally); }
.map-token.faction-neutral { border-color: var(--faction-neutral); }
.map-token.faction-enemy   { border-color: var(--faction-enemy); border-width: 3px; background-color: color-mix(in srgb, var(--faction-enemy) 35%, var(--soot-700)); }

/* HOW HIGH, ON THE TOKEN. A creature thirty feet up looks exactly like one
   standing on the floor, which is the thing a board drawn from above cannot
   say by itself - so it is said in a number, in the one place nobody has to
   go looking for it.

   OUTSIDE THE CIRCLE, at its shoulder. A band across the top was tried first
   and it landed on the initials: a token is one grid square wide, the two
   letters are centered in it, and anything drawn over the top third of it is
   drawn over them. So the badge sits off the ring, which is what made the
   clip above move onto the portrait.

   Only drawn when there is something to draw, so a fight fought on the floor
   carries none of these. */
.map-token-height {
    position: absolute;
    top: -0.35rem;
    right: -0.5rem;
    z-index: 2;
    padding: 0 0.2rem;
    border: 1px solid var(--brass-700);
    border-radius: var(--radius-small);
    font-size: var(--text-2xs);
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
    color: var(--parchment-bright);
    background-color: var(--soot-900);
}

.map-tray {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.6rem;
}

.map-tray-label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--brass-500);
}

.map-tray-chip {
    font-size: var(--text-xs);
    padding: 0.2rem 0.5rem;
    border-left-width: 3px;
    border-left-style: solid;
}

.map-tray-chip.faction-party   { border-left-color: var(--faction-party); }
.map-tray-chip.faction-ally    { border-left-color: var(--faction-ally); }
.map-tray-chip.faction-neutral { border-left-color: var(--faction-neutral); }
.map-tray-chip.faction-enemy   { border-left-color: var(--faction-enemy); }

.map-pickup-form { margin-top: 0.4rem; }
#map-pickup { font-size: var(--text-xs); padding: 0.2rem 0.5rem; }

/* ARRANGING, NOT PLAYING. A rehearsal draws the same board a fight does, and
   the difference - nothing recorded, nobody else looking - is invisible. So
   the bar is loud on purpose: it is the only thing on screen saying that what
   the DM is about to spend ten minutes on is not the session. */
.rehearsal-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.6rem;
    padding: 0.4rem 0.6rem;
    border: 1px dashed var(--brass-500);
    border-radius: var(--radius);
    background-color: var(--soot-600);
}

.rehearsal-what {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    color: var(--brass-300);
    margin-right: auto;
}

.rehearsal-bar button { font-size: var(--text-xs); padding: 0.2rem 0.6rem; }

.map-scatter { margin-top: 0.5rem; }
.map-scatter button {
    font-size: var(--text-xs);
    padding: 0.2rem 0.6rem;
    letter-spacing: 0.06em;
}

.map-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    margin-top: 0.6rem;
}

.map-upload-form,
.map-grid-form {
    display: flex;
    gap: 0.35rem;
    align-items: center;
    flex-wrap: wrap;
}

.map-grid-form label {
    font-size: var(--text-xs);
    color: var(--brass-500);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.map-grid-form input[type="number"] { width: 3.5rem; }

/* The two things Rollies does by itself, each with the sentence that says what
   it does; then the boxes, written as the sentences they stand for. */
.map-grid-form { flex-direction: column; align-items: stretch; gap: 0.5rem; }
.map-grid-auto { display: flex; flex-wrap: wrap; gap: 0.5rem 1.2rem; }
.map-grid-way { flex: 1 1 18rem; display: flex; align-items: center; gap: 0.6rem; }
.map-grid-way button { flex: 0 0 auto; }
.map-grid-why { font-size: var(--text-xs); line-height: 1.4; color: var(--brass-500); }
.map-grid-hand-title {
    margin: 0.4rem 0 0; font-size: var(--text-2xs); letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--brass-600); font-weight: normal;
    text-align: left; width: 100%;
}
.map-grid-hand { display: flex; flex-direction: column; gap: 0.35rem; }
.map-grid-line { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.45rem; font-size: var(--text-xs); color: var(--parchment); }
.map-grid-line label { display: inline-flex; align-items: center; gap: 0.35rem; text-transform: none; letter-spacing: 0; font-size: var(--text-xs); color: var(--parchment); }
.map-grid-line small { color: var(--brass-600); font-size: var(--text-xs); }
.map-grid-toggles { margin-top: 0.2rem; gap: 0.35rem 0.9rem; }
.map-grid-toggles .btn-primary { margin-left: auto; }

.map-library {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    width: 100%;
}

.map-entry {
    display: flex;
    align-items: stretch;
}

.map-entry.is-active .map-name {
    border-color: var(--brass-200);
    color: var(--brass-200);
}

.map-fit-btn,
.map-name,
.map-delete,
.map-clear {
    font-size: var(--text-xs);
    padding: 0.25rem 0.5rem;
}


/* ============================================
   FITTING ON ONE SCREEN — the map is the middle of the dashboard
   ============================================ */

/* How much vertical room everything else needs: the page header, the turn
   strip above the board, and the map controls below it. The board gets what is
   left, and is capped so it never pushes itself off the bottom of the screen. */
/* Header, the order and its head, the controls, the tray and readout, and
   the action bar, all in the board's column now - what the board must leave
   room for so the page fits one screen. */
.war-room.is-dm { --map-reserve: 41rem; }
.war-room.is-player { --map-reserve: 33rem; }
:root { --map-reserve: 33rem; }

/* A SQUARE IS FIVE FEET, AND FIVE FEET IS THIS MANY PIXELS.

   The board used to be scaled to fit its panel, so a cell was panel-width over
   columns and a 5 ft square was whatever was left after the division. Measured
   at one panel size: 56px on a 16-column room, 12px on a 60-column town square,
   9px on an 80-column battlefield. A six-fold swing, and at the bottom of it a
   token is smaller than a line of this text and its faction ring is two pixels.
   MATCH IMAGE made a square SQUARE; nothing made it a consistent SIZE.

   So the cell is fixed and the board overflows, which is what a physical table
   does: a battle mat has one-inch squares whether the fight is in a closet or
   across a field, and you lean over the part that matters rather than shrinking
   the field. 30px because a token is one cell less a 2px ring on each side, and
   26 pixels of interior is the point where a portrait stops being a smudge.

   A small map now leaves space around itself rather than stretching to fill
   the panel. That is the trade and it is the right way round: a 16x9 room IS
   small, and reading its squares as bigger than a battlefield's was the lie. */
:root { --map-cell: 30px; }

/* Capping the FRAME rather than the board, because the labels down the side are
   part of the frame - narrowing the board alone would leave the numbers along
   the top spanning a width the squares no longer occupy, and a heading would
   stop sitting over its own column. */
/* The viewport the board is looked at through. Its HEIGHT is what the panel
   has spare - the same measurement `fitBoard` always made - but that budget now
   decides how much of the map you see rather than how big a square is. */
.map-viewport {
    overflow: auto;
    max-width: 100%;
    /* The one box left with square corners after the sweep, and it gets them
       the same way everything else does - on the VIEWPORT, which clips the
       board, rather than on the cells, which have to tile. */
    border-radius: var(--radius);
    scrollbar-color: var(--wood-700) transparent;
    scrollbar-width: thin;
    /* A board smaller than the viewport sits in the middle of it rather than in
       the corner, the way a small mat sits in the middle of a table. */
    display: flex;
    align-items: flex-start;
}

/* Centered with `margin: auto` on the CHILD rather than `justify-content` on the
   flex box. They look identical until the content is wider than the box, at
   which point justify-content puts the overflow on both sides and the browser
   refuses to scroll to the start of it: an 1826px board reported a scrollWidth
   of 1111 and the first third of the map was unreachable. */
.map-viewport > .map-frame { margin: 0 auto; }

/* The DM's pointer: a ring in their color, pulsing out from the spot. */
.map-ping {
    position: absolute;
    width: calc(var(--map-cell) * 1.6);
    height: calc(var(--map-cell) * 1.6);
    margin: calc(var(--map-cell) * -0.8) 0 0 calc(var(--map-cell) * -0.8);
    border: 4px solid var(--ping-color, var(--accent-bright));
    border-radius: 50%;
    /* Figure and ground: a dark halo either side of the ring, so it stands
       off a busy map in any theme instead of sinking into it. */
    box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.55), inset 0 0 0 3px rgba(0, 0, 0, 0.45);
    pointer-events: none;
    z-index: 6;
    animation: map-ping 0.8s ease-out 4;
}
/* The spot itself, held still while the ring pulses out from it. */
.map-ping::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 8px;
    height: 8px;
    margin: -4px 0 0 -4px;
    border-radius: 50%;
    background: var(--ping-color, var(--accent-bright));
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.6);
}
@keyframes map-ping {
    from { transform: scale(0.3); opacity: 1; }
    to { transform: scale(1.4); opacity: 0; }
}
.map-board.is-pinging { cursor: crosshair; }

/* One finger pans the board the way it always has; two pinch it, and the
   script does the zooming rather than the browser zooming the whole page. */
.map-viewport { touch-action: pan-x pan-y; }

/* The board's zoom, in the well's bottom corner over the board, the same
   three buttons the world map has. */
.board-zoom {
    position: absolute;
    right: 0.5rem;
    bottom: 0.5rem;
    z-index: 4;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.25rem;
}
.board-zoom button {
    min-width: 2rem;
    height: 2rem;
    padding: 0 0.45rem;
    font-size: var(--text-md);
    line-height: 1;
    background-color: color-mix(in srgb, var(--soot-900) 85%, transparent);
}
.board-zoom .board-zoom-word { font-size: var(--text-xs); letter-spacing: 0.06em; }

/* Zoomed far out a token is a few pixels across: its letters would only be
   a smudge on top of the color, so they go, and so do the headings. */
.map-frame.is-tiny .map-token-initial,
.map-frame.is-tiny .map-token-height { display: none; }
/* Hidden, not removed: the gutters are grid tracks the window is measured
   against, and taking them out would move the board under the pointer. */
.map-frame.is-tiny .map-colheads,
.map-frame.is-tiny .map-rowheads { visibility: hidden; }

.map-viewport::-webkit-scrollbar { width: 10px; height: 10px; }
.map-viewport::-webkit-scrollbar-thumb {
    background: var(--wood-700);
    border-radius: var(--radius);
}
.map-viewport::-webkit-scrollbar-track { background: transparent; }

/* The board is the center of the dashboard, so it sits in the center of its
   panel rather than against the left edge of it. */
.panel-center {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 0;
}

.panel-center > * { width: 100%; }

.map-controls,
.map-tray,
.map-pickup-form { max-width: 100%; }

/* ============================================
   MODAL — combat prep, behind a pencil
   ============================================ */
.modal {
    /* A modal dialog is centerd by the browser's own margin:auto, which any
       margin of ours replaces. Said explicitly so it cannot be lost again -
       and dragging replaces it deliberately, with a left and a top. */
    margin: auto;
    position: fixed;
    /* Fifteen conditions make a tall sheet. It scrolls inside itself rather
       than growing past the bottom of a smaller screen and taking APPLY with
       it. */
    max-height: 92vh;
    overflow-y: auto;
    background-color: var(--soot-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--parchment);
    padding: 1rem 1.2rem;
    max-width: min(46rem, 92vw);
    width: 100%;
}

.modal::backdrop { background-color: rgba(0, 0, 0, 0.65); }

/* The handle. It says so with a cursor, because a thing that can be dragged
   and does not look like it may as well not be. */
.modal-title {
    cursor: move;
    user-select: none;
    font-size: var(--text-md);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--brass-300);
    margin: 0 0 0.8rem;
}

.modal-dismiss {
    float: right;
    margin: -0.4rem -0.4rem 0 0;
}

.modal-dismiss button {
    background: transparent;
    border: 0; border-radius: var(--radius-small);
    color: var(--brass-500);
    font-size: var(--text-lg);
    line-height: 1;
    padding: 0.2rem 0.4rem;
    cursor: pointer;
}

.modal .prep-controls { margin-bottom: 1rem; }
.modal .prep-list { display: flex; flex-direction: column; gap: 0.4rem; }


/* Spinner arrows on a number box are half its width at these sizes, and every
   one of these values is typed rather than stepped - nobody clicks an arrow
   eighteen times to enter an initiative roll. Reclaiming that space is what
   lets "amount" fit in the box that asks for it. */
.combat-controls input[type="number"],
.initiative-entry input[type="number"],
.chip-stats input[type="number"],
.map-grid-form input[type="number"] {
    appearance: textfield;
    -moz-appearance: textfield;
}

.combat-controls input[type="number"]::-webkit-outer-spin-button,
.combat-controls input[type="number"]::-webkit-inner-spin-button,
.initiative-entry input[type="number"]::-webkit-outer-spin-button,
.initiative-entry input[type="number"]::-webkit-inner-spin-button,
.chip-stats input[type="number"]::-webkit-outer-spin-button,
.chip-stats input[type="number"]::-webkit-inner-spin-button,
.map-grid-form input[type="number"]::-webkit-outer-spin-button,
.map-grid-form input[type="number"]::-webkit-inner-spin-button {
    appearance: none;
    -webkit-appearance: none;
    margin: 0;
}

/* SET ALL is one button, not a column. Stretching it to the height of the
   chips made it the largest thing in the strip, which is not what it is. */
.initiative-setall-btn {
    align-self: center;
    padding: 0.4rem 0.7rem;
    font-size: var(--text-xs);
}
/* It is the strip's last child, so ten creatures pushed it past the strip's
   edge, cut to "SET" against END COMBAT. Held to the right edge instead: the
   cards scroll under it, and the shadow is the room's own ground so they fade
   rather than end at a line. */
.initiative-setall-btn {
    position: sticky;
    right: 0;
    z-index: 2;
    box-shadow: -0.7rem 0 0.6rem var(--soot-900);
}


/* ============================================
   THE PORTRAIT ON THE CHARACTER CARD — and the control that sets it
   ============================================ */

.card-header {
    display: flex;
    align-items: center;
    gap: 0.7rem;
}

/* The text keeps the layout it had before the portrait arrived beside it:
   name on the left, player on the right, on one line. Wrapping the two in a
   box had quietly made them a right-aligned stack. */
.card-header-text {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}

.card-portrait {
    position: relative;
    flex: 0 0 auto;
    width: 3.6rem;
    height: 4.4rem;
    border-radius: var(--radius);
    overflow: hidden;
    border: 2px solid var(--line);
    background-color: var(--soot-600);
    display: flex;
    align-items: center;
    justify-content: center;
}

.card-portrait img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 50% 25%;
}

/* The face is the button. A file input at the bottom of the card, below the
   ability scores, was nowhere near the thing it changed and was the last place
   anyone looked - which is how "how do I add photos?" was a fair question
   about a feature that had been there all along. */
.card-portrait.is-editable { cursor: pointer; }
.card-portrait.is-editable:hover { border-color: var(--brass-300); }

.card-portrait-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.card-portrait-hint {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 0.1rem 0;
    text-align: center;
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    color: var(--parchment-bright);
    background-color: rgba(8, 9, 12, 0.8);
    opacity: 0;
}

.card-portrait.is-editable:hover .card-portrait-hint { opacity: 1; }

/* The form is empty and only exists to be the target of an input that sits
   somewhere else, so it must not occupy any of the card's layout. */
.portrait-upload-form { display: none; }


/* A link rather than a button since the sheet moved into the rail: it goes
   somewhere now. Sized to the card's own type and bordered like the STATS link
   it sits beside, because the two do the same thing to the same region and
   looked like two different kinds of control while one of them was a form. */
.chip-stats-toggle {
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    padding: 0.1rem 0.3rem;
    line-height: 1.3;
    color: var(--brass-600);
    text-decoration: none;
    border: 1px solid var(--soot-500);
    border-radius: var(--radius-small);
}

.chip-stats-toggle:hover { color: var(--brass-300); border-color: var(--brass-500); }

/* The name box is the one that takes a whole stat block, so it gets the room
   the others do not need. */
/* Written with the form in front of them, because .initiative-add-form input
   outranks a bare class and quietly won the last time these were set. */
.initiative-add-form .add-name-input { flex: 3 1 20rem; }
.initiative-add-form .add-hp-input { flex: 0 0 6rem; }


/* ============================================
   CONTROLS — the default, so a new one arrives themed
   ============================================
   Every button in this sheet used to be styled through a selector of its own:
   .damage-form button, .temp-hp-form button, and so on. Which means a button
   added afterwards is not styled at all - it is a macOS button, in system
   blue, in a room lit by gaslight. NEXT TURN, UPLOAD MAP, SET and MATCH IMAGE
   all arrived that way.

   Same failure as the colors, one level down: a default nobody had to opt
   into was missing, so opting in was something to forget. These are the floor.
   A rule with a class still beats them, so nothing that was deliberately
   styled changes. */

button {
    font-family: inherit;
    font-size: var(--text-sm);
    letter-spacing: 0.06em;
    color: var(--parchment);
    background-color: var(--wood-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.35rem 0.7rem;
    cursor: pointer;
}

button:hover {
    border-color: var(--brass-500);
    color: var(--parchment-bright);
}

button:disabled {
    opacity: 0.45;
    cursor: default;
    border-color: var(--line);
    color: var(--brass-600);
}

/* A link that does a button's job - EDIT, a way to another page - drawn as the
   ordinary button, so it weighs what its neighbors weigh. */
.btn-link {
    display: inline-block; font-size: var(--text-sm); letter-spacing: 0.06em; text-decoration: none;
    color: var(--parchment); background-color: var(--wood-800);
    border: 1px solid var(--line); border-radius: var(--radius); padding: 0.35rem 0.7rem;
}
.btn-link:hover { border-color: var(--brass-500); color: var(--parchment-bright); }

/* The one thing on screen you press every turn. */
.btn-primary {
    background-color: var(--brass-400);
    border-color: var(--brass-300);
    color: var(--soot-900);
    font-weight: 600;
}

.btn-primary:hover { background-color: var(--brass-300); color: var(--soot-900); }

/* Ending a fight and clearing a table are not the same kind of press as
   advancing a turn, and should not look like it. */
/* An outline in the danger color rather than a fill, because a dark fill
   under a light palette read as a disabled gray button. */
.btn-danger {
    background-color: transparent;
    border: 1px solid var(--faction-enemy);
    color: var(--parchment);
}

.btn-danger:hover { background-color: color-mix(in srgb, var(--faction-enemy) 18%, transparent); }

select,
input[type="text"],
input[type="search"],
input[type="number"],
input[type="password"],
textarea {
    font-family: inherit;
    font-size: var(--text-sm);
    color: var(--parchment);
    background-color: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.35rem 0.5rem;
}

select:focus,
input:focus,
textarea:focus {
    outline: none;
    border-color: var(--brass-300);
}

::placeholder { color: var(--brass-600); }

/* A native checkbox paints itself with the operating system's accent color -
   the same system blue - unless it is told otherwise. One line, everywhere. */
input[type="checkbox"],
input[type="radio"] { accent-color: var(--brass-400); }

/* A file input dressed as one of Rollies' own buttons - see `file_pick` in
   _macros.html. The native control is two controls in one trench coat and
   only the button half ever took a stylesheet; "No file chosen" stayed in
   the browser's typeface whatever was done. So the input is visually hidden
   inside a label that carries the button and the file's name, and it stays
   focusable: the ring lands on the button when the keyboard reaches the
   input. `position: relative` on the label is not optional - a hidden input
   is `position: absolute`, and one placed against the page grew the bestiary
   a sideways scrollbar. */
.file-pick { position: relative; display: inline-flex; align-items: center; gap: 0.5rem; cursor: pointer; }
.file-pick-btn {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    color: var(--parchment);
    background-color: var(--wood-800);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.3rem 0.6rem;
}
.file-pick:hover .file-pick-btn { border-color: var(--brass-500); }
.file-pick-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.file-pick:has(.file-pick-input:focus-visible) .file-pick-btn { outline: 2px solid var(--brass-300); outline-offset: 2px; }
.file-pick-name { font-size: var(--text-xs); color: var(--brass-500); }


/* A row of controls that belong to one job. The gap pushes what follows to the
   right, which is how "run the fight" is kept visually apart from "look at it"
   without drawing a line between them. */
.control-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
}

.control-gap { flex: 1 1 1rem; }

.control-label {
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    color: var(--brass-600);
    text-transform: uppercase;
}

/* The map controls are three jobs too: put a picture on the table, say how big
   its squares are, and manage the pile. Same treatment. */
.map-controls {
    flex-direction: column;
    align-items: stretch;
    gap: 0.4rem;
}

.map-upload-form,
.map-library {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
}
/* The grid form is a column of sentences, not a row of boxes, at every width. */
.map-grid-form {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0.5rem;
}

.map-grid-form label {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}


/* A heading inside a dialog, so the three jobs in it are three jobs and not
   one long row of controls that happen to be about maps. */
.modal-section {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--brass-600);
    margin: 0.9rem 0 0.4rem;
    padding-bottom: 0.25rem;
    border-bottom: 1px solid var(--wood-800);
}

.modal-section:first-child { margin-top: 0; }

/* In a dialog there is room, so the map list is a list rather than a scatter
   of chips squeezed under the board. */
.modal .map-library {
    flex-direction: column;
    align-items: stretch;
    gap: 0.3rem;
}

.modal .map-entry {
    justify-content: space-between;
    gap: 0.4rem;
    padding: 0.15rem 0;
}

.modal .map-name { flex: 1; text-align: left; }


/* A focus ring the browser draws is the operating system's blue, which is the
   same reason the checkboxes were blue. Keyboard focus should be visible and
   should be brass. */
button:focus-visible,
select:focus-visible,
input:focus-visible,
textarea:focus-visible,
a:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
.chip-card:focus-visible {
    outline: 2px solid var(--brass-300);
    outline-offset: 2px;
}

/* A dialog opens with its first control focused, which put a ring on the close
   button every time. Focus that the browser gave rather than the person is not
   worth drawing. */
.modal-dismiss button:focus:not(:focus-visible) { outline: none; }


/* The way into the map menu, under the board where the controls used to live.
   Filing something away without leaving a mark where it was is how a feature
   becomes one nobody finds. */

/* An empty table is the one moment the menu is certainly what you want, so the
   placeholder is the button rather than a note about where the button is. */
.map-empty {
    width: 100%;
    cursor: pointer;
    font-style: normal;
    line-height: 1.7;
}

.map-empty:hover { border-color: var(--brass-300); color: var(--parchment); }

.map-empty-hint {
    font-style: normal;
    font-size: var(--text-xs);
    letter-spacing: 0.12em;
    color: var(--brass-300);
}


/* ============================================
   REACH — how far a combatant can actually go
   ============================================ */

/* Drawn on the squares rather than as a ring around them, because the question
   is "can I stand there", and a square either can be stood in or cannot. */
.map-cell.is-reachable {
    background-color: rgba(95, 158, 134, 0.16);
    box-shadow: inset 0 0 0 1px rgba(95, 158, 134, 0.28);
}

.map-cell.is-reachable:hover { background-color: rgba(95, 158, 134, 0.3); }

.map-readout {
    display: flex;
    align-items: center;
    gap: 0.8rem;
    margin-top: 0.4rem;
    font-size: var(--text-xs);
    color: var(--brass-500);
}

.map-reach-label { letter-spacing: 0.06em; }

.map-dash {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    cursor: pointer;
}


/* The distance under the mouse. Verdigris while it is a move you can make and
   oxblood past that, so the budget is felt rather than worked out. */
.map-distance {
    margin-left: auto;
    font-weight: 600;
    color: var(--verdigris-400);
}

.map-distance.is-beyond { color: var(--oxblood-400); }

/* Up and down, for the token in hand. Beside the reach label rather than on
   the token, because it is a control and the token is the thing it is about -
   and because two arrows on a token one grid square wide are two arrows
   nobody can hit. */
.map-height {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
}

/* `hidden` is a UA rule and this class is not, so the class wins and the
   control sat under the board with nothing in hand, saying "ground" about
   nobody. Every display: on a thing that is sometimes hidden owes this line. */
.map-height[hidden] { display: none; }

.map-height-btn {
    font-size: var(--text-xs);
    line-height: 1;
    padding: 0.2rem 0.4rem;
}

/* The box is typed into, so it is an input dressed as the readout it
   replaced: the same brass number, a hairline under it to say "type here",
   and no spinner - a text field has none, which is the point of its being
   one. The unit sits outside the box so "15" is what gets typed. */
.map-height-field {
    display: inline-flex;
    align-items: baseline;
    gap: 0.2rem;
}

.map-height-value {
    width: 3.2rem;
    padding: 0.1rem 0.2rem;
    text-align: center;
    font-weight: 600;
    font-size: var(--text-xs);
    color: var(--brass-300);
    background: transparent;
    border: 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
}

.map-height-value:focus { outline: none; border-bottom-color: var(--brass-500); }

.map-height-unit {
    font-size: var(--text-2xs);
    letter-spacing: 0.08em;
    color: var(--brass-700);
}


/* ============================================
   AREAS OF EFFECT — the cone on the board
   ============================================
   A template is an INSTRUMENT laid over the map, not a creature on it, so it
   is brass rather than any faction's color: a fireball belongs to whoever is
   standing in it, and drawing it in the caster's green would say the wrong
   thing about who it catches.

   Two states, and telling them apart is the whole of the styling. One being
   AIMED is brighter and dashed, because it is a question - nothing has
   happened yet and Esc takes it away. One PLACED is dimmer and solid, because
   it is a thing on the table that will still be there next turn. */
:root { --area: var(--brass-400); }

/* Over the picture, under the tokens - the tokens are what a mouse reaches
   for, and a creature standing in a fireball should be in front of it rather
   than behind it. Deaf to the mouse, so every square underneath stays a
   square you can click. */
.map-areas {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.map-area { position: absolute; inset: 0; }

/* The squares the shape catches - what the DM is actually being offered, and
   what becomes the ticked names on a saving throw. */
.map-area-square {
    position: absolute;
    background-color: color-mix(in srgb, var(--area) 22%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--area) 30%, transparent);
}

/* The true shape over the top. The viewBox is the grid itself, so this box
   has only to be the board; the stroke is kept off the scale by
   `vector-effect` on the shape, or a hairline would come out thirty pixels
   thick on a twenty-square map. */
.map-area-true {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    fill: none;
    stroke: var(--area);
    stroke-width: 1.5;
}

.map-area.is-aiming .map-area-square {
    background-color: color-mix(in srgb, var(--area) 34%, transparent);
}

.map-area.is-aiming .map-area-true {
    stroke-width: 2;
    stroke-dasharray: 5 4;
}

/* `is-on-board` rather than `is-placed`, which the map image already uses for
   something else entirely - a picture laid to its own printed grid. Two
   meanings on one class name is how a rule reaches a thing it was never
   written for. */
.map-area.is-on-board .map-area-true { stroke-opacity: 0.75; }

/* The board says what it is doing. A crosshair over a token as well as over a
   square, because while a template is being aimed a token is a place to drop
   one rather than a thing to pick up. */
body.is-aiming .map-token,
body.is-aiming .map-cell { cursor: crosshair; }

/* And the movement ring goes out of the way while a template is being aimed.
   Both are highlights over the same squares, and looked at together neither
   could be read - which was not obvious until there were two of them on the
   board at once. The ring comes back the moment the aim is placed or dropped;
   nothing is deselected. */
body.is-aiming .map-cell.is-reachable {
    background-color: transparent;
    box-shadow: none;
}

.map-aim {
    font-weight: 600;
    letter-spacing: 0.06em;
    color: var(--area);
}

/* What is lying on the board, in words - because a translucent shape on a
   dark map is a thing you can miss and a name is not. */
.map-area-strip {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.35rem;
    font-size: var(--text-xs);
}

.map-area-strip-label {
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--brass-600);
}

.map-area-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.15rem 0.4rem;
    border: 1px solid color-mix(in srgb, var(--area) 40%, transparent);
    border-radius: var(--radius-small);
    background-color: color-mix(in srgb, var(--area) 10%, transparent);
}

.map-area-chip-shape {
    color: var(--area);
    font-weight: 600;
}

.map-area-chip-caster { color: var(--brass-500); }
.map-area-chip-who { color: var(--parchment); }
.map-area-chip-who.is-empty { color: var(--brass-600); font-style: normal; }

.map-area-ask,
.map-area-clear {
    border: 1px solid var(--brass-600);
    border-radius: var(--radius-small);
    background: transparent;
    color: var(--brass-300);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    padding: 0.1rem 0.35rem;
    cursor: pointer;
}

.map-area-ask:hover,
.map-area-clear:hover { color: var(--parchment-bright); border-color: var(--brass-300); }

/* The × that takes one off. A form rather than a button, so it works without
   the script the rest of this feature needs - which matters here more than
   anywhere else in the board, because a template nobody can remove is a
   template that stays on the map forever. */
.map-area-off { display: inline-flex; }

.map-area-off button {
    border: 0; border-radius: var(--radius-small);
    background: transparent;
    color: var(--brass-600);
    font-size: var(--text-sm);
    line-height: 1;
    padding: 0 0.15rem;
    cursor: pointer;
}

.map-area-off button:hover { color: var(--oxblood-400); }


/* ============================================
   THE SHEET — one combatant's hit points, conditions and range
   ============================================ */

/* The sheet is in the inspector rail, not in a dialog. What that costs in
   style is this block and nothing else: everything below it was already
   written against unscoped classes, so it came across unchanged.

   The faction stripe moves onto the RAIL's title, which is the only place the
   name is drawn now. It is written against `.inspector:has(...)` rather than
   set by the noun, because whose side a creature is on is a fact about the
   creature and the rail's head is rendered before the noun's template runs. */
.inspector:has(.combatant-sheet.faction-party)   .inspector-title { border-left-color: var(--faction-party); }
.inspector:has(.combatant-sheet.faction-ally)    .inspector-title { border-left-color: var(--faction-ally); }
.inspector:has(.combatant-sheet.faction-neutral) .inspector-title { border-left-color: var(--faction-neutral); }
.inspector:has(.combatant-sheet.faction-enemy)   .inspector-title { border-left-color: var(--faction-enemy); }
.inspector:has(.combatant-sheet) .inspector-title {
    border-left: 4px solid var(--line);
    padding-left: 0.5rem;
}

/* 22rem is narrower than the 30rem the dialog took, and the conditions table
   is the one thing in the sheet that noticed: four columns, two of them form
   controls. The number box gives up the width, because a duration is one or
   two digits and the unit beside it is a word.

   The HIT POINTS boxes are deliberately NOT narrowed with them, and the first
   version of this narrowed both. 6rem holds the word "amount"; 4.5rem holds
   "amoun" and a spinner, and a placeholder cut off mid-word is worse than no
   placeholder at all. Measured in the rail rather than guessed: the row has
   316px and the input, DMG and HEAL want 232 of it. */
.combatant-sheet .modal-conditions input[type="number"] { width: 2.6rem; }
.combatant-sheet .modal-conditions select { max-width: 5.5rem; }
.combatant-sheet .modal-row { flex-wrap: wrap; }

/* "25 ft from Cassar - in reach". The sentence the click was asking for. */
.combatant-range {
    min-height: 1em;
    margin: -0.4rem 0 0.6rem;
    font-size: var(--text-xs);
    color: var(--verdigris-400);
}

.combatant-vitals {
    display: flex;
    gap: 1rem;
    font-size: var(--text-sm);
    color: var(--brass-500);
}

.combatant-hp { font-weight: 600; color: var(--hp-good); }
.combatant-hp.is-bloodied { color: var(--hp-bloodied); }
.combatant-hp.is-down { color: var(--hp-down); }
.combatant-untracked { font-weight: normal; color: var(--brass-600); }

.combatant-conditions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin-top: 0.5rem;
}

.modal-row {
    display: flex;
    gap: 0.4rem;
    align-items: center;
    margin-bottom: 0.4rem;
}

.modal-row input[type="number"] { width: 6rem; }

.modal-note {
    font-size: var(--text-xs);
    color: var(--brass-600);
    font-style: normal;
}

.modal-conditions table { width: 100%; font-size: var(--text-xs); }
.modal-conditions td { padding: 0.15rem 0.3rem; }
.modal-conditions input[type="number"] { width: 3.5rem; }
.modal-conditions button { margin-top: 0.5rem; }

.modal-remove { margin-top: 1rem; justify-content: flex-end; }

/* SET ALL's boxes on the back of an enemy card, where AC and a missing
   maximum can be typed without opening anything. */
.chip-back-input {
    width: 2.4rem;
    padding: 0.05rem 0.15rem;
    font-size: var(--text-xs);
    text-align: center;
}

.chip-back-maxhp { width: 3.4rem; }
.chip-back-ac { display: inline-flex; align-items: center; gap: 0.2rem; }


/* ============================================
   WHOSE TURN — said in words, on the banner
   ============================================ */

.turn-indicator.is-your-turn {
    border-color: var(--brass-300);
    box-shadow: 0 0 0 1px rgba(201, 168, 76, 0.25), 0 0 18px rgba(201, 168, 76, 0.12);
}

.turn-yours {
    color: var(--brass-200);
    font-weight: 600;
    letter-spacing: 0.12em;
}

.turn-wait {
    margin-left: 0.6rem;
    font-size: 0.7em;
    letter-spacing: 0.06em;
    color: var(--brass-600);
    text-transform: none;
}

/* Who comes next. Quiet for everybody; for the player who is next it is the
   same brass as "Your turn" at half the weight, a step before it. Its own
   class: it once shared the NEXT TURN button's, and this rule's margin pushed
   the button ten pixels out of its column, which then scrolled. */
.turn-coming {
    margin-left: 0.6rem;
    font-size: 0.7em;
    letter-spacing: 0.06em;
    color: var(--brass-600);
    text-transform: none;
}

.turn-deck {
    margin-left: 0.6rem;
    font-size: 0.8em;
    letter-spacing: 0.06em;
    color: var(--brass-200);
    text-transform: none;
}

/* The cards the banner names as next: a short bar under each, brightest for
   the very next one - the same brass the banner's line is written in. */
.initiative-entry.is-up-next { position: relative; }
.initiative-entry.is-up-next::after {
    content: "";
    position: absolute;
    left: 25%;
    right: 25%;
    bottom: -5px;
    height: 3px;
    border-radius: 2px;
    background: var(--brass-200);
}
.initiative-entry.is-up-2::after { background: var(--brass-500); }

.turn-indicator.is-on-deck {
    border-color: var(--brass-500);
}

.free-movement-btn.is-on {
    border-color: var(--verdigris-500);
    color: var(--verdigris-400);
}


/* ============================================
   HISTORY — back and forward, in the corner of the map
   ============================================ */

/* On the corner of the board, over it, the way the reference overlays its
   tools - so they stay with the thing they act on however wide the column
   is. The stage is what the arrows sit on: the board, or the placeholder. */
.map-stage { position: relative; width: 100%; margin-inline: auto; flex: 0 0 auto; }
.history-arrows {
    position: absolute;
    top: 0.4rem;
    left: 0.4rem;
    z-index: 3;
    display: flex;
    gap: 0.25rem;
}
/* With labels on, the corner where the row and column heads meet is empty
   but for a caption; the arrows take it rather than a square of the board. */
.map-stage.is-labeled .history-arrows { top: 0; left: 0; }

.history-btn {
    width: 2.2rem;
    padding: 0.25rem 0;
    font-size: var(--text-sm);
    line-height: 1;
    color: var(--brass-300);
    background-color: var(--soot-800);
    border-color: var(--line);
}

.history-btn:hover:not(:disabled) {
    border-color: var(--brass-300);
    color: var(--brass-200);
}

/* A grayed arrow is the whole message: there is nothing that way. */
.history-btn:disabled { color: var(--brass-700); }


.map-dash-btn {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    padding: 0.2rem 0.5rem;
}

.map-dash-btn:disabled { color: var(--verdigris-400); opacity: 0.8; }

/* WALK 30 · FLY 60: one row of small buttons, the speed in use pressed. */
.map-mode { display: inline-flex; gap: 0.2rem; }

.map-mode-btn {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    padding: 0.2rem 0.4rem;
}

.map-mode-btn.is-on { color: var(--verdigris-400); border-color: currentColor; }

/* ---- The action bar ---------------------------------------------------
   The turn, spent by name, at the foot of the map column. Live for the
   acting creature's player and the DM; grayed for everyone else, who watch
   the same bar rather than hear about it. A spent row keeps its label so the
   table can see what the action went on. */
.action-bar {
    position: relative;
    margin-top: 0.6rem;
    padding: 0.5rem 0.7rem;
    border: 1px solid var(--line); border-radius: var(--radius);
    border-top: 2px solid var(--brass-600);
    background: var(--soot-800);
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    font-size: var(--text-xs);
}

.action-bar.is-locked { opacity: 0.55; filter: saturate(0.4); }

.bar-head {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--parchment);
}

.bar-name {
    font-family: var(--font-display);
    font-size: var(--text-sm);
    letter-spacing: 0.06em;
    color: var(--brass-300);
}

.bar-attacks { color: var(--oxblood-400); font-weight: 600; letter-spacing: 0.06em; }

.bar-extra-btn { margin-left: auto; }

.bar-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.3rem;
    padding: 0.2rem 0;
    border-top: 1px dashed var(--wood-800);
}

.bar-row.is-spent .bar-kind { color: var(--brass-700); }

.bar-kind {
    min-width: 4.2rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-size: var(--text-2xs);
    color: var(--brass-500);
}

.bar-spent-label {
    color: var(--verdigris-400);
    font-style: normal;
    margin-right: 0.3rem;
}

.bar-form { display: inline; }

.bar-btn {
    padding: 0.2rem 0.5rem;
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    background: var(--soot-700);
    color: var(--parchment);
    border: 1px solid var(--line); border-radius: var(--radius-small);
}

.bar-btn:hover:not(:disabled) { border-color: var(--brass-500); color: var(--brass-300); }

.bar-btn:disabled { color: var(--brass-700); border-color: var(--wood-800); cursor: default; }

.bar-btn.is-spent { text-decoration: line-through; }

.bar-count { color: var(--brass-500); margin-left: 0.25rem; }

.bar-amount { width: 3.2rem; padding: 0.15rem 0.3rem; font-size: var(--text-xs); }

.bar-concentration { color: var(--verdigris-400); border-color: var(--verdigris-700); }

.bar-rest-btn { align-self: flex-end; color: var(--brass-400); }

.bar-idle { color: var(--brass-700); font-style: normal; letter-spacing: 0.06em; }

/* Spell slots as pips: filled while the slot is there, hollow once burnt. */

.pip {
    display: inline-block;
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    background: var(--verdigris-500);
    border: 1px solid var(--verdigris-700);
}

.pip.is-used { background: transparent; border-color: var(--line); }

/* The economy as three marks. Filled while it is still to be had, hollow
   once spent - in the banner, on the bar, read at a glance. */
.econ-icons { display: inline-flex; align-items: center; gap: 0.2rem; vertical-align: middle; margin-left: 0.4rem; }

.econ {
    width: 0.95rem;
    height: 0.95rem;
    fill: var(--brass-400);
    stroke: var(--brass-400);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.econ.econ-reaction { fill: none; }
.econ.econ-reaction .econ-head { fill: var(--brass-400); }
.econ.econ-bonus { fill: var(--verdigris-500); stroke: var(--verdigris-500); }
.econ.econ-bonus.is-spent, .econ.econ-action.is-spent { fill: none; stroke: var(--wood-600); }
.econ.econ-reaction.is-spent { stroke: var(--wood-600); }
.econ.econ-reaction.is-spent .econ-head { fill: var(--wood-600); }

/* The pickers. */
.modal-form { display: flex; flex-direction: column; gap: 0.5rem; }
.modal-field { display: flex; flex-direction: column; gap: 0.2rem; font-size: var(--text-xs); color: var(--brass-500); text-transform: uppercase; letter-spacing: 0.06em; }
.modal-check { font-size: var(--text-xs); color: var(--parchment); display: flex; align-items: center; gap: 0.4rem; }
.modal-note { font-size: var(--text-xs); color: var(--brass-600); margin: 0.2rem 0 0; }
/* The average beside the dice: a number the eye can skip (2026-11-02). */
.attack-avg { color: var(--brass-600); font-size: var(--text-xs); }
.rest-choices { display: flex; gap: 1.2rem; }
.rest-choices form { flex: 1; display: flex; flex-direction: column; gap: 0.3rem; }
.react-choices { display: flex; flex-direction: column; gap: 0.4rem; }
.react-cast, .react-other { flex-direction: row; align-items: center; gap: 0.4rem; }

/* ---- Attacks ------------------------------------------------------------
   A token the acting creature cannot reach - by its default mode, melee or
   ranged - is grayed on the board, so the question "can I hit that" is
   answered before it is asked. The attack modal shows the server's range
   sentence and the swing's verdict as it is rolled. */
.map-token.is-out-of-reach { opacity: 0.75; }
.map-token.is-out-of-reach:hover { opacity: 1; }
/* Present, not gone. At 0.4 and desaturated the token vanished against a
   busy map and read as a creature that had left the fight rather than one
   that was too far to hit. A dashed ring in the muted brown says "there,
   and not from here"; the selection outline keeps its place when both
   apply, because picking something up is the feedback the board rests on. */
.map-token.is-out-of-reach:not(.is-selected) { outline: 2px dashed var(--wood-600); outline-offset: 2px; }

.bar-mode { margin-left: 0.4rem; display: inline-flex; gap: 0; }
/* A joined pair - MELEE|RANGED - so the corners belong to the GROUP and not
   to each button, or the seam in the middle bulges. */
.bar-mode-btn { border-radius: 0; text-transform: uppercase; font-size: var(--text-2xs); letter-spacing: 0.12em; }
.bar-mode-btn.is-on { background: var(--brass-600); color: var(--soot-900); border-color: var(--brass-500); }
.bar-mode-btn:first-child { border-radius: var(--radius-small) 0 0 var(--radius-small); }
.bar-mode-btn:last-child  { border-radius: 0 var(--radius-small) var(--radius-small) 0; }


.attack-modal { min-width: 34rem; }
/* The dialog in three boxes (Gestalt: enclosure and proximity) - tight
   inside, a clear gap between, so what changes the to-hit sits with the
   d20 and what rides on a hit sits with the damage. */
.attack-form { gap: 0.9rem; }
.attack-step {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    padding: 0.6rem 0.7rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
}
.attack-step-what { border-color: transparent; padding: 0; }
.attack-step-title {
    margin: 0;
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brass-600);
}
/* Similarity: a die that changes the to-hit is marked as one, unlike a
   rider that adds damage - a + in the to-hit's green, a share in brass. */
.attack-held .attack-rider { padding-left: 0.5rem; border-left: 2px solid var(--verdigris-500); }
.attack-riders .attack-rider { padding-left: 0.5rem; border-left: 2px solid var(--brass-500); }
.attack-grid { display: grid; grid-template-columns: 6rem 1fr 1fr; gap: 0.5rem; }
.attack-range { font-size: var(--text-xs); color: var(--verdigris-400); margin: 0; min-height: 1em; }
.attack-range.is-beyond { color: var(--oxblood-400); }
.attack-roll { display: flex; align-items: center; gap: 0.4rem; font-size: var(--text-xs); color: var(--parchment); }
.attack-roll input[type="number"] { width: 4.2rem; }
.attack-roll b { color: var(--brass-300); font-size: var(--text-md); min-width: 1.5rem; }
/* "or the total": wide enough for its own placeholder. When it is filled the
   die's arithmetic beside it is not what settles the roll, so it is dimmed
   rather than hidden - the boxes stay where the hand expects them. */
.attack-roll input[data-role="typed-total"], .attack-roll input[name="strike_total"],
.call-answer-form input[name="total"] { width: 6.6rem; }
.attack-roll.has-total [data-role="natural"], .attack-roll.has-total [data-role="modifier"],
.attack-roll.has-total [data-role="hit-adjust"], .attack-roll.has-total [data-role="auto"],
.attack-roll.has-total input[name="strike_roll"] { opacity: 0.45; }
/* The fighting style, asked on the sheet. */
.sheet-styles { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.7rem; }
.sheet-styles-todo { color: var(--brass-200); }

/* A die somebody is holding for later, on their sheet in the rail. */
.combatant-held { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.held-die { font-size: var(--text-xs); padding: 0.1rem 0.45rem; border: 1px solid var(--brass-600); border-radius: var(--radius); color: var(--brass-200); }
.attack-held:empty { display: none; }
.attack-held { display: flex; flex-direction: column; gap: 0.3rem; }
.call-held { font-size: var(--text-xs); }
.cast-buff { display: flex; flex-wrap: wrap; gap: 0.4rem 0.8rem; border: 0; padding: 0; }
/* display:flex would beat the hidden attribute; a buff box is for Bless and Guidance only. */
.cast-buff[hidden] { display: none; }

/* What rides on the hit: one row each, under the weapon's own damage. */
.attack-riders { display: flex; flex-direction: column; gap: 0.3rem; }
.attack-riders:empty { display: none; }
.attack-rider { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; font-size: var(--text-xs); color: var(--parchment); }
.attack-rider input[type="number"] { width: 4.2rem; }
.attack-rider-note { color: var(--brass-600); font-style: normal; }
.attack-new { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.5rem; }
/* `hidden` loses to a display rule; this puts it back. */
.attack-new[hidden] { display: none; }
.attack-new-name { flex: 1 1 14rem; }
.attack-verdict { font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; font-size: var(--text-xs); }
.attack-verdict.is-hit { color: var(--verdigris-400); }
.attack-verdict.is-miss { color: var(--oxblood-400); }

/* How the die is rolled. Three exclusive choices drawn as one segmented row,
   because they are one question - and the two that are not "straight" carry
   the colors the rest of the app already uses for good news and bad, so the
   state is legible without reading the word. */
.attack-swing { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; font-size: var(--text-xs); }
.swing-choice {
    display: inline-flex; align-items: center; gap: 0.3rem; cursor: pointer;
    border: 1px solid var(--wood-800); border-radius: var(--radius-small); padding: 0.22rem 0.5rem;
    color: var(--brass-600); text-transform: lowercase; letter-spacing: 0.06em;
}
.swing-choice input { accent-color: var(--brass-400); margin: 0; }
.swing-choice:has(input:checked) { color: var(--parchment); border-color: var(--brass-500); background: var(--soot-600); }
.swing-advantage:has(input:checked) { border-color: var(--verdigris-500); color: var(--verdigris-400); }
.swing-disadvantage:has(input:checked) { border-color: var(--oxblood-500); color: var(--oxblood-400); }
.swing-choice:has(input:focus-visible) { outline: 2px solid var(--brass-300); outline-offset: 2px; }
/* Why the default moved, when the distance moved it. Quiet: it is an
   explanation for a thing that already happened, not an instruction. */
.swing-why { color: var(--brass-600); font-size: var(--text-xs); font-style: normal; }
/* Both dice, beside the one that counted. */
.attack-pair { color: var(--brass-600); font-size: var(--text-xs); min-width: 3.5rem; }
.attack-dice { color: var(--brass-600); font-size: var(--text-xs); }

.sheet-attacks { list-style: none; padding: 0; margin: 0 0 0.4rem; display: flex; flex-direction: column; gap: 0.2rem; }
.sheet-attacks li { display: flex; align-items: center; gap: 0.4rem; font-size: var(--text-xs); color: var(--parchment); }
.sheet-attack { font-variant-numeric: tabular-nums; }
.sheet-spell-tags { color: var(--brass-500); font-size: var(--text-2xs); letter-spacing: 0.02em; }
.sheet-attack-remove { padding: 0 0.35rem; font-size: var(--text-xs); background: transparent; color: var(--oxblood-400); border: 1px solid var(--line); border-radius: var(--radius-small); }
.sheet-attack-add input[type="text"] { flex: 1; min-width: 0; font-size: var(--text-xs); }

/* ---- Sizes --------------------------------------------------------------
   A Large token is two squares wide and stays round; the board tells you
   what it is by how much of the floor it takes. */
.combatant-size { color: var(--brass-500); }
.sheet-size label { display: flex; align-items: center; gap: 0.5rem; font-size: var(--text-xs); color: var(--brass-500); text-transform: uppercase; letter-spacing: 0.06em; }
.initiative-add-form .add-size { flex: 0 0 7rem; }


/* ---- The layout pass ----------------------------------------------------
   Toward the reference: the side panels run the full height, the order and
   the turn sit above the board inside its column, and the controls are one
   row. Panels carry a title between two rules, the way a plate does. */
.panel-title {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-family: var(--font-display);
    font-size: var(--text-xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brass-400);
    margin: 0 0 0.4rem;
}
.panel-title::before, .panel-title::after { content: ""; flex: 1; height: 1px; background: var(--wood-700); }

.panel-center {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow-y: auto;
    gap: 0.5rem;
}
.center-head { display: flex; flex-direction: column; gap: 0.4rem; flex: 0 0 auto; }
.panel-center > .map-frame, .panel-center > .map-empty { flex: 0 0 auto; }

/* The strip's head: an arrow either side of the turn, on one slim line. */
.strip-head { display: flex; align-items: center; gap: 0.4rem; }
.strip-head .turn-indicator { flex: 1; padding: 0.35rem 0.8rem; font-size: var(--text-sm); }
.strip-scroll {
    flex: 0 0 auto;
    padding: 0.3rem 0.5rem;
    font-size: var(--text-xs);
    background: var(--soot-800);
    color: var(--brass-500);
    border: 1px solid var(--line); border-radius: var(--radius);
}
.strip-scroll:hover { color: var(--brass-300); border-color: var(--brass-500); }
.initiative-strip { scrollbar-width: thin; }

/* One row of controls, the add form taking what is left - but the turn group
   may be squeezed rather than overflow when there is not enough for it. */
.initiative-add-form .add-hp-input { flex: 0 0 5rem; }
.initiative-add-form .add-size { flex: 0 0 6rem; }

/* The party cards: the face first, the armor class on its corner, the name,
   the line and the hit points beside it (2026-10-01). */
.roster-row {
    height: auto;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "portrait body";
    align-items: center;
    gap: 0.35rem 0.6rem;
    padding: 0.5rem 0.6rem 0.4rem;
}
.roster-portrait { grid-area: portrait; width: 3.6rem; height: 3.6rem; }
.roster-body { grid-area: body; display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.roster-name { flex: none; font-family: var(--font-display); font-size: var(--text-sm); color: var(--brass-300); }
.roster-sub { font-size: var(--text-xs); color: var(--brass-600); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.roster-bar { display: flex; align-items: center; gap: 0.35rem; font-size: var(--text-2xs); }
.roster-bar-label { flex: 0 0 1.2rem; color: var(--brass-500); letter-spacing: 0.06em; }
.roster-bar .roster-hp-track { flex: 1; }
.roster-bar .roster-hp-values { flex: 0 0 auto; min-width: 2.6rem; text-align: right; }

/* The turn controls prefer one line and take a second when they must.

   Same fault as the add form below, found the same way and worth recording
   twice: this was nowrap when the row held START COMBAT, NEXT TURN, FREE
   MOVEMENT and a pencil. SCENES joined it in v0.20.0 and nothing checked the
   arithmetic again, so at 900px - a window beside Discord, which is the shape
   this is meant to be used in - PREP was cut off mid-word and SCENES was gone
   entirely. A control you cannot see is a control you do not have.

   The spacer is a fixed gap rather than a stretch, so wrapping does not leave
   it holding a line open on its own. */

/* A player cannot choose the DM's selected character, but their party is
   not grayed out for it - the cards are there to be read. */
.roster-row:disabled { opacity: 1; cursor: default; }
.roster-row:disabled:hover { border-color: var(--line); }
.map-pickup-form { display: inline; }
.map-pickup-btn { padding: 0.2rem 0.5rem; font-size: var(--text-xs); letter-spacing: 0.06em; }

/* The bar, tightened: five lines, not seven. */
.action-bar { gap: 0.2rem; padding: 0.35rem 0.7rem 0.45rem; }
/* Pinned to the foot of the board's column: whatever height the board does
   not take sits above the bar rather than inside it. See fitBoard. */
.panel-center > .action-bar { margin-top: auto; }
.action-bar .panel-title { margin-bottom: 0.15rem; }
.bar-row { padding: 0.12rem 0; gap: 0.25rem; }
.bar-head-tools { margin-left: auto; display: inline-flex; gap: 0.3rem; }
.bar-extra-btn { margin-left: 0; }
/* The player's NEXT TURN, in the brass the DM's card at the front of the
   strip wears, so the two controls that do one thing look like one thing. */
.bar-end-turn { background: var(--brass-500); color: var(--soot-900); border-color: var(--brass-400); font-weight: 600; }
.bar-end-turn:hover:not(:disabled) { background: var(--brass-400); color: var(--soot-900); border-color: var(--brass-400); }
.bar-rest-btn { align-self: auto; }
.bar-reactors { margin-left: auto; display: inline-flex; align-items: center; gap: 0.25rem; padding-left: 0.6rem; border-left: 1px dashed var(--wood-800); }
.bar-reactors::before { content: "others"; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-700); margin-right: 0.2rem; }

/* ---- The header, stacked --------------------------------------------------
   The title with the campaign and session under it, at the left, the way the
   reference reads. Same height as the single line it replaces. */
.site-header { padding: 0.45rem 2rem; }
/* The mark large, and the campaign beside it rather than under it - "Sandbox:
   Session 13" to the right, on the name's baseline (2026-10-01). */
.header-title { display: flex; flex-direction: row; align-items: center; gap: 1.1rem; line-height: 1.15; }
.header-title .site-id { font-size: 2.3rem; }
.header-title .header-center { font-size: var(--text-sm); letter-spacing: 0.06em; color: var(--brass-600); padding-top: 0.5rem; }


/* The reach ring in the color of whose reach it is. The faction token is
   mixed with transparency rather than restated, so the palette stays the
   one place these colors are named. */
.map-grid { --reach: var(--verdigris-500); }
.map-grid[data-reach="party"]   { --reach: var(--faction-party); }
.map-grid[data-reach="ally"]    { --reach: var(--faction-ally); }
.map-grid[data-reach="neutral"] { --reach: var(--faction-neutral); }
.map-grid[data-reach="enemy"]   { --reach: var(--faction-enemy); }
.map-cell.is-reachable {
    background-color: color-mix(in srgb, var(--reach) 18%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--reach) 32%, transparent);
}
.map-cell.is-reachable:hover { background-color: color-mix(in srgb, var(--reach) 34%, transparent); }
.map-readout[data-reach="party"]   .map-reach-label { color: var(--faction-party); }
.map-readout[data-reach="ally"]    .map-reach-label { color: var(--faction-ally); }
.map-readout[data-reach="neutral"] .map-reach-label { color: var(--faction-neutral); }
.map-readout[data-reach="enemy"]   .map-reach-label { color: var(--faction-enemy); }

/* ---- Condition icons ------------------------------------------------------
   Inline SVG, so an icon is whatever color the badge around it is: brass
   for a hindrance, oxblood for a condition that takes the creature out of
   the fight. Sized to the text beside it, in every place a condition is
   named. */
:root {
    --condition: var(--brass-400);
    /* Deliberately not used. Severity is a fill now, not a hue - see the
       is-dire rule below for why hue could not carry it. Kept as the name of
       the idea so the next person does not reintroduce it by reaching for
       oxblood; delete it if that feels like a museum piece. */
    --condition-dire: var(--oxblood-400);
}
.cond-icon { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; flex: 0 0 auto; }
.condition-badge { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--condition); font-style: normal; }
.condition-badge .cond-icon { width: 1.1em; height: 1.1em; }
.chip-condition-badge { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--condition); }
.chip-back-condition { display: inline-flex; align-items: center; justify-content: center; color: var(--condition); }
.chip-back-condition .cond-icon { width: 1.25em; height: 1.25em; vertical-align: 0; }
.cond-cell { white-space: nowrap; }
/* The name ticks the box. It is a <label> rather than a <span> now, and the
   cursor has to say so - a word that does something and looks like a word is
   the same complaint section 16 makes about a grayed control, from the other
   side. */
.cond-cell label { display: inline-flex; align-items: center; cursor: pointer; }
.cond-cell .cond-icon { margin-right: 0.35rem; color: var(--brass-500); }

/* ---- The armor class, on the card that can change it ----

   A number, a box to correct it in, and what is stacked on top tonight. The
   total is what an attack is rolled against; the box holds the BASE, because
   the base is the thing a person knows ("I put the plate on") and the total
   is the thing Rollies works out. */
.ac-block {
    display: grid;
    /* minmax(0, 1fr) rather than the `auto` a grid gives you for free, the
       same reason .panel-left says it further up: an auto column is as wide
       as its content's minimum, so the two rows below - each a flex line with
       a fixed-width number input and a button that will not shrink - pushed
       this column seven pixels wider than the box drawn around it, and both
       buttons sat outside their own border.

       The two controls are named because three rules have to agree about
       them: the number input on each row, and the button at the end of each.
       A SET that is 49px wide over an ADD that is 53px is four pixels of
       raggedness in the middle of the one number a character carries all
       session. */
    --ac-control: 3.4rem;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.3rem;
    margin: 0.5rem 0 0.2rem;
    padding: 0.4rem 0.5rem;
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--soot-900);
}
.ac-head { display: flex; align-items: center; gap: 0.5rem; }
.ac-label {
    font-family: var(--font-display);
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    color: var(--brass-600);
}
.ac-set-form { display: flex; gap: 0.25rem; margin-left: auto; min-width: 0; }
.ac-set-form input { width: var(--ac-control); flex: 0 0 auto; }
.ac-mods { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.ac-mod {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.1rem 0.35rem;
    border: 1px solid var(--brass-700);
    border-radius: var(--radius-small);
    font-size: var(--text-xs);
    color: var(--brass-400);
}
.ac-mod-amount { font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums; }
.ac-mod-note { color: var(--parchment); }
.ac-mod form { display: inline; }
.ac-mod button {
    background: none;
    border: none;
    color: var(--brass-700);
    cursor: pointer;
    padding: 0 0.1rem;
    font-size: var(--text-sm);
    line-height: 1;
}
.ac-mod button:hover { color: var(--oxblood-400); }
/* An item's modifier while it is not in the number: bracers under a shield. */
.ac-mod.is-off { opacity: 0.5; }
.ac-mod.is-off .ac-mod-note { text-decoration: line-through; }
.ac-mod-form { display: flex; gap: 0.25rem; min-width: 0; }
.ac-mod-form input[type="number"] { width: var(--ac-control); flex: 0 0 auto; }
/* A 6rem basis rather than `auto`, so what the box WANTS is something that
   fits rather than the twenty characters an input asks for by default. */
.ac-mod-form input[type="text"] { flex: 1 1 6rem; min-width: 0; }
/* One width for both, so SET sits exactly over ADD and the field to the left
   of each ends on the same pixel. The buttons are the right-hand column of a
   two-row grid that is not a grid, and this is what makes them read as one. */
.ac-set-form button,
.ac-mod-form button { width: var(--ac-control); flex: 0 0 auto; padding-left: 0; padding-right: 0; }

/* ---- Dying, where the table is already looking ----

   The state had one reader: the detail card of whoever was selected. Now it
   is a mark on the initiative card and on the roster row, which are the two
   places a DM's eye is during a fight.

   Oxblood for dying, verdigris for stable, and neither of them carries the
   meaning alone - `ux.md` section 3 asks every signal for a shape as well as
   a hue, and the shape is the Unconscious glyph, which is the same glyph the
   condition badge uses for the same fact. The word rides along in the
   document for whoever is listening rather than looking. */
.dying-mark {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    color: var(--oxblood-400);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
}
.dying-mark.is-stable { color: var(--verdigris-400); }
.dying-mark.is-dead { color: var(--oxblood-500); }
/* Private: brass, because this mark is reporting a fact rather than sounding
   an alarm. The glyph is the same one - what a viewer under this rule knows
   is that the character is on the floor, which is what the glyph has always
   said - and the word beside it is DOWN rather than DYING. */
.dying-mark.is-private { color: var(--brass-500); }
/* The numerals read under pressure, which is the hedge section 2 asks for
   against Georgia at small sizes. */
.dying-count { font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums; }
.dying-mark .cond-icon { width: 1.15em; height: 1.15em; }

/* On a card 4.6rem wide the word does not fit and does not need to: a strip
   across the foot of the portrait, the glyph, and how many failures stand.
   Opaque rather than translucent, because it is drawn over a photograph and
   a wound tint at full strength. */
.chip-front .dying-mark {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    justify-content: center;
    gap: 0.2rem;
    padding: 0.12rem 0;
    background: var(--soot-900);
    border-top: 1px solid currentColor;
}

.chip-front .dying-word {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* The roster row has the width for the word, and the roster is what a DM
   reads down rather than across. */
.roster-row .dying-mark { margin-top: 0.2rem; }

/* Each condition in its own color. The badge and the icon inside it read
   the same variable, set by the class the badge carries; the dire five keep
   a heavier border on top of their color, so what takes a creature out of
   the fight still reads as that at a glance. */
.cond-blinded       { --cond: var(--cond-blinded); }
.cond-charmed       { --cond: var(--cond-charmed); }
.cond-deafened      { --cond: var(--cond-deafened); }
.cond-exhausted     { --cond: var(--cond-exhausted); }
.cond-frightened    { --cond: var(--cond-frightened); }
.cond-grappled      { --cond: var(--cond-grappled); }
.cond-incapacitated { --cond: var(--cond-incapacitated); }
.cond-invisible     { --cond: var(--cond-invisible); }
.cond-paralyzed     { --cond: var(--cond-paralyzed); }
.cond-petrified     { --cond: var(--cond-petrified); }
.cond-poisoned      { --cond: var(--cond-poisoned); }
.cond-prone         { --cond: var(--cond-prone); }
.cond-restrained    { --cond: var(--cond-restrained); }
.cond-stunned       { --cond: var(--cond-stunned); }
.cond-unconscious   { --cond: var(--cond-unconscious); }

.condition-badge, .chip-condition-badge, .chip-back-condition { color: var(--cond, var(--condition)); }

/* SEVERITY IS A FILL, NOT A HUE, and that is the whole of this decision.

   It used to be `color: var(--cond, var(--condition-dire))`, which never fired:
   the fallback only applies when `--cond` is UNSET, and every condition with an
   icon sets it. So the oxblood `icons.py` still describes had been silently
   overridden for the life of the feature - unconscious rendered blue-violet,
   paralyzed yellow - and all that survived of the dire five was a border a
   pixel heavier, which is not a thing you read across a table.

   Hue could not be taken back for severity either, because hue is carrying
   identity here AND because the two colors severity wants - oxblood for dire,
   verdigris for its opposite - are `--faction-enemy` and `--faction-party`. The
   palette's own rule is that a faction color means that faction wherever it
   appears, so a stunned party member drawn in oxblood would be a party member
   wearing enemy red.

   So severity inverts the badge instead. Filled against outlined survives a
   red-green deficiency, survives distance, and cannot be mistaken for a faction
   because a faction is a border on a card or a ring on a token and is never a
   fill. The identity hue does double duty as the ground, so each dire condition
   is still its own color - and the contrast fix below makes the inverted form
   legible for free, since a contrast ratio is symmetric. */
.condition-badge.is-dire,
.chip-condition-badge.is-dire,
.chip-back-condition.is-dire {
    color: var(--soot-900);
    background: var(--cond, var(--condition));
    border-color: var(--cond, var(--condition));
    border-width: 2px;
    box-shadow: 0 0 6px color-mix(in srgb, var(--cond, var(--condition)) 45%, transparent);
}
.condition-badge {
    padding: 0.1rem 0.45rem 0.1rem 0.35rem;
    border: 1px solid color-mix(in srgb, var(--cond, var(--condition)) 55%, transparent);
    border-radius: var(--radius-small);
    background: color-mix(in srgb, var(--cond, var(--condition)) 10%, transparent);
}
.chip-condition-badge { border-color: color-mix(in srgb, var(--cond, var(--condition)) 60%, transparent); background: color-mix(in srgb, var(--cond, var(--condition)) 10%, transparent); }
.chip-back-condition { border: 1px solid color-mix(in srgb, var(--cond, var(--condition)) 60%, transparent); border-radius: var(--radius-small); background: color-mix(in srgb, var(--cond, var(--condition)) 12%, transparent); }
.cond-cell .cond-icon { color: var(--cond, var(--brass-500)); }

/* The spell picker: the list, and the text box it reveals for a spell not
   on it. */
.spell-pick { max-width: 100%; }
.spell-other { margin-top: 0.3rem; }
.react-cast .spell-other { margin-top: 0; width: 9rem; }

/* ---- Inventory ------------------------------------------------------------
   The reference's last panel: a row of kind chips, the list, and how much
   of the carrying capacity it comes to. */
.gear-filter { display: flex; flex-wrap: wrap; gap: 0.25rem; margin: 0.3rem 0 0.5rem; }
.gear-chip {
    padding: 0.15rem 0.5rem;
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    background: var(--soot-800);
    color: var(--brass-500);
    border: 1px solid var(--line); border-radius: var(--radius-small);
}
.gear-chip.is-on { background: var(--brass-600); color: var(--soot-900); border-color: var(--brass-500); }
.gear-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; overflow-y: auto; min-height: 0; }
/* The filter sets `hidden`, which `display: grid` below would overrule - the
   chips marked every other item hidden and nothing disappeared. */
.gear-item[hidden] { display: none; }
.gear-chip-n { opacity: 0.7; font-size: 0.9em; }
.gear-item {
    display: grid;
    grid-template-columns: 1.6rem minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 0.5rem;
    padding: 0.3rem 0.5rem;
    border: 1px solid var(--wood-800); border-radius: var(--radius);
    background: var(--soot-800);
    font-size: var(--text-xs);
}
.gear-item.is-attuned { border-color: var(--verdigris-700); }
.gear-kind {
    width: 1.6rem; height: 1.6rem;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--line); border-radius: var(--radius-small);
    font-family: var(--font-display); font-size: var(--text-xs); color: var(--brass-400);
}
.gear-body { display: flex; flex-direction: column; min-width: 0; }
.gear-name { color: var(--parchment); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gear-attuned { color: var(--verdigris-400); }
.gear-sub { font-size: var(--text-2xs); color: var(--brass-600); }
.gear-qty { font-variant-numeric: tabular-nums; color: var(--brass-300); min-width: 1.5rem; text-align: right; }
.gear-tools { display: inline-flex; gap: 0.2rem; }
.gear-btn { padding: 0.05rem 0.35rem; font-size: var(--text-2xs); background: var(--soot-700); color: var(--brass-400); border: 1px solid var(--line); border-radius: var(--radius-small); }
.gear-btn:hover { color: var(--brass-300); border-color: var(--brass-500); }
.gear-btn.gear-attunes { opacity: 0.6; border-style: dashed; }
.gear-btn.gear-attunes:hover { opacity: 1; }
.gear-remove { color: var(--oxblood-400); }
.gear-add { display: flex; gap: 0.3rem; margin-top: 0.5rem; }
.gear-add input[type="text"] { flex: 1; min-width: 0; font-size: var(--text-xs); }
/* What an item typed without a weight is probably like (rules/weights.py):
   quiet, under its box, and only once a name is typed. */
.weigh-hint { display: flex; flex-direction: column; gap: 0.15rem; margin-top: 0.2rem;
    font-size: var(--text-xs); color: var(--brass-600); }
.weigh-hint[hidden] { display: none; }
.weigh-hint-row { display: block; }
.gear-foot { margin-top: 0.6rem; padding-top: 0.4rem; border-top: 1px solid var(--wood-800); font-size: var(--text-xs); color: var(--brass-500); display: grid; grid-template-columns: 1fr auto; gap: 0.3rem 0.6rem; align-items: center; }
.gear-attune-label { color: var(--verdigris-400); }
.gear-enc-track { grid-column: 1 / -1; height: 0.4rem; background: var(--soot-900); border: 1px solid var(--line); border-radius: var(--radius-small); overflow: hidden; }
.gear-enc-fill { display: block; height: 100%; background: var(--brass-500); }
.gear-enc-fill.is-over { background: var(--oxblood-500); }
/* Magic items that count (rules/item_effects): the line under a name while
   its effect is in force, WEAR lit while worn, and the one question asked
   before an item touches the armor class, on a row of its own. */
.gear-effect { font-size: var(--text-2xs); color: var(--verdigris-400); }
.gear-btn.is-on { color: var(--verdigris-400); border-color: var(--verdigris-700); }
.gear-ask { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
.gear-ask[hidden] { display: none; }
.gear-ask-q { flex: 1 1 10rem; min-width: 0; color: var(--parchment); }

/* A bag of holding: under the kit, the party's to see into. One quiet box -
   the name, the pounds against the limit and a hairline bar in the summary,
   the contents as small rows, one line to put something in. The bar is the
   encumbrance bar's colors so the two read as the same kind of number. */
.bag { margin-top: 0.5rem; padding: 0.35rem 0.5rem; border: 1px solid var(--wood-800); border-radius: var(--radius); background: var(--soot-800); font-size: var(--text-xs); }
.bag.is-full { border-color: var(--oxblood-500); }
.bag-head { cursor: pointer; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.25rem 0.5rem; align-items: center; list-style: none; }
.bag-head::-webkit-details-marker { display: none; }
.bag-name { color: var(--parchment); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bag-who { color: var(--brass-600); }
.bag-load { font-variant-numeric: tabular-nums; color: var(--brass-400); }
.bag-track { grid-column: 1 / -1; height: 0.25rem; background: var(--soot-900); border: 1px solid var(--line); border-radius: var(--radius-small); overflow: hidden; }
.bag-fill { display: block; height: 100%; background: var(--brass-500); }
.bag.is-full .bag-fill { background: var(--oxblood-500); }
.bag-warn { margin-top: 0.3rem; color: var(--oxblood-400); }
.bag-list { list-style: none; margin: 0.35rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.bag-item { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 0.4rem; align-items: center; }
.bag-item-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bag-take { display: inline-flex; gap: 0.2rem; align-items: center; margin: 0; }
.bag-take input[type="number"], .bag-put input[type="number"] { width: 3.2rem; font-size: var(--text-2xs); padding: 0.05rem 0.25rem; }
.bag-empty { margin-top: 0.3rem; color: var(--brass-600); }
.bag-put { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; margin-top: 0.4rem; }
.bag-put select { flex: 1 1 8rem; min-width: 0; max-width: 20rem; font-size: var(--text-2xs); }
.bag-rift { flex-basis: 100%; font-size: var(--text-2xs); color: var(--oxblood-400); }
.bag-note { margin-top: 0.35rem; font-size: var(--text-2xs); color: var(--brass-600); }

/* The controls never wrap inside their own button. The turn group keeps its
   width while there is width to keep, and gives way when there is not.

   It was flex-shrink: 0 - "the turn group keeps its width; the add form is
   what gives" - which is right until the column is narrower than the group
   itself. At 900px the group wants 305px inside a 188px column, and a thing
   that cannot shrink does not wrap either: it simply overflows, so PREP was
   cut off mid-word and SCENES was past the edge. Letting it shrink is what
   lets the flex-wrap above actually do something. */

/* Nobody acting: REST is a button, not a bar. */
.action-bar > .bar-rest-btn { align-self: flex-start; }

/* The stage fills its panel and the viewport inside it does the clipping. This
   used to carry a second copy of the width cap deleted further up - the one on
   `.map-frame` - and it was the copy that actually bound, so the board was
   still being squeezed to 396px after the first was gone. Two rules doing one
   job, in two places, is how a fix half-lands. */
.map-stage { max-width: 100%; }
.history-btn { background-color: color-mix(in srgb, var(--soot-900) 85%, transparent); }

/* ---- A 15-inch screen ------------------------------------------------------
   The whole table on one screen, no scrolling: the party and the selected
   character's sheet down the left, the board and the bar in the middle,
   the log, the notes and the kit down the right in three equal cards. */

/* The left column: the roster takes what the sheet leaves, and scrolls only
   if the party will not fit; the sheet is pinned to the bottom, whole. */
.panel-left {
    display: grid;
    /* One track, because there is one list.

       It was three - roster, sheet, and an empty third to catch the leftover -
       back when the open character's card was a second panel pinned below the
       roster. The card is a row in the list now, so the roster is the whole
       column and takes all of it; the leftover it used to need a track for is
       the scroll it has instead. */
    grid-template-rows: minmax(0, 1fr);
    gap: 0.6rem;
}
.party-roster { min-height: 0; overflow-y: auto; gap: 0.3rem; scrollbar-width: thin;
                border-bottom: 2px dotted transparent; transition: border-color 0.2s; }
/* MORE BELOW. The roster's bottom edge is level with the action card's, but
   with nothing drawn there the next character's card looked sliced off by the
   window (2026-10-06). A dotted line on the edge, only while there is more to
   scroll: a boundary that is respected, and a hint that it is not the end.
   At the end of the list it fades, so it never promises more than there is. */
.party-roster.has-more { border-bottom-color: var(--brass-500); }

/* The party cards, compact. */
.roster-row {
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0.2rem 0.5rem;
    padding: 0.35rem 0.5rem 0.3rem;
}
.roster-portrait { width: 3.4rem; height: 3.4rem; }
.roster-body { gap: 0.08rem; }
.roster-name { font-size: var(--text-xs); }
.roster-sub { font-size: var(--text-2xs); }
.roster-bar { font-size: var(--text-2xs); gap: 0.3rem; }
.roster-bar .roster-hp-track { height: 5px; }
.roster-bar .roster-hp-values { min-width: 2.4rem; }

/* The sheet, compact: the same card, less air. */
.detail-card { padding: 0.7rem 0.8rem 0.6rem; }
/* The open card's header: a tall portrait, and beside it the four things
   somebody reads at a glance - who they are, who plays them, what they are,
   and the two numbers. `align-items: flex-start` because the facts stack from
   the top of the face rather than centering against it; a name floating level
   with the middle of a portrait reads as a caption. */
.detail-card .card-header { margin-bottom: 0.35rem; align-items: flex-start; gap: 0.6rem; }
.detail-card .card-header-text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.1rem;
    min-width: 0;
    flex: 1 1 auto;
}
.detail-card .card-header-text h2 { margin: 0; }
/* The two numbers, each a glyph and a value. A shape as well as a color, the
   way ux.md section 3 asks: a heart and a shield are what they are at a
   glance and across a table, and neither depends on being red or being blue. */
.head-stat {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--text-md);
    line-height: 1.2;
}
.head-glyph { width: 0.95rem; height: 0.95rem; flex: 0 0 auto; fill: currentColor; }
.head-hp { color: var(--hp-good); margin-top: 0.3rem; }
.head-ac { color: var(--brass-300); }
/* Modified reads in the brighter brass, the same distinction the number in
   the block below used to draw: this is not what is on the sheet tonight. */
.head-ac.is-modified { color: var(--brass-200); }
.head-of { color: var(--brass-600); }
.head-value { letter-spacing: 0.02em; }
/* The face, at the size a face is worth. It was 2.8rem - smaller than the
   compressed roster row's, on the one card that has room - which made the
   open character the least visible person in the column. The card has the
   height to spend now that its box fills the leftover space, and a portrait
   is the only thing on this sheet that is a picture rather than a number. */
/* About a third of the card's height at the sizes this column actually runs
   at, and portrait-shaped, which is why the facts sit beside it rather than
   under it - a face this tall with nothing next to it leaves a column of
   empty card to its right. */
.detail-card .card-portrait { width: 7.2rem; height: 8.8rem; }
.detail-card h2 { font-size: var(--text-md); line-height: 1.1; }
.detail-card .subtitle { font-size: var(--text-xs); margin-bottom: 0.4rem; }
.detail-card .ability-scores { gap: 0.25rem; margin-bottom: 0.45rem; }
.detail-card .ability { padding: 0.2rem 0.1rem; }
.detail-card .ab-label { font-size: var(--text-2xs); }
.detail-card .ab-score { font-size: var(--text-md); line-height: 1.1; }
.detail-card .ab-mod { font-size: var(--text-2xs); }
.detail-card .combat-controls { gap: 0.3rem; }
.detail-card .combat-controls input, .detail-card .combat-controls button, .detail-card .conditions-toggle {
    padding: 0.25rem 0.45rem;
    font-size: var(--text-xs);
}
.detail-card .active-conditions { margin-top: 0.35rem; }

/* The right column: three cards in equal thirds. Each keeps its title and
   scrolls its body, so the log growing does not push the kit off screen. */
.panel-right {
    display: grid;
    grid-template-rows: repeat(3, minmax(0, 1fr));
    gap: 0.6rem;
}
.side-card {
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* A grid item is as wide as its widest row unless told otherwise, and the
       notes row - a box, PRIVATE (you and the DM), ADD - is 371px. In a 300px
       track that pushed the whole right column off the screen at every width
       the three columns are shown at. */
    min-width: 0;
    padding: 0.4rem 0.6rem 0.5rem;
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--soot-800);
}
.side-card .panel-title { margin-bottom: 0.3rem; flex: 0 0 auto; }
.side-card .tab-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-width: thin; }
.side-card .panel-placeholder { padding: 0.6rem 0.8rem; font-size: var(--text-xs); min-height: 0; flex: 0 0 auto; }
.side-card[data-panel] { }
/* the inventory card: the list is what scrolls; chips, add and the footer stay. */
.side-card .gear-list { flex: 1 1 auto; }
.side-card .tab-body[data-panel="inventory"] { display: flex; flex-direction: column; }
.side-card .gear-filter { margin: 0.1rem 0 0.3rem; flex: 0 0 auto; }
.side-card .gear-add, .side-card .gear-foot { flex: 0 0 auto; }
.side-card .gear-foot { margin-top: 0.4rem; }
.side-card .notes-for { flex: 0 0 auto; }

/* The add form is a row of its own, and it takes a second line rather than
   running off the side.

   It was nowrap - "one line, the name box giving way" - and that was honest
   when the row held six controls. It now holds nine: an initiative box and a
   roll tick, a count, and the button. Everything except the name box is a
   fixed width, the name box stops shrinking at 6rem, and the remainder simply
   overflowed. Seen at 1200px: the size select was cut off mid-word and + ADD
   was off the edge of the column entirely, so a DM could not add a combatant
   at all. That is also the half-a-monitor case the roadmap cares about, since
   Rollies is meant to sit beside Discord rather than fill a screen.

   Wrapping alone would break wherever the arithmetic happened to land, so the
   two pairs that must never be split are groups of their own: the initiative
   box with its roll tick, and the count with the button it multiplies. */
.add-group { display: inline-flex; align-items: center; gap: 0.4rem; flex: 0 0 auto; }

/* Five party cards and the sheet on an 800-pixel-tall viewport: a little
   less air in each card. */
.roster-row { padding: 0.25rem 0.45rem 0.2rem; gap: 0.08rem 0.45rem; }
.roster-portrait { width: 3rem; height: 3rem; }
.roster-body { gap: 0.03rem; }
.roster-sub { font-size: var(--text-2xs); }
.roster-bar { font-size: var(--text-2xs); }
.roster-bar .roster-hp-track { height: 4px; }
.party-roster { gap: 0.25rem; }

/* ---- The bar as tiles ------------------------------------------------------
   Short and wide: a strip of icon tiles per kind, the icon over a small
   caption. Hover fills the tile a little and brightens the icon, so the
   pointer's place is never in doubt. */
/* Seven tiles in four groups, the icon over its word. The groups are the
   layout: whitespace between them says "these belong together" without a
   border, a heading or a color. The attack is the primary action and is the
   one thing drawn larger. */
/* ONE GRID FOR BOTH ROWS, the width of the card. Seven equal columns, so a
   bonus-action tile sits under an action tile, every gap is the same gap,
   and the tiles use the room the card has instead of leaving half of it
   empty. The groups stay in the order they were written; the spacing no
   longer tries to draw them. */
.act-tiles { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.3rem; flex: 1 1 auto; min-width: 0; }
.act-group, .act-tiles .bar-form { display: contents; }
.act-tile {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    width: auto;
    min-width: 0;
    padding: 0.25rem 0.1rem 0.2rem;
    background: var(--soot-800);
    color: var(--brass-400);
    border: 1px solid var(--wood-800);
    border-radius: var(--radius-small);
    transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
/* EVERY TILE AND EVERY PICTURE ONE SIZE. Only the attack's icon had a size,
   and a small one, while every other icon filled its tile; drawing the attack
   a step larger instead was the same inconsistency the other way round. The
   attack is told apart by its place (first) and its brighter color, not by
   being a different shape of box. */
.act-tile > svg { width: 1.75rem; height: 1.75rem; flex: 0 0 auto; }
/* One height too: a tile inside a form does not stretch like a bare one. */
.act-tile { height: 3.4rem; }
.act-tile-primary { color: var(--brass-300); }

.act-caption { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-500); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.act-tile-primary .act-caption { color: var(--brass-400); }
.act-tile:hover:not(:disabled), .act-tile:focus-visible {
    background: color-mix(in srgb, var(--brass-500) 14%, var(--soot-800));
    border-color: var(--brass-500);
    color: var(--brass-200);
}
.act-tile:hover:not(:disabled) .act-caption { color: var(--brass-400); }
.act-tile:active:not(:disabled) { background: color-mix(in srgb, var(--brass-500) 26%, var(--soot-800)); }
.act-tile:disabled { color: var(--wood-600); border-color: var(--wood-800); cursor: default; }
.act-tile:disabled .act-caption { color: var(--wood-600); }

/* The Other dialog's named row is for the action alone; the same dialog
   serves the bonus action and the reaction, which have no Hide. */
.other-named { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.6rem; }
.other-modal[data-kind]:not([data-kind="action"]) .other-named { display: none; }


/* ---- The bar, short and wide ---------------------------------------------
   The head carries the name, the glyphs, the slots and the class's
   resources on one line; under it, one strip of action tiles, then the
   bonus and reaction strips side by side. No plates: the head says whose
   bar it is. */
.bar-head { flex-wrap: wrap; gap: 0.4rem 0.6rem; }
.bar-strips { display: flex; flex-direction: column; gap: 0.15rem; }
.bar-strips .bar-row { border-top: 0; padding: 0.1rem 0; align-items: flex-start; }
.bar-strips .bar-kind { margin-top: 0.7rem; min-width: 1.3rem; }
.bar-minor { display: flex; flex-wrap: wrap; gap: 0.2rem 1.2rem; }
.bar-minor .bar-row { flex: 1 1 100%; }

.bar-reactors { margin-left: 0; padding-left: 0.6rem; align-self: center; }


.cast-heals { display: flex; align-items: center; gap: 0.4rem; }
/* What the picked spell does: a line saying so, then a row per dart, ray or
   beam. The rows are the attack dialog's own .attack-roll, a target first;
   the dice sit in a box the caster may change before rolling ("3d10" for a
   monster's cantrip at its own level). */
/* A flex box ignores the hidden attribute unless told; these are shown and
   hidden by what the spell does. */
.cast-modal [hidden] { display: none; }
.cast-effect { display: flex; flex-direction: column; gap: 0.4rem; }
.cast-effect-what { margin: 0; color: var(--brass-300); font-size: var(--text-sm); }
.cast-bonus { flex-direction: row; align-items: center; gap: 0.5rem; }
.cast-bonus input { width: 4.2rem; flex: 0 0 auto; }
.cast-custom { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.cast-strikes { display: flex; flex-direction: column; gap: 0.35rem; }
.cast-strike { flex-wrap: wrap; }
.cast-saves { display: flex; flex-direction: column; gap: 0.4rem; }
.cast-save-damage { display: flex; flex-direction: column; gap: 0.3rem; }
.cast-save-damage input { width: 8.5rem; }
.cast-save-targets { border: 0; padding: 0; margin: 0; }
.cast-save-targets > div { display: flex; flex-wrap: wrap; gap: 0.2rem 0.9rem; }
/* The DM's review of a save spell: the call panel's frame and table, with a
   number box where Rollies' figure can be typed over. */
.cast-review-damage { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; margin-bottom: 0.4rem; }
.cast-review-table input[type="number"] { width: 4.2rem; }
.cast-review-table .is-pass { color: var(--hp-good); }
.cast-review-table .is-fail { color: var(--danger); }
.cast-review-table small { display: block; color: var(--brass-600); }
.cast-review-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.5rem; }
.cast-strike select { flex: 1 1 9rem; min-width: 0; }
.cast-strike input.attack-dice { width: 4.6rem; color: var(--parchment); font-size: var(--text-xs); }
.cast-strike input[name="strike_damage"] { width: 5.2rem; }
.modal-field-label { font-size: var(--text-xs); color: var(--brass-500); text-transform: uppercase; letter-spacing: 0.06em; }
.cast-heals-amount { width: 4.2rem; }
.cast-heal-target { flex: 1; }
.session-advance { display: flex; flex-wrap: wrap; gap: 0.3rem 0.7rem; align-items: center; }

/* ---- The Chronicle -------------------------------------------------------- */
.chronicle { max-width: 64rem; margin: 0 auto; display: flex; flex-direction: column; gap: 0.8rem; }
.chronicle-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem 1.2rem; }
.chronicle-title { font-family: var(--font-display); font-size: var(--text-lg); letter-spacing: 0.12em; color: var(--brass-300); margin: 0; }
.chronicle-sub { margin: 0; font-size: var(--text-sm); color: var(--brass-600); flex: 1; }
/* Below a laptop's width the line under a page's name goes under it, rather
   than into a column beside it nine lines tall. */
@media (max-width: 1000px) { .chronicle-sub { flex-basis: 100%; } }
.chronicle-import { display: flex; gap: 0.4rem; align-items: center; font-size: var(--text-xs); }
.chronicle-card { padding: 0.6rem 0.9rem 0.8rem; }
.chronicle-stats { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; font-size: var(--text-xs); color: var(--brass-500); align-items: center; }
.chronicle-stats b { color: var(--parchment); }
.chronicle-export { margin-left: auto; font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--brass-400); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.15rem 0.5rem; text-decoration: none; }
.chronicle-export:hover { border-color: var(--brass-500); color: var(--brass-300); }
.chronicle-table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; font-size: var(--text-xs); }
.chronicle-table th { text-align: left; font-weight: normal; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); padding: 0.2rem 0.4rem; border-bottom: 1px solid var(--wood-800); }
.chronicle-table td { padding: 0.25rem 0.4rem; border-bottom: 1px solid var(--wood-800); color: var(--parchment); }
.chronicle-board-title { font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-500); margin-bottom: 0.2rem; }
.chronicle-board ol { margin: 0; padding-left: 1.2rem; font-size: var(--text-xs); }
.chronicle-board li { display: flex; justify-content: space-between; gap: 0.5rem; color: var(--parchment); }
.chronicle-board li b { color: var(--brass-300); font-variant-numeric: tabular-nums; }

/* ---- Death saves ------------------------------------------------------------
   Three pips each way on the dying character's card - verdigris filling for
   successes, oxblood for failures - and the four things a death save can be. */
.death-saves { margin-top: 0.4rem; padding: 0.35rem 0.5rem; border: 1px solid var(--oxblood-600); border-radius: var(--radius-small); background: color-mix(in srgb, var(--oxblood-600) 10%, var(--soot-900)); }
.death-saves.is-stable { border-color: var(--verdigris-600); background: color-mix(in srgb, var(--verdigris-600) 10%, var(--soot-900)); }
.death-saves.is-dead { border-color: var(--line); background: var(--soot-900); }
.death-saves-head { display: flex; align-items: center; gap: 0.6rem; }
.death-saves-title { font-family: var(--font-display); font-size: var(--text-xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--oxblood-400); flex: 1; }
.death-saves.is-stable .death-saves-title { color: var(--verdigris-400); }
.death-saves.is-dead .death-saves-title { color: var(--wood-600); }
.death-pips { display: inline-flex; gap: 0.2rem; }
.death-pip { width: 0.6rem; height: 0.6rem; border-radius: 50%; border: 1px solid var(--line); background: transparent; }
.death-pip.is-success.is-on { background: var(--verdigris-500); border-color: var(--verdigris-500); }
.death-pip.is-failure.is-on { background: var(--oxblood-500); border-color: var(--oxblood-500); }
/* The table's half of a private death save. Drawn in the furniture rather
   than in oxblood: a red-bordered panel with nothing in it reads as alarm
   about a fact the reader has not been given, where this is meant to read as
   a door somebody else is behind. */
.death-saves.is-private { border-color: var(--line); background: var(--soot-900); }
.death-saves.is-private .death-saves-title { color: var(--brass-500); }
.death-saves-note { font-size: var(--text-2xs); color: var(--brass-700); font-style: normal; }

.death-saves-form { display: flex; gap: 0.25rem; margin-top: 0.35rem; }
.death-btn { flex: 1; padding: 0.2rem 0.2rem; font-size: var(--text-2xs); letter-spacing: 0.06em; background: var(--soot-800); border: 1px solid var(--line); border-radius: var(--radius-small); color: var(--parchment); }
.death-btn.is-success:hover { border-color: var(--verdigris-500); color: var(--verdigris-400); }
.death-btn.is-failure:hover { border-color: var(--oxblood-500); color: var(--oxblood-400); }
.attribution { margin: 1.5rem auto 0; max-width: 28rem; text-align: center; font-size: var(--text-2xs); line-height: 1.5; color: var(--brass-700); }
.attribution a { color: var(--brass-600); }

/* ---- the analytics page: charts as inline SVG over the chronicle's cards ---- */
.analytics { max-width: 72rem; }
.an-scopes { display: flex; flex-wrap: wrap; gap: 0.3rem; width: 100%; }
.an-scope { font-size: var(--text-xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-400); border: 1px solid var(--line); border-radius: var(--radius-small); padding: 0.15rem 0.55rem; text-decoration: none; }
.an-scope:hover { border-color: var(--brass-500); color: var(--brass-300); }
.an-scope.is-on { color: var(--soot-900); background: var(--brass-400); border-color: var(--brass-400); }
.an-scope-label { font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); align-self: center; margin-left: 0.5rem; }
/* ---- the recap: the DM's words beside the log's numbers ---------------- */
.recap { margin: 0.5rem 0 0.6rem; padding: 0.5rem 0.7rem; border: 1px solid var(--wood-800); border-radius: var(--radius); border-left: 3px solid var(--line); }
.recap.is-shared { border-left-color: var(--brass-500); }
.recap-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.8rem; }
.recap-title { font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-500); }
.recap-state { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-600); }
.recap-share { margin-left: auto; }
.recap-share button { font-size: var(--text-2xs); letter-spacing: 0.06em; }
.recap-body { margin-top: 0.4rem; font-size: var(--text-sm); }
/* The leak check: a margin mark before each line, never colored text. */
.recap-leaks { margin-top: 0.6rem; border-left: 2px solid var(--brass-600); padding-left: 0.7rem; }
.recap-leaks-head { margin: 0 0 0.3rem; font-size: var(--text-xs); color: var(--brass-400); }
.recap-leak-lines { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.recap-leak { display: grid; grid-template-columns: auto 1fr; column-gap: 0.5rem; font-size: var(--text-sm); }
.recap-leak-mark { font-weight: 700; color: var(--brass-300); }
.recap-leak-line { color: var(--parchment); }
.recap-leak-why { grid-column: 2; font-size: var(--text-xs); color: var(--brass-600); }
.recap-leak-why a { color: var(--brass-400); }
.recap-edit { margin-top: 0.4rem; }
.recap-edit-summary { cursor: pointer; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-500); }
.recap-form { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.4rem; }
.recap-form textarea { width: 100%; font-family: inherit; font-size: var(--text-sm); line-height: 1.45; }
.recap-form-row { display: flex; align-items: center; gap: 0.7rem; }
.recap-hint { font-size: var(--text-xs); color: var(--brass-600); }
.recap-log { margin-top: 0.4rem; }
.recap-draft { white-space: pre-wrap; font-size: var(--text-xs); line-height: 1.45; color: var(--brass-400); margin: 0.3rem 0 0; }
/* ---- pictures on a page ------------------------------------------------ */
.lore-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 0.7rem; margin-top: 0.4rem; }
.lore-figure { margin: 0; border: 1px solid var(--wood-800); border-radius: var(--radius-small); background: var(--soot-700); }
.lore-figure img { display: block; width: 100%; height: auto; }
.lore-figure figcaption { padding: 0.4rem 0.55rem 0.5rem; display: flex; flex-direction: column; gap: 0.25rem; }
.lore-figure-caption { font-size: var(--text-sm); color: var(--parchment); }
.lore-figure-tags { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.lore-tag { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-500); border: 1px solid var(--line); border-radius: var(--radius-small); padding: 0.05rem 0.4rem; text-decoration: none; }
.lore-tag:hover { color: var(--brass-300); border-color: var(--brass-500); }
.lore-tag.is-here { color: var(--brass-300); border-color: var(--brass-600); }
.lore-figure-edit { margin-top: 0.2rem; }
.lore-figure-form { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.3rem; }
.lore-figure-form label { display: flex; flex-direction: column; gap: 0.15rem; font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-600); }
.lore-figure-form input { font-size: var(--text-sm); }
.lore-figure-form-row { display: flex; gap: 0.5rem; }
.lore-picture-add { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.7rem; }
.lore-picture-add input[type="text"] { flex: 1 1 14rem; }
.inspect-pictures { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.5rem; }
.inspect-pictures img { width: 5.5rem; height: 5.5rem; object-fit: cover; display: block; border: 1px solid var(--wood-800); border-radius: var(--radius-small); }

.scene-lore { max-width: 44rem; margin: 0.6rem auto 0; text-align: left; }
.inspector-close-form { display: inline; margin: 0; }
.inspector-close-form .inspector-close { background: none; border: 0; border-radius: var(--radius-small); cursor: pointer; font: inherit; }

.an-fight-controls { margin: 0.2rem 0 0.35rem; }
.an-fight-controls + .an-fight-controls { margin-top: 0; }
.an-fight-controls + .chart { margin-top: 0.5rem; }
.an-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 0.8rem; align-items: start; }
.an-fights, .an-boards, .an-patterns, .an-opposition.is-alone { grid-column: 1 / -1; }
.an-boards .an-board { break-inside: avoid; }
.an-boards-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: 0.6rem 1.4rem; }
.an-note { margin: 0.5rem 0 0; font-size: var(--text-xs); color: var(--brass-600); }
@media (max-width: 60rem) { .an-grid { grid-template-columns: minmax(0, 1fr); } .an-times, .an-opposition { grid-column: 1 / -1; } }
.an-toughest { display: flex; flex-wrap: wrap; gap: 0.3rem 0.8rem; align-items: baseline; margin-top: 0.6rem; padding: 0.45rem 0.6rem; border: 1px solid var(--line); border-radius: var(--radius); border-left: 3px solid var(--oxblood-500); font-size: var(--text-sm); color: var(--brass-500); }
.an-toughest-title { font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
.an-toughest b { color: var(--parchment); }
.an-people { display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); gap: 0.6rem; }
.an-person { border: 1px solid var(--wood-800); border-radius: var(--radius); padding: 0.45rem 0.6rem; }
.an-person-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.2rem 0.6rem; font-size: var(--text-xs); color: var(--brass-500); }
.an-person-head b { color: var(--parchment); }
.an-person-body { display: flex; gap: 0.7rem; margin-top: 0.4rem; align-items: flex-start; }
.an-person-splits { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.7rem; }
.an-reveal { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.6rem; font-size: var(--text-xs); color: var(--brass-600); }
.an-reveal button { font-size: var(--text-2xs); letter-spacing: 0.06em; }

.chart { color: var(--brass-400); }
.chart-bars { display: grid; grid-template-columns: minmax(6rem, max-content) 1fr max-content; gap: 0.25rem 0.5rem; align-items: center; font-size: var(--text-xs); }
.chart-row { display: contents; }
.chart-label { color: var(--parchment); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chart-bar { width: 100%; height: 0.7rem; display: block; background: var(--soot-900); }
.chart-bar rect { fill: currentColor; }
.tone-ember { color: var(--oxblood-500); }
.chart-value { color: var(--parchment); white-space: nowrap; }
.chart-value small { color: var(--brass-600); font-size: 0.85em; }
.chart-stack { width: 100%; height: 0.7rem; display: block; background: var(--soot-900); }
.chart-legend { display: flex; flex-wrap: wrap; gap: 0.15rem 0.7rem; font-size: var(--text-xs); color: var(--brass-500); margin-top: 0.2rem; }
.legend-item b { color: var(--parchment); }
.legend-item i { display: inline-block; width: 0.6rem; height: 0.6rem; margin-right: 0.3rem; vertical-align: -0.05rem; }
.seg-0 { fill: var(--brass-400); background: var(--brass-400); }
.seg-1 { fill: var(--oxblood-500); background: var(--oxblood-500); }
.seg-2 { fill: var(--brass-600); background: var(--brass-600); }
.seg-3 { fill: var(--parchment); background: var(--parchment); }
.seg-4 { fill: var(--wood-600); background: var(--wood-600); }
.seg-5 { fill: var(--brass-300); background: var(--brass-300); }
.chart-ring { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; flex: 0 0 5rem; }
.chart-ring svg { width: 4.2rem; height: 4.2rem; }
.ring-track { fill: none; stroke: var(--soot-900); stroke-width: 3.5; }
.ring-fill { fill: none; stroke: var(--brass-400); stroke-width: 3.5; stroke-linecap: butt; }
.chart-ring text { fill: var(--parchment); font-size: var(--text-2xs); font-family: inherit; }
.ring-caption { font-size: var(--text-2xs); color: var(--brass-600); text-align: center; }

/* ---- the usability pass: help, confirmations, the table-link strip, keys ---- */
/* `.field-help-sample` is the same circle, not a link: the manual's opening
   line shows the reader what the control it is describing looks like. */
.field-help, .field-help-sample { display: inline-flex; align-items: center; justify-content: center; width: 1.1rem; height: 1.1rem; border: 1px solid var(--line); border-radius: 50%; font-size: var(--text-xs); color: var(--brass-500); text-decoration: none; flex: 0 0 auto; margin-left: 0.2rem; }
.field-help:hover { border-color: var(--brass-400); color: var(--brass-300); }
.confirm-modal { max-width: min(28rem, 92vw); }
.confirm-row { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 0.6rem; }
.prep-delete-btn, .accounts-button { cursor: pointer; }
/* The way back to full screen after a page load, which the browser will not
   take by itself. A small fixed pill in the corner, out of the board's way,
   gone the moment full screen is back or the person leaves it on purpose. */
.fullscreen-hint {
    position: fixed; right: 1rem; bottom: 4.2rem; z-index: 60;
    padding: 0.35rem 0.7rem; border-radius: var(--radius-small);
    background: var(--soot-700); border: 1px solid var(--brass-600);
    color: var(--brass-300); font-size: var(--text-xs); letter-spacing: 0.06em; text-transform: uppercase;
    cursor: pointer;
}
.fullscreen-hint kbd { font: inherit; font-weight: 600; color: var(--parchment); margin-right: 0.3rem; }
.fullscreen-hint:hover { border-color: var(--brass-400); color: var(--parchment); }

.table-link { position: sticky; top: 0; z-index: 50; margin: 0 auto 0.5rem; padding: 0.35rem 0.9rem; text-align: center; font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--parchment); background: var(--oxblood-700); border-bottom: 1px solid var(--oxblood-500); }
.flash-area.is-inline { margin: 0 0 0.6rem; max-width: none; }
.flash-area.is-inline .flash { margin-bottom: 0; }
kbd { display: inline-block; min-width: 1.4rem; padding: 0.05rem 0.35rem; border: 1px solid var(--wood-600); border-bottom-width: 2px; border-radius: var(--radius-small); background: var(--soot-900); color: var(--brass-300); font-family: var(--font-body); font-size: var(--text-xs); text-align: center; }
.help { max-width: 52rem; }
.help p { font-size: var(--text-sm); line-height: 1.5; color: var(--parchment); margin: 0.4rem 0; }
.help p b { color: var(--brass-300); font-weight: normal; }
.help-nav { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.help-nav a { font-size: var(--text-xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-400); border: 1px solid var(--line); border-radius: var(--radius-small); padding: 0.15rem 0.55rem; text-decoration: none; }
.help-nav a:hover { border-color: var(--brass-500); color: var(--brass-300); }
.help-grammar { margin: 0.4rem 0; padding: 0.5rem 0.7rem; background: var(--soot-900); border: 1px solid var(--wood-800); border-radius: var(--radius); color: var(--brass-200); font-size: var(--text-sm); line-height: 1.5; overflow-x: auto; }
.help-keys td:first-child { width: 4rem; }
.help section { scroll-margin-top: 4rem; }

/* ============================================
   SCENE MODE — the picture between the fights
   ============================================
   The war room is dense on purpose: a fight needs every number at once. This
   is the opposite page and is styled as one. One picture, the words under it,
   and the controls last and quietest — because the players are looking at the
   art and the DM is the only one who needs to find a button.

   Sized in viewport units rather than rem. This page is often on a television
   across the room while the DM drives it from a laptop, and a picture that
   fills a 13" screen and floats in the middle of a 55" one is the failure the
   whole view exists to avoid. */
.scene-body { background-color: var(--soot-900); }

.scene-stage {
    max-width: min(90rem, 96vw);
    margin: 0 auto;
    /* 4rem at the foot: the feedback bubble is fixed to a bottom corner, and
       scrolled to the end the controls sat under it - it covered BACK. */
    padding: 0.5rem 0 4rem;
    display: flex;
    flex-direction: column;
    gap: 0.8rem;
}

.scene-frame {
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--soot-800);
    border: 1px solid var(--line); border-radius: var(--radius);
    /* Bounded so the caption is on screen with the picture. Read-aloud text
       the DM has to scroll to is text that gets read from the sheet of paper
       instead, which is the thing this replaces. */
    max-height: 68vh;
    overflow: hidden;
}

.scene-image {
    display: block;
    max-width: 100%;
    max-height: 68vh;
    /* contain, not cover: a scene is a picture someone drew, and cropping the
       top off a chapel to fill a frame is not this page's decision to make. */
    object-fit: contain;
}

.scene-noimage {
    padding: 4rem 1rem;
    color: var(--brass-600);
    font-size: var(--text-sm);
    text-align: center;
}

.scene-caption { max-width: 62rem; }

.scene-title {
    font-size: var(--text-lg);
    font-variant: small-caps;
    letter-spacing: 0.06em;
    color: var(--brass-300);
    margin: 0 0 0.4rem;
}

/* Boxed text, and it reads like boxed text: a rule down the left, generous
   line height, and a size that carries across a room. */
.scene-read-aloud {
    margin: 0;
    padding: 0.6rem 0 0.6rem 0.9rem;
    border-left: 3px solid var(--brass-700);
    color: var(--parchment-bright);
    font-family: var(--font-body);
    font-size: var(--text-md);
    line-height: 1.6;
    white-space: pre-wrap;
}

.scene-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--wood-800);
}

.scene-position {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    color: var(--brass-600);
    min-width: 4rem;
    text-align: center;
}

.scene-combat-btn { margin-left: 0.3rem; }
/* A page on the scene that is still the DM's: dashed, like anything not yet
   the table's, with the one press that makes it theirs. */
.scene-secret { margin-top: 0.8rem; padding: 0.6rem 0.8rem; border: 1px dashed var(--brass-700); border-radius: var(--radius); }
.scene-secret .scene-lore { margin: 0; }
.scene-secret-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.4rem; }

.scene-tools-btn {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    color: var(--brass-400);
}

/* No picture and nothing on the table: the words are the scene, large. The
   title alone when there are no words either. */
.scene-words {
    margin: 0;
    padding: 1.5rem 2rem;
    max-width: 44rem;
    color: var(--parchment-bright);
    font-family: var(--font-body);
    font-size: var(--text-xl);
    line-height: 1.5;
    text-align: center;
    white-space: pre-wrap;
}
.scene-words-title { font-variant: small-caps; letter-spacing: 0.08em; color: var(--brass-300); }

/* ---- the scene as a panel of the war room (2026-10-08) --------------------
   The frame and the caption above are the full-screen page's; inside
   `.scene-panel` they take the board column's size instead of the screen's.
   The stage is normally only as tall as the board (`flex: 0 0 auto`, and
   fitBoard measures the rest); with a scene in it, it grows to what the
   column has left and the picture fills that, the way the board would. The
   picture is positioned in its frame rather than sized, because a flex
   item's percentage height is not a number the browser will promise, and an
   absolutely placed box inside a laid-out frame is. */
.map-stage.is-scene { flex: 1 1 auto; display: flex; align-items: stretch; gap: 0.6rem; }
/* The DM's stage has the tool rail beside the board and sets its items to
   flex-start further down the sheet, which won the cascade and left the
   panel as tall as its words - a 160px picture in a 548px stage (seen in a
   browser, 2026-10-08). Said again with the rail named, so it wins. */
.map-stage.is-scene:has(.tool-rail) { align-items: stretch; }
/* No min-height: 0 on the stage or the panel, on purpose. With it, a short
   column (a phone, 390px) squeezed the panel below its own contents and the
   controls ran out under the action bar; without it the panel is never
   shorter than its floor - the 10rem frame, the words, the controls - and the
   column scrolls instead, which is what it does for every other panel at
   that width. */
.map-stage.is-scene > .scene-panel { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.scene-panel .scene-frame { flex: 1 1 auto; min-height: 10rem; max-height: none; position: relative; }
.scene-panel .scene-image { position: absolute; inset: 0; width: 100%; height: 100%; max-height: none; }
.scene-panel .scene-words { font-size: var(--text-lg); }
/* The words under the picture are bounded so the picture keeps most of the
   column; a long passage scrolls here rather than pushing the action bar
   off the screen. They never shrink: the frame gives first, down to its
   10rem floor, and then the column scrolls. */
.scene-panel .scene-caption { flex: 0 0 auto; max-width: none; max-height: 30vh; overflow-y: auto; }
.scene-panel .scene-title { font-size: var(--text-md); margin-bottom: 0.25rem; }
.scene-panel .scene-read-aloud { font-size: var(--text-sm); padding: 0.4rem 0 0.4rem 0.8rem; }
.scene-panel .scene-controls { flex: 0 0 auto; padding-top: 0.4rem; }
.scene-full-link { font-size: var(--text-2xs); letter-spacing: 0.06em; }
/* The jump list: the running order as one select, and GO. */
.scene-jump { display: inline-flex; align-items: center; gap: 0.3rem; margin: 0; min-width: 0; }
.scene-jump select { max-width: 14rem; font-size: var(--text-xs); }
.scene-jump button { font-size: var(--text-2xs); letter-spacing: 0.06em; }

/* "New scene: The Drowned Chapel" - one line at the foot of the screen,
   fading in and out, over nothing anyone is pressing (under the header it sat
   on the turn banner; seen 2026-10-08). pointer-events none so a click that
   lands where it was goes through to the page. */
.scene-toast {
    position: fixed;
    bottom: 1.5rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 60;
    padding: 0.35rem 0.9rem;
    border: 1px solid var(--brass-600);
    border-radius: var(--radius-small);
    background: var(--soot-700);
    color: var(--parchment-bright);
    font-size: var(--text-sm);
    white-space: nowrap;
    max-width: calc(100vw - 2 * var(--page-gutter, 1rem));
    overflow: hidden;
    text-overflow: ellipsis;
    opacity: 0;
    transition: opacity 0.4s ease;
    pointer-events: none;
}
.scene-toast.is-shown { opacity: 1; }

/* END SCENE sits in the controls under the picture, dressed as SCENES beside
   it. It was the header's WAR ROOM button until 2026-10-08, when leaving the
   page and ending the scene stopped being the same thing: the way back is a
   plain link now (.scene-back-link, a .header-link), for everyone. */
.scene-end-form { display: inline; }

/* ---- the running order, in the dialog ---- */
.scene-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.scene-row {
    border: 1px solid var(--wood-800); border-radius: var(--radius);
    border-left: 3px solid var(--line);
    padding: 0.4rem 0.55rem;
}

/* The one on screen, marked the way the map library marks the map on the
   table: a brass edge, because it is the same question asked twice. */
.scene-row.is-showing { border-left-color: var(--brass-400); }

.scene-row-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.2rem 0.5rem;
}

.scene-row-name { color: var(--parchment); font-size: var(--text-sm); }

.scene-row-live {
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    color: var(--soot-900);
    background-color: var(--brass-400);
    padding: 0.05rem 0.35rem;
}

.scene-row-shown {
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    color: var(--brass-600);
    border: 1px solid var(--brass-600);
    padding: 0 0.3rem;
}

.scene-row-image {
    margin-left: auto;
    font-size: var(--text-xs);
    color: var(--brass-600);
}

.scene-row-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.3rem;
    margin-top: 0.35rem;
}

.scene-show-btn { font-size: var(--text-2xs); letter-spacing: 0.06em; }
/* The same size as SHOW beside it, because they are the two halves of one
   decision: put this in front of them, or set it on them. */
.scene-fight-btn { font-size: var(--text-2xs); letter-spacing: 0.06em; }
.scene-move button { padding: 0.1rem 0.4rem; font-size: var(--text-xs); }
.scene-delete-btn { margin-left: auto; padding: 0.1rem 0.45rem; cursor: pointer; }

/* EDIT is a link to a page of its own now. It was a <details> with a form in
   it on every row, and a real deck made the war room six megabytes. */
.scene-edit-link {
    display: inline-block; margin-top: 0.35rem;
    font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--brass-600); text-decoration: none; border-bottom: 1px dotted var(--brass-700);
}
.scene-edit-link:hover { color: var(--brass-300); border-bottom-color: var(--brass-400); }
.scene-edit-page { max-width: 44rem; }

/* Sections of the running order: a session's cover and what folds under it.
   A real deck is 173 rows; the section the table is on opens, the rest read
   as one line each. */
.scene-section { margin: 0.4rem 0; border: 1px solid var(--wood-800); border-radius: var(--radius-small); }
.scene-section-summary {
    display: flex; align-items: baseline; gap: 0.6rem; cursor: pointer;
    padding: 0.4rem 0.6rem; list-style: none;
}
.scene-section-summary::-webkit-details-marker { display: none; }
.scene-section-summary::before { content: "▸"; color: var(--brass-600); font-size: var(--text-xs); }
.scene-section[open] > .scene-section-summary::before { content: "▾"; }
.scene-section-name { font-size: var(--text-sm); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-300); }
.scene-section-count { font-size: var(--text-xs); color: var(--brass-600); margin-left: auto; }
.scene-section > .scene-list { padding: 0 0.4rem 0.4rem; }
.scene-row.is-heading .scene-row-name { letter-spacing: 0.06em; text-transform: uppercase; }
.scene-check { display: flex; gap: 0.5rem; align-items: baseline; font-size: var(--text-sm); color: var(--brass-300); margin: 0.4rem 0; }

/* The DM's notes under a scene: drawn only for the DM, and drawn so that it
   cannot be mistaken for the read-aloud beside it. */
.scene-notes {
    margin-top: 0.9rem; padding: 0.6rem 0.8rem;
    border-left: 3px solid var(--brass-500); background: var(--soot-700);
}
.scene-notes-label { display: block; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); margin-bottom: 0.3rem; }
.scene-notes-body { margin: 0; white-space: pre-wrap; font-size: var(--text-sm); color: var(--parchment); line-height: 1.5; }

/* THE NOTE BOX (insight/jot.py): T, a line, Enter. One row between the
   picture and the words - WRITE on the left, the field in its place when
   it opens, the count of lines here on the right. The lines hang under the
   row, oldest first, the newest nearest the box, as the rail's notes read.
   Quiet by design: this is the DM's margin, not a control the table sees. */
.scene-now { margin: 0.45rem 0 0.3rem; }
.scene-now-form { display: flex; align-items: center; gap: 0.5rem; margin: 0; min-width: 0; }
.scene-now-open { font-size: var(--text-2xs); letter-spacing: 0.08em; }
.scene-now-field { flex: 1; min-width: 0; display: flex; }
/* `hidden` must win over the display rules above and the button classes:
   the field is closed until T, and the count is gone while there is nothing
   to count. */
.scene-now [hidden] { display: none; }
.scene-now-box {
    width: 100%; font-size: var(--text-sm); padding: 0.35rem 0.6rem;
    border: 1px solid var(--brass-600); background: var(--soot-900); color: var(--parchment-bright);
}
.scene-now-box:focus { outline: none; border-color: var(--brass-400); }
.scene-now-count { margin-left: auto; font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-600); white-space: nowrap; }
.scene-now-lines { list-style: none; margin: 0.3rem 0 0; padding: 0.3rem 0 0 0.6rem; border-left: 2px solid var(--wood-800); display: flex; flex-direction: column; gap: 0.2rem; }
.scene-now-line { display: flex; align-items: baseline; gap: 0.45rem; font-size: var(--text-sm); color: var(--parchment); line-height: 1.4; }
.scene-now-clock { font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-600); white-space: nowrap; }
.scene-now-text { flex: 1; min-width: 0; }
.scene-now-delete { margin: 0; }
.scene-now-delete button { color: var(--wood-700); padding: 0 0.2rem; }
.scene-now-delete button:hover { color: var(--oxblood-600); }
/* A player's reach: to the table unless PRIVATE is ticked. */
.scene-now-reach { display: inline-flex; align-items: center; gap: 0.2rem; font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-600); white-space: nowrap; margin-left: 0.5rem; }
.scene-now-reach input { margin: 0; accent-color: var(--brass-400); }
.scene-now-reach-who { color: var(--brass-700); font-size: 0.85em; }
.scene-now-kept { margin: 0.2rem 0 0; font-size: var(--text-2xs); font-style: italic; color: var(--brass-500); }
/* What the table said tonight: everyone's, under the box, the newest nearest it. */
.scene-said { list-style: none; margin: 0.3rem 0 0; padding: 0.3rem 0 0 0.6rem; border-left: 2px solid var(--wood-800); max-height: 9rem; overflow-y: auto; display: flex; flex-direction: column; gap: 0.15rem; }
.scene-said-line { display: flex; align-items: baseline; gap: 0.45rem; font-size: var(--text-sm); color: var(--parchment); line-height: 1.4; }
.scene-said-by { font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-500); white-space: nowrap; }
.scene-said-line.is-dm .scene-said-by { color: var(--brass-300); }

/* ---- Seen tonight (2026-10-22) -------------------------------------------
   The night's pictures, small, in the order the table saw them, under the
   words. A row that scrolls sideways rather than wraps: on a phone it is one
   thumb-high line, and a scene deck is thirty pictures by the end of a
   night. The one on screen is marked, so the row reads as a timeline. */
.scene-seen { margin-top: 0.6rem; }
.scene-seen-row {
    list-style: none; margin: 0; padding: 0.1rem 0 0.3rem;
    display: flex; gap: 0.4rem; overflow-x: auto; scrollbar-width: thin;
}
.scene-seen-item { flex: 0 0 auto; }
.scene-seen-tile {
    display: flex; align-items: center; justify-content: center;
    width: 6rem; height: 4rem; overflow: hidden;
    border: 2px solid var(--wood-800); border-radius: var(--radius-small);
    background: var(--soot-800); text-decoration: none;
}
.scene-seen-tile:hover { border-color: var(--brass-500); }
.scene-seen-item.is-now .scene-seen-tile { border-color: var(--brass-300); }
.scene-seen-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A scene with no picture of its own is named, not drawn. */
.scene-seen-words {
    padding: 0 0.4rem; font-size: var(--text-2xs); letter-spacing: 0.04em; line-height: 1.25;
    color: var(--brass-400); text-align: center; overflow: hidden;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
}
/* The lightbox: one earlier picture, large, on this screen alone. */
.scene-look { max-width: min(96vw, 80rem); padding: 0.8rem 1rem 1rem; }
.scene-look .modal-title { cursor: default; margin-bottom: 0.5rem; }
.scene-look-frame { display: flex; flex-direction: column; align-items: center; gap: 0.6rem; }
.scene-look-image { display: block; max-width: 100%; max-height: 72vh; object-fit: contain; border-radius: var(--radius-small); }
.scene-look-words {
    margin: 0; max-width: 44rem; padding: 0.4rem 0 0.4rem 0.9rem; border-left: 3px solid var(--brass-700);
    color: var(--parchment-bright); font-family: var(--font-body); font-size: var(--text-md); line-height: 1.6; white-space: pre-wrap;
}
.scene-look-again { margin: 0.8rem 0 0; display: flex; justify-content: flex-end; }
.scene-look [hidden] { display: none; }
/* What is in their rails, and the press that takes it out. */
.scene-pushed { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.7rem; margin-top: 0.6rem; padding: 0.4rem 0.8rem; border: 1px dashed var(--brass-700); border-radius: var(--radius); }
.scene-pushed .scene-notes-label { margin: 0; }
.scene-pushed-what { flex: 1 1 10rem; min-width: 0; font-size: var(--text-sm); color: var(--parchment); }
.scene-pushed-form { margin: 0; }
/* SHOW on a prep card, first of the picture's tools. */
.sp-show { margin: 0; }
.sp-show button { font-size: var(--text-2xs); letter-spacing: 0.06em; padding: 0.25rem 0.6rem; }
.session-title-edit { margin: 0.2rem 0 0.4rem; }
.session-title-edit input { flex: 1 1 14rem; min-width: 0; }

/* ---- Sheet and picture one press apart (2026-10-22) -----------------------
   Two jumps within the page, for the stacked layout only: MY SHEET under the
   picture goes down to the open card, and the card's own link comes back up
   to the stage. Side by side (the three-column war room, 1251px and up) both
   are already on screen and the links would jump to nowhere, so they go.
   The targets clear the fixed header on landing - the sticky-header rule,
   once more. */
.scene-sheet-link { font-size: var(--text-2xs); letter-spacing: 0.06em; }
.card-up { display: inline-block; margin: 0.1rem 0 0.2rem; font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-600); text-decoration: none; }
.card-up:hover { color: var(--brass-300); }
#the-table, #open-card { scroll-margin-top: calc(var(--header-height) + 0.5rem); }
@container page (min-width: 1187px) {
    .scene-sheet-link, .card-up { display: none; }
}

.scene-form {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin-top: 0.4rem;
}

.scene-field { display: flex; flex-direction: column; gap: 0.15rem; }
.scene-field > span {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--brass-600);
}
.scene-field textarea { font-family: var(--font-body); line-height: 1.5; resize: vertical; }

.scene-add { margin-top: 0.4rem; }

/* ============================================
   THE BESTIARY — the SRD, and the way into a fight
   ============================================
   A table, not cards. A DM choosing a monster is comparing four numbers —
   challenge, armor class, hit points, speed — and numbers compare down a
   column. Cards would put each one in a different place on the screen. */
/* 88rem rather than 72: the audit measured the table using 60% of a 1920px
   screen with a gutter either side wider than the attacks column. A row of
   numbers compared down a column wants the room. */
.bestiary { max-width: 88rem; }

.bestiary-clear { font-size: var(--text-xs); color: var(--brass-600); text-decoration: none; }
.bestiary-clear:hover { color: var(--brass-300); }

.bestiary-orders { display: flex; gap: 0.3rem; }
.bestiary-order {
    font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--brass-500); border: 1px solid var(--wood-800); border-radius: var(--radius-small);
    padding: 0.12rem 0.5rem; text-decoration: none;
}
.bestiary-order:hover { border-color: var(--brass-500); color: var(--brass-300); }
.bestiary-order.is-on { border-color: var(--brass-400); color: var(--brass-200); }


/* Wide content scrolls inside itself rather than pushing the page sideways —
   the attacks column is the one that gets long. */
/* `position: relative` is load-bearing. The table scrolls sideways inside
   itself on a narrow screen, and the visually-hidden label in its ADD column
   is `position: absolute` - so without a positioned ancestor it was placed
   against the PAGE, a thousand pixels to the right, and the page grew a
   sideways scrollbar the table itself never needed. Found by asking every
   element on the bestiary at 900px where its right edge was. */
.bestiary-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); position: relative; }
.bestiary-table thead th {
    text-align: left; font-size: var(--text-2xs); letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--brass-600); font-weight: normal;
    border-bottom: 1px solid var(--line); padding: 0.3rem 0.5rem;
}
.bestiary-table tbody tr { border-bottom: 1px solid var(--soot-500); }
.bestiary-table tbody tr:hover { background: var(--soot-700); }
.bestiary-table td, .bestiary-table tbody th { padding: 0.35rem 0.5rem; vertical-align: top; text-align: left; }

.bestiary-name { color: var(--parchment); font-weight: normal; white-space: nowrap; }
.bestiary-name small { display: block; font-size: var(--text-2xs); color: var(--brass-600); text-transform: capitalize; }
.bestiary-name .bestiary-seen { text-transform: none; color: var(--brass-400); white-space: normal; max-width: 16rem; }
.bestiary-table td small { color: var(--brass-600); font-size: 0.9em; }
.bestiary-speed { white-space: nowrap; color: var(--brass-500); }


.bestiary-add { text-align: right; }
/* Present for a screen reader, absent for everyone else. */
.bestiary-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

.bestiary-attribution { margin-top: 1rem; font-size: var(--text-2xs); color: var(--brass-600); }

/* In the war room, dressed as the control it sits beside rather than as a link. */
.bestiary-link {
    font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--brass-400);
    border: 1px solid var(--line); border-radius: var(--radius-small); padding: 0.2rem 0.55rem;
    text-decoration: none; white-space: nowrap; align-self: center;
}
.bestiary-link:hover { border-color: var(--brass-500); color: var(--brass-300); }

/* ---- the ADD box: the reading, the roll, the count ---- */
/* Never blank, so the row below the box does not jump as you type. */
.add-reading {
    margin: 0.15rem 0 0 2.6rem;
    font-size: var(--text-xs);
    color: var(--brass-600);
    min-height: 1em;
}
.add-init { width: 4rem; }
.add-roll, .add-count {
    display: inline-flex; align-items: center; gap: 0.2rem;
    font-size: var(--text-xs); color: var(--brass-500); white-space: nowrap;
}
.add-count-input { width: 3.2rem; }


/* ---- the shelf of campaigns ---- */
.campaigns { max-width: 46rem; }
.campaign-list { list-style: none; margin: 0.6rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.campaign-row {
    display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
    border: 1px solid var(--wood-800); border-radius: var(--radius); border-left: 3px solid var(--line);
    padding: 0.5rem 0.7rem;
}
/* The one on the table, marked the way the map library marks the map on it. */
.campaign-row.is-current { border-left-color: var(--brass-400); }
.campaign-name { color: var(--parchment); font-size: var(--text-md); }
.campaign-live {
    margin-left: 0.4rem; font-size: var(--text-2xs); letter-spacing: 0.12em;
    color: var(--soot-900); background: var(--brass-400); padding: 0.05rem 0.35rem;
}
.campaign-facts { margin-left: auto; font-size: var(--text-xs); color: var(--brass-600); }
.campaign-newer { color: var(--oxblood-400); margin-left: 0.5rem; }
.campaign-use { font-size: var(--text-2xs); letter-spacing: 0.06em; }
.campaign-delete { font-size: var(--text-2xs); letter-spacing: 0.06em; margin-left: 0.4rem; }
.campaign-new { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin-top: 1.2rem; }

/* The first run's two doors, side by side until the page is narrow. */
.campaign-doors { display: flex; gap: 1rem; flex-wrap: wrap; align-items: stretch; margin-top: 0.4rem; }
.campaign-door { flex: 1 1 18rem; border: 1px solid var(--wood-800); border-radius: var(--radius); padding: 0.9rem 1rem 1rem; margin-top: 0; }
.campaign-door .modal-note { line-height: 1.5; }
.campaign-example-door { align-content: flex-start; }
.campaign-example { margin-left: 0.4rem; font-size: var(--text-2xs); letter-spacing: 0.12em; color: var(--brass-300); border: 1px solid var(--brass-600); border-radius: var(--radius); padding: 0.05rem 0.35rem; }
.campaign-run-your-own { margin-top: 1.2rem; }
.campaign-run-your-own-how { font-size: var(--text-sm); color: var(--brass-500); line-height: 1.5; margin: 0; }
.campaign-run-your-own-how a { color: var(--brass-300); }

/* The other door on a refusal: beside the way back, quieter. */
.error-way-on { display: inline-block; margin: 0.6rem 0 0 0.2rem; font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--brass-400); text-decoration: underline; text-underline-offset: 0.2em; }

/* The first fight: three steps above the strip, one line each, one x. */
.first-fight { display: flex; align-items: center; gap: 0.6rem 1rem; flex-wrap: wrap; border: 1px solid var(--brass-600); border-radius: var(--radius); padding: 0.4rem 0.7rem; margin-bottom: 0.5rem; font-size: var(--text-xs); color: var(--brass-400); }
.first-fight-title { font-size: var(--text-2xs); letter-spacing: 0.12em; color: var(--brass-300); }
.first-fight-step.is-done { color: var(--brass-600); text-decoration: line-through; }
.first-fight-step b { color: var(--brass-200); }
.first-fight-hide-form { margin-left: auto; }
.first-fight-hide { background: none; border: 0; color: var(--brass-500); font-size: var(--text-md); line-height: 1; padding: 0 0.2rem; cursor: pointer; }
.first-fight-hide:hover { color: var(--brass-200); }
.campaign-new h2, .campaign-new p { flex: 1 1 100%; }
.campaign-new input { flex: 0 1 18rem; }

.empty-campaign { max-width: 40rem; }
.empty-campaign-note { font-size: var(--text-sm); color: var(--brass-500); line-height: 1.5; margin: 0.8rem 0; }
.empty-campaign-note code { color: var(--brass-300); }
.empty-campaign-note a { color: var(--brass-300); }


/* ---- character creation ----

   A wizard is read, not scanned. Everything here is one column with a
   generous measure, because the person on this page is a first-time player
   deciding what their character is - not a DM mid-fight looking for a number.
   That is the opposite of the war room's density, and it is deliberate.

   Checked at 1700, 1240 and 900 pixels. 900 is the one that matters: Rollies
   beside Discord on half a monitor, which is where this will actually be
   used. The choice grid is `auto-fill` rather than a fixed column count so it
   reflows there without a breakpoint of its own. */
.creation { max-width: 44rem; margin: 0 auto; padding-bottom: 3rem; }
.creation-start { max-width: 40rem; }

/* Where you are, and how much is left. A wizard that will not say feels
   endless at step two. */
.creation-rail {
    display: flex; flex-wrap: wrap; gap: 0.15rem 0.5rem;
    list-style: none; margin: 0 0 1.4rem; padding: 0;
    font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase;
}
.rail-step { color: var(--wood-600); }
.rail-step a { color: var(--brass-600); text-decoration: none; }
.rail-step a:hover { color: var(--brass-300); }
.rail-step.is-done { color: var(--brass-600); }
.rail-step.is-here { color: var(--brass-200); }
.rail-step + .rail-step::before { content: "·"; margin-right: 0.5rem; color: var(--wood-700); }

.creation-question { font-size: var(--text-lg); color: var(--parchment-bright); margin: 0 0 0.4rem; font-weight: 600; }
.creation-lede { font-size: var(--text-sm); color: var(--brass-500); line-height: 1.55; margin: 0 0 1.2rem; max-width: 38rem; }
.creation-note { font-size: var(--text-sm); color: var(--brass-500); line-height: 1.5; }
.creation-body { display: flex; flex-direction: column; gap: 1rem; }

.creation-controls {
    display: flex; align-items: center; gap: 0.8rem;
    margin-top: 1.8rem; padding-top: 1rem; border-top: 1px solid var(--wood-800);
}
/* NEXT sits on the right, away from BACK, because the two are pressed for
   opposite reasons and a mis-click costs an answer. */
.creation-controls .btn-primary { margin-left: auto; }
.btn-quiet {
    background: none; border: 1px solid var(--line); border-radius: var(--radius-small); color: var(--brass-600);
    padding: 0.35rem 0.8rem; font-size: var(--text-2xs); letter-spacing: 0.06em;
    text-transform: uppercase; text-decoration: none; cursor: pointer;
}
.btn-quiet:hover { border-color: var(--brass-500); color: var(--brass-300); }

/* A card per option: the name, what it is, and the numbers it changes. The
   numbers are on the card because "what does picking this do to me" is the
   only question being asked, and making it a click away makes it unaskable. */
.choice-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
    gap: 0.4rem;
}
.choice-card {
    display: flex; flex-direction: column; gap: 0.1rem;
    border: 1px solid var(--wood-800); border-radius: var(--radius); border-left: 3px solid var(--line);
    padding: 0.5rem 0.6rem; cursor: pointer;
}
.choice-card:hover { border-color: var(--line); border-left-color: var(--brass-500); }
/* Which one is chosen has to be unmistakable at a glance, so it is said three
   ways: the whole border lifts, the marker on the left goes bright, and the
   card sits on a lighter ground. One of those alone reads as a hover. */
.choice-card:has(input:checked) {
    border-color: var(--brass-600); border-left-color: var(--brass-300);
    background: var(--soot-500);
}
.choice-card:has(input:checked) .choice-name { color: var(--parchment-bright); }
.choice-card input { position: absolute; opacity: 0; pointer-events: none; }
/* The radio is invisible, so its focus ring has to be drawn on the card that
   stands in for it - without this a keyboard cannot see where it is. */
.choice-card:has(input:focus-visible),
.checkbox-chip:has(input:focus-visible) {
    outline: 2px solid var(--brass-300); outline-offset: 2px;
}
.choice-name { color: var(--parchment); font-size: var(--text-sm); }
.choice-sub { color: var(--brass-600); font-size: var(--text-2xs); }
.choice-facts { color: var(--brass-500); font-size: var(--text-xs); line-height: 1.4; margin-top: 0.2rem; }

.field { display: flex; flex-direction: column; gap: 0.25rem; }
.field-label { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-600); }
.field-note { font-size: var(--text-xs); color: var(--brass-600); line-height: 1.45; }
.field input, .field select, .field textarea {
    background: var(--soot-700); border: 1px solid var(--wood-800); border-radius: var(--radius-small);
    color: var(--parchment); padding: 0.4rem 0.5rem; font: inherit; font-size: var(--text-sm);
}
.field input:focus, .field select:focus, .field textarea:focus {
    outline: none; border-color: var(--brass-500);
}
.creation-custom { margin-top: 0.6rem; padding-top: 0.9rem; border-top: 1px dashed var(--wood-800); }


.checkbox-row { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.checkbox-chip {
    display: inline-flex; align-items: center; gap: 0.35rem;
    border: 1px solid var(--wood-800); border-radius: var(--radius-small); padding: 0.3rem 0.6rem;
    font-size: var(--text-sm); color: var(--parchment); cursor: pointer;
}
.checkbox-chip:has(input:checked) { border-color: var(--brass-400); background: var(--soot-600); }
/* Native checkboxes and radios are drawn in the platform's own chrome, which
   is white. This is the one property that themes them. */
.checkbox-chip input, .method-row input { accent-color: var(--brass-400); }
.checkbox-chip.is-granted {
    border-style: dashed; color: var(--brass-600); cursor: default;
}
.checkbox-chip.is-granted input { opacity: 0.35; }
.chip-note { font-size: var(--text-2xs); color: var(--brass-600); text-transform: uppercase; letter-spacing: 0.06em; }

.method-row { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
/* The browser's own spinner arrows are drawn in the platform's light chrome
   and cannot be themed, so they arrive as two white boxes in a dark room. */
.ability-grid input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
.ability-grid input[type="number"]::-webkit-outer-spin-button,
.ability-grid input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.ability-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); gap: 0.6rem; }
/* Eight short answers - age, height, eyes, hair - each of which needs a box a
   few characters wide. Down the page at full width they made this the longest
   step in the wizard, and it is the one that matters least. */
.detail-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); gap: 0.7rem; }

.kit-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.kit-item { border-left: 2px solid var(--wood-800); padding-left: 0.7rem; }
.kit-fixed { font-size: var(--text-sm); color: var(--brass-500); }
.kit-choice { display: flex; flex-wrap: wrap; gap: 0.35rem; }

.draft-list { list-style: none; margin: 0.6rem 0 1.4rem; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.draft-row {
    display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
    border: 1px solid var(--wood-800); border-radius: var(--radius); border-left: 3px solid var(--brass-500);
    padding: 0.5rem 0.7rem;
}
.draft-name { color: var(--parchment); font-size: var(--text-md); }
.draft-facts { margin-left: auto; font-size: var(--text-xs); color: var(--brass-600); }
.creation-begin { margin-top: 1rem; }

/* ---- the question in front of the table -------------------------------
   Across the top of the war room, because a prompt a player has to find is
   one the DM reads out anyway - which is the thing it replaces. Bordered in
   brass rather than in a faction color: it is not anybody's side, it is the
   table being asked something. */
.call-panel {
    border: 1px solid var(--brass-500); border-radius: var(--radius); border-left: 3px solid var(--brass-300);
    background: var(--soot-700); padding: 0.7rem 0.9rem; margin-bottom: 0.7rem;
    display: flex; flex-direction: column; gap: 0.6rem;
}
/* Answered by everybody: the urgency goes, the record stays. */
.call-panel.is-settled { border-left-color: var(--verdigris-500); }
.call-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 0.9rem; }
.call-title { margin: 0; font-size: var(--text-md); color: var(--parchment-bright); letter-spacing: 0.02em; }
.call-reason { margin: 0; font-size: var(--text-sm); color: var(--brass-500); font-style: normal; }
.call-note { margin: 0; font-size: var(--text-xs); color: var(--brass-600); }

/* A group check's one answer, above the rolls it comes from. */
.call-group {
    margin: 0.3rem 0 0;
    font-weight: 600;
    color: var(--brass-300);
}

/* A player's own row, or two of them when they run a companion. */
.call-mine { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.9rem; }
.call-answer-form { display: flex; align-items: center; gap: 0.35rem; }
.call-answer-form input[type="number"] { width: 4rem; }
.call-who-name { color: var(--parchment); font-size: var(--text-sm); min-width: 5rem; }
.call-modifier { color: var(--brass-300); font-size: var(--text-sm); }
.call-answered { color: var(--brass-500); font-size: var(--text-sm); }
.call-answered b { color: var(--brass-200); }
.call-adds { margin-left: auto; }

.call-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.call-table th {
    text-align: left; font-size: var(--text-2xs); letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--brass-600); font-weight: normal;
    padding-bottom: 0.2rem;
}
.call-table td { padding: 0.18rem 0.6rem 0.18rem 0; color: var(--parchment); }
.call-table tr.is-waiting td { color: var(--brass-600); }
.call-pass { color: var(--verdigris-400); }
.call-fail { color: var(--oxblood-400); }
.call-pending { color: var(--brass-600); font-style: normal; }
/* Who rolled it, when it was not the person whose character it is. */
.call-by-dm {
    font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-600);
    border: 1px solid var(--wood-800); border-radius: var(--radius); padding: 0 0.22rem; vertical-align: middle;
}
.call-dm-roll { display: flex; align-items: center; gap: 0.3rem; }
.call-dm-roll input[type="number"] { width: 3.4rem; }
.call-dc { display: flex; align-items: center; gap: 0.3rem; }
.call-dc input[type="number"] { width: 3.4rem; }
.call-dc-own, .call-asks { font-size: var(--text-xs); color: var(--brass-600); font-style: italic; }
.call-dc-mine { font-size: var(--text-xs); color: var(--brass-600); margin-right: 0.3rem; }
.call-table tr.is-asking td { color: var(--brass-600); }
.call-ask-in { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin: 0.4rem 0; }

/* What's new: big, crisp, minimal. The version is the heading, the date
   under it, the notes a plain list with a small word for what each is. */
.whats-new-modal { max-width: min(40rem, 92vw); padding: 1.4rem 1.8rem 1.2rem; }
.whats-new-modal .modal-title { cursor: default; font-size: var(--text-lg); }
.whats-new-release + .whats-new-release { margin-top: 1.2rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.whats-new-version { margin: 0; font-size: var(--text-md); color: var(--parchment); font-weight: 600; }
.whats-new-number { color: var(--brass-300); font-family: var(--mono, monospace); font-weight: 400; margin-right: 0.3rem; }
.whats-new-date { margin: 0.1rem 0 0.6rem; font-size: var(--text-xs); color: var(--brass-600); letter-spacing: 0.04em; }
.whats-new-notes { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.45rem; }
.whats-new-note { display: grid; grid-template-columns: 4.2rem 1fr auto; gap: 0.5rem; align-items: baseline; line-height: 1.45; }
.whats-new-kind { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--brass-500); }
.whats-new-note.kind-new .whats-new-kind { color: var(--verdigris-500); }
.whats-new-note.kind-fix .whats-new-kind { color: var(--brass-600); }
.whats-new-for { font-size: var(--text-xs); color: var(--brass-600); border: 1px solid var(--line); border-radius: var(--radius-small); padding: 0 0.35rem; }
.whats-new-picture { grid-column: 2 / -1; max-width: 100%; border-radius: var(--radius-small); border: 1px solid var(--line); margin-top: 0.3rem; }
.whats-new-reactions { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; position: relative; }
.whats-new-chip {
    font: inherit; font-size: var(--text-xs); line-height: 1.3; padding: 0.05rem 0.45rem;
    border: 1px solid var(--line); border-radius: 1rem; background: transparent; color: var(--parchment); cursor: pointer;
}
.whats-new-chip.is-mine { border-color: var(--brass-400); background: var(--soot-700); }
.whats-new-chip-add { color: var(--brass-600); }
.whats-new-chip:hover { border-color: var(--brass-400); }
.whats-new-picker[hidden] { display: none; }
.whats-new-picker { display: flex; gap: 0.15rem; padding: 0.25rem; border: 1px solid var(--line); border-radius: 1.2rem; background: var(--soot-800); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45); }
.whats-new-picker button { font: inherit; font-size: 1.1rem; line-height: 1; padding: 0.25rem 0.35rem; border: 0; border-radius: 1rem; background: transparent; cursor: pointer; }
.whats-new-picker button:hover { background: var(--soot-700); }
.whats-new-note { user-select: none; -webkit-user-select: none; }
.whats-new-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-top: 1.2rem; }
.whats-new-row .inspect-link { margin-right: auto; }
.whats-new .chronicle-card.is-unseen { border-color: var(--brass-500); }
.help-nav-whats-new { margin-left: auto; }
.badge-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.badge-row { display: grid; grid-template-columns: 11rem 1fr auto; gap: 0.6rem; align-items: baseline; }
.badge-title { color: var(--brass-300); font-weight: 600; }
.badge-since { font-size: var(--text-xs); color: var(--brass-600); }
.me-links { display: flex; gap: 1.2rem; flex-wrap: wrap; margin: 0; }

/* Spark's little rating: five bubbles on one quiet line. */
.rate { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin: 0.5rem 0 0; font-size: var(--text-xs); color: var(--brass-600); }
.rate-bubbles { display: inline-flex; gap: 0.25rem; }
.rate-bubble {
    width: 1.5rem; height: 1.5rem; padding: 0; border-radius: 50%;
    border: 1px solid var(--line); background: transparent; color: var(--brass-600);
    font: inherit; font-size: var(--text-xs); line-height: 1; cursor: pointer;
}
.rate-bubble:hover, .rate-bubble:focus-visible { border-color: var(--brass-400); color: var(--brass-300); background: var(--soot-700); }
.rate-more { display: inline; }
.rate-more summary { cursor: pointer; list-style: none; color: var(--brass-600); }
.rate-more summary::-webkit-details-marker { display: none; }
.rate-more[open] summary { display: none; }
.rate-more input { width: 14rem; max-width: 100%; font-size: var(--text-xs); }
.call-dm { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.call-dm .call-note { margin-right: auto; }

/* The dialog that asks. */
.call-modal { min-width: 32rem; }
/* The session's state in the header: next, or on a break; and the DM's press. */
.session-state { font-size: var(--text-2xs); letter-spacing: 0.1em; text-transform: uppercase; color: var(--brass-600); margin-left: 0.4rem; }
.session-state.is-break { color: var(--brass-300); }
.rail-form { margin: 0; display: contents; }
.rail-btn.is-primary { border-color: var(--brass-400); color: var(--brass-200); }
/* One row per thing they may roll; the second row is the "or". */
.call-options { display: flex; flex-direction: column; gap: 0.4rem; }
.call-option { display: grid; gap: 0.6rem; grid-template-columns: 1fr 1fr auto auto; align-items: end; }
.call-option .modal-field[hidden] { display: none; }
.call-option-drop { align-self: center; padding: 0.2rem 0.5rem; }
.call-option-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1rem; }
/* The ability a skill settled: shown, and not for picking. */
.call-ability.is-settled, select.is-settled { pointer-events: none; opacity: 0.7; }
/* The player's choice on the prompt, the pill the attack form uses. */
.call-pick { display: inline-flex; flex-wrap: wrap; gap: 0.3rem; }
.call-pick-one { text-transform: none; letter-spacing: 0.02em; }
/* The DM's key to a blind call, and the press that tells the table. */
.call-key { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; font-size: var(--text-xs); color: var(--brass-500); }
.call-key-item { display: inline-flex; align-items: center; gap: 0.4rem; }
.call-key-item form { display: inline; }
.call-revealed { color: var(--brass-600); font-style: normal; }
.call-grid { display: grid; gap: 0.6rem; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
.call-who { border: 1px solid var(--wood-800); border-radius: var(--radius); padding: 0.5rem 0.7rem; margin: 0; }
.call-who legend { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-600); padding: 0 0.3rem; }
.call-form { display: flex; flex-direction: column; gap: 0.7rem; }

/* Hit dice, beside the other two hit-point controls and drawn like them.
   The count leads because it is the thing worth knowing at a glance - how
   much the party can still put back without a long rest. */
.hit-dice-form { display: flex; align-items: center; gap: 0.4rem; }
.hit-dice-count {
    color: var(--brass-300); font-size: var(--text-sm); min-width: 2.4rem;
    letter-spacing: 0.02em;
}
.hit-dice-form button[disabled] { opacity: 0.4; cursor: default; }

/* How many creatures a folded opposition row stands for. Quiet and beside
   the name rather than a column of its own: it qualifies the name, and a
   column would be empty for every row that is one creature. */
.an-tally { color: var(--brass-600); font-size: var(--text-xs); letter-spacing: 0.06em; }

/* ---- reading a sheet in ------------------------------------------------
   The review page is a form whose job is to be SCANNED first and typed in
   second: a DM importing six characters is looking for the one box that came
   out wrong, not reading eighteen labels. So the groups are the sheet's own
   groups, and everything that is a number sits in a narrow box beside its
   neighbors rather than on a row of its own.

   `auto-fit` rather than a fixed column count, because this page is the one
   the DM may well have open beside the PDF it came from on half a screen. */
.creation-alternative {
    margin-top: 1.8rem; padding-top: 1.1rem;
    border-top: 1px dashed var(--wood-800);
    display: flex; flex-direction: column; gap: 0.5rem; align-items: flex-start;
}
.intake-form { display: flex; flex-direction: column; gap: 0.9rem; align-items: flex-start; }
.intake-review { max-width: 52rem; }
.intake-review .creation-form { display: flex; flex-direction: column; gap: 0.9rem; }

/* What the reader guessed, and what it could not read. First on the page
   because it is the reason the page exists. */
.intake-notes { list-style: none; margin: 0 0 1.2rem; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.intake-note {
    border-left: 3px solid var(--brass-500); background: var(--soot-700);
    padding: 0.5rem 0.7rem; font-size: var(--text-sm); color: var(--parchment);
    line-height: 1.5;
}
/* A player with no account cannot move their own character, which is a
   different kind of wrong from a box that did not parse. */
.intake-note.is-warning { border-left-color: var(--danger); }
/* Sheets in either direction: the players' waiting on the DM, and a player's
   own, waiting or handed back with a note. */
.intake-waiting { margin: 0 0 1.4rem; }
.intake-sent { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.45rem; font-size: var(--text-sm); }
.intake-returned { color: var(--brass-300); }
.intake-hand-back { margin-top: 1.6rem; }
/* On a player's own card when nothing is written under it - a nudge, not an alarm. */
.card-sheet-empty { margin: 0.4rem 0 0; font-size: var(--text-xs); color: var(--brass-500); line-height: 1.45; }
.card-sheet-empty a { color: var(--brass-300); }
/* The ready check on Sessions: one line a seat, what is left in brass,
   ready in the quiet green the rest of the page uses for done. */
.ready-list { list-style: none; margin: 0.4rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; font-size: var(--text-sm); }
.ready-row { display: flex; flex-wrap: wrap; gap: 0.3rem 0.8rem; align-items: baseline; }
.ready-todo { color: var(--brass-300); }
.ready-todo a, .ready-check a { color: var(--brass-200); }
.ready-tag { color: var(--verdigris-400); font-size: var(--text-xs); letter-spacing: 0.08em; text-transform: uppercase; }
/* A note under the box it is about: the same brass rule as the pile at the
   top, drawn where the DM's eye already is. Block, so a field's own note and
   the importer's do not run together on one line. */
.intake-flag {
    display: block; margin-top: 0.3rem; padding: 0.35rem 0.6rem;
    border-left: 3px solid var(--brass-500); background: var(--soot-700);
    color: var(--parchment); font-size: var(--text-xs);
}

.intake-grid {
    display: grid; gap: 0.7rem;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
/* No flex-shrink here on purpose: these wrap, and a shrink of 0 on a wrapping
   row is how the side cards ended up off the edge. */
.intake-scores, .intake-slots { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.field-narrow { max-width: 6.5rem; }

/* Walk, fly, swim, climb, and the hover tick: one row that wraps on a phone. */
.speed-boxes { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.6rem; }
.speed-boxes .field-narrow { width: 5.5rem; }
.speed-boxes .field-narrow input { width: 100%; box-sizing: border-box; }
.speed-hover { display: inline-flex; align-items: center; gap: 0.3rem; font-size: var(--text-xs); color: var(--brass-500); padding-bottom: 0.45rem; }
.slot-pair { display: flex; align-items: center; gap: 0.25rem; }
.slot-pair input { width: 2.6rem; text-align: center; }
.slot-of { color: var(--brass-600); font-size: var(--text-sm); }

.intake-trained { display: grid; gap: 0.9rem; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
.intake-set { border: 1px solid var(--wood-800); border-radius: var(--radius); padding: 0.6rem 0.8rem; margin: 0; }
.intake-set legend { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-600); padding: 0 0.3rem; }
.check-line { display: flex; align-items: center; gap: 0.4rem; font-size: var(--text-sm); color: var(--parchment); padding: 0.12rem 0; cursor: pointer; }
.check-line input { accent-color: var(--brass-400); }

/* The sheet, before it is real. Laid out like the thing it is going to be, so
   what is being agreed to is legible at a glance rather than as a form dump. */
.sheet-preview { border: 1px solid var(--wood-800); border-radius: var(--radius); padding: 1rem 1.1rem; }
.sheet-headline h2 { margin: 0; font-size: var(--text-lg); color: var(--parchment-bright); }
.sheet-headline p { margin: 0.2rem 0 1rem; font-size: var(--text-sm); color: var(--brass-500); }
.sheet-numbers { display: flex; flex-wrap: wrap; gap: 1.4rem; padding-bottom: 0.9rem; border-bottom: 1px solid var(--wood-800); }
.sheet-stat { display: flex; flex-direction: column; }
.stat-value { font-size: var(--text-lg); color: var(--brass-200); line-height: 1.1; }
.stat-label { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-600); }
.sheet-abilities { display: flex; flex-wrap: wrap; gap: 0.4rem; padding: 0.9rem 0; border-bottom: 1px solid var(--wood-800); }
.sheet-ability {
    display: flex; flex-direction: column; align-items: center;
    border: 1px solid var(--wood-800); border-radius: var(--radius-small); padding: 0.35rem 0.7rem; min-width: 3.6rem;
}
/* A save the class is proficient in, marked the way the sheet marks it. */
.sheet-ability.is-proficient { border-color: var(--brass-500); }
.ability-name { font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-600); }
.ability-score { font-size: var(--text-md); color: var(--parchment); }
.ability-mod { font-size: var(--text-xs); color: var(--brass-400); }
.sheet-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: 1.4rem; padding-top: 0.9rem; }
.sheet-columns h3 { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-600); margin: 0 0 0.35rem; }
.sheet-columns section > h3 + * { margin-top: 0; }
.sheet-columns section > * + h3 { margin-top: 1rem; }
.sheet-columns p { font-size: var(--text-sm); color: var(--parchment); margin: 0; }
.sheet-list { list-style: none; margin: 0; padding: 0; font-size: var(--text-sm); color: var(--parchment); }
.sheet-list li { display: flex; gap: 0.5rem; padding: 0.1rem 0; }
.sheet-number { margin-left: auto; color: var(--brass-400); }
/* What the DM has to be told, in the color this project reserves for
   "something here needs a person to look at it". */
.sheet-flag { margin-top: 1rem; padding: 0.6rem 0.8rem; border-left: 3px solid var(--oxblood-500); background: var(--soot-600); }
.sheet-flag p { margin: 0.2rem 0; font-size: var(--text-xs); color: var(--oxblood-400); }

.empty-campaign-start { display: inline-block; padding: 0.55rem 1.1rem; text-decoration: none; margin-top: 0.4rem; }

/* The way up, on the line that already says what level they are. Quiet: it is
   used twice a campaign, and it sits inside a panel read every round. */
.subtitle-action {
    font-size: var(--text-2xs); letter-spacing: 0.06em; line-height: 1.3;
    color: var(--brass-600); text-decoration: none;
    border: 1px solid var(--wood-800); border-radius: var(--radius-small); padding: 0.15rem 0.4rem;
}
.subtitle-action:hover { color: var(--brass-300); border-color: var(--brass-500); }

/* One level, laid out as what changes rather than as a form. */
/* ---- going up a level -----------------------------------------------------
   The whole sheet on the left, what changes on the right. Two columns where
   there is room, because "what does this do to my character" is a question
   with both halves on one screen; one column below the stacking width, sheet
   first, so the numbers being changed are read before the choices. */
.levelup { max-width: 64rem; }
/* The decision leads: which class, the hit points, what you gain, TAKE THE
   LEVEL - left and top, and first when the page is one column. The sheet as
   it stands is the reference beside it, not the thing to scroll past. */
.levelup-grid { display: grid; grid-template-columns: minmax(20rem, 6fr) minmax(18rem, 5fr); gap: 1.4rem; align-items: start; margin-top: 1rem; }
.levelup-grid > .levelup-offer { order: -1; }
.levelup-sheet { border: 1px solid var(--wood-800); border-radius: var(--radius); padding: 0.9rem 1rem 1rem; background: var(--soot-900); display: grid; gap: 0.8rem; }
.levelup-sheet .card-header { margin: 0; }
.levelup-sheet .ability-scores { margin-bottom: 0; }
.levelup-level { font-family: var(--font-display); font-size: var(--text-xs); letter-spacing: 0.12em; color: var(--brass-500); }
.levelup-level b { color: var(--brass-300); }
.levelup-block-title { margin: 0 0 0.3rem; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
.levelup-saves, .levelup-skills { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.1rem 0.8rem; font-size: var(--text-xs); color: var(--parchment); }
.levelup-skills { grid-template-columns: repeat(3, 1fr); }
.levelup-saves li, .levelup-skills li { display: flex; gap: 0.3rem; }
.levelup-saves .sheet-number, .levelup-skills .sheet-number { margin-left: auto; }
.levelup-saves .is-proficient, .levelup-skills .is-proficient { color: var(--brass-200); }
.levelup-saves .is-proficient::before, .levelup-skills .is-proficient::before { content: "●"; font-size: var(--text-2xs); align-self: center; color: var(--brass-400); }
.levelup-features { list-style: none; margin: 0; padding: 0; font-size: var(--text-xs); color: var(--parchment); columns: 2; column-gap: 1rem; }
.levelup-features li { break-inside: avoid; padding: 0.05rem 0; }
.levelup-features .sheet-number { margin-left: 0.3rem; font-size: var(--text-2xs); }
.levelup-granted { font-size: var(--text-xs); color: var(--brass-400); margin: 0 0 0.8rem; }
/* Which class takes the level: a small form of its own above the one that
   takes it, because its answer redraws everything below. */
.levelup-offer { display: flex; flex-direction: column; gap: 1.2rem; }
.levelup-class .checkbox-row { align-items: center; }
.levelup-new select, .levelup-new input[type="text"] { font: inherit; background: transparent; border: 0; border-radius: var(--radius-small); color: inherit; min-width: 9rem; }
.levelup-prereq .is-unmet, .levelup-prereq .is-unmet .sheet-number { color: var(--danger); }
.level-block { border-left: 2px solid var(--wood-800); padding-left: 0.9rem; }
/* A multiclass caster's page is two pages stacked, so each class announces
   itself: brass, the width of the column, and a rule above it. Only drawn
   when there are two - a single-class caster said all of this in the heading
   at the top of the page and does not need it again. */
.prepare-class { border-left-color: var(--brass-700); margin-top: 1.4rem; }
.prepare-class-title { font-size: var(--text-md); margin: 0 0 0.3rem; }
.level-block .modal-section { margin-top: 0; }
.levelup-typed { display: inline-flex; align-items: center; gap: 0.4rem; }
.levelup-typed input { width: 3.2rem; }
.levelup-arith { font-size: var(--text-xs); color: var(--brass-500); margin: 0.5rem 0 0; }
@media (max-width: 1100px) {
    .levelup-grid { grid-template-columns: 1fr; }
}
/* On the war room: the way up, lit when a level is waiting; the DM's GRANT
   beside it; and the arrow on the roster that says one is. */
/* ---- drinking a potion ------------------------------------------------- */
.potion-ways { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.6rem; }
.potion-way {
    display: flex; flex-direction: column; align-items: flex-start; gap: 0.15rem;
    text-align: left; padding: 0.55rem 0.75rem; font-family: inherit;
}
.potion-way-name { font-size: var(--text-xs); letter-spacing: 0.06em; text-transform: uppercase; }
.potion-way-note { font-size: var(--text-xs); color: var(--brass-500); text-transform: none; letter-spacing: 0; }
.potion-give { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; border: 1px solid var(--wood-800); border-radius: var(--radius); }
.potion-give-to { display: inline-flex; align-items: center; gap: 0.4rem; font-size: var(--text-xs); color: var(--parchment); }
.potion-give-btn { font-size: var(--text-xs); letter-spacing: 0.06em; }
.gear-drink { letter-spacing: 0.06em; }

.subtitle-form { display: inline; margin: 0; }
.subtitle-form .subtitle-action { background: none; cursor: pointer; font-family: inherit; }
.subtitle-action.is-granted { color: var(--brass-200); border-color: var(--brass-400); background: var(--soot-500); }
.subtitle-note { font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-600); }
.roster-granted { color: var(--brass-300); }

/* ---- choosing spells ------------------------------------------------------
   A class list is thirty to eighty spells and the question at each one is
   "what does this do", so the row has to answer it without leaving the page.
   Hence a card that carries the name, the facts you compare spells BY, and
   the whole text behind one disclosure. */
.spell-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 0.4rem; align-items: start;
}
.spell-pick {
    display: flex; flex-direction: column; gap: 0.15rem;
    border: 1px solid var(--wood-800); border-radius: var(--radius); border-left: 3px solid var(--line);
    padding: 0.45rem 0.6rem;
}
.spell-pick:hover { border-left-color: var(--brass-500); }
/* Said two ways, for the reason .choice-card gives at length: the left marker
   alone reads as a hover, and on a page where twenty of eighty rows are ticked
   the ticked ones have to be findable by scanning rather than by reading. */
.spell-pick.is-chosen {
    border-color: var(--brass-600); border-left-color: var(--brass-300);
    background: var(--soot-500);
}
.spell-pick.is-chosen .spell-name { color: var(--parchment-bright); }
.spell-head {
    display: flex; align-items: baseline; gap: 0.45rem; cursor: pointer;
}
.spell-head input { accent-color: var(--brass-400); flex-shrink: 0; }
.spell-pick:has(.spell-head input:focus-visible) {
    outline: 2px solid var(--brass-300); outline-offset: 2px;
}
.spell-name { color: var(--parchment); font-size: var(--text-sm); }
.spell-facts { color: var(--brass-600); font-size: var(--text-2xs); line-height: 1.4; }
/* The way to the spell's text: the inspector rail, not a disclosure under
   the row. Small and quiet, the way the old summary was, and a link rather
   than a button because reading is navigation here - the rail's state is in
   the querystring, and the back button brings the picker back as it was. */
.spell-read {
    display: inline-block; margin-top: 0.25rem;
    font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--brass-600);
}

/* The count, kept in view down a long page. */
.prepare-tally {
    /* The site header is `position: fixed` at z-index 100, so a tally stuck to
       the top of the VIEWPORT slides underneath it and is gone exactly when it
       is wanted. It sticks below the header instead - and the header's height
       changes at the stacked breakpoint, so the page measures it rather than
       guessing. The 5rem fallback is the body's own top padding, which is the
       app's existing statement of how much room the header takes: with no
       script it leaves a small gap and never an overlap. */
    position: sticky; top: var(--tally-top, 5rem); z-index: 4;
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 1.1rem;
    background: var(--soot-700); border-bottom: 1px solid var(--wood-800);
    padding: 0.55rem 0.1rem; margin-bottom: 0.8rem;
}
.tally-item { display: inline-flex; align-items: baseline; gap: 0.4rem; }
.tally-label {
    font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--brass-600);
}
.tally-count { font-size: var(--text-md); color: var(--parchment); font-variant-numeric: tabular-nums; }
.tally-item.is-full .tally-count { color: var(--brass-300); }
/* Over the allowance is not an error yet - the server has the last word - but
   it is the one state worth interrupting for, since the button will bounce. */
.tally-item.is-over .tally-count { color: var(--danger); font-weight: 600; }
.prepare-book-add { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.prepare-book-add input[type="text"] { flex: 1 1 16rem; min-width: 0; }
.prepare-book-list { list-style: none; padding: 0; margin: 0.75rem 0 0; columns: 2 14rem; column-gap: 1.5rem; }
.prepare-book-list li { break-inside: avoid; padding: 0.15rem 0; }
.tally-note {
    font-size: var(--text-2xs); color: var(--wood-600); margin-left: auto;
    max-width: 22rem; line-height: 1.4;
}
.prepare-empty { margin: 1rem 0; }

/* Half a monitor is the target case, so the grid gives up its columns before
   it gives up the text inside them. */
@media (max-width: 700px) {
    .spell-grid { grid-template-columns: 1fr; }
    .tally-note { margin-left: 0; }
}
.sheet-prepare { margin: 0 0 0.5rem; }
.sheet-prepare a { color: var(--brass-300); }


/* ============================================
   WHERE THE RACIAL BONUSES LAND
   ============================================
   Two ways of answering one question, and only one of them is on screen at a
   time. The pair of cards reuses .choice-card, which is already the shape this
   wizard uses for "pick one of these" - a second look for the same decision
   would be a second thing to learn for no gain.

   The row of selects is one per bonus rather than a box of checkboxes, because
   a +2 and a +1 is not the same character as three +1s and a checkbox cannot
   say which it is. The amount is the label, so the shape of the heritage is
   readable before anything is chosen. */
.origin-choice {
    display: flex; flex-wrap: wrap; gap: 0.5rem;
    margin-bottom: 0.9rem;
}
.origin-choice .choice-card { flex: 1 1 15rem; }
.choice-note { font-size: var(--text-xs); color: var(--brass-500); }

.place-row { display: flex; flex-wrap: wrap; gap: 0.6rem; }
/* The climb: one row per level above 1st, each a class. */
.climb-rows { display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); gap: 0.6rem 1rem; }
.climb-row select { max-width: 100%; }
.place-field { flex: 0 0 auto; min-width: 9rem; }
/* The amount reads as a value rather than as a caption: it is the part of this
   page that is fixed, and the select beside it is the part that is not. */
.place-field .field-label {
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--text-md); color: var(--brass-200);
}

/* ============================================
   MOTION, FOR WHOEVER CANNOT TAKE IT
   ============================================
   A vestibular disorder is not a preference and neither is the setting that
   reports it. Somebody who has asked their operating system for less motion has
   asked every application on it, and honoring that is one block rather than a
   decision per transition.

   Last in the file on purpose. Every rule above may set a transition and this
   one has to win, and winning by source order is cheaper and more honest than
   winning by specificity - a later rule cannot be out-specified by an earlier
   one the way `!important` gets argued with.

   Reduced, not removed: 0.01ms rather than 0 leaves transitionend firing, so
   any script that waits for a transition to finish still hears it end instead
   of waiting forever for one that never started. */

/* ---- Lore: the campaign's world ------------------------------------------
   Plain, the way the shelf and the channel are: read between sessions. The
   three doors are equal in weight on purpose - none of them is the "real"
   way in - and the list beneath them is grouped by kind so a dropped folder
   of forty files reads as six headings rather than forty rows. */
.lore-doors { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.6rem; }
.lore-door {
    display: flex; flex-direction: column; gap: 0.35rem;
    border: 1px solid var(--wood-800); border-radius: var(--radius); border-left: 3px solid var(--line);
    padding: 0.6rem 0.75rem; text-decoration: none; color: inherit;
}
.lore-door:hover { border-color: var(--line); border-left-color: var(--brass-500); }
.lore-door-name { font-family: var(--font-display); font-size: var(--text-sm); letter-spacing: 0.06em; color: var(--brass-300); }
.lore-door-note { font-size: var(--text-xs); color: var(--brass-600); line-height: 1.45; flex: 1; }
.lore-door .file-pick { margin-top: 0.2rem; }
.lore-door .btn-primary { align-self: flex-start; }
/* The Lore index: one card per page under its kind's heading, drawn with
   the session cards' rules (2026-11-05). */
.lore-group { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1.4rem; }
.lore-group-title { margin: 0; font-size: var(--text-xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
/* Typing in the search box hides rows and whole groups with `hidden`, which
   `display: flex` above would otherwise overrule: the box filtered nothing. */
.lore-row[hidden], .lore-group[hidden] { display: none; }
.lore-one-players { display: flex; align-items: center; gap: 0.4rem; font-size: var(--text-sm); color: var(--brass-500); }
.lore-door-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.lore-door-files { margin: 0; }
.lore-door-btn { display: inline-block; padding: 0.4rem 0.9rem; font-size: var(--text-xs); letter-spacing: 0.06em; cursor: pointer;
                 color: var(--parchment); text-decoration: none; border: 1px solid var(--wood-600); border-radius: var(--radius-small); }
.lore-door-btn:hover, .lore-door-btn:focus-within { border-color: var(--brass-400); color: var(--brass-200); }
.lore-search { width: 100%; max-width: 28rem; padding: 0.45rem 0.7rem; font-size: var(--text-md); }
.lore-filters-fold > summary { margin-bottom: 0.4rem; }
.lore-row-toggle { margin-top: 0; margin-bottom: 0; }
.lore-crumb { margin: 0 0 0.2rem; font-size: var(--text-xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-600); }
.lore-crumb a { color: var(--brass-500); text-decoration: none; }
.lore-steps { display: inline-flex; gap: 0.3rem; margin-right: 0.6rem; vertical-align: middle; }
.lore-step { padding: 0.1rem 0.45rem; font-size: var(--text-2xs); letter-spacing: 0.08em; }
.lore-step:disabled { opacity: 0.35; cursor: default; }
.lore-crumb a:hover { color: var(--brass-300); text-decoration: underline; }
.lore-status { flex: 1 1 100%; }
.lore-controls { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-top: 0.4rem; flex: 1 1 100%; }
.lore-controls form { margin: 0; }
.lore-body { line-height: 1.6; color: var(--parchment); font-size: var(--text-sm); }
.lore-body h2, .lore-body h3, .lore-body h4 { font-family: var(--font-display); letter-spacing: 0.06em; color: var(--brass-300); margin: 1rem 0 0.4rem; }
.lore-body h2 { font-size: var(--text-md); }
.lore-body h3 { font-size: var(--text-md); }
.lore-body h4 { font-size: var(--text-sm); }
.lore-body p { margin: 0 0 0.7rem; }
.lore-body ul, .lore-body ol { margin: 0 0 0.7rem; padding-left: 1.2rem; }
.lore-body li { margin-bottom: 0.2rem; }
.lore-body blockquote { margin: 0 0 0.7rem; padding: 0.2rem 0 0.2rem 0.8rem; border-left: 3px solid var(--line); color: var(--brass-500); }
.lore-body hr { border: 0; border-radius: var(--radius); border-top: 1px solid var(--wood-800); margin: 0.9rem 0; }
.lore-body code { font-size: 0.8em; padding: 0 0.25em; background: var(--soot-500); }
.lore-body .lore-outlink { color: var(--brass-400); }
/* A name with no page behind it - or, for a player, a page that is not
   theirs. Drawn as a name: not underlined, so it does not promise a click. */
.lore-empty { color: var(--brass-600); font-style: normal; }
/* The register's number, small print after a person's title, and the
   names they went by before. The number is for the DM's "same as 0123";
   it is never the thing a player reads. */
.lore-number { font-family: var(--font-body); font-size: var(--text-2xs); letter-spacing: 0.08em; color: var(--brass-600); margin-left: 0.6em; vertical-align: middle; }
.lore-formerly { color: var(--brass-500); font-style: italic; }
.inspect-lore { font-size: var(--text-sm); }
.inspect-kind { margin: 0 0 0.6rem; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
.lore-form { display: grid; gap: 0.8rem; }
.lore-form-row { display: grid; grid-template-columns: minmax(10rem, 1fr) 2fr; gap: 0.8rem; }

/* The facts a page gives (`_lore_facts.html`), the filters that read them,
   and the face of a person with no picture. */
.lore-fields { border: 1px solid var(--wood-800); border-radius: var(--radius); padding: 0.6rem 0.8rem 0.8rem; margin: 0; display: grid; gap: 0.4rem; }
.lore-fields legend { padding: 0 0.3rem; }
.lore-fields-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 0.6rem 0.8rem; }
.lore-fields [hidden] { display: none; }
.lore-filters { display: flex; flex-direction: row; flex-wrap: wrap; align-items: flex-end; gap: 0.5rem 0.8rem; }
.lore-filters label { display: flex; flex-direction: column; gap: 0.2rem; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
.lore-filters select { font-size: var(--text-sm); min-width: 8rem; }
.lore-filters-clear { font-size: var(--text-xs); letter-spacing: 0.12em; color: var(--brass-500); text-decoration: none; padding-bottom: 0.45rem; }
.lore-filters-clear:hover { color: var(--brass-300); text-decoration: underline; }
.lore-facts-card { display: flex; flex-direction: row; align-items: flex-start; gap: 1rem; }
.lore-face { flex: 0 0 auto; width: 5rem; height: 5rem; border: 1px solid var(--wood-800); border-radius: var(--radius); background: var(--soot-700); display: flex; align-items: flex-end; justify-content: center; overflow: hidden; }
.lore-face .silhouette { width: 78%; height: 90%; }
.lore-face-small { width: 3.4rem; height: 3.4rem; float: right; margin: 0 0 0.4rem 0.6rem; }
.lore-facts { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.45rem 1rem; margin: 0; flex: 1; }
.lore-fact dt { font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
.lore-fact dd { margin: 0.1rem 0 0; font-size: var(--text-sm); color: var(--parchment); }
.lore-fact-link { color: var(--brass-300); }
.lore-fact-name { color: var(--parchment); }
.inspect-lore .lore-facts { grid-template-columns: 1fr; gap: 0.3rem; margin-bottom: 0.6rem; }
.inspect-lore .lore-fact dd { font-size: var(--text-sm); }
.lore-related { display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: 0.6rem 1rem; }
.lore-related-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.2rem; font-size: var(--text-sm); }
.lore-players-field .lore-check { display: flex; align-items: center; gap: 0.4rem; font-size: var(--text-sm); color: var(--parchment); }
.lore-form .btn-primary { justify-self: start; }
.lore-question-yours, .lore-question-table { margin-left: 0.4rem; font-size: var(--text-2xs); letter-spacing: 0.12em; padding: 0.05rem 0.3rem; border: 1px solid var(--line); border-radius: var(--radius); color: var(--brass-600); }
.lore-question-table { color: var(--brass-400); border-color: var(--brass-600); }
@media (max-width: 640px) {
    .lore-form-row { grid-template-columns: 1fr; }
}


/* ---- campaign codes and membership ---------------------------------------- */
.campaign-code-line { display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; margin: 0; }
.campaign-code-value { display: inline-block; white-space: nowrap; font-size: var(--text-lg); letter-spacing: 0.12em; color: var(--brass-200); background: var(--soot-900); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.25rem 0.6rem; }
.campaign-code-how { font-size: var(--text-xs); color: var(--brass-600); flex: 1 1 18rem; line-height: 1.45; }
.campaign-role { margin-left: 0.5rem; font-size: var(--text-2xs); letter-spacing: 0.12em; color: var(--brass-500); border: 1px solid var(--wood-800); border-radius: var(--radius); padding: 0.05rem 0.3rem; }
.campaign-row.is-viewing { border-left-color: var(--brass-500); background: var(--soot-800); }
.campaign-open { white-space: nowrap; }
.accounts-code-form { margin-bottom: 0.6rem; }
.accounts-since { font-size: var(--text-xs); color: var(--brass-600); }
.login-code { letter-spacing: 0.12em; text-transform: uppercase; }

/* ---- Rearranging the party ------------------------------------------------
   The grip is the one control on a roster row that is not about the character
   - it is about where they sit - so it is quiet until the row is under the
   pointer, and then it is obvious. Always present in the document either way,
   because a control that appears on hover is a control a keyboard never finds;
   `opacity` rather than `display` keeps it in the tab order and keeps the row
   from reflowing when the mouse crosses it. */
.roster-grip {
    position: absolute;
    left: 0.15rem;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    padding: 0.1rem 0.15rem;
    border: none;
    background: none;
    color: var(--brass-600);
    font-size: var(--text-xs);
    line-height: 1;
    cursor: grab;
    opacity: 0;
    /* The browser must not claim the gesture for scrolling before the drag
       handler sees it. Without this a finger on the grip pans the column and
       the row never moves, which is the whole reason a tablet could not use
       this feature at all. */
    touch-action: none;
    transition: opacity 0.12s ease;
}
.roster-row-form, .detail-card { position: relative; }
/* The detail card's grip sits at its top rather than its middle: the card is
   tall, and a handle floating beside the ability scores reads as belonging to
   them. */
.detail-card .roster-grip { top: 0.6rem; transform: none; }
.roster-row-form:hover .roster-grip,
.detail-card:hover .roster-grip,
/* One focus ring color in this file, so learning what focus looks like once
   is enough - test_accessibility holds every :focus-visible outline to it. */
.roster-grip:focus-visible { opacity: 1; outline: 1px solid var(--brass-300); border-radius: var(--radius-small); }
.roster-grip:active { cursor: grabbing; }
/* The row being carried. Dimmed rather than hidden, so the gap it leaves is
   the gap it will fill and the list does not appear to lose somebody. */
.is-dragging { opacity: 0.4; }
/* While a drag is on, the whole column stops being text. A pointer drag with
   a mouse selects the names it crosses otherwise, which reads as the list
   being highlighted rather than rearranged - and the grips stay up, so the
   one being held does not blink out when the pointer leaves it. */
.party-roster.is-arranging { user-select: none; -webkit-user-select: none; }
.party-roster.is-arranging .roster-grip { opacity: 1; }

/* ---- When we play, and who is here tonight -------------------------------- */
.schedule-card .schedule-slot, .schedule-card .schedule-next { margin: 0 0 0.4rem; font-size: var(--text-sm); color: var(--parchment); }
.schedule-card b { color: var(--brass-300); }
.schedule-remind { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.6rem; margin: 0.5rem 0 0.3rem; }
.schedule-bell { display: inline-block; padding: 0.35rem 0.8rem; border-radius: var(--radius); text-decoration: none; }
.schedule-inline { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin: 0; }
.schedule-list { list-style: none; margin: 0.6rem 0; padding: 0; display: grid; gap: 0.3rem; }
.schedule-night { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.7rem; font-size: var(--text-sm); color: var(--parchment); padding: 0.25rem 0; border-bottom: 1px solid var(--wood-800); }
.schedule-night.is-off .schedule-when { text-decoration: line-through; color: var(--brass-600); }
.schedule-when { flex: 1 1 14rem; }
.schedule-tag { font-size: var(--text-xs); color: var(--brass-500); }
.schedule-tag.is-here { color: var(--faction-party); }
.schedule-tag.is-short { color: var(--faction-enemy); }
.balance-line { margin-left: 0.8rem; font-size: var(--text-xs); letter-spacing: 0.02em; text-transform: none; color: var(--brass-500); }
.balance-line b { color: var(--brass-300); font-weight: normal; }
.prep-balance { font-size: var(--text-xs); color: var(--brass-500); }
.schedule-why { width: 11rem; }
.schedule-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.5rem 0.8rem; margin: 0.5rem 0; }
.schedule-form .field { min-width: 7rem; }
/* ADD A PAST SESSION: the schedule form's row of fields, a row of ticks
   for who was there, and the recap under them. */
.past-session { margin: 0.4rem 0 0.8rem; }
.past-form { display: flex; flex-direction: column; gap: 0.6rem; }
.past-form .past-number { width: 5.5rem; }
.past-form .past-title { flex: 1 1 14rem; }
.past-form textarea { width: 100%; }
.past-present { border: 0; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.3rem 0.9rem; }
.past-present legend { margin-bottom: 0.3rem; }
.past-who { font-size: var(--text-sm); display: inline-flex; align-items: center; gap: 0.3rem; }
.past-imports { display: flex; flex-direction: column; gap: 0.2rem; margin: -0.4rem 0 0.8rem; }
/* IMPORT YOUR NOTES, the preview: one card per session, its lines under its
   fields. A line is quiet text; hovering it offers CUT HERE on the right. */
.history-preview .hist-section { padding-bottom: 0.6rem; }
.hist-preamble { opacity: 0.75; }
.hist-fields { align-items: flex-end; }
.hist-fields .past-title { flex: 1 1 18rem; }
.hist-fields .past-title input { width: 100%; }
.hist-action.is-quiet { opacity: 0.45; }
.hist-action.is-quiet select { pointer-events: none; }
.hist-join { margin-left: auto; }
.hist-lines { margin-top: 0.5rem; border-top: 1px solid var(--wood-800, rgba(255,255,255,0.08)); padding-top: 0.4rem; max-height: 22rem; overflow-y: auto; }
.hist-line { display: flex; align-items: flex-start; gap: 0.6rem; font-size: var(--text-sm); line-height: 1.45; padding: 0 0.3rem; border-radius: 3px; }
.hist-line:hover { background: rgba(255, 255, 255, 0.04); }
.hist-text { flex: 1 1 auto; white-space: pre-wrap; overflow-wrap: anywhere; min-width: 0; }
.hist-line.is-heading .hist-text, .hist-heading-sample { color: var(--brass-400); font-weight: 600; }
.hist-cut { visibility: hidden; flex: 0 0 auto; font-size: var(--text-2xs); padding: 0 0.4rem; }
.hist-line:hover .hist-cut, .hist-cut:focus { visibility: visible; }
@media (hover: none) { .hist-cut { visibility: visible; } }
.hist-submit { flex-wrap: wrap; margin: 0.8rem 0 2rem; }
/* Lore from the recaps: one suggestion per row - its page title and kind to
   accept with, DISMISS beside it, and the quotes it was found in under them. */
.suggest-item { display: grid; grid-template-columns: 1fr auto; gap: 0.2rem 0.8rem; padding: 0.7rem 0; border-top: 1px solid var(--wood-800); }
.suggest-item:first-of-type { border-top: 0; }
.suggest-accept { margin: 0; }
.suggest-accept .past-title { flex: 1 1 14rem; }
.suggest-dismiss { align-self: end; }
.suggest-seen, .suggest-line, .suggest-quotes { grid-column: 1 / -1; margin: 0; }
.suggest-line { font-size: var(--text-sm); }
.suggest-quotes { padding-left: 1.1rem; font-size: var(--text-sm); line-height: 1.5; }
.suggest-quotes li { margin: 0.15rem 0; }
.suggest-thread { grid-template-columns: 1fr auto auto; align-items: center; }
.suggest-thread-text { margin: 0; font-size: var(--text-sm); }
.suggest-restore { margin: 0.6rem 0 2rem; }
.schedule-webhook, .schedule-message { width: min(26rem, 100%); }
.schedule-discord, .schedule-tonight, .schedule-handoff { margin-top: 0.8rem; }
.handoff-warning { font-weight: 600; }
.tonight-strip { max-width: 100%; margin: 0 0 0.6rem; padding: 0.45rem 0.8rem; border: 1px solid var(--brass-700); border-radius: var(--radius); background: var(--soot-700); display: grid; gap: 0.35rem; font-size: var(--text-sm); }
.tonight-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.6rem; margin: 0; color: var(--parchment); }
.tonight-who b { color: var(--brass-300); }
.tonight-done { margin: 0 0 0 auto; }

/* ---- The war room, one subject (2026-09-23) ----------------------------- */
.map-under { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 1rem; margin-top: 0; }
/* The board's two forms are all hidden inputs; they submit the same with no
   box of their own, and without one they stop collecting the column's gap. */
#map-move-form, #area-form { display: contents; }
.map-under .map-tray, .map-under .map-readout { margin-top: 0; }
.map-under .map-readout { flex: 1 1 auto; }
.map-token-name { font-size: var(--text-xs); line-height: 1.1; text-transform: none; text-align: center; padding: 0 0.3rem; overflow-wrap: anywhere; }
.turn-open-form { display: inline; margin: 0; }
.turn-open { background: none; border: 0; padding: 0; font: inherit; letter-spacing: inherit; color: inherit; text-decoration: underline; text-decoration-color: currentColor; text-underline-offset: 0.2em; cursor: pointer; }
.token-react { display: none; position: absolute; left: 50%; top: 100%; transform: translate(-50%, 0.3rem); z-index: 4; padding: 0.15rem 0.45rem; font-size: var(--text-2xs); letter-spacing: 0.06em; white-space: nowrap; pointer-events: auto; }
.map-token.is-selected .token-react { display: block; }
.combatant-react { margin: 0 0 0.5rem; }
.hit-dice-word { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-500); }

/* ---- A board that does not grow and shrink (2026-09-23) ------------------- */
@media (min-width: 1251px) {
    /* Everything that comes and goes sits in the band between the header and
       the war room, instead of taking a row the board pays for. The header was
       about 51px of the 5rem the page reserves; the large mark (PR 319) made it
       69, and the band - "you missed", every flash on the war room - slid
       under it, half a line showing. The war room reserves 6.25rem now, so
       the band sits below the header again (found 2026-10-04). */
    body:has(.war-room) { --header-height: 6.25rem; }
    .tonight-strip, body:has(.war-room) .flash-area {
        position: fixed; z-index: 40;
        top: calc(var(--header-height) - 1.75rem);
        left: var(--page-gutter); right: var(--page-gutter);
        max-width: none; margin: 0;
    }
    .tonight-strip { padding: 0.15rem 0.7rem; display: block; }
    .tonight-strip .tonight-row { flex-wrap: nowrap; white-space: nowrap; overflow: hidden; }
    .tonight-strip .tonight-who { overflow: hidden; text-overflow: ellipsis; }
    /* A flash covers the band whole while it is up, rather than half of the
       line beneath it - two sentences interleaved read as neither. */
    body:has(.war-room) .flash-area { z-index: 41; display: flex; flex-direction: column; gap: 0.2rem; }
    body:has(.war-room) .flash { margin: 0; padding: 0.2rem 0.8rem; background: var(--soot-800); cursor: pointer; }
    /* The action card keeps one height whatever it is showing - two rows of
       tiles, a death save, or somebody else's turn - so the board above it
       does not change with the turn. */
    .war-room .action-bar { min-height: 9.8rem; }
}

/* ---- The initiative card, the picture first (2026-09-23) ------------------- */
.chip-fullname { position: relative; z-index: 1; padding: 0 0.2rem; font-size: var(--text-2xs); font-weight: 600; line-height: 1.15; text-align: center; color: var(--parchment); overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.chip-plate { position: absolute; left: 0; right: 0; bottom: 0; padding: 0.1rem 0.2rem 0.1rem 2.1rem; font-size: var(--text-2xs); line-height: 1.2; text-align: right; color: var(--parchment-bright); background: color-mix(in srgb, var(--soot-900) 78%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chip-init-badge { position: absolute; left: 0.15rem; bottom: 0.15rem; z-index: 3; min-width: 1.35rem; padding: 0.05rem 0.25rem; border-radius: var(--radius-small); background: color-mix(in srgb, var(--soot-900) 82%, transparent); color: var(--parchment-bright); font-size: var(--text-sm); font-weight: 600; line-height: 1.2; text-align: center; text-shadow: 0 1px 2px var(--soot-900); }
.chip-init-badge.is-editable { cursor: text; }
/* The box, when it shows, is the badge's size - a number input's wider
   default rule was covering the nameplate beside it. */
.initiative-entry input.chip-init { width: 1.7rem; height: 1.2rem; padding: 0 0.1rem; font-size: var(--text-xs); border-radius: var(--radius-small); border: 1px solid var(--brass-600); }
.chip-init-badge.is-editable:hover { outline: 1px solid var(--brass-500); }
/* One line, and inside the card: STATS and SHEET wrapped onto two and ran
   out of the rounder corners (2026-10-01). */
.chip-back-doors { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; }
.chip-back-doors a { font-size: 0.5rem; letter-spacing: 0.02em; padding: 0 0.3rem; line-height: 1.3; }
.chip-back { overflow: hidden; padding: 0.2rem 0.2rem; gap: 0.05rem; }
.chip-back-ac { line-height: 1.1; }
.chip-back .chip-back-condition .cond-icon { width: 1em; height: 1em; }

/* ---- The turn at the strip's end, the tools on a rail (2026-09-23) -------- */
.strip-row { display: flex; align-items: stretch; gap: 0.6rem; min-width: 0; }
.strip-row > .initiative-strip { flex: 1 1 auto; min-width: 0; }
/* The strip's two end cards: the same size and shape as a combatant's card,
   an icon where the portrait would be and a word where the name is. NEXT
   TURN at the front of the order in the turn's brass; START COMBAT at the
   end, and END COMBAT there in the danger color while a fight runs. */
.strip-control { flex: 0 0 auto; margin: 0; display: flex; flex-direction: column; gap: 0.3rem; width: 4.6rem; }
.strip-control-card {
    /* The combatant card's own size: 80% of its column, four by five. */
    width: 80%;
    align-self: center;
    aspect-ratio: 4 / 5;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    padding: 0.3rem 0.2rem;
    border-radius: var(--radius);
    border: 2px solid var(--brass-500);
    background: var(--soot-800);
    color: var(--brass-200);
    cursor: pointer;
}
.strip-control-card:hover { border-color: var(--brass-300); color: var(--parchment-bright); }
.strip-control-card svg { width: 1.7rem; height: 1.7rem; }
.strip-control-word { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; text-align: center; line-height: 1.15; }
.strip-control-next .strip-control-card { background: var(--brass-500); color: var(--soot-900); border-color: var(--brass-400); }
.strip-control-next .strip-control-card:hover { background: var(--brass-400); color: var(--soot-900); }
.strip-control-combat.is-ending .strip-control-card { border-color: var(--oxblood-500); color: var(--oxblood-400); }
.strip-control .round-name { width: 100%; font-size: var(--text-xs); }
/* The rail is in the flow beside the board, not pinned over its edge. Pinned,
   it took no height, so where the board is short (900px beside Discord) it hung
   past the stage and LIBRARIAN sat on SCATTER. Beside it, the stage is as tall
   as whichever of the two is taller. */
.map-stage:has(.tool-rail) { display: flex; align-items: flex-start; gap: 0.6rem; }
.board-well { position: relative; }
.map-stage:has(.tool-rail) > .board-well { flex: 1 1 auto; min-width: 0; }
.tool-rail { flex: 0 0 4.8rem; margin-top: 2.4rem; z-index: 3; width: 4.8rem; display: flex; flex-direction: column; gap: 0.3rem; }
.tool-rail form { margin: 0; }
.rail-btn { display: block; width: 100%; box-sizing: border-box; padding: 0.4rem 0.3rem; font-size: var(--text-2xs); letter-spacing: 0.06em; line-height: 1.2; text-align: center; text-decoration: none; color: var(--parchment); background-color: var(--soot-800); border: 1px solid var(--line); border-radius: var(--radius-small); cursor: pointer; }
.rail-btn:hover { border-color: var(--brass-500); color: var(--parchment-bright); }
.rail-btn.is-on { border-color: var(--brass-400); color: var(--brass-300); }
.rail-btn.rail-break { margin-top: 0.7rem; }

/* ---- The action card in three panels ----------------------------------- */
.bar-panels { display: flex; gap: 0.6rem; align-items: stretch; min-width: 0; }
.bar-panel { min-width: 0; }
.bar-panel-actions { flex: 1 1 auto; }
.bar-panel-slots, .bar-panel-resources { flex: 0 0 auto; width: 11rem; padding: 0.3rem 0.5rem; border-left: 1px solid var(--wood-800); display: flex; flex-direction: column; gap: 0.25rem; }
.bar-panel-title { margin: 0 0 0.15rem; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; font-weight: normal; color: var(--brass-500); }
.slot-row { display: flex; align-items: center; gap: 0.4rem; }
.slot-row-level { width: 1.6rem; font-size: var(--text-xs); color: var(--brass-400); }
.slot-row-pips { display: flex; gap: 0.25rem; }
.bar-panel-resources .bar-resource { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; }
.resource-btn { width: 100%; display: flex; justify-content: space-between; gap: 0.4rem; text-transform: capitalize; }
/* A resource with an amount and a target - Lay on Hands: how many points, and
   on whom - is three controls in an 11rem panel. In one row they crushed each
   other: the name cut to "Cas", the button one word a line and out past the
   card's edge, the number box stretched to the button's height (2026-10-06).
   So the row wraps: the amount and the target share the first line, the
   button takes the next whole line, on one line. */
.bar-panel-resources .bar-amount { flex: 0 0 auto; align-self: center; }
.bar-panel-resources select.bar-amount { flex: 1 1 4rem; min-width: 0; width: auto; }
.bar-panel-resources .resource-btn { flex: 1 1 100%; white-space: nowrap; min-width: 0; }
.bar-panel-resources .resource-btn .resource-name { overflow: hidden; text-overflow: ellipsis; }

/* ARRANGE: every tile a thing to drag, and nothing a tile does fires. The
   dashed edge says "these move" without a word; touch-action lets a finger
   drag rather than scroll the page. See bar_order.py. */
.is-arranging .act-tile { cursor: grab; touch-action: none; outline: 1px dashed var(--brass-500); outline-offset: -3px; }
.is-arranging .act-tile.is-lifted { cursor: grabbing; opacity: 0.55; }

/* A PHONE IN A PLAYER'S HAND. Beside the tiles, the slots panel left the
   actions 154px on a 390px screen, and seven columns of that is an 18px tile
   nobody can read or press. Under 700px the panels stack - actions first,
   because that is the turn - and the tiles take four columns of about 78px,
   which is a thumb. */
@media (max-width: 700px) {
    .bar-panels { flex-direction: column; }
    .bar-panel-slots, .bar-panel-resources { width: auto; border-left: none; border-top: 1px solid var(--wood-800); padding: 0.4rem 0 0; }
    .act-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    /* The banner already says whose turn it is; the name was the header's
       widest thing and pushed sign-out off the glass. */
    .header-user-name { display: none; }
}

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

/* The bestiary's order controls, which are toggles rather than radio buttons.
   The arrow reads as a statement about the list below - "A–Z" means the list IS
   A to Z - and not as a promise about what the click will do. Those are
   opposite conventions and a control cannot hold both; the one that matches
   what you are looking at wins. */
.bestiary-arrow {
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    color: var(--brass-700);
    font-variant: normal;
    margin-left: 0.15rem;
}

.bestiary-order.is-on .bestiary-arrow { color: var(--brass-500); }

/* How many the list holds, beside the controls that reorder it. It used to say
   315 in the subtitle while showing forty. */
.bestiary-count {
    margin-left: auto;
    font-size: var(--text-xs);
    color: var(--brass-700);
    font-variant: small-caps;
    letter-spacing: 0.06em;
}

/* An initiative score you may read but not set. Same size and position as the
   box it replaces, so the strip does not reflow between a DM's view and a
   player's - but no border and no caret, because it is not a control. */

/* A bar control that is somebody else's turn: the readout without the button.
   Same metrics as .bar-btn so the bar does not reflow between the acting
   player's view and everyone else's, but no border, no pointer and nothing
   that reads as pressable. */
.bar-readout,
span.bar-mode-btn {
    border-color: transparent;
    background: none;
    cursor: default;
    color: var(--brass-600);
}

span.bar-mode-btn.is-on { color: var(--brass-300); }

/* The roster row a player reads rather than presses. */
div.roster-row { cursor: default; }

/* Read, not seen. For a label a screen reader needs and a sighted person does
   not - the action tiles' captions, where the icon already says it and twelve
   pixels of eight-point uppercase under every glyph was costing the map a
   third of the action bar.

   The clip-rect shape rather than `display: none` or `visibility: hidden`,
   because both of those remove the element from the accessibility tree as
   well as from the page, which would take the button's name with it. */
.visually-hidden {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0; border-radius: var(--radius);
}


/* The grid without its picture. Centered on the board it should have filled,
   in the register the rest of the app uses for an absence: what is missing,
   and where the control that fixes it lives. */
.map-missing {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 1.5rem;
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.5;
    color: var(--brass-600);
    font-style: normal;
}

/* ---- the header at narrow widths ------------------------------------------
   Words while they fit. When they do not, base.html's script sets
   .header-tight on the root and the utilities go into MENU; if the sections
   still do not fit, .header-compact takes them too. MENU always opens every
   link as its word. Sign out stays on the bar at every width - "am I
   still me" is not a question to hide behind a menu. */
.header-menu { display: none; position: relative; }
.header-tight .site-utils, .header-compact .site-nav { display: none; }
.header-tight .header-menu { display: block; }
.header-tight .header-right-group { overflow: visible; }
.header-menu-button { list-style: none; cursor: pointer; padding: 0.3rem 0.7rem; font-size: var(--text-xs); letter-spacing: 0.06em;
                      color: var(--parchment); border: 1px solid var(--wood-600); border-radius: var(--radius-small); }
.header-menu-button::-webkit-details-marker { display: none; }
.header-menu[open] .header-menu-button { background: var(--brass-300); color: var(--soot-900); border-color: var(--brass-300); }
.header-menu-button:focus-visible { outline: 2px solid var(--brass-300); outline-offset: 1px; }
.header-menu-list { position: absolute; right: 0; top: calc(100% + 0.5rem); z-index: 120; min-width: 12rem;
                    display: flex; flex-direction: column; padding: 0.4rem 0; background: var(--soot-800);
                    border: 1px solid var(--line); border-radius: var(--radius-small); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); }
.header-menu-list a { padding: 0.5rem 1rem; font-size: var(--text-md); color: var(--parchment); text-decoration: none; }
.header-menu-list a.is-quiet { font-size: var(--text-sm); color: var(--brass-300); }
.header-menu-list a:hover, .header-menu-list a:focus-visible { background: var(--soot-700); }
.header-menu-list a[aria-current="page"] { color: var(--brass-200); text-decoration: underline; text-underline-offset: 4px; }
.header-menu-rule { height: 1px; margin: 0.3rem 1rem; background: var(--wood-700); }
@media (max-width: 600px) {
    .header-compact .header-user-name { display: none; }
}

/* ============================================
   THE INSPECTOR RAIL — one region, many nouns (design/ux.md §1)
   ============================================
   Modals are for verbs; this is for nouns. It never moves and never blocks,
   which is the pair of properties the whole argument rests on: a DM reads a
   stat block WHILE typing damage into a box, and a dialog carrying it would
   own the focus the box needs.

   position: fixed, from under the header to the bottom of the viewport. Fixed
   rather than a fourth grid column because the rail belongs to the SHELL and
   not to the war room - the bestiary, the sheet and the prepare page all want
   it, and none of them is a grid. Its top is the header's own height, measured
   from the token rather than guessed; `.table-link` is the cautionary tale
   here, still shipping `top: 0` under a fixed header it cannot see. */
.inspector {
    /* The fragment on every link into the rail scrolls it to the top of the
       viewport, and `.site-header` is `position: fixed` at z-index 100 - so
       without this the rail's own title lands UNDERNEATH the header, which is
       the same trap `.table-link` and the prepare page's tally each found on
       their own. It matters more since v0.26: a POST from a form inside the
       rail comes back to `#inspector`, so this is now the landing every DMG
       press makes rather than only the one a link makes.

       The value is a fallback. `_inspector.html` measures the header and
       overrides it, because the header is one row wide and two rows stacked
       and 5rem is only true of the first. */
    scroll-margin-top: calc(var(--header-height) + 0.5rem);
    position: fixed;
    top: var(--header-height);
    right: 0;
    bottom: 0;
    width: var(--inspector-width);
    z-index: 40;
    display: flex;
    flex-direction: column;
    /* The rail scrolls, and that does not break "nothing scrolls" - that rule
       is about the PAGE moving under a DM who did not ask it to. A region with
       a stat block longer than the screen has to give somewhere, and giving
       inside itself is the one place that costs nothing else. */
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 1rem 1.1rem 1.4rem;
    background: var(--soot-800);
    border-left: 1px solid var(--brass-700);
    box-shadow: -8px 0 24px rgba(0, 0, 0, 0.45);
}

/* The page gets out of the rail's way rather than under it. `:has()` instead
   of a body class because every template supplies the whole of its own
   `body_class` attribute - there is no seam to append to without editing
   thirty-two files for something the stylesheet can ask about directly. */
body:has(.inspector) {
    padding-right: calc(var(--inspector-width) + var(--page-gutter));
}

.inspector-head {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: start;
    gap: 0.5rem;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid var(--soot-500);
}

.inspector-kind {
    grid-column: 1;
    margin: 0;
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brass-600);
}

.inspector-title {
    grid-column: 1;
    margin: 0;
    font-size: var(--text-lg);
    font-weight: normal;
    color: var(--parchment);
}

/* Grid row 1, column 2 - so the close control sits level with the kind label
   and stays put when a long name wraps the title onto a second line. */
.inspector-close {
    grid-column: 2;
    grid-row: 1 / span 2;
    align-self: center;
    /* 44px, which is the touch target the viewport meta tag exists to protect.
       A close control that needs aiming is the one control on the rail that
       gets pressed under pressure. */
    min-width: 2.75rem;
    min-height: 2.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-lg);
    line-height: 1;
    color: var(--brass-600);
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: var(--radius-small);
}

.inspector-close:hover { color: var(--brass-300); border-color: var(--brass-700); }

.inspector-body { flex: 1 1 auto; padding-top: 0.7rem; font-size: var(--text-sm); }

.inspector-missing { margin: 0 0 0.5rem; color: var(--parchment); }
.inspector-elsewhere { margin: 0; font-size: var(--text-sm); }
.inspector-elsewhere a { color: var(--brass-400); }

.inspector-attribution {
    margin: 1.2rem 0 0;
    padding-top: 0.6rem;
    border-top: 1px solid var(--soot-500);
    font-size: var(--text-2xs);
    line-height: 1.5;
    color: var(--wood-600);
}

/* ---- what the nouns draw inside it ---- */

.inspect-flavor {
    margin: 0 0 0.8rem;
    font-style: normal;
    color: var(--brass-600);
    text-transform: lowercase;
}

.inspect-stats { margin: 0 0 0.9rem; display: grid; gap: 0.25rem; }

.inspect-stat {
    display: grid;
    grid-template-columns: 7.5rem 1fr;
    gap: 0.5rem;
    align-items: baseline;
}

.inspect-stat dt {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--brass-600);
}

.inspect-stat dd { margin: 0; color: var(--parchment); }
.inspect-stat dd small { color: var(--brass-600); }

/* Six columns, because six ability scores in two rows of three is the one
   arrangement nobody who has held a character sheet has to read twice. */
.inspect-abilities {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 0.3rem;
    margin: 0 0 1rem;
}

.inspect-ability {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.05rem;
    padding: 0.35rem 0.1rem;
    border: 1px solid var(--soot-500);
    border-radius: var(--radius-small);
    background: var(--soot-700);
}

.inspect-ability-name {
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    color: var(--brass-600);
}

/* The mono face for numbers read under pressure, which is the hedge §2 asks
   for against Georgia's numerals at small sizes. */
.inspect-ability-score { font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums; font-size: var(--text-md); color: var(--parchment); }
.inspect-ability-mod { font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums; font-size: var(--text-xs); color: var(--brass-500); }

.inspect-heading {
    margin: 1rem 0 0.4rem;
    font-size: var(--text-xs);
    font-weight: normal;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brass-600);
}

.inspect-attacks { margin: 0; padding-left: 1rem; display: grid; gap: 0.2rem; }
.inspect-attacks li { color: var(--parchment); }
.inspect-none { margin: 0; color: var(--wood-600); }

/* The SRD's prose arrives as one string with newlines in it. Honoring them
   here beats splitting it into paragraphs in the template, which would be a
   second parser for text `tools/srd_text.py` has already cleaned. */
.inspect-text {
    margin: 0 0 0.7rem;
    white-space: pre-line;
    line-height: 1.55;
    color: var(--parchment);
}

.inspect-classes {
    margin: 0.8rem 0 0;
    font-size: var(--text-xs);
    color: var(--brass-600);
}

/* ---- a condition, in the rail ----

   Fourteen of them are a bulleted list of effects. Exhaustion is prose, a
   table of six levels and more prose, because it is the one condition that is
   a track rather than a state - so these three rules are not alternatives to
   each other, they are the three things one condition can print. */

/* The icon the chip was wearing, big enough to be looked at on purpose. It
   reads `--cond` like every other place a condition is drawn, so the mark at
   the top of the rail is the same color as the badge that was clicked to get
   here - which is the whole reason it is here rather than decoration. */
.inspect-mark { margin: 0 0 0.9rem; line-height: 1; color: var(--cond, var(--condition)); }
.inspect-mark .cond-icon { width: 2rem; height: 2rem; vertical-align: 0; }

.inspect-rules { margin: 0; padding-left: 1.05rem; display: grid; gap: 0.5rem; }
.inspect-rules li { color: var(--parchment); line-height: 1.55; }

.inspect-levels { width: 100%; margin: 0.1rem 0 0.9rem; border-collapse: collapse; }
.inspect-levels th {
    padding: 0 0.5rem 0.3rem 0;
    border-bottom: 1px solid var(--soot-500);
    font-size: var(--text-2xs);
    font-weight: normal;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-align: left;
    color: var(--brass-600);
}
.inspect-levels td { padding: 0.3rem 0.5rem 0.3rem 0; color: var(--parchment); vertical-align: top; }
/* The mono face for a number read under pressure, the same hedge the ability
   scores take against Georgia's numerals at small sizes. */
.inspect-levels td:first-child { width: 2.5rem; font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums; color: var(--brass-400); }

.inspect-see-also { margin: 0.9rem 0 0; font-size: var(--text-xs); color: var(--brass-600); }

/* WHAT THEY HAVE: six saves and eighteen skills read as two columns, because
   a one-per-line list of twenty-four numbers is a scroll and the sheet they
   were typed from is a grid. The proficient rows are the bright ones rather
   than the only ones - an untrained skill still has a number to roll. */
/* `.inspect-rules` is a grid already, so this is a second column on it
   rather than CSS multicol - `columns: 2` on a grid container lays out nothing. */
.inspect-rules.inspect-columns {
    grid-template-columns: 1fr 1fr;
    gap: 0.3rem 1.2rem;
}
.inspect-rules .is-proficient { color: var(--brass-200); }
.inspect-rules .is-proficient strong { color: var(--brass-300); }
.inspect-dim { font-size: var(--text-xs); color: var(--brass-600); }
.skill-edit { margin-top: 0.5rem; }
.skill-edit summary { cursor: pointer; }
.skill-edit-table { width: 100%; border-collapse: collapse; margin-top: 0.4rem; font-size: var(--text-xs); }
.skill-edit-table th { text-align: left; font-weight: normal; padding: 0.15rem 0.4rem 0.15rem 0; white-space: nowrap; }
.skill-edit-table td { padding: 0.15rem 0; }
.skill-edit-train, .skill-edit-bonus { display: inline-flex; gap: 0.2rem; align-items: center; margin: 0; }
.skill-edit-bonus input[type="number"] { width: 3.2em; }
.skill-edit-bonus input[type="text"] { width: 7em; min-width: 0; }

/* ---- the manual, in the rail ----

   The help page's prose arrives here as its own markup - bare paragraphs, a
   grammar block, a table of keys - because it is the SAME macro the page
   renders and it would be a second copy of the manual otherwise. What it does
   not arrive with is the page's spacing: on `/help` each section sits in a
   card that separates its paragraphs, and in the rail they ran together into
   one wall of text.

   Scoped to this wrapper rather than to `.inspector p`, which would be a
   heavier selector than `.inspect-text` and would quietly take over every
   other noun's prose. */
.inspect-manual p { margin: 0 0 0.7rem; line-height: 1.55; color: var(--parchment); }
.inspect-manual p:last-child { margin-bottom: 0; }
/* The same treatment the manual's own page gives a bold run - brass, and not
   actually bold. Matched rather than invented: it is one manual, and a term
   that is brass on `/help` and white in the rail reads as two different kinds
   of word. Links here are underlined, which is what keeps the two apart. */
.inspect-manual b { color: var(--brass-300); font-weight: normal; }
/* The grammar block is the widest thing the manual prints - `Longbow ranged
   150/600 +6 1d8+3 piercing` is 41 characters of monospace - and the rail is
   22rem. It keeps its own scrollbar rather than setting the width of the
   region, which is the same rule the war room's wide tables follow. */
.inspect-manual .help-grammar { font-size: var(--text-xs); padding: 0.45rem 0.6rem; }
.inspect-manual table { width: 100%; font-size: var(--text-xs); }
.inspect-manual .help-keys td:first-child { width: 3rem; }
.inspect-manual .an-note { font-size: var(--text-xs); color: var(--wood-600); }

/* The manual's index of the fifteen. Wraps, because the rail is 22rem and the
   help page is a column - the badges are the same ones the cards wear, so the
   thing learned here is the thing recognized there. */
.help-conditions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.5rem 0 0; }

/* A condition badge that is also a door to the rules. The badge keeps every
   pixel it had - the same color, border and fill - and gains the underline a
   link owes the reader, because color alone is never a signal here and a badge
   that was not clickable yesterday has to say that it is today. */
/* CURRENTCOLOR, not `--cond`, and the dire five are why.

   The first version drew this underline in the condition's own hue, which is
   the obvious thing to reach for and is invisible on exactly the badges that
   matter: severity is a FILL, so a stunned badge is soot text ON its hue, and
   an underline in that same hue disappears into the ground behind it. Five of
   the fifteen had nothing saying they were clickable but the cursor - and they
   are the five a DM most wants the rules for.

   `currentColor` is the badge's own text color either way: the hue on an
   outlined badge, the dark ink on a filled one. Same lesson the `is-dire` rule
   above learned about fallbacks, from the other end. Found by looking at the
   manual's row of all fifteen, which is the one place they are side by side. */
a.condition-badge, a.chip-condition-badge, a.chip-back-condition { text-decoration: none; cursor: pointer; }
a.condition-badge span, a.chip-condition-badge span {
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
    text-underline-offset: 0.15em;
}
a.condition-badge:hover span, a.chip-condition-badge:hover span,
a.condition-badge:focus-visible span, a.chip-condition-badge:focus-visible span {
    text-decoration-color: currentColor;
}
a.chip-back-condition:hover, a.chip-back-condition:focus-visible {
    border-color: var(--cond, var(--condition));
}

/* A link that opens the rail, wherever one sits in running text or a table. */
.inspect-link { color: var(--brass-400); text-decoration: none; border-bottom: 1px dotted var(--brass-700); }
.inspect-link:hover { color: var(--brass-300); border-bottom-color: var(--brass-400); }
/* A page this table wrote, told from the SRD's by color alone (2026-10-27). */
.inspect-link.is-homebrew, .card-spells-names .inspect-link.is-homebrew { color: var(--verdigris-400); border-bottom: 1px solid var(--verdigris-600); }
.inspect-link.is-homebrew:hover { color: var(--verdigris-500); border-bottom-color: var(--verdigris-400); }

/* Half a monitor and narrower: the rail stops being a rail.

   1100px, and the number is arithmetic rather than taste. The war room's grid
   is two 300px side panels and two 1.5rem gaps - 648px that CANNOT shrink,
   because a fixed grid column does not - plus the page's own gutters, plus the
   rail's 22rem. That adds up to about 1032px, and below it the grid overflows
   its container and slides UNDER the rail rather than beside it: at 900px the
   combat log, the notes and the inventory were all behind the stat block, and
   the board had been squeezed to nothing.

   Found by looking at it at 900px, which is the width Rollies is actually for -
   it sits beside Discord. The roadmap's first look-through had already named
   ~1100px as where this layout needs a real breakpoint, from the other
   direction and before the rail existed.

   So below it the rail goes into the flow, under the page, and the
   `#inspector` fragment every link carries is what takes you to it. That is
   why the links carry a fragment at all: on a wide screen it does nothing. */
@media (max-width: 1100px) {
    .inspector {
        position: static;
        width: auto;
        max-height: none;
        margin-top: 1.5rem;
        border-left: none;
        border-top: 1px solid var(--brass-700);
        box-shadow: none;
    }

    body:has(.inspector) { padding-right: var(--page-gutter); }

    /* In flow the rail is as wide as the page, and a line of spell text 830
       pixels long is harder to read than the same text in the 22rem column it
       came from. Cap the measure rather than the region: the panel still
       spans the page, its contents stop at something a person can read. */
    .inspector-head,
    .inspector-body,
    .inspector-attribution { max-width: 46rem; }

    /* The war room stacks at this same width - see its own rule, up beside
       `.war-room` - so the rail now sits under a column of panels rather than
       under a pinned grid.

       What that rule inherits from this one is the reason it hands each
       stacked panel an explicit height. The first version of this block
       unpinned the war room with `height: auto`, reasoning that a page with a
       region below the fold should own its own scrolling. That made the war
       room 26,000 pixels tall: the combat log had been scrolling inside a
       fixed-height panel, and releasing the height let two thousand events set
       the row. The rail ended up 26,741px down the page.

       So the page scrolls by a screenful at a time and by a known number of
       them, which is what the `#inspector` fragment on every link is for.
       "Nothing scrolls" is a rule about the page moving under a DM who did not
       ask - not about a DM who clicked a link to a stat block. */
}

/* The way into a combatant's stat block, beside its name in the initiative
   order. A link rather than a button because it goes somewhere - the rail -
   and the browser's own "open in a new tab" is worth having on a stat block. */
.initiative-statblock {
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    color: var(--brass-600);
    text-decoration: none;
    padding: 0.15rem 0.3rem;
    border: 1px solid var(--soot-500);
    border-radius: var(--radius-small);
}

.initiative-statblock:hover { color: var(--brass-300); border-color: var(--brass-500); }

/* A map that carries its own printed grid is laid rather than fitted.

   `object-fit: contain` is right for a picture with no grid of its own - it
   never crops and never stretches, and the bars that appear when the counts
   disagree are the page saying so. It is exactly wrong for a printed grid: the
   letterboxing insets the map's lines from the board's edge, so Rollies' squares
   and the cartographer's are guaranteed to disagree by however wide the bars
   are.

   So when the grid has been measured (or typed), the image is positioned
   absolutely: scaled so one printed square is one cell, and shifted by the
   offset so the first printed line falls on the first of Rollies'. Everything is
   in CELLS, which keeps `--map-cell` the only place that knows a square is
   thirty pixels. */
.map-image.is-placed {
    position: absolute;
    left: calc(var(--img-x) * var(--map-cell));
    top: calc(var(--img-y) * var(--map-cell));
    width: calc(var(--img-cells-x) * var(--map-cell));
    height: calc(var(--img-cells-y) * var(--map-cell));
    max-width: none;
    object-fit: fill;
}

/* The map tools' newer rows: pixels per square, the nudges, and the note that
   says a picture's own grid is being followed. */
.map-cell-field input { width: 5.5rem; }
.map-grid-note {
    margin: 0.4rem 0 0;
    font-size: var(--text-xs);
    color: var(--brass-600);
}
.map-measure-btn { white-space: nowrap; }
.map-paste-hint {
    flex-basis: 100%;
    margin: 0.35rem 0 0;
    font-size: var(--text-xs);
    color: var(--wood-600);
}
.map-paste-hint kbd {
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: 0.9em;
    padding: 0.05rem 0.25rem;
    border: 1px solid var(--soot-500);
    border-radius: var(--radius-small);
    color: var(--brass-600);
}


/* ---- The feedback bubble, and the channel it posts to ----

   One control on every signed-in page, in the corner the inspector rail is
   NOT in. Fixed, because it belongs to the application rather than to the
   page under it; below the header's z-index, because nothing sits over the
   header; and a <dialog> is the top layer regardless, so the bubble never has
   to be on top of what it opens. */
.feedback-bubble {
    position: fixed;
    left: 1rem;
    bottom: 1rem;
    z-index: 90;
    width: 2.6rem;
    height: 2.6rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--brass-700);
    border-radius: 50%;
    background: var(--soot-800);
    color: var(--brass-400);
    cursor: pointer;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
}
.feedback-bubble:hover { border-color: var(--brass-400); color: var(--brass-300); }
/* Half a monitor: the page runs to the left edge, so a bubble in that corner
   sits on the first column of whatever is there - the bestiary's names, the
   war room's party. The rail is in the flow below 1100px rather than pinned
   to the right, which makes the other corner the free one at exactly the
   width this one stops being. Same number as the rail's, for the same
   reason. */
@media (max-width: 1100px) {
    .feedback-bubble { left: auto; right: 1rem; }
}
/* Hosted: the war room puts the bubble inside the action card instead, where
   it is a control on a surface rather than a thing floating over one. Every
   fixed-position property is undone rather than overridden, because a half
   un-fixed element is the kind of bug that only shows up at one width. The
   shadow goes too - it was there to lift the bubble off the page, and inside
   a card there is nothing to lift it off. */
/* In the card's bottom-right corner rather than a row of its own: in flow
   it added a full row of height to the action card, and every row there is a
   row of board. */
.feedback-bubble.is-hosted {
    position: absolute;
    right: 0.5rem;
    bottom: 0.5rem;
    left: auto;
    top: auto;
    box-shadow: none;
    background: var(--soot-500);
}
.feedback-bubble.is-hosted .feedback-count { top: -0.2rem; right: -0.2rem; }
/* ...and the card keeps that corner clear. Pinned over the card, the bubble sat
   on whatever reached the bottom-right - Channel Divinity, Lay on Hands - and
   on the card's own rounded border (2026-10-06). A gutter down the card's right
   side, the bubble's width and a little air, instead of a row at its foot:
   width is the cheaper thing to give here, since every row of height the card
   takes is a row of board. */
.action-bar:has(.feedback-bubble.is-hosted) { padding-right: 3.6rem; }
.feedback-bubble.is-hosted { right: 0.6rem; bottom: 0.6rem; }
.feedback-glyph { width: 1.25rem; height: 1.25rem; fill: currentColor; }
/* The DM's count of threads waiting on an answer. A number, not a dot:
   "three people are stuck" and "one question" are different things. */
.feedback-count {
    position: absolute;
    top: -0.35rem;
    right: -0.35rem;
    min-width: 1.2rem;
    height: 1.2rem;
    padding: 0 0.3rem;
    border-radius: var(--radius);
    background: var(--oxblood-400);
    color: var(--soot-900);
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--text-xs);
    line-height: 1.2rem;
    text-align: center;
}

.feedback-modal { max-width: min(30rem, 92vw); }
.feedback-form { display: grid; gap: 0.6rem; }
.feedback-form textarea,
.channel-post textarea {
    width: 100%;
    box-sizing: border-box;
    padding: 0.5rem 0.6rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    background: var(--soot-900);
    color: var(--parchment);
    font: inherit;
    resize: vertical;
}
.feedback-kinds { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.feedback-kind {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.6rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    font-size: var(--text-sm);
    color: var(--parchment);
    cursor: pointer;
}
.feedback-kind:has(input:checked) { border-color: var(--brass-400); background: var(--soot-700); }
.feedback-row { display: flex; justify-content: space-between; align-items: center; gap: 0.6rem; }

/* The page. Plain, like the shelf: read between sessions. */
.channel-post { display: grid; gap: 0.5rem; margin-bottom: 1.2rem; }
.channel-post .btn-primary { justify-self: end; }
.channel-thread { margin-bottom: 0.8rem; }
.channel-thread.is-resolved { opacity: 0.75; }
.channel-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.4rem; }
.channel-subject { margin: 0; font-size: var(--text-md); font-weight: normal; color: var(--parchment); }
/* The kind is a word in a color - never the color alone - and each of the
   five reads on its own the way the fifteen conditions do. */
.channel-kind {
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    padding: 0.1rem 0.45rem;
    border: 1px solid currentColor;
    border-radius: var(--radius-small);
    color: var(--brass-500);
}
.channel-kind-bug { color: var(--oxblood-400); }
.channel-kind-feature { color: var(--brass-300); }
.channel-kind-question { color: var(--verdigris-400); }
.channel-kind-announcement { color: var(--parchment); }
.channel-kind-fix { color: var(--verdigris-400); }
.channel-resolved { font-size: var(--text-2xs); letter-spacing: 0.12em; color: var(--verdigris-400); }
.channel-body { margin: 0 0 0.4rem; white-space: pre-line; color: var(--parchment); }
.channel-page { margin: 0 0 0.5rem; font-size: var(--text-xs); color: var(--brass-600); }
.channel-page a { color: var(--brass-400); }
.channel-reply { margin: 0.4rem 0 0 0.8rem; padding-left: 0.6rem; border-left: 2px solid var(--line); }
.channel-reply.is-dm { border-left-color: var(--brass-500); }
.channel-reply p { margin: 0.1rem 0 0; color: var(--parchment); white-space: pre-line; }
.channel-reply-by { font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--brass-600); }
.channel-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.6rem; }
.channel-reply-form { display: flex; gap: 0.4rem; flex: 1 1 16rem; }
.channel-reply-form input { flex: 1; min-width: 0; }

/* The page for when there is no page. One sentence, one way back. */
.error-page .chronicle-head { margin-bottom: 1rem; }
/* The same button the empty campaign offers, for the same reason: a page
   with one thing to do gets one control, not a link among links. */
.error-way-back { display: inline-block; padding: 0.55rem 1.1rem; text-decoration: none; margin-top: 0.4rem; }

/* THE TURN THAT IS A DEATH SAVE — the action bar for a creature at nothing.
   Oxblood rather than the bar's usual brass, because this row is the one
   thing on the page that says a character is about to stop existing, and it
   should not read as one more control. The death pips and buttons are the
   detail card's, reused: two shapes for one state is two things to keep true. */
.bar-down-word {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--oxblood-400);
}

/* FREE PLAY, the bar between fights. The word says why there are no action
   pips: nobody has a turn. Brass rather than oxblood - it is a state of the
   table, not a warning. */
.bar-free-word {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--brass-600);
}

.free-tiles { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.free-btn { min-width: 4.5rem; }
.roll-modal .modal-hint { font-size: var(--text-xs); color: var(--brass-600); align-self: center; }

/* Scoped through `.bar-strips` on purpose, because `.bar-strips .bar-row` sets
   the padding and the alignment for the three plates this row replaces, and a
   bare `.bar-down-row` loses to it on specificity - which it did, silently,
   until the D of DYING was seen sitting on the border. */
.bar-strips .bar-down-row {
    gap: 0.5rem;
    align-items: center;
    border-left: 2px solid var(--oxblood-400);
    padding: 0.1rem 0 0.1rem 0.6rem;
}

/* NAT 20 wrapped onto two lines beside three buttons that did not, which put
   the row's height at the mercy of its longest label. Found by looking. */
.bar-down-row .death-btn { white-space: nowrap; }

.bar-down-title {
    font-size: var(--text-sm);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--oxblood-400);
}

/* Stable is not dying and should not be lit like it. Dead is neither: the row
   keeps the word and loses the urgency, because there is nothing left to do
   about it on this turn. */
.bar-down-row.is-stable { border-left-color: var(--verdigris-400); }
.bar-down-row.is-stable .bar-down-title { color: var(--verdigris-400); }
.bar-down-row.is-dead { border-left-color: var(--soot-500); }
.bar-down-row.is-dead .bar-down-title { color: var(--brass-600); }


/* THE COMPANIONS — NPCs traveling with the party, under their own heading.
   "The party" means the players' characters, so this is a second section
   rather than five more rows: the count under CHARACTERS should be the count
   anybody at the table would give. */
.roster-divider {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0.6rem 0 0.3rem;
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--brass-600);
}

/* The rule runs to the end of the column on the right of the words, which is
   what makes it read as a heading for what follows rather than as a caption
   on what came before. */
.roster-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background-color: var(--wood-700);
}

.roster-none {
    margin: 0 0 0.4rem;
    font-size: var(--text-xs);
    color: var(--brass-600);
}

/* The row and its set-aside press. The row is itself a form, so the press
   cannot live inside it - it sits OVER the card's top-right corner instead,
   small and quiet, and shows itself when the pointer is on the card. It was a
   full-height bordered column beside every card, which made the rarest thing
   the DM does (someone steps away for a few sessions) the loudest thing on
   the party list. On a touch screen there is no hover, so it stays faintly
   visible there. DM only; players never see it. */
.roster-pair { position: relative; }

.roster-aside-form {
    position: absolute;
    top: 0.3rem;
    right: 0.4rem;
    z-index: 1;
}

.roster-aside-btn {
    width: 1.3rem;
    height: 1.3rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background-color: transparent;
    color: var(--brass-600);
    font-size: var(--text-xs);
    line-height: 1;
    opacity: 0;
}

.roster-pair:hover .roster-aside-btn,
.roster-aside-btn:focus-visible { opacity: 1; }

.roster-aside-btn:hover {
    color: var(--oxblood-400);
    background-color: color-mix(in srgb, var(--oxblood-400) 12%, transparent);
}

@media (hover: none) {
    .roster-aside-btn { opacity: 0.55; }
}

/* Set aside, and still findable. One thin line of names, each of them the
   press that brings that person back — because a control that makes something
   vanish with no way back is not a toggle, it is a delete. */
.roster-elsewhere {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.3rem;
    margin-top: 0.4rem;
}

.roster-elsewhere-label {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--brass-700);
}

.roster-elsewhere-form { display: inline-flex; }

.roster-elsewhere-btn {
    padding: 0.1rem 0.35rem;
    border: 1px dashed var(--line);
    border-radius: var(--radius-small);
    background-color: transparent;
    color: var(--brass-600);
    font-size: var(--text-xs);
    cursor: pointer;
}

.roster-elsewhere-btn.is-readonly { cursor: default; }

.roster-elsewhere-form .roster-elsewhere-btn:hover {
    border-style: solid;
    border-color: var(--verdigris-500);
    color: var(--verdigris-400);
}

/* The tick on the review page that says what a sheet's owner is. */
.intake-kind { margin: 0.6rem 0; }
/* What approving a sheet changes, beside the button; what would hurt in red. */
.intake-review .creation-controls { flex-wrap: wrap; }
.intake-changes { flex: 1 1 16rem; font-size: var(--text-sm); }
.intake-changes ul { margin: 0.3rem 0 0.4rem; padding-left: 1.1rem; }
.intake-changes .is-warning { color: var(--danger); font-weight: 600; }
.intake-kept { color: var(--parchment); opacity: 0.8; }

/* ---- The theme picker -----------------------------------------------------
   Ten cards, each carrying its own palette scoped to itself, so every rule
   below is written in tokens and means something different ten times on one
   page. That is the whole demonstration: if a card needed a single hardcoded
   color to look right, the theme mechanism would not work for the app either.

   The sample is deliberately a piece of the war room rather than a row of
   swatches. Six hexes in a line do not tell anybody what four hours in a
   palette feels like, and the six under each card are there for the person
   who does want the numbers. */
.theme-showing { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 1rem; }
.theme-clear {
    background: none;
    border: 1px solid var(--line);
    color: var(--brass-500);
    font-family: var(--font-body);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    padding: 0.2rem 0.6rem;
    border-radius: var(--radius-small);
    cursor: pointer;
}
.theme-clear:hover { color: var(--brass-300); border-color: var(--brass-600); }

/* auto-fit rather than a fixed count, because this page is read on the same
   half-monitor beside Discord that the war room is, and ten cards that insist
   on five across are ten cards nobody can see. */
.theme-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 1rem;
}
.theme-card {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--soot-900);
    display: flex;
    flex-direction: column;
}
.theme-card.is-mine { border-color: var(--brass-400); box-shadow: 0 0 0 1px var(--brass-400); }

.theme-sample { background: var(--soot-800); padding: 0.6rem; border-bottom: 1px solid var(--wood-800); }
.theme-sample-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.5rem; }
.theme-sample-turn { color: var(--brass-600); font-size: var(--text-2xs); letter-spacing: 0.12em; }

.theme-sample-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    background: var(--soot-500);
    border: 1px solid var(--faction-party);
    border-radius: var(--radius-small);
    padding: 0.35rem 0.45rem;
}
/* The blank face, in the side's color, the same answer the chips give. */
.theme-sample-face {
    width: 1.4rem; height: 1.4rem; border-radius: 50%; flex: 0 0 auto;
    background: var(--soot-700);
    border: 1px solid var(--faction-party);
}
.theme-sample-who { color: var(--parchment); font-size: var(--text-sm); }
.theme-sample-bar { flex: 1; height: 0.4rem; background: var(--soot-700); border-radius: var(--radius-small); overflow: hidden; }
.theme-sample-fill { display: block; width: 64%; height: 100%; background: var(--hp-good); }
.theme-sample-hp { color: var(--brass-500); font-size: var(--text-2xs); font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums; }

.theme-sample-acts { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.5rem; }
.theme-sample-btn { background: var(--brass-400); color: var(--soot-900); font-size: var(--text-2xs); font-weight: 600; letter-spacing: 0.06em; padding: 0.2rem 0.55rem; border-radius: var(--radius-small); }
.theme-sample-tag { color: var(--oxblood-400); border: 1px solid var(--oxblood-400); font-size: var(--text-2xs); letter-spacing: 0.06em; padding: 0.1rem 0.4rem; border-radius: var(--radius-small); }
.theme-sample-sides { display: flex; align-items: center; gap: 0.35rem; margin-top: 0.5rem; }
.theme-dot { width: 0.7rem; height: 0.7rem; border-radius: 50%; }
.theme-dot-party   { background: var(--faction-party); }
.theme-dot-ally    { background: var(--faction-ally); }
.theme-dot-neutral { background: var(--faction-neutral); }
.theme-dot-enemy   { background: var(--faction-enemy); }

.theme-meta { padding: 0.6rem; display: flex; flex-direction: column; gap: 0.3rem; flex: 1; }
.theme-class { font-size: 0.72em; letter-spacing: 0.06em; color: var(--brass-500); font-weight: normal; }
.theme-name { color: var(--parchment-bright); font-size: var(--text-sm); font-variant: small-caps; letter-spacing: 0.06em; }
.theme-tag {
    font-variant: normal;
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    color: var(--soot-900);
    background: var(--brass-500);
    border-radius: var(--radius-small);
    padding: 0 0.25rem;
    vertical-align: 0.1em;
}
.theme-tag-table { background: var(--brass-300); }
.theme-blurb { color: var(--brass-500); font-size: var(--text-xs); line-height: 1.4; flex: 1; }

.theme-actions { display: flex; gap: 0.4rem; padding: 0 0.6rem 0.6rem; }
.theme-take, .theme-set-table {
    flex: 1;
    font-family: var(--font-body);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    padding: 0.3rem 0.4rem;
    border-radius: var(--radius-small);
    cursor: pointer;
    background: var(--soot-500);
    border: 1px solid var(--line);
    color: var(--parchment);
}
.theme-take:hover:not(:disabled), .theme-set-table:hover:not(:disabled) {
    border-color: var(--brass-400);
    color: var(--brass-300);
}
/* A grayed control has to say it is off in more than one channel, which is
   section 16's complaint and the reason the cursor changes too. */
.theme-take:disabled, .theme-set-table:disabled {
    color: var(--brass-600);
    border-color: var(--wood-800);
    cursor: default;
}

/* The door, in the reader's own corner of the header beside their name. A
   theme is a fact about the person, not a section of the application, and
   nav.py's utility row is capped at five for a reason it writes down. */
.header-theme {
    color: var(--brass-500);
    text-decoration: none;
    font-size: var(--text-md);
    line-height: 1;
    padding: 0 0.15rem;
}
.header-theme:hover { color: var(--brass-300); }

/* The picker: a summary that looks like the glyph it replaced, and a small
   panel of ten dots under it. Each dot is half room, half instrument. */
.theme-pick { position: relative; display: inline-block; }
.theme-pick > summary { list-style: none; cursor: pointer; }
.theme-pick > summary::-webkit-details-marker { display: none; }
.theme-pick[open] > summary { color: var(--brass-300); }
.theme-pick-pop {
    /* Fixed to the viewport, not absolute to the summary: the header's right
       group scrolls sideways (`overflow: auto`) so a header can never wrap,
       and an absolute box inside it is clipped to one row and pushes the
       group's scroll width. The header is itself fixed, so "just under it,
       at the right" is a viewport fact. */
    position: fixed; right: 1rem; top: 3.5rem; z-index: 120;
    display: flex; flex-direction: column; gap: 0.45rem;
    padding: 0.6rem 0.7rem;
    background: var(--soot-800);
    border: 1px solid var(--line); border-radius: var(--radius);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
.theme-dots { display: grid; grid-template-columns: repeat(5, 1.3rem); gap: 0.45rem; }
.theme-dot {
    width: 1.3rem; height: 1.3rem; padding: 0;
    border-radius: 50%;
    border: 2px solid var(--line);
    cursor: pointer;
}
.theme-dot:hover, .theme-dot:focus-visible { border-color: var(--brass-300); }
.theme-dot.is-on { border-color: var(--parchment); box-shadow: 0 0 0 1px var(--soot-800), 0 0 0 2px var(--brass-400); }
.theme-pick-note { font-size: var(--text-2xs); color: var(--brass-500); letter-spacing: 0.02em; max-width: 14rem; line-height: 1.5; }
.theme-pick-note a { color: var(--brass-400); }

/* The lighting control, which is a different question from which theme and
   sits above the ten for that reason. Three buttons rather than a checkbox
   because AUTO is not the absence of a choice - it is the choice to let the
   machine answer, and a two-state control cannot say that. */
.theme-modes { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 1.2rem; }
.theme-modes-label {
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brass-600);
    margin-right: 0.2rem;
}
.theme-mode {
    font-family: var(--font-body);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    padding: 0.2rem 0.7rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    background: var(--soot-500);
    color: var(--parchment);
    cursor: pointer;
}
.theme-mode:hover { border-color: var(--brass-400); color: var(--brass-300); }
.theme-mode.is-on {
    border-color: var(--brass-400);
    background: var(--brass-400);
    color: var(--soot-900);
    cursor: default;
}
.theme-modes-note { font-size: var(--text-xs); color: var(--brass-600); margin-left: 0.3rem; }

/* Full screen, beside the theme door in the reader's own corner of the
   header. A button rather than a link, so it carries aria-pressed: the state
   is a fact about the window and the control should say which way it is. */
.header-fullscreen {
    background: none;
    border: none;
    padding: 0 0.15rem;
    cursor: pointer;
    font-size: var(--text-md);
    line-height: 1;
    font-family: inherit;
}
.header-fullscreen[aria-pressed="true"] { color: var(--brass-300); }

/* ---- What a person adds to the log ----------------------------------------
   The log had one author and now has three, so it needs three registers a
   reader can tell apart at a glance while a fight is running.

   Rollies' own record is `.turn-line` as it always was. A person's line is
   marked by who said it and by a left edge in their own color - the DM's in
   brass, because that is what the instrument family is for and the DM is the
   one running the world; a player's in the party's blue, which is the color
   that already means "one of us" everywhere else on this page.

   Deliberately NOT a chat bubble. This is a log with some lines in it that a
   person wrote, not a chat window that happens to hold events - the moment it
   looks like a chat, the mechanical record starts reading as somebody's
   opinion. */
.turn-line.is-said {
    padding-left: 0.45rem;
    border-left: 2px solid var(--brass-600);
    color: var(--parchment);
}
.turn-line.is-said.is-player { border-left-color: var(--faction-party); }
.said-by {
    color: var(--brass-500);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    margin-right: 0.35rem;
}
.turn-line.is-said.is-player .said-by { color: var(--faction-party); }

.say-form {
    display: flex;
    gap: 0.3rem;
    padding: 0.4rem 0.5rem 0.5rem;
    border-top: 1px solid var(--wood-800);
}
.say-input {
    flex: 1 1 auto;
    min-width: 0;
    background: var(--soot-900);
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    color: var(--parchment);
    font-family: var(--font-body);
    font-size: var(--text-xs);
    padding: 0.25rem 0.4rem;
}
.say-input:focus-visible { outline: 1px solid var(--brass-300); }
.say-btn {
    flex: 0 0 auto;
    background: var(--soot-500);
    border: 1px solid var(--line);
    border-radius: var(--radius-small);
    color: var(--parchment);
    font-family: var(--font-body);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    padding: 0.25rem 0.6rem;
    cursor: pointer;
}
.say-btn:hover { border-color: var(--brass-400); color: var(--brass-300); }

/* Both whole-party presses together at the foot of the action card - the
   rest, and the level at the end of the evening. */
.bar-party { display: flex; gap: 0.4rem; align-items: center; }
/* AWARD XP: an amount, who, the button - one row that wraps rather than
   pushing the action card wider. */
.bar-xp { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; }
.bar-xp input[type="number"] { width: 5.5rem; }
.bar-xp select { max-width: 10rem; }

/* ============================================
   ADMIN — the meta layer
   ============================================
   What this installation is, rather than what happened in it. Quiet by
   design: it is read between sessions, not during one, so nothing here
   competes with the war room for attention. */
.admin-section { margin-bottom: 2rem; }

.admin-heading {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--brass-300);
    border-bottom: 1px solid var(--soot-500);
    padding-bottom: 0.3rem;
    margin-bottom: 0.8rem;
}

.admin-subheading {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--brass-500);
    margin: 0.8rem 0 0.4rem;
}

.admin-count { color: var(--brass-300); margin-left: 0.4rem; }

/* Definition lists rather than a table: these are labeled single facts, and
   a two-column table of one-word answers reads as a spreadsheet of nothing. */
.admin-facts {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem 1.6rem;
    margin: 0;
}

.admin-facts > div { min-width: 8rem; }
.admin-facts dt {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--brass-500);
}
.admin-facts dd { margin: 0.1rem 0 0; color: var(--parchment); font-size: var(--text-md); }

.admin-path { font-size: var(--text-xs); word-break: break-all; color: var(--brass-500); }

.admin-ok { color: var(--faction-party); }
.admin-warn { color: var(--faction-enemy); }
.admin-muted { color: var(--brass-500); }

.admin-alert {
    border-left: 3px solid var(--faction-enemy);
    padding: 0.5rem 0.8rem;
    margin-bottom: 0.8rem;
    background-color: var(--soot-600);
    font-size: var(--text-sm);
}

.admin-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.admin-table th {
    text-align: left;
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--brass-500);
    padding-bottom: 0.3rem;
}
.admin-table td { padding: 0.3rem 0.6rem 0.3rem 0; border-top: 1px solid var(--soot-500); }

.admin-group { margin-bottom: 0.8rem; }

/* Two columns where there is room, one on a phone. A feature list is scanned
   for the gaps rather than read, so density helps. */
.admin-feature-list {
    list-style: none;
    padding: 0;
    margin: 0;
    columns: 2;
    column-gap: 1.5rem;
    font-size: var(--text-sm);
}

.admin-feature-list li { break-inside: avoid; padding: 0.1rem 0; }
.admin-feature-list .is-used { color: var(--parchment); }
.admin-feature-list .is-unused { color: var(--brass-500); }
.admin-mark { display: inline-block; width: 1.1rem; }
.admin-feature-list .is-used .admin-mark { color: var(--faction-party); }

@media (max-width: 700px) {
    .admin-feature-list { columns: 1; }
}

/* The bundle, printed in full above the button that hands it over. Monospace
   and scrollable rather than summarized: a summary of what you are about to
   share is not the thing you are about to share. */
.admin-bundle {
    max-height: 18rem;
    overflow: auto;
    background-color: var(--soot-600);
    border: 1px solid var(--soot-500);
    border-radius: var(--radius);
    padding: 0.6rem 0.8rem;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--brass-300);
    margin-bottom: 0.7rem;
}

/* ============================================
   SEED — filling in a sheet somebody else wrote
   ============================================ */
.seed-form { max-width: 52rem; }
.seed-section { margin-bottom: 1.6rem; }

.seed-heading {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--brass-300);
    border-bottom: 1px solid var(--soot-500);
    padding-bottom: 0.3rem;
    margin-bottom: 0.6rem;
}

/* Eighteen checkboxes read as a grid rather than a column: a DM is copying
   ticks off a sheet, so the shape on screen should match the shape on paper. */
.seed-skills {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: 0.2rem 0.8rem;
}

.seed-skill {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: var(--text-sm);
    cursor: pointer;
    padding: 0.15rem 0;
}

/* One heading per class, because a fighter 3 / rogue 2 has two lists chosen
   against two levels and a single run of ticks would hide which was which. */
.seed-group {
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--brass-500);
    margin: 0.7rem 0 0.3rem;
}

/* The level that granted it, beside the name: a DM reading a sheet checks the
   list against a level, and a bare run of names does not say where to stop. */
.seed-level {
    font-size: var(--text-xs);
    color: var(--brass-600);
}

/* The second box on a skill row is expertise. Right after the name and
   drawn small with its x2 beside it - NOT pushed to the row's end, which was
   tried first and put the box against the gap before the next column, so
   "Acrobatics [ ] [ ] Animal Handling" read as two boxes for the second
   name. A box belongs to the word it touches. */
.seed-skill .seed-expertise, .check-line .seed-expertise { margin-left: 0.3rem; transform: scale(0.8); }
.seed-expertise-mark { font-size: var(--text-xs); color: var(--brass-600); letter-spacing: 0.06em; }

.seed-box {
    width: 100%;
    font-family: var(--font-body); font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--text-sm);
    line-height: 1.6;
}

/* ---- The librarian, in the rail ------------------------------------------ */
/* A link wearing the control row's button, so LIBRARIAN sits beside PREP
   and SCENES as one more door and not a different kind of thing. */
.librarian-form { margin: 0 0 0.9rem; }
.librarian-answer p { margin: 0 0 0.6rem; line-height: 1.55; color: var(--parchment); }
.librarian-cite { font-size: var(--text-2xs); color: var(--brass-400); margin-left: 0.1rem; }
.librarian-sources { margin: 0.2rem 0 0.4rem; padding-left: 1.2rem; font-size: var(--text-xs); color: var(--brass-500); display: grid; gap: 0.2rem; }
.librarian-asked { font-size: var(--text-sm); }
.panel-title-link { float: right; font-size: var(--text-2xs); letter-spacing: 0.12em; }

/* ---- A fight, watched again ---------------------------------------------- */
.replay { max-width: 72rem; margin: 0 auto; display: flex; flex-direction: column; gap: 0.8rem; }
.replay-controls { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.replay-step { text-decoration: none; padding: 0.25rem 0.55rem; }
.replay-step.is-off { opacity: 0.35; pointer-events: none; }
.replay-slider { flex: 1 1 12rem; min-width: 8rem; accent-color: var(--brass-400); }
.replay-where { font-size: var(--text-sm); color: var(--brass-400); letter-spacing: 0.06em; }
.replay-stage { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(16rem, 1fr); gap: 0.8rem; align-items: start; }
@media (max-width: 60rem) { .replay-stage { grid-template-columns: minmax(0, 1fr); } }
.replay-board { position: relative; width: 100%; background: var(--soot-900); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.replay-map { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; display: block; }
.replay-grid { position: absolute; inset: 0; pointer-events: none;
    background-image: linear-gradient(to right, rgba(255, 255, 255, 0.12) 1px, transparent 1px), linear-gradient(to bottom, rgba(255, 255, 255, 0.12) 1px, transparent 1px);
    background-size: calc(100% / var(--map-cols)) calc(100% / var(--map-rows)); }
.replay-token.is-acting { box-shadow: 0 0 0 3px var(--brass-300); }
.replay-token.is-target { box-shadow: 0 0 0 3px var(--faction-enemy); }
.replay-token.is-down { opacity: 0.45; }
.replay-token.is-dead { opacity: 0.25; filter: grayscale(1); }
.replay-empty { position: absolute; inset: 0; margin: 0; display: grid; place-items: center; font-size: var(--text-sm); color: var(--brass-600); }
.replay-side { display: flex; flex-direction: column; gap: 0.8rem; }
.replay-card { padding: 0.6rem 0.9rem 0.8rem; }
.replay-lines { display: grid; gap: 0.3rem; }
.replay-line { margin: 0; font-size: var(--text-sm); line-height: 1.45; }
.replay-line.is-past, .replay-line.is-next { color: var(--brass-700); }
.replay-line.is-now { color: var(--parchment-bright); border-left: 2px solid var(--brass-400); padding-left: 0.5rem; }
.replay-seats { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.3rem; font-size: var(--text-sm); }
.replay-seat { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.1rem 0.6rem; align-items: center; padding: 0.2rem 0.4rem; border-left: 3px solid var(--line); border-radius: var(--radius-small); }
.replay-seat.faction-party { border-left-color: var(--faction-party); }
.replay-seat.faction-ally { border-left-color: var(--faction-ally); }
.replay-seat.faction-enemy { border-left-color: var(--faction-enemy); }
.replay-seat.is-acting { background: var(--soot-700); }
.replay-seat.is-down { opacity: 0.55; }
.replay-seat-name { color: var(--parchment); }
.replay-seat-state { color: var(--brass-400); display: inline-flex; align-items: center; gap: 0.35rem; white-space: nowrap; }
.replay-seat-conditions { grid-column: 1 / -1; font-size: var(--text-xs); color: var(--brass-600); }
.replay-hp { display: inline-block; width: 3.5rem; height: 0.4rem; background: var(--soot-900); border: 1px solid var(--line); border-radius: var(--radius-small); overflow: hidden; }
.replay-hp-fill { display: block; height: 100%; background: var(--faction-party); }
.fight-report .fight-recap { margin: 0 0 0.6rem; font-size: var(--text-sm); line-height: 1.55; color: var(--parchment); }
.fight-report .fight-links { flex-basis: 100%; margin: 0; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.fight-report .fight-verdict { margin: 0 0 0.5rem; font-size: var(--text-md); color: var(--parchment-bright); }
.fight-report .fight-note { margin-top: 0.6rem; }
.fight-report .fight-events { margin: 0; font-size: var(--text-xs); color: var(--brass-500); }
.fight-line { margin: 0 0 0.45rem; font-size: var(--text-sm); line-height: 1.5; color: var(--parchment); }
.fight-line b { color: var(--brass-300); }
.table-fight { color: var(--parchment); text-decoration: underline; text-decoration-color: currentColor; text-underline-offset: 0.15em; }


/* Joined under a name no sheet carries - said, at the top of the party. */
.plays-nobody {
    margin: 0 0 0.6rem;
    padding: 0.5rem 0.7rem;
    border: 1px solid var(--brass-500);
    border-radius: var(--radius-small);
    font-size: var(--text-sm);
    color: var(--brass-300);
}

/* Who plays this character, on the DM's card: the name, and one press to change it. */
.played-by > summary { list-style: none; cursor: pointer; }
.played-by > summary::-webkit-details-marker { display: none; }
.played-by-form { display: flex; gap: 0.3rem; margin-top: 0.25rem; }
.played-by-form input { width: 8rem; }

/* The table's names under the name box, with room before the next label. */
.join-names { margin-bottom: 0.9rem; }

/* ---- the world map (atlas.html) ---------------------------------------------
   The picture has the whole width of the page, and everything else sits on it:
   the bar top left, zoom bottom right, a pin's card beside the pin. The stage
   is the picture's own size, moved and scaled by one transform; the pins ride
   inside it and undo the scale with --inverse, anchored at their point
   (transform-origin 0 0, then pulled back by half their own size) so a pin
   stays on its town and the size of a fingertip at every zoom. */
.atlas { height: var(--screen-height); }
.atlas-view { position: relative; height: 100%; overflow: hidden; border-radius: var(--radius);
              background: var(--soot-800); touch-action: none; cursor: grab; user-select: none; outline: none; }
.atlas-view:focus-visible { outline: 2px solid var(--brass-300); outline-offset: -2px; }
.atlas-view:active { cursor: grabbing; }
.atlas-view.is-placing, .atlas-view.is-placing .atlas-pin { cursor: crosshair; }
.atlas-view:not(.is-ready) .atlas-stage { visibility: hidden; }
.atlas-stage { position: absolute; left: 0; top: 0; transform-origin: 0 0; --inverse: 1; }
.atlas-picture { display: block; width: 100%; height: 100%; pointer-events: none; -webkit-user-drag: none; }

/* Pins: one color for pages, a different shape for notes. Color is kept for
   the one mark that means something else - the party. */
.atlas-pin { position: absolute; width: 1.15rem; height: 1.15rem; padding: 0; border: 0; background: none; min-width: 0;
             transform-origin: 0 0; transform: scale(var(--inverse)) translate(-50%, -50%); cursor: pointer; }
.atlas-pin-head { display: block; width: 100%; height: 100%; border-radius: 50%; background: var(--brass-400);
                  border: 2px solid var(--soot-900); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); }
.atlas-pin.has-below .atlas-pin-head { box-shadow: 0 0 0 3px var(--brass-300), 0 1px 4px rgba(0, 0, 0, 0.6); }
.atlas-pin-note { width: 0.95rem; height: 0.95rem; }
.atlas-pin-note .atlas-pin-head { border-radius: 2px 50% 50% 50%; background: var(--parchment-pale); }
.atlas-pin-label { position: absolute; left: 1.45rem; top: 50%; transform: translateY(-50%); white-space: nowrap;
                   font-size: var(--text-sm); color: var(--parchment-pale); pointer-events: none;
                   text-shadow: 0 0 3px var(--soot-900), 0 0 3px var(--soot-900), 0 1px 2px var(--soot-900); }
.atlas-pin.is-crowded .atlas-pin-label { visibility: hidden; }
.atlas-pin.is-crowded:hover .atlas-pin-label, .atlas-pin.is-crowded:focus-visible .atlas-pin-label,
.atlas-pin.is-crowded.is-open .atlas-pin-label, .atlas-pin.is-crowded.is-found .atlas-pin-label { visibility: visible; z-index: 2; }
.atlas-pin:hover, .atlas-pin:focus-visible, .atlas-pin.is-open { z-index: 2; }
.atlas-pin:hover .atlas-pin-head, .atlas-pin:focus-visible .atlas-pin-head, .atlas-pin.is-open .atlas-pin-head { outline: 2px solid var(--brass-300); outline-offset: 1px; }
.atlas-pin.is-found .atlas-pin-head { animation: atlas-found 1.2s ease-out 3; }
.atlas-pin.is-dragging { opacity: 0.75; }
.atlas-view.is-moving .atlas-pin[data-mine="yes"] { cursor: move; }
@keyframes atlas-found { 0% { box-shadow: 0 0 0 0 var(--brass-300); } 100% { box-shadow: 0 0 0 14px transparent; } }

.atlas-party { position: absolute; width: 1.6rem; height: 1.6rem; transform-origin: 0 0;
               transform: scale(var(--inverse)) translate(-50%, -50%); pointer-events: none; z-index: 1; }
.atlas-party-mark { display: block; width: 100%; height: 100%; border-radius: 50%; border: 3px solid var(--parchment-pale);
                    background: var(--faction-party); box-shadow: 0 0 0 2px var(--soot-900), 0 0 12px var(--faction-party); }
.atlas-party.is-inside .atlas-party-mark { background: transparent; border-color: var(--faction-party); border-style: dashed; }
.atlas-party-label { position: absolute; left: 50%; top: 100%; transform: translate(-50%, 0.2rem); white-space: nowrap;
                     font-size: var(--text-xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--parchment-pale);
                     text-shadow: 0 0 3px var(--soot-900), 0 0 3px var(--soot-900); }
@media (prefers-reduced-motion: no-preference) {
    .atlas-party:not(.is-inside) .atlas-party-mark { animation: atlas-here 2.4s ease-in-out infinite; }
}
@keyframes atlas-here { 50% { box-shadow: 0 0 0 2px var(--soot-900), 0 0 22px var(--faction-party); } }

/* The bar, top left. Which map, the way up, what a click does, and the DM's
   one switch - in that order, with room between the groups. */
.atlas-bar { position: absolute; left: 0.6rem; top: 0.6rem; right: 0.6rem; z-index: 4; display: flex; flex-wrap: wrap;
             align-items: center; gap: 0.5rem 1rem; pointer-events: none; }
.atlas-bar > * { pointer-events: auto; }
.atlas-switch { position: relative; }
.atlas-switch summary { list-style: none; display: flex; align-items: center; gap: 0.45rem; cursor: pointer;
                        padding: 0.35rem 0.75rem; background: var(--soot-800); border: 1px solid var(--line);
                        border-radius: var(--radius-small); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); }
.atlas-switch summary::-webkit-details-marker { display: none; }
.atlas-switch summary:focus-visible { outline: 2px solid var(--brass-300); outline-offset: 1px; }
.atlas-title { margin: 0; font-size: var(--text-md); color: var(--brass-200); font-weight: normal; }
.atlas-caret { font-size: var(--text-xs); color: var(--brass-400); }
.atlas-menu { position: absolute; left: 0; top: calc(100% + 0.3rem); min-width: 14rem; padding: 0.6rem 0.8rem;
              display: flex; flex-direction: column; gap: 0.6rem; background: var(--soot-800);
              border: 1px solid var(--line); border-radius: var(--radius-small); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5); }
.atlas-maps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; font-size: var(--text-md); }
.atlas-maps a { color: var(--parchment); }
.atlas-maps li[aria-current="page"] a { color: var(--brass-200); text-decoration: underline; text-underline-offset: 3px; }
.atlas-maps li.is-hidden-map a { color: var(--brass-500); font-style: normal; }
.atlas-menu-link { padding-top: 0.5rem; border-top: 1px solid var(--line); font-size: var(--text-sm); color: var(--parchment); }
.atlas-menu-row { display: flex; flex-wrap: wrap; gap: 0.4rem; padding-top: 0.5rem; border-top: 1px solid var(--line); }
.atlas-up { padding: 0.35rem 0.7rem; font-size: var(--text-sm); color: var(--parchment); text-decoration: none;
            background: var(--soot-800); border: 1px solid var(--line); border-radius: var(--radius-small); }
.atlas-tools { display: flex; gap: 0.35rem; }
.atlas-tools button, .atlas-show button { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); }
.atlas-tools button[aria-pressed="true"] { background: var(--brass-300); color: var(--soot-900); border-color: var(--brass-300); }
.atlas-show { margin: 0 0 0 auto; }

.atlas-first { position: absolute; left: 50%; bottom: 1.2rem; transform: translateX(-50%); margin: 0; padding: 0.5rem 1rem;
               background: var(--soot-800); border: 1px solid var(--line); border-radius: var(--radius-small);
               font-size: var(--text-md); pointer-events: none; }
.atlas-hint { position: absolute; left: 50%; bottom: 1.2rem; transform: translateX(-50%); margin: 0; padding: 0.35rem 0.9rem;
              font-size: var(--text-sm); background: var(--soot-800); border: 1px solid var(--brass-500); border-radius: var(--radius-small);
              pointer-events: none; z-index: 3; }

.atlas-card { position: absolute; z-index: 5; width: max-content; max-width: min(18rem, 80%); padding: 0.6rem 0.8rem;
              background: var(--soot-800); border: 1px solid var(--wood-600); border-radius: var(--radius-small);
              box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5); cursor: auto; user-select: text; }
.atlas-card-title { margin: 0 0 0.5rem; font-size: var(--text-md); color: var(--brass-200); }
.atlas-card-row { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.atlas-card-go { display: inline-block; padding: 0.3rem 0.7rem; font-size: var(--text-xs); letter-spacing: 0.06em; text-decoration: none;
                 border: 1px solid var(--wood-600); border-radius: var(--radius-small); color: var(--parchment); }
.atlas-card-note { margin: 0 0 0.3rem; line-height: 1.4; }
.atlas-card-by { margin: 0; font-size: var(--text-xs); color: var(--brass-400); }
.atlas-card-remove { margin-top: 0.5rem; }
.atlas-card-remove button { font-size: var(--text-xs); padding: 0.2rem 0.5rem; }

.atlas-zoom { position: absolute; right: 0.6rem; bottom: 0.6rem; z-index: 4; display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem; }
.atlas-zoom button { min-width: 2.2rem; height: 2.2rem; padding: 0 0.5rem; font-size: var(--text-lg); line-height: 1; }
.atlas-zoom .atlas-zoom-word { font-size: var(--text-xs); letter-spacing: 0.06em; }

.atlas-form { display: flex; flex-direction: column; gap: 0.6rem; }
.atlas-field { display: flex; flex-direction: column; gap: 0.25rem; font-size: var(--text-sm); color: var(--brass-300); }
.atlas-check { display: flex; align-items: center; gap: 0.4rem; font-size: var(--text-sm); }
.atlas-reach { display: flex; gap: 1rem; font-size: var(--text-sm); }
.atlas-dm-row { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.4rem; }
.atlas-party-off { margin-top: 0.8rem; padding-top: 0.8rem; border-top: 1px solid var(--line); }
.atlas-modal { max-width: min(30rem, 92vw); }
.atlas-empty { max-width: 40rem; }

@media (max-width: 600px) {
    .atlas { height: 78vh; }
    .atlas-show { margin-left: 0; }
}
/* The map's controls are in the right-hand corners, so its free corner is the
   bottom left at every width - the bubble stays there rather than moving onto
   the zoom buttons below 1100px. */
.atlas-body .feedback-bubble { left: 1rem; right: auto; }
/* The scene's words run down the left under the picture, and the bubble sat
   on them; the bottom-right corner of a scene is empty. */
.scene-body .feedback-bubble { left: auto; right: 1rem; }
.atlas-bar button, .atlas-up, .atlas-switch summary { white-space: nowrap; }
.atlas-tools { flex-wrap: wrap; }

/* ---- Table: House rules, Accounts, Admin behind one link ------------------- */
.table-head { flex-wrap: wrap; row-gap: 0.4rem; }
.table-tabs { display: flex; gap: 1.2rem; flex-basis: 100%; order: 2; font-size: var(--text-md); }
.table-tabs a { color: var(--brass-300); text-decoration: none; padding-bottom: 0.2rem; border-bottom: 2px solid transparent; }
.table-tabs a:hover { color: var(--parchment); }
.table-tabs a[aria-current="page"] { color: var(--parchment); border-bottom-color: var(--brass-300); }
.table-head .chronicle-sub { order: 3; flex-basis: 100%; }
/* While the sections are still on the bar, MENU holds only what left it. */
.header-tight:not(.header-compact) .header-menu-list a:not(.is-quiet),
.header-tight:not(.header-compact) .header-menu-rule { display: none; }


/* Watching somebody else's turn: a row with no tiles and nothing spent is a
   pip and an empty line, 130px of a phone's first screen with nothing to read
   and nothing to press. It comes back the moment something is spent. */
.action-bar.is-locked .bar-row:not(:has(.bar-spent-label)) { display: none; }

.replay-count { font-size: var(--text-xs); color: var(--brass-500); white-space: nowrap; }
.replay-now { margin: 0.5rem 0 0.8rem; font-size: var(--text-md); color: var(--parchment-bright); }
.replay-now .replay-where { font-size: var(--text-xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-400); margin-right: 0.4rem; }
.replay .fight-links { flex-basis: 100%; margin: 0; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.scene-noimage p { margin: 0 0 1rem; }
.scene-noimage form { display: inline; }

.lore-foot { margin: 0.4rem 0 0; text-align: right; }
.lore-delete:hover { border-color: var(--danger); color: var(--danger); }

/* A note under a heading on the pages that carry one - quiet, spaced from what
   follows, and its links the app's links rather than the browser's blue. */
.page-note { margin: 0.2rem 0 0.8rem; font-size: var(--text-sm); line-height: 1.5; color: var(--brass-600); }
.page-note a, .admin-page td a { color: var(--brass-300); }
.page-note a:hover, .admin-page td a:hover { color: var(--parchment-bright); }

/* Still in full before the button - nobody should download a thing they have
   not been shown - but in a box of its own height, so it no longer pushes the
   rest of the page below the fold. */
.admin-page .admin-bundle { max-height: 14rem; overflow: auto; }
.admin-page .admin-section { margin-top: 1.4rem; }

.accounts-table .accounts-inline-form { flex-wrap: nowrap; }
.accounts-table .accounts-inline-form .accounts-input { min-width: 0; width: 10rem; }
.accounts-unmatched { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-top: 0; }

/* A session with no recap: no box, no heading - just WRITE IT. */
.recap.is-empty { border: 0; background: none; padding: 0; margin: 0.2rem 0 0.4rem; }
.recap.is-empty .recap-head { display: none; }
/* On a phone the fight tables keep what a person reads first - the fight, the
   damage, who died - and drop the columns that were being cut off. */
@media (max-width: 600px) {
    .chronicle-table .col-minor { display: none; }
    .chronicle-table .table-fight + .chronicle-export { display: none; }
}

.an-min { position: relative; min-width: 4rem; }
.an-minbar { position: absolute; left: 0; bottom: 0.15rem; height: 3px; background: var(--brass-500); border-radius: 2px; }

.prepare-tally .tally-save { margin-left: auto; }

.theme-card.is-showing { border-color: var(--brass-300); box-shadow: 0 0 0 1px var(--brass-300); }
.theme-tag-showing { color: var(--brass-200); border-color: var(--brass-300); }
/* The first page of the wizard: room between one question and the next, and
   START a step away from the last box rather than glued to it. */
.creation-begin { display: flex; flex-direction: column; gap: 0.9rem; max-width: 26rem; }
.creation-begin > .btn-primary { align-self: flex-start; margin-top: 0.3rem; }
/* A placeholder is an example, not an answer: dim enough that an empty box
   never reads as already filled in. */
.login-input::placeholder { color: var(--brass-700); opacity: 1; }
.login-button + .login-note, form + .login-note { margin-top: 0.9rem; }

.login-notice {
    margin: 1rem auto 0;
    text-align: center;
    color: var(--brass-700);
    font-size: var(--text-2xs);
    letter-spacing: 0.04em;
}

/* Help speaks to the screen it is on: no keyboard shortcut for a finger, and
   one sentence for a phone, where three columns are one. */
.for-phones { display: none; }
@media (hover: none) { .for-keys { display: none; } }
@media (max-width: 700px) { .for-phones { display: block; } }

.house-rules-form .choice-card:has(input:checked) .choice-name::before { content: "\2713  "; color: var(--brass-300); }

.note-take-back { font-size: var(--text-2xs); letter-spacing: 0.06em; padding: 0.15rem 0.45rem; white-space: nowrap; }
.note-reach-who { color: var(--brass-600); font-size: 0.85em; }
/* Room at the foot of a long page for the feedback bubble, so the last line
   can be scrolled out from under it. */
.chronicle { padding-bottom: 3.5rem; }
.fight-links .chronicle-export { margin-left: 0; }

.accounts-unmatched .btn-quiet { text-transform: none; font-size: var(--text-sm); letter-spacing: 0.02em; }
.table-head .chronicle-sub { margin-top: 0.3rem; }

/* ============================================
   PREP — next session as cards (session_prep.py)
   ============================================ */
.sp { max-width: 72rem; }
.sp-title { margin-top: 0.4rem; }
.sp-title input { width: min(34rem, 100%); font-size: var(--text-md); }
/* The drop zone: dashed, like anything not yet the table's, and the one
   primary button on the page until there are cards. */
.sp-drop {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.6rem 1rem;
    margin: 0.8rem 0 1rem; padding: 1rem 1.1rem;
    border: 2px dashed var(--brass-700); border-radius: var(--radius);
    background: var(--soot-800);
}
.sp-drop.is-over { border-color: var(--brass-400); background: var(--soot-700); }
.sp-drop-line { margin: 0; font-size: var(--text-sm); color: var(--parchment); max-width: 40rem; }
/* The plan box (insight/filer.py): one big place to write, and what was
   read out of it - the writing with its passages marked, a numbered list of
   where each goes, and what the lore already said. */
.plan { margin: 0.8rem 0 1rem; padding: 1rem 1.1rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--soot-800); }
.plan-title { margin: 0 0 0.5rem; font-size: var(--text-md); letter-spacing: 0.08em; text-transform: uppercase; color: var(--brass-300); }
.plan-box { width: 100%; min-height: 6rem; font-size: var(--text-md); line-height: 1.5; resize: vertical; }
.plan-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.8rem; margin-top: 0.5rem; }
.plan-actions form { margin: 0; }
.plan-all { font-size: var(--text-sm); }
.plan-read { margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--line); }
.plan-text { margin: 0 0 0.7rem; white-space: pre-wrap; line-height: 1.6; color: var(--parchment); }
.plan-mark { background: color-mix(in srgb, var(--brass-400) 22%, transparent); color: inherit; border-radius: 3px; padding: 0 0.1rem; }
.plan-mark sup { font-size: var(--text-2xs); color: var(--brass-300); margin-left: 0.1rem; }
.plan-mark.is-lit { background: color-mix(in srgb, var(--brass-400) 45%, transparent); }
.plan-notices { list-style: none; margin: 0 0 0.7rem; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.plan-notice { padding: 0.5rem 0.7rem; border-left: 3px solid var(--brass-400); background: var(--soot-700); font-size: var(--text-sm); }
.plan-notice.is-contradiction { border-left-color: var(--danger); }
.plan-pages { color: var(--brass-500); }
.plan-placements { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.plan-placements label { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.5rem; font-size: var(--text-sm); cursor: pointer; }
.plan-n { display: inline-block; min-width: 1.3rem; text-align: center; font-size: var(--text-xs); color: var(--soot-900); background: var(--brass-400); border-radius: 999px; }
.plan-new { color: var(--brass-200); }
.plan-within, .plan-quote, .plan-hint, .plan-none, .plan-kept { color: var(--brass-500); font-size: var(--text-xs); }
.plan-quote { flex-basis: 100%; padding-left: 2rem; }
.sp-cards { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.9rem; }
/* A card: the picture at the size it is judged by, the words beside it, what
   is held back underneath across the whole width. */
.sp-card {
    display: grid; grid-template-columns: minmax(12rem, 22rem) minmax(0, 1fr);
    grid-template-areas: "picture fields" "table table" "held held";
    gap: 0.7rem 1rem; padding: 0.8rem;
    border: 1px solid var(--line); border-radius: var(--radius); background: var(--soot-800);
}
.sp-card.is-showing { border-color: var(--brass-400); }
.sp-card.is-dragging { opacity: 0.5; }
.sp-picture { grid-area: picture; display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
.sp-picture img { width: 100%; max-height: 16rem; object-fit: contain; border-radius: var(--radius-small); background: var(--soot-900); }
.sp-nopicture { padding: 1.4rem 0.8rem; font-size: var(--text-xs); color: var(--brass-600); text-align: center; border: 1px dashed var(--line); border-radius: var(--radius-small); }
.sp-picture-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
.sp-picture-tools button { font-size: var(--text-xs); padding: 0.2rem 0.5rem; }
.sp-picture-tools button.is-on { border-color: var(--brass-400); color: var(--brass-300); }
.sp-map-tag { font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-400); padding: 0.15rem 0.45rem; border: 1px solid var(--brass-700); border-radius: var(--radius-small); }
.sp-picture.is-over { outline: 2px dashed var(--brass-400); outline-offset: 3px; }
.sp-repick-btn { cursor: pointer; font-size: var(--text-xs); letter-spacing: 0.06em; padding: 0.2rem 0.5rem; border: 1px solid var(--line); border-radius: var(--radius-small); color: var(--brass-400); }
.sp-repick-btn:hover { border-color: var(--brass-500); }
.sp-status { font-size: var(--text-sm); color: var(--faction-enemy); }
body.sp-busy { cursor: progress; }
.sp-handle { cursor: grab; padding: 0 0.3rem; font-size: var(--text-md); color: var(--brass-500); user-select: none; }
.sp-fields { grid-area: fields; display: flex; flex-direction: column; gap: 0.45rem; min-width: 0; }
.sp-fields textarea { width: 100%; }
.sp-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.8rem; }
.sp-pickers > .field { flex: 1 1 12rem; min-width: 0; }
.sp-pickers select { width: 100%; }
.sp-name { flex: 1 1 14rem; min-width: 0; font-family: var(--font-display); font-size: var(--text-md); }
.sp-saved { font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--brass-600); min-width: 4rem; }
.sp-hint { margin: 0; font-size: var(--text-xs); color: var(--brass-600); }
/* What the DM typed under this card at the table (the note box). */
.sp-table { grid-area: table; padding-top: 0.5rem; border-top: 1px solid var(--wood-800); }
.sp-table-lines { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.2rem; font-size: var(--text-sm); color: var(--parchment); }
.sp-table-clock { font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-600); margin-right: 0.3rem; }
.plan-lines { white-space: nowrap; }
.sp-held { grid-area: held; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.4rem 1rem; padding-top: 0.5rem; border-top: 1px solid var(--wood-800); }
.sp-held-title { flex: 1 0 100%; margin: 0; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
.sp-held-none { text-transform: none; letter-spacing: 0; }
.sp-reveals { flex: 1 0 100%; list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.sp-reveal { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.6rem; font-size: var(--text-sm); }
.sp-reveal-kind { font-size: var(--text-2xs); letter-spacing: 0.12em; color: var(--brass-500); }
.sp-reveal-thumb { width: 2.4rem; height: 2.4rem; object-fit: cover; border-radius: var(--radius-small); }
.sp-reveal-what { flex: 1 1 12rem; min-width: 0; color: var(--parchment); }
.sp-reveal-to { font-size: var(--text-xs); color: var(--brass-600); }
.sp-x { padding: 0.05rem 0.45rem; }
.sp-hold summary, .sp-remove summary { cursor: pointer; font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--brass-400); }
.sp-remove summary { color: var(--brass-600); }
.sp-hold[open] { flex: 1 0 100%; }
.sp-hold-form { display: flex; flex-direction: column; gap: 0.45rem; margin-top: 0.45rem; max-width: 36rem; }
.sp-hold-form textarea { width: 100%; }
.sp-hold-form .btn-primary { align-self: flex-start; }
.sp-to { display: flex; align-items: center; gap: 0.4rem; font-size: var(--text-sm); }
.sp-empty { font-size: var(--text-sm); color: var(--brass-600); }
.sp-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.6rem; margin-top: 1rem; }
.sp-blank { display: flex; gap: 0.4rem; flex: 1 1 20rem; }
.sp-blank input { flex: 1; min-width: 0; }
@media (max-width: 700px) {
    .sp-card { grid-template-columns: minmax(0, 1fr); grid-template-areas: "picture" "fields" "table" "held"; }
}
.session-prep-link { margin: 0.5rem 0.6rem 0 0; }

/* On the scene view: what is held back, for the DM; what was handed out, for
   the player it was handed to. */
.scene-held { margin-top: 0.6rem; padding: 0.5rem 0.8rem; border: 1px dashed var(--brass-700); border-radius: var(--radius); max-width: 64rem; }
.scene-held-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.scene-held-list li { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.4rem; font-size: var(--text-sm); }
.scene-held-what { flex: 1 1 16rem; min-width: 0; }
.scene-held-to { color: var(--brass-600); font-size: var(--text-xs); }
.scene-handouts { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 0.6rem; }
.scene-handout { flex: 1 1 16rem; max-width: 32rem; padding: 0.6rem 0.8rem; border-left: 3px solid var(--brass-600); background: var(--soot-800); border-radius: var(--radius-small); }
.scene-handout img { width: 100%; border-radius: var(--radius-small); }
.scene-handout p { margin: 0.3rem 0 0; white-space: pre-wrap; }
.scene-handout-to { font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--brass-500); }
.handout-picture { width: 100%; border-radius: var(--radius-small); }
.handout-text { white-space: pre-wrap; }

/* ---- Coin: the inventory card's second tab (purse.py) ---- */
.inv-tabs { display: flex; gap: 0.3rem; margin: 0 0 0.35rem; flex: 0 0 auto; }
.inv-tab { font-size: var(--text-2xs); letter-spacing: 0.06em; padding: 0.2rem 0.6rem; background: transparent; color: var(--brass-500); border: 1px solid var(--line); }
.inv-tab.is-on { background: var(--brass-400); border-color: var(--brass-400); color: var(--soot-900); }
.inv-tab-worth { font-size: 0.9em; opacity: 0.75; letter-spacing: 0; margin-left: 0.2rem; }
.inv-coin { display: flex; flex-direction: column; gap: 0.5rem; }
/* As specific as the card's own `.side-card .tab-body[data-panel]` flex rule,
   and after it, so a hidden tab is hidden rather than laid out beside the
   open one. */
.side-card [data-inv-body][hidden] { display: none; }
.purse { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 0.3rem; }
.purse-coin { display: flex; flex-direction: column; align-items: center; padding: 0.3rem 0.1rem; border: 1px solid var(--line); border-radius: var(--radius-small); background: var(--soot-700); }
.purse-n { font-size: var(--text-md); color: var(--parchment-bright); font-variant-numeric: tabular-nums; }
.purse-d { font-size: var(--text-2xs); letter-spacing: 0.12em; color: var(--brass-500); text-transform: uppercase; }
.purse-gp .purse-n, .purse-pp .purse-n { color: var(--brass-300); }
.purse-small .purse-n { font-size: var(--text-sm); }
.purse-form { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; }
.purse-form input[type="number"] { width: 4.5rem; }
.purse-form button { font-size: var(--text-xs); padding: 0.2rem 0.55rem; }
.purse-exact summary { cursor: pointer; font-size: var(--text-2xs); letter-spacing: 0.06em; color: var(--brass-500); }
.purse-set { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)) auto; gap: 0.3rem; align-items: end; margin-top: 0.3rem; }
.purse-set label { display: flex; flex-direction: column; font-size: var(--text-2xs); color: var(--brass-500); text-transform: uppercase; }
.purse-set input { width: 100%; min-width: 0; }
.fund { display: flex; flex-direction: column; gap: 0.35rem; padding-top: 0.5rem; border-top: 1px solid var(--wood-800); }
.fund-head { display: flex; justify-content: space-between; align-items: baseline; }
.fund-worth { font-size: var(--text-xs); color: var(--brass-300); }

/* ---- a session's transcript (transcripts.py) ----
   The time in a narrow column, the voice in the accent, the words as prose.
   A long night is hundreds of lines, so they sit close and read like a
   script rather than a stack of cards. */
.transcript-card { margin: 0.5rem 0 0; display: flex; gap: 0.6rem; align-items: baseline; flex-wrap: wrap; }
.transcript-card .chronicle-export { margin-left: 0; }
.transcript-lines { padding: 0.6rem 1rem; }
.transcript-line { display: grid; grid-template-columns: 4.2rem 1fr; gap: 0 0.7rem; margin: 0; padding: 0.3rem 0; font-size: var(--text-md); line-height: 1.5; scroll-margin-top: 5rem; }
.transcript-line:target { background: var(--wood-800); }
.transcript-at { grid-column: 1; color: var(--brass-600); font-variant-numeric: tabular-nums; font-size: var(--text-sm); text-decoration: none; padding-top: 0.1rem; }
.transcript-said { grid-column: 2; overflow-wrap: anywhere; }
.transcript-who { color: var(--brass-400); font-weight: 600; margin-right: 0.3rem; }
.transcript-voice-form { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 0.6rem 1rem; }
.transcript-voice-form .recap-form-row { grid-column: 1 / -1; }
.transcript-foot { margin: 1rem 0 2rem; }
@media (max-width: 520px) {
    .transcript-line { grid-template-columns: 1fr; }
    .transcript-at, .transcript-said { grid-column: 1; }
}
/* The transcript's drafts (summarize.py): the recap in its edit box, the
   sentences with the minutes they came from, and ticks for what to keep. */
.transcript-drafts .modal-section { margin-top: 1rem; }
.summary-sources { padding-left: 1.2rem; font-size: var(--text-sm); line-height: 1.55; }
.summary-sources li { margin: 0.25rem 0; }
.summary-sources li.is-flagged { color: var(--brass-300); }
.summary-cite { font-size: var(--text-xs); font-variant-numeric: tabular-nums; color: var(--brass-400); border: 1px solid var(--line); border-radius: var(--radius); padding: 0 0.3rem; margin-left: 0.3rem; text-decoration: none; white-space: nowrap; }
.summary-check { display: block; font-size: var(--text-xs); color: var(--brass-500); font-style: normal; }
.summary-pick { display: flex; gap: 0.5rem; align-items: baseline; font-size: var(--text-sm); padding: 0.2rem 0; }
.summary-pick .summary-cite { margin-left: 0; }
.summary-run { margin-top: 1rem; flex-wrap: wrap; }
.summary-kept { display: flex; gap: 0.5rem; align-items: baseline; font-size: var(--text-sm); padding: 0.2rem 0; }
.summary-kept .btn-quiet { margin-left: auto; }
.summary-moments { list-style: none; padding: 0; margin: 0.4rem 0 0; font-size: var(--text-sm); }
.summary-moments li { margin: 0.15rem 0; }
.summary-moments .summary-cite { margin: 0 0.4rem 0 0; }
.summary-gains { margin: 0.5rem 0 0; }
.summary-gain { display: flex; gap: 0.5rem; align-items: center; font-size: var(--text-sm); padding: 0.1rem 0; }
.summary-box { width: 1.1rem; height: 1.1rem; padding: 0; line-height: 1; font-size: var(--text-xs); flex: 0 0 auto; }
.is-done { color: var(--brass-600); text-decoration: line-through; }
.summary-box.is-done { text-decoration: none; }
.summary-pick.is-kept { color: var(--brass-600); }
.summary-kept-mark { font-size: var(--text-2xs); letter-spacing: 0.06em; text-transform: uppercase; color: var(--brass-600); min-width: 2.2rem; }
.transcript-drafts .recap-hint a { color: var(--brass-400); }
/* A heading that sorts: the whole label is the link, the arrow sits beside it
   and only on the column the list is sorted by. Same weight as a heading, so
   the row of them still reads as a header and not a row of buttons. */
.bestiary-sortable a { color: inherit; text-decoration: none; display: inline-flex; align-items: baseline; gap: 0.15rem; }
.bestiary-sortable a:hover, .bestiary-sortable a:focus-visible { color: var(--brass-300); text-decoration: underline; text-underline-offset: 3px; }
.bestiary-sortable.is-sorted a { color: var(--brass-200); }
.bestiary-sortable .bestiary-arrow { font-size: 0.7em; }

/* ---- the bestiary, rebuilt around its four jobs (2026-09-29) ----
   Finding at the top, the list in the middle, the fight being built in a tray
   pinned to the foot of the window - so + always lands somewhere visible and
   the difficulty is in view while the list scrolls. */
.bestiary { padding-bottom: 7rem; }
.bestiary-head { margin-bottom: 0.3rem; }
.bestiary-find { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.4rem 0 0.8rem; }
.bestiary-find input[type="search"] { width: 100%; max-width: 36rem; font-size: var(--text-md); padding: 0.5rem 0.75rem; }
.bestiary-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.9rem; font-size: var(--text-sm); }
.bestiary-filter { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--brass-500); }
.bestiary-filter > span:first-child { font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
.bestiary-filter select { font-size: var(--text-sm); padding: 0.15rem 0.3rem; text-transform: capitalize; }
.bestiary-moves { display: inline-flex; align-items: center; gap: 0.3rem; border: 0; padding: 0; margin: 0; }
.bestiary-moves legend { float: left; margin-right: 0.35rem; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-600); }
.bestiary-move, .bestiary-chip {
    display: inline-flex; align-items: center; gap: 0.25rem; cursor: pointer;
    font-size: var(--text-xs); color: var(--brass-500); text-decoration: none;
    border: 1px solid var(--line); border-radius: 999px; padding: 0.1rem 0.6rem;
}
.bestiary-move input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.bestiary-move:focus-within { outline: 2px solid var(--brass-400); outline-offset: 1px; }
.bestiary-move:hover, .bestiary-chip:hover { border-color: var(--brass-500); color: var(--brass-300); }
.bestiary-move.is-on, .bestiary-chip.is-on { border-color: var(--brass-400); color: var(--brass-200); background: var(--soot-700); }
.bestiary-move.is-on::before, .bestiary-chip.is-on::before { content: "✓"; font-size: 0.8em; }
.bestiary-live .bestiary-go { display: none; }

.bestiary-results-head { display: flex; align-items: baseline; gap: 0.8rem; margin: 0.2rem 0 0.3rem; }
.bestiary-results-head .bestiary-count { margin-left: 0; }
.bestiary-row { cursor: pointer; }
.bestiary-row td, .bestiary-row th { vertical-align: middle; }
.bestiary-num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.bestiary-name a { font-size: var(--text-sm); }
.bestiary-name small { text-transform: none; }
.bestiary-attacks { color: var(--brass-300); }
.bestiary-add { white-space: nowrap; width: 1%; }
.bestiary-add form { display: inline; }
.bestiary-plus {
    width: 1.9rem; height: 1.9rem; padding: 0; border-radius: 50%;
    font-size: var(--text-lg); line-height: 1; font-weight: 600;
}
.bestiary-plus.is-added { background: var(--brass-400); color: var(--soot-900); }
.bestiary-now { margin-left: 0.3rem; font-size: var(--text-2xs); letter-spacing: 0.06em; }

/* The tray: pinned to the foot of the window, as wide as the page. */
.bestiary-tray {
    position: sticky; bottom: 0; z-index: 5; margin-top: 1rem;
    background: var(--soot-800); border: 1px solid var(--line); border-bottom: 0;
    border-radius: var(--radius) var(--radius) 0 0; padding: 0.55rem 0.9rem;
    box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.25);
}
.tray-empty { margin: 0; font-size: var(--text-sm); color: var(--brass-500); }
.tray-summary { display: flex; align-items: baseline; gap: 0.6rem; cursor: pointer; font-size: var(--text-sm); flex-wrap: wrap; }
.tray-who { font-size: var(--text-xs); color: var(--brass-500); }
.tray-band { font-size: var(--text-xs); letter-spacing: 0.06em; text-transform: uppercase; border: 1px solid currentColor; border-radius: 999px; padding: 0.02rem 0.5rem; }
.band-easy { color: var(--verdigris-400); }
.band-medium { color: var(--brass-300); }
.band-hard { color: var(--oxblood-400); }
.band-deadly, .band-beyond-deadly { color: var(--oxblood-400); background: var(--soot-700); font-weight: 600; }
.tray-lines { list-style: none; margin: 0.5rem 0; padding: 0; max-height: 30vh; overflow-y: auto; }
.tray-line { display: flex; align-items: center; gap: 0.7rem; padding: 0.2rem 0; font-size: var(--text-sm); border-top: 1px solid var(--soot-600); }
.tray-name { flex: 1 1 auto; min-width: 0; }
.tray-name small { color: var(--brass-600); margin-left: 0.3rem; }
.tray-count { display: inline-flex; align-items: center; gap: 0.35rem; }
.tray-count form, .tray-line form { display: inline; margin: 0; }
.tray-count b { min-width: 1.4rem; text-align: center; font-variant-numeric: tabular-nums; }
.tray-step { width: 1.6rem; height: 1.6rem; padding: 0; border-radius: 50%; line-height: 1; }
.tray-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.8rem; }
.tray-actions form { margin: 0; }
.tray-save { display: inline-flex; gap: 0.4rem; flex: 1 1 18rem; }
.tray-save input { flex: 1 1 auto; min-width: 0; max-width: 18rem; }
.tray-clear { margin-left: auto !important; }
@media (max-width: 700px) {
    .bestiary-col-speed, .bestiary-speed, .bestiary-col-attacks, .bestiary-attacks { display: none; }
}
.bestiary-scroll { overflow-x: auto; position: relative; }
/* The feedback bubble is fixed in a bottom corner, and the tray is pinned to
   the bottom of the window: the bubble rides above the tray instead of on its
   buttons, by the tray's own height, which the page's script keeps in
   --tray-lift. Width rules could not do it - where the bubble lands depends
   on the rail, not only on the window. */
body:has(.bestiary-tray) .feedback-bubble:not(.is-hosted) { bottom: calc(var(--tray-lift, 0px) + 1rem); }
/* How your table fights (calibration.py). Trivial is quieter than easy. */
.band-trivial { color: var(--brass-600); }
.bestiary-chip-table { border-style: dashed; }
.bestiary-why { font-size: var(--text-xs); color: var(--brass-600); }
.fights-table { max-width: 60rem; }
.fights-table-list tr.is-skipped td { opacity: 0.5; }
.fights-table-list select { font-size: var(--text-xs); }
.fight-mark { margin-top: 0.4rem; }


/* How They Fight in two groups (Gestalt: enclosure). Damage stands alone;
   what they used is boxed together, a quieter title over its four kinds. */
.an-group { display: flex; flex-direction: column; gap: 0.25rem; }
.an-group-used {
    padding: 0.45rem 0.55rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
}
.an-group-title {
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brass-600);
}


/* No italics anywhere: secondary words are smaller and quieter, not slanted. */
em, i, cite, dfn, var, address { font-style: normal; }

/* The lighting pop-over under the header's glyph: three words in a row. */
.lighting-pop { display: flex; gap: 0.4rem; }
.chip-back-conditions { line-height: 1; }
.chip-back .initiative-remove-btn { position: absolute; top: 0.15rem; right: 0.25rem; }


/* ---- The open card, trimmed (2026-10-01) --------------------------------
   The face is the card: full width, the name and the two numbers a table
   asks for laid over its foot. Damage and healing share one box. The rest
   is behind MORE. */
.detail-card .card-header { position: relative; display: block; }
.detail-card .card-portrait { width: 100%; height: auto; aspect-ratio: 1 / 1; border-radius: var(--radius); }
.detail-card .card-portrait img, .detail-card .card-portrait .portrait-placeholder { width: 100%; height: 100%; object-fit: cover; }
.detail-card .card-header-text {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    padding: 2.2rem 0.9rem 0.7rem;
    border-radius: 0 0 var(--radius) var(--radius);
    background: linear-gradient(to top, color-mix(in srgb, var(--soot-900) 92%, transparent), transparent);
    display: grid;
    grid-template-columns: 1fr auto auto;
    align-items: end;
    column-gap: 0.7rem;
    pointer-events: none;
}
.detail-card .card-header-text > * { pointer-events: auto; }
.detail-card .card-header-text h2 { grid-column: 1; margin: 0; }
.detail-card .card-header-text .played-by,
.detail-card .card-header-text .player-name { grid-column: 1; grid-row: 2; }
.detail-card .card-header-text .head-hp { grid-column: 2; grid-row: 1 / span 2; }
.detail-card .card-header-text .head-ac { grid-column: 3; grid-row: 1 / span 2; }
.hp-form { display: flex; gap: 0.4rem; grid-column: 1 / -1; }
.hp-form input { flex: 1 1 auto; min-width: 0; }
.hp-form .hp-dmg { color: color-mix(in srgb, var(--faction-enemy) 55%, var(--parchment)); }
.hp-form .hp-heal { color: color-mix(in srgb, var(--faction-ally) 55%, var(--parchment)); }
.card-more { margin-top: 0.4rem; }
.card-more-toggle { cursor: pointer; font-size: var(--text-xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-500); list-style: none; text-align: center; padding: 0.3rem; }
.card-more-toggle::-webkit-details-marker { display: none; }
.card-more-toggle::after { content: " ▾"; }
.card-more[open] .card-more-toggle::after { content: " ▴"; }
.card-more-body { display: flex; flex-direction: column; gap: 0.8rem; padding-top: 0.5rem; }
/* The card's spells: a quiet ledger (2026-10-27). The level in the margin,
   its slots as pips, the names in the card's ink and set like text. */
.card-spells { margin: 0.6rem 0 0.2rem; padding-top: 0.5rem; border-top: 1px solid var(--line); }
.card-spells-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.35rem; }
.card-spells-title { margin: 0; font-size: var(--text-2xs); font-weight: normal; letter-spacing: 0.14em; text-transform: uppercase; color: var(--brass-500); }
.card-spells-prepare { font-size: var(--text-2xs); letter-spacing: 0.1em; color: var(--brass-500); text-decoration: none; }
.card-spells-prepare:hover { color: var(--brass-300); }
.card-spells-ledger { margin: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.card-spells-row { display: grid; grid-template-columns: 1rem 1fr; column-gap: 0.4rem; align-items: baseline; }
.card-spells-level { margin: 0; text-align: right; font-size: var(--text-xs); color: var(--brass-600); font-variant-numeric: tabular-nums; }
.card-spells-names { margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.1rem 0.75rem; font-size: var(--text-sm); line-height: 1.4; color: var(--parchment); }
.card-spells-pips { display: inline-flex; gap: 0.18rem; margin-right: -0.25rem; align-self: center; }
.card-spells-pips .pip { width: 0.42rem; height: 0.42rem; }
.card-spell { white-space: nowrap; }
.card-spells-names .inspect-link { color: inherit; border-bottom-color: var(--brass-700); }
.card-spells-names .inspect-link:hover { color: var(--brass-300); }
.card-spells-empty { margin: 0; font-size: var(--text-xs); color: var(--brass-600); }
.card-spells-unwritten { margin: 0.35rem 0 0 1.4rem; font-size: var(--text-xs); }
.card-spells-unwritten a { color: var(--brass-600); text-decoration: none; }
.card-spells-unwritten a:hover { color: var(--brass-300); }
.card-spells-book { margin-top: 0.35rem; padding-left: 1.4rem; }
.card-spells-book summary { cursor: pointer; list-style: none; font-size: var(--text-xs); color: var(--brass-600); }
.card-spells-book summary::-webkit-details-marker { display: none; }
.card-spells-book[open] summary { margin-bottom: 0.2rem; }
.card-spells-book .card-spells-names { opacity: 0.75; }

/* The party rows: a bigger face, the armor class on its corner, no foot. */
.roster-row .roster-portrait { position: relative; }
.roster-row .roster-ac {
    position: absolute; right: 0; bottom: 0;
    min-width: 1.5rem; padding: 0.05rem 0.25rem;
    font-size: var(--text-2xs); text-align: center;
    border-radius: var(--radius-small); border: 1px solid var(--line);
    background: var(--soot-800); color: var(--parchment);
}

/* A saving throw this character is proficient in: a small filled dot in the
   score's corner, the accent's color. No word - the dot is the mark the
   paper sheet uses for the same thing. */
.ability.is-save::after {
    content: "";
    position: absolute;
    top: 0.22rem;
    right: 0.22rem;
    width: 0.32rem;
    height: 0.32rem;
    border-radius: 50%;
    background: var(--brass-300);
}

/* The journal (world/journal.py): a player's own notes from before Rollies,
   and the sections the party shares. A section's owner reads the chip first:
   YOURS or PARTY, in words, never hue alone. */
.journal .journal-shelf { display: grid; gap: 0.75rem; margin-top: 1.25rem; }
.journal-entry { display: grid; gap: 0.5rem; }
.journal-entry-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.75rem; }
.journal-entry-title { margin: 0; font-family: var(--font-display); font-size: var(--text-md); font-weight: 600; }
.journal-entry-meta { font-size: var(--text-xs); color: var(--brass-500); }
.journal-who { margin-left: auto; font-size: var(--text-2xs); letter-spacing: 0.12em; color: var(--brass-500);
    border: 1px solid var(--line); border-radius: 999px; padding: 0.1rem 0.5rem; }
.journal-who.is-party { color: var(--parchment-bright); border-color: var(--brass-400); }
.journal-entry.is-party { border-left: 3px solid var(--brass-400); }
.journal-text { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 18rem; overflow-y: auto; font-size: var(--text-sm); line-height: 1.5; }
.journal-entry-acts { gap: 0.5rem; }
.journal-entry-acts form { display: inline; }
.journal-imports { display: grid; gap: 0.35rem; margin-top: 1.25rem; }
.journal-undo { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }

/* The spell pages (web/spell_pages.py). */
.spell-pages-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.spell-pages-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 0.75rem; }
.spell-pages-name { font-size: var(--text-md); color: var(--parchment); }
.spell-pages-actions { display: inline-flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.spell-pages-back input { width: 12rem; max-width: 100%; }
.spell-pages-note { flex-basis: 100%; margin: 0; }
.spell-write-row { display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; align-items: flex-end; }
.spell-write-row .field { flex: 1 1 10rem; min-width: 0; }
.spell-write-row .field-narrow { flex: 0 0 6rem; }
.spell-write-checks { align-items: center; }
.spell-write-checks input[type="text"] { flex: 1 1 12rem; min-width: 0; }
.spell-write-effect { border: 1px solid var(--line); border-radius: var(--radius-small); padding: 0.6rem 0.8rem; margin: 0.5rem 0; }

/* An attack where Rollies and the paper disagree (2026-10-27). */
.sheet-attack-flag { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 0.3rem 0.5rem; align-items: center; margin: 0.2rem 0 0.4rem; padding: 0.35rem 0.5rem; border-left: 2px solid var(--brass-500); background: var(--soot-700); font-size: var(--text-xs); }
.sheet-attack-flag-text { flex-basis: 100%; color: var(--parchment); }
.sheet-attack-keep input[type="text"] { width: 14rem; max-width: 100%; }

/* The sheet's EDIT page (2026-10-27): a value waiting on the DM, in its own color. */
.seed-numbers { display: flex; flex-wrap: wrap; gap: 0.5rem 0.8rem; align-items: flex-end; }
.seed-numbers .field-narrow { flex: 0 0 4.5rem; }
.seed-page .is-pending, .seed-page input.is-pending, .seed-page textarea.is-pending { color: var(--verdigris-400); border-color: var(--verdigris-600); }
.seed-page .seed-skills.is-pending { outline: 1px dashed var(--verdigris-600); outline-offset: 4px; }
.seed-pending-key { color: var(--verdigris-400); }
.seed-pending-note { margin: 0.5rem 0 0; padding: 0.4rem 0.6rem; border-left: 2px solid var(--verdigris-600); font-size: var(--text-sm); }
.seed-bonus-grid { display: grid; grid-template-columns: minmax(8rem, auto) 4.5rem 1fr; gap: 0.25rem 0.6rem; align-items: center; margin-top: 0.5rem; }
.seed-bonus-skill { font-size: var(--text-sm); }
.edit-review { width: 100%; border-collapse: collapse; margin: 0.5rem 0 1rem; font-size: var(--text-sm); }
.edit-review th, .edit-review td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--line); vertical-align: top; white-space: pre-line; }
.edit-review-new { color: var(--verdigris-400); }
=======
/* A die against the roll - Bane - in the dialogs (2026-10-27). */
.modal-check.is-against { color: var(--oxblood-400); }

/* The DM's one press from an inventory line to the item's card (2026-10-27). */
.gear-card-new { margin-left: 0.3rem; font-size: var(--text-2xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--brass-600); text-decoration: none; }
.gear-card-new:hover { color: var(--brass-300); }

/* T everywhere (2026-10-29): the box docked at the foot of any page, hidden
   until T. A scene's own box, where one is drawn, still takes the key. The
   night's lines sit above the field, newest nearest it. */
.scene-now.is-docked {
    position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
    width: min(680px, calc(100vw - 32px)); z-index: 60; margin: 0;
    padding: 0.55rem 0.7rem; background: var(--wood-900);
    border: 1px solid var(--brass-700); border-radius: var(--radius-small);
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
}
.scene-now.is-docked[hidden] { display: none; }
.scene-now.is-docked .scene-now-open { display: none; }
.scene-now.is-docked .scene-now-lines { max-height: 40vh; overflow-y: auto; margin: 0 0 0.45rem; }
.scene-now.is-docked .scene-now-lines:empty { display: none; }
.scene-now-line.is-secret { color: var(--brass-500); }
.scene-now-line.is-secret .scene-now-text { font-style: italic; }
.scene-now-secret { font-size: var(--text-2xs); letter-spacing: 0.08em; color: var(--oxblood-500); white-space: nowrap; }
.scene-now.is-secret .scene-now-box { border-color: var(--oxblood-500); }
.scene-now-tag { display: block; font-size: var(--text-2xs); letter-spacing: 0.04em; color: var(--brass-600); font-style: normal; }

/* The close-out: what Spark filed (sessions_filed.html). */
.filed-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.55rem; }
.filed-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 0.55rem; font-size: var(--text-sm); color: var(--parchment); line-height: 1.45; }
.filed-quote { flex: 1 1 18rem; min-width: 0; }
.filed-where { font-size: var(--text-2xs); letter-spacing: 0.04em; color: var(--brass-600); }
.filed-undo, .filed-to { margin: 0; display: inline-flex; gap: 0.35rem; align-items: center; }
.filed-to select { max-width: 14rem; }
.filed-done { margin: 1rem 0 2rem; display: flex; align-items: center; gap: 0.8rem; }
.filed-review-sum { margin: 0; font-size: var(--text-sm); }
.filed-review a:not(.btn-link), .filed-review-sum a { color: var(--brass-400); text-decoration-color: currentColor; }

/* What Spark noticed (templates/_spark_notices.html): a "!" on the mark,
   the list under the pointer. Over the header, never over the board. */
.header-title { position: relative; }
.spark-notices { position: absolute; left: 2.75rem; top: -0.1rem; z-index: 70; }
.spark-notices[hidden] { display: none; }
.spark-flag {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.05em; height: 1.05em; border-radius: 50%;
    font-size: var(--text-2xs); font-weight: 700; line-height: 1;
    background: var(--oxblood-500); color: var(--parchment); cursor: default;
}
.spark-notices-list {
    display: none; position: absolute; left: -1em; top: 1.6em;
    width: min(26rem, calc(100vw - 32px)); padding: 0.6rem 0.75rem;
    background: var(--wood-900); border: 1px solid var(--brass-700); border-radius: var(--radius-small);
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
}
.spark-notices:hover .spark-notices-list, .spark-notices:focus-within .spark-notices-list { display: block; }
.spark-notices-head { font-size: var(--text-2xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--brass-500); margin-bottom: 0.4rem; }
.spark-notices-list ol { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.55rem; }
.spark-notice { display: flex; flex-direction: column; gap: 0.1rem; font-size: var(--text-sm); line-height: 1.4; color: var(--parchment); }
.spark-notice-kind { font-size: var(--text-2xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--brass-600); }
.spark-notice.is-contradiction .spark-notice-kind { color: var(--oxblood-500); }
.spark-notice-from { font-size: var(--text-2xs); color: var(--brass-600); }
.spark-notice-from a { color: var(--brass-400); text-decoration-color: currentColor; }

/* SPARK MOVES (templates/_spark_moves.html). Every motion carries the
   drawing's center itself (translate to 256,256 and back), for WebKit's sake
   - see the note at .site-mark .mark-table. One class at a time, set by the
   player for its length and taken off. */
/* Out of reach of the table: the mark grays, and the transition rekindles
   it when the poll answers again. Color only, so it holds under reduced
   motion too. */
.is-offline .site-mark { color: var(--wood-600); }
@media (prefers-reduced-motion: no-preference) {
    /* Leap: the party's twenty. Spark jumps, the chairs push back, light. */
    .site-mark.is-leap .mark-spark { animation: spark-leap 1.5s cubic-bezier(0.22, 1.2, 0.36, 1); }
    .site-mark.is-leap .mark-table { animation: table-push 1.5s cubic-bezier(0.22, 1.2, 0.36, 1); }
    .site-mark.is-leap { filter: drop-shadow(0 0 0.35em currentColor); }
    /* Fizzle: the party's one. The table stalls, Spark sinks and grays,
       then rekindles. */
    .site-mark.is-fizzle .mark-spark { animation: spark-fizzle 2.3s ease-in-out; }
    .site-mark.is-fizzle .mark-table { animation: table-stall 2.3s ease-in-out; }
    /* Heartbeat: a death save passed. */
    .site-mark.is-heartbeat .mark-spark { animation: spark-beat 1.4s ease-in-out; }
    /* Your turn: a hop, the whole mark. */
    .site-mark.is-turn, .site-mark.is-notice { animation: mark-hop 0.65s cubic-bezier(0.34, 1.56, 0.64, 1); }
    /* Combat: the chairs pull in; at its end they ease out. */
    .site-mark.is-combat-start .mark-table { animation: table-pull 1.05s ease-in-out; }
    .site-mark.is-combat-end .mark-table { animation: table-push 1.05s ease-in-out; }
    /* Rests: a long rest glows down to an ember and back; a short, a breath. */
    .site-mark.is-rest-long .mark-spark { animation: spark-ember 3.1s ease-in-out; }
    .site-mark.is-rest-short .mark-spark { animation: spark-breathe 1.4s ease-in-out; }
}
@media (prefers-reduced-motion: reduce) {
    .site-mark.is-leap { filter: drop-shadow(0 0 0.35em currentColor); }
    .site-mark.is-fizzle .mark-spark, .site-mark.is-rest-long .mark-spark { opacity: 0.4; }
}
@keyframes spark-leap {
    0%   { transform: translate(256px, 256px) translate(0, 0) scale(1) translate(-256px, -256px); }
    35%  { transform: translate(256px, 256px) translate(0, -95px) scale(1.35) translate(-256px, -256px); }
    60%  { transform: translate(256px, 256px) translate(0, 12px) scale(0.92) translate(-256px, -256px); }
    100% { transform: translate(256px, 256px) translate(0, 0) scale(1) translate(-256px, -256px); }
}
@keyframes table-push {
    0%, 100% { transform: translate(256px, 256px) scale(1) translate(-256px, -256px); }
    35%      { transform: translate(256px, 256px) scale(1.09) translate(-256px, -256px); }
}
@keyframes table-pull {
    0%, 100% { transform: translate(256px, 256px) scale(1) translate(-256px, -256px); }
    45%      { transform: translate(256px, 256px) scale(0.9) translate(-256px, -256px); }
}
@keyframes spark-fizzle {
    0%   { transform: translate(256px, 256px) translate(0, 0) scale(1) translate(-256px, -256px); opacity: 1; }
    40%  { transform: translate(256px, 256px) translate(0, 70px) scale(0.6) translate(-256px, -256px); opacity: 0.3; fill: var(--wood-600); }
    70%  { transform: translate(256px, 256px) translate(0, 70px) scale(0.6) translate(-256px, -256px); opacity: 0.3; fill: var(--wood-600); }
    100% { transform: translate(256px, 256px) translate(0, 0) scale(1) translate(-256px, -256px); opacity: 1; }
}
@keyframes table-stall {
    0%, 100% { transform: translate(256px, 256px) rotate(0deg) translate(-256px, -256px); }
    15%      { transform: translate(256px, 256px) rotate(-9deg) translate(-256px, -256px); }
    30%      { transform: translate(256px, 256px) rotate(4deg) translate(-256px, -256px); }
    45%      { transform: translate(256px, 256px) rotate(-1deg) translate(-256px, -256px); }
}
@keyframes spark-beat {
    0%, 40%, 100% { transform: translate(256px, 256px) scale(1) translate(-256px, -256px); }
    15%           { transform: translate(256px, 256px) scale(1.3) translate(-256px, -256px); }
    55%           { transform: translate(256px, 256px) scale(1.2) translate(-256px, -256px); }
}
@keyframes mark-hop {
    0%, 100% { transform: translateY(0); }
    40%      { transform: translateY(-0.35em); }
}
@keyframes spark-ember {
    0%, 100% { opacity: 1; }
    45%, 60% { opacity: 0.35; fill: var(--oxblood-500); }
}
.filed-undo-one button { padding: 0 0.15rem; color: var(--wood-700); }
.filed-undo-one button:hover { color: var(--oxblood-500); }
.filed-waiting { font-size: var(--text-2xs); letter-spacing: 0.04em; color: var(--oxblood-500); }
/* The tonight strips' container takes no box of its own (index.html). */
.tonight-strips { display: contents; }

/* ---- Session cards (2026-11-02) ----------------------------------------
   One rounded card per session, newest first: the cover the table saw that
   night, the number and name, who came, the recap's first line. One column
   on a phone, two on a laptop, three on a wide screen. A player sees a
   session the DM has not shared as a numbered blank. */
.chronicle-jumps { display: flex; flex-wrap: wrap; gap: 0.3rem 1.2rem; font-size: var(--text-2xs); letter-spacing: 0.1em; }
.chronicle-jumps a { color: var(--brass-600); text-decoration: none; }
.chronicle-jumps a:hover { color: var(--brass-300); }
.session-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
@media (max-width: 700px) { .session-grid { grid-template-columns: minmax(0, 1fr); } }
@media (min-width: 1400px) { .session-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .chronicle { max-width: 96rem; } }
.session-card { display: flex; flex-direction: column; min-width: 0; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius); background: var(--soot-800); }
.session-card.is-shared { border-color: var(--brass-600); }
.session-cover { display: block; aspect-ratio: 16 / 9; background: var(--wood-800); text-decoration: none; }
.session-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
.session-cover-blank { display: flex; align-items: center; justify-content: center; position: relative; color: var(--brass-600); }
.session-cover-blank::before {
    content: ""; position: absolute; inset: 0; opacity: 0.18; background: currentColor;
    -webkit-mask: url("/static/rollies-mark.svg") center / 38% no-repeat; mask: url("/static/rollies-mark.svg") center / 38% no-repeat;
}
.session-cover-blank b { position: relative; font-family: var(--font-display); font-size: 2.6rem; letter-spacing: 0.08em; color: var(--brass-500); }
.session-card-body { display: flex; flex-direction: column; gap: 0.35rem; padding: 0.6rem 0.9rem 0.8rem; }
.session-card-title { font-family: var(--font-display); font-size: var(--text-md); letter-spacing: 0.06em; color: var(--parchment); text-decoration: none; }
a.session-card-title:hover { color: var(--parchment-bright); }
.session-excerpt { margin: 0; font-size: var(--text-sm); line-height: 1.45; color: var(--brass-400); }
.session-card.is-blank .session-excerpt { color: var(--brass-600); }
.session-card-actions { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.2rem; }
.session-card-actions .recap-share { margin-left: auto; }
.chronicle-more { display: flex; flex-direction: column; gap: 0.8rem; margin-top: 0.6rem; }
/* the session's own page */
.session-hero { aspect-ratio: 21 / 9; max-height: 46vh; overflow: hidden; border-radius: var(--radius); background: var(--wood-800); }
.session-hero img { display: block; width: 100%; height: 100%; object-fit: cover; }
.session-hero.session-cover-blank b { font-size: 4rem; }
.session-dm-row { display: flex; flex-wrap: wrap; gap: 0.4rem 1.2rem; }
.session-cover-form { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.session-cover-option { display: block; width: 7.5rem; aspect-ratio: 16 / 9; border: 2px solid transparent; border-radius: var(--radius-small); overflow: hidden; cursor: pointer; }
.session-cover-option.is-picked, .session-cover-option:has(input:checked) { border-color: var(--brass-500); }
.session-cover-option input { position: absolute; opacity: 0; width: 0; height: 0; }
.session-cover-option img { display: block; width: 100%; height: 100%; object-fit: cover; }
.session-part { display: flex; flex-direction: column; gap: 0.4rem; }
.session-part-title { margin: 0; font-size: var(--text-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass-500); }
.session-pictures { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 0.6rem; }
.session-picture { margin: 0; border-radius: var(--radius-small); overflow: hidden; background: var(--wood-800); }
.session-picture img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.session-picture figcaption { padding: 0.25rem 0.5rem; font-size: var(--text-xs); color: var(--brass-500); }
.session-pages { margin: 0; padding-left: 1.1rem; font-size: var(--text-sm); }
.session-chat { margin: 0; padding: 0; list-style: none; font-size: var(--text-sm); line-height: 1.5; }
.session-chat li.is-dm b { color: var(--brass-300); }
.session-chat li.is-player b { color: var(--parchment); }
.session-blank-page { max-width: 28rem; }
