:root{
  /* surfaces — elevation by lightness, dark→light */
  --bg:#0d1117; --surface:#161b22; --raised:#1c2330; --overlay:#232b3a;
  --scrim:rgba(5,8,12,.82);
  /* borders */
  --border:#2a3342; --border-strong:#3a465c;
  /* text — three tiers + non-text glyph tier */
  --text:#e6edf3; --text-2:#c3ccd8; --muted:#8b98a9; --faint:#5f6d80;
  --ink:#0d1117; /* text on solid accent / colored chips */
  /* accent + semantic roles (desaturated ~20%; all ≥4.5:1 AA on every surface
     in the ladder — see DESIGN.md §1 for the computed matrix. --faint is the
     one exception and is non-essential text only.) */
  --accent:#58b6ea; --accent-hover:#6fc1ee;
  --success:#7fd49e; --gold:#d9b25c; --danger:#ef8fb8;
  --male:#6aa8f0; --female:#e0708f;
  /* tint recipe: each semantic color gets exactly two alphas */
  --accent-tint:rgba(88,182,234,.08); --accent-line:rgba(88,182,234,.35);
  --success-tint:rgba(127,212,158,.08); --success-line:rgba(127,212,158,.35);
  --gold-tint:rgba(217,178,92,.08); --gold-line:rgba(217,178,92,.35);
  --danger-tint:rgba(239,143,184,.08); --danger-line:rgba(239,143,184,.35);
  /* element colors — data-viz identity, used as chip bg with --ink text */
  --neutral:#9aa5b1; --fire:#ff8a5c; --water:#5cb3ff; --electric:#ffd75c;
  --grass:#7ed67e; --dark:#b78aff; --dragon:#8f7dff; --ground:#d9a066; --ice:#7fe3e3;
  /* three element hues are borrowed by the rarity and catch-difficulty ramps,
     which are not element typing — so they take the same two alphas every
     other tinted role does, instead of six hand-written rgba()s (§1). */
  --neutral-tint:rgba(154,165,177,.08); --neutral-line:rgba(154,165,177,.35);
  --water-tint:rgba(92,179,255,.08); --water-line:rgba(92,179,255,.35);
  --dark-tint:rgba(183,138,255,.08); --dark-line:rgba(183,138,255,.35);
  /* type — 7 sizes; heading jumps 15→19→24 (≥1.26×) */
  --fs-xs:11px; --fs-sm:12.5px; --fs-md:14px; --fs-base:15px;
  --fs-lg:17px; --fs-h2:19px; --fs-h1:24px;
  /* space — 4px base */
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px; --sp-6:24px; --sp-7:32px;
  /* radius */
  --r-sm:6px; --r-md:10px; --r-lg:14px; --r-xl:18px; --r-full:999px;
  /* motion */
  --t-fast:120ms; --t-base:180ms; --ease-out:cubic-bezier(.2,.7,.3,1);
  /* render native UI (checkboxes, select dropdowns, scrollbars, search-clear
     buttons, autofill) in the browser's dark theme instead of light defaults */
  color-scheme:dark;
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
/* The UA's [hidden] rule is a bare attribute selector, so ANY class that sets
   its own display outranks it and the element stays on screen. That has now
   bitten three separate controls; make it structural instead of remembering
   to write .cls[hidden] every time. */
[hidden]{display:none!important}
::selection{background:rgba(88,182,234,.35);color:var(--text)}
::placeholder{color:var(--muted);opacity:1}
select option{background:var(--surface);color:var(--text)}
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}
}
button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.skip{position:absolute;left:-9999px;top:0;z-index:400;background:var(--accent);color:var(--ink);padding:10px 20px;border-radius:0 0 12px 0;font-weight:700;text-decoration:none}
.skip:focus{left:0}
html{scrollbar-color:var(--border-strong) var(--surface);-webkit-text-size-adjust:100%;text-size-adjust:100%}
/* strip the native inset search styling (iOS) so inputs match the theme;
   Chrome keeps its clear button, which color-scheme renders dark */
input[type="search"]{-webkit-appearance:none;appearance:none}
body{background:var(--bg);color:var(--text);font:var(--fs-base)/1.5 "Segoe UI",system-ui,sans-serif;min-height:100vh;min-height:100dvh}
button{font:inherit;color:inherit;background:none;border:none;cursor:pointer;
  transition:color var(--t-fast),border-color var(--t-fast),background-color var(--t-fast),transform var(--t-fast),box-shadow var(--t-fast)}
button:not(:disabled):active{transform:translateY(1px)}
button:disabled{opacity:.45;cursor:default}
input{font:inherit;color:inherit}
header{position:sticky;top:0;z-index:50;background:rgba(13,17,23,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--border)}
/* The header is sticky at every width and syncHeaderHeight() keeps --hh
   current, so sequential focus navigation clears it instead of parking a
   control underneath (WCAG 2.4.11). Forward Tab never exposes this; only
   reverse traversal does. */
html{scroll-padding-top:calc(var(--hh, 64px) + 8px)}
.hwrap{max-width:1200px;margin:0 auto;padding:12px 20px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
/* The wordmark is artwork, not text — outlined to paths so it needs no webfont
   and looks identical everywhere. Kept inside the h1 so the page still has a
   heading, and it's a real link so ctrl/middle-click opens a new tab (the JS
   handler only intercepts plain clicks). */
.logo{font-size:0;line-height:0}
.logo a{display:inline-block;border-radius:var(--r-sm)}
.logo img{display:block;height:31px;width:auto;transition:opacity var(--t-fast)}
.logo a:hover img{opacity:.82}
@media(max-width:640px){.logo img{height:26px}}
@media(max-width:360px){.logo img{height:23px}}
.tabs{display:flex;gap:4px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);padding:4px;align-items:center}
.tabs button{padding:7px 14px;border-radius:var(--r-sm);color:var(--muted);font-weight:600;font-size:var(--fs-md)}
.tabs button.active{background:var(--raised);color:var(--text);box-shadow:inset 0 0 0 1px var(--border-strong)}
.tabs button:hover:not(.active){color:var(--text)}
.tabs .sep{width:1px;align-self:stretch;background:var(--border);margin:3px 2px;flex-shrink:0}
.ghlink{margin-left:auto;display:flex;align-items:center;gap:7px;color:var(--muted);text-decoration:none;font-size:var(--fs-md);font-weight:600;padding:6px 12px;border:1px solid var(--border);border-radius:var(--r-md);transition:color var(--t-fast),border-color var(--t-fast)}
.ghlink:hover{color:var(--text);border-color:var(--border-strong)}
@media(max-width:720px){
  .ghlink span{display:none}
  .hwrap{flex-wrap:nowrap;gap:10px;padding:10px 12px}
  .logo{flex-shrink:0}
  .ghlink{flex-shrink:0;padding:6px 8px}
  .tabs{overflow-x:auto;flex-wrap:nowrap;max-width:100%;scrollbar-width:none;-webkit-overflow-scrolling:touch;
    -webkit-mask-image:linear-gradient(90deg,var(--eL,#000),#000 26px,#000 calc(100% - 26px),var(--eR,#000));
    mask-image:linear-gradient(90deg,var(--eL,#000),#000 26px,#000 calc(100% - 26px),var(--eR,#000))}
  .tabs.fadeL{--eL:transparent}
  .tabs.fadeR{--eR:transparent}
  .tabs::-webkit-scrollbar{display:none}
  .tabs button{white-space:nowrap;flex-shrink:0;padding:7px 12px}
}
/* first-run setup checklist (replaces the old prose tipbar) */
.setup{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:var(--accent-tint);border:1px solid var(--accent-line);border-radius:var(--r-lg);padding:10px 14px;font-size:var(--fs-md);margin-bottom:20px}
.setup[hidden]{display:none}
.setup .lb{font-weight:700}
.setup .step{display:inline-flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-full);padding:6px 14px;font-size:var(--fs-sm);font-weight:600;color:var(--muted)}
.setup .step:hover{color:var(--text);border-color:var(--border-strong)}
.setup .step.done{color:var(--success);border-color:var(--success-line)}
.setup .dismiss{margin:-5px -6px -5px auto;color:var(--muted);font-size:var(--fs-base);padding:10px 12px;border-radius:var(--r-sm);flex-shrink:0}
.setup .dismiss:hover{color:var(--text);background:var(--surface)}
.viewsub{color:var(--muted);font-size:var(--fs-md);margin-bottom:14px}
main{max-width:1200px;margin:0 auto;padding:28px 20px 80px}
@media(max-width:640px){main{padding:18px 12px 60px}}
.hint{color:var(--muted);text-align:center;padding:40px 0;font-size:var(--fs-base)}
.view{display:none}.view.active{display:block}

/* ---------- bottom tab bar (thumb zone, ≤640px) ---------- */
.bottomnav{display:none}
.moresheet{display:none}
@media(max-width:640px){
  .tabs{display:none}
  /* The bar composites rgba(13,17,23,.94) over whatever is scrolling under it,
     so the hairline's contrast is a range, not a number: --border measured
     1.33-1.49:1 (1.49 over the surface ladder, 1.33 over the Paldex's pal art)
     — a line drawing nothing at the bottom of that. --border-strong is
     1.78-1.99 over the same range. Decorative either way (the bar's own fill is
     what separates it), so this is legibility, not 1.4.11. */
  .bottomnav{position:fixed;left:0;right:0;bottom:0;z-index:90;display:flex;background:rgba(13,17,23,.94);backdrop-filter:blur(8px);border-top:1px solid var(--border-strong);padding-bottom:env(safe-area-inset-bottom)}
  .bottomnav button{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 4px 10px;font-size:var(--fs-xs);font-weight:600;color:var(--muted);border-radius:0}
  .bottomnav button .bi{display:block}
  /* The top tabs mark the current one with a raised fill AND an inset ring; down
     here it was the accent hue alone (1.4.1). A rule along the top edge is the
     shape signal a bar of this height has room for — same accent, drawn as an
     inset shadow so it costs no layout and cannot shift the row. */
  .bottomnav button.active{color:var(--accent);box-shadow:inset 0 2px 0 var(--accent)}
  .moresheet.open{display:flex}
  .moresheet{position:fixed;right:8px;bottom:calc(64px + env(safe-area-inset-bottom));z-index:95;flex-direction:column;min-width:190px;background:var(--overlay);border:1px solid var(--border-strong);border-radius:var(--r-lg);box-shadow:0 12px 40px rgba(0,0,0,.5);overflow:hidden}
  .moresheet button{text-align:left;padding:12px 16px;font-size:var(--fs-md);font-weight:600;color:var(--muted)}
  .moresheet button:hover{color:var(--text);background:var(--raised)}
  .moresheet button.active{color:var(--text);background:var(--raised)}
  main{padding:18px 12px calc(88px + env(safe-area-inset-bottom))}
  /* .toasts and footer also have to clear the nav, but their base rules are
     declared further down the file and would win this tie — those two live in
     the phone block at the end instead */
}

/* ---------- picker ---------- */
.picker{position:relative;flex:1;min-width:230px;max-width:340px}
.picker-btn{width:100%;display:flex;align-items:center;gap:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:10px 14px;text-align:left}
.picker-btn:hover{border-color:var(--border-strong)}
.picker.open .picker-btn{border-color:var(--accent)}
.picker-btn.invalid{border-color:var(--danger)}
.picker-btn .ph{color:var(--muted)}
.picker-btn .sel{display:flex;align-items:center;gap:12px;min-width:0}
.picker-btn .sel .nm{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.picker-btn .sel .zk{color:var(--muted);font-size:var(--fs-sm)}
/* a sibling of the trigger, laid over its right edge — a button cannot be
   nested in a button, and as a <span> it was pointer-only (js/core.js) */
.pclear{position:absolute;right:var(--sp-2);top:50%;transform:translateY(-50%);z-index:2;
  display:inline-flex;align-items:center;justify-content:center;min-width:28px;min-height:28px;
  color:var(--muted);font-size:var(--fs-lg);border-radius:var(--r-sm)}
.pclear:hover{background:var(--raised);color:var(--text)}
.pclear[hidden]{display:none}
/* room for it, so a long name never runs under the ✕ */
.picker-btn:has(+ .pclear:not([hidden])){padding-right:var(--sp-6)}
.picker-btn .caret{margin-left:auto;color:var(--muted);flex-shrink:0}
.pop{position:absolute;top:calc(100% + 6px);left:0;width:min(340px,calc(100vw - 24px));z-index:60;background:var(--overlay);border:1px solid var(--border-strong);border-radius:var(--r-lg);box-shadow:0 12px 40px rgba(0,0,0,.5);overflow:hidden;display:none}
.pop.flip{left:auto;right:0}
/* opens upward when the trigger sits too close to the bottom nav (see fitPopup) */
.pop.up,.ptag .tpop.up{top:auto;bottom:calc(100% + 6px)}
.picker.open .pop{display:block}
.pop input{width:100%;background:var(--raised);border:none;border-bottom:1px solid var(--border);padding:10px 14px;outline:none}
.pop .list{max-height:340px;overflow-y:auto;overscroll-behavior:contain}
.pop .row{display:flex;align-items:center;gap:11px;width:100%;padding:7px 12px;text-align:left}
.pop .row:hover,.pop .row.hl{background:var(--raised)}
.pop .row .nm{font-weight:600;font-size:var(--fs-md)}
.pop .row .zk{color:var(--muted);font-size:var(--fs-sm)}
.pop .row .own{color:var(--gold);font-size:var(--fs-sm)}
.pop .row .types{margin-left:auto;display:flex;gap:4px}
.pop .empty{padding:16px;color:var(--muted);text-align:center;font-size:var(--fs-md)}
.pop .lgroup{padding:8px 12px 3px;font-size:var(--fs-xs);font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--muted)}
@media(max-width:640px){
  .pop .list{display:grid;grid-template-columns:repeat(4,1fr)}
  .pop .lgroup{grid-column:1/-1}
  .pop .row{flex-direction:column;gap:4px;padding:9px 4px;align-items:center}
  .pop .row .pico{width:44px;height:44px}
  .pop .row .zk,.pop .row .types{display:none}
  .pop .row .nm{font-size:var(--fs-xs);white-space:normal;text-align:center;line-height:1.15;word-break:break-word}
}

/* ---------- pal icon / chips ---------- */
.pico{width:var(--ico);height:var(--ico);border-radius:50%;background:var(--raised);flex-shrink:0;object-fit:cover;border:2px solid var(--border-strong)}
.pico.f{display:flex;align-items:center;justify-content:center;font-weight:700;color:var(--muted)}
.pico.click{cursor:pointer;transition:border-color var(--t-fast)}
.pico.click:hover{border-color:var(--border-strong)}
.dot{width:9px;height:9px;border-radius:50%;display:inline-block;flex-shrink:0}
.types{display:inline-flex;align-items:center;gap:3px}
.chip{font-size:var(--fs-xs);font-weight:700;padding:2px 9px 2px 6px;border-radius:var(--r-full);color:var(--ink);text-transform:capitalize;
  display:inline-flex;align-items:center;gap:3px}
/* Game UI icons, extracted from the paks. .uii is the shared base: never let
   one stretch a flex row, and never let a missing file reserve empty space. */
.uii{display:inline-block;flex-shrink:0;vertical-align:-2px}
/* the stroke-icon twin of .uii: inline Lucide art, aligned once for the whole
   app rather than patched per component (DESIGN.md §7) */
.lui{display:inline-block;flex-shrink:0;vertical-align:-2px}
/* The element icon is element-coloured and so is its chip, so it's knocked
   down to the chip's ink colour instead of vanishing into the background. */
.chip .uii{filter:brightness(0);opacity:.82;vertical-align:0}
.works span .uii{vertical-align:-3px;margin-right:5px}
.mchip .uii{vertical-align:0}
.tworks .uii{vertical-align:-3px;margin-right:1px}
.mchip.drop{padding-left:5px}
.mchip{font-size:var(--fs-sm);font-weight:600;padding:2px 10px;border-radius:var(--r-full);background:var(--bg);border:1px solid var(--border-strong);color:var(--muted);
  display:inline-flex;align-items:center;gap:6px}
.tier{font-size:var(--fs-xs);font-weight:700;letter-spacing:.4px;padding:1px 8px;border-radius:var(--r-sm);text-transform:uppercase}
/* the rarity ramp is the game's own ladder (grey -> blue -> purple -> gold), so
   the hues are recognition, not decoration. The uppercase word is the signal;
   the tint is atmosphere at 1.13:1 against the plain surface. */
.tier.common{background:var(--neutral-tint);color:var(--neutral)}
.tier.rare{background:var(--water-tint);color:var(--water)}
.tier.epic{background:var(--dark-tint);color:var(--dark)}
.tier.legendary{background:var(--gold-tint);color:var(--gold)}

/* ---------- breed view ---------- */
/* the status sentence — also the view's polite live region */
.resline{color:var(--muted);font-size:var(--fs-md);line-height:1.4;min-height:1.4em}
.resline b{color:var(--text);font-weight:600}
/* swap sits in the divider between the two pickers, the flight-search convention */
.parents{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  align-items:end;gap:var(--sp-3);max-width:820px;margin-bottom:var(--sp-4)}
.pwrap{min-width:0;max-width:none}
.pwrap .picker{max-width:none;min-width:0}
.swap{width:40px;height:40px;padding:0;display:grid;place-items:center;margin-bottom:var(--sp-1);
  border-radius:var(--r-full);background:var(--raised);border:1px solid var(--border-strong);color:var(--muted)}
.swap:hover{color:var(--text);border-color:var(--accent);background:var(--accent-tint)}
.swap svg{width:18px;height:18px}
/* answer left, reasoning right — the column was 48% used with everything centred */
/* minmax(0,1fr) on the base track, not an implicit auto: the chain card's
   min-content is 734px, which floored the single column and ran the whole
   view off a 360px phone. Same trap as .roster.rowview — see DESIGN.md §4. */
.result-zone{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--sp-5) var(--sp-6);align-items:start;margin-top:var(--sp-5)}
@media(min-width:900px){
  .result-zone{grid-template-columns:minmax(0,1.2fr) minmax(280px,1fr)}
  /* peers, so they share a height and their portraits share a baseline */
  .result-zone.two{grid-template-columns:repeat(2,minmax(0,1fr));align-items:stretch}
  /* .child-card centres its body, which puts the two names ~17px apart when one
     card wraps a fourth work chip — the pair is meant to be read side by side,
     so the bodies start at the same edge. The ≤640 rule already does this. */
  .result-zone.two .child-card{align-items:flex-start}
  .result-zone.two>.resrail{grid-column:1/-1}
  .result-zone:not(.two) .resrail .linkrow{flex-direction:column;align-self:stretch}
  .result-zone:not(.two) .resrail .linkrow>.alink{text-align:left}
}
.result-zone>.chaincard{grid-column:1/-1}
.resrail{display:flex;flex-direction:column;align-items:flex-start;gap:var(--sp-2)}
.resrail .sub b{color:var(--text);font-weight:600}
/* every state starts at the same edge — the empty and one-parent link rows are
   direct children of the zone, so scoping this to .resrail left them centred */
.result-zone .linkrow,#reverseResult .linkrow{margin-top:var(--sp-2);justify-content:flex-start}
/* spacing is the tier break — a rule would read as a section boundary. The
   4px indent .gjump carries for inline use in the Guide reads as misalignment
   when it is the last item in a start-aligned column. */
.resrail .gjump{margin-top:var(--sp-4);margin-left:0}
.child-card{position:relative;width:100%;background:linear-gradient(180deg,var(--raised),var(--surface));border:1px solid var(--border-strong);border-radius:var(--r-xl);padding:var(--sp-6) var(--sp-7);display:flex;gap:var(--sp-6);align-items:center;animation:pop var(--t-base) var(--ease-out)}
.child-card.clickable{cursor:pointer;transition:border-color var(--t-fast),transform var(--t-fast)}
.child-card.clickable:hover{border-color:var(--accent-line);transform:translateY(-1px)}
/* the press target lies OVER the card instead of wrapping it, so the card's
   heading and chips stay in the AT tree (same idea as .dextile-open) */
/* pointer-events:none or the overlay hit-tests above every chip in the card,
   killing their tooltips and text selection. The click handler lives on the
   card; keyboard activation of this button bubbles to it. */
.cardopen{position:absolute;inset:0;border:0;background:none;pointer-events:none;border-radius:var(--r-xl)}
.child-card:has(.cardopen:focus-visible){outline:2px solid var(--accent);outline-offset:2px}
.cardopen:focus-visible{outline:none}
/* The global button:active translate lands on a transparent overlay, so nothing
   visible moves — the card takes the press instead (DESIGN.md §11). Both routes
   are needed: the overlay is pointer-events:none, so a mouse press never reaches
   it and :active lands on the card, while a keyboard press lands on the overlay.
   Must stay after the :hover rule — same specificity, source order decides. */
.child-card.clickable:active,
.child-card.clickable:has(.cardopen:active){transform:translateY(1px)}
.cardopen:active{transform:none}
@keyframes pop{from{transform:scale(.97);opacity:0}to{transform:scale(1);opacity:1}}
.child-card .pico{width:84px;height:84px;font-size:30px}
.child-card h3{font-size:var(--fs-h1);line-height:1.2}
.child-card .zk{color:var(--muted);font-weight:400;font-size:var(--fs-base)}
.crow{display:flex;align-items:center;gap:var(--sp-2);margin-top:var(--sp-2);flex-wrap:wrap}
.badge{font-size:var(--fs-xs);font-weight:700;letter-spacing:.6px;padding:3px 10px;border-radius:var(--r-sm);text-transform:uppercase}
.badge.unique{background:var(--gold-tint);color:var(--gold);border:1px solid var(--gold-line)}
.badge.same{background:var(--success-tint);color:var(--success);border:1px solid var(--success-line)}
/* Breed doesn't use this one (a badge repeated on both cards distinguishes
   nothing), but the Planner's route steps do — js/planner.js stepEl. */
.badge.gender{background:var(--danger-tint);color:var(--danger);border:1px solid var(--danger-line)}
.gbar{display:flex;align-items:center;gap:8px;margin-top:9px;font-size:var(--fs-sm);color:var(--muted)}
.gtrack{width:130px;height:7px;border-radius:var(--r-full);background:var(--female);overflow:hidden}
.gtrack i{display:block;height:100%;background:var(--male);border-right:2px solid var(--bg)}
.works{display:flex;gap:9px;margin-top:9px;flex-wrap:wrap;font-size:var(--fs-sm);color:var(--muted)}
.works span{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-sm);padding:2px 8px 2px 6px;display:inline-flex;align-items:center}
.works span.hot{border-color:var(--accent);color:var(--text)}
.mathline{color:var(--muted);font-size:var(--fs-sm);text-align:center}
.mathline b{color:var(--text);font-weight:600}
/* the only thing telling two gender-combo cards apart, so it is not a footnote */
.gtag{font-size:var(--fs-md);color:var(--text-2);font-weight:600;margin-top:var(--sp-1)}
.linkrow{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
.alink{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);padding:7px 16px;font-size:var(--fs-md);font-weight:600;color:var(--muted)}
.alink:hover{color:var(--text);border-color:var(--border-strong)}
/* the one solid primary per view */
.alink.primary{background:var(--accent);border-color:var(--accent);color:var(--ink);font-weight:700}
.alink.primary:hover{background:var(--accent-hover);border-color:var(--accent-hover);color:var(--ink)}

/* ---------- reverse view ---------- */
/* one row class for filter/order/present controls (DESIGN.md section 9).
   .controls was a second copy used exactly once; only the label-bottom
   alignment was a real difference, so that is all that survives. */
.revcontrols{align-items:flex-end;margin-bottom:22px}
.ctl label{display:block;font-size:var(--fs-sm);font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--muted);margin-bottom:6px}
.search-inp{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:10px 14px;outline:none;width:210px}
.search-inp:focus{border-color:var(--accent)}
.toggle{display:flex;align-items:center;gap:9px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:10px 14px;cursor:pointer;user-select:none;font-size:var(--fs-md);color:var(--muted)}
.toggle:hover:not(.on){border-color:var(--border-strong);color:var(--text)}
.toggle.on{color:var(--text);border-color:var(--accent)}
.toggle .kn{width:36px;height:20px;border-radius:var(--r-full);background:var(--raised);border:1px solid var(--border-strong);position:relative;transition:background var(--t-fast),border-color var(--t-fast);flex-shrink:0}
.toggle .kn::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--muted);transition:left var(--t-fast),background var(--t-fast)}
.toggle.on .kn{background:var(--accent);border-color:var(--accent)}
.toggle.on .kn::after{left:18px;background:var(--ink)}
.rsummary{display:flex;align-items:baseline;gap:10px;margin-bottom:14px;flex-wrap:wrap}
.rsummary .cnt{font-size:var(--fs-lg);font-weight:700}
.rsummary .sub{color:var(--muted);font-size:var(--fs-md)}
.pairs{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(380px,100%),1fr));gap:10px}
@media(max-width:560px){.pairs{grid-template-columns:1fr}}
.pair{display:flex;align-items:center;gap:10px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:9px 14px;text-align:left}
.pair:hover{border-color:var(--border-strong)}
.pair .pside{display:flex;align-items:center;gap:9px;flex:1;min-width:0}
.pair .pside .nm{font-weight:600;font-size:var(--fs-md);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pair .pside .g{font-size:var(--fs-md)}
.pair .own{font-size:var(--fs-sm);color:var(--gold)}
.pair .x{color:var(--muted)}
.pair .badge{margin-left:auto;flex-shrink:0}

/* ---------- find parents: one parent, then every partner that works with it ----------
   Measured: the same left parent repeated up to 21 rows in a row, each with its
   own 32px portrait, and the one row that carried a badge was the one row whose
   two names both ellipsised. Grouping costs one row instead of 21, and chips
   wrap where a name would have truncated — the call .combo already made. */
/* wrap, don't stack: a two-partner group stretched across 1160px is the same
   dead column the Breed answer had. Groups size to content, so short ones
   share a row and a long one takes the row to itself. */
#reverseResult .pairs{display:flex;flex-wrap:wrap;gap:var(--sp-2);list-style:none;margin:0;padding:0}
/* flex-basis, not min-width: `1 1 auto` sizes each item to its content and THEN
   shares the free space, so the content difference survives into the rendered
   width and the interior seam wandered ~96px down the page. */
/* flex-grow makes the last group on a line absorb the row, so the tiers a
   player acts on - almost always 1-2 groups - drew as near-empty full-width
   bars while "Both parents missing" rendered tidy columns. Density was inverted
   against actionability. */
@media (min-width:908px){#reverseResult .pgroup{max-width:calc((100% - var(--sp-2)) / 2)}}
.pgroup{display:flex;align-items:center;gap:var(--sp-2);flex:1 1 min(430px,100%);
  max-width:100%;background:var(--surface);
  border:1px solid var(--border);border-radius:var(--r-lg);padding:var(--sp-2) var(--sp-4)}
/* Two 430px columns need 908px of viewport, so 641-907 got one stretched card
   with more than half of it empty. A smaller basis buys the second column back;
   don't apply it wider, where it produces a ragged three. */
@media (min-width:721px) and (max-width:907px){.pgroup{flex-basis:min(340px,100%)}}
.pgroup .anchor{display:flex;align-items:center;gap:var(--sp-2);flex:0 0 auto;
  max-width:min(280px,40%);min-width:0;background:none;border:0;padding:var(--sp-1) 0;color:var(--text)}
.pgroup .anchor .pico{--ico:28px}
.pgroup .anchor .nm{font-weight:600;font-size:var(--fs-md);min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pgroup .anchor .own{color:var(--gold);font-size:var(--fs-sm)}
@media (hover:hover){.pgroup .anchor:hover .nm{color:var(--accent)}}
.pgroup .x{color:var(--muted);flex:0 0 auto}
.pgroup .chiprow{flex:1 1 auto}
/* the chip sits ON the card, so it is a step lighter than it */
.pgroup .tchip{background:var(--raised)}
@media (hover:hover){.pgroup .tchip:hover{border-color:var(--accent);color:var(--text)}}
/* Colour is never the only signal: the triangle glyph and the visible .why text
   both carry it. The reason is ALSO in the chip's aria-label, because an
   .sr-only child would be pruned by that label and never announced. */
/* a warn chip carries a sentence, not a name — like .needrow's catch chips it
   wraps inside its own border instead of pushing the row past the viewport
   (385px of nowrap chip was the page's width at 320) */
/* the .warn rule below wins on source order, so the plain :hover never applied:
   on the one tier where a chip most needs to look pressable it read as inert */
@media (hover:hover){.pgroup .tchip.warn:hover{border-color:var(--danger)}}
/* section 4 rules this for .mchip.warn already: a --male blue glyph on a pink
   chip stops reading as a gender colour */
.pgroup .tchip.warn .g{color:inherit}
.pgroup .tchip.warn{color:var(--danger);border-color:var(--danger-line);background:var(--danger-tint);
  white-space:normal;text-align:left}
.pgroup .tchip .why{font-size:var(--fs-xs);font-weight:600;opacity:.9}
.pgroup .tchip .why::before{content:'— ';}
.wglyph{flex:none;width:16px;height:16px}
/* base rule; the roster's .rosband/.rostile variants keep winning on specificity */
.revhow{color:var(--muted);font-size:var(--fs-sm);margin:calc(var(--sp-3) * -1) 0 var(--sp-3)}
.chiprow{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-1);min-width:0}
/* the roster band never wrapped and must not start: wrapping cost it 28px per
   species section at 360, on the view whose whole pass was about density */
.rosband .chiprow{flex-wrap:nowrap}
/* ---------- unique combos ----------
   Two tiers per card: the result is the heading (these rows sort by it), the
   recipe is a muted caption. Nothing truncates — the recipe wraps instead. */
/* a <ul> since 78b6b06, so the board reports its size the way .dexgrid does.
   The <li> is the grid item and the .combo fills it — display:contents on the
   li would be tidier but drops listitem from the a11y tree in some engines. */
.combos{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(330px,100%),1fr));gap:10px;list-style:none;margin:0;padding:0}
.combos>li{display:flex}
.combos>li>.combo{flex:1 1 auto;min-width:0}
.combo{display:flex;align-items:center;gap:13px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:11px 15px;text-align:left}
.combo:hover{border-color:var(--border-strong)}
.combo>.pico{width:40px;height:40px}
.combo .cbody{flex:1;min-width:0}
.combo .cres{display:block;font-weight:600;font-size:var(--fs-md);line-height:1.3}
.combo .crecipe{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:4px;font-size:var(--fs-sm);color:var(--muted)}
.combo .cp{display:inline-flex;align-items:center;gap:5px}
.combo .cp .pico{width:20px;height:20px;border-width:1px}
/* --faint is 3.59:1 and non-essential text only (§1). The × is decorative
   punctuation and stays; "two of the same" is a fact about the recipe. */
.combo .cx{color:var(--faint)}
.combo .cself{font-style:italic;color:var(--muted)}
.more{margin:22px auto 0;display:block;background:var(--surface);border:1px solid var(--border-strong);padding:10px 26px;border-radius:var(--r-md);font-weight:600}
.more:hover{border-color:var(--accent);color:var(--text)}

/* ---------- dex view ---------- */
.dex-controls{display:flex;gap:12px;margin-bottom:18px;flex-wrap:wrap;align-items:center}
/* The table pans inside its own container rather than panning the page — but
   overflow-x makes .tablewrap a scrollport, and a scrollport is what a sticky
   <th> resolves against, so the column header silently stops sticking. JS adds
   .panning only when the table genuinely can't fit, keeping the sticky header
   on every screen wide enough for it. The mask marks the clipped edge. */
.tablewrap.panning{overflow-x:auto;-webkit-overflow-scrolling:touch;
  -webkit-mask-image:linear-gradient(90deg,#000,#000 calc(100% - 24px),transparent);
  mask-image:linear-gradient(90deg,#000,#000 calc(100% - 24px),transparent)}
.tablewrap.panning.atend{-webkit-mask-image:none;mask-image:none}
.dex-controls .search-inp{width:auto;flex:1;min-width:160px;max-width:300px}
/* The roster row labels its controls. stretch + justify-content:flex-end puts
   every control on one bottom line whatever its height, so the uppercase
   labels above them line up instead of staggering by a few px. */
.roscontrols{align-items:stretch}
.roscontrols[hidden]{display:none} /* a display: of its own outranks the UA's [hidden] */
.roscontrols .ctl{display:flex;flex-direction:column;justify-content:flex-end}
/* the search field is the one that should take the slack, not sit at 202px */
.roscontrols>.ctl:first-child{flex:1 1 260px;max-width:340px}
.roscontrols .search-inp{max-width:none;width:100%}
.dex-controls .isel{flex:0 1 auto}
/* §4: a segmented choice's consequence line goes UNDER it. On one flex line it
   sat 13px from #comboCount at the same baseline and colour, so the two read as
   one string — "Every unique combo in the game. 250 combos". Correct already at
   ≤640, where .dexcontrols becomes a grid; this makes it true at every width. */
.dex-controls #comboKindWhy{flex:1 1 100%;margin:0}
@media(max-width:640px){.dex-controls select,.dex-controls .isel{flex:1;min-width:140px}}
select{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);color:var(--text);padding:var(--sp-3) 14px;outline:none;cursor:pointer;
  /* Without the font-size a select inherits the UA's 13.333px — off §3's scale
     entirely — and came out 39px against .search-inp and .isel-btn at 44.5px,
     which is what pushed .roscontrols' bottom-aligned labels out of line.
     Chrome clamps line-height on a select, so the last 5px comes from padding:
     --sp-3 lands it at 45px, within half a pixel of its siblings. */
  font-size:var(--fs-base)}
select:focus{border-color:var(--accent)}

/* Dropdowns that show an icon. A native <option> can't hold an image, so the
   <select> stays as the state (and the thing every listener talks to) while
   this draws over it. Sized to sit flush next to the plain selects it doesn't
   replace. */
.nativehide{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.isel{position:relative;display:inline-flex}
.isel-btn{display:inline-flex;align-items:center;gap:8px;width:100%;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);
  color:var(--text);padding:10px 32px 10px 14px;font-size:var(--fs-base);text-align:left;
  background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
  background-position:calc(100% - 17px) calc(50% + 2px),calc(100% - 12px) calc(50% + 2px);
  background-size:5px 5px,5px 5px;background-repeat:no-repeat}
.isel-btn:hover{border-color:var(--border-strong)}
.isel.open .isel-btn{border-color:var(--accent)}
.isel-pop{position:absolute;top:calc(100% + 4px);left:0;min-width:100%;max-height:320px;overflow-y:auto;
  z-index:60;display:none;flex-direction:column;background:var(--overlay);
  border:1px solid var(--border-strong);border-radius:var(--r-md);
  box-shadow:0 14px 40px rgba(0,0,0,.55);padding:4px}
.isel.open .isel-pop{display:flex}
.isel-row{display:flex;align-items:center;gap:9px;padding:8px 12px;border-radius:var(--r-sm);
  font-size:var(--fs-md);color:var(--text-2);cursor:pointer;white-space:nowrap}
.isel-row:hover{background:var(--raised);color:var(--text)}
.isel-row.on{background:var(--accent-tint);color:var(--text);font-weight:600}
table{width:100%;border-collapse:collapse}
/* thead th, not th: this is the sticky COLUMN-header treatment, and unscoped it
   also landed on any <th scope="row"> in a body — which is why the Paldex table
   had no row headers at all and .ranktbl carries two lines undoing it. */
thead th{position:sticky;top:var(--hh,64px);background:var(--bg);text-align:left;font-size:var(--fs-sm);letter-spacing:.5px;text-transform:uppercase;color:var(--muted);padding:8px 12px;border-bottom:1px solid var(--border-strong);cursor:pointer;user-select:none;white-space:nowrap;z-index:5}
thead th:hover{color:var(--text)}
thead th:not([data-s]){cursor:default}
thead th:not([data-s]):hover{color:var(--muted)}
thead th .arr{color:var(--accent)}
/* the click target is the <th>, but the FOCUS RING is this button — 23.6x18.8
   without a floor. Same recipe .rosrow .thbtn already carries. */
.thbtn{font:inherit;color:inherit;letter-spacing:inherit;text-transform:inherit;cursor:pointer;padding:0;min-height:24px;min-width:24px}
td{padding:5px 12px;border-bottom:1px solid var(--border);font-size:var(--fs-md)}
/* the species name is the row's header, so it is a <th scope="row"> — it gets
   the body-cell treatment, not the sticky column-header one */
#dexTableWrap tbody th{padding:5px 12px;border-bottom:1px solid var(--border);font-size:var(--fs-md);
  text-align:left;font-weight:400;color:var(--text);background:none;position:static;white-space:normal}
/* The metric column exists only for the phone table, where three columns keep
   the table inside its container. .tablewrap only becomes a scrollport when it
   has to pan, and a scrollport is what the sticky <th> resolves against — so
   panning is what stopped the header sticking at all. */
th.thmetric,td.tmetric{display:none}
tbody tr{cursor:pointer}
tbody tr:hover{background:var(--surface)}
.tname{display:flex;align-items:center;gap:11px;font-weight:600}
.tworks{color:var(--muted);font-size:var(--fs-sm)}
/* inline where there's room, dropping to its own line in the narrow mobile
   column — nowrap keeps it from breaking into "unique" / "only" */
.uq{color:var(--muted);font-size:var(--fs-xs);white-space:nowrap;margin-left:5px}
.tworks b{color:var(--accent);font-weight:700}
/* --muted, not --faint: the glyph is this button's entire visual rendering, so
   it is the state indicator, and §1 reserves --faint for text nobody needs.
   --faint measured 3.28:1 at rest but 2.99:1 over the --raised hover fill,
   under 1.4.11's 3:1 floor. --muted is 6.45 / 5.90 / 5.37 on bg/surface/raised.
   axe cannot see this — it reports the glyph "incomplete" because the content
   is non-text. */
.star{font-size:var(--fs-lg);color:var(--muted);padding:10px 11px;margin:-8px -5px;border-radius:var(--r-sm);line-height:1}
.star.on{color:var(--gold)}
.star:hover{background:var(--raised)}
@media(max-width:840px){
  th.hn,td.hn{display:none}
  td{padding:6px 8px}th{padding:7px 8px}
  /* .child-card used to stack and centre here; it stays a row at every width
     now and only the art shrinks at <=640 — stacking cost ~100px of height and
     centred the running text the redesign moved to the start edge. */
  /* .modal / .mbar are re-sized in the ≤640 block near the end of the file —
     declared here they sit BEFORE the base rules at equal specificity and lose
     the tie, which is how .mbar kept a -24px margin against 16px of padding and
     hung 7px past the dialog on both sides. */
  .mhead{flex-direction:column;text-align:center}
  .mhead h2{justify-content:center}
  .mhead .crow{justify-content:center}
  .mbtns{justify-content:center}
}

/* ---------- modal ---------- */
/* The DIALOG scrolls, not the overlay. Every one of the 299 pal cards is taller
   than the viewport at 360 (median 1581px, tallest 2332px) and the close button
   used to be absolutely positioned at the top of the card, so it left the screen
   85px in — with no Escape on touch, the only remaining exit was a 16px strip
   of scrim down each edge, under 2.5.8's floor and undiscoverable. */
.overlay{position:fixed;inset:0;z-index:100;background:var(--scrim);display:none;align-items:center;justify-content:center;padding:var(--sp-6) var(--sp-4);overflow:hidden}
.overlay.open{display:flex}
.modal{width:min(640px,100%);background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--r-xl);padding:0 var(--sp-6) var(--sp-6);position:relative;animation:pop var(--t-base) var(--ease-out);
  max-height:calc(100vh - var(--sp-6) * 2);max-height:calc(100dvh - var(--sp-6) * 2);overflow-y:auto;overscroll-behavior:contain}
/* the card's own action bar: sticky to the top of the scrollport, so ‹ › and ✕
   are reachable at any scroll depth. Negative margins carry it out to the
   modal's padding box so it spans the full width and can carry the surface. */
.mbar{position:sticky;top:0;z-index:3;display:flex;align-items:center;gap:var(--sp-1);
  margin:0 calc(var(--sp-6) * -1) var(--sp-3);padding:var(--sp-2) var(--sp-4);
  background:var(--surface);border-bottom:1px solid var(--border);border-radius:var(--r-xl) var(--r-xl) 0 0}
.modal .close{margin-left:auto;color:var(--muted);font-size:var(--fs-h2);padding:var(--sp-1) var(--sp-3);border-radius:var(--r-md)}
.modal .close:hover{background:var(--raised);color:var(--text)}
.mhead{display:flex;gap:20px;align-items:center}
.mhead .pico{width:96px;height:96px;font-size:34px}
.mtitle{display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap}
.mhead h2{font-size:var(--fs-h1);display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
/* --fs-lg, not a bare 16px: 16 is not on the type scale (§3) */
.mhead .zk{color:var(--muted);font-size:var(--fs-lg);font-weight:400}
.mdesc{margin-top:var(--sp-4);color:var(--text-2);font-size:var(--fs-md);background:var(--bg);border:1px solid var(--border);border-radius:var(--r-lg);padding:var(--sp-3) var(--sp-4);font-style:italic}
/* The card's section interiors predate the §2 scale. Moved onto it here rather
   than inside a functional change, because it is a 1-2px shift across seven
   rules and the visual diff is worth reading on its own. */
.msec{margin-top:var(--sp-5)}
.msec h3{font-size:var(--fs-sm);letter-spacing:.7px;text-transform:uppercase;color:var(--accent);margin-bottom:var(--sp-2);font-weight:700}
.statgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:var(--sp-2)}
.stat{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-md);padding:var(--sp-2) var(--sp-3)}
.stat .lb{font-size:var(--fs-xs);color:var(--muted)}
.stat .vl{font-weight:700;font-size:var(--fs-base)}
.pskill{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-lg);padding:var(--sp-4)}
.pskill .psn{font-weight:700;color:var(--gold);font-size:var(--fs-base)}
.pskill .pst{display:flex;gap:6px;flex-wrap:wrap;margin:var(--sp-2) 0}
.pskill .psd{color:var(--text-2);font-size:var(--fs-md)}
.pskill .psn .badge{margin-left:8px;vertical-align:1px}
/* The rank table can be wider than a phone card; it pans inside itself rather
   than pushing the page sideways (same deal as .tablewrap in the Paldex). */
.ranktbl{margin-top:10px;width:100%;font-size:var(--fs-sm);border-collapse:collapse}
.ranktbl caption{caption-side:top;text-align:left;color:var(--muted);font-size:var(--fs-xs);padding-bottom:4px}
.ranktbl th{position:static;background:none;padding:4px 8px;font-size:var(--fs-xs);cursor:default;white-space:nowrap}
.ranktbl thead th{color:var(--muted)}
/* row headers are effect names, not column labels — the Paldex's uppercase
   table-header treatment makes a phrase like "Extra mid-air jumps" shout */
.ranktbl tbody th{text-align:left;font-weight:600;color:var(--text-2);font-size:var(--fs-sm);
  white-space:normal;text-transform:none;letter-spacing:0}
/* who the effect lands on — Sekhmet grants work speed to the base and to
   itself, and without this the two rows read as one repeated twice */
.ranktbl .rtgt{display:block;font-weight:400;font-size:var(--fs-xs);color:var(--muted)}
.ranktbl td{padding:4px 8px;border-bottom:1px solid var(--border);text-align:right;font-variant-numeric:tabular-nums}
.ranktbl tbody th{border-bottom:1px solid var(--border)}
/* rank 5 is the one people are deciding whether to pay for */
.ranktbl td:last-child{color:var(--text);font-weight:600}
.rosentry{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-lg);padding:12px 16px;display:flex;flex-direction:column;gap:8px}
.rosentry .row1{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.rosentry .row1 .g{font-size:var(--fs-base)}
.rosentry .rnote{color:var(--muted);font-size:var(--fs-sm);font-style:italic}
.mbtns{display:flex;gap:var(--sp-3);margin-top:var(--sp-5);flex-wrap:wrap}
.mbtns .alink{padding:var(--sp-2) var(--sp-5)}
.droplist{display:flex;gap:8px;flex-wrap:wrap}
.breedmeta{display:flex;gap:8px;flex-wrap:wrap;align-items:center}

/* ---------- guide ---------- */
.guide{max-width:780px;margin:0 auto}
.gcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-xl);padding:26px 30px;margin-bottom:22px}
/* inline-flex + gap, §7's own prescription: the margin below targeted a <span>
   neither heading has carried since they became an <img> and an <svg>, so the
   cake art sat flush against the "B" at 0.0px */
.gcard>h2{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-h2);margin-bottom:6px}
.gcard .sub{color:var(--muted);font-size:var(--fs-md);margin-bottom:16px}
.gcard ol,.gcard ul{margin:10px 0 10px 22px;display:flex;flex-direction:column;gap:8px}
.gcard p{margin:10px 0}
/* --text, not --accent: §1 reserves the accent for interactive things, and this
   view had 71 bold prose runs in it against six real buttons at --muted 12.5px —
   the only controls in the view were dimmer and smaller than the emphasis beside
   them. Emphasis comes from weight, which is §3's mechanism. */
.gcard b{color:var(--text)}
.gcard .gold{color:var(--gold)}
/* box-decoration-break keeps each wrapped fragment a complete pill instead of
   splitting one border across two lines (the breeding formula on narrow screens) */
.gcard code{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-sm);padding:1px 7px;font-size:var(--fs-md);font-family:Consolas,monospace;
  overflow-wrap:break-word;-webkit-box-decoration-break:clone;box-decoration-break:clone}
.gcard details{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-lg);padding:0;margin:10px 0}
.gcard summary{cursor:pointer;font-weight:700;padding:13px 18px;user-select:none;color:var(--text)}
.gcard summary:hover{color:var(--accent)}
.gcard details>div{padding:2px 18px 16px;color:var(--text-2);font-size:var(--fs-md)}
.gcard details ul{margin-left:20px}
/* .gcard ol,.gcard ul above sets flex-direction:column and a 22px indent, and
   it is 0-1-1 — a bare .recipe (0-1-0) never matched while these were <div>s
   and lost to it the moment they became real <ul>s, stacking five chips into
   five full-width pills. Scoped to .gcard to outrank it. */
.gcard .recipe{display:flex;flex-direction:row;gap:var(--sp-2);flex-wrap:wrap;margin:var(--sp-2) 0}
.note{border-left:3px solid var(--gold);background:var(--gold-tint);padding:10px 14px;border-radius:0 10px 10px 0;font-size:var(--fs-md);margin:12px 0;color:var(--text-2)}
/* ---------- planner ---------- */
.pcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-xl);padding:22px 26px;margin-bottom:22px}
.pcard>h2{font-size:var(--fs-h2);margin-bottom:4px}
.pcard>.sub{color:var(--muted);font-size:var(--fs-md);margin-bottom:16px}
.addrow{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
.ptag{position:relative;flex:1;min-width:220px;max-width:360px}
.ptag .taginp{width:100%;background:var(--raised);border:1px solid var(--border);border-radius:var(--r-lg);padding:10px 14px}
.ptag .taginp:focus{border-color:var(--accent)}
.ptag .tpop{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:60;background:var(--overlay);border:1px solid var(--border-strong);border-radius:var(--r-lg);box-shadow:0 12px 40px rgba(0,0,0,.5);max-height:260px;overflow-y:auto;display:none}
.ptag.open .tpop{display:block}
.ptag .trow{display:flex;align-items:center;gap:8px;width:100%;padding:7px 12px;text-align:left;font-size:var(--fs-md)}
.ptag .trow:hover,.ptag .trow.hl{background:var(--raised)}
.ptag .trow .tr-r{margin-left:auto;font-size:var(--fs-xs);color:var(--muted)}
.pchips{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
.pchip .uii,.trow-n .uii{margin-right:1px}
.trow-n{display:inline-flex;align-items:center;gap:7px}
.pchip{display:inline-flex;align-items:center;gap:6px;background:var(--success-tint);border:1px solid var(--success-line);color:var(--success);border-radius:var(--r-full);padding:2px 11px;font-size:var(--fs-sm);font-weight:600;cursor:pointer}
.pchip:hover{border-color:var(--danger);color:var(--danger)}
.pchip.neg{background:var(--danger-tint);border-color:var(--danger-line);color:var(--danger)}
.pchip.ro{cursor:default}
.pchip.ro:hover{border-color:var(--success-line);color:var(--success)}
.pchip.neg.ro:hover{border-color:var(--danger-line);color:var(--danger)}
/* Two views over one grid. Tiles: a board of species. Rows: the full list.
   In Tiles, the open species' panel is a real sibling placed after the last
   tile of that tile's visual row and spanning every column, so grid
   auto-placement puts it on its own row and resumes the board underneath —
   DOM order stays visual order, and grid-auto-flow:dense stays forbidden
   (and unnecessary). */
.roster{display:grid;gap:10px;margin-top:16px}
/* One track above 640, not a ladder. Every extra breakpoint stepped the tile
   size up and the column count down, so a wider window produced a TALLER
   board — measured +97px across the old 1024 seam. Phones keep a smaller
   track because 120px would drop them to two columns. */
.roster.tileview{grid-template-columns:repeat(auto-fill,minmax(min(120px,100%),1fr))}
/* minmax(0,1fr), not 1fr: a bare 1fr floors the track at the widest section's
   min-content, so one long passive chip row pushed the whole page past 320px */
.roster.rowview{grid-template-columns:minmax(0,1fr)}
@media (max-width:640px){.roster.tileview{grid-template-columns:repeat(auto-fill,minmax(min(96px,100%),1fr));gap:8px}}
.rospanel,.roster>.hint{grid-column:1/-1}
.gm,.gf{font-weight:700}
.gm{color:var(--male)}.gf{color:var(--female)}
.g svg{width:.95em;height:.95em;display:inline-block;vertical-align:-.08em}
.rstats{color:var(--muted);font-size:var(--fs-sm);white-space:nowrap}
select#genderSel{padding:10px 12px}
.ivrow{display:flex;gap:6px}
.iv{width:62px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:10px 8px;outline:none;text-align:center;-moz-appearance:textfield}
.iv::-webkit-outer-spin-button,.iv::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.iv:focus{border-color:var(--accent)}
.ivchip{font-size:var(--fs-xs);color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:var(--r-sm);padding:1px 7px;margin-left:6px;white-space:nowrap;display:inline-block}
/* a roster note is a caption, not a callout — reset the guide's .note blockquote */
.rosrow .note{border:0;background:none;padding:0;border-radius:0;
  color:var(--muted);font-size:var(--fs-sm);font-style:italic;margin:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.warnbox{border-left:3px solid var(--danger);background:var(--danger-tint);padding:10px 14px;border-radius:0 10px 10px 0;font-size:var(--fs-md);margin-bottom:12px;color:var(--text-2)}
/* inline, not a flex item: the message can be a fragment of text nodes and
   gender spans, and a flex .warnbox would gap every one of them apart */
.warnbox .lui{color:var(--danger);margin-right:6px;vertical-align:-3px}
.rmodal{width:min(480px,100%)}
.rmtitle{font-size:var(--fs-h2);margin-bottom:16px}
/* said once to a new roster, then gone — see openRosterEditor */
.dlgwhy{color:var(--muted);font-size:var(--fs-sm);margin:-8px 0 16px;line-height:1.5}
.dlgwhy b{color:var(--text-2)}
/* what "Save & add another" carried over, and the way back out of it */
.carrynote{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:-6px 0 16px;
  background:var(--accent-tint);border:1px solid var(--accent);border-radius:var(--r-lg);padding:9px 13px;
  font-size:var(--fs-sm);color:var(--text-2)}
.carrynote[hidden]{display:none} /* a display: of its own outranks the UA's [hidden] */
.carrynote>span{flex:1;min-width:120px}
.carrynote .alink{padding:4px 12px;font-size:var(--fs-sm)}
/* passive sets offered back for one tap — dashed, so they read as a suggestion
   rather than as the selected chips sitting underneath them */
.psets{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:8px}
.psets[hidden]{display:none}
.psetlb{font-size:var(--fs-xs);letter-spacing:.6px;text-transform:uppercase;color:var(--muted);font-weight:700}
.pset{background:var(--surface);border:1px dashed var(--border-strong);border-radius:var(--r-full);
  padding:3px 12px;font-size:var(--fs-sm);font-weight:600;color:var(--muted);max-width:100%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* :not([aria-pressed="true"]) or this outranks the pressed rule below on
   specificity and a hovered pressed chip loses BOTH its state cues — the
   --success text and the solid --success-line — leaving only a ~1.05:1 tint,
   which §1 calls atmosphere and never a signal */
.pset:hover:not(:disabled):not([aria-disabled="true"]):not([aria-pressed="true"]){color:var(--text);border-color:var(--accent);border-style:solid}
/* the planner's carry row toggles rather than consuming its chips — a chip that
   vanishes under a finger reflows the row mid-tap. Solid border + tint, so the
   pressed state is shape and fill, not hue alone (§1). 8.31:1 on --surface. */
.pset[aria-pressed="true"]{color:var(--success);border:1px solid var(--success-line);background:var(--success-tint)}
.pset[aria-pressed="true"]:hover{border-color:var(--accent)}
.pset:disabled,.pset[aria-disabled="true"]{opacity:.45;cursor:not-allowed}
/* 24px target floor without moving anything, the .rosrow .thbtn recipe */
#carryFrom .pset{padding-top:6px;padding-bottom:6px;margin-top:-3px;margin-bottom:-3px}
/* The toolbar is the single chip display. The picker's own .pchips render the
   identical pill — same fill, border, radius and weight — so every carried
   passive appeared twice, 40px apart, one toggling and one removing. */
#carryPass .pchips{display:none}
#carryRow{margin-bottom:var(--sp-4)}
/* The remedy on a legacy plan's warn chip. §4's rule for .mchip.warn: state it
   in words wherever the width allows, and in an sr-only tail where it does not —
   never display:none, which would take it from AT as well. */
.mchip.warn .whytail{color:inherit}
@media (max-width:640px){
  .mchip.warn .whytail{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
}
.vform{display:flex;flex-direction:column;gap:15px}
.fld label{display:block;font-size:var(--fs-xs);letter-spacing:.6px;text-transform:uppercase;color:var(--muted);margin-bottom:6px;font-weight:700}
.fld label .opt{text-transform:none;letter-spacing:0;font-weight:400;opacity:.75}
.fld .picker{max-width:none;min-width:0}
.fld .search-inp{width:100%}
.fld .ivrow .iv{flex:1;min-width:0}
.segrow{display:flex;gap:6px}
.segrow button{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:9px 10px;border-radius:var(--r-md);background:var(--raised);border:1px solid var(--border);font-size:var(--fs-md);font-weight:600;color:var(--muted)}
.segrow button:hover:not(.on){color:var(--text);border-color:var(--border-strong)}
.segrow button.on{color:var(--text);border-color:var(--accent);background:var(--accent-tint)}
.moredet{border:1px solid var(--border);border-radius:var(--r-lg);background:var(--bg)}
.moredet summary{cursor:pointer;padding:11px 16px;font-weight:600;color:var(--muted);user-select:none;font-size:var(--fs-md);list-style-position:inside}
.moredet summary:hover{color:var(--text)}
.moredet>div{padding:4px 16px 15px;display:flex;flex-direction:column;gap:13px}
.formacts{margin-top:18px;display:flex;gap:10px;align-items:center}
.formacts .primary{flex:1;text-align:center}
.collhead{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:14px}
.collhead h2{font-size:var(--fs-h2)}
.collhead .collacts{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
/* a view-style switch sits at the far end of its heading block, so it reads as
   a property of the view rather than another filter */
.collhead .viewseg{margin-left:auto;flex:0 0 auto}
/* .collacts also has margin-left:auto; two autos share the slack and leave the
   switch stranded mid-header. Give the pair a fixed gap so the one auto on
   .viewseg absorbs everything and both sit flush right, matching the Paldex. */
@media (min-width:641px){.collhead .viewseg + .collacts{margin-left:var(--sp-3)}}
.segrow[hidden]{display:none}
/* ---- roster: one species section, one pal per row ---- */

.sub{color:var(--muted);font-size:var(--fs-md);line-height:1.5}

/* ---- Paldex gallery ----
   299 species, every one a distinct art, and 84 of them near-duplicate variant
   names that only the picture separates. Recognition is the job here, which is
   what the table is worst at. */
.dexgrid{list-style:none;margin:16px 0 0;padding:0;display:grid;gap:8px;
  grid-template-columns:repeat(auto-fill,minmax(min(88px,100%),1fr))}
.dexgrid[hidden]{display:none} /* a display: of its own outranks the UA's [hidden] */
.dextile{position:relative;background:var(--surface);border:1px solid var(--border);
  border-radius:var(--r-lg);transition:border-color var(--t-fast),background var(--t-fast)}
.dextile:hover{border-color:var(--border-strong);background:var(--raised)}
/* owned is a ring and a fill, never a filter on the art: greying out 260 of
   299 tiles would wreck the recognition this grid exists for (DESIGN.md §7) */
.dextile.on{border-color:var(--gold-line);background:var(--gold-tint)}
.dextile-open{display:flex;flex-direction:column;align-items:center;gap:4px;
  width:100%;background:none;border:0;padding:8px 6px;cursor:pointer;color:var(--text)}
/* the whole tile is the press target; the star sits above it */
.dextile-open::after{content:'';position:absolute;inset:0;border-radius:var(--r-lg)}
.dextile .pico{--ico:52px}
.dt-name{font-size:var(--fs-sm);font-weight:600;line-height:1.25;text-align:center;
  overflow-wrap:anywhere;color:var(--text)}
.dt-meta{font-size:var(--fs-xs);color:var(--muted);text-align:center;line-height:1.3}
.dt-meta .uq{display:block}
.dextile .star{position:absolute;top:0;right:0;z-index:1;padding:11px;margin:0;line-height:1;
  background:none;border:0;font-size:var(--fs-md)}
/* 36x36, not the 44 §8 prefers for a primary mobile action: at 44 the star's
   hit box covers a quarter of an 80px tile and overlaps the art, stealing
   presses from the tile itself. Both are real targets here. */
/* owned through the roster rather than starred here — same gold, lighter weight */
.star.viaroster{color:var(--gold);opacity:.75}
@media (max-width:640px){
  .dexgrid{grid-template-columns:repeat(auto-fill,minmax(min(72px,100%),1fr));gap:6px}
  .dextile .pico{--ico:40px}
  .dextile-open{padding:6px 4px;gap:2px}
  .dt-name{line-height:1.2}
}

/* visually hidden but announced. .nativehide is the opposite job — it hides a
   control from AT while leaving it on screen. */
.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}

/* ---- Rows view: one static section per species ---- */
.rosgrp{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-lg);padding:0}
/* the species header band, used by a Rows section and by the panel */
.rosband{display:flex;align-items:center;gap:10px;font-weight:700;font-size:var(--fs-md);
  padding:9px 15px;margin:0;color:var(--text)}
.rosband .cntb{background:var(--raised);border:1px solid var(--border-strong);border-radius:var(--r-full);
  font-size:var(--fs-xs);padding:1px 9px;color:var(--muted);font-weight:600;white-space:nowrap}
.rosband .gname2{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rosband .chiprow{display:flex;align-items:center;gap:8px;min-width:0}
.rosband .pclose{margin-left:auto}

/* ---- Tiles view: one species per tile ---- */
.rostile{scroll-margin-top:64px;position:relative;display:flex;flex-direction:column;align-items:center;gap:6px;
  background:var(--raised);border:1px solid var(--border);border-radius:var(--r-lg);
  padding:12px 8px 20px;cursor:pointer;color:var(--text);text-align:center;
  transition:border-color var(--t-fast),background var(--t-fast)}
@media (hover:hover){.rostile:hover{border-color:var(--border-strong);background:var(--overlay)}}
.rostile:active{background:var(--raised);transform:scale(.98)}
.rostile[aria-expanded="true"]{border-color:var(--accent);background:var(--accent-tint)}
@media (hover:hover){.rostile[aria-expanded="true"]:hover{border-color:var(--accent-hover)}}
.rostile[aria-expanded="true"]::after{content:'';position:absolute;left:50%;bottom:-9px;
  transform:translateX(-50%);width:0;height:0;border:5px solid transparent;border-top-color:var(--accent)}
.rostile .tart{position:relative;display:block;line-height:0}
.rostile .pico{--ico:56px}
/* the count rides the art instead of taking a whole chip line */
.rostile .cntb{position:absolute;top:-4px;right:-8px;background:var(--bg);
  border:1px solid var(--muted);border-radius:var(--r-full);
  font-size:var(--fs-xs);padding:0 6px;color:var(--text);font-weight:700;line-height:1.5}
.rostile .tname{font-size:var(--fs-sm);font-weight:600;line-height:1.25;overflow-wrap:anywhere}
.rostile .chiprow{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;min-width:0;max-width:100%}
.rostile .chiprow .mchip{flex-wrap:wrap;justify-content:center;row-gap:0;max-width:100%}
/* The tally drops its chip chrome, the warning keeps it — at EVERY width, not
   just on phones. The tile's warn text is a bare count ("all 3 ♂"), so without
   this the two chips are geometrically identical and hue is the only signal. */
.rostile .chiprow .mchip:not(.warn){background:none;border:0;padding:0;
  font-size:var(--fs-xs);color:var(--muted)}
/* a persistent, non-hover-gated "this opens" cue */
.tchev{position:absolute;bottom:4px;right:6px;color:var(--muted);
  transition:transform var(--t-fast) var(--ease-out)}
.rostile[aria-expanded="true"] .tchev{transform:rotate(180deg);color:var(--accent)}
@media (max-width:640px){
  /* the ✕ takes ~32px and .gname2 is the only shrinkable child, so the name
     was ellipsised to "Lam…" — give it the first line to itself.
     Unscoped: §4 says .rosband is one implementation with two homes, and only
     the panel got this. In a Rows section the one-gender warn chip ate the row
     the same way, so at 360 a section header read "La…" and nothing on screen
     named the species. */
  .rosband{flex-wrap:wrap;row-gap:4px}
  .rosband .gname2{flex:1 1 auto}
  .rospanel .rosband .pclose{order:2}
  .rosband .chiprow{order:3;flex:1 1 100%}
  .rostile{padding:8px 6px 16px;gap:2px}
  .rostile .pico{--ico:44px}
}

/* ---- the expanding panel ---- */
.rospanel{scroll-margin-top:64px;background:var(--bg);border:1px solid var(--accent);border-radius:var(--r-lg);
  overflow:hidden;animation:panelin .15s var(--ease-out)}
@keyframes panelin{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.rospanel{animation:none}}
.rospanel .rosband{border-bottom:1px solid var(--border);background:var(--surface)}

.roslist{list-style:none;margin:0;padding:0}
/* Identical tracks on every row, so columns line up down the whole section.
   The identity track has to be able to give ground: fixed at 240px it starved
   the passives track to 0px between 641 and ~860px and painted the chips on
   top of the note. Passives are what you scan for, so they yield last. */
.rosrow{display:grid;grid-template-columns:minmax(140px,240px) minmax(200px,1fr) minmax(0,220px) 116px;
  align-items:center;gap:12px;padding:6px 15px;border-top:1px solid var(--border);
  transition:background var(--t-fast)}
/* below this the note can't have a column of its own without squeezing the
   chips, so it drops to its own line and keeps the row on two tracks */
@media (max-width:1000px){
  .rosrow{grid-template-columns:minmax(0,1fr) minmax(0,2fr) 116px}
  /* selection mode renders no actions track but does add a 28px checkbox lead.
     Without these the .who below lands in the checkbox column and every name in
     the list clips to 28px — silently, because it ellipsises rather than
     overflowing. The =<640 block already had them; this range did not. */
  .rosrow.selectable{grid-template-columns:28px minmax(0,1fr) minmax(0,2fr)}
  .rosrow.selectable .rchk{grid-area:1/1}
  .rosrow.selectable .who{grid-area:1/2}
  .rosrow.selectable .pscol{grid-area:1/3}
  .rosrow.selectable .note{grid-area:2/2/3/-1}
  /* the toolbar has to be pinned to row 1 explicitly: the note spans the full
     width before it in DOM order, which would otherwise push it to a third row
     and leave an empty band under every row that has a note */
  .rosrow .who{grid-area:1/1}
  .rosrow .pscol{grid-area:1/2}
  .rosrow .acts{grid-area:1/3}
  .rosrow .note{grid-area:2/1/3/-1;margin-top:2px}
}
.rosrow:hover{background:var(--raised)}
.rosrow.editing{box-shadow:inset 2px 0 0 var(--accent);background:var(--accent-tint)}
.rosrow .who{display:flex;align-items:center;gap:2px;min-width:0;flex-wrap:wrap}
.rosrow .nm{font-weight:600;font-size:var(--fs-md);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* the species standing in for a name the entry doesn't have — --muted at 400 so
   it reads as a fallback, not as the band's name (--text/700) repeated */
.rosrow .nm .nmfb{color:var(--muted);font-weight:400}
/* gender not recorded — same slot and weight as ♂/♀, so the column still aligns */
.rosrow .gu{color:var(--muted);font-weight:700}
.rosrow .pscol{min-width:0}
.rosrow .note:empty{display:none}
/* "No passives" is a fact about the pal, not decoration — --muted (6.45:1),
   never --faint, which is below AA for text */
.rosrow .nops{color:var(--muted);font-size:var(--fs-sm);font-style:italic}
.rosrow .acts{display:flex;gap:5px;justify-content:flex-end}
/* the gutter is reserved at all times, so revealing the buttons shifts nothing */
@media (hover:hover) and (pointer:fine){
  .rosrow .acts{opacity:0;transition:opacity var(--t-fast)}
  .rosrow:hover .acts,.rosrow:focus-within .acts{opacity:1}
}
/* a meta chip that carries a warning — never colour alone, it says the words */
.mchip.warn{color:var(--danger);border-color:var(--danger-line);background:var(--danger-tint)}
/* A pink ♀ inside a pink chip reads as chip text, not as a gender colour — the
   glyph's shape already carries the gender, so let it join the warning. */
.mchip.warn .g{color:inherit}
.rosband .pico{--ico:28px}
/* compact: the same row, less air. Scoped to open sections — a tile is
   already the dense presentation of a species, which keeps "Compact rows"
   an honest label. */
.roster.dense .rosrow{padding:3px 15px;gap:10px}
.roster.dense .rosband{padding:7px 15px}
/* every action for one entry, named, at comfortable size — see openModal */
.rentacts{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
/* shared small ghost button: roster actions, plan delete, step-open, chain nav */
.acts button,.rospanel .pclose,.stepopen,.planhead .del,.chainhead .nav button{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);padding:4px 10px;font-size:var(--fs-sm);font-weight:600;color:var(--muted)}
.acts button:hover,.rospanel .pclose:hover,.stepopen:hover,.planhead .del:hover,.chainhead .nav button:hover:not(:disabled){color:var(--text);border-color:var(--border-strong)}
/* the destructive tier at REST, not only on hover: on touch it was
   indistinguishable from the Tree disclosure beside it (DESIGN.md section 4) */
.planhead .del.danger{color:var(--danger);border-color:var(--danger-line)}
.planhead .del.danger:hover{background:var(--danger-tint);border-color:var(--danger)}
/* the stack spans the viewport and centres its toasts, rather than starting at
   the 50% mark — anchored there, a toast could never be wider than half the
   screen, so on a 320px phone every message over a few words became a column
   of one-word lines. The container ignores pointer events; the toasts don't. */
.toasts{position:fixed;bottom:18px;left:0;right:0;z-index:300;display:flex;flex-direction:column;gap:8px;align-items:center;pointer-events:none}
.toast{pointer-events:auto;display:flex;align-items:center;gap:14px;background:var(--overlay);border:1px solid var(--border-strong);border-radius:var(--r-lg);padding:11px 16px;box-shadow:0 10px 34px rgba(0,0,0,.55);font-size:var(--fs-md);animation:pop var(--t-base) var(--ease-out);max-width:min(480px,92vw)}
/* both measured under the 24px floor of WCAG 2.2 2.5.8 — the Undo at 36×21 and
   the dismiss at 23×25 — on the one control the whole Alt+Z route exists to reach */
/* At 320 a long message ate the row: "Un-star Lamball" was squeezed to 27px and
   rendered as a column of single letters, and Undo sat on its 24px min-width
   with the word inside it clipped. The actions travel as one .tacts block that
   never shrinks, so when the message needs the width the block drops to a row
   of its own (two flex items, so it wraps whole) rather than the message being
   crushed to 15 lines to keep the buttons beside it. No breakpoint: the wrap
   happens exactly when the two no longer fit, at whatever width that is. */
.toast{flex-wrap:wrap}
.toast > span{flex:1 1 auto;min-width:0}
.tacts{display:flex;align-items:center;gap:10px;margin-left:auto;flex:none}
.toast .undo,.toast .tx{min-width:24px;min-height:24px;display:inline-flex;align-items:center;justify-content:center;flex:none;white-space:nowrap}
.toast .undo{color:var(--accent);font-weight:700}
.toast .undo:hover{text-decoration:underline}
/* the quiet tier, one rule for both its homes (DESIGN.md §4) */
.toast .tx,.bulkbar .tx{color:var(--muted);padding:4px 10px;border-radius:var(--r-md);min-height:24px}
.toast .tx:hover,.bulkbar .tx:hover{color:var(--text);background:var(--raised)}
.subtabs{margin-bottom:18px;width:fit-content}
.subtabs button{padding:6px 15px;font-size:var(--fs-md)}
@media(max-width:640px){.subtabs{display:flex}}
.subtabs[hidden]{display:none}
.hatchpanel .rstep{background:var(--surface)}
#hatchDepth,#comboKind{flex-shrink:0}
#hatchDepth button,#comboKind button{white-space:nowrap;flex:0 0 auto;padding:9px 12px}
.hatchgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(230px,100%),1fr));gap:10px;list-style:none;margin:0;padding:0}
/* the grid item is the cell; the card and the panel fill it */
.hatchgrid>li{display:flex;min-width:0}
.hatchgrid>li.hpanelcell,.hatchgrid>li.hint{grid-column:1/-1}
.hatchgrid>li.hint{display:block}
.hcell>.hcard,.hpanelcell>.hatchpanel{flex:1 1 auto;min-width:0}
/* Name on one row, badges on the next — a fixed two-row grid, so a card with a
   rarity badge is the same height as one without (DESIGN.md §4). --raised, not
   --surface: this card sits on a --surface .pcard, and a card nested in a card
   goes one rung up the ladder (§1). */
.hcard{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;column-gap:var(--sp-3);row-gap:var(--sp-1);background:var(--raised);border:1px solid var(--border);border-radius:var(--r-lg);padding:10px 14px;text-align:left}
.hcard>.pico{grid-row:1/3}
.hcard:hover{border-color:var(--border-strong)}
.hcard .nm{font-weight:600;font-size:var(--fs-md);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* --sp-1, not --sp-2: the widest real row is `legendary · New · 1 pair` at
   169.7px in a 180px track, and 8px gaps put it 1.7px over — one card wrapping
   takes its whole grid row with it, which is the raggedness this layout exists
   to remove. .ways's auto margin supplies the visual separation anyway, so the
   gap only has to hold the badge run apart. flex-wrap stays as a safety valve. */
.hcard .hmeta{display:flex;align-items:center;gap:var(--sp-1);min-width:0;flex-wrap:wrap}
/* the count keeps its own column: it is the card's one varying quantity, and
   packed behind two optional badges its x wandered over 103px of the board */
.hcard .ways{margin-left:auto;font-size:var(--fs-xs);color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:var(--r-full);padding:1px 8px;flex-shrink:0}
.hcard .newb{font-size:var(--fs-xs);font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--success);border:1px solid var(--success-line);border-radius:var(--r-sm);padding:1px 6px}
.hcard.expanded{border-color:var(--accent)}
.hatchpanel{background:var(--bg);border:1px solid var(--border-strong);border-radius:var(--r-lg);padding:13px 15px;display:flex;flex-direction:column;gap:10px}
.hatchpanel .hpttl{font-size:var(--fs-sm);font-weight:700;color:var(--muted)}
.hatchpanel .pair{background:var(--surface)}
.warnchip{font-size:var(--fs-xs);color:var(--danger);border:1px solid var(--danger-line);border-radius:var(--r-sm);padding:1px 7px;flex-shrink:0;
  display:inline-flex;align-items:center;gap:4px}
/* min-height, not padding alone: at 1px 7px this measured 70.9×20.5 and missed
   the 24px floor of WCAG 2.2 2.5.8 (DESIGN.md §11) */
.odds{font-size:var(--fs-xs);color:var(--muted);border:1px solid var(--border);border-radius:var(--r-sm);padding:1px 8px;flex-shrink:0;cursor:pointer;
  display:inline-flex;align-items:center;gap:4px;min-height:24px}
.odds:hover{color:var(--text);border-color:var(--border-strong)}
.odds[aria-expanded="true"]{color:var(--text);border-color:var(--accent)}
.oddsinfo{flex-basis:100%;font-size:var(--fs-sm);color:var(--muted);line-height:1.45}
/* "where do I catch one" — same expand-in-place shape as .odds/.oddsinfo, but
   its own class so the two toggles can't remove each other's panel */
.odds.wild{border-color:var(--accent-line);color:var(--text-2)}
.odds.wild:hover{border-color:var(--accent)}
.wildinfo{flex-basis:100%;min-width:0;font-size:var(--fs-sm);color:var(--muted);line-height:1.45;
  display:flex;flex-direction:column;align-items:flex-start;gap:6px;
  border-top:1px solid var(--border);margin-top:2px;padding-top:9px}
.wildinfo .wline{display:flex;align-items:center;flex-wrap:wrap;gap:8px;max-width:100%}
.wildinfo b{color:var(--text);font-weight:700}
.wildinfo .wnote{color:var(--text-2)}
.wildinfo .wtag{font-size:var(--fs-xs);font-weight:700;color:var(--accent);background:var(--accent-tint);
  border:1px solid var(--accent-line);border-radius:var(--r-full);padding:1px 8px;
  display:inline-flex;align-items:center;gap:4px}
.wildinfo .wlb{font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.06em;font-weight:700;color:var(--muted)}
.wildinfo .whub{display:flex;align-items:center;gap:6px;min-width:0;max-width:100%;color:var(--text-2);
  background:var(--surface);border:1px solid var(--border);border-radius:var(--r-sm);padding:2px 9px}
.wildinfo .whub>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wildinfo .whub i{font-style:normal;font-size:var(--fs-xs);color:var(--muted);flex-shrink:0}
.wildinfo .wlink{color:var(--accent);font-weight:600;text-decoration:none;border-bottom:1px solid var(--accent-line)}
.wildinfo .wlink:hover{border-bottom-color:var(--accent)}
.needrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 12px;font-size:var(--fs-sm);color:var(--muted)}
/* how gettable a species is — a named tier plus the facts it came from.
   Four ordered steps on one severity ramp (green -> neutral -> gold -> pink)
   so the order is readable without the labels, and two categorical ones off
   that scale: an alpha and an uncatchable pal aren't "harder", they're a
   different kind of answer. */
.accb{font-size:var(--fs-xs);font-weight:700;letter-spacing:.3px;padding:1px 8px;border-radius:var(--r-sm);white-space:nowrap}
.accb.easy{background:var(--success-tint);color:var(--success);border:1px solid var(--success-line)}
.accb.mid{background:var(--neutral-tint);color:var(--neutral);border:1px solid var(--border-strong)}
.accb.hard{background:var(--gold-tint);color:var(--gold);border:1px solid var(--gold-line)}
.accb.grind{background:var(--danger-tint);color:var(--danger);border:1px solid var(--danger-line)}
.accb.alpha{background:var(--dark-tint);color:var(--dark);border:1px solid var(--dark-line)}
.accb.none{background:transparent;color:var(--muted);border:1px dashed var(--border-strong)}
.accr{font-size:var(--fs-xs);font-weight:500;color:var(--muted);white-space:nowrap}
.wildinfo .accb{flex-shrink:0}
.fldhint{font-size:var(--fs-sm);color:var(--muted);margin-top:6px}
/* optional "my level" — deliberately the smallest control in the slot, since
   it changes only how catches are worded and ordered, never what's offered */
.mylv{display:flex;align-items:center;gap:9px;font-size:var(--fs-sm);color:var(--muted)}
.mylv label{font-weight:600}
.mylv input{width:74px;background:var(--bg);border:1px solid var(--border);border-radius:var(--r-md);
  padding:7px 9px;color:var(--text);font:inherit;font-weight:600}
.mylv input:hover{border-color:var(--border-strong)}
/* --fs-md, not --fs-sm: a control should not be the smallest text in its own
   sentence. --accent gives it a real cue against the --muted prose it sits in —
   §1 forbids a 1.36:1 border being the sole boundary, and inline mid-paragraph
   there is no .linkrow grouping to carry it. */
.gjump{padding:3px 12px;font-size:var(--fs-md);margin-left:4px;color:var(--accent)}
.gjump:hover{color:var(--accent-hover)}
.slotlb.mt{margin-top:12px}
#reverseResult .slotlb.lead{font-size:var(--fs-sm);color:var(--text-2)}
@media (min-width:641px){#reverseResult .slotlb.mt{margin-top:var(--sp-5)}}
.slot .ptag{max-width:none;min-width:0}
.plannerhow{margin-bottom:16px}
.plannerhow>div{padding:2px 16px 14px;color:var(--text-2);font-size:var(--fs-md)}
.plannerhow b{color:var(--accent)}
.tree{display:flex;justify-content:center;padding:6px 0 14px}
/* pan-y, not none. The viewport is 23% of the height at 360 and sits in the page's
   scroll path, so touch-action:none meant a vertical swipe that started anywhere
   on the tree scrolled nothing at all. pan-y hands vertical swipes back to the
   page and keeps the horizontal pan these trees actually need — they are wide,
   not tall — and pinch still reaches the app, since pan-y does not imply
   pinch-zoom. Reset view puts a nudged tree back. */
.tvp{position:relative;overflow:hidden;border:1px solid var(--border);border-radius:var(--r-lg);background:var(--bg);cursor:grab;touch-action:pan-y;margin-bottom:10px;width:100%;user-select:none}
.tvp *{user-select:none}
.tvp img{-webkit-user-drag:none}
.tvp.grabbing{cursor:grabbing}
.tvp-inner{transform-origin:0 0;width:max-content;padding:16px}
.tvp-ctrl{position:absolute;right:10px;bottom:10px;display:flex;gap:6px;z-index:5}
.tvp-ctrl button{width:32px;height:32px;border-radius:var(--r-md);background:var(--surface);border:1px solid var(--border-strong);color:var(--muted);
  display:grid;place-items:center}
.tvp-ctrl button:hover{color:var(--text);border-color:var(--border-strong)}
.tvp-hint{position:absolute;left:12px;bottom:12px;font-size:var(--fs-xs);color:var(--muted);pointer-events:none}
.tchip.cur{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
.stepopen{margin-left:auto;flex-shrink:0}
/* spans the zone: at min(760px,100%) inside grid-column:1/-1 its right edge
   landed 140px past the answer column and 116px into the rail, aligned with
   nothing (DESIGN.md §11) */
.chaincard{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-xl);padding:16px 18px}
.chainhead{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}
.chainhead .ttl{font-weight:700;font-size:var(--fs-md)}
.chainhead .nav{margin-left:auto;display:flex;gap:6px}
/* the chevron sits inline with the label; measured 33px tall, clear of the
   24px floor in WCAG 2.2 2.5.8 */
.chainhead .nav button{display:inline-flex;align-items:center;gap:var(--sp-1);padding-block:var(--sp-2)}
.plantree{margin:6px 0 10px}
.tn{display:flex;align-items:center;gap:0}
.tn-parents{display:flex;flex-direction:column;gap:10px;align-items:flex-end;position:relative;padding-right:14px}
.tn-parents>*{position:relative}
.tn-parents>*::after{content:'';position:absolute;right:-14px;top:50%;margin-top:-1px;width:14px;height:2px;background:var(--border-strong)}
/* vertical connector drawn per child, anchored to each child's true center —
   a single group-level bar assumed chip-height children and left orphaned
   stubs next to taller nested subtrees */
.tn-parents>*::before{content:'';position:absolute;right:-14px;top:-10px;bottom:-10px;width:2px;background:var(--border-strong);border-radius:2px}
.tn-parents>:first-child::before{top:50%}
.tn-parents>:last-child::before{bottom:50%}
.tn-join{width:20px;height:2px;background:var(--border-strong);flex-shrink:0;position:relative}
.tn-join::after{content:'▸';position:absolute;right:-6px;top:-9px;color:var(--accent);font-size:var(--fs-sm)}
.tchip{display:inline-flex;align-items:center;gap:7px;background:var(--bg);border:1px solid var(--border);border-radius:var(--r-full);padding:4px 12px 4px 5px;font-size:var(--fs-sm);font-weight:600;white-space:nowrap}
/* one rule for every pressable .tchip — the roster strip, Find parents' need row
   and the passive groups all had their own cursor/hover (§4 reuse-first) */
button.tchip{cursor:pointer}
button.tchip:hover{border-color:var(--border-strong)}
.tchip.final{border-color:var(--gold)}
.tchip .own{color:var(--gold);font-size:var(--fs-xs)}
.tchip .g{font-size:var(--fs-sm)}
/* a resting fill, not a bare glyph: what these do lived entirely in a title,
   and touch has no hover (§8). They sit together at the left of .mbar so the
   cluster reads as [‹ ›] … [✕] — two groups, not three peers. */
.mnav{display:inline-flex;align-items:center;justify-content:center;color:var(--muted);
  background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);
  line-height:1;padding:var(--sp-1) var(--sp-2);min-width:32px;min-height:32px}
.mnav:hover{color:var(--text);background:var(--raised);border-color:var(--border-strong)}
.pop .srcrow{display:flex;gap:4px;padding:7px 10px;border-bottom:1px solid var(--border)}
.pop .srcrow button{flex:1;padding:5px 10px;border-radius:var(--r-sm);font-size:var(--fs-sm);font-weight:700;color:var(--muted);background:var(--raised)}
.pop .srcrow button.on{color:var(--text);background:var(--accent-tint);box-shadow:inset 0 0 0 1px var(--accent-line)}
.pop .srcrow button:hover:not(.on){color:var(--text)}
/* A text-styled button that opens the pal card from inside a row. It is the
   only way into that card, so it carries a real 24px target — padding plus a
   matching negative margin, which grows the hit area and moves nothing. */
.rosrow .thbtn{background:none;border:0;padding:5px 6px;margin:-5px -6px;min-height:24px;min-width:24px;
  display:inline-flex;align-items:center;color:var(--text);text-align:left;cursor:pointer}
.rosrow .thbtn:hover{color:var(--accent)}
.plansec{display:flex;align-items:center;gap:12px;font-size:var(--fs-sm);font-weight:700;letter-spacing:.7px;text-transform:uppercase;color:var(--accent);margin:22px 0 12px}
.plansec::after{content:'';flex:1;height:1px;background:var(--border);order:1}
.plansec .alink{order:2;text-transform:none;letter-spacing:0}
.quicklb{font-size:var(--fs-sm);color:var(--muted);margin-bottom:8px}
/* .chiprow, not a .strip of its own: since the strip became a roving toolbar it
   has been doing .chiprow's documented job under a second name (§4) */
#rosterStrip{margin-bottom:16px}
/* auto-FILL, not auto-fit. The slots reveal progressively, and auto-fit collapses
   the tracks the hidden ones would have taken — so slot 1 alone was a 1106px
   picker at 1366, and filling it halved the width of the control under the
   user's cursor (-559px at 1366, -409px at 900, -343px at 768). auto-fill keeps
   the tracks sized, so the row never moves. Fixed repeat(4,…) would hold it
   still too, but at 900 it gives four 192.5px tracks — under this rule's own
   220px minimum — so the column count still has to be responsive. */
.slotgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-bottom:14px}
.slot{background:var(--bg);border:1px dashed var(--border-strong);border-radius:var(--r-lg);padding:12px 14px}
.slotlb{font-size:var(--fs-xs);font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--muted);margin-bottom:7px}
.slot .picker{max-width:none;min-width:0}
.slot .picker-btn{padding:8px 12px}
/* flex-start, not stretch: with the passive block moved to section 1 the target
   card holds one picker, and stretching it to the options card's height left
   87px of dead space under the row's only --accent border (§11) */
.targetrow{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-start}
.targetrow .slot{flex:1;min-width:240px;max-width:380px;border-style:solid;border-color:var(--accent);border-width:1px}
/* max-width sized to the three-way partner control: at 320px "Any species"
   was the only label wrapping to two lines, which read as a broken column */
.targetrow .slot.opts{border-style:dashed;border-color:var(--border-strong);flex:0 1 auto;min-width:220px;max-width:352px;display:flex;flex-direction:column;gap:8px;align-items:stretch}
#partnerMode button{padding-left:7px;padding-right:7px}
.targetrow .slot.opts .slotlb{margin-bottom:0}
.routewrap{margin-top:20px}
.rstep{display:flex;align-items:center;gap:12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--r-lg);padding:12px 16px;margin-bottom:8px;flex-wrap:wrap}
.rstep .stepno{width:26px;height:26px;border-radius:50%;background:var(--raised);border:1px solid var(--border-strong);display:flex;align-items:center;justify-content:center;font-size:var(--fs-sm);font-weight:700;color:var(--accent);flex-shrink:0}
.rstep .unit{display:flex;align-items:center;gap:8px;min-width:0}
.rstep .unit .nm{font-weight:600;font-size:var(--fs-md)}
.rstep .unit .g{font-size:var(--fs-md)}
.rstep .sym{color:var(--muted);font-size:16px}
.rstep .arr2{color:var(--accent);font-size:var(--fs-lg);font-weight:700}
.rstep .own{color:var(--gold);font-size:var(--fs-sm)}
.rstep .badge{flex-shrink:0}
.routemeta{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:12px 0}
.saverow{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px;align-items:center}
.planhead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:10px}
.planhead .nm{font-weight:700;font-size:16px}
.planhead .pushr{margin-left:auto}
.plan{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-lg);padding:16px 18px;margin-bottom:12px}
.plan .rstep{background:var(--surface);cursor:default}
/* .45 is this app's DISABLED recipe (button:disabled), and at that alpha a
   done step's names computed 4.01:1 - below AA for the very content the
   checklist exists to show, on a row whose controls are still operable. */
.plan .rstep.done{transition:opacity var(--t-base)}
.plan .rstep.done .unit .nm{color:var(--muted);text-decoration:line-through}
.plan .rstep.done .badge,.plan .rstep.done .pico{opacity:.55}
/* one checkbox recipe, used by the plan's step ticks and the roster's selection
   mode. Promoted from .plan .rstep .chk rather than forked (DESIGN.md §4). */
/* --muted, not --border-strong: the resting box has no fill delta from its row
   (--raised on --surface is 1.10:1), so its border is the sole thing saying a
   control is there — and §1 forbids a sub-3:1 border in that role. --muted is
   5.90:1 on --surface, and was already the :hover value below. */
.plan .rstep .chk,.rchk{appearance:none;-webkit-appearance:none;width:22px;height:22px;margin:0;border-radius:var(--r-sm);background:var(--raised);border:1px solid var(--muted);cursor:pointer;flex-shrink:0;transition:background var(--t-fast),border-color var(--t-fast)}
.plan .rstep .chk:hover,.rchk:hover{border-color:var(--muted)}
.plan .rstep .chk:checked,.rchk:checked{border-color:var(--success);background:var(--success) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M3 8.5 6.5 12 13 4.5" fill="none" stroke="%230d1117" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>') center/14px no-repeat}
/* the third state the two tristate levels need: a dash, not a tick */
.rchk:indeterminate{border-color:var(--accent);background:var(--accent) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M4 8h8" fill="none" stroke="%230d1117" stroke-width="2.5" stroke-linecap="round"/></svg>') center/14px no-repeat}
/* 22px box in a >=24px hit area, bought with padding and taken back with margin
   so nothing moves (DESIGN.md §8) */
/* 22px box in a >=24px target. The plan's tick is the stated purpose of Saved
   plans and got left at 22 when its roster twin was fixed (2.5.8). */
.plan .rstep .chk,.rchk{padding:1px;outline-offset:1px;box-sizing:content-box;border-width:1px}
.rosrow .rchk,.rosband .rchk,.rosselall .rchk{margin:1px}

/* ---------- roster: bulk selection ---------- */
.rosselall{display:flex;align-items:center;gap:12px;padding:9px 15px;background:var(--surface);
  border-bottom:1px solid var(--border);font-size:var(--fs-md);font-weight:700;color:var(--text);cursor:pointer}
.rosrow.selectable{cursor:pointer;grid-template-columns:28px minmax(140px,240px) minmax(200px,1fr) minmax(0,220px)}
.rosrow.selectable:hover{background:var(--raised)}
.rosrow.selectable.on{background:var(--accent-tint)}
.rosbandsel{display:flex;align-items:center;gap:10px;cursor:pointer;
  background:var(--surface);border-bottom:1px solid var(--border);padding-left:15px}
.rosbandsel:hover{background:var(--raised)}
.rosbandsel .rosband{flex:1;min-width:0;background:none;border-bottom:0;padding-left:0}
/* Same surface recipe as a toast — --overlay, a hairline, the same shadow — so
   the bottom of the viewport reads as one family. z-index under .toasts (300)
   and over the mobile tab bar (90). */
/* z-index BELOW .overlay (100), not above: at 250 the bar rendered on top of a
   modal's scrim and Remove fired through it, emptying the roster in one press
   that looked inert. Above the mobile tab bar (90) is all it needs. */
.bulkbar{position:fixed;left:0;right:0;bottom:0;z-index:95;min-height:56px;
  padding:var(--sp-2) var(--sp-4);background:var(--overlay);border-top:1px solid var(--border-strong);
  box-shadow:0 -10px 34px rgba(0,0,0,.45)}
/* full-bleed surface, contents on the app's own column — nothing else in the
   app is full-bleed, and a count 115px left of every other word read as chrome */
.bulkbar .barin{max-width:1200px;margin:0 auto;padding:0 20px;
  display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap}
.bulkbar .cnt{font-size:var(--fs-md);color:var(--text-2);font-weight:600;min-width:0;flex:1}

/* not .alink.primary: a solid --accent fill on a destructive default invites the
   mis-press this whole mode exists to make safe. The view's primary stays +Add. */
/* unscoped: the pal card's ✕ Remove is the same destructive tier, and was
   drawn as a peer of Edit and Duplicate (§4 button tiers) */
.alink.danger{color:var(--danger);border-color:var(--danger-line)}
.alink.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}
/* the last row must be able to scroll clear of the bar (2.4.11) */
body.selecting #rosterList{padding-bottom:var(--bulkbar-h,72px)}
body.selecting .toasts{bottom:calc(var(--bulkbar-h,72px) + 18px)}
footer{max-width:1200px;margin:0 auto;padding:0 20px 30px;color:var(--muted);font-size:var(--fs-sm);text-align:center}
footer code{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-sm);padding:0 5px;font-family:Consolas,monospace}
@media(pointer:coarse){.kbdonly{display:none}}

/* ===================== interactive map =====================
   A tile pyramid (assets/map/<layer>/<z>/<x>_<y>.webp) drawn into a fixed
   8192x8192 "map pixel" stage that gets one transform. Everything inside —
   tiles, markers, the link line — is positioned in map pixels, so nothing has
   to be repositioned when the view moves; only the stage's transform changes.
   Markers counter-scale off --iz (= 1/scale, written once per frame on the
   stage) so they stay a constant size on screen without 255 style writes. */
.mapbar{margin-bottom:12px}
.mapbar .mfilters{flex-wrap:wrap}
.mapbar .mlabels{align-items:center;gap:4px;background:var(--surface);border:1px solid var(--border);
  border-radius:var(--r-lg);padding:4px 4px 4px 12px}
.mapbar .mlabels .mlb-cap{font-size:var(--fs-sm);color:var(--muted);font-weight:600;padding-right:5px;white-space:nowrap}
.mapbar .mlabels button{flex:0 0 auto;padding:6px 11px;font-size:var(--fs-sm);background:none;border-color:transparent}
.mapbar .mlabels button.on{background:var(--accent-tint);border-color:var(--accent)}
.mapbar .mfilters button{flex:0 1 auto;gap:7px;padding:9px 13px}
.mdot{width:16px;height:16px;flex-shrink:0;background:center/contain no-repeat}
.mdot.alpha{background-image:url(../assets/ui/map/alpha.webp)}
.mdot.ft{background-image:url(../assets/ui/map/waypoint.webp)}
.mdot.tower{background-image:url(../assets/ui/map/tower.webp)}
.mdot.region{background:none;border:1px dashed var(--muted);border-radius:3px;width:14px;height:11px}
.mapresults{display:flex;gap:8px;flex-wrap:wrap;margin:-4px 0 12px}
.mapresults[hidden]{display:none}
.mapresults .mres{display:inline-flex;align-items:center;gap:7px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-full);padding:4px 12px 4px 5px;font-size:var(--fs-sm);font-weight:600;color:var(--text-2)}
.mapresults .mres:hover{border-color:var(--accent);color:var(--text)}
.mapresults .mres img{width:22px;height:22px;border-radius:50%;object-fit:cover;background:var(--raised)}
.mapresults .mres .rt{width:22px;height:22px;border-radius:50%;background:var(--raised) center/13px no-repeat}
.mapresults .mres .rt.ft{background-image:url(../assets/ui/map/waypoint.webp)}
.mapresults .mres .rt.tw{background-image:url(../assets/ui/map/tower.webp);background-size:16px}
.mapresults .mres.sp{border-color:var(--accent-line)}
.mapresults .mres.sp:hover{background:var(--accent-tint)}
.mapresults .more{color:var(--muted);font-size:var(--fs-sm);align-self:center}

/* the persistent "you are looking at X's range" bar above the viewport */
/* The legend is 182px of nowrap and .sb-clear can't shrink, so on a phone the
   row had nothing left to give and took it out of .sb-txt (min-width:0) — the
   pal's name and level range were squeezed to 0px at 320 and 15px at 360, and
   the Clear button still hung 2px off the right edge of a 320px viewport. Let
   the row wrap instead: Clear (with the legend) drops to a second line and the
   name gets its width back. Inert at any width the row already fits. */
.spawnbar{display:flex;flex-wrap:wrap;align-items:center;gap:11px;margin:0 0 12px;padding:8px 12px 8px 8px;
  background:var(--accent-tint);border:1px solid var(--accent-line);border-radius:var(--r-lg)}
.spawnbar[hidden]{display:none}
.spawnbar .pico{flex-shrink:0}
.spawnbar .sb-txt{display:flex;flex-direction:column;line-height:1.3;min-width:0}
.spawnbar .sb-txt b{font-size:var(--fs-md)}
.spawnbar .sb-txt span{color:var(--muted);font-size:var(--fs-sm)}
.spawnbar .sbadge{font-size:var(--fs-xs);font-weight:700;padding:3px 9px;border-radius:var(--r-full);
  background:var(--raised);border:1px solid var(--border-strong);color:var(--text-2);white-space:nowrap;
  display:inline-flex;align-items:center;gap:4px}
.spawnbar .sb-legend{margin-left:auto;display:flex;align-items:center;gap:8px;
  font-size:var(--fs-xs);color:var(--muted);white-space:nowrap}
/* the swatches carry the same alpha the map does, so the key matches the
   thing it's a key for */
.spawnbar .sb-ramp{display:flex;height:11px;border-radius:3px;overflow:hidden;
  border:1px solid var(--border-strong);background:var(--bg)}
.spawnbar .sb-ramp i{width:15px;height:100%}
.spawnbar .sb-clear{flex-shrink:0;padding:6px 12px;font-size:var(--fs-sm)}
.mapinfo .inote{margin-top:9px;line-height:1.45}

/* 66vh, not 72: the view's h2 costs 43px above 640 (below it the height is
   computed and the heading is free), and that pushed .mapinfo's action row —
   Pal card and Copy link, its only two — entirely below the fold at 1366. */
.mapview{position:relative;overflow:hidden;height:min(66vh,720px);min-height:420px;
  background:#0a1b2a;border:1px solid var(--border);border-radius:var(--r-xl);
  touch-action:none;cursor:grab;user-select:none;-webkit-user-select:none;contain:layout paint}
.mapview.drag{cursor:grabbing}
.mapstage{position:absolute;top:0;left:0;width:8192px;height:8192px;transform-origin:0 0}
/* Spawn areas are a canvas, not SVG: a common species has thousands of
   overlapping circles. Its position and size are set in map pixels by the
   drawing code, which sizes the backing store to the resolution it's actually
   displayed at — a fixed canvas spanning the whole 8192px map was a
   quarter-scale texture being stretched 4x at maximum zoom. */
.mapzones{position:absolute;pointer-events:none}
.mapzones[hidden]{display:none}
.maptiles{position:absolute;inset:0}
.maptiles img{position:absolute;display:block;pointer-events:none;-webkit-user-drag:none}
/* the z0 tile stays underneath every detail level, so switching level never
   flashes the empty background while the new tiles decode */
.maptiles img.base{left:0;top:0;width:8192px;height:8192px}
.maplink{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}
.maplink line{stroke:var(--accent);stroke-linecap:round;stroke-dasharray:14 18;opacity:.9}
.maplink.off{display:none}
.mapmarks{position:absolute;inset:0}
/* Region names sit under the markers and never take a click. They're bucketed
   by the real size of the game's region volume, so a zoomed-out map shows the
   handful of big biomes and the small landmarks arrive as you go in. */
.mapregions{position:absolute;inset:0;pointer-events:none}
.mapregions[hidden]{display:none}
.rg{position:absolute;transform:translate(-50%,-50%) scale(var(--iz,1));transform-origin:50% 50%;
  white-space:nowrap;font-size:12px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:rgba(230,237,243,.72);text-shadow:0 0 4px #000,0 0 9px #000,0 1px 2px #000;display:none}
.mapstage.r0 .rg.t0,
.mapstage.r1 .rg.t0,.mapstage.r1 .rg.t1,
.mapstage.r2 .rg.t0,.mapstage.r2 .rg.t1,.mapstage.r2 .rg.t2,
.mapstage.r3 .rg{display:block}
.mapstage.r2 .rg.t0,.mapstage.r3 .rg.t0{font-size:13px;color:rgba(230,237,243,.55)}
.mapregions .rg.nolb{display:none}

/* --fx/--fy fan apart the two spawners that hold a pair of alphas (see
   MARK_FAN). They come after scale(--iz), which undoes the stage's zoom, so
   the offset is a fixed number of screen pixels however far you are zoomed in
   — the same trick that keeps the icon itself one size. Default 0: every other
   marker is untouched. */
.mk{position:absolute;padding:0;background:none;border:none;line-height:0;
  transform:translate(-50%,-50%) scale(var(--iz,1)) translate(var(--fx,0px),var(--fy,0px));transform-origin:50% 50%;
  transition:none}
.mk:not(:disabled):active{transform:translate(-50%,-50%) scale(var(--iz,1)) translate(var(--fx,0px),var(--fy,0px))}
.mk .g{display:block;box-sizing:border-box}
.mk .lb{position:absolute;left:50%;top:100%;transform:translateX(-50%);margin-top:3px;
  font-size:10.5px;font-weight:700;line-height:1.25;white-space:nowrap;color:var(--text);
  text-shadow:0 0 3px #000,0 0 6px #000,0 1px 2px #000;pointer-events:none;opacity:0;transition:opacity var(--t-fast)}
/* the three alternative anchors the placer tries before giving up on a label */
.mk.lb-t .lb{top:auto;bottom:100%;margin:0 0 3px}
/* alphas wear their level badge above the icon, so a label anchored up there
   has to clear it — matches MK_TOP in the placer */
.mk-alpha.lb-t .lb{margin-bottom:16px}
.mk.lb-r .lb{left:100%;top:50%;transform:translateY(-50%);margin:0 0 0 5px}
.mk.lb-l .lb{left:auto;right:100%;top:50%;transform:translateY(-50%);margin:0 5px 0 0}
.lab .mk .lb,.mk.sel .lb{opacity:1}
/* .nolb is "the placer couldn't fit this one" — hover still reveals it, which
   is what makes Off a usable mode rather than a blindfold. The hover rules have
   to out-specify the .lab zoom tier above, or a suppressed label stays hidden
   under the cursor exactly when you're zoomed in enough to want it. */
.lab .mk.nolb .lb,.mk.nolb .lb{opacity:0}
.mk:hover .lb,.lab .mk.nolb:hover .lb,.mk.nolb:hover .lb{opacity:1}
.mk:focus-visible{outline:none}
.mk:focus-visible .g{outline:2px solid var(--accent);outline-offset:2px}

/* Waypoints and towers use the game's own compass icons, so a marker here reads
   the same as the one on your screen in-game. They're artwork on transparent,
   so legibility over a busy map comes from a drop-shadow rather than a border. */
.mk-fastTravel,.mk-tower,.mk-middleBoss{z-index:1}
.mk-fastTravel .g,.mk-tower .g,.mk-middleBoss .g{background:center/contain no-repeat;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.9))}
.mk-fastTravel .g{width:19px;height:19px;background-image:url(../assets/ui/map/waypoint.webp)}
.mk-tower,.mk-middleBoss{z-index:3}
.mk-tower .g{width:26px;height:26px;background-image:url(../assets/ui/map/tower.webp)}
.mk-middleBoss .g{width:21px;height:21px;background-image:url(../assets/ui/map/alpha.webp)}
.mk-tower .lb,.mk-middleBoss .lb{color:var(--gold);opacity:1}
.mk-tower.nolb .lb,.mk-middleBoss.nolb .lb{opacity:0}
.mk-tower:hover .lb,.mk-middleBoss:hover .lb{opacity:1}
.mk-fastTravel:hover .g,.mk-fastTravel.sel .g,
.mk-tower:hover .g,.mk-tower.sel .g,
.mk-middleBoss:hover .g,.mk-middleBoss.sel .g{filter:drop-shadow(0 0 5px var(--accent)) brightness(1.2)}

/* alphas — the pal's own icon, which is the only label that scans at a glance */
.mk-alpha{z-index:2}
.mk-alpha .g{width:26px;height:26px;border-radius:50%;overflow:hidden;background:var(--raised);
  border:2px solid var(--danger);box-shadow:0 1px 4px rgba(0,0,0,.7)}
.mk-alpha img{width:100%;height:100%;display:block;object-fit:cover;pointer-events:none}
.mk-alpha .fb{width:100%;height:100%;display:grid;place-items:center;font-size:12px;font-weight:800;color:var(--text)}
.mk-alpha:hover .g,.mk-alpha.sel .g{border-color:var(--text);box-shadow:0 0 0 3px var(--danger-line)}
.mk-alpha .lvl{position:absolute;left:50%;top:-4px;transform:translate(-50%,-100%);
  background:var(--danger);color:var(--ink);font-size:9px;font-weight:800;line-height:1;
  padding:2px 4px;border-radius:var(--r-full);pointer-events:none;opacity:0;transition:opacity var(--t-fast)}
.lab .mk .lvl,.mk-alpha:hover .lvl,.mk-alpha.sel .lvl{opacity:1}
/* Search dims the misses rather than hiding them, so you keep the sense of
   where everything else is. Their labels go, though: 16%-opacity text is a
   real contrast failure, and it's noise the search was meant to cut. */
.mk.dim{opacity:.16;pointer-events:none}
.mk.dim .lb,.mk.dim .lvl{display:none}

/* nearest-waypoint highlight, driven from the info panel */
.mk.near .g{filter:drop-shadow(0 0 4px var(--accent)) drop-shadow(0 0 9px var(--accent)) brightness(1.25)}
.mk-alpha.near .g{filter:none;box-shadow:0 0 0 3px var(--accent),0 0 12px var(--accent)}

.mapzoom{position:absolute;top:12px;right:12px;z-index:6;display:flex;flex-direction:column;gap:6px}
.mapzoom button{width:36px;height:36px;display:grid;place-items:center;font-size:19px;font-weight:700;
  background:rgba(22,27,34,.92);border:1px solid var(--border-strong);border-radius:var(--r-md);color:var(--text-2);backdrop-filter:blur(6px)}
.mapzoom button:hover{color:var(--text);border-color:var(--accent)}
/* The .mapzoom recipe beside it, not --muted on a .78 tint: composited over
   bright terrain that measured 3.76-4.07:1 at 11px, under AA. This pairing
   measures 8.5-10.7 across the same tiles. axe cannot see either number —
   it returns incomplete when the backdrop is an image. */
.maphelp{position:absolute;left:12px;top:12px;z-index:5;pointer-events:none;
  background:rgba(22,27,34,.92);border:1px solid var(--border);border-radius:var(--r-full);
  padding:5px 13px;font-size:var(--fs-xs);color:var(--text-2);backdrop-filter:blur(6px);
  transition:opacity var(--t-base)}
.maphelp.gone{opacity:0}

.mapinfo{position:absolute;left:12px;bottom:12px;z-index:6;width:290px;max-width:calc(100% - 24px);
  max-height:calc(100% - 90px);overflow-y:auto;overscroll-behavior:contain;
  background:rgba(22,27,34,.96);border:1px solid var(--border-strong);border-radius:var(--r-lg);
  padding:14px 16px 15px;box-shadow:0 12px 40px rgba(0,0,0,.55);backdrop-filter:blur(8px);
  font-size:var(--fs-md);touch-action:pan-y}
.mapinfo[hidden]{display:none}
.mapinfo .ihead{display:flex;align-items:flex-start;gap:10px;margin-bottom:2px}
.mapinfo .ihead img{width:44px;height:44px;border-radius:var(--r-md);flex-shrink:0;background:var(--raised);cursor:pointer}
.mapinfo h3{font-size:var(--fs-lg);line-height:1.25;font-weight:700}
.mapinfo .isub{color:var(--muted);font-size:var(--fs-sm)}
.mapinfo .iclose{position:absolute;top:6px;right:8px;font-size:15px;color:var(--muted);padding:4px 7px;border-radius:var(--r-sm);line-height:1}
.mapinfo .iclose:hover{color:var(--text);background:var(--raised)}
.mapinfo .crow{display:flex;gap:6px;flex-wrap:wrap;margin:9px 0 4px}
.mapinfo .nlb{margin-top:12px;font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:700}
.mapinfo .near{display:flex;flex-direction:column;gap:4px;margin-top:6px}
.mapinfo .near button{display:flex;align-items:center;gap:8px;width:100%;text-align:left;
  background:var(--bg);border:1px solid var(--border);border-radius:var(--r-md);padding:7px 10px;font-size:var(--fs-sm);color:var(--text-2)}
.mapinfo .near button:hover{border-color:var(--accent);color:var(--text)}
.mapinfo .near .d{margin-left:auto;color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}
.mapinfo .iacts{display:flex;gap:8px;margin-top:12px}
.mapinfo .iacts .alink{flex:1;text-align:center;padding:7px 8px;font-size:var(--fs-sm)}
.mapfoot{margin:12px 0 0}

/* ===================== skills catalog =====================
   Two indexes and the base-aura group. Everything here is a card grid, so the
   shared shape lives in .auracard/.skillcard/.pvcard and the differences are
   only in what each card leads with. */
.psfam{flex-wrap:wrap;margin:-6px 0 18px}
.psfam button{flex:1 1 auto;min-width:88px}
/* a section heading inside a view — smaller than the page h1, with its count
   trailing it rather than sitting in a separate row */
.aurasec,.pvsec{margin-bottom:26px}
/* --fs-lg, not --fs-h2: the view's own h2 is 19px, so a group heading rendered
   at the same size made "Skills" the smallest heading on screen. §3's ladder
   wants the step down to be visible, not only announced. */
.aurasec h3,.pvsec h3{font-size:var(--fs-lg);font-weight:700;margin-bottom:6px;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.aurasec h3 .rstats,.pvsec h3 .rstats{font-weight:400}
.aurasec .viewsub{max-width:78ch}
.pvsec.bad h3{color:var(--danger)}
.auragrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(280px,100%),1fr));gap:10px}
.auracard,.skillcard,.pvcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:13px 15px;display:flex;flex-direction:column;gap:8px;min-width:0}
.auracard:hover,.skillcard:hover,.pvcard:hover{border-color:var(--border-strong)}
.ahead,.shead,.pvhead{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
.atitle{font-weight:700;font-size:var(--fs-lg);color:var(--text)}
/* the work auras are the headline fact of the whole tab — the icon leads */
.auracard .ahead .uii{filter:none}
.askill{color:var(--gold);font-weight:600;font-size:var(--fs-md)}
.aclause,.skillcard .psd{color:var(--text-2);font-size:var(--fs-md);margin:0}
.rankstrip{display:flex;flex-direction:column;gap:3px;font-size:var(--fs-sm);color:var(--muted);
  border-top:1px solid var(--border);padding-top:7px;margin-top:auto}
.rankstrip .rscap{font-size:var(--fs-xs);color:var(--muted);letter-spacing:.4px;text-transform:uppercase}
.rankstrip .rk{display:flex;gap:8px;justify-content:space-between;align-items:baseline}
.rankstrip .rk b{color:var(--text-2);font-weight:600}
.rankstrip .rk span{white-space:nowrap;font-variant-numeric:tabular-nums}
/* pal name + icon as one control, wherever a card points back at a pal card */
.palref{display:inline-flex;align-items:center;gap:8px;font-weight:600;color:var(--text);
  border:1px solid transparent;border-radius:var(--r-full);padding:2px 10px 2px 2px;max-width:100%}
.palref>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.palref:hover{border-color:var(--border-strong);background:var(--raised)}
.badge.nostack{background:var(--raised);color:var(--muted);border:1px solid var(--border);text-transform:none;letter-spacing:0}
.badge.aura{background:var(--success-tint);color:var(--success);border:1px solid var(--success-line)}
.badge.mut{background:var(--accent-tint);color:var(--accent);border:1px solid var(--accent-line);text-transform:none;letter-spacing:0;
  display:inline-flex;align-items:center;gap:4px}
.badge.have{background:var(--gold-tint);color:var(--gold);border:1px solid var(--gold-line);text-transform:none;letter-spacing:0}
button.badge.aura:hover,button.badge.have:hover{border-color:currentColor}
/* an effect tag is a filter you can click from any pal card */
.mchip.tagbtn:hover{color:var(--text);border-color:var(--accent)}
.skillgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(330px,100%),1fr));gap:10px;align-items:start}
.skillcard .psn{font-weight:700;color:var(--gold);font-size:var(--fs-base)}
.skillcard .pst{display:flex;gap:6px;flex-wrap:wrap}
.flatnote{color:var(--muted);font-size:var(--fs-sm)}
.pskill .flatnote{margin-top:var(--sp-2)}
/* a number the game's own description ships without — see psDesc */
.phold{display:inline-block;min-width:1.1em;text-align:center;color:var(--muted);
  background:var(--raised);border:1px dashed var(--border-strong);border-radius:var(--r-sm);
  font-size:var(--fs-sm);line-height:1.3;cursor:help}
.rankdet{border-top:1px solid var(--border);padding-top:6px;margin-top:auto}
.rankdet summary{color:var(--accent);font-size:var(--fs-sm);font-weight:600;cursor:pointer;list-style:none;
  display:inline-flex;align-items:center;gap:6px;border-radius:var(--r-sm);padding:3px 2px;margin:-3px -2px;min-height:24px}
.rankdet summary::-webkit-details-marker{display:none}
.rankdet summary::before{content:'▸';color:var(--muted);transition:transform var(--t-fast)}
.rankdet[open] summary::before{transform:rotate(90deg)}
/* the pan moves to a focusable .rankscroll (js/skills.js, js/core.js) — on the
   <table> itself it was a scrollport with no focusable cell, so axe flagged
   scrollable-region-focusable and Rank 4-5 were pointer-only. Unscoped: the pal
   card shows the same table in a 294px box at 360 and needs the same wrapper. */
.rankscroll{margin-top:6px;overflow-x:auto}
.rankscroll .ranktbl{margin-top:0}
.pvgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(270px,100%),1fr));gap:10px}
.pvcard{gap:6px}
.pvn{font-weight:700;font-size:var(--fs-base);flex:1;min-width:0}
.pvrank{margin-left:auto;letter-spacing:1px;color:var(--gold);border-color:var(--gold-line)}
.pvcard.s-bad .pvrank{color:var(--danger);border-color:var(--danger-line)}
.pvcard.s-bad{border-color:var(--danger-line)}
.pvfx{list-style:none;display:flex;flex-direction:column;gap:2px;font-size:var(--fs-md);color:var(--text-2)}
.pvfx .up::before{content:'▲';color:var(--success);font-size:9px;margin-right:6px;vertical-align:1px}
.pvfx .dn::before{content:'▼';color:var(--danger);font-size:9px;margin-right:6px;vertical-align:1px}
.pvfx li:not(.up):not(.dn)::before{content:'•';color:var(--muted);margin-right:6px}
.pvfoot{display:flex;gap:6px;flex-wrap:wrap;margin-top:2px}


/* ===================== read a save file =====================
   Sits beside Export/Import data but must not read as the same thing: one
   restores a Palarium backup, this one reads a game file. Hence its own icon
   and wording, and a preview before anything is written. */
/* An .alink whose visible text sits beside a leading icon. inline-flex because
   a glyph sits on the text baseline where a stroke icon does not (§7). Was
   .savebtn, which named one caller rather than the recipe. */
.icolink{display:inline-flex;align-items:center;gap:6px}
.rentacts .icolink{justify-content:center}
.smodal{width:min(680px,100%)}
/* the promise the whole feature rests on, said where the file is picked */
.privacy{background:var(--raised);border:1px solid var(--border);border-left:3px solid var(--accent);
  border-radius:0 10px 10px 0;padding:12px 15px;font-size:var(--fs-sm);line-height:1.55;
  color:var(--text-2);margin:-4px 0 18px}
.privacy b{color:var(--text)}
.alink.big{padding:12px 20px;font-size:var(--fs-md)}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:var(--fs-xs);
  background:var(--raised);border:1px solid var(--border);border-radius:var(--r-md);
  padding:8px 10px;overflow-x:auto;white-space:nowrap;color:var(--text-2)}
.pbar{height:8px;background:var(--raised);border:1px solid var(--border);border-radius:999px;
  overflow:hidden;margin:12px 0 14px}
.pbarfill{height:100%;width:4%;background:var(--accent);border-radius:999px;transition:width .18s linear}
.smsum{font-size:var(--fs-md);color:var(--text-2);line-height:1.55;margin:-4px 0 14px}
.smfilter{margin-bottom:14px}
.smh3{font-size:var(--fs-md);margin:18px 0 6px}
.smconf{display:flex;flex-direction:column;gap:12px;margin-top:10px}
.confrow{border:1px solid var(--border);border-radius:var(--r-lg);padding:12px 14px;background:var(--raised)}
.confmine,.confsave{font-size:var(--fs-sm);color:var(--text-2);line-height:1.5;margin-bottom:4px;overflow-wrap:anywhere}
.confsave.amb{color:var(--muted);font-style:italic}
.conflab{display:block;font-size:var(--fs-xs);color:var(--muted);margin:8px 0 4px}
.confseg,#smMode{margin-top:8px}
/* The row is already --raised, so its buttons had a 1.00:1 fill delta against
   it and only --border at 1.24:1 to say where they were — the two unchosen
   options read as empty space with a hairline (§1: a boundary below 3:1 must
   never be the sole indicator). One more rung up the ladder. */
.confrow .segrow button{background:var(--overlay)}
/* the select carried the row's answer truncated to "Keep them separate — lea⌄"
   beside three full-width segmented buttons */
#smConflicts select{width:100%}
/* the consequence of the selected option, in visible text — touch has no hover */
.confeff{font-size:var(--fs-sm);color:var(--muted);margin:6px 0 0;line-height:1.45}
/* the way out to the other import path — quiet, but always in the same place
   at the foot of both entry screens */
.smxlink{margin-top:22px;padding-top:14px;border-top:1px solid var(--border)}
.smxlink .alink{margin-left:6px}
.smprev{margin-top:4px}
.smlist{display:flex;flex-direction:column;gap:4px;max-height:260px;overflow-y:auto;
  border:1px solid var(--border);border-radius:var(--r-lg);padding:8px 10px;background:var(--raised)}
.smitem{display:flex;align-items:center;gap:8px;font-size:var(--fs-sm);color:var(--text-2);
  overflow-wrap:anywhere;line-height:1.4}
.smacts{display:flex;gap:10px;margin-top:20px}
/* .smerr was .warnbox one type step smaller and without its triangle — the
   error is now a .warnbox like every other warning in the app (§4 reuse). */
/* the level and the in-game name, both from the save. The in-game name is
   muted and separate because it is not the nickname you typed. */
.lvchip{font-size:var(--fs-xs);color:var(--muted);background:var(--surface);border:1px solid var(--border);
  border-radius:var(--r-sm);padding:1px 7px;margin-left:6px;white-space:nowrap;display:inline-block}
.gname{color:var(--muted);font-style:italic}

/* the two ways in, and the world list a folder produces */
.smchoose{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:8px}
.smchoose[hidden],.segrow[hidden]{display:none} /* a display: of its own outranks the UA's [hidden] */
.pathrow{display:flex;gap:8px;align-items:center}
.pathrow .mono{flex:1;min-width:0}
.worldlist{display:flex;flex-direction:column;gap:8px;margin:4px 0 4px}
.worldbtn{display:block;width:100%;text-align:left;background:var(--raised);border:1px solid var(--border);
  border-radius:var(--r-lg);padding:12px 15px;cursor:pointer;color:var(--text);transition:border-color var(--t-fast),background var(--t-fast)}
/* --overlay, not --surface: the row rests on --raised, so hovering to --surface
   stepped the fill DOWN the ladder and the row's own fill delta was 1.00 (§1) */
.worldbtn:hover{border-color:var(--accent);background:var(--overlay)}
/* flex, not block, so a leading icon sits beside the title */
.worldbtn .wname{display:flex;align-items:center;gap:8px;font-weight:600;font-size:var(--fs-md);margin-bottom:2px}
.worldbtn .wname svg{flex:none;width:18px;height:18px;color:var(--muted)}
.worldbtn .wsub{display:block;color:var(--text-2);font-size:var(--fs-sm);line-height:1.45}
/* a second caption is a caveat, not the value — one rank quieter than the first */
.worldbtn .wsub + .wsub{color:var(--muted);margin-top:2px}
.worldbtn .wpath{display:block;color:var(--muted);font-size:var(--fs-xs);margin-top:3px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* the disclosure is the same width, radius and border as a choice row — without
   this it sits closer to the stack than the rows sit to each other, and reads
   as a third route */
.worldlist + .moredet{margin-top:var(--sp-3)}

/* ===================== phones (≤640px) =====================
   Desktop lays these views out as wrap-when-needed flex rows. At phone width
   almost every one of them wraps into a ragged stack of differently-sized
   controls, and a few squeeze a text column down to a couple of characters.
   The rules below give each row one column of equal, full-bleed widths.
   Kept last in the file so they win the equal-specificity ties above. */
@media(max-width:640px){
  /* --- clear the fixed nav bar. These two belong with the bottom-nav rules
         near the top of the file, but their base declarations come later and
         would win the equal-specificity tie, leaving the footer's last line and
         every toast underneath the nav. --- */
  footer{padding-bottom:calc(88px + env(safe-area-inset-bottom))}
  .toasts{bottom:calc(76px + env(safe-area-inset-bottom))}
  /* clear of the More sheet while it is open — see syncSheetHeight in
     js/router.js. The sheet precedes #toasts in the markup, so this needs
     no JS beyond publishing the height. */
  .moresheet.open ~ .toasts{bottom:calc(76px + var(--sheet-h, 0px) + 12px + env(safe-area-inset-bottom))}
  /* sequential focus navigation honours scroll-padding, so tabbing to a control
     near the fold scrolls it clear of the fixed nav instead of parking it
     underneath (WCAG 2.4.11 Focus Not Obscured) */
  html{scroll-padding-bottom:calc(76px + env(safe-area-inset-bottom))}

  /* --- density: desktop gutters cost ~50px of a 390px screen --- */
  .pcard{padding:16px 14px}
  .gcard{padding:18px 16px}
  .gcard ol,.gcard ul{margin:10px 0 10px 18px}
  .modal{padding:0 var(--sp-4) var(--sp-5)}
  /* the bar bleeds out to the padding box, so it moves with the padding — or it
     overhangs the dialog's rounded border and gives the dialog its own 8px of
     horizontal scroll, which the suite's overflow() cannot see (it measures the
     document, and the modal is what scrolls) */
  .mbar{margin:0 calc(var(--sp-4) * -1) var(--sp-3);padding:var(--sp-2) var(--sp-3)}
  .setup{position:relative;padding:10px 42px 10px 14px}
  .setup .dismiss{position:absolute;top:5px;right:4px;margin:0}

  /* --- filter/search rows: one full-width control per line --- */
  .dex-controls{gap:10px}
  .dex-controls>.ctl,
  .dex-controls>.search-inp,.dex-controls>select,.dex-controls>.isel,
  .dex-controls>.toggle,.dex-controls>.segrow{flex:1 1 100%;width:100%;min-width:0;max-width:none}
  /* Five stacked full-width controls put the first tile 514px down a 740px
     screen. Two up, with search across the top, halves that. */
  .dexcontrols{display:grid;grid-template-columns:1fr 1fr;gap:10px;align-items:end}
  .dexcontrols>.ctl{width:auto}
  /* Show has three words in it and cannot shrink into a half-width column.
     Kept off :has() deliberately — one unsupported selector in a list drops
     the whole rule, and that would take the search span down with it. */
  .dexcontrols>.ctl:first-child,.dexcontrols>.ctlwide,
  .dexcontrols>.rstats,.dexcontrols>.alink{grid-column:1/-1}
  /* The roster's four controls stacked to 275px, putting the first tile at
     y=632 on a 360 screen — the same problem, and §9 already settles the shape.
     Search spans; Passive, Sort and Compact rows pair up underneath. */
  .roscontrols{display:grid;grid-template-columns:1fr 1fr;gap:10px;align-items:end}
  .roscontrols>.ctl{width:auto;min-width:0}
  .roscontrols>.ctl:first-child{grid-column:1/-1;max-width:none}
  .dexcontrols #dexShow button{padding:9px 4px;min-width:0}
  /* Three columns: star, pal, and whatever the sort is. Anything wider makes
     .tablewrap pan, and a panning wrap breaks the sticky header. */
  th[data-s="z"],td.tnum,th[data-s="r"],td.tpow,th[data-s="w"],td.tworks{display:none}
  th.thmetric,td.tmetric{display:table-cell}
  .search-inp{width:100%}
  .dex-controls .search-inp,.dex-controls select,.dex-controls .isel{max-width:none}
  .ctl .picker,.ctl select,.ctl .search-inp,.ctl .toggle{width:100%;max-width:none;min-width:0}
  /* an &nbsp; label exists only to bottom-align the desktop row — on a stack
     it renders as a phantom gap above the toggle */

  .dex-controls .ctl label[aria-hidden="true"]{display:none}
  #hatchDepth button,#comboKind button{flex:1 1 0;padding:9px 6px}

  /* --- breed: equal full-width parent slots, swap between them.
         No order: overrides any more — the swap is between the pickers in the
         DOM now, so visual order and focus order agree for free. --- */
  .parents{grid-template-columns:minmax(0,1fr);gap:var(--sp-1);margin-bottom:var(--sp-3)}
  .swap{justify-self:center;margin:calc(var(--sp-1) * -1) 0}
  .collhead .resline{flex:1 0 100%}
  .result-zone{margin-top:var(--sp-4);gap:var(--sp-4)}
  /* stay a row and shrink the art: stacking it centred cost ~100px per card,
     which pushed the answer below the fold on a 640-tall phone */
  .child-card{flex-direction:row;text-align:left;padding:var(--sp-4);gap:var(--sp-3);align-items:flex-start}
  .child-card .crow,.child-card .gbar,.child-card .works{justify-content:flex-start}
  .child-card .pico{width:64px;height:64px;font-size:24px}
  .child-card h3{font-size:var(--fs-h2)}
  .gtrack{width:96px}

  /* --- find parents: the anchor takes its own line, the chips take the next --- */
  .pgroup{flex-wrap:wrap;padding:var(--sp-2) var(--sp-3)}
  /* the separator has no job once the chips are on their own line, and parked
     in the card's top-right corner it read as a close button */
  .pgroup .x{display:none}
  /* two controls per row, target species full width — the §9 .dex-controls
     pattern. Three stacked rows put the first answer at y=425 on a 360 phone. */
  /* same two-column shape .dexcontrols uses; the target picker spans both */
  .revcontrols{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-2)}
  .revcontrols .ctl:first-child{grid-column:1/-1}
  .pgroup .anchor{max-width:none;flex:1 1 auto;min-height:32px}
  .pgroup .chiprow{flex:1 1 100%}
  .pgroup .tchip{min-height:32px}
  /* still used by Breedable now's .hatchpanel .pair */
  .pair{flex-wrap:wrap}
  /* identical to .pgroup .x above: once the sides wrap to their own lines the
     separator parks in the card's top-right and reads as a close button on a
     row that is itself a press target. The sr-only "and" carries the meaning. */
  .pair .x{display:none}
  .pair .pside{flex:1 1 128px;min-width:128px}

  /* --- roster: the header buttons stop being pushed into the middle by
         margin-left:auto, and a row stacks into three lines --- */
  .collhead .collacts{margin-left:0;width:100%}
  /* identity + toolbar on line 1, passives on line 2, note on line 3 */
  /* flush on the tab bar. 76px is the .toasts offset, where a floating pill
     wants air; an opaque full-bleed bar with a border-top showed 19px of
     scrolling list between two fixed surfaces and read as a paint bug. */
  .bulkbar{bottom:var(--nav-h,58px)}
  body.selecting .toasts{bottom:calc(var(--nav-h,58px) + var(--bulkbar-h,64px) + 12px)}
  .rosrow.selectable{grid-template-columns:28px minmax(0,1fr);padding-left:15px}
  .rosrow.selectable .who{grid-area:1/2}
  .rosrow.selectable .pscol{grid-area:2/2/3/-1}
  .rosrow.selectable .note{grid-area:3/2/4/-1}
  .rosrow.selectable .rchk{grid-area:1/1}
  .rosrow{grid-template-columns:minmax(0,1fr) auto;gap:4px 8px;padding:10px 12px}
  /* Full grid-areas, not just grid-column: the <=1000px block above places
     .pscol at 1/2, and overriding only the column left it in row 1 painting
     over the name button — a focusable control entirely obscured (2.4.11). */
  .rosrow .who{grid-area:1/1}
  .rosrow .acts{grid-area:1/2;opacity:1}
  .rosrow .pscol{grid-area:2/1/3/-1}
  /* the note has the full width here, so it wraps rather than ellipsising into
     a title attribute touch users can never open (DESIGN.md §8) */
  .rosrow .note{grid-area:3/1/4/-1;white-space:normal}
  /* thumb-sized targets without changing how the buttons look */
  .rosrow .acts button{min-width:32px;min-height:32px}
  .rosband{min-height:44px}
  .roster.dense .rosrow{padding:7px 12px}

  /* --- add-pal dialog: three buttons in one row collapsed the primary to a
         73px column of single words --- */
  .formacts{flex-wrap:wrap}
  .formacts .primary{flex:1 1 100%}
  .formacts .alink:not(.primary){flex:1 1 0;text-align:center}

  /* --- planner --- */
  .slotgrid{grid-template-columns:1fr}
  .targetrow .slot,.targetrow .slot.opts{flex:1 1 100%;min-width:0;max-width:none}
  /* same treatment #hatchDepth and #comboKind get above: three equal columns
     that shrink their padding rather than ellipsizing the middle label */
  #partnerMode button{flex:1 1 0;padding:9px 4px}
  /* a catch-list chip carries a tier and a reason now, so at 320px it is a
     paragraph, not a pill — let it wrap inside its own border instead of
     pushing the row sideways */
  /* same reason, and the one .tchip property the quick-add strip cannot inherit:
     a nickname is free text with no length cap, and nowrap floored the chip at
     its min-content width, so a 38-character name pushed the page past 320 */
  .needrow .tchip,#rosterStrip .tchip{white-space:normal;text-align:left}
  .needrow .accr{white-space:normal}
  /* margin-left:auto on a wrapping row gave the ↗ button a whole empty line */
  .rstep{position:relative;--gutter:50px;padding-right:var(--gutter)}
  .rstep .stepopen{position:absolute;top:11px;right:12px;margin-left:0}
  /* the gutter only clears the ↗ on the first line; the panel underneath isn't
     beneath anything, so give it the width back rather than ellipsizing statue
     names 34px early. flex-basis as well as the margin — the base rule pins the
     basis at 100%, so a negative margin alone moves the box without widening
     it. Same custom property throughout so the two can't drift apart. */
  .rstep .wildinfo{flex-basis:calc(100% + var(--gutter) - 16px);margin-right:calc(16px - var(--gutter))}
  .saverow>.search-inp{flex:1 1 100%}
  .planhead .pushr{margin-left:0}

  /* --- pal card: aligned action grid instead of a centred staircase --- */
  /* the same grid .mbtns gets two sections below — it was a 3-line, 127px
     staircase against an aligned block */
  .rentacts{display:grid;grid-template-columns:1fr 1fr;margin-left:0;width:100%}
  .rentacts .alink{text-align:center;padding:var(--sp-2)}
  .mbtns{display:grid;grid-template-columns:1fr 1fr}
  .mbtns .alink{text-align:center;padding:var(--sp-2)}

  /* --- picker grid: a ★ made starred cells a whole row taller --- */
  .pop .row{position:relative}
  .pop .row .own{position:absolute;top:5px;right:6px}

  /* --- paldex: trim the row (rarity chip, icon, gutters, wrapping headers)
         from 589px down to ~364px, so it fits outright on most phones instead
         of needing 220px of sideways panning --- */
  th{white-space:normal}
  .tname{gap:6px}
  .tname .tier{display:none}
  .tname .pico{width:28px;height:28px;border-width:1px}
  td{padding:6px 4px}th{padding:7px 4px}

  /* --- map: the viewport is the content, so give it the screen. The info
         panel becomes a bottom sheet — at 290px wide it covered half a phone
         and its own markers. --- */
  .mapview{height:calc(100dvh - var(--hh) - 190px);min-height:340px;border-radius:var(--r-lg)}
  .mapbar .mfilters{display:flex}
  .mapbar .mfilters button{flex:1 1 0;padding:9px 6px;justify-content:center}
  .mapinfo{left:8px;right:8px;bottom:8px;width:auto;max-width:none;max-height:56%}
  /* right, so the pill stops short of the zoom column instead of running under
     it — the keyboard half of this line was the part being clipped. And
     --fs-xs (11) rather than 10px, which is below §3's floor. */
  .maphelp{font-size:var(--fs-xs);padding:4px 10px;right:56px}
  .mapzoom button{width:32px;height:32px}
  .spawnbar{gap:9px;padding:7px 10px 7px 7px}
  .spawnbar .sb-clear{padding:6px 9px}
  .spawnbar .sbadge{display:none}
  .rg{font-size:10.5px;letter-spacing:.06em}
  /* 83 alphas over a 364px-wide island is dense; give the icons back a little
     of the gap rather than making the user zoom before anything is readable */
  .mk-alpha .g{width:22px;height:22px}
  /* 14, not 10: the same pin is 19px for a mouse and was 28% of that area for a
     finger. Map pins take §8's Essential exception on size — position IS the
     information — but nothing about that requires them to shrink on the device
     with the coarser pointer. */
  .mk-fastTravel .g{width:14px;height:14px}
  .mk-tower .g,.mk-middleBoss .g{width:18px;height:18px;font-size:11px}

  /* --- read-a-save dialog: the segmented choice per collision and the
         action row both need one full-width column here --- */
  .confseg,#smMode{flex-direction:column}
  .confseg button,#smMode button{width:100%}
  .smacts{flex-direction:column}
  .smacts .alink{width:100%;justify-content:center}
  .smlist{max-height:200px}
  .mono{font-size:var(--fs-xs)}   /* was 10px — below the scale, on the one string you have to read character by character */

  /* --- the two entry buttons and each world row want a full column --- */
  .smchoose{flex-direction:column}
  .smchoose .alink{width:100%;justify-content:center}
  .pathrow{flex-direction:column;align-items:stretch}
}
