/* =============================================================================
   Sudoku · generator — visual system
   Single-theme dark "observatorium". One set of tokens drives colour, type,
   spacing, radius, shadow and motion; everything below composes from them.
   ========================================================================== */
:root{
  /* surfaces & ink */
  --ink:#0b0d0c; --panel:#121614; --panel-2:#171d1a; --panel-3:#1c2320;
  --rule:#232b28; --rule-2:#2c3733;
  --dim:#4f5b57; --base:#8a9d94; --white:#dbe7e0; --link:#eef5f1;
  /* functional phase colours (do not repurpose) */
  --grid:#3a8f6c; --givens:#46a37a; --solve:#6cb6ff; --check:#e6c46b;
  --rej:#d9695f; --accent:#ff7a45; --win:#51d88a;
  /* type */
  --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
  --sans:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  /* radius / shadow / motion */
  --r-xs:6px; --r-sm:8px; --r:12px; --r-lg:16px; --r-pill:999px;
  --sh-1:0 1px 0 rgba(255,255,255,.02) inset, 0 8px 24px rgba(0,0,0,.28);
  --sh-2:0 18px 46px rgba(0,0,0,.42);
  --ease:cubic-bezier(.4,.14,.2,1);
  --ease-out:cubic-bezier(.18,.72,.24,1);
  --t:.16s;
  /* theme hooks — the four worlds below override these; :root is the
     default "observatorium" so an unset/unknown data-theme falls back here */
  --accent-rgb:255,122,69;   /* accent as an r,g,b triplet, for rgba() tints */
  --accent-ink:var(--ink);   /* text/icon that sits on an accent fill */
  --display:var(--sans);     /* heading / display face */
  --brand-font:var(--mono);  /* the wordmark face */
}
*{box-sizing:border-box}
html{color-scheme:dark}
::selection{background:rgba(var(--accent-rgb),.28);color:var(--white)}
body{
  margin:0;background:var(--ink);color:var(--white);font-family:var(--sans);
  line-height:1.5;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  background-image:
    linear-gradient(var(--rule) 1px,transparent 1px),
    linear-gradient(90deg,var(--rule) 1px,transparent 1px);
  background-size:56px 56px;background-position:-1px -1px;
}
/* faint plate that softens the hard grid into a lit room */
body::before{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;background:
  radial-gradient(120% 90% at 72% -12%,rgba(var(--accent-rgb),.07),transparent 55%),
  radial-gradient(90% 80% at 8% 112%,rgba(108,182,255,.05),transparent 55%),
  radial-gradient(140% 120% at 50% 50%,transparent 60%,rgba(6,8,7,.55) 100%)}

.wrap{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:30px 24px 72px}

/* --- header / brand ------------------------------------------------------- */
header{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;
  border-bottom:1px solid var(--rule);padding-bottom:18px;margin-bottom:28px}
.brand{font-family:var(--brand-font);font-size:19px;letter-spacing:.01em;color:var(--white);
  display:inline-flex;align-items:baseline;gap:.5ch;text-decoration:none}
a.brand:hover b{text-shadow:0 0 20px rgba(var(--accent-rgb),.55)}
a.brand:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:4px}
.brand b{color:var(--accent);font-weight:500;font-style:normal;
  text-shadow:0 0 16px rgba(var(--accent-rgb),.4)}
.brand .car{color:var(--dim)}
.tag{font-family:var(--mono);font-size:12px;color:var(--base);letter-spacing:.05em}
.nav{margin-left:auto;display:flex;gap:18px;align-items:baseline}
.nav a{font-family:var(--mono);font-size:11px;color:var(--base);text-decoration:none;
  letter-spacing:.04em;transition:color var(--t);position:relative;padding-bottom:2px}
.nav a::after{content:"";position:absolute;left:0;right:100%;bottom:-1px;height:1px;
  background:var(--accent);transition:right var(--t) var(--ease)}
.nav a:hover{color:var(--white)}
.nav a:hover::after{right:0}
.nav a.here{color:var(--accent)}
.nav a.here::after{right:0;background:var(--accent)}
.nav a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}
/* Identity pill: who you are signed in as. Was the hostname, which the
   address bar already showed. */
.whoami{font-family:var(--mono);font-size:11px;color:var(--base);
  border:1px solid var(--rule);border-radius:var(--r-pill);padding:5px 12px;
  display:inline-flex;align-items:center;gap:7px;background:rgba(23,29,26,.5)}
header .whoami{margin-left:14px}
.whoami i{width:6px;height:6px;border-radius:50%;background:var(--grid);font-style:normal;
  box-shadow:0 0 0 3px rgba(58,143,108,.16);animation:beacon 2.6s var(--ease) infinite}
@keyframes beacon{0%,100%{box-shadow:0 0 0 3px rgba(58,143,108,.16)}
  50%{box-shadow:0 0 0 5px rgba(58,143,108,.04)}}

/* --- theme switcher ------------------------------------------------------- */
/* Shared by the top bar (bare swatches) and the settings Appearance card
   (.labeled: swatch + name). Each pick's swatch is a split disc — the theme's
   ground on one half, its accent on the other — keyed by the data-t below.
   theme.js applies + saves the choice on click. */
.theme-switch{display:inline-flex;align-items:center;gap:7px}
header .theme-switch{margin-left:14px}
.theme-pick{-webkit-appearance:none;appearance:none;background:none;border:0;padding:2px;cursor:pointer;
  display:inline-flex;align-items:center;gap:8px;border-radius:var(--r-pill)}
.theme-pick .sw{width:18px;height:18px;border-radius:50%;flex:0 0 auto;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.35);transition:transform var(--t),box-shadow var(--t);
  background:linear-gradient(135deg,var(--sw-bg) 0 50%,var(--sw-ac) 50% 100%)}
.theme-pick[data-t="observatorium"]{--sw-bg:#121614;--sw-ac:#ff7a45}
.theme-pick[data-t="atelier"]{--sw-bg:#f7f3ea;--sw-ac:#20618d}
.theme-pick[data-t="sumi"]{--sw-bg:#efe8d8;--sw-ac:#c14631}
.theme-pick[data-t="nocturne"]{--sw-bg:#161235;--sw-ac:#37e6cf}
.theme-pick:hover .sw{transform:scale(1.08)}
.theme-pick:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* bare (top bar): active swatch gets an accent ring */
.theme-switch:not(.labeled) .theme-pick.on .sw{
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.35),0 0 0 2px var(--accent)}
/* labeled (settings): swatch + name in a selectable pill */
.theme-switch.labeled{display:flex;flex-wrap:wrap;gap:8px;margin-top:2px}
.theme-switch.labeled .theme-pick{border:1px solid var(--rule);border-radius:var(--r-sm);
  padding:8px 12px;background:var(--panel-2);transition:border-color var(--t),background var(--t)}
.theme-switch.labeled .theme-pick:hover{border-color:var(--rule-2);background:var(--panel-3)}
.theme-switch.labeled .theme-pick.on{border-color:var(--accent);background:rgba(var(--accent-rgb),.1)}
.theme-pick .lbl{font-family:var(--mono);font-size:12px;color:var(--base)}
.theme-pick.on .lbl{color:var(--white)}

/* --- language switch ------------------------------------------------------ */
/* Two segments in one pill, current one lifted — the same shape as the
   switcher on wip.systemsoft.nl, in this app's palette. The flag is drawn as
   inline SVG because emoji flags do not render on Windows. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}
.lang-switch{display:inline-flex;align-items:center;gap:2px;margin-left:14px;padding:2px;
  background:var(--panel-2);border:1px solid var(--rule);border-radius:var(--r-pill)}
.lang-opt{-webkit-appearance:none;appearance:none;background:none;border:0;cursor:pointer;
  display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:var(--r-pill);
  font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:var(--dim);
  transition:background var(--t),color var(--t),opacity var(--t)}
.lang-opt .flag{display:block;border-radius:2px;opacity:.55;transition:opacity var(--t)}
.lang-opt:hover{color:var(--white)}
.lang-opt:hover .flag{opacity:.9}
.lang-opt.is-current{background:var(--panel-3);color:var(--white)}
.lang-opt.is-current .flag{opacity:1}
.lang-opt:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* login page: no top bar to sit in, so it gets its own row above the form */
.login-lang{display:flex;justify-content:center;margin:0 0 22px}
.login-lang .lang-switch{margin-left:0}

/* labeled (settings): same control, laid out as full-width choices */
.lang-switch.labeled{display:flex;flex-wrap:wrap;gap:8px;margin:2px 0 0;padding:0;
  background:none;border:0}
.lang-switch.labeled .lang-opt{border:1px solid var(--rule);border-radius:var(--r-sm);
  padding:8px 12px;background:var(--panel-2);font-size:12px}
.lang-switch.labeled .lang-opt:hover{border-color:var(--rule-2);background:var(--panel-3)}
.lang-switch.labeled .lang-opt.is-current{border-color:var(--accent);
  background:rgba(var(--accent-rgb),.1)}

/* --- layout --------------------------------------------------------------- */
/* Create screen: just the setup card, full width. The arena (the swarm
   preview) is hidden until Generate takes it full-screen — no animation
   plays behind the constraint picker. */
.grid2{display:flex;justify-content:center;gap:24px;align-items:start}
#setup{width:100%}
.card{background:linear-gradient(180deg,var(--panel),var(--panel-2));
  border:1px solid var(--rule);border-radius:var(--r-lg);padding:20px;box-shadow:var(--sh-1)}

/* --- create "console": setup fans out on wide screens ---------------------
   A full-width type bar on top, then two columns: the constraint picker (or
   the classic techniques) on the left, and a compact run-controls rail on the
   right. Below ~980px it all stacks into one column (the old behaviour). */
.console{display:grid;gap:20px 26px;grid-template-columns:1fr;align-items:start}
.typebar{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;
  gap:18px 26px;flex-wrap:wrap;border-bottom:1px solid var(--rule);padding-bottom:18px}
.typebar .lede{max-width:52ch}
.typebar .lede h1{font-family:var(--display);font-weight:500;font-size:20px;letter-spacing:.01em;
  color:var(--white);margin:0 0 4px}
.typebar .lede p{font-family:var(--mono);font-size:11.5px;color:var(--dim);line-height:1.6;margin:0}
.seg-mode button{padding:9px 20px;font-size:12.5px}
.zone{min-width:0}
.rail{display:flex;flex-direction:column;gap:18px;min-width:0}
.rail .eyebrow{margin:0 0 9px}
@media(min-width:980px){
  .console{grid-template-columns:minmax(0,1fr) 320px}
  .rail{position:sticky;top:20px}
}

/* --- shared type helpers -------------------------------------------------- */
.eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--dim);margin:0 0 12px}
.con-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin:0 0 14px}
.con-head .eyebrow{margin:0}
.con-meta{font-family:var(--mono);font-size:11px;color:var(--dim);
  display:inline-flex;align-items:baseline;gap:9px;letter-spacing:.02em}
.con-meta b{color:var(--accent);font-weight:500;font-variant-numeric:tabular-nums}
.linkbtn{font-family:var(--mono);font-size:11px;color:var(--base);background:none;border:none;
  padding:0;cursor:pointer;letter-spacing:.02em;transition:color var(--t);
  text-decoration:underline;text-decoration-color:var(--rule);text-underline-offset:3px}
.linkbtn:hover{color:var(--white);text-decoration-color:var(--dim)}
.linkbtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px}

/* --- constraint chips ----------------------------------------------------- */
/* Families pack into columns (masonry-ish) so they fan out across the zone
   instead of forming one tall list; each family is its own bordered panel. */
#fams{margin-top:14px;column-width:232px;column-gap:16px}
.fam{margin:0 0 16px;break-inside:avoid;background:rgba(0,0,0,.14);
  border:1px solid var(--rule);border-radius:var(--r);padding:13px 13px 14px}
.fam h4{font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--base);margin:0 0 9px;font-weight:400}
.chips{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.fam .chips{grid-template-columns:1fr}
.chip{font-family:var(--mono);font-size:11px;letter-spacing:.01em;color:var(--base);
  background:var(--panel-2);border:1px solid var(--rule);border-radius:var(--r-sm);
  padding:6px 9px 6px 8px;cursor:pointer;transition:background var(--t),border-color var(--t),
  color var(--t),transform var(--t);user-select:none;display:flex;align-items:center;gap:8px;
  min-height:31px;text-align:left;-webkit-appearance:none;appearance:none;line-height:1.25}
.chip::before{content:"";width:12px;height:12px;flex:0 0 auto;border:1px solid var(--dim);
  border-radius:4px;background-position:center;background-repeat:no-repeat;background-size:12px 12px;
  transition:border-color var(--t),background-color var(--t)}
.chip:hover{border-color:var(--rule-2);color:var(--white);background:var(--panel-3)}
.chip:hover::before{border-color:var(--base)}
.chip:active{transform:translateY(1px)}
.chip.on{background:rgba(var(--accent-rgb),.12);border-color:rgba(var(--accent-rgb),.55);color:var(--white)}
.chip.on::before{border-color:var(--accent);background-color:var(--accent);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.6 6.3 5 8.6 9.4 3.6' fill='none' stroke='%230b0d0c' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}
.chip.on:hover{background:rgba(var(--accent-rgb),.17)}

/* A .chips.choice group is a radio group, not a set of tick-boxes: exactly one
   of its chips is on at any time and "None" is one of the options. Give it the
   affordance to match -- a ring with a filled dot -- so nothing suggests these
   can be combined the way the ordinary chips can. */
.chips.choice .chip::before{border-radius:50%}
.chips.choice .chip.on::before{background-color:transparent;border-color:var(--accent);
  background-image:radial-gradient(circle at center,var(--accent) 0 3px,transparent 3.5px)}

/* --- segmented method picker --------------------------------------------- */
.seg{display:flex;gap:6px;flex-wrap:wrap}
.seg button{font-family:var(--mono);font-size:12px;background:var(--panel-2);color:var(--base);
  border:1px solid var(--rule);border-radius:var(--r-sm);padding:7px 13px;cursor:pointer;
  transition:background var(--t),border-color var(--t),color var(--t)}
.seg button:hover{color:var(--white);border-color:var(--rule-2);background:var(--panel-3)}
.seg button.on{background:rgba(70,163,122,.16);border-color:var(--givens);color:var(--white)}
.seg button:disabled{opacity:.38;cursor:not-allowed}

select{font-family:var(--mono);font-size:12px;background:var(--panel-2);color:var(--white);
  border:1px solid var(--rule);border-radius:var(--r-sm);padding:9px 30px 9px 11px;width:100%;
  cursor:pointer;-webkit-appearance:none;appearance:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5' fill='none' stroke='%238a9d94' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 11px center;background-size:12px}
select:hover{border-color:var(--rule-2)}
.note{font-family:var(--mono);font-size:11px;color:var(--dim);line-height:1.65;margin:9px 0 0}

/* --- toggles -------------------------------------------------------------- */
.row{display:flex;align-items:center;gap:11px;margin-top:6px}
.toggle{width:36px;height:21px;border-radius:var(--r-pill);background:var(--panel-2);
  border:1px solid var(--rule);position:relative;cursor:pointer;flex:0 0 auto;
  transition:background var(--t),border-color var(--t);padding:0;-webkit-appearance:none;appearance:none}
.toggle::after{content:"";position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;
  background:var(--dim);transition:transform var(--t) var(--ease),background var(--t)}
.toggle.on{background:rgba(var(--accent-rgb),.22);border-color:var(--accent)}
.toggle.on::after{transform:translateX(15px);background:var(--accent)}
.toggle-lbl{font-family:var(--mono);font-size:12px;color:var(--base)}
.card button:focus-visible,.card select:focus-visible,.chip:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px}

/* --- primary action ------------------------------------------------------- */
.go{width:100%;margin-top:20px;font-family:var(--mono);font-size:13px;letter-spacing:.05em;
  color:var(--accent-ink);background:var(--accent);border:none;border-radius:var(--r);padding:14px;
  cursor:pointer;font-weight:600;position:relative;overflow:hidden;
  transition:filter var(--t),transform var(--t),box-shadow var(--t);
  box-shadow:0 8px 22px rgba(var(--accent-rgb),.16)}
.go:hover{filter:brightness(1.07);box-shadow:0 10px 28px rgba(var(--accent-rgb),.24)}
.go:active{transform:translateY(1px)}
.go:disabled{background:var(--panel-2);color:var(--dim);cursor:default;filter:none;box-shadow:none}
.go:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Cancel mode: the Generate button turns red mid-run so you can abort a
   search that isn't landing (same slot, no layout shift). */
.go.cancel{background:var(--rej);color:var(--white);box-shadow:0 8px 22px rgba(217,105,95,.18)}
.go.cancel:hover{box-shadow:0 10px 28px rgba(217,105,95,.28)}
.go.cancel:focus-visible{outline-color:var(--rej)}

/* Pre-generate wait estimate (see webapp/odds.py). Colour tracks how long it
   takes, NOT how likely it is to work — the old odds band predicted success
   backwards and was removed. Green = quick, amber = slow or unmeasured. */
.odds{margin-top:16px;padding:11px 13px;border-radius:var(--r);border:1px solid var(--rule);
  background:var(--panel-2);display:flex;flex-direction:column;gap:5px}
/* The label carries two facts now ("≈ 5 givens · usually under a minute"), so
   it wraps to two lines in the rail more often than the old wait-only one did.
   Align the dot to the first line rather than to the middle of the block. */
.odds-band{font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.02em;
  display:flex;align-items:flex-start;gap:8px;color:var(--dim);line-height:1.4}
.odds-band::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--dim);
  flex:0 0 auto;margin-top:5px}
.odds-why{font-family:var(--sans);font-size:11.5px;color:var(--base);line-height:1.45}
.odds[data-tone="quick"]{border-color:rgba(81,216,138,.38)}
.odds[data-tone="quick"] .odds-band{color:var(--win)}
.odds[data-tone="quick"] .odds-band::before{background:var(--win)}
.odds[data-tone="slow"]{border-color:rgba(230,178,90,.4)}
.odds[data-tone="slow"] .odds-band{color:#e6b25a}
.odds[data-tone="slow"] .odds-band::before{background:#e6b25a}
.odds[data-tone="unknown"]{border-color:rgba(230,178,90,.4)}
.odds[data-tone="unknown"] .odds-band{color:#e6b25a}
.odds[data-tone="unknown"] .odds-band::before{background:#e6b25a}
/* "heavy" sits between amber and red: buildable, but a global rule makes every
   attempt a full-grid solve, so this is a stronger "slow" than the wait-table
   cases -- a burnt orange to read as "this is the expensive corner". */
.odds[data-tone="heavy"]{border-color:rgba(221,138,74,.46)}
.odds[data-tone="heavy"] .odds-band{color:#dd8a4a}
.odds[data-tone="heavy"] .odds-band::before{background:#dd8a4a}
/* "blocked" is the one case that is not about waiting: this combination was
   measured as unbuildable, so the red is a stop, not a severity. */
.odds[data-tone="blocked"]{border-color:rgba(217,105,95,.42)}
.odds[data-tone="blocked"] .odds-band{color:var(--rej)}
.odds[data-tone="blocked"] .odds-band::before{background:var(--rej)}

/* "Surprise me": pick a sensible, likely-to-land set for you. A quiet ghost
   button — Generate stays the primary action. */
.surprise{width:100%;margin-top:20px;font-family:var(--mono);font-size:12px;letter-spacing:.03em;
  color:var(--base);background:transparent;border:1px dashed var(--rule-2);border-radius:var(--r);
  padding:11px;cursor:pointer;transition:color var(--t),border-color var(--t),background var(--t)}
.surprise:hover{color:var(--white);border-color:var(--accent);background:rgba(var(--accent-rgb),.06)}
.surprise:disabled{opacity:.5;cursor:default}
.surprise:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* The surprise block is a second entrance, not a shortcut for filling in the
   form above, so it is set apart rather than tucked in beside Generate: an
   "or" rule, its own heading, its own controls, its own button. */
.go-note{margin:8px 0 0;text-align:center}
.surprise-block{margin-top:26px}
.or-rule{display:flex;align-items:center;gap:12px;margin:0 0 18px;
  font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--dim)}
.or-rule::before,.or-rule::after{content:"";flex:1;height:1px;background:var(--rule)}
.eyebrow.sub{margin-top:16px}
.seg-find{width:100%}
.seg-find button{flex:1}
.surprise-drew{margin-top:10px;text-align:center;color:var(--accent);
  font-family:var(--mono);font-size:11px}

/* =============================================================================
   ARENA  —  stats · living field · worker ledger · ticker · winner
   ========================================================================== */
.arena{display:none;flex-direction:column;min-height:568px}

.arena-top{display:flex;align-items:center;gap:22px;margin-bottom:16px;flex-wrap:wrap}
.stat{font-family:var(--mono);position:relative;padding-right:22px}
.stat+.stat::before{content:"";position:absolute;left:-11px;top:2px;bottom:2px;width:1px;
  background:var(--rule)}
.stat .k{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim)}
.stat .v{font-size:23px;color:var(--white);font-variant-numeric:tabular-nums;line-height:1.25}
.stat .v small{font-size:12px;color:var(--base);margin-left:1px}
#lead{color:var(--accent)}
.legend{margin-left:auto;display:flex;gap:13px;flex-wrap:wrap}
.legend span{font-family:var(--mono);font-size:10px;color:var(--base);
  display:inline-flex;align-items:center;gap:5px;letter-spacing:.02em}
.dot{width:8px;height:8px;border-radius:2px;display:inline-block;
  box-shadow:0 0 8px 0 currentColor}
.legend .dot{color:inherit}

/* ---- the living field (canvas hero) ---- */
.stage{position:relative;flex:1 1 auto;min-height:640px;border:1px solid var(--rule);
  border-radius:var(--r);overflow:hidden;
  background:
    radial-gradient(120% 150% at 50% 128%,rgba(var(--accent-rgb),.06),transparent 58%),
    linear-gradient(180deg,#0e1210,#0b0f0d)}
.stage iframe{position:absolute;inset:0;width:100%;height:100%;display:block;border:0}
/* instrument framing: vignette + a hairline goal seam near the top */
.stage-veil{position:absolute;inset:0;pointer-events:none;
  box-shadow:inset 0 0 70px rgba(0,0,0,.5);
  background:
    linear-gradient(180deg,rgba(11,13,12,.35),transparent 22%,transparent 78%,rgba(11,13,12,.4))}
.stage-veil::before,.stage-veil::after{content:"";position:absolute;width:11px;height:11px;
  border:1px solid var(--rule-2);opacity:.7}
.stage-veil::before{top:9px;left:9px;border-right:0;border-bottom:0}
.stage-veil::after{bottom:9px;right:9px;border-left:0;border-top:0}

/* base idle / empty state (also used by the settings "no puzzles yet" panel) */
.idle{display:flex;flex-direction:column;align-items:center;justify-content:center;
  text-align:center;gap:9px;color:var(--base);font-family:var(--mono);font-size:12px;
  line-height:1.65;border:1px dashed var(--rule);border-radius:var(--r);padding:34px 24px}
.idle b{color:var(--white);font-family:var(--display);font-weight:400;font-size:14px;letter-spacing:.01em}
/* the generator's invitation, laid over the resting field */
.stage .idle{position:absolute;inset:0;border:0;border-radius:0;padding:28px 34px;
  background:radial-gradient(78% 78% at 50% 46%,rgba(9,11,10,.82),rgba(9,11,10,.28) 90%,transparent)}
.idle .idle-sub{max-width:44ch;color:var(--base)}
.idle .idle-hint{margin-top:4px;color:var(--dim);font-size:11px;letter-spacing:.06em}
/* The stage is a dark "screen" in every theme, so its overlaid invitation
   keeps a fixed light palette (theme ink would go dark-on-dark). The bare
   .idle used elsewhere — e.g. the settings "no puzzles yet" panel — still
   follows the theme. */
.stage .idle{color:#8a9d94}
.stage .idle b{color:#dbe7e0}
.stage .idle .idle-sub{color:#8a9d94}
.stage .idle .idle-hint{color:#5b6863}

/* ---- worker ledger (precise, accessible read-out) ---- */
#live{margin-top:16px}
.swarm{display:grid;grid-template-columns:repeat(4,1fr);gap:9px}
@media(max-width:560px){.swarm{grid-template-columns:repeat(2,1fr)}}
.worker{background:linear-gradient(180deg,var(--panel-2),var(--panel));
  border:1px solid var(--rule);border-radius:var(--r-sm);padding:10px 11px 11px;
  position:relative;overflow:hidden;transition:border-color .25s var(--ease),box-shadow .25s}
.worker::before{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;
  background:currentColor;opacity:.55;transition:background .25s,opacity .25s}
.worker .id{font-family:var(--mono);font-size:10px;color:var(--dim);letter-spacing:.08em;
  display:flex;align-items:center;gap:6px}
.worker .ph{font-family:var(--mono);font-size:11.5px;margin:6px 0 9px;transition:color .25s;
  display:flex;align-items:center;gap:6px;color:var(--base)}
.spin{width:7px;height:7px;border-radius:50%;background:currentColor;
  box-shadow:0 0 7px currentColor;animation:pulse 1.1s var(--ease) infinite}
@keyframes pulse{0%,100%{opacity:.32;transform:scale(.82)}50%{opacity:1;transform:scale(1)}}
.bar{height:5px;border-radius:var(--r-pill);background:rgba(0,0,0,.28);overflow:hidden}
.bar i{display:block;height:100%;width:0;border-radius:var(--r-pill);
  transition:width .4s var(--ease),background .25s}
.worker .att{font-family:var(--mono);font-size:10px;color:var(--dim);margin-top:7px;
  font-variant-numeric:tabular-nums;letter-spacing:.02em}
.worker.leader{border-color:rgba(var(--accent-rgb),.7);color:var(--accent);
  box-shadow:0 0 0 1px rgba(var(--accent-rgb),.3),0 0 26px rgba(var(--accent-rgb),.14)}
.worker.leader .id::after{content:"· closest";color:var(--accent);letter-spacing:.02em}
.worker.won{border-color:var(--win);color:var(--win);
  box-shadow:0 0 0 1px rgba(81,216,138,.55),0 0 34px rgba(81,216,138,.26)}
.worker{color:var(--base)}
.flash{position:absolute;inset:0;background:var(--rej);opacity:0;pointer-events:none}
.flash.go{animation:fl .55s var(--ease)}
@keyframes fl{0%{opacity:.16}100%{opacity:0}}

/* ---- telemetry ticker ---- */
.ticker{margin-top:16px;background:rgba(0,0,0,.22);border:1px solid var(--rule);
  border-radius:var(--r);padding:11px 15px;height:98px;overflow:hidden;
  font-family:var(--mono);font-size:11px;position:relative}
.ticker::after{content:"";position:absolute;left:0;right:0;bottom:0;height:26px;
  pointer-events:none;background:linear-gradient(transparent,rgba(11,13,12,.9))}
.ticker div{padding:2.5px 0;color:var(--base);opacity:0;animation:tin .34s var(--ease-out) forwards;
  display:flex;gap:2px;align-items:baseline}
.ticker .t{color:var(--dim);white-space:pre}
@keyframes tin{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}

/* ---- winner reveal ---- */
.winner{margin-top:16px;border:1px solid var(--win);border-radius:var(--r-lg);padding:20px;
  background:
    radial-gradient(120% 90% at 12% -30%,rgba(81,216,138,.14),transparent 60%),
    linear-gradient(180deg,rgba(81,216,138,.08),rgba(81,216,138,.02));
  display:none;box-shadow:0 0 44px rgba(81,216,138,.1)}
.winner.show{display:block;animation:winIn .5s var(--ease-out)}
@keyframes winIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.winner h3{margin:0 0 5px;font-family:var(--display);font-size:17px;font-weight:500;letter-spacing:.005em;
  display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.winner h3 .who{font-family:var(--mono);font-size:12px;color:var(--dim);font-weight:400}
.winner .rt{font-family:var(--mono);font-size:12px;color:var(--check)}
.winner .lk{margin-top:16px;display:flex;gap:10px;flex-wrap:wrap}
.winner a{font-family:var(--mono);font-size:12px;text-decoration:none;padding:10px 15px;
  border-radius:var(--r-sm);border:1px solid var(--rule);color:var(--link);cursor:pointer;
  transition:border-color var(--t),background var(--t),color var(--t)}
.winner a:not(.primary):hover{border-color:var(--rule-2);background:var(--panel-3)}
.winner a.primary{background:var(--win);color:#04120a;border-color:var(--win);font-weight:600;
  position:relative;overflow:hidden;box-shadow:0 8px 22px rgba(81,216,138,.2)}
.winner a.primary:hover{filter:brightness(1.05)}
.winner a.primary::after{content:"";position:absolute;inset:0;transform:translateX(-130%);
  background:linear-gradient(105deg,transparent 20%,rgba(255,255,255,.4),transparent 80%)}
.winner.show a.primary::after{animation:sheen 1.05s .35s var(--ease-out)}
@keyframes sheen{to{transform:translateX(130%)}}
.winner a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.made{margin-top:13px;display:flex;flex-wrap:wrap;gap:6px}
.made span{font-family:var(--mono);font-size:11px;color:var(--win);background:rgba(81,216,138,.1);
  border:1px solid rgba(81,216,138,.32);border-radius:var(--r-xs);padding:3px 9px}

/* =============================================================================
   FULL-SCREEN GENERATE  —  on Generate the app collapses to just the running
   swarm: the setup form and page chrome hide, the arena fills the viewport,
   and a top stat bar + a cancel control overlay it. Constraints are out of
   sight while it searches. Toggled by body.generating (showLive / toIdle).
   ========================================================================== */
.gen-title{display:none;align-items:center;gap:9px;font-family:var(--mono);font-size:13px;
  color:var(--white);letter-spacing:.02em}
.gen-title .gsp{width:8px;height:8px;border-radius:50%;background:var(--accent);flex:0 0 auto;
  box-shadow:0 0 10px var(--accent);animation:pulse 1.1s var(--ease) infinite}
.gen-cancel{display:none}

body.generating{overflow:hidden}
/* The full-screen generate view sits on a deliberately dark stage in every
   theme (the viz worlds are dark canvases). Pin the surface + ink tokens to
   the dark "observatorium" palette here, so the reveal, winner card and
   worker ledger never flip to a light theme's dark-ink-on-dark — only the
   accent (and fonts) stay themed. Without this, Atelier/Sumi render the
   winner's secondary buttons as dark-on-dark and the reveal as a cream box. */
body.generating{
  --ink:#0b0d0c;--panel:#121614;--panel-2:#171d1a;--panel-3:#1c2320;
  --rule:#232b28;--rule-2:#2c3733;
  --dim:#4f5b57;--base:#8a9d94;--white:#dbe7e0;--link:#eef5f1}
body.generating header,
body.generating #setup{display:none}
body.generating .wrap{max-width:none;margin:0;padding:0}
body.generating .grid2{display:block;gap:0;justify-content:initial}
body.generating .arena{display:flex;position:fixed;inset:0;z-index:40;min-height:0;border:0;
  border-radius:0;padding:0;box-shadow:none;overflow-y:auto;background:
    radial-gradient(120% 150% at 50% 120%,rgba(var(--accent-rgb),.06),transparent 58%),
    linear-gradient(180deg,#0e1210,#0b0f0d)}
body.generating .arena-top{margin:0;padding:17px 28px;gap:22px;flex:0 0 auto;
  border-bottom:1px solid var(--rule);background:rgba(11,15,13,.55)}
body.generating .gen-title{display:flex;margin-right:auto}
body.generating .arena-top .legend{display:none}
body.generating .arena-top .stat .v{font-size:20px}
/* The search brief — what this run is looking for, spelled out. Its own row
   below the title and the counters (arena-top already wraps), full width, so
   a long rule list can breathe instead of squeezing the numbers. Hidden
   outside the full-screen search, like the title it belongs to. */
.gen-brief{display:none}
body.generating .gen-brief{display:grid;order:9;flex:0 0 100%;margin:0;
  grid-template-columns:auto 1fr;gap:2px 14px;font-family:var(--mono);
  font-size:12px;line-height:1.45;
  /* Two columns while there is room; one stacked pair per row when there
     isn't, which is what a phone gets. */
  max-width:min(100%,760px)}
.gen-brief dt{color:var(--dim);letter-spacing:.02em;white-space:nowrap}
.gen-brief dd{margin:0;color:var(--white)}
@media (max-width:560px){
  body.generating .gen-brief{grid-template-columns:1fr;gap:0 0}
  .gen-brief dt{margin-top:6px}
}
/* search-budget runway bar (full-screen only) */
.gen-energy{display:none}
body.generating .gen-energy{display:block;flex:0 0 auto;padding:14px 28px 4px;
  background:rgba(11,15,13,.55)}
.ge-row{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin:0 0 7px}
.ge-lbl{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim)}
.ge-val{font-family:var(--mono);font-size:11px;color:var(--base);font-variant-numeric:tabular-nums}
.ge-track{height:7px;border-radius:var(--r-pill);background:rgba(0,0,0,.34);
  border:1px solid var(--rule);overflow:hidden}
.ge-track i{display:block;height:100%;width:100%;border-radius:var(--r-pill);
  transition:width .5s var(--ease),background .5s var(--ease),box-shadow .5s var(--ease)}
.ge-track i.ok{background:linear-gradient(90deg,var(--grid),var(--win));box-shadow:0 0 12px rgba(81,216,138,.35)}
.ge-track i.low{background:linear-gradient(90deg,var(--check),#e0a94a);box-shadow:0 0 12px rgba(230,196,107,.32)}
.ge-track i.crit{background:linear-gradient(90deg,var(--rej),#e8564a);box-shadow:0 0 14px rgba(217,105,95,.4)}
.ge-note{font-family:var(--mono);font-size:10.5px;line-height:1.5;color:var(--check);
  min-height:16px;margin-top:6px}
.ge-note:empty{margin-top:0}
.ge-reason{font-family:var(--mono);font-size:10.5px;line-height:1.5;color:var(--solve);margin-top:5px}
/* the animation fills the top; the 16-worker ledger sits in a band below it */
body.generating .stage{flex:1 1 auto;min-height:40vh;border:0;border-radius:0}
body.generating #live{margin:0;padding:16px 28px 0;flex:0 0 auto;
  display:flex;flex-direction:column;align-items:center}
body.generating .swarm{width:100%;grid-template-columns:repeat(8,1fr)}
body.generating .ticker{display:none}       /* the ledger is the read-out here, not the log */
body.generating .winner{width:100%;max-width:560px;margin:16px 0 0}
/* once a winner has landed (body.found): the search is over, so drop the
   worker ledger entirely — the found puzzle is the whole story now — and let
   the stage (and its reveal) grow to fill the freed space. */
body.generating.found .swarm{display:none}
/* The search is over, so the immersion is done: drop the fixed full-screen
   takeover and bring the nav bar back. Guests otherwise have no header here —
   this restores their familiar way home (create / exit). A plain column:
   header on top, the winner arena filling the rest. */
body.generating.found .wrap{display:flex;flex-direction:column;height:100vh}
body.generating.found header{display:flex;flex:0 0 auto;margin:0;padding:12px 28px;
  border-bottom:1px solid var(--rule);background:rgba(11,15,13,.55)}
body.generating.found .grid2{display:flex;flex-direction:column;align-items:stretch;flex:1 1 auto;min-height:0}
body.generating.found .arena{position:static;inset:auto;z-index:auto;flex:1 1 auto;min-height:0}
/* let the reveal stage shrink so the winner card AND the New-puzzle control
   always stay on screen at any viewport height (the reported guest dead-end:
   the only way back fell below the fold). Grows to fill on tall screens. */
body.generating.found .stage{min-height:0}
/* cancel (while searching) / new-puzzle (after a winner), centered below */
body.generating .gen-cancel{display:inline-flex;align-self:center;margin:18px auto 26px;flex:0 0 auto;
  align-items:center;gap:9px;font-family:var(--mono);font-size:12.5px;letter-spacing:.03em;
  color:var(--white);background:rgba(217,105,95,.14);border:1px solid var(--rej);border-radius:var(--r);
  padding:12px 28px;cursor:pointer;transition:background var(--t),box-shadow var(--t),border-color var(--t)}
body.generating .gen-cancel:hover{background:rgba(217,105,95,.22);box-shadow:0 8px 26px rgba(217,105,95,.2)}
body.generating .gen-cancel.done{color:var(--win);background:rgba(81,216,138,.12);border-color:var(--win)}
body.generating .gen-cancel.done:hover{background:rgba(81,216,138,.2);box-shadow:0 8px 26px rgba(81,216,138,.2)}
body.generating .gen-cancel:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
@media(max-width:900px){body.generating .swarm{grid-template-columns:repeat(4,1fr)}}

/* =============================================================================
   "still searching" modal · tooltip
   ========================================================================== */
.scrim{position:fixed;inset:0;background:rgba(6,8,7,.72);backdrop-filter:blur(2px);
  display:none;align-items:center;justify-content:center;padding:24px;z-index:50}
.scrim.show{display:flex;animation:tin .25s var(--ease-out)}
.modal{max-width:472px;width:100%;background:linear-gradient(180deg,var(--panel),var(--panel-2));
  border:1px solid var(--rule);border-radius:var(--r-lg);padding:24px 24px 20px;box-shadow:var(--sh-2)}
.modal .mh{display:flex;align-items:center;gap:10px;font-size:15px;margin:0;
  font-family:var(--display);font-weight:500}
.modal .mh .sp{width:8px;height:8px;border-radius:50%;background:var(--check);
  box-shadow:0 0 8px var(--check);animation:pulse 1.1s var(--ease) infinite;flex:0 0 auto}
.modal p{color:var(--base);font-size:13px;line-height:1.65;margin:10px 0 0}
.modal .why{background:rgba(0,0,0,.24);border:1px solid var(--rule);border-radius:var(--r-sm);
  padding:12px 14px;margin-top:13px;font-family:var(--mono);font-size:11.5px;color:var(--base);
  line-height:1.65;white-space:pre-line}
.modal .why:empty{display:none}
.modal .why b{color:var(--check);font-weight:500}
.mbtns{display:flex;gap:8px;flex-wrap:wrap;margin-top:18px}
.mbtns button{font-family:var(--mono);font-size:12px;border-radius:var(--r-sm);padding:10px 14px;
  cursor:pointer;border:1px solid var(--rule);background:var(--panel-2);color:var(--white);
  transition:border-color var(--t),background var(--t)}
.mbtns button:hover{border-color:var(--rule-2);background:var(--panel-3)}
.mbtns button.primary{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);
  font-weight:600}
.mbtns button.primary:hover{filter:brightness(1.06);background:var(--accent)}
.mbtns button:focus-visible,.mchk:focus-within{outline:2px solid var(--accent);outline-offset:2px}
.mchk{display:inline-flex;align-items:center;gap:8px;margin-top:15px;font-family:var(--mono);
  font-size:11px;color:var(--dim);cursor:pointer}
.mchk input{accent-color:var(--accent)}
#tip{position:fixed;z-index:60;max-width:208px;background:var(--panel-3);
  border:1px solid var(--rule-2);border-radius:var(--r-sm);padding:6px 9px;
  font-family:var(--mono);font-size:10.5px;line-height:1.45;color:var(--white);
  pointer-events:none;opacity:0;transform:translateY(3px);
  transition:opacity .13s,transform .13s;box-shadow:var(--sh-2)}
#tip.show{opacity:1;transform:none}

/* =============================================================================
   login
   ========================================================================== */
.login-wrap{position:relative;z-index:1;min-height:100vh;display:flex;align-items:center;
  justify-content:center;padding:24px}
.login-card{max-width:364px;width:100%;
  background:linear-gradient(180deg,var(--panel),var(--panel-2));
  border:1px solid var(--rule);border-radius:var(--r-lg);padding:30px 26px;box-shadow:var(--sh-2)}
.login-card .brand{font-size:18px}
.field{display:block}
.flabel{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--dim);margin:0 0 6px}
.inp{font-family:var(--mono);font-size:13px;background:var(--panel-2);color:var(--white);
  border:1px solid var(--rule);border-radius:var(--r-sm);padding:11px 12px;width:100%;
  transition:border-color var(--t),box-shadow var(--t)}
.inp:hover{border-color:var(--rule-2)}
.inp:focus-visible{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(var(--accent-rgb),.14)}
.showpw{display:inline-flex;align-items:center;gap:8px;margin-top:14px;font-family:var(--mono);
  font-size:11px;color:var(--dim);cursor:pointer}
.showpw input{accent-color:var(--accent)}
.err{font-family:var(--mono);font-size:12px;color:var(--rej);background:rgba(217,105,95,.1);
  border:1px solid rgba(217,105,95,.4);border-radius:var(--r-sm);padding:10px 12px;margin:0 0 16px;
  line-height:1.5}

/* guest entry (login page) */
.guest-split{display:flex;align-items:center;gap:12px;margin:20px 0 16px;
  font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim)}
.guest-split::before,.guest-split::after{content:"";flex:1;height:1px;background:var(--rule)}
.guest-btn{width:100%;font-family:var(--mono);font-size:12.5px;letter-spacing:.03em;cursor:pointer;
  color:var(--base);background:transparent;border:1px solid var(--rule-2);border-radius:var(--r-sm);
  padding:11px 12px;transition:color var(--t),border-color var(--t),background var(--t)}
.guest-btn:hover{color:var(--white);border-color:var(--base);background:var(--panel-3)}
.guest-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.guest-note{font-family:var(--mono);font-size:10.5px;line-height:1.5;color:var(--dim);
  margin:11px 2px 0;text-align:center}
/* guest badge in the header tag */

/* =============================================================================
   settings — history / "my puzzles"
   ========================================================================== */
.saved{font-family:var(--mono);font-size:11px;color:var(--win);text-align:center;margin:12px 0 0}
.saved.err{color:var(--rej)}
.acct-pw{display:grid;gap:8px}
.hist{display:flex;flex-direction:column;gap:10px}
.hitem{background:linear-gradient(180deg,var(--panel-2),var(--panel));border:1px solid var(--rule);
  border-radius:var(--r);padding:14px 15px;transition:border-color var(--t)}
.hitem:hover{border-color:var(--rule-2)}
.hitem-top{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
.hitem h5{margin:0;font-family:var(--mono);font-size:13px;font-weight:500;color:var(--white)}
.hitem .rt{font-family:var(--mono);font-size:11px;color:var(--check);white-space:nowrap;
  font-variant-numeric:tabular-nums}
.hitem .sub{font-family:var(--mono);font-size:10.5px;color:var(--dim);margin-top:4px;
  font-variant-numeric:tabular-nums}
.hitem .cons{margin-top:10px;display:flex;flex-wrap:wrap;gap:5px}
.hitem .cons span{font-family:var(--mono);font-size:10.5px;color:var(--base);
  background:var(--panel);border:1px solid var(--rule);border-radius:var(--r-xs);padding:2px 8px}
.hstarrow{margin-top:12px;display:flex;align-items:center;gap:10px}
.hstarrow .hlbl{font-family:var(--mono);font-size:11px;color:var(--dim);letter-spacing:.02em}
.hstars{display:inline-flex;gap:1px}
.hstar{background:none;border:none;padding:0 2px;cursor:pointer;font-size:17px;line-height:1;
  color:var(--dim);transition:color .12s,transform .12s}
.hstar:hover,.hstar.on{color:var(--check)}
.hstar:hover{transform:scale(1.14)}
.hstar:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px}
.hedit{margin-top:12px;display:grid;grid-template-columns:120px 1fr;gap:8px;align-items:start}
.hedit .inp{font-size:12px;padding:9px 10px}
.hedit textarea.inp{resize:vertical;min-height:38px;line-height:1.5}
.hrow{margin-top:10px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.hrow a{font-family:var(--mono);font-size:11px;color:var(--link);text-decoration:none;
  border:1px solid var(--rule);border-radius:var(--r-sm);padding:7px 12px;transition:border-color var(--t)}
.hrow a:hover{border-color:var(--rule-2)}
.hsave{font-family:var(--mono);font-size:11px;border-radius:var(--r-sm);padding:7px 13px;cursor:pointer;
  border:1px solid var(--rule);background:var(--panel);color:var(--white);transition:border-color var(--t)}
.hsave:hover{border-color:var(--rule-2)}
.hsave:disabled{opacity:.45;cursor:default}
.hsaved{font-family:var(--mono);font-size:10.5px;color:var(--win)}

/* ── admin console ─────────────────────────────────────────────────── */
.admin-grid{display:flex;flex-direction:column;gap:24px}

/* user table */
.utable{font-family:var(--mono);font-size:12.5px}
.uhead,.urow{display:grid;grid-template-columns:minmax(0,2fr) 104px 92px minmax(0,1.7fr);
  gap:12px;align-items:center;padding:10px 4px}
.uhead{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);
  border-bottom:1px solid var(--rule)}
.urow{border-bottom:1px solid var(--rule)}
.urow:last-child{border-bottom:none}
.urow.me{background:rgba(var(--accent-rgb),.05);border-radius:var(--r-xs)}
.uname{color:var(--white);word-break:break-all}
.uname em{color:var(--accent);font-style:normal;font-size:10px;margin-left:4px}
.badge{display:inline-block;font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--solve);border:1px solid rgba(108,182,255,.4);border-radius:var(--r-pill);
  padding:1px 7px;margin-left:6px}
.urole{font-family:var(--mono);font-size:12px;background:var(--panel-2);color:var(--white);
  border:1px solid var(--rule);border-radius:var(--r-sm);padding:5px 8px;width:100%}
.ufnl{accent-color:var(--accent);width:16px;height:16px;cursor:pointer}
.uactions{display:flex;gap:12px;flex-wrap:wrap;justify-content:flex-end}
.linkbtn.danger{color:var(--rej)}
.linkbtn.danger:hover{color:#ff8f84}

/* add-account form */
.add-grid{display:grid;grid-template-columns:1fr 1fr auto auto;gap:10px;align-items:center}
@media(max-width:720px){.add-grid{grid-template-columns:1fr 1fr}}
.add-grid .inp{width:100%}
.fnl{font-family:var(--mono);font-size:11.5px;color:var(--base);display:inline-flex;
  align-items:center;gap:7px;white-space:nowrap;cursor:pointer}
.fnl input{accent-color:var(--accent);width:15px;height:15px}
.add-msg{font-family:var(--mono);font-size:11.5px;margin:10px 0 0}
.add-msg.ok{color:var(--win)}

/* puzzle catalog */
.ptable{display:flex;flex-direction:column}
.prow{display:flex;align-items:center;gap:14px;justify-content:space-between;
  padding:11px 4px;border-bottom:1px solid var(--rule)}
.prow:last-child{border-bottom:none}
.pmain{min-width:0}
.ptitle{font-family:var(--sans);font-size:14px;color:var(--white)}
.pmeta{font-family:var(--mono);font-size:11px;color:var(--dim);margin-top:3px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pcons{color:var(--base)}
.pactions{display:flex;gap:14px;align-items:center;flex:0 0 auto}

/* respect users who ask for calm */
@media(prefers-reduced-motion:reduce){
  *{animation:none!important;transition:none!important;scroll-behavior:auto!important}
}

/* ---- post-winner "set" reveal (see static/reveal.js) ---------------------
   Overlays the stage a few seconds after a winner: the found puzzle draws
   itself onto an empty grid. Own motion curves (matching the approved
   preview); all classes scoped under .reveal so .dot etc. can't leak. */
.reveal{
  --ease:cubic-bezier(.22,.61,.36,1);
  --ease-soft:cubic-bezier(.33,.02,.28,1);
  position:absolute;inset:0;z-index:6;display:none;
  flex-direction:column;align-items:center;justify-content:center;gap:12px;
  padding:20px;cursor:default;
  background:radial-gradient(130% 130% at 50% -10%,var(--panel-3),var(--panel) 72%);
  opacity:0;transition:opacity .5s var(--ease)}
.reveal.show{display:flex;opacity:1}
.reveal.set,.reveal.live{cursor:pointer}
.reveal:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}
.rv-ttl{font-family:var(--mono);font-size:15px;color:var(--accent);
  letter-spacing:.01em;text-align:center;min-height:1.2em;
  opacity:0;transition:opacity .5s var(--ease)}
.reveal.set .rv-ttl{opacity:1}
.rv-boardwrap{flex:1;min-height:0;width:100%;
  display:flex;align-items:center;justify-content:center}
.rv-board{height:100%;aspect-ratio:1/1;max-width:100%;overflow:visible}
.reveal.set:hover .rv-board,.reveal.live:hover .rv-board{filter:drop-shadow(0 0 9px #51d88a24)}
.rv-open{font-family:var(--mono);font-size:11px;color:var(--dim);
  letter-spacing:.04em;opacity:0;transition:opacity .5s var(--ease)}
.reveal.live .rv-open{opacity:.5}
.reveal.set .rv-open{opacity:.85}

.reveal .cellline{stroke:var(--rule-2);stroke-width:1.1;opacity:0;
  transition:opacity .55s var(--ease)}
.reveal .cellline.in{opacity:.9}
.reveal .boxline{fill:none;stroke:var(--dim);stroke-width:2.3;
  stroke-linecap:round;stroke-linejoin:round;opacity:.72}
.reveal .cage{fill:none;stroke:var(--base);stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round;opacity:.5}
.reveal .cagelabel{font-family:var(--mono);font-weight:600;fill:var(--base);
  opacity:0;transition:opacity .5s var(--ease)}
.reveal .cagelabel.in{opacity:.85}
.reveal .cline{fill:none;stroke-linecap:round;stroke-linejoin:round;opacity:.5}
.reveal .thermo-stem{fill:none;stroke:var(--dim);stroke-linecap:round;
  stroke-linejoin:round;opacity:.62}
.reveal .thermo-bulb{fill:var(--dim);opacity:0;transform-box:fill-box;
  transform-origin:center;transform:scale(.4);
  transition:opacity .4s var(--ease),transform .5s var(--ease)}
.reveal .thermo-bulb.in{opacity:.62;transform:scale(1)}
.reveal .between-stem{fill:none;stroke:var(--base);stroke-linecap:round;
  stroke-linejoin:round;opacity:.6}
.reveal .between-bulb{fill:none;stroke:var(--base);stroke-width:4;opacity:0;
  transform-box:fill-box;transform-origin:center;transform:scale(.4);
  transition:opacity .4s var(--ease),transform .5s var(--ease)}
.reveal .between-bulb.in{opacity:.85;transform:scale(1)}
.reveal .dot{transform-box:fill-box;transform-origin:center;transform:scale(.5);
  opacity:0;transition:opacity .4s var(--ease),transform .45s var(--ease)}
.reveal .dot.in{opacity:1;transform:scale(1)}
.reveal .xv{font-family:var(--sans);font-weight:600;text-anchor:middle;
  dominant-baseline:central;fill:var(--base);opacity:0;
  transition:opacity .45s var(--ease)}
.reveal .xv.in{opacity:.92}
/* Outside clues (sandwich, x-sum, little killer, skyscraper) in the ring. */
.reveal .outside{font-family:var(--sans);font-weight:600;text-anchor:middle;
  dominant-baseline:central;fill:var(--base);opacity:0;
  transition:opacity .45s var(--ease)}
.reveal .outside.in{opacity:.92}
.reveal .lk-arrow{fill:none;stroke:var(--base);stroke-width:5;
  stroke-linecap:round;stroke-linejoin:round;opacity:0;
  transition:opacity .45s var(--ease)}
.reveal .lk-arrow.in{opacity:.8}

/* Grid marks on the reveal board. `.gm` is the fade-in wrapper the animator
   adds; the per-kind rules below set the colour and the resting opacity, so
   `.gm.in` restores whatever that kind wants rather than a blanket 1. */
.reveal .gm{opacity:0;transition:opacity .45s var(--ease)}
.reveal .gm-odd,.reveal .gm-even{fill:var(--base)}
.reveal .gm-odd.in,.reveal .gm-even.in{opacity:.15}
.reveal .gm-extreme{fill:var(--base)}
.reveal .gm-extreme.in{opacity:.20}
.reveal .gm-clone{fill:var(--base)}
.reveal .gm-clone.in{opacity:.15}
.reveal .gm-region{fill:var(--base)}
.reveal .gm-region.in{opacity:.12}
.reveal .gm-min,.reveal .gm-max{fill:none;stroke:var(--base);stroke-width:3;
  stroke-linecap:round;stroke-linejoin:round}
.reveal .gm-min.in,.reveal .gm-max.in{opacity:.85}
.reveal .gm-palindrome{fill:none;stroke:var(--dim);
  stroke-linecap:round;stroke-linejoin:round}
.reveal .gm-palindrome.in{opacity:.7}
.reveal .gm-diagonal{stroke:var(--dim);stroke-width:2}
.reveal .gm-diagonal.in{opacity:.7}
.reveal .gm-quad{fill:var(--ink);stroke:var(--base);stroke-width:2}
.reveal .gm-quad.in{opacity:.95}
.reveal .gm-quadtext{font-family:var(--sans);font-weight:600;text-anchor:middle;
  dominant-baseline:central;fill:var(--base)}
.reveal .gm-quadtext.in{opacity:.95}
.reveal .gm-cosmetic{stroke-width:2}
.reveal .gm-cosmetic.in{opacity:.95}
.reveal .gm-cosmetic-text{font-family:var(--sans);font-weight:600;
  text-anchor:middle;dominant-baseline:central}
.reveal .gm-cosmetic-text.in{opacity:.95}
.reveal .given{font-family:var(--sans);font-weight:500;text-anchor:middle;
  dominant-baseline:central;fill:var(--white);opacity:0;
  transform-box:fill-box;transform-origin:center;transform:scale(.82);
  transition:opacity .5s var(--ease),transform .55s var(--ease)}
.reveal .given.in{opacity:1;transform:scale(1)}

/* ---- Classic / Variant mode switch -------------------------------------- */
.seg-mode{display:flex;gap:6px}
.seg-mode button{flex:1;font-family:var(--mono);font-size:12.5px;letter-spacing:.02em;
  background:var(--panel-2);color:var(--base);border:1px solid var(--rule);
  border-radius:var(--r-sm);padding:9px 0;cursor:pointer;
  transition:color var(--t),border-color var(--t),background var(--t)}
.seg-mode button:hover{color:var(--white);border-color:var(--rule-2);background:var(--panel-3)}
.seg-mode button.on{background:rgba(var(--accent-rgb),.12);border-color:rgba(var(--accent-rgb),.55);color:var(--white)}
.seg-mode button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Show only the sections that belong to the active mode. */
#setup[data-mode="classic"] .variant-only{display:none}
#setup[data-mode="variant"] .classic-only{display:none}

/* =============================================================================
   HUB  —  the landing page: a hero line + three destination tiles + a strip of
   recently-made puzzles. Reached at "/"; the wordmark is home.
   ========================================================================== */
.hub-hero{margin:6px 0 34px}
.hub-hero h1{font-family:var(--display);font-weight:400;font-size:30px;letter-spacing:-.01em;
  margin:0 0 8px;color:var(--white);text-wrap:balance}
.hub-hero h1 b{color:var(--accent);font-weight:500}
.hub-hero p{color:var(--base);font-size:14px;max-width:58ch;margin:0}

.hub{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
@media(max-width:820px){.hub{grid-template-columns:1fr}}
.tile{display:block;text-decoration:none;color:inherit;
  background:linear-gradient(180deg,var(--panel),var(--panel-2));border:1px solid var(--rule);
  border-radius:var(--r-lg);padding:24px 22px 22px;box-shadow:var(--sh-1);position:relative;
  transition:border-color var(--t),transform var(--t),box-shadow var(--t)}
.tile:hover{border-color:var(--rule-2);transform:translateY(-3px);box-shadow:var(--sh-2)}
.tile:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.tile .ic{width:40px;height:40px;border-radius:var(--r);display:grid;place-items:center;
  margin-bottom:18px;border:1px solid var(--rule-2)}
.tile.create .ic{background:rgba(var(--accent-rgb),.12);color:var(--accent);
  border-color:rgba(var(--accent-rgb),.4)}
.tile.library .ic{background:rgba(108,182,255,.1);color:var(--solve);border-color:rgba(108,182,255,.35)}
.tile.settings .ic{background:rgba(70,163,122,.1);color:var(--givens);border-color:rgba(70,163,122,.35)}
.tile h3{margin:0 0 6px;font-family:var(--display);font-size:18px;font-weight:500;color:var(--white)}
.tile p{margin:0;color:var(--base);font-size:12.5px;line-height:1.6}
.tile .go-hint{margin-top:16px;font-family:var(--mono);font-size:11px;color:var(--dim);
  display:inline-flex;align-items:center;gap:6px;letter-spacing:.03em;
  transition:color var(--t),gap var(--t)}
.tile:hover .go-hint{color:var(--base);gap:9px}
.tile.create:hover .go-hint{color:var(--accent)}

.recent{margin-top:34px}
.recent-head{display:flex;align-items:baseline;justify-content:space-between;margin:0 0 14px}
.recent-head .eyebrow{margin:0}
.recent-head a{font-family:var(--mono);font-size:11px;color:var(--base);text-decoration:underline;
  text-decoration-color:var(--rule);text-underline-offset:3px;transition:color var(--t)}
.recent-head a:hover{color:var(--white)}
.rstrip{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
@media(max-width:820px){.rstrip{grid-template-columns:repeat(2,1fr)}}
@media(max-width:520px){.rstrip{grid-template-columns:1fr}}
.rcard{display:block;text-decoration:none;background:var(--panel-2);border:1px solid var(--rule);
  border-radius:var(--r);padding:14px 15px;transition:border-color var(--t)}
.rcard:hover{border-color:var(--rule-2)}
.rcard .rt{font-family:var(--mono);font-size:12px;color:var(--white);font-weight:500}
.rcard .rd{font-family:var(--mono);font-size:10.5px;color:var(--dim);margin-top:4px;
  font-variant-numeric:tabular-nums}
.rcard .rb{margin-top:11px;font-family:var(--mono);font-size:10.5px;display:inline-block;
  padding:2px 8px;border-radius:var(--r-xs);border:1px solid var(--rule);color:var(--base)}

/* difficulty colour chips — shared by the hub strip (.rb) and library (.dbadge) */
.d-easy{color:var(--givens)!important;border-color:rgba(70,163,122,.4)!important;background:rgba(70,163,122,.08)}
.d-medium{color:var(--check)!important;border-color:rgba(230,196,107,.4)!important;background:rgba(230,196,107,.08)}
.d-hard{color:var(--accent)!important;border-color:rgba(var(--accent-rgb),.4)!important;background:rgba(var(--accent-rgb),.08)}
.d-vhard{color:var(--rej)!important;border-color:rgba(217,105,95,.4)!important;background:rgba(217,105,95,.08)}

/* =============================================================================
   LIBRARY  —  the generation history as a sortable, filterable table. Each row
   opens into the review editor (stars / solve time / note), reusing the .hstar,
   .hedit, .hrow classes from the old settings history.
   ========================================================================== */
.lib-head{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin:0 0 6px}
.lib-head h1{font-family:var(--display);font-weight:400;font-size:26px;margin:0;color:var(--white)}
.lib-head .cnt{font-family:var(--mono);font-size:12px;color:var(--dim);margin-right:auto}
/* The filter row: right-aligned, borderless until you touch it. It narrows
   the list; it is not what the page is about, so it holds no colour and no
   weight of its own until it is carrying a choice. */
.lib-tools{display:flex;gap:6px;align-items:center;flex-wrap:wrap;
  justify-content:flex-end;margin:14px 0 8px}
.lfwrap{position:relative;display:inline-flex}
/* One chevron for both the selects and the constraint menu — drawn, not an
   image, so it takes the theme's colour without a second asset. */
.lfwrap::after{content:"";position:absolute;right:11px;top:50%;width:5px;height:5px;
  border-right:1px solid var(--dim);border-bottom:1px solid var(--dim);
  transform:translateY(-70%) rotate(45deg);pointer-events:none;transition:border-color var(--t)}
.lfwrap:hover::after{border-color:var(--base)}
.lfil{font-family:var(--mono);font-size:11px;letter-spacing:.03em;color:var(--dim);
  background:transparent;border:1px solid transparent;border-radius:var(--r-pill);
  padding:6px 26px 6px 11px;cursor:pointer;max-width:230px;text-align:left;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  -webkit-appearance:none;appearance:none;
  transition:color var(--t),border-color var(--t),background var(--t)}
.lfil:hover{color:var(--base);border-color:var(--rule)}
.lfil:focus-visible{outline:none;color:var(--base);border-color:var(--accent);
  box-shadow:0 0 0 3px rgba(var(--accent-rgb),.12)}
.lfil:disabled{opacity:.45;cursor:default}
.lfil option{background:var(--panel-2);color:var(--base)}
/* A filter that is actually holding something says so — that is the only
   moment this row asks for attention. */
.lfil.on{color:var(--accent);border-color:rgba(var(--accent-rgb),.35)}
.lfwrap:has(.lfil.on)::after{border-color:var(--accent)}
.lfpop{position:absolute;z-index:30;top:calc(100% + 6px);right:0;min-width:200px;
  max-height:300px;overflow-y:auto;padding:6px;background:var(--panel-2);
  border:1px solid var(--rule-2);border-radius:var(--r-sm);
  box-shadow:0 16px 38px rgba(0,0,0,.34);display:flex;flex-direction:column;gap:1px}
.lfpop[hidden]{display:none}
.lfopt{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:var(--r-xs);
  font-family:var(--mono);font-size:11.5px;color:var(--base);cursor:pointer}
.lfopt:hover{background:var(--panel-3);color:var(--white)}
/* Same tick-box as the constraint chips on the generator (.chip::before), so
   the two places you pick constraints look like the same act. A native
   checkbox would take its colours from the OS and read as a foreign object in
   every theme. */
.lfopt input{margin:0;flex:0 0 auto;width:12px;height:12px;cursor:pointer;
  -webkit-appearance:none;appearance:none;background:transparent;
  border:1px solid var(--dim);border-radius:4px;background-position:center;
  background-repeat:no-repeat;background-size:12px 12px;
  transition:border-color var(--t),background-color var(--t)}
.lfopt:hover input{border-color:var(--base)}
.lfopt input:checked{border-color:var(--accent);background-color:var(--accent);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.6 6.3 5 8.6 9.4 3.6' fill='none' stroke='%230b0d0c' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}
.lfopt input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.lfopt span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lfopt i{font-style:normal;font-size:10px;color:var(--dim)}
.search{flex:1;min-width:200px;font-family:var(--mono);font-size:12.5px;background:var(--panel-2);
  color:var(--white);border:1px solid var(--rule);border-radius:var(--r-sm);padding:10px 12px}
.search::placeholder{color:var(--dim)}
.search:focus-visible{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(var(--accent-rgb),.14)}
.filter{font-family:var(--mono);font-size:11.5px;background:var(--panel-2);color:var(--base);
  border:1px solid var(--rule);border-radius:var(--r-sm);padding:10px 12px;cursor:pointer;
  -webkit-appearance:none;appearance:none}
.filter:hover{border-color:var(--rule-2)}
.lib-import{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:2px 0 14px}
.lib-import .filter{white-space:nowrap}
.lib-import .filter:disabled{opacity:.5;cursor:default}
.imp-msg{font-family:var(--mono);font-size:11.5px;color:var(--dim)}
.imp-msg.ok{color:var(--accent)}
.imp-msg.err{color:#e8663d}
.tablewrap{border:1px solid var(--rule);border-radius:var(--r-lg);overflow-x:auto;background:var(--panel)}
.ltable{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12.5px}
.ltable thead th{text-align:left;font-size:9.5px;letter-spacing:.11em;text-transform:uppercase;
  color:var(--dim);font-weight:400;padding:13px 16px;border-bottom:1px solid var(--rule);
  cursor:pointer;user-select:none;white-space:nowrap;background:var(--panel-2)}
.ltable thead th:hover{color:var(--base)}
.ltable thead th .ar{opacity:0;margin-left:5px;transition:opacity var(--t)}
.ltable thead th.sort .ar{opacity:1;color:var(--accent)}
.ltable thead th.num{text-align:right}
.ltable tbody td{padding:13px 16px;border-bottom:1px solid var(--rule);color:var(--base);
  vertical-align:middle}
.ltable tbody tr:last-child td{border-bottom:0}
.lrow{cursor:pointer;transition:background var(--t)}
.lrow:hover,.lrow.open{background:var(--panel-2)}
.lrow.open td{border-bottom-color:transparent}
.lrow:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.ltable .tt{color:var(--white);font-family:var(--sans);font-size:14px}
/* who made the puzzle, under its title */
.ltable .tt .owner{display:block;font-family:var(--mono);font-size:10px;color:var(--dim);
  letter-spacing:.03em;margin-top:3px;text-transform:none}
/* owner's canonical review, shown read-only to non-owners above their editor */
.oreview{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:12px;
  padding:8px 11px;border:1px solid var(--rule);border-radius:7px;background:var(--panel-2);
  font-family:var(--mono);font-size:11.5px;color:var(--dim)}
.oreview .hlbl{color:var(--dim)}
.oreview .onote{font-style:italic;color:var(--base)}
.oreview .osolve{color:var(--base)}
.clonehint{font-family:var(--mono);font-size:10.5px;color:var(--dim);margin-left:2px}
/* per-row play control (opens the puzzle; our own player later) */
.ltable th.pcol,.ltable td.pcol{width:44px;padding-left:14px;padding-right:0;text-align:center;
  cursor:default}
.playbtn{-webkit-appearance:none;appearance:none;width:26px;height:26px;border-radius:50%;
  border:1px solid var(--rule);background:var(--panel-2);color:var(--accent);cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:1;font-size:10px;
  transition:border-color var(--t),background var(--t),transform var(--t)}
.playbtn:hover{border-color:var(--accent);background:rgba(var(--accent-rgb),.14)}
.playbtn:active{transform:scale(.92)}
.playbtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.playbtn.off{color:var(--dim);cursor:default;background:none}
.ltable .num{text-align:right;font-variant-numeric:tabular-nums;color:var(--white)}
.ltable .solve{color:var(--solve)}
.ltable .cons{display:flex;gap:5px;flex-wrap:wrap}
.ltable .cons span{font-size:10px;color:var(--base);background:var(--panel-2);
  border:1px solid var(--rule);border-radius:var(--r-xs);padding:2px 7px}
.lrow:hover .cons span,.lrow.open .cons span{background:var(--panel-3)}
.ltable .dash{color:var(--dim)}
.dbadge{font-family:var(--mono);font-size:10.5px;display:inline-block;padding:2px 9px;
  border-radius:var(--r-xs);border:1px solid var(--rule);color:var(--base)}
.stars{color:var(--check);letter-spacing:1px;font-size:12px}
.stars .off{color:var(--rule-2)}
.lib-none{text-align:center;color:var(--dim);padding:30px!important}
.ldetail td{padding:0;background:var(--panel-2);border-bottom:1px solid var(--rule)}
.leditor{padding:14px 18px 18px}
.lib-note{font-family:var(--mono);font-size:10.5px;color:var(--dim);margin:14px 2px 0;line-height:1.6}
.lib-note b{color:var(--check);font-weight:400}
.lib-note a,.hub-hero a{color:var(--base);text-decoration:underline;text-decoration-color:var(--rule);
  text-underline-offset:3px;transition:color var(--t)}
.lib-note a:hover,.hub-hero a:hover{color:var(--white)}

/* Settings is two stacked cards now: Appearance (theme) + Puzzle defaults;
   the generation history moved out to the Library. */
.settings-stack{max-width:560px;display:flex;flex-direction:column;gap:20px}
/* field label under a section eyebrow — sentence case, lighter than the
   uppercase eyebrow so the section header stays the dominant line. */
.setlbl{font-family:var(--mono);font-size:11.5px;color:var(--base);
  letter-spacing:.01em;margin:0 0 8px}

/* =============================================================================
   THEMES  —  four visual "worlds", chosen per-user in settings and stamped on
   <html data-theme="..."> (see base.html + webapp/themes.py). Every component
   above composes from the tokens; a theme is just a new set of token values.
   :root is "observatorium" (the default), so only the other three appear here.

   The swarm stage stays a dark "screen" in every theme on purpose — the viz
   worlds are dark canvases; the instrument around them is what re-skins.
   ========================================================================== */

/* ---- Atelier — light drafting table: warm paper, ink navy, a serif hand -- */
html[data-theme="atelier"]{
  --ink:#e8e1d1; --panel:#f7f3ea; --panel-2:#f1ebde; --panel-3:#e7e0ce;
  --rule:#d8d0bd; --rule-2:#c6bca2;
  --dim:#9a927e; --base:#67614f; --white:#222a33; --link:#1b2731;
  --accent:#20618d; --accent-rgb:32,97,141; --accent-ink:#f7f3ea;
  --display:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  --brand-font:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  --sh-1:0 1px 0 rgba(255,255,255,.5) inset,0 8px 22px rgba(28,37,48,.08);
  --sh-2:0 18px 44px rgba(28,37,48,.14);
}
html[data-theme="atelier"] body{background-image:
  linear-gradient(rgba(32,97,141,.05) 1px,transparent 1px),
  linear-gradient(90deg,rgba(32,97,141,.05) 1px,transparent 1px);
  background-size:26px 26px}
html[data-theme="atelier"] body::before{background:
  radial-gradient(120% 90% at 72% -12%,rgba(32,97,141,.05),transparent 55%)}

/* ---- Sumi — ink & paper, calm, one vermilion seal ----------------------- */
html[data-theme="sumi"]{
  --ink:#e6dfcf; --panel:#efe8d8; --panel-2:#e9e1d0; --panel-3:#e0d7c3;
  --rule:#d6ccb6; --rule-2:#c5ba9e;
  --dim:#9c9481; --base:#6f685b; --white:#26231d; --link:#26231d;
  --accent:#c14631; --accent-rgb:193,70,49; --accent-ink:#efe8d8;
  --display:"Hiragino Mincho ProN","Hoefler Text","Songti SC",Georgia,serif;
  --brand-font:"Hiragino Mincho ProN","Hoefler Text","Songti SC",Georgia,serif;
  --sh-1:0 1px 0 rgba(255,255,255,.4) inset,0 6px 20px rgba(38,35,29,.07);
  --sh-2:0 16px 40px rgba(38,35,29,.12);
}
html[data-theme="sumi"] body{background-image:none}
html[data-theme="sumi"] body::before{background:
  radial-gradient(90% 80% at 50% -10%,rgba(193,70,49,.04),transparent 60%)}

/* ---- Nocturne — deep indigo, luminous cyan, playful geometry ------------ */
html[data-theme="nocturne"]{
  --ink:#0b0920; --panel:#161235; --panel-2:#1d1740; --panel-3:#251d54;
  --rule:#2b2456; --rule-2:#3a316f;
  --dim:#6c64a2; --base:#a9a2d8; --white:#f0edff; --link:#f4f2ff;
  --accent:#37e6cf; --accent-rgb:55,230,207; --accent-ink:#06121f;
  --display:"Avenir Next","Futura","Century Gothic",system-ui,sans-serif;
  --brand-font:"Avenir Next","Futura","Century Gothic",system-ui,sans-serif;
}
html[data-theme="nocturne"] body{background-image:
  linear-gradient(rgba(58,49,111,.5) 1px,transparent 1px),
  linear-gradient(90deg,rgba(58,49,111,.5) 1px,transparent 1px);
  background-size:56px 56px}
html[data-theme="nocturne"] body::before{background:
  radial-gradient(120% 90% at 80% -10%,rgba(155,123,255,.16),transparent 55%),
  radial-gradient(100% 80% at 0% 110%,rgba(55,230,207,.10),transparent 55%),
  radial-gradient(140% 120% at 50% 50%,transparent 60%,rgba(4,3,12,.5) 100%)}

/* ---- Player (/play/<pid>) ------------------------------------------------
   Solve one of your own puzzles in the browser. The board reuses reveal.js's
   geometry, so the constraint classes here mirror the .reveal ones — but
   static (no draw-on animation) and with an interactive selection + entries
   layer on top. Everything is token-driven so all four themes re-skin it. */
.play-view{display:flex;flex-direction:column;align-items:center;gap:22px}
.play-top{width:100%;max-width:640px;display:flex;align-items:flex-end;
  justify-content:space-between;gap:18px;flex-wrap:wrap}
.pmeta h1{font-family:var(--display);font-size:24px;font-weight:500;color:var(--white);
  margin:0 0 5px;letter-spacing:.01em}
.psub{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.pcons{font-family:var(--mono);font-size:11.5px;color:var(--dim);letter-spacing:.02em}
.ptimer{font-family:var(--mono);font-size:26px;color:var(--base);font-variant-numeric:tabular-nums;
  letter-spacing:.02em;line-height:1}
/* The guest's welcome: one line of text under the heading, and one word in
   the heading row that leads to the generator. It used to be a bannered box
   with a full-width accent button, which read as the page's main offer — it
   is not. The library is; this only says what the library is for. */
.lib-welcome{font-family:var(--mono);font-size:11.5px;line-height:1.6;
  color:var(--base);margin:0 2px 6px;max-width:72ch}
.lib-welcome span{color:var(--dim)}
.lw-make{flex:none;align-self:center;font-family:var(--mono);font-size:11.5px;letter-spacing:.05em;
  color:var(--accent);text-decoration:none;white-space:nowrap;
  border:1px solid rgba(var(--accent-rgb),.35);border-radius:var(--r-pill);padding:6px 15px;
  transition:border-color var(--t),background var(--t),color var(--t)}
.lw-make:hover{border-color:var(--accent);background:rgba(var(--accent-rgb),.10);color:var(--link)}
.lw-make:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

.play-view.is-solved .ptimer{color:var(--win)}
.ptimerow{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
.ppause{min-width:88px}
/* Paused: the clock is stopped, so the board goes with it. A timer you can
   stop while still reading the grid is not a pause. The veil above carries
   the resume control; the blur is what makes stopping the clock honest.
   pointer-events:none keeps stray clicks off the hidden board — play.js
   refuses them too, since a blur is styling and styling is not a guard. */
.play-view.is-paused #board{filter:blur(14px) saturate(.6);pointer-events:none;
  user-select:none}
.play-view.is-paused .ptimer{color:var(--dim)}
@media (prefers-reduced-motion:no-preference){
  .play-view #board{transition:filter .18s ease}
}

.board-stage{position:relative;width:100%;max-width:min(640px,86vw)}
/* The board is a fixed light "sheet" in every theme (SudokuPad-style), so the
   translucent cell colours always read well. Board-local ink tokens keep it
   dark-on-light regardless of the page theme. */
#board{display:block;width:100%;aspect-ratio:1/1;overflow:visible;
  touch-action:manipulation;border-radius:8px;
  --bd-bg:#f5f2ea; --bd-ink:#20242e; --bd-entry:#2f6be0;
  --bd-line:#d0cabd; --bd-box:#565b68; --bd-soft:#7b8090;
  background:var(--bd-bg);box-shadow:0 3px 20px rgba(0,0,0,.20)}
/* The moving cell highlight is the real focus indicator, so the board itself
   only needs a whisper of a ring for keyboard users — not a big accent box. */
#board:focus{outline:none}
#board:focus-visible{outline:1px solid rgba(var(--accent-rgb),.3);outline-offset:3px;
  border-radius:var(--r-xs)}

/* interaction + selection */
#board .hit{fill:transparent;cursor:pointer}
#board .hit:hover{fill:rgba(var(--accent-rgb),.10)}
#board .selcell{fill:rgba(var(--accent-rgb),.20);stroke:var(--accent);stroke-width:3;
  pointer-events:none}

/* grid + constraints (dark-on-light board) */
#board .cellline{stroke:var(--bd-line);stroke-width:1.1;opacity:1}
#board .boxline{fill:none;stroke:var(--bd-box);stroke-width:2.6;
  stroke-linecap:round;stroke-linejoin:round;opacity:1}
#board .cage{fill:none;stroke:var(--bd-soft);stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round;opacity:.75}
#board .cagelabel{font-family:var(--mono);font-weight:600;fill:var(--bd-ink);opacity:.9}
#board .cline{fill:none;stroke-linecap:round;stroke-linejoin:round;opacity:.75}
#board .thermo-stem{fill:none;stroke:var(--bd-soft);stroke-linecap:round;
  stroke-linejoin:round;opacity:.7}
#board .thermo-bulb{fill:var(--bd-soft);opacity:.7}
#board .between-stem{fill:none;stroke:var(--bd-soft);stroke-linecap:round;
  stroke-linejoin:round;opacity:.75}
#board .between-bulb{fill:none;stroke:var(--bd-soft);stroke-width:4;opacity:.9}
#board .arrow-stem,#board .arrow-head{fill:none;stroke:var(--bd-soft);
  stroke-linecap:round;stroke-linejoin:round;opacity:.75}
/* Hollow so the digits inside a pill stay readable. */
#board .arrow-bulb{fill:none;stroke:var(--bd-soft);stroke-width:4;opacity:.9}
#board .dot{opacity:1}
#board .xv{font-family:var(--sans);font-weight:600;text-anchor:middle;
  dominant-baseline:central;fill:var(--bd-ink);opacity:.92}
/* Outside clues (sandwich, x-sum, little killer, skyscraper) sit in the ring
   around the grid. Board ink, not the JSON's fontC: the board is a fixed light
   sheet in every theme, every other clue on it is drawn in --bd-ink, and
   f-puzzles does not trust that colour either (it inverts it in dark mode). */
#board .outside{font-family:var(--sans);font-weight:600;text-anchor:middle;
  dominant-baseline:central;fill:var(--bd-ink)}
#board .lk-arrow{fill:none;stroke:var(--bd-ink);stroke-width:5;
  stroke-linecap:round;stroke-linejoin:round;opacity:.85}

/* Grid marks — see grid_marks.js. Opacities are f-puzzles' own (odd/even 15%,
   min/max 20%, clone 15%) so a puzzle reads the same here as on SudokuPad. */
#board .gm-odd,#board .gm-even{fill:var(--bd-ink);opacity:.15}
#board .gm-extreme{fill:var(--bd-ink);opacity:.20}
#board .gm-clone{fill:var(--bd-ink);opacity:.15}
#board .gm-region{fill:#e0dcd2}
#board .gm-min,#board .gm-max{fill:none;stroke:var(--bd-ink);stroke-width:3;
  stroke-linecap:round;stroke-linejoin:round}
#board .gm-palindrome{fill:none;stroke:#c9c3b6;
  stroke-linecap:round;stroke-linejoin:round}
#board .gm-diagonal{stroke:var(--bd-soft);stroke-width:2;opacity:.75}
#board .gm-quad{fill:var(--bd-bg);stroke:var(--bd-ink);stroke-width:2}
#board .gm-quadtext{font-family:var(--sans);font-weight:600;text-anchor:middle;
  dominant-baseline:central;fill:var(--bd-ink)}
/* A cosmetic circle/rectangle keeps the colours the puzzle gave it: for a
   negative quadruple the red outline IS the rule, and recolouring it to board
   ink would state the opposite of what the puzzle means. */
#board .gm-cosmetic{stroke-width:2}
#board .gm-cosmetic-text{font-family:var(--sans);font-weight:600;
  text-anchor:middle;dominant-baseline:central}

/* digits */
#board .given{font-family:var(--sans);font-weight:500;text-anchor:middle;
  dominant-baseline:central;fill:var(--bd-ink)}
#board .entry{font-family:var(--sans);font-weight:500;text-anchor:middle;
  dominant-baseline:central;fill:var(--bd-entry)}

/* solved card, floating over the board */
.solved-card{position:absolute;inset:0;display:none;flex-direction:column;
  align-items:center;justify-content:center;gap:8px;text-align:center;
  background:radial-gradient(120% 120% at 50% 40%,rgba(11,15,13,.82),rgba(11,15,13,.94));
  border-radius:var(--r-sm);backdrop-filter:blur(2px)}
.play-view.is-solved .solved-card{display:flex}
/* paused veil, same footprint as the solved card but lighter: this state is
   temporary and the player is coming back. */
.paused-card{position:absolute;inset:0;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:10px;text-align:center;
  background:radial-gradient(120% 120% at 50% 40%,rgba(11,15,13,.55),rgba(11,15,13,.78));
  border-radius:var(--r-sm)}
.paused-card[hidden]{display:none}
.paused-card b{font-family:var(--display);font-size:24px;color:var(--white)}
.pv-hint{font-family:var(--mono);font-size:11.5px;color:var(--base)}
.sv-ring{width:58px;height:58px;border-radius:50%;display:flex;align-items:center;
  justify-content:center;font-size:30px;color:var(--ink);background:var(--win);
  box-shadow:0 0 26px rgba(81,216,138,.5)}
.solved-card b{font-family:var(--display);font-size:26px;color:var(--white)}
.solved-card span{font-family:var(--mono);font-size:12px;color:var(--base)}
.sv-saved{color:var(--win) !important}
/* post-solve review: rate + note, straight into the Library (MR player) */
.sv-review{display:flex;flex-direction:column;align-items:center;gap:9px;
  margin-top:8px;width:min(320px,82%)}
.sv-review .hstarrow{margin-top:0}
.sv-review .hstar{font-size:22px}
.sv-note{width:100%;resize:vertical;min-height:42px}

/* controls */
.play-controls{width:100%;max-width:min(640px,86vw);display:flex;flex-direction:column;
  align-items:center;gap:16px}
.digitpad{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;width:100%}
.digitpad button{font-family:var(--mono);font-size:20px;color:var(--white);
  background:var(--panel-2);border:1px solid var(--rule);border-radius:var(--r-sm);
  padding:14px 0;cursor:pointer;transition:border-color var(--t),background var(--t),color var(--t)}
.digitpad button:hover{border-color:var(--rule-2);background:var(--panel-3)}
.digitpad button:active{border-color:var(--accent);background:rgba(var(--accent-rgb),.12)}
.digitpad .erase{color:var(--base);font-size:18px}
.pactions{display:flex;gap:20px;align-items:center;flex-wrap:wrap;justify-content:center}
.pactions .hopen,.pactions .pback{font-family:var(--mono);font-size:11.5px;color:var(--base);
  text-decoration:none;letter-spacing:.02em}
.pactions .hopen:hover,.pactions .pback:hover{color:var(--white)}
.perr{font-family:var(--mono);font-size:12px;color:var(--rej);text-align:center;max-width:520px}

/* Import (/open): paste a link to play an existing puzzle here. While the form
   is up the board + controls stay hidden; once a puzzle loads the form is. */
.open-form{width:100%;max-width:520px;display:flex;flex-direction:column;gap:12px;
  margin:8vh auto 0}
.open-form h1{font-family:var(--display);font-size:24px;color:var(--white);letter-spacing:-.01em}
.open-hint{font-family:var(--sans);font-size:13px;color:var(--base);line-height:1.55}
.open-form textarea.inp{width:100%;resize:vertical;min-height:56px;line-height:1.5}
.open-row{display:flex;align-items:center;gap:16px}
.open-row .go{width:auto;margin-top:0;padding:12px 24px}
.play-view.awaiting-open .play-top,
.play-view.awaiting-open .board-stage,
.play-view.awaiting-open .play-controls,
.play-view.awaiting-open>.perr{display:none}

@media (max-width:560px){
  .play-top{justify-content:center;text-align:center}
  .digitpad{grid-template-columns:repeat(5,1fr)}
}

/* Player — pencilmarks + entry-mode switch (MR-2) */
#board .pm-corner{font-family:var(--sans);text-anchor:middle;dominant-baseline:central;
  fill:var(--bd-entry);opacity:.9}
#board .pm-center{font-family:var(--sans);text-anchor:middle;dominant-baseline:central;
  fill:var(--bd-entry);opacity:.9;letter-spacing:.5px}
/* A candidate the rules have ruled out — corner mark, or one digit (tspan)
   inside a centre row. Red overrides the inherited mark fill. */
#board .pm-dead{fill:var(--rej);opacity:1}
.pmodes{width:100%}
.pmodes button{flex:1}

/* Player — cell colours + undo/redo (MR-3) */
#board .cellcol{pointer-events:none}
.ubtn{font-family:var(--mono);font-size:11.5px;color:var(--base);letter-spacing:.02em;
  background:var(--panel-2);border:1px solid var(--rule);border-radius:var(--r-sm);
  padding:7px 12px;cursor:pointer;transition:border-color var(--t),background var(--t),color var(--t)}
.ubtn:hover:not(:disabled){color:var(--white);border-color:var(--rule-2);background:var(--panel-3)}
.ubtn:disabled{opacity:.4;cursor:default}

/* Player — live conflicts + same-digit highlight (MR-4) */
#board .entry.conflict,#board .given.conflict{fill:#d63d3d}
#board .samecell{fill:rgba(70,78,96,.16);pointer-events:none}
/* Shared-unknown label (Label mode) — reads as a variable, not a digit. */
#board .pletter{font-family:var(--display),serif;font-style:italic;text-anchor:middle;
  dominant-baseline:central;fill:#6d5bd0;opacity:.92}
