/* Crumb — Modern Pixel UI.
   White tiles x warm bakery browns x pixel pastry icons.

   Every colour in this file comes from a variable. The light set below sits on
   the page's root and on any element with data-theme: Pastry shop and Bakery
   use it as it is, and a shop with colours of its own gives every one of its
   variables a value in a [data-theme="<id>"] set of its own. Scripts read
   --bg, --digit, --wordmark, --chevron and --sign-ink to draw, so those, like
   every variable used as a text colour, hold one plain colour. */

:root, [data-theme] {
  color-scheme: light;

  /* Grounds. --bg is one plain colour (the tab icon is drawn on it); --page-art
     is drawn over it on the page, --phone-art on the phone's screen. */
  --bg: #f2e7ce;
  --page-art:
    repeating-linear-gradient(0deg, rgba(122, 74, 38, 0.025) 0 2px, transparent 2px 24px),
    repeating-linear-gradient(90deg, rgba(122, 74, 38, 0.025) 0 2px, transparent 2px 24px);
  --phone-art: none;
  --tile: #fffdf6;
  --paper: #fbf3e0;
  --chosen: #fff8e6;

  /* Text. */
  --ink: #4a2f1b;
  --ink-soft: #8a6a4c;
  --accent: #7a4a26;
  --plus: #3e8e5a;
  --minus: #c4483c;

  /* Drawn by assets/app.js on canvases: the balance, CRUMB and the chevron. */
  --digit: #4a2f1b;
  --wordmark: #4a2f1b;
  --chevron: #8a6a4c;

  /* The focus ring. */
  --select: #2bc8d6;

  /* Buttons, and what marks a choice. */
  --get-bg: #7a4a26;
  --get-ink: #fff8e6;
  --get-ledge: #5c3a1d;
  --switch-on-bg: #7a4a26;
  --switch-on-ink: #fffdf6;
  --grant-bg: #efb63c;
  --grant-ledge: #c98f1f;
  --grant-ink: #5c3a1d;
  --confirm-bg: #efb63c;
  --confirm-border: #c98f1f;
  --confirm-ink: #5c3a1d;
  --frame: #7a4a26;
  --highlight: #efb63c;

  /* The app icon, the phone's bezel, the scene dots, the log's dot. */
  --app-icon-bg: var(--slot-bg);
  --app-icon-border: var(--slot-border);
  --app-icon-shadow: 0 8px 20px rgba(92, 58, 29, 0.16);
  --bezel: #5c3a1d;
  --bezel-shadow: 0 14px 34px rgba(92, 58, 29, 0.18);
  --dot-on: #c98f1f;
  --bar-dot: #efb63c;

  /* Slots, the cards' rule, and the ground under a single picture (--face-*:
     nothing at all in the light set). */
  --slot-bg: #efdcb6;
  --slot-border: #a9793f;
  --slot-shadow: none;
  --slot-empty-bg: var(--slot-bg);
  --slot-empty-border: var(--slot-border);
  --slot-empty-shadow: none;
  --limited-bg: #e6d3ab;
  --rule-bg: var(--slot-bg);
  --rule-ink: #5c3a1d;
  --face-bg: none;
  --face-ring: none;
  --face-pad: 0;
  --face-box: border-box;
  --face-radius: 0;

  /* The name bubble, the keypad sheet's scrim, toasts. */
  --bubble-bg: #5c3a1d;
  --bubble-ink: #fff8e6;
  --sheet-scrim: rgba(74, 47, 27, 0.34);
  --toast-bg: #5c3a1d;
  --toast-ink: #fff8e6;
  --toast-error-bg: var(--minus);
  --toast-error-ink: #fff8e6;

  /* Lines: --line between sections, --row-line in --rule-style between the
     rows of a list. */
  --line: #e4d2ae;
  --row-line: var(--line);
  --rule-style: dashed;

  /* How far the counter fades while the tour drives the demo: visibly not the
     visitor's yet, and still readable. */
  --tour-fade: 0.72;
}

/* Layout, the same in every shop. */
:root {
  --radius-tile: 20px;
  --radius-btn: 16px;

  /* The phone's screen and its bezel. The counter box is sized from these so
     the two panels start and finish on the same lines; change the screen here
     and both sides follow. */
  --phone-screen: 700px;
  --phone-bezel: 12px;
}

/* The shutter that comes down over the phone when the shop changes, and its
   sign: wood, cream and dark brown in every shop, so never in a [data-theme]
   set. The sign takes the new shop's name before the shop changes behind it
   (assets/app.js, shutter()), so a sign that changed colours with the shop
   would draw that name in the old shop's colours. drawSign() reads --sign-ink. */
:root {
  --shutter-slat: #7a4a26;
  --shutter-seam: #5c3a1d;
  --sign-bg: #fffdf6;
  --sign-border: #5c3a1d;
  --sign-ink: #4a2f1b;
}

/* The café: a candle-lit café after dark, always, whatever the system's own
   setting. The same colours as the app's café (app/app.css); the parts only the
   demo has are its own. Every variable of the light set has a value here, the
   shutter's none: the shutter and its sign are the same in every shop. */
[data-theme="cafe"] {
  color-scheme: dark;

  --bg: #160f0a;
  --page-art: radial-gradient(120% 600px at 50% 0%, #251910 0%, #160f0a 55%, #0f0a07 100%);
  --phone-art: radial-gradient(120% 600px at 50% 0%, #251910 0%, #160f0a 55%, #0f0a07 100%);
  --tile: linear-gradient(180deg, #221912 0%, #1b140f 100%);
  --paper: #2a2017;
  --chosen: #2e2318;

  --ink: #f4e7d0;
  --ink-soft: #b9a68b;
  --accent: #ecbc7c;
  --plus: #9fd8a8;
  --minus: #f4a58e;

  --digit: #f9e0b4;
  --wordmark: #ecbc7c;
  --chevron: #b9a68b;

  --select: #7fd3dc;

  --get-bg: radial-gradient(120% 150% at 50% 0%, #f6cd88 0%, #e6ac62 55%, #d6964c 100%);
  --get-ink: #2b1708;
  --get-ledge: #84522a;
  --switch-on-bg: #ecbc7c;
  --switch-on-ink: #2b1708;
  --grant-bg: radial-gradient(120% 150% at 50% 0%, #f6cd88 0%, #e6ac62 55%, #d6964c 100%);
  --grant-ledge: #84522a;
  --grant-ink: #2b1708;
  --confirm-bg: radial-gradient(120% 150% at 50% 0%, #f6cd88 0%, #e6ac62 55%, #d6964c 100%);
  --confirm-border: #84522a;
  --confirm-ink: #2b1708;
  --frame: #b98c58;
  --highlight: #ecbc7c;

  --app-icon-bg: #160f0a;
  --app-icon-border: #5e4632;
  --app-icon-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
  --bezel: #2c2118;
  --bezel-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
  --dot-on: #ecbc7c;
  --bar-dot: #e6ac62;

  /* Porcelain under every picture, so the drawings keep their dark outlines;
     the empty place on the shelf is a dark hole. */
  --slot-bg: radial-gradient(circle at 50% 40%, #fdf3e1 0%, #f1dfc0 70%, #e7cfa4 100%);
  --slot-border: #b98c58;
  --slot-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
  --slot-empty-bg: radial-gradient(circle at 50% 35%, #1d150f, #130d0a);
  --slot-empty-border: #5e4632;
  --slot-empty-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.55);
  --limited-bg: #2a2017;
  --rule-bg: #2e2318;
  --rule-ink: #ecbc7c;
  --face-bg: radial-gradient(circle at 50% 42%, #fff4de 0%, #f3dcb3 65%, #e6c793 100%);
  --face-ring: 0 0 0 1px rgba(255, 214, 160, 0.22), 0 2px 4px rgba(0, 0, 0, 0.45);
  --face-pad: 3px;
  --face-box: content-box;
  --face-radius: 10px;

  --bubble-bg: #ecbc7c;
  --bubble-ink: #2b1708;
  --sheet-scrim: rgba(0, 0, 0, 0.6);
  --toast-bg: #2c2118;
  --toast-ink: #f4e7d0;
  --toast-error-bg: #5a271d;
  --toast-error-ink: #fbe3da;

  --line: #3a2b1f;
  --row-line: #2e2219;
  --rule-style: solid;

  /* The counter fades less while the tour plays: its text stays at 4.5:1. */
  --tour-fade: 0.8;
}

/* Forced colours (a high-contrast mode) put the system's own colours on the page,
   a light ground under a light high-contrast theme, but cannot reach into a
   canvas: the café's cream digits and caramel CRUMB would vanish there, so they
   take the system's text colour. The other shops keep 0.3's dark brown (assumption 39). */
@media (forced-colors: active) {
  [data-theme="cafe"] {
    --digit: CanvasText;
    --wordmark: CanvasText;
  }
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  -webkit-tap-highlight-color: transparent;
}

/* For the jump link below the demo. Scoped off the phone's own scroller, which
   should keep snapping to the top instantly when the person on screen changes. */
html { scroll-behavior: smooth; }
.phone-scroll { scroll-behavior: auto; }

body {
  background: var(--bg);
  background-image: var(--page-art);
  color: var(--ink);
  /* Font fallback is per character, so the Latin faces must come before the CJK
     ones. Without a Windows Latin face in the list the whole page landed on
     Microsoft JhengHei, where a curly apostrophe is a full-width glyph — four
     times the width of a straight one — and "You're" rendered as "You' re".
     The CJK faces stay at the end for the Chinese pastry names. */
  font-family: ui-rounded, 'SF Pro Rounded', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial,
    'PingFang TC', 'Heiti TC', 'Microsoft JhengHei', sans-serif;
  line-height: 1.6;
  min-height: 100dvh;
  overflow-x: hidden;
}

button { font: inherit; color: inherit; border: none; background: none; cursor: pointer; }
button:focus-visible, input:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--select);
  outline-offset: 2px;
  border-radius: 12px;
}
canvas { display: block; image-rendering: pixelated; }
.num { font-variant-numeric: tabular-nums; }
.sr-only {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

.wrap { width: 100%; max-width: 1040px; margin: 0 auto; padding: 0 20px; }

/* ------------------------------------------------------------------ store listing head */

.listing { padding: 44px 0 30px; }

.listing-top {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 22px;
  align-items: center;
}

.app-icon {
  width: 118px; height: 118px;
  display: flex; align-items: center; justify-content: center;
  background: var(--app-icon-bg);
  border: 3px solid var(--app-icon-border);
  border-radius: 26px;
  box-shadow: var(--app-icon-shadow);
}

.listing-id canvas { margin-bottom: 10px; }
.tagline { font-size: 16px; color: var(--accent); font-weight: 700; }
.category { font-size: 13px; color: var(--ink-soft); margin-top: 4px; }

.listing-cta { display: flex; flex-direction: column; align-items: center; gap: 7px; }
.get {
  background: var(--get-bg);
  color: var(--get-ink);
  font-weight: 800; font-size: 14px; letter-spacing: 0.02em;
  padding: 10px 26px;
  border-radius: 999px;
  border-bottom: 4px solid var(--get-ledge);
  transition: transform 0.08s, filter 0.14s;
  white-space: nowrap;
}
.get:hover { filter: brightness(1.08); }
.get:active { transform: translateY(3px); border-bottom-width: 1px; }
.get-note { font-size: 11px; color: var(--ink-soft); }

/* Which shop the demo shows. It sits outside the stage, so pressing it never
   takes the tour over. */
.shop-switch {
  display: flex; gap: 2px;
  padding: 2px;
  background: var(--tile);
  border: 2px solid var(--line);
  border-radius: 999px;
}
.shop-switch button {
  padding: 3px 12px;
  border-radius: 999px;
  font-size: 12px; font-weight: 700;
  color: var(--ink-soft);
  white-space: nowrap;
  transition: background 0.14s, color 0.14s;
}
.shop-switch button:hover { color: var(--accent); }
.shop-switch button[aria-pressed='true'] { background: var(--switch-on-bg); color: var(--switch-on-ink); cursor: default; }

.stats {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
  margin-top: 26px; padding-top: 20px;
  border-top: 2px dashed var(--line);
  text-align: center;
}
.stats dt {
  font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-soft);
}
.stats dd { font-size: 26px; font-weight: 800; color: var(--accent); line-height: 1.2; }
.stats dd.fine { font-size: 11px; font-weight: 500; color: var(--ink-soft); }
.stats > div + div { border-left: 2px dashed var(--line); }

/* Why the thing exists, before the demo of it. Without this the page went
   straight from a name to a working app and never said what the situation was. */
.lede {
  max-width: 68ch; margin: 22px auto 0;
  text-align: center;
  font-size: 15px; color: var(--ink-soft);
  text-wrap: pretty;
}

/* Where to go from the demo: the real thing, its source, and a way to say something. */
.listing-links {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 22px;
  margin-top: 14px;
  font-size: 14px; font-weight: 700;
}
.listing-links a { color: var(--accent); text-underline-offset: 3px; }

/* Says plainly, before anyone touches it, that nothing here is real. */
.demo-note {
  max-width: 72ch; margin: 0 auto 22px;
  padding: 10px 16px;
  text-align: center;
  font-size: 13px; color: var(--ink-soft);
  background: var(--paper);
  border: 2px dashed var(--line);
  border-radius: 14px;
  text-wrap: pretty;
}
.demo-note b { color: var(--accent); }

/* ------------------------------------------------------------------ preview column */

/* A definite width, because the stage's right column is `auto` and would
   otherwise be sized by the caption's max-content — one long sentence on a
   single line. A wordy scene stretched this column to 527px, overhanging the
   phone by 127px and squeezing the counter beside it, and the whole layout
   shifted sideways every time the caption changed. Now the caption wraps
   inside the phone's width instead of dictating it. */
.phone-col { display: flex; flex-direction: column; gap: 10px; width: 400px; }

/* Both columns open with this bar, so their boxes start on the same line.
   The counter used to carry its heading inside the box while the phone had a
   label above it, which set the two panels 28px apart at the top. */
.panel-bar {
  display: flex; align-items: center; gap: 10px;
  height: 18px; padding: 0 4px;
}
.panel-label {
  font-size: 11px; font-weight: 700; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-soft);
}
.scene-dots { display: flex; gap: 5px; margin-left: auto; }
.scene-dots i {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--line);
  transition: background 0.2s, transform 0.2s;
}
.scene-dots i.on { background: var(--dot-on); transform: scale(1.3); }

/* The box is always two lines tall so a change of scene never resizes it, and
   the sentence is centred inside rather than pinned to the top — otherwise a
   one-line caption rides high and every scene sits at a different height.
   The centring is done on an inner span, NOT on .caption directly: every child
   element of a flex or grid container becomes its own item, so centring the <p>
   would tear the bold phrase out of the sentence onto a line of its own.
   text-wrap: balance evens out a two-liner; ignored where unsupported. */
.caption {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 3.2em;
  margin-top: 8px;
  padding: 0 6px;
  font-size: 14px; color: var(--accent); text-align: center;
}
.caption > span { text-wrap: balance; }
.caption b { color: var(--ink); }
.caption > span.swap { animation: capIn 0.32s ease; }
@keyframes capIn { from { opacity: 0; transform: translateY(5px) } to { opacity: 1; transform: none } }

/* While the demo drives itself the counter is visibly along for the ride:
   still readable, clearly not yours yet, and one click takes it. */
body.autoplay .counter { opacity: var(--tour-fade); }
body.autoplay .grant,
body.autoplay .person,
body.autoplay .reset { cursor: default; }
.grant.pressed { transform: translateY(3px); border-bottom-width: 1px; filter: brightness(1.12); }
.person.pressed { border-color: var(--frame); background: var(--chosen); }

.tile.highlight { box-shadow: 0 0 0 3px var(--highlight); }
.slots.pulse { animation: trayPulse 1.1s ease; }
@keyframes trayPulse {
  0%, 100% { transform: none }
  30% { transform: scale(1.045) }
  60% { transform: scale(0.995) }
}

/* ------------------------------------------------------------------ stage: counter + phone */

.stage {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 32px;
  align-items: start;
  padding-bottom: 20px;
}

/* Not sticky, deliberately. Once this column was matched to the phone's height
   the two were within 83px of each other, so it stuck for about 60px of scroll
   and then let go — long enough to feel the panel freeze and lurch back into
   motion, far too short to keep anything usefully in view. Alignment and
   stickiness pull against each other here, and alignment is worth more. */
.counter-col { display: flex; flex-direction: column; gap: 10px; }

/* Same height as the phone frame, so both panels close on the same line.
   The controls sit at the top and the guidance is pushed to the bottom by
   .nudge's auto margin — the extra room lands between the two groups rather
   than trailing off the end of the box. */
.counter {
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--radius-tile);
  padding: 22px 22px 20px;
  min-height: calc(var(--phone-screen) + var(--phone-bezel) * 2);
  display: flex;
  flex-direction: column;
}
.counter .sub { font-size: 13px; color: var(--ink-soft); margin-bottom: 18px; }

.field-label {
  font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-soft); margin-bottom: 8px;
}

.people { display: grid; gap: 8px; margin-bottom: 22px; }
.person {
  display: flex; align-items: center; gap: 12px;
  width: 100%; text-align: left;
  background: var(--tile);
  border: 2px solid var(--line);
  border-radius: var(--radius-btn);
  padding: 10px 14px;
  transition: border-color 0.14s, transform 0.1s;
}
.person:hover { border-color: var(--slot-border); }
.person:active { transform: translateY(1px); }
.person[aria-pressed='true'] { border-color: var(--frame); background: var(--chosen); }
.person .who { display: flex; flex-direction: column; line-height: 1.3; }
.person .nm { font-weight: 700; }
.person .meta { font-size: 12px; color: var(--ink-soft); }
.person .tick { margin-left: auto; color: var(--accent); opacity: 0; font-weight: 700; }
.person[aria-pressed='true'] .tick { opacity: 1; }

.grant-row { display: flex; gap: 8px; }
.grant {
  flex: 1;
  background: var(--grant-bg);
  border-bottom: 4px solid var(--grant-ledge);
  border-radius: var(--radius-btn);
  padding: 12px 6px;
  font-weight: 800; color: var(--grant-ink);
  transition: transform 0.08s, filter 0.14s;
}
.grant:hover { filter: brightness(1.04); }
.grant:active { transform: translateY(3px); border-bottom-width: 1px; }
.grant:disabled { opacity: 0.5; cursor: not-allowed; }

/* The manager side keeps its own record of what it handed out. It also fills
   the room the box gained when it was matched to the phone's height. */
.recent { margin-top: 20px; }
.recent-row {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 0; font-size: 13px;
  border-top: 1px var(--rule-style) var(--row-line);
}
.recent-row:first-child { border-top: none; }
.recent-amt { font-weight: 800; color: var(--plus); min-width: 60px; }
.recent-when { margin-left: auto; font-size: 12px; color: var(--ink-soft); }
.recent-empty { padding: 7px 0; font-size: 13px; color: var(--ink-soft); }

.nudge {
  margin-top: auto; padding-top: 14px;
  border-top: 2px dashed var(--line);
  font-size: 13px; color: var(--ink-soft);
}
.nudge b { color: var(--accent); }
.nudge ol { margin: 8px 0 0; padding-left: 18px; }
.nudge li { margin-bottom: 4px; }
.nudge li.done { color: var(--plus); text-decoration: line-through; }

.reset {
  margin-top: 14px; font-size: 12px; color: var(--ink-soft);
  text-decoration: underline; text-underline-offset: 3px;
}

/* ------------------------------------------------------------------ phone */

.phone-frame {
  width: 400px;
  background: var(--bezel);
  border-radius: 34px;
  padding: var(--phone-bezel);
  box-shadow: var(--bezel-shadow);
}
/* A screen, not a growing box. Robin has one row of slots and Alex has four, so
   a phone sized to its contents would resize every time the tour changes person
   — shoving everything below it up and down. Real hardware has a fixed screen
   and scrolls inside it, which is both steadier and more honest. */
.phone {
  position: relative;
  background: var(--bg);
  background-image: var(--phone-art);
  border-radius: 24px;
  height: var(--phone-screen);
  overflow: hidden;
}
.phone-scroll {
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 18px 16px 22px;
  scrollbar-width: none;
}
.phone-scroll::-webkit-scrollbar { display: none; }

.home-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.head-brand { display: flex; align-items: center; gap: 8px; font-weight: 800; letter-spacing: 0.08em; color: var(--accent); }
.pastry-egg { display: block; line-height: 0; }
.pastry-egg:active { transform: scale(0.92); }
.pastry-egg.crack { animation: crack 0.46s ease; }
@keyframes crack {
  0% { transform: rotate(0) }
  25% { transform: rotate(-9deg) }
  55% { transform: rotate(7deg) }
  100% { transform: rotate(0) }
}
.pastry-egg canvas.regrow { animation: regrow 0.4s ease; }
@keyframes regrow { from { transform: scale(0.7); opacity: 0.5 } to { transform: scale(1); opacity: 1 } }

/* The ground a single picture sits on: the header mascot, the person buttons',
   the cards' and the "next item" picture. Nothing at all in the light set; the
   canvas keeps its drawing size, and any padding goes outside it. */
.pastry-egg canvas,
.person canvas,
.card .icons canvas,
.oven-caption canvas {
  background: var(--face-bg);
  box-shadow: var(--face-ring);
  padding: var(--face-pad);
  box-sizing: var(--face-box);
  border-radius: var(--face-radius);
}

/* Entrance animations below deliberately do NOT use animation-fill-mode: both.
   Holding the opening keyframe means an animation that never runs — a frozen
   timeline in a background tab, an old engine, motion switched off at the OS —
   leaves the element invisible or parked off screen. Without the fill, the
   resting styles apply and the worst case is simply no animation. The one
   exception is .slot.pop, which needs the fill to sit still through its
   stagger delay; see there. */
.tile {
  background: var(--tile);
  border-radius: var(--radius-tile);
  border: 2px solid var(--line);
  padding: 16px 16px 18px;
  animation: tileSlide 0.42s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
@keyframes tileSlide { from { opacity: 0; transform: translateY(14px) } to { opacity: 1; transform: none } }

.tile-holder { display: flex; justify-content: space-between; font-size: 13px; color: var(--ink-soft); }
.tile-holder .nm { font-weight: 700; color: var(--ink); }
.tile-label {
  margin-top: 12px; text-align: center;
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-soft);
}
.big-number { display: flex; justify-content: center; padding: 10px 0 14px; }
.big-number.dip { animation: dip 0.5s ease; }
@keyframes dip { 0%,100% { transform: none } 40% { transform: translateY(6px) scale(0.96) } }
.big-number.jump { animation: jump 0.5s ease; }
@keyframes jump { 0%,100% { transform: none } 40% { transform: translateY(-7px) scale(1.05) } }

/* ------------------------------------------------------------------ collection tray */

.slots {
  display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px;
  padding: 12px 0 4px;
}
.slot {
  position: relative;
  aspect-ratio: 1;
  display: flex; align-items: center; justify-content: center;
  background: var(--slot-bg);
  border: 2px solid var(--slot-border);
  border-radius: 10px;
  box-shadow: var(--slot-shadow);
}
/* A place on the shelf with nothing in it yet. */
.slots > .slot:not(.filled) {
  background: var(--slot-empty-bg);
  border-color: var(--slot-empty-border);
  box-shadow: var(--slot-empty-shadow);
}
.slot.filled { cursor: pointer; }
/* The one place the fill earns its keep: pastries drop in staggered, and each
   waits out an animation-delay first. Without the fill they would sit there at
   full size and then snap back to the start of the drop when their turn came. */
.slot.pop { animation: slotPop 0.42s cubic-bezier(0.2, 1.4, 0.4, 1) both; }
@keyframes slotPop {
  0% { transform: translateY(-16px) scale(0.4); opacity: 0 }
  70% { transform: translateY(2px) scale(1.08); opacity: 1 }
  100% { transform: none; opacity: 1 }
}
.pastry-name {
  position: absolute; bottom: calc(100% + 6px); left: 50%;
  transform: translateX(-50%);
  background: var(--bubble-bg); color: var(--bubble-ink);
  font-size: 11px; line-height: 1.35; white-space: nowrap;
  padding: 5px 9px; border-radius: 8px;
  z-index: 4; pointer-events: none;
  animation: nameIn 0.16s ease;
}
.pastry-name .en { display: block; opacity: 0.72; font-size: 10px; }
@keyframes nameIn { from { opacity: 0; transform: translate(-50%, 4px) } to { opacity: 1; transform: translate(-50%, 0) } }

.oven-caption {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  padding-top: 10px; font-size: 13px; color: var(--ink-soft);
}
.oven-caption b { color: var(--accent); }

/* ------------------------------------------------------------------ actions + log */

.actions { margin-top: 14px; }
.act {
  width: 100%;
  background: var(--tile);
  border: 2px solid var(--line);
  border-bottom-width: 4px;
  border-radius: var(--radius-btn);
  padding: 14px; font-weight: 800; color: var(--minus);
  transition: transform 0.08s;
}
.act:active:not(:disabled) { transform: translateY(2px); border-bottom-width: 2px; }
.act:disabled { opacity: 0.55; cursor: not-allowed; }
.actions-hint { margin-top: 8px; text-align: center; font-size: 12px; color: var(--ink-soft); }

.quest {
  margin-top: 16px;
  background: var(--tile);
  border: 2px solid var(--line);
  border-radius: var(--radius-tile);
  overflow: hidden;
}
.quest-head {
  display: flex; align-items: center; gap: 8px;
  width: 100%; padding: 12px 14px;
  font-size: 13px; font-weight: 700;
}
.quest-head:disabled { opacity: 0.6; cursor: default; }
.quest-head .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--bar-dot); }
.quest-head .toggle { margin-left: auto; font-weight: 600; color: var(--ink-soft); font-size: 12px; }
.quest-head .chev { display: inline-block; margin-left: 3px; transition: transform 0.2s; }
.quest.open .chev { transform: rotate(180deg); }
.quest-scroller { max-height: 148px; overflow-y: auto; padding: 0 14px 12px; }
.quest.open .quest-scroller { max-height: 300px; }
.quest-empty, .quest-month {
  padding: 8px 0; font-size: 12px; color: var(--ink-soft);
}
.quest-month { font-weight: 700; color: var(--accent); border-bottom: 2px var(--rule-style) var(--row-line); }

.rec {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 0; font-size: 13px;
  border-top: 1px var(--rule-style) var(--row-line);
}
.rec:first-child { border-top: none; }
.rec-diamond { width: 7px; height: 7px; transform: rotate(45deg); flex: none; }
.rec-diamond.plus { background: var(--plus); }
.rec-diamond.minus { background: var(--minus); }
.rec-amt { font-weight: 800; min-width: 72px; }
.rec-amt.plus { color: var(--plus); }
.rec-amt.minus { color: var(--minus); }
.rec-label { color: var(--ink-soft); }
.rec-time { margin-left: auto; font-size: 12px; color: var(--ink-soft); }

.daily { margin-top: 14px; text-align: center; font-size: 13px; color: var(--ink-soft); }

/* ------------------------------------------------------------------ oven intro, sheet, toast */

.oven {
  position: absolute; inset: 0; z-index: 6;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  background: var(--bg);
  transition: opacity 0.4s;
}
.oven.leaving { opacity: 0; pointer-events: none; }
.oven canvas { animation: ovenPop 0.5s cubic-bezier(0.2, 1.5, 0.4, 1); }
@keyframes ovenPop { from { transform: translateY(18px) scale(0.5); opacity: 0 } to { transform: none; opacity: 1 } }
.oven-text { font-size: 17px; font-weight: 800; color: var(--accent); }
.oven-sub { font-size: 12px; color: var(--ink-soft); }

/* The shutter that comes down over the phone when the shop changes (assets/app.js,
   shutter()). A layer inside the phone like .oven, above everything else in it, the
   toast included. The slats are hard colour stops, 10px of wood and a 2px seam, so
   nothing blurs between them; every move is set from script in whole steps, so
   nothing here transitions. Its colours are the :root group of their own at the
   top of this file, the same in every shop. */
.shutter {
  position: absolute; inset: 0; z-index: 10;
  background: repeating-linear-gradient(var(--shutter-slat) 0 10px, var(--shutter-seam) 10px 12px);
  transform: translateY(-100%);
}
/* Hangs a little above the middle. It flips by squashing sideways around its centre
   line and wobbles from where it hangs, so one origin serves both. */
.shutter-sign {
  position: absolute; top: 38%; left: 0; right: 0;
  width: max-content; margin: 0 auto;
  padding: 10px 12px;
  background: var(--sign-bg);
  border: 3px solid var(--sign-border);
  border-bottom-width: 6px;
  border-radius: 10px;
  transform-origin: 50% 0;
}
/* From the sign's flip until the shutter has gone, the new stock waits out of sight,
   the way a slot waits out its .pop delay, so it first shows when it pops in after
   the shutter. Whatever redraws the shelf meanwhile stays hidden too. */
.slots.restocking .slot.filled { visibility: hidden; }

.sheet-scrim {
  position: absolute; inset: 0; z-index: 8;
  background: var(--sheet-scrim);
  display: flex; align-items: flex-end;
}
.sheet {
  width: 100%;
  background: var(--tile);
  border-radius: var(--radius-tile) var(--radius-tile) 0 0;
  padding: 18px 16px 20px;
  animation: sheetUp 0.26s cubic-bezier(0.2, 0.9, 0.3, 1.1);
}
@keyframes sheetUp { from { transform: translateY(100%) } to { transform: none } }
.sheet h3 { font-size: 15px; margin-bottom: 2px; }
.sheet .avail { font-size: 12px; color: var(--ink-soft); margin-bottom: 14px; }
.amount {
  display: flex; align-items: center; justify-content: center; gap: 4px;
  font-size: 34px; font-weight: 800; padding: 6px 0 14px;
}
.amount .cur { font-size: 20px; color: var(--ink-soft); }
.amount.over { color: var(--minus); }
.keys { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.key {
  background: var(--paper); border: 2px solid var(--line);
  border-bottom-width: 4px; border-radius: 12px;
  padding: 13px 0; font-size: 18px; font-weight: 700;
}
.key:active { transform: translateY(2px); border-bottom-width: 2px; }
.sheet-actions { display: flex; gap: 8px; margin-top: 12px; }
.sheet-actions button {
  flex: 1; border-radius: var(--radius-btn); padding: 13px;
  font-weight: 800; border: 2px solid var(--line); border-bottom-width: 4px;
}
.btn-cancel { background: var(--paper); color: var(--ink-soft); }
.btn-confirm { background: var(--confirm-bg); border-color: var(--confirm-border); color: var(--confirm-ink); }
.btn-confirm:disabled { opacity: 0.45; cursor: not-allowed; }
.sheet-err { margin-top: 10px; font-size: 12px; color: var(--minus); text-align: center; min-height: 1.2em; }

.toast {
  position: absolute; left: 50%; bottom: 20px; z-index: 9;
  transform: translateX(-50%);
  background: var(--toast-bg); color: var(--toast-ink);
  font-size: 13px; padding: 10px 16px; border-radius: 12px;
  animation: nameIn 0.2s ease;
  max-width: calc(100% - 32px); text-align: center;
}
.toast.error { background: var(--toast-error-bg); color: var(--toast-error-ink); }

.particle { position: fixed; z-index: 40; pointer-events: none; border-radius: 1px; }

/* ------------------------------------------------------------------ scroll reveals

   The hidden state is applied only by a class JS adds, never in the resting
   stylesheet. No script, no IntersectionObserver, an observer that never
   fires — in every one of those cases the section is simply visible, which is
   the failure everyone can live with. */

.reveal-armed { opacity: 0; }
.card.rise { animation: tileSlide 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.2) both; }
.section-head.rise { animation: tileSlide 0.45s cubic-bezier(0.2, 0.9, 0.3, 1.2) both; }

/* ------------------------------------------------------------------ read on

   The demo fills the first screen and the sections below it start with small
   centred type, so it was easy to reach the bottom of the phone and assume the
   page ended there. This names what is next rather than just saying "scroll" —
   the point is not that there is more, it is what the more is. */

.more {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 4px 0 30px;
  text-decoration: none;
  font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-soft);
  transition: color 0.16s;
}
.more:hover { color: var(--accent); }
.more canvas { animation: nudgeDown 2s ease-in-out infinite; }
@keyframes nudgeDown {
  0%, 100% { transform: translateY(0) }
  50% { transform: translateY(4px) }
}

/* ------------------------------------------------------------------ how it works */

.how { padding: 44px 0 8px; }
.section-head { text-align: center; margin-bottom: 22px; }
.section-head h2 {
  font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent);
}
.section-head p { font-size: 14px; color: var(--ink-soft); margin-top: 6px; }

.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
.card {
  background: var(--tile);
  border: 2px solid var(--line);
  border-radius: var(--radius-tile);
  padding: 18px;
}
.card h3 { font-size: 15px; margin: 10px 0 6px; }
.card p { font-size: 14px; color: var(--ink-soft); }
.card .icons { display: flex; gap: 5px; align-items: center; min-height: 40px; }
.card .rule {
  display: inline-block; margin-top: 10px;
  font-size: 12px; font-weight: 700; color: var(--rule-ink);
  background: var(--rule-bg); border-radius: 8px; padding: 3px 9px;
}

/* ------------------------------------------------------------------ cabinet */

.cabinet { padding: 44px 0 20px; }
.case {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(62px, 1fr));
  gap: 8px;
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--radius-tile);
  padding: 16px;
}
.case .slot { border-radius: 8px; }
.case .slot.limited { background: var(--limited-bg); border-style: dashed; }
.legend {
  display: flex; gap: 16px; justify-content: center; flex-wrap: wrap;
  margin-top: 12px; font-size: 12px; color: var(--ink-soft);
}
.legend > span { display: flex; align-items: center; gap: 6px; }
.legend > span[hidden] { display: none; }
.legend i { width: 11px; height: 11px; border: 2px solid var(--slot-border); border-radius: 4px; background: var(--slot-bg); }
.legend i.limited { background: var(--limited-bg); border-style: dashed; }

/* ------------------------------------------------------------------ footer */

.page-foot {
  padding: 40px 0 56px;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  border-top: 2px dashed var(--line);
  margin-top: 36px;
}
.page-foot img { display: block; width: 168px; height: auto; border-radius: 10px; }
.page-foot .credit { font-size: 13px; color: var(--ink-soft); }
.page-foot a { color: var(--accent); }

/* ------------------------------------------------------------------ narrow */

@media (max-width: 900px) {
  .stage { grid-template-columns: minmax(0, 1fr); }
  .counter-col { order: 2; width: 100%; max-width: 400px; margin: 0 auto; }
  .phone-col { order: 1; width: 100%; max-width: 400px; margin: 0 auto; }
  .phone-frame { width: 100%; }
  /* Stacked, there is nothing to line up with, so let the box be its own size
     again rather than padding it out to a phone's height. */
  .counter { min-height: 0; }
  .nudge { margin-top: 16px; }
  .listing { padding-top: 30px; }
  .listing-top { grid-template-columns: auto minmax(0, 1fr); row-gap: 18px; }
  .listing-cta { grid-column: 1 / -1; flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 12px; }
  .app-icon { width: 92px; height: 92px; border-radius: 20px; }
}

@media (max-width: 560px) {
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 10px; }
  .stats > div:nth-child(odd) { border-left: none; }
  .stats > div:nth-child(3) { border-left: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* iteration-count matters as much as duration: squashing a looping animation
     to 0.01ms without it leaves it cycling hundreds of times a second, which is
     worse than the motion it was meant to remove. */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  html { scroll-behavior: auto; }
}
