:root {
    /* Every skin is dark, and saying so lets the browser draw its native controls (the
       select, a scrollbar) to match instead of in their light defaults. */
    color-scheme: dark;
    --color-canvas: #15110d;
    --color-panel: #211a14;
    --color-panel-raised: #2b2118;
    --color-text: #f3e7cc;
    --color-text-muted: #b9aa8c;
    --color-accent: #d0a85a;
    --color-accent-strong: #edc978;
    /* The text an accent-filled control carries. It is read from the accent rather than
       from the page's own text colour, so a skin that fills a button with something other
       than the accent states the pair together. */
    --color-on-accent: #17110b;
    --color-border: #6f5633;
    --color-danger: #d47a72;
    --color-success: #83a86b;
    /* A critical success is green like an ordinary success and brighter than one, so that
       the recent-rolls panel tells the two tiers apart by colour as well as by their text. */
    --color-critical: #a9dd63;
    /* What an effect is drawn in: what is helping a character and what is working against
       them. Both read from the palette's own success and danger, which every skin already
       values, so a skin only states these where it wants them apart from those. */
    --color-buff: var(--color-success);
    --color-debuff: var(--color-danger);
    --font-body: ui-sans-serif, system-ui, sans-serif;
    --font-display: ui-serif, Georgia, serif;
    --text-sm: 0.875rem;
    --text-base: 1rem;
    --text-lg: 1.25rem;
    /* A page title inside the world shell, which has less room than a full-width page. */
    --text-md: 1.5rem;
    --text-xl: clamp(1.75rem, 3vw, 2.5rem);
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-6: 1.5rem;
    --space-8: 2rem;
    --space-12: 3rem;
    --radius-sm: 0.25rem;
    --radius-md: 0.5rem;
    --shell-nav-width: 16rem;
    --shell-widget-width: 17rem;
    --shadow-panel: 0 0.5rem 1.5rem rgb(0 0 0 / 0.2);
    /* The button has a colour of its own because a skin may draw it in something other than
       the accent. The default skin sets it to the accent the rules below read before, so it
       renders exactly as it did. */
    --color-button: var(--color-accent);
    --color-button-hover: var(--color-accent-strong);
    /* The surface a field, a stat box, a placeholder, an image frame and a disabled button are
       recessed into: the canvas, except on a skin whose canvas is lighter than its panels. */
    --color-well: var(--color-canvas);
    /* What the modal dialog dims the page behind it with. */
    --color-backdrop: rgb(0 0 0 / 0.6);
}

/* The Frostmoor skin: a slate canvas under steel-grey panels, with bone text, an ice-blue
   accent and steel buttons, chosen to sit opposite the default skin's amber on umber. This
   block redefines tokens and nothing else — no selector here and no rule the default skin
   does not have, because a skin is a set of values; anything that needed a rule of its own
   would be a missing token on `:root` instead. Every surface is a flat colour. */
[data-skin="frost"] {
    --color-canvas: #0f1319;
    --color-panel: #1a2029;
    --color-panel-raised: #242c38;
    --color-text: #e6ecf2;
    --color-text-muted: #a3b0bf;
    --color-accent: #8ec5e6;
    --color-accent-strong: #b8def5;
    --color-on-accent: #eef3f8;
    --color-border: #3d4a5c;
    /* The warm colours are kept warm on purpose: a refusal and a success read as the same
       things they are on the default skin, only against slate. */
    --color-danger: #ea837c;
    --color-success: #8fcc8c;
    --color-critical: #cdf27f;
    --color-backdrop: rgb(4 7 12 / 0.7);
    --shadow-panel: 0 0.5rem 1.5rem rgb(0 0 0 / 0.5);
    --color-button: #34495f;
    --color-button-hover: #41596f;
}

/* The Arcana Aurora skin, after a fantasy MMO interface the user brought: a warm near-black
   canvas behind plum panels, with rose-gold hairlines, rose buttons and an orchid accent.
   Every skin is dark, so hue is what tells them apart: amber on umber, ice on slate, orchid
   on plum. */
[data-skin="arcana"] {
    --color-canvas: #14100f;
    --color-panel: #1e161c;
    --color-panel-raised: #2a1e28;
    --color-text: #f1e6ea;
    --color-text-muted: #b2a0aa;
    --color-accent: #d477b3;
    --color-accent-strong: #eaa0d0;
    --color-on-accent: #f6eaf0;
    --color-border: #8c6a58;
    --color-danger: #e8766a;
    --color-success: #8fcf86;
    --color-critical: #c9ec7a;
    --color-backdrop: rgb(12 6 10 / 0.75);
    --shadow-panel: 0 0.5rem 1.5rem rgb(0 0 0 / 0.55);
    --color-button: #6e3a5a;
    --color-button-hover: #85476e;
}

/* The Sapphire Sky skin, after a second interface mock the user brought: navy panels under a
   near-black indigo canvas, gold hairlines, a gold accent for links and marked pips, and
   deep teal buttons carrying white text. Gold rather than blue as the accent is what keeps
   it apart from Frostmoor, whose accent is ice. */
[data-skin="sapphire"] {
    --color-canvas: #111420;
    --color-panel: #1c2136;
    --color-panel-raised: #232b48;
    --color-text: #eef1f8;
    --color-text-muted: #aab3cc;
    --color-accent: #dcb964;
    --color-accent-strong: #f0d58a;
    --color-on-accent: #f2fbff;
    --color-border: #a88a52;
    --color-danger: #ef8a80;
    --color-success: #8fd39a;
    --color-critical: #c8ef7c;
    --color-backdrop: rgb(5 8 18 / 0.75);
    --shadow-panel: 0 0.5rem 1.5rem rgb(0 0 0 / 0.5);
    --color-button: #16596e;
    --color-button-hover: #1a6d87;
}

/* The Travels and Trepidation skin, after a third mock the user brought: dark brown panels on
   a lighter, smoky brown canvas — the one skin whose canvas is lighter than its panels,
   which is what the mock does and what keeps it apart from Emberlight's near-black umber —
   with stone-grey hairlines, bone text and a burnt-orange accent and buttons. */
[data-skin="travels"] {
    --color-canvas: #3a332b;
    --color-panel: #1f1b19;
    --color-panel-raised: #2d2723;
    --color-text: #e6dccb;
    --color-text-muted: #b7ab9b;
    --color-accent: #ee9448;
    --color-accent-strong: #f4a45c;
    --color-on-accent: #fbefe2;
    --color-border: #6b6157;
    --color-danger: #f08a92;
    --color-success: #7fbf6c;
    --color-critical: #dcf27a;
    --color-backdrop: rgb(20 16 12 / 0.7);
    --shadow-panel: 0 0.5rem 1.5rem rgb(0 0 0 / 0.5);
    --color-button: #8a4119;
    --color-button-hover: #9c4a1c;
    /* Darker than both panels, the canvas here being lighter than either. */
    --color-well: #181513;
}

/* One colour per class, the banner's body colour lightened where it needed it to read as
   text on the dark panels — Rogue's banner is black, so its colour here is a light grey.
   Each value clears 4.5:1 against every skin's `--color-panel` and `--color-panel-raised`,
   every skin being dark. Sapphire's raised panel is the worst ground of the ten and so the
   one every value is bounded by. Warrior and Druid sit higher above that floor than they
   need to: a class sharing a primary domain with one of them takes the deeper colour of the
   pair, and with these two at the floor there was no room under them to take it in.
   The directory's class name is the only element styled by these, and the rule under them is
   the only one that reads the token. The character form's subclass `<option>`s carry the same
   attribute for `character-form.js` to filter on, so the rule names the span rather than the
   attribute alone and leaves that select untouched.
   The four classes under the nine key to the domain each shares with one of the nine, the
   first-listed breaking a tie, so each is a sibling of the class it shares that domain with:
   the Assassin takes Blade's red beside the Warrior's, the Brawler Valor's orange beside the
   Guardian's, and the Witch Sage's green beside the Druid's — Sage being the one domain the
   Witch shares, its first-listed Dread having no class among the nine to anchor to. Two of
   them are the deeper of their pair, which at this floor is
   bought with chroma and hue rather than with darkness — there is no room under 4.5:1 for a
   blood red or a forest green — and the Brawler is the paler of its pair instead, which the
   floor gives away freely. The Warlock has no sibling, Dread being new: its value is the
   domain's own #2C2A67 lightened along its own indigo hue until it clears the floor, the
   treatment the Rogue's black banner gets. */
[data-class="bard"] { --color-class: #d376b6; }
[data-class="rogue"] { --color-class: #959599; }
[data-class="sorcerer"] { --color-class: #a587cd; }
[data-class="druid"] { --color-class: #82bc86; }
[data-class="ranger"] { --color-class: #d8dce3; }
[data-class="warrior"] { --color-class: #e09a96; }
[data-class="guardian"] { --color-class: #dd7b25; }
[data-class="seraph"] { --color-class: #e0b73a; }
[data-class="wizard"] { --color-class: #7397d5; }
[data-class="assassin"] { --color-class: #f2684a; }
[data-class="brawler"] { --color-class: #f0c096; }
[data-class="warlock"] { --color-class: #8e8be0; }
[data-class="witch"] { --color-class: #38a492; }
span[data-class] { color: var(--color-class); }

* { box-sizing: border-box; }

/* The `hidden` attribute is mapped to `display: none` only by the UA stylesheet, so any
   author rule setting `display` on an element wins over it regardless of specificity —
   `button { display: inline-block }` below would keep a hidden button visible. The
   attribute needs an author rule of its own to hold. */
[hidden] { display: none !important; }

html {
    background: var(--color-canvas);
    color: var(--color-text);
    font-family: var(--font-body);
    font-size: var(--text-base);
    line-height: 1.5;
    /* The stream's jump links are plain same-page anchors; this is what makes the move
       between its ends readable rather than a cut. */
    scroll-behavior: smooth;
}
/* A reader who asks for reduced motion gets the instant jump the anchor performs by itself. */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
}

body { margin: 0; min-width: 20rem; }
h1, h2, h3, p { margin: 0; }
h1, h2, h3 { color: var(--color-text); font-family: var(--font-display); line-height: 1.15; }
h1 { font-size: var(--text-xl); }
h2 { font-size: var(--text-lg); }
/* The browser default (~1.17em) sits so close to --text-lg that a feature name inside a
   section reads as a second section title; base size keeps the levels apart. */
h3 { font-size: var(--text-base); }
a { color: var(--color-accent-strong); }
a:hover { color: var(--color-text); }

button, .button {
    appearance: none;
    background: var(--color-button);
    border: 1px solid var(--color-accent-strong);
    border-radius: var(--radius-sm);
    color: var(--color-on-accent);
    cursor: pointer;
    display: inline-block;
    font: inherit;
    font-weight: 700;
    padding: var(--space-2) var(--space-4);
    text-align: center;
    text-decoration: none;
}

button:hover, .button:hover { background: var(--color-button-hover); color: var(--color-on-accent); }
button:disabled, button:disabled:hover {
    background: var(--color-well);
    border-color: var(--color-text-muted);
    color: var(--color-text-muted);
    cursor: not-allowed;
}
button:focus-visible, a:focus-visible, input:focus-visible, textarea:focus-visible {
    outline: 3px solid var(--color-accent-strong);
    outline-offset: 2px;
}

form { display: grid; gap: var(--space-3); }
label { color: var(--color-text-muted); font-size: var(--text-sm); font-weight: 700; }
input, textarea {
    background: var(--color-well);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    color: var(--color-text);
    font: inherit;
    max-width: 100%;
    padding: var(--space-2) var(--space-3);
    width: 100%;
}
textarea { min-height: 9rem; resize: vertical; }
input[type="file"] { padding: var(--space-2); }
/* The full-width rule above is for the fields text is typed into. A checkbox or a radio draws a
   glyph at its own intrinsic size, so stretching its box leaves the glyph centred in a box the
   width of the row and pushes the label beside it to the far edge. */
input[type="checkbox"], input[type="radio"] { width: auto; }

.notice {
    background: color-mix(in srgb, var(--color-danger) 18%, var(--color-panel));
    border: 1px solid var(--color-danger);
    border-radius: var(--radius-sm);
    color: var(--color-text);
    padding: var(--space-3) var(--space-4);
}
/* The one notice that reports something that worked rather than something that was refused.
   It reuses the box above and repaints it, so the two cannot drift apart in shape. */
.notice.success {
    background: color-mix(in srgb, var(--color-success) 18%, var(--color-panel));
    border-color: var(--color-success);
}

/* The auto side margins centre these pages, but they also switch off the stretch a grid
   item would otherwise get, leaving the box sized to its own content: the character form
   then narrows as soon as a lightweight NPC hides the stat grid. An explicit width fills
   the column up to the cap instead, so the content no longer sets the page width. */
.landing, .world-picker, .form-page {
    margin: var(--space-12) auto;
    max-width: 42rem;
    padding: 0 var(--space-4);
    width: 100%;
}
.landing, .world-picker, .form-page, .world-overview, .location-page { display: grid; gap: var(--space-6); }
.landing form, .world-card {
    background: var(--color-panel);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-panel);
    padding: var(--space-4);
}
.world-cards { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
.world-card { display: grid; gap: var(--space-3); }
/* One release on the changelog. The reset takes the margins off headings and paragraphs, so
   the entry spaces its own parts; the list keeps its bullets and its indent and gives up only
   the margins the grid already draws. */
.release, .guide-example { display: grid; gap: var(--space-2); }
.release ul { margin: 0; }
/* The markup guide's lead: the paragraph a reader is meant to read before the markup, set
   off by a rule down its edge rather than by the notice box, which reports refusals. */
.guide-lead {
    border-inline-start: 3px solid var(--color-accent);
    display: grid;
    gap: var(--space-2);
    padding-inline-start: var(--space-4);
}
/* A guide example's own text — the text to type and the text it is stored as — sits in a
   well so it reads as something to copy. The stacking is the release list's rule above. */
.guide-example code {
    background: var(--color-well);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
}
.description { overflow-wrap: anywhere; white-space: pre-wrap; color: var(--color-text-muted); }
.muted { color: var(--color-text-muted); }

.world-shell {
    display: grid;
    gap: var(--space-6);
    grid-template-columns: minmax(0, var(--shell-nav-width)) minmax(0, 1fr) minmax(0, var(--shell-widget-width));
    margin: 0 auto;
    max-width: 120rem;
    min-height: 100vh;
    padding: var(--space-6);
}

.shell-nav, .shell-content, .shell-widgets { align-self: start; }
.shell-nav {
    background: var(--color-panel);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-panel);
    display: grid;
    gap: var(--space-6);
    padding: var(--space-4);
}
.shell-nav header, .shell-nav nav, .shell-widgets { display: grid; gap: var(--space-3); }
.shell-nav h1 { font-size: var(--text-lg); }
/* Every link in the nav column is styled the same, whether it is a top-level destination,
   a location in the list or the world's own name: no underline by default, and a bottom
   border that appears on hover. */
.shell-nav a { border-bottom: 1px solid transparent; text-decoration: none; }
.shell-nav a:hover { border-color: var(--color-accent); }
/* The link to the page being read keeps the hover border. The weight is left alone so the
   column's widths do not move. */
.shell-nav a[aria-current="page"] { border-color: var(--color-accent); }
.shell-nav nav > a { padding: var(--space-1) 0; }
.shell-nav ul { display: grid; gap: var(--space-2); list-style: none; margin: 0; padding: 0; }
.shell-nav li a { display: block; overflow-wrap: anywhere; }
/* A location holding posts this reader has not been past. The dot is drawn by the element
   itself, which holds nothing but the text a screen reader is given instead of it. */
.unread {
    background: var(--color-accent);
    border-radius: 50%;
    display: inline-block;
    height: 0.5em;
    margin-inline-start: var(--space-2);
    vertical-align: middle;
    width: 0.5em;
}
/* The navigation's own entries are direct children of the <nav>; the Game Master's sit inside
   a fold, so they are given that same box explicitly. */
.shell-nav .gm-menu a { display: block; padding: var(--space-1) 0; }
.shell-nav .gm-menu { border-top: 1px solid var(--color-border); margin-top: var(--space-2); padding-top: var(--space-4); }
.shell-content { display: grid; gap: var(--space-6); min-width: 0; }

.world-overview, .location-page, .form-page {
    background: var(--color-panel-raised);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-panel);
    padding: var(--space-6);
}
.world-overview section, .location-page, .table-stream { display: grid; gap: var(--space-4); }
.world-card, .shell-widgets section, .reserved-directory, .map-form {
    background: var(--color-panel);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-panel);
    padding: var(--space-4);
}
.world-map, .location-image {
    background: var(--color-well);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    height: auto;
    max-width: 100%;
}
.location-page > h1 { font-size: var(--text-md); }
.scene-setting { display: grid; gap: var(--space-4); }
.scene-setting summary { cursor: pointer; width: fit-content; }
.location-image { display: block; margin-inline: auto; max-height: 25rem; object-fit: contain; }
.shell-widgets section { display: grid; gap: var(--space-2); }
.world-date, .eyebrow, .shell-widgets p { color: var(--color-text-muted); }
.eyebrow { font-size: var(--text-sm); letter-spacing: 0.08em; text-transform: uppercase; }

.character-directory, .character-sheet, .character-form fieldset, .composer {
    background: var(--color-panel);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-4);
}
/* A sub-heading inside a panel: the directory's groups, and the loadout above the cards in
   the shell's widget column. */
.character-directory h3, .character-widget h3 {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    margin: 0;
}
/* The row of buttons a panel or a form ends with, and the controls under one card. A form is
   a grid and stretches every child across it, which is right for its inputs and wrong for its
   buttons: inside this row they are flex items and so are sized by their own labels. Only a
   form that opts in by carrying the row gets that — the login and character forms keep their
   wide buttons. */
.directory-actions, .action-row, .card-controls {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}
/* A table's seating, which opens its page. The list of who sits there is plain text with a
   control beside each name, and the row under it holds whatever this viewer may do to the
   table itself.

   The list carries the columns and each seat spans them as a subgrid, so the name column is
   as wide as the longest name and every control starts at the same x whatever the name beside
   it reads. A seat with no control for this viewer simply leaves its second cell empty. The
   name column's floor is 0 rather than its content, because a name runs to 120 characters and
   a bare `max-content` track never wraps: one long name would push the controls off the page
   instead of wrapping under itself. */
.table-seating { display: grid; gap: var(--space-4); }
.table-members {
    display: grid;
    gap: var(--space-2) var(--space-4);
    grid-template-columns: minmax(0, max-content) max-content;
    list-style: none;
    margin: 0;
    padding: 0;
}
.table-members li {
    align-items: baseline;
    display: grid;
    grid-column: span 2;
    grid-template-columns: subgrid;
}
/* The table's own actions, in one line under the seating. `baseline` is what lines the
   rename fold's summary up with the controls beside it and keeps them there while it is
   open, which `center` would not. */
.table-actions { align-items: baseline; display: flex; flex-wrap: wrap; gap: var(--space-4); }
/* A control that stands in a row of text rather than at the end of a form. It carries the
   button's own colours and border and only its weight comes down, so one word beside a
   character's name reads as a control without outshouting the name. */
.compact { font-size: var(--text-sm); font-weight: 400; padding: var(--space-1) var(--space-3); }
.character-table-wrap, .invite-table-wrap { overflow-x: auto; }
.character-table, .invite-table { border-collapse: collapse; width: 100%; }
.character-table th, .character-table td, .invite-table th, .invite-table td {
    border-bottom: 1px solid var(--color-border);
    padding: var(--space-2);
    text-align: left;
    vertical-align: middle;
}
.character-table th, .invite-table th { color: var(--color-text-muted); font-size: var(--text-sm); }
.character-cell { align-items: center; display: flex; gap: var(--space-3); }
.directory-portrait {
    border-radius: var(--radius-sm);
    flex: 0 0 44px;
    height: 44px;
    object-fit: cover;
    width: 44px;
}
.stacked-pair { display: grid; gap: var(--space-1); }
.character-summary { align-items: flex-start; display: flex; gap: var(--space-3); margin-top: var(--space-3); }
.character-widget img { aspect-ratio: 1; border-radius: var(--radius-sm); flex: 0 0 4rem; object-fit: cover; width: 4rem; }
.character-sheet { display: grid; gap: var(--space-6); }
/* Half a line under the section title and between features; the global margin reset
   would otherwise set the feature names hard against both. */
.rule-features { display: grid; gap: var(--space-2); }
.character-sheet header { display: flex; flex-wrap: wrap; gap: var(--space-4); }
.character-sheet .portrait { border-radius: var(--radius-md); height: 12rem; object-fit: cover; width: 12rem; }
.portrait-placeholder { align-content: center; background: var(--color-well); color: var(--color-text-muted); text-align: center; }
.stats, .stat-grid { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); }
/* An NPC form with the stats box unchecked hides the grid; a PC form has no such checkbox,
   so its grid is always shown. */
form:has(input[name="has_stats"]:not(:checked)) .stat-grid { display: none; }
/* A note inside a grid takes a whole row rather than one cell beside the inputs. */
.stat-grid p { grid-column: 1 / -1; margin: 0; }
/* The sheet's boxes share the form grid's own track width: six traits fit one row there, and
   a wider minimum fits only five and wraps the sixth. The room the spellcast die needs comes
   out of the box's padding instead, and the dt lays the word and the die out as a row. The
   die never shrinks, and overflow-wrap is what lets the word break inside its box at the
   narrowest track — a flex item's min-content width is otherwise the whole word, and the
   pair would overflow the well instead. */
.stats { margin: 0; }
.stats dt { align-items: center; display: flex; gap: var(--space-1); overflow-wrap: anywhere; }
.stats div { background: var(--color-well); border-radius: var(--radius-sm); padding: var(--space-2); }
.stats dd { font-size: var(--text-lg); margin: 0; }
.status { color: var(--color-accent-strong); text-transform: capitalize; }

/* Text for a screen reader and nothing else: each pip is a square with no writing on it, so
   the button carries what clicking it will do where only a reader that needs it will find it.
   The size and the clip are what keep it out of the layout without hiding it from the
   accessibility tree, which `display: none` would do. */
.visually-hidden {
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}
/* One pool: its name and count above, its pips below. The row is the unit htmx swaps, so it
   is what the write answers with and nothing outside it moves. */
/* The sheet lays its pools out side by side, as many to a row as fit; the sidebar's copy of the
   same rows keeps one to a row, its column being narrow. */
.pool-tracks { display: grid; gap: var(--space-3); }
.character-sheet .pool-tracks { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }
/* The heading and a failed read's notice each take the whole row: the tracks flow around them
   otherwise, leaving either stranded in one column beside a pool. */
.character-sheet .pool-tracks > h2,
.character-sheet .pool-tracks > .notice { grid-column: 1 / -1; }
/* The Identity list prints each entry on one line, the term and its value side by side, with
   every value starting at one x: the list is a two-column grid and each entry's wrapper
   dissolves into it. */
.character-identity dl { column-gap: var(--space-2); display: grid; grid-template-columns: max-content 1fr; margin: 0; row-gap: var(--space-1); }
.character-identity dl > div { display: contents; }
.character-identity dt::after { content: ":"; }
.character-identity dd { margin: 0; }
.pool-track, .resource-track { display: grid; gap: var(--space-1); }
.pool-summary { align-items: baseline; display: flex; gap: var(--space-2); margin: 0; }
.pool-name { font-family: var(--font-display); }
.pool-count { color: var(--color-text-muted); font-size: var(--text-sm); }
/* Each pip is its own form, so the row lays the forms out rather than the buttons. */
.pips { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.pips form { display: contents; }
/* Every pip is a symbol rather than a box: a heart for HP, a shield for Armor, a spark for
   Hope, a bolt for Stress, an eight-armed star for Chaos, a sword for a progress track and a
   hexagonal token for a class resource. The pip is the icon alone, sized to the square the
   boxes used to have. Each symbol is drawn twice, once per state, and each drawing carries its
   own fills, fill opacities and stroke widths: an empty pip is an outline barely tinted, a
   filled one the same outline more strongly tinted with a highlight line over it. So nothing
   about the drawing is set here — a rule here would fight a presentation attribute the body
   carries — and this file supplies only the colour, which the bodies read as `currentColor`.
   `fill-rule` is no longer set either; every body fills as drawn under the default `nonzero`. */
.pip {
    background: none;
    border: 0;
    color: var(--color-text-muted);
    display: block;
    font-size: 1.25rem;
    line-height: 0;
    padding: 0;
}
/* A marked pip is the accent, which is the one colour this app spends on what is in play. */
.pip.filled { color: var(--color-accent-strong); }
button.pip:hover { color: var(--color-accent-strong); }

/* The scene's own controls, in one row under its header. Every button in it is an ordinary
   button of the same size, so the recommended direction is marked by a ring drawn outside its
   border, which changes no box and so moves nothing beside it. The ring is a shadow rather
   than an outline, leaving `outline` to the focus rule, which would otherwise be outranked on
   the recommended button. */
.scene-controls .recommended { box-shadow: 0 0 0 2px var(--color-accent-strong); }

/* The Chaos dial in the shell's widget column. It is read here and moved only at a scene's
   end, so the row carries no buttons and the spacing is all this rule sets. */
.chaos-widget .chaos-track { margin-top: var(--space-2); }

/* What the group is fighting through, above the stream that narrates it. Tracks sit side by
   side: two share a row of the content column, and a third wraps under them. */
.track-list { display: grid; gap: var(--space-3); }
/* The same grid holds a character's domain cards and what it carries, on the sheet: a loadout
   is three cards and a vault a handful, so two share a row and the rest wrap under them. */
.track-grid, .card-grid {
    display: grid;
    gap: var(--space-3);
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}
.track-grid:empty, .card-grid:empty { display: none; }
/* One box in either grid. The grid stretches a box to the height of the tallest one on its
   row, and the box lays its own content out as a flex column: the text stacks from the top and
   `align-items: start` sizes each child by its content instead of stretching it across the box,
   which is what keeps the controls row and the pip rows the width of what they hold. Any height
   left over by the row goes to whichever child claims it, which is the rule below. */
.progress-track, .domain-card, .carried-item, .character-experience, .character-effect,
.character-saying {
    align-items: start;
    background: var(--color-panel-raised);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3);
}
/* The controls under a card, an item or an Experience, and the rewrite fold that follows them.
   `margin-top: auto` absorbs the box's surplus height above the controls, so the group sits at
   the bottom of the box and the controls of every box on a row line up, however much text each
   one carries above them. Scoped to the boxes named below: the sheet's Milestones section
   carries a `.card-controls` of its own outside any box, which stays where it is written. */
.domain-card .card-controls,
.carried-item .card-controls,
.character-experience .card-controls,
.character-effect .card-controls,
.character-saying .card-controls {
    margin-top: auto;
}

/* A character's domain cards, what it carries and the milestones behind it, on the sheet. All
   of them sit in the same boxes the tracks do. */
.domain-cards, .character-items, .character-experiences, .character-milestones,
.character-effects, .character-sayings {
    display: grid;
    gap: var(--space-3);
}

/* An item's picture: a thumbnail on its row, framed on the well the other recessed surfaces
   use, and the whole image in a popover card opened from it. The thumbnail is decorative — the
   button beside it carries the name — and the card's image is the one that describes itself. */
.item-thumb {
    background: var(--color-well);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    display: block;
    line-height: 0;
    padding: 0;
}
/* `button:hover` paints every button the accent ground, which would swallow the frame this one
   is drawn as. The class with the hover pseudo-class outweighs the element with it, so naming
   the well again here is what keeps the frame; the same rule below does it for an effect's
   symbol, which has the same shape. */
.item-thumb:hover { background: var(--color-well); }
.item-thumb img { border-radius: var(--radius-sm); height: 3.5rem; object-fit: cover; width: 3.5rem; }
.item-image-card img { border-radius: var(--radius-sm); display: block; max-height: 80vh; }

/* An item may print two perk lines: the one its weapon type carries, which is the same text
   for every weapon of that type, and the enchantment its owner wrote. The tint is what tells
   the written line from the derived one where an item carries both. */
.item-enchantment { color: var(--color-accent-strong); }

/* Which way an effect cuts, which is the one place its kind becomes a colour. The sheet tints
   the word beside the name and the sidebar tints the symbol; both read the same token. */
.effect-buff { color: var(--color-buff); }
.effect-debuff { color: var(--color-debuff); }
/* The effects on the reader's own character, under its pools in the widget column: one symbol
   each, drawn like a marked pip and tinted by kind, opening a card that names the effect and
   prints its note. The symbol is decorative — the button carries the name as text. As with a
   pip, the drawing carries its own strokes and fill opacity, so this file sets only the
   colour the body reads as `currentColor`. */
.effect-icons {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    list-style: none;
    margin: var(--space-3) 0 0;
    padding: 0;
}
.effect-icon {
    background: none;
    border: 0;
    display: block;
    font-size: 1.25rem;
    line-height: 0;
    padding: 0;
}
/* Hovering a symbol must not take its kind's colour off it or put a button's ground behind it.
   `button:hover` carries one element and one pseudo-class and so outweighs the kind's class on
   its own; adding the element to the kind's selector outweighs it back, which is what
   `button.pip:hover` does for a pip. The well is the ground the symbol takes instead. */
button.effect-buff:hover { color: var(--color-buff); }
button.effect-debuff:hover { color: var(--color-debuff); }
.effect-icon:hover { background: var(--color-well); }
/* Where anchor positioning exists the card opens against the symbol that opened it, exactly as
   a roll's card opens against its chip. Every symbol carries the same anchor name, so each
   effect's own `<li>` confines that name to the one pair — without the scope the name resolves
   across the whole list and a card lands beside another effect's symbol. Both properties are
   required, a browser with the name but not the scope doing exactly that; without either the
   card stays centred in the viewport, which the top layer makes safe. */
@supports (anchor-name: --effect-icon) and (anchor-scope: --effect-icon) {
    .effect-icons li { anchor-scope: --effect-icon; }
    .effect-icon { anchor-name: --effect-icon; }
    .effect-card {
        margin: var(--space-1) 0 0 0;
        position-anchor: --effect-icon;
        position-area: bottom span-right;
        position-try-fallbacks: flip-block, flip-inline;
    }
}
/* The inventory form asks for an enchantment only where the item was marked magical, and for
   the choice between an enchantment on top of the weapon type's perk and one replacing it only
   where the item is both a weapon and magical. The trait a weapon is swung with is asked for
   only where the item is a weapon at all. Hiding is the whole of what this does: a hidden
   field still submits what it holds, and the write reads the checkbox rather than the text, so
   unticking the box is what makes an item mundane again. A browser without `:has` shows every
   field, which is a form that still works. */
form:has(input[name="magical"]:not(:checked))
    :is(.enchantment-label, .enchantment-field, .replaces-choice),
form:has(select[name="weapon_type"] option[value=""]:checked)
    :is(.replaces-choice, .attack-trait-label, .attack-trait-field) {
    display: none;
}
/* A checkbox and its words on one line, the form's grid laying out the pair rather than each. */
.magical-choice, .replaces-choice, .checkbox-line {
    align-items: center;
    display: flex;
    gap: var(--space-2);
}
/* The cards in hand, in the shell's widget column: names alone, under the pools. */
.loadout-list { display: grid; gap: var(--space-1); list-style: none; margin: 0; padding: 0; }

/* The level-up screen: the options a milestone may buy, one radio each, with the fields that
   option needs under it. */
.milestone-picks { display: grid; gap: var(--space-3); }
.milestone-option { display: grid; gap: var(--space-2); }
.milestone-option > label { align-items: baseline; display: flex; gap: var(--space-2); }
.milestone-fields { display: grid; gap: var(--space-2); padding-left: var(--space-4); }
/* Only the chosen option's fields are shown, and only the one training that names an
   Experience asks for one. Hiding is the whole of what this does: no field here is `required`,
   and the write reads only the fields of the pick it was given, so a browser without `:has`
   shows every field and the form still works. */
.milestone-option:not(:has(input[name="pick"]:checked)) .milestone-fields,
.milestone-training:not(:has(option[value="intelligent"]:checked))
    .milestone-training-experience {
    display: none;
}
/* Each completed milestone as the sheet lists it: what it bought, in one line. The list is
   ordered, so the browser draws the numbers; the marker is muted to sit behind the line the
   way the counts beside a tracker's name do. */
.milestone-list { display: grid; gap: var(--space-1); margin: 0; padding-left: var(--space-5); }
.milestone-list li::marker { color: var(--color-text-muted); font-size: var(--text-sm); }


.visibility-badge {
    align-self: start;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    padding: 0 var(--space-2);
}
.visibility-badge.shared { color: var(--color-success); }
.visibility-badge.private { color: var(--color-text-muted); }
.action-row form { display: inline; }

.post-stream { display: grid; gap: var(--space-4); }
.post {
    background: var(--color-panel);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    display: grid;
    gap: var(--space-2);
    padding: var(--space-4);
}
.post-header { align-items: center; display: flex; gap: var(--space-3); }
.post-portrait { border-radius: var(--radius-sm); flex: 0 0 44px; height: 44px; object-fit: cover; width: 44px; }
.post-byline { align-items: baseline; display: flex; flex-wrap: wrap; gap: var(--space-2); }
/* Sized close to body text so it fits the byline, and bold so the actor's name is still
   the part that stands out in it. */
.post-byline .sheet-link {
    font-family: var(--font-display);
    font-size: var(--text-base);
    font-weight: 700;
}
/* .post-source names where a post in a merged stream was written, and reads as a byline
   detail rather than as the post's own heading. */
.post-time, .post-author, .post-edited, .post-source { color: var(--color-text-muted); font-size: var(--text-sm); }
.post-body { margin: 0; }
/* Wide enough for the composer this also carries: a post runs to 4000 characters, so a
   narrower textarea would cost more in writing comfort than the width saves. Content
   taller than the viewport scrolls inside the dialog rather than out of it. */
.sheet-popover {
    background: var(--color-panel-raised);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    color: var(--color-text);
    max-height: 85vh;
    max-width: 54rem;
    overflow: auto;
    padding: var(--space-4);
    width: 92vw;
}
/* A sheet fragment is one panel that spaces its own children; a post form is several
   sibling elements that do not, and paragraphs have no margin of their own here. */
.popover-body { display: grid; gap: var(--space-3); }
.sheet-popover::backdrop { background: var(--color-backdrop); }
.post-controls { display: flex; gap: var(--space-3); font-size: var(--text-sm); }
/* The actor picker shows a single name, so it is sized to its content rather than to the
   form column: `justify-self` is what stops a grid item from stretching. */
.composer select { justify-self: start; max-width: min(100%, 22rem); }
/* The remaining-characters hint. post-stream.js unhides it for the last stretch of the
   limit and adds `danger` closer to it; the muted colour comes from the class the server
   renders alongside, and the two-class selector is what lets `danger` win over it. */
.composer-count, .composer-hint { font-size: var(--text-sm); margin: 0; }
.composer-hint { overflow-wrap: anywhere; }
.composer-count.danger { color: var(--color-danger); }
/* The button that opens the fuller roll examples and the chip a roll's label is set in look
   the same: the type of the text around them, a thin outline and no fill, so neither reads as
   a badge. `popovertarget` is the whole mechanism in both, so each works with scripting off
   and takes focus like any other button; the chip is a label for a hidden button (see
   `roll_card`). Only how each sits in its line differs, which is what the rules below carry. */
.hint-more, .roll-chip {
    background: none;
    border: 1px solid var(--color-border);
    color: inherit;
    font: inherit;
    padding: 0 var(--space-2);
}
.hint-more:hover, .roll-chip:hover, .roll-chip:has(+ .roll-chip-button:focus-visible) {
    background: none;
    border-color: var(--color-accent);
    color: var(--color-accent-strong);
}
/* The button a chip labels is visually hidden and cannot show a focus ring, so the chip shows
   the one `:focus-visible` gives a button. */
.roll-chip:has(+ .roll-chip-button:focus-visible) {
    outline: 3px solid var(--color-accent-strong);
    outline-offset: 2px;
}
/* The hint's button follows the sentence it was written after. */
.hint-more { margin-inline-start: var(--space-2); }
/* The examples themselves. The card takes the same box as a roll's breakdown (see
   `.roll-card` below) but no anchor name, so it opens centred in the top layer; Esc and a
   click outside close it. Each example is the markup to type with a line on what it shows. */
.hint-examples { display: grid; gap: var(--space-3); list-style: none; margin: 0; padding: 0; }
.hint-examples li { display: grid; font-size: var(--text-sm); gap: var(--space-1); }
.hint-examples code { color: var(--color-text); }
.hint-examples code, .guide-example code { overflow-wrap: anywhere; }
.hint-examples span { color: var(--color-text-muted); }
.stream-pages, .stream-ends {
    align-items: baseline;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
}
.stream-ends { justify-content: space-between; }
/* Quieter than a page link: the jump moves the reader inside the page they are already on,
   and the export hands them the same stream as a file. `margin-left: auto` keeps the pair
   together at the end of the row, with the row's own gap between them. */
.stream-jump, .stream-export {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    text-decoration: none;
}
.stream-export { margin-left: auto; }
/* The switcher and the scene's own controls sit above the stream, each sized by its
   content rather than stretched across the column, so a one-field form takes a single row
   rather than stacking a full-width input and button the way the world and character forms
   do. */
.scene-switcher { align-items: baseline; display: flex; flex-wrap: wrap; gap: var(--space-3); }
.scene-switcher select { max-width: min(100%, 26rem); }
.scene-form summary, .scene-state { cursor: pointer; width: fit-content; }
.scene-form form { display: grid; gap: var(--space-2); margin-top: var(--space-3); }
/* The two scene forms sit side by side. `align-items: start` keeps the collapsed one at
   the top of the row while the other is expanded, instead of stretching it. */
.scene-actions { align-items: start; display: flex; flex-wrap: wrap; gap: var(--space-3); }
.cast-list { display: grid; gap: var(--space-2); list-style: none; margin: 0; padding: 0; }
.cast-list li { align-items: center; display: flex; gap: var(--space-2); }
.cast-portrait { border-radius: var(--radius-sm); flex: 0 0 32px; height: 32px; object-fit: cover; width: 32px; }
/* A roll stands inside the prose it was written in, so the chip is inline; the body around
   it is `pre-wrap`, which the chip and the card both opt out of, so a label's own line
   breaks and space runs render the same in either. Each tier sets one tint, which the
   card's verdict and the panel's line read. */
.roll-critical { --roll-tint: var(--color-critical); }
.roll-success-hope { --roll-tint: var(--color-success); }
.roll-success-fear { --roll-tint: var(--color-accent); }
.roll-failure-hope { --roll-tint: var(--color-text-muted); }
.roll-failure-fear { --roll-tint: var(--color-danger); }
/* A reaction is judged outside the Hope and Fear economy, so its three tiers take the plain
   readings of the same tints: the critical keeps the one criticals are known by, and success
   and failure take the palette's green and red with nothing between them. */
.roll-reaction-critical { --roll-tint: var(--color-critical); }
.roll-reaction-success { --roll-tint: var(--color-success); }
.roll-reaction-failure { --roll-tint: var(--color-danger); }
/* A fate check answers a question rather than beating a Difficulty, so its four tiers take the
   plain yes and no readings of the same green and red. */
.fate-yes-and, .fate-yes { --roll-tint: var(--color-success); }
.fate-no, .fate-no-and { --roll-tint: var(--color-danger); }
/* The wrapper only groups a chip with its own card; it must not become a box of its own in
   the middle of the prose. */
.roll { display: inline; }
/* The chip is the roll's label set in the reader's prose, so a post reads as a post and
   everything the roll consisted of stays in the card behind it. It is an inline element, so
   a label longer than the room left on its line breaks across lines like the words around
   it, and `box-decoration-break: clone` gives each line's piece its own border and padding.
   Its padding is inline only (see the shared rule above), which keeps the outline off the
   text without growing the line box; the margin keeps two chips written back to back from
   painting one border against the other. */
.roll-chip {
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
    cursor: pointer;
    display: inline;
    margin-inline: var(--space-1);
    white-space: normal;
}
/* The card is a popover, so it is drawn in the top layer: however long the breakdown, it
   cannot stretch a post or push the stream around. Its laid-out state is the default here
   because a browser without the Popover API ignores both the `popover` attribute and the
   `popovertarget` on the chip: the button opens nothing, so the card is left standing in the
   flow under it as a breakdown the reader can still read. */
.roll-card, .hint-card, .effect-card, .item-image-card {
    background: var(--color-panel-raised);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-panel);
    color: var(--color-text);
    display: grid;
    gap: var(--space-2);
    max-height: 80vh;
    max-width: min(26rem, 92vw);
    overflow-y: auto;
    padding: var(--space-4);
    white-space: normal;
}
/* A closed popover must not lay itself out, so a card is hidden until it is opened. The rule
   states the closed state rather than the open one on purpose, and is the whole feature guard:
   a browser that does not know `:popover-open` cannot parse the selector, and an invalid
   selector makes the browser drop the rule it belongs to. Such a browser is exactly the one
   that ignores the `popover` attribute and the chip's `popovertarget` as well, so dropping
   this rule leaves the card laid out in the flow under the chip, which is the fallback. */
.roll-card:not(:popover-open), .hint-card:not(:popover-open),
.effect-card:not(:popover-open), .item-image-card:not(:popover-open) { display: none; }
.roll-card-name { font-family: var(--font-display); font-weight: 700; }
.roll-card-question { font-style: italic; }
.roll-card-tier { color: var(--roll-tint); font-weight: 700; }
/* A critical card holds the dice and this line and nothing else, so the verdict is set in
   capitals there. The casing is presentation only: the tier's own label stays as it is
   written, which is what the rolls panel and everything else read. The compound selector is
   what keeps the panel's one-line entry out of it. */
.roll-card-tier.roll-critical, .roll-card-tier.roll-reaction-critical,
.roll-card-tier.fate-yes-and, .roll-card-tier.fate-no-and { text-transform: uppercase; }
/* The word marking a reaction sits in the verdict's line but is not part of the verdict, so
   it is set back to the body weight and the muted colour while the tier keeps the tint. */
.roll-card-kind { color: var(--color-text-muted); font-weight: 400; }
.roll-breakdown { display: grid; font-size: var(--text-sm); gap: var(--space-1); list-style: none; margin: 0; padding: 0; }
.roll-breakdown li { display: flex; gap: var(--space-4); justify-content: space-between; }
/* A die's row carries its icon and its name. Only the icon is tinted: the words stay in the
   card's own text colour, so the die is named at full contrast whatever the tint does. */
.roll-die { align-items: center; display: inline-flex; gap: var(--space-2); }
.roll-die-hope, .roll-die-fear, .roll-die-fate, .roll-die-combo { display: inline-flex; }
/* .muted dims a row by its colour, and the combo die reads no currentColor, so a die below the
   hit is dimmed by opacity instead. */
.roll-breakdown li.muted .roll-die-combo { opacity: .45; }
/* The same silhouette beside a trait the character's subclass casts with. The icon is
   decorative and the visually-hidden text beside it is what names it, so the colour carries
   nothing on its own. */
.spellcast-die { display: inline-flex; flex-shrink: 0; }
.roll-breakdown .roll-total { border-top: 1px solid var(--color-border); font-weight: 700; padding-top: var(--space-1); }
/* Where anchor positioning exists the card opens against the chip that opened it. Every
   chip carries the same anchor name, so the wrapper confines each name to its own roll —
   without that scope the name resolves across the whole stream and a card lands beside
   somebody else's chip. Both properties are required here because a browser with the name
   but not the scope would do exactly that; without either the card stays centred in the
   viewport, which the top layer makes safe. */
@supports (anchor-name: --roll-chip) and (anchor-scope: --roll-chip) {
    .roll { anchor-scope: --roll-chip; }
    .roll-chip { anchor-name: --roll-chip; }
    .roll-card {
        margin: var(--space-1) 0 0 0;
        position-anchor: --roll-chip;
        position-area: bottom span-right;
        position-try-fallbacks: flip-block, flip-inline;
    }
}

/* One line per roll, in the narrow widget column: the actor, what was rolled, and how it
   landed, each on its own row so a long name or label wraps instead of squeezing the tier. */
.roll-log { display: grid; gap: var(--space-3); list-style: none; margin: 0; padding: 0; }
.roll-log li { display: grid; gap: 0; }
.roll-log-name { font-family: var(--font-display); }
.roll-log-tier { color: var(--roll-tint); font-size: var(--text-sm); }

.scene-header { display: grid; gap: var(--space-1); }
.scene-header p { align-items: baseline; display: flex; gap: var(--space-2); }
/* The group playing the scene and the scene's kind, marked like the closed badge beside them
   so the header's three pieces of standing information read as one row. */
.scene-table, .scene-kind, .scene-closed {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    padding: 0 var(--space-2);
}

@media (max-width: 62rem) {
    .world-shell { grid-template-columns: minmax(12rem, var(--shell-nav-width)) minmax(0, 1fr); }
    .shell-widgets { grid-column: 1 / -1; grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 44rem) {
    .world-shell { grid-template-columns: 1fr; padding: var(--space-4); }
    .shell-widgets { grid-column: auto; grid-template-columns: 1fr; }
    .landing, .world-picker, .form-page { margin: var(--space-8) auto; }
}
