/* =====================================================
   The Commit Café — a cozy software engineer simulator
   ===================================================== */

:root {
  /* morning rush at the café */
  --room-top: #f9eeda;
  --room-bottom: #f1dfc4;
  --paper: #fffaf0;
  --paper-deep: #f3e8d3;
  --frame: #6b4a35;
  --frame-dark: #503626;
  --frame-light: #9c7150;
  --lamp: #ffc873;
  --accent: #2e7d6e;
  --accent-2: #b3593f;
  --syntax-str: #2e7d6e;
  --syntax-key: #7350a8;
  --text: #43301f;
  --muted: #8a7154;
  --term-bg: #2b2622;
  --term-text: #e8dcc5;
  --term-green: #9ece7f;
  --chunk: rgba(80, 54, 38, 0.22);
  --glyph: rgba(107, 74, 53, 0.18);
  --awn-a: #2e7d6e;
  --awn-b: #f6efdf;
  --chalk-bg: #33403a;
  --chalk-frame: #503626;
  --chalk-text: #f0ead8;
  --ring: rgba(107, 74, 53, 0.14);

  --head-font: "Pixelify Sans", "Courier New", monospace;
  --body-font: "Nunito", "Avenir Next", -apple-system, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
  --maxw: 820px;
}

/* evening café, string lights on */
@media (prefers-color-scheme: dark) {
  :root {
    --room-top: #241d1a;
    --room-bottom: #2e2521;
    --paper: #382e28;
    --paper-deep: #2f2620;
    --frame: #55402f;
    --frame-dark: #402f22;
    --frame-light: #7a5c42;
    --lamp: #f5c469;
    --accent: #8fcfbb;
    --accent-2: #f0a58f;
    --syntax-str: #a3d9a5;
    --syntax-key: #c0a8f0;
    --text: #efe6d4;
    --muted: #b0a08a;
    --term-bg: #1c1714;
    --term-text: #d8d0c0;
    --term-green: #9ece7f;
    --chunk: rgba(0, 0, 0, 0.42);
    --glyph: rgba(239, 230, 212, 0.10);
    --awn-a: #3c6e63;
    --awn-b: #3a2f27;
    --chalk-bg: #262f2a;
    --chalk-frame: #402f22;
    --chalk-text: #e6dfc9;
    --ring: rgba(239, 230, 212, 0.07);
  }
}

* { box-sizing: border-box; }

/* pixel art must never be smoothed when scaled */
img { image-rendering: pixelated; image-rendering: crisp-edges; }

/* inline pixel icons — sizes are exact divisions of the 48px source art */
.px { image-rendering: pixelated; flex: none; }
.px-16 { width: 16px; height: 16px; vertical-align: -.15em; }
.px-24 { width: 24px; height: 24px; vertical-align: -.32em; }
.px-32 { width: 32px; height: 32px; }
.px-48 { width: 48px; height: 48px; }

body {
  margin: 0;
  font-family: var(--body-font);
  font-size: 17px;
  line-height: 1.7;
  color: var(--text);
  background: linear-gradient(var(--room-top), var(--room-bottom) 460px, var(--room-bottom));
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

h1, h2, h3 { font-family: var(--head-font); font-weight: 600; line-height: 1.2; }
h1 { font-size: 2.3rem; margin: 0 0 .5rem; }
h2 { font-size: 1.45rem; margin: 3rem 0 1.25rem; color: var(--accent); }
h2 .h2-sprite { margin-right: .4rem; }
h3 { font-size: 1.12rem; margin: 0 0 .3rem; }

p { margin: 0 0 1rem; }
a { color: var(--accent); text-decoration-thickness: 2px; text-underline-offset: 3px; }
a:hover { color: var(--accent-2); }
.muted { color: var(--muted); }
.small { font-size: .9rem; }
code, .mono { font-family: var(--mono); font-size: .88em; }

.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 24px; width: 100%; }

/* ---------- the desk scene ---------- */
.scene {
  position: relative;
  overflow: hidden;
  padding: 2.5rem 0 1.25rem;
}
.scene .wrap { position: relative; z-index: 1; }
.lamp-glow {
  position: absolute;
  top: -40px; right: 6%;
  width: 150px; height: 150px;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--lamp) 55%, transparent), transparent 70%);
  animation: flicker 6s ease-in-out infinite;
}
.glyph {
  position: absolute;
  font-family: var(--mono);
  font-weight: 700;
  color: var(--glyph);
  user-select: none;
  pointer-events: none;
}
.glyph-1 { font-size: 2.2rem; top: 38px;  animation: float-by 70s linear infinite; }
.glyph-2 { font-size: 1.6rem; top: 120px; animation: float-by 95s linear infinite; animation-delay: -35s; }
.glyph-3 { font-size: 1.9rem; top: 200px; animation: float-by 82s linear infinite; animation-delay: -60s; }

@keyframes float-by {
  from { left: -80px;  transform: rotate(-6deg); }
  to   { left: 108%;   transform: rotate(6deg); }
}
@keyframes flicker {
  0%, 100% { opacity: .85; }
  50%      { opacity: 1; transform: scale(1.05); }
}
@keyframes blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}
@keyframes bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}
@keyframes steam-rise {
  0%   { transform: translateY(0) scaleX(1); opacity: 0; }
  25%  { opacity: .7; }
  100% { transform: translateY(-14px) scaleX(1.6); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .lamp-glow, .glyph, .cursor, .duck, .steam i, .customer-stage { animation: none !important; }
  .speech .tip-fact { transition: none !important; }
  /* an arrival still has to register, just without the travel */
  .customer-sprite.arriving { animation: fade-in .3s ease-out both !important; }
}

/* ---------- HUD nav ---------- */
.hud {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--frame);
  border-bottom: 4px solid var(--frame-dark);
  box-shadow: 0 3px 0 var(--chunk);
}
.hud-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 10px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.hud-brand {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-family: var(--head-font);
  font-size: 1.12rem;
  color: #fff6e3;
  text-decoration: none;
  text-shadow: 2px 2px 0 var(--frame-dark);
}
.hud-links { display: flex; gap: .5rem; }
.hud-links a {
  font-family: var(--head-font);
  font-size: .95rem;
  color: #f3e8d0;
  text-decoration: none;
  padding: .3rem .7rem;
  border-radius: 8px;
  border: 2px solid transparent;
}
.hud-links a:hover { background: var(--frame-dark); color: #fff; }
.hud-links a.active {
  background: var(--paper);
  color: var(--text);
  border-color: var(--frame-dark);
  box-shadow: 0 2px 0 var(--chunk);
}

/* coffee-ring stains on the table */
.coffee-ring {
  position: absolute;
  border: 7px solid var(--ring);
  border-radius: 50%;
  pointer-events: none;
}
.ring-1 { width: 78px; height: 78px; bottom: 8px; left: 6%; transform: rotate(-8deg) scaleX(1.05); }
.ring-2 { width: 44px; height: 44px; bottom: 30px; left: 11%; opacity: .7; }

/* ---------- terminal hero ---------- */
.term {
  background: var(--term-bg);
  border: 3px solid var(--frame-dark);
  border-radius: 14px;
  box-shadow: 0 6px 0 var(--chunk);
  margin: 1.25rem 0 0;
  overflow: hidden;
}
.term-bar {
  display: flex;
  align-items: center;
  gap: .45rem;
  background: var(--frame);
  border-bottom: 3px solid var(--frame-dark);
  padding: .5rem .9rem;
}
.term-dot { width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--frame-dark); }
.term-dot.r { background: #e2695c; }
.term-dot.y { background: #edbf5c; }
.term-dot.g { background: #83b876; }
.term-title {
  margin-left: .4rem;
  font-family: var(--mono);
  font-size: .78rem;
  color: #f3e8d0;
}
.term-body { padding: 1.4rem 1.6rem 1.5rem; color: var(--term-text); }
/* min-height holds the heading's slot while it waits to be printed —
   an empty block has no line box and would collapse (terminal.js) */
.term-body h1 { color: #fffaf0; min-height: 1.2em; }
.prompt-line {
  font-family: var(--mono);
  font-size: .82rem;
  color: var(--term-green);
  margin: 0 0 .6rem;
}
.prompt-line .cursor {
  display: inline-block;
  width: .5em; height: 1em;
  vertical-align: -0.15em;
  background: var(--term-green);
  animation: blink 1.1s steps(1) infinite;
}
/* solid while keys are landing, blinking once the line goes idle */
.term-body.keys-down .cursor { animation: none; opacity: 1; }
.term-body .lede { color: var(--term-text); opacity: .85; margin-bottom: 0; }
.term-body .lede + .lede { margin-top: .7rem; font-size: 1rem; }
/* content that has not been printed yet still holds its space */
.term-body .to-type { opacity: 0; transform: translateY(4px); }
.term-body .to-type.typed-in {
  opacity: 1;
  transform: none;
  transition: opacity .45s ease-out, transform .45s ease-out;
}
.term-body .lede.to-type.typed-in { opacity: .85; }

/* ---------- terminal menu (front page) ---------- */
/* the pages offered as a shell menu: a caret marks the row you are on, each
   row is one line (name then note, the way a command and its summary sit in
   a man page), and the whole row is the link so a tap anywhere lands. */
.menu-prompt { margin: 1.4rem 0 .5rem; }
.term-menu { display: flex; flex-direction: column; gap: .1rem; }
.term-opt {
  display: grid;
  grid-template-columns: 1.6em 1.1em auto minmax(0, 1fr);
  align-items: baseline;
  gap: 0 .55rem;
  padding: .38rem .55rem;
  border-radius: 7px;
  border: 2px solid transparent;
  font-family: var(--mono);
  text-decoration: none;
  color: var(--term-text);
}
.term-opt::before {
  content: "\00a0\00a0";
  color: var(--term-green);
}
.term-opt.here::before { content: "\276f\00a0"; }
.term-opt.here {
  background: rgba(255, 250, 240, .1);
  border-color: rgba(255, 250, 240, .28);
}
.term-opt:focus-visible { outline: 2px solid var(--term-green); outline-offset: 2px; }
.opt-key { width: 1.1em; color: var(--term-green); opacity: .8; }
.opt-key::after { content: ")"; }
.opt-name { font-family: var(--head-font); font-size: 1.05rem; color: #fffaf0; }
.opt-desc {
  font-family: var(--body-font);
  font-size: .86rem;
  color: var(--term-text);
  opacity: .68;
}
.menu-group {
  margin: .85rem 0 .2rem;
  padding: .55rem 0 0 1.7em;
  border-top: 1px dashed rgba(255, 250, 240, .14);
  font-family: var(--head-font);
  font-size: .78rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--term-text);
  opacity: .45;
}
/* the outbound rows are secondary — dimmer, tighter, and the arrow says
   they leave the site. same face as everything else: opacity does the
   de-emphasis, a font change would just look like a different UI */
.term-opt.out { padding: .16rem .55rem; }
.term-opt.out .opt-key { color: var(--term-text); opacity: .5; }
.term-opt.out .opt-name {
  font-size: .95rem;
  color: var(--term-text);
  opacity: .85;
}
.term-opt.out .opt-desc { opacity: .45; }
.term-opt.out .opt-name::after {
  content: "\2197";
  margin-left: .3em;
  font-size: .8em;
  opacity: .6;
}
/* on a wide screen the menu sits beside the introduction rather than under
   it, so the whole terminal fits a laptop screen without scrolling. The DOM
   stays flat — the columns are pure placement — so the typing sequence in
   terminal.js is unaffected. */
@media (min-width: 900px) {
  .term-body.has-menu {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(16rem, 20rem);
    column-gap: 2.4rem;
    align-items: start;
  }
  .term-body.has-menu > * { grid-column: 1; }
  .term-body.has-menu > .menu-prompt { grid-column: 2; grid-row: 1; margin-top: 0; }
  .term-body.has-menu > .term-menu { grid-column: 2; grid-row: 2 / span 5; }
  /* the left column is narrower now, so the introduction breathes less:
     everything below is shaved to keep the terminal inside a laptop screen */
  main.scene { padding: 1.5rem 0 1rem; }
  .term-body.has-menu { padding: 1.2rem 1.5rem 1.3rem; }
  .term-body.has-menu h1 { font-size: 2rem; margin-bottom: .35rem; }
  .term-body.has-menu .lede { font-size: 1.02rem; line-height: 1.55; }
  .term-body.has-menu .lede + .lede { margin-top: .55rem; }
  .term-body.has-menu .stat-row { margin-top: .8rem; }
}
.menu-hint {
  margin: .7rem 0 0;
  font-family: var(--mono);
  font-size: .74rem;
  color: var(--term-text);
  opacity: .5;
}
@media (max-width: 560px) {
  /* the note drops to its own line rather than squeezing beside the name */
  .term-opt { grid-template-columns: 1.6em 1.1em minmax(0, 1fr); }
  .opt-desc { grid-column: 2 / -1; }
}

/* ---------- stat chips ---------- */
.stat-row { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.1rem; }
.stat img { margin-right: .3rem; }
.stat {
  font-family: var(--head-font);
  font-size: .85rem;
  background: var(--paper-deep);
  border: 2px solid var(--frame-light);
  border-radius: 999px;
  padding: .25rem .75rem;
  color: var(--text);
}
.term .stat {
  background: rgba(255, 250, 240, .08);
  border-color: rgba(255, 250, 240, .25);
  color: var(--term-text);
}

/* coffee with steam */
.coffee { position: relative; display: inline-block; }
.steam { position: absolute; top: -8px; left: 2px; width: 100%; height: 10px; }
.steam i {
  position: absolute;
  bottom: 0;
  width: 3px; height: 8px;
  border-radius: 999px;
  background: currentColor;
  opacity: 0;
  animation: steam-rise 2.6s ease-out infinite;
}
.steam i:nth-child(1) { left: 3px; }
.steam i:nth-child(2) { left: 9px;  animation-delay: .9s; }
.steam i:nth-child(3) { left: 15px; animation-delay: 1.7s; }

/* ---------- main ---------- */
main { flex: 1; padding-bottom: 3.5rem; }
/* no chrome on the game page — the café floats alone, like the front door */
/* the game owns the page, so it gets a wider column than the reading
   pages — more wall to hang things on, more floor to stand them on */
main.game-page { flex: 1; padding: 2.5rem 0 1.4rem; max-width: 940px; }
.lede { font-size: 1.12rem; color: var(--muted); margin-bottom: 1.5rem; }

/* ---------- build cards (projects) ---------- */
.build {
  background: var(--paper);
  border: 3px solid var(--frame-light);
  border-radius: 14px;
  box-shadow: 0 5px 0 var(--chunk);
  padding: 1.25rem 1.4rem 1.2rem;
  margin-bottom: 1.4rem;
  transition: transform .15s ease, box-shadow .15s ease;
}
.build:hover {
  transform: translateY(-3px);
  box-shadow: 0 8px 0 var(--chunk);
}
.build-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.build-title { display: flex; align-items: baseline; gap: .55rem; }
.build-icon { display: inline-block; align-self: center; }
.version {
  font-family: var(--mono);
  font-size: .78rem;
  color: var(--muted);
  white-space: nowrap;
  background: var(--paper-deep);
  border: 2px solid var(--frame-light);
  border-radius: 6px;
  padding: .1rem .5rem;
}
.build-sub { color: var(--muted); font-size: .95rem; margin-bottom: .6rem; }
.build ul { margin: .5rem 0 0; padding-left: 1.2rem; }
.build li { margin-bottom: .35rem; }
.build li::marker { content: "▸ "; color: var(--accent); }

/* ---------- package chips ---------- */
.deps { display: flex; flex-wrap: wrap; gap: .45rem; margin-top: .9rem; }
.dep {
  font-family: var(--mono);
  font-size: .76rem;
  padding: .2rem .6rem;
  border-radius: 8px;
  background: var(--paper-deep);
  border: 2px solid var(--accent);
  color: var(--accent);
  box-shadow: 0 2px 0 var(--chunk);
}

/* ---------- commit log (experience) ---------- */
.commit {
  border-left: 4px dotted var(--frame-light);
  padding: 0 0 1.6rem 1.35rem;
  margin-left: .5rem;
  position: relative;
}
.commit::before {
  /* drawn rather than typed: this used to be a "●" glyph, and where a
     glyph's ink sits inside its advance width is the font's business, not
     ours — which is why the dots never quite met the line. the line is
     the 4px border, so its middle is 2px left of the padding box, and a
     10px dot centred on that starts 7px further left again. */
  content: "";
  position: absolute;
  left: -7px;
  top: 5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent-2);
  /* the page's own colour, to punch the border's dots out from behind */
  box-shadow: 0 0 0 3px var(--room-bottom);
}
.commit-hash {
  font-family: var(--mono);
  font-size: .74rem;
  color: var(--muted);
  background: var(--paper-deep);
  border-radius: 5px;
  padding: .06rem .4rem;
  margin-right: .45rem;
  vertical-align: middle;
}
.commit-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.commit-when { font-family: var(--mono); font-size: .78rem; color: var(--muted); white-space: nowrap; }
.commit-sub { color: var(--muted); font-size: .95rem; margin-bottom: .5rem; }
.commit ul { margin: .5rem 0 0; padding-left: 1.2rem; }
.commit li { margin-bottom: .35rem; }
.commit li::marker { content: "+ "; color: var(--accent); font-family: var(--mono); }

/* ---------- loadout (skills) ---------- */
.loadout {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: .7rem;
  margin: 1rem 0 0;
}
.slot {
  background: var(--paper-deep);
  border: 3px solid var(--frame-light);
  border-radius: 10px;
  box-shadow: inset 0 3px 0 var(--chunk);
  padding: .7rem .5rem .6rem;
  text-align: center;
  transition: transform .12s ease;
}
.slot:hover { transform: translateY(-2px); }
.slot .slot-icon { display: block; margin: 0 auto .2rem; }
.slot .slot-name {
  font-family: var(--head-font);
  font-size: .82rem;
  display: block;
  color: var(--text);
}
.loadout-label {
  font-family: var(--head-font);
  font-size: 1rem;
  color: var(--muted);
  margin: 1.5rem 0 .4rem;
}

/* ---------- chalkboard (skills) ---------- */
.chalkboard {
  background: var(--chalk-bg);
  border: 6px solid var(--chalk-frame);
  border-radius: 14px;
  box-shadow: 0 6px 0 var(--chunk);
  padding: 1.2rem 1.4rem 1.5rem;
  margin-top: 1rem;
}
.chalkboard .loadout-label {
  color: var(--chalk-text);
  opacity: .75;
  letter-spacing: .05em;
}
.chalkboard .loadout-label:first-child { margin-top: .25rem; }
.chalkboard .slot {
  background: transparent;
  border: 2px dashed color-mix(in srgb, var(--chalk-text) 45%, transparent);
  box-shadow: none;
}
.chalkboard .slot .slot-name { color: var(--chalk-text); }
.chalk-title {
  font-family: var(--head-font);
  text-align: center;
  color: var(--chalk-text);
  font-size: 1.1rem;
  margin: 0 0 .25rem;
  letter-spacing: .08em;
}
.chalk-rule {
  border: 0;
  border-top: 2px dashed color-mix(in srgb, var(--chalk-text) 35%, transparent);
  margin: .5rem auto 1rem;
  width: 60%;
}

/* ---------- desk footer ---------- */
.desk {
  background: linear-gradient(var(--frame-light), var(--frame));
  border-top: 4px solid var(--frame-dark);
  padding: 1.4rem 0 1.6rem;
  color: #fff6e3;
  text-shadow: 1px 1px 0 rgba(0,0,0,.25);
}
.desk .wrap {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
.desk a { color: #fff; }
.desk-items { display: flex; align-items: center; gap: .45rem; user-select: none; }
.desk-items .duck { animation: bob 3.2s ease-in-out infinite; }

/* ---------- signpost buttons ---------- */
.keycap {
  display: inline-block;
  font-family: var(--head-font);
  font-size: .95rem;
  background: var(--frame-light);
  color: #fff6e3;
  text-decoration: none;
  border: 3px solid var(--frame-dark);
  border-radius: 10px;
  box-shadow: 0 4px 0 var(--chunk);
  padding: .45rem 1rem;
  margin: .2rem .35rem .2rem 0;
  transition: transform .12s ease, box-shadow .12s ease;
}
.keycap:hover { transform: translateY(2px); box-shadow: 0 2px 0 var(--chunk); color: #fff; }

/* ---------- mobile ---------- */
@media (max-width: 640px) {
  body { font-size: 16px; }
  h1 { font-size: 1.7rem; }
  .hud-inner { padding: 8px 16px; gap: .5rem; }
  .hud-links { gap: .2rem; }
  .hud-links a { padding: .25rem .45rem; font-size: .85rem; }
  .wrap { padding: 0 18px; }
  .term-body { padding: 1.1rem 1.2rem 1.25rem; }
}
@media (max-width: 480px) {
  /* brand on one line, the four links centered on the next */
  .hud-inner { flex-wrap: wrap; justify-content: center; }
  .hud-links { flex-wrap: wrap; justify-content: center; }
}

/* =====================================================
   Barista Shift — the playable counter
   ===================================================== */

.game {
  background: var(--paper);
  border: 3px solid var(--frame-light);
  border-radius: 14px;
  box-shadow: 0 6px 0 var(--chunk);
  overflow: hidden;
  margin: 1.5rem 0;
}
.game-bar {
  /* the toasts hang from this bar — see .toast */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--frame);
  border-bottom: 3px solid var(--frame-dark);
  padding: .5rem .9rem;
}
.game-bar .title {
  font-family: var(--head-font);
  color: #fff6e3;
  font-size: .95rem;
  text-shadow: 1px 1px 0 var(--frame-dark);
}
/* on the game page this bar carries the page heading — it should still
   read as a bar label, not as a title */
.game-bar h1.title { margin: 0; font-weight: 600; line-height: 1.2; }
.game-score {
  font-family: var(--head-font);
  font-size: .85rem;
  color: #f3e8d0;
  display: flex;
  align-items: center;
  gap: .9rem;
}
/* one height across the row. the buttons are drawn by their 24px icons and
   the tally plate by its digits, which left the plate a hair taller than
   everything standing next to it — enough to read as a wobble in a row
   this short. 34px is what the modal bars settle on too */
.game-score .reset-btn,
.game-score .score-tally {
  min-height: 34px;
  line-height: 1;
}

/* the till and the cup count sit in one sunken plate, split down the
   middle, so each icon is plainly welded to its own number rather than
   the four of them floating in a row and leaving you to pair them up.
   each tally is its own flex line, which centres the icon against the
   digits properly — the em nudge on .px-24 is for icons dropped into a
   line of prose, and inside here it would only push them back off. */
.score-tally {
  display: inline-flex;
  align-items: stretch;
  border-radius: 7px;
  background: var(--frame-dark);
  box-shadow: inset 0 2px 0 rgba(0, 0, 0, .35),
              0 1px 0 rgba(255, 255, 255, .07);
}
.score-tally > span {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  line-height: 1;
  padding: .3rem .65rem;
}
.score-tally > span + span { box-shadow: inset 2px 0 0 rgba(0, 0, 0, .3); }
.score-tally .px-24 { vertical-align: baseline; }

.counter { padding: 1.3rem 1.4rem 1.5rem; }

/* customer + speech: the front of house is a room now, not a strip.
   panelled wall, plank floor, and the back door the regulars come
   through — the same visual language as the shops in town, because
   it is the same town. */
/* the scaler: the row below is laid out at a fixed 889x280 — the size it
   reaches at the widest page — and shrunk as one unit to the width at
   hand. every offset in the room is tuned against that one canvas, so
   phones get the same scene smaller instead of a rearranged one. the
   aspect-ratio keeps the wrapper's layout height in step with the
   scaled visual height without javascript measuring anything twice. */
.room-scaler {
  --room-scale: 1;
  width: 100%;
  aspect-ratio: 889 / 280;
  max-height: 280px;
  margin-bottom: 1rem;
  overflow: hidden;
}
.customer-row {
  position: relative;
  /* the row is its own stacking context, so the shelf plank's negative
     z-index lands above this wall instead of vanishing behind it */
  z-index: 0;
  display: flex;
  align-items: flex-end;
  gap: .9rem;
  width: 889px;
  height: 280px;
  transform: scale(var(--room-scale));
  transform-origin: 0 0;
  padding: .8rem .9rem 0 1.1rem;
  background:
    radial-gradient(150px 110px at 26% 34%, rgba(255, 198, 112, .16), transparent 70%),
    repeating-linear-gradient(90deg,
      rgba(0, 0, 0, .10) 0 2px, transparent 2px 22px) top / 100% 64% no-repeat,
    linear-gradient(180deg, #3b2a20 0%, #52392a 64%, #6b4a33 64%, #57402c 100%);
  border: 3px solid var(--frame-dark);
  border-radius: 12px;
  overflow: hidden;
}
/* the stations went diegetic: the door, the menu board and the radio ARE
   the buttons now. a hotspot is a piece of the room that happens to be
   clickable, so it dresses like scenery and only admits to being a
   button when asked — a lift on hover, and a named tag for the pointer,
   the tab key and the finger holding it down. */
.hotspot {
  position: absolute;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  line-height: 0;
  border-radius: 8px;
}
/* the counter run is packed tight and the small fixtures deliberately
   stand in front of the big ones — so where a piece's depth lived
   matters. it used to live on the button, which made every hotspot its
   own stacking context and sealed its tag inside: a tag hung under a
   back piece could not climb over the dish or bowl standing in front of
   it, however high its own z-index went. so the depth moved off the
   button and onto what is actually drawn. the button keeps no z of its
   own (the same reason the shelf's plank can reach for a negative z and
   land above the wall but under the chairs), each piece passes its layer
   down through --tier, and the tags — outside every sprite's context
   now — sit above the lot. the room's stacking order is untouched: the
   same art in the same order, only the tags moved. */
.hotspot > * { z-index: var(--tier, auto); transition: translate .12s ease; }
.hotspot img { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
/* z-index needs a positioned box. direct children only: the weather art
   nested inside a window's view keeps its own absolute placement. */
.hotspot > img { position: relative; }
/* the lift is a `translate`, not a `transform`: it composes with the
   keyframed transforms the tells run on these same sprites instead of
   fighting them, and it stays on the children so the button never turns
   into a context again. */
.hotspot:hover > *, .hotspot:focus-visible > * { translate: 0 -3px; }
/* the focus ring is painted by the button, which now sits under the art
   — so keyboard focus lifts the whole piece a tier clear of its
   neighbours, the one place the order does move, and the one place you
   want it to. */
.hotspot:focus-visible { --tier: 20; outline: 3px solid var(--accent-2); outline-offset: 3px; }
/* the three fixtures that stand in front of a bigger one — the dish, the
   jar and the bowl — carry a box cut to their art, so their transparent
   corners cannot punch holes in the piece behind. that leaves them small
   things to point at, and smaller still as the room scales down: a dish
   40x20 on a canvas that shrinks with the page. so the grab is a halo
   rather than a bigger box. it costs the neighbour a few pixels along one
   edge, which is the right trade — the piece in front should own the ring
   around its own art — and it carries the fixture's tier so it beats the
   sprite it is standing on. */
.spot-butter::before,
.spot-honey::before,
.spot-whisk::before {
  content: "";
  position: absolute;
  inset: -10px;
  z-index: var(--tier);
}
.spot-label {
  position: absolute;
  /* above every tier the room uses, so no fixture can slice a tag */
  z-index: 20;
  left: 50%;
  bottom: -4px;
  transform: translateX(-50%);
  font-family: var(--head-font);
  /* the tags push back against the room shrinking — divided by the
     scale they hold their on-screen size, capped so they never balloon
     over the scenery on the smallest phones */
  font-size: min(calc(.62rem / var(--room-scale, 1)), 1.3rem);
  line-height: 1;
  color: #fff6e3;
  background: rgba(24, 14, 8, .8);
  border: 2px solid var(--frame-dark);
  border-radius: 6px;
  padding: .25rem .45rem;
  white-space: nowrap;
  opacity: 0;
  transition: opacity .15s ease;
  pointer-events: none;
}
.hotspot:hover .spot-label,
.hotspot:focus-visible .spot-label { opacity: 1; }
/* a finger has no hover, so the tags used to pin on — all seventeen of
   them, at once, which turned the room into a labelled diagram and set
   the counter's tags colliding with each other where the fixtures stand
   in front of one another. so they answer the press instead: hold a
   piece and it names itself, let go and the panel opens anyway. one tag
   at a time, the way the pointer has always had it. */
@media (hover: none) {
  /* :active, not :focus — a tap leaves focus sitting on the hotspot
     after the panel closes, which would strand one tag lit behind it */
  .hotspot:active .spot-label { opacity: 1; }
}
/* the back door, standing where the walk-in starts from — the 2x scale
   keeps it on the same pixel grid as the customers. bottom-anchored so
   it stays planted just past the floor seam if the room grows again.
   the sprite is JUST the door — the old baked-in lamp, plant and mat
   are gone (a mat and plants may return as purchasable furniture).
   the door art spans the left 74px of the box with 7 empty rows below
   (14px at 2x), so the dot, label and bottom offset all aim at the
   door itself rather than the box. */
/* the sprite is full-bleed 40x76 now — no padding to compensate for:
   bottom 98 plants the slab just past the floor seam, and 2x of 76
   puts the top at 30px, level with the window frame's visible top */
.spot-door  { left: 126px; bottom: 98px; width: 80px; height: 152px; }
.spot-door .spot-label { left: 42px; }
/* the restock dot hangs off the door frame's top-right corner. the
   two-class selector outranks .station-dot's own top/right, which sits
   later in the stylesheet and would win a plain class-to-class tie */
/* pinned straddling the frame's top-right corner (the sprite is
   full-bleed now, so the box corner IS the door's corner) */
.spot-door .spot-dot { top: -4px; right: -4px; }
/* the window fills the stretch of wall the art used to hang on */
.spot-window { top: 26px; left: 232px; width: 96px; height: 96px; }
/* the second pane of the same glass, on the right stretch of wall the
   shelf used to claim — centered on the old plank's span (outer
   x 685–857, so 771 - 48) at the same height as the window button.
   a button like the big window: either pane opens the view picker */
.spot-window-back { top: 26px; left: 723px; width: 96px; height: 96px; }
/* the menu board hangs level with the window, right of the glass */
.spot-board { top: 26px; left: 352px; width: 96px; height: 96px; }
/* the wall art by the entrance — click it to swap what hangs there
   (a framed painting or a neon sign on its own board) */
.spot-art { top: 34px; left: 14px; width: 96px; height: 96px; }
/* the neon pieces buzz, a slow breath of brightness — renderArt flips
   the class with the art, so the paintings hold perfectly still */
.spot-art.art-neon img { animation: neon-buzz 2.8s ease-in-out infinite; }
@keyframes neon-buzz {
  0%, 100% { filter: brightness(1); }
  50%      { filter: brightness(.8); }
}
@media (prefers-reduced-motion: reduce) {
  .spot-art.art-neon img { animation: none; }
}
/* the radio sits ON the counter — in front of the strip (which paints
   at z-index 2), base overlapping its top edge so it reads as resting
   there instead of hovering on the floor behind it */
/* bottom: 14px plants the visible base (the sprites carry 5 transparent
   rows, 10px at 2x) a couple of pixels past the counter's rim, so the
   radio and machine stand on the woodwork instead of hovering over it */
/* the table for two against the back wall, in the quiet stretch between
   the recipe board and the shelf, drawn a touch larger than 2x so the
   armchairs hold their own. its visible base (58 rows into the sprite,
   145px at 2.5x) lands just past the floor seam at 179. a hotspot like
   the machine: click it to swap in another set from Hearth & Home */
.cafe-seating {
  top: 45px;
  /* centered in the stretch between the board and the right window,
     sharing the shelf's center (579) so the pair hang as one piece */
  left: 469px;
  width: 220px;
  height: 160px;
}
.cafe-seating .spot-label { bottom: 6px; }

/* while a record spins, three little notes drift up off the radio in
   turn, each on the same loop a beat apart. the radio stands on the
   shelf now, so the box is dealt the same slot classes and rides along
   with it; game.js builds it in renderRoomDecor and flips [hidden] with
   the music. */
.radio-notes {
  position: absolute;
  top: 2px;
  width: 48px;
  height: 26px;
  pointer-events: none;
}
.radio-notes img {
  position: absolute;
  bottom: 0;
  width: 16px;
  height: 16px;
  opacity: 0;
  animation: note-float 2.7s ease-out infinite;
}
.radio-notes img:nth-child(1) { left: 12%; }
.radio-notes img:nth-child(2) { left: 44%; animation-delay: .9s; }
.radio-notes img:nth-child(3) { left: 72%; animation-delay: 1.8s; }
@keyframes note-float {
  0%   { transform: translate(0, 8px); opacity: 0; }
  20%  { opacity: .95; }
  100% { transform: translate(5px, -26px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no drifting — one steady note says the same thing */
  .radio-notes img { animation: none; }
  .radio-notes img:nth-child(2) { opacity: .9; }
}
/* the espresso machine, on the counter beside the regular — drawn
   face-on on the 48px grid with the radio's own 5 rows of bottom
   padding, so the radio's offsets place it exactly */
/* the counter run is spaced by the GAPS between the visible art, not by
   the boxes and not by the centres: the pieces are all different widths,
   so even boxes would read as uneven and even centres would jam the wide
   machine into its neighbours. the pieces that are meant to overlap count
   as one slot each, measured across the whole huddle: the butter dish
   over the flour bin, and the honey jar and matcha bowl over the two
   ends of the spice rack. the register's left edge and the ice maker's
   right edge are the fixed ends of the run. */
.spot-machine { bottom: 14px; right: 61.47%; width: 96px; height: 96px; --tier: 3; }
/* the commercial two-groups are drawn on a 64x48 canvas — the spot
   widens to match. it used to grow leftward only, which put the second
   bay 42px into the gap on that side and none on the other; the 17px
   nudge centres the wide sprite on the narrow one instead, so a machine
   swap never shifts the run to one side. */
.spot-machine-wide { width: 128px; right: calc(59.58% - 17px); }

/* a two-group machine is 50px wider than a one-group, which is most of a
   gap. rather than reserve the wide slot always — that left the common
   one-group machine sitting in a pool of air twice the size of every
   other gap — the run has two even layouts. the base numbers above space
   the one-group run at 25.67px a gap; renderMachine puts .wide-machine
   on the room when a two-group is plumbed in, and these pull the
   neighbours outward to an even 17.33px instead. both ends stay pinned:
   the register's left edge and the ice maker's right edge never move. */
.wide-machine .spot-grinder  { right: 48.21%; }
.wide-machine .spot-flour    { right: 40.98%; }
.wide-machine .spot-butter   { right: calc(39.38% + 28px); }
.wide-machine .spot-fruit    { right: 31.50%; }
.wide-machine .spot-spice    { right: 19.61%; }
.wide-machine .spot-honey    { right: calc(19.61% + 76px); }
.wide-machine .spot-whisk    { right: calc(18.01% - 22px); }
/* the machine answers the "pull shot" button. the portafilter sits flush
   on the cup, so there is no gap to run a stream down — the tell is steam
   off the top and the shudder of a pump, which also reads on the two
   two-group machines without moving with their second bay. */
.spot-machine.pulling > .px { animation: shot-shudder .16s steps(2, end) 7; }
@keyframes shot-shudder {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(0, -1px); }
}
.shot-steam {
  /* bottom edge on the machine's top, so the wisps leave the warming tray */
  position: absolute;
  top: -8px;
  left: 0;
  right: 0;
  height: 18px;
  color: #eee7c4;
  pointer-events: none;
}
.shot-steam i {
  position: absolute;
  bottom: 0;
  width: 3px;
  height: 8px;
  border-radius: 999px;
  background: currentColor;
  opacity: 0;
  animation: steam-rise 1.2s ease-out both;
}
.shot-steam i:nth-child(1) { left: 42%; }
.shot-steam i:nth-child(2) { left: 50%; animation-delay: .18s; }
.shot-steam i:nth-child(3) { left: 58%; animation-delay: .36s; }
/* steaming milk is the wand's job, not the pump's — so no shudder, and
   the steam comes off the wand tip instead of the warming tray. the rod
   sits 30% across on the single-group machines and 15% on the two-group
   ones, which are the same sprite scale in a wider box. */
.milk-steam {
  position: absolute;
  bottom: 36px;
  left: var(--wand, 30%);
  width: 0;
  height: 24px;
  color: #eee7c4;
  pointer-events: none;
}
.spot-machine-wide .milk-steam { --wand: 15%; }
.milk-steam i {
  position: absolute;
  bottom: 0;
  width: 4px;
  height: 6px;
  border-radius: 999px;
  background: currentColor;
  opacity: 0;
  animation: wand-steam 1s ease-out both;
}
.milk-steam i:nth-child(1) { left: -1px; }
.milk-steam i:nth-child(2) { left: -3px; animation-delay: .15s; }
.milk-steam i:nth-child(3) { left:  0px; animation-delay: .3s; }
.milk-steam i:nth-child(4) { left: -4px; animation-delay: .45s; }
/* drifting left as it climbs, so it ends up off the machine and against
   the wall where a pale wisp can actually be seen */
@keyframes wand-steam {
  0%   { transform: translate(0, 4px) scale(.6); opacity: 0; }
  25%  { opacity: .8; }
  100% { transform: translate(-13px, -20px) scale(1.5); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no shudder and no rise — the steam just sits there for the pull */
  .spot-machine.pulling > .px { animation: none; }
  .shot-steam i, .milk-steam i { animation: none; opacity: .75; }
}
/* the grinder, one step along the counter from the machine — same grid
   and the same 5 rows of bottom padding, so the two stand on one line */
.spot-grinder { bottom: 14px; right: 51.99%; width: 96px; height: 96px; --tier: 3; }
/* the grinder answers the "grind beans" button: half a second of rattle
   while a few beans hop out of the hopper. game.js adds .grinding and
   unhides the puff, then clears both when the run is over. */
.spot-grinder img { transform-origin: 50% 100%; }
.spot-grinder.grinding > .px { animation: grind-rattle .14s steps(2, end) 4; }
@keyframes grind-rattle {
  0%   { transform: translate(0, 0) rotate(0deg); }
  25%  { transform: translate(-1px, 0) rotate(-1.5deg); }
  75%  { transform: translate(1px, 0) rotate(1.5deg); }
  100% { transform: translate(0, 0) rotate(0deg); }
}
.grind-puff {
  position: absolute;
  /* clear of the sprite, against the wall, where the flecks can be seen */
  top: -16px;
  left: 0;
  right: 0;
  height: 34px;
  pointer-events: none;
}
/* chaff, not beans: a whole bean is the same brown as the panelling and
   vanishes against it, so these are the sprites' own cream instead */
.grind-puff i {
  position: absolute;
  bottom: 0;
  width: 4px;
  height: 4px;
  background: #eee7c4;
  opacity: 0;
  animation: grind-hop .6s ease-out both;
}
.grind-puff i:nth-child(1) { left: 36%; --hop: -8px; }
.grind-puff i:nth-child(2) { left: 47%; --hop: 2px; width: 3px; height: 3px; animation-delay: .1s; }
.grind-puff i:nth-child(3) { left: 57%; --hop: 9px; animation-delay: .2s; }
@keyframes grind-hop {
  0%   { transform: translate(0, 9px); opacity: 0; }
  30%  { opacity: .95; }
  100% { transform: translate(var(--hop, 6px), -16px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no rattle and no arc — the beans just show up for a moment */
  .spot-grinder.grinding > .px { animation: none; }
  .grind-puff i { animation: none; opacity: .9; }
}
/* the spice rack, in the clear stretch of counter between the armchairs
   and the radio — the one length of counter with nothing standing on it */
.spot-spice { bottom: 14px; right: 20.55%; width: 96px; height: 96px; --tier: 3; }
/* the rack answers "grate cinnamon" and "chocolate" both: a jar tipped
   and shaken over the cup. the grinder rattles upright and the ice maker
   slides sideways, so this one rocks about its base — four fixtures,
   four different tells at a glance. */
.spot-spice img { transform-origin: 50% 100%; }
.spot-spice.shaking > .px { animation: spice-rock .16s steps(2, end) 3; }
@keyframes spice-rock {
  0%, 100% { transform: rotate(0deg); }
  35%      { transform: rotate(-3deg); }
  70%      { transform: rotate(3deg); }
}
.spice-dust {
  position: absolute;
  /* over the sprite rather than above it: the grains fall off the jars,
     so they start at the rack's shoulders and drop past its foot */
  top: 24px;
  left: 0;
  right: 0;
  height: 60px;
  pointer-events: none;
}
/* ground spice, not whole bark: finer than the grinder's chaff, but lit
   the same way it is and for the same reason — the jars' true cinnamon is
   the panelling's own brown and disappears into it, so these are the
   colour spice is when the light is on it. orange where the chaff is
   cream, and falling where the chaff hops up, so the two never read as
   the same fleck */
.spice-dust i {
  position: absolute;
  top: 0;
  width: 3px;
  height: 3px;
  background: #eda94c;
  opacity: 0;
  animation: spice-fall .55s ease-in both;
}
.spice-dust i:nth-child(1) { left: 40%; --drift: -3px; }
.spice-dust i:nth-child(2) { left: 47%; background: #d98a3a; animation-delay: .08s; }
.spice-dust i:nth-child(3) { left: 54%; --drift: 3px; animation-delay: .17s; }
.spice-dust i:nth-child(4) { left: 49%; background: #f5c67a; --drift: -1px; animation-delay: .26s; }
/* the same shake, shaved rather than grated. cocoa is a deeper, redder
   brown than cinnamon, but it still has to clear the panelling it falls
   past — so this is the same trick as the ochre above, one stop down the
   ramp rather than into the wall's own brown */
.spice-dust.cocoa i:nth-child(1) { background: #c97a44; }
.spice-dust.cocoa i:nth-child(2) { background: #a85f31; }
.spice-dust.cocoa i:nth-child(3) { background: #c97a44; }
.spice-dust.cocoa i:nth-child(4) { background: #e0a06b; }
@keyframes spice-fall {
  0%   { transform: translate(0, 0); opacity: 0; }
  25%  { opacity: .95; }
  100% { transform: translate(var(--drift, 1px), 34px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no rock and no fall — the grains just show up for a moment */
  .spot-spice.shaking > .px { animation: none; }
  .spice-dust i { animation: none; opacity: .9; }
}
/* the flour bin, one place along from the grinder. it keeps the counter
   run's spacing, and it is the narrowest thing on it: the armchairs stand
   behind this stretch, so a canister rather than a chest leaves them
   visible either side of it */
.spot-flour { bottom: 14px; right: 43.81%; width: 96px; height: 96px; --tier: 3; }
.spot-flour img { transform-origin: 50% 100%; }
/* the bin settles rather than rattles — a scoop going in and coming out,
   not a motor. one nudge down and up, twice */
.spot-flour.sifting > .px { animation: flour-nudge .19s steps(2, end) 2; }
@keyframes flour-nudge {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(0, 1px); }
}
.flour-puff {
  position: absolute;
  /* the grains start at the surface of the flour and go up from there.
     the heap rises behind the collar now rather than sitting in an open
     top, and its peak is source row 8 — so at the room's 2x the grains
     leave from 18px down the box. the height is the only thing putting
     that bottom edge in the right place */
  top: 0;
  left: 0;
  right: 0;
  height: 18px;
  pointer-events: none;
}
/* a cloud, not a handful. the grinder's chaff is three hard cream flecks
   arcing up out of the burrs, and flour thrown the same way would read as
   more of the same from the fixture right next door — so these are bigger,
   half-transparent, cooler in colour, and they grow as they go */
.flour-puff i {
  position: absolute;
  bottom: 0;
  width: 5px;
  height: 5px;
  background: #f6f2ea;
  opacity: 0;
  /* the curve matters more here than anywhere else on the counter: the
     grains start on the mound, which is the one surface in the room they
     cannot be seen against. so nearly all the travel happens in the first
     third, and the fade-in waits for it — by the time a grain is bright
     it has cleared the lid and is against the panelling */
  animation: flour-billow .7s cubic-bezier(.12, .85, .3, 1) both;
}
.flour-puff i:nth-child(1) { left: 38%; --bx: -8px; --by: -16px; }
.flour-puff i:nth-child(2) { left: 46%; width: 7px; height: 7px; --bx: 1px; --by: -22px; animation-delay: .06s; }
.flour-puff i:nth-child(3) { left: 55%; --bx: 8px; --by: -14px; animation-delay: .12s; }
.flour-puff i:nth-child(4) { left: 44%; width: 7px; height: 7px; --bx: -3px; --by: -10px; animation-delay: .2s; }
@keyframes flour-billow {
  0%   { transform: translate(0, 2px) scale(.45); opacity: 0; }
  22%  { opacity: .75; }
  100% { transform: translate(var(--bx, 0), var(--by, -16px)) scale(1.8); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no settle and no billow — the flour just hangs there for a moment */
  .spot-flour.sifting > .px { animation: none; }
  .flour-puff i { animation: none; opacity: .5; }
}
/* the butter dish. the counter run was full, so this one does not join
   the line — it stands in FRONT of the flour bin, on the same counter
   top as everything else but painted over it. that overlap is the whole
   depth cue; short and wide for the same reason, so the bin it stands
   in front of still reads over the top of it.
   and because it stands in front, its BOX is the dish and nothing else.
   the neighbours can afford a 96px square of mostly-transparent sprite;
   this one cannot — every empty pixel of it would be a hole punched in
   the flour bin behind, which is exactly what happened. the dish is
   source rows 33-42, cols 14-33, so at the room's 2x the box is 40x20
   and the sprite is hung off it by the padding it dropped. */
.spot-butter {
  bottom: 24px;                    /* 14 + the 10px of sprite below the base */
  right: calc(42.21% + 28px);      /* + the 28px of sprite right of the dish */
  width: 40px;
  height: 20px;
  --tier: 4;
}
/* the box is the dish, so the tag would sit on the dish. drop it to the
   line the rest of the counter run's tags hang on */
.spot-butter .spot-label { bottom: -18px; }
.spot-butter img {
  position: absolute;
  top: -66px;
  left: -28px;
  width: 96px;
  height: 96px;
  /* the sprite hangs outside the box on three sides, and every one of
     those pixels is transparent — so the clicks belong to the bin */
  pointer-events: none;
}
/* the dish answers "cut butter". the grinder rattles upright, the ice
   maker slides sideways, the rack rocks and the bin settles — so this
   one gives, once, under the knife, and a pat lifts off the block. */
.spot-butter img { transform-origin: 50% 100%; }
.spot-butter.cutting > .px { animation: butter-press .18s ease-out 2; }
@keyframes butter-press {
  0%, 100% { transform: scaleY(1); }
  45%      { transform: scaleY(.93); }
}
.butter-pat {
  position: absolute;
  /* the box starts at the block's top face, so the pat leaves from the
     top edge of it — nothing to measure down */
  top: 0;
  left: 0;
  right: 0;
  height: 0;
  pointer-events: none;
}
/* one pat, not a spray — you cut butter off, you do not throw it. it is
   the same yellow as the block it came from, and it turns as it goes. */
.butter-pat i {
  position: absolute;
  bottom: 0;
  left: 46%;
  width: 6px;
  height: 5px;
  background: #f5d676;
  border-bottom: 2px solid #cdaa52;
  opacity: 0;
  animation: butter-lift .6s cubic-bezier(.2, .7, .3, 1) both;
}
@keyframes butter-lift {
  0%   { transform: translate(0, 2px) rotate(0deg); opacity: 0; }
  25%  { opacity: 1; }
  100% { transform: translate(7px, -15px) rotate(28deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no give and no lift — the pat just sits there for a moment */
  .spot-butter.cutting > .px { animation: none; }
  .butter-pat i { animation: none; opacity: 1; }
}
/* the matcha bowl and whisk, standing in front of the spice rack the way
   the butter dish stands in front of the flour bin — clipping its right
   end and a layer nearer, so the rack still reads behind and the jars stay
   uncovered. the 1.6% step off the rack is the dish's exact step off the
   bin, so the two clear their neighbours by the same 12px at every room
   width — but only once the box gives back the 22px the bowl's sprite sits
   further in from its right edge than the rack's does. */
/* and like the dish and the jar, its BOX is the bowl and nothing else. a
   96px square here was mostly transparent and sat a layer above the rack,
   so it swallowed every click on the rack's right end. bowl and chasen
   are source rows 27-42, cols 16-31, so at the room's 2x the box is 32x32
   and the sprite is hung off it by the padding it dropped. */
.spot-whisk {
  bottom: 24px;                    /* 14 + the 10px of sprite below the base */
  right: calc(18.95% + 10px);      /* + the 32px of sprite right of the bowl */
  width: 32px;
  height: 32px;
  --tier: 4;
}
/* the box is the bowl, so the tag would sit in it. drop it to the line
   the rest of the counter run's tags hang on */
.spot-whisk .spot-label { bottom: -18px; }
/* the bowl answers "whisk matcha". the grinder rattles upright, the ice
   maker slides sideways, the rack rocks, the bin settles and the dish
   gives under the knife — so this one shivers, fast and small, the way a
   chasen does in a bowl that is not going anywhere. */
.spot-whisk img {
  position: absolute;
  top: -54px;
  left: -32px;
  width: 96px;
  height: 96px;
  /* the sprite hangs outside the box on three sides, and every one of
     those pixels is transparent — so the clicks belong to the rack */
  pointer-events: none;
}
/* the bowl cannot come along for the ride: whisking is the chasen moving
   inside a bowl somebody is holding still. it used to be one sprite laid
   down twice and cut at the rim, but a straight cut cannot hold once the
   whisk dips past it — so the chasen is its own file now. every colourway
   shares the same bamboo, which is why there is one of it and four bowls.
   the bowl sits after the arm in the markup, so it paints over it: the
   chasen stands in the bowl, and the rim swallows everything below
   source row 35 at every angle of the swing.
   the bowl file still carries the chasen's top half, because the decor
   picker shows the frame whole and a bare bowl would not read as a
   matcha set — the room cuts it away at the rim, source row 35. */
.whisk-bowl { clip-path: inset(72.917% 0 0 0); }
.whisk-arm { transform-origin: 48.96% 51.04%; }
/* the pivot is where a hand would be: the top of the handle, source
   x 23.5, y 24.5 of the 48px frame. the head hangs far enough below it
   that a modest tilt sweeps it a couple of pixels each way — back and
   forth, the way a chasen is actually worked, rather than the whole
   thing sliding sideways. 10deg and no more: past that the tines reach
   the bowl's wall. steps() keeps it snapping between poses instead of
   smearing the pixel grid through the angles between. */
.spot-whisk.whisking .whisk-arm { animation: whisk-sweep .1s steps(2, end) 6; }
@keyframes whisk-sweep {
  0%, 100% { transform: rotate(-10deg); }
  50%      { transform: rotate(10deg); }
}
.whisk-foam {
  position: absolute;
  /* pinned to the sprite's frame, not the box, so the flecks keep the
     offsets they were drawn against. the froth leaves where the tines
     enter the matcha — source row 36, 72px down at the room's 2x */
  top: -54px;
  left: -32px;
  width: 96px;
  height: 72px;
  pointer-events: none;
}
/* three flecks, not a plume — froth comes off a whisk in specks. pale
   green rather than white: it is matcha it was lifted out of. */
.whisk-foam i {
  position: absolute;
  bottom: 0;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: #e2efc4;
  opacity: 0;
  animation: whisk-froth .55s ease-out both;
}
.whisk-foam i:nth-child(1) { left: 45%; animation-delay: 0s; }
.whisk-foam i:nth-child(2) { left: 53%; animation-delay: .09s; }
.whisk-foam i:nth-child(3) { left: 49%; animation-delay: .18s; }
@keyframes whisk-froth {
  0%   { transform: translate(0, 3px) scale(.7); opacity: 0; }
  30%  { opacity: 1; }
  100% { transform: translate(4px, -13px) scale(1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no shiver and no flecks — the froth just sits there for a moment */
  .spot-whisk.whisking .whisk-arm { animation: none; }
  .whisk-foam i { animation: none; opacity: 1; }
}
/* the fruit bowl, out in the clear stretch between the flour bin and the
   spice rack. the armchairs stand behind this whole run, which is why the
   bin next door is a narrow canister — and the bowl is the shortest thing
   on the counter at 24 rows, so it hides less of them than anything else
   here does. */
.spot-fruit { bottom: 14px; right: 33.39%; width: 96px; height: 96px; --tier: 3; }
/* the bowl answers "fold berries". by now every other fixture on this
   counter has claimed a motion — the grinder rattles upright, the ice
   maker slides, the rack rocks, the bin settles, the dish gives, the till
   kicks — so this one tips: a slow lean each way, the only rotation in
   the room, pivoting on the foot the way a bowl nudged on a counter does. */
.spot-fruit img { transform-origin: 50% 100%; }
.spot-fruit.folding > .px { animation: fruit-tip .34s ease-in-out 2; }
@keyframes fruit-tip {
  0%, 100% { transform: rotate(0deg); }
  30%      { transform: rotate(-2.2deg); }
  70%      { transform: rotate(2.2deg); }
}
.fruit-hop {
  /* the top of the mound is source row 19, so at the room's 2x the
     berries leave from 38px down the box. the height is what puts that
     bottom edge there. */
  position: absolute;
  top: 20px;
  left: 0;
  right: 0;
  height: 18px;
  pointer-events: none;
}
/* two berries jostled loose. every other particle on this counter leaves
   and is gone — chaff, steam, grains, a puff, a pat, the price flags —
   so these are the ones that come back down into the bowl they left,
   which is the whole reason the fruit reads as fruit and not as more
   dust. round, too: nothing else here is. */
.fruit-hop i {
  position: absolute;
  bottom: 0;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #7c4a8e;
  opacity: 0;
  animation: berry-hop .62s ease-out both;
}
.fruit-hop i:nth-child(1) { left: 40%; --bx: -7px; }
.fruit-hop i:nth-child(2) { left: 54%; width: 4px; height: 4px; background: #93436b; --bx: 8px; animation-delay: .09s; }
@keyframes berry-hop {
  0%   { transform: translate(0, 3px); opacity: 0; }
  18%  { opacity: 1; }
  /* the apex, and the easing either side of it is what makes it an arc
     rather than a hover: fast up, hang, fast down */
  50%  { transform: translate(calc(var(--bx, 0px) * .6), -8px); opacity: 1; }
  88%  { opacity: 1; }
  100% { transform: translate(var(--bx, 0px), 3px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no tip and no hop — the berries just sit above the bowl for a moment */
  .spot-fruit.folding > .px { animation: none; }
  .fruit-hop i { animation: none; opacity: 1; transform: translateY(-6px); }
}
/* the honey jar, at the rack's left end — the matcha bowl stands on its
   right end the same way, so the rack ends up bookended: two small
   fixtures a layer nearer, one at each shoulder, and the jars between
   them still read. the jar half-covers the rack's left end and hangs the
   other half off it, about the 14px the bowl clips off the right.
   it is anchored to the rack in px, not to the room in %, so it rides
   the rack's end at every room width — which is why there is no
   .wide-machine override for it: the rack does not move either.
   its BOX is the jar and nothing else: a 96px square here would be
   mostly transparent and would swallow the clicks belonging to the rack
   behind it. jar and dipper together are source rows 23-42, cols 17-30,
   so at the room's 2x the box is 28x40 and the two sprites are hung off
   it by the padding they dropped. */
.spot-honey {
  bottom: 24px;                    /* 14 + the 10px of sprite below the base */
  /* the rack's leftmost pixel is at 20.55% + 90px; the jar's 28px straddle
     it, half on the rack and half off the end of it */
  right: calc(20.55% + 76px);
  width: 28px;
  height: 40px;                    /* the dipper stands 4px above the old lid */
  /* in front of the rack, like the bowl on the other end */
  --tier: 4;
}
/* the box is the jar, so the tag would sit on the jar. drop it to the
   line the rest of the counter run's tags hang on */
.spot-honey .spot-label { bottom: -18px; }
.spot-honey img {
  position: absolute;
  top: -46px;
  left: -34px;
  width: 96px;
  height: 96px;
  /* the sprite hangs outside the box on three sides, and every one of
     those pixels is transparent — so the clicks belong to the rack
     behind it */
  pointer-events: none;
}
/* the jar answers "honey" — except it is the dipper that answers, not the
   jar. the handle lifts the grooved head up out of the honey, holds it
   over the open mouth while the threads run off it, and lowers it back.
   that is the whole reason the dipper is its own <img>: every other
   fixture back here moves as one body — the grinder rattles, the ice
   maker slides, the rack rocks, the bowl tips — and a single sprite can
   tip, but it cannot lift a piece of itself out of the rest. */
.spot-honey .honey-dip {
  /* the head, not the foot. the head is the thing being held still and
     dripped off; put the origin anywhere else and the tilt swings the
     head out from under its own threads. 48/75 is its centre inside the
     96px sprite, which hangs at -34,-46 off the box. */
  transform-origin: 48px 75px;
}
.spot-honey.drizzling .honey-dip {
  animation: honey-lift 1.3s cubic-bezier(.35, 0, .3, 1) both;
}
@keyframes honey-lift {
  0%       { transform: translateY(0) rotate(0deg); }
  /* 24px, and not a pixel more: that is exactly what puts the foot of
     the head level with the lip. lift it further and the dipper stops
     being held over its jar and starts flying out of it; lift it less
     and the thread has to start down inside, where amber on amber is
     nothing at all. it holds there for half the second — the hold is
     what sells the viscosity. */
  14%, 69% { transform: translateY(-24px) rotate(-6deg); }
  100%     { transform: translateY(0) rotate(0deg); }
}
.honey-drip {
  position: absolute;
  /* where the foot of the head hangs at the top of its lift: 24px above
     the 36px mark it sits at when the dipper is down */
  top: 12px;
  left: 0;
  right: 0;
  height: 24px;
  pointer-events: none;
}
/* the one particle in the room that does not fly. chaff, cubes, grains,
   a puff, a pat, two berries — everything else here leaves the fixture
   at speed. honey cannot: it stretches out of the jar, necks, hangs on
   a moment too long, and only then drops. the scaleY IS the viscosity. */
.honey-drip i {
  position: absolute;
  top: 0;
  /* under the middle of the head, which is the middle of the box */
  left: 12px;
  width: 4px;
  height: 7px;
  /* the LIT honey, not the body colour — a drop the same #e0a02a as the
     jar behind it vanishes into it and leaves you looking at an empty
     navy hook. a fresh thread catches the light anyway. */
  background: #f0c257;
  /* the outline every sprite in this room carries, so the drop reads
     against the lip, the neck and the honey it falls past. ONE pixel of
     it, not two: the thread hangs against light amber and cream now
     rather than out over the counter, and at two the ring swallowed the
     drop and left a dark hole in the honey. a border would eat the fill
     at this size; a shadow is drawn outside it. */
  box-shadow: 0 0 0 1px #151a28;
  border-radius: 0 0 3px 3px;
  transform-origin: 50% 0;
  opacity: 0;
  animation: honey-thread .9s cubic-bezier(.4, 0, .9, .35) both;
  /* nothing runs off it until it is up out of the honey */
  animation-delay: .18s;
}
/* the second one is thinner and later — the tail of the pour, not a
   twin of it */
.honey-drip i:nth-child(2) {
  left: 16px;
  width: 3px;
  height: 5px;
  /* far enough behind the first that the two never hang side by side —
     at this size a pair of them merges into one shapeless blob */
  animation-delay: .46s;
}
@keyframes honey-thread {
  0%   { transform: translate(0, 0) scaleY(.35); opacity: 0; }
  22%  { transform: translate(0, 3px) scaleY(1.5); opacity: 1; }
  /* the neck breaking: the thread shortens the instant it lets go */
  46%  { transform: translate(0, 8px) scaleY(.9); opacity: 1; }
  /* it fades out as it reaches the honey it came off */
  100% { transform: translate(-1px, 16px) scaleY(1.25); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no lift and no fall, but the dipper still has to be UP — a drop
     hanging in the air over a jar the dipper is still sitting in makes
     no sense. it cuts to held-up instead of travelling there. */
  .spot-honey.drizzling .honey-dip {
    animation: none;
    transform: translateY(-24px) rotate(-6deg);
  }
  .honey-drip i { animation: none; opacity: 1; transform: translateY(4px); }
}
/* the ice maker, at the far right of the counter run — it closes the run
   the way the till opens it, and its right edge is the fixed end every
   other piece is spaced back from. same grid and the same bottom padding
   as its neighbours, so the whole run stands on one line. */
.spot-icemaker { bottom: 14px; right: 9.24%; width: 96px; height: 96px; --tier: 3; }
/* the ice maker answers the "ice" button: a short judder while a batch
   drops in through the lid. game.js adds .dropping and unhides the cubes,
   then clears both when the batch has landed. sideways, not up and down —
   the spice rack next door already rocks. */
.spot-icemaker img { transform-origin: 50% 100%; }
.spot-icemaker.dropping > .px { animation: ice-judder .17s steps(2, end) 4; }
@keyframes ice-judder {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(1px, 0); }
}
.ice-drop {
  /* sits over the lid: the cubes fall into the box rather than out of it,
     which is the opposite arc to the grinder's chaff and keeps the two
     fixtures telling different stories at a glance */
  position: absolute;
  top: 4px;
  left: 0;
  right: 0;
  height: 30px;
  pointer-events: none;
}
.ice-drop i {
  position: absolute;
  top: 0;
  width: 5px;
  height: 5px;
  /* the same bluish white the pile in the hopper is painted, so a cube
     landing does not change colour on the way down */
  background: #eef6fc;
  opacity: 0;
  animation: ice-fall .5s ease-in both;
}
.ice-drop i:nth-child(1) { left: 40%; --tilt: 70deg; }
.ice-drop i:nth-child(2) { left: 49%; width: 4px; height: 4px; animation-delay: .08s; --tilt: -50deg; }
.ice-drop i:nth-child(3) { left: 57%; animation-delay: .17s; --tilt: 100deg; }
@keyframes ice-fall {
  0%   { transform: translateY(-10px) rotate(0deg); opacity: 0; }
  25%  { opacity: 1; }
  100% { transform: translateY(14px) rotate(var(--tilt, 90deg)); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no judder and no fall — the cubes just show up for a moment */
  .spot-icemaker.dropping > .px { animation: none; }
  .ice-drop i { animation: none; opacity: .9; }
}
/* the till, at the far left of the counter. the widest customer sprite
   reaches 139px into the room and the till's 60px of art runs 175-235:
   clear of the customer with room to spare, and the left end the rest of
   the run is spaced out from. */
.spot-register { bottom: 14px; right: 71.33%; width: 96px; height: 96px; --tier: 3; }
.spot-register img { transform-origin: 50% 100%; }
/* a sale rings it: the body kicks up off the counter the way a drawer
   springing open shoves a till, and the price flags snap up out of the
   display. up-and-down here, where the ice maker next door goes
   side-to-side, so the two never read as the same event. */
.spot-register.ringing > .px { animation: till-kick .12s steps(2, end) 3; }
@keyframes till-kick {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(0, -2px); }
}
.till-pop {
  /* the flags start tucked inside the display and finish clear of the
     register's top edge: 30 rows of art on the row-42 baseline puts that
     edge 26px down the 96px box */
  position: absolute;
  top: 10px;
  left: 0;
  right: 0;
  height: 20px;
  pointer-events: none;
}
.till-pop i {
  position: absolute;
  bottom: 0;
  width: 6px;
  height: 9px;
  background: #eee7c4;
  opacity: 0;
  animation: till-flag .7s ease-out both;
}
.till-pop i:nth-child(1) { left: 43%; }
.till-pop i:nth-child(2) { left: 52%; height: 7px; animation-delay: .07s; }
/* snap up, hang for a beat, then drop back behind the display */
@keyframes till-flag {
  0%   { transform: translateY(8px); opacity: 0; }
  20%  { transform: translateY(-3px); opacity: 1; }
  30%  { transform: translateY(-1px); opacity: 1; }
  70%  { transform: translateY(-1px); opacity: 1; }
  100% { transform: translateY(7px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* no kick and no spring — the flags just show up for a moment */
  .spot-register.ringing > .px { animation: none; }
  .till-pop i { animation: none; opacity: .9; transform: translateY(-2px); }
}
/* a wooden shelf on the back wall, sized for exactly three trinkets:
   6px of breathing room at each end, 6px between slots, 3 × 48px of
   trinket. it hangs centered on the wall stretch between the recipe
   board (right edge 448) and the second window (left edge 723) — so
   right 889 - 585 - 84 — high enough that the plank and its trinket
   headroom clear the chairbacks. it is a hotspot too —
   click the plank to curate it — but it sits before the decor layer in
   the DOM so the trinkets paint on top, and it holds still on hover:
   lifting the plank without the trinkets would leave them floating, so
   it warms up instead. */
.spot-shelf {
  top: 62px;
  right: 220px;
  width: 168px;
  /* 84x10 of sprite at the room's 2x. the slab is the top six rows, so
     the surface the trinkets stand on is still exactly where it was —
     the extra 8px is the cast shadow and the corbels hanging below. */
  height: 20px;
}
/* the plank's wood lives on a negative-z pseudo so the armchairs paint
   in front of it — the row is its own stacking context, so -1 lands
   above the wall but under the seating, and the plank's shadow tucks
   behind the chairbacks. the button box itself stays unpainted and
   later in the DOM, which keeps clicks on the plank and trinkets
   routed to the shelf ahead of the seating behind them. the hover
   warmth rides the pseudo too: a filter on the button would wrap it
   in a new context and drag the wood back in front of the chairs. */
.spot-shelf::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  /* a drawn plank rather than a gradient bar. everything else in the
     room is a sprite, so a smooth ramp with rounded ends and a blurred
     shadow was the one thing here that read as interface instead of
     scenery — this one has grain, hard end caps, a one-row shadow on
     the wall and two corbels under it. its wood is the room's own,
     baked in: the old bar took --frame, which meant the shelf was the
     only object in the café that changed colour with the page theme
     while the walls and floors around it did not. */
  background: url("../img/decor/shelf-plank.png") 0 0 / 100% 100% no-repeat;
  image-rendering: pixelated;
  transition: filter .12s ease;
}
.spot-shelf:hover > *, .spot-shelf:focus-visible > * { translate: none; }
.spot-shelf:hover::after, .spot-shelf:focus-visible::after { filter: brightness(1.25); }
/* the trinkets are the shelf's face, and the plank is a 12px sliver —
   clicking what stands ON the shelf should open the shelf menu too. an
   invisible hitbox grows the button up over the stands (the trinkets
   are 48px tall from top 47, the plank top is 92): the decor layer
   ignores the mouse, so a click on a trinket falls through onto this,
   and hovering one warms the plank and raises its tag like any hotspot */
.spot-shelf::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 46px;
}
/* the plank is too short to wear its tag the usual way — a chip grown up
   from bottom:-4px would cover the trinkets, so this one hangs below */
.spot-shelf .spot-label { bottom: auto; top: calc(100% + 6px); }
/* the stands are dealt by the count — game.js pairs each shelf-slot-N
   with a shelf-of-M class. three fill the plank as ever; two flank the
   middle at even thirds of the 168px inner span; one stands alone in
   the center, on the same spot as the middle of three */
.shelf-slot-0 { right: 334px; }
.shelf-slot-1 { right: 280px; }
.shelf-slot-2 { right: 226px; }
.shelf-of-1.shelf-slot-0 { right: 280px; }
.shelf-of-2.shelf-slot-0 { right: 318px; }
.shelf-of-2.shelf-slot-1 { right: 246px; }
/* the wall and the floor are the room itself rather than objects in
   it, so they are painted surfaces and not hotspots — a click target
   the size of the scene made most of the café feel like a button, and
   the hover tint swept across half the room. the decorate control in
   the game bar changes them now, and they ignore the mouse entirely.
   between them they cover the row's own gradient, which stays
   underneath as the fallback. */
.room-wall, .room-floor {
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
}
.room-wall {
  top: 0;
  height: 64%;
  border-radius: 9px 9px 0 0;
  /* the shelf plank rides at -1 so the armchairs paint in front of it,
     back when the wall was only the row's background. now that the
     wall is an element it has to sit a layer deeper still, or it would
     paint over the plank and the shelf would vanish */
  z-index: -2;
}
/* the floor covers the bottom 36% — the same band the row's gradient
   paints, so the seam with the wall never moves */
.room-floor {
  bottom: 0;
  height: 36%;
  border-radius: 0 0 9px 9px;
}
/* the lamp over the counter rides above whatever is on the wall, so
   every covering catches the same light without repeating the glow in
   each class. 53% of the wall band is where 34% of the whole room fell
   before the wall became an element of its own */
.room-wall::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(150px 110px at 26% 53%,
    rgba(255, 198, 112, .16), transparent 70%);
}
/* the coverings. painted panel is the wall the café opened with:
   grooves every 22px over a gradient that lifts toward the lamp */
.wall-panel {
  background:
    repeating-linear-gradient(90deg, rgba(0, 0, 0, .10) 0 2px, transparent 2px 22px),
    linear-gradient(180deg, #3b2a20, #52392a);
}
/* the rest are pixel patterns shown at 2x and anchored bottom-left, so
   a full course always lands flush on the floor seam. the back wall is
   seen straight on, so these stay square — no foreshortening like the
   floors. a tint over each keeps the room dark up at the ceiling */
.wall-brick {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, .34), rgba(0, 0, 0, .06)),
    url("../img/decor/wall-brick.png") left bottom / 64px 32px repeat,
    linear-gradient(180deg, #3b2a20, #52392a);
  image-rendering: pixelated;
}
.wall-subway {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, .42), rgba(0, 0, 0, .14)),
    url("../img/decor/wall-subway.png") left bottom / 64px 32px repeat,
    linear-gradient(180deg, #3b2a20, #52392a);
  image-rendering: pixelated;
}
.wall-paper {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, .28), rgba(0, 0, 0, .04)),
    url("../img/decor/wall-paper.png") left bottom / 32px 32px repeat,
    linear-gradient(180deg, #2f4438, #3a5245);
  image-rendering: pixelated;
}
/* the designs — hand-pixelled strips shown at 2x, anchored to the
   front edge and repeated sideways. the rows foreshorten toward the
   wall seam and step half a tile each row, so the floor recedes instead
   of hanging flat like wallpaper. a soft tint keeps the room's light
   falling toward the back. same geometry, five finishes.
   worn planks: one board per receding row, butt joints staggered */
.floor-planks {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, .12), rgba(255, 214, 160, .05)),
    url("../img/decor/floor-planks.png?v=3") left bottom / 64px 102px repeat-x,
    linear-gradient(180deg, #6b4a33, #57402c);
  image-rendering: pixelated;
}
/* diamond parquet: the same wood laid on the bias, block by block */
.floor-parquet {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, .12), rgba(255, 214, 160, .05)),
    url("../img/decor/floor-parquet.png?v=3") left bottom / 64px 102px repeat-x,
    linear-gradient(180deg, #6b4a33, #57402c);
  image-rendering: pixelated;
}
.floor-terracotta {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, .12), rgba(255, 214, 160, .05)),
    url("../img/decor/floor-terracotta.png") left bottom / 64px 102px repeat-x,
    linear-gradient(180deg, #8a4a2c, #6e3a22);
  image-rendering: pixelated;
}
.floor-sage {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, .12), rgba(255, 214, 160, .05)),
    url("../img/decor/floor-sage.png") left bottom / 64px 102px repeat-x,
    linear-gradient(180deg, #5a6a50, #46543f);
  image-rendering: pixelated;
}
.floor-slate {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, .14), rgba(160, 180, 255, .05)),
    url("../img/decor/floor-slate.png") left bottom / 64px 102px repeat-x,
    linear-gradient(180deg, #333b52, #262c40);
  image-rendering: pixelated;
}
/* the idle bob lives on the stage and the walk-in on the sprite itself. both
   animate transform, and two transform animations on one element do not
   compose - the later one simply wins, which silently ate the walk-in. */
.customer-stage {
  /* the regular is scenery to the mouse — their box would otherwise
     shade a big patch of the clickable floor they stand on */
  pointer-events: none;
  /* source art is 64px square - scale by whole numbers only, or pixelated
     rendering makes some source pixels twice as wide as their neighbours */
  width: 128px;
  height: 128px;
  flex: none;
  display: block;
  position: relative;
  z-index: 1;
  /* feet tucked behind the counter strip — the regular stands on the
     customer side, the same way the keepers do in town */
  margin-bottom: -10px;
  transform-origin: 50% 100%;
  animation: bob 3s ease-in-out infinite;
}
.customer-sprite {
  width: 100%;
  height: 100%;
  display: block;
  transform-origin: 50% 100%;
  filter: drop-shadow(0 3px 0 rgba(45, 30, 18, .18));
}
/* waiting on the next portrait to decode - hold the spot but show nothing */
.customer-sprite.pending { opacity: 0; animation: none; }
.customer-sprite.arriving { animation: walk-in .9s linear both; }
/* the regular comes in the back door and walks up to the counter — small
   and far away first, full size by the time they reach you, hopping on
   each step and covering less ground as they slow to a stop */
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes walk-in {
  0%   { transform: translate(86px, -92px) scale(.5);   opacity: 0; }
  10%  { opacity: 1; }
  18%  { transform: translate(69px, -85px) scale(.58); }
  34%  { transform: translate(53px, -64px) scale(.68); }
  50%  { transform: translate(37px, -53px) scale(.78); }
  66%  { transform: translate(22px, -29px) scale(.87); }
  82%  { transform: translate(9px,  -17px) scale(.94); }
  100% { transform: translate(0, 0)        scale(1); }
}
/* the dialogue box, JRPG style: a full-width panel under the scene
   rather than a bubble floating in it — nothing in the room gets
   covered while somebody talks. a min-height keeps the cup tray from
   hopping as lines of different lengths come and go. */
.speech {
  position: relative;
  margin: .7rem 0 .9rem;
  /* tall enough for name + line + the longest fact card, so the box never
     grows when a regular starts talking about Riley — the fact fades in
     inside the reserved space and the cup tray stays put. the empty space
     when nobody is telling a story is the price of the serve buttons
     never moving under the cursor. */
  min-height: 9em;
  background: var(--paper-deep);
  border: 2px solid var(--frame-light);
  border-radius: 12px;
  padding: .65rem .9rem;
  font-size: .97rem;
}
/* with the regulars' stories turned down the fact card never opens, so
   the box keeps a smaller constant instead: the name-and-docket strip
   plus the two lines the wrong-cup message takes at desk width. still a
   fixed height rather than fit-to-content — lines run one or two long,
   and a box that followed them would hop the serve buttons between
   customers, which is the one thing this box exists to prevent.
   measured at 6.6em with the wrong-cup line in the box; a touch more so
   nothing on the menu nudges it. */
.speech.quiet { min-height: 6.8em; }
/* the tail points up at whoever is standing at the counter */
.speech::before {
  content: "";
  position: absolute;
  left: 68px; top: -8px;
  width: 12px; height: 12px;
  background: var(--paper-deep);
  border-left: 2px solid var(--frame-light);
  border-top: 2px solid var(--frame-light);
  transform: rotate(45deg);
}
/* name on the left, docket on the right, both on the strip above the
   dialogue. it wraps rather than squeezing, so a long name and a long
   order stack instead of colliding. */
.speech-head {
  display: flex;
  align-items: center;
  gap: .3rem .7rem;
  flex-wrap: wrap;
  margin-bottom: .1rem;
}
.speech .who {
  font-family: var(--head-font);
  font-size: .8rem;
  color: var(--accent);
  margin-bottom: 0;
}
/* the tip expands the bubble, so grow it rather than snapping. a grid row
   from 0fr to 1fr is the one way to transition to a height the content
   decides for itself. */
.speech .tip-fact {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .28s ease, opacity .22s ease;
}
.speech .tip-fact.show {
  grid-template-rows: 1fr;
  opacity: 1;
}
/* the clipping layer carries no spacing of its own - any margin, padding or
   border here would set a minimum track size and stop the row collapsing */
.speech .tip-inner {
  min-height: 0;
  overflow: hidden;
}
.speech .tip-body {
  display: block;
  margin-top: .4rem;
  padding-top: .4rem;
  border-top: 2px dashed color-mix(in srgb, var(--muted) 40%, transparent);
  color: var(--muted);
  font-size: .92rem;
}

/* the docket, sitting across the header strip from the name. plain text,
   no rule and no panel of its own: it is a caption on the bubble, and any
   border here would be a box inside a box. laid out as normal inline flow
   rather than flex cells, so the longest order on the menu wraps under
   itself with the label still at the head of the first line. */
/* an author `display` rule would beat the browser's own
   `[hidden] { display: none }`, leaving an empty ORDER label in the header
   once the drink is served */
.speech .order-slip[hidden] { display: none; }
.speech .order-slip {
  display: block;
  margin-left: auto;   /* pushed to the far side, opposite the name */
  text-align: right;
  font-size: .88rem;
}
.speech .order-slip .slip-label {
  font-family: var(--head-font);
  font-size: .7rem;
  letter-spacing: .06em;
  color: var(--muted);
  text-transform: uppercase;
}
.speech .order-slip .slip-text { color: var(--text); }

/* the same link, said mid-sentence by a customer holding the wrong cup.
   no box: it is typed in one character at a time with the rest of the
   line, and a border would sit there empty while the words arrive. */
/* the link and its full stop travel together — see BOARD_LINK in game.js */
.speech .board-cue { white-space: nowrap; }
.speech .board-link {
  font: inherit;
  color: var(--accent);
  background: none;
  border: 0;
  padding: 0;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.speech .board-link:hover,
.speech .board-link:focus-visible { color: var(--text); }

/* the cup */
.cup-row {
  display: flex;
  align-items: stretch;
  gap: .8rem;
  margin: 1.1rem 0 .9rem;
  flex-wrap: wrap;
}
/* the cup is a recessed tray rather than a text field, and its footprint is
   fixed at the maximum order — the longest drink beside the longest pastry —
   so the counter never reflows as ingredients land in it */
.cup {
  --slot: 34px;
  --slots: 8;
  flex: none;
  width: calc(var(--slot) * var(--slots) + 6px * (var(--slots) - 1) + 22px);
  height: calc(var(--slot) + 18px);
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--paper-deep);
  border: 3px solid var(--frame-light);
  border-radius: 10px;
  box-shadow: inset 0 3px 0 var(--chunk);
  padding: 6px 8px;
}
.cup .placeholder {
  margin: 0 auto; /* centred while the tray is empty */
  color: var(--muted);
  font-size: .85rem;
  font-family: var(--body-font);
}
/* every ingredient sits in the tray as its own tile */
.cup .in {
  width: var(--slot);
  height: var(--slot);
  flex: none;
  padding: 3px;
  object-fit: contain;
  background: var(--paper);
  border: 2px solid var(--frame-light);
  border-radius: 8px;
  box-shadow: 0 2px 0 var(--chunk);
  animation: drop-in .25s ease-out;
}
@keyframes drop-in {
  from { transform: translateY(-10px); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}

/* ingredient + action buttons */
.pantry { display: flex; flex-wrap: wrap; gap: .5rem; }
.ing { display: inline-flex; align-items: center; gap: .35rem; }
.ing-icon { width: 24px; height: 24px; }
.ing {
  font-family: var(--head-font);
  font-size: .92rem;
  background: var(--paper-deep);
  color: var(--text);
  border: 3px solid var(--frame-light);
  border-radius: 10px;
  box-shadow: 0 3px 0 var(--chunk);
  padding: .4rem .8rem;
  cursor: pointer;
  transition: transform .1s ease, box-shadow .1s ease;
}
.ing:hover { transform: translateY(-2px); }
.ing:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--chunk); }
.actions { display: flex; align-items: stretch; gap: .5rem; }
.btn-serve {
  font-family: var(--head-font);
  font-size: .95rem;
  background: var(--accent);
  color: #fffaf0;
  border: 3px solid color-mix(in srgb, var(--accent) 60%, black);
  border-radius: 10px;
  box-shadow: 0 4px 0 var(--chunk);
  padding: .45rem 1.1rem;
  cursor: pointer;
  transition: transform .1s ease;
}
.btn-serve:hover { transform: translateY(-2px); }
.btn-serve:active { transform: translateY(2px); }
/* the bin. it stretches to the serve button's height on its own (.actions
   is align-items: stretch), so only the width is set here — wide enough
   that the tap target clears 44px on a phone. */
.btn-dump {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 52px;
  background: var(--paper-deep);
  border: 3px solid var(--frame-light);
  border-radius: 10px;
  box-shadow: 0 4px 0 var(--chunk);
  padding: .3rem .8rem;
  cursor: pointer;
}
.btn-dump:active { transform: translateY(2px); box-shadow: 0 2px 0 var(--chunk); }
.btn-dump img { width: 32px; height: 32px; display: block; }
/* both frames are the same 48px canvas with the body in the same place,
   so swapping them lifts the lid and moves nothing else */
.btn-dump .bin-open { display: none; }
.btn-dump:hover .bin-shut,
.btn-dump:focus-visible .bin-shut { display: none; }
.btn-dump:hover .bin-open,
.btn-dump:focus-visible .bin-open { display: block; }

/* result flourish */
.customer-sprite.result-happy { animation: happy-pop .4s ease-out; }
@keyframes happy-pop {
  0% { transform: scale(1); }
  40% { transform: scale(1.15) rotate(-3deg); }
  100% { transform: scale(1); }
}

/* recipe chalkboard tweaks */
.recipes .loadout { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
.recipes .slot { text-align: left; padding: .6rem .8rem; }
.recipes .slot .r-name {
  font-family: var(--head-font);
  font-size: .85rem;
  color: var(--chalk-text);
  display: block;
}
.recipes .slot .r-steps { font-size: 1rem; letter-spacing: .12rem; display: flex; gap: .15rem; align-items: center; }
.recipes .slot .r-steps img { width: 24px; height: 24px; }
/* the board is one grid, so a section heading has to span every column */
.recipes .board-head {
  grid-column: 1 / -1;
  font-family: var(--head-font);
  font-size: .85rem;
  letter-spacing: .05em;
  color: var(--chalk-text);
  opacity: .7;
  margin: .5rem 0 -.2rem;
}
.recipes .board-head:first-child { margin-top: 0; }

@media (max-width: 640px) {
  /* the room itself no longer relayouts here — the scaler shrinks the
     whole desk-size scene as one piece instead */
  /* narrower lines wrap further — the reserved box grows with them */
  .speech { min-height: 12.4em; }
  /* stories off: the docket takes its own line here and the wrong-cup
     message runs three lines, so the smaller constant is taller too */
  .speech.quiet { min-height: 10.2em; }
  /* eight slots at desk size overflow a phone, so the tiles shrink rather
     than the tray wrapping — the reserved footprint is the point */
  .cup { --slot: 26px; width: 100%; }
  /* there is no room beside the name here, so the docket takes the line
     under it and reads left-to-right like everything else. flush right is
     only worth having while it is actually sitting at the right. */
  .speech .order-slip {
    width: 100%;
    margin-left: 0;
    text-align: left;
  }
}

/* unlock toasts + hints. they hang from the game bar, inside the frame,
   where the eye already is — they used to be pinned to the bottom of the
   viewport, and on a page this tall that was a screen away from the room
   they were talking about. game.js mounts each one into the bar of
   whichever panel is on top: the café's, or an open modal's, since a
   <dialog> in the top layer covers anything mounted behind it. */
.toast {
  position: absolute;
  /* --stack is game.js's: the newest toast sits at the default, and each
     older one is pushed one toast further along */
  top: calc(100% + var(--stack, 10px));
  left: 50%;
  transform: translateX(-50%);
  /* lit, not panelled: in the bar's own browns it read as a piece of the
     bar that had come loose. the lamp yellow is the room's "something
     lit up" colour and is spoken for by nothing else — mint is Serve,
     peach is clickable. */
  background: var(--lamp);
  color: var(--frame-dark);
  border: 3px solid var(--frame-dark);
  border-radius: 12px;
  box-shadow: 0 5px 0 var(--chunk);
  padding: .6rem 1.1rem;
  font-family: var(--head-font);
  font-size: .9rem;
  z-index: 50;
  /* anchored at the bar's midpoint, a shrink-to-fit box would only be
     offered the right-hand half of the bar to fit into, and wrap at
     that — so it takes its natural width and is only capped by the bar */
  width: max-content;
  max-width: calc(100% - 1.6rem);
  text-align: center;
  /* not drop-in: those keyframes set their own transform, which would
     overwrite the centering translateX for the animation's 0.3s and
     park the toast half a width to the right until the snap. this one
     carries the -50% through the fall. */
  animation: toast-in .3s ease-out;
}
@keyframes toast-in {
  from { transform: translate(-50%, -10px); opacity: 0; }
  to   { transform: translate(-50%, 0); opacity: 1; }
}
/* a toast with somewhere to go. same frame, same place, same six
   seconds — the only difference is that this one takes the pointer, and
   the arrow is there so you can tell before you try. a <button> arrives
   with a font, a text-align and a system focus ring of its own, so all
   three are put back to what the div had. */
.toast-go {
  font: inherit;
  font-family: var(--head-font);
  font-size: .9rem;
  text-align: center;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition: background .12s ease, border-color .12s ease;
}
.toast-go:hover { border-color: var(--accent-2); }
.toast-go:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 3px; }
/* the same glyph the Serve and Next buttons use — it is in the pixel
   font, which is the whole reason those two use it. in the text's own
   ink: peach on lamp yellow is a whisper in dark mode */
.toast-arrow { color: var(--frame-dark); margin-left: .15rem; }
/* the exit: game.js flags the toast just before removing it, and it
   sinks away in the last few frames instead of blinking off */
.toast.going {
  opacity: 0;
  transform: translate(-50%, 8px);
  /* opacity and transform only: game.js writes --stack inline to stack
     toasts, and a transition on the offset would make the others slide
     about every time one of them expires */
  transition: opacity .4s ease, transform .4s ease;
  pointer-events: none;
}
/* on a phone the scaled room is a hundred-odd pixels tall, and a toast
   under the bar covers the whole of it — the pair that serve 30 hands
   over takes the speech bubble too. so here they sit at the foot of the
   frame instead, over the ingredient row, which is roughly where they
   always used to be. the bar stops being the containing block and the
   frame takes over; --stack is the same number, measured from the other
   end. */
@media (max-width: 640px) {
  .game { position: relative; }
  .game-bar { position: static; }
  .toast { top: auto; bottom: var(--stack, 10px); }
}
.pay-note { font-family: var(--mono); font-size: .74rem; }

/* ---------- the market: ingredients + furnishings ---------- */
.shop-section {
  font-family: var(--head-font);
  font-size: .82rem;
  color: var(--muted);
  padding: .9rem .9rem 0;
  letter-spacing: .03rem;
}
.shop-section img { margin-right: .35rem; }
.shop-grid { padding: .9rem; }
.shop-grid:not(:last-child) { padding-bottom: .2rem; }
.decor-row {
  display: flex;
  align-items: center;
  gap: .7rem;
  width: 100%;
  text-align: left;
  background: var(--paper-deep);
  border: 3px solid var(--frame-light);
  border-radius: 10px;
  box-shadow: 0 3px 0 var(--chunk);
  padding: .55rem .8rem;
  margin-bottom: .55rem;
  cursor: pointer;
  transition: transform .1s ease;
  font-family: var(--body-font);
  color: var(--text);
  font-size: .95rem;
}
.decor-row:hover:not(:disabled) { transform: translateY(-2px); }
.decor-row:disabled { opacity: .5; cursor: default; box-shadow: none; }
.decor-row.owned { opacity: 1; border-color: var(--accent); cursor: default; }
.decor-icon { font-size: 1.6rem; width: 36px; display: flex; justify-content: center; }
.decor-icon img { width: 36px; height: 36px; object-fit: contain; }
.decor-info { flex: 1; }
.decor-name { font-family: var(--head-font); font-size: .9rem; display: block; }
.decor-meta { font-family: var(--mono); font-size: .74rem; color: var(--muted); }
.decor-check { font-family: var(--head-font); color: var(--accent); font-size: .9rem; }

/* something in stock you can afford — worn by the door and the town lots */
.station-dot {
  position: absolute;
  top: -.25rem;
  right: -.6rem;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent-2);
  box-shadow: 0 0 0 2px var(--frame-dark);
  animation: dot-pulse 1.8s ease-in-out infinite;
}
@keyframes dot-pulse { 0%, 100% { opacity: .5; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .station-dot { animation: none; } }

/* every station opens in the same kind of window */
.cafe-modal {
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  width: min(680px, 93vw);
  max-height: 88vh;
  overflow: visible;
}
/* the panel owns the height and only its body scrolls, so the bar with the
   way out never leaves the screen */
.cafe-modal .game,
.cafe-modal .chalkboard {
  display: flex;
  flex-direction: column;
  max-height: 88vh;
}
.cafe-modal .modal-body { overflow-y: auto; min-height: 0; }
.cafe-modal::backdrop { background: rgba(20, 12, 8, .72); }
.cafe-modal .game,
.cafe-modal .chalkboard { margin: 0; }
.cafe-modal[open] { animation: radio-in .16s ease-out; }
.cafe-modal .game-bar { flex: none; }
.chalk-head { position: relative; flex: none; }
.chalk-close { position: absolute; top: -.15rem; right: 0; }

.radio-modal { width: min(560px, 92vw); }
/* the decorate panel carries two catalogues at once, so it runs wider
   than the single-fixture pickers and packs its rows tighter — at the
   radio's width the wall would push the floor off the bottom */
.decor-modal { width: min(760px, 94vw); }
.decor-modal /* the radio's crate row: the decor panel's chips, put to work filtering
   the shelf. the modal body carries no padding of its own, so the row
   brings its own side gutters — ten chips wrap to a second line, and one
   landing flush against the modal edge reads like it fell off the shelf */
.radio-crates {
  padding-left: 1.1rem;
  padding-right: 1.1rem;
}
/* ten crates is three more than the decor panel ever shows, and at the
   decor chip's size they wrapped to three lines — most of the modal
   before a single record. narrower pills and a slightly smaller face fit
   them on two. the 44px height stays: it is the thumb target, and it is
   not the dimension that was costing the room */
.radio-crates .decor-chip {
  font-size: .8rem;
  padding-left: .8rem;
  padding-right: .8rem;
}
/* a crate you own nothing in yet: still browsable, since the ??? rows
   behind it are what send you to the market — but dimmed, so the row
   shows at a glance where the collection actually is */
.decor-chip.empty { opacity: .45; }
.decor-chip.empty[aria-selected="true"] { opacity: 1; }
/* the crate the radio is confined to, marked on the row that stays put
   while the list scrolls. this is not the same thing as the chip you have
   selected — you can browse one crate while another plays — so it is said
   with the speaker glyph and a ring rather than the accent fill, which
   already means "the crate you are looking at" */
.radio-crates .decor-chip.on-air {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  color: var(--text);
  border-color: var(--accent);
  box-shadow: 0 3px 0 var(--chunk), inset 0 0 0 1px var(--accent);
}

.track-list { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
/* the chip row: which fixture the panel is showing. it wraps to a second
   line on a phone rather than scrolling sideways, so no chip can hide
   off the edge where nobody would look for it */
.decor-tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* centred like the section heading and the note beneath it, so when
     the row wraps the short line sits under the middle of the long one
     instead of leaving a chip stranded at the left edge */
  justify-content: center;
  gap: .55rem;
  /* the row reads as a shelf of tabs rather than loose buttons: a dashed
     rule under it, the same one the fact card uses to divide the speech
     bubble, so the panel has one vocabulary for "a line between things".
     the modal body carries no padding of its own, so the top margin here
     is what keeps the chips off the REDECORATE bar — they sat flush
     against it otherwise */
  margin: 1.05rem 0 1.1rem;
  padding-bottom: .95rem;
  border-bottom: 2px dashed color-mix(in srgb, var(--muted) 40%, transparent);
}
.decor-chip {
  font-family: var(--head-font);
  font-size: .85rem;
  line-height: 1;
  letter-spacing: .02em;
  color: var(--muted);
  background: var(--paper-deep);
  border: 2px solid var(--frame-light);
  border-radius: 999px;
  box-shadow: 0 3px 0 var(--chunk);
  /* a comfortable thumb target — the whole point of the panel is that it
     works where the fixtures in the room have shrunk too small to hit.
     the height comes from min-height so the padding is free to shape the
     pill sideways rather than fight it for the vertical */
  min-height: 44px;
  padding: .5rem 1.15rem;
  cursor: pointer;
  transition: color .12s ease, background .12s ease,
              border-color .12s ease, transform .12s ease;
}
.decor-chip:hover {
  color: var(--text);
  border-color: var(--frame);
  transform: translateY(-1px);
}
/* pressed, the chip sinks onto its own shadow — the chunky-button feel
   the serve and ingredient buttons already have */
.decor-chip:active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--chunk);
}
.decor-chip:focus-visible {
  outline: 3px solid var(--accent-2);
  outline-offset: 2px;
}
/* the chosen chip wears the same solid accent as the serve button, so
   "this is the one you are looking at" reads the same everywhere */
.decor-chip[aria-selected="true"] {
  color: #fffaf0;
  background: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 60%, black);
  box-shadow: 0 3px 0 color-mix(in srgb, var(--accent) 45%, black);
}
.decor-chip[aria-selected="true"]:hover {
  color: #fffaf0;
  border-color: color-mix(in srgb, var(--accent) 60%, black);
}
@media (prefers-reduced-motion: reduce) {
  .decor-chip { transition: none; }
  .decor-chip:hover, .decor-chip:active { transform: none; }
}
/* these three carry a display of their own, and an author rule beats the
   browser's [hidden] no matter how specific it is — so hiding one from
   script does nothing at all unless the sheet says so here */
.decor-chip[hidden] { display: none; }
.decor-section[hidden] { display: none; }
.decor-row[hidden] { display: none; }
/* and the shelf's two, for the same reason — the crate chips filter by
   flipping [hidden] on the rows and their headings */
.track[hidden] { display: none; }
.track-group[hidden] { display: none; }

/* the same chips on a shop counter — the record crates and the eight
   aisles of Hearth & Home. that row is not inside a dialog, so it carries
   the padding the modal body was giving it, and a chip can wear the
   affordable-dot the lots and the toolbar use: with the grid filtered,
   that dot is the only sign that the thing you can afford is one chip
   over. */
.crate-tabs {
  margin: 0;
  padding: 1rem .9rem .95rem;
}
.crate-tabs .decor-chip { position: relative; }
.crate-tabs .crate-dot[hidden] { display: none; }
/* on the selected chip the dot sits on the accent it is drawn in, so it
   takes the paper colour instead and stays a dot rather than a smudge */
.crate-tabs .decor-chip[aria-selected="true"] .crate-dot {
  background: var(--paper-deep);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 60%, black);
}
/* the crate or aisle name sits between the dashed rule under the chips
   and the goods it names. more room above than below, so it reads as
   the head of the list rather than a caption hanging off the chips. */
#crateHead,
.shop-section .shop-head { padding-top: 1.05rem; }
/* now that every button in here carries an icon the row is wider than a
   phone, so it wraps onto a second line rather than pushing close off
   the edge of the panel */
.radio-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: .5rem;
}
/* the bar's buttons hold different things — guillemets that fall back to a
   font with taller metrics, a 24px icon, plain words — and each was being
   drawn to fit its own contents. one height for all of them, and a flat
   line-height so the fallback glyphs stop pushing their box around */
.radio-actions .reset-btn {
  min-height: 34px;
  min-width: 34px;
  justify-content: center;
  line-height: 1;
}
@keyframes radio-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .cafe-modal[open] { animation: none; }
}

/* the radio library: one row per record, the playing one lit */
/* the radio's crate row: the decor panel's chips, put to work filtering
   the shelf. the modal body carries no padding of its own, so the row
   brings its own side gutters — ten chips wrap to a second line, and one
   landing flush against the modal edge reads like it fell off the shelf */
.radio-crates {
  padding-left: 1.1rem;
  padding-right: 1.1rem;
}
/* ten crates is three more than the decor panel ever shows, and at the
   decor chip's size they wrapped to three lines — most of the modal
   before a single record. narrower pills and a slightly smaller face fit
   them on two. the 44px height stays: it is the thumb target, and it is
   not the dimension that was costing the room */
.radio-crates .decor-chip {
  font-size: .8rem;
  padding-left: .8rem;
  padding-right: .8rem;
}
/* a crate you own nothing in yet: still browsable, since the ??? rows
   behind it are what send you to the market — but dimmed, so the row
   shows at a glance where the collection actually is */
.decor-chip.empty { opacity: .45; }
.decor-chip.empty[aria-selected="true"] { opacity: 1; }

.track-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: .55rem;
  padding: 1rem 1.1rem .3rem;
}
.track {
  display: flex;
  align-items: center;
  gap: .7rem;
  text-align: left;
  background: var(--paper-deep);
  border: 3px solid var(--frame-light);
  border-radius: 10px;
  box-shadow: 0 3px 0 var(--chunk);
  padding: .5rem .7rem;
  cursor: pointer;
  font: inherit;
  color: inherit;
  transition: transform .1s ease, border-color .12s ease;
}
.track:hover:not(:disabled) { transform: translateY(-2px); }
.track.locked { opacity: .5; cursor: default; box-shadow: none; }
.track.playing { border-color: var(--accent); }
.track-art { width: 24px; display: flex; justify-content: center; flex: none; }
/* the wide machine sprites letterbox inside the square thumbnail */
.track-art img { object-fit: contain; }
.track-info { flex: 1; min-width: 0; }
.track-name { font-family: var(--head-font); font-size: .84rem; display: block; }
.track-blurb {
  font-family: var(--mono);
  font-size: .7rem;
  color: var(--muted);
  display: block;
}
/* the speaker on the playing row pulses, like a needle in a groove */
.track-state { width: 16px; flex: none; line-height: 0; }
.track.playing .track-state img { animation: track-pulse 1.6s ease-in-out infinite; }
@keyframes track-pulse { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .track.playing .track-state img { animation: none; opacity: 1; }
}
/* the two deck modes light up the same way when they are on. locked is
   the game running the deck — each place plays its own music — and the
   padlock closes with it; unlocked, the records are yours to pick */
.shuffle-btn.on, .lock-btn.on { border-color: var(--accent-2); color: var(--accent-2); }

/* the little shuffle stamp on each row: lit means "in the rotation",
   dim means the record is benched and shuffle passes it over */
/* the rotation stamp. every record you own is in the rotation until you
   bench it, so "in" is the state of an untouched shelf — thirty-eight lit
   stamps that between them say nothing. the emphasis runs the other way:
   in the rotation is quiet to the point of nearly disappearing, and the
   records you have taken out are the ones that catch the eye. */
.track-rot {
  flex: none;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 2px solid transparent;
  border-radius: 7px;
  cursor: pointer;
  opacity: .3;
  transition: opacity .12s ease, border-color .12s ease;
}
/* benched: the stamp is struck through and sits in its own box, the one
   loud thing in a row that has otherwise gone quiet */
.track-rot:not(.on) {
  opacity: 1;
  border-color: var(--accent-2);
}
/* the strike. drawn rather than a glyph so it lands on the same diagonal
   at any zoom, and marked aria-hidden by being a pseudo-element at all */
.track-rot:not(.on)::after {
  content: "";
  position: absolute;
  left: 1px;
  right: 1px;
  top: 50%;
  height: 2px;
  background: var(--accent-2);
  transform: rotate(-45deg);
  transform-origin: center;
}
/* quiet is not invisible: the stamp still has to answer a pointer, or the
   only control on the row stops looking like one */
.track-rot:hover, .track-rot:focus-visible { opacity: 1; border-color: var(--frame-light); }
.track-rot:not(.on):hover, .track-rot:not(.on):focus-visible { border-color: var(--accent-2); }

/* a benched row: still readable, still clickable — dropping a benched
   record on the platter by hand is allowed — but visibly stood down. the
   sleeve loses its colour, which is what reads first at this size. */
.track.benched { opacity: .62; }
.track.benched .track-art img { filter: grayscale(1); }
/* unless it is the one playing. a benched record put on by hand is an
   odd pair of facts to hold at once, and the row saying what is on the
   platter has to win — the struck stamp still says the rest. */
.track.benched.playing { opacity: 1; }
.track.benched.playing .track-art img { filter: none; }
.shuffle-btn:disabled { opacity: .45; cursor: default; }

/* hearts in the speech bubble */
.hearts { margin-left: .35rem; }
.hearts img { margin-right: 1px; }

/* new-game reset */
.reset-btn {
  font-family: var(--head-font);
  font-size: .78rem;
  background: transparent;
  color: #f3e8d0;
  border: 2px solid rgba(243, 232, 208, .4);
  border-radius: 8px;
  padding: .15rem .55rem;
  cursor: pointer;
  transition: background .12s ease;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
}
.reset-btn:hover { background: var(--frame-dark); }
/* these carry no words in the radio bar, so the ring the keyboard leaves
   behind is the only thing saying where you are */
.reset-btn:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 2px; }
.reset-btn.confirm { border-color: var(--accent-2); color: var(--accent-2); }
/* the score row can still outgrow a phone's width, so let it wrap
   rather than clip. the speaker and its slider moved to the pause menu:
   a drag control is a poor fit for a bar that has to survive 320px */
@media (max-width: 620px) {
  .game-bar { flex-wrap: wrap; gap: .45rem .8rem; }
  .game-score { flex-wrap: wrap; gap: .5rem .7rem; }
  /* the radio's deck is six buttons now — transport, shuffle, lock,
     close — and at desktop spacing the last one wrapped onto a row of
     its own. pulled in a touch so the whole deck stays on one line */
  .radio-actions { gap: .35rem; }
  .radio-actions .reset-btn { padding-left: .35rem; padding-right: .35rem; }
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- the town: a walk from the counter, on the same stage ---------- */
/* the game frame holds one stage; walking somewhere dips it dark for a
   beat while the scenery is swapped underneath (game.js does the swap) */
.stage { transition: opacity .2s ease; }
.stage.walking { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .stage { transition: none; }
}

.town-note {
  font-family: var(--mono);
  font-size: .78rem;
  color: #f3e8d0;
  opacity: .7;
  text-align: center;
  margin: 0 0 .9rem;
}

.town-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: .8rem;
  align-items: end;
}

/* each lot is a building you can walk into. the art sits on a strip of
   pavement so four separate sprites read as one street. */
.lot {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .2rem;
  font: inherit;
  color: inherit;
  background: linear-gradient(180deg, #2a3348 0%, #3d4460 62%, #4a4034 62%, #3a3128 100%);
  border: 3px solid var(--frame-dark);
  border-radius: 12px;
  box-shadow: 0 5px 0 var(--chunk);
  padding: .6rem .5rem .7rem;
  cursor: pointer;
  transition: transform .12s ease, box-shadow .12s ease, filter .12s ease;
}
.lot:hover { transform: translateY(-3px); box-shadow: 0 8px 0 var(--chunk); filter: brightness(1.08); }
.lot:active { transform: translateY(1px); box-shadow: 0 3px 0 var(--chunk); }
.lot:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 3px; }

.lot-art {
  width: 112px;
  height: 112px;
  image-rendering: pixelated;
}

.lot-name { font-family: var(--head-font); font-size: 1rem; text-align: center; line-height: 1.15; }
.lot-sign {
  font-family: var(--mono);
  font-size: .68rem;
  color: #f3e8d0;
  opacity: .68;
}
.lot-dot { top: .45rem; right: .45rem; }

/* ---------- inside a shop ---------- */
/* each shop is the café's room again: the same 889x280 canvas scaled as
   one piece, the same wall and floor surfaces — only furnished for its
   keeper instead of the player, and every offset below is tuned against
   that same canvas. instead of the café's counter strip, a drawn
   checkout counter stands in the right portion of the room with the
   keeper behind it, the way a shop faces you when you walk in. */

/* the door: the café's own sprite at the café's own 2x, planted just
   past the floor seam like the café's (bottom 98 → slab past the seam,
   top level with the window frame). the one working fixture in
   somebody else's room — it walks back out to the street. */
.shop-door {
  left: 48px;
  bottom: 98px;
  width: 80px;
  height: 152px;
  z-index: 1;
}
.shop-door .spot-label { left: 42px; }

/* a pane of the town's one sky on the right stretch of wall, hung level
   with the café's windows. the inner .window-view is the café's own
   sandwich — scene, weather, frame — so the shops rain and snow the
   moment the café does. looking only: you don't get to change the
   weather from somebody else's shop. */
.shop-window {
  position: absolute;
  top: 26px;
  left: 486px;
  width: 96px;
  height: 96px;
  display: block;
}

/* the wares, standing about the way the café's decor does: pinned, not
   flowed, at the room's 2x, and scenery only — nothing here is for
   sale to keep. the stock stands in shelving rather than in heaps on
   the floor: two units to a shop, planted at the wall seam so they
   read as standing against the back wall, and further off than the
   counter. the pair are two different drawings sharing a canvas size,
   so one rule stands them all up: a shop furnished out of a single
   sprite reads as a copy of itself, flipped or not. */
.shop-decor {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.shop-shelf {
  position: absolute;
  bottom: 92px;
  width: 144px;
  height: 168px;
  image-rendering: pixelated;
}
.shelf-a { left: 176px; }
.shelf-b { left: 328px; }

/* the checkout: keeper behind, counter in front. the keeper is pinned
   to the right portion of the room, feet planted higher than the
   counter's base so its front face hides their legs; the counter is
   112px art at the room's 2x, drawn over them (z 2 beats the keeper's
   z 1), reaching the bottom of the scene like the café's own bar. */
.shop-keeper {
  position: absolute;
  left: 636px;
  bottom: 92px;
  margin-bottom: 0;
}
/* Pip's ears spend the top of the sprite, leaving her face low in the
   box — stood at the shared height only her ears cleared the counter,
   so she gets a stool. */
#view-records .shop-keeper { bottom: 112px; }
.shop-bar {
  position: absolute;
  left: 545px;
  bottom: 40px;
  width: 320px;
  height: 192px;
  z-index: 2;
  image-rendering: pixelated;
  pointer-events: none;
}

/* the keeper talks in the café's own speech box, tail bent to point at
   them behind the checkout instead of at the counter's end. no
   reserved fact-card space — a keeper's line never grows. */
.keeper-speech { min-height: 0; }
/* the shopkeepers have no docket, so no header strip: their name goes
   back on its own line above the line they say. */
.keeper-speech .who { display: block; margin-bottom: .1rem; }
.keeper-speech::before { left: 79%; margin-left: -6px; }

/* what you bought at Hearth & Home, standing in the front of house where
   the customers walk in. every item is drawn at 2x like the customers,
   so the whole room is one pixel grid, and each one is pinned to the
   wall, the floor or the counter rather than flowed — a café is a place,
   not a row of icons. the right half is theirs; the left belongs to the
   door and whoever just came through it. */
/* no z-index here on purpose: the layer must not become a stacking
   context, so any item that needs to could raise itself above the
   counter strip (the cat did, back when it slept there) */
.cafe-decor {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.cafe-item {
  position: absolute;
  width: 96px;
  height: 96px;
  image-rendering: pixelated;
  animation: drop-in .35s ease-out;
}
/* (the string lights retired — the neon wall art glows now instead) */
/* the trophy and the fern live on the wall shelf, drawn at 1x — half the
   room's scale, the cheap and honest way to say "further away". the
   sprites carry transparent padding (trophy 4 rows, fern 2), so each
   top is tuned so the VISIBLE base lands a pixel into the plank at 62.
   which slot they stand in comes from the shelf-slot-N classes. */
.item-fern,
.item-radio,
.item-trophy { width: 48px; height: 48px; }
.item-fern   { top: 17px; }
.item-trophy { top: 19px; }
/* the radio came down off the counter and onto the shelf. 5 empty rows
   under it, so top 20 puts its feet on the wood with the others. */
.item-radio  { top: 20px; }
/* the cat found a higher perch: asleep on the shelf among the trinkets,
   drawn at 1x like them. the sprite carries 6 empty rows under the
   tail, so top 21 lands the visible base a pixel into the plank. it
   still breathes instead of bobbing — a bob would lift a sleeping cat
   clear off the wood. */
.item-cat    {
  width: 48px;
  height: 48px;
  top: 21px;
  transform-origin: 50% 100%;
  animation: drop-in .35s ease-out, cat-breathe 3.6s ease-in-out .35s infinite;
}
@keyframes cat-breathe {
  0%, 100% { transform: scaleY(1); }
  50%      { transform: scaleY(1.03); }
}

/* a phone gets a third of the width and the same room to fit in it. the
   answer is not to crowd the furniture but to drop the whole scene to
   1x — the room shrinks, the arrangement survives, and every sprite is
   still on a whole-pixel grid. the shelf pulls in a touch so the menu
   board has wall between the art and the plank. */
/* (the old 620px café relayout is gone — the room-scaler shrinks the one
   desk-size scene to any width instead of rearranging it) */

/* the counter cuts across the bottom, in front of the keeper. a drawn
   board rather than a gradient bar, for the reason the shelf's plank is
   one: everything standing on this counter is a sprite, so a smooth ramp
   with a flat border on top was the last thing in the room that read as
   interface instead of scenery. it is one long piece of wood in the
   shelf's own five browns — lit along the top edge, grain flecked
   through the body, shadowed where it meets the floor — and NOT a
   panelled front: stiles and recessed panels turned the bar into a row
   of blocks rather than a board. the tile is 160x13 at the room's 2x, so
   the grain only comes round every 320px of an 883px room. its wood is
   baked into the art like the shelf's: --frame follows the page theme
   and the walls and floors do not, which made the counter shift colour
   under a room that stayed put. */
.shop-counter {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 26px;
  z-index: 2;
  background: url("../img/decor/counter.png") left top / 320px 26px repeat-x;
  image-rendering: pixelated;
}

/* (the keeper's old vignette — its bob, its brown box, its caption bar —
   retired: the keeper stands in a customer-stage now and talks in the
   speech box, both defined with the café's room) */

/* the café's own lot lists rather than sells */
.stat-line { cursor: default; }
.stat-line .decor-check {
  font-family: var(--mono);
  font-weight: 700;
  color: var(--accent-2);
}

@media (max-width: 620px) {
  .lot-art { width: 88px; height: 88px; }
}

/* .reset-btn sets its own display, which outranks the hidden attribute —
   the back button needs saying so explicitly */
.reset-btn[hidden] { display: none; }

/* ---------- the town map (desktop) ---------- */
/* narrow screens keep the card grid above — a town seen from above needs
   room in both directions, and a map squeezed into one column is a list
   with scenery. from here up the lots stop being cards entirely.

   The map is a single painted image. Composing it out of tiled ground and
   separate building sprites never held together: procedural noise and
   model-painted art do not sit in the same picture, and matching the
   palette does not fix that. One image makes the style unified by
   construction; all this stylesheet does is pin invisible buttons over
   the buildings, in percentages, so they stay put at any width. */

/* the name and blurb are one unit on the map and two flex children in the
   card grid — contents lets the same markup do both */
.lot-label { display: contents; }

@media (min-width: 621px) {
  .town-row {
    position: relative;
    display: block;
    /* locked to the source image, so a hotspot given in percent lands on
       the same pixels no matter how wide the dialog gets */
    aspect-ratio: 400 / 240;
    min-height: 0;
    border: 3px solid var(--frame-dark);
    border-radius: 12px;
    /* the bottom row's signboards hang below the frame on purpose —
       clipping them would cost two labels */
    overflow: visible;
    background: url("../img/town/map-town.png") no-repeat center / 100% 100%;
    image-rendering: pixelated;
  }

  /* evening light over the whole map, so it belongs to the same page as
     the rest of the café rather than looking like a bright cutout */
  .town-row::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
      radial-gradient(120% 90% at 50% 45%, transparent 46%, rgba(24, 15, 10, .46) 100%),
      linear-gradient(180deg, rgba(58, 40, 24, .10), rgba(28, 18, 12, .20));
  }

  /* the same street in the weather the windows are showing. these are not
     filters over the one map — each is its own image, derived from it
     pixel for pixel, so the buildings and the roads are identical and the
     hotspot percentages above still land on the same paint. */
  .town-row.town-snow  { background-image: url("../img/town/map-town-snow.png"); }
  .town-row.town-rain  { background-image: url("../img/town/map-town-rain.png"); }
  .town-row.town-fall  { background-image: url("../img/town/map-town-fall.png"); }
  .town-row.town-night { background-image: url("../img/town/map-town-night.png"); }

  /* the evening wash has to give way, or a snowfield reads as mud and a
     night map goes black. each mood keeps a vignette — the map still has
     to sit on the page — but in its own light and at its own weight. */
  .town-row.town-snow::after {
    background: radial-gradient(120% 90% at 50% 45%, transparent 58%, rgba(22, 30, 48, .30) 100%);
  }
  .town-row.town-rain::after {
    background:
      radial-gradient(120% 90% at 50% 45%, transparent 52%, rgba(16, 22, 36, .36) 100%),
      linear-gradient(180deg, rgba(30, 44, 66, .12), rgba(14, 20, 32, .18));
  }
  .town-row.town-night::after {
    background: radial-gradient(120% 90% at 50% 45%, transparent 62%, rgba(8, 8, 26, .34) 100%);
  }

  /* one hotspot per building, boxed from where the paint actually is
     rather than by eye: the four shops were segmented out of the image
     against the grass and the dirt, and these are their bounding boxes
     divided by 400 x 240. */
  .lot {
    position: absolute;
    z-index: 1;
    display: block;
    background: none;
    border: 0;
    border-radius: 8px;
    box-shadow: none;
    padding: 0;
    gap: 0;
    transition: background-color .12s ease, box-shadow .12s ease;
  }
  .lot[data-shop="cafe"]    { left: 11.25%; top:  7.08%; width: 32.00%; height: 43.33%; }
  .lot[data-shop="grocer"]  { left: 57.25%; top:  4.58%; width: 27.00%; height: 41.67%; }
  .lot[data-shop="records"] { left: 17.00%; top: 52.08%; width: 24.25%; height: 41.25%; }
  .lot[data-shop="goods"]   { left: 58.25%; top: 54.58%; width: 24.75%; height: 38.33%; }

  /* nothing on the map moves or lights up on hover — a glowing rectangle
     over a painted building gives away that the building is not really a
     button. the signboard does the reacting instead. */
  .lot:hover, .lot:active { background: none; box-shadow: none; }
  .lot:focus-visible { outline-offset: 3px; }

  /* the buildings are in the image, so the cards' sprites are dead weight */
  .lot-art { display: none; }

  /* name and blurb butt together into one signboard, hung under the
     building rather than laid out beside it */
  .lot-label {
    display: block;
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    margin-top: 4px;
    text-align: center;
    pointer-events: none;
  }
  .lot-name,
  .lot-sign {
    display: block;
    background: rgba(20, 13, 9, .84);
    border: 2px solid var(--frame-dark);
    padding: .05rem .5rem;
    white-space: nowrap;
  }
  .lot-name {
    font-size: .82rem;
    border-bottom: 0;
    border-radius: 6px 6px 0 0;
    padding-top: .15rem;
  }
  .lot-sign {
    font-size: .62rem;
    border-top: 0;
    border-radius: 0 0 6px 6px;
    padding-bottom: .15rem;
  }
  /* the top row hangs its sign above the roof — below, it would land on
     the roof of whichever shop is underneath it */
  .lot[data-shop="cafe"] .lot-label,
  .lot[data-shop="grocer"] .lot-label {
    top: auto;
    bottom: 100%;
    margin-top: 0;
    margin-bottom: 4px;
  }

  .lot-name,
  .lot-sign { transition: color .12s ease, border-color .12s ease, box-shadow .12s ease; }
  .lot:hover .lot-name,
  .lot:focus-visible .lot-name {
    color: var(--lamp);
    border-color: var(--lamp);
    box-shadow: 0 0 12px rgba(245, 196, 105, .3);
  }
  .lot:hover .lot-sign,
  .lot:focus-visible .lot-sign {
    border-color: var(--lamp);
    box-shadow: 0 0 12px rgba(245, 196, 105, .3);
  }

  /* the "something here" marker hangs over the roof like a quest pin */
  .lot-dot {
    top: -5px;
    right: auto;
    left: 50%;
    transform: translateX(-50%);
    width: 11px;
    height: 11px;
    box-shadow: 0 0 0 2px var(--frame-dark), 0 0 10px 3px rgba(230, 168, 92, .55);
  }

  /* signboards hang past the frame at both ends */
  .town-row { margin: 20px 0 30px; }
}

/* ---------- the boot screen ----------
   game.html only: a dark CRT that sits over the whole page while the
   "cartridge" starts up, then collapses to a bright line like an old
   television being switched off. boot.js runs the timeline. */
/* the inline script in game.html tags <html> with this before first
   paint when the boot should not play — the overlay never renders */
.no-boot .boot { display: none; }
/* boot.js parks the screen here between the power-off and the pause
   menu — hidden, not removed, so MENU can bring it back */
.boot.boot-hidden { display: none; }

.boot {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: #0c0907;
  color: #e8dcc5;
  font-family: var(--head-font);
  text-align: center;
  cursor: pointer;
}
/* scanlines, with the faint uneven flicker of a tired tube */
.boot::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(0deg, rgba(0, 0, 0, .30) 0 1px, transparent 1px 3px);
  animation: boot-flicker 2.6s steps(1) infinite;
}
/* the glass curves away into darkness at the edges */
.boot::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, .6));
}
@keyframes boot-flicker {
  0%, 100% { opacity: .9; }
  7%  { opacity: .72; }
  8%  { opacity: .9; }
  44% { opacity: .78; }
  45% { opacity: .9; }
  71% { opacity: .82; }
  72% { opacity: .9; }
}

.boot-card {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .35rem;
  padding: 0 24px;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .5s ease, transform .5s ease;
}
.boot[data-phase="presents"] .boot-presents,
.boot[data-phase="title"] .boot-title { opacity: 1; transform: none; }
/* the presents card gets out of the way faster than it arrived */
.boot[data-phase="title"] .boot-presents { transition-duration: .25s; }

.boot[data-phase="start"] .boot-start { opacity: 1; transform: none; }
.boot-press {
  font-size: clamp(1.3rem, 5vw, 1.7rem);
  letter-spacing: .3em;
  text-indent: .3em; /* recenters the letterspaced line */
  color: #f5c469;
  animation: boot-blink 1.1s steps(1) infinite;
}

.boot-mark { width: 48px; height: 48px; margin-bottom: .6rem; }
.boot-small {
  font-size: 1.05rem;
  letter-spacing: .35em;
  text-indent: .35em; /* recenters the letterspaced line */
  color: #f5c469;
}
.boot-tiny { font-size: .8rem; letter-spacing: .12em; color: rgba(232, 220, 197, .55); }

.boot-logo {
  width: 96px;
  height: 96px;
  margin-bottom: .5rem;
  filter: drop-shadow(0 0 18px rgba(245, 196, 105, .35));
  animation: boot-bob 1.6s ease-in-out infinite;
}
@keyframes boot-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}
.boot-name {
  font-size: clamp(2rem, 8vw, 3.1rem);
  letter-spacing: .08em;
  line-height: 1.1;
  color: #f5c469;
  text-shadow: 3px 3px 0 #503626;
}

/* the loading bar fills in chunks, the way loading bars used to lie */
.boot-bar {
  width: min(260px, 60vw);
  height: 16px;
  margin-top: 1.2rem;
  border: 2px solid #e8dcc5;
  border-radius: 2px;
  padding: 2px;
}
.boot-fill { display: block; height: 100%; width: 0; background: #f5c469; }
.boot[data-phase="title"] .boot-fill { animation: boot-fill 2.3s steps(12, end) forwards; }
@keyframes boot-fill { from { width: 0; } to { width: 100%; } }

.boot-status {
  min-height: 1.4em;
  margin-top: .55rem;
  font-family: var(--mono);
  font-size: .72rem;
  color: rgba(232, 220, 197, .6);
}

.boot[data-phase="menu"] { cursor: default; }
.boot[data-phase="menu"] .boot-menu { opacity: 1; transform: none; }
.boot[data-phase="menu"] .boot-title { transition-duration: .25s; }

.boot-options {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .3rem;
  margin-top: 1.4rem;
}
/* a flex container outranks the browser's own [hidden] rule, and the
   audio page spends most of its life hidden */
.boot-options[hidden] { display: none; }
/* the audio page names itself, so a card that otherwise looks identical
   to the start menu reads as a page deeper in */
.boot-subhead {
  margin: 0 0 .35rem;
  font-family: var(--mono);
  font-size: .7rem;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: rgba(232, 220, 197, .35);
}
/* the line under the save & load rows where the file picker answers
   back — a filename, or why the file was refused. it keeps its height
   while empty so the rows do not hop when it speaks. */
.boot-note {
  min-height: 1.3em;
  margin: .4rem 0 0;
  font-family: var(--mono);
  font-size: .68rem;
  color: rgba(232, 220, 197, .5);
}
.boot-note-warn { margin: 0 0 .5rem; color: #f5c469; }
/* the file input never shows: the menu's own row clicks it */
.boot-file { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.boot-opt {
  position: relative;
  background: none;
  border: 0;
  padding: .18rem 1.5rem;
  font-family: var(--head-font);
  font-size: 1.1rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(232, 220, 197, .5);
  cursor: pointer;
}
.boot-opt.selected { color: #f5c469; }
/* the little arrow that has fronted every menu since 1986 */
.boot-opt.selected::before {
  content: "\25B8";
  position: absolute;
  left: .3rem;
  animation: boot-blink .9s steps(1) infinite;
}
.boot-opt:focus-visible { outline: 2px solid #f5c469; outline-offset: 2px; }
@keyframes boot-blink { 50% { opacity: 0; } }

.boot-hint {
  margin-top: 1.1rem;
  font-family: var(--mono);
  font-size: .68rem;
  color: rgba(232, 220, 197, .35);
}

.boot-skip {
  position: absolute;
  left: 0; right: 0; bottom: 22px;
  font-family: var(--mono);
  font-size: .68rem;
  color: rgba(232, 220, 197, .35);
  opacity: 0;
  transition: opacity .8s ease .6s;
}
/* only during the intro — once the menu is up there is nothing to skip */
.boot[data-phase="presents"] .boot-skip,
.boot[data-phase="title"] .boot-skip { opacity: 1; }

/* exiting to the desktop: the picture just goes dark — the TV-off
   collapse below would reveal the café behind it, which is exactly
   where the exit is not going */
.boot-exit .boot-card { opacity: 0 !important; transition: opacity .35s ease; }

/* switching the television off: squash to a bright line, then gone */
.boot-off {
  animation: boot-off .5s ease-in forwards;
  transform-origin: 50% 50%;
}
.boot-off::before, .boot-off::after { display: none; }
@keyframes boot-off {
  0%   { transform: scale(1, 1); filter: brightness(1); opacity: 1; }
  60%  { transform: scale(1, .005); filter: brightness(5); opacity: 1; }
  100% { transform: scale(.25, .005); filter: brightness(5); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .boot::before, .boot-logo, .boot-opt.selected::before { animation: none; }
}

/* ---------- the front door ----------
   index.html carries no hud and no footer — the terminal is the menu.
   the scene takes the whole window and holds the terminal centered,
   with the copyright folded into one quiet line beneath it. */
.front-door {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 2.5rem 0;
}
.front-copy {
  margin: 1.6rem 0 0;
  text-align: center;
  font-family: var(--mono);
  font-size: .72rem;
  color: var(--muted);
}

/* ---------- the window ----------
   three layers on the room's pixel grid: a frameless 32px scene
   backdrop (img/decor/scene-*.png), the animated weather, and one
   shared frame sprite (window-frame.png) whose panes are transparent.
   the weather falls between scene and woodwork, so drops and leaves
   pass BEHIND the mullions — and the frame never changes with the
   season. DOM order does the stacking: scene, fx, frame. */
.window-view {
  position: absolute;
  inset: 0;
  display: block;
}
/* the glass opening inside the 48px frame sprite: panes span
   x 9-39, y 8-36 — scene and weather hug that box (a hair wider,
   so the art tucks under the wood with no seam) */
/* .hotspot img paints its images full-bleed — the scene needs the
   extra selector weight to claim just the glass opening instead */
.window-view .w-scene,
.w-fx {
  position: absolute;
  top: 14.6%;
  left: 16.6%;
  /* an abspos <img> will not stretch from insets alone — the box is
     spelled out: 100% minus the left/right and top/bottom margins */
  width: 66.8%;
  height: 62.5%;
}
.w-scene {
  object-fit: cover;         /* 32x32 art in a wider opening: crop, don't squish */
  image-rendering: pixelated;
}
.w-fx {
  overflow: hidden;
  pointer-events: none;
}
.w-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  pointer-events: none;
}

/* the weather itself. every bit falls its own lane at its own phase —
   game.js staggers the delays so the pane never marches in step. */
.fx-bit { position: absolute; top: -12%; }
.drop {
  width: 2px;
  height: 14%;
  background: rgba(210, 230, 250, .75);
  animation: fx-fall .8s linear infinite;
}
.flake {
  width: 4px;
  height: 4px;
  background: #fff;
  animation: fx-drift 4.5s linear infinite;
}
.leaf {
  width: 5px;
  height: 4px;
  background: #d1712f;
  animation: fx-drift 5.5s linear infinite;
}
.leaf:nth-child(even) { background: #e09a3c; }
.leaf:nth-child(3n)   { background: #a8542a; }
.star {
  top: auto;                 /* game.js pins each one to its patch of sky */
  width: 2px;
  height: 2px;
  background: #fff;
  animation: fx-twinkle 2.6s ease-in-out infinite;
}
@keyframes fx-fall {
  to { transform: translateY(880%); }
}
@keyframes fx-drift {
  0%   { transform: translate(0, 0); }
  50%  { transform: translate(4px, 1400%); }
  100% { transform: translate(-3px, 2800%); }
}
@keyframes fx-twinkle {
  0%, 100% { opacity: .25; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .drop, .flake, .leaf { animation: none; top: auto; bottom: 20%; opacity: .6; }
  .star { animation: none; opacity: .8; }
}

/* the same weather falling over the whole town instead of through one
   pane. it is a much bigger box, so the bits are re-sized and given their
   own keyframes: fx-fall and fx-drift travel in multiples of the bit's own
   height, and a 14%-tall raindrop that crosses a window is a gash across
   a map. below 621px the row is a grid of cards and this would land in it
   as a fifth cell, so it is dropped there and the cards carry the mood. */
.town-fx {
  display: none;
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 2;
}
@media (min-width: 621px) {
  .town-fx { display: block; }
}
.town-fx .drop {
  height: 4%;
  animation: fx-fall-town .9s linear infinite;
}
.town-fx .flake,
.town-fx .leaf {
  height: 1.2%;
  animation: fx-drift-town 5s linear infinite;
}
.town-fx .flake { width: 4px; }
.town-fx .leaf { width: 5px; }
@keyframes fx-fall-town {
  to { transform: translateY(3000%); }
}
@keyframes fx-drift-town {
  0%   { transform: translate(0, 0); }
  50%  { transform: translate(5px, 4900%); }
  100% { transform: translate(-4px, 9800%); }
}
@media (prefers-reduced-motion: reduce) {
  .town-fx .drop, .town-fx .flake, .town-fx .leaf { animation: none; }
}

/* under 621px the map is gone and the town is four cards. the gradient on
   a lot is sky over pavement, so the weather is told there instead: the
   same four moods, in the colours the map is painted in. */
@media (max-width: 620px) {
  .town-row.town-snow .lot {
    background: linear-gradient(180deg, #4a5468 0%, #6b7280 62%, #d3dae4 62%, #b9c2d0 100%);
  }
  /* the only mood whose ground is brighter than the lettering — cream on
     snow is unreadable, so the names go dark for this one card only */
  .town-row.town-snow .lot { color: #2a3140; }
  .town-row.town-snow .lot-sign { color: #39445a; opacity: .85; }
  .town-row.town-rain .lot {
    background: linear-gradient(180deg, #232a3a 0%, #39415a 62%, #3b3830 62%, #2c2a24 100%);
  }
  .town-row.town-fall .lot {
    background: linear-gradient(180deg, #3a3b4a 0%, #5a5148 62%, #6b5334 62%, #4a3a26 100%);
  }
  .town-row.town-night .lot {
    background: linear-gradient(180deg, #14162e 0%, #1e2140 62%, #232036 62%, #17141f 100%);
  }
}

/* the window shrunk into a shop tile: the scene tucked behind the
   same shared frame, both pinned to the tile's box */
.view-swatch {
  position: relative;
  display: block;
  width: 36px;
  height: 36px;
  flex: none;
}
.view-swatch img {
  position: absolute;
  image-rendering: pixelated;
}
.view-swatch img:first-child { top: 14.6%; left: 16.6%; width: 66.8%; height: 62.5%; object-fit: cover; }
.view-swatch img:last-child  { inset: 0; width: 100%; height: 100%; }
.track-art .view-swatch { width: 24px; height: 24px; }

/* a floor sold by showing it — the swatch wears the same floor-<id>
   class as the room, just cropped to a tile */
.floor-swatch {
  display: block;
  width: 36px;
  height: 36px;
  border: 2px solid var(--frame-dark);
  border-radius: 6px;
}
.track-art .floor-swatch { width: 24px; height: 24px; }

/* and a wall sold the same way. the room's coverings carry a tint that
   keeps the café dim under the ceiling; at swatch size that just reads
   as mud, so the patch is lifted back up to show the pattern */
.wall-swatch {
  display: block;
  width: 36px;
  height: 36px;
  border: 2px solid var(--frame-dark);
  border-radius: 6px;
  filter: brightness(1.4);
}
.track-art .wall-swatch { width: 24px; height: 24px; }

/* the second heading inside Hearth & Home, over the panes for sale */
.shop-head {
  font-family: var(--head-font);
  font-size: .8rem;
  color: var(--muted);
  text-align: center;
  letter-spacing: .08em;
  padding: .4rem .9rem 0;
}

/* ---------- now playing ---------- */
/* the header pill naming the record on the platter; tapping it opens
   the radio */
.now-playing {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  max-width: 180px;
  overflow: hidden;
  color: var(--muted);
}
.now-playing #nowPlayingName {
  font-family: var(--mono);
  font-size: .72rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.now-playing img { animation: track-pulse 1.6s ease-in-out infinite; }
/* with nothing on the platter the pill is still the door to the radio,
   it just stops advertising */
.now-playing.is-quiet img { animation: none; opacity: .55; }
@media (prefers-reduced-motion: reduce) {
  .now-playing img { animation: none; }
}
@media (max-width: 640px) {
  /* the bar is full on phones, so the pill keeps the note and drops the
     name — it is the only way to the radio and cannot just leave */
  .now-playing { max-width: none; }
  .now-playing #nowPlayingName { display: none; }
}

/* a collection divider in the radio's record list. the name sits in the
   middle the way it always did; the crate's play button hangs off the
   right, and on a narrow list the two stack rather than squeeze. */
.track-group {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: .5rem .8rem;
  font-family: var(--head-font);
  font-size: .72rem;
  color: var(--muted);
  text-align: center;
  letter-spacing: .08em;
  padding: .7rem 0 .1rem;
}
.crate-play {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  font: inherit;
  letter-spacing: .02em;
  color: var(--muted);
  background: var(--paper-deep);
  border: 2px solid var(--frame-light);
  border-radius: 999px;
  box-shadow: 0 2px 0 var(--chunk);
  padding: .3rem .7rem;
  cursor: pointer;
  transition: color .12s ease, border-color .12s ease, transform .12s ease;
}
.crate-play:hover { color: var(--text); border-color: var(--frame); transform: translateY(-1px); }
.crate-play:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--chunk); }
.crate-play:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 2px; }
.crate-play img { width: 14px; height: 14px; }
/* the crate the radio is actually confined to. it wears the accent the
   chosen chip does, because it is the same fact said twice — and it is a
   button that undoes rather than does, so it has to look like a state the
   room is in and not another thing to press for more of the same */
.crate-play.on-air {
  color: #fffaf0;
  background: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 60%, black);
  box-shadow: 0 2px 0 color-mix(in srgb, var(--accent) 45%, black);
}
.crate-play.on-air:hover {
  color: #fffaf0;
  border-color: color-mix(in srgb, var(--accent) 60%, black);
}
@media (prefers-reduced-motion: reduce) {
  .crate-play { transition: none; }
  .crate-play:hover, .crate-play:active { transform: none; }
}

