/* =========================================================================
   Fidget Clicker — Variant C: "INDUSTRIAL WORKSHOP"
   Painted steel machine housing, stencilled signage type, amber safety
   accent. The 3D amber spinner stays the brightest, most saturated thing
   on screen — everything else here is deliberately restrained.
   ========================================================================= */

/* ---------- fonts (local only, no network) ---------- */

@font-face{
  font-family: "Saira Condensed";
  src: url("./fonts/SairaCondensed-600-latin.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family: "Saira Condensed";
  src: url("./fonts/SairaCondensed-600-latin-ext.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family: "Saira Condensed";
  src: url("./fonts/SairaCondensed-700-latin.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family: "Saira Condensed";
  src: url("./fonts/SairaCondensed-700-latin-ext.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family: "IBM Plex Mono";
  src: url("./fonts/IBMPlexMono-500-latin.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family: "IBM Plex Mono";
  src: url("./fonts/IBMPlexMono-500-latin-ext.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family: "IBM Plex Mono";
  src: url("./fonts/IBMPlexMono-600-latin.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family: "IBM Plex Mono";
  src: url("./fonts/IBMPlexMono-600-latin-ext.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* ---------- tokens ---------- */

/* Registers --accent* as animatable <color> custom properties (Task 3) so
   the transition below actually interpolates through intermediate colours
   frame-by-frame instead of a hard cut - every rule that reads var(--accent)
   etc recomputes as it animates, so the whole UI cross-fades to the new
   coating-tier theme. initial-value matches the "plastic" tier (level 0)
   so an unsupporting browser (no @property) still gets a correct, if
   non-animated, colour from the plain :root declarations below. */
@property --accent{ syntax: '<color>'; inherits: true; initial-value: #ffab2e; }
@property --accent-hi{ syntax: '<color>'; inherits: true; initial-value: #ffc866; }
@property --accent-dim{ syntax: '<color>'; inherits: true; initial-value: #a8701f; }
@property --accent-ink{ syntax: '<color>'; inherits: true; initial-value: #1a1200; }

:root{
  /* painted steel, greenish-grey undertone in shadow */
  --bg:            #1b1f1e;
  --bg-alt:        #23282a;
  --panel:         #2b3132;
  --panel-2:       #333a3b;
  --panel-shadow:  #14181a;
  --seam:          rgba(0,0,0,0.55);
  --edge-hi:       rgba(255,255,255,0.10);
  --edge-hi-strong:rgba(255,255,255,0.16);

  --text:          #e7e6dd;
  --text-dim:      #9aa19c;
  --text-faint:    #6c746f;

  /* Accent = the current Premium Coating tier's colour, written onto this
     element's inline style by applyCoatingTier() (game.js) on every tier
     change and on load. The values below are just the "plastic" (level 0)
     default shown before game.js has run a single frame. Never edit these
     to reflect a different tier - edit COATING_TIERS in game.js instead. */
  --accent:         #ffab2e;
  --accent-hi:      #ffc866;
  --accent-dim:     #a8701f;
  --accent-ink:     #1a1200;
  transition: --accent 450ms ease, --accent-hi 450ms ease,
              --accent-dim 450ms ease, --accent-ink 450ms ease;

  /* Money is deliberately the ONE colour that does NOT follow the coating
     tier. It stays a fixed green so the player's balance is readable at a
     glance no matter what the accent is doing, and because green reads as
     money. Hue ~137deg (green), kept clear of ~170deg mint/teal on purpose
     - that mint was part of the generic look this UI was rebuilt to escape. */
  --money:         #5ed17a;
  --money-hi:      #86e6a0;

  --warn:          #d9503a;
  --warn-hi:       #ef6a4e;
  --warn-ink:      #2a0d07;

  --row-h: 72px;

  font-family: "Saira Condensed", "Arial Narrow", Arial, sans-serif;
}

*{ box-sizing: border-box; }

html, body{
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  overflow-x: hidden;
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
}

body{
  touch-action: manipulation;
  /* faint procedural brushed-steel grain, CSS only */
  background-image:
    repeating-linear-gradient(0deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 3px);
}

#app{
  display: flex;
  flex-direction: column;
  height: 100dvh;
  min-height: 100dvh;
  width: 100%;
  overflow: hidden;
}

/* ---------- top bar: a stencilled control-panel strip ---------- */

#topbar{
  flex: 0 0 auto;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: env(safe-area-inset-top, 0px) 16px 12px 16px;
  padding-top: max(16px, env(safe-area-inset-top, 16px));
  background:
    linear-gradient(to bottom, var(--bg-alt), var(--bg));
  border-bottom: 1px solid var(--seam);
  box-shadow: 0 1px 0 var(--edge-hi) inset;
  user-select: none;
  position: relative;
}

/* single hazard-stripe accent, used once, along the very top edge as a
   signature detail rather than a repeated motif */
#topbar::before{
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: repeating-linear-gradient(
    -45deg,
    var(--accent) 0px, var(--accent) 8px,
    #1a1a1a 8px, #1a1a1a 16px
  );
  opacity: 0.85;
}

.stats-row{
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  justify-content: space-around;
  align-items: center;
}

.stat{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 40%;
}

/* ---------- round housing buttons (mute / lang) ---------- */

.topbar-actions{
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
}

.mute-btn,
.lang-btn{
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  min-width: 40px;
  min-height: 40px;
  padding: 0;
  border-radius: 3px;
  border: 1px solid var(--seam);
  background: linear-gradient(180deg, var(--panel-2) 0%, var(--panel) 65%, var(--panel-shadow) 100%);
  box-shadow:
    0 1px 0 var(--edge-hi) inset,
    0 -2px 0 var(--panel-shadow) inset,
    0 2px 3px rgba(0,0,0,0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform 120ms ease, background 120ms ease, box-shadow 120ms ease;
}

.mute-btn:active,
.lang-btn:active{
  transform: translateY(1px) scale(0.97);
  box-shadow:
    0 1px 0 var(--edge-hi) inset,
    0 1px 4px rgba(0,0,0,0.5) inset;
}

.lang-flag{
  width: 22px;
  height: 15px;
  border-radius: 1px;
  border: 1px solid rgba(255,255,255,0.3);
  display: block;
  overflow: hidden;
}

.icon-sound{
  width: 20px;
  height: 20px;
}

.icon-sound .speaker-body{
  fill: var(--text);
  stroke: none;
}

.icon-sound .wave{
  fill: none;
  stroke: var(--text);
  stroke-width: 2;
  stroke-linecap: round;
  opacity: 1;
  transition: opacity 150ms ease;
}

.icon-sound .mute-x{
  fill: none;
  stroke: var(--text-dim);
  stroke-width: 2;
  stroke-linecap: round;
  opacity: 0;
  transition: opacity 150ms ease;
}

.mute-btn.muted{
  border-color: var(--accent-dim);
}
.mute-btn.muted .icon-sound .wave{
  opacity: 0;
}
.mute-btn.muted .icon-sound .mute-x{
  opacity: 1;
  stroke: var(--accent);
}

.stat-label{
  font-family: "Saira Condensed", sans-serif;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.stat-value{
  font-family: "IBM Plex Mono", "Courier New", monospace;
  font-size: clamp(22px, 6vw, 32px);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

#moneyValue{
  color: var(--money); /* fixed green, not the tier accent - see --money */
}

/* ---------------------------------------------------------------------
   Tachometer (RPM gauge) — replaces the old plain RPM number. Sits in the
   same .stat slot as the money readout: label on top, the SVG dial in the
   middle, a small IBM Plex Mono digital readout underneath. Sized with
   clamp()/vw like the rest of the responsive layout so it shrinks on a
   narrow phone instead of pushing the topbar buttons off-screen — the
   buttons keep their fixed 40x40px, this is the element that gives way.
   --------------------------------------------------------------------- */
.gauge-stat{
  /* the base .stat rule sets min-width:40%, sized for a two-stat row with
     no gauge; this element needs to be free to shrink well below that on
     a narrow phone so the topbar buttons never get squeezed */
  min-width: 0;
  flex: 0 1 auto;
  gap: 1px;
}

.rpm-gauge{
  display: block;
  width: clamp(58px, 19vw, 130px);
  height: auto; /* viewBox aspect (120x80) sets it from the width above */
  overflow: visible;
}

.gauge-track{
  fill: none;
  stroke: var(--panel-2);
  stroke-width: 6;
  stroke-linecap: round;
}

.gauge-redline{
  fill: none;
  stroke: var(--warn);
  stroke-width: 6;
  stroke-linecap: round;
  opacity: 0.75;
  transition: opacity 200ms ease, stroke 200ms ease;
}

/* strain cue: brightens while the spinner is actually revving inside the
   redline zone (class toggled by updateUI() in game.js) - kept to a plain
   colour/opacity shift, no blinking, so it reads as mechanical heat rather
   than an alarm */
.gauge-redline.hot{
  stroke: var(--warn-hi);
  opacity: 1;
}

.gauge-ticks line{
  stroke: var(--text-faint);
  stroke-width: 1.6;
  stroke-linecap: round;
}

.gauge-ticks .gauge-tick-redline{
  stroke: var(--warn);
}

.gauge-needle{
  stroke: var(--accent-hi);
  stroke-width: 3;
  stroke-linecap: round;
  transform-origin: 60px 64px;
  transform: rotate(-90deg); /* rpm = 0 -> left end of the sweep */
  /* Smooths the needle between the ~10Hz updateUI() writes (see
     CONFIG.uiUpdateHz in game.js) - the browser interpolates every
     rotate() change over this duration instead of the needle visibly
     stepping 10 times a second. */
  transition: transform 110ms ease-out;
}

.gauge-hub{
  fill: var(--panel-2);
  stroke: var(--accent);
  stroke-width: 1.5;
}

.gauge-max-label{
  font-family: "IBM Plex Mono", "Courier New", monospace;
  font-size: 9px;
  font-weight: 500;
  fill: var(--text-faint);
}

.gauge-readout{
  /* smaller than the plain .stat-value (declared later in the cascade, so
     no !important needed to win over it at equal specificity) - the dial
     itself carries the reading, the number underneath is just precision */
  font-size: clamp(14px, 4.4vw, 20px);
  line-height: 1;
}

/* ---------- spinner area: the machine bay ---------- */

#spinnerArea{
  /* Measurement container for #spinnerWrap below: makes cqw/cqh resolve to
     THIS element's real box, so the spinner can be sized from the space it
     actually has instead of guessing with vw/vh and then being clipped. */
  container-type: size;
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  padding: 12px;
  position: relative;
  background:
    radial-gradient(ellipse 70% 60% at 50% 45%, var(--bg-alt) 0%, var(--bg) 60%, #141817 100%);
}

/* subtle seam lines suggesting floor plating, sparse and low-contrast */
#spinnerArea::before{
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    repeating-linear-gradient(90deg, rgba(255,255,255,0.02) 0px, rgba(255,255,255,0.02) 1px, transparent 1px, transparent 120px),
    repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0px, rgba(255,255,255,0.02) 1px, transparent 1px, transparent 120px);
  opacity: 0.6;
}

#spinnerWrap{
  position: relative;
  aspect-ratio: 1 / 1;
  /* Measured at 375x812: the spinner area is 363px tall while the box was
     only 270px (72vw), leaving 93px of vertical slack unused - the spinner
     read as small on a phone. Widened to use it. max-height:100% below
     still clamps this to whatever the area actually has, so a shorter
     phone shrinks it instead of overflowing. */
  /* Fallback first, for a browser without container query units. */
  width: min(90vw, 60vh, 440px);
  height: min(90vw, 60vh, 440px);
  /* The real rule. cqw/cqh are #spinnerArea's own width and height, so this
     is simply "the largest square that fits the space", in any orientation.
     Sizing from vw/vh could not do this: the viewport is not the cell, so
     max-height had to clamp the overflow and the box came out oblong -
     measured 338x255 on a 375x667 phone and 234x46 on a landscape one. */
  width: min(94cqw, 94cqh, 440px);
  height: min(94cqw, 94cqh, 440px);
  max-width: 100%;
  max-height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
}

#spinner3d{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
}

#spinner3d canvas{
  display: block;
  width: 100%;
  height: 100%;
}

/* warm halo behind the spinner, brightens with RPM via inline opacity/
   transform from game.js. Driven off --accent (not a fixed orange) so the
   glow always matches the current coating tier's colour - color-mix()
   gives the fade-to-transparent stops without needing a separate --accent
   rgb-triplet variable. */
#spinnerGlow{
  position: absolute;
  left: 50%;
  top: 50%;
  width: 85%;
  height: 85%;
  transform: translate(-50%, -50%) scale(0.75);
  border-radius: 50%;
  background: radial-gradient(circle at center,
    color-mix(in srgb, var(--accent-hi) 95%, transparent) 0%,
    color-mix(in srgb, var(--accent) 38%, transparent) 45%,
    color-mix(in srgb, var(--accent-dim) 0%, transparent) 72%);
  filter: blur(20px);
  pointer-events: none;
  opacity: 0;
  transition: transform 220ms ease, opacity 220ms ease, background 450ms ease;
}

/* hard-edged industrial ground shadow, less soft than a typical UI shadow */
#spinnerShadow{
  position: absolute;
  left: 50%;
  top: 55%;
  width: 66%;
  height: 16%;
  transform: translate(-50%, -50%) scale(1);
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0) 72%);
  filter: blur(5px);
  pointer-events: none;
  opacity: 0.6;
  transition: transform 200ms ease, opacity 200ms ease;
}

#floatLayer{
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
}

.floatText{
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  color: var(--money-hi); /* the floating "+$" is money, so it matches the balance */
  font-family: "IBM Plex Mono", monospace;
  font-weight: 600;
  font-size: 18px;
  white-space: nowrap;
  pointer-events: none;
  text-shadow: 0 1px 0 rgba(0,0,0,0.5);
  animation: floatUp 900ms ease-out forwards;
}

@keyframes floatUp{
  0%{
    opacity: 1;
    transform: translate(-50%, -50%) translateY(0);
  }
  100%{
    opacity: 0;
    transform: translate(-50%, -50%) translateY(-70px);
  }
}

#particleLayer{
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
}

.particle{
  position: absolute;
  left: 50%;
  top: 50%;
  width: 6px;
  height: 6px;
  border-radius: 1px;
  background: var(--accent);
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
  transform: translate(-50%, -50%);
}

.particle.pop{
  animation: particlePop 500ms cubic-bezier(0.3, 0.7, 0.2, 1) forwards;
}

@keyframes particlePop{
  0%{
    opacity: 1;
    transform: translate(-50%, -50%) translate(0, 0) scale(1) rotate(0deg);
  }
  100%{
    opacity: 0;
    transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) scale(0.3) rotate(45deg);
  }
}

/* mechanical jolt on upgrade purchase - a switch throwing, not a wobble */
#spinnerArea.shake{
  animation: screenShake 280ms cubic-bezier(.36,.07,.19,.97);
}

@keyframes screenShake{
  8%{  transform: translate(calc(-1 * var(--shake-amp)), calc(0.3 * var(--shake-amp))); }
  22%{ transform: translate(calc(0.8 * var(--shake-amp)), calc(-0.5 * var(--shake-amp))); }
  40%{ transform: translate(calc(-0.6 * var(--shake-amp)), calc(0.4 * var(--shake-amp))); }
  60%{ transform: translate(calc(0.35 * var(--shake-amp)), calc(-0.25 * var(--shake-amp))); }
  80%{ transform: translate(calc(-0.15 * var(--shake-amp)), calc(0.1 * var(--shake-amp))); }
  100%{ transform: translate(0, 0); }
}

/* ---------- upgrades: machined control-panel plates ---------- */

#upgrades{
  flex: 0 0 auto;
  max-height: 42vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 10px 12px calc(16px + env(safe-area-inset-bottom, 0px)) 12px;
  background: var(--bg-alt);
  border-top: 1px solid var(--seam);
  box-shadow: 0 1px 0 var(--edge-hi) inset;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.upgrade-row{
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: var(--row-h);
  padding: 10px 14px;
  border-radius: 3px;
  background: linear-gradient(180deg, var(--panel-2) 0%, var(--panel) 100%);
  border: 1px solid var(--seam);
  box-shadow:
    0 1px 0 var(--edge-hi) inset,
    0 -2px 0 rgba(0,0,0,0.35) inset,
    0 2px 4px rgba(0,0,0,0.25);
  transition: opacity 150ms ease, border-color 150ms ease, box-shadow 150ms ease, filter 200ms ease;
}

/* small stamped rivet, one per row, deliberately sparse */
.upgrade-row::before{
  content: "";
  position: absolute;
  top: 8px;
  left: 8px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #565f5f 0%, #22282a 70%, #0e1111 100%);
  box-shadow: 0 1px 0 rgba(255,255,255,0.08);
}

.up-info{
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding-left: 6px;
}

.up-name{
  font-family: "Saira Condensed", sans-serif;
  font-size: 16px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.up-level{
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  letter-spacing: 0.02em;
}

.up-desc{
  font-size: 12px;
  color: var(--text-dim);
  line-height: 1.3;
}

/* Lock mark (see UNLOCK_CHAIN/.locked in game.js) - hidden by default,
   shown only while the row carries .locked. Small and quiet, matching the
   understated rivet/seam language rather than a loud "denied" badge. */
.up-lock{
  display: none;
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  color: var(--text-faint);
}
.upgrade-row.locked .up-lock{ display: inline-block; }

/* "next material tier" preview line (Premium Coating only) - anticipation
   cue for the milestone tiers in COATING_TIERS (game.js). Small, mono,
   amber-dim so it reads as a secondary readout rather than competing with
   up-name/up-desc. [hidden] (set once the player is past the last tier)
   beats the flex display below, same pattern as .offline-panel[hidden]. */
.up-next-tier{
  margin-top: 1px;
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--accent-dim);
  font-variant-numeric: tabular-nums;
}

.up-next-tier[hidden]{ display: none; }

.up-buy{
  flex: 0 0 auto;
  min-width: 84px;
  min-height: 44px;
  padding: 0 16px;
  border-radius: 2px;
  border: 1px solid var(--panel-shadow);
  border-top-color: var(--edge-hi-strong);
  background: linear-gradient(180deg, #3a4142 0%, #2a3031 60%, #22282a 100%);
  color: var(--text);
  font-family: "IBM Plex Mono", monospace;
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  box-shadow: 0 2px 0 var(--panel-shadow), 0 3px 3px rgba(0,0,0,0.3);
  transition: transform 90ms ease, box-shadow 90ms ease, background 120ms ease, color 120ms ease, border-color 120ms ease;
}

/* physical press feedback on the buy button itself */
.up-buy:active{
  transform: translateY(2px);
  box-shadow: 0 0 0 var(--panel-shadow), 0 1px 1px rgba(0,0,0,0.3);
}

/* affordable = accent-coloured, energised */
.upgrade-row.affordable{
  border-color: var(--accent-dim);
  box-shadow:
    0 1px 0 var(--edge-hi) inset,
    0 -2px 0 rgba(0,0,0,0.35) inset,
    0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent),
    0 2px 4px rgba(0,0,0,0.25);
}

.upgrade-row.affordable .up-buy{
  background: linear-gradient(180deg, var(--accent-hi) 0%, var(--accent) 55%, var(--accent-dim) 100%);
  border-color: #7a5015;
  border-top-color: rgba(255,255,255,0.5);
  color: var(--accent-ink);
  box-shadow: 0 2px 0 #7a5015, 0 3px 5px rgba(0,0,0,0.35);
}

.upgrade-row.affordable .up-buy:active{
  box-shadow: 0 0 0 #7a5015, 0 1px 1px rgba(0,0,0,0.3);
}

/* unaffordable = dimmed, housing looks powered-down */
.upgrade-row.unaffordable{
  opacity: 0.5;
}

.upgrade-row.unaffordable .up-buy{
  cursor: not-allowed;
}

/* draw the eye once when a row first becomes affordable - a brief accent
   pulse, like an indicator lamp catching */
.upgrade-row.pulse{
  animation: rowPulse 600ms ease-out;
}

@keyframes rowPulse{
  0%{ box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 65%, transparent), 0 1px 0 var(--edge-hi) inset; }
  70%{ box-shadow: 0 0 0 10px color-mix(in srgb, var(--accent) 0%, transparent), 0 1px 0 var(--edge-hi) inset; }
  100%{ box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent), 0 1px 0 var(--edge-hi) inset; }
}

/* Locked upgrade (see UNLOCK_CHAIN/isUnlocked() in game.js) - visibly
   powered-down: desaturated + dimmed, same "housing looks off" language
   as .unaffordable but a notch further so the two read as distinct
   states at a glance. affordable/unaffordable are never set on a locked
   row (game.js), so this is the only styling in play here. */
.upgrade-row.locked{
  filter: grayscale(0.7) brightness(0.85);
  opacity: 0.6;
}

.upgrade-row.locked .up-buy{
  cursor: not-allowed;
}

.upgrade-row.locked .up-cost{
  font-size: 12px;
  letter-spacing: 0.01em;
}

/* Row-unlock reveal (Task: unlock chain) - a brief mechanical snap: the
   accent floods in and slams back out, like a latch releasing. Fired
   exactly once per LIVE locked->unlocked transition, see
   triggerUnlockReveal() in game.js. Never plays on page load. */
.upgrade-row.unlock-reveal{
  animation: unlockReveal 650ms cubic-bezier(0.2, 0.9, 0.3, 1);
}

@keyframes unlockReveal{
  0%{
    border-color: var(--accent);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 90%, transparent), 0 1px 0 var(--edge-hi) inset;
  }
  35%{
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 55%, transparent), 0 1px 0 var(--edge-hi) inset;
  }
  100%{
    box-shadow: 0 0 0 14px color-mix(in srgb, var(--accent) 0%, transparent), 0 1px 0 var(--edge-hi) inset;
  }
}

#upgrades::-webkit-scrollbar{
  width: 6px;
}
#upgrades::-webkit-scrollbar-thumb{
  background: rgba(255,255,255,0.12);
  border-radius: 1px;
}

/* ---------- wide / desktop layout ---------- */

/* Two-column layout. The second condition is the important one: a phone
   held LANDSCAPE (e.g. 844x390) is under 860px wide, so it used to keep
   the stacked layout - and there the topbar ate 40% of the screen while
   the spinner box collapsed to 234x46, a crushed non-square sliver. Any
   short viewport needs the side-by-side layout, whatever its width. */
@media (min-width: 860px), (min-width: 600px) and (max-height: 560px){
  #app{
    display: grid;
    grid-template-columns: 1fr 380px;
    grid-template-rows: auto 1fr;
    height: 100vh;
  }

  #topbar{
    grid-column: 1 / -1;
    grid-row: 1;
  }

  #spinnerArea{
    grid-column: 1;
    grid-row: 2;
  }

  #upgrades{
    grid-column: 2;
    grid-row: 2;
    max-height: none;
    height: 100%;
    border-top: none;
    border-left: 1px solid var(--seam);
    box-shadow: 1px 0 0 var(--edge-hi) inset;
  }

  #spinnerWrap{
    width: min(46vw, 72vh, 580px);
    height: min(46vw, 72vh, 580px);
    width: min(94cqw, 94cqh, 580px);
    height: min(94cqw, 94cqh, 580px);
    max-width: 100%;
    max-height: 100%;
  }

  /* wide topbar, plenty of room - let the dial read as a deliberate
     instrument rather than a stretched phone widget */
  .rpm-gauge{
    width: clamp(90px, 8vw, 150px);
  }
}

/* ---------------------------------------------------------------------
   Reset button — same housing as mute/lang. Confirming state throws to
   warning-red like a covered stop switch flipped open.
   --------------------------------------------------------------------- */
.reset-btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-width: 44px;
  height: 44px;
  padding: 0 15px 0 13px;
  border-radius: 3px;
  border: 1px solid var(--seam);
  border-top-color: var(--edge-hi-strong);
  background: linear-gradient(180deg, var(--panel-2) 0%, var(--panel) 65%, var(--panel-shadow) 100%);
  color: var(--text-dim);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  box-shadow: 0 2px 3px rgba(0,0,0,0.3);
  transition: background .16s ease, color .16s ease,
              border-color .16s ease, min-width .16s ease, box-shadow .16s ease;
}

.reset-btn .icon-reset{
  width: 20px;
  height: 20px;
  display: block;
}

.reset-btn .reset-label,
.reset-btn .reset-text{
  font-family: "Saira Condensed", sans-serif;
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  white-space: nowrap;
}

.reset-btn .reset-label{ display: block; }
.reset-btn .reset-text{ display: none; }

.reset-btn.confirming{
  background: linear-gradient(180deg, var(--warn-hi) 0%, var(--warn) 60%, #a03c29 100%);
  border-color: #6e2417;
  border-top-color: rgba(255,255,255,0.45);
  color: var(--warn-ink);
  animation: switchThrow 180ms ease-out;
}

.reset-btn.confirming .icon-reset,
.reset-btn.confirming .reset-label{ display: none; }
.reset-btn.confirming .reset-text{ display: block; }

@keyframes switchThrow{
  0%{ transform: scale(1); }
  40%{ transform: scale(0.92); }
  100%{ transform: scale(1); }
}

@media (max-width: 480px){
  .reset-btn{ padding: 0; gap: 0; }
  .reset-btn .reset-label{ display: none; }

  /* Narrowest phones: the dial gives up space, not the buttons (those stay
     40x40 touch targets - see .mute-btn/.lang-btn/.reset-btn). */
  .rpm-gauge{
    width: clamp(46px, 17vw, 90px);
  }
  .gauge-max-label{
    font-size: 8px;
  }
  .gauge-readout{
    font-size: clamp(12px, 4vw, 16px);
  }
}

/* ---------------------------------------------------------------------
   Offline earnings panel — full-screen scrim, a machine readout card.
   ---------------------------------------------------------------------
   NOTE: [hidden] must beat display:flex below or the panel always shows. */
.offline-panel{
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(8, 10, 9, 0.78);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: offlineFade .2s ease both;
}

.offline-panel[hidden]{ display: none; }

@keyframes offlineFade{ from{ opacity: 0 } to{ opacity: 1 } }
@keyframes offlineRise{
  from{ opacity: 0; transform: translateY(12px) scale(.97) }
  to{ opacity: 1; transform: none }
}

.offline-card{
  position: relative;
  width: min(360px, 100%);
  padding: 28px 24px 22px;
  border-radius: 4px;
  border: 1px solid var(--seam);
  border-top-color: var(--edge-hi-strong);
  background: linear-gradient(180deg, var(--panel-2) 0%, var(--bg-alt) 100%);
  box-shadow: 0 24px 60px rgba(0,0,0,0.6), 0 1px 0 var(--edge-hi) inset;
  text-align: center;
  animation: offlineRise .22s cubic-bezier(.2,.8,.3,1) both;
}

/* hazard-stripe cap along the top of the readout card - the signature
   detail's second and last appearance */
.offline-card::before{
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 3px;
  border-radius: 4px 4px 0 0;
  background: repeating-linear-gradient(
    -45deg,
    var(--accent) 0px, var(--accent) 8px,
    #1a1a1a 8px, #1a1a1a 16px
  );
}

.offline-title{
  font-family: "Saira Condensed", sans-serif;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.offline-sub{
  margin-top: 6px;
  font-size: 13px;
  color: var(--text-faint);
}

.offline-amount{
  margin: 16px 0 20px;
  font-family: "IBM Plex Mono", monospace;
  font-size: 38px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
  color: var(--money); /* offline earnings are money too */
  text-shadow: 0 0 22px color-mix(in srgb, var(--money) 30%, transparent);
}

.offline-collect{
  width: 100%;
  height: 46px;
  border: 1px solid #7a5015;
  border-top-color: rgba(255,255,255,0.5);
  border-radius: 2px;
  font-family: "Saira Condensed", sans-serif;
  font-size: 15px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--accent-ink);
  background: linear-gradient(180deg, var(--accent-hi) 0%, var(--accent) 60%, var(--accent-dim) 100%);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  box-shadow: 0 2px 0 #7a5015, 0 3px 5px rgba(0,0,0,0.35);
  transition: filter .15s ease, transform .08s ease, box-shadow .08s ease;
}
.offline-collect:hover{ filter: brightness(1.06); }
.offline-collect:active{
  transform: translateY(2px);
  box-shadow: 0 0 0 #7a5015, 0 1px 1px rgba(0,0,0,0.3);
}

/* Set by game.js around the first applyCoatingTier() call on load, then
   removed ~60ms later. Without it the accent cross-fade runs on every
   launch and the whole UI visibly slides from the default amber to the
   saved tier's colour, which looks like a bug rather than progress. */
:root.no-theme-anim{ transition: none; }


/* ---------------------------------------------------------------------
   Short viewports (landscape phones, small embedded frames).
   Measured at 844x390: the topbar was 157px - 40% of the whole screen -
   because the gauge and the stat block are sized for a tall layout. Here
   everything in the bar shrinks so the spinner keeps the space.
   --------------------------------------------------------------------- */
@media (max-height: 560px){
  #topbar{
    padding-top: max(6px, env(safe-area-inset-top, 6px));
    padding-bottom: 6px;
    gap: 6px;
  }
  .stat-label{ font-size: 10px; }
  .stat-value{ font-size: clamp(16px, 3.4vw, 22px); }
  .rpm-gauge{ width: 78px; height: 52px; }
  .mute-btn, .lang-btn{ width: 36px; height: 36px; min-width: 36px; min-height: 36px; }
  .reset-btn{ height: 36px; }

  /* Keep the box genuinely square. Setting width AND height from viewport
     units does not work here: max-height:100% then clamps only the height
     and the box goes oblong (measured 320x262, and 234x46 before that).
     On a short viewport the spinner cell is reliably WIDER than it is
     tall, so drive the square from the height and let width follow the
     ratio - then it can never exceed the space it has. */
}
