/* ============================================================================
   Table Log — site stylesheet
   ============================================================================ */

/* -- 1. Design tokens + global resets ------------------------------------- */
:root{
  /* 2026-08-15 (bug report: "the size on dark mode is hard to see the text
     boxes"): --field-border was 0.28 alpha, which over --surface-2 (#241E30)
     lands around 1.4:1 against the card behind it -- technically a border,
     practically invisible, so every input read as a hole in the page rather
     than as something you could type in. 0.45 puts it near 2.2:1, which is
     the point a 1px edge actually resolves on a dim panel. Light mode is
     unchanged; it was never the problem. */
  --void:#141019; --surface:#1D1826; --surface-2:#241E30; --surface-3:#2C2438; --line:rgba(237,233,242,0.09); --field-border:rgba(237,233,242,0.45);
  --ink:#EDE9F2; --ink-dim:#9C93AE; --ink-faint:#6A6280;
  --fury:#E5484D; --order:#D4B106; --body:#4E9F5B; --chaos:#A468C7; --mind:#3E8FD0; --calm:#3AAFA0;
  --danger:#E5484D; --safe-top:env(safe-area-inset-top,0px); --safe-bottom:env(safe-area-inset-bottom,0px);
  --body-gradient: radial-gradient(circle at 15% 0%, #221b30 0%, var(--void) 60%);
}
:root[data-theme="light"]{
  --void:#F3F1F8; --surface:#FFFFFF; --surface-2:#F1EDF7; --surface-3:#E6E0F0; --line:rgba(30,20,50,0.12); --field-border:rgba(30,20,50,0.32);
  --ink:#1D1826; --ink-dim:#544D66; --ink-faint:#83798F;
  --fury:#C6353A; --order:#8A6A00; --body:#256B32; --chaos:#7B3FA0; --mind:#1F5F9E; --calm:#207468;
  --danger:#C6353A;
  --body-gradient: radial-gradient(circle at 15% 0%, #FFFFFF 0%, var(--void) 60%);
}
*{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; touch-action:manipulation; }
html,body{ height:100%; }
body{
  margin:0; background:var(--body-gradient); transition:background .2s ease;
  color:var(--ink); font-family:'Inter',sans-serif; min-height:100vh; min-height:100dvh;
  padding-top:var(--safe-top); padding-bottom:calc(var(--safe-bottom) + 16px); overscroll-behavior:none;
}
.wrap{ max-width:640px; margin:0 auto; padding:0 14px; }

/* -- Selection and caret --------------------------------------------------
   2026-08-21: both were the browser default until now, which meant every
   drag across a spell description or a campaign note painted Chrome's
   selection blue over a near-black purple ground, and every text cursor in
   the app was the UA caret -- wrong weight and often the wrong colour on a
   dark field. --chaos is the app's own accent and is already re-pointed by
   :root[data-theme="light"] and .theme-parchment, so both rules follow every
   theme without a per-theme override. --void as the selection foreground is
   the same accent-on-void pairing the app already uses for a.storage-upgrade-cta
   and .sub-tab.active: measured 4.8:1 dark, 6.1:1 light, 5.8:1 parchment,
   so selected text stays past the 4.5:1 body minimum in all three. */
::selection{ background:var(--chaos); color:var(--void); }
input, textarea, select, [contenteditable]{ caret-color:var(--chaos); }

/* -- 2. Game tabs / headers / turn rows ----------------------------------- */
.game-tabs{ display:flex; gap:8px; padding:14px 0 0; }
.game-tab{
  flex:1; padding:12px 8px; text-align:center; border-radius:12px; border:1px solid var(--line);
  background:var(--surface); color:var(--ink-dim); font-family:'Cinzel',serif; font-weight:700;
  font-size:clamp(12px,3.4vw,15px); letter-spacing:.03em; cursor:pointer; min-height:46px;
}
.game-tab.active{ color:var(--ink); border-color:var(--chaos); background:var(--surface-2); }

header{ text-align:center; padding:16px 0 4px; }
.eyebrow{ font-family:'JetBrains Mono',monospace; font-size:10.5px; letter-spacing:.2em; color:var(--ink-dim); text-transform:uppercase; margin:0 0 4px; }
h1{ font-family:'Cinzel',serif; font-weight:900; font-size:clamp(19px,5.5vw,26px); margin:0; letter-spacing:.03em;
    background:linear-gradient(90deg, var(--chaos), var(--mind) 45%, var(--order) 90%);
    -webkit-background-clip:text; background-clip:text; color:transparent;
    filter:drop-shadow(0 0 14px rgba(164,104,199,0.35)); }
.turn-row{ display:flex; align-items:center; justify-content:center; gap:10px; margin-top:10px; }
.turn-btn{ width:38px; height:38px; border-radius:50%; border:1px solid var(--line); background:var(--surface); color:var(--ink); font-size:18px; }
.turn-label{ font-family:'JetBrains Mono',monospace; font-size:13px; color:var(--ink-dim); min-width:96px; text-align:center; }

/* -- 3. Cards ------------------------------------------------------------- */
.card{ background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-lg); padding:18px; margin-top:var(--space-3);
       border-top:2px solid var(--card-accent, var(--chaos)); box-shadow:var(--shadow-1); }
.card h2{ font-family:'Cinzel',serif; font-size:13px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-dim); margin:0 0 10px; font-weight:700; }
#dnd-panel-creatures .card{ --card-accent: var(--body); }
#dnd-panel-init .card{ --card-accent: var(--fury); }
#dnd-panel-campaigns .card{ --card-accent: var(--order); }
#dnd-panel-maps .card{ --card-accent: var(--mind); }
#dnd-panel-chars .card{ --card-accent: var(--chaos); }
#dnd-panel-admin .card{ --card-accent: var(--fury); }
#dnd-panel-notes .card{ --card-accent: var(--calm); }
#dnd-panel-notes .card{
  background-image:repeating-linear-gradient(0deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 3px);
}
.dnd-panel{ display:flex; flex-direction:column; gap:12px; }
/* !important: without it this loses a same-specificity cascade tie against
   .theme-parchment .card{margin-top:20px} (both two-class selectors -- source
   order decides -- and the theme rule comes later in the file), which let
   parchment's cards inside every .dnd-panel silently gain an extra 20px on
   top of .dnd-panel's own `gap`, while dark mode (no such override) fell
   through to this reset correctly. That mismatch was the actual root cause
   of "spacing between all the cards is different" reported across the app
   -- not a flat 12px-vs-20px gap, but this reset losing the tie in one theme
   only. Forcing it wins in both themes, so .dnd-panel's own `gap` (12px
   mobile / 20px at the desktop grid breakpoint -- see section 3 and the
   min-width:900px block) is the only thing controlling spacing between
   grouped stat-cards, consistently. */
.dnd-panel .card{ margin-top:0 !important; }
#dnd-sub-creatures.active{ background:var(--body); border-color:var(--body); color:var(--void); }
#dnd-sub-init.active{ background:var(--fury); border-color:var(--fury); color:var(--void); }
#dnd-sub-campaigns.active{ background:var(--order); border-color:var(--order); color:var(--void); }
#dnd-sub-maps.active{ background:var(--mind); border-color:var(--mind); color:var(--void); }
#dnd-sub-chars.active{ background:var(--chaos); border-color:var(--chaos); color:var(--void); }
#dnd-sub-admin.active{ background:var(--fury); border-color:var(--fury); color:var(--void); }
#dnd-sub-notes.active{ background:var(--calm); border-color:var(--calm); color:var(--void); }

/* -- 4. Conquest / life counters (Riftbound + MTG shared) ----------------- */
.duo{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.side{ background:var(--surface-2); border-radius:14px; padding:12px 8px; text-align:center; position:relative; overflow:hidden; }
.side.win{ box-shadow:0 0 0 2px var(--order) inset; }
.name-input{
  width:100%; background:transparent; border:none; color:var(--ink); text-align:center;
  font-family:'Inter',sans-serif; font-weight:600; font-size:14px; padding:4px; border-bottom:1px solid transparent;
}
.name-input:focus-visible{ border-bottom:1px solid var(--chaos); }
.count{ font-family:'JetBrains Mono',monospace; font-weight:700; font-size:clamp(38px,12vw,54px); line-height:1; margin:6px 0; letter-spacing:-.02em; }
.bar{ height:6px; border-radius:4px; background:var(--surface-3); overflow:hidden; margin:6px 0 10px; }
.bar-fill{ height:100%; background:var(--order); transition:width .25s ease; }
.banner{ font-family:'Cinzel',serif; font-size:12px; color:var(--order); min-height:16px; font-weight:700; }
.btn-row{ display:flex; gap:6px; justify-content:center; }
.btn{
  min-width:44px; min-height:44px; border-radius:10px; border:1px solid var(--line); background:var(--surface-3);
  color:var(--ink); font-family:'JetBrains Mono',monospace; font-weight:700; font-size:16px; flex:1;
}
.btn:active{ background:var(--chaos); }
.btn.minus:active{ background:var(--fury); }

.target-row{ display:flex; align-items:center; justify-content:center; gap:8px; margin-top:12px; font-size:12px; color:var(--ink-dim); }
.target-row input{
  width:52px; background:var(--surface-2); border:1px solid var(--line); border-radius:8px; color:var(--ink);
  text-align:center; padding:8px 4px; font-family:'JetBrains Mono',monospace; font-size:14px; min-height:36px;
}
.reset-btn{
  display:block; margin:14px auto 0; background:none; border:1px solid var(--line); color:var(--ink-faint);
  padding:9px 16px; border-radius:10px; font-size:12px; min-height:40px;
}

/* -- 5. Battlefields + toggles -------------------------------------------- */
.bf-list{ display:flex; flex-direction:column; gap:8px; }
.bf-row{ display:flex; align-items:center; gap:8px; background:var(--surface-2); border-radius:12px; padding:8px 10px; }
.bf-name{ flex:1; background:transparent; border:none; color:var(--ink); font-size:13px; font-weight:600; min-width:0; }
.bf-pick{ display:flex; gap:4px; }
.bf-opt{
  min-width:44px; min-height:36px; border-radius:8px; border:1px solid var(--line); background:var(--surface-3);
  color:var(--ink-dim); font-size:11px; font-weight:600; padding:0 8px;
}
.bf-opt.sel-a{ background:var(--mind); color:var(--void); border-color:var(--mind); }
.bf-opt.sel-b{ background:var(--fury); color:var(--void); border-color:var(--fury); }
.bf-opt.sel-c{ background:var(--surface-3); color:var(--order); border-color:var(--order); }

.toggle-row{ display:flex; align-items:center; justify-content:space-between; }
.toggle{ display:flex; gap:6px; }
.toggle button{ padding:9px 14px; border-radius:10px; border:1px solid var(--line); background:var(--surface-2); color:var(--ink-dim); font-size:13px; font-weight:600; min-height:40px; }
.toggle button.on{ background:var(--calm); color:var(--void); border-color:var(--calm); }

/* -- 6. MTG player grid + commander-damage table -------------------------- */
.player-count-row{ display:flex; gap:6px; justify-content:center; margin-bottom:10px; }
.pc-btn{ min-width:44px; min-height:38px; border-radius:10px; border:1px solid var(--line); background:var(--surface-2); color:var(--ink-dim); font-size:13px; font-weight:700; }
.pc-btn.on{ background:var(--chaos); color:var(--void); border-color:var(--chaos); }
.preset-row{ display:flex; gap:6px; justify-content:center; margin-bottom:10px; }
.preset-btn{ padding:8px 14px; border-radius:10px; border:1px solid var(--line); background:var(--surface-2); color:var(--ink-dim); font-size:12px; font-weight:600; min-height:38px; }
.players-grid{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.p-card{ background:var(--surface-2); border-radius:14px; padding:10px; text-align:center; }
.p-card.dead{ opacity:.4; }
.p-life{ font-family:'JetBrains Mono',monospace; font-weight:700; font-size:clamp(30px,9vw,40px); margin:4px 0; }
.p-sub{ display:flex; justify-content:center; gap:14px; font-size:10.5px; color:var(--ink-dim); margin-top:6px; font-family:'JetBrains Mono',monospace; }
.p-sub .flag{ color:var(--fury); font-weight:700; }
.small-btn-row{ display:flex; gap:4px; margin-top:6px; }
/* 2026-08-16: `.small-btn` dresses BOTH <button> and <a>, and the two rendered
   visibly differently side by side (obvious on the community-sheet cards, and
   on the Library's Download link). Two causes: no font-family here meant
   buttons fell back to the BROWSER's UI font while anchors inherited Inter
   from body, and an anchor is inline — so its text sat left-aligned, baseline
   positioned and underlined inside a box built for a centred button.
   font-family:inherit follows the pattern .linkish already established. */
.small-btn{ flex:1; min-height:38px; border-radius:8px; border:1px solid var(--line); background:var(--surface-3); color:var(--ink); font-size:13px; font-weight:700; font-family:inherit; }
/* Make an <a class="small-btn"> behave like the button it is dressed as. */
a.small-btn{
  display:inline-flex; align-items:center; justify-content:center;
  text-align:center; text-decoration:none; padding:0 10px; box-sizing:border-box;
}

.cd-table{ width:100%; border-collapse:collapse; font-size:12px; }
.cd-table th{ color:var(--ink-faint); font-weight:600; padding:6px 4px; text-align:center; font-size:10.5px; }
.cd-table td{ text-align:center; padding:4px; }
.cd-cell{ display:flex; align-items:center; justify-content:center; gap:4px; }
.cd-val{ min-width:20px; font-family:'JetBrains Mono',monospace; font-weight:700; }
.cd-val.lethal{ color:var(--fury); }
.cd-btn{ width:26px; height:26px; border-radius:6px; border:1px solid var(--line); background:var(--surface-3); color:var(--ink); font-size:13px; padding:0; }

.hidden{ display:none !important; }
footer{ text-align:center; padding:20px 14px 6px; font-size:10.5px; color:var(--ink-faint); font-family:'JetBrains Mono',monospace; }

/* -- 7. D&D tools: sub-tabs, inputs, result lists, stat blocks ------------ */
.sub-tab{
  flex:1; padding:10px 6px; text-align:center; border-radius:10px; border:1px solid var(--line);
  background:var(--surface); color:var(--ink-dim); font-family:'Inter',sans-serif; font-weight:600;
  font-size:clamp(10.5px,3vw,12.5px); cursor:pointer; min-height:42px;
}
.sub-tab.active{ color:var(--void); border-color:var(--body); background:var(--body); }
.dnd-panel{ margin-top:0; }
.wide-input{
  width:100%; background:var(--surface-2); border:1px solid var(--line); border-radius:10px; color:var(--ink);
  padding:10px 12px; font-family:'Inter',sans-serif; font-size:13px; min-height:44px; resize:vertical;
}
.wide-input:focus-visible{ border-color:var(--chaos); }
.result-list{ display:flex; flex-direction:column; gap:8px; margin-top:10px; }
.result-item{
  background:var(--surface-2); border-radius:12px; padding:10px 12px; display:flex; justify-content:space-between;
  align-items:center; gap:8px; cursor:pointer; border:1px solid transparent;
}
.result-item:active{ border-color:var(--chaos); }
.result-item .rname{ font-weight:600; font-size:13px; }
.result-item .rsub{ font-size:11px; color:var(--ink-dim); font-family:'JetBrains Mono',monospace; }
.icon-btn{
  min-width:36px; min-height:36px; border-radius:8px; border:1px solid var(--line); background:var(--surface-3);
  color:var(--ink-dim); font-size:14px;
}
.stat-block{ background:var(--surface-2); border-radius:14px; padding:14px; margin-top:12px; }
.stat-block h3{ font-family:'Cinzel',serif; margin:0 0 2px; font-size:16px; }
.stat-block .stype{ font-size:11px; color:var(--ink-dim); font-style:italic; margin-bottom:8px; }
.stat-grid{ display:grid; grid-template-columns:repeat(6,1fr); gap:4px; margin:10px 0; text-align:center; }
.stat-grid div{ background:var(--surface-3); border-radius:8px; padding:6px 2px; }
.stat-grid .slabel{ font-size:9px; color:var(--ink-faint); text-transform:uppercase; }
.stat-grid .sval{ font-family:'JetBrains Mono',monospace; font-weight:700; font-size:13px; }
.stat-meta{ font-size:12px; color:var(--ink-dim); line-height:1.6; }
.stat-actions p{ font-size:12px; line-height:1.5; margin:4px 0 10px; }
.stat-actions b{ color:var(--ink); }

/* -- 8. Initiative tracker ------------------------------------------------ */
.init-list{ display:flex; flex-direction:column; gap:6px; }
.init-row{
  display:flex; align-items:center; gap:8px; background:var(--surface-2); border-radius:10px; padding:8px 10px;
  border-left:3px solid var(--surface-3);
}
.init-row.active-turn{ border-left-color:var(--order); background:var(--surface-3); }
.init-row .init-roll{ font-family:'JetBrains Mono',monospace; font-weight:700; min-width:26px; text-align:center; color:var(--chaos); }
/* 2026-08-15 (bug report: "you cant see initiative"): both of these set
   `border:1px solid var(--line)` at specificity 0,2,0, which beat the
   `border-color:var(--field-border)` field rule further down the file
   (0,1,0) -- so the two boxes in the initiative row were the ONLY inputs in
   the app still drawing a 0.09-alpha border, i.e. none you could see. They
   now use the same --field-border every other field uses. The roll box also
   went 32px -> 46px: 32px could not show a two-digit number plus padding,
   which is most of what initiative ever is. */
.init-row .init-roll-input{ font-family:'JetBrains Mono',monospace; font-weight:700; width:46px; text-align:center; color:var(--chaos); background:var(--surface-3); border:1px solid var(--field-border); border-radius:6px; padding:4px 2px; }
.init-row .init-hp-input{ font-family:'JetBrains Mono',monospace; font-weight:700; width:44px; text-align:center; color:var(--ink); background:var(--surface-3); border:1px solid var(--field-border); border-radius:6px; padding:4px 2px; }
/* Column legend above the rows. Widths mirror the inputs above (roll 46, hp
   44, ac 42) plus .init-row's own 8px gap and 10px side padding, so the
   captions sit over the boxes they name. */
.init-legend{ display:flex; align-items:center; gap:8px; padding:0 10px 2px; font-family:'JetBrains Mono',monospace; font-size:9.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); }
.init-legend .init-legend-roll{ width:46px; text-align:center; }
.init-legend .init-legend-name{ flex:1; min-width:0; }
.init-legend .init-legend-hp{ width:44px; text-align:center; }
.init-legend .init-legend-ac{ width:42px; text-align:center; }
/* Trailing spacer standing in for each row's four action buttons, so HP/AC in
   the legend land over the HP/AC boxes rather than 136px to their right.
   Measured against a live row: .init-cond 20.1 + .init-reroll 28 +
   .init-inspect 28 + .init-remove 28, plus four 8px .init-row gaps = 136.1.
   Declared as one variable so the four button widths above have a single
   place to be reconciled if any of them changes. */
.init-legend .init-legend-actions{ width:var(--init-actions-w, 136px); flex:0 0 auto; }
.init-row .init-name{ flex:1; font-size:13px; font-weight:600; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.init-row .init-name.pc{ color:var(--mind); }
.init-row .init-hp{ display:flex; align-items:center; gap:4px; font-family:'JetBrains Mono',monospace; font-size:12px; }
.init-row .init-hp button{ width:24px; height:24px; border-radius:6px; border:1px solid var(--line); background:var(--surface-3); color:var(--ink); font-size:12px; padding:0; }
.init-row .init-remove{ width:28px; height:28px; border-radius:8px; border:1px solid var(--line); background:transparent; color:var(--fury); font-size:14px; }
.init-inspect{ width:28px; height:28px; border-radius:8px; border:1px solid var(--line); background:transparent; color:var(--mind); font-size:13px; cursor:pointer; }
.init-entry{ margin-bottom:2px; }
.init-detail{ background:var(--surface-2); border:1px solid var(--field-border); border-radius:10px; padding:10px 12px; margin:2px 0 8px; }
.stat-card .stat-top{ font-size:13px; margin-bottom:6px; }
.stat-abils{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:6px; }
.stat-abils span{ font-family:'JetBrains Mono',monospace; font-size:12px; background:var(--surface-3); border:1px solid var(--line); border-radius:6px; padding:2px 6px; }
.stat-abils i{ color:var(--ink-faint); font-style:normal; margin-right:3px; }
.stat-line{ font-size:12px; margin-bottom:6px; color:var(--ink-dim); }
.stat-section{ margin-top:6px; }
.stat-h{ text-transform:uppercase; letter-spacing:.05em; font-size:10px; color:var(--chaos); margin-bottom:2px; }
.stat-action{ font-size:12px; margin-bottom:4px; line-height:1.4; }
.stat-notes{ font-size:12px; color:var(--ink-dim); white-space:pre-wrap; }

.add-combatant-grid{ display:grid; grid-template-columns:2fr 1fr 1fr auto; gap:6px; margin-bottom:10px; }
.add-combatant-grid input{
  background:var(--surface-2); border:1px solid var(--line); border-radius:8px; color:var(--ink); padding:8px; font-size:13px; min-height:40px; min-width:0;
}
.pc-toggle{ display:flex; align-items:center; gap:4px; font-size:11px; color:var(--ink-dim); white-space:nowrap; }

/* -- 9. Character sheets + notes ------------------------------------------ */
.char-form-grid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-bottom:8px; }
/* 2026-08-14 (mobile-friendliness pass): Saving Throws + Skills squeeze into
   two ~150px columns on a phone otherwise -- see the @media(max-width:700px)
   override below that stacks them to one column. */
.profs-grid{ align-items:start; }
.char-form-grid input{
  background:var(--surface-2); border:1px solid var(--line); border-radius:10px; color:var(--ink); padding:10px; font-size:13px; min-height:42px;
}
.char-card{ background:var(--surface-2); border-radius:12px; padding:12px; }
.char-card .rname{ font-weight:700; font-size:14px; }
.char-card .rsub{ font-size:11px; color:var(--ink-dim); margin-top:2px; }
.char-card .notes-text{ font-size:12px; color:var(--ink-dim); margin-top:6px; line-height:1.5; }
.char-card a{ color:var(--calm); font-size:11px; }
.char-card .char-actions{ display:flex; justify-content:flex-end; margin-top:6px; }

.note-card{ background:var(--surface-2); border-radius:12px; padding:12px; }
.note-card .rname{ font-weight:700; font-size:14px; }
.note-card .rsub{ font-size:10.5px; color:var(--ink-faint); font-family:'JetBrains Mono',monospace; margin-bottom:6px; }
.note-card .note-body{ font-size:12.5px; line-height:1.55; white-space:pre-wrap; overflow-wrap:anywhere; }
.note-card .char-actions{ display:flex; justify-content:flex-end; margin-top:6px; }
.ability-input{ width:100%; background:transparent; border:none; color:var(--ink); text-align:center; font-family:'JetBrains Mono',monospace; font-weight:700; font-size:13px; }
#account-card{ margin-top:12px; }

/* -- 10. Screens / game-select / nav -------------------------------------- */
.game-select-grid{ display:flex; flex-direction:column; gap:12px; margin-top:20px; }
.game-select-card{
  padding:28px 20px; border-radius:18px; border:1px solid var(--line); background:var(--surface);
  color:var(--ink); font-family:'Cinzel',serif; font-weight:800; font-size:20px; text-align:left;
  border-left:4px solid var(--gsc-accent, var(--chaos)); box-shadow:0 8px 24px -12px rgba(0,0,0,0.5);
}
.game-select-card:active{ background:var(--surface-2); }
.vis-btn{ padding:9px 14px; border-radius:10px; border:1px solid var(--line); background:var(--surface-2); color:var(--ink-dim); font-size:12px; font-weight:600; flex:1; }
.vis-btn.on{ background:var(--calm); color:var(--void); border-color:var(--calm); }

#game-dnd .game-tabs{ margin-bottom:12px; }
.dnd-panel{ margin-top:0; }

/* -- 11. Maps: stage, viewport, tokens, lens, crop, layers, fog ----------- */
.map-stage{ position:relative; margin-top:10px; }
#map-canvas{ display:block; }
#map-stage{ position:relative; transform-origin:50% 50%; will-change:transform; }
#map-viewport{ position:relative; overflow:hidden; border-radius:12px; touch-action:none; }

.layer-row{ display:flex; align-items:center; gap:8px; padding:5px 0; border-bottom:1px solid var(--line); }
.layer-row:last-child{ border-bottom:0; }
.layer-row .lname{ flex:1; font-size:12px; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.layer-row .lvis{ cursor:pointer; background:none; border:0; font-size:15px; line-height:1; padding:2px 4px; color:var(--ink); }
.layer-row .lvis.off{ opacity:.35; }
.layer-row input[type=range]{ width:82px; }
.layer-row .lbtn{ cursor:pointer; background:var(--surface-3); border:1px solid var(--line); color:var(--ink-dim); border-radius:6px; font-size:11px; padding:2px 6px; line-height:1.4; }
.layer-row .lbtn:hover{ color:var(--ink); }
#map-mode-fog.has-fog{ box-shadow:inset 0 0 0 1px var(--chaos); }

.map-token.tok-active-turn{ box-shadow:0 0 0 3px var(--order), 0 0 18px 4px rgba(212,177,6,0.55); z-index:5; }

.init-ac-input{ width:42px; text-align:center; background:var(--surface-2); border:1px solid var(--line);
                color:var(--ink); border-radius:8px; font-size:12px; padding:4px 2px; }
.init-cond{ background:none; border:0; color:var(--ink-dim); cursor:pointer; font-size:14px; padding:2px 4px; }
.init-cond:hover{ color:var(--order); }
.init-name.downed{ color:var(--ink-faint); text-decoration:line-through; }
.init-chips{ display:flex; flex-wrap:wrap; gap:5px; align-items:center; padding:2px 0 6px 4px; }
.cond-chip{ font-size:10px; text-transform:uppercase; letter-spacing:.05em; padding:2px 7px; border-radius:99px;
            border:1px solid var(--chaos); color:var(--chaos); background:rgba(164,104,199,0.12); cursor:pointer; }
.cond-chip:hover{ background:rgba(164,104,199,0.28); }
.death-saves{ font-size:10px; color:var(--ink-faint); display:inline-flex; align-items:center; gap:3px; }
.ds-pip{ width:16px; height:16px; border-radius:50%; border:1px solid var(--line); background:var(--surface-2);
         color:var(--ink-faint); font-size:9px; line-height:1; cursor:pointer; padding:0; }
.ds-pip.succ.on{ background:var(--body); border-color:var(--body); color:var(--void); }
.ds-pip.fail.on{ background:var(--fury); border-color:var(--fury); color:var(--void); }
.cond-picker{ margin-top:10px; padding:10px; border:1px solid var(--chaos); border-radius:12px; background:var(--surface-2); }
.cond-grid{ display:flex; flex-wrap:wrap; gap:6px; }
.cond-opt{ font-size:11px; padding:4px 9px; border-radius:99px; border:1px solid var(--line);
           background:var(--surface); color:var(--ink-dim); cursor:pointer; }
.cond-opt.on{ background:var(--chaos); border-color:var(--chaos); color:var(--void); }
.combatant-count{ width:44px; margin-left:auto; text-align:center; background:var(--surface-2);
                  border:1px solid var(--line); color:var(--ink); border-radius:8px; font-size:12px; padding:3px 2px; }

.map-token.tok-hidden{ opacity:.42; filter:grayscale(.6); outline:2px dashed var(--order); outline-offset:-2px; }
.tokrow{ display:flex; align-items:center; gap:8px; padding:5px 0; border-bottom:1px solid var(--line); }
.tokrow:last-child{ border-bottom:0; }
.tokrow.off .tokname{ color:var(--ink-faint); text-decoration:line-through; }
.tokrow .tokname{ flex:1; font-size:12px; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.tokeye{ cursor:pointer; background:none; border:0; font-size:14px; line-height:1; padding:2px 4px; }

/* -- 12. Admin panel ------------------------------------------------------ */
.adm-row{ display:flex; align-items:center; gap:8px; padding:8px 0; border-bottom:1px solid var(--line); flex-wrap:wrap; }
.adm-row:last-child{ border-bottom:0; }
.adm-main{ flex:1; min-width:150px; }
.adm-name{ font-size:13px; color:var(--ink); font-weight:600; }
.adm-sub{ font-size:11px; color:var(--ink-faint); word-break:break-all; }
.adm-tag{ font-size:10px; padding:1px 6px; border-radius:99px; border:1px solid var(--line); color:var(--ink-dim); }
.adm-tag.admin{ color:var(--order); border-color:var(--order); }
.adm-tag.off{ color:var(--danger); border-color:var(--danger); }
.adm-btn{ cursor:pointer; background:var(--surface-3); border:1px solid var(--line); color:var(--ink-dim); border-radius:6px; font-size:11px; padding:4px 8px; }
.adm-btn:hover{ color:var(--ink); }
.adm-btn.danger{ color:var(--danger); border-color:var(--danger); }
.adm-stat{ display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px solid var(--line); font-size:12px; }
.adm-stat:last-child{ border-bottom:0; }
.adm-stat b{ color:var(--ink); font-family:'JetBrains Mono',monospace; }

/* -- 13. Auth link styles + verify banner --------------------------------- */
.linkish{ background:none; border:0; padding:0; color:var(--mind); font-size:12px; cursor:pointer; text-decoration:underline; font-family:inherit; }
.linkish:hover{ color:var(--ink); }
#verify-banner{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; background:rgba(212,177,6,0.10); border:1px solid var(--order); border-radius:10px; padding:8px 12px; margin-top:10px; }
#verify-banner .vb-text{ flex:1; min-width:180px; font-size:12px; color:var(--ink-dim); }

/* -- 14. Book reader ------------------------------------------------------ */
.reader-layout{ display:grid; grid-template-columns:1fr; gap:14px; }
@media (min-width: 1000px){ .reader-layout{ grid-template-columns:320px 1fr; align-items:start; } }
#reader-canvas-wrap{ margin-top:10px; overflow:auto; max-height:78vh; background:var(--surface-2); border-radius:10px; text-align:center; }
#reader-canvas{ max-width:100%; height:auto; display:inline-block; }
.hit{ padding:7px 8px; border-bottom:1px solid var(--line); cursor:pointer; }
.hit:last-child{ border-bottom:0; }
.hit:hover{ background:var(--surface-3); }
.hit .hit-page{ font-size:11px; color:var(--order); font-family:'JetBrains Mono',monospace; }
.hit .hit-snip{ font-size:12px; color:var(--ink-dim); margin-top:2px; line-height:1.45; }
.hit .hit-snip mark{ background:rgba(212,177,6,0.30); color:var(--ink); border-radius:3px; padding:0 2px; }
.ocr-chip{ font-size:10px; padding:1px 6px; border-radius:99px; border:1px solid var(--line); color:var(--ink-faint); }
.ocr-chip.ready{ color:var(--body); border-color:var(--body); }
.ocr-chip.working{ color:var(--order); border-color:var(--order); }
#map-token-layer{ position:absolute; left:0; top:0; pointer-events:none; }
#map-token-layer .map-token{ pointer-events:auto; }
#map-lens{ position:absolute; border-radius:50%; overflow:hidden; border:4px solid rgba(255,255,255,.92); box-shadow:0 0 0 2px rgba(0,0,0,.55), 0 10px 30px rgba(0,0,0,.55); cursor:grab; z-index:25; touch-action:none; }
#map-lens.hidden{ display:none; }
#map-lens-canvas{ display:block; width:100%; height:100%; }
#map-crop-overlay{ position:absolute; inset:0; z-index:24; cursor:crosshair; touch-action:none; }
#map-crop-overlay.hidden{ display:none; }
#map-crop-rect{ position:absolute; display:none; border:2px dashed #fff; background:rgba(90,141,239,.16); box-shadow:0 0 0 9999px rgba(0,0,0,.4); pointer-events:none; }
#map-crop-btn.on{ background:var(--chaos); border-color:var(--chaos); color:var(--void); }
#map-lens::after{ content:''; position:absolute; inset:0; border-radius:50%; background:radial-gradient(circle at 32% 26%, rgba(255,255,255,.30), rgba(255,255,255,0) 46%); pointer-events:none; }
#map-lens-toggle.on{ background:var(--mind); border-color:var(--mind); color:var(--void); }
#lens-zoom-ctrl{ position:absolute; top:6px; left:50%; transform:translateX(-50%); display:flex; gap:8px; z-index:2; }
#lens-zoom-ctrl button{ width:32px; height:32px; border-radius:50%; border:1px solid rgba(255,255,255,.55); background:rgba(10,10,16,.72); color:#fff; font-size:19px; line-height:1; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; }
.token-grid{ display:flex; flex-wrap:wrap; gap:10px; }
.token-asset{ width:64px; text-align:center; }
.token-asset .tok-img{ width:56px; height:56px; border-radius:50%; background-size:cover; background-position:center; background-color:var(--surface-3); margin:0 auto; }
.token-asset .tok-name{ font-size:10px; color:var(--ink-dim); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.token-override-row{ display:flex; align-items:center; gap:10px; padding:4px 0; }
.token-override-row .tok-thumb{ width:44px; height:44px; border-radius:50%; background-size:cover; background-position:center; background-color:var(--surface-3); flex-shrink:0; }
.tb-none{ border:3px solid #fff; --tb-ring-color:#fff; }
.tb-gold{ border:3px solid #e0c060; box-shadow:0 0 0 2px #7a5a10; --tb-ring-color:#e0c060; }
.tb-iron{ border:3px solid #9aa3ad; box-shadow:0 0 0 2px #3a4048; --tb-ring-color:#9aa3ad; }
.tb-wood{ border:3px solid #8a5a28; box-shadow:0 0 0 2px #4a2f14; --tb-ring-color:#8a5a28; }
.tb-silver{ border:3px solid #d8dde3; box-shadow:0 0 0 2px #7a8088; --tb-ring-color:#d8dde3; }
.tb-blood{ border:3px solid #c0392b; box-shadow:0 0 0 2px #5a1510; --tb-ring-color:#c0392b; }
.tb-arcane{ border:3px solid #8a63d8; box-shadow:0 0 0 2px #3a1f6a; --tb-ring-color:#8a63d8; }

/* 2026-08-07 ("you should be able to have different shaped borders not just
   circles... conform to the shape of the immage"): border SHAPE is a sibling
   of border COLOR above (.tb-*), applied together e.g. "tb-gold tbs-hex".
   hex/diamond/none use clip-path for the crop and drop-shadow (not
   box-shadow, which stays rectangular) stacked on four sides to trace the
   clipped/alpha shape with the ring colour -- this also covers "conform to
   the shape of the immage" for transparent uploads left at tbs-none.
   !important overrides both .map-token's own border-radius and any inline
   borderRadius style.
   2026-08-13 bug report ("when i use these assets they come out in a
   circle, they should be their actual shape... only use the borders like
   that for custom tokens"): .tbs-circle used to be "the default" -- no
   rule needed, per this comment's old wording -- because .map-token's own
   base rule (below) was hardcoded to border-radius:50%/background-size:
   cover. That meant ANY token without an explicit border_shape (a plain
   creature/library stamp, or an uploaded photo placed directly -- neither
   goes through Token Builder) got force-cropped into a circle it never
   asked for, while only Token Builder's actual custom-token flow
   (TokenBuilder/index.tsx, which always submits an explicit border_shape)
   reliably opts in. .map-token's base now matches .tbs-none's "show the
   real image" look instead, and circle is this now-real .tbs-circle rule
   -- applied only when t.borderShape is actually 'circle' (Token
   Builder's own default pick for a genuine custom token), the same way
   tbs-square/hex/diamond/none already only apply when explicitly set. */
.tbs-circle{ border-radius:50% !important; background-size:cover !important; background-repeat:no-repeat; }
.tbs-square{ border-radius:6px !important; }
.tbs-hex{ border-radius:0 !important; clip-path:polygon(93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%, 50% 0%); border:none !important; box-shadow:none !important; filter:drop-shadow(1.5px 0 0 var(--tb-ring-color)) drop-shadow(-1.5px 0 0 var(--tb-ring-color)) drop-shadow(0 1.5px 0 var(--tb-ring-color)) drop-shadow(0 -1.5px 0 var(--tb-ring-color)); }
.tbs-diamond{ border-radius:0 !important; clip-path:polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); border:none !important; box-shadow:none !important; filter:drop-shadow(1.5px 0 0 var(--tb-ring-color)) drop-shadow(-1.5px 0 0 var(--tb-ring-color)) drop-shadow(0 1.5px 0 var(--tb-ring-color)) drop-shadow(0 -1.5px 0 var(--tb-ring-color)); }
.tbs-none{ border-radius:0 !important; clip-path:none; border:none !important; box-shadow:none !important; background-size:contain !important; background-repeat:no-repeat; filter:drop-shadow(1.5px 0 0 var(--tb-ring-color)) drop-shadow(-1.5px 0 0 var(--tb-ring-color)) drop-shadow(0 1.5px 0 var(--tb-ring-color)) drop-shadow(0 -1.5px 0 var(--tb-ring-color)); }
.init-reroll{ width:28px; height:28px; border-radius:8px; border:1px solid var(--line); background:transparent; color:var(--chaos); font-size:13px; cursor:pointer; }
.char-form-grid .fld{ display:flex; flex-direction:column; gap:3px; }
.char-form-grid .fld > span{ font-size:9.5px; text-transform:uppercase; letter-spacing:.04em; color:var(--ink-faint); padding-left:2px; }
.char-form-grid .fld > input, .char-form-grid .fld > select{ width:100%; }

.lu-step{ border:1px solid var(--line); border-radius:10px; padding:10px; margin-bottom:8px; background:var(--surface-2); }
.lu-step-head{ font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-dim); margin-bottom:8px; display:flex; align-items:center; gap:7px; }
.lu-num{ display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px; border-radius:50%; background:var(--calm); color:var(--void); font-size:10px; font-weight:700; flex:0 0 auto; }
.lu-gain{ display:flex; gap:7px; align-items:baseline; padding:4px 0; border-bottom:1px solid var(--line); }
.lu-gain:last-child{ border-bottom:none; }
.lu-gain b{ color:var(--ink); font-weight:600; }
.lu-tag{ font-size:9px; text-transform:uppercase; letter-spacing:.04em; padding:1px 6px; border-radius:10px; background:var(--body); color:var(--void); font-weight:700; flex:0 0 auto; }
.map-tool-group{ display:flex; flex-wrap:wrap; align-items:center; gap:6px; }
#map-color-custom{ width:34px; height:32px; padding:0; border:1px solid var(--field-border); border-radius:8px; background:none; cursor:pointer; }
.map-swatch{ width:26px; height:26px; border-radius:50%; border:1px solid var(--void); padding:0; cursor:pointer; box-shadow:0 1px 3px rgba(0,0,0,.4); }
.map-swatch:hover{ transform:scale(1.12); }
.map-brush-label{ font-size:11px; color:var(--ink-dim); display:flex; align-items:center; gap:4px; }
.map-tool-sep{ width:1px; height:24px; background:var(--line); display:inline-block; }
/* 2026-08-14 (mobile-friendliness pass): MapToolbar only renders below the
   1100px desktop-docking breakpoint (see MapEditor/index.tsx's `desktop`
   matchMedia check), so this override is unconditional -- no media query
   needed, it can never collide with the desktop ColorPanel/docking chrome
   that also uses .pill-toggle. Baseline .pill-toggle (padding:5px 10px,
   font-size:11px) renders ~22-24px tall, well under a comfortable touch
   target for a toolbar that gets tapped constantly mid-session. */
.map-toolbar .pill-toggle{ min-height:40px; padding:8px 14px; }
.tray-item{ width:40px; height:40px; border-radius:10px; border:1px solid var(--line); background:var(--surface-3) center/70% no-repeat; color:var(--ink); font-size:20px; line-height:1; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; }
.tray-item:hover{ border-color:var(--chaos); }
.tray-item.sel{ border-color:var(--chaos); box-shadow:0 0 0 2px var(--chaos); }
/* 2026-08-14 ("they still have a border there should be no border"): the base rule above used to hardcode a 3px white ring + drop-shadow (border +
   box-shadow) on EVERY token regardless of shape class -- so even a plain library stamp with no t.border/t.borderShape at all (the "actual shape, no
   chrome" case the 2026-08-13 fix above was supposed to give it) still got a ring around it. The ring now comes ONLY from an explicit .tb-* class
   (.tb-gold, .tb-none, etc.), which is added to shapeCls only when t.border is set -- and only Token Builder's own custom-token flow
   (TokenBuilder/index.tsx) ever submits a border value. Library/creature stamps placed straight from the asset picker never set t.border, so they
   now render with zero border/box-shadow by default, same as .tbs-none. Resize handle and name label are unaffected -- they're sibling <span>s
   of this shape div, not children, per the 2026-08-03 note above. */
.map-token{ position:absolute; width:38px; height:38px; border-radius:6px; background-size:contain; background-repeat:no-repeat; background-position:center; cursor:grab; touch-action:none; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:#ffffff; text-shadow:0 1px 2px rgba(0,0,0,.95); font-family:'JetBrains Mono',monospace; }
.map-token.pc{ background-color:var(--mind); }
.map-token.npc{ background-color:var(--body); }
#screen-battle.present header,
#screen-battle.present .toggle-row,
#screen-battle.present #map-dm-tools,
#screen-battle.present #map-token-tray-wrap,
#screen-battle.present #map-save-draw,
#screen-battle.present footer{ display:none !important; }
#screen-battle.present #map-draw-card.card{ border:none; background:transparent; padding:0; box-shadow:none; }
#screen-battle.present #map-canvas{ margin-top:0; }
#map-live-toggle.on{ background:#dc2626; border-color:#dc2626; color:#fff; }
.map-token.obj{ border:none; box-shadow:0 2px 5px rgba(0,0,0,.6); background-color:transparent; border-radius:6px; background-size:contain; background-repeat:no-repeat; background-position:center; color:transparent; }
#map-fullscreen-btn{ position:fixed; top:14px; right:14px; z-index:60; width:46px; height:46px; border-radius:12px; border:1px solid var(--field-border); background:rgba(0,0,0,.55); color:#fff; font-size:22px; display:none; align-items:center; justify-content:center; cursor:pointer; }
#screen-battle.present #map-fullscreen-btn{ display:flex; }
#screen-battle.present #map-draw-card.card{ padding:0; }
#screen-battle.present #map-stage{ display:flex; align-items:center; justify-content:center; min-height:100vh; }
#screen-battle.present #map-viewport{ min-height:100vh; border-radius:0; }
#screen-battle.present #map-cam-bar{ position:fixed; bottom:14px; left:50%; transform:translateX(-50%); z-index:60; background:rgba(0,0,0,.6); border-radius:12px; padding:6px 10px; margin:0; width:auto; backdrop-filter:blur(4px); }
#screen-battle.present #map-cam-bar .rsub{ display:none; }
#screen-battle.present #map-fog-bar,
#screen-battle.present #map-layers-panel{ display:none; }
#map-exit-btn{ position:fixed; top:14px; left:14px; z-index:60; width:46px; height:46px; border-radius:12px; border:1px solid var(--field-border); background:rgba(0,0,0,.55); color:#fff; font-size:20px; display:none; align-items:center; justify-content:center; cursor:pointer; }
#screen-battle.present #map-exit-btn{ display:flex; }
#follow-waiting{ position:fixed; inset:0; z-index:55; background:var(--void); display:flex; align-items:center; justify-content:center; text-align:center; padding:24px; }
#follow-waiting .fw-title{ font-family:'Cinzel',serif; font-size:30px; color:var(--ink); margin-bottom:10px; letter-spacing:1px; }
#follow-waiting .fw-sub{ color:var(--ink-dim); font-size:15px; }
#follow-waiting.has-bg{ background-size:cover; background-position:center; }
#follow-waiting.has-bg::before{ content:''; position:absolute; inset:0; background:rgba(6,6,10,0.5); }
#follow-waiting .fw-inner{ position:relative; z-index:1; }
#follow-waiting.has-bg .fw-title{ text-shadow:0 2px 14px rgba(0,0,0,0.95); }
#follow-waiting.has-bg .fw-sub{ color:#e8e8ee; text-shadow:0 2px 14px rgba(0,0,0,0.95); }
.tok-label{ position:absolute; top:calc(100% + 3px); left:50%; transform:translateX(-50%); font-size:10px; font-weight:700; color:#fff; background:rgba(0,0,0,.72); padding:1px 6px; border-radius:6px; white-space:nowrap; pointer-events:none; text-shadow:none; font-family:'Inter',sans-serif; letter-spacing:.2px; max-width:120px; overflow:hidden; text-overflow:ellipsis; }
#map-present-btn.on{ background:var(--order); border-color:var(--order); color:var(--void); }
.token-tray{ display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
.token-chip{ display:flex; align-items:center; gap:6px; padding:4px 10px 4px 4px; border-radius:20px; border:1px solid var(--line); background:var(--surface-3); color:var(--ink); font-size:12px; cursor:pointer; }
.token-chip .swatch{ width:22px; height:22px; border-radius:50%; background-size:cover; background-position:center; background-color:var(--surface-2); }
.map-vis-row{ display:flex; align-items:center; gap:6px; }
.pill-toggle{ padding:5px 10px; border-radius:20px; border:1px solid var(--line); background:var(--surface-2); color:var(--ink-dim); font-size:11px; font-weight:600; cursor:pointer; }
.pill-toggle.on{ background:var(--calm); color:var(--void); border-color:var(--calm); }

/* -- 15. Library search --------------------------------------------------- */
.lib-hit mark{ background:var(--order); color:var(--void); padding:0 2px; border-radius:3px; }
.lib-snippet{ font-size:11.5px; color:var(--ink-dim); line-height:1.5; margin-top:4px; max-height:120px; overflow:auto; }

/* -- 16. Battle screen ---------------------------------------------------- */
.battle-layout{ display:flex; flex-direction:column; gap:16px; margin-top:14px; }
.battle-sidebar{ display:flex; flex-direction:column; gap:16px; }
.battle-combatant-row{ display:flex; align-items:center; gap:10px; padding:9px 12px; background:var(--surface-2); border-radius:10px; margin-bottom:6px; cursor:pointer; transition:background .15s ease; }
.battle-combatant-row:hover{ background:var(--surface-3); }
.battle-combatant-row input[type="checkbox"]{ width:18px; height:18px; accent-color:var(--fury); flex-shrink:0; }
.grid-size-input{ width:56px; background:var(--surface-2); border:1px solid var(--line); border-radius:8px; color:var(--ink); padding:6px; font-family:'JetBrains Mono',monospace; font-size:12px; text-align:center; }
.grid-size-label{ display:flex; align-items:center; gap:6px; font-size:11px; color:var(--ink-dim); white-space:nowrap; }

/* -- 17. Dice roller + theme toggle (fixed, global) ----------------------- */
#dice-roller-btn{
  position:fixed; bottom:calc(20px + var(--safe-bottom)); right:20px; z-index:9999;
  width:52px; height:52px; border-radius:50%; background:var(--chaos); color:var(--void);
  display:flex; align-items:center; justify-content:center; font-size:24px; border:none;
  box-shadow:0 4px 14px rgba(0,0,0,.45);
}
#theme-toggle-btn{
  position:fixed; bottom:calc(20px + var(--safe-bottom)); right:82px; z-index:9999;
  width:52px; height:52px; border-radius:50%; background:var(--surface-3); color:var(--ink);
  display:flex; align-items:center; justify-content:center; font-size:22px; border:1px solid var(--field-border);
  box-shadow:0 4px 14px rgba(0,0,0,.35);
}
#dice-roller-panel{
  position:fixed; bottom:calc(82px + var(--safe-bottom)); right:20px; z-index:9999; width:260px; max-width:calc(100vw - 40px);
  background:var(--surface); border:1px solid var(--field-border); border-radius:16px; padding:14px;
  box-shadow:0 10px 28px rgba(0,0,0,.5);
}
.dice-type-grid{ display:grid; grid-template-columns:repeat(4,1fr); gap:6px; margin:10px 0; }
.dice-type-btn{ padding:10px 4px; border-radius:8px; border:1px solid var(--field-border); background:var(--surface-2); color:var(--ink); font-weight:700; font-size:12px; }
.dice-pool-row{ display:flex; justify-content:space-between; align-items:center; padding:4px 0; font-size:12px; }
#dice-result{ margin-top:10px; }

/* Shared dice-roll history (RollLog.tsx) -- scrollable, chronological, v1
   scope per NERDLAB.md's "shared dice-roll history" item. Reuses the panel's
   existing tokens (--field-border/--ink-dim/--chaos), no new hex values. */
.dice-log{ max-height:150px; overflow-y:auto; display:flex; flex-direction:column; gap:4px; }
.dice-log-row{ display:grid; grid-template-columns:1fr 1fr auto; gap:6px; align-items:baseline; font-size:11px; padding:2px 0; border-bottom:1px solid var(--field-border); }
.dice-log-roller{ color:var(--ink-dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dice-log-expr{ color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dice-log-total{ color:var(--chaos); font-weight:700; text-align:right; }

/* Dice-system upgrade (2026-08-14): animated roll tiles (DiceAnimation.tsx)
   + roll-log header/search/filter/clear controls + private badge
   (RollLog.tsx). Reuses the same tokens as the v1 dice-log block above --
   no new hex values. */
.dice-anim-row{ display:flex; flex-wrap:wrap; gap:8px; justify-content:center; padding:8px 0; }
.dice-anim-die{ display:flex; flex-direction:column; align-items:center; justify-content:center; width:46px; height:46px; border-radius:10px; background:var(--surface-2); border:1px solid var(--field-border); transition:background .15s ease, border-color .15s ease; }
.dice-anim-die.settled{ background:var(--chaos); border-color:var(--chaos); }
.dice-anim-face{ font-size:16px; font-weight:700; color:var(--ink); line-height:1; }
.dice-anim-die.settled .dice-anim-face{ color:var(--void); }
.dice-anim-sides{ font-size:8px; color:var(--ink-dim); margin-top:2px; text-transform:uppercase; letter-spacing:.03em; }
.dice-anim-die.settled .dice-anim-sides{ color:var(--void); opacity:.75; }

.dice-log-header{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; }
.dice-log-clear-btn{ font-size:10px; padding:3px 8px; border-radius:6px; border:1px solid var(--field-border); background:var(--surface-2); color:var(--ink-dim); cursor:pointer; }
.dice-log-clear-btn:hover{ color:var(--chaos); border-color:var(--chaos); }
.dice-log-filters{ display:flex; gap:6px; margin-bottom:6px; }
.dice-log-search{ flex:1; min-width:0; background:var(--surface-2); border:1px solid var(--field-border); border-radius:8px; color:var(--ink); padding:5px 8px; font-size:11px; }
.dice-log-roller-filter{ background:var(--surface-2); border:1px solid var(--field-border); border-radius:8px; color:var(--ink); padding:5px 6px; font-size:11px; max-width:96px; }
.dice-log-private-badge{ display:inline-block; margin-left:6px; padding:1px 5px; border-radius:5px; background:var(--surface-3); border:1px solid var(--field-border); color:var(--ink-dim); font-size:9px; text-transform:uppercase; letter-spacing:.03em; }

.slot-level-row{ display:flex; align-items:center; gap:8px; margin-bottom:6px; }
.slot-level-label{ font-size:11px; color:var(--ink-dim); min-width:58px; }
.slot-pips{ display:flex; gap:5px; flex-wrap:wrap; }
.slot-pip{ width:22px; height:22px; border-radius:50%; border:1px solid var(--field-border); background:var(--surface-2); cursor:pointer; padding:0; }
.slot-pip.used{ background:var(--fury); border-color:var(--fury); }

/* -- 18. Spell browser / entry modals ------------------------------------- */
#spell-browser-modal{ position:fixed; inset:0; z-index:10000; display:flex; align-items:center; justify-content:center; padding:20px; }
#entry-desc-modal{ position:fixed; inset:0; z-index:10001; display:flex; align-items:center; justify-content:center; padding:20px; }
#entry-desc-modal.hidden{ display:none; }
#entry-desc-backdrop{ position:absolute; inset:0; background:rgba(0,0,0,.6); }
#entry-desc-box{ position:relative; background:var(--surface); border:1px solid var(--field-border); border-radius:16px; padding:18px 20px; max-width:540px; width:100%; max-height:80vh; overflow-y:auto; }
.entry-link{ cursor:pointer; text-decoration:underline dotted; text-underline-offset:2px; }
.entry-link:hover{ color:var(--mind); }
#spell-browser-backdrop{ position:absolute; inset:0; background:rgba(0,0,0,.65); }
#spell-browser-panel{ position:relative; background:var(--surface); border:1px solid var(--field-border); border-radius:16px; padding:16px; width:min(560px, 100%); max-height:82vh; overflow-y:auto; box-shadow:0 12px 40px rgba(0,0,0,.6); }
.spell-browse-item{ background:var(--surface-2); border-radius:10px; padding:10px 12px; margin-bottom:8px; }
.spell-browse-header{ display:flex; justify-content:space-between; align-items:center; gap:8px; cursor:pointer; }
.spell-browse-desc{ font-size:12px; color:var(--ink-dim); margin-top:0; line-height:1.5; display:none; }
.spell-browse-desc.open{ display:block; margin-top:8px; }
.spell-level-group-label{ font-size:10px; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-faint); margin:10px 0 6px; }

/* -- 19. General interaction polish + responsive breakpoints -------------- */
button{ cursor:pointer; transition:background-color .15s ease, border-color .15s ease, color .15s ease, opacity .15s ease, transform .08s ease; }
button:active{ transform:scale(0.97); }
/* Hover states below are gated on :not(:disabled). Without the gate a
   disabled .small-btn still lit its border on hover -- the control said
   "unavailable" and "press me" at the same time. See the disabled block. */
.reset-btn:not(:disabled):hover{ border-color:var(--chaos); }
.icon-btn:not(:disabled):hover{ border-color:var(--fury); color:var(--fury); }
.small-btn:not(:disabled):hover{ background:var(--surface-2); border-color:var(--chaos); }
.pill-toggle:not(:disabled):hover{ border-color:var(--calm); }
.sub-tab:not(:disabled):hover:not(.active){ background:var(--surface-2); color:var(--ink); }
.game-tab:not(:disabled):hover:not(.active){ background:var(--surface-2); }
.game-select-card:active{ transform:scale(0.98); }
.result-item:hover{ background:var(--surface-3); }

/* -- 19a. Focus: one designed ring, keyboard only -------------------------
   2026-08-21. Before this, 12 of 12 tabbable elements on the public landing
   page fell through to Chrome's UA ring (rgb(16,16,16) auto 1px) -- a
   near-invisible dark hairline on a near-black ground, on the first surface
   a prospective GM ever meets. The handful of designed rings that did exist
   (.showcase-carousel, .social-pill, a.storage-upgrade-cta, the map editor's
   float panels) were per-component and covered none of it.

   Two deliberate choices here:

   :where() keeps the whole selector list at specificity 0, so this rule sits
   UNDER every bespoke :focus-visible further down the file rather than
   fighting it -- the carousel keeps its 6px offset, the social pills keep
   their per-brand --tint ring, and this only fills the gaps.

   No border-radius:inherit here. Every current browser already clips an outline
   to the element's own border-radius, and inheriting the PARENT's radius
   would change the element's real shape (a button inside an 18px-radius card
   would suddenly be an 18px-radius button). The ring follows the control's
   own corners without help.

   --chaos measures 4.45:1 against --surface and 4.82:1 against --void in dark,
   6.9:1 light, 5.4:1 parchment -- past the 3:1 minimum for a non-text
   indicator in every theme. */
:where(a, button, summary, input, select, textarea, [tabindex]):focus-visible{
  outline:2px solid var(--chaos);
  outline-offset:2px;
}
/* Text fields sit inside dense grids (.stat-grid's 4px gutters, .init-row),
   where a 2px offset ring reaches into the neighbouring cell. Tight to the
   field instead; the field also still takes its --chaos border. */
:where(input, textarea, select):focus-visible{ outline-offset:1px; }

/* -- 19b. Press: a receipt on every control -------------------------------
   The audit counted 38 :hover rules against 16 :active, and 14-to-1 inside
   the map editor. button:active's scale(0.97) covers real <button>s, but at
   34px (the map tool rail) a 3% scale is invisible, and it covers no anchor
   dressed as a button and no non-button control at all. This matters most on
   touch, where hover does not exist: without it a tap on a tool produces no
   acknowledgement whatsoever until the state changes.

   A filter rather than a background swap, for one reason: several of these
   controls carry their fill inline (TokenBuilder's save button) or in an
   .active / .on variant, and a background rule would either lose to the
   inline style or stamp over the selected state. brightness() composes with
   whatever is underneath, so one rule covers plain, selected and inline-styled
   controls identically. Direction flips per theme -- lifting toward light on
   a dark ground, pressing toward dark on a light one -- which is the same
   call the dice roller's own light-mode :active already made. */
:root{ --press-filter:brightness(1.28); }
:root[data-theme="light"]{ --press-filter:brightness(0.93); }
.theme-parchment{ --press-filter:brightness(0.95); }

.btn:not(:disabled):active, .reset-btn:not(:disabled):active, .small-btn:not(:disabled):active,
.icon-btn:not(:disabled):active, .sub-tab:not(:disabled):active, .game-tab:not(:disabled):active,
.pill-toggle:not(:disabled):active, .vis-btn:not(:disabled):active, .bf-opt:not(:disabled):active,
.cd-btn:not(:disabled):active, .turn-btn:not(:disabled):active, .slot-pip:not(:disabled):active,
.showcase-nav:not(:disabled):active, .showcase-dot:not(:disabled):active,
.result-item:active, .hit:active, .linkish:not(:disabled):active,
a.small-btn:active, .social-pill:active{
  filter:var(--press-filter);
}
/* a.storage-upgrade-cta:hover already sets a filter, and hover is live during
   a press -- so this needs to out-specify it rather than ride the list above. */
a.storage-upgrade-cta:active{ filter:brightness(0.9); }

/* -- 19c. Disabled: unavailable, not broken -------------------------------
   Three disabled rules existed in the whole stylesheet against 30 uses of the
   disabled prop in the Maps screens alone, so a disabled map tool arrived as
   a flat opacity:.5 wash -- which on this ground reads as a rendering fault
   rather than as a state.

   filter, again for the inline-style reason above: a disabled button carrying
   "background: var(--chaos)" inline cannot be re-coloured by any class rule,
   but it can be desaturated. Dropping the saturation is what separates
   "switched off" from "dimmed": the control keeps its shape and label and
   loses its charge. Opacity stays well above the old .5 so the label is still
   readable -- an unavailable control the user cannot read is a worse state
   than a bright one. */
button:disabled, [aria-disabled="true"]{
  cursor:not-allowed;
  opacity:1;
  filter:grayscale(0.75) opacity(0.68);
  transform:none;
  box-shadow:none;
}
button:disabled:active, [aria-disabled="true"]:active{
  filter:grayscale(0.75) opacity(0.68);
  transform:none;
}
input:disabled, textarea:disabled, select:disabled{
  cursor:not-allowed; background:var(--surface); color:var(--ink-faint);
  border-color:var(--line); filter:none;
}
input:disabled::placeholder, textarea:disabled::placeholder{ color:var(--ink-faint); }
input[type="file"]:disabled::file-selector-button{ cursor:not-allowed; filter:grayscale(0.75) opacity(0.68); }

/* -- 19d. <button> wearing a non-button class -----------------------------
   .result-item and .hit were divs with onClick. They are now real buttons
   (see the Cluster 2 pass), which means they arrive carrying the UA button
   defaults -- centred text, the browser UI font, a fixed intrinsic width.
   Same normalisation a.small-btn already needed in the other direction. */
button.result-item, button.hit{
  width:100%; text-align:left; font:inherit; color:inherit;
  background-clip:padding-box;
}
button.hit{ border:0; border-bottom:1px solid var(--line); border-radius:0; background:none; display:block; }
button.hit:last-child{ border-bottom:0; }
input, textarea, select{ transition:border-color .15s ease; }
input:focus-visible, textarea:focus-visible, select:focus-visible{ border-color:var(--chaos); }
:where(input[type="text"], input[type="number"], input[type="password"], input[type="email"],
       input[type="search"], input[type="tel"], input[type="url"], input[type="date"],
       input[type="time"], textarea, select){
  background:var(--surface-2); color:var(--ink); border:1px solid var(--field-border);
  border-radius:10px; padding:10px 12px; font-family:'Inter',sans-serif; font-size:13px;
}
input[type="file"]::file-selector-button{
  background:var(--surface-3); color:var(--ink); border:1px solid var(--field-border);
  border-radius:8px; padding:6px 10px; margin-right:10px; cursor:pointer; font-size:12px;
}
input[type="file"]::file-selector-button:hover{ border-color:var(--chaos); }
select option{ background:var(--surface); color:var(--ink); }
input[type="text"], input[type="number"], input[type="file"], textarea, select,
.wide-input, .char-form-grid input, .grid-size-input, .init-roll-input, .init-hp-input{
  border-color: var(--field-border);
}
/* 2026-08-15: placeholder text was falling through to the browser default,
   which on a dark field is dimmer than --ink-faint and effectively unreadable
   -- that is why the initiative box (placeholder "—") and the AC box
   (placeholder "AC") both looked like empty black squares in the screenshots
   rather than like labelled, fillable fields. --ink-dim is the same token the
   app already uses for secondary text, so this makes placeholders as legible
   as any other hint on the page without making them compete with real
   values, which stay at --ink. */
input::placeholder, textarea::placeholder{ color:var(--ink-dim); opacity:1; }
.stat-grid div{ border:1px solid var(--field-border); }
input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; }
input[type="number"]{ -moz-appearance:textfield; }

@media (max-width: 700px){
  body{ overflow-x:hidden; }
  .wrap{ padding:0 12px; }
  .game-tabs{ flex-wrap:nowrap; overflow-x:auto; overflow-y:hidden; -webkit-overflow-scrolling:touch; scroll-snap-type:x proximity; margin-left:-12px; margin-right:-12px; padding-left:12px; padding-right:12px; padding-bottom:4px; }
  .game-tabs::-webkit-scrollbar{ display:none; }
  .game-tab, .sub-tab{ flex:0 0 auto; scroll-snap-align:start; white-space:nowrap; }
  .sub-tab{ min-width:100px; padding:11px 16px; }
  .game-tab{ min-width:112px; }
  .card{ border-radius:14px; }
  input, textarea, select{ font-size:16px; }
  .reset-btn{ min-height:46px; }
  /* 2026-08-14 (mobile-friendliness pass) -------------------------------- */
  .icon-btn{ min-width:44px; min-height:44px; }
  .slot-pip{ width:30px; height:30px; }
  .ds-pip{ width:28px; height:28px; font-size:10px; }
  .profs-grid{ grid-template-columns:1fr; }
  /* Initiative row: name was squeezed to a few px wide alongside six fixed-
     width siblings on a phone -- give it its own full-width line above the
     roll/hp/ac/action controls instead of truncating it to nothing. */
  /* The legend can only line up with a single-line row; once .init-row wraps
     the columns no longer exist, so hide it rather than mislabel them. */
  .init-legend{ display:none; }
  .init-row{ flex-wrap:wrap; }
  .init-row .init-name{ order:-1; flex:1 1 100%; white-space:normal; overflow:visible; text-overflow:clip; margin-bottom:4px; }
  /* Multiclass/Attack rows: 4-5 side-by-side fields collapse to a legible
     2-column layout instead of squeezing every field to ~10 characters wide.
     !important is deliberate here -- these rows also carry an inline
     grid-template-columns from React (desktop layout), which a plain class
     rule cannot outrank. */
  .multiclass-row{ grid-template-columns:1fr 1fr !important; }
  .multiclass-row > button{ grid-column:1 / -1 !important; justify-self:end; }
  .attack-row{ grid-template-columns:1fr 1fr !important; }
  .attack-row > label:first-child, .attack-row > label:nth-child(3), .attack-row > button{ grid-column:1 / -1 !important; }
  .attack-row > button{ justify-self:end; }
}
@media (min-width: 900px){
  .wrap{ max-width:1400px; padding:0 40px; }
  .game-select-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); }
  #dnd-panel-init, #dnd-panel-chars, #dnd-panel-notes{
    display:grid; grid-template-columns:1fr 1fr; gap:20px; align-items:start;
  }
  #dnd-panel-maps{ grid-template-columns:1fr 2fr; }
  #dnd-panel-creatures .card, #dnd-panel-init .card, #dnd-panel-maps .card,
  #dnd-panel-chars .card, #dnd-panel-lib .card, #dnd-panel-notes .card{ margin-top:0; }
  .players-grid{ grid-template-columns:repeat(4,1fr); }
  .duo{ max-width:520px; margin:0 auto; }
  .battle-layout{ display:grid; grid-template-columns:2fr 1fr; align-items:start; }
}
@media (min-width: 1600px){
  .wrap{ max-width:1900px; padding:0 56px; }
  #dnd-panel-maps{ grid-template-columns:1fr 3fr; }
  .battle-layout{ grid-template-columns:3fr 1fr; }
  #dnd-panel-init, #dnd-panel-chars, #dnd-panel-notes{
    grid-template-columns:1fr 1.4fr;
  }
}

/* ============================================================================
   12. Parchment / illuminated-manuscript re-skin
   ----------------------------------------------------------------------------
   Real, user-provided raster chrome (not procedurally generated) drives every
   shape below: /border_frame.png, /tab_active.png, /tab_inactive.png,
   /btn_primary.png, /btn_success.png, /btn_accent.png, /input_field.png,
   /checkbox.png (all in public/, text baked into the originals was stripped
   via inpainting so any label can be overlaid). border-image (9-slice) is
   used everywhere a shape must stretch to fit variable-length text, so a
   short "Party" tab and a long "Maps / Token Manager" tab share one asset.

   Scope: `.theme-parchment` wraps the app shell (DndShell, CampaignSelect,
   Login, Admin) and the public Landing page, so every screen that renders
   .card / .reset-btn / .game-tab / .sub-tab / inputs picks this up
   automatically -- no per-screen CSS needed.
   ============================================================================ */
.theme-parchment{
  --parch-bg:#e8dcc0; --parch-bg-2:#ddcda3; --parch-panel:#efe4c8; --parch-panel-2:#e6d8b8; --parch-panel-3:#ddc9a0;
  --parch-ink:#2a1810; --parch-ink-dim:#5b4630; --parch-ink-faint:#7a6650;
  --parch-maroon:#6b1f2a; --parch-maroon-dark:#4c1620; --parch-maroon-line:rgba(107,31,42,0.55);
  --parch-cream:#f5ecd8;
  --font-display:'Cinzel', serif; --font-body:'EB Garamond', serif; --font-script:'Dancing Script', cursive;

  /* Token remap -- this is the piece that actually standardizes the whole UI.
     Every component in this codebase is built on --surface/--ink/--line/etc
     (the "no raw hex" design rule), the same way :root[data-theme="light"]
     already re-themes the whole app for light mode. Re-pointing those same
     tokens at the parchment palette means .char-card, .adm-row, .result-item,
     .stat-block, .init-row, .layer-row, .cond-chip, .pill-toggle, the dice
     roller panel, the spell browser, literally everything -- re-themes
     automatically, instead of chasing individual component classes and
     leaving dark-theme fragments behind (as happened on first pass: My
     Campaigns cards stayed dark purple because .char-card was never
     special-cased). The accent colors below are the exact values this
     stylesheet's own light theme already uses on light backgrounds (see
     :root[data-theme="light"] above) -- reused here rather than invented,
     since they're already tuned for contrast on a light surface. */
  --void:var(--parch-cream);
  --surface:var(--parch-panel); --surface-2:var(--parch-panel-2); --surface-3:var(--parch-panel-3);
  --line:rgba(107,31,42,0.22); --field-border:rgba(107,31,42,0.4);
  --ink:var(--parch-ink); --ink-dim:var(--parch-ink-dim); --ink-faint:var(--parch-ink-faint);
  --fury:#C6353A; --order:#8A6A00; --body:#256B32; --chaos:#7B3FA0; --mind:#1F5F9E; --calm:#207468; --danger:#C6353A;

  position:relative;
  color:var(--parch-ink);
  font-family:var(--font-body);
}
.theme-parchment .eyebrow{
  font-family:var(--font-body); font-weight:600; font-variant:small-caps; letter-spacing:.16em;
  color:var(--parch-maroon); font-size:13px;
}
.theme-parchment h1{
  font-family:var(--font-display); font-weight:800; color:var(--parch-maroon);
  background:none; -webkit-text-fill-color:unset; filter:none; letter-spacing:.02em;
}
.theme-parchment .rsub{ color:var(--parch-ink-dim); }
.theme-parchment a{ color:var(--parch-maroon); }
.theme-parchment footer{ color:var(--parch-ink-faint); }

/* Full-viewport parchment fill + real ornate cracked-border frame image. A
   fixed, empty, aria-hidden div (see Landing.tsx / DndShell wrapper) rather
   than a body background so this stays scoped to routes wrapped in
   .theme-parchment. No dedicated background-texture asset was provided, so
   the fill is a plain CSS gradient in the same tones as the border art
   rather than a fake generated texture; the border itself is the real image. */
.parchment-frame{
  position:fixed; inset:0; z-index:-1; box-sizing:border-box; pointer-events:none;
  background:radial-gradient(circle at 50% 0%, var(--parch-bg) 0%, var(--parch-bg-2) 100%);
  border-style:solid; border-color:transparent; border-width:clamp(24px,3.6vw,52px);
  border-image-source:url('/border_frame.png');
  border-image-slice:90; border-image-width:clamp(24px,3.6vw,52px); border-image-repeat:round;
}

/* Cards: double-line frame (border + offset outline) + small corner flourish
   marks, panel bg one shade off the page bg so it reads as "inset". No
   dedicated panel-chrome asset was provided (only full pricing-card
   compositions), so this stays a CSS approximation. */
.theme-parchment .card{
  background:var(--parch-panel);
  border:1px solid var(--parch-maroon-line);
  outline:1px solid var(--parch-maroon-line); outline-offset:5px;
  box-shadow:0 4px 10px -6px rgba(28,19,12,0.35);
  position:relative; margin-top:20px;
}
.theme-parchment .card h2{ font-family:var(--font-display); color:var(--parch-maroon); }
.theme-parchment .card::before, .theme-parchment .card::after{
  content:'\2767'; position:absolute; color:var(--parch-maroon); font-size:13px; opacity:.75; line-height:1;
}
.theme-parchment .card::before{ top:-9px; left:-9px; }
.theme-parchment .card::after{ bottom:-9px; right:-9px; transform:rotate(180deg); }

/* Buttons: real carved-plaque art via border-image (9-slice so the shape
   holds at any label length). .reset-btn is the primary (maroon) button
   site-wide; .btn-success / .btn-accent are opt-in modifiers for the green
   "Start"/purple "Get Started" family (pricing page, upsells).
   !important on background/border-color/box-shadow: many call sites carry
   inline style={{background:'var(--chaos)', borderColor:'var(--chaos)', ...}}
   left over from the old dark theme's solid-color pill buttons (used to
   recolor a button without switching class -- .reset-btn styled as
   "Enter"/purple, "Delete"/red, etc). Inline styles beat any class
   selector in the cascade, so without !important those leftover inline
   colors paint through wherever the plaque PNG has a transparent or
   antialiased edge, showing as a stray colored halo around the carved
   plaque. Forcing these three properties keeps the plaque art authoritative
   regardless of what an individual call site sets inline; which PNG variant
   renders is still controlled by class (.reset-btn/.btn-success/.btn-accent),
   so the different button "colors" the app uses stay visually distinct. */
.theme-parchment .reset-btn, .theme-parchment .btn-success, .theme-parchment .btn-accent{
  color:var(--parch-cream); font-family:var(--font-display); font-weight:700; letter-spacing:.03em;
  border-style:solid; border-color:transparent !important; border-width:14px 18px;
  border-image-slice:18 fill; border-image-repeat:stretch;
  background:none !important; box-shadow:none !important; padding:10px 20px;
}
.theme-parchment .reset-btn{ border-image-source:url('/btn_primary.png'); }
.theme-parchment .btn-success{ border-image-source:url('/btn_success.png'); }
.theme-parchment .btn-accent{ border-image-source:url('/btn_accent.png'); }
.theme-parchment .reset-btn:active, .theme-parchment .btn-success:active, .theme-parchment .btn-accent:active{
  filter:brightness(0.92);
}
.theme-parchment .game-select-card{
  background:var(--parch-cream); color:var(--parch-ink); border:1px solid var(--parch-maroon-line);
  outline:1px solid var(--parch-maroon-line); outline-offset:4px;
  font-family:var(--font-display); border-left:4px solid var(--parch-maroon);
  box-shadow:0 4px 10px -6px rgba(28,19,12,0.3);
}
.theme-parchment .game-select-card:active{ background:var(--parch-panel-2); }

/* Nav-tab plaques: real tan (inactive) / maroon (active) plaque art via
   border-image, same 9-slice approach as buttons so "Party" and "Maps /
   Token Manager" both hold their corner facets. */
.theme-parchment .game-tab, .theme-parchment .sub-tab{
  color:var(--parch-ink-dim); font-family:var(--font-display);
  border-style:solid; border-color:transparent; border-width:12px 16px;
  border-image-source:url('/tab_inactive.png'); border-image-slice:16 fill; border-image-repeat:stretch;
  background:none; box-shadow:none;
}
.theme-parchment .game-tab.active, .theme-parchment .sub-tab.active{
  color:var(--parch-cream);
  border-image-source:url('/tab_active.png');
}

/* Inputs: cream bg, thin maroon border. Selects use the real dropdown-field
   chrome (it has a baked-in arrow, which reads correctly only on <select>);
   plain text inputs/textareas stay a flat CSS style since the asset's arrow
   would look wrong on them. */
/* input[type="file"] excluded: the generic cream-bg + maroon-border box wraps
   the WHOLE native file control (button + "No file chosen" label), doubling
   up with the file-selector-button's own chrome (section 19) as an ugly
   outer rectangle around an already-styled pill (user feedback: "get rid of
   the border around choose file"). The file-selector-button itself keeps its
   own styling untouched; this only removes the redundant outer box. */
.theme-parchment input:not([type="file"]), .theme-parchment textarea{
  background:var(--parch-cream); color:var(--parch-ink); border:1px solid var(--parch-maroon-line);
}
.theme-parchment select{
  color:var(--parch-ink); font-family:var(--font-body);
  border-style:solid; border-color:transparent; border-width:14px 18px;
  border-image-source:url('/input_field.png'); border-image-slice:18 fill; border-image-repeat:stretch;
  background:none; appearance:none; -webkit-appearance:none;
}
.theme-parchment input::placeholder, .theme-parchment textarea::placeholder{ color:var(--parch-ink-faint); }
.theme-parchment .name-input:focus-visible, .theme-parchment .wide-input:focus-visible{ border-color:var(--parch-maroon); }

/* Checkboxes: real tan swatch art. Checked state was originally a flat inset
   maroon square (::after{content:''; inset:4px; background:...}), then a
   centered checkmark glyph via ::after on the input -- both still reported
   as "weird" (user feedback, twice, after live-verifying the deploy landed).
   Root cause: ::after generated content on <input> targets a replaced
   element, which is not reliably rendered across browsers per spec, and the
   native UA checkbox margin was never reset, which can shift a custom 22px
   box off its row's alignment. This version drops ::after entirely -- the
   checked state layers a second background-image (an inline maroon
   checkmark SVG) on top of the tan swatch. background-image is a normal
   property and applies to <input> reliably everywhere, no generated content
   involved. margin/vertical-align/flex-shrink are reset explicitly so
   nothing can push the box off-row. */
.theme-parchment input[type="checkbox"]{
  appearance:none; -webkit-appearance:none; width:22px; height:22px; min-width:22px;
  margin:0; padding:0; vertical-align:middle; flex-shrink:0;
  /* background-color:transparent is load-bearing, not decoration: the generic
     ".theme-parchment input" rule above sets the `background` SHORTHAND
     (which includes background-color), and while this rule's higher
     specificity wins for background-image/size/repeat/position (all set
     explicitly below), it never set background-color itself -- so that
     property fell through to the generic rule's cream fill, showing as a
     pale halo around the swatch art (user feedback: "get rid of that white
     border around the checkboxes"). Explicit transparent here closes that
     gap regardless of what the generic input rule does in the future. */
  background-color:transparent;
  background-image:url('/checkbox.png'); background-size:contain; background-repeat:no-repeat; background-position:center;
  border:none; border-radius:0; cursor:pointer;
}
.theme-parchment input[type="checkbox"]:checked{
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%236b1f2a' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round' d='M5 13l4 4L19 7'/%3E%3C/svg%3E"),
    url('/checkbox.png');
  background-size:62%, contain;
  background-position:center, center;
  background-repeat:no-repeat, no-repeat;
}

/* Personalization touches (e.g. a player name) get the script face. */
.theme-parchment .script-accent{ font-family:var(--font-script); color:var(--parch-maroon); }

/* .vis-btn.on (Basic/Advanced, public/private, PC/NPC, and other segmented
   mode toggles -- see section 6/19), .toggle button.on, and .pill-toggle.on
   (map-tool visibility pills -- LayersPanel, FogTray, MapToolbar, etc.) all
   default to --calm (teal) as a generic "selected" indicator. That reads
   fine against the flat dark/light palettes it was designed for, but
   clashes with parchment's maroon "selected" language used everywhere else
   (active nav-tab, .reset-btn plaque) -- user feedback: Basic/Advanced
   "should match the red color of the other buttons when selected ...
   this should apply to all buttons". Retint every plain/generic "on" state
   here rather than touching --calm globally, since --calm's teal is still
   the right "on" color outside this theme, and game-color-coded toggles
   (.pc-btn.on, .cond-opt.on, .bf-opt.sel-a/b/c, .ds-pip.succ/fail, the
   #dnd-sub-*.active panel tabs) intentionally keep their distinct colors --
   those carry real meaning (which player, which side, pass/fail, which
   panel) that a single uniform maroon would erase. */
.theme-parchment .vis-btn.on, .theme-parchment .toggle button.on, .theme-parchment .pill-toggle.on{
  background:var(--parch-maroon); color:var(--parch-cream); border-color:var(--parch-maroon);
}

/* Oval/pill and rounded-rect toggle buttons (.pill-toggle, .token-chip,
   .cond-chip, .vis-btn, .toggle button) read as flat and plain next to the
   carved-plaque .reset-btn buttons elsewhere in this theme (user feedback:
   "any button thats an oval needs to have a pattern around it ... thicker
   ornate border" -- then, after the first pass only covered the fully-round
   pills, "those oval buttons are still there all across the website": the
   segmented toggle-group buttons (.vis-btn/.toggle button -- Basic/Advanced,
   public/private, PC/NPC, HP mode, ASI mode, spell-class filters, and more,
   confirmed via grep to always render as <button>) read the same way and
   needed the identical treatment). No plaque art was provided for pill or
   rounded-rect shapes (border-image's 9-slice only cleanly wraps rectangles
   at fixed proportions), so this uses a box-shadow double-ring instead of
   new art: a cream inset line plus a thin maroon outer ring -- unlike
   `outline`, box-shadow reliably follows border-radius on any radius (from
   .vis-btn's 10px rounded rect to a fully round pill) in every browser.
   Scoped to actual <button> elements only: .adm-tag and .ocr-chip are
   non-interactive <span> badges, not buttons, so they're left alone.
   .cond-chip keeps its own purple identity (border-color/color untouched)
   -- this only adds the ring on top, same "don't erase game-color-coding"
   rule as the retint above. */
.theme-parchment .pill-toggle, .theme-parchment .token-chip, .theme-parchment .cond-chip, .theme-parchment .vis-btn, .theme-parchment .toggle button{
  box-shadow:0 0 0 2px var(--parch-cream), 0 0 0 3.5px var(--parch-maroon-line);
}
.theme-parchment .pill-toggle.on, .theme-parchment .token-chip.on, .theme-parchment .vis-btn.on, .theme-parchment .toggle button.on{
  box-shadow:0 0 0 2px var(--parch-cream), 0 0 0 3.5px var(--parch-maroon);
}

/* Dice roller + theme-toggle FABs render via GlobalChrome outside this
   wrapper (App.tsx mounts <GlobalChrome/> as a sibling of the routed .wrap
   content -- see the identical note under .theme-dark-ornate below), so they
   need a :root[data-theme="light"] scope rather than a .theme-parchment
   descendant selector. Hardcoded parchment hex here (not var(--parch-*))
   because those custom properties are declared inside .theme-parchment and
   don't inherit across to GlobalChrome's sibling position in the DOM --
   same constraint that forced the dark theme's FAB rules to hardcode too. */
:root[data-theme="light"] #dice-roller-btn{
  background:radial-gradient(circle at 30% 25%, #f0dfb8, #6b1f2a 65%);
  box-shadow:0 4px 14px rgba(107,31,42,.45), 0 0 0 2px rgba(107,31,42,.2);
}
:root[data-theme="light"] #theme-toggle-btn{
  background:radial-gradient(circle at 30% 25%, #f5ecd8, #ddc9a0 70%);
  border:1px solid rgba(107,31,42,.55);
  box-shadow:0 4px 14px rgba(0,0,0,.25), 0 0 0 2px rgba(107,31,42,.2), 0 0 12px rgba(107,31,42,.18);
}

/* Dice roller panel in light mode -- user feedback: "make the light mode
   dice roller look like a light version of the dark mode one ... same theme
   as the rest of light mode". Mirrors the dark theme's
   :root[data-theme="dark"] #dice-roller-panel treatment below (panel chrome
   + dice-type-btn/preset-btn chips), except Save/Roll/Clear here get the
   real /btn_primary.png plaque art (parchment's actual primary-button
   asset, already used by .theme-parchment .reset-btn) instead of a flat
   color, since that asset exists and .reset-btn is exactly the class these
   three buttons use. Same scoping constraint as the FAB rules above applies
   to --parch-* and --font-display (both declared inside .theme-parchment,
   out of reach here) -- hardcoded to match. --surface-3/--ink-dim/--line
   are NOT hardcoded below: those are genuine :root[data-theme="light"]
   tokens (set on :root itself, not a class), so they already cascade
   correctly to GlobalChrome's dice panel with no extra scoping needed. */
:root[data-theme="light"] #dice-roller-panel{
  background:#efe4c8;
  border:1px solid rgba(107,31,42,.35);
  box-shadow:0 10px 28px rgba(43,24,12,.28), 0 0 0 1px rgba(107,31,42,.12) inset;
  color:#2a1810;
}
:root[data-theme="light"] #dice-roller-panel h2{ color:#6b1f2a; font-family:'Cinzel',serif; }
:root[data-theme="light"] #dice-roller-panel .rsub{ color:#5b4630; }
:root[data-theme="light"] #dice-roller-panel .dice-type-btn{
  background:linear-gradient(160deg, #f5ecd8, #ddc9a0);
  border:1px solid rgba(107,31,42,.35); color:#2a1810;
}
:root[data-theme="light"] #dice-roller-panel .preset-btn{
  background:linear-gradient(160deg, #f5ecd8, #ddc9a0);
  border:1px solid rgba(107,31,42,.3); color:#2a1810;
}
:root[data-theme="light"] #dice-roller-panel .icon-btn{
  background:#e6d8b8; border:1px solid rgba(107,31,42,.3); color:#5b4630;
}
:root[data-theme="light"] #dice-roller-panel .reset-btn{
  color:#f5ecd8; font-family:'Cinzel',serif; font-weight:700; letter-spacing:.03em;
  border-style:solid; border-color:transparent !important; border-width:14px 18px;
  border-image-source:url('/btn_primary.png'); border-image-slice:18 fill; border-image-repeat:stretch;
  background:none !important; box-shadow:none !important; padding:10px 20px;
}
:root[data-theme="light"] #dice-roller-panel .reset-btn:active{ filter:brightness(0.92); }

/* ============================================================================
   13. Ornate dark chrome re-skin
   ----------------------------------------------------------------------------
   Real, user-provided raster chrome drives the shapes below: /dark_btn.png,
   /dark_tab_active.png, /dark_tab_inactive.png, /dark_input.png,
   /dark_title.png (all in public/, baked-in text stripped via inpainting so
   any label can be overlaid, same technique as the parchment set above).
   This is the "Dark Theme" companion to .theme-parchment -- same wrapper
   pattern and the same border-image (9-slice) technique, but WITHOUT a
   token remap section: the base :root dark tokens (section 1) already match
   the sampled colors of these assets, so .theme-dark-ornate only layers
   real chrome on top of the existing dark palette instead of re-pointing
   every token the way .theme-parchment has to.

   Scope: `.theme-dark-ornate` wraps the same five screens as
   .theme-parchment (Landing, DndShell, CampaignSelect, Login, Admin) when
   the theme store (stores/theme.ts) is 'dark' -- see the conditional
   wrapper class in each screen file.
   ============================================================================ */
.theme-dark-ornate{
  position:relative;
}

/* Buttons: real carved-plaque art via border-image (9-slice so the shape
   holds at any label length). Only one plaque source image was provided in
   the Dark Theme folder (an "Add NPC" label repurposed as generic button
   chrome, consistent with earlier approval to relabel plaque art), so
   .reset-btn, .btn-success, and .btn-accent all share /dark_btn.png --
   there's no separate success/accent variant to swap in the way parchment
   has three distinct button PNGs.
   color and !important on background/border-color/box-shadow for the same
   reason as the parchment button rule above: several call sites carry a
   leftover inline style={{background:'var(--chaos)', color:'var(--void)', ...}}
   from the old flat pill-button design, where near-black text made sense on
   a bright solid fill. Unlike .theme-parchment, this theme does NOT remap
   --void to a light color, so that inline color:var(--void) would otherwise
   render near-black text directly on the dark plaque art and vanish --
   color needs the same !important treatment as background here. */
.theme-dark-ornate .reset-btn, .theme-dark-ornate .btn-success, .theme-dark-ornate .btn-accent,
:root[data-theme="dark"] #dice-roller-panel .reset-btn{
  color:var(--ink) !important; font-family:'Cinzel',serif; font-weight:700; letter-spacing:.03em;
  border-style:solid; border-color:transparent !important; border-width:14px 18px;
  border-image-source:url('/dark_btn.png'); border-image-slice:18 fill; border-image-repeat:stretch;
  background:none !important; box-shadow:none !important; padding:10px 20px;
  /* dark_btn.png is a flat oxblood/maroon plaque that read as dull and
     mismatched against the app's purple/gold accent palette (user feedback:
     'still that dark red ... looks ugly'). hue-rotate shifts its hue from
     maroon (~351 deg) toward --chaos purple (~278 deg); saturate/brightness
     keep it rich rather than washed out. Confirmed live in Chrome before
     committing -- this does not meaningfully affect the near-white --ink
     button text since hue-rotate has ~no effect on low-saturation colors.
     Also applied to the dice roller's Save/Roll/Clear buttons below -- that
     panel renders via GlobalChrome, outside any .theme-parchment/
     .theme-dark-ornate wrapper (see App.tsx), so it needs its own
     [data-theme] selector rather than piggybacking on the class scope. */
  filter:hue-rotate(-73deg) saturate(1.4) brightness(1.1);
}
.theme-dark-ornate .reset-btn:not(:disabled):active, .theme-dark-ornate .btn-success:not(:disabled):active,
.theme-dark-ornate .btn-accent:not(:disabled):active,
:root[data-theme="dark"] #dice-roller-panel .reset-btn:not(:disabled):active{
  filter:hue-rotate(-73deg) saturate(1.4) brightness(1.25);
}
/* 2026-08-21: the plaque filter above is unconditional and carries two
   classes, so it out-specifies section 19c's button:disabled -- which is why
   a disabled Founder's-License button on the landing page kept its full
   purple charge and read as pressable. Restating the disabled treatment at
   this scope fixes it. Dropping the hue-rotate along with it is the point:
   the state is the plaque losing its colour, not the plaque in a different
   colour. Verified in Chrome on the dev landing page. */
.theme-dark-ornate .reset-btn:disabled, .theme-dark-ornate .btn-success:disabled,
.theme-dark-ornate .btn-accent:disabled,
.theme-dark-ornate .reset-btn[aria-disabled="true"],
:root[data-theme="dark"] #dice-roller-panel .reset-btn:disabled,
.theme-dark-ornate .game-tab.active:disabled, .theme-dark-ornate .sub-tab.active:disabled{
  filter:grayscale(0.75) opacity(0.68);
  cursor:not-allowed;
}

/* Dice roller panel + the two fixed FABs (dice-roller-btn, theme-toggle-btn)
   render via GlobalChrome outside the per-screen theme wrapper (App.tsx
   mounts <GlobalChrome/> as a sibling of the routed .wrap content, not a
   descendant), so they cannot be reached by .theme-dark-ornate descendant
   selectors and need their own [data-theme="dark"] scope. Chip-sized controls
   (dice-type-btn, preset-btn) get a purple-tinted gradient rather than the
   full border-image plaque -- the plaque's 14-18px border-image-width is
   sized for full .reset-btn buttons and would overwhelm the small dice grid. */
:root[data-theme="dark"] #dice-roller-panel{
  border-color:rgba(164,104,199,0.35);
  box-shadow:0 10px 28px rgba(0,0,0,.5), 0 0 0 1px rgba(164,104,199,0.15) inset;
}
:root[data-theme="dark"] #dice-roller-panel .dice-type-btn{
  background:linear-gradient(160deg, var(--surface-3), var(--surface-2));
  border-color:rgba(164,104,199,0.4); color:var(--ink);
}
:root[data-theme="dark"] #dice-roller-panel .preset-btn{
  background:linear-gradient(160deg, var(--surface-3), var(--surface-2));
  border-color:rgba(164,104,199,0.35);
}
:root[data-theme="dark"] #dice-roller-btn{
  background:radial-gradient(circle at 30% 25%, #c48fe0, var(--chaos) 65%);
  box-shadow:0 4px 14px rgba(164,104,199,.5), 0 0 0 2px rgba(164,104,199,.25);
}
:root[data-theme="dark"] #theme-toggle-btn{
  background:radial-gradient(circle at 30% 25%, #3a2d4e, var(--surface-3) 70%);
  border:1px solid rgba(164,104,199,.55);
  box-shadow:0 4px 14px rgba(0,0,0,.4), 0 0 0 2px rgba(164,104,199,.2), 0 0 12px rgba(164,104,199,.25);
}

/* Nav-tab plaques: real inactive/active plaque art via border-image, same
   9-slice approach as parchment's tabs. */
.theme-dark-ornate .game-tab, .theme-dark-ornate .sub-tab{
  color:var(--ink-dim); font-family:'Cinzel',serif;
  border-style:solid; border-color:transparent; border-width:12px 16px;
  border-image-source:url('/dark_tab_inactive.png'); border-image-slice:16 fill; border-image-repeat:stretch;
  background:none; box-shadow:none;
}
/* /dark_tab_active.png is the same flat oxblood/maroon plaque as
   /dark_btn.png (follow-up user feedback: "make those buttons purple not
   red" -- the Creatures/Battle Manager/Maps/etc. nav-tab bar reads red when
   active and was missed by the original parity pass, which only covered
   .vis-btn/.pill-toggle/.token-chip/.cond-chip). Same hue-rotate treatment
   as .reset-btn above shifts it from maroon toward --chaos purple. The
   inactive plaque (/dark_tab_inactive.png) is already a neutral dark tone,
   so it's left unfiltered. */
.theme-dark-ornate .game-tab.active, .theme-dark-ornate .sub-tab.active{
  color:var(--ink);
  border-image-source:url('/dark_tab_active.png');
  filter:hue-rotate(-73deg) saturate(1.4) brightness(1.1);
}

/* Inputs: real dark pill-field chrome via border-image. Selects are left
   out of this rule and keep the flat default styling from section 19 --
   no dropdown-arrow variant of this asset was provided for dark (unlike
   parchment's dedicated /input_field.png with a baked-in arrow), so
   forcing the plain pill chrome onto <select> would hide its native
   dropdown indicator. */
/* input[type="file"] excluded, mirroring the identical fix already made in
   .theme-parchment: the border-image pill chrome would otherwise wrap the
   WHOLE native file control, doubling up with input[type="file"]::file-
   selector-button's own chrome (section 19) as a redundant outer box.
   select is now included here (reusing /dark_input.png) for parity with
   parchment, which gives select its own dedicated /input_field.png -- no
   distinct dropdown-arrow asset exists for dark, so appearance is left
   alone (not reset to none) and the native OS arrow renders on top of the
   border-image chrome. */
/* 2026-08-15 bug report ("the size on dark mode is hard to see the text
   boxes" / "you cant see initiative"): border-width here is not decoration,
   it is LAYOUT -- a 16px left+right inset removes 32px from every input's
   content box before a single character is drawn. That is invisible on a
   full-width field and fatal on a narrow one:

     .init-roll-input  46px - 32 = 14px content, "20" needs 21px  -> clipped
     .init-hp-input    44px - 32 = 12px content
     .init-ac-input    42px - 32 = 10px content, placeholder "AC" -> "A"

   which is exactly what the initiative row looked like: three black squares
   with fragments of characters in them. The initiative value was being
   rendered the whole time; there was nowhere for it to go.

   This is the same defect the 2026-08-13 note below already fixed for
   <select> (LayerPicker rendering "Default" as "Defa") -- that fix gave
   select a ~half-size inset but left input/textarea, where the identical
   problem was waiting on any field narrower than ~120px. Inputs now take the
   same 6px/8px inset, which is the general fix: it scales with no per-field
   enumeration, so the next narrow input added anywhere cannot reintroduce
   this. The plaque art still renders (border-image just draws into a
   slimmer frame); it reads a little less heavy on wide fields, which is a
   fair trade for fields whose contents fit.

   Note the interaction with --field-border: these rules set
   `border-color:transparent` + `background:none`, so the contrast bump made
   to --field-border on the same day does NOT reach ornate-themed screens --
   their field definition comes from this art. The two fixes cover different
   halves of the app (the map editor is deliberately outside the
   .theme-dark-ornate wrapper, see the note at the top of this section). */
.theme-dark-ornate input:not([type="file"]), .theme-dark-ornate textarea{
  color:var(--ink); font-family:'Inter',sans-serif;
  border-style:solid; border-color:transparent; border-width:6px 8px;
  border-image-source:url('/dark_input.png'); border-image-slice:16 fill; border-image-repeat:stretch;
  background:none;
}
/* 2026-08-13 bug report ("text boxes cut off in dark mode"): <select> used to
   share the exact same 12px/16px border-image inset as input/textarea above,
   PLUS the native OS dropdown-arrow -- appearance is intentionally left at
   its default on <select> (see the unchanged comment above this block) since
   no dark dropdown-arrow asset exists, so the browser draws its own arrow on
   top of the border-image chrome. That's two things competing for room in
   the same box. Fine on a wide select (the Custom Shape dialog's Shape
   picker, ~290px), but ate almost the entire box on a narrow one --
   LayerPicker's 92-160px .layer-picker-select rendered "Default" as "Defa".
   Selects get their own, much smaller inset here (roughly half the
   input/textarea chrome) so the native arrow has room without swallowing
   the label -- still the same decorative border-image, just not sized for
   a full-width text field. */
.theme-dark-ornate select{
  color:var(--ink); font-family:'Inter',sans-serif;
  border-style:solid; border-color:transparent; border-width:6px 8px;
  border-image-source:url('/dark_input.png'); border-image-slice:16 fill; border-image-repeat:stretch;
  background:none;
}
.theme-dark-ornate input::placeholder, .theme-dark-ornate textarea::placeholder{ color:var(--ink-faint); }
.theme-dark-ornate .name-input:focus-visible, .theme-dark-ornate .wide-input:focus-visible{ border-color:var(--chaos); }

/* Title art: DndShell's "D&D Tools" h1 is the only screen where the baked
   title text in /dark_title.png is an exact match to the copy actually
   rendered, so this class is applied only there (see DndShell.tsx). This
   base rule is now overridden for .theme-dark-ornate by the "Dark-theme
   parity pass" section below -- follow-up user feedback ("make the dark
   title like the light title") asked for the real art to be flattened to
   plain text after all, superseding the original "keep the art" call. */
.dnd-title-art{
  width:377px; max-width:100%; height:56px;
  background:url('/dark_title.png') center/contain no-repeat;
  text-indent:-9999px; overflow:hidden;
}

/* -- Dark-theme parity pass -----------------------------------------------
   User feedback: "make dark mode mirror light mode exactly ... except make
   the accessory color that purple instead of red". Ports every treatment
   added to .theme-parchment this session and last (card double-line frame +
   corner flourish, checkbox swatch+checkmark, retinted toggle-button "on"
   state, oval/pill-button double-ring border, solid-color h1, link/eyebrow
   accent) into .theme-dark-ornate, using --chaos (the app's existing purple
   accent, already sampled into dark_btn.png via the hue-rotate above)
   everywhere parchment used --parch-maroon. Parchment's ring/frame colors
   (--parch-cream, --parch-maroon-line) have no dark equivalent token, so
   the double-ring/frame rules below reuse var(--void) (dark's page
   background) for the inner separating line -- the exact same relationship
   parchment has, since --void:var(--parch-cream) inside .theme-parchment's
   own token remap; using var(--void) here mirrors that relationship rather
   than inventing a new color. One thing stays intentionally un-mirrored:
   the full-viewport ornate cracked-border frame (.parchment-frame) has no
   dark equivalent asset (only /dark_btn.png, /dark_tab_*.png,
   /dark_input.png, /dark_title.png were provided -- no border-frame image),
   so recreating it in flat CSS would be a much larger, unapproved creative
   undertaking than a color swap. (.dnd-title-art WAS initially left as real
   art for the same "don't downgrade real art" reasoning, but follow-up
   feedback asked for it to match light mode's flattened title exactly --
   see the override a few rules below.) */

/* Cards: same double-line frame (border + offset outline) + corner
   flourish as parchment. background/box-shadow are left alone -- the base
   .card rule (section 3) already resolves correctly against dark's
   un-remapped tokens. margin-top is NOT left alone (unlike the original
   parity pass assumed): parchment explicitly sets margin-top:20px to clear
   its own outline-offset/corner-flourish, and dark needs the identical
   clearance for the identical reason -- see below (follow-up user feedback:
   "the spaccing between all the cards is different... make sure the card
   spacing is consistent"). */
.theme-dark-ornate .card{
  border:1px solid rgba(164,104,199,0.55);
  outline:1px solid rgba(164,104,199,0.4); outline-offset:5px;
  position:relative; margin-top:20px;
}
.theme-dark-ornate .card::before, .theme-dark-ornate .card::after{
  content:'\2767'; position:absolute; color:var(--chaos); font-size:13px; opacity:.75; line-height:1;
}
.theme-dark-ornate .card::before{ top:-9px; left:-9px; }
.theme-dark-ornate .card::after{ bottom:-9px; right:-9px; transform:rotate(180deg); }
.theme-dark-ornate .card h2{ color:var(--chaos); }

/* 2026-08-16 user report ("fix the spacing of the buttons ... make sure they
   arent too close to the lines"). Both ornate themes draw TWO lines around
   every card -- the 1px border plus a second 1px outline at
   outline-offset:5px -- and their buttons are border-image plaques whose art
   runs all the way to the button's own box edge, with no transparent margin
   the way a flat CSS button's box-shadow ring has. So a button sitting on
   .card's padding was ~15px off the inner line and only ~10px off the outer
   one, which reads as touching it, worst on the cards whose last child IS a
   button (Create campaign / Join / Upload PDF / Scan character sheet). The
   base .card padding went 14px -> 18px in section 3 for every theme; this
   adds the remaining inset only where the second line and the corner
   flourishes (::before/::after at -9px) actually exist, rather than taxing
   the flat default theme with padding it has no reason to carry. */
.theme-parchment .card, .theme-dark-ornate .card{ padding:22px 20px; }

/* h1: parchment flattens every h1 to a solid accent color; dark mode
   previously left every h1 except DndShell's on the default rainbow
   gradient (a deliberate earlier choice, not an oversight -- see the
   .dnd-title-art comment above). Full parity flattens those too, INCLUDING
   .dnd-title-art per follow-up feedback ("make the dark title like the
   light title") -- the rule right below resets its image-era box model
   (fixed width/height, background image, hidden text) so plain text
   renders instead, same as parchment's h1. */
.theme-dark-ornate h1{
  color:var(--chaos); background:none; -webkit-text-fill-color:unset; filter:none; letter-spacing:.02em;
}
.theme-dark-ornate .dnd-title-art{
  width:auto; height:auto; text-indent:0; overflow:visible;
}
.theme-dark-ornate .eyebrow{ color:var(--chaos); }
.theme-dark-ornate a{ color:var(--chaos); }

/* Checkboxes: no raster checkbox asset exists for the Dark Theme folder
   (unlike parchment's /checkbox.png), so this is a CSS-only swatch+checkmark
   in the same spirit as the oval-button box-shadow ring below -- built from
   existing tokens rather than new art. Unchecked: an outlined dark swatch.
   Checked: filled solid --chaos with a white checkmark (mirrors how
   .cond-opt.on and .ds-pip.*.on already fill solid on this palette). */
.theme-dark-ornate input[type="checkbox"]{
  appearance:none; -webkit-appearance:none; width:22px; height:22px; min-width:22px;
  margin:0; padding:0; vertical-align:middle; flex-shrink:0;
  background:var(--surface-2); border:2px solid rgba(164,104,199,0.55); border-radius:4px; cursor:pointer;
}
.theme-dark-ornate input[type="checkbox"]:checked{
  background-color:var(--chaos); border-color:var(--chaos);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23EDE9F2' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round' d='M5 13l4 4L19 7'/%3E%3C/svg%3E");
  background-size:62%; background-position:center; background-repeat:no-repeat;
}

/* .vis-btn.on / .toggle button.on / .pill-toggle.on: same retint parchment
   does (teal --calm -> the theme's own "selected" accent), purple instead
   of maroon. var(--ink) (near-white in dark's base palette) gives the same
   readable-text-on-solid-accent result parchment gets from --parch-cream. */
.theme-dark-ornate .vis-btn.on, .theme-dark-ornate .toggle button.on, .theme-dark-ornate .pill-toggle.on{
  background:var(--chaos); color:var(--ink); border-color:var(--chaos);
}

/* Oval/pill and rounded-rect toggle buttons: same double-ring treatment as
   parchment (.pill-toggle, .token-chip, .cond-chip, .vis-btn, .toggle
   button), same <button>-only scoping and same "keep .cond-chip's own
   identity" rule -- .cond-chip already uses --chaos for its own
   border/color by default, so the ring here is purely additive on top. */
.theme-dark-ornate .pill-toggle, .theme-dark-ornate .token-chip, .theme-dark-ornate .cond-chip, .theme-dark-ornate .vis-btn, .theme-dark-ornate .toggle button{
  box-shadow:0 0 0 2px var(--void), 0 0 0 3.5px rgba(164,104,199,0.55);
}
.theme-dark-ornate .pill-toggle.on, .theme-dark-ornate .token-chip.on, .theme-dark-ornate .vis-btn.on, .theme-dark-ornate .toggle button.on{
  box-shadow:0 0 0 2px var(--void), 0 0 0 3.5px var(--chaos);
}


/* ============================================================================
   20. Design tokens (scale) + motion accessibility + dark-mode ambient warmth
       + battle-tracker felt + dice-settle micro-interaction
   ----------------------------------------------------------------------------
   NERDLAB.md §8 specifies a spacing/radius/type/shadow/z-index/motion scale
   that was never actually added as reusable tokens -- only color was
   tokenized. These are additive CSS custom properties; nothing consumes them
   automatically, so adding them here changes nothing on its own. They're
   consumed piecemeal below and by the Map editor chrome pass (map-desktop.css)
   rather than a blanket retrofit of the whole file, to keep this a low-risk,
   reviewable slice instead of a mass find-and-replace across ~1100 lines of
   working CSS.
   ============================================================================ */
:root{
  --space-1:4px; --space-2:8px; --space-3:12px; --space-4:16px; --space-5:24px; --space-6:32px; --space-7:40px; --space-8:56px;
  --radius-sm:8px; --radius-md:12px; --radius-lg:16px; --radius-pill:999px;
  --text-xs:11px; --text-sm:13px; --text-base:15px; --text-lg:18px; --text-xl:22px; --text-2xl:28px; --text-3xl:34px;
  /* Reuses this file's own existing shadow values (the .card / dice-roller-panel /
     spell-browser-panel rules already establish these three depths) rather than
     inventing new ones, so tokenizing them changes zero rendered pixels. */
  --shadow-1:0 8px 24px -12px rgba(0,0,0,0.5);
  --shadow-2:0 10px 28px rgba(0,0,0,.5);
  --shadow-3:0 12px 40px rgba(0,0,0,.6);
  --z-base:1; --z-overlay:60; --z-modal:1000; --z-toast:2000;
  --motion-fast:.12s; --motion-base:.2s;
}

/* Global motion-safety net -- NERDLAB.md §8 requires every motion token stay
   "behind prefers-reduced-motion". A blanket reset here covers every existing
   transition/animation in this file (the button:active press, hover states,
   etc.) plus anything new (dice-settle below) in one place, rather than
   auditing each rule individually. */
@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration:0.001ms !important; animation-iteration-count:1 !important;
    transition-duration:0.001ms !important; scroll-behavior:auto !important;
  }
}

/* -- Dark-mode ambient warmth --------------------------------------------
   .theme-parchment already gets a full-viewport ornate frame (.parchment-frame,
   real /border_frame.png art). .theme-dark-ornate's own header comment
   explains why it never got an equivalent: no dark border-frame asset was
   provided, and recreating one in flat CSS was called out as "a much larger,
   unapproved creative undertaking than a color swap". This is deliberately
   NOT that -- no new asset, no attempt at a literal border-image match. It's
   a soft ambient vignette built entirely from tokens already sampled into
   this theme (--chaos / --mind, the same purple/blue the nav-tab and button
   hue-rotate already lean on) plus an inset dark falloff, so dark mode gets
   some of the same "this room has an edge, not just infinite void" warmth
   parchment has, without inventing new chrome. Mounted only on
   `.theme-dark-ornate` screens (DndShell/CampaignSelect/Login/Admin/Landing),
   same scope as .parchment-frame, via a sibling `aria-hidden` div -- see
   DndShell.tsx. */
.dark-ornate-frame{
  position:fixed; inset:0; z-index:-1; pointer-events:none;
  background:
    radial-gradient(ellipse 60% 40% at 50% 0%, rgba(164,104,199,0.12), transparent 70%),
    radial-gradient(circle at 50% 100%, rgba(62,143,208,0.07), transparent 60%),
    var(--void);
  box-shadow: inset 0 0 0 clamp(10px,1.4vw,22px) rgba(0,0,0,0.32), inset 0 0 140px 46px rgba(0,0,0,0.5);
}

/* -- Battle tracker: felt table-top ---------------------------------------
   A table metaphor for the one screen that's most literally "the table" --
   the initiative tracker. A faint repeating dot weave, blended multiply over
   the existing --surface fill so it reads as a woven felt nap rather than a
   printed pattern, and stays subtle enough not to fight the row content
   sitting on top of it. Scoped to the Battle Manager panel only
   (#dnd-panel-init, set in DndShell.tsx's route wrapper) -- not a global
   .card change. */
#dnd-panel-init .card{
  background-image:radial-gradient(rgba(0,0,0,0.15) 1px, transparent 1.6px);
  background-size:5px 5px;
  background-blend-mode:multiply;
}

/* -- Storage meter (Account screen) ---------------------------------------
   One magnitude against a known maximum, so the right form is a meter plus the
   number in words — not a chart. Colour here is a STATUS encoding and uses the
   app's already-reserved status tokens (--body good / --order warning / --fury
   critical), each of which is separately tuned for light mode in section 1, so
   nothing new needs validating against either surface.

   Colour is never the only signal: the sentence rendered above the bar says
   the same thing in words ("Running low — 38 MB left"), and the bar carries
   role="progressbar" with an aria-valuetext. That is what keeps it readable
   under colour blindness, forced-colors, and a greyscale screenshot. */
.storage-meter{
  height:10px; border-radius:999px; background:var(--surface-3);
  border:1px solid var(--line); overflow:hidden; margin:10px 0 6px;
}
.storage-meter-fill{
  display:block; height:100%; border-radius:999px; background:var(--body);
  transition:width var(--motion-base) ease;
}
.storage-meter.warning .storage-meter-fill{ background:var(--order); }
.storage-meter.critical .storage-meter-fill{ background:var(--fury); }
.storage-note.warning{ color:var(--order); }
.storage-note.critical{ color:var(--fury); }
.storage-figures{ display:flex; justify-content:space-between; align-items:baseline; gap:10px; flex-wrap:wrap; }
.storage-figures .storage-used{ font-family:'JetBrains Mono',monospace; font-size:14px; color:var(--ink); }
.storage-figures .storage-pct{ font-family:'JetBrains Mono',monospace; font-size:12px; color:var(--ink-dim); }

/* -- Dice roller: a settle, not a snap ------------------------------------
   The result block used to just appear. `key`-ing the result div per roll
   (DiceRoller.tsx) makes React remount it so this replays every time,
   including re-rolling the same preset back to back. Overshoots slightly
   past 1 then settles -- reads as the dice actually coming to rest instead
   of a UI element popping in. Respects the reduced-motion reset above. */
@keyframes dice-settle{
  0%{ transform:scale(0.7) rotate(-6deg); opacity:0; }
  55%{ transform:scale(1.08) rotate(2deg); opacity:1; }
  100%{ transform:scale(1) rotate(0deg); opacity:1; }
}
.dice-settle{ animation:dice-settle var(--motion-base) cubic-bezier(0.34, 1.56, 0.64, 1) both; }

/* -- Landing showcase: one carousel (screenshots + video previews) and the
   social row underneath ------------------------------------------------------
   Added 2026-08-16; reworked 2026-08-18. Content lives in PocketBase and is
   edited in Admin -> Landing; this is layout only.

   The 16:9 frame is fixed by aspect-ratio so slides of different dimensions
   cannot make the page jump as you flip through them, and so the space is
   reserved before the first image decodes (no layout shift). */
.showcase-carousel{
  margin-top:14px;
  margin-left:auto; margin-right:auto;
  /* TWO caps, not one, and both are load-bearing.
       1240px  -- the width. Up from 880px: Evan asked for "much wider and a
                  bit bigger", and 880 was chosen back when the frame cropped
                  its images, so UI detail in a screenshot was unreadable at
                  the size it left. Still short of the 1450px content column
                  so the carousel reads as a framed object on the page rather
                  than as the page.
       132vh   -- the height, expressed as width. A 16:9 frame 1240px wide is
                  698px tall, which on a laptop is the whole viewport: the
                  carousel would swallow the page and push pricing far below
                  the fold, the exact failure the old 880px cap existed to
                  avoid. 132vh caps the frame at ~74vh however wide the window
                  is, so the section is big on a desktop monitor and still
                  leaves the rest of the page visible on a short screen. */
  max-width:min(1240px, 132vh);
}
.showcase-carousel:focus-visible{ outline:2px solid var(--chaos); outline-offset:6px; border-radius:20px; }
.showcase-frame{
  position:relative; aspect-ratio:16/9; width:100%; overflow:hidden;
  border-radius:18px; border:1px solid var(--line); background:var(--surface-2);
  box-shadow:0 18px 50px -20px rgba(0,0,0,0.7);
}
.showcase-slide{ position:absolute; inset:0; transition:opacity .28s ease; }
/* FIT, NEVER CROP. This was object-fit:cover, which filled the frame by
   cutting the edges off anything that was not exactly 16:9 -- the reported
   bug ("the carousel should actually fit the images not cut them off").
   contain letterboxes instead, and .showcase-blur fills the bars. */
.showcase-img{
  position:absolute; inset:0; z-index:1;
  width:100%; height:100%; object-fit:contain;
}
/* A blown-up, blurred copy of the same image behind it, so a portrait or
   ultrawide screenshot sits in a soft wash of its own colours rather than in
   two flat slabs. Costs nothing extra to fetch -- same URL, already decoded.
   Oversized and clipped by the frame's overflow so the blur has no visible
   edge feathering at the borders. */
.showcase-blur{
  position:absolute; inset:-8%; z-index:0; pointer-events:none;
  width:116%; height:116%; object-fit:cover;
  filter:blur(28px) saturate(1.15) brightness(.5);
}
.showcase-videolink{
  position:absolute; inset:0; z-index:2; display:block; text-decoration:none;
  color:var(--ink);
  background:linear-gradient(to top, rgba(0,0,0,0.5), transparent 42%);
}
.showcase-videolink:focus-visible{ outline:2px solid var(--chaos); outline-offset:-4px; border-radius:18px; }
.showcase-play{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:clamp(54px, 7vw, 80px); height:clamp(54px, 7vw, 80px); border-radius:50%;
  display:flex; align-items:center; justify-content:center; padding-left:5px;
  font-size:clamp(20px, 2.6vw, 30px); line-height:1; color:#fff;
  background:rgba(0,0,0,0.55); border:2px solid rgba(255,255,255,0.85);
  box-shadow:0 10px 34px -10px rgba(0,0,0,0.9);
  transition:transform .15s ease, background .15s ease;
}
.showcase-videolink:hover .showcase-play,
.showcase-videolink:focus-visible .showcase-play{
  transform:translate(-50%,-50%) scale(1.08); background:rgba(200,0,0,0.85);
}
.showcase-badge{
  position:absolute; right:12px; bottom:10px;
  font-family:'JetBrains Mono',monospace; font-size:10.5px; letter-spacing:.04em;
  color:#fff; opacity:.85; text-shadow:0 1px 6px rgba(0,0,0,0.9);
}
/* Shown when a thumbnail fails to load -- see Showcase.tsx's badThumbs note.
   A titled panel that still links through, rather than a broken-image glyph
   the size of the carousel. */
.showcase-fallback{
  position:absolute; inset:0; display:flex; flex-direction:column; gap:8px;
  align-items:center; text-align:center;
  background:linear-gradient(160deg, var(--surface-2), var(--surface-3));
  /* Lower third, NOT centred: the play button is dead-centre and centring the
     title here stacked the two on top of each other. Sitting the text under
     the button also puts it inside the videolink's bottom scrim, which is
     what makes it readable over a real thumbnail. */
  justify-content:flex-end; padding:28px 28px 13%;
}
.showcase-fallback-title{
  font-family:'Cinzel',serif; font-weight:700; font-size:clamp(15px, 2.2vw, 22px);
  color:var(--ink);
}
.showcase-nav{
  position:absolute; top:50%; transform:translateY(-50%); z-index:3;
  width:44px; height:44px; border-radius:50%; cursor:pointer;
  border:1px solid var(--line); background:rgba(0,0,0,0.55); color:#fff;
  font-size:22px; line-height:1; display:flex; align-items:center; justify-content:center;
}
.showcase-nav:hover{ background:rgba(0,0,0,0.78); }
.showcase-nav.prev{ left:12px; }
.showcase-nav.next{ right:12px; }
.showcase-caption{
  text-align:center; margin-top:10px; min-height:18px;
  font-size:12.5px; color:var(--ink-dim);
}
.showcase-caption-title{ font-family:'Cinzel',serif; font-weight:700; font-size:13.5px; color:var(--ink); }
.showcase-dots{ display:flex; gap:8px; justify-content:center; margin-top:10px; flex-wrap:wrap; }
.showcase-dot{
  width:10px; height:10px; padding:0; border-radius:50%; cursor:pointer;
  border:1px solid var(--line); background:var(--surface-3);
}
/* Video slides get a squared dot, so the dot row tells you at a glance that
   there is a video in there and roughly where -- the one piece of information
   a row of identical circles cannot carry. */
.showcase-dot.vid{ border-radius:3px; }
.showcase-dot.on{ background:var(--chaos); border-color:var(--chaos); }

/* -- social row ------------------------------------------------------------
   Named pills, no brand logos -- lib/showcase.ts's SOCIALS table explains why.
   The brand colour arrives as an inline --tint on each pill. */
.showcase-social{ margin-top:24px; text-align:center; }
.social-row{ display:flex; flex-wrap:wrap; gap:10px; justify-content:center; margin-top:10px; }
.social-pill{
  display:inline-flex; align-items:center; gap:7px;
  padding:9px 18px; border-radius:999px; text-decoration:none;
  font-family:'Cinzel',serif; font-weight:700; font-size:12.5px; letter-spacing:.03em;
  color:var(--ink); background:var(--surface);
  border:1px solid color-mix(in srgb, var(--tint) 45%, var(--line));
  transition:background .15s ease, border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
.social-pill:hover{
  background:color-mix(in srgb, var(--tint) 16%, var(--surface-2));
  border-color:var(--tint);
  box-shadow:0 8px 24px -10px var(--tint);
  transform:translateY(-1px);
}
.social-pill:focus-visible{ outline:2px solid var(--tint); outline-offset:3px; }
.social-pill-arrow{ font-size:10px; opacity:.6; }

@media (max-width:560px){
  .showcase-nav{ width:36px; height:36px; font-size:18px; }
  .showcase-nav.prev{ left:8px; }
  .showcase-nav.next{ right:8px; }
  .social-pill{ padding:8px 14px; font-size:11.5px; }
}

/* Respect a reduced-motion preference: the crossfade and the pill/play-button
   lifts are the only animations here, and someone who has asked for less
   motion should get a hard cut and a still hover state. */
@media (prefers-reduced-motion: reduce){
  .showcase-slide, .social-pill, .showcase-play{ transition:none; }
  .social-pill:hover{ transform:none; }
  .showcase-videolink:hover .showcase-play,
  .showcase-videolink:focus-visible .showcase-play{ transform:translate(-50%,-50%); }
}

/* ============================================================================
   Admin → Landing (2026-08-18)
   ----------------------------------------------------------------------------
   AdminLanding reuses .fld, .small-btn and .vis-btn, all of which are only
   styled for the narrow character-sheet / battle panels: .fld has no layout of
   its own outside .char-form-grid, so its label, input and hint ran together on
   one line (a hint colliding with the next field's label), and .small-btn /
   .vis-btn are flex:1, so in a full-width admin card each button stretched
   across a third of the screen. These rules are scoped to .landing-admin so the
   originals are untouched.
   ========================================================================== */
.landing-admin .fld{ display:flex; flex-direction:column; gap:4px; }
.landing-admin .fld > span:first-child{
  font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.05em;
  color:var(--ink-dim);
}
.landing-admin .fld > .fld-hint{
  font-size:11px; font-weight:400; text-transform:none; letter-spacing:0;
  color:var(--ink-faint); line-height:1.45;
}
.landing-admin .fld > input{ width:100%; }
.landing-admin .field-max{ max-width:620px; }
.landing-admin .file-input{ color:var(--ink-dim); font-size:12px; }

.landing-admin .add-block{ margin-top:16px; border-top:1px solid var(--line); padding-top:14px; }
.landing-admin .add-block > h3{
  margin:0 0 10px; font-size:12px; text-transform:uppercase; letter-spacing:.05em;
  color:var(--ink-dim);
}

.landing-admin .toggle-row{ justify-content:flex-start; }
.landing-admin .toggle-row .small-btn,
.landing-admin .toggle-row .vis-btn{
  flex:0 0 auto; min-height:34px; padding:0 12px;
  display:inline-flex; align-items:center; justify-content:center;
  text-decoration:none; font-size:12px;
}
.landing-admin .toggle-row a.small-btn{ color:var(--ink); }
