/* Tapp The Button - the widget is 325x512 on the standalone page and 325x290 in
   the hero embed, and both are the same markup. Everything below is clamped in
   vh so the smaller box keeps the composition the bigger one was drawn for.

   The catalog themes set --tapp-*, --sky-* and --grass on :root, and the campaign
   sets --campaign. Nothing here hard-codes a theme colour. */

@font-face {
  font-family: 'Silkscreen';
  src: url('/assets/fonts/silkscreen-700-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-display: swap;
}
@font-face {
  font-family: 'Titan One';
  src: url('/assets/fonts/titan-one-latin.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

:root {
  --widget-w: 325px;
  --widget-h: 512px;
  /* the button, its rim and its glow - the catalog overwrites these */
  --tapp-body: #f5f0e8;
  --tapp-rim: #9b90b6;
  --tapp-glow: #fff8e7;
  --tapp-accent: #ff6b9d;
  --sky-top: #87ceeb;
  --sky-bottom: #c8e8f5;
  --grass: #6bcb77;
  --tapp-filter: none;
  /* the ad slot's colour, set from the campaign */
  --campaign: #c8a2ff;
  --campaign-ink: #2b1b4d;
  --premium: #ffcf3f;
  --ink: #2c2416;
  --ink-soft: rgba(44, 36, 22, .62);
  --panel: #fffaf0;
  --shadow: rgba(44, 36, 22, .28);
  --shout: 'Titan One', 'Segoe UI', system-ui, sans-serif;
  --pixel: 'Silkscreen', ui-monospace, 'Cascadia Mono', monospace;
  --ui: 'Segoe UI', Nunito, system-ui, -apple-system, sans-serif;
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  font-size: 14px;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* The icon sprite: marks are drawn once and referenced with <use>. Size comes
   from font-size, so a 19px chip mark and a 40px gift mark are the same path. */
/* `hidden` means not rendered. Without this, any element that carries a display
   of its own beats the attribute: the premium chip stayed on screen for signed
   out visitors, and the dungeon's death card sat on top of a live canvas. */
[hidden] { display: none !important; }

.icons { position: absolute; width: 0; height: 0; overflow: hidden; }
.ico { display: block; width: 1em; height: 1em; }

html, body {
  height: 100%;
  overflow: hidden;
  font-family: var(--ui);
  color: var(--ink);
  background: #3f7f93;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}
body { display: flex; align-items: center; justify-content: center; }

#app {
  position: relative;
  width: var(--widget-w);
  height: var(--widget-h);
  max-width: var(--widget-w);
  max-height: var(--widget-h);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 3px solid var(--ink);
  border-radius: 18px;
  /* The fallback sky meets the field over four per cent instead of on one hard
     line: a stop at exactly 52 per cent drew a second horizon. */
  background: linear-gradient(180deg, var(--sky-top) 0%, var(--sky-bottom) 44%, #cfeaf6 49%, var(--grass) 54%, #43a05a 100%);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .22);
}

/* ---------------------------------------------------------------- the scene */

/* The rendered world, when there is one. scene.js adds .gl to <html> after the
   first successful render, and only then does this cover the CSS sky - so a
   browser without WebGL keeps exactly the scene it had before. */
#scene-gl { position: absolute; inset: 0; z-index: 0; display: none; pointer-events: none; }
#scene-gl canvas { display: block; width: 100%; height: 100%; }
html.gl #scene-gl { display: block; }
html.gl .scene, html.gl .ground { display: none; }

.scene { position: absolute; inset: 0 0 46% 0; z-index: 0; pointer-events: none; overflow: hidden; }

/* The sun sits behind the coins chip, with two rings for a soft glow. */
.sun {
  position: absolute;
  top: 6%;
  right: 8%;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: radial-gradient(circle at 42% 38%, #fffbe8, #ffe08a 58%, rgba(255, 216, 120, .25) 78%, transparent 80%);
  box-shadow: 0 0 26px rgba(255, 226, 138, .75), 0 0 60px rgba(255, 226, 138, .35);
  animation: sun-breathe 7s ease-in-out infinite alternate;
}
@keyframes sun-breathe { from { transform: scale(.97); } to { transform: scale(1.05); } }

/* Three bands of cloud, each drifting at its own speed: parallax is what makes a
   flat sky read as a place rather than as a backdrop.

   One transform animation per cloud and nothing else. The first version animated
   margin-left and margin-top with a drop-shadow filter, which relaid out the sky
   and re-rastered seven filtered layers every frame; in a headless run that alone
   was enough to make the page stop answering. Size is set per band instead of with
   a scale transform, so the keyframes can own the transform outright. */
.clouds { position: absolute; inset: 0; }
.clouds i {
  position: absolute;
  display: block;
  border-radius: 999px;
  background: inherit;
  box-shadow: 0 3px 0 rgba(120, 170, 200, .28);
  will-change: transform;
  animation: cloud-drift var(--drift, 52s) linear infinite alternate;
}
.clouds i::before,
.clouds i::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  background: inherit;
}
.clouds i::before { width: 58%; height: 148%; left: 12%; top: -78%; }
.clouds i::after { width: 38%; height: 118%; left: 46%; top: -44%; }
.clouds--far i { background: rgba(255, 255, 255, .7); --drift: 74s; width: 44px; height: 14px; }
.clouds--mid i { background: rgba(255, 255, 255, .88); --drift: 50s; width: 56px; height: 18px; }
.clouds--near i { background: #fff; --drift: 32s; width: 68px; height: 22px; }
.clouds--far i:nth-child(1) { top: 27%; left: 4%; }
.clouds--far i:nth-child(2) { top: 54%; left: 42%; }
.clouds--far i:nth-child(3) { top: 13%; left: 70%; }
.clouds--mid i:nth-child(1) { top: 40%; left: 17%; }
.clouds--mid i:nth-child(2) { top: 66%; left: 63%; }
.clouds--near i:nth-child(1) { top: 76%; left: 33%; animation-delay: -6s; }
.clouds--near i:nth-child(2) { top: 21%; left: 53%; animation-delay: -14s; }
@keyframes cloud-drift { from { transform: translate3d(-15px, -2px, 0); } to { transform: translate3d(22px, 5px, 0); } }

/* The horizon used to carry a second, rounder hill under the far ridge. One dome
   behind a field of grass reads as a green balloon, not as distance - the far
   ridge alone is the better horizon, so the near one is gone from here and from
   the WebGL scene alike. */

/* ------------------------------------------------------------------ ground */

.ground { position: absolute; left: 0; right: 0; bottom: 0; height: 48%; z-index: 0; pointer-events: none; overflow: hidden; }
.grass {
  position: absolute;
  inset: 0 0 34% 0;
  background:
    /* blades, at two angles and two tones: what makes a flat rectangle read as a
       field instead of as a coloured box. All straight from --grass, so a theme
       that changes the green still gets a field rather than a smear. */
    repeating-linear-gradient(76deg, rgba(255, 255, 255, .17) 0 1px, transparent 1px 4px),
    repeating-linear-gradient(104deg, rgba(20, 60, 30, .20) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(90deg, rgba(255, 255, 255, .05) 0 3px, transparent 3px 9px),
    /* sunlight: two soft pools, where the light in the scene comes from */
    radial-gradient(ellipse 44% 26% at 24% 8%, rgba(255, 255, 255, .3), transparent 72%),
    radial-gradient(ellipse 50% 30% at 74% 32%, rgba(255, 243, 180, .24), transparent 74%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--grass) 62%, #fff) 0%,
      color-mix(in srgb, var(--grass) 90%, #fff) 26%,
      var(--grass) 52%,
      color-mix(in srgb, var(--grass) 68%, #12522c) 82%,
      color-mix(in srgb, var(--grass) 52%, #0c3a1e) 100%);
  transform-origin: 50% 100%;
  will-change: transform;
  animation: wind 7s ease-in-out infinite alternate;
}
/* The lit edge where the field starts, so the horizon is grass catching sun and
   not one hard line between two boxes. */
.grass::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 7px;
  background: linear-gradient(180deg, rgba(228, 255, 208, .85), rgba(228, 255, 208, 0));
}
/* Three flowers and two clover patches, placed by hand instead of tiled: at 325px
   a regular pattern is the thing that makes ground look printed. */
.ground::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 14% 26%, #fff3c4 0 2.2px, transparent 2.6px),
    radial-gradient(circle at 17% 21%, #ffe9a8 0 1.6px, transparent 2px),
    radial-gradient(circle at 61% 47%, #ffd9e8 0 2px, transparent 2.4px),
    radial-gradient(circle at 86% 19%, #fff3c4 0 1.8px, transparent 2.2px),
    radial-gradient(ellipse 12% 5% at 38% 74%, rgba(255, 255, 255, .16), transparent 70%),
    radial-gradient(ellipse 16% 6% at 70% 86%, rgba(10, 50, 26, .22), transparent 72%);
  pointer-events: none;
}
/* Transform only: skewing and lifting the band reads as wind without touching
   layout, and the dither gradients stay crisp instead of being re-rastered. */
@keyframes wind {
  from { transform: translate3d(0, 0, 0) skewX(-.5deg); }
  to { transform: translate3d(0, -2px, 0) skewX(.6deg); }
}
/* Tufts: a row of little blades along the top edge of the grass, drawn with a
   stripe gradient (a conic gradient here was cheaper to write and much more
   expensive to paint on every frame). */
.tufts {
  position: absolute;
  left: -6px;
  right: -6px;
  top: -11px;
  height: 19px;
  background:
    repeating-linear-gradient(108deg, #7ada86 0 3px, transparent 3px 7px),
    repeating-linear-gradient(72deg, #a6ee9f 0 2px, transparent 2px 8px),
    repeating-linear-gradient(126deg, color-mix(in srgb, var(--grass) 70%, #123f22) 0 2px, transparent 2px 9px);
  opacity: .95;
  will-change: transform;
  animation: wind 5.5s ease-in-out infinite alternate;
}
.soil {
  position: absolute;
  inset: 66% 0 0 0;
  background:
    repeating-linear-gradient(90deg, rgba(255, 255, 255, .05) 0 3px, transparent 3px 7px),
    repeating-linear-gradient(0deg, rgba(0, 0, 0, .06) 0 2px, transparent 2px 5px),
    linear-gradient(180deg, #3f7d47 0%, #7a5c37 22%, #63492c 64%, #513c24 100%);
}
.soil::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 18% 40%, rgba(0, 0, 0, .12) 0 3px, transparent 4px),
    radial-gradient(circle at 62% 66%, rgba(0, 0, 0, .1) 0 2px, transparent 3px),
    radial-gradient(circle at 84% 30%, rgba(255, 255, 255, .06) 0 3px, transparent 4px);
}

#fx-layer { position: absolute; inset: 0; z-index: 40; pointer-events: none; }

/* ------------------------------------------------------------------ topbar */

.topbar {
  position: relative;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  padding: 7px 8px 3px;
  flex-shrink: 0;
}

.icon-btn {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  box-shadow: 2px 2px 0 var(--shadow), inset 0 -2px 0 rgba(0, 0, 0, .08);
  transition: transform .1s ease, box-shadow .1s ease;
}
.icon-btn svg { width: 17px; height: 17px; }
.icon-btn:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--shadow); }

/* The logo is the owner's mascotte PNG: the character, his hands and the
   wordmark are one drawing. It is shown as it was drawn - only bigger, and on a
   plate, because at 30px against a sky it read as a sticker that fell on the
   scene instead of as the name of the game. */
.brand {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  margin: 0 auto;
  padding: 1px 8px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: color-mix(in srgb, var(--panel) 92%, transparent);
  box-shadow: 2px 2px 0 var(--shadow);
  pointer-events: none;
}
.brand-logo {
  height: clamp(34px, 12vh, 60px);
  width: auto;
  max-width: 100%;
  object-fit: contain;
  filter: drop-shadow(1px 2px 0 var(--shadow)) var(--tapp-filter);
  transition: filter .4s ease;
}

.wallet { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px 3px 4px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--panel);
  box-shadow: 2px 2px 0 var(--shadow), inset 0 -2px 0 rgba(0, 0, 0, .06);
  font-family: var(--pixel);
  font-size: .72rem;
  color: var(--ink);
}
.chip-ico {
  width: 21px;
  height: 21px;
  flex-shrink: 0;
  display: block;
  filter: drop-shadow(0 1px 0 rgba(61, 50, 32, .45));
}
.chip--premium { background: #fff7dd; }
.chip--premium .chip-ico { width: 20px; height: 20px; }
.chip--premium.is-new { animation: pop .5s ease; }
button.chip {
  font: inherit;
  color: inherit;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
button.chip:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--shadow); }
button.chip:focus-visible { outline: 3px solid #6432a0; outline-offset: 2px; }
/* Signed out, the coin is still there but it is not pretending to be a number
   you own: dimmed, with a plus that says it is something you collect. */
.chip--premium.is-locked { background: #f2ece0; opacity: .92; }
.chip--premium.is-locked .chip-ico { filter: grayscale(.55) opacity(.75); }
.chip--premium.is-locked b { color: var(--ink-soft); }
.chip--premium { position: relative; }
.chip--premium.is-locked::after {
  content: '+';
  position: absolute;
  top: -3px;
  right: -2px;
  display: grid;
  place-items: center;
  width: 12px;
  height: 12px;
  border: 1.5px solid var(--ink);
  border-radius: 50%;
  background: #ffd166;
  font-family: var(--pixel);
  font-size: .46rem;
  line-height: 1;
  color: #3d3220;
}

/* ------------------------------------------------------------ kit decor scale
   The props are drawn at 1em, which at a 325px widget made a pumpkin a 17px
   blob on dark grass. A hairline shadow and a little more size is the whole
   difference between a pumpkin and a smudge. */
#decor-layer .decor { filter: drop-shadow(0 1px 1px rgba(16, 10, 0, .5)); }
#decor-layer .decor--pumpkin { font-size: calc(var(--s, 1.4rem) * 1.45); bottom: 14%; }
#decor-layer .decor--snowman { bottom: 14%; }
#decor-layer .decor--tree { bottom: 15%; }
#decor-layer .decor--flag { bottom: 15%; }
#decor-layer .decor--egg { bottom: 14%; }
#decor-layer .decor--blossom { bottom: 17%; }

/* A kit can put a whole phrase on the button. "TRICK OR TAP" at the short-word
   size ran edge to edge across the disc, so the label takes a step down by
   length instead of pushing against the rim. */
.tapp-label[data-size="mid"] { font-size: clamp(.64rem, 2.05vh, .86rem); letter-spacing: .04em; }
.tapp-label[data-size="long"] { font-size: clamp(.56rem, 1.8vh, .74rem); letter-spacing: .02em; }

/* The CSS fallback scene draws its own sun. When the worn backdrop puts a moon
   or a planet up instead, that sun steps aside: two bodies in one sky is the
   thing that made the night scenes look wrong. */
html[data-celestial="moon"] .scene .sun,
html[data-celestial="planet"] .scene .sun,
html[data-celestial="none"] .scene .sun { display: none; }
@keyframes pop { 0% { transform: scale(1); } 40% { transform: scale(1.16); } 100% { transform: scale(1); } }

/* ---------------------------------------------------------------- the ad slot */

.adslot {
  position: relative;
  z-index: 9;
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 3px 8px 0;
  padding: 5px 7px;
  border: 2px solid var(--ink);
  border-radius: 12px;
  background: linear-gradient(180deg, color-mix(in srgb, var(--campaign) 78%, #fff) 0%, var(--campaign) 100%);
  color: var(--campaign-ink);
  font-family: var(--ui);
  text-align: left;
  cursor: pointer;
  flex-shrink: 0;
  box-shadow: 2px 2px 0 var(--shadow);
  transition: transform .12s ease, box-shadow .12s ease;
}
.adslot:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--shadow); }
/* The campaign's own emoji, on a badge of its own. It reads as a designed
   sticker instead of a glyph floating loose next to the words. */
.adslot-art {
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border: 2px solid var(--campaign-ink);
  border-radius: 50%;
  background: rgba(255, 255, 255, .6);
  font-size: 12px;
  line-height: 1;
}
.adslot-copy { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.adslot-copy b { font-size: .74rem; letter-spacing: .01em; }
.adslot-copy em {
  font-style: normal;
  font-size: .62rem;
  opacity: .85;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.adslot-cta {
  flex-shrink: 0;
  padding: 3px 7px;
  border: 2px solid var(--campaign-ink);
  border-radius: 999px;
  background: rgba(255, 255, 255, .38);
  font-family: var(--pixel);
  font-size: .55rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  white-space: nowrap;
}

/* ------------------------------------------------------------------- stage */

.stage {
  position: relative;
  z-index: 2;
  flex: 1;
  display: grid;
  /* Three columns: the chest, the button, the shop. The sides are equal
     fractions rather than 62px and 70px, because unequal sides pushed the
     button 4px off the widget's centre line - small enough to read as a
     mistake, and impossible to unsee once you have. Equal sides put the button
     exactly in the middle and leave the props room to grow. */
  grid-template-columns: 1fr auto 1fr;
  /* The props are centred in the field, not stacked on its floor. With
     align-items: end the button sat on the field's bottom edge, where the daily
     row covered its lower third - and the space the scene had above the props
     was the empty band the eye kept landing on. Centring splits that space above
     and below the props, so the button reads as the middle of the field and the
     notification strip has somewhere to live that is not on top of it. */
  align-items: center;
  gap: 2px;
  padding: 4px 7px 10px;
  min-height: 0;
}

.side-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 2px;
  border: none;
  background: transparent;
  cursor: pointer;
  transition: transform .1s ease;
}
.side-btn:active { transform: scale(.95) translateY(1px); }
/* Sized by width, with the height left to the artwork's own ratio. The old rule
   set a height and then capped the width to the column, and the chest - drawn 32
   by 28, wider than it is tall - got squashed into a square to fit a 62px slot.
   Width first keeps both props their real shape, and both of them larger. */
.side-btn svg { display: block; height: auto; max-width: 100%; filter: drop-shadow(2px 3px 0 var(--shadow)); }
#btn-chest svg { width: clamp(60px, 30vh, 90px); }
#btn-shop svg { width: clamp(42px, 20vh, 58px); }
.side-btn .label {
  padding: 2px 8px;
  border: 2px solid var(--wood-dark, #8b6914);
  border-radius: 7px;
  background: var(--wood, #c4a484);
  color: #fff;
  font-family: var(--pixel);
  font-size: .6rem;
  letter-spacing: .06em;
  text-shadow: 1px 1px 0 #0005;
}

.tapp-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
  padding-bottom: 2px;
  min-width: 0;
}
/* The button sits in the field, so it needs a shadow in the field. Without it
   the disc floats over the grass instead of standing on it. */
.tapp-wrap::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 13px;
  width: clamp(78px, 20vh, 116px);
  height: 15px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(ellipse at 50% 38%, rgba(8, 40, 18, .46), rgba(8, 40, 18, .17) 52%, transparent 74%);
  pointer-events: none;
}
#tapp-btn { z-index: 1; }

/* The button: a glossy disc, lit from the top left, in the theme's colours with
   the campaign's art sitting on it. */
#tapp-btn {
  position: relative;
  width: clamp(92px, 24vh, 134px);
  height: clamp(92px, 24vh, 134px);
  /* A column, not a grid: the ad art and the word TAPP used to land side by side
     on the same row, which is what made the art look dropped in at random. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border: none;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.tapp-glow {
  position: absolute;
  inset: -12%;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--tapp-accent) 42%, transparent) 0%, transparent 68%);
  /* Held back on purpose: with a drawn face on the button the halo stopped
     reading as light and started reading as a smudge on the sky. */
  opacity: .26;
  will-change: transform;
  animation: glow-pulse 4.5s ease-in-out infinite alternate;
}
@keyframes glow-pulse { from { transform: scale(.94); } to { transform: scale(1.06); } }
.tapp-face {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* The rim takes a little of the advertiser's colour: the button belongs to the
     campaign that is running, and that has to be visible before a click. */
  /* The drawn button has a pale ring and a coloured outline. A kit that painted
     the whole ring its own dark colour turned the button into a black tyre, so
     the kit now owns the outline, the ledge and the glow - not the ring. */
  border: 7px solid color-mix(in srgb, var(--tapp-rim) 22%, #f4f0f7);
  background: var(--tapp-body);
  /* The drawn button has a soft ring and a hard outline around it, not a black
     tyre. The rim keeps the kit's colour, the hairline keeps it a drawing. */
  box-shadow:
    0 0 0 2.5px color-mix(in srgb, var(--tapp-rim) 62%, var(--ink)),
    0 6px 0 color-mix(in srgb, var(--tapp-rim) 46%, #000),
    0 12px 18px rgba(0, 0, 0, .22),
    inset 0 -8px 14px rgba(120, 70, 20, .10),
    inset 0 6px 10px rgba(255, 255, 255, .45);
  transition: transform .08s ease, box-shadow .08s ease, filter .4s ease;
  filter: var(--tapp-filter);
}
/* The campaign art sits on the face as a sticker: no white disc behind it (that
   read as a pill dropped on the button), a soft pool of the campaign's own
   colour instead, and a shadow to lift it off the plastic. */
.tapp-art {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  padding: 1px 7px;
  border-radius: 999px;
  background: radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--campaign) 88%, #fff) 0%, color-mix(in srgb, var(--campaign) 52%, transparent) 46%, transparent 70%);
  font-size: clamp(19px, 5.2vh, 26px);
  line-height: 1.05;
  filter: drop-shadow(0 2px 1px rgba(60, 40, 10, .32));
  pointer-events: none;
}
/* The owner's face sits on the button: a crop of his drawing, no new character.
   The disc behind it keeps doing the work it did - skin colour, gloss, press -
   so every kit still repaints the button it was sold as. */
.tapp-portrait {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
  z-index: 1;
  /* The face is a full circle cut out of the drawing and sits flush inside the
     rim, so the button is the drawing: white face, kit-coloured ring, and not a
     white sticker dropped in the middle of a plastic disc. */
  transition: transform .08s ease;
  pointer-events: none;
}
/* An empty art slot is no art: a campaign that has no picture must not leave the
   gift emoji sitting on the face. */
.tapp-art:empty { display: none; }
.tapp-label {
  position: relative;
  z-index: 2;
  margin-top: 3px;
  font-family: var(--shout);
  font-size: clamp(.72rem, 2.1vh, .9rem);
  letter-spacing: .06em;
  max-width: 100%;
  text-align: center;
  line-height: 1.02;
  /* The name sits on the grass now, so it is drawn the way the drawing draws
     it: white letters with a dark outline, legible on anything. */
  color: #fff;
  text-shadow: 2px 2px 0 var(--ink), -1px -1px 0 var(--ink), 1px -1px 0 var(--ink), -1px 1px 0 var(--ink);
  pointer-events: none;
}
/* The portrait rides inside the disc, so the disc's own press moves it: no
   second transform, or the press would happen twice as deep as the rim. */
#tapp-btn:active .tapp-portrait,
#tapp-btn.pressed .tapp-portrait { transform: none; }
#tapp-btn:active .tapp-face,
#tapp-btn.pressed .tapp-face {
  transform: translateY(4px) scale(.94);
  box-shadow:
    0 2px 0 color-mix(in srgb, var(--tapp-rim) 80%, #000),
    0 5px 10px rgba(0, 0, 0, .2),
    inset 0 -4px 10px rgba(0, 0, 0, .14),
    inset 0 6px 8px rgba(255, 255, 255, .34);
}
.tap-float {
  position: absolute;
  z-index: 4;
  top: 4%;
  left: 50%;
  transform: translate(-50%, 6px);
  font-family: var(--pixel);
  font-size: .78rem;
  color: #fff;
  text-shadow: 2px 2px 0 var(--ink);
  opacity: 0;
  pointer-events: none;
}
.tap-float.go { animation: float-up 700ms ease-out; }
@keyframes float-up {
  0% { opacity: 0; transform: translate(-50%, 6px) scale(.9); }
  25% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -26px) scale(1.06); }
}
.tap-hint {
  font-family: var(--pixel);
  font-size: .55rem;
  color: #fff;
  text-shadow: 1px 1px 0 rgba(44, 36, 22, .55);
  letter-spacing: .04em;
}

/* -------------------------------------------------------------- daily row */

.dailyrow {
  position: relative;
  z-index: 9;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 8px calc(7px + var(--safe-bottom));
  border-top: 2px solid var(--ink);
  background: linear-gradient(180deg, #5b4529 0%, #4b3820 100%);
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .08);
  flex-shrink: 0;
}
.daily-btn {
  display: flex;
  align-items: center;
  gap: 7px;
  flex: 1;
  min-width: 0;
  padding: 5px 8px;
  border: 2px solid var(--ink);
  border-radius: 12px;
  background: linear-gradient(180deg, #fff6d9 0%, #ffe08a 100%);
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, .3);
  transition: transform .1s ease, box-shadow .1s ease;
}
.daily-btn:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 rgba(0, 0, 0, .3); }
.daily-btn.is-done { background: linear-gradient(180deg, #eef1f4 0%, #d8dee4 100%); }
.daily-btn.is-ready { animation: nudge 2.6s ease-in-out infinite; }
@keyframes nudge {
  0%, 92%, 100% { transform: translateY(0); }
  94% { transform: translateY(-2px); }
  96% { transform: translateY(1px); }
}
.daily-star { width: 19px; height: 19px; flex-shrink: 0; display: block; filter: drop-shadow(0 1px 0 rgba(61, 50, 32, .35)); }
.daily-btn.is-ready .daily-star { filter: drop-shadow(0 0 5px rgba(255, 207, 63, .95)); }
.daily-copy { display: flex; flex-direction: column; min-width: 0; }
.daily-copy b { font-size: .74rem; }
.daily-copy em {
  font-style: normal;
  font-family: var(--pixel);
  font-size: .55rem;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.daily-side { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
/* No giveaway running means no odds to read and nothing to boost. The row then
   carries the premium drop on its own, which is all that is on offer; the idle
   game below stays a separate thing instead of crowding it. */
.daily-side.is-off { display: none; }
.odds {
  font-family: var(--pixel);
  font-size: .55rem;
  color: #ffe9a8;
  letter-spacing: .03em;
  white-space: nowrap;
}
.mini-btn {
  padding: 5px 7px;
  border: 2px solid var(--ink);
  border-radius: 9px;
  background: linear-gradient(180deg, #fff 0%, #dfe6ea 100%);
  font-family: var(--pixel);
  font-size: .55rem;
  cursor: pointer;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, .3);
}
.mini-btn:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 rgba(0, 0, 0, .3); }
.mini-btn:disabled { opacity: .5; cursor: not-allowed; }

.banner {
  /* Pinned to the top of the field: the sky band is the one place in the widget
     that carries no control, so a message can sit there without covering the
     button, the chest, the shop or the daily row. */
  position: absolute;
  z-index: 20;
  left: 8px;
  right: 8px;
  top: 4px;
  padding: 6px 8px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: #fff3c4;
  font-size: .68rem;
  line-height: 1.3;
  box-shadow: 2px 3px 0 var(--shadow);
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity .25s ease, transform .25s ease;
  pointer-events: none;
}
.banner.show { opacity: 1; transform: translateY(0); }

.toast {
  /* Same band as the banner, for the same reason: it is a message, and a message
     that lands on the button is a message the player has to fight to dismiss. */
  position: absolute;
  z-index: 60;
  left: 50%;
  top: 4px;
  width: max-content;
  max-width: 92%;
  padding: 5px 9px;
  border: 2px solid var(--ink);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 3px 4px 0 var(--shadow);
  transform: translate(-50%, -10px);
  opacity: 0;
  transition: opacity .2s ease, transform .2s ease;
  pointer-events: none;
  text-align: center;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast h3 { font-family: var(--shout); font-size: .8rem; font-weight: 400; margin-bottom: 1px; }
.toast p { font-size: .64rem; line-height: 1.25; }
/* In the hero frame the sky band is only a few pixels tall now that the mode row
   lives under the ad, so a two-line toast would land on the button. One clipped
   line, no heading: the same news, out of the way. */
html.embed .toast { top: 2px; padding: 3px 8px; }
html.embed .toast h3 { display: none; }
html.embed .toast p { font-size: .58rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.toast .tier { font-family: var(--pixel); font-size: .55rem; text-transform: uppercase; color: var(--violet, #7c3aed); }
.toast .tier.wild { color: #d97706; }
.toast .tier.rare { color: #2563eb; }

/* ----------------------------------------------------------------- panels */

.overlay {
  position: absolute;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px;
  background: rgba(24, 18, 40, .42);
  backdrop-filter: blur(3px);
  opacity: 0;
  pointer-events: none;
  transition: opacity .18s ease;
}
.overlay.open { opacity: 1; pointer-events: auto; }
.panel {
  width: 100%;
  max-width: 300px;
  max-height: 92%;
  overflow-y: auto;
  padding: 11px 12px 13px;
  border: 3px solid var(--ink);
  border-radius: 16px;
  background: var(--panel);
  box-shadow: 5px 6px 0 var(--shadow), inset 0 2px 0 rgba(255, 255, 255, .7);
  transform: translateY(10px) scale(.97);
  opacity: 0;
  transition: transform .22s cubic-bezier(.16, .84, .28, 1), opacity .2s ease;
  overscroll-behavior: contain;
}
.overlay.open .panel { transform: translateY(0) scale(1); opacity: 1; }
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
  padding-bottom: 6px;
  border-bottom: 2px solid rgba(44, 36, 22, .12);
}
.panel-head h2 { font-family: var(--shout); font-size: 1.05rem; font-weight: 400; }
.close-btn {
  width: 26px;
  height: 26px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border: 2px solid var(--ink);
  border-radius: 8px;
  background: #fff;
  font-size: .7rem;
  cursor: pointer;
  box-shadow: 2px 2px 0 var(--shadow);
}
.close-btn:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--shadow); }
.panel-h3 {
  margin: 10px 0 4px;
  font-family: var(--pixel);
  font-size: .62rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.panel-lead { font-size: .72rem; line-height: 1.4; color: var(--ink-soft); }
.panel-tip { margin-top: 8px; font-size: .62rem; line-height: 1.4; color: var(--ink-soft); }

.upgrade-grid, .shop-grid { display: grid; gap: 6px; margin-top: 6px; }
.upgrade-item, .shop-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 2px solid rgba(44, 36, 22, .16);
  border-radius: 11px;
  background: #fff;
}
.shop-item { grid-template-columns: 1fr auto; }
.upgrade-icon { font-size: 17px; }
.upgrade-copy .name, .shop-item .name { font-size: .72rem; font-weight: 700; }
.upgrade-copy .level { font-family: var(--pixel); font-size: .55rem; color: var(--ink-soft); font-weight: 400; }
.upgrade-copy .desc, .shop-item .desc { font-size: .62rem; color: var(--ink-soft); line-height: 1.3; }
.shop-item .desc { grid-column: 1 / -1; }
.buy-btn {
  padding: 5px 8px;
  border: 2px solid var(--ink);
  border-radius: 9px;
  background: linear-gradient(180deg, #ffe9a8 0%, #ffcf3f 100%);
  font-family: var(--pixel);
  font-size: .58rem;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 2px 2px 0 var(--shadow);
}
.buy-btn:active:not(:disabled) { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--shadow); }
.buy-btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }

.primary-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px 14px;
  border: 2px solid var(--ink);
  border-radius: 11px;
  background: linear-gradient(180deg, #7ee38a 0%, #43b657 100%);
  color: #10331a;
  font-family: var(--shout);
  font-size: .78rem;
  text-decoration: none;
  cursor: pointer;
  box-shadow: 3px 3px 0 var(--shadow);
}
.primary-btn:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--shadow); }
.primary-btn:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
.ghost-btn {
  padding: 8px 12px;
  border: 2px solid rgba(44, 36, 22, .28);
  border-radius: 11px;
  background: #fff;
  font-family: var(--ui);
  font-size: .7rem;
  cursor: pointer;
}
.ghost-btn:active { transform: translate(1px, 1px); }

.chest-body, .gift-body { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px; }
.chest-big { line-height: 0; filter: drop-shadow(2px 3px 0 var(--shadow)); }
.chest-big svg { width: 78px; height: auto; display: block; }
.gift-day { font-family: var(--pixel); font-size: .58rem; letter-spacing: .06em; text-transform: uppercase; color: #b8860b; }
/* font-size in the overrides below still steers the icon: it is 1em square. */
.gift-icon { font-size: 40px; line-height: 1; filter: drop-shadow(0 2px 0 rgba(0, 0, 0, .18)); }
.gift-icon .ico { width: 1em; height: 1em; }
.gift-body h3 { font-family: var(--shout); font-size: .92rem; font-weight: 400; }
.gift-body p { font-size: .72rem; line-height: 1.45; color: var(--ink-soft); }
.gift-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 7px; margin-top: 4px; }

.boost-facts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px 10px;
  width: 100%;
  margin: 2px 0 4px;
  text-align: left;
}
.boost-facts dt { font-family: var(--pixel); font-size: .52rem; text-transform: uppercase; color: var(--ink-soft); }
.boost-facts dd { font-family: var(--shout); font-size: .95rem; }

.calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-top: 4px; }
.cal-day {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  border: 2px solid rgba(44, 36, 22, .14);
  border-radius: 8px;
  background: #fff;
  font-family: var(--pixel);
  font-size: .55rem;
}
.cal-day.done { background: linear-gradient(180deg, #ffe9a8, #ffcf3f); border-color: var(--ink); }
.cal-day.today { outline: 2px solid var(--violet, #7c3aed); outline-offset: 1px; }

.inv-list { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 4px; }
.inv-chip {
  padding: 3px 7px;
  border: 2px solid rgba(44, 36, 22, .14);
  border-radius: 999px;
  background: #fff;
  font-size: .6rem;
}
.account-row { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.account-mail { font-size: .68rem; color: var(--ink-soft); }

.panel-campaign .camp-body { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px; }
.camp-art { font-size: 40px; line-height: 1; filter: drop-shadow(0 2px 0 rgba(0, 0, 0, .14)); }
.camp-body h3 { font-family: var(--shout); font-size: .95rem; font-weight: 400; }
.camp-body p { font-size: .72rem; line-height: 1.45; color: var(--ink-soft); }
.camp-embed { width: 100%; aspect-ratio: 16 / 9; margin: 2px 0; border: 2px solid var(--ink); border-radius: 10px; overflow: hidden; background: #000; }
.camp-embed iframe { width: 100%; height: 100%; border: 0; display: block; }

/* --------------------------------------------------------------- onboarding */

#onboarding {
  position: absolute;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(24, 18, 40, .5);
  backdrop-filter: blur(3px);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease;
}
#onboarding.open { opacity: 1; pointer-events: auto; }
#onboarding .card {
  max-width: 280px;
  padding: 12px 14px 14px;
  border: 3px solid var(--ink);
  border-radius: 16px;
  background: var(--panel);
  text-align: center;
  box-shadow: 5px 6px 0 var(--shadow);
}
#onboarding img { width: 128px; height: auto; margin-bottom: 4px; }
#onboarding h2 { font-family: var(--shout); font-size: 1.1rem; font-weight: 400; margin-bottom: 4px; }
#onboarding p { font-size: .72rem; line-height: 1.45; color: var(--ink-soft); margin-bottom: 10px; }

/* ------------------------------------------------------------- fx hooks */

.bounce { animation: fx-bounce 340ms ease; }
@keyframes fx-bounce {
  0% { transform: scale(1); }
  35% { transform: scale(1.14) rotate(-2deg); }
  70% { transform: scale(.97); }
  100% { transform: scale(1); }
}
.shake-screen { animation: fx-shake 340ms ease-in-out; }
@keyframes fx-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-4px); }
  40% { transform: translateX(4px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(3px); }
}

/* --------------------------------------------------- the 290px hero embed */

/* The frame around the widget already says the name at full size, and the box
   is 90px shorter than the game was drawn for. So the embed drops the hint line,
   the season pill and the onboarding art margin, and squeezes the paddings - the
   stage keeps its height, because that is where the game is. */
/* The hero frame is 325x290, not 325x512. The widget used to stay 512 tall in
   that box, which cropped the daily drop, the odds and the Boost button off the
   bottom - the whole giveaway was invisible where most people meet the game.
   So the embed takes the height it is given and composes into it: every part of
   the loop is on screen, and the fixed 512px card stays for the standalone page. */
html.embed #app { height: 100%; max-height: 100%; }
html.embed .topbar { padding: 5px 7px 2px; }
/* The plate adds 6px of frame, so the art steps down by that much to keep the
   hero's top bar exactly as tall as it was. */
html.embed .brand { padding: 0 6px; }
html.embed .brand-logo { height: 29px; }
html.embed .adslot { margin: 2px 7px 0; padding: 4px 6px; }
html.embed .adslot-copy em { font-size: .58rem; }
/* The hero itself already says "Tapp The Button" and "Tap for coins", so the
   card that repeats that over the game is what the widget does not need there.
   Taps reach the button immediately, and the card still opens on /tapp/. */
html.embed #onboarding { display: none; }
/* In the hero the field is the whole widget, so the top of it is kept clear for
   the message that slides in there. Without the reserve a two-line message lands
   on the button again - the very thing it is meant to stay off. */
html.embed .stage { padding: 30px 6px 6px; }
html.embed .tap-hint { display: none; }
/* The hero frame is 290px tall and the dungeon panel has to fit in 94 per cent
   of it with the stage, the log and the belt all on screen. */
html.embed .dg-stage { height: clamp(118px, 34vh, 190px); }
html.embed .hatch { margin: 2px 6px 0; padding: 3px 7px 3px 5px; }
html.embed .hatch-copy b { font-size: .78rem; }
html.embed #tapp-btn { width: clamp(78px, 26vh, 118px); height: clamp(78px, 26vh, 118px); }
html.embed .dailyrow { padding: 4px 7px 5px; gap: 5px; }
html.embed .toast { top: 2px; }
html.embed .daily-btn { padding: 4px 7px; gap: 6px; }
html.embed .daily-star { width: 16px; height: 16px; }
html.embed .daily-copy b { font-size: .68rem; }
html.embed .daily-copy em { font-size: .5rem; }
html.embed .panel { max-height: 94%; padding: 9px 10px 11px; }
html.embed .gift-icon { font-size: 28px; }
html.embed .panel-gift .panel-tip { display: none; }

@media (max-width: 420px) {
  #app { border-radius: 14px; }
}

/* ---------------------------------------------------------------- the dungeon */

/* The hatch on the main screen. The crawl used to live three taps deep in the
   menu and Josh could not find it, so the way down is now a strip on the widget
   itself, where the fiction always put it: under the shop.

   It has to own a stacking layer. The sky is .scene, an absolutely positioned
   layer at z-index 0, and positioned layers paint over in-flow content whatever
   the DOM order - so as a plain static button the strip was laid out here but
   painted under the sky, leaving one loose door icon on the clouds where the
   whole bar should be. z-index 9 puts it in the bar of things over the scene
   with the ad slot above it and the daily row below. */
.hatch {
  position: relative;
  z-index: 9;
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 2px 7px 0;
  padding: 4px 8px 4px 6px;
  border: 0;
  border-radius: 8px;
  background: linear-gradient(180deg, #3a332c, #241f1b);
  color: #f4efe4;
  text-align: left;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 2px 0 rgba(20, 16, 12, .28);
  transition: transform .1s ease, box-shadow .18s ease;
}
.hatch:active { transform: translateY(1px); }
.hatch-door { flex: 0 0 auto; width: 30px; height: 23px; filter: drop-shadow(1px 2px 0 rgba(0, 0, 0, .3)); }
.hatch-door svg { display: block; width: 100%; height: 100%; }
.hatch-copy { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.hatch-copy b { font-family: var(--shout); font-weight: 400; font-size: .84rem; line-height: 1.1; color: #ffd166; }
.hatch-copy em {
  font-family: var(--pixel);
  font-size: .46rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: rgba(244, 239, 228, .6);
}
.hatch-go {
  flex: 0 0 auto;
  padding: 3px 6px;
  border: 1px solid rgba(244, 239, 228, .3);
  border-radius: 3px 8px 8px 8px;
  font-family: var(--pixel);
  font-size: .48rem;
  letter-spacing: .08em;
  text-transform: uppercase;
}

/* The crawl fills the panel: two globes, a sheet you can open, the stage, one
   line of log and the belt. Ink on near-black, because a dungeon lit by one
   torch is not a white card. */
.panel-dungeon { background: #14100f; color: #f4efe4; }
.dg-tools { display: flex; align-items: center; gap: 6px; }
.dg-tool {
  padding: 3px 7px;
  border: 1px solid rgba(244, 239, 228, .28);
  border-radius: 999px;
  background: transparent;
  color: rgba(244, 239, 228, .7);
  font-family: var(--pixel);
  font-size: .46rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  cursor: pointer;
}
.dg-tool[aria-pressed="false"] { color: #ffd166; border-color: rgba(255, 209, 102, .5); }
.panel-dungeon .panel-head h2 { color: #f4efe4; }
.panel-dungeon .close-btn { color: #f4efe4; border-color: rgba(244, 239, 228, .3); }
.wide-btn { width: 100%; justify-content: center; }
.wide-btn-note {
  display: block;
  font-family: var(--ui);
  font-size: .56rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  opacity: .72;
}
/* Life on the left and mana on the right, the way Diablo has always put them:
   those two globes are the whole health system, so they earn the space. */
.dg-hud {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) 34px;
  align-items: center;
  gap: 8px;
  margin: 4px 0 8px;
}
.dg-orb {
  position: relative;
  width: 34px;
  height: 46px;
  border-radius: 50% / 46%;
  border: 2px solid #4a4038;
  overflow: hidden;
  background: #1b1512;
  box-shadow: inset 0 0 6px rgba(0, 0, 0, .8);
}
.dg-orb-fill {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 0;
  transition: height .2s ease;
}
.dg-orb--hp .dg-orb-fill { background: linear-gradient(180deg, #e4463a, #7c1a12); }
.dg-orb--mp .dg-orb-fill { background: linear-gradient(180deg, #4a8fe4, #17356e); }
.dg-orb b {
  position: absolute;
  inset: auto 0 3px;
  text-align: center;
  font-family: var(--pixel);
  font-size: .5rem;
  color: #fff;
  text-shadow: 0 1px 2px #000;
}
.dg-core { min-width: 0; }
.dg-core-top { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.dg-lvl {
  font-family: var(--pixel);
  font-size: .56rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: rgba(244, 239, 228, .7);
}
.dg-lvl b { color: #ffd166; }
.dg-xp { flex: 1; height: 8px; border: 1px solid #4a4038; border-radius: 3px; background: #241d19; overflow: hidden; }
.dg-xp i {
  display: block;
  height: 100%;
  width: 0;
  background: linear-gradient(180deg, #d8b34a, #8a6a12);
  transition: width .25s ease;
}
.dg-facts { display: flex; flex-wrap: wrap; gap: 3px 13px; margin: 0; padding: 0; }
.dg-facts div { display: flex; align-items: baseline; gap: 5px; }
.dg-facts dt {
  font-family: var(--pixel);
  font-size: .5rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: rgba(244, 239, 228, .55);
}
.dg-facts dd { margin: 0; font-family: var(--pixel); font-size: .58rem; color: #ffd166; }
.dg-stage {
  position: relative;
  height: clamp(150px, 38vh, 240px);
  background: #0b0908;
  border: 2px solid #4a4038;
  border-radius: 6px;
  overflow: hidden;
  touch-action: manipulation;
}
.dg-stage canvas { display: block; width: 100%; height: 100%; }
/* A boss gets announced in letters across the room, the way it should. */
.dg-banner {
  position: absolute;
  left: 0;
  right: 0;
  top: 38%;
  display: grid;
  justify-items: center;
  gap: 1px;
  padding: 6px 8px;
  background: linear-gradient(90deg, transparent, rgba(10, 4, 4, .88) 10%, rgba(10, 4, 4, .88) 90%, transparent);
  text-align: center;
  pointer-events: none;
}
.dg-banner b { font-family: var(--shout); font-weight: 400; font-size: .96rem; color: #ff8b7a; }
.dg-banner em {
  font-style: normal;
  font-family: var(--pixel);
  font-size: .46rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: rgba(244, 239, 228, .68);
}
.dg-dead {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 6px;
  padding: 10px;
  text-align: center;
  background: rgba(10, 6, 6, .82);
}
.dg-dead h3 {
  margin: 0;
  font-family: var(--shout);
  font-size: 1.05rem;
  color: #ff8b7a;
}
.dg-dead p { margin: 0; font-size: .68rem; color: rgba(244, 239, 228, .82); }
.dg-dead i, .dg-dead b { font-style: normal; color: #ffd166; }
.dg-log {
  margin: 6px 0 4px;
  min-height: 2em;
  font-size: .66rem;
  line-height: 1.35;
  color: rgba(244, 239, 228, .82);
}
/* The log says what just happened, and its colour says how good it was. */
.dg-log[data-rarity="magic"] { color: #7fb2ff; }
.dg-log[data-rarity="unique"] { color: #ffd166; }
.dg-log[data-rarity="showpiece"] { color: #ffd166; }
.dg-log[data-rarity="curse"] { color: #ff8b7a; }
.dg-log[data-rarity="level"] { color: #7bd389; }

.dg-belt { display: flex; flex-wrap: wrap; gap: 5px; margin: 0 0 4px; }
.dg-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 9px;
  border: 1px solid rgba(244, 239, 228, .3);
  border-radius: 6px;
  background: #241d19;
  color: #f4efe4;
  font-family: var(--pixel);
  font-size: .5rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  cursor: pointer;
}
.dg-chip b { color: #ffd166; }
.dg-chip:disabled { opacity: .36; cursor: default; }
.dg-chip--go { border-color: rgba(255, 209, 102, .55); }
.dg-chip--none {
  border-style: dashed;
  border-color: rgba(244, 239, 228, .18);
  color: rgba(244, 239, 228, .42);
  cursor: default;
}
/* The belt and the spell row are their own groups so a full belt never pushes
   the stairs out of reach. */
.dg-belt-group { display: inline-flex; flex-wrap: wrap; gap: 5px; }

.dg-sheet { margin-top: 4px; border-top: 1px solid rgba(244, 239, 228, .14); padding-top: 5px; }
.dg-sheet summary {
  font-family: var(--pixel);
  font-size: .5rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: rgba(244, 239, 228, .7);
  cursor: pointer;
}
.dg-points { margin: 4px 0 0; font-size: .6rem; color: #7bd389; }
.dg-attrs { display: grid; grid-template-columns: 1fr 1fr; gap: 3px 8px; margin: 5px 0; }
.dg-attr { display: flex; align-items: center; gap: 4px; font-size: .58rem; }
.dg-attr em { flex: 1; font-style: normal; color: rgba(244, 239, 228, .6); }
.dg-attr b { font-family: var(--pixel); font-size: .54rem; }
.dg-attr b i { font-style: normal; color: #7bd389; font-size: .46rem; margin-left: 2px; }
.dg-plus {
  width: 17px;
  height: 17px;
  border: 1px solid rgba(244, 239, 228, .34);
  border-radius: 4px;
  background: transparent;
  color: #ffd166;
  font-size: .68rem;
  line-height: 1;
  cursor: pointer;
}
.dg-plus:disabled { opacity: .28; cursor: default; }
.dg-gearline { display: flex; flex-wrap: wrap; gap: 4px; }
.dg-slot { padding: 2px 6px; border: 1px solid rgba(244, 239, 228, .2); border-radius: 4px; font-size: .54rem; }
.dg-slot--empty { color: rgba(244, 239, 228, .4); font-style: italic; }
.dg-slot--common { color: #e8e4d5; }
.dg-slot--magic { color: #7fb2ff; border-color: rgba(127, 178, 255, .45); }
.dg-slot--unique { color: #ffd166; border-color: rgba(255, 209, 102, .5); }
.dg-slot--showpiece { color: #ffd166; border-color: rgba(255, 209, 102, .5); }
.dg-stats { margin: 5px 0 0; font-size: .52rem; color: rgba(244, 239, 228, .6); }
.dg-hint { margin: 6px 0 0; font-size: .54rem; color: rgba(244, 239, 228, .48); }
.panel-dungeon .ghost-btn { color: #f4efe4; border-color: rgba(244, 239, 228, .34); }
.panel-dungeon .ghost-btn:disabled { opacity: .4; }

/* ------------------------------------------------------- the crawl, full width
   A Diablo tile is 64x32 pixels of isometric stone, and that is the unit the
   whole Cathedral is drawn in. Ten of them crushed into a 300px panel is why the
   crawl read as brown noise: at that size a brick wall is three pixels of grey.
   So this one panel leaves the phone frame. It is the whole window, the stage
   takes every pixel the bars do not, and the art is drawn at its own size. */
/* Fixed, not absolute: the crawl is not a panel inside the 325px widget, it is
   the window. #app has no transform, so a fixed child is not clipped by its
   overflow. */
.overlay--dungeon {
  position: fixed;
  inset: 0;
  padding: 0;
  background: #070605;
  backdrop-filter: none;
  z-index: 80;
}
.overlay--dungeon.open .panel-dungeon { transform: none; opacity: 1; }
.overlay--dungeon .panel-dungeon {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: #0b0908;
  box-shadow: none;
  overflow: hidden;
  color: #f4efe4;
}

/* The title bar: the floor's name on the left, the two controls on the right. */
.overlay--dungeon .panel-head {
  margin: 0;
  padding: 9px clamp(12px, 2vw, 22px);
  border-bottom: 1px solid #241c17;
  background: #0f0b09;
}
.overlay--dungeon .panel-head h2 {
  font-size: clamp(.95rem, 1.5vw, 1.2rem);
  letter-spacing: .02em;
}
.overlay--dungeon .dg-tools { gap: 8px; }
.overlay--dungeon .dg-tool { padding: 5px 11px; font-size: .56rem; }
.overlay--dungeon .close-btn {
  width: 30px;
  height: 30px;
  padding: 0;
  display: grid;
  place-items: center;
  background: transparent;
  color: #f4efe4;
  border: 1px solid rgba(244, 239, 228, .3);
}

/* Life at one end, mana at the other, the sheet between them. This is the bar
   the eye comes back to every second, so nothing on it is under 11px. */
.overlay--dungeon .dg-hud {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(10px, 2vw, 28px);
  margin: 0;
  padding: 8px clamp(12px, 2vw, 24px);
  border-bottom: 1px solid #241c17;
  background: linear-gradient(180deg, #17110e, #100c09);
}
.overlay--dungeon .dg-orb {
  width: clamp(44px, 4.2vw, 60px);
  height: clamp(58px, 5.6vw, 80px);
  border-width: 3px;
}
.overlay--dungeon .dg-orb b {
  inset: auto 0 11px;
  font-size: clamp(.58rem, .78vw, .74rem);
  line-height: 1;
  letter-spacing: .02em;
}
.overlay--dungeon .dg-core-top { gap: 10px; margin-bottom: 6px; }
.overlay--dungeon .dg-lvl { font-size: clamp(.6rem, .82vw, .78rem); }
.overlay--dungeon .dg-xp { height: 9px; max-width: 360px; }
.overlay--dungeon .dg-facts { gap: 5px clamp(12px, 2.2vw, 30px); }
.overlay--dungeon .dg-facts dt { font-size: clamp(.52rem, .7vw, .66rem); }
.overlay--dungeon .dg-facts dd { font-size: clamp(.7rem, .92vw, .88rem); }

/* The stage: whatever the window has left after the bars. */
.overlay--dungeon .dg-stage {
  position: relative;
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
  border: 0;
  border-radius: 0;
  background: #060504;
}
.overlay--dungeon .dg-stage canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.overlay--dungeon .dg-banner { top: 30%; padding: 14px 20px; }
.overlay--dungeon .dg-banner b { font-size: clamp(1.2rem, 2.4vw, 2rem); }
.overlay--dungeon .dg-banner em { font-size: .6rem; }

.overlay--dungeon .dg-log {
  margin: 0;
  padding: 7px clamp(12px, 2vw, 24px);
  min-height: 2.1em;
  font-size: clamp(.62rem, .8vw, .76rem);
  border-top: 1px solid #241c17;
  background: #0d0908;
}

/* The belt sits under the log, one row, with the way down pushed to the far
   side so a full belt can never cover it. */
.overlay--dungeon #dg-belt-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 7px clamp(12px, 2vw, 24px) 9px;
  background: #0f0b09;
  border-top: 1px solid #241c17;
}
.overlay--dungeon #dg-stair { margin-left: auto; }
.overlay--dungeon .dg-chip { padding: 6px 11px; font-size: .55rem; }

/* Closed, the sheet is a chip in the belt row; open, it is a drawer over the
   stage. Either way it never takes room away from the map, and never covers the
   log line the way the old full-width strip did. */
.overlay--dungeon .dg-sheet:not([open]) {
  margin: 0;
  padding: 5px 10px;
  border: 1px solid #3a2f28;
  border-radius: 999px;
  background: rgba(24, 18, 16, .92);
}
.overlay--dungeon .dg-sheet:not([open]) summary {
  font-size: .55rem;
  line-height: 1;
  color: rgba(244, 239, 228, .72);
}

/* The sheet is a drawer over the stage, not a block that squashes it. */
.overlay--dungeon .dg-sheet[open] {
  position: absolute;
  left: clamp(12px, 2vw, 24px);
  right: clamp(12px, 2vw, 24px);
  bottom: 62px;
  z-index: 6;
  max-height: 56vh;
  overflow-y: auto;
  margin: 0;
  padding: 10px 14px 14px;
  border: 1px solid #3a2f28;
  border-radius: 10px;
  background: #161110;
  box-shadow: 0 12px 30px rgba(0, 0, 0, .55);
}
.overlay--dungeon .dg-sheet[open] summary { font-size: .62rem; }

/* A phone has half the width for the same five facts, so the labels shrink
   rather than the numbers: "FLOOR 1 AREA CATHEDRAL" still reads as a pair. */
@media (max-width: 560px) {
  .overlay--dungeon .dg-hud { gap: 8px; padding: 6px 10px; }
  .overlay--dungeon .dg-facts { gap: 1px 10px; }
  .overlay--dungeon .dg-facts dt { font-size: .46rem; }
  .overlay--dungeon .dg-facts dd { font-size: .66rem; }
  .overlay--dungeon .dg-lvl { font-size: .56rem; }
}
.overlay--dungeon .dg-attrs { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px 18px; }
.overlay--dungeon .dg-attr { font-size: .68rem; }
.overlay--dungeon .dg-attr b { font-size: .62rem; }
.overlay--dungeon .dg-slot { font-size: .62rem; padding: 3px 8px; }
.overlay--dungeon .dg-stats,
.overlay--dungeon .dg-hint { font-size: .62rem; }

/* ---------------------------------------------------------------- the bag
   Loot is the reason to open a chest, so the bag is a real list and not a line
   in the log: one row per item, its slot, and the two things you can do with it.
   Worn gear sits above it as chips, so the comparison is one glance. */
.dg-baghead {
  margin: 12px 0 6px;
  font-family: var(--pixel);
  font-size: .56rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: rgba(244, 239, 228, .55);
}
.dg-baglist { display: grid; gap: 4px; }
.dg-bagline {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 1px solid rgba(244, 239, 228, .14);
  border-radius: 7px;
  background: #1d1715;
  font-size: .66rem;
  line-height: 1.25;
}
.dg-bagline b { font-weight: 400; color: #e8e4d5; letter-spacing: .02em; }
.dg-bagline em {
  font-style: normal;
  font-family: var(--pixel);
  font-size: .5rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: rgba(244, 239, 228, .6);
}
.dg-bagline--magic b { color: #7fb2ff; }
.dg-bagline--showpiece b { color: #ffd166; }
.dg-bagline--empty {
  display: block;
  padding: 8px;
  color: rgba(244, 239, 228, .5);
  font-size: .64rem;
}
.dg-bagbtn {
  padding: 4px 9px;
  border: 1px solid rgba(255, 209, 102, .45);
  border-radius: 5px;
  background: transparent;
  color: #ffd166;
  font-family: var(--pixel);
  font-size: .5rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  cursor: pointer;
}
.dg-bagbtn:hover { background: rgba(255, 209, 102, .12); }
.dg-bagbtn--sell { border-color: rgba(244, 239, 228, .28); color: rgba(244, 239, 228, .75); }
.dg-bagbtn--sell:hover { background: rgba(244, 239, 228, .08); }

/* --------------------------------------------------------- the page around it
   The widget was the whole page at every width, which on a laptop meant one
   325px card in the middle of an empty field. The field now holds the column
   that explains what the thing is. Under 980px that column is dropped and the
   widget is the page again, which is the right composition on a phone. */
.tapp-intro { display: none; }

@media (min-width: 980px) {
  html, body {
    background: radial-gradient(120% 90% at 18% 8%, #4d92a8 0%, #3f7f93 44%, #2b5e6f 100%);
  }
  body {
    display: grid;
    grid-template-columns: minmax(280px, 400px) auto;
    align-items: center;
    justify-content: center;
    gap: clamp(28px, 5vw, 88px);
    padding: 24px clamp(24px, 4vw, 72px);
  }
  .tapp-intro {
    display: block;
    max-width: 400px;
    color: #f4fbfd;
    text-shadow: 0 1px 0 rgba(11, 38, 46, .35);
  }
  .intro-kicker {
    margin: 0 0 10px;
    font-family: var(--pixel);
    font-size: .56rem;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: rgba(244, 251, 253, .72);
  }
  .intro-title {
    margin: 0 0 14px;
    font-family: var(--shout);
    font-size: clamp(2.1rem, 3.6vw, 3.2rem);
    line-height: .94;
    letter-spacing: -.01em;
    color: #fffdf7;
  }
  .intro-lede {
    margin: 0 0 22px;
    font-size: clamp(.86rem, 1.05vw, .98rem);
    line-height: 1.5;
    color: rgba(244, 251, 253, .86);
  }
  .intro-points {
    display: grid;
    gap: 12px;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .intro-points li {
    padding-left: 14px;
    border-left: 2px solid rgba(255, 209, 102, .55);
  }
  .intro-points b {
    display: block;
    font-family: var(--pixel);
    font-size: .62rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: #ffd166;
  }
  .intro-points span {
    display: block;
    margin-top: 3px;
    font-size: .78rem;
    line-height: 1.45;
    color: rgba(244, 251, 253, .78);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sun, .clouds i, .grass, .tufts, .daily-btn.is-ready, .tapp-glow { animation: none !important; }
  .overlay, .panel, .toast, .banner { transition: none; }
}

/* ---------------------------------------------------------- campaign pictures
The button badge used to be one emoji. A campaign can carry a PNG or a GIF now, so
every art slot has to hold a picture as happily as a glyph: fill the box it already
had (border, radius, padding stay put), never stretch, never overflow it. */
.adslot-art img,
.tapp-art img,
.camp-art img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
.adslot-art img { width: 100%; height: 100%; }
.tapp-art img { height: 1.35em; width: auto; }
.camp-art img { width: 44px; height: 44px; }

/* ---------------------------------------------------------------- one object
   The widget read as a stack of separate things: a white header bar, a purple
   ad bar, a bronze daily bar, and two buttons floating in the field. They are
   all the same object - a button you press, with what it has earned on it - so
   they now sit on one surface: soft translucent strips over the same sky, one
   hairline between the field and the daily line, and no competing borders.
   Sizes come down with them, because a 325px widget cannot afford five
   different margins. */
.topbar { padding: 6px 8px 0; }
.brand-logo { height: 42px; }

.adslot {
  border: 0;
  border-radius: 10px;
  background: rgba(255, 255, 255, .46);
  margin: 0 8px 2px;
  padding: 3px 8px;
  box-shadow: none;
}
.adslot-copy b { font-size: .58rem; letter-spacing: 0; }
.adslot-copy em { font-size: .54rem; }
.adslot-cta { font-size: .5rem; padding: 2px 7px; }

.stage { padding: 2px 6px 0; }

.side-btn {
  border: 0;
  border-radius: 14px;
  background: rgba(255, 255, 255, .26);
  box-shadow: inset 0 0 0 2px rgba(61, 50, 32, .16);
  padding: 6px 4px;
}
.side-btn .label { font-size: .48rem; letter-spacing: .04em; }

.dailyrow {
  margin-top: 2px;
  padding: 4px 8px calc(5px + var(--safe-bottom));
  border-top: 1px solid rgba(61, 50, 32, .35);
  background: rgba(255, 255, 255, .3);
  box-shadow: none;
}
.daily-btn { background: transparent; border: 0; }
.daily-btn b { font-size: .58rem; }
.daily-btn em { font-size: .5rem; }
.odds { font-size: .48rem; padding: 2px 6px; }
.mini-btn { font-size: .5rem; padding: 2px 8px; }

/* A flex child with content taller than its share pushes the footer out of the
   card instead of shrinking; `min-height: 0` is what lets the field give up
   height. Without it the daily line was clipped off the bottom of the widget. */
.stage { min-height: 0; }
.dailyrow { flex-shrink: 0; }
.daily-star { width: 16px; height: 16px; }
.daily-copy b { font-size: .64rem; }
.daily-copy em { font-size: .48rem; }
.daily-btn { padding: 4px 7px; }
.dailyrow { min-height: 54px; align-items: center; padding: 6px 8px calc(6px + var(--safe-bottom)); }
.daily-side { flex-shrink: 0; }

/* A single control deck keeps the game actions together in both frame sizes. */
.stage, html.embed .stage { display: flex; align-items: stretch; padding: 8px; }
.tapp-console {
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  /* Props in the field, then the two paper bars pinned to the floor of the
     widget. Left to itself the console stopped halfway down and the bars
     floated on the grass with a hundred empty pixels underneath. */
  grid-template-rows: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 2px 4px;
  /* The console is not a card. In the drawing Tapp stands in the meadow with the
     chest and the machine either side of him; a cream panel behind the three of
     them turned the button into a face pasted on paper. */
  padding: 2px 4px 0;
}
.side-btn { background: transparent; box-shadow: none; }
/* The props are the drawing's own chest and vending machine now, so they are
   sized by width and keep their shape. */
.side-btn img { display: block; height: auto; max-width: 100%; filter: drop-shadow(2px 3px 0 rgba(61, 50, 32, .22)); }
/* The chest is drawn, the machine is the drawing; both get the same weight on
   the field, because the two doors into the widget should not look like a shop
   and its afterthought. */
#btn-chest .chest-art {
  display: block;
  width: clamp(56px, 15vh, 76px);
  height: auto;
  filter: drop-shadow(2px 3px 0 rgba(61, 50, 32, .22));
}
#btn-shop img { width: clamp(46px, 13vh, 62px); }
/* The machine is drawn to the same 32x28 box as the chest, so it is sized the
   same way and the two props stand at one weight on the field. The hand-lettered
   sign that came with the photograph is gone: both props wear the pixel plaque
   further down, which is what makes CHEST and SHOP match. */
.shop-btn .shop-art {
  display: block;
  width: clamp(56px, 15vh, 76px);
  height: auto;
  filter: drop-shadow(2px 3px 0 rgba(61, 50, 32, .22));
}
html.embed #btn-chest .chest-art { width: 54px; }
html.embed .shop-btn .shop-art { width: 54px; }
/* On the field a bare word is unreadable, so each prop carries its own little
   paper tag, like the signs in the drawing. */
.side-btn .label {
  padding: 1px 7px;
  border: 2px solid rgba(61, 50, 32, .5);
  border-radius: 7px;
  background: rgba(255, 250, 240, .94);
  color: var(--ink);
  text-shadow: none;
  font-size: .5rem;
}
.tapp-wrap::before { display: block; }
/* The line under the disc says what the next press does, because the same
   button is the entry in one mode and the coin tap in the other. */
.tap-hint {
  display: block;
  margin-top: 1px;
  font-family: var(--pixel);
  font-size: .44rem;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: rgba(61, 50, 32, .72);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .55);
  text-align: center;
}
/* The daily row and the machine floor now sit on the grass instead of a card, so
   each of them is a paper bar of its own. */
.dailyrow, html.embed .dailyrow {
  grid-column: 1 / -1;
  min-height: 0;
  margin: 7px 2px 0;
  padding: 5px 8px;
  border: 2px solid rgba(61, 50, 32, .5);
  border-radius: 14px;
  background: rgba(255, 250, 240, .94);
  box-shadow: 0 3px 0 rgba(61, 50, 32, .22);
}
.daily-btn { box-shadow: none; }
.odds { color: var(--ink); }
#tapp-btn:focus-visible, .side-btn:focus-visible, .daily-btn:focus-visible { outline: 3px solid #6432a0; outline-offset: 3px; }

/* ---------------------------------------------------- what the press does */

/* Two modes, two buttons, one row - and the row sits under the ad, above the
   field, so changing mode can never hide the campaign. Giveaway is the default:
   it is the one thing a new player has to see, and idle is what keeps them here
   after they have entered. */
.modes {
  position: relative;
  z-index: 5;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  margin: 6px 10px 0;
}
.mode-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  padding: 5px 7px;
  border: 2px solid rgba(61, 50, 32, .4);
  border-radius: 12px;
  background: rgba(255, 250, 240, .86);
  box-shadow: 0 2px 0 rgba(61, 50, 32, .16);
  color: var(--ink);
  font-family: var(--ui);
  text-align: left;
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease;
}
.mode-ico { width: 15px; height: 15px; flex: 0 0 auto; }
.mode-copy { display: grid; min-width: 0; }
.mode-copy b {
  font-family: var(--pixel);
  font-size: .54rem;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.mode-copy em {
  font-style: normal;
  font-size: .48rem;
  line-height: 1.2;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mode-btn.is-on {
  border-color: var(--ink);
  background: var(--panel, #fffaf0);
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--tapp-accent) 52%, transparent), 0 2px 0 rgba(61, 50, 32, .2);
}
.mode-btn:focus-visible { outline: 3px solid #6432a0; outline-offset: 2px; }
/* A dot, not a strip across the sky: there is a press waiting, and it is in here. */
.mode-btn.is-ready .mode-ico { filter: drop-shadow(0 0 4px rgba(255, 207, 63, .95)); }
/* The note under a mode's name can wrap to two lines now that it is set at
   reading size; the dot rides on the heading rather than the middle of the row,
   where it landed on top of the second line. */
.mode-btn.is-ready::after {
  content: '';
  align-self: flex-start;
  margin: 5px 0 0 auto;
  width: 7px;
  height: 7px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: #e0533f;
  box-shadow: 0 0 0 2px rgba(224, 83, 63, .22);
}

/* Giveaway mode's floor bar. Idle mode swaps it for the machine floor. */
.giveaway-bar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 7px 2px 0;
  padding: 5px 8px;
  border: 2px solid rgba(61, 50, 32, .5);
  border-radius: 14px;
  background: rgba(255, 250, 240, .94);
  box-shadow: 0 3px 0 rgba(61, 50, 32, .22);
}
.gb-state { display: grid; gap: 1px; min-width: 0; }
.gb-state b { font-size: .62rem; font-weight: 600; }
.gb-state em {
  font-style: normal;
  font-family: var(--pixel);
  font-size: .44rem;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.giveaway-bar .odds { margin-left: auto; color: var(--ink-soft); }
/* No draw running means no odds and nothing to boost. */
.giveaway-bar.is-off-draw .odds,
.giveaway-bar.is-off-draw .mini-btn { display: none; }
/* The last winner. It wraps under the row rather than squeezing the odds, and it
   stays when nothing is running: the proof is the point when nobody is in yet. */
.gb-last {
  flex: 1 0 100%;
  order: 9;
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px dashed rgba(61, 50, 32, .22);
  font-family: var(--pixel);
  font-size: .44rem;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.gb-last[hidden] { display: none; }

html[data-mode='giveaway'] .status-strip { display: none; }
html[data-mode='idle'] .giveaway-bar { display: none; }

/* ------------------------------------------------------- the machine floor */

/* The band under the console used to be grass and nothing else, so the widget
   read as a card that stopped early and left the field to fill the gap. It is
   now the game's floor: what the machines pay, what one tap is worth, and how
   far away the next purchase is. The strip is a button because the thing a
   player wants after reading "Next: Coffee cart" is that row - so pressing it
   opens the shop, one tap instead of two. */
.status-strip {
  grid-column: 1 / -1;
  display: grid;
  gap: 4px;
  width: auto;
  margin: 6px 2px 0;
  padding: 5px 8px 6px;
  border: 2px solid rgba(61, 50, 32, .5);
  border-radius: 14px;
  background: rgba(255, 250, 240, .94);
  box-shadow: 0 3px 0 rgba(61, 50, 32, .22);
  color: var(--ink);
  font-family: var(--ui);
  text-align: left;
  cursor: pointer;
  transition: background .15s ease, transform .1s ease;
}
.status-strip:active { transform: translateY(1px); background: rgba(61, 50, 32, .14); }
.status-strip:focus-visible { outline: 3px solid #6432a0; outline-offset: 2px; }

.strip-metrics { display: flex; align-items: baseline; gap: 8px; }
.strip-metric { display: flex; align-items: baseline; gap: 3px; min-width: 0; }
.strip-metric + .strip-metric { padding-left: 8px; border-left: 1px solid rgba(61, 50, 32, .22); }
.strip-metric b { font-family: var(--pixel); font-size: .84rem; letter-spacing: .02em; }
.strip-metric em {
  font-style: normal;
  font-family: var(--pixel);
  font-size: .5rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ink-soft);
  white-space: nowrap;
}

.strip-goal { display: grid; gap: 3px; }
.strip-goal-label {
  display: flex;
  align-items: baseline;
  gap: 3px;
  min-width: 0;
  font-size: .6rem;
  color: var(--ink-soft);
  white-space: nowrap;
}
.strip-goal-label b {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--ink);
  font-weight: 600;
}
/* The price is a coin mark and a number, not "71c": at five figures a trailing
   letter next to a K reads like a typo. */
.strip-cost {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
  margin-left: auto;
  font-style: normal;
}
.strip-cost .ico { width: .62rem; height: .62rem; }
.strip-cost b {
  font-family: var(--pixel);
  font-size: .58rem;
  font-weight: 400;
  color: var(--ink);
}
/* The track is inset on all four sides: run edge to edge it collided with the
   bar's rounded bottom and read as a smear rather than as a progress line. */
.strip-track {
  display: block;
  height: 5px;
  margin: 1px 1px 0;
  border-radius: 99px;
  background: rgba(61, 50, 32, .16);
  box-shadow: inset 0 1px 1px rgba(61, 50, 32, .18);
  overflow: hidden;
}
.strip-track i {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 99px;
  background: linear-gradient(90deg, #ffcf3f, #ff9f43);
  transition: width .35s ease;
}
/* The goal is met: the bar turns into the colour of yes, and the strip is one
   press away from spending it. */
.status-strip.is-ready .strip-track i { background: linear-gradient(90deg, #8fe39a, #3faf62); }
.status-strip.is-ready .strip-cost b { color: #2f8b4c; }

/* The same coin mark in the shop, so a price reads as a price in both places. */
.buy-coin { display: inline-block; width: .62rem; height: .62rem; margin-left: 2px; vertical-align: -1px; }

/* --------------------------------------------------------- the tap streak */

.combo-badge {
  position: absolute;
  z-index: 3;
  right: -6px;
  bottom: 8px;
  display: grid;
  place-items: center;
  padding: 2px 6px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: linear-gradient(180deg, #fff6d2, #ffcf3f);
  font-family: var(--pixel);
  font-size: .6rem;
  color: var(--ink);
  box-shadow: 2px 2px 0 var(--shadow);
  transform: rotate(-7deg);
  pointer-events: none;
}
.combo-badge.hot { background: linear-gradient(180deg, #ffe3c0, #ff8a5c); }
.combo-badge.bump { animation: combo-bump 220ms ease; }
@keyframes combo-bump {
  0% { transform: rotate(-7deg) scale(1); }
  45% { transform: rotate(-4deg) scale(1.24); }
  100% { transform: rotate(-7deg) scale(1); }
}

/* A tap has to land in the wallet too, not only in the counter. */
.chip--coins.is-bump { animation: chip-bump 180ms ease; }
@keyframes chip-bump {
  0% { transform: scale(1); }
  45% { transform: scale(1.09); }
  100% { transform: scale(1); }
}

/* The hero embed is 325 by 290 and has no room for the metrics line. The goal
   bar is the part that earns its keep there, so the numbers step aside. */
/* The floor bar is the last thing above the rounded bottom of the hero frame,
   so it keeps a margin of its own: pressed against the corner it looked cut off. */
html.embed .status-strip { margin: 4px 2px 7px; padding: 4px 7px 7px; gap: 2px; }
html.embed .strip-metrics { display: none; }
html.embed .strip-goal-label { font-size: .56rem; }
html.embed .strip-track { height: 4px; }
/* The iframe is the viewport in here, so the height of the hero frame is a media
   query. 340 fits the whole loop; the 276px phone frame needs the field to give
   back what the mode row took, or the floor bar is the row the bezel eats. */
@media (max-height: 335px) {
  html.embed .stage { padding-top: 24px; }
  html.embed .modes { margin: 4px 6px 0; gap: 5px; }
  html.embed .mode-btn { padding: 4px 6px; gap: 4px; }
  html.embed .mode-copy em { display: none; }
  html.embed #tapp-btn { width: clamp(66px, 23vh, 100px); height: clamp(66px, 23vh, 100px); }
}
html.embed .modes { margin: 4px 8px 0; }
html.embed .mode-btn { padding: 4px 6px; gap: 5px; }
html.embed .mode-copy em { font-size: .44rem; }

/* ----------------------------------------------------------- the shop shelves
   A shop that mixes machines, power and cosmetics in one list makes every price
   look like the same kind of price. The shelves are tabs, so a player who wants
   a backdrop never has to scroll past six income machines to find it. */
/* Six shelves on a 300px panel do not fit on one line, and a shelf that is
   scrolled out of sight is a shelf nobody finds: the row wraps instead. */
.shop-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 2px 0 10px;
  padding: 0 0 2px;
}
.shop-tab {
  flex: 0 0 auto;
  padding: 4px 7px;
  line-height: 1.15;
  border: 2px solid rgba(44, 36, 22, .18);
  border-radius: 999px;
  background: #fff;
  font-family: var(--pixel);
  font-size: .52rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  cursor: pointer;
}
.shop-tab.is-on {
  border-color: var(--ink);
  background: linear-gradient(180deg, #ffe9a8 0%, #ffcf3f 100%);
  color: var(--ink);
  box-shadow: 2px 2px 0 var(--shadow);
}
.shop-tab:active { transform: translate(1px, 1px); }
.shop-tab:focus-visible { outline: 3px solid #6432a0; outline-offset: 2px; }
.shop-pane[hidden] { display: none; }

/* Skins and themes are bought for how they look, so the card shows the look:
   a disc in the skin's own colours, or three bands of the theme's palette. */
.skin-grid, .theme-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; margin-top: 6px; }
.skin-card, .theme-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 7px;
  min-width: 0;
  padding: 6px;
  border: 2px solid rgba(61, 50, 32, .3);
  border-radius: 11px;
  background: rgba(255, 255, 255, .55);
  text-align: left;
  cursor: pointer;
  transition: transform .1s ease, border-color .15s ease, box-shadow .15s ease;
}
.skin-card:active, .theme-card:active { transform: translateY(1px); }
.skin-card:disabled, .theme-card:disabled { opacity: .62; cursor: not-allowed; }
.skin-card.is-equipped, .theme-card.is-equipped { border-color: #2f8b4c; box-shadow: 0 0 0 2px rgba(47, 139, 76, .22); }
.skin-swatch {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 4px solid var(--skin-rim, #2a2a2a);
  background:
    radial-gradient(ellipse 74% 50% at 33% 14%, rgba(255, 255, 255, .95) 0%, rgba(255, 255, 255, .28) 44%, transparent 68%),
    radial-gradient(circle at 50% 34%, #fffdf7 0 11%, var(--skin-glow, #fff8e7) 46%, var(--skin-body, #f5f0e8) 76%, color-mix(in srgb, var(--skin-accent, #ff6b9d) 34%, var(--skin-body, #f5f0e8)) 100%);
  box-shadow: inset 0 -4px 6px rgba(120, 70, 20, .13), 2px 2px 0 var(--shadow);
}
.theme-swatch {
  width: 34px;
  height: 34px;
  border-radius: 9px;
  background: linear-gradient(180deg, var(--th-sky, #87ceeb) 0 44%, var(--th-grass, #6bcb77) 44% 76%, var(--th-body, #f5f0e8) 76%);
  box-shadow: inset 0 0 0 2px rgba(0, 0, 0, .16), 2px 2px 0 var(--shadow);
}
.skin-copy, .theme-copy { min-width: 0; }
.skin-copy .name, .theme-copy .name {
  display: block;
  overflow: hidden;
  font-size: .64rem;
  font-weight: 700;
  color: var(--ink);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.skin-copy .meta, .theme-copy .meta {
  display: flex;
  align-items: center;
  gap: 3px;
  font-family: var(--pixel);
  font-size: .48rem;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.skin-copy .meta .ico, .theme-copy .meta .ico { width: .6rem; height: .6rem; }
.skin-copy .meta.is-season, .theme-copy .meta.is-season { color: #b45309; }

/* The chest: what came out of it has to be the loudest thing in the panel. */
.chest-stage { position: relative; display: grid; place-items: center; padding: 4px 0 2px; }
#chest-art.is-opening { animation: chest-rattle .55s ease-in-out; }
@keyframes chest-rattle {
  0%, 100% { transform: rotate(0) translateY(0); }
  18% { transform: rotate(-5deg) translateY(-3px); }
  36% { transform: rotate(4deg) translateY(-1px); }
  54% { transform: rotate(-3deg) translateY(-2px); }
  72% { transform: rotate(2deg) translateY(0); }
}
.chest-result {
  min-height: 2.6em;
  font-size: .68rem;
  line-height: 1.4;
  color: var(--ink);
}
.chest-result .tier {
  display: block;
  font-family: var(--pixel);
  font-size: .52rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--violet, #7c3aed);
}
.chest-result .tier.rare { color: #2563eb; }
.chest-result .tier.wild { color: #d97706; }
.chest-result strong { font-family: var(--shout); font-weight: 400; }

/* A chip you can press is a chip that looks pressable. */
.inv-chip--btn {
  border-color: rgba(44, 36, 22, .3);
  cursor: pointer;
}
.inv-chip--btn:hover { background: #fff6d9; }
.inv-chip--btn.is-on {
  border-color: #2f8b4c;
  background: linear-gradient(180deg, #e8ffe9, #c8f0d0);
}

/* ------------------------------------------------------------ the scene shop */

/* Two across, because a backdrop is a picture: one per row would be a list of
   labels and the player buys the picture. The thumb is the palette itself -
   sky on top, field under it - so the grid reads as the places before it reads
   as prices. */
.scene-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.scene-card {
  display: grid;
  gap: 4px;
  min-width: 0;
  padding: 5px;
  border: 2px solid rgba(61, 50, 32, .3);
  border-radius: 11px;
  background: rgba(255, 255, 255, .5);
  text-align: left;
  cursor: pointer;
  transition: transform .1s ease, border-color .15s ease, box-shadow .15s ease;
}
.scene-card:active { transform: translateY(1px); }
.scene-card .scene-thumb {
  position: relative;
  display: block;
  height: 34px;
  border-radius: 7px;
  overflow: hidden;
  background: linear-gradient(180deg, var(--s1) 0%, var(--s2) 56%, var(--s3) 56%, var(--s4) 100%);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .14);
}
/* The body in the sky: without it every thumb is two bars of colour, and a
   grid of two-bar thumbs reads as a palette test rather than as places. */
.scene-card .scene-thumb::after {
  content: "";
  position: absolute;
  top: 16%;
  right: 16%;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--s5, rgba(255, 255, 255, .9));
  box-shadow: 0 0 7px 2px color-mix(in srgb, var(--s5, #ffffff) 50%, transparent);
  opacity: .9;
}
.scene-card .scene-name {
  display: block;
  overflow: hidden;
  font-size: .6rem;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.scene-card .scene-meta {
  display: flex;
  align-items: center;
  gap: 2px;
  font-style: normal;
  font-family: var(--pixel);
  font-size: .5rem;
  color: var(--ink-soft);
}
.scene-card .scene-meta .ico { width: .6rem; height: .6rem; }
.scene-card.is-equipped { border-color: #2f8b4c; box-shadow: 0 0 0 2px rgba(47, 139, 76, .22); }
.scene-card.is-equipped .scene-meta { color: #2f8b4c; }
.scene-card:disabled { opacity: .62; cursor: not-allowed; }
.scene-card:focus-visible { outline: 3px solid #6432a0; outline-offset: 2px; }

/* ------------------------------------------------------------------ reboot */

.reboot-box {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  border: 2px solid rgba(61, 50, 32, .35);
  border-radius: 12px;
  background: rgba(255, 255, 255, .5);
}
.reboot-copy { display: grid; gap: 2px; min-width: 0; flex: 1; }
.reboot-copy b { font-family: var(--pixel); font-size: .62rem; letter-spacing: .02em; }
.reboot-copy span { font-size: .58rem; line-height: 1.3; color: var(--ink-soft); }
.reboot-btn {
  flex-shrink: 0;
  padding: 6px 10px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: linear-gradient(180deg, #fff 0%, #dfe6ea 100%);
  font-family: var(--pixel);
  font-size: .55rem;
  color: var(--ink);
  cursor: pointer;
  box-shadow: 2px 2px 0 var(--shadow);
}
.reboot-btn:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--shadow); }
.reboot-btn:disabled { opacity: .5; cursor: not-allowed; }
.reboot-btn.is-ready {
  background: linear-gradient(180deg, #8fe39a, #3faf62);
  color: #0c2f18;
  animation: nudge 2.6s ease-in-out infinite;
}

/* --------------------------------------------------------------- shelf rows
   A machine row answers three questions in a fixed order: what it is, what it
   pays right now, and what the next one costs. The old rows printed the same
   "you own N" under a level chip that already said N, and a machine with no
   description printed undefined. Both are gone: the number is in the line and
   the description belongs to the machine, not to the markup. */
.upgrade-copy { min-width: 0; }
.upgrade-copy .desc {
  font-size: .6rem;
  line-height: 1.35;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.upgrade-copy .blurb {
  margin-top: 1px;
  font-size: .55rem;
  line-height: 1.35;
  color: var(--ink-soft);
}
.upgrade-item.has-levels { background: linear-gradient(180deg, #fffdf4, #fff); }
.upgrade-item.has-levels .upgrade-icon { filter: saturate(1.1); }
.upgrade-item .buy-btn { min-width: 52px; }

/* ------------------------------------------------------------- the job board
   A job is a shelf row that fills up: what it is, what it counts, a bar, and
   the pay behind a button that only lights when the bar is full. Same bones as
   a machine row, so the board reads as another shelf rather than another
   place. */
.contract-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 2px solid rgba(44, 36, 22, .16);
  border-radius: 11px;
  background: #fff;
  transition: border-color .2s ease, background .2s ease;
}
.contract-item .contract-line { font-size: .58rem; }
.contract-item .contract-bar {
  position: relative;
  height: 5px;
  margin-top: 4px;
  border-radius: 99px;
  background: rgba(44, 36, 22, .12);
  overflow: hidden;
}
.contract-item .contract-bar i {
  display: block;
  width: 0;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, #ffcf3f, #ff9f1a);
  transition: width .3s ease;
}
.contract-item.is-done { border-color: var(--ink); background: linear-gradient(180deg, #fffbe8, #fff); }
.contract-item.is-done .contract-bar i { background: linear-gradient(90deg, #6bd47f, #2f9d4a); }
.contract-item .contract-claim { min-width: 56px; }
/* A core price is not a coin price: the mark is a violet star rather than the
   gold coin, so two currencies never read as the same number on two shelves. */
.buy-btn.core-buy { background: linear-gradient(180deg, #efe1ff 0%, #d9c2ff 100%); }
.core-ico { color: #6d28b8; }
.buy-btn .core-ico { vertical-align: -1px; }
.core-bank-line {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 2px 0 6px;
  font-size: .62rem;
  color: var(--ink-soft);
}
.core-bank {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 1px 7px;
  border: 2px solid rgba(44, 36, 22, .16);
  border-radius: 999px;
  background: #fff;
  font-family: var(--pixel);
  font-size: .58rem;
  color: var(--ink);
}
.core-bank .core-ico { width: .66rem; height: .66rem; }

/* The season strip: one line that says a holiday is running, how long it has
   left, and the press that puts its kit on. It only exists while it applies. */
.season-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 2px 0 6px;
  padding: 6px 8px;
  border: 2px solid var(--ink);
  border-radius: 11px;
  background: linear-gradient(135deg, #fff3d1 0%, #ffe0b8 100%);
  box-shadow: 2px 2px 0 var(--shadow);
}
.season-bar[hidden] { display: none; }
.season-copy { display: grid; gap: 1px; min-width: 0; flex: 1; }
.season-copy b { font-family: var(--pixel); font-size: .6rem; letter-spacing: .02em; }
.season-copy span { font-size: .55rem; line-height: 1.3; color: var(--ink-soft); }
.season-wear {
  flex-shrink: 0;
  padding: 5px 9px;
  border: 2px solid var(--ink);
  border-radius: 9px;
  background: linear-gradient(180deg, #fff 0%, #ffd98a 100%);
  font-family: var(--pixel);
  font-size: .52rem;
  color: var(--ink);
  cursor: pointer;
  box-shadow: 2px 2px 0 var(--shadow);
}
.season-wear:active:not(:disabled) { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--shadow); }
.season-wear:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }

/* Kits are a picture and a sentence, so they get the full width of the panel
   instead of sharing a row with the button faces. The row is as tall as the
   taller of the two: stretching the copy to the thumbnail's height is what
   pushed a kit's name up under the card edge and left its ribbon on the blurb. */
.theme-grid { grid-template-columns: 1fr; gap: 7px; }
.theme-card { align-items: center; padding: 6px 7px 6px 6px; }
.theme-card .theme-thumb { width: 52px; height: 44px; border-radius: 8px; flex: 0 0 auto; }
.theme-copy { display: grid; gap: 1px; align-content: start; }
.theme-copy .name { font-size: .66rem; }
.theme-copy .blurb {
  display: block;
  overflow: hidden;
  font-size: .55rem;
  line-height: 1.3;
  color: var(--ink-soft);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.theme-meta {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 4px;
  margin-top: 1px;
  overflow: hidden;
}
.theme-meta .meta { color: var(--ink-soft); }
/* The season mark rides on the picture instead of taking a line of its own:
   three chips on a 190px row wrapped onto a second line and fell outside the
   card. On the thumbnail it also reads as a badge on the thing itself. */
.theme-card { position: relative; }
.theme-card .season-tag {
  position: absolute;
  top: 3px;
  left: 3px;
  margin: 0;
  padding: 1px 4px;
  border: 1px solid rgba(120, 60, 0, .35);
  border-radius: 999px;
  background: #ffd27a;
  font-family: var(--pixel);
  font-size: .4rem;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: #4a2400;
  box-shadow: 1px 1px 0 rgba(61, 50, 32, .25);
}
.effect-note { grid-column: 1 / -1; }

/* A panel that scrolls needs a scrollbar you can see past. */
.panel::-webkit-scrollbar { width: 8px; }
.panel::-webkit-scrollbar-track { background: transparent; }
.panel::-webkit-scrollbar-thumb {
  border: 2px solid transparent;
  border-radius: 99px;
  background: rgba(61, 50, 32, .28);
  background-clip: content-box;
}

/* The chest's own stage lives in chest.css; these two rules are the parts the
   game writes into, so they stay here with the markup that owns them. */
.reveal-art .ico { width: 60%; height: 60%; }
.reveal-art { display: grid; place-items: center; font-size: 1.25rem; line-height: 1; }

/* ------------------------------------------------------------ frame sizes
   The hero frame is 325x290 and its top bar already carries the menu, the two
   chips and the logo. The logo on a plate is wider than the bare art was, so in
   the hero it steps down again here - after the shared rules above, or the
   later 42px would win and the premium chip would clip off the right edge. */
html.embed .brand-logo { height: 29px; }
html.embed .brand { padding: 0 5px; }

/* ------------------------------------------------------------- readable type
   The widget was set in 6-7px pixel type with every quiet line at 62% ink, and
   the shop's own descriptions at 8.7px. In the 325px frame that is a texture
   rather than words, so every line the player has to read gets a floor here and
   the two faded greys become a real brown.

   The floor is 10px, not 9: the first pass landed on 9-10px and it was still a
   squint in the embedded frame. In rem, with the root at 14px that the widget
   sets, 0.72rem is 10.1px and 0.92rem is 12.9px. Pixel-font sizes stay on the
   whole-ish steps so Silkscreen's 5px grid does not smear at 1x. */

:root {
  /* 62% ink over cream was the reason a caption read as a smudge. A solid tone
     about three steps darker keeps it secondary and makes it a sentence again. */
  --ink-soft: #4a3f2f;
}

/* The one word on each prop: one plaque, one face, so CHEST and SHOP match.
   Cream on a dark rule rather than white on wood: the wood plaque at 9px was
   about 2.4:1, which is a shaded smudge in sunlight. This is 8:1. */
.side-btn .label,
html.embed .side-btn .label {
  font-size: .9rem;
  line-height: 1.2;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: 3px 8px;
  background: #fff6e2;
  border: 2px solid #6b5326;
  border-radius: 7px;
  color: #3a2f1c;
  text-shadow: none;
  box-shadow: 0 2px 0 rgba(61, 50, 32, .22);
}

/* The lines under the mode row and along the daily row. */
.gb-state em,
.daily-copy em,
.mode-copy em,
html.embed .gb-state em,
html.embed .daily-copy em,
html.embed .mode-copy em {
  font-size: .76rem;
  line-height: 1.45;
  letter-spacing: .01em;
}
/* Two mode notes on a 325px row do not fit on one line at reading size, and a
   truncated "press Tapp to take p..." is not a note, so those two wrap. */
.mode-copy em,
html.embed .mode-copy em {
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
}
/* The bar under the field is a row of three things, not a paragraph: its two
   lines stay on their own line, and the timer copy is short enough to fit one. */
.gb-state em,
.daily-copy em,
html.embed .gb-state em,
html.embed .daily-copy em {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.gb-state b, .daily-copy b, .mode-copy b,
html.embed .gb-state b, html.embed .daily-copy b, html.embed .mode-copy b {
  font-size: .78rem;
  line-height: 1.25;
}
.odds, .mini-btn,
html.embed .odds, html.embed .mini-btn { font-size: .76rem; }

/* The ad bar: the headline, the line under it and the button word. */
.adslot-copy b { font-size: .8rem; }
.adslot-copy em { font-size: .78rem; letter-spacing: .01em; opacity: 1; }
#camp-headline { font-size: .78rem; }
.adslot-cta, #camp-label { font-size: .78rem; }
#coin-count { font-size: .78rem; }
/* Panel chrome: the close cross and the "Later" / "Spend" buttons. */
.close-btn, .ghost-btn { font-size: .78rem; }

/* Panel copy: the body of the drop, the shop and the menu. The shop's own
   description lines were the worst of it at 8.7px, so they get the largest
   step of the lot - a description nobody can read is a button with no price. */
.gift-body p,
.panel p,
.panel-lead,
.panel li,
.panel .muted {
  font-size: .92rem;
  line-height: 1.55;
}
.upgrade-copy .name,
.shop-item .name,
.theme-copy .name { font-size: .86rem; line-height: 1.3; }
/* The shop row's own description is `.blurb`, not `.desc`: the first pass at
   this block named a class that does not exist, so the one line a player reads
   before spending coins stayed at 7.7px in the shop. */
.upgrade-copy .blurb,
.effect-note { font-size: .84rem; line-height: 1.5; }
/* What the thing pays: "+9 a second, each". It sits between the name and the
   description and was 8.4px - the number the whole shelf is about. */
.upgrade-copy .desc,
.shop-item .desc { font-size: .82rem; line-height: 1.4; }
/* A shelf's price chip: one coin, one number. */
.buy-btn { font-size: .78rem; }
/* Theme cards already get a full-width row, so the blurb wraps instead of
   becoming "A meadow with a hammock and...". */
.theme-copy .blurb {
  font-size: .8rem;
  line-height: 1.45;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.boost-facts dd, .boost-facts dt { font-size: .78rem; line-height: 1.45; }
.gift-day, .gift-tip { font-size: .72rem; line-height: 1.4; }
.panel-h3 { font-size: .8rem; letter-spacing: .05em; }
.shop-tab { font-size: .78rem; }
.inv-chip { font-size: .78rem; }
.strip-metric em { font-size: .76rem; }
.strip-metric b { font-size: .84rem; }
.tap-hint { font-size: .76rem; }

/* The chest's own panel: the names of the pins, what state each one is in, and
   the counter above them. This is the screen a giveaway entry is actually
   checked on, so it is set at reading size, not at caption size. */
.chest-progress { font-size: .76rem; }
.pin-name { font-size: .78rem; }
.pin-when { font-size: .76rem; }
.pin-note, .chest-note { font-size: .76rem; line-height: 1.45; }

/* The hint under the disc was white type with a shadow on grass, which is the
   one background in the widget that has no quiet area. It sits on a dark pill
   now, so it reads at every backdrop without touching the field behind it. */
.tap-hint {
  margin-top: 3px;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(30, 23, 12, .72);
  color: #fffdf2;
  text-shadow: none;
  text-align: center;
  line-height: 1.35;
  max-width: 17ch;
}


/* ------------------------------------------------------ the way down */
/* A stone arch at the edge of the field, under the mode buttons. It is a prop
   like the chest and the shop, so it carries the same wooden sign. */
.crawl-door {
  position: absolute;
  right: 8px;
  top: 4px;
  z-index: 3;
  display: grid;
  justify-items: center;
  gap: 1px;
  text-decoration: none;
  transform-origin: 50% 100%;
  transition: transform .2s ease;
}
.crawl-door svg { width: 34px; height: auto; display: block; filter: drop-shadow(2px 3px 0 var(--shadow, rgba(0,0,0,.25))); }
.crawl-door .label {
  padding: 1px 6px;
  border: 2px solid var(--wood-dark, #8b6914);
  border-radius: 6px;
  background: #4a3a52;
  color: #ffd38a;
  font-family: var(--pixel);
  font-size: .46rem;
  letter-spacing: .06em;
  text-shadow: 1px 1px 0 #0006;
}
.crawl-door:hover { transform: scale(1.06) rotate(-2deg); }
.crawl-door:active { transform: scale(.95); }
.crawl-door:focus-visible { outline: 2px solid #ffd38a; outline-offset: 2px; border-radius: 6px; }
html.embed .crawl-door { top: 2px; right: 4px; }
html.embed .crawl-door svg { width: 24px; }
html.embed .crawl-door .label { font-size: .38rem; padding: 0 4px; }
