/* self-hosted fonts — no fonts.googleapis.com at runtime (native app must work offline) */
@font-face{
  font-family:'Fredoka'; font-style:normal; font-weight:300 700; font-display:swap;
  src:url('assets/fonts/fredoka-var-latin.woff2') format('woff2');
}
@font-face{
  font-family:'Press Start 2P'; font-style:normal; font-weight:400; font-display:swap;
  src:url('assets/fonts/press-start-2p-latin.woff2') format('woff2');
}
:root{
  /* ---------- UI TEXT SCALE (bug B1) ----------
     Every font-size in the game — this sheet and the JS-injected style blocks — is written as
     calc(<px> * var(--fs)), so this ONE number moves all of them and nothing else. Padding,
     gaps and fixed boxes stay exactly where they are, which is the point: the drawer has
     whitespace to spare and the type does not. The game shipped at hard 11-13px, which is fine
     on a desktop window and too small on a phone, where `user-scalable=no` also rules out
     pinch-zoom and CSS px ignores the Android system font setting.
     Owned by ui/settings.js (state.textScale, Text Size in the ⚙️ menu), which writes it onto
     <html>. Rewritten in bulk by tools/scale_font_sizes.mjs.
     REVERT SWITCH: --fs:1 is pixel-identical to everything before B1. */
  --fs:1;
  /* Cozy Warm — cream + terracotta + honey wood (palette pulled from the room art) */
  --wall:#f3ead9; --wall-sh:#e4d5bd;
  --wood:#c8763a; --wood-d:#9c4a26; --wood-dd:#7a3720;
  --terra:#e07a2f; --terra-d:#c05f1e;   /* primary CTA */
  --teal:#2f9e93; --teal-d:#237a72;     /* secondary */
  --cream:#fbf4e6; --cream2:#f3e7d0;    /* panels */
  --card:#fff;                          /* every raised card/row in the drawer */
  --pale1:#fff; --pale2:#f2e7d1;        /* the neutral button (Mgr, recycle, unlock, skip) */
  --iconbg1:#f7ecd6; --iconbg2:#ecdcbc; /* printer/item thumbnail plate */
  --gold1:#fff8e2; --gold2:#f7e7b6;     /* stamp/collection + wallet gold surfaces */
  --gold-line:#e6c96a; --gold-ink:#b4820f;
  /* Body text ON a gold surface. --gold-ink is a mid amber and sits at ~2.7:1 on --gold2 — under
     the 4.5:1 a paragraph needs before it is halved again by .off/.pr-sub opacity, which is what
     made the permanent-power card unreadable ("the gold card where the gold text looks
     terrible"). Gold stays for the ONE headline figure; everything else on gold uses this. */
  --gold-ink-body:#5a3d06;
  --terra-ink:#c05f1e;                  /* accent TEXT (not fills) — must stay readable in both themes */
  --ink:#4a2f1e; --ink-soft:#8a6b52;    /* text on cream */
  --line:#d9c4a1;
  --good:#4c9f52; --good-d:#2e6b33;
  --bad:#d1544f; --coin:#f2b01e;
  --font-head:'Fredoka',system-ui,sans-serif;
}
/* ---------- Lights Out: the UI follows the room into the dark ----------
   Every surface in this sheet is painted from the variables above, so the whole
   drawer/HUD re-themes from this one block. Purple stands in for terracotta so the
   CTAs still read as "the warm one".

   --ui-night is a REGISTERED property, so it can be transitioned — every variable
   below is a color-mix along it, which makes the whole UI cross-fade continuously
   with the sky instead of snapping at the midpoint of the room fade. It is a second
   signal deliberately kept apart from #board-view's --night: that one has to stay a
   plain compositor-driven opacity transition (see the note there), and a registered
   property animates on the main thread. board3d writes both from the same toggle.
   The plain :root block above stays the day palette AND the fallback for engines
   without color-mix() — every line here simply loses and leaves the light theme. */
@property --ui-night{syntax:'<number>';inherits:true;initial-value:0;}
:root{
  --ui-night:0;
  transition:--ui-night 1.1s ease;
  --wall:color-mix(in oklab,#f3ead9,#0e1024 calc(var(--ui-night)*100%));
  --wall-sh:color-mix(in oklab,#e4d5bd,#080a18 calc(var(--ui-night)*100%));
  --wood:color-mix(in oklab,#c8763a,#3b2f70 calc(var(--ui-night)*100%));
  --wood-d:color-mix(in oklab,#9c4a26,#241c4a calc(var(--ui-night)*100%));
  --wood-dd:color-mix(in oklab,#7a3720,#161130 calc(var(--ui-night)*100%));
  --terra:color-mix(in oklab,#e07a2f,#8a44ff calc(var(--ui-night)*100%));
  --terra-d:color-mix(in oklab,#c05f1e,#6a25e0 calc(var(--ui-night)*100%));
  /* the "secondary" slot goes electric blue at night */
  --teal:color-mix(in oklab,#2f9e93,#2f9dff calc(var(--ui-night)*100%));
  --teal-d:color-mix(in oklab,#237a72,#1f6ad0 calc(var(--ui-night)*100%));
  --cream:color-mix(in oklab,#fbf4e6,#181b33 calc(var(--ui-night)*100%));
  --cream2:color-mix(in oklab,#f3e7d0,#12152b calc(var(--ui-night)*100%));
  --card:color-mix(in oklab,#fff,#1e2242 calc(var(--ui-night)*100%));
  /* blue is the secondary voice here — it keeps the whole UI from reading as one purple wash */
  --pale1:color-mix(in oklab,#fff,#2b3059 calc(var(--ui-night)*100%));
  --pale2:color-mix(in oklab,#f2e7d1,#212545 calc(var(--ui-night)*100%));
  --iconbg1:color-mix(in oklab,#f7ecd6,#252a4d calc(var(--ui-night)*100%));
  --iconbg2:color-mix(in oklab,#ecdcbc,#1a1e3a calc(var(--ui-night)*100%));
  --gold1:color-mix(in oklab,#fff8e2,#3d3316 calc(var(--ui-night)*100%));
  --gold2:color-mix(in oklab,#f7e7b6,#2a2210 calc(var(--ui-night)*100%));
  --gold-line:color-mix(in oklab,#e6c96a,#6b551c calc(var(--ui-night)*100%));
  --gold-ink:color-mix(in oklab,#b4820f,#ffd76a calc(var(--ui-night)*100%));
  --gold-ink-body:color-mix(in oklab,#5a3d06,#ffe9a8 calc(var(--ui-night)*100%));
  --terra-ink:color-mix(in oklab,#c05f1e,#c9b0ff calc(var(--ui-night)*100%));
  --ink:color-mix(in oklab,#4a2f1e,#ece8ff calc(var(--ui-night)*100%));
  --ink-soft:color-mix(in oklab,#8a6b52,#a79ed6 calc(var(--ui-night)*100%));
  --line:color-mix(in oklab,#d9c4a1,#332c5e calc(var(--ui-night)*100%));
  --good:color-mix(in oklab,#4c9f52,#3fbf6a calc(var(--ui-night)*100%));
  --good-d:color-mix(in oklab,#2e6b33,#2a8a4b calc(var(--ui-night)*100%));
  --bad:color-mix(in oklab,#d1544f,#ff5d6e calc(var(--ui-night)*100%));
  --coin:color-mix(in oklab,#f2b01e,#ffcc3d calc(var(--ui-night)*100%));
  /* the smoked-glass chrome that floats over the board (scene pill, inventory readout,
     toast, 💡 button) — it sat on hard-coded warm brown and never followed the room */
  --glass:color-mix(in oklab,rgba(58,34,18,.62),rgba(22,17,58,.7) calc(var(--ui-night)*100%));
  --glass-soft:color-mix(in oklab,rgba(58,34,18,.5),rgba(22,17,58,.6) calc(var(--ui-night)*100%));
  --glass-solid:color-mix(in oklab,rgba(58,34,18,.92),rgba(26,21,64,.94) calc(var(--ui-night)*100%));
  --glass-line:color-mix(in oklab,rgba(255,255,255,.22),rgba(178,146,255,.36) calc(var(--ui-night)*100%));
  --glass-ink:color-mix(in oklab,#ffe9d2,#e2dcff calc(var(--ui-night)*100%));
}
/* Surfaces painted with literal colours, plus the panels whose CSS is injected from JS
   (store, reward/welcome-back modal, dev tools) and never sees the variables at all. */
body.lights-ui #topbar{background:linear-gradient(#241a52,#150f30);}
body.lights-ui .tab-btn.active .tab-dot{box-shadow:0 0 0 2px var(--card);}
body.lights-ui .qual-tag{background:#173050;color:#7fd0ff;}
body.lights-ui ::-webkit-scrollbar-thumb:hover{background:#4a4084;}
body.lights-ui .pip.off{border-color:#4a4275;}
body.lights-ui .stamp-seal{background:radial-gradient(circle at 50% 38%,#3a2f12 0 60%,#241d0c 100%);
  box-shadow:0 0 0 2px #6b551c inset,0 1px 3px rgba(0,0,0,.4);}
body.lights-ui .modal{background:rgba(4,6,18,.68);}
/* store (monetize.js) */
body.lights-ui .store-wallet{background:linear-gradient(135deg,#3d3316,#2a2210);color:#ffd76a;
  box-shadow:inset 0 0 0 2px #6b551c;}
body.lights-ui .store-sec.iap{background:var(--card);border-color:#6b551c;}
body.lights-ui .bundle{background:var(--card);border-color:#6b551c;color:var(--ink);}
body.lights-ui .bundle-bonus{background:#12331f;color:#5fd98a;}
body.lights-ui .store-btn{background:#3d4a7f;box-shadow:0 3px 0 #29325a;}
body.lights-ui .store-btn.gold{background:#c9931f;box-shadow:0 3px 0 #8a6512;}
body.lights-ui .store-btn.up{background:#2f6f9e;box-shadow:0 3px 0 #21506f;}
body.lights-ui .store-owned{color:#5fd98a;}
/* reward modal — shared by Market Day Complete and Welcome Back */
body.lights-ui .rw-top{background:linear-gradient(135deg,#2f2668,#1b1440);}
body.lights-ui .rw-top h2{color:#ffd76a;}
body.lights-ui .rw-big{color:#6ee39a;}
body.lights-ui .rw-cap-bar{background:#262a4d;}
body.lights-ui .rw-cap-bar span{background:linear-gradient(90deg,#ffd76a,#e09a2f);}
body.lights-ui .rw-cap-miss b{color:#ffb84d;}
body.lights-ui .rw-li{border-bottom-color:rgba(255,255,255,.09);}
body.lights-ui .rw-li-total{background:rgba(255,215,106,.14);}
body.lights-ui .rw-li-total .rw-li-n,body.lights-ui .rw-li-total .rw-li-c{color:#ffd76a;}
/* Hire modal (hire.js) — the crew cards + their green perk pills */
body.lights-ui .hire-card .hire-cand{background:var(--card);}
body.lights-ui .hire-card .hire-cand:hover{box-shadow:0 5px 12px rgba(138,68,255,.28);}
body.lights-ui .hc-perk,body.lights-ui .hire-chip{background:#12331f;border-color:#2b6b3f;color:#6ee39a;}
body.lights-ui .hire-chip.muted{background:var(--cream2);border-color:var(--line);color:var(--ink-soft);}
/* dev tools (dev.js) */
body.lights-ui #dev-entry button{background:var(--card);color:#ffd76a;border-color:#6b551c;}
body.lights-ui #dev-panel button{background:var(--card);color:var(--ink);}
body.lights-ui #dev-panel button.danger{border-color:#7a3038;color:#ff8b95;box-shadow:0 2px 0 #7a3038;}
/* (no background-color transition here on purpose — --ui-night already eases every
   variable continuously, and a second transition on top would smear behind it) */

/* ---------- UI light standard (docs/UI_STANDARDS.md) ----------
   One key light from the top-left. Two elevations only: --el-rest for cards and rows,
   --el-float for sheets and modals. Wells are recessed, buttons get a hard base.
   The palette does NOT change — every token below is light, not colour.

   Consumers are gated on `body.ui-lit` (config.js UI_LIT is the revert switch), so this
   block is inert until a screen opts in, and one constant takes every screen back.
   Faces are color-mix'd off the themed variables rather than the literals the standard
   quotes, so the whole set follows the Lights Out fade instead of staying daylight-white. */
:root{
  /* elevation */
  --el-rest:inset 0 1px 0 rgba(255,255,255,.95),0 1px 2px rgba(90,60,30,.14),0 6px 14px rgba(90,60,30,.10);
  --el-float:0 -7px 20px rgba(70,45,20,.22),inset 0 1px 0 rgba(255,255,255,.9);
  --el-well:inset 0 2px 4px rgba(120,85,40,.30),inset 0 -1px 0 rgba(255,255,255,.75);
  /* surfaces */
  --card-face:linear-gradient(var(--card),color-mix(in oklab,var(--card),var(--line) 16%));
  --drawer-face:linear-gradient(var(--cream),var(--cream2));
  --chip-face:linear-gradient(color-mix(in oklab,var(--cream),var(--card) 40%),var(--cream2));
  --well-face:radial-gradient(120% 100% at 30% 18%,var(--iconbg1),var(--iconbg2));
  /* pressables — body / rim / base, per hue */
  --btn-terra:linear-gradient(color-mix(in oklab,var(--terra),#fff 8%),var(--terra-d));
  --btn-terra-base:var(--wood-d);
  --btn-teal:linear-gradient(color-mix(in oklab,var(--teal),#fff 10%),var(--teal-d));
  --btn-teal-base:color-mix(in oklab,var(--teal-d),#000 34%);
  /* the payout button — saturated, not the pale gold SURFACE, so it stays the loudest
     thing on a card whose face is now plain cream (see "gold is reserved for the payout") */
  --btn-gold:linear-gradient(#f0bb42,#d19116);
  --btn-gold-base:#9c6a10;
  --btn-pale:linear-gradient(var(--pale1),var(--pale2));
  --btn-pale-base:var(--line);
  --btn-rim:inset 0 1px 0 rgba(255,226,186,.70);
  /* the scale */
  --fs-xs:11px; --fs-sm:12.5px; --fs-md:14px; --fs-lg:16px; --fs-xl:19px; --fs-2xl:24px;
  --r-sm:9px; --r-md:13px; --r-lg:20px;
}
/* at night the room light is gone: shadows go cool and deep, highlights drop to a faint
   rim, and the recess reads as a hole rather than a warm dent */
body.lights-ui{
  --el-rest:inset 0 1px 0 rgba(255,255,255,.10),0 1px 2px rgba(0,0,0,.45),0 6px 16px rgba(0,0,0,.35);
  --el-float:0 -7px 22px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.12);
  --el-well:inset 0 2px 5px rgba(0,0,0,.5),inset 0 -1px 0 rgba(255,255,255,.08);
  --btn-rim:inset 0 1px 0 rgba(255,255,255,.16);
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
/* overscroll-behavior on <body> alone does NOT stop Chrome's pull-to-refresh — the gesture is
   owned by the ROOT element, and body is not the scroller here (main is). This is an app, not
   a page: a stray downward drag must never reload it mid-print. */
html,body{margin:0;padding:0;height:100%;overscroll-behavior:none;}
html{overflow:hidden;}
body{
  background:var(--wall);
  color:var(--ink);
  font-family:'Trebuchet MS',system-ui,sans-serif;
  font-size:calc(15px * var(--fs));
  display:flex;flex-direction:column;
  position:relative;min-height:100%;
  overscroll-behavior:none;
  user-select:none;-webkit-user-select:none;
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
}
canvas{image-rendering:pixelated;image-rendering:crisp-edges;}
button{font-family:inherit;}

/* ---------- soft themed scrollbars ---------- */
*{scrollbar-width:thin;scrollbar-color:var(--line) transparent;}
::-webkit-scrollbar{width:8px;height:8px;}
::-webkit-scrollbar-track{background:transparent;}
::-webkit-scrollbar-thumb{background:var(--line);border-radius:5px;}
::-webkit-scrollbar-thumb:hover{background:#c9ae82;}

h2,h3{font-family:var(--font-head);}

/* ---------- top HUD (warm wood bar) ---------- */
/* SLIM (2026-08-28, user: "the top bar is pretty thick, could be reduced to give more screen
   space"). Measured at the real viewport with tools/board3d-verify/topbar_probe.js — 360 CSS px,
   Large text (--fs 1.4), which is how they play: 68px -> 55px, all of it given to the board.
   The height is the CASH PILL plus this padding, and the pill is two lines on purpose (see
   .cashpill), so the room came from the padding around it, not from any font size. */
#topbar{
  display:flex;align-items:center;gap:8px;
  background:linear-gradient(#a95f2f,#8a4a24);
  padding:calc(env(safe-area-inset-top) + 5px) 12px 5px;
  border-bottom:2px solid var(--wood-dd);
  box-shadow:inset 0 2px 0 rgba(255,255,255,.18);
  position:sticky;top:0;z-index:10;
}
.coinpill{
  display:flex;align-items:center;gap:7px;background:rgba(255,255,255,.14);
  border:1.5px solid rgba(255,255,255,.28);border-radius:20px;padding:4px 12px 4px 7px;min-width:0;
}
.coinpill b{font-family:var(--font-head);font-weight:700;color:#fff;font-size:calc(15px * var(--fs));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
/* Cash over rate, one pill. Two lines rather than two pills because they are one fact — what you
   have, and how fast it is arriving — and because the header's spare room is vertical, not
   horizontal. Taking the rate off the board also gives the location name back its whole row. */
.cashpill{cursor:pointer;padding-top:1px;padding-bottom:1px;text-align:left;}
.cashpill:active{transform:translateY(1px);}
.cash-stack{display:flex;flex-direction:column;align-items:flex-start;min-width:0;line-height:1.0;}
.cash-stack small{font-family:var(--font-head);font-weight:700;color:#ffdcb0;
  font-size:calc(10.5px * var(--fs));white-space:nowrap;letter-spacing:.2px;}
.coinpill .dot{width:18px;height:18px;flex:none;border-radius:50%;
  background:radial-gradient(circle at 35% 30%,#ffe07a,var(--coin));border:2px solid #b4820f;}
.coinpill .gem{width:15px;height:15px;flex:none;margin:0 2px;
  background:linear-gradient(#7de3d6,#2f9e93);border:2px solid #1c6b63;transform:rotate(45deg);border-radius:3px;}
.spoolpill{cursor:pointer;background:rgba(233,194,78,.22);border-color:rgba(240,205,90,.55);}
.spoolpill:active{transform:translateY(1px);}
.hud-spool{width:19px;height:19px;flex:none;}
.mini-stat{color:#ffe9d2;font-size:calc(12px * var(--fs));opacity:.92;white-space:nowrap;}
.mini-stat b{font-family:var(--font-head);color:#fff;font-weight:600;}
#topbar .spacer{flex:1;}
.hudbtn{width:34px;height:34px;flex:none;border-radius:10px;border:1.5px solid rgba(255,255,255,.25);
  background:rgba(255,255,255,.12);color:#fff;font-size:calc(15px * var(--fs));display:grid;place-items:center;cursor:pointer;padding:0;}
.hudbtn:active{transform:translateY(1px);}

/* LANDSCAPE (2026-08-31, user: "when I rotate the screen horizontal the top title bar is huge").
   Rotating flips which axis is scarce: the bar's height is the cash pill, and the pill is two
   lines. So in a short landscape viewport the rate moves to sit BESIDE the balance instead of
   under it, and the bar collapses to one line of text. The max-height guard is what keeps a
   desktop window -- also "landscape" -- on the stacked pill it was designed for. */
@media (orientation:landscape) and (max-height:560px){
  /* Sideways, a hole-punch camera and a swiped-in nav bar live on the LEFT/RIGHT edges, so the
     side padding is the inset when there is one and 12px when there is not. */
  #topbar{padding:calc(env(safe-area-inset-top) + 3px)
          max(12px, env(safe-area-inset-right)) 3px max(12px, env(safe-area-inset-left));gap:6px;}
  .coinpill{padding-top:2px;padding-bottom:2px;}
  .cash-stack{flex-direction:row;align-items:baseline;gap:6px;}
  .cash-stack small{font-size:calc(11px * var(--fs));}
  .hudbtn{width:30px;height:30px;}
}

/* ---------- the world (Phaser shop board = main screen) ---------- */
#board-view{position:relative;flex:1;min-height:120px;overflow:hidden;
  background:linear-gradient(#bcdcea 0%,#d8ecf0 42%,#e9d8b8 62%,#e0b070 100%);}
/* Lights Out sky. A CSS gradient is a background-IMAGE, which browsers won't transition —
   so the night sky is its own layer UNDER the canvas, cross-faded by --night over the
   same 1.1s the room lights take.
   The fade is a CSS transition, not a per-frame JS write, on purpose: the first
   activation blocks the main thread for ~0.6s compiling shaders, and a compositor-driven
   opacity fade keeps running through that — so the sky is visibly transitioning instead
   of the whole board looking hung. */
#board-view::before{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:linear-gradient(#080b1c 0%,#101638 45%,#1b1030 72%,#241033 100%);
  opacity:var(--night,0);transition:opacity 1.1s ease;}
#board-view canvas{position:absolute;inset:0;display:block;z-index:1;}
/* The board's top row: location on the left, items/rate on the right, sharing one line.
   Both used to be position:absolute in opposite corners, which works right up until one of them
   grows — then the location name runs underneath the readout and gets eaten ("MAKER WORKSH").
   Bigger text made that the normal case rather than the edge case. The readout keeps its natural
   width because it is the number you are reading; the NAME is what gives way, with an ellipsis. */
.board-bar{
  position:absolute;top:10px;left:10px;right:10px;z-index:3;
  display:flex;flex-wrap:wrap;align-items:center;gap:8px;pointer-events:none;
}
/* Effects and Lights Out share a second row, so neither can overlap the other
   or the location/readout above. An empty row has no height. */
.board-controls{flex-basis:100%;display:flex;align-items:center;justify-content:flex-end;gap:6px;}
.fx-strip{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px;min-width:0;}
/* Icon and a timer, nothing else — small enough to read as a status light rather than a control,
   big enough to tap (the padding carries the target past 30px even at the smallest text scale). */
.fx-chip{
  display:flex;align-items:center;gap:4px;white-space:nowrap;cursor:pointer;
  font-family:var(--font-head);font-size:calc(12px * var(--fs));line-height:1;color:#fff;
  background:var(--glass-soft);backdrop-filter:blur(3px);
  border:1.5px solid var(--glass-line);border-radius:11px;padding:5px 8px;
  text-shadow:0 1px 2px rgba(0,0,0,.5);
}
.fx-chip:active{transform:translateY(1px);}
/* the remaining time reads quieter than the icon it belongs to */
.fx-chip small{font-weight:700;font-size:calc(9.5px * var(--fs));opacity:.9;font-variant-numeric:tabular-nums;}
.fx-chip.boost{border-color:rgba(255,214,120,.85);box-shadow:0 0 10px rgba(242,176,30,.35);}
.fx-chip.prem{border-color:rgba(255,190,235,.8);box-shadow:0 0 10px rgba(226,120,200,.32);}
.board-bar > *, .board-controls > *{pointer-events:auto;}
.board-bar > .board-controls{pointer-events:none;}
.scene-title{
  display:flex;align-items:center;gap:6px;min-width:0;flex:0 1 auto;
  font-family:var(--font-head);font-weight:600;font-size:calc(11.5px * var(--fs));letter-spacing:.3px;color:#fff;
  background:var(--glass);backdrop-filter:blur(3px);
  border:1.5px solid var(--glass-line);border-radius:14px;padding:6px 11px;
  text-shadow:0 1px 2px rgba(0,0,0,.5);cursor:pointer;
}
.scene-title #hq-loc{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.scene-title .pin,.scene-title .hq-dot{flex:none;}
/* "somewhere new is ready" reads on the WHOLE pill, not just the 9px dot — a dot that small on
   a glass banner over a 3D room is genuinely missable (user: "I saw the green dot but it never
   nudged me"). Same glow the board rail uses for the market/gift buttons, so the two signals
   read as one language. Keyframes are duplicated here on purpose: rail-glow is injected by
   daily.js at runtime and this pill must not depend on that having run. */
.scene-title.alert{border-color:var(--terra-d,#c05f1e);animation:hq-glow 1.6s ease-in-out infinite;}
@keyframes hq-glow{0%,100%{box-shadow:none}
  50%{box-shadow:0 0 0 5px color-mix(in oklab,var(--terra,#e07a2f) 45%,transparent)}}
@media (prefers-reduced-motion: reduce){
  .scene-title.alert{animation:none;box-shadow:0 0 0 4px color-mix(in oklab,var(--terra,#e07a2f) 40%,transparent);}
}
/* hitbox review mode (dev panel "Show hitboxes"): the pill's DOM hit area */
body.dev-hitboxes .scene-title{outline:2px dashed #f0f;background:rgba(255,0,255,.35);}
.scene-title .pin{color:#ffd27a;}
.scene-title:active{transform:translateY(1px);}
/* "somewhere new is ready" — relocation affordable, or IP waiting to be claimed.
   Class-toggled, not style.display: an inline '' would fall back to this none. */
.hq-dot{width:9px;height:9px;background:#6ee86e;border:1px solid #063;border-radius:50%;
  box-shadow:0 0 6px #6ee86e;animation:pulse 1.2s ease-in-out infinite;display:none;}
.hq-dot.on{display:block;}
.inv-readout{
  margin-left:auto;flex:none;white-space:nowrap;
  font-family:var(--font-head);font-weight:600;font-size:calc(11px * var(--fs));color:#fff;
  background:var(--glass-soft);backdrop-filter:blur(3px);
  border:1.5px solid var(--glass-line);border-radius:14px;padding:6px 11px;
  text-shadow:0 1px 2px rgba(0,0,0,.5);
}

/* ---------- bottom drawer (tabs lip + content) ---------- */
/* Overlays the board (absolute, not in flow) so the canvas keeps a constant size —
   the board glides its camera on toggle instead of resizing/restarting. */
#drawer{
  position:absolute;left:0;right:0;bottom:0;z-index:6;
  display:flex;flex-direction:column;
  background:var(--cream);
  border-top:4px solid var(--wood-d);
  border-radius:20px 20px 0 0;
  box-shadow:0 -8px 22px rgba(50,25,8,.30);
  padding-bottom:env(safe-area-inset-bottom);
}
#tabs{
  position:relative;display:flex;gap:7px;
  padding:17px 10px 8px;flex-shrink:0;cursor:pointer;
}
#tabs::before{content:"";position:absolute;top:7px;left:50%;transform:translateX(-50%);
  width:44px;height:5px;border-radius:3px;background:var(--line);}
.tab-btn{
  flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;
  padding:7px 2px 5px;border-radius:12px;cursor:pointer;color:var(--ink-soft);
  border:1.5px solid transparent;background:none;min-width:0;
}
.tab-btn .ico{font-size:calc(19px * var(--fs));line-height:1;height:20px;display:grid;place-items:center;}
.tab-btn .ico svg{width:20px;height:20px;display:block;}
.tab-btn .lbl{font-family:var(--font-head);font-weight:600;font-size:calc(9.5px * var(--fs));letter-spacing:.2px;}
.tab-btn:not(.active) .ico{filter:grayscale(.4);opacity:.75;}
.tab-btn.active{background:var(--card);border-color:var(--line);color:var(--terra-ink);
  box-shadow:0 2px 5px rgba(60,30,10,.12);}
.tab-btn.active .ico{filter:none;opacity:1;}
.tab-btn:active{transform:translateY(1px);}
.tab-modal .ico{filter:none !important;opacity:1 !important;}   /* Market launcher: always lit */

/* Notification dots (v6.13): ONE consistent little green dot — on the tab icon, and on the icon of
   the specific new/ready item inside the panel. No borders, tags, or counts. Green = there's
   something new or ready here (new printer, unlockable filament/model, buyable upgrade, claimable
   contract, ready market); it reflects live state and stays until you act on it. */
.tab-btn{position:relative;}   /* anchor each dot to its OWN button (was falling back to #tabs → all piled on the grab-handle line) */
.tab-dot{position:absolute;top:5px;left:calc(50% + 7px);width:8px;height:8px;border-radius:50%;
  display:none;pointer-events:none;background:#35c63f;border:1px solid #147a1c;box-shadow:0 0 0 2px var(--cream);}
.tab-btn.active .tab-dot{box-shadow:0 0 0 2px #fff;}   /* active tab bg is white — match the halo */
.tab-dot.on{display:block;animation:newdot 1.8s ease-in-out infinite;}
.tab-dot.live{display:block;background:#ff9a2e;border:1px solid #8a4a00;animation:newdot 1s ease-in-out infinite;}
@keyframes newdot{0%,100%{transform:scale(1)}50%{transform:scale(1.22)}}

/* the SAME dot, tucked onto the icon of the specific new item */
.printer.is-new .printer-icon::after,
.supply.is-new .swatch::before,
.supply.is-new .cat-ic::after{
  content:"";position:absolute;width:8px;height:8px;border-radius:50%;
  background:#35c63f;border:1px solid #147a1c;box-shadow:0 0 0 2px #fff;z-index:3;
  animation:newdot 1.8s ease-in-out infinite;}
.printer.is-new .printer-icon{position:relative;}
.printer.is-new .printer-icon::after{top:3px;right:3px;}     /* inside — .printer-icon clips overflow */
.supply.is-new .swatch::before{top:-3px;right:-3px;}         /* swatch overflow is visible */
.supply.is-new .cat-ic{position:relative;}
.supply.is-new .cat-ic::after{top:-3px;right:-3px;}
/* upgrade rows have no icon — a leading inline dot on the name. The slot is ALWAYS there and
   only its colour changes: is-new toggles every tick as cash crosses each card's price, and a
   dot that appears in the text flow re-wrapped the name inside a 170px strip card, changed that
   card's height, and — flex row, so every card matches the tallest — made the whole strip jump
   on every purchase.
   SCOPED to #upgrade-list (user, 2026-08-29). `is-new` is only ever toggled on rows in that
   strip, so everywhere else — the Franchise panel's IP list above all — the reserved 15px was an
   indent that could never be filled: every title sat tabbed in while its own description ran
   flush to the card edge. */
#upgrade-list .upg .upg-name::before{content:"";display:inline-block;width:8px;height:8px;border-radius:50%;
  background:transparent;border:1px solid transparent;margin-right:7px;vertical-align:middle;}
#upgrade-list .upg.is-new .upg-name::before{background:#35c63f;border-color:#147a1c;}
/* A Hire sitting on a seat you PAID for wears the store's gold instead of the manager terracotta,
   so "this seat is one you bought, and it is yours for good" is legible without a tooltip
   (docs/CREW_SLOTS.md). Same #e0a825 as .store-btn.gold. */
.btn-mgr.hire-bought{background:linear-gradient(#eab63f,#c9931f);box-shadow:0 2px 0 #a3760f;}
body.ui-lit .btn-mgr.hire-bought{background:var(--btn-gold,linear-gradient(#eab63f,#c9931f));
  box-shadow:0 2px 0 #a3760f,var(--btn-rim);}
/* the SAME green dot on the Mgr button when it's a Hire (a new crew member is available) */
.btn-mgr.hire-ready{position:relative;}
.btn-mgr.hire-ready::after{content:"";position:absolute;top:-3px;right:-3px;width:8px;height:8px;border-radius:50%;
  background:#35c63f;border:1px solid #147a1c;box-shadow:0 0 0 2px #fff;z-index:3;
  animation:newdot 1.8s ease-in-out infinite;}

/* height animates 0 ↔ 48vh for a smooth slide; padding lives on the panels so height:0 fully closes */
main{display:block;height:0;overflow:hidden;-webkit-overflow-scrolling:touch;background:var(--cream);
  transition:height .3s cubic-bezier(.33,0,.2,1);}
body.drawer-open main{height:48vh;overflow-y:auto;}
.tab-panel{display:none;padding:4px 14px 18px;}
.tab-panel.active{display:block;}

.hint,.empty{color:var(--ink-soft);font-size:calc(12.5px * var(--fs));text-align:center;margin:4px 0 12px;}
.empty{margin-top:28px;}

/* drawer heads (per-tab title + right control) */
.drawer-head{display:flex;align-items:center;gap:8px;padding:4px 2px 10px;}
.drawer-head h2{font-family:var(--font-head);font-weight:700;font-size:calc(17px * var(--fs));color:var(--ink);margin:0;}
.drawer-head .sub{font-size:calc(11px * var(--fs));color:var(--ink-soft);}
.buyseg{display:flex;margin-left:auto;background:var(--cream2);border:1.5px solid var(--line);
  border-radius:10px;overflow:hidden;flex:none;}
.buyamt{border:0;background:transparent;font-family:var(--font-head);font-weight:600;font-size:calc(11px * var(--fs));
  color:var(--ink-soft);padding:6px 9px;cursor:pointer;}
.buyamt.active{background:var(--terra);color:#fff;}
/* the spool multiplier sits ABOVE the filament list rather than in a drawer head. The bar is the
   flex row that right-aligns it; the segment itself keeps its natural width. */
.spoolbar{display:flex;justify-content:flex-end;margin:0 0 8px;}
.spoolseg{margin:0;width:fit-content;}

/* section dividers */
.section-title{display:flex;align-items:center;gap:9px;margin:8px 2px 11px;color:var(--ink-soft);
  font-family:var(--font-head);font-weight:600;font-size:calc(11px * var(--fs));letter-spacing:1px;}
.section-title::before,.section-title::after{content:"";flex:1;height:1.5px;background:var(--line);}
.section-title::before{display:none;}
.ip-wallet{font-size:calc(10px * var(--fs));color:var(--ink-soft);font-weight:500;letter-spacing:0;}

/* ---------- printer cards ---------- */
.printer{
  background:var(--card);border:1.5px solid var(--line);border-radius:15px;
  padding:11px;margin-bottom:11px;position:relative;
  box-shadow:0 2px 6px rgba(60,30,10,.08);
}
.printer.locked{opacity:.72;}
.printer.locked .printer-icon{filter:grayscale(.6);}
.printer-top{display:flex;align-items:center;gap:11px;}
/* The quality badge lives UNDER the thumbnail, not in the name row (user, 2026-08-28: "it's a
   dead line for a small piece of info"). In the name row it was a wrapping chip, so on any
   machine whose name plus count plus badge overran the width it took a whole line of the card to
   say "+60% Q". The icon column had empty space beside it and the badge is the same width as the
   thumbnail, so it costs nothing here. Machine 0 has no bonus and renders no badge — the column
   just collapses to the icon, and .printer-top centres it either way. */
.printer-ic-col{display:flex;flex-direction:column;align-items:center;gap:4px;flex:none;}
.printer-icon{
  width:68px;height:68px;flex:none;
  display:flex;align-items:center;justify-content:center;
  background:linear-gradient(var(--iconbg1),var(--iconbg2));border:1.5px solid var(--line);border-radius:12px;
  overflow:hidden;
}
.printer-icon canvas{width:64px;height:64px;}
.printer-icon img{width:62px;height:62px;object-fit:contain;filter:drop-shadow(0 2px 2px rgba(60,30,10,.18));}
.printer-info{flex:1;min-width:0;}
/* The rate is NOT part of the name's wrapping flow. It used to be the last chip in one
   flex-wrap row, so on any machine carrying a quality badge the row ran out of width and the
   rate dropped onto a second line — which meant a card with a badge and a card without one had
   different header heights and different shapes. Two parts now: the name and its chips wrap
   among THEMSELVES on the left, the rate stays pinned top-right on every card. */
.printer-name{font-family:var(--font-head);font-weight:600;font-size:calc(14.5px * var(--fs));color:var(--ink);
  display:flex;align-items:flex-start;gap:7px;}
.pn-main{flex:1;min-width:0;display:flex;align-items:center;gap:7px;flex-wrap:wrap;}
.printer-count{background:var(--teal);color:#fff;font-family:var(--font-head);font-size:calc(10.5px * var(--fs));font-weight:700;
  border-radius:8px;padding:1px 7px;}
.printer-count::before{content:"×";}
.qual-tag{background:#fdf1dc;color:#b4820f;font-family:var(--font-head);font-size:calc(9px * var(--fs));font-weight:600;
  padding:1px 6px;border:1px solid #ecd9a8;border-radius:8px;}
.printer-rev{font-size:calc(12px * var(--fs));color:var(--ink-soft);margin-top:4px;}
.mgr-tag{color:var(--teal);font-weight:700;}
.printer-rev .warn{color:var(--bad);font-weight:600;}

.printer-actions{display:flex;gap:8px;margin-top:10px;}
.btn{
  border:0;border-radius:12px;font-family:var(--font-head);font-weight:700;
  font-size:calc(12.5px * var(--fs));cursor:pointer;padding:9px 6px;flex:1;line-height:1.25;
}
.btn:active{transform:translateY(2px);box-shadow:none !important;}
.btn:disabled{filter:grayscale(.5);opacity:.55;cursor:default;transform:none;}
.btn-buy{background:linear-gradient(var(--terra),var(--terra-d));color:#fff;box-shadow:0 3px 0 var(--wood-dd);}
.btn-run{background:linear-gradient(var(--teal),var(--teal-d));color:#fff;box-shadow:0 3px 0 #17564f;flex:0 0 88px;}
.btn-mgr{background:linear-gradient(var(--pale1),var(--pale2));color:var(--ink);box-shadow:0 3px 0 var(--line);flex:0 0 auto;padding:9px 11px;}
.btn small{display:block;font-weight:500;font-size:calc(9.5px * var(--fs));opacity:.9;margin-top:2px;}

/* item / filament pickers on the card */
.item-pick{display:flex;align-items:center;gap:6px;margin-top:6px;}
.ipk{background:var(--cream2);color:var(--ink-soft);border:1.5px solid var(--line);border-radius:8px;
  font-size:calc(10px * var(--fs));padding:4px 9px;cursor:pointer;line-height:1;}
.ipk:active{transform:translateY(1px);}
/* both rows lock to the same height: the model row carries a 24px canvas and the filament row
   a 15px dot, so left to their content they came out visibly different thicknesses */
.item-pick .ipk,.item-cur{min-height:32px;box-sizing:border-box;}
.ipk{display:flex;align-items:center;justify-content:center;}
.item-cur{flex:1;display:flex;align-items:center;gap:6px;background:var(--cream);border:1.5px solid var(--line);
  border-radius:8px;padding:3px 7px;font-size:calc(12px * var(--fs));color:var(--ink);min-width:0;white-space:nowrap;overflow:hidden;
  cursor:pointer;font-family:inherit;text-align:left;width:100%;}
.item-cur:active{transform:translateY(1px);}
.item-cur::after{content:"▾";margin-left:auto;padding-left:6px;color:var(--ink-soft);font-size:calc(10px * var(--fs));flex:none;}
.item-cur canvas{width:24px;height:24px;flex:none;}
.fil-pick{margin-top:5px;}
.fil-swatch{width:18px;height:18px;flex:none;border-radius:50%;border:1.5px solid rgba(0,0,0,.18);}

/* the sheet behind those two rows — same card as the contract LOAD sheet */
.pick-card{max-width:360px;text-align:left;}
.pick-card h2{text-align:center;margin-bottom:2px;}
.pick-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;max-height:46vh;overflow-y:auto;padding:2px;}
/* .modal-card button paints every button terracotta — these are cards, so they have to
   out-specify it (that rule is 0,1,1) rather than just setting a background. */
.pick-card .pick-cell{background:var(--card);border:1.5px solid var(--line);border-radius:12px;
  padding:8px 5px 7px;text-align:center;cursor:pointer;font:inherit;color:var(--ink);
  box-shadow:none;margin:0;text-shadow:none;display:block;}
.pick-card .pick-cell canvas{width:52px;height:52px;display:block;margin:0 auto 3px;}
.pick-card .pick-cell .pn{display:block;font-size:calc(10.5px * var(--fs));font-weight:700;line-height:1.15;color:var(--ink);}
.pick-card .pick-cell .pv{display:block;font-size:calc(9.5px * var(--fs));color:var(--ink-soft);margin-top:1px;}
.pick-card .pick-cell.on{border-color:var(--teal);box-shadow:0 0 0 2px var(--teal);}
.pick-fil{display:flex;flex-direction:column;gap:7px;max-height:46vh;overflow-y:auto;padding:2px;}
.pick-card .pick-frow{display:flex;align-items:center;gap:9px;width:100%;background:var(--card);
  border:1.5px solid var(--line);border-radius:12px;padding:9px 11px;cursor:pointer;font:inherit;
  color:var(--ink);box-shadow:none;margin:0;text-align:left;text-shadow:none;}
.pick-card .pick-frow.on{border-color:var(--teal);box-shadow:0 0 0 2px var(--teal);}
/* lit: these are CARDS, so they rest (rule 2) — never the 3px base, which belongs to buttons
   alone (rule 3). They out-specify `.modal-card button` flat and have to keep doing it lit. */
body.ui-lit .pick-card .pick-cell,body.ui-lit .pick-card .pick-frow{
  background:var(--card-face);box-shadow:var(--el-rest);
  border-width:1px;border-color:color-mix(in oklab,var(--line),#fff 30%);}
/* a selected cell has to still read as selected now that every cell carries a shadow */
body.ui-lit .pick-card .pick-cell.on,body.ui-lit .pick-card .pick-frow.on{
  border-color:var(--teal);box-shadow:0 0 0 2px var(--teal),var(--el-rest);}
.pick-frow i{width:20px;height:20px;border-radius:50%;flex:none;border:1.5px solid rgba(0,0,0,.2);}
.pick-frow b{flex:1;font-size:calc(12.5px * var(--fs));}
.pick-frow small{font-size:calc(10.5px * var(--fs));color:var(--ink-soft);}
.pick-frow .pf-x{font-size:calc(11px * var(--fs));font-weight:700;color:var(--teal-d);}

/* progress bar */
.prog-wrap{height:8px;background:var(--cream2);border:1px solid var(--line);border-radius:5px;
  margin-top:9px;overflow:hidden;}
.prog-bar{height:100%;width:0;border-radius:5px;background:linear-gradient(90deg,var(--teal),#57c9bb);}

/* ---------- Supplies sub-tabs: Filament | Models ----------
   The catalog used to sit UNDER the whole filament list, starting 594px into a 3047px scroll —
   below the fold of the 432px drawer on every phone. Two panes, one level, no scrolling past. */
.subtabs{display:flex;gap:6px;background:var(--pale2);padding:4px;border-radius:14px;margin:2px 0 12px;}
.sub-btn{flex:1;border:0;background:none;font-family:var(--font-head);font-weight:600;font-size:calc(12.5px * var(--fs));
  color:var(--ink-soft);padding:9px 4px;border-radius:11px;cursor:pointer;position:relative;}
.sub-btn.active{background:var(--card);color:var(--terra-ink);box-shadow:0 1px 4px rgba(90,60,30,.16);}
.sub-btn:active{transform:translateY(1px);}
.sub-dot{position:absolute;top:6px;right:8px;width:8px;height:8px;border-radius:50%;
  background:#35c63f;border:1px solid #147a1c;display:none;}
.sub-dot.on{display:block;animation:newdot 1.8s ease-in-out infinite;}
.sub-pane{display:none;}
.sub-pane.active{display:block;}

/* ---------- Models: a grid of the actual prints, not a list of names ---------- */
/* minmax(0,1fr), NOT 1fr: a grid item's min-width is `auto`, so a cell whose content is wider
   than its share of the row forces the TRACK wider — and two of those push the second column
   clean off the screen. At the larger text sizes on a narrow phone (a Samsung with Screen Zoom
   up reports ~360 CSS px, not 412) that is exactly what happened. minmax(0,…) lets the track
   shrink and makes the CONTENT wrap instead, which is what min-width:0 on the cells then allows. */
/* align-items:stretch, not start: cards in the same row hold different amounts of text (some
   models name a filament, some don't) and with `start` each one was only as tall as its own
   content, so a row came out ragged — worse at the bigger text sizes, where the extra line is
   likelier. Stretch makes the row one height and `margin-top:auto` on the action drops every
   button onto the same line. */
.catalog-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;align-items:stretch;}
.supply.cat-cell{flex-direction:column;align-items:center;text-align:center;gap:4px;
  padding:10px 8px 9px;margin-bottom:0;min-width:0;}
/* "$14 · Sparrow 3+ · ♥Silk" has no break opportunity inside "♥Silk", so it needs one */
.cat-cell .cat-nm,.cat-cell .cat-sub{min-width:0;max-width:100%;overflow-wrap:anywhere;}
.cat-cell .cat-ic,.cat-cell .cat-ic canvas{width:64px;height:64px;max-width:100%;flex:none;}
.cat-cell .cat-nm{font-family:var(--font-head);font-weight:700;font-size:calc(12.5px * var(--fs));color:var(--ink);line-height:1.2;}
.cat-cell .cat-sub{font-size:calc(10.5px * var(--fs));color:var(--ink-soft);line-height:1.35;}
/* the filament on its own line, with the swatch colour the pickers use */
.cat-cell .cat-fil{display:flex;align-items:center;justify-content:center;gap:4px;max-width:100%;
  font-size:calc(10.5px * var(--fs));color:var(--ink-soft);line-height:1.3;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.cat-cell .fdot{width:9px;height:9px;flex:none;border-radius:50%;border:1.5px solid rgba(0,0,0,.22);}
.cat-cell .pips{justify-content:center;}
/* the action is the last thing in the cell and gets pushed to the floor of it, so every button
   in a row sits on the same line however much text the cards above them carry */
.cat-cell .sbtn{width:100%;margin-top:auto;}
.cat-cell .owned-tag,.cat-cell .locked-tag{margin-top:auto;}
.cat-cell .stamp-seal{margin-top:auto;}
/* no min-gap rule needed: the cell is a flex column with gap:4px, and margin-top:auto stacks on
   top of that — so the action never touches the row above it even on the tallest card. */

/* ---------- upgrade strip (above the machines, not buried under them) ---------- */
.upgwrap{margin:2px 0 14px;}
.upg-head{display:flex;align-items:center;gap:8px;font-family:var(--font-head);font-weight:700;
  font-size:calc(11px * var(--fs));letter-spacing:.6px;color:var(--ink-soft);margin-bottom:7px;}
.upg-count{font-weight:600;letter-spacing:0;font-size:calc(11px * var(--fs));color:var(--terra-ink);flex:1;}
.upg-more{background:none;border:0;font-family:var(--font-head);font-weight:700;font-size:calc(11px * var(--fs));
  color:var(--teal-d);cursor:pointer;padding:2px 0;letter-spacing:0;}
.upg-strip{display:flex;gap:9px;overflow-x:auto;padding-bottom:5px;-webkit-overflow-scrolling:touch;}
.upg-strip::-webkit-scrollbar{height:5px;}
.upg-strip::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;}
.upg-strip .upg{flex:none;width:170px;flex-direction:column;align-items:stretch;gap:6px;margin-bottom:0;
  transition:width .2s ease,opacity .2s ease,padding .2s ease,margin .2s ease,border-width .2s ease;}
/* bought → collapse in place. The negative margin eats the flex gap, so the neighbours slide
   in smoothly instead of the row snapping one card-width to the left. */
.upg-strip .upg.upg-bought{width:0!important;opacity:0;padding-left:0;padding-right:0;
  border-width:0;margin-left:-9px;overflow:hidden;}
/* …and its content must not reflow while it shrinks — the height is pinned in JS, this stops
   the text inside trying to lay out in a zero-width column on the way there */
.upg-strip .upg.upg-bought .upg-body,.upg-strip .upg.upg-bought .upg-buy{white-space:nowrap;}
@media (prefers-reduced-motion: reduce){.upg-strip .upg{transition:none;}}
/* a rung this floor cannot hold: greyed, and its button names the board that can */
.upg.upg-locked{opacity:.62;border-style:dashed;}
.upg.upg-locked .upg-buy{background:var(--pale2);color:var(--ink-soft);box-shadow:none;font-size:calc(11px * var(--fs));}
.upg-strip .upg-buy{width:100%;}
/* "see all" wraps the same chips into a grid in place — no second screen to go to.
   It is CAPPED and scrolls inside itself: 70 upgrades exist, ~25 can be available at once,
   and an uncapped 2-column grid grew the panel by a dozen rows — which shoved every printer
   card off the screen and moved the whole drawer under your finger. The cap keeps the
   machines where they were; the grid scrolls in place instead of the page growing. */
.upgwrap.open .upg-strip{flex-wrap:wrap;overflow-x:hidden;overflow-y:auto;
  max-height:min(46vh,300px);align-content:flex-start;padding-right:3px;}
.upgwrap.open .upg-strip .upg{width:calc(50% - 5px);}
.upgwrap.open .upg-strip::-webkit-scrollbar{width:5px;}

.upg{
  background:var(--card);border:1.5px solid var(--line);border-radius:13px;
  box-shadow:0 2px 6px rgba(60,30,10,.08);
  padding:10px 11px;margin-bottom:9px;display:flex;align-items:center;gap:10px;
}
.upg-body{flex:1;min-width:0;}
.upg-name{font-family:var(--font-head);font-weight:600;font-size:calc(13px * var(--fs));color:var(--ink);}
/* the perk sits UNDER the machine name, on its own line, in the accent — so the card reads
   "Sparrow 3 / ×1.5 output" at every text size instead of breaking mid-phrase wherever the
   card happens to run out of room */
.upg-perk{font-family:var(--font-head);font-weight:700;font-size:calc(12.5px * var(--fs));
  color:var(--terra-ink);line-height:1.15;}
.upg-desc{font-size:calc(12px * var(--fs));color:var(--ink-soft);margin-top:3px;}
.upg-buy{background:linear-gradient(var(--terra),var(--terra-d));border:0;border-radius:11px;color:#fff;
  font-family:var(--font-head);font-weight:700;font-size:calc(11.5px * var(--fs));
  padding:9px 12px;cursor:pointer;white-space:nowrap;box-shadow:0 3px 0 var(--wood-dd);}
.upg-buy:active{transform:translateY(2px);box-shadow:none;}
.upg-buy:disabled{filter:grayscale(.5);opacity:.55;transform:none;}
.upg-buy.prestige{background:linear-gradient(var(--teal),var(--teal-d));box-shadow:0 3px 0 #17564f;}
.rd-lvl{color:var(--terra-ink);font-size:calc(11px * var(--fs));font-weight:600;}
/* the Fleet ladder heads the prestige list — gold, like the board chip that shows its total,
   so the permanent axis reads as the headline instead of one perk among six */
.upg.rd-hero{background:linear-gradient(var(--gold1),var(--gold2));border-color:var(--gold-line);}
.upg.rd-hero .upg-name,.upg.rd-hero .rd-lvl{color:var(--gold-ink);}
.upg.rd-hero .upg-desc{color:var(--gold-ink);opacity:.8;}

/* ---------- lit skin: Printers (docs/UI_STANDARDS.md step 3; config.js UI_LIT reverts it) ----------
   Machine cards and upgrade chips are raised, the thumbnail plate becomes a well the machine
   sits IN, the pickers read as pressable chips over a recessed progress channel, and every
   button gets the same anatomy: gradient body, warm rim, hard base that compresses on press.
   The drawer itself takes --el-float here (it is the sheet every tab sits on, so it lifts once
   for all of them rather than per screen). */
body.ui-lit #drawer{box-shadow:var(--el-float),0 -8px 22px rgba(50,25,8,.30);background:var(--drawer-face);}
body.ui-lit .printer,body.ui-lit .upg{border-width:1px;border-color:color-mix(in oklab,var(--line),#fff 30%);
  background:var(--card-face);box-shadow:var(--el-rest);}
/* the machine sits in the plate, not on it */
body.ui-lit .printer-icon{background:var(--well-face);border:none;box-shadow:var(--el-well);}
body.ui-lit .printer-icon img{filter:drop-shadow(0 3px 3px rgba(60,30,10,.30));}
body.ui-lit .printer-count{box-shadow:var(--btn-rim),0 1px 2px rgba(90,60,30,.25);}
body.ui-lit .qual-tag{box-shadow:var(--el-well);border-color:transparent;}
/* Pickers: one row, two raised chips, no steppers. The ◀ ▶ pair was four small targets per
   card for something the chip itself already does — it opens the picker sheet, which shows
   every model at once. The buttons stay in the DOM (hidden), so UI_LIT still reverts this
   screen without touching panels.js. */
body.ui-lit .printer-info{display:flex;flex-wrap:wrap;align-items:center;gap:6px;}
body.ui-lit .printer-name,body.ui-lit .printer-rev,body.ui-lit .printer-job{flex:0 0 100%;}
body.ui-lit .item-pick{flex:1 1 0;min-width:0;margin-top:0;}
body.ui-lit .item-pick .ipk{display:none;}
body.ui-lit .item-cur{background:var(--chip-face);border-color:color-mix(in oklab,var(--line),#fff 25%);
  border-width:1px;box-shadow:var(--el-rest);}
body.ui-lit .item-cur:active{transform:translateY(1px);box-shadow:none;}
/* rule 5 — a filament swatch is a spool end, not a bullet */
body.ui-lit .fil-swatch{border-color:rgba(255,255,255,.85);
  box-shadow:0 1px 2px rgba(90,60,30,.35),inset 0 -2px 3px rgba(0,0,0,.22);}
body.ui-lit .prog-wrap{border:none;box-shadow:var(--el-well);background:var(--pale2);}
body.ui-lit .prog-bar{box-shadow:inset 0 1px 0 rgba(255,255,255,.5);}
/* buttons — one anatomy, three hues */
body.ui-lit .btn-buy,body.ui-lit .upg-buy{background:var(--btn-terra);
  box-shadow:0 3px 0 var(--btn-terra-base),var(--btn-rim),0 2px 6px rgba(90,60,30,.20);}
body.ui-lit .btn-run,body.ui-lit .upg-buy.prestige{background:var(--btn-teal);
  box-shadow:0 3px 0 var(--btn-teal-base),var(--btn-rim),0 2px 6px rgba(90,60,30,.20);}
body.ui-lit .btn-mgr{background:var(--btn-pale);
  box-shadow:0 3px 0 var(--btn-pale-base),var(--btn-rim),0 2px 6px rgba(90,60,30,.14);}
body.ui-lit .btn:active{box-shadow:var(--btn-rim) !important;}
body.ui-lit .upg-buy:active{box-shadow:var(--btn-rim);}
/* the ×1 / ×10 / ×100 / MAX segment is a groove with one raised pill in it */
body.ui-lit .buyseg{border:none;box-shadow:var(--el-well);background:var(--pale2);}
body.ui-lit .buyamt.active{background:var(--btn-terra);
  box-shadow:var(--btn-rim),0 1px 3px rgba(90,60,30,.35);border-radius:8px;}
/* Locked: dimmed, flat, and COLLAPSED. A machine on a board you haven't reached had every
   control a working machine has — two pickers, a progress bar, Run/Buy/Hire — all disabled,
   209px of furniture per machine and four or five of them below the fold. A locked thing is
   one row that names the board it lives on; the controls come back with the machine. */
body.ui-lit .printer.locked,body.ui-lit .upg.upg-locked{box-shadow:none;background:var(--chip-face);}
body.ui-lit .printer.locked .item-pick,
body.ui-lit .printer.locked .prog-wrap,
body.ui-lit .printer.locked .printer-actions{display:none;}
body.ui-lit .printer.locked{padding:9px 11px;}
body.ui-lit .printer.locked .printer-icon{width:46px;height:46px;border-radius:10px;box-shadow:var(--el-well);}
body.ui-lit .printer.locked .printer-icon img{width:40px;height:40px;}
body.ui-lit .printer.locked .printer-rev{margin-top:3px;}
body.ui-lit .printer.locked .printer-rev .warn{display:inline-block;color:var(--ink-soft);font-size:calc(11px * var(--fs));
  background:var(--pale2);border-radius:7px;padding:2px 8px;box-shadow:var(--el-well);}
body.ui-lit .upg.upg-locked .upg-buy{background:var(--pale2);box-shadow:var(--el-well);}

/* the one link between the two panels: HQ tells you where you are, this goes to what you keep */
/* Gold SURFACE, dark ink — never gold on gold. --gold-ink on the gold gradient is ~2.7:1, which
   is the same fault as the permanent-power card and reads as a button someone forgot to finish.
   The gold stays because it is the thread linking this button, that card and the board chip. */
.hq-jump{display:block;width:100%;margin:0 0 12px;padding:11px;border-radius:13px;cursor:pointer;
  font-family:var(--font-head);font-weight:700;font-size:calc(13.5px * var(--fs));color:#fff;
  background:var(--btn-terra,linear-gradient(#e58a45,#c05f1e));border:none;}
.hq-jump:active{transform:translateY(1px);}
/* lit: GOLD, not terracotta. It is deliberately the same gold as .perm-card and the board's
   permanent-power chip — one number in three places — so re-hueing it to the default
   terracotta would break the only thing linking the two panels. Base in its own hue (rule 3). */
/* The PRIMARY button, same as every Buy button and Maya's "Show me" — --btn-terra with a white
   label (user, 2026-08-29). It was gold, to thread this button to .perm-card and the board's
   permanent-power chip, and the white label the user asked for could not sit on it: white on
   #f0bb42 is 1.9:1. Darkening the gold to carry white invented a mud brown that appears nowhere
   else in the game ("that is one shit brown button"). The thread is gone anyway — .perm-card is
   a white plate with a gold border now — so this is simply the primary action it always was, in
   the primary hue, and it introduces no new colour at all. */
body.ui-lit .hq-jump{background:var(--btn-terra);border:none;color:#fff;
  box-shadow:0 3px 0 var(--btn-terra-base),var(--btn-rim),0 2px 6px rgba(90,60,30,.20);}
body.ui-lit .hq-jump:active{box-shadow:var(--btn-rim);transform:translateY(2px);}

/* The one caveat worth stating and not worth a paragraph: it answers "does spending my IP undo
   my bonus", which is the question that stops people spending. */
.prestige-fine{margin:6px 0 0;font-size:calc(11px * var(--fs));color:var(--ink-soft);}

/* ---------- HQ: the permanent-power card ---------- */
/* the permanent total, itemised. Gold, like the board chip that carries the same figure —
   one number in two places, so the chip has somewhere to land that explains itself. */
/* WHITE plate, gold border (user, 2026-08-29). The gold FILL made the panel's most important
   card read as a warning strip against the cream sheet; the border and the +N% figure carry the
   gold identity on their own, and white is what every other card in the game sits on. */
.perm-card{background:#fff;border:1.5px solid var(--gold-line);
  border-radius:14px;padding:10px 12px;margin-bottom:12px;color:var(--gold-ink-body);}
/* lit: keeps its gold gradient, gains the resting elevation and the quieter border */
body.ui-lit .perm-card{box-shadow:var(--el-rest);border-width:1px;}
.perm-card-top{display:flex;align-items:baseline;justify-content:space-between;
  font-family:var(--font-head);font-weight:700;font-size:calc(11px * var(--fs));letter-spacing:.6px;}
.perm-card-top b{font-size:calc(22px * var(--fs));letter-spacing:0;color:var(--gold-ink);}
/* GRID, not a flex line: the source names and their explanations are different lengths and a
   baseline-aligned flex row left the % floating beside a three-line wrap on a phone. Name and
   figure hold line one at every width; the explanation gets line two to itself. */
.perm-row{display:grid;grid-template-columns:17px 1fr auto;gap:1px 7px;
  align-items:baseline;padding:7px 0 0;font-size:calc(12.5px * var(--fs));}
.perm-row + .perm-row{border-top:1px solid rgba(120,80,20,.13);}
.perm-row .pr-ic{grid-column:1;grid-row:1;font-size:calc(13px * var(--fs));}
.perm-row .pr-nm{grid-column:2;grid-row:1;font-weight:700;}
.perm-row .pr-x{grid-column:3;grid-row:1;font-family:var(--font-head);font-weight:700;}
.perm-row .pr-sub{grid-column:2/4;grid-row:2;opacity:.85;font-size:calc(11px * var(--fs));line-height:1.35;padding-bottom:6px;}
/* .off is "you have not bought this yet", not "you may not read this". Half opacity on a gold
   surface was invisible; at .7 over the dark body ink it reads as quiet. */
.perm-row.off{opacity:.7;}
.perm-note{margin:8px 0 0;font-size:calc(11px * var(--fs));opacity:.85;}

/* ---------- HQ: location rows ---------- */
/* The boards behind you, as one line. They are history: you cannot go back to them and their
   "unlocks…" text describes machines you already own. */
.loc-done-strip{display:flex;align-items:center;gap:8px;margin-bottom:9px;padding:7px 11px;
  border-radius:11px;background:rgba(120,90,50,.07);border:1px solid var(--line);
  font-size:calc(11.5px * var(--fs));color:var(--ink-soft);}
.loc-done-strip .lds-tick{color:var(--good,#3f9e52);font-weight:700;flex:none;}
.loc-row{display:flex;align-items:center;gap:10px;background:var(--card);border:1.5px solid var(--line);
  border-radius:13px;padding:9px 11px;margin-bottom:8px;box-shadow:0 2px 6px rgba(60,30,10,.06);}
.loc-row.loc-current{border-color:var(--teal);box-shadow:0 0 0 1px var(--teal),0 2px 6px rgba(60,30,10,.06);}
.loc-row.loc-future{opacity:.65;}
/* the next shop is a sales pitch, not a rumour: full strength so MOVE IN is the same terracotta
   as every other purchase button (opacity on the row is a group, a child cannot undo it) */
.loc-row.loc-future.loc-next{opacity:1;}
.loc-done{color:var(--good);font-family:var(--font-head);font-weight:700;font-size:calc(14px * var(--fs));}
.loc-lock{color:var(--ink-soft);font-size:calc(11px * var(--fs));white-space:nowrap;}
#hq-location{margin-bottom:6px;}
#hq-modal .prestige-box{margin-top:10px;}

/* ---------- contracts ---------- */
.contract{background:var(--card);border:1.5px solid var(--line);border-radius:15px;
  box-shadow:0 2px 6px rgba(60,30,10,.08);
  padding:11px;margin-bottom:11px;}
.con-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}
.con-client{font-family:var(--font-head);font-weight:600;font-size:calc(12.5px * var(--fs));color:var(--ink);}
.con-reward{font-family:var(--font-head);font-weight:700;font-size:calc(14px * var(--fs));color:var(--good);}
.con-stars{color:var(--coin);font-size:calc(10.5px * var(--fs));letter-spacing:1.5px;margin-left:6px;vertical-align:1px;}
.con-flavor{font-size:calc(11px * var(--fs));font-style:italic;color:var(--ink-soft);margin:-4px 0 8px;}
.con-reqs{display:flex;flex-direction:column;gap:5px;margin-bottom:9px;}
.con-req{display:flex;align-items:center;gap:8px;}
.con-bar{height:8px;background:var(--cream2);border:1px solid var(--line);border-radius:5px;
  margin:3px 0 2px;overflow:hidden;}
.con-fill{height:100%;border-radius:5px;background:linear-gradient(90deg,var(--terra),#f2a361);transition:width .2s linear;}
.con-fill.done{background:linear-gradient(90deg,var(--good),#6fc276);}
.con-ic{width:28px;height:28px;flex:none;}
.con-ic canvas{width:28px;height:28px;}
.con-need{flex:1;font-size:calc(12.5px * var(--fs));color:var(--ink);}
.con-need small{color:var(--terra-ink);font-size:calc(11px * var(--fs));}
.con-count{font-family:var(--font-head);font-weight:600;font-size:calc(11px * var(--fs));}
.con-count.ok{color:var(--good);}
.con-count.no{color:var(--bad);}
.con-count.locked{color:var(--terra-ink);}
.con-fil{display:inline-flex;align-items:center;gap:3px;font-size:calc(11px * var(--fs));color:var(--ink-soft);vertical-align:middle;}
.con-fil i{width:9px;height:9px;border:1px solid rgba(0,0,0,.2);border-radius:50%;display:inline-block;}
/* per-line LOAD button — sits inline on the requirement row (mockup-contract-load.html, Row A) */
.con-load{font-family:var(--font-head);font-weight:700;font-size:calc(10.5px * var(--fs));letter-spacing:.3px;flex:none;
  max-width:96px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;
  padding:9px 10px;border:none;border-radius:9px;color:#fff;cursor:pointer;
  background:linear-gradient(var(--teal,#2f9e93),var(--teal-d,#237a72));box-shadow:0 2px 0 #1c5f59;}
.con-load:active{transform:translateY(2px);box-shadow:none;}
.con-load.buy{background:linear-gradient(var(--coin),#d99612);box-shadow:0 2px 0 #a06d08;
  text-shadow:0 1px 1px rgba(120,80,10,.45);}
.con-load.done{background:var(--pale2);color:var(--good-d);box-shadow:none;border:1.5px solid var(--line);}
.con-load.warn{background:var(--pale2);color:var(--bad);box-shadow:none;border:1.5px solid var(--bad);}
/* …and the sheet it opens. Rows reuse the rush picker's .rushpick-row/.rp-* styles. */
.loadpick-card{max-width:360px;text-align:left;}
.loadpick-card h2{text-align:center;margin-bottom:2px;}
.loadpick-card .lp-sub{text-align:center;font-size:calc(12px * var(--fs));margin:0 0 10px;display:flex;align-items:center;justify-content:center;gap:5px;}
.lp-sw{width:10px;height:10px;border-radius:50%;border:1px solid rgba(0,0,0,.25);display:inline-block;}
.lp-cost{display:flex;align-items:center;gap:6px;flex-wrap:wrap;justify-content:center;margin-bottom:10px;}
.lp-chip{display:inline-flex;align-items:center;gap:4px;background:var(--card);border:1.5px solid var(--line);
  border-radius:8px;padding:3px 8px;font-family:var(--font-head);font-weight:600;font-size:calc(11px * var(--fs));color:var(--ink);}
.lp-chip.tot{background:linear-gradient(var(--gold1),var(--gold2));border-color:var(--gold-line);color:var(--gold-ink);}
.loadpick-card .rushpick-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;background:var(--card);
  border:1.5px solid var(--line);border-radius:12px;padding:9px 11px;margin:8px 0 0;cursor:pointer;font:inherit;
  color:var(--ink);box-shadow:none;}
.loadpick-card .rushpick-row.off{opacity:.5;background:var(--pale2);cursor:default;}
/* lit: a row is a card, so it rests. The unavailable one keeps its flat face rather than
   sitting raised — same call as `.supply.locked`, which is the same idea one tab over. */
body.ui-lit .loadpick-card .rushpick-row{background:var(--card-face);box-shadow:var(--el-rest);
  border-width:1px;border-color:color-mix(in oklab,var(--line),#fff 30%);}
body.ui-lit .loadpick-card .rushpick-row.off{box-shadow:none;background:var(--pale2);}
.loadpick-card .rp-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.lp-go{flex:none;font-family:var(--font-head);font-weight:700;font-size:calc(10.5px * var(--fs));color:#fff;padding:6px 9px;border-radius:9px;
  background:linear-gradient(var(--teal,#2f9e93),var(--teal-d,#237a72));}
.lp-go.buy{background:linear-gradient(var(--coin),#d99612);}
.lp-no{flex:none;font-size:calc(10.5px * var(--fs));color:var(--ink-soft);text-align:right;line-height:1.3;font-style:italic;}
.lp-none{text-align:center;padding:14px 6px 2px;}
.lp-none .lp-lock{font-size:calc(26px * var(--fs));margin-bottom:6px;}
.lp-none p{font-size:calc(12.5px * var(--fs));line-height:1.5;}
.lp-none b{color:var(--ink);}
.loadpick-card .lp-cta{width:auto;padding:9px 16px;font-size:calc(12.5px * var(--fs));}
.loadpick-card .rw-skip{display:block;margin:10px auto 0;background:none;border:none;box-shadow:none;width:auto;
  padding:4px;color:var(--ink-soft);font-size:calc(13px * var(--fs));cursor:pointer;}
/* Next-step nudge ring (v6.98, ui/nudge.js) — ONE at a time, on the tab button or on the
   exact control once that tab is open. An OUTLINE, not a box-shadow: the buttons it lands on
   (con-load, btn-mgr, con-deliver) carry their own 3D lip in box-shadow and would lose it.

   It was var(--terra), and a player reported missing it entirely — "I didn't even see you do
   the money." That is a HUE COLLISION, not a brightness one: the ring's two most important
   targets, the cash pill and Buy x1, ARE terracotta, so it was drawing orange on orange. Shot
   side by side with tools/board3d-verify/shot_ring_colors.js; a halo alone did not save it.
   Three fixes, and the last two matter as much as the colour:
     * neon green — the one colour in play that no panel, button or board prop uses, and it
       reads as "go" next to --good. It also holds up when the night theme turns --terra purple.
     * the pulse floor is 60%, not 18%. At 18% the ring spent half of every cycle invisible.
     * a dark halo + soft glow, so it separates from cream panels AND from terracotta buttons.
   The halo is a FILTER, not a box-shadow, for the same reason the ring is an outline: these
   buttons keep their 3D lip in box-shadow and setting one here would erase it.
   REVERT SWITCH: set --nudge back to var(--terra) and the floor to 18%. */
:root{--nudge:#39ff14;}
/* THE GLOW IS NOT A FILTER. `filter: drop-shadow()` applies to the whole element SUBTREE and
   traces its alpha, so every icon and label inside the ringed card picked up a green halo of
   its own — a ringed printer row lit up its machine icon and its text, which read as a bug.
   A pseudo-element carries the glow instead: it sits on the border box only, leaves the
   contents completely untouched, and does not fight the elevation box-shadow that .printer,
   .upg and the buttons already wear under body.ui-lit. */
/* ⚠ `position:relative` here is load-bearing for the ::after glow — and it OVERRIDES whatever
   position the target had. Ringing an absolutely-positioned element therefore drops it into
   normal flow, where a block-level one stretches to full width: that is exactly what happened to
   the board's items readout ("the tutorial makes the per sec bar stretch far"). Every ring target
   must be a flex child or inline-block, never position:absolute. The board bar is a flex row now
   for this reason as well as the overlap one. */
.nudge-ring{outline:3px solid var(--nudge);outline-offset:2px;position:relative;
  animation:nudge-pulse 1.7s ease-in-out infinite;}
.nudge-ring::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  box-shadow:0 0 3px 1px rgba(20,10,4,.45),0 0 10px 3px color-mix(in oklab,var(--nudge) 60%,transparent);}
@keyframes nudge-pulse{
  0%,100%{outline-color:var(--nudge);outline-offset:2px;}
  50%{outline-color:color-mix(in oklab,var(--nudge) 60%,transparent);outline-offset:6px;}
}
/* The x/x counter is the one target that is a bare inline <span>, so the ring drew a
   hard-cornered box around the digits and read as a red square wedged between rounded pills.
   It gets the same rounding as everything else the ring lands on, and inline-block so the
   outline is one clean box instead of a run of inline fragments.
   The pulse ALSO stopped there: .con-count.bump flashes the number every time the count
   climbs, and at (0,2,0) it outranks .nudge-ring (0,1,0), so its `animation` REPLACED the
   pulse outright — during a print, which is exactly when the player is looking, the ring sat
   frozen. Both animations run together now. */
.con-count.nudge-ring{border-radius:7px;display:inline-block;}
.con-count.bump.nudge-ring{animation:con-bump .5s ease-out,nudge-pulse 1.7s ease-in-out infinite;}
@media (prefers-reduced-motion: reduce){.nudge-ring{animation:none;}}
/* Income sheet behind the board's items/rate chip — same card as the LOAD sheet. */
.inc-rate{font-family:var(--font-head);font-weight:700;font-size:calc(13px * var(--fs));color:var(--good-d);}
.income-card .lp-sub{font-size:calc(12.5px * var(--fs));}
.inc-warn{flex:none;font-size:calc(10.5px * var(--fs));color:var(--terra-ink,#c05f1e);font-style:normal;font-weight:700;text-align:right;}
.inc-h{font-family:var(--font-head);font-size:calc(12.5px * var(--fs));margin:16px 0 6px;color:var(--ink);}
.inc-il{display:flex;justify-content:space-between;gap:10px;font-size:calc(12px * var(--fs));color:var(--ink-soft);padding:3px 2px;}
.inc-il b{color:var(--ink);}
.inc-il.inc-more{font-style:italic;}
.inc-note{font-size:calc(11.5px * var(--fs));color:var(--ink-soft);margin:8px 2px 0;line-height:1.45;}
.income-card .lp-cta{display:block;margin:10px auto 0;}
/* The shelf, behind the board's 📦 chip. A full list rather than the top four with a "+N more"
   that went nowhere — if you tapped a stock count you wanted the stock. It scrolls, because a
   late shop holds thirty kinds of thing and the sheet must not grow past the screen. */
.stk-list{max-height:44vh;overflow-y:auto;margin:12px 0 4px;
  border-top:1.5px solid var(--line);border-bottom:1.5px solid var(--line);}
.stk-row{display:flex;align-items:center;gap:9px;padding:7px 3px;
  font-size:calc(12.5px * var(--fs));color:var(--ink-soft);}
.stk-row + .stk-row{border-top:1px solid color-mix(in oklab,var(--line),transparent 45%);}
.stk-sw{width:13px;height:13px;flex:none;border-radius:4px;border:1.5px solid rgba(0,0,0,.2);}
.stk-nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink);}
.stk-n{flex:none;font-variant-numeric:tabular-nums;}
.stk-w{flex:none;color:var(--good-d);font-family:var(--font-head);}
.inv-readout{cursor:pointer;}
.inv-readout:active{transform:translateY(1px);}
.contract-asp{border-color:var(--coin);box-shadow:0 0 0 1px var(--coin),0 2px 10px rgba(242,176,30,.28);}
.contract-asp .con-client{color:#b4820f;}
.con-actions{display:flex;gap:8px;}
.con-deliver{flex:1;font-family:var(--font-head);font-weight:700;font-size:calc(13px * var(--fs));
  background:linear-gradient(#5cb15f,var(--good));border:0;border-radius:12px;
  color:#fff;padding:11px;cursor:pointer;box-shadow:0 3px 0 var(--good-d);}
.con-deliver:active{transform:translateY(2px);box-shadow:none;}
.con-deliver:disabled{filter:grayscale(.5);opacity:.55;transform:none;}
.con-skip{flex:0 0 48px;font-family:var(--font-head);font-weight:700;font-size:calc(13px * var(--fs));
  background:linear-gradient(var(--pale1),var(--pale2));border:0;border-radius:12px;
  color:var(--ink-soft);cursor:pointer;box-shadow:0 3px 0 var(--line);}
.con-skip:active{transform:translateY(2px);box-shadow:none;}

/* ---------- lit skin: Contracts (docs/UI_STANDARDS.md step 5; config.js UI_LIT reverts it) ----------
   A contract is a job card: raised, with the requirement's progress cut into it as a channel and
   the model thumbnail sitting in a well. DELIVER, LOAD and skip take the button anatomy. The
   aspirational (gold-starred) contract keeps its coin edge but loses the outer glow — with a
   shadow under every card, a second halo just made it blurry. */
body.ui-lit .contract{border-width:1px;border-color:color-mix(in oklab,var(--line),#fff 30%);
  background:var(--card-face);box-shadow:var(--el-rest);}
body.ui-lit .contract-asp{border-color:var(--coin);box-shadow:var(--el-rest),0 0 0 1px var(--coin);}
body.ui-lit .con-bar{border:none;box-shadow:var(--el-well);background:var(--pale2);}
body.ui-lit .con-fill{box-shadow:inset 0 1px 0 rgba(255,255,255,.5);}
body.ui-lit .con-ic{background:var(--well-face);box-shadow:var(--el-well);border-radius:9px;
  width:34px;height:34px;display:grid;place-items:center;}
body.ui-lit .con-ic canvas{filter:drop-shadow(0 2px 2px rgba(60,30,10,.30));}
/* the filament dot on a requirement row is a spool end, like everywhere else */
body.ui-lit .con-fil i{border-color:rgba(255,255,255,.8);
  box-shadow:0 1px 2px rgba(90,60,30,.32),inset 0 -1px 2px rgba(0,0,0,.22);}
body.ui-lit .con-load{box-shadow:0 2px 0 var(--btn-teal-base),var(--btn-rim);background:var(--btn-teal);}
body.ui-lit .con-load.buy{background:var(--btn-gold);box-shadow:0 2px 0 var(--btn-gold-base),var(--btn-rim);}
/* done and warn are states, not offers: flat, recessed, no base to press */
body.ui-lit .con-load.done,body.ui-lit .con-load.warn{border:none;background:var(--pale2);box-shadow:var(--el-well);}
body.ui-lit .con-load:active{box-shadow:var(--btn-rim);}
body.ui-lit .con-deliver{box-shadow:0 3px 0 var(--good-d),var(--btn-rim),0 2px 6px rgba(90,60,30,.20);}
body.ui-lit .con-skip{background:var(--btn-pale);
  box-shadow:0 3px 0 var(--btn-pale-base),var(--btn-rim),0 2px 6px rgba(90,60,30,.14);}
body.ui-lit .con-deliver:active,body.ui-lit .con-skip:active{box-shadow:var(--btn-rim);}

/* ---------- prestige ---------- */
.prestige-box{background:var(--card);border:1.5px solid var(--line);border-radius:15px;
  box-shadow:0 2px 6px rgba(60,30,10,.08);
  padding:15px;text-align:center;margin-bottom:16px;}
.prestige-box h2{margin:0 0 8px;font-family:var(--font-head);font-weight:700;font-size:calc(15px * var(--fs));color:var(--terra-ink);}
.prestige-box p{font-size:calc(12.5px * var(--fs));color:var(--ink-soft);margin:6px 0;}
.ip-gain{font-size:calc(14px * var(--fs)) !important;color:var(--ink) !important;}
/* the "is franchising worth it yet" line under the gain — advisory, never a blocker */
/* Head Start deserves its own line, not a clause: "you don't restart in the Garage" is the
   single fact that decides whether franchising feels survivable. */
.ip-head{font-size:calc(12px * var(--fs)) !important;color:var(--ink) !important;margin:0 0 8px !important;
  background:var(--gold1);border:1.5px solid var(--gold-line);border-radius:9px;
  padding:6px 9px;line-height:1.35;}
.sub{font-size:calc(12px * var(--fs)) !important;}
#prestige-btn{margin-top:10px;background:linear-gradient(var(--terra),var(--terra-d));border:0;border-radius:13px;
  color:#fff;font-family:var(--font-head);font-weight:700;
  padding:13px;font-size:calc(13.5px * var(--fs));cursor:pointer;width:100%;box-shadow:0 3px 0 var(--wood-dd);}
#prestige-btn:active{transform:translateY(2px);box-shadow:none;}
#prestige-btn:disabled{filter:grayscale(.5);opacity:.55;transform:none;}
/* lit: the box is a card that already carried a weak shadow — normalise it onto --el-rest
   rather than leave a third elevation in the file (rule 2). */
body.ui-lit .prestige-box{background:var(--card-face);box-shadow:var(--el-rest);
  border-width:1px;border-color:color-mix(in oklab,var(--line),#fff 30%);}
/* the one genuinely flat button in this sheet: it had a base but no rim and no ambient */
body.ui-lit #prestige-btn{background:var(--btn-terra);
  box-shadow:0 3px 0 var(--btn-terra-base),var(--btn-rim),0 2px 6px rgba(90,60,30,.20);}
body.ui-lit #prestige-btn:active{box-shadow:var(--btn-rim);}
body.ui-lit #prestige-btn:disabled{box-shadow:0 3px 0 var(--btn-terra-base),var(--btn-rim);}

/* ---------- supplies tab ---------- */
/* The filament list goes TWO-UP once the drawer is wide enough (v6.132). At the phone widths
   the game is actually built for it stays one column — this exists only to close the dead
   zone a 960px-wide row opened between the stock line and the buy button. Scoped to
   #filament-list so the catalog grid (.supply.cat-cell) is untouched. Revert: drop this block. */
#filament-list{display:grid;grid-template-columns:1fr;gap:9px;}
#filament-list .supply{margin-bottom:0;}
@media (min-width:620px){#filament-list{grid-template-columns:1fr 1fr;}}
.supply{background:var(--card);border:1.5px solid var(--line);border-radius:13px;
  box-shadow:0 2px 6px rgba(60,30,10,.06);
  padding:9px 10px;margin-bottom:9px;display:flex;align-items:center;gap:10px;}
.supply.active-fil{border-color:var(--teal);box-shadow:0 0 0 1px var(--teal),0 2px 6px rgba(60,30,10,.06);}
/* filament swatch drawn as a spool (colored winding + hub) */
.swatch{width:40px;height:40px;flex:none;border-radius:50%;position:relative;
  border:2px solid rgba(0,0,0,.15);box-shadow:inset 0 0 0 3px rgba(255,255,255,.3);
  display:flex;align-items:center;justify-content:center;font-size:0;}
.swatch::after{content:"";position:absolute;inset:12px;background:var(--cream);border-radius:50%;
  border:2px solid rgba(0,0,0,.15);}
.swatch.shelved{filter:grayscale(.5);opacity:.7;}   /* owned, but this board cannot melt it */
.swatch.locked{filter:grayscale(.35) brightness(.9);}
.swatch.locked::after{content:"🔒";display:grid;place-items:center;font-size:calc(11px * var(--fs));inset:9px;}
.cat-ic{width:36px;height:36px;flex:none;}
.cat-ic canvas{width:36px;height:36px;}
.supply-body{flex:1;min-width:0;}
.supply-name{font-family:var(--font-head);font-weight:600;font-size:calc(12.5px * var(--fs));color:var(--ink);}
/* the multiplier is one token — it broke as "CF-Nylon ×34 / value" once the type grew */
.supply-name .mult{color:var(--teal);font-weight:600;white-space:nowrap;}
.supply-sub{font-size:calc(11.5px * var(--fs));color:var(--ink-soft);margin-top:3px;}
.supply-btns{display:flex;flex-direction:column;gap:5px;align-items:stretch;}
.sbtn{font-family:var(--font-head);font-weight:700;font-size:calc(10.5px * var(--fs));border:0;border-radius:10px;
  cursor:pointer;padding:7px 10px;white-space:nowrap;line-height:1.25;}
.sbtn:active{transform:translateY(1px);box-shadow:none;}
.sbtn:disabled{filter:grayscale(.5);opacity:.55;transform:none;}
.sbtn small{display:block;font-weight:500;font-size:calc(9px * var(--fs));opacity:.9;margin-top:2px;}
.sbtn.select{background:linear-gradient(var(--teal),var(--teal-d));color:#fff;box-shadow:0 2px 0 #17564f;}
.sbtn.buy{background:linear-gradient(var(--terra),var(--terra-d));color:#fff;box-shadow:0 2px 0 var(--wood-dd);}
/* Unlock was the NEUTRAL pale button (--pale1/--pale2), sitting on a WHITE model card — so it
   read as a shaded strip rather than a control: "it doesn't look like an unlock, it blends in
   with the card". It is a purchase, so it takes the purchase anatomy, reusing .sbtn.buy's
   terracotta rather than inventing a fourth button colour. The lock makes the ACT legible
   instead of only the price. Filament, models and auto-supply share this class on purpose —
   one look for "spend cash to open this up" (user: "same for filament to stay consistent"). */
.sbtn.unlock{background:linear-gradient(var(--terra),var(--terra-d));color:#fff;
  box-shadow:0 2px 0 var(--wood-dd);border:0;}
.sbtn.unlock::before{content:'🔓 ';}
/* auto-supply borrows the class but its label says Buy — a lock on it would be a lie */
.owned-tag{font-family:var(--font-head);font-weight:700;font-size:calc(10.5px * var(--fs));color:var(--good);}
.owned-tag.idle{color:var(--ink-soft);font-size:calc(9.5px * var(--fs));white-space:nowrap;}   /* auto-supply waiting on a load */
.supply.locked{opacity:.82;}
.supply.locked .cat-ic{filter:grayscale(.55) brightness(.95);}
.locked-tag{font-family:var(--font-head);font-weight:700;font-size:calc(9px * var(--fs));color:var(--ink-soft);text-align:center;line-height:1.3;white-space:nowrap;}

/* ---------- lit skin: Supplies (docs/UI_STANDARDS.md step 4; config.js UI_LIT reverts it) ----------
   Same two elevations. The Filament/Models switch becomes a groove with one raised pill (the
   same shape as the ×1/×10 segment on Printers, so one control reads one way everywhere), the
   spool and the model art each get a well to sit in, and the three small buttons take the
   button anatomy at 2px — they are small, so the base is proportional. */
body.ui-lit .supply{border-width:1px;border-color:color-mix(in oklab,var(--line),#fff 30%);
  background:var(--card-face);box-shadow:var(--el-rest);}
/* the loaded filament: a teal edge, no second glow ring — the shadow already separates it */
body.ui-lit .supply.active-fil{border-color:var(--teal);box-shadow:var(--el-rest),0 0 0 1px var(--teal);}
body.ui-lit .subtabs{background:var(--pale2);box-shadow:var(--el-well);}
body.ui-lit .sub-btn.active{background:var(--card-face);box-shadow:var(--btn-rim),0 2px 5px rgba(90,60,30,.22);}
/* rule 5 — the spool is an object: a lit winding over a shaded rim, with its own cast shadow */
body.ui-lit .swatch{border-color:rgba(255,255,255,.75);
  box-shadow:inset 0 0 0 3px rgba(255,255,255,.35),inset 0 -3px 5px rgba(0,0,0,.25),0 2px 4px rgba(90,60,30,.30);}
/* the print sits in the card, not on it */
body.ui-lit .cat-cell .cat-ic{background:var(--well-face);box-shadow:var(--el-well);border-radius:12px;
  width:76px;height:76px;display:grid;place-items:center;}
body.ui-lit .cat-cell .cat-ic canvas{filter:drop-shadow(0 3px 3px rgba(60,30,10,.30));}
body.ui-lit .sbtn.select{background:var(--btn-teal);box-shadow:0 2px 0 var(--btn-teal-base),var(--btn-rim);}
body.ui-lit .sbtn.buy{background:var(--btn-terra);box-shadow:0 2px 0 var(--btn-terra-base),var(--btn-rim);}
body.ui-lit .sbtn.unlock{background:var(--btn-terra);border:none;box-shadow:0 2px 0 var(--btn-terra-base),var(--btn-rim);}
body.ui-lit .sbtn:active{box-shadow:var(--btn-rim);}
/* auto-supply is a setting, not a card — it reads as a slot cut into the panel */
/* a stamped pip is a spool end too; an unstamped one stays an empty socket */
body.ui-lit .pip{border-color:rgba(255,255,255,.7);box-shadow:0 1px 2px rgba(90,60,30,.30),inset 0 -1px 2px rgba(0,0,0,.25);}
body.ui-lit .pip.off{border:1.5px dashed #cbb891;box-shadow:var(--el-well);}
body.ui-lit .pip.part{box-shadow:var(--el-well);}   /* part-lit sits in the well, not raised */
/* locked: dimmed and flat, like a locked machine */
body.ui-lit .supply.locked{box-shadow:none;background:var(--chip-face);}

/* ---------- E5 collection stamps (catalog) ---------- */
/* Twelve filaments is twelve dots in a cell half a phone wide, so the row has to break — but a
   free-flowing wrap broke wherever it ran out of room and left a ragged last line. A fixed
   6-column grid gives the full roster an even 6-over-6 (user 2026-08-26: "id rather a 6 over 6
   layout so its balanced"), and any shorter roster still centres. */
.pips{display:grid;grid-template-columns:repeat(6,auto);justify-content:center;
  gap:5px;margin-top:5px;max-width:100%;}
.pip{width:11px;height:11px;border-radius:50%;border:1.5px solid rgba(0,0,0,.18);}
.pip.off{background:transparent;border:1.5px dashed #cbb891;}
/* run started but short of STAMP_QTY — the colour is there, drained, so an unfinished run
   reads as different from both "never touched" and "done" */
.pip.part{opacity:.42;border-style:dotted;}
.stamp-seal{flex:none;width:50px;height:50px;transform:rotate(-10deg);
  border:2.5px solid #b4820f;border-radius:50%;display:grid;place-items:center;text-align:center;
  color:#b4820f;font-family:var(--font-head);font-weight:700;font-size:calc(7px * var(--fs));line-height:1.15;letter-spacing:.4px;
  background:radial-gradient(circle at 50% 38%,#fff8e2 0 60%,#f7e7b6 100%);
  box-shadow:0 0 0 2px #fff inset,0 1px 3px rgba(60,30,10,.2);}
.stamp-seal b{font-size:calc(14px * var(--fs));display:block;line-height:1;}

.mini{background:var(--card);color:var(--ink);border:1.5px solid var(--line);border-radius:10px;
  font-family:var(--font-head);font-weight:600;
  padding:6px 12px;font-size:calc(11px * var(--fs));cursor:pointer;box-shadow:0 2px 0 var(--line);}
.mini:active{transform:translateY(1px);box-shadow:none;}

/* ---------- modal ---------- */
.modal{position:fixed;inset:0;background:rgba(42,26,12,.55);backdrop-filter:blur(2px);
  display:flex;align-items:center;justify-content:center;z-index:50;padding:20px;}
.modal.hidden{display:none;}
.modal-card{background:var(--cream);border:1.5px solid var(--line);border-radius:20px;
  box-shadow:0 14px 40px rgba(50,25,8,.35);
  padding:20px;text-align:center;max-width:340px;width:100%;max-height:88vh;overflow-y:auto;-webkit-overflow-scrolling:touch;}
.modal-card h2{margin:0 0 10px;font-family:var(--font-head);font-weight:700;font-size:calc(17px * var(--fs));color:var(--terra-ink);}
.modal-card p{color:var(--ink-soft);font-size:calc(13px * var(--fs));margin:6px 0;}
.offline-earn{font-size:calc(26px * var(--fs)) !important;color:var(--good) !important;font-family:var(--font-head);font-weight:700;}
.modal-card button{margin-top:12px;background:linear-gradient(var(--terra),var(--terra-d));border:0;border-radius:13px;
  color:#fff;font-family:var(--font-head);font-weight:700;
  padding:13px;font-size:calc(13.5px * var(--fs));cursor:pointer;width:100%;box-shadow:0 3px 0 var(--wood-dd);}
.modal-card button:active{transform:translateY(2px);box-shadow:none;}
/* lit: the sheet floats (rule 2) and its border steps back now the shadow separates it (rule 6).
   DELIBERATELY NOT a blanket `body.ui-lit .modal-card button` rule. The flat one above is a
   legacy catch-all that almost everything inside a sheet already overrides — the store's
   .store-btn set, the teal .upg-buy.prestige rungs, the picker cells, the .rw-skip text links.
   A lit copy of it out-specifies every one of them and would repaint the lot terracotta. Each
   button that actually needs the anatomy gets it beside its own rule instead. */
body.ui-lit .modal-card{background:var(--drawer-face);
  box-shadow:var(--el-float),0 18px 44px rgba(50,25,8,.34);
  border-width:1px;border-color:color-mix(in oklab,var(--line),#fff 30%);}

/* ---------- toast ---------- */
#toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(20px);
  background:var(--glass-solid);color:var(--glass-ink);padding:10px 18px;
  font-family:var(--font-head);font-weight:600;font-size:calc(12.5px * var(--fs));border-radius:16px;
  border:1.5px solid var(--glass-line);
  box-shadow:0 6px 18px color-mix(in oklab,rgba(50,25,8,.35),rgba(12,8,40,.5) calc(var(--ui-night)*100%));
  /* opacity/transform only — the colours ride --ui-night, so don't transition them here */
  opacity:0;transition:opacity .2s,transform .2s;pointer-events:none;z-index:60;}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0);}

.float-cash{position:absolute;font-family:var(--font-head);font-weight:700;font-size:calc(12px * var(--fs));color:#6ee86e;pointer-events:none;
  text-shadow:0 1px 2px rgba(0,0,0,.6);animation:floatUp 1s ease-out forwards;}
@keyframes floatUp{from{opacity:1;transform:translateY(0);}to{opacity:0;transform:translateY(-34px);}}
@keyframes pulse{0%,100%{box-shadow:0 0 4px #6ee86e88;}50%{box-shadow:0 0 12px #6ee86e;}}

/* ---------- market mini-game ---------- */
.market-card{background:var(--cream);border:1.5px solid var(--line);border-radius:20px;
  box-shadow:0 14px 40px rgba(50,25,8,.35);
  padding:16px;max-width:440px;width:100%;position:relative;max-height:88vh;overflow-y:auto;-webkit-overflow-scrolling:touch;}
/* lit: surface only. Its buttons already carry a gradient body and a hard base, and .perm-card
   and .prestige-box already read as raised — this sheet was mostly converted already. */
body.ui-lit .market-card{background:var(--drawer-face);
  box-shadow:var(--el-float),0 18px 44px rgba(50,25,8,.34);
  border-width:1px;border-color:color-mix(in oklab,var(--line),#fff 30%);}
.market-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;}
.market-top h2{margin:0;font-family:var(--font-head);font-weight:700;font-size:calc(16px * var(--fs));color:var(--ink);}
#market-close,#hq-close,#franchise-close{background:linear-gradient(#e0685f,var(--bad));border:0;border-radius:10px;color:#fff;
  font-family:var(--font-head);font-weight:700;font-size:calc(13px * var(--fs));padding:7px 11px;cursor:pointer;
  box-shadow:0 2px 0 #a13a36;}
#market-close:active,#hq-close:active,#franchise-close:active{transform:translateY(2px);box-shadow:none;}
/* Three STACKED chips, label over value (v6.168). They used to sit label-beside-value on one
   nowrap line with a 6px gap: on a ~360px phone "Earned $1.42M ⏱1:07 Combo ×5.8" ran together as
   one unreadable string and the earned figure ellipsised away (user, on the device). Stacking
   buys the width back without a second line — the row's HEIGHT is fixed either way, so the modal
   still never resizes on a sale, which is what the old nowrap rule was protecting. */
.market-stats{display:flex;flex-wrap:nowrap;align-items:flex-start;justify-content:space-between;gap:8px;
  font-size:calc(11.5px * var(--fs));color:var(--ink-soft);margin-bottom:12px;}
/* equal thirds, so hiding the timer outside a rush just re-centres the other two */
.market-stats > span{flex:1 1 0;min-width:0;display:flex;flex-direction:column;align-items:center;gap:1px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.market-stats > span b{font-size:calc(15px * var(--fs));}
.combo-box b.combo-hot{color:var(--bad);}
.good{color:var(--good);}

/* Market Day (v6.11): tab badge + the ready-bell / closed-sign gate faces */
#tab-market-btn{position:relative;}
.market-dot{display:none;position:absolute;top:5px;right:16px;width:9px;height:9px;border-radius:50%;}
.market-dot.ready{display:block;background:#6ee86e;border:1px solid #063;animation:mdot-pulse 1.2s infinite;}
.market-dot.live{display:block;background:#ffab3d;border:1px solid #7a4a00;animation:mdot-pulse .55s infinite;}
@keyframes mdot-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.4)}}
.mtimer b{font-variant-numeric:tabular-nums;color:var(--ink);}
.mtimer b.mt-low{color:var(--bad);}
.mgate{text-align:center;padding:16px 12px 20px;}
.mgate-ico{font-size:calc(42px * var(--fs));line-height:1;margin-bottom:8px;}
.mgate-title{font-family:var(--font-head);font-weight:700;font-size:calc(16px * var(--fs));letter-spacing:.6px;color:var(--terra-ink);}
.mgate-sub{font-size:calc(13px * var(--fs));color:var(--ink-soft);margin:7px auto 14px;max-width:300px;}
.mgate-sub b{color:var(--ink);font-variant-numeric:tabular-nums;}
.mgate-btn{width:100%;max-width:280px;}
.mgate-hint{font-size:calc(11.5px * var(--fs));color:var(--ink-soft);margin-top:10px;}
/* the closed sign's ad bell: same shape as OPEN THE STALL, deliberately NOT the green one —
   green is the button that sells, and an ad button wearing it reads like the reward is the sale */
.mgate-btn.ad{margin-top:4px;background:linear-gradient(var(--pale1),var(--pale2));color:var(--ink);
  box-shadow:0 3px 0 var(--line);}
.mgate-btn.ad:disabled{filter:none;opacity:.5;}

/* shelf strip — what you have to sell */
.inv-strip-label{font-family:var(--font-head);font-weight:600;font-size:calc(10px * var(--fs));letter-spacing:1px;color:var(--ink-soft);margin-bottom:6px;}
#market-inv-strip{display:flex;gap:6px;flex-wrap:nowrap;overflow-x:auto;background:var(--cream2);
  border:1.5px solid var(--line);border-radius:12px;padding:7px;margin-bottom:14px;height:48px;align-items:center;}
.shelf-chip{flex:none;display:flex;align-items:center;gap:3px;background:var(--card);border:1.5px solid var(--line);
  border-radius:9px;padding:2px 6px 2px 2px;font-size:calc(12px * var(--fs));color:var(--ink);}
.shelf-chip canvas{width:30px;height:30px;}
.shelf-empty{color:var(--ink-soft);font-size:calc(12px * var(--fs));padding:4px 8px;}

/* single customer */
#market-customer{height:194px;overflow:hidden;position:relative;}
.cust-card{display:flex;align-items:center;gap:12px;background:var(--card);border:1.5px solid var(--line);
  border-radius:15px;padding:12px;height:84px;overflow:hidden;box-shadow:0 2px 6px rgba(60,30,10,.08);}
.cust-avatar{font-size:calc(36px * var(--fs));flex:none;width:54px;height:54px;display:flex;align-items:center;justify-content:center;
  background:var(--cream2);border:1.5px solid var(--line);border-radius:12px;}
.cust-body{flex:1;min-width:0;}
.cust-line{font-size:calc(13px * var(--fs));color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.cust-item{display:flex;align-items:center;gap:6px;font-size:calc(12px * var(--fs));color:var(--ink);margin:4px 0;white-space:nowrap;overflow:hidden;}
.cust-item canvas{width:30px;height:30px;flex:none;}
.cust-item b{font-family:var(--font-head);font-weight:600;font-size:calc(12.5px * var(--fs));overflow:hidden;text-overflow:ellipsis;}
.cust-offer{font-size:calc(11.5px * var(--fs));color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.cust-offer .demand{color:var(--terra-ink);font-weight:700;margin-left:4px;}
.cust-offer .demand.demand-hot{color:var(--bad);}
.cust-pay{flex:none;min-width:88px;text-align:right;font-family:var(--font-head);font-weight:700;font-size:calc(15px * var(--fs));color:var(--good);}
.cust-pay small{display:block;font-size:calc(9px * var(--fs));font-weight:500;color:var(--ink-soft);margin-bottom:3px;}
.cust-empty{color:var(--ink-soft);font-size:calc(13px * var(--fs));text-align:center;padding:22px 10px;}
.cust-empty small{font-size:calc(11.5px * var(--fs));display:block;margin-top:8px;}
.pat-wrap{height:9px;background:var(--cream2);border:1px solid var(--line);border-radius:5px;margin:12px 0;overflow:hidden;}
.pat-bar{height:100%;width:100%;border-radius:5px;background:linear-gradient(90deg,var(--coin),#f2c552);transition:width .12s linear;}
.cust-actions{display:flex;gap:8px;}
.cust-sell{flex:1;font-family:var(--font-head);font-weight:700;font-size:calc(15px * var(--fs));
  background:linear-gradient(#5cb15f,var(--good));border:0;border-radius:14px;
  color:#fff;padding:14px 6px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  box-shadow:0 3px 0 var(--good-d);}
.cust-sell:active{transform:translateY(2px);box-shadow:none;}
.cust-sell:disabled{filter:grayscale(.5);opacity:.55;transform:none;}
.cust-skip{flex:0 0 96px;font-family:var(--font-head);font-weight:600;font-size:calc(12px * var(--fs));
  background:linear-gradient(var(--pale1),var(--pale2));border:0;border-radius:14px;
  color:var(--ink-soft);padding:14px 6px;cursor:pointer;box-shadow:0 3px 0 var(--line);}
.cust-skip:active{transform:translateY(2px);box-shadow:none;}
#market-fx{position:absolute;inset:0;pointer-events:none;overflow:hidden;}
.mfloat{position:absolute;left:50%;top:46%;transform:translateX(-50%);font-family:var(--font-head);font-weight:700;
  font-size:calc(14px * var(--fs));color:var(--good);animation:mfloat .9s ease-out forwards;white-space:nowrap;}
.mfloat.t2{font-size:calc(16px * var(--fs));color:#c98a10;}
.mfloat.t3{font-size:calc(18px * var(--fs));color:var(--terra-ink);}
.mfloat.t4{font-size:calc(20px * var(--fs));color:var(--bad);}
@keyframes mfloat{from{opacity:1;transform:translate(-50%,0) scale(1);}to{opacity:0;transform:translate(-50%,-52px) scale(1.2);}}

/* combo meter — colour heats up with the streak */
.combo-box{display:flex;align-items:center;gap:3px;}
#market-combo{display:inline-block;font-family:var(--font-head);font-weight:700;font-size:calc(14px * var(--fs));transition:color .15s;}
#market-combo.ct0{color:var(--ink-soft);}
#market-combo.ct1{color:#b4820f;}
#market-combo.ct2{color:#d9930f;}
#market-combo.ct3{color:var(--terra-ink);}
#market-combo.ct4{color:var(--bad);}
.combo-bar-wrap{width:54px;height:8px;background:var(--cream2);border:1px solid var(--line);border-radius:5px;overflow:hidden;flex:none;}
#market-combo-bar{display:block;height:100%;width:0;border-radius:5px;background:var(--terra);transition:width .1s linear;}

/* streak callouts — brief centered text on milestones only */
.mcallout{position:absolute;left:50%;top:34%;transform:translate(-50%,-50%);font-family:var(--font-head);font-weight:700;
  white-space:nowrap;pointer-events:none;z-index:6;animation:mcallout .7s ease-out forwards;}
.mcallout.t2{font-size:calc(16px * var(--fs));color:#c98a10;}
.mcallout.t3{font-size:calc(18px * var(--fs));color:var(--terra-ink);}
.mcallout.t4{font-size:calc(20px * var(--fs));color:var(--bad);}
@keyframes mcallout{0%{opacity:0;transform:translate(-50%,-50%) scale(.7);}30%{opacity:1;transform:translate(-50%,-50%) scale(1);}70%{opacity:1;}100%{opacity:0;transform:translate(-50%,-64px) scale(1);}}


/* ---------- Maya, the guide (ui/guide.js, docs/GUIDE_ARC.md) ----------
   The strip must never block the board: it is a floating card with pointer events only on
   itself, sitting just above the drawer. guide.js keeps `bottom` in step with the drawer's
   height so it tracks open/closed without the two ever overlapping. */
.guide-strip{
  position:fixed;left:12px;right:12px;bottom:80px;z-index:20;
  display:flex;align-items:center;gap:12px;
  padding:11px 14px;border-radius:16px;
  background:var(--cream);border:1.5px solid var(--line);
  box-shadow:0 6px 18px rgba(50,25,8,.22);
  cursor:pointer;animation:guide-in .22s ease-out;
}
.guide-strip.hidden{display:none;}
.guide-face{width:52px;height:52px;flex:none;border-radius:50%;
  border:3px solid var(--terra);background:var(--card);}
.guide-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;}
.guide-text b{font-family:var(--font-head);font-weight:700;font-size:calc(11px * var(--fs));letter-spacing:.4px;
  text-transform:uppercase;color:var(--terra-ink);}
.guide-text span{font-size:calc(13px * var(--fs));line-height:1.3;color:var(--ink);}
.guide-go{flex:none;color:var(--line);font-size:calc(15px * var(--fs));}
@keyframes guide-in{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}

/* first-launch modal — the one interrupt she gets */
.guide-card{position:relative;}
.guide-hero{width:150px;height:150px;border-radius:50%;display:block;margin:2px auto 12px;
  border:4px solid var(--terra);background:var(--card);}
.guide-card h2{font-size:calc(19px * var(--fs));line-height:1.25;color:var(--terra-ink);margin-bottom:8px;}
.guide-body{font-size:calc(13.5px * var(--fs));line-height:1.4;color:var(--ink);margin-bottom:16px;}
/* `.modal-card button` paints every button terracotta and full-width (that rule is 0,1,1),
   so this has to out-specify it rather than just set a background — same trap as .pick-cell. */
.guide-card .guide-skip{position:absolute;top:8px;right:10px;background:none;border:none;
  box-shadow:none;color:var(--ink-soft);font:inherit;font-size:calc(12px * var(--fs));font-weight:600;
  padding:4px 6px;margin:0;cursor:pointer;text-shadow:none;width:auto;display:inline-block;}

/* "printing for" chip on the printer card (contracts.js printerJobs) — the reverse of the
   printer name the contract row already shows. Muted on purpose: it reports a fact, it is
   not a call to action, so it must not compete with the row's buttons. */
.printer-job{display:flex;flex-wrap:wrap;gap:5px;margin-top:3px;}
.printer-job .pj{display:inline-flex;align-items:center;gap:3px;
  background:var(--cream2);border:1px solid var(--line);border-radius:8px;
  padding:1px 6px;font-size:calc(10px * var(--fs));font-weight:600;color:var(--ink-soft);
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.printer-job .pj.rush{background:var(--gold1);border-color:var(--gold-line);color:var(--gold-ink);}

/* the ×N badge flashes on every purchase — see updatePrinters. The board's extra printer
   copies only appear at much higher counts, so this is the one thing that visibly moves
   each time you buy, and a playtester missed it entirely. */
.printer-count.bump{animation:count-bump .5s ease-out;}
@keyframes count-bump{
  0%{transform:scale(1);background:var(--teal);}
  35%{transform:scale(1.45);background:var(--terra);box-shadow:0 0 0 4px rgba(224,122,47,.28);}
  100%{transform:scale(1);background:var(--teal);}
}

/* the contract counter flashes as it climbs, same reasoning as .printer-count.bump */
.con-count.bump{animation:con-bump .5s ease-out;display:inline-block;}
@keyframes con-bump{
  0%{transform:scale(1);}
  35%{transform:scale(1.35);color:var(--terra-ink);}
  100%{transform:scale(1);}
}

/* "already on another order" line in the LOAD printer picker — you are about to take this
   machine off whatever it was doing, so the sheet has to say what that is. */
.rp-busy{display:block;font-size:calc(10.5px * var(--fs));font-weight:600;color:var(--terra-ink);margin-top:1px;}

/* per-machine income, pushed to the top-right of the printer card. Managed printers only —
   see printerRate(). Green like every other earned number in the drawer. */
.printer-rate{margin-left:auto;flex:none;font-family:var(--font-head);font-weight:700;
  font-size:calc(11.5px * var(--fs));color:var(--good);background:var(--cream2);border:1px solid var(--line);
  border-radius:8px;padding:1px 7px;}

/* hand-run machines quote what one press pays instead of a rate (see printerJobPay) — same
   pill, calmer colour, because it is a per-tap figure and not passive income. */
.printer-rate.perjob{color:var(--terra-ink);}

/* The filament bill on the income sheet (v6.155). Income used to be shown gross, with the
   auto-supply spend invisible — so a richer material looked like pure upside. Itemised by
   filament, because that is the choice it informs. */
.inc-bill{margin-top:10px;padding-top:9px;border-top:1.5px dashed var(--line);
  display:flex;flex-direction:column;gap:5px;}
.inc-billrow{display:flex;align-items:center;justify-content:space-between;gap:10px;
  font-size:calc(12.5px * var(--fs));color:var(--ink-soft);}
.inc-billnm{display:flex;align-items:center;gap:7px;}
.inc-billnm i{width:9px;height:9px;border-radius:50%;display:inline-block;flex:none;
  border:1px solid rgba(0,0,0,.25);}
.inc-billv{font-variant-numeric:tabular-nums;white-space:nowrap;}
.inc-billrow.net{margin-top:3px;padding-top:6px;border-top:1px solid var(--line);
  color:var(--ink);font-weight:700;font-family:var(--font-head);
  font-size:calc(13.5px * var(--fs));}

/* The contract fuse (v6.156). Cards rotate now, so each one says how long it has left; a card a
   machine is loaded onto says so instead, because its clock is frozen while it is being worked. */
.con-fuse{font-family:var(--font-head);font-size:calc(10.5px * var(--fs));font-weight:600;
  letter-spacing:.04em;color:var(--ink-soft);opacity:.75;margin:-2px 0 4px;}
.con-fuse.hold{color:var(--teal-d,#17564f);opacity:.9;}
.con-fuse.urgent{color:#c0392b;opacity:1;animation:fusePulse 1.4s ease-in-out infinite;}
@keyframes fusePulse{0%,100%{opacity:1}50%{opacity:.45}}
@media (prefers-reduced-motion: reduce){.con-fuse.urgent{animation:none}}

/* The premium window line at the top of the contracts tab (v6.156). Quiet when the window is
   hours away, gold and awake when it is open. */
.con-window{font-family:var(--font-head);font-weight:700;font-size:calc(11px * var(--fs));
  letter-spacing:.04em;color:var(--ink-soft);background:var(--pale2);
  border:1px solid var(--line);border-radius:9px;padding:6px 10px;margin-bottom:9px;text-align:center;}
.con-window.open{color:var(--gold-ink,#b4820f);background:linear-gradient(var(--gold1,#fff8e2),var(--gold2,#f7e7b6));
  border-color:var(--gold-line,#e6c96a);}
