/* TableSync — player app chrome.
 *
 * Adam Analytics Brand Standard v4.0 "Graphite" wearing the Ledger register
 * (RM41 task 7, requirements 1.5-1.11, 3.2-3.6, 4.5). Companion to
 * css/tokens.css, which player.html links first. Every value below resolves to
 * a token defined there: no colour literal, no non-zero radius, no shadow, no
 * blur, no gradient, no glow. Depth is value, hairline and space (brand 6.2).
 * The sheet ladder is --aa-ground (the pane field) > --aa-sheet (panels,
 * bottom sheets, chrome bands) > --aa-recess (wells, inputs, sunken rows).
 *
 * WHY THE REGISTER DOES MORE WORK HERE THAN ANYWHERE ELSE
 * This surface carries the most PROSE in TableSync — spell text, item
 * descriptions, class features, feat text — so Spectral (--aa-read) is not
 * decoration on the player app, it is the reading face doing its job. It sets
 * proper names (character, spell, item, feat, creature), section titles, and
 * every description block. IBM Plex Sans (--aa-operate) operates: buttons,
 * tabs, field labels, interface copy. IBM Plex Mono (--aa-measure) measures:
 * HP, AC, spell DC, ability modifiers, slot counts, coin, dice, quantities —
 * always with tabular figures so a column of them compares by eye (brand 4.3).
 *
 * THE ACCENT, SPENT ONCE (req 5.1, 1.7)
 * The cockpit spends its accent on the present turn. The player app's
 * equivalent of "the present" is THE RECORD YOU HAVE OPEN: the spell, item,
 * feature or pick whose <details> is expanded. That entry takes a marginal
 * pilcrow in --aa-accent and its name in the accent, and its row takes
 * --aa-accent-wash. Nothing else on this surface is allowed the accent — not
 * the tab bar, not a button, not a border, not "prepared", not "proficient".
 *
 * Which is why there is no danger colour here either. Marks that used to be
 * carried by hue alone are now carried by ink weight plus a drawn mark:
 *   prepared / equipped / proficient  a filled square rule mark, ink weight
 *   over the limit, unusable, fumble  --aa-warn plus a drawn glyph
 *   struck out (dead, spent, off-list) a line-through, not a tint
 * That is requirement 5.4's "never by colour alone" and brand 3.4's own
 * instruction for a view whose accent is already committed.
 *
 * THE RULED ROSTER (req 1.9)
 * Every list of records on this surface — spells, feats, inventory, skills,
 * attacks, pick lists, characters — is one shape: a ruled row inside a panel.
 * Read-face name on the left, mono figure right-aligned, hairline between
 * entries, no pill, no radius, no nested bordered box.
 *
 * Functional text floors at --t-ui (14px) and controls clear 44px, which is
 * the phone target rather than the 32px desktop minimum (req 2.7). Nothing on
 * this surface is set below --t-meta (12px).
 *
 * Zero selector-contract changes: every id and class that js/player/*.js
 * queries, toggles or reads is still here, still doing the same job. Nothing
 * was renamed or removed — only re-painted.
 */

:root {
  /* the running head: a banner strip, the band gap, then the character row */
  --topbar-banner-h: 24px;      /* the lockup strip: TableSync + surface tag */
  --topbar-rule-h: 6px;         /* the brand band's gap (6.3), made of borders */
  --topbar-content-h: 68px;     /* character/vitals row + RM59's 12px foot */
  --topbar-h: calc(var(--topbar-banner-h) + var(--topbar-rule-h) + var(--topbar-content-h));
  /* RM66: what the panes actually clear. --topbar-h is the DESIGN minimum; the
     masthead can be taller when the identity plates flow to a second row, so
     js/player/main.js measures the real header and writes this. Defaulting it
     to --topbar-h means the page is correct before any script runs and stays
     correct if the script never does. Separate variable on purpose - feeding a
     measurement back into the property that sizes the thing measured is how a
     layout starts oscillating. */
  --topbar-real-h: calc(var(--topbar-h) + var(--safe-t));   /* RM343: the header includes the status bar */
  --tabbar-h: 58px;
  /* RM74: where a record's sub-content lines up.
     A row's name sits after the 14px pilcrow gutter PLUS the summary's --s-2
     gap, so anything below it - a feature's prose, a spell's dateline, a pack
     note - has to clear both or it hangs 8px to the left of the name it
     belongs to. One token, because four rules use it and four hand-copied
     numbers is four chances to drift. */
  --rec-indent: calc(14px + var(--s-2));
  /* RM68: every masthead plate is exactly this. Set by the longest caption
     (SPELL ATK at --t-meta with .06em tracking) and the tallest content
     (caption over a --t-4 figure), measured rather than guessed. */
  /* RM68: the widest content decides it. HP's "18/18" measures 63px of figure
     at --t-4; the plate spends 18 on padding and rule (--s-2 each side plus
     two 1px borders), so 81 is the floor and 84 leaves a little margin.
     Measured, not guessed - 73 and 78 were both tried and both clipped HP. */
  --plate-w: 84px;
  --plate-h: 46px;
  --safe-b: env(safe-area-inset-bottom, 0px);
  /* RM343: the status bar. player.html asks iOS for viewport-fit=cover and a
     black-translucent bar, so the page runs under the clock; the header stood at
     top 0 and the switch button and the name sat under it. Android's MainActivity
     pads the WebView by the system bars and consumes the insets, so this is 0
     there and nothing is padded twice. */
  --safe-t: env(safe-area-inset-top, 0px);

  /* js/player/character.js and js/player/spells.js write `var(--danger)` into
   * inline styles for over-the-limit and illegal-score notes. Those files
   * belong to another task, so the NAME stays and is re-pointed at the brand
   * token that now carries that job: there is no danger colour under Graphite
   * (the accent is committed to the open record), so a refusal reads as warn
   * plus the drawn glyph its rule already carries. */
  --danger: var(--aa-warn);
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
/* The type steps are rem values, so the ROOT font-size must stay at the
 * browser default. Setting font-size on <html> re-bases rem and silently
 * shrinks every step by 12.5% — measured on this file during RM41 task 7,
 * where a tab label declared at --t-ui resolved to 12.25px and --t-meta to
 * 10.5px, under the 12px floor. The interface size therefore lands on <body>,
 * never on <html>. */
html, body {
  margin: 0; height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}
body {
  background: var(--aa-ground); color: var(--aa-ink-body);
  font-family: var(--aa-operate);
  font-size: var(--t-ui);
  line-height: 1.5;
}

/* ---------------- controls (brand 6.4) ----------------
 * Secondary is the default: no fill, 1px rule, ink label. Hover moves the
 * border to ink rather than lifting the surface. 44px is the phone target. */
button {
  background: none; color: var(--aa-ink);
  border: 1px solid var(--aa-rule); border-radius: 0;
  padding: var(--s-2) var(--s-4);
  font-family: var(--aa-operate); font-size: var(--t-ui); font-weight: 500;
  line-height: 1.2; cursor: pointer;
  min-height: 44px;
  transition: background var(--aa-dur-state) var(--aa-ease),
              border-color var(--aa-dur-state) var(--aa-ease),
              color var(--aa-dur-state) var(--aa-ease);
}
button:hover { border-color: var(--aa-ink); }
button:active { background: var(--aa-recess); }
button:disabled { opacity: .45; cursor: not-allowed; }
button:disabled:hover { border-color: var(--aa-rule); }

/* primary / toggled-on: the ink plate from brand 6.4, reused wherever a
 * control is already ".active" so no JS learns a new class name. The RM23
 * "flood spot" is retired — emphasis is ink, because the accent is spent on
 * the open record. */
button.active,
.pk2-take.active,
button.wiz-next {
  background: var(--aa-ink); border-color: var(--aa-ink); color: var(--aa-sheet);
  font-weight: 500;
}
button.active:hover, .pk2-take.active:hover, button.wiz-next:hover {
  background: var(--aa-ink); border-color: var(--aa-ink);
}
button.active:active, .pk2-take.active:active, button.wiz-next:active {
  background: var(--aa-ink-body); border-color: var(--aa-ink-body); color: var(--aa-sheet);
}

/* destructive: warn label and a drawn strike glyph on a rule box, never a
 * bare fill and never the accent (req 5.1, 5.4). */
button.danger, .pk2-take.warn {
  border-color: var(--aa-rule); color: var(--aa-warn);
}
button.danger:hover, .pk2-take.warn:hover { border-color: var(--aa-warn); }
button.danger::before, .pk2-take.warn::before {
  content: ''; display: inline-block; width: 9px; height: 2px;
  background: currentColor; margin-right: var(--s-2); vertical-align: 2px;
}
/* RM781 (PP-20): a label that carries its own minus ("− damage") reads "– − damage" with the dash */
button.danger.no-dash::before { content: none; }

button.small { min-height: 44px; padding: var(--s-1) var(--s-3); font-size: var(--t-ui); }

/* ---------------- inputs ---------------- */
input[type=text], input[type=number], input[type=search], select, textarea {
  background: var(--aa-recess); color: var(--aa-ink);
  border: 1px solid var(--aa-hairline); border-radius: 0;
  padding: var(--s-2) var(--s-3);
  font-family: var(--aa-operate); font-size: var(--t-ui);
  min-height: 44px;
  -webkit-appearance: none; appearance: none;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--aa-ink); }
/* <option> does not inherit the select's face in Chrome and falls back to a UA
 * face at a UA size, so it is named explicitly. */
option { font-family: var(--aa-operate); font-size: var(--t-ui); }
input[type=number] { width: 78px; font-family: var(--aa-measure); font-variant-numeric: tabular-nums; }
input::placeholder, textarea::placeholder { color: var(--aa-ink-muted); opacity: 1; }
input[type=checkbox], input[type=radio] { accent-color: var(--aa-ink); width: 20px; height: 20px; }
/* the real tap target of a checkbox is the <label> that wraps it, which
 * toggles it on click; min-height puts that target over the phone floor. */
label { display: inline-flex; align-items: center; gap: var(--s-2); min-height: 44px; }
/* marginalia — the aside a player reads past. Set in the read face so it
 * reads as page furniture beside the sans interface (req 1.5). Roman, not
 * italic: brand 4.3 permits italic for emphasis inside prose, never for a
 * whole paragraph, and a hint is a paragraph. */
.hint {
  font-family: var(--aa-read); font-size: var(--t-ui);
  color: var(--aa-ink-muted); line-height: 1.55;
  max-width: 34rem;
}
/* RM783 (SE-02): why the chooser opened by itself ("Emily was moved to another device by the DM") - read, not passed */
.hint.picker-note { color: var(--aa-warn); border-left: 2px solid var(--aa-warn); padding-left: var(--s-2); }
/* RM461: what the Hit Dice spent this rest rolled. It is a .hint by construction - same read face,
   same place in the card's hierarchy - but a hint is deliberately muted marginalia and this is a
   NUMBER, read off a phone that is often lying under the projector. So it keeps the face and prints
   in the ink colour at measure width, wraps at phone width instead of running past the card, and
   holds the running total apart from the list of dice. */
.hd-rolled {
  color: var(--aa-ink-body); max-width: none;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--s-2);
  margin-top: var(--s-2);
}
.hd-rolled .hd-what { color: var(--aa-ink-muted); }
.hd-rolled .hd-roll { font-family: var(--aa-measure); font-variant-numeric: tabular-nums; }
.hd-rolled .hd-healed {
  font-family: var(--aa-measure); font-variant-numeric: tabular-nums;
  font-weight: 500; color: var(--aa-ink); margin-left: auto;
}
.hidden { display: none !important; }

/* ---------------- the running head + the thumb index ----------------
 * RM23 faked a "module double-rule" with a linear gradient painted into the
 * background (slop
 * V1). The real device is brand 6.3's BAND: two 1px rules held apart by a
 * 6px gap, made of borders — here the banner strip's bottom rule and the
 * character row's own bottom rule, with --topbar-rule-h between them. */
#topbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 40;
  /* RM66: a MINIMUM, not a fixed height - the identity plates may take a
     second row and the masthead grows to hold them. */
  min-height: calc(var(--topbar-h) + var(--safe-t));       /* RM343 */
  display: flex; align-items: center; gap: var(--s-3);
  flex-wrap: wrap; row-gap: var(--s-1);
  /* RM59: the plates were sitting ON the bottom rule. --s-3 (12px) is the
     nearest step on the spacing scale to the 10pt asked for (13.3px); an
     off-scale 13.3px would be the only such value in the sheet, and 1.3px is
     not worth that. The content height grows by the same amount so nothing is
     squeezed to pay for it. */
  padding: calc(var(--safe-t) + var(--topbar-banner-h) + var(--topbar-rule-h)) var(--s-3) var(--s-3);   /* RM343 */
  background: var(--aa-sheet);
  border-bottom: 1px solid var(--aa-rule);
}
/* The product wordmark, set in the read face. Brand 2.2 governs the Adam
 * Analytics wordmark specifically; TableSync is its own product name. Drawn
 * as generated content so no markup changes — the banner is page furniture,
 * and this app's actual masthead (character name, live vitals) lives in the
 * row below it. */
#topbar::before {
  content: 'TableSync';
  position: absolute; left: var(--s-3); right: var(--s-3);
  top: var(--safe-t); height: var(--topbar-banner-h);   /* RM343: below the status bar */
  display: flex; align-items: center;
  border-bottom: 1px solid var(--aa-hairline);
  font-family: var(--aa-read); font-weight: 600; font-size: var(--t-ui);
  letter-spacing: .01em; color: var(--aa-ink); pointer-events: none;
}
/* the surface tag: an uppercase label in the measure face at --t-meta, which
 * is the only place brand 4.3 permits uppercase. */
#topbar::after {
  content: 'PLAYER';
  position: absolute; right: var(--s-3); top: var(--safe-t); height: var(--topbar-banner-h);   /* RM343 */
  display: flex; align-items: center;
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .12em; color: var(--aa-ink-muted); pointer-events: none;
}
#charSummary { display: flex; align-items: center; gap: var(--s-2); min-width: 0; flex: 1 1 auto; }
/* A masthead that has been squeezed to "E…" is not a masthead. Measured at
 * 390x844 with a caster loaded: four vitals plates at the 12px caption step
 * take 271px of a 390px row and leave the name 19px for a 45px word. The
 * running head therefore breaks to two lines on a phone — the name over the
 * figures, which is how a page carries a head and a folio anyway — and
 * --topbar-content-h grows to match so the JS that reads --topbar-h
 * (js/player/main.js positions the table-change notice against it) stays
 * correct with no script change. */
@media (max-width: 520px) {
  /* RM56 req 1.1. 96px was measured against RM55's smaller name and no menu
     button. The name is a step larger now and carries a caret and a baseline
     rule, so the head grew and the DC/ATK/AC/HP plates were pushed out of the
     box. Re-measured rather than nudged: name 21px x 1.2 + 1px rule + the 12px
     dateline, over a 34px plate row, inside the banner and rule. */
  :root { --topbar-content-h: 124px; }   /* RM56's 112 + RM59's 12px foot */
  #topbar { flex-wrap: wrap; align-content: center; row-gap: var(--s-1); }
  #charSummary { flex: 1 1 100%; min-height: 48px; }
}
#charSwitch {
  min-height: 44px; min-width: 44px; flex: 0 0 auto;
  font-size: var(--t-4); padding: 0 var(--s-2);
}
/* RM66: the head holds the NAME and nothing else now, so it centres against
   the portrait button instead of sitting above a second line of metadata. */
#charHead { min-width: 0; display: flex; align-items: center; }
/* RM130: the portrait, now inside the switch button in place of the silhouette. Square and hard-edged like .portrait in
   the build sheet (brand: radius 0, the rule colour, no shadow); sized to the
   name's line so the masthead does not grow and --topbar-real-h holds. */
#charPic {
  width: 44px; height: 44px; flex: 0 0 auto;
  border-radius: 0; object-fit: cover; display: block;
}
#charPic[hidden], #charSwitchIcon[hidden] { display: none; }
/* The picture fills the switch button edge to edge; the button's own border
   stays as the frame. */
#charSwitch.has-pic { padding: 0; overflow: hidden; }
#charSwitchIcon { display: inline-flex; }


/* a character's name is a proper name: read face (req 1.6) */
#charName {
  /* RM56 req 1.2: up a step from --t-body to --t-4. It is the masthead and it is
     now also the way into the character's menu, so it has to out-weigh the
     figures beside it rather than sit level with them. */
  font-family: var(--aa-read); font-weight: 500; font-size: var(--t-4);
  color: var(--aa-ink); line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* RM68: ONE row of plates with a gap of its own.
   The two old containers (DC/ATK and AC/HP) wrapped as units, so five fixed
   plates could never share a 390px row. One container also means the gap
   between plates is set here - --s-1, not the header's --s-3 - which is what
   makes five 68px boxes fit: 5*68 + 4*4 = 356 inside 366. */
#plates {
  /* Wide: ONE row of fixed columns. `auto-fill` + `1fr` was tried first and
     collapsed - a grid with only flexible tracks has no intrinsic width, so as
     a flex item it shrank to one column and stacked all five plates vertically
     at 1100px. `grid-auto-flow: column` gives the row a real width to be. */
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--plate-w);
  gap: var(--s-1);
  flex: 0 0 auto;
}
/* Narrow phones take three columns and two rows. Five plates cannot share a
   390px row without clipping: HP's "18/18" needs 63px of figure at --t-4, so a
   plate needs 73px, and 5*73 + 4*4 is 381 into 366. Three columns keeps every
   box identical AND balanced (3 + 2) rather than stranding one plate alone on
   a second row, and a non-caster's three plates then make exactly one row.
   AFTER the base rule on purpose - the media block higher up in this file
   loses to it on source order, which is how the first attempt at this did
   nothing at all. */
@media (max-width: 520px) {
  #charSummary { flex: 1 1 100%; }
  #plates { flex: 1 1 100%; grid-auto-flow: row; grid-template-columns: repeat(3, 1fr); }
}
/* RM689 (PL-22): a non-caster has four plates - one row of four (4 x 73px + 3 gaps fits 366px), never 3 + 1 */
@media (max-width: 520px) {
  #plates.plates-4 { grid-template-columns: repeat(4, 1fr); }
}
/* the vitals plates: a caption in the measure face over a mono figure, on a
 * recessed well with a hairline. No fill, no radius, no tint. */
/* RM68: FIVE IDENTICAL BOXES.
 *
 * These were content-sized, so SPELL ATK was wide, AC was narrow, and the row
 * read as a ragged set of unrelated chips rather than one instrument panel.
 * They are one size now - a fixed width that holds the longest label, and a
 * fixed height so a two-line label could never make one box taller than its
 * neighbours.
 *
 * The width is set by SPELL ATK, the longest caption, at --t-meta with .06em
 * tracking; the figures below are tabular so 18/18 and 8/8 occupy the same
 * space. Anything longer than the box is the value's problem to shrink, not
 * the row's problem to reflow - see .vt-val below. */
.cs, .vt {
  background: var(--aa-recess); border: 1px solid var(--aa-hairline); border-radius: 0;
  padding: var(--s-1) var(--s-2); text-align: center; line-height: 1.15;
  /* the grid sets the width, identically for every cell; the plate fixes only
     its height so a wrapped caption can never make one taller than its
     neighbours. */
  box-sizing: border-box;
  min-width: 0;
  height: var(--plate-h);
  display: flex; flex-direction: column; justify-content: center;
  overflow: hidden;
}
.cs-lab, .vt-lab {
  display: block;
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .06em; color: var(--aa-ink-muted);
}
.cs-val, .vt-val {
  display: block;
  font-family: var(--aa-measure); font-size: var(--t-4); font-weight: 500;
  color: var(--aa-ink); font-variant-numeric: tabular-nums;
  /* a fixed box needs a value that cannot burst it: 18/18 fits, and a
     hypothetical 188/188 clips rather than widening the plate and breaking
     the row's rhythm. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* RM66: the roll control, as a plate in the vitals row. It is a button, so it
   takes the interactive treatment the other plates do not: ink border on
   hover, and the ink plate while its drawer is open. */
.vt-act {
  /* RM68: no size of its own. It is one of the five plates and takes the
     grid's width and .vt's height; only the interactive treatment is its own.

     NO `font:` shorthand. It was `font: inherit` and that reset line-height to
     the inherited value - 18px on the caption and 31.5px on the figure against
     the 13.8/24.15 every other plate gets - so the button's stack came to
     49.5px inside a 46px box and "d20" was cropped. #charName carries a
     comment about this exact shorthand for the same reason. The spans set
     their own family and size; the plate sets the line-height. */
  min-height: 0; cursor: pointer;
  font-family: inherit; font-size: inherit; font-weight: inherit;
}
.vt-act:hover { border-color: var(--aa-ink); background: var(--aa-recess); }
.vt-act .vt-val { font-family: var(--aa-measure); }
.vt-act[aria-expanded="true"] {
  background: var(--aa-ink); border-color: var(--aa-ink);
}
.vt-act[aria-expanded="true"] .vt-lab, .vt-act[aria-expanded="true"] .vt-val {
  color: var(--aa-sheet);
}
#hdrHPWrap { cursor: pointer; border-radius: 0; }
/* HP state: colour is never the only signal — hurt takes the warn border and
 * a weight step, down inverts to a warn plate and strikes the figure. */
#hdrHPWrap.hurt { border-color: var(--aa-warn); }
#hdrHPWrap.hurt .vt-val { color: var(--aa-warn); font-weight: 500; }
#hdrHPWrap.down { background: var(--aa-warn); border-color: var(--aa-warn); }
#hdrHPWrap.down .vt-lab, #hdrHPWrap.down .vt-val { color: var(--aa-recess); }
/* RM781 (PP-14): struck through only when dead - a strike on a dying character read as dead */
#hdrHPWrap.dead .vt-val { text-decoration: line-through; }
/* RM707 (NEW-16): a speed the load cut - the plate in the warn ink (as a hurt HP plate), and the reason in words on
 * the Character tab and the Combat tab's Move group */
#hdrSpeedPlate.slowed { border-color: var(--aa-warn); }
#hdrSpeedPlate.slowed .vt-val { color: var(--aa-warn); font-weight: 500; }
.speed-note { margin: var(--s-1) 0 0; color: var(--aa-warn); }

/* THE THUMB INDEX (req 1.9 applied to navigation).
 * A ledger is tabbed at its edge and each tab carries a short section name.
 * Seven destinations will not hold seven 14px names across a 390px phone, and
 * the 14px floor is not negotiable (trap 2 in the handoff), so the index shows
 * a drawn mark for every section and prints the name of the section you are
 * IN. The active tab widens to hold it. RM23's sliding spot plate is retired:
 * the accent is spent on the open record, and the active tab takes ink and a
 * rule instead — the same answer css/dm.css gives its nav. */
#tabs {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  height: calc(var(--tabbar-h) + var(--safe-b));
  padding-bottom: var(--safe-b);
  display: flex;
  background: var(--aa-sheet);
  border-top: 1px solid var(--aa-rule);
}
#tabs .tab {
  position: relative;
  flex: 1 1 0; min-width: 0; min-height: 0;
  display: flex; align-items: center; justify-content: center; gap: var(--s-2);
  background: none; border: none; border-radius: 0;
  border-top: 2px solid transparent;
  color: var(--aa-ink-muted);
  font-family: var(--aa-operate); font-size: var(--t-ui); font-weight: 400;
  padding: 0 8px;
  transition: color var(--aa-dur-state) var(--aa-ease),
              border-color var(--aa-dur-state) var(--aa-ease);
}
#tabs .tab .ti { font-size: var(--t-4); line-height: 1; flex: 0 0 auto; display: flex; }
/* a section name is a proper name: read face (req 1.6) */
#tabs .tab .tl {
  display: none;
  font-family: var(--aa-read); letter-spacing: .01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#tabs .tab:hover { color: var(--aa-ink); }
#tabs .tab.active {
  flex: 2.6 1 0;
  color: var(--aa-ink); border-top-color: var(--aa-rule);
  background: var(--aa-recess);
}
#tabs .tab.active .tl { display: block; }

#panes {
  position: fixed; inset: var(--topbar-real-h) 0 calc(var(--tabbar-h) + var(--safe-b)) 0;
  background: var(--aa-ground);
}
/* `padding: 0 16px` is the page gutter: body text on this surface never
 * touches the edge of the viewport (slop G4), which on a phone is also where
 * the palm rests. The vertical padding is set separately so the gutter reads
 * as one declared value. */
.pane {
  display: none; position: absolute; inset: 0;
  overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch;
  padding: 0 16px;
  /* RM69: --s-5, not --s-3.
     The masthead ends in a rule and the first card begins with a border, so at
     12px the page opened with two horizontal lines 12px apart directly under a
     dense band of plates - the content read as stuck to the header rather than
     placed below it. 24px is the next step on the scale that separates them,
     and it is the same step the cards use between each other's sections, so
     the top of the page now breathes like the rest of it. */
  padding-top: var(--s-5);
  padding-bottom: var(--s-10);
}
.pane.active { display: block; }
.pane-fill { padding: 0; overflow: hidden; }
.pane input, .pane select, .pane textarea { max-width: 100%; }

/* wide screens: the index moves to the top of the page, where there is room
 * to print every section name, and the column takes a measure. */
@media (min-width: 768px) {
  :root { --tabbar-h: 0px; }
  #tabs {
    /* RM69: the MEASURED height, matching #panes. This was --topbar-h, the
       design minimum, so the tab row and the pane below it agreed only while
       the masthead happened to be exactly that tall - a taller header would
       have slid under the tabs on desktop while the pane still cleared it.
       They track the same number now. */
    top: var(--topbar-real-h); bottom: auto; height: 46px; padding-bottom: 0;
    border-top: none; border-bottom: 1px solid var(--aa-rule);
  }
  #tabs .tab { border-top: none; border-bottom: 2px solid transparent; }
  #tabs .tab.active { flex: 1 1 0; background: none; border-bottom-color: var(--aa-rule); }
  #tabs .tab .tl { display: block; }
  #panes { inset: calc(var(--topbar-real-h) + 46px) 0 0 0; }   /* RM343: the measured header, as #tabs above */
  .pane {
    padding-left: max(var(--s-5), calc(50vw - 430px));
    padding-right: max(var(--s-5), calc(50vw - 430px));
    padding-top: var(--s-5);
  }
  .pane-fill { padding: 0; }
}

/* very narrow phones: let the vitals plates tighten instead of pushing wide.
 * Type does NOT shrink here — 12px is the floor everywhere (req 3.6). */
@media (max-width: 420px) {
  .cs, .vt { padding: var(--s-1); }
  #tabs .tab { padding: 0 var(--s-1); gap: var(--s-1); }
  #tabs .tab.active { flex: 3 1 0; }
}
/* RM689 (PL-56, PLAN decision 6): a word under EVERY tab icon on a phone held upright - a first-time player could not
   tell eight bare icons apart. It reverses RM41's "name only the section you are in" for portrait: the short label
   (.ts) sits at the 12px floor (--t-meta) in the operate face, every tab is the same width, and the active tab keeps
   its ink, its rule and the recess ground. The tab bar keeps --tabbar-h (58px holds a 21px icon, 2px and a 15px
   label). From 768px the bar is exactly as before: full names beside the icons, no short labels. */
#tabs .tab { flex-direction: column; gap: 2px; padding: 0 2px; }
#tabs .tab.active { flex: 1 1 0; }
#tabs .tab .tl, #tabs .tab.active .tl { display: none; }
#tabs .tab .ts {
  display: block; font-family: var(--aa-operate); font-size: var(--t-meta); font-weight: 500;
  line-height: 1.2; letter-spacing: 0; white-space: nowrap;
}
@media (min-width: 768px) {
  #tabs .tab { flex-direction: row; gap: var(--s-2); padding: 0 8px; }
  #tabs .tab .tl, #tabs .tab.active .tl { display: block; }
  #tabs .tab .ts { display: none; }
}

/* ---------------- panels ----------------
 * A flat sheet, one hairline, no radius, no corner ticks, no halftone. RM23
 * drew "drafting ticks" from four stacked linear gradients per corner; brand
 * 6.2 has no gradients and 7.2 has no ornament, so the panel is just a sheet.
 * No nested cards: everything that lists multiple records inside a panel is a
 * ruled row, never its own bordered box. */
.card,
#sheetHost .sheet,
.wiz-preview, .wiz-review, .lu-spells, .spell-slot-block, .granted-box {
  position: relative;
  background: var(--aa-sheet); border: 1px solid var(--aa-hairline); border-radius: 0;
  padding: var(--s-4); margin-bottom: var(--s-3);
}
/* section titles: read face, sentence case, no eyebrow, no tick, no number
 * (brand 4.4; slop T1 and L1). */
.card h3, .sheet-body h3 {
  margin: var(--s-5) 0 var(--s-2);
  font-family: var(--aa-read); font-weight: 500; font-size: var(--t-4);
  letter-spacing: -.005em; color: var(--aa-ink);
  display: flex; align-items: baseline; gap: var(--s-2);
}
.card h3 { margin-top: 0; }
.sheet-body > h3:first-child { margin-top: 0; }
.row { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; margin-bottom: var(--s-2); }
.row:last-child { margin-bottom: 0; }
.grow { flex: 1 1 auto; min-width: 0; }
.sep { border: none; border-top: 1px solid var(--aa-hairline); margin: var(--s-4) 0; }

/* empty state: every "no character" panel renders as EXACTLY two children,
 * h3 then .hint (verified against every card('No character') call site), and
 * :nth-child(2):last-child pins that shape. RM23 filled it with a halftone
 * dot texture built from a radial gradient (slop C1); the Graphite answer is
 * white space and a rule, which is what a blank ledger page looks like.
 * (Written without the hyphenated property name on purpose: the brand auditor
 * greps the stylesheet TEXT, comments included, so naming a banned device in
 * prose flags the file. Measured on this file, RM41 task 7.) */
.card:has(> h3:first-child):has(> .hint:nth-child(2):last-child) {
  text-align: center; padding: var(--s-6) var(--s-4);
}
.card:has(> h3:first-child):has(> .hint:nth-child(2):last-child) > h3 {
  justify-content: center; margin-bottom: var(--s-3);
  padding-bottom: var(--s-3); border-bottom: 1px solid var(--aa-hairline);
}
.card:has(> h3:first-child):has(> .hint:nth-child(2):last-child) > .hint {
  font-family: var(--aa-read); font-size: var(--t-body);
  color: var(--aa-ink-muted); margin: 0 auto;
}

/* the character file sheet (RM50 req 2.5). Its Export and Import panels print
 * a status line — and, when an import collides, a choice — directly after the
 * control that produced it. .hint and .row carry no leading of their own
 * against a preceding button, so without this they close up against it.
 * Nothing else was needed: the panels, titles, prose and 44px controls are the
 * same .card / h3 / .hint / button this surface already uses everywhere. */
#sheetHost .card > button + .hint,
#sheetHost .card > button + .row { margin-top: var(--s-3); }

/* ability scores: six wells of one mono figure each */
.statgrid { display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--s-1); }
.stat {
  background: var(--aa-recess); border: 1px solid var(--aa-hairline); border-radius: 0;
  text-align: center; padding: var(--s-2) var(--s-1);
}
.stat .s-lab {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .06em; color: var(--aa-ink-muted);
}
.stat .s-mod {
  font-family: var(--aa-measure); font-size: var(--t-4); font-weight: 500;
  color: var(--aa-ink); font-variant-numeric: tabular-nums;
}
.stat .s-score {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  color: var(--aa-ink-muted); font-variant-numeric: tabular-nums;
}
/* a save proficiency is a mark against a name, not a tint: the well takes the
 * ink rule and the caption steps to ink weight. */
.stat.save-prof { border-color: var(--aa-ink); }
.stat.save-prof .s-lab { color: var(--aa-ink); font-weight: 500; }

.derived { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: var(--s-1); }
.dv {
  background: var(--aa-recess); border: 1px solid var(--aa-hairline); border-radius: 0;
  text-align: center; padding: var(--s-2) var(--s-1);
}
.dv b {
  display: block;
  font-family: var(--aa-measure); font-size: var(--t-4); font-weight: 500;
  color: var(--aa-ink); font-variant-numeric: tabular-nums;
}
.dv span {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .06em; color: var(--aa-ink-muted); text-transform: uppercase;
}

/* ---------------- THE RULED ROSTER ----------------
 * The one shape for every record list: a hairline row inside its panel, never
 * its own bordered box. Read-face name, mono figures, tabular and
 * right-aligned. The open record takes the marginal pilcrow — see the accent
 * note at the top of this file. */
.feat, .spell, .inv, .pk2 {
  border: none; border-bottom: 1px solid var(--aa-hairline); border-radius: 0;
  background: none; margin: 0;
}
.feat > summary, .spell > summary, .inv > summary {
  list-style: none; cursor: pointer;
  /* RM74: indented to the NAME's axis, with the pilcrow pulled back into
     the gutter below. These rows wrap, and without this a long name -
     "Channel Divinity: Harness Divine Power" - dropped to a second flex
     line beginning 22px left of every short name above it. Every line of
     every row now starts on one axis, wrapped or not. */
  padding: var(--s-3) 0 var(--s-3) var(--rec-indent);
  display: flex; align-items: baseline; gap: var(--s-2); min-height: 44px; flex-wrap: wrap;
}
.feat > summary::-webkit-details-marker, .spell > summary::-webkit-details-marker, .inv > summary::-webkit-details-marker { display: none; }
/* RUBRICATION (req 1.7). In a manuscript the red mark in the margin says
 * "attend to this line". Here it marks the record you have open — this
 * surface's single accent role. The pilcrow occupies its gutter at all times
 * so the names stay on one axis whether a row is open or shut. */
.feat > summary::before, .spell > summary::before,
.inv > summary::before, .pk2 > summary::before {
  content: '\00B6';
  font-family: var(--aa-read); font-size: var(--t-body); line-height: 1;
  color: transparent; width: 14px; flex: 0 0 auto; text-align: center;
  margin-left: calc(-1 * var(--rec-indent));   /* into the gutter the padding made */
}
.feat[open] > summary::before, .spell[open] > summary::before,
.inv[open] > summary::before, .pk2[open] > summary::before { color: var(--aa-accent); }
.feat[open], .spell[open], .inv[open], .pk2[open] { background: var(--aa-accent-wash); }
/* a spell, item, feat or creature is a proper name: read face (req 1.6) */
.feat > summary .f-name, .spell .sp-name, .inv .inv-name {
  font-family: var(--aa-read); font-size: var(--t-body); font-weight: 500;
  color: var(--aa-ink); flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis;
}
/* RM196: the open-row name sat at 4.49:1 on the accent wash (AA needs 4.5) - the
   lighter accent step reads 6:1 and keeps the same hue */
.feat[open] > summary .f-name, .spell[open] .sp-name, .inv[open] .inv-name { color: var(--aa-accent-hover); }
.feat > summary .f-src, .spell .sp-meta {
  font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted);
}
/* RM74 for the spell, RM80 for the feature: the caption takes its OWN line,
   under the name.

   Sharing the summary's line, the spell's school and casting time were squeezed
   by the name and the badges and ran off the right edge on a phone -
   "Divination · 1 action" was clipped mid word at 390px.

   The feature's source was worse, because it did not fail consistently. It is a
   plain flex item sitting after a name that grows, so whether it stayed on the
   name's line or wrapped under it depended on how long that particular feature
   happened to be called. Measured across four cards at 390px: thirteen rows,
   SIX different left edges for the caption (55, 121, 151, 217, 239, 282) and
   six different row heights (50, 52, 78, 86, 88, 136). "Bonus Proficiencies"
   dropped its source under the header while "Eyes of Night" two rows down kept
   it inline, from the same data - which reads as two components, not one list.

   A full-width flex item settles it by construction: the caption is always the
   second line, always on --rec-indent with the name and the prose, and the
   badges always finish the first line at the right edge. Nothing here depends
   on the name's length, so it holds for every class, subclass, race and feat
   rather than for the ones that happened to measure short. */
.spell .sp-meta, .feat > summary .f-src {
  flex: 1 1 100%;
  white-space: normal;
}
.feat[open] > summary, .spell[open] > summary, .inv[open] > summary { border-bottom: 1px solid var(--aa-hairline); }
/* the description: prose, so the read face at the body step on a measure. */
/* pre-LINE, not pre-wrap: the description arrives as a text node whose
 * paragraph breaks are real newlines, but it also carries leading whitespace
 * after the stat-line div. pre-wrap preserved that as a 48px hole between the
 * dateline and the first sentence (measured at 390x844); pre-line keeps the
 * paragraph breaks and collapses the hole. Same choice css/dm.css makes for
 * its prose blocks. */
.feat .f-body, .spell .sp-body {
  font-family: var(--aa-read); font-size: var(--t-body); line-height: 1.65;
  color: var(--aa-ink-body); white-space: pre-line;
  padding: var(--s-3) 0 var(--s-4) var(--rec-indent); max-width: 34rem;
}
/* RM74: the feature's provenance, lifted out of its prose by
   rules.js splitFeatureText. Measure face at the caption step, on the same
   axis as the name and the description. */
.feat .f-meta {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .04em; color: var(--aa-ink-muted);
  padding: var(--s-3) 0 0 var(--rec-indent);
}
.feat .f-meta + .f-body { padding-top: var(--s-1); }
/* THE DROP CAP (req 1.8, and RM49 req 6.1 for spells). A description opens
 * with a read-face capital at a brand step: the oldest device in the book, and
 * pure typography — no asset, no illustration. It goes where the block's first
 * formatted line is really prose.
 *
 * Feature, spell and item prose all take the SAME declarations — one rule for
 * the first two, and .inv-txt > p:first-child::first-letter repeats them for
 * items, whose description is wrapped in real <p> elements. There is no
 * second drop-cap size on this surface.
 *
 * WHY THE SPELL RULE READS .sp-body AND NOT ITS FIRST CHILD: ::first-letter
 * resolves against the first formatted line of the block it is set on. Until
 * RM49 task 6, js/player/spells.js built .sp-body with the <div class="hint">
 * stat line as its FIRST CHILD ("Range Touch · Duration Instantaneous ·
 * Components V, S"), so .sp-body::first-letter landed on the R of "Range" and
 * set a 33px capital on a mechanical line — measured in the live page at
 * 390x844, which is why the cap was withheld from spells then. spells.js now
 * appends that stat line to the <details> as a SIBLING of .sp-body, so
 * .sp-body's first formatted line is the description's first sentence and the
 * cap lands on its first letter. Keep the stat line a sibling: putting any
 * element back inside .sp-body ahead of the text moves the cap onto it. */
.feat .f-body::first-letter, .spell .sp-body::first-letter {
  font-family: var(--aa-read); font-weight: 600; font-size: var(--t-2);
  line-height: .82; float: left; padding: 0 var(--s-2) 0 0; color: var(--aa-ink);
}
/* The spell's dateline: range, duration and components are metadata, so they
 * take the measure face at the meta step (brand 4.4) instead of the read face
 * they inherited from .hint. It is what a rulebook prints between a spell's
 * name and its prose.
 *
 * It sits BETWEEN summary and .sp-body as a sibling of both, so it repeats
 * .sp-body's 14px pilcrow gutter and its --s-3 top padding by hand rather than
 * inheriting them.
 *
 * No rule and no margin under it ON PURPOSE. The separation is .sp-body's own
 * --s-3 top padding, which puts the dateline the same 12px from the prose as
 * it is from the summary above. (Before RM49 task 6 the separation was two
 * literal newlines that spells.js prefixed to the description and pre-line
 * preserved; that text is gone, the padding replaced it, and the reason a rule
 * would be wrong is unchanged — a hairline here would divide metadata from its
 * own prose, which the space already does.) */
.spell > .sp-stat {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  color: var(--aa-ink-muted); line-height: 1.5; max-width: none;
  margin: 0; padding: var(--s-3) 0 0 var(--rec-indent);
}
/* RM50's pack note under the dateline. It is a prose sentence, not metadata,
 * so it keeps .hint's read face and borrows from .sp-stat only the 14px pilcrow
 * gutter and the --s-3 step — the alignment it lost by becoming a sibling of
 * .sp-body rather than a child of it. Same rhythm as the dateline above and the
 * prose below, so the three read as one block. */
.spell > .sp-mark { margin: 0; padding: var(--s-3) 0 0 var(--rec-indent); }
.spell .sp-tags { padding: 0 0 var(--s-3) 14px; }
/* prepared / granted: a mark against the name, drawn, plus ink weight — never
 * a tint and never the accent, which belongs to the open record. */
.spell.prepped .sp-name, .spell.granted .sp-name { font-weight: 600; }
.spell.prepped .sp-name::before {
  content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 0;
  background: var(--aa-ink); margin-right: var(--s-2); vertical-align: 1px;
}
.spell.granted .sp-name::before {
  content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 0;
  border: 1px solid var(--aa-ink); margin-right: var(--s-2); vertical-align: 1px;
}
/* a spell level is a section title inside the roster */
.lvhead {
  margin: var(--s-5) 0 var(--s-2);
  font-family: var(--aa-read); font-weight: 500; font-size: var(--t-body);
  color: var(--aa-ink);
  display: flex; align-items: baseline; gap: var(--s-2);
  border-bottom: 1px solid var(--aa-rule); padding-bottom: var(--s-1);
}
/* RM75: on the record axis with everything else in the tab. These rows carry
   no pilcrow, so they sat at the card's own edge while every expandable row
   beside them sat 22px in - two left edges on one page. The empty gutter is
   already the page's convention: a closed record's pilcrow is transparent,
   so that column reads as blank there too. */
.slot-lv { display: flex; align-items: center; gap: var(--s-2);
           padding: var(--s-1) 0 var(--s-1) var(--rec-indent); }
.slot-lv .sl-lab {
  width: 28px; font-family: var(--aa-measure); font-size: var(--t-ui);
  font-weight: 500; color: var(--aa-ink-muted); font-variant-numeric: tabular-nums;
}

/* a badge is a COUNT or a TAG, not an action, so it takes brand 6.4's chip
 * spec: measure face at --t-meta, 1px border, no fill, no radius. State
 * badges pair their colour with a drawn mark. */
.badge {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  padding: var(--s-1) var(--s-2); border-radius: 0; letter-spacing: .04em;
  background: none; border: 1px solid var(--aa-hairline); color: var(--aa-ink-muted);
  flex: 0 0 auto; font-variant-numeric: tabular-nums;
}
.badge::before {
  content: ''; display: inline-block; width: 5px; height: 5px; border-radius: 0;
  background: currentColor; margin-right: var(--s-1); vertical-align: 1px;
}
.badge.lv, .badge.lvl, .badge.src, .badge.ed-classic, .badge.ed-one { color: var(--aa-ink-muted); }
.badge.ua { color: var(--aa-warn); border-color: var(--aa-warn); }
.badge.on { color: var(--aa-ok); border-color: var(--aa-ok); }
.badge.no { color: var(--aa-warn); border-color: var(--aa-warn); }
.badge.no::before { width: 9px; height: 2px; }
.badge.eq, .badge.att, .atk-head .badge.prof-b { color: var(--aa-ink); border-color: var(--aa-ink); }
/* USER CONTENT (RM50 req 8.3). An entry out of a player's own content pack is
 * marked wherever it appears, and it is marked with a WORD — RM41 req 5.4
 * forbids colour as the only channel, and the case that rule exists for is
 * exactly this one: a statblock read at speed mid-session. Colour is the
 * second channel and it is full ink, the weight EQUIPPED and ATTUNED already
 * take, because homebrew is a FACT about an entry and not a warning about it;
 * the warn colour is spent on `.badge.no`, which is the pack that has gone.
 * No new token, no new hex. Read, never clicked, so it keeps brand 6.4's
 * status-chip spec rather than the 32px control floor. */
.badge.pack { color: var(--aa-ink); border-color: var(--aa-ink); }
/* the statblock's origin line: the chip, then the sentence naming the pack */
.sb-origin {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--s-2);
  margin-bottom: var(--s-2); padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--aa-hairline);
}
.sb-origin .hint { flex: 1 1 12rem; min-width: 0; margin: 0; }

/* a chip HERE performs an action (roll this, take this), so it takes the
 * control spec and clears the target floor, not brand's 24px status chip
 * (req 2.7, handoff trap 4). */
.chip {
  display: inline-flex; align-items: center; justify-content: center;
  margin: var(--s-1) var(--s-1) 0 0; min-height: 44px; min-width: 44px;
  background: none; border: 1px solid var(--aa-rule); color: var(--aa-ink);
  border-radius: 0; padding: var(--s-1) var(--s-4);
  font-family: var(--aa-measure); font-size: var(--t-ui); font-weight: 500;
  cursor: pointer; font-variant-numeric: tabular-nums;
}
/* RM385: a feature's figures (DC 13 (DEX save), pool: 25 hit points) sit in the row of its dice chips */
.feat .f-rolls .badge { display: inline-block; margin: var(--s-1) var(--s-1) 0 0; vertical-align: middle; }
.chip:hover { border-color: var(--aa-ink); }
.chip:active { background: var(--aa-ink); border-color: var(--aa-ink); color: var(--aa-sheet); }

/* pips (resources, spell slots): a ruled box per use — filled while it is
 * yours, an empty outline once spent. The visible mark stays small; the tap
 * target is the whole 44px button. */
.pips { display: flex; flex-wrap: wrap; gap: var(--s-1); }
.pip {
  width: 44px; height: 44px; min-height: 44px;
  border: none; background: none; border-radius: 0;
  padding: 0; position: relative; flex: 0 0 auto;
}
.pip::before {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 20px; height: 20px; margin: -10px 0 0 -10px;
  border-radius: 0;
  border: 1px solid var(--aa-ink); background: var(--aa-ink);
}
.pip:hover::before { border-color: var(--aa-ink); }
.pip.spent::before { background: none; border-color: var(--aa-rule); }
.res-row {
  display: flex; align-items: center; gap: var(--s-2);
  /* RM75: same axis as the features listed above it in this tab. */
  padding: var(--s-2) 0 var(--s-2) var(--rec-indent);
  border-bottom: 1px solid var(--aa-hairline); flex-wrap: wrap;
}
.res-row:last-child { border-bottom: none; }
.res-name { flex: 0 0 auto; order: 1; font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink); }
.res-row .pips { flex: 1 1 auto; order: 2; margin-left: var(--s-1); }
.res-count {
  font-family: var(--aa-measure); font-size: var(--t-ui); color: var(--aa-ink-muted);
  order: 3; margin-left: auto; font-variant-numeric: tabular-nums;
}
/* RM791 (BUILD2-07): a species trait's DC and dice under its pips */
.res-row .res-rolls { order: 4; flex: 1 0 100%; }
.res-rolls .badge { display: inline-block; margin: var(--s-1) var(--s-1) 0 0; vertical-align: middle; }
/* RM791: the sheet in play - notes and backstory (js/player/notes.js), Inspiration, resistances */
.notes-card textarea { width: 100%; box-sizing: border-box; resize: vertical; line-height: 1.4; }
.notes-field { display: block; margin-bottom: var(--s-2); }
.notes-lab { display: block; font-family: var(--aa-operate); font-size: var(--t-ui); color: var(--aa-ink-muted);
  margin: var(--s-2) 0 var(--s-1); }
.notes-suggest { margin: 0 0 var(--s-2); }
.notes-pick { width: 100%; text-align: left; white-space: normal; }
.notes-dm { margin-top: var(--s-4); padding-top: var(--s-3); border-top: 1px solid var(--aa-hairline); }
.notes-dm textarea { width: 100%; box-sizing: border-box; }
.notes-send { margin-top: var(--s-2); }
.insp-row { margin-top: var(--s-3); }
.insp-btn { width: 100%; text-align: left; }
.def-resist { font-family: var(--aa-operate); font-size: var(--t-ui); color: var(--aa-ink); margin-bottom: var(--s-2); }
/* RM791: the Pack - folding groups, the filter, use, charges and the undo bar */
.inv-group > summary { list-style: none; cursor: pointer; }
.inv-group > summary::-webkit-details-marker { display: none; }
.inv-group > summary h3 { margin-bottom: 0; }
.inv-group[open] > summary h3 { margin-bottom: var(--s-2); }
.inv-group > summary h3::after { content: 'show'; margin-left: auto; font-family: var(--aa-operate);
  font-size: var(--t-meta); color: var(--aa-ink-muted); }
.inv-group[open] > summary h3::after { content: 'hide'; }
.inv-filter input { width: 100%; box-sizing: border-box; }
.inv-filter .inv-filter-count:empty { display: none; }
.inv-charges, .inv-use { margin: var(--s-2) 0; }
#invUndo { position: fixed; left: 50%; transform: translateX(-50%); z-index: 91;
  bottom: calc(var(--tabbar-h) + var(--safe-b) + 128px);
  display: flex; align-items: center; gap: var(--s-3); max-width: 92vw;
  background: var(--aa-sheet); border: 1px solid var(--aa-ink); border-radius: 0;
  padding: var(--s-1) var(--s-1) var(--s-1) var(--s-3);
  font-family: var(--aa-operate); font-size: var(--t-ui); color: var(--aa-ink); }
#invUndo[hidden], .resilient-warn[hidden] { display: none; }   /* .inv-warn is display:flex */
#invUndo .inv-undo-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------------- skills, picks, characters: same ruled row ---------------- */
.skill-row, .pick-row, .char-item {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-3) 0; min-height: 48px;
  border: none; border-bottom: 1px solid var(--aa-hairline); border-radius: 0;
  background: none; margin: 0; cursor: pointer;
}
/* proficiency and expertise are marks against a name, carried by a drawn
 * gutter mark and ink weight rather than by a fill. */
.skill-row .sk-name {
  flex: 1 1 auto; font-family: var(--aa-read); font-size: var(--t-body);
  color: var(--aa-ink-body);
}
.skill-row.prof .sk-name, .skill-row.expert .sk-name { color: var(--aa-ink); font-weight: 500; }
.skill-row.prof::before, .skill-row.expert::before {
  content: ''; flex: 0 0 auto; width: 7px; height: 7px;
  background: var(--aa-ink); margin-right: var(--s-1);
}
.skill-row.expert::before { width: 7px; height: 16px; }
.skill-row.prof .sk-tag, .skill-row.expert .sk-tag { color: var(--aa-ink); }
.skill-row .sk-tag {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  color: var(--aa-ink-muted); letter-spacing: .05em;
}
.skill-row .sk-mod {
  font-family: var(--aa-measure); font-weight: 500; font-size: var(--t-4);
  color: var(--aa-ink); width: 44px; text-align: right; font-variant-numeric: tabular-nums;
}
.skill-row.offlist { opacity: .62; }
.skill-row .sk-tag.off { color: var(--aa-ink-muted); }
/* RM688 (PL-15): the expertise chip on a proficient skill row - a 44px control that does not grow the 48px row */
.skill-row .sk-exp { flex: 0 0 auto; min-height: 44px; margin: calc(-1 * var(--s-2)) 0; padding: 0 var(--s-2); }
.pick-row:active, .char-item:active { background: var(--aa-recess); }
/* RM689 (PL-03): the chooser for a player with no character yet - one big Create, a quiet "I already have one",
   and the device tools folded under More. */
.picker-lead { margin: var(--s-3) 0; }
.picker-lead:empty { display: none; }
.picker-lead .picker-create { width: 100%; min-height: 56px; font-size: var(--t-body); }
.picker-have, .picker-more { margin-top: var(--s-3); }
.picker-have > summary, .picker-more > summary {
  cursor: pointer; min-height: 44px; display: flex; align-items: center;
  font-family: var(--aa-operate); font-size: var(--t-ui); color: var(--aa-ink);
}
.pick-row .pk-name {
  flex: 1 1 auto; font-family: var(--aa-read); font-size: var(--t-body);
  font-weight: 500; color: var(--aa-ink);
}
.pick-row .pk-meta {
  font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted);
}
.char-item .ci-name {
  flex: 1 1 auto; font-family: var(--aa-read); font-size: var(--t-body);
  font-weight: 500; color: var(--aa-ink);
}
.char-item .ci-meta {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  color: var(--aa-ink-muted); font-variant-numeric: tabular-nums;
}
/* RM476: a multiclass row names every class with its level, which can run long - six classes is
 * three lines at 390 px. It wraps inside the row as any meta line does, stops at two lines with an
 * ellipsis, and never widens the row (a long unbroken name breaks rather than pushing it out). Only
 * this modifier: a single-class row's .ci-meta is untouched. */
.char-item .ci-meta.ci-classes {
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; line-clamp: 2;
  overflow: hidden; overflow-wrap: anywhere;
}
/* RM50 req 3.1: an upload row is a ruled record that carries its own control,
 * so unlike the picker's rows the row itself is not the tap target and must
 * not claim to be one. */
.char-item.jp-row { cursor: default; }
.char-item.jp-row:active { background: none; }

/* RM50 req 3.4 — the two sides of a collision.
 * When the same id exists on this device and on the DM's table, the player is
 * asked which one they mean, and they can only answer that if both are in
 * front of them. Two ruled rows, one per side, in the ruled-roster shape this
 * file already uses for every record list — never a nested box, and never two
 * panels side by side, which would read as a comparison table and imply the
 * app is about to reconcile them. It is not: it is about to do exactly one of
 * the things the buttons underneath name. */
.jp-side {
  display: flex; align-items: baseline; gap: var(--s-3);
  padding: var(--s-3) 0; min-height: 44px;
  border: none; border-bottom: 1px solid var(--aa-hairline); border-radius: 0;
  background: none; margin: 0;
}
.jp-side .jp-where {
  flex: 0 0 auto; width: 7.5rem;
  font-family: var(--aa-measure); font-size: var(--t-ui); letter-spacing: .06em;
  text-transform: uppercase; color: var(--aa-ink-muted);
}
.jp-side .jp-what {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink);
}
.jp-side .jp-what .hint { margin: var(--s-1) 0 0; }
.jp-side .jp-when {
  flex: 0 0 auto;
  font-family: var(--aa-measure); font-size: var(--t-meta);
  color: var(--aa-ink-muted); font-variant-numeric: tabular-nums;
}
/* A 390px phone has no room for a 7.5rem gutter beside a name and a date, so
 * the label stacks over the record instead of standing in a column. */
@media (max-width: 480px) {
  .jp-side { display: block; }
  .jp-side .jp-where { width: auto; display: block; margin-bottom: var(--s-1); }
}

/* ---------------- table view (board mirror) ----------------
 * The canvas container and everything containing it: no filter, no blur, no
 * backdrop effect, no shadow, no animation, ever (RM19 paint budget). The board
 * itself is painted by js/table/* through js/core/palette.js; this file only
 * positions it. The HUD siblings are chrome and take the plate treatment the
 * projector's own chrome takes (css/table.css). */
#tableView { position: absolute; inset: 0; background: var(--tf-board); overflow: hidden; touch-action: none; }
#tableView canvas { position: absolute; left: 0; top: 0; }
#tvHud {
  position: absolute; left: var(--s-3); top: var(--s-3); z-index: 5;
  font-family: var(--aa-measure); font-size: var(--t-meta);
  color: var(--aa-ink-muted); font-variant-numeric: tabular-nums;
  background: var(--aa-sheet); border: 1px solid var(--aa-hairline); border-radius: 0;
  padding: var(--s-1) var(--s-2);
  pointer-events: none;
}
#tvPlaceBar {
  position: absolute; left: 50%; transform: translateX(-50%);
  top: var(--s-3); z-index: 6;
  background: var(--aa-sheet); border: 1px solid var(--aa-rule); border-radius: 0;
  color: var(--aa-ink); font-family: var(--aa-operate); font-size: var(--t-ui); font-weight: 500;
  padding: var(--s-2) var(--s-3); display: flex; gap: var(--s-2); align-items: center;
}
/* RM384: an armed area template's bar carries the spell, how it is placed and up to four controls - on a
   390px phone they wrap under the words instead of running off both edges */
#tvPlaceBar.tv-tpl { flex-wrap: wrap; width: calc(100% - 2 * var(--s-3)); max-width: 32rem; box-sizing: border-box; }
#tvPlaceBar .tv-tpl-words { flex: 1 1 100%; display: flex; flex-direction: column; gap: var(--s-1); }
#tvPlaceBar .tv-tpl-words span { font-weight: 400; color: var(--aa-ink-muted); }
/* RM651: aiming from the Combat tab - what is picked, Confirm / Cancel; or Move's feet, Difficult terrain, Dash, Done */
#tvPlaceBar.tv-aim { flex-wrap: wrap; width: calc(100% - 2 * var(--s-3)); max-width: 32rem; box-sizing: border-box; }
#tvPlaceBar .tv-aim-words { flex: 1 1 100%; display: flex; flex-direction: column; gap: var(--s-1); }
#tvPlaceBar .tv-aim-words span { font-weight: 400; color: var(--aa-ink-muted); }
#tvPlaceBar .tv-aim-left { font-family: var(--aa-measure); font-variant-numeric: tabular-nums; }
#tvZoom {
  position: absolute; left: var(--s-3); bottom: var(--s-4); z-index: 5;
  display: flex; flex-direction: column; gap: var(--s-1);
}
#tvZoom button {
  width: 46px; height: 46px; padding: 0;
  background: var(--aa-sheet); font-size: var(--t-4);
}
/* RM66: the player's own-token control, with the board's other controls but
   on the opposite edge so it is never under the zoom column's thumb. */
#tvSelf { position: absolute; right: var(--s-3); bottom: var(--s-4); z-index: 5; }
#tvSelf button { background: var(--aa-sheet); }
#tvSelf[hidden] { display: none; }                  /* RM676 (PL-28) */
/* RM676 (PL-27, PL-45): aiming from the Combat tab - the header keeps the name and the turn bar, the zoom column gives
   way to + and - in the aim bar, and the board gets the height (js/player/table.js aimLayout) */
body.tv-aiming #plates, body.tv-aiming #hdrTurn, body.tv-aiming #hdrConds { display: none; }
#tableView.aiming #tvZoom { display: none; }
#tvPlaceBar .tv-aim-zoom { display: inline-flex; gap: var(--s-1); }
#tvPlaceBar .tv-aim-zoom button { min-width: 44px; min-height: 44px; }

/* the table tab with no table (RM50 req 1A.1, 4.1-4.3).
 * js/player/table.js mounts this INSTEAD of #tableView when the active
 * character store reports it cannot mirror a board, so the two never coexist
 * and there is no dimmed canvas behind it pretending a table is loading.
 * #pane-table is .pane-fill — no gutter and no scroll, because a canvas fills
 * it edge to edge — so the join state brings the page gutter and the scroll
 * back with it rather than the pane learning two layouts. The panels inside
 * are ordinary .card: the first is the two-child h3 + .hint empty state
 * declared above and takes its centring from there, the second is a panel with
 * an action. No new vocabulary, no accent (this view spends none), and the
 * button inherits the 44px control height. */
#tvJoin {
  position: absolute; inset: 0;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: var(--s-3) 16px var(--s-10);
}
/* The offer panel's sentences are three paragraphs, not one block: .hint is a
 * div carrying no margin of its own, so consecutive ones close up into a wall
 * of prose. Leading between them and above the action, nothing else. */
#tvJoin .card > .hint + .hint { margin-top: var(--s-3); }
#tvJoin .card > button { margin-top: var(--s-4); }
@media (min-width: 768px) {
  #tvJoin {
    padding-top: var(--s-5);
    padding-left: max(var(--s-5), calc(50vw - 430px));
    padding-right: max(var(--s-5), calc(50vw - 430px));
  }
}

/* ---------------- dice: the one persistent action ---------------- */
/* RM66: #diceFab is no longer a fixed disc above the tab bar - it is a plate
   in the masthead (see .vt-act above), so every rule that positioned, sized
   and hid it is gone: position/right/bottom, the 56px disc, the ink fill, and
   the .dodge step-aside that existed only because it floated over a scrolling
   list. The drawer it opens now hangs from the masthead instead of rising
   from the tab bar. */
/* RM652: the open drawer is a modal popover. The host covers the page with a
   transparent backdrop: a tap beside the drawer closes it and reaches nothing
   under it (it used to close the drawer AND press the control under the
   finger), and a drag there scrolls nothing. The host CONTAINS the
   aria-modal drawer, as #sheetHost contains its .sheet. */
#diceHost { position: fixed; inset: 0; z-index: 60; }
#diceHost[hidden] { display: none; }
#diceHost .dice-back {
  position: absolute; inset: 0; background: transparent;
  touch-action: none; overscroll-behavior: contain;
}
#diceDrawer {
  position: fixed; right: var(--s-3); top: calc(var(--topbar-real-h) + var(--s-1));
  z-index: 1; width: min(340px, calc(100vw - 20px));
  max-height: calc(100vh - var(--topbar-real-h) - var(--tabbar-h) - var(--safe-b) - var(--s-4));
  /* RM251: 100vh is the height with the address bar hidden - the drawer's foot sat under it */
  max-height: calc(100dvh - var(--topbar-real-h) - var(--tabbar-h) - var(--safe-b) - var(--s-4));
  overflow-y: auto;
  background: var(--aa-sheet); border: 1px solid var(--aa-rule); border-radius: 0;
  padding: var(--s-3);
}
.dice-btns { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s-1); margin-bottom: var(--s-2); }
.dice-btns button { font-family: var(--aa-measure); font-weight: 500; }
/* RM352: the typed roll - the field takes the row, Roll keeps its own width */
.dice-expr { flex-wrap: nowrap; }
.dice-expr input { flex: 1 1 auto; min-width: 0; width: auto; font-family: var(--aa-measure); }
.hint.dice-expr-err { color: var(--aa-warn); margin: 0 0 var(--s-2); }
.hint.dice-expr-err:empty { display: none; }
/* RM343's 16px on touch screens, stated for this field too: iOS zooms the page on a smaller one */
@media (pointer: coarse) { .dice-expr input { font-size: max(16px, var(--t-ui)); } }
/* RM646: the physical-dice switch shares the drawer's foot with Clear history, and the ✎ sits in the result's
   top-right corner - as rows of their own they pushed the open drawer down to the tab bar on a 390x844 phone */
.dice-foot { justify-content: space-between; align-items: center; }
.pd-switch { display: inline-flex; align-items: center; gap: var(--s-2); font-size: var(--t-ui); min-height: 44px; }
.pd-switch input { width: auto; margin: 0; }
#diceResult { position: relative; }
#diceResult .dr-edit { position: absolute; top: 0; right: 0; margin: 0; padding: 0;
  min-width: 44px; min-height: 44px; font-size: var(--t-4); line-height: 1; }
#diceResult {
  text-align: center; font-family: var(--aa-measure);
  padding: var(--s-2) 0 var(--s-1); min-height: 54px;
}
/* the total is the figure the table waits for: the largest measure step */
#diceResult .dr-total {
  font-family: var(--aa-measure); font-size: var(--t-1); font-weight: 500;
  color: var(--aa-ink); line-height: 1.05; font-variant-numeric: tabular-nums;
}
#diceResult .dr-detail {
  font-family: var(--aa-measure); font-size: var(--t-ui); color: var(--aa-ink-muted);
}
#diceHist {
  /* RM54 req 4.1: was max-height 180px + overflow-y auto — a scrollbar inside
     .pane's scrollbar. The list is capped in JS instead (first 8, then "Show
     all"), so the page has one scroll and the content is still reachable. */
  margin-top: var(--s-2);
  border-top: 1px solid var(--aa-hairline); padding-top: var(--s-2);
}
/* the roll log IS a ledger: who, what, how much, ruled */
.dh-row {
  display: flex; gap: var(--s-2); font-family: var(--aa-measure); font-size: var(--t-ui);
  padding: var(--s-1) 0; border-bottom: 1px solid var(--aa-hairline);
}
.dh-row:last-child { border-bottom: none; }
.dh-row .dh-who {
  font-family: var(--aa-read); color: var(--aa-ink); flex: 0 0 auto;
  max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dh-row .dh-what { color: var(--aa-ink-muted); flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dh-row .dh-tot { color: var(--aa-ink); font-weight: 500; flex: 0 0 auto; font-variant-numeric: tabular-nums; }

/* ---------------- bottom sheet / modal ----------------
 * RM23 gave the sheet a drop shadow; brand 6.2 has no shadows, so it commits
 * to the edge instead — a 1px rule and a sheet ground a value-step off the
 * field, the same answer css/dm.css gives its log dock. */
#sheetHost { position: fixed; inset: 0; z-index: 80; display: none; }
#sheetHost.open { display: block; }
#sheetHost .sheet-back { position: absolute; inset: 0; background: var(--aa-recess); opacity: .84; }
/* RM118. --kb is how much of the viewport the phone keyboard is covering,
   published by js/core/keyboard.js; it is 0px whenever there is no keyboard, so
   this is the old rule on every other surface. Without the lift the bottom of
   the sheet - which is where its input usually is - sits behind the keys, and
   the DM types blind. The max-height falls back to a percentage when --vvh has
   not been published (no visualViewport support). */
#sheetHost .sheet {
  position: absolute; left: 0; right: 0; bottom: var(--kb, 0px);
  max-height: 86%;
  max-height: calc((var(--vvh, 100vh)) * 0.86);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: var(--aa-sheet); border: 1px solid var(--aa-rule); border-bottom: none;
  border-radius: 0;
  padding: var(--s-4) var(--s-4) calc(var(--s-5) + var(--safe-b));
  margin-bottom: 0;
}
@media (min-width: 768px) {
  #sheetHost .sheet {
    left: 50%; right: auto; bottom: 50%; transform: translate(-50%, 50%);
    width: min(680px, 92vw); max-height: 84%;
    max-height: calc((var(--vvh, 100vh)) * 0.84);
    border: 1px solid var(--aa-rule); border-radius: 0;
  }
  /* A tablet with a keyboard up has the same problem as a phone; centring on
     the layout viewport puts the sheet behind the keys. */
  :root.kb-open #sheetHost .sheet {
    bottom: var(--kb, 0px); transform: translate(-50%, 0);
  }
}
/* the sheet's subject is a proper name (req 1.6) */
.sheet h2 {
  margin: 0 0 var(--s-1);
  font-family: var(--aa-read); font-weight: 500; font-size: var(--t-3);
  letter-spacing: -.01em; color: var(--aa-ink);
}
.sheet .sheet-x {
  position: sticky; float: right; top: 0; margin-left: var(--s-2);
  width: 44px; height: 44px; border-radius: 0; padding: 0;
  font-size: var(--t-4); min-height: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--aa-recess);
}

/* ---------------- statblock ---------------- */
.sb-line { font-family: var(--aa-read); font-size: var(--t-body); line-height: 1.6; margin: var(--s-1) 0; max-width: 34rem; }
.sb-line b { color: var(--aa-ink); font-weight: 600; }
.sb-stats { display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--s-1); margin: var(--s-2) 0; }
.sb-stats .stat { padding: var(--s-1); }
.sb-sec { margin-top: var(--s-4); padding-top: var(--s-2); border-top: 1px solid var(--aa-hairline); }
.sb-sec h4 {
  margin: 0 0 var(--s-2);
  font-family: var(--aa-read); font-weight: 500; font-size: var(--t-body);
  color: var(--aa-ink);
  display: flex; align-items: baseline; gap: var(--s-2);
}
.sb-entry { font-family: var(--aa-read); font-size: var(--t-body); line-height: 1.6; margin: var(--s-1) 0; max-width: 34rem; }
.sb-entry b { color: var(--aa-ink); font-weight: 600; }

/* pick lists (creature browser, spell picker) */
.pick-row button { flex: 0 0 auto; }

/* character list */
/* .char-item already covered under the ruled roster above */

/* ---------------- starting equipment (RM382) ----------------
 * One option per group, a row each: the choice is a list of goods, so it reads left to right like one. */
.start-gear .sg-group { margin: var(--s-2) 0; }
.start-gear .sg-group.off { color: var(--aa-ink-muted); }
.start-gear .sg-opt { display: block; width: 100%; text-align: left; white-space: normal; margin: 0 0 var(--s-1); }
.start-gear .sg-take { width: 100%; margin-top: var(--s-2); }

/* ---------------- the Cast sheet's steps (RM384) ----------------
 * A cast reads top to bottom: what was spent, the attack, the save, the dice, the map, Done. */
.cast-done { font-family: var(--aa-read); color: var(--aa-ink); margin: 0 0 var(--s-2); }
.cast-save { font-family: var(--aa-read); color: var(--aa-ink); margin: var(--s-2) 0; }
.cast-step { display: block; width: 100%; margin: 0 0 var(--s-2); text-align: left; }
.cast-place { margin: var(--s-3) 0; border-top: 1px solid var(--aa-hairline); padding-top: var(--s-2); }
.sp-tags .sp-dice { font-family: var(--aa-measure); }

/* ---------------- the Feats step (RM383) ---------------- */
.feat-card .feat-skills, .feat-card .feat-spells { margin-top: var(--s-2); }
.feat-card .feat-skill { margin: 0 var(--s-1) var(--s-1) 0; }

/* ---------------- toast ----------------
 * A sheet plate with a rule and a leading drawn mark; a refusal recolours both
 * the mark and the label to warn so it reads as a refusal, not information
 * (RM20's 403/409/503 character-save refusals pass 'bad'/'warn'). It is ABSENT
 * at rest, not transparent at rest — display, not opacity (slop G2).
 * js/player/app.js only toggles .show, so no script changes. */
#toast {
  /* RM54 req 3.2: a roll reads on ONE line. "d20 +4 = 15" is short, but the
     label rides in front of it ("Athletics: d20 +4 = 15") and at 375px that was
     wrapping mid-expression, which is the one place a break is unreadable. */
  white-space: nowrap; max-width: calc(100vw - 24px); overflow: hidden; text-overflow: ellipsis;
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--tabbar-h) + var(--safe-b) + 76px);
  z-index: 90;
  background: var(--aa-sheet); border: 1px solid var(--aa-rule); border-radius: 0;
  color: var(--aa-ink); font-family: var(--aa-operate); font-size: var(--t-ui);
  padding: var(--s-2) var(--s-3);
  display: none; align-items: center; gap: var(--s-2);
  pointer-events: none;
  max-width: 86vw; text-align: left;
}
#toast::before {
  content: ''; flex: 0 0 auto; width: 8px; height: 12px; background: var(--aa-ink);
}
#toast.show { display: flex; }
/* RM800 (TABLER-05): a notice (anything but a roll result - js/core/util.js isRollLine) wraps to the lines it needs */
#toast.notice { white-space: normal; overflow-wrap: break-word; width: max-content; }   /* max-content, capped by max-width: shrink-to-fit at left:50% would stop at half the screen */
#toast.bad { border-color: var(--aa-warn); color: var(--aa-warn); }
#toast.bad::before { background: var(--aa-warn); }
#toast.warn { border-color: var(--aa-warn); color: var(--aa-warn); }
#toast.warn::before { background: var(--aa-warn); }
/* RM797 (BUILDR-03): a sentence (a purse refusal, the name-in-use notice) wraps everywhere - cut at the phone's edge it
   read "Plate Armor costs 1,500 gp and your purse holds" and hid the way out. Roll lines keep RM54's one line. */
#toast.prose { white-space: normal; width: max-content; }   /* max-content: left 50% would shrink it to half the phone */
/* RM380: a roll's own toast is the roll card's detail line while the card shows */
#toast.show.rc-dup { display: none; }
/* RM784 (PB-27): while the guided build is open the toast sits above its Back/Next bar and wraps instead of being cut */
body:has(#sheetHost.open .wiz-nav) #toast { white-space: normal; bottom: calc(var(--safe-b) + 84px); }
@media (min-width: 768px) {
  body:has(#sheetHost.open .wiz-nav) #toast { bottom: calc((var(--vvh, 100vh)) * 0.08 + 84px); }
}
.ability-pick .abil-amt { flex: 0 0 auto; }

/* ---------------- roll card (RM380) ----------------
 * The figure the table waits for: what was rolled, the total at the largest measure, the dice. A sheet plate
 * like the toast, ruled in ink; a natural 20 rules it in ok, a natural 1 in warn. It takes no taps, so it never
 * covers a control; any press closes it (js/player/app.js). */
#rollCard {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--tabbar-h) + var(--safe-b) + var(--s-3));
  z-index: 91; width: min(420px, calc(100vw - 24px));
  background: var(--aa-sheet); border: 2px solid var(--aa-ink); border-radius: 0;
  padding: var(--s-2) var(--s-3) var(--s-3);
  text-align: center;
  pointer-events: none;
}
#rollCard[hidden] { display: none; }
#rollCard .rc-label {
  font-family: var(--aa-read); font-size: var(--t-4); color: var(--aa-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#rollCard .rc-total {
  font-family: var(--aa-measure); font-weight: 500; color: var(--aa-ink);
  font-size: calc(var(--t-1) * 1.6); line-height: 1.05; font-variant-numeric: tabular-nums;
}
#rollCard .rc-detail {
  font-family: var(--aa-measure); font-size: var(--t-body); color: var(--aa-ink-muted);
  overflow-wrap: anywhere;
}
#rollCard.crit { border-color: var(--aa-ok); }
#rollCard.crit .rc-total { color: var(--aa-ok); }
#rollCard.fumble { border-color: var(--aa-warn); }
#rollCard.fumble .rc-total { color: var(--aa-warn); }
/* RM646: the card's one control - "I rolled real dice". The card takes no taps; this button does, and a press on
   it does not close the card first (js/player/app.js). */
#rollCard .rc-edit {
  pointer-events: auto; margin-top: var(--s-2); padding: var(--s-1) var(--s-3);
  font-family: var(--aa-operate); font-size: var(--t-ui);
}

/* ---------------- physical-dice box (RM646, js/core/physdice.js) ----------------
 * The same rules are in css/dm.css: the box opens in both windows. Above every sheet and the roll card. */
.pd-root { position: fixed; inset: 0; z-index: 300; display: flex; align-items: center; justify-content: center;
  padding: var(--s-3); }
.pd-back { position: absolute; inset: 0; background: var(--aa-recess); opacity: .84; }
.pd-box { position: relative; width: min(420px, 100%); max-height: calc(100dvh - 2 * var(--s-3)); overflow-y: auto;
  box-sizing: border-box; margin: 0;
  background: var(--aa-sheet); border: 2px solid var(--aa-ink); border-radius: 0; color: var(--aa-ink);
  padding: var(--s-3) var(--s-4) var(--s-4); font-family: var(--aa-operate); }
.pd-title { font-family: var(--aa-read); font-size: var(--t-4); font-weight: 500; margin: 0 0 var(--s-1);
  overflow-wrap: anywhere; }
.pd-hint { font-size: var(--t-ui); color: var(--aa-ink-muted); margin-bottom: var(--s-3); }
.pd-fields { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: var(--s-2);
  margin-bottom: var(--s-3); }
.pd-field { display: flex; flex-direction: column; gap: var(--s-1); font-family: var(--aa-measure);
  font-size: var(--t-meta); color: var(--aa-ink-muted); }
.pd-field input { box-sizing: border-box; width: 100%; padding: var(--s-2); text-align: center;
  font-family: var(--aa-measure); font-size: max(16px, var(--t-body)); }
.pd-field input[aria-invalid="true"] { border-color: var(--aa-warn); outline: 2px solid var(--aa-warn); }
.pd-err { color: var(--aa-warn); font-size: var(--t-meta); margin-bottom: var(--s-2); }
.pd-err:empty { display: none; }
.pd-btns { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.pd-btns .pd-ok { border-color: var(--aa-ink); font-weight: 600; }

/* RM267: TableSync was updated while this page stayed open. It stays until the
 * player reloads - a toast would be gone before a phone player looked up. */
.update-bar {
  position: fixed; left: 12px; right: 12px; z-index: 91;
  bottom: calc(var(--tabbar-h) + var(--safe-b) + 12px);
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  background: var(--aa-sheet); border: 1px solid var(--aa-rule); border-radius: 0;
  color: var(--aa-ink); font-family: var(--aa-operate); font-size: var(--t-ui);
}
.update-bar span { flex: 1 1 auto; min-width: 0; }

/* an unsaved change on the character name: a drawn mark in the margin, never
 * a tint of the name itself. */
#charName.dirty::after {
  content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 0;
  background: var(--aa-warn); margin-left: var(--s-1); vertical-align: 2px;
}

/* ---------------- level-up ---------------- */
.lu-feat { margin: var(--s-2) 0; padding: var(--s-2) 0; border-bottom: 1px solid var(--aa-hairline); }
.lu-feat b { font-family: var(--aa-read); font-size: var(--t-body); font-weight: 500; color: var(--aa-ink); }
.lu-feat .lu-txt {
  /* RM54 req 4.1/4.2: prose does not get its own scrollbar. A feature's text is
     read start to finish, so it is laid out at full height and the page carries
     it. */
  font-family: var(--aa-read); font-size: var(--t-ui); color: var(--aa-ink-body);
  margin-top: var(--s-1); line-height: 1.55;
  white-space: pre-wrap; max-width: 34rem;
}
.abil-edit { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-2); }
.abil-edit .ae {
  background: var(--aa-recess); border: 1px solid var(--aa-hairline); border-radius: 0;
  padding: var(--s-2); text-align: center;
}
.abil-edit .ae .ae-lab {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .06em; color: var(--aa-ink-muted);
}
.abil-edit .ae input, .abil-edit .ae select { width: 100%; text-align: center; margin-top: var(--s-1); }
.abil-edit .ae .ae-final {
  font-family: var(--aa-measure); font-size: var(--t-ui); color: var(--aa-ink);
  margin-top: var(--s-1); font-variant-numeric: tabular-nums;
}

/* sheet body wrapper (js/player/main.js builds title + body inside .sheet) */
.sheet-body { clear: both; }
.sheet-body > .hint { margin-bottom: var(--s-2); }
.sheet-body input[type=text],
.sheet-body select { width: 100%; margin-bottom: var(--s-2); }
.sheet-body .row input[type=number] { width: 84px; margin-bottom: 0; }
.sheet-body .row select { width: auto; margin-bottom: 0; }
.sheet-body .row input[type=text] { margin-bottom: 0; }
.sheet-body .lvhead { margin-top: var(--s-4); }
.card .row > button.grow { text-align: left; }

/* ---------------- attacks & weapons ---------------- */
.atk-mode { margin-bottom: var(--s-3); }
.atk-row {
  border: none; border-bottom: 1px solid var(--aa-hairline); border-radius: 0;
  background: none; padding: var(--s-3) 0; margin: 0;
}
.atk-head { display: flex; align-items: baseline; gap: var(--s-2); }
.atk-head .atk-name {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--aa-read); font-size: var(--t-body); font-weight: 500; color: var(--aa-ink);
}
.atk-meta {
  font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted);
  margin: var(--s-1) 0 var(--s-2); line-height: 1.5; font-variant-numeric: tabular-nums;
}
.atk-btns { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.atk-btns .atk-hit, .atk-btns .atk-dmg {
  flex: 1 1 120px; min-height: 46px;
  font-family: var(--aa-measure); font-weight: 500; font-size: var(--t-ui);
  font-variant-numeric: tabular-nums;
}
.atk-btns .atk-hit { border-color: var(--aa-ink); color: var(--aa-ink); }
.atk-btns .atk-dmg { border-color: var(--aa-rule); color: var(--aa-ink); }
/* a crit and a fumble are events, not decoration: a warn plate and a drawn
 * strike, held until the next roll. RM23 pulsed a glowing box shadow as a ring
 * here — that is slop C4 and brand 6.2 forbids it, so the state is carried by
 * the plate and the glyph alone. */
.atk-btns .is-crit {
  background: var(--aa-warn); border-color: var(--aa-warn); color: var(--aa-recess);
  font-weight: 600;
}
.atk-btns .is-fumble { border-color: var(--aa-warn); color: var(--aa-warn); }
.atk-btns .is-fumble::before {
  content: ''; display: inline-block; width: 9px; height: 2px;
  background: currentColor; margin-right: var(--s-1); vertical-align: 2px;
}
.atk-lab { min-width: 96px; flex: 0 0 auto; }
.sheet-body .row .atk-lab + input[type=text] { margin-bottom: 0; }

/* ================================================================
   INVENTORY — the pack, kept as a ledger
   ================================================================ */
.inv-coins { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--s-1); margin-bottom: var(--s-3); }
.inv-coin {
  background: var(--aa-recess); border: 1px solid var(--aa-hairline); border-radius: 0;
  text-align: center; padding: var(--s-2) var(--s-1); min-width: 0;
}
.inv-coin b {
  display: block;
  font-family: var(--aa-measure); font-size: var(--t-4); font-weight: 500; color: var(--aa-ink);
  overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums;
}
.inv-coin span {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .06em; color: var(--aa-ink-muted);
}
/* gold and platinum are the columns a purse is actually read by: an ink rule
 * and ink weight, not a tint. */
.inv-coin.gp, .inv-coin.pp { border-color: var(--aa-rule); }
.inv-coin.gp b, .inv-coin.pp b { color: var(--aa-ink); font-weight: 600; }
.inv-clab {
  flex: 0 0 auto; min-width: 52px;
  font-family: var(--aa-measure); font-size: var(--t-meta); letter-spacing: .06em;
  text-transform: uppercase; color: var(--aa-ink-muted);
}
.inv-count {
  font-family: var(--aa-measure); font-size: var(--t-4); color: var(--aa-ink);
  min-width: 34px; text-align: center; font-variant-numeric: tabular-nums;
}
.inv-warn {
  color: var(--aa-warn); background: var(--aa-recess); border: 1px solid var(--aa-hairline);
  border-radius: 0; padding: var(--s-2) var(--s-3);
  font-family: var(--aa-operate); font-size: var(--t-ui); line-height: 1.5;
  margin: var(--s-2) 0; display: flex; align-items: flex-start; gap: var(--s-2);
}
.inv-warn::before {
  content: ''; flex: 0 0 auto; margin-top: 5px; width: 0; height: 0;
  border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-bottom: 8px solid var(--aa-warn);
}

.inv .inv-qty {
  font-family: var(--aa-measure); font-size: var(--t-ui); color: var(--aa-ink-muted);
  flex: 0 0 auto; font-variant-numeric: tabular-nums;
}
.inv-body { padding: var(--s-1) 0 var(--s-3) 14px; }
/* RM792 (ECONOMY-08): what sits in a container, under it */
.inv-inside { margin-left: var(--s-3); padding-left: var(--s-3); border-left: 2px solid var(--aa-rule); }
/* RM792 (ECONOMY-03): the "pay it with change?" offer inside the Spend / Give sheet */
.inv-change { margin-top: var(--s-3); padding: var(--s-2) var(--s-3); border: 1px solid var(--aa-rule); border-radius: 6px; }
.inv-change button { margin-top: var(--s-2); width: 100%; }
.inv-det { margin: var(--s-2) 0 var(--s-1); }
.inv-det h5 {
  margin: var(--s-4) 0 var(--s-1);
  font-family: var(--aa-read); font-weight: 500; font-size: var(--t-body);
  color: var(--aa-ink);
  display: flex; align-items: baseline; gap: var(--s-2);
}
/* the item's own record: label in the measure face, value in the read face */
.inv-kv { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-5); font-size: var(--t-ui); }
.inv-kv span { min-width: 0; }
.inv-kv b {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  color: var(--aa-ink-muted); font-weight: 400; margin-right: var(--s-1);
}
.inv-ul { margin: var(--s-1) 0 0; padding-left: var(--s-5); font-size: var(--t-ui); }
/* the item description: prose, on a measure, with the drop cap (req 1.8) */
.inv-txt {
  font-family: var(--aa-read); font-size: var(--t-body); line-height: 1.65;
  color: var(--aa-ink-body); margin-top: var(--s-2); max-width: 34rem;
}
.inv-txt p { margin: var(--s-3) 0; }
.inv-txt > p:first-child::first-letter {
  font-family: var(--aa-read); font-weight: 600; font-size: var(--t-2);
  line-height: .82; float: left; padding: 0 var(--s-2) 0 0; color: var(--aa-ink);
}

/* RM54 req 4.1: search results are capped in JS (first 8 + "Show all"), not
   by a nested scroll box. */
.inv-results { margin-top: var(--s-2); }
.inv-res {
  display: flex; align-items: center; gap: var(--s-2); padding: var(--s-3) 0; min-height: 46px;
  border-bottom: 1px solid var(--aa-hairline); cursor: pointer;
}
.inv-res:hover { background: var(--aa-recess); }
.inv-res .nm {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--aa-read); font-size: var(--t-body); font-weight: 500; color: var(--aa-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.inv-res .meta { flex: 0 0 auto; font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted); }

/* something the DM just handed you: a slip laid on the page, committed to a
 * rule rather than lifted on a shadow. It leaves by being removed, not by
 * fading to nothing in place (slop G2) — js/player/inventory.js removes the
 * node on its own timer, so .out only has to stop drawing it. */
.inv-arrive {
  position: fixed; left: 50%; transform: translateX(-50%);
  top: calc(var(--topbar-real-h) + 10px); z-index: 70;   /* RM343: the measured header, status bar included */
  width: min(420px, calc(100vw - 24px));
  display: flex; flex-direction: column; gap: var(--s-1);
  background: var(--aa-sheet); border: 1px solid var(--aa-rule); border-radius: 0;
  color: var(--aa-ink); padding: var(--s-3); cursor: pointer;
}
.inv-arrive b { font-family: var(--aa-read); font-size: var(--t-body); font-weight: 500; color: var(--aa-ink); }
.inv-arrive span { font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted); }
.inv-arrive.taken { border-color: var(--aa-hairline); }
.inv-arrive.taken b { color: var(--aa-ink-muted); text-decoration: line-through; }
.inv-arrive.out { display: none; }
@media (min-width: 768px) { .inv-arrive { top: calc(var(--topbar-real-h) + 54px); } }   /* RM343 */
/* RM366: the slip is a notice, not a lid. It lay over the top of the open tab for
 * five seconds - on Abilities over "Change ability scores" - and a tap meant for
 * that button only dismissed the slip. While a slip shows, js/player/inventory.js
 * puts its lower edge (measured from the top of #panes) in --inv-arrive-room and
 * the open tab's page starts below it. The board tabs (.pane-fill) draw edge to
 * edge and are left as they were. */
#panes.inv-arriving > .pane:not(.pane-fill) { padding-top: calc(var(--inv-arrive-room, 0px) + var(--s-3)); }

/* ================================================================
   CHARACTER BUILDER — guided flow, rich pickers, spell steps
   ================================================================ */
.steps { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0; }
.steps .step {
  flex: 1 1 auto; min-width: 84px; min-height: 44px;
  font-family: var(--aa-operate); font-size: var(--t-ui); letter-spacing: .01em;
  padding: var(--s-1) var(--s-2);
  border: 1px solid var(--aa-hairline); border-radius: 0; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; color: var(--aa-ink-muted);
  display: flex; align-items: center;
}
/* done / outstanding / here: a drawn mark plus a colour, never a colour alone */
.steps .step.ok { color: var(--aa-ok); }
.steps .step.ok::before {
  content: ''; display: inline-block; width: 6px; height: 10px; flex: 0 0 auto;
  border-right: 2px solid var(--aa-ok); border-bottom: 2px solid var(--aa-ok);
  transform: rotate(45deg) translate(-1px, -2px); margin-right: var(--s-2);
}
.steps .step.todo { color: var(--aa-warn); }
.steps .step.todo::before {
  content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 0;
  background: var(--aa-warn); margin-right: var(--s-2); flex: 0 0 auto;
}
.steps .step.now {
  border-color: var(--aa-ink); background: var(--aa-ink); color: var(--aa-sheet); font-weight: 500;
}
.steps .step.now::before { display: none; }

.issues { margin-top: var(--s-2); display: flex; flex-direction: column; gap: var(--s-2); }
.issue {
  border: 1px solid var(--aa-hairline); border-radius: 0;
  background: var(--aa-recess); padding: var(--s-2) var(--s-3) var(--s-2) var(--s-6);
  font-family: var(--aa-operate); font-size: var(--t-ui); line-height: 1.5;
  min-height: 44px; cursor: pointer; position: relative;
}
.issue::before {
  content: ''; position: absolute; left: var(--s-3); top: 14px;
  width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-bottom: 8px solid var(--aa-warn);
}
.issue b { color: var(--aa-warn); display: block; font-weight: 500; }
.issue span { color: var(--aa-ink-muted); }
.ok-note {
  border: 1px solid var(--aa-hairline); border-radius: 0; background: var(--aa-recess);
  color: var(--aa-ink-body); font-family: var(--aa-operate); font-size: var(--t-ui);
  padding: var(--s-2) var(--s-3) var(--s-2) var(--s-6); margin-bottom: var(--s-2); position: relative;
}
.ok-note::before {
  content: ''; position: absolute; left: var(--s-3); top: 13px;
  width: 6px; height: 10px; border-right: 2px solid var(--aa-ok); border-bottom: 2px solid var(--aa-ok);
  transform: rotate(45deg) translate(-1px, -2px);
}

/* RM452 (UX-3): a debt this step's choice just put on a step already walked past. It reads
   as a note, not a refusal - nothing here is wrong yet - so it takes .ok-note's frame with
   the warn rule down its left edge, and its jump control sits under the sentence. */
.wiz-carry {
  border: 1px solid var(--aa-hairline); border-left: 3px solid var(--aa-warn); border-radius: 0;
  background: var(--aa-recess); color: var(--aa-ink-body);
  font-family: var(--aa-operate); font-size: var(--t-ui); line-height: 1.5;
  padding: var(--s-2) var(--s-3); margin-top: var(--s-2); margin-bottom: var(--s-2);
}
.wiz-carry button { margin-top: var(--s-2); }

.pk-search, input[type=search] {
  width: 100%; margin-bottom: var(--s-2); min-height: 44px;
  background: var(--aa-recess); color: var(--aa-ink);
  border: 1px solid var(--aa-hairline); border-radius: 0;
  padding: var(--s-2) var(--s-3);
  font-family: var(--aa-operate); font-size: var(--t-ui);
  -webkit-appearance: none; appearance: none;
}
.pk-search:focus { outline: none; border-color: var(--aa-ink); }
/* RM343: iOS zooms the page when a field under 16px takes focus, and with
   html/body overflow hidden it stayed zoomed - the sheet ran off the side of the
   phone. A touch screen gets 16px fields; a mouse keeps the 14px interface size. */
@media (pointer: coarse) {
  input[type=text], input[type=number], input[type=search], input:not([type]),
  select, textarea, .pk-search {
    font-size: max(16px, var(--t-ui));
  }
}
.pk-filters { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; margin-bottom: var(--s-2); }
.pk-filters .seg { display: flex; flex-wrap: wrap; gap: var(--s-1); }
.pk-filters .seg button { min-height: 44px; }
.pk-filters .pk-ua { min-height: 44px; }
/* L-04 (RM784): at phone widths the UA switch takes its own line, at the left - it ran off the right edge beside the
   three edition buttons, and once the list scrolled the sheet's sticky back button sat on top of it */
@media (max-width: 480px) { .pk-filters .pk-ua { flex: 1 1 100%; } }
/* L-05 (RM784): picker sheets are full height from the start (character.js tallSheet) */
#sheetHost .sheet.sheet-tall { height: 86%; height: calc((var(--vvh, 100vh)) * 0.86); }
@media (min-width: 768px) { #sheetHost .sheet.sheet-tall { height: 84%; height: calc((var(--vvh, 100vh)) * 0.84); } }
/* RM470: the type filter (browsePicker opts.kinds) - a labelled native select that takes the rest of
   the filter row, and the whole row on a phone */
.pk-filters .pk-kind { display: flex; align-items: center; gap: var(--s-2); flex: 1 1 14rem; min-height: 44px; }
.pk-filters .pk-kind select { flex: 1 1 auto; width: auto; min-width: 0; margin-bottom: 0; min-height: 44px; }
/* RM470: a character option's Remove control, inside its opened row, on the prose's axis */
.feat .charopt-act { padding: 0 0 var(--s-3) var(--rec-indent); }
/* the folio line: how many records this list is showing, in the measure face */
.pk-count {
  font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted);
  padding: var(--s-1) 0 var(--s-2); border-bottom: 1px solid var(--aa-hairline);
  margin-bottom: var(--s-2); font-variant-numeric: tabular-nums;
}
.pk-count.over { color: var(--aa-warn); }
.pk-count.over::before {
  content: ''; display: inline-block; width: 9px; height: 2px;
  background: currentColor; margin-right: var(--s-1); vertical-align: 2px;
}
.pk-list { display: block; }
.pk-more { width: 100%; margin-top: var(--s-1); }
.pk-more.hidden { display: none; }

/* RM61 - the spell picker's level filter.
   Wrapped, never a horizontal scroller: a strip that scrolls sideways inside a
   sheet that scrolls down is the nested-scroller trap RM54 removed everywhere
   else, and the eleven chips fit two lines on the narrowest phone anyway.
   The active chip inverts its fill - aria-pressed carries the state for anyone
   the fill does not reach, so the selection is never colour alone (RM41 5.4). */
.pk-lvls { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-bottom: var(--s-2); }
.pk-lvl {
  min-height: 44px; padding: var(--s-1) var(--s-2);
  background: var(--aa-recess); color: var(--aa-ink-muted);
  border: 1px solid var(--aa-hairline); border-radius: 0;
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .04em; text-transform: uppercase; cursor: pointer;
}
.pk-lvl.on {
  background: var(--aa-ink); color: var(--aa-sheet); border-color: var(--aa-ink);
}
.pk-matches {
  margin: 0 0 var(--s-2); font-variant-numeric: tabular-nums;
}

.pk2 > summary {
  list-style: none; cursor: pointer; min-height: 48px;
  /* RM75: the same indent the feat/spell/inv rows take. The pilcrow rule is
     SHARED with those three and RM74 gave it a negative margin to sit in the
     gutter - without the matching padding here the picker's pilcrow hung 22px
     outside its own row. Sharing a rule means sharing both halves of it. */
  padding: var(--s-2) 0 var(--s-2) var(--rec-indent);
  display: flex; align-items: flex-start; gap: var(--s-2);
  /* RM64: the name column and the stat line sit side by side until they cannot.
     Without this they never wrapped, so on a 320px screen the source badge ran
     straight into the description - measured collisions of 4 to 26px. Wrapping
     lets the stat line drop to its own row instead of overprinting. */
  flex-wrap: wrap;
}
.pk2 > summary::-webkit-details-marker { display: none; }
.pk2-head { display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap;
  /* min-width:0 or a long name refuses to shrink and pushes the stat line out */
  flex: 1 1 11rem; min-width: 0; }
.pk2-name {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--aa-read); font-size: var(--t-body); font-weight: 500; color: var(--aa-ink);
}
.pk2[open] .pk2-name { color: var(--aa-accent); }
/* already taken: a filled gutter mark and ink weight, not a tint */
.pk2.picked .pk2-name { font-weight: 600; }
.pk2.picked .pk2-name::before {
  content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 0;
  background: var(--aa-ink); margin-right: var(--s-2); vertical-align: 1px;
}
.pk2-badges { display: flex; flex-wrap: wrap; gap: var(--s-1); flex: 0 0 auto; }
.pk2-meta {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  color: var(--aa-ink-muted); line-height: 1.5; word-break: break-word;
  flex: 1 1 10rem; min-width: 0;
}
/* the summary's own affordance line, in the measure face at meta. Factual and
 * device-neutral: this app runs under a thumb and under a mouse (req 1.11). */
.pk2 > summary .pk2-head::after {
  content: 'read';
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .06em; color: var(--aa-ink-muted); flex: 0 0 auto;
}
.pk2[open] > summary .pk2-head::after { content: 'collapse'; }
/* RM688 (PL-18): a spell row's name and its "read" share the first line and the badges take their own line below,
   so a row with three badges lines up with a row with one (they pushed "read" onto a line of its own). */
.pk2-head.sp-pick-head .pk2-name { order: 1; }
.pk2 > summary .pk2-head.sp-pick-head::after { order: 2; }
.pk2-head.sp-pick-head .pk2-badges { order: 3; flex: 1 0 100%; }
.pk2-body {
  font-family: var(--aa-read); font-size: var(--t-body); line-height: 1.65;
  color: var(--aa-ink-body);
  /* RM54 req 4.1: a spell's description is prose; it scrolls with the page. */
  /* RM75: its OWN indent - .pk2-body is a SIBLING of the summary, not inside
     it, so the summary's padding does not reach it. Zeroing this put the
     description 22px left of the name it belongs to. */
  padding: 0 0 var(--s-3) var(--rec-indent); max-width: 34rem;
}
.pk2[open] > summary { border-bottom: 1px solid var(--aa-hairline); margin-bottom: var(--s-2); }
.pk2-take {
  width: 100%; min-height: 44px; margin: 0 0 var(--s-3);
  /* RM64. An OPAQUE surface, unlike every other secondary button in this app.
     Brand 6.4 makes secondary controls unfilled, which is right when a control
     sits on the page and wrong when it can ever land on top of words: two
     unfilled layers read through each other and neither is legible. The rule
     below stops that happening, and this is the belt to its braces. */
  background: var(--aa-sheet);
}

/* RM64 - the picker's Choose button belongs to the row it chooses.
 *
 * A <details> that is closed sizes to its <summary>, but this button is a
 * sibling of that summary and was still being laid out - so it rendered BELOW
 * the row's own box, on top of the next entry's name, badges and description.
 * Measured at 320px: a 148x44 collision with the next row's .pk2-meta. It was
 * happening at every width; a narrow screen only makes it obvious.
 *
 * It was also a mis-selection waiting to happen. The button reading "Choose
 * Aarakocra" was sitting over the NEXT entry's text, so a tap aimed at what
 * you were reading chose the record above it.
 *
 * Hiding it while the row is closed fixes both. Choosing now means opening the
 * entry first, which is the interaction the <details> row was already asking
 * for - the summary says "read" and the body is the thing you read before
 * committing. */
.pk2:not([open]) .pk2-take { display: none; }
/* L-06 (RM784): the one-tap choose box at the start of a picker row. Square, a full thumb target, outlined until
   the row is chosen; it sits in the summary, so the row's name and "read" still open the description. */
.pk2-tick {
  flex: 0 0 auto; width: 44px; height: 44px; min-height: 44px; padding: 0; margin: -2px 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.15rem; line-height: 1; background: var(--aa-sheet);
}
.pk2-tick.active { background: var(--aa-ink); color: var(--aa-sheet); border-color: var(--aa-ink); }
.pk2-tick.warn { opacity: .55; }
.pk2 > summary .pk2-head { align-self: center; }
/* RM784 (PB-20): the chooser's "Delete a character from this device" list, under More */
.picker-delete > summary { cursor: pointer; min-height: 44px; display: flex; align-items: center; }
.picker-delete .row { align-items: center; }

/* a key/value record: measure-face label, read-face value, hairline between */
.kv { display: flex; gap: var(--s-2); padding: var(--s-1) 0; border-bottom: 1px solid var(--aa-hairline); }
.kv:last-child { border-bottom: none; }
.kv-k {
  flex: 0 0 104px;
  font-family: var(--aa-measure); font-size: var(--t-meta); letter-spacing: .06em;
  text-transform: uppercase; color: var(--aa-ink-muted); padding-top: 3px;
}
.kv-v {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink-body);
  word-break: break-word;
}
.det-feat { margin: var(--s-2) 0; padding: var(--s-2) 0; border-bottom: 1px solid var(--aa-hairline); }
.det-feat b { font-family: var(--aa-read); font-size: var(--t-body); font-weight: 500; color: var(--aa-ink); }
/* RM66: the provenance caption lifted out of the prose (see featBox). Measure
   face at the caption step, the same register every other piece of metadata in
   this app uses, so a feature box reads title / caption / prose every time. */
.det-meta {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .04em; color: var(--aa-ink-muted);
  margin-top: 2px;
}
.det-txt {
  font-family: var(--aa-read); font-size: var(--t-ui); color: var(--aa-ink-body);
  line-height: 1.55; margin-top: var(--s-1); white-space: pre-wrap;
  word-break: break-word; max-width: 34rem;
}
.det-warn {
  margin: var(--s-2) 0; padding: var(--s-2) var(--s-3) var(--s-2) var(--s-5); border-radius: 0;
  border: 1px solid var(--aa-hairline); background: var(--aa-recess);
  color: var(--aa-warn); font-family: var(--aa-operate); font-size: var(--t-ui);
  position: relative;
}
.det-warn::before {
  content: ''; position: absolute; left: var(--s-2); top: 12px;
  width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-bottom: 7px solid var(--aa-warn);
}

.lu-note {
  border: 1px solid var(--aa-hairline); border-radius: 0; background: var(--aa-recess);
  color: var(--aa-warn); font-family: var(--aa-operate); font-size: var(--t-ui); line-height: 1.5;
  padding: var(--s-2) var(--s-3) var(--s-2) var(--s-6); margin-bottom: var(--s-2); position: relative;
}
.lu-note::before {
  content: ''; position: absolute; left: var(--s-3); top: 14px;
  width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-bottom: 8px solid var(--aa-warn);
}
.lu-spells .hint { white-space: pre-wrap; }
.lu-choose { margin-top: var(--s-2); }
.lu-choose button { text-align: left; min-height: 44px; }
/* RM686 (PL-51): a level that cannot be confirmed yet says why ON the button - a disabled Confirm reads as a sentence
   in the warn register, not a faded button - and the section it points at carries the unset mark a chooser row
   already uses (button.chooser.is-unset). */
button.wiz-next:disabled {
  opacity: 1; background: var(--aa-recess); border-color: var(--aa-warn); color: var(--aa-warn);
}
button.is-unset:not(.chooser), select.is-unset { border-color: var(--aa-warn); box-shadow: inset 3px 0 0 0 var(--aa-warn); }
.card.is-unset { box-shadow: inset 3px 0 0 0 var(--aa-warn); }
/* RM686 (PL-12): an option's own rule text under the choice chips */
.choice-detail { margin-top: var(--s-2); }
.choice-detail b { font-weight: 600; color: var(--aa-ink); }

.wiz-title {
  font-family: var(--aa-read); font-weight: 500; font-size: var(--t-4);
  letter-spacing: -.005em; color: var(--aa-ink); margin: var(--s-1) 0 var(--s-2);
  display: flex; align-items: baseline; gap: var(--s-2);
}
.wiz-pane { margin-bottom: var(--s-3); }
.wiz-pane .card { margin-bottom: var(--s-3); }
.wiz-nav {
  position: sticky; bottom: 0; background: var(--aa-sheet);
  padding-top: var(--s-2); border-top: 1px solid var(--aa-hairline);
}
.wiz-nav button { min-height: 46px; }
/* RM790 (ECONOMY-14): a spell picker's Done sat under every row (7,800 px down after picks at the top); it stays in
   view at the bottom of the sheet now, like the build's step nav */
.pick-done { position: sticky; bottom: 0; z-index: 2; width: 100%; min-height: 46px; margin-top: var(--s-2);
  box-shadow: 0 -8px 12px -6px rgba(0, 0, 0, .25); }
/* RM688 (PL-21): the guided build holds ONE height from step to step - its top edge no longer jumps as the steps
   change length - and its content is padded by the sticky nav's height (--wiz-nav-h: the 46px buttons, the 8px top
   padding and the 1px rule), with the same scroll padding, so the last card and a focused field clear the nav. */
#sheetHost .sheet.sheet-wiz {
  --wiz-nav-h: 56px;
  height: 86%;
  height: calc((var(--vvh, 100vh)) * 0.86);
  scroll-padding-bottom: calc(var(--wiz-nav-h) + var(--s-2));
}
@media (min-width: 768px) {
  #sheetHost .sheet.sheet-wiz { height: 84%; height: calc((var(--vvh, 100vh)) * 0.84); }
}
.sheet-wiz .wiz-nav { min-height: var(--wiz-nav-h); }
.sheet-wiz .wiz-pane { margin-bottom: 0; padding-bottom: var(--wiz-nav-h); }
/* RM790 (BUILD2-14): the sticky nav stopped at the sheet's bottom padding, and the Class step's SKILLS / SPELLCASTING
   rows showed through the gap under the buttons on a phone. The bar now reaches the sheet's bottom edge. */
.sheet-wiz .wiz-nav { bottom: calc(-1 * (var(--s-5) + var(--safe-b))); padding-bottom: calc(var(--s-5) + var(--safe-b)); }

.chips { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-top: var(--s-2); }
/* a static chip is a label, not a control: brand's chip spec, no radius */
.chip-static {
  font-family: var(--aa-measure); font-size: var(--t-meta);
  padding: var(--s-1) var(--s-2); border-radius: 0;
  border: 1px solid var(--aa-hairline); background: none; color: var(--aa-ink-body);
  word-break: break-word;
}
.granted-row {
  display: flex; align-items: center; gap: var(--s-2); min-height: 44px;
  padding: var(--s-2) 0; border-bottom: 1px solid var(--aa-hairline);
}
.granted-row:last-of-type { border-bottom: none; }

.chooser-lab { flex: 0 0 92px; font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted); }
/* RM64 - the build's selection boxes.
 *
 * They were ink text on nothing, inside a 1px --aa-rule border: correct for a
 * secondary control under brand 6.4, and too quiet for the ONE control a step
 * exists to make you press. A build step with a single job should not make you
 * hunt for it.
 *
 * Two changes. The box becomes a recessed well, so it reads as a slot with
 * something to put in it rather than a line of text. And a slot that is still
 * EMPTY carries --aa-warn: not a new colour role, but the register that
 * already means "this needs your attention" where it marks a count over its
 * limit. A filled slot drops back to ink, because a decision that has been
 * made is not outstanding. The placeholder still reads "choose a race..." so
 * the state never rests on colour alone (RM41 req 5.4). */
button.chooser {
  min-height: 46px; line-height: 1.35; white-space: normal; word-break: break-word;
  background: var(--aa-recess);
  border-color: var(--aa-rule);
  font-family: var(--aa-read); font-size: var(--t-body);
}
button.chooser:hover { border-color: var(--aa-ink); background: var(--aa-recess); }

button.chooser.is-unset {
  border-color: var(--aa-warn);
  color: var(--aa-warn);
}
/* the left edge carries the same signal at a glance down a column of rows */
button.chooser.is-unset { box-shadow: inset 3px 0 0 0 var(--aa-warn); }
button.chooser.is-unset:hover { border-color: var(--aa-warn); color: var(--aa-warn); }

/* narrow phones: geometry tightens, type does not (the 12px floor is absolute
 * and functional text stays at 14px — handoff trap 2). */
@media (max-width: 420px) {
  .kv-k { flex-basis: 84px; }
  .steps .step { min-width: 72px; }
  .chooser-lab { flex-basis: 100%; }
}
/* RM784 (PB-21): on a phone the build's step chips are ONE row that scrolls sideways (the current step is scrolled
   into view), not three rows above every step's first control */
@media (max-width: 600px) {
  /* RM786 (journey_screens): overflow-y hidden - with overflow-x auto the browser turns a visible overflow-y into auto,
     which made the one-row strip a pane that could scroll both ways (journey_screens' overflow check) */
  .steps.wiz-steps { flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
    -webkit-overflow-scrolling: touch; scroll-snap-type: x proximity; }
  .steps.wiz-steps::-webkit-scrollbar { display: none; }
  .steps.wiz-steps .step { flex: 0 0 auto; min-width: 0; scroll-snap-align: center; }
}
.issues-more > summary { cursor: pointer; font-size: var(--t-meta); color: var(--aa-ink-muted); min-height: 32px;
  display: flex; align-items: center; }
.issues-more[open] { display: flex; flex-direction: column; gap: var(--s-2); }
/* RM64: the 50vh clamp that used to live here is GONE.
 *
 * It set max-height on .wiz-preview and .pk2-body above 900px and set no
 * overflow with it. A max-height without an overflow does not scroll and does
 * not clip - it only makes the box report a height its content does not fit
 * in, and `overflow: visible` then paints the rest straight over whatever
 * follows. On a 1072x684 window that meant a 342px box holding 514px of race
 * detail, with the remainder landing on top of the next three rows: the
 * "scrunched text" that was reported.
 *
 * It also only bit at >=900px, which is why narrow-phone testing never saw it,
 * and it got WORSE as the window got shorter, because the clamp is measured in
 * vh - the opposite of what a reader on a small screen needs.
 *
 * Removing it restores what .pk2-body's own comment already says this should
 * do (RM54 req 4.1: "a spell's description is prose; it scrolls with the
 * page") and what it already did below 900px. The sheet is the scroller; the
 * panel inside it is not, which also keeps this clear of the nested-scroller
 * rule RM54 set. */

/* ---------------- RM54 -----------------------------------------------------
   Four small additions, all inside the existing Ledger register: no new colour,
   no radius, no shadow. */

/* req 5.1 — the how-to-roll line at the top of Abilities. Set as marginalia,
   which is what it is: an instruction about the page, not content on it. */
.tf-howroll {
  font-style: italic; margin: 0 0 var(--s-3);
  padding-left: 14px; border-left: 2px solid var(--aa-hairline);
}

/* req 5.2 — the rename affordance. It sits beside the field it points at, so it
   must not outweigh it: no accent, no fill. */

/* req 5.3 — the build card's action row. Wraps rather than scrolls sideways at
   375px, where four controls do not fit on one line. */

/* req 5.5 — the portrait. Square, hard-edged, ruled like every other plate in
   this register. 64px is a face at a glance, not a feature demanding the top of
   the sheet. */
.portrait {
  width: 64px; height: 64px; border-radius: 0;
  border: 1px solid var(--aa-rule); object-fit: cover; display: block;
}

/* ---------------- RM55 ---------------------------------------------------- */

/* req 3.1 — the name as a control. It must still READ as the character's name
   (it is the largest thing in the header) while showing it can be opened, so
   the affordance is a caret and a hairline underline, not a button chrome. */
#charName {
  /* Button reset only. NO `font:` shorthand here — it resets font-size and, being
     later in the file, silently beat the masthead rule above that sets --t-4.
     Typography for this element lives in one place, with the other header type. */
  display: inline-flex; align-items: baseline; gap: var(--s-2);
  background: none; border: 0; border-radius: 0; padding: 0;
  color: inherit; text-align: left;
  border-bottom: 1px solid var(--aa-rule); cursor: pointer;
}
/* RM66: a MENU mark, not a caret.
   The empty-state prompt told players to press a hamburger, and the control
   was a downward triangle - so the instruction named something that was not on
   screen. A triangle also promises a dropdown of values; this opens a menu of
   actions. Three rules, drawn with borders so there is no glyph and no font
   dependency, sized against the name it sits beside. */
#charCaret {
  display: inline-block; flex: 0 0 auto;
  width: 15px; height: 11px;
  border-top: 2px solid var(--aa-ink-muted);
  border-bottom: 2px solid var(--aa-ink-muted);
  position: relative;
}
/* RM791 (ECONOMY-20): a long name beside the Handouts button pushed the mark past the control's clipped edge - it
   read as three dots, not a menu. The control may shrink, the NAME takes the ellipsis, the mark keeps its size. */
#charName { min-width: 0; max-width: 100%; }
#charNameText { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#charCaret::before {
  content: ''; position: absolute; left: 0; right: 0; top: 50%;
  margin-top: -1px; height: 2px; background: var(--aa-ink-muted);
}
#charName[aria-expanded="true"] #charCaret,
#charName[aria-expanded="true"] #charCaret::before { border-color: var(--aa-ink); background: var(--aa-ink); }
#charName[aria-expanded="true"] #charCaret { background: none; }

/* req 2.1 — the read-only ability strip on the play sheet */

/* req 4.3 — the random control at the head of the first build step */
.wiz-random { align-items: center; gap: var(--s-3); flex-wrap: wrap; margin-bottom: var(--s-3); }

/* RM123: the two routes that moved off the Character tab when its duplicate
   ability strip and skill grid came off it. Quiet: they are a way back to the
   build, not something to press every round. */
.tf-editscores { margin: 0 0 var(--s-3); }

/* RM184: a checkbox label is the tap target, not the 20px box inside it */
label.tap-check { display: inline-flex; align-items: center; gap: var(--s-2, 8px); min-height: 40px; padding-right: 8px; cursor: pointer; }
label.tap-check input[type=checkbox] { width: 24px; height: 24px; margin: 0; }

/* RM190: conditions on this character's token, a row under the header plates */
/* RM500: the turn strip - round, whose turn, who is next; inverted when it is this player's */
#hdrTurn {
  padding: 6px 12px; font-size: var(--t-meta); line-height: 1.3; color: var(--aa-ink-body);
  border-top: 1px solid var(--aa-hairline); font-variant-numeric: tabular-nums;
}
#hdrTurn b { color: var(--aa-ink); }
#hdrTurn.mine { background: var(--aa-ink); color: var(--aa-sheet); }
#hdrTurn.mine b { color: var(--aa-sheet); }
#hdrTurn { display: flex; flex-wrap: wrap; align-items: center; gap: 0 4px; }
#hdrTurn.hidden { display: none; }
#hdrInitBtn { margin-left: auto; min-height: 44px; }
/* The INIT figure on the sheet is a roll, like the saves under it */
.derived .dv.dv-roll { cursor: pointer; }
#hdrConds { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 12px 8px; }
/* RM781 (PP-08): a feat choice still to make, on the Feats card; (PP-09) what the DM's conditions do to a roll */
.feat-pending { align-items: center; gap: var(--s-2); }
.hint.warn, .hint.cond-note { color: var(--aa-warn); }
/* RM823: an ability score outside its method's range, said while it is typed */
.abil-edit .ae input[aria-invalid="true"] { border-color: var(--aa-warn); outline: 2px solid var(--aa-warn); }
/* RM781 (PP-05): a pool resource (Lay on Hands) - a count and a Spend button, no pips */
.res-pool { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.res-pool .res-count { flex: 1 1 auto; }
/* RM781 (PP-17): the roll card's Lucky button under the ✎ */
#rollCard .rc-lucky { margin-top: var(--s-2); }
/* RM781 (PP-13): a fight on a phone - the header took 285-322 px of 844. The banner goes, the plates share one row
   (six narrow cells, the figure a step smaller), so HP, AC, the turn and the conditions fit in about half that. */
@media (max-width: 520px) {
  body.in-fight #topbar { padding-top: calc(var(--safe-t) + var(--s-2)); }
  body.in-fight #topbar::before, body.in-fight #topbar::after { display: none; }
  body.in-fight #charSummary { min-height: 44px; }
  body.in-fight #plates, body.in-fight #plates.plates-4 { grid-template-columns: none; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: column; }
  body.in-fight .cs, body.in-fight .vt { padding: 2px; height: calc(var(--plate-h) - 6px); }
  body.in-fight .cs-val, body.in-fight .vt-val { font-size: var(--t-body); }
  body.in-fight .cs-lab, body.in-fight .vt-lab { letter-spacing: .02em; }
  /* the round shares the turn bar's row instead of standing alone over it */
  body.in-fight #hdrTurn { flex: 0 0 auto; border-top: 0; padding: 4px 8px; }
  body.in-fight #turnBar { flex: 1 1 0; min-width: 0; border-top: 0; }
  body.in-fight #turnBar { min-width: 220px; }
  body.in-fight #hdrConds, body.in-fight #hdrOffline, body.in-fight #ccRecBadge { flex: 1 1 100%; }
  body.in-fight #hdrConds { padding: 2px 12px 6px; }
}
#hdrConds .cond-chip.down { border-color: var(--aa-warn); color: var(--aa-warn); }
/* RM781 (PP-14): the Sheet tab's "you are down" card */
.dying-card { border: 2px solid var(--aa-warn); }
.dying-card .dying-tally { font-family: var(--aa-measure); font-size: var(--t-body); color: var(--aa-ink); margin: var(--s-1) 0 var(--s-2); }
/* RM781 (PP-01): changes kept on this phone while the DM's server cannot be reached */
#hdrOffline { flex: 1 1 100%; padding: 4px 12px 6px; font-size: var(--t-ui); color: var(--aa-warn); border-top: 1px solid var(--aa-rule); }
#hdrOffline[hidden] { display: none; }
/* RM516: a handout from the DM, over whatever tab is open */
.ho-phone {
  position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center;
  background: var(--aa-ground); padding: var(--s-4);
}
.ho-phone .ho-box {
  max-width: 560px; width: 100%; max-height: 100%; overflow-y: auto; background: var(--aa-sheet);
  border: 1px solid var(--aa-rule); padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-3);
}
.ho-phone .ho-title { margin: 0; font-family: var(--aa-read); font-size: var(--t-3); color: var(--aa-ink); }
.ho-phone .ho-img { max-width: 100%; height: auto; }
.ho-phone .ho-text { margin: 0; color: var(--aa-ink-body); line-height: 1.5; }
.ho-phone .ho-close { align-self: flex-end; }
/* RM795 (PREP-07): Close and "Later (n in Handouts)" side by side, both full touch targets */
.ho-phone .ho-btns { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--s-2); }
/* RM798 (ECONOMYR-05): a 32-line stock list was taller than the phone and Close started half cut off at the bottom
   edge; the buttons now stay on screen at the foot of the card while the list scrolls under them */
.ho-phone .ho-btns { position: sticky; bottom: calc(-1 * var(--s-4)); margin: 0 calc(-1 * var(--s-4)) calc(-1 * var(--s-4));
  padding: var(--s-2) var(--s-4) var(--s-4); background: var(--aa-sheet); border-top: 1px solid var(--aa-rule); }
/* (sticky measures from the box's content edge, so the bar reaches down over the box's own bottom padding) */
.ho-phone .ho-btns button { min-height: 44px; }
/* RM680 (SEC-04, DM-29): the card says how many handouts wait behind it; the header's Handouts button (unread count)
   and the list it opens */
.ho-phone .ho-more { margin: 0; color: var(--aa-ink-muted); }
#inboxBtn { flex: 0 0 auto; margin-left: var(--s-2); min-height: 44px; display: inline-flex; align-items: center;
  gap: var(--s-1); font-size: var(--t-ui); }
#inboxBtn[hidden] { display: none; }
#inboxBtn .ib-n { min-width: 1.5em; padding: 0 var(--s-1); background: var(--aa-ink); color: var(--aa-sheet);
  font-family: var(--aa-measure); font-variant-numeric: tabular-nums; text-align: center; }
#inboxBtn .ib-n[hidden] { display: none; }
.inbox-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2); width: 100%; min-height: 44px;
  margin-bottom: var(--s-1); text-align: left; }
.inbox-row .ir-what { flex: 1 1 auto; }
.inbox-row.unread .ir-what { font-weight: 600; }
.inbox-row .ir-when { font-family: var(--aa-measure); font-variant-numeric: tabular-nums; color: var(--aa-ink-muted); }
.inbox-row .ir-note { flex: 1 1 100%; color: var(--aa-ink-muted); }
/* RM649: the DM asks for a roll - over whatever tab is open, above a handout, under the dice box (z 300).
   Its own classes: the handout listener removes any .ho-phone it finds (js/player/main.js). */
.ask-phone {
  position: fixed; inset: 0; z-index: 210; display: flex; align-items: center; justify-content: center;
  background: transparent; padding: var(--s-4);
}
.ask-phone .ask-box {
  max-width: 560px; width: 100%; max-height: 100%; overflow-y: auto; background: var(--aa-sheet);
  border: 2px solid var(--aa-ink); padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-3);
}
.ask-phone .ask-count { font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted); }
.ask-phone .ask-title { margin: 0; font-family: var(--aa-read); font-size: var(--t-3); color: var(--aa-ink);
  overflow-wrap: anywhere; }
.ask-phone .ask-roll { margin: 0; font-family: var(--aa-measure); font-size: var(--t-body); color: var(--aa-ink-body); }
.ask-phone .ask-result { font-family: var(--aa-measure); font-weight: 500; font-size: calc(var(--t-1) * 1.2);
  color: var(--aa-ink); font-variant-numeric: tabular-nums; }
.ask-phone .ask-note { margin: 0; color: var(--aa-ink-body); line-height: 1.5; }
.ask-phone .ask-btns { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.ask-phone .ask-btns button { min-height: 44px; }
/* RM651: the DM asks this player to react - over whatever tab is open and above an ask card (z 210), under the dice
   box (z 300); only the character's own choices, a countdown, No reaction */
.react-phone {
  position: fixed; inset: 0; z-index: 215; display: flex; align-items: center; justify-content: center;
  background: transparent; padding: var(--s-4);
}
.react-phone .react-box {
  max-width: 560px; width: 100%; max-height: 100%; overflow-y: auto; background: var(--aa-sheet);
  border: 2px solid var(--aa-ink); padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-3);
}
.react-phone .react-title { margin: 0; font-family: var(--aa-read); font-size: var(--t-3); color: var(--aa-ink); }
.react-phone .react-count { font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted);
  font-variant-numeric: tabular-nums; }
.react-phone .react-state { margin: 0; color: var(--aa-ink-body); line-height: 1.5; }
.react-phone .react-btns { display: flex; flex-direction: column; gap: var(--s-2); }
.react-phone .react-btns button {
  min-height: 48px; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left;
}
.react-phone .react-n { font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink); }
.react-phone .react-d { font-size: var(--t-meta); color: var(--aa-ink-muted); }
/* RM664 (PL-29): the cards sit on a see-through scrim, not a blank screen - the table stays in view behind them */
.ask-phone::before, .react-phone::before {
  content: ''; position: absolute; inset: 0; background: var(--aa-recess); opacity: .84;
}
.ask-phone .ask-box, .react-phone .react-box { position: relative; }
.ask-phone .ask-src { margin: 0; font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted);
  overflow-wrap: anywhere; }
/* RM665 (2.4): the death saving throw - one big card, the two buttons full width */
.ask-phone.ask-death .ask-title { font-size: var(--t-2); }
.ask-phone.ask-death .ask-btns { flex-direction: column; }
.ask-phone.ask-death .ask-btns button { min-height: 56px; font-size: var(--t-body); }
/* RM799 (COMBATR-03): a finished death-save result - a tap outside its box reaches the Fight tab underneath */
.ask-phone.ask-pass { pointer-events: none; }
.ask-phone.ask-pass .ask-box { pointer-events: auto; }
/* RM664 (PL-23): "You were hit" - the third card (js/player/modals.js shows one at a time) */
.hit-phone {
  position: fixed; inset: 0; z-index: 212; display: flex; align-items: center; justify-content: center;
  background: transparent; padding: var(--s-4);
}
.hit-phone::before { content: ''; position: absolute; inset: 0; background: var(--aa-recess); opacity: .84; }
.hit-phone .hit-box {
  position: relative; max-width: 560px; width: 100%; max-height: 100%; overflow-y: auto; background: var(--aa-sheet);
  border: 2px solid var(--aa-ink); padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-3);
}
.hit-phone .hit-title { margin: 0; font-family: var(--aa-read); font-size: var(--t-3); color: var(--aa-ink);
  overflow-wrap: anywhere; }
.hit-phone .hit-line { margin: 0; color: var(--aa-ink-body); line-height: 1.5; overflow-wrap: anywhere; }
.hit-phone .hit-btns { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.hit-phone .hit-btns button { min-height: 44px; }
/* the hit card put away (Not now) - bottom left, the roll ask's chip keeps the right */
.hit-chip {
  position: fixed; left: var(--s-3); bottom: calc(var(--tabbar-h) + var(--safe-b) + var(--s-3)); z-index: 92;
  min-height: 44px; padding: var(--s-1) var(--s-3); background: var(--aa-sheet); color: var(--aa-ink);
  border: 2px solid var(--aa-ink); border-radius: 0; font-family: var(--aa-operate); font-size: var(--t-ui);
}
/* RM649: the minimised card (Later) - pinned beside the roll card's spot, above the tab bar, covering no control */
.ask-chip {
  position: fixed; right: var(--s-3); bottom: calc(var(--tabbar-h) + var(--safe-b) + var(--s-3)); z-index: 92;
  min-height: 44px; padding: var(--s-1) var(--s-3); background: var(--aa-sheet); color: var(--aa-ink);
  border: 2px solid var(--aa-ink); border-radius: 0; font-family: var(--aa-operate); font-size: var(--t-ui);
}
#hdrConds .cond-chip {
  font-size: var(--t-meta); line-height: 1.3; padding: 3px 8px; border-radius: 0;
  border: 1px solid var(--aa-warn); color: var(--aa-ink);
}
#hdrConds .cond-chip.conc { border-color: var(--aa-rule); color: var(--aa-ink-body); }
/* RM677 (PL-40): a chip is a button (a 44 px target) that opens its rule */
#hdrConds button.cond-chip { min-height: 44px; font-family: inherit; background: var(--aa-sheet); cursor: pointer; }
.cond-rule { margin: 0 0 var(--s-2); color: var(--aa-ink-body); line-height: 1.5; }
/* RM651: the turn bar - what is left of this character's turn, on every tab while a fight is on (inverted on its
   turn, like the turn strip above it); its Reaction button opens the Combat tab at REACTION */
#turnBar { display: flex; align-items: stretch; gap: 1px; border-top: 1px solid var(--aa-hairline); }
#turnBar[hidden] { display: none; }
#turnBar button {
  min-height: 44px; border: 0; border-radius: 0; background: var(--aa-sheet); color: var(--aa-ink-body);
  font-family: var(--aa-measure); font-size: var(--t-meta); font-variant-numeric: tabular-nums; text-align: left;
  padding: var(--s-1) var(--s-3);
}
#turnBar .tb-main { flex: 1 1 auto; min-width: 0; }
#turnBar .tb-react { flex: 0 0 auto; }
#turnBar.mine .tb-main { background: var(--aa-ink); color: var(--aa-sheet); }
/* RM651: the Combat tab's thumb-index button shows only in a fight (D2) */
#tabs .tab[hidden] { display: none; }
/* RM651: the Combat tab - Action, Bonus, Move, Reaction; every option a row, a greyed row says why */
.pcb-head { margin: 0 0 var(--s-3); font-family: var(--aa-measure); font-size: var(--t-ui); letter-spacing: .06em; color: var(--aa-ink-muted); }
.pcb-head.mine { color: var(--aa-ink); font-weight: 600; }
.pcb-group { margin: 0 0 var(--s-4); }
.pcb-group h3 {
  margin: 0 0 var(--s-2); font-family: var(--aa-measure); font-size: var(--t-meta); letter-spacing: .08em;
  text-transform: uppercase; color: var(--aa-ink-muted);
}
.pcb-opt {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%; min-height: 44px;
  margin: 0 0 var(--s-1); padding: var(--s-2) var(--s-3); text-align: left; border-radius: 0;
}
.pcb-opt .pcb-n { font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink); }
.pcb-opt .pcb-d, .pcb-opt .pcb-why { font-size: var(--t-meta); color: var(--aa-ink-muted); }
.pcb-opt .pcb-warn { font-size: var(--t-meta); color: var(--aa-warn); }
.pcb-opt[aria-disabled="true"] { opacity: .5; }
.pcb-end { width: 100%; min-height: 48px; margin: var(--s-2) 0 var(--s-4); border-radius: 0; }
/* RM677 (PL-30, PL-32): the rest of the options folded; a bonus-action spell says so; End turn asking once more */
.pcb-more { margin: 0 0 var(--s-4); }
.pcb-more > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer;
  font-family: var(--aa-measure); font-size: var(--t-ui); color: var(--aa-ink-muted); }
.pcb-opt .pcb-slot { font-size: var(--t-meta); color: var(--aa-ink-muted); }
.pcb-end.armed { border-color: var(--aa-warn); color: var(--aa-ink); font-weight: 600; }
/* RM704 (retest NEW-07): End my turn stays on screen however long the list above it is. RM781 (SE-06): it is a
   footer under the scrolling list rather than sticky inside it - sticky, it covered the Move row whenever a status line
   pushed the list down. The pane stops scrolling itself; the list scrolls, the footer stays. */
#pane-combat.pcb-host { padding: 0; overflow: hidden; }
#pane-combat.pcb-host.active { display: flex; flex-direction: column; }
#pane-combat .pcb-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
  -webkit-overflow-scrolling: touch; padding: var(--s-5) 16px var(--s-5); }
#pane-combat .pcb-foot { flex: 0 0 auto; padding: var(--s-2) 16px; background: var(--aa-sheet);
  border-top: 1px solid var(--aa-rule); }
#pane-combat .pcb-foot:empty { display: none; }
#pane-combat .pcb-foot .pcb-end { margin: 0; }
.pcb-status { display: flex; flex-direction: column; gap: var(--s-2); margin: 0 0 var(--s-3); }
.pcb-status:empty { display: none; }
.pcb-act {
  padding: var(--s-2) var(--s-3); background: var(--aa-recess); border: 1px solid var(--aa-hairline);
  color: var(--aa-ink-body); font-size: var(--t-ui); line-height: 1.4;
}
.pcb-act .pcb-act-state { display: block; font-family: var(--aa-measure); color: var(--aa-ink); }
/* RM677 (PL-30): each status line's time, and the older lines folded */
.pcb-act .pcb-act-time { display: block; font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted); }
.pcb-earlier > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; color: var(--aa-ink-muted); }
.pcb-earlier .pcb-act { margin-top: var(--s-2); }
.pcb-act button { margin-top: var(--s-1); min-height: 40px; }
/* RM661 (SY-02, PL-02): the link is polling - updates arrive a little late */
.pcb-link { margin: 0 0 var(--s-2); font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-warn); }
/* RM665 (PL-33, PL-39): down at 0 HP - above the rows; dead - the only line */
.pcb-down { margin: 0 0 var(--s-3); padding: var(--s-2) var(--s-3); border: 2px solid var(--aa-warn); color: var(--aa-ink);
  font-family: var(--aa-measure); font-size: var(--t-ui); }
.pcb-down.dead { border-color: var(--aa-ink); font-weight: 600; }
/* RM760: the name a phone gives when it joins a hosted table (js/player/main.js askPlayerName) - over everything,
   sheets included, until a name is given */
.name-gate { position: fixed; inset: 0; z-index: 120; display: flex; align-items: center; justify-content: center;
  padding: 16px; background: rgba(14, 17, 18, .78); }
.name-gate-box { width: 100%; max-width: 360px; background: var(--aa-sheet); color: var(--aa-ink);
  border: 1px solid var(--aa-rule); padding: 20px 18px 18px; display: flex; flex-direction: column; gap: 10px; }
.name-gate-box h2 { margin: 0; font-family: var(--aa-read); font-weight: 500; font-size: 1.4rem; }
.name-gate-box p { margin: 0; }
.name-gate-box input { width: 100%; min-height: 44px; font-size: 1.05rem; padding: 8px 10px; }
.name-gate-box button { min-height: 44px; font-size: 1rem; background: var(--aa-ink); color: var(--aa-sheet); border-color: var(--aa-ink); }
.name-gate-msg:empty { display: none; }
.name-gate-msg { color: var(--aa-warn); }

/* RM760: what a shield does for this character (js/player/character.js defenseCard) */
.shield-note { margin: 6px 0 0; padding: 6px 8px; border-left: 3px solid var(--aa-ok); font-size: .85rem; line-height: 1.45; }
.shield-note.warn { border-left-color: var(--aa-warn); color: var(--aa-ink); }
.shield-note b { font-weight: 600; }
/* RM761: the Choose button of an open option (races, subraces, classes, subclasses, backgrounds, feats, options,
   spells - every browsePicker and chooseSpells row) stays at the foot of the sheet while its description scrolls
   under it, so a player who knows what they want picks it without reading to the end. Sticky within its own
   <details>: it scrolls away with the row once the row is past. */
.pk2[open] > .pk2-take {
  position: sticky; bottom: 0; z-index: 2;
  margin-bottom: 0;
  border-top: 1px solid var(--aa-hairline);
  box-shadow: 0 -8px 12px -6px rgba(0, 0, 0, .25);
}

/* ==== BEGIN RM771 PLAYER CAMPAIGN TAB (only RM771 edits between these lines; wave 3) ==== */
/* the player's Campaign tab (js/player/campaign.js): what the DM shares, phone first */
.pc-wrap { display: flex; flex-direction: column; gap: var(--s-3); padding: var(--s-3); }
.pc-status { font-family: var(--aa-operate); font-size: var(--t-meta); color: var(--aa-ink-muted); }
.pc-nav { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.pc-chip {
  font-family: var(--aa-operate); font-size: var(--t-ui); padding: var(--s-1) var(--s-3);
  border: 1px solid var(--aa-hairline); background: var(--aa-sheet); color: var(--aa-ink-body); border-radius: 0;
}
.pc-chip.on { border-color: var(--aa-accent); color: var(--aa-accent); background: var(--aa-accent-wash); }
.pc-sec { display: flex; flex-direction: column; gap: var(--s-3); }
.pc-group { display: flex; flex-direction: column; gap: var(--s-2); }
.pc-card { padding: var(--s-3); border: 1px solid var(--aa-hairline); background: var(--aa-sheet); border-radius: 0; }
.pc-card > summary { cursor: pointer; font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink); }
.pc-h { margin: 0 0 var(--s-1); font-family: var(--aa-read); color: var(--aa-ink); }
.pc-sub {
  margin-top: var(--s-2); font-family: var(--aa-operate); font-size: var(--t-meta); color: var(--aa-ink-muted);
  text-transform: uppercase;
}
.pc-text { white-space: pre-wrap; font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink-body); }
.pc-text mark { background: var(--aa-accent-wash); color: inherit; }
/* RM796 (PREP-09, PREP-11): a wiki entry's picture, a quest's steps, an event's in-world date */
.pc-pic { display: block; max-width: 100%; max-height: 50vh; object-fit: contain; margin: var(--s-2) 0; border: 1px solid var(--aa-hairline); }
.pc-steps { list-style: none; margin: var(--s-2) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-1);
  font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink-body); }
.pc-step.done { color: var(--aa-ink-muted); text-decoration: line-through; }
.pc-when { margin: 0 0 var(--s-1); font-family: var(--aa-operate); font-size: var(--t-meta); color: var(--aa-ink-muted); }
.pc-links { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-top: var(--s-2); }
.pc-link {
  font-family: var(--aa-operate); font-size: var(--t-meta); padding: var(--s-1) var(--s-3);
  border: 1px solid var(--aa-hairline); background: var(--aa-sheet); color: var(--aa-ink-body); border-radius: 0;
}
.pc-search { display: flex; gap: var(--s-2); }
.pc-search input { flex: 1; min-width: 0; }
.pc-result {
  display: block; width: 100%; text-align: left; padding: var(--s-3);
  border: 1px solid var(--aa-hairline); background: var(--aa-sheet); border-radius: 0;
}
.pc-empty { font-family: var(--aa-operate); font-size: var(--t-meta); color: var(--aa-ink-muted); }
[class^="pc-"][hidden] { display: none; }
/* W3-FINAL: with the Campaign tab shown there are nine tabs; from 768 px each carries its full name beside
   its icon, and below about 960 px a ninth tab leaves ~58 px for a name ("Character" needs 63, "Campaign"
   65), so journey_screens saw them clipped at 820 px. In that band the bar uses the phone's icon-over-short-
   label tabs (RM689) instead; the 46 px row holds the 21 px icon, 2 px and the 15 px label. */
@media (min-width: 768px) and (max-width: 959px) {
  #tabs:has(> .tab[data-tab="campaign"]:not([hidden])) .tab { flex-direction: column; gap: 2px; padding: 0 2px; }
  #tabs:has(> .tab[data-tab="campaign"]:not([hidden])) .tab .tl { display: none; }
  #tabs:has(> .tab[data-tab="campaign"]:not([hidden])) .tab .ts { display: block; }
}
/* ==== END RM771 PLAYER CAMPAIGN TAB ==== */

/* ==== RM824 PLAYER APP AAA PASS ====
   Motion, the dice tray, text size and the Settings sheet. Every keyframe below runs only where the player has not
   asked for reduced motion; css/tokens.css already cuts every duration to 1 ms when they have. Graphite holds: motion
   is position and opacity, depth is value - no shadow, blur, glow or gradient. */

/* req D2: text size. The root font-size stays the browser's default at "normal" (hard rule 4 is about a SMALLER
   root re-basing every rem step under the 12 px floor); a player who asks for larger type gets every step larger,
   which is the only way a rem scale can grow. A percentage keeps the device's own font setting underneath it. */
html[data-text="large"] { font-size: 112.5%; }
html[data-text="larger"] { font-size: 125%; }
/* the header plates are a fixed height so a wrapped caption cannot unbalance the row; that height grows with the type */
html[data-text="large"] { --plate-h: 52px; }
html[data-text="larger"] { --plate-h: 58px; }

/* req B1: the dice tray - above every sheet, card and toast for the length of a throw; it takes the tap that skips */
#diceTray { position: fixed; inset: 0; z-index: 400; touch-action: none; cursor: pointer; }
#diceTray[hidden] { display: none; }
#diceTray canvas { display: block; }
/* the result painted under a throw waits for the dice: the card, the toast and the drawer's total appear as it fades */
/* opacity, not visibility: a live region that is visibility:hidden is never announced, so a screen reader would hear no
   roll at all (RM824 review). Faded out it stays in the accessibility tree and speaks at once. */
body.tray-on #rollCard, body.tray-on #toast, body.tray-on #diceResult { opacity: 0; }

/* req C3: a press answers in the frame it lands */
button:active:not(:disabled) { transform: scale(.97); }
#tabs .tab:active { transform: none; }

@media (prefers-reduced-motion: no-preference) {
  button { transition: background var(--aa-dur-state) var(--aa-ease), border-color var(--aa-dur-state) var(--aa-ease),
                       color var(--aa-dur-state) var(--aa-ease), transform 90ms var(--aa-ease); }

  /* req C1: a pane comes in from the side of the tab pressed (js/player/main.js enterPane) */
  @keyframes tfPaneR { from { opacity: 0; transform: translateX(28px); } to { opacity: 1; transform: none; } }
  @keyframes tfPaneL { from { opacity: 0; transform: translateX(-28px); } to { opacity: 1; transform: none; } }
  .pane.active.enter-r { animation: tfPaneR var(--aa-dur-enter) var(--aa-ease); }
  .pane.active.enter-l { animation: tfPaneL var(--aa-dur-enter) var(--aa-ease); }

  /* the active tab's widening is the index moving under the thumb */
  #tabs .tab { transition: color var(--aa-dur-state) var(--aa-ease), border-color var(--aa-dur-state) var(--aa-ease),
                           flex-grow var(--aa-dur-enter) var(--aa-ease), background var(--aa-dur-state) var(--aa-ease); }

  /* req C2: sheets rise, swap and fall; the scrim fades (js/player/app.js renderSheet, closeSheetNow) */
  @keyframes tfSheetIn { from { transform: translateY(100%); } to { transform: none; } }
  @keyframes tfSheetOut { from { transform: none; } to { transform: translateY(100%); } }
  @keyframes tfSheetSwap { from { opacity: .4; transform: translateY(18px); } to { opacity: 1; transform: none; } }
  @keyframes tfFadeIn { from { opacity: 0; } }
  @keyframes tfFadeOut { to { opacity: 0; } }
  @keyframes tfBackIn { from { opacity: 0; } to { opacity: .84; } }
  #sheetHost .sheet.sheet-in { animation: tfSheetIn 260ms var(--aa-ease); }
  #sheetHost .sheet.sheet-swap > .sheet-body { animation: tfSheetSwap var(--aa-dur-enter) var(--aa-ease); }
  #sheetHost .sheet-back.back-in { animation: tfBackIn var(--aa-dur-enter) var(--aa-ease); }
  #sheetHost.closing { display: block; pointer-events: none; }
  #sheetHost.closing .sheet { animation: tfSheetOut var(--aa-dur-enter) var(--aa-ease) forwards; }
  #sheetHost.closing .sheet-back { animation: tfFadeOut var(--aa-dur-enter) var(--aa-ease) forwards; }
  /* from 768 px the sheet is centred by a transform (above); a slide would fight it, so it fades there instead */
  @media (min-width: 768px) {
    #sheetHost .sheet.sheet-in { animation: tfFadeIn var(--aa-dur-enter) var(--aa-ease); }
    #sheetHost.closing .sheet { animation: tfFadeOut var(--aa-dur-enter) var(--aa-ease) forwards; }
  }

  /* req C4: the toast and the roll card arrive (both are centred with translateX(-50%), which the keyframes keep) */
  @keyframes tfToastIn { from { opacity: 0; transform: translate(-50%, 12px); } to { opacity: 1; transform: translate(-50%, 0); } }
  @keyframes tfCardIn { from { opacity: 0; transform: translate(-50%, 18px) scale(.96); } to { opacity: 1; transform: translate(-50%, 0); } }
  @keyframes tfTotalIn { from { transform: scale(.82); } 60% { transform: scale(1.06); } to { transform: none; } }
  #toast.show { animation: tfToastIn var(--aa-dur-enter) var(--aa-ease); }
  #rollCard:not([hidden]) { animation: tfCardIn var(--aa-dur-enter) var(--aa-ease); }
  #rollCard:not([hidden]) .rc-total { animation: tfTotalIn 320ms var(--aa-ease); }
  #diceResult .dr-total { animation: tfTotalIn 320ms var(--aa-ease); }

  /* the dice drawer drops from the ROLL plate it hangs under */
  @keyframes tfDrawerIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } }
  #diceHost:not([hidden]) #diceDrawer:not(.hidden) { animation: tfDrawerIn var(--aa-dur-enter) var(--aa-ease); }

  /* new cards settle in on a pane's first paint, one after another */
  @keyframes tfRise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
  .pane.active.enter-r > .card, .pane.active.enter-l > .card { animation: tfRise 300ms var(--aa-ease) both; }
  .pane.active > .card:nth-child(2) { animation-delay: 30ms; }
  .pane.active > .card:nth-child(3) { animation-delay: 60ms; }
  .pane.active > .card:nth-child(4) { animation-delay: 90ms; }

  /* C6: the boot screen fades instead of cutting (js/player/main.js dismissBoot removes it after 200 ms either way) */
  #tfBoot.tf-boot-done { animation: tfFadeOut 190ms var(--aa-ease) forwards; pointer-events: none; }
}

/* req C5: the header HP counts to its new value; the plate takes a value step for damage or healing (moments.js) */
#hdrHPWrap.hp-dmg { background: var(--aa-accent-wash); }
#hdrHPWrap.hp-heal { background: var(--aa-recess); }
#hdrHPWrap { transition: background var(--aa-dur-enter) var(--aa-ease); }
@media (prefers-reduced-motion: no-preference) {
  @keyframes tfHit { 0%, 100% { transform: none; } 25% { transform: translateX(-3px); } 50% { transform: translateX(3px); } 75% { transform: translateX(-2px); } }
  #hdrHPWrap.hp-dmg .vt-val { display: inline-block; animation: tfHit 280ms var(--aa-ease); }
  @keyframes tfLift { 0% { transform: none; } 40% { transform: translateY(-3px); } 100% { transform: none; } }
  #hdrHPWrap.hp-heal .vt-val { display: inline-block; animation: tfLift 360ms var(--aa-ease); }
}
/* req D1: the Settings sheet (js/player/settings.js) */
.set-head { margin: var(--s-5) 0 var(--s-2); font-family: var(--aa-read); font-weight: 500; font-size: var(--t-4); color: var(--aa-ink); }
.set-row { display: block; margin: 0 0 var(--s-4); }
.set-label { display: block; font-family: var(--aa-operate); font-size: var(--t-ui); font-weight: 500; color: var(--aa-ink); margin-bottom: var(--s-1); }
.set-seg { display: flex; gap: 0; }
.set-seg button + button { border-left: none; }
.set-seg button.active { background: var(--aa-ink); color: var(--aa-ground); border-color: var(--aa-ink); }
.set-toggle { display: flex; align-items: center; justify-content: space-between; gap: var(--s-4); min-height: 44px; cursor: pointer; }
.set-toggle .set-text { display: flex; flex-direction: column; min-width: 0; }
.set-toggle .set-text .hint { margin: 0; }
/* a square switch: a ruled track and a block that crosses it - Graphite has no pills */
input.set-switch {
  appearance: none; -webkit-appearance: none; flex: 0 0 auto;
  width: 52px; height: 30px; min-width: 52px; margin: 0; padding: 0; position: relative; cursor: pointer;
  border: 1px solid var(--aa-rule); border-radius: 0; background: var(--aa-recess);
  transition: background var(--aa-dur-state) var(--aa-ease), border-color var(--aa-dur-state) var(--aa-ease);
}
input.set-switch::after {
  content: ''; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; background: var(--aa-ink-muted);
  transition: transform var(--aa-dur-enter) var(--aa-ease), background var(--aa-dur-state) var(--aa-ease);
}
input.set-switch:checked { background: var(--aa-ink); border-color: var(--aa-ink); }
input.set-switch:checked::after { transform: translateX(22px); background: var(--aa-ground); }
.set-volume input[type="range"] { width: 100%; accent-color: var(--aa-ink); }
.set-test { margin-top: var(--s-2); }
/* req B6-B7: saved rolls and the stats line in the dice drawer (js/player/dice.js) */
.dice-macros { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: 0 0 var(--s-2); }
.dice-macros > .hint { margin: 0; }
.dm-chip { display: inline-flex; }
.dm-chip .dm-go { font-family: var(--aa-operate); max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dm-chip .dm-rm { border-left: none; min-width: 36px; padding: 0 var(--s-2); color: var(--aa-ink-muted); }
.dice-stats { margin: var(--s-2) 0 var(--s-1); font-family: var(--aa-measure); }
.dice-stats:empty { display: none; }
#diceResult .dr-name { font-family: var(--aa-read); font-size: var(--t-4); color: var(--aa-ink); }
/* req E5: the player's own conditions (HP sheet > Conditions) */
.self-conds .sc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: var(--s-1); margin-top: var(--s-2); }
.self-conds .sc-toggle { justify-content: flex-start; text-align: left; }
.self-conds .sc-toggle.on { background: var(--aa-ink); color: var(--aa-ground); border-color: var(--aa-ink); }
/* req F1-F2: a moment across the middle of the screen (js/player/moments.js moment) - a plate ruled in ink, ok or warn */
#tfMoment {
  position: fixed; left: 50%; top: 38%; transform: translate(-50%, -50%); z-index: 350;
  width: min(360px, calc(100vw - 32px)); pointer-events: none; text-align: center;
  background: var(--aa-sheet); border: 2px solid var(--aa-ink); border-radius: 0;
  padding: var(--s-4) var(--s-4) var(--s-5);
}
#tfMoment[hidden] { display: none; }
#tfMoment .tm-title { font-family: var(--aa-read); font-weight: 500; font-size: var(--t-1); line-height: 1.1; color: var(--aa-ink); }
#tfMoment .tm-sub { margin-top: var(--s-2); font-family: var(--aa-operate); font-size: var(--t-ui); color: var(--aa-ink-muted); }
#tfMoment.tm-good { border-color: var(--aa-ok); }
#tfMoment.tm-good .tm-title { color: var(--aa-ok); }
#tfMoment.tm-bad { border-color: var(--aa-warn); }
#tfMoment.tm-bad .tm-title { color: var(--aa-warn); }
@media (prefers-reduced-motion: no-preference) {
  @keyframes tfMoment {
    0% { opacity: 0; transform: translate(-50%, -50%) scale(.9); }
    12% { opacity: 1; transform: translate(-50%, -50%) scale(1.02); }
    20% { transform: translate(-50%, -50%) scale(1); }
    82% { opacity: 1; transform: translate(-50%, -50%); }
    100% { opacity: 0; transform: translate(-50%, -56%); }
  }
  #tfMoment.tm-show { animation: tfMoment 1900ms var(--aa-ease) forwards; }
}
/* req G1: ready-made heroes (js/player/main.js openPregens) */
.picker-pregen { width: 100%; margin-top: var(--s-2); }
.pregen-row {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--s-1);
  width: 100%; text-align: left; margin-top: var(--s-3); padding: var(--s-3) var(--s-4);
  background: var(--aa-sheet); border: 1px solid var(--aa-rule); border-radius: 0;
}
.pregen-row .pg-top { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s-3); }
.pregen-row .pg-name { font-family: var(--aa-read); font-size: var(--t-4); font-weight: 500; color: var(--aa-ink); }
.pregen-row .pg-role { font-family: var(--aa-measure); font-size: var(--t-meta); letter-spacing: .06em; text-transform: uppercase; color: var(--aa-ink-muted); }
.pregen-row .pg-line { font-family: var(--aa-operate); font-size: var(--t-ui); color: var(--aa-ink-body); }
.pregen-row .pg-blurb { font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink-muted); font-weight: 400; line-height: 1.45; }
/* req G6: the app's own question (js/player/app.js confirmSheet) */
.confirm-text { font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink-body); margin: var(--s-2) 0 var(--s-4); line-height: 1.5; }
.confirm-row { gap: var(--s-2); }
.confirm-row .confirm-danger { border-color: var(--aa-warn); color: var(--aa-warn); }
/* req G2: find a spell on the Spells tab (js/player/spells.js filterBox) */
.spell-filter { margin: 0 0 var(--s-4); }
.spell-filter input { width: 100%; }
.spell-filter .spell-filter-none { margin-top: var(--s-2); }
/* RM824 review: compact encounter rows - a tap on the row opens its controls (js/player/kit.js entryRow) */
#kit .kit-row-top { cursor: pointer; }
#kit .kit-more { margin-top: var(--s-2); }
#kit .kit-more[hidden], #kit .kit-adds[hidden], #kit .kit-rolls[hidden] { display: none; }
#kit .kit-edit { margin-left: auto; min-height: 36px; padding: 0 var(--s-3); color: var(--aa-ink-muted); }
/* req G1: the first-launch line (js/player/main.js openPicker) */
.picker-welcome { margin: var(--s-2) 0 var(--s-4); font-family: var(--aa-read); font-size: var(--t-4); line-height: 1.35; color: var(--aa-ink); }
/* RM824: dice colour swatches (js/player/settings.js) - the series pigments, from tokens */
.set-swatches { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-1); }
.set-swatch { display: flex; align-items: center; gap: var(--s-2); justify-content: flex-start; padding: var(--s-1) var(--s-2); }
.set-swatch .sw-chip { flex: 0 0 auto; width: 22px; height: 22px; border: 1px solid var(--aa-rule); }
.set-swatch .sw-name { font-size: var(--t-ui); }
.set-swatch.active { border-color: var(--aa-ink); background: var(--aa-recess); color: var(--aa-ink); }
.set-swatch.active .sw-name { font-weight: 600; }
.sw-bone .sw-chip { background: var(--aa-ink); }
.sw-oxblood .sw-chip { background: var(--aa-s2); }
.sw-slate .sw-chip { background: var(--aa-s3); }
.sw-umber .sw-chip { background: var(--aa-s4); }
.sw-verdigris .sw-chip { background: var(--aa-s5); }
.sw-brass .sw-chip { background: var(--aa-s6); }
/* req G1: Fill the rest for me, on the build's Review step (js/player/character.js) */
.wiz-fill { margin: var(--s-4) 0 var(--s-2); }
.wiz-fill > button { width: 100%; }
.wiz-fill > .hint { margin-top: var(--s-1); }
/* ==== END RM824 MOTION ==== */

/* ==== RM824 TABLE KIT (js/player/kit.js) ==== */
/* ---------------- RM824: the Table Kit (js/player/kit.js) ----------------
 * Encounter / Oracle / Library on the Table tab when no DM's board is present. Everything is scoped under #kit,
 * except the kit's bottom sheets, which app.js renders into #sheetHost outside the kit: those are scoped under
 * #sheetHost .kit-sheet. Graphite rules: radius 0, no shadow, colours and sizes from tokens only. The accent is
 * spent once in this view - the active turn's row (--tf-turn, the tracker's rubrication role). */

#kit .kit-seg {
  display: grid; grid-template-columns: repeat(3, 1fr);
  margin-top: var(--s-3);
  border: 1px solid var(--aa-rule); border-radius: 0;
}
#kit .kit-seg > button { border: 0; border-radius: 0; min-height: 44px; }
#kit .kit-seg > button + button { border-left: 1px solid var(--aa-rule); }

#kit .kit-field, #sheetHost .kit-sheet .kit-field {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--s-1);
  margin-bottom: var(--s-2);
}
#kit .kit-lab, #sheetHost .kit-sheet .kit-lab {
  font-family: var(--aa-measure); font-size: var(--t-meta); letter-spacing: .06em; text-transform: uppercase;
  color: var(--aa-ink-muted);
}
#kit .kit-meta, #sheetHost .kit-sheet .kit-meta {
  display: block; font-family: var(--aa-operate); font-size: var(--t-meta); color: var(--aa-ink-muted);
}

/* ---- encounter ---- */
#kit .kit-turnline {
  font-family: var(--aa-read); font-size: var(--t-4); color: var(--aa-ink);
  padding: var(--s-3) 0 var(--s-2); border-top: 1px solid var(--aa-hairline); margin-top: var(--s-2);
}
#kit .kit-steps .kit-next { min-height: 52px; font-size: var(--t-body); }
#kit .kit-list { list-style: none; margin: var(--s-3) 0 0; padding: 0; border-top: 1px solid var(--aa-hairline); }
#kit .kit-empty { padding: var(--s-4) 0; }
#kit .kit-row {
  padding: var(--s-3) var(--s-2) var(--s-3) var(--s-3);
  border-bottom: 1px solid var(--aa-hairline);
  border-left: 3px solid transparent;
  transition: background var(--aa-dur-state) var(--aa-ease), border-color var(--aa-dur-state) var(--aa-ease);
}
#kit .kit-row.kit-now { border-left-color: var(--tf-turn); background: var(--tf-turn-wash); }
#kit .kit-row.kit-down { opacity: .5; }
#kit .kit-row.kit-down .kit-name { text-decoration: line-through; }
#kit .kit-row-top { display: flex; align-items: center; gap: var(--s-2); }
#kit .kit-init, #kit .kit-init-in {
  width: 56px; min-width: 56px; min-height: 44px; padding: 0;
  font-family: var(--aa-measure); font-size: var(--t-4); font-variant-numeric: tabular-nums; text-align: center;
}
#kit .kit-who { display: flex; flex-direction: column; min-width: 0; }
#kit .kit-name { font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink); overflow-wrap: anywhere; }
#kit button.kit-name-link {
  border: 0; padding: 0; min-height: 44px; text-align: left; background: none;
  text-decoration: underline; text-decoration-color: var(--aa-rule); text-underline-offset: 3px;
}
#kit button.kit-name-link:hover { text-decoration-color: var(--aa-ink); }
#kit .kit-ac {
  font-family: var(--aa-measure); font-size: var(--t-ui); color: var(--aa-ink-body); white-space: nowrap;
  padding: 0 var(--s-2); border-left: 1px solid var(--aa-hairline);
}
#kit .kit-rm { min-width: 44px; padding: 0; font-size: var(--t-4); color: var(--aa-ink-muted); }
#kit .kit-row-hp, #kit .kit-row-conds {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-top: var(--s-2);
  padding-left: calc(56px + var(--s-2));
}
#kit .kit-hpbar {
  flex: 1 1 96px; min-width: 64px; height: 8px;
  background: var(--aa-recess); border: 1px solid var(--aa-hairline); border-radius: 0;
}
#kit .kit-hpfill {
  display: block; width: 100%; height: 100%; background: var(--aa-ink-body);
  transform-origin: left center;   /* RM824: the bar moves by transform, never width (brand M5: no layout animation) */
  transition: transform var(--aa-dur-enter) var(--aa-ease);
}
#kit .kit-hpfill.low { background: var(--aa-warn); }
#kit .kit-hpnum {
  font-family: var(--aa-measure); font-size: var(--t-ui); font-variant-numeric: tabular-nums;
  color: var(--aa-ink); white-space: nowrap;
}
#kit .kit-hpnum.zero { color: var(--aa-warn); }
#kit input.kit-amt { width: 72px; }
#kit .kit-chip {
  min-height: 44px; padding: var(--s-1) var(--s-3);
  font-size: var(--t-ui); border-color: var(--aa-hairline); background: var(--aa-recess);
}
#kit .kit-foot { margin-top: var(--s-4); }

/* ---- oracle ---- */
#kit .kit-answer:empty, #kit .kit-genout:empty { display: none; }
#kit .kit-answer, #kit .kit-genout {
  margin-top: var(--s-3); padding: var(--s-3) 0 0; border-top: 1px solid var(--aa-hairline);
}
#kit .kit-answer-word { font-family: var(--aa-read); font-size: var(--t-2); color: var(--aa-ink); line-height: 1.15; }
#kit .kit-answer-twist { font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink-body); margin: var(--s-1) 0; }
#kit .kit-gentext { font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink); line-height: 1.5; }
#kit .kit-gen-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--s-2);
}
#kit .kit-loglist { list-style: none; margin: 0 0 var(--s-3); padding: 0; }
#kit .kit-logrow { padding: var(--s-2) 0; border-bottom: 1px solid var(--aa-hairline); }
#kit .kit-logtext { font-family: var(--aa-read); font-size: var(--t-ui); color: var(--aa-ink-body); line-height: 1.5; }

/* ---- the kit's sheets (rendered in #sheetHost) ---- */
#sheetHost .kit-sheet .row > button.grow { text-align: left; }
#sheetHost .kit-sheet .kit-pick { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-1); }
#sheetHost .kit-sheet .kit-name { font-family: var(--aa-read); font-size: var(--t-body); color: var(--aa-ink); }
#sheetHost .kit-sheet .kit-qty {
  min-width: 48px; text-align: center; font-family: var(--aa-measure); font-size: var(--t-body);
}
#sheetHost .kit-sheet .kit-step { min-width: 44px; padding: 0; }
#sheetHost .kit-sheet .kit-toggle { font-size: var(--t-ui); }
#sheetHost .kit-sheet .kit-cond { padding: var(--s-2) 0; border-bottom: 1px solid var(--aa-hairline); }
#sheetHost .kit-sheet .kit-cond > button { min-width: 140px; text-align: left; }
#sheetHost .kit-sheet .kit-cond > .hint { margin-top: var(--s-1); }
#sheetHost .kit-sheet input[type=search] { width: 100%; }

/* ---- motion: entrances only, and only when motion is welcome ---- */
@media (prefers-reduced-motion: no-preference) {
  @keyframes kit-row-in {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes kit-now-in {
    from { background: transparent; border-left-color: transparent; }
    to   { background: var(--tf-turn-wash); border-left-color: var(--tf-turn); }
  }
  @keyframes kit-answer-in {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
  }
  #kit .kit-row.kit-new { animation: kit-row-in var(--aa-dur-enter) var(--aa-ease) both; }
  #kit .kit-row.kit-now-in { animation: kit-now-in var(--aa-dur-enter) var(--aa-ease) both; }
  #kit .kit-answer-in { animation: kit-answer-in var(--aa-dur-enter) var(--aa-ease) both; }
}

/* ==== RM824 COMPENDIUM (js/player/compendium.js) ==== */
/* RM824 (req E4) - the offline compendium, js/player/compendium.js. Fragment: the lead folds it into css/player.css.
 * The view sits under #compendium. The entry opens in the app's bottom sheet, outside #compendium, so its few
 * rules are scoped by the .comp-entry wrapper the module builds instead. Results are capped in JS, never a nested
 * scroller (DESIGN.md hard rule 6a). */

#compendium .comp-q { display: block; width: 100%; }

/* category filter: wrapped, never a sideways strip; the pressed chip inverts its fill and aria-pressed carries
   the state for anyone the fill does not reach */
#compendium .comp-cats { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-top: var(--s-2); }
#compendium .comp-cat {
  min-height: 44px; padding: var(--s-1) var(--s-3);
  background: var(--aa-recess); color: var(--aa-ink-muted);
  border: 1px solid var(--aa-hairline); border-radius: 0;
  font-family: var(--aa-measure); font-size: var(--t-meta);
  letter-spacing: .04em; text-transform: uppercase;
}
#compendium .comp-cat:hover { border-color: var(--aa-ink); color: var(--aa-ink); }
#compendium .comp-cat.on { background: var(--aa-ink); color: var(--aa-sheet); border-color: var(--aa-ink); }

#compendium .comp-status { margin: var(--s-2) 0 0; font-variant-numeric: tabular-nums; }
#compendium .comp-status:empty { display: none; }
#compendium .comp-prompt { margin: var(--s-2) 0 0; }
#compendium .comp-results { margin-top: var(--s-1); }

#compendium .comp-row {
  display: block; min-height: 44px; padding: var(--s-2) 0;
  border-bottom: 1px solid var(--aa-hairline); cursor: pointer;
}
#compendium .comp-row:hover { background: var(--aa-recess); }
#compendium .comp-row:focus-visible { outline: 2px solid var(--aa-ink); outline-offset: -2px; }
#compendium .comp-top { display: flex; align-items: baseline; gap: var(--s-2); }
#compendium .comp-nm {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--aa-read); font-size: var(--t-body); font-weight: 500; color: var(--aa-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#compendium .comp-ed {
  flex: 0 0 auto; font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted);
}
#compendium .comp-meta {
  margin-top: var(--s-1);
  font-family: var(--aa-measure); font-size: var(--t-meta); color: var(--aa-ink-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* the entry, in the bottom sheet */
.comp-entry .comp-printings { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-bottom: var(--s-3); }
.comp-entry .comp-pr { border-radius: 0; font-family: var(--aa-measure); }
.comp-entry .comp-pr.on { background: var(--aa-ink); color: var(--aa-sheet); border-color: var(--aa-ink); }
.comp-entry .comp-sub { margin: 0 0 var(--s-2); }
.comp-entry .comp-kv { margin: var(--s-1) 0 0; font-family: var(--aa-operate); font-size: var(--t-ui); color: var(--aa-ink-body); }
.comp-entry .comp-kv b { color: var(--aa-ink); font-weight: 600; margin-right: var(--s-1); }
.comp-entry .comp-p {
  margin: var(--s-3) 0 0;
  font-family: var(--aa-read); font-size: var(--t-body); line-height: 1.5; color: var(--aa-ink-body);
}
.comp-entry .comp-dice { display: flex; flex-wrap: wrap; }
.comp-entry .comp-src { margin-top: var(--s-4); }

/* a result set arrives with a short rise; a repaint builds a fresh .comp-list, so it plays once per search */
@media (prefers-reduced-motion: no-preference) {
  #compendium .comp-list { animation: comp-in var(--aa-dur-enter) var(--aa-ease) both; }
  @keyframes comp-in {
    from { opacity: 0; transform: translateY(4px); }
    to { opacity: 1; transform: none; }
  }
}
/* ==== END RM824 KIT AND COMPENDIUM ==== */
/* RM844: the Player in the TableSync family (js/player/linkapps.js). The ruled list css/apps/shell.css gives the
   family apps (.tf-family*), re-stated in the ledger idiom here: a hairline-ruled roster, the name in Spectral, what
   it does in Plex Sans, the action a plain secondary control. In a browser renderFamily draws an <a>, so it is given
   the button's box. */
.set-family { margin: 0 0 var(--s-4); }
.tf-family { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--aa-hairline); }
.tf-family-row { display: flex; align-items: center; gap: var(--s-3); padding: var(--s-3) 0; border-bottom: 1px solid var(--aa-hairline); }
.tf-family-text { flex: 1 1 auto; min-width: 0; }
.tf-family-name { font-family: var(--aa-read); font-weight: 600; color: var(--aa-ink); }
.tf-family-tag { color: var(--aa-ink-muted); font-size: var(--t-ui); }
.tf-family-go { flex: 0 0 auto; white-space: nowrap; }
a.tf-family-go {
  display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; min-height: 44px;
  padding: var(--s-2) var(--s-4); border: 1px solid var(--aa-rule); color: var(--aa-ink); text-decoration: none;
  font-family: var(--aa-operate); font-size: var(--t-ui); font-weight: 500; line-height: 1.2;
  transition: border-color var(--aa-dur-state) var(--aa-ease);
}
a.tf-family-go:hover { border-color: var(--aa-ink); }
a.tf-family-go:focus-visible { outline: 2px solid var(--aa-ink); outline-offset: 2px; }
/* Send to another app, in the Character file sheet */
.link-send-row { flex-wrap: wrap; gap: var(--s-2); }
.link-send-row button { flex: 1 1 12rem; }
.link-status:empty { display: none; }
textarea.link-code {
  display: block; width: 100%; box-sizing: border-box; margin-top: var(--s-2); resize: vertical;
  font-family: var(--aa-measure); font-size: var(--t-ui); color: var(--aa-ink); background: var(--aa-recess);
  border: 1px solid var(--aa-rule); border-radius: 0; padding: var(--s-2); word-break: break-all;
}
textarea.link-code.hidden { display: none; }
