/* Tokens lifted from javiercarnicer.com/assets/styles.css so the two sites read
   as one brand.
   
   The web fonts used to be the exception: this file said "minus the web fonts"
   and fell back to system-ui, which is why the arcade never actually looked
   like the site it belongs to — same colours, completely different voice. The
   main site self-hosts its faces now, so they can simply be copied in and
   served from this domain, which keeps the strict `font-src 'self'` this
   project's _headers is built around. */
/* ── Dark is the GAME's palette, and it stays that way ──
   The world inside Infinity Drive is lit, and a light chrome around a lit 3D
   scene fights it. These are the tokens the game uses and they are unchanged.
   The landing page overrides them — see arcade-home.css — because the landing
   page is a page on this brand's site and should look like one. */
:root {
  --bg: #0a0a0c;
  --text: #ECEAE3;
  --muted: #9b9b92;
  --dim: #6a6a63;
  --accent: #ff7a2f;
  --border: rgba(255, 255, 255, 0.09);
  --serif: 'Bricolage Grotesque', 'Bricolage Fallback', Arial, sans-serif;
  --sans: 'Space Grotesk', 'Grotesk Fallback', system-ui, -apple-system, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', SFMono-Regular, Menlo, monospace;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

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

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  /* The whole page is a viewport onto the world; nothing here ever scrolls,
     and on iOS this is also what stops a drag on the canvas bouncing the page. */
  overflow: hidden;
  overscroll-behavior: none;
}

body { -webkit-text-size-adjust: 100%; }

#scene {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* Steering is a drag gesture, so the browser must not claim it as a scroll. */
  touch-action: none;
}

/* ── Loader ─────────────────────────────────────────────────────────────── */

.loader {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  background: var(--bg);
  transition: opacity 0.6s var(--ease);
}

/* Faded out rather than removed, so the world is already rendering behind it. */
.loader.is-done { opacity: 0; pointer-events: none; }

.loader-inner { width: min(420px, 82vw); text-align: center; }

.loader-kicker {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 0.6rem;
}

.loader-title {
  font-size: clamp(2.4rem, 9vw, 3.6rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1;
  margin-bottom: 1.6rem;
}

.loader-bar {
  height: 2px;
  background: var(--border);
  border-radius: 2px;
  overflow: hidden;
}

.loader-bar span {
  display: block;
  height: 100%;
  width: 0%;
  background: var(--accent);
  transition: width 0.3s var(--ease);
}

.loader-status {
  margin-top: 0.9rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--muted);
  min-height: 1.2em;
}

.loader-status.is-error { color: var(--accent); }

/* ── Fallbacks ──────────────────────────────────────────────────────────── */

.fallback {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  align-content: center;
  justify-items: start;
  gap: 0.9rem;
  padding: 2rem clamp(1.5rem, 8vw, 6rem);
  background: var(--bg);
}

.fallback[hidden] { display: none; }
.fallback h1 { font-size: clamp(2rem, 7vw, 3rem); font-weight: 600; letter-spacing: -0.02em; }
.fallback p { max-width: 46ch; color: var(--muted); line-height: 1.6; }
.fallback a { color: var(--accent); }

/* ── Chrome ─────────────────────────────────────────────────────────────── */

.home-link {
  position: fixed;
  z-index: 10;
  left: max(1rem, env(safe-area-inset-left));
  bottom: max(1rem, env(safe-area-inset-bottom));
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--dim);
  text-decoration: none;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: rgba(10, 10, 12, 0.5);
  backdrop-filter: blur(6px);
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}

.home-link:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.2); }

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.01ms !important; }
}

/* ── Menu ──────────────────────────────────────────────────────────────────
   Top-left corner. The minimap moves down to sit under it — the home link owns
   bottom-left, reverse owns bottom-right and respawn owns top-right, so this is
   the last free spot and the two share it vertically. */

.menu {
  position: fixed;
  z-index: 19;
  top: max(0.9rem, env(safe-area-inset-top));
  left: max(0.9rem, env(safe-area-inset-left));
}

.menu-toggle {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: rgba(10, 10, 12, 0.55);
  backdrop-filter: blur(6px);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}

.menu-toggle:hover { border-color: rgba(255, 255, 255, 0.24); }
.menu.is-open .menu-toggle { border-color: var(--accent); }

.menu-bars { display: grid; gap: 4px; }
.menu-bars i {
  display: block;
  width: 17px;
  height: 1.5px;
  background: var(--text);
  transition: transform 0.2s var(--ease), opacity 0.2s var(--ease);
}
/* Into a cross when open — the button is its own close button. */
.menu.is-open .menu-bars i:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.menu.is-open .menu-bars i:nth-child(2) { opacity: 0; }
.menu.is-open .menu-bars i:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

.menu-panel {
  position: absolute;
  top: 50px;
  left: 0;
  width: min(21rem, calc(100vw - 2rem));
  padding: 1rem 1.1rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: rgba(10, 10, 12, 0.9);
  backdrop-filter: blur(10px);
  /* Taller than a short phone in landscape, so it scrolls rather than running
     off the bottom of the screen. */
  max-height: calc(100vh - 5rem);
  overflow-y: auto;
}

.menu-panel[hidden] { display: none; }

.menu-heading {
  font-family: var(--mono);
  font-size: 0.64rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 0.5rem;
}

.menu-heading + .menu-row, .menu-table + .menu-heading { margin-top: 1rem; }

.menu-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.74rem;
}

.menu-table th, .menu-table td {
  text-align: left;
  padding: 0.28rem 0.4rem 0.28rem 0;
  font-weight: 400;
  vertical-align: top;
}

.menu-table thead th {
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.35rem;
}

.menu-table tbody th { color: var(--text); }
.menu-table tbody td { color: var(--muted); font-family: var(--mono); font-size: 0.68rem; }

/* A control that exists but has not been bought. Struck through rather than
   hidden: a key you cannot use yet is a reason to go and find a garage, and a
   key you have never heard of is not. */
/* Dimmed with COLOUR, not opacity. Opacity on the row would take the orange
   "locked" caption down with it, and that caption is the one part that has to
   stay legible. */
.menu-table tbody tr.is-locked th { color: rgba(255, 255, 255, 0.4); }
.menu-table tbody tr.is-locked td { opacity: 0.38; text-decoration: line-through; }
/* A caption under the action rather than a suffix beside it. The action column
   is about eighty pixels wide, so "Boost · locked" was always going to wrap —
   better to put the break where it was meant to go than to let it fall there. */
.menu-table tbody tr.is-locked th::after {
  content: 'locked';
  display: block;
  margin-top: 1px;
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #ff7a2f;
  opacity: 1;
  text-decoration: none;
}

.menu-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.4rem 0;
  font-size: 0.8rem;
  cursor: pointer;
  /* A comfortable target without making the panel enormous. */
  min-height: 34px;
}

.menu-row input { accent-color: var(--accent); width: 16px; height: 16px; cursor: pointer; }

.menu-note {
  margin-top: 0.8rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 0.62rem;
  line-height: 1.5;
  color: var(--dim);
}

/* ── Minimap ───────────────────────────────────────────────────────────────
   Under the menu button, not beside it. */

.minimap {
  position: fixed;
  z-index: 12;
  top: calc(max(0.9rem, env(safe-area-inset-top)) + 52px);
  left: max(0.9rem, env(safe-area-inset-left));
  /* Never takes input — a drag started here has to reach the world. */
  pointer-events: none;
  border-radius: 50%;
  backdrop-filter: blur(4px);
}

/* ── Condition ─────────────────────────────────────────────────────────────
   Bottom-centre on a wide screen, above the home link on a narrow one. Small
   and quiet: it is a driving folio first, and a bar that shouts would change
   what the page feels like it is. */

.health {
  position: fixed;
  z-index: 13;
  left: 50%;
  transform: translateX(-50%);
  bottom: max(1rem, env(safe-area-inset-bottom));
  width: min(220px, 42vw);
  pointer-events: none;
  text-align: center;
}

.health[hidden] { display: none; }

.health-track {
  height: 5px;
  border-radius: 3px;
  background: rgba(10, 10, 12, 0.55);
  border: 1px solid var(--border);
  overflow: hidden;
  backdrop-filter: blur(4px);
}

.health-fill {
  height: 100%;
  width: 100%;
  background: var(--accent);
  /* Eased, so a hit reads as a drop rather than a jump — you can see how much
     it cost even if you were looking at the road. */
  transition: width 0.25s var(--ease), background 0.3s var(--ease);
}

.health.is-low .health-fill { background: #e0453a; }

/* Replayable on consecutive hits — see the reflow in HealthBar.damage(). */
.health.is-hit .health-track { animation: health-knock 0.28s var(--ease); }

@keyframes health-knock {
  0%   { transform: translateX(0); }
  25%  { transform: translateX(-3px); }
  60%  { transform: translateX(3px); }
  100% { transform: translateX(0); }
}

.health-label {
  display: block;
  margin-top: 0.3rem;
  font-family: var(--mono);
  font-size: 0.55rem;
  letter-spacing: 0.16em;
  color: var(--dim);
}

/* The home link owns the bottom-left corner; on a narrow screen the bar lifts
   clear of it rather than overlapping. */
@media (max-width: 34rem) {
  .health { bottom: max(3.6rem, calc(env(safe-area-inset-bottom) + 3rem)); }
}

/* ── Project card ──────────────────────────────────────────────────────────
   Opens when you stop inside a project's ring. Anchored bottom-centre on a
   phone and bottom-left on a wide screen, so it never lands where the drag
   hand is and never covers the car. */

.board-panel {
  position: fixed;
  z-index: 18;
  left: 50%;
  bottom: max(1.2rem, env(safe-area-inset-bottom));
  transform: translateX(-50%);
  width: min(30rem, calc(100vw - 2rem));
  /* Only the card itself takes clicks; the rest of the strip must not steal
     drags meant for the world. */
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s var(--ease), translate 0.25s var(--ease);
  translate: 0 12px;
}

.board-panel[hidden] { display: none; }
.board-panel.is-open { opacity: 1; translate: 0 0; }

@media (min-width: 60rem) {
  .board-panel {
    left: max(1.5rem, env(safe-area-inset-left));
    transform: none;
  }
}

.board-card {
  pointer-events: auto;
  padding: 1.25rem 1.4rem 1.1rem;
  border: 1px solid var(--border);
  border-left: 2px solid var(--accent);
  border-radius: 12px;
  background: rgba(10, 10, 12, 0.82);
  backdrop-filter: blur(10px);
}

.board-year {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim);
}

.board-title {
  margin-top: 0.35rem;
  font-size: 1.4rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
}

.board-subtitle {
  margin-top: 0.4rem;
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--muted);
}

.board-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1rem;
}

.board-visit {
  font-family: var(--mono);
  font-size: 0.76rem;
  color: var(--bg);
  background: var(--accent);
  text-decoration: none;
  padding: 0.55rem 0.85rem;
  border-radius: 8px;
  /* Full touch target even though it reads as a small chip. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.board-close {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.75rem;
  min-height: 44px;
}

.board-close:hover { color: var(--text); }

.board-hint {
  margin-top: 0.7rem;
  font-family: var(--mono);
  font-size: 0.64rem;
  color: var(--dim);
}

/* ── Touch controls ────────────────────────────────────────────────────────
   There is no joystick and no buttons. Steering, throttle, boost, jump and horn
   are all one drag started anywhere on screen — see State/TouchControls.js for why,
   and for how folio-2019 does the same with invisible regions.

   The only other control is an invisible reverse region in the bottom-RIGHT.
   The bottom-left is reserved: that is where the "← javiercarnicer.com" link
   lives, and the old joystick sat directly on top of it. */

.touch-controls {
  position: fixed;
  inset: 0;
  z-index: 15;
  /* The container must not swallow drags meant for the world; only the buttons
     take input, and the drag is read from the window. */
  pointer-events: none;
}

@media (pointer: fine) {
  /* A mouse gets the keyboard. Hidden rather than not built, so a laptop with a
     touchscreen still has them if it reports coarse. */
  .touch-controls { display: none; }
}

.touch-controls button {
  pointer-events: auto;
  /* Stops the long-press callout, the double-tap zoom and the blue tap flash,
     all three of which fire constantly while driving. */
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  user-select: none;
  touch-action: none;
}

/* The drag indicator. Present only while a thumb is down and past the dead
   zone, so nothing is ever drawn over the world at rest. Both marks are
   positioned from the viewport origin and moved entirely by transform, which
   keeps them off the layout path at 60fps. */

.touch-drag {
  position: fixed;
  inset: 0;
  pointer-events: none;
}

.touch-drag[hidden] { display: none; }

/* Both marks are moved by `transform: translate(clientX, clientY)`, which puts
   their TOP-LEFT on the thumb. The negative margin is what centres them, and it
   has to be half the size — done here in CSS so the JS only ever writes the raw
   pointer coordinates and never has to know how big these are. */
.touch-drag-origin,
.touch-drag-cursor {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 50%;
}

.touch-drag-origin {
  width: 52px;
  height: 52px;
  margin: -26px 0 0 -26px;
  border: 1.5px solid rgba(255, 255, 255, 0.28);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18);
}

.touch-drag-cursor {
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  background: rgba(255, 122, 47, 0.85);
  box-shadow: 0 0 24px rgba(255, 122, 47, 0.5);
}

.touch-drag.is-boosting .touch-drag-cursor {
  background: #fff;
  box-shadow: 0 0 32px rgba(255, 255, 255, 0.8);
}

/* Quiet, but present. These started invisible, the way folio-2019's controls
   are, and reverse then could not be found at all — which is the same as not
   having reverse. Big touch targets, small labels, low contrast: they read as
   part of the HUD rather than as buttons over the world, and the bottom-left
   still belongs to the home link. */
.touch-zone {
  position: absolute;
  display: grid;
  place-items: center;
  width: 96px;
  height: 68px;
  padding: 0;
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  color: var(--text);
  background: rgba(10, 10, 12, 0.34);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 12px;
  opacity: 0.5;
  backdrop-filter: blur(4px);
  transition: opacity 0.12s var(--ease), background 0.12s var(--ease),
              border-color 0.12s var(--ease), transform 0.12s var(--ease);
}

.touch-zone:active {
  opacity: 1;
  background: rgba(255, 122, 47, 0.3);
  border-color: var(--accent);
  transform: scale(0.96);
}

/* Reverse, under the thumb that is already resting there. */
.touch-zone-reverse {
  right: max(0.9rem, env(safe-area-inset-right));
  bottom: max(0.9rem, env(safe-area-inset-bottom));
}

/* Respawn. Diagonally opposite the drag hand and HELD rather than tapped, so it
   takes deliberate effort to throw the car back to spawn. Smaller, because it
   is wanted far less often than reverse. */
.touch-zone-reset {
  right: max(0.9rem, env(safe-area-inset-right));
  top: max(0.9rem, env(safe-area-inset-top));
  width: 84px;
  height: 44px;
  font-size: 0.5rem;
  opacity: 0.36;
}

/* ── The coin count ─────────────────────────────────────────────────────────
   Top right, opposite the menu button, and out of the way of the minimap. */
.purse {
  position: fixed;
  top: 18px;
  right: 18px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  padding: 7px 13px 8px 12px;
  border-radius: 14px;
  background: rgba(10, 10, 12, 0.55);
  /* The backdrop blur is what stops a dark pill reading as a hole punched in a
     bright sky. Cheap, and the fallback is simply the flat panel above. */
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  border: 1px solid rgba(255, 255, 255, 0.09);
  font: 600 15px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: #ffd873;
  letter-spacing: 0.04em;
  pointer-events: none;
  user-select: none;
}

/* Drawn rather than an image: it is a circle. */
.purse-coins {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}

.purse-coin {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #fff0bd 0%, #ffc63b 45%, #d08d12 100%);
  box-shadow: 0 0 8px rgba(255, 198, 59, 0.45);
}

/* ── The level strip ────────────────────────────────────────────────────────

   Under the coins, and quieter than them on purpose. Coins change every few
   seconds and want to be read at a glance; a level changes a few times an hour
   and only wants to be FINDABLE — so it gets the small type, the hairline rule
   above it and none of the gold.                                              */

/* Only the first of the two rows takes the rule, so the pair reads as one
   block rather than as two separate readouts stacked up. */
.purse-level.is-driver {
  padding-top: 6px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.purse-level {
  display: grid;
  grid-template-columns: auto auto 1fr;
  align-items: center;
  gap: 7px;
  padding-top: 5px;
  font: 500 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.42);
}

/* Two rows: who you are, and what you are driving. The label is set in the
   muted colour and the number in the accent, so the digit is the thing the eye
   lands on rather than the word in front of it. */
.purse-tag {
  min-width: 68px;
  max-width: 116px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.purse-lv-num {
  font-weight: 700;
  font-size: 12px;
  color: #ff7a2f;
}

.purse-bar {
  display: block;
  height: 3px;
  min-width: 52px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
}

.purse-bar i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: #ff7a2f;
  transition: width 320ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* An award, floating off the strip and gone in a second and a half. Absolute so
   it cannot reflow the pill it is leaving — a purse that changes width every
   time it pays out reads as a layout bug. */
.purse-note {
  position: absolute;
  right: 13px;
  top: 100%;
  margin-top: 4px;
  white-space: nowrap;
  font: 700 12px/1 var(--mono);
  letter-spacing: 0.08em;
  color: #ff7a2f;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.8);
  pointer-events: none;
  animation: purse-note 1500ms var(--ease) forwards;
}

@keyframes purse-note {
  0% { opacity: 0; transform: translateY(4px); }
  14% { opacity: 1; transform: translateY(0); }
  62% { opacity: 1; transform: translateY(-2px); }
  100% { opacity: 0; transform: translateY(-14px); }
}

@media (prefers-reduced-motion: reduce) {
  .purse-note { animation: purse-note-still 1500ms steps(1) forwards; }
  @keyframes purse-note-still {
    0%, 88% { opacity: 1; }
    100% { opacity: 0; }
  }
}

.purse-car {
  /* Wide enough for the longest name in VEHICLES rather than a round number.
     "The Orange One" clipped to "THE ORANGE O…" read as a bug in the layout. */
  max-width: 132px;
  letter-spacing: 0.06em;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* The one moment the strip is allowed to be loud. Nothing takes the screen and
   nothing makes a noise — this fires mid-corner as often as not. */
.purse.is-levelled {
  animation: purse-levelled 1400ms ease-out;
}

.purse.is-levelled .purse-lv-num {
  animation: purse-lv-pop 1400ms cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes purse-levelled {
  0%, 100% { box-shadow: none; }
  15% { box-shadow: 0 0 0 2px rgba(255, 122, 47, 0.55), 0 0 26px rgba(255, 122, 47, 0.35); }
  70% { box-shadow: 0 0 0 1px rgba(255, 122, 47, 0.2); }
}

@keyframes purse-lv-pop {
  0% { transform: scale(1); }
  20% { transform: scale(1.9); color: #ffd0a8; }
  100% { transform: scale(1); }
}

/* One kick per coin. See Purse.render() for the reflow that makes it repeat. */
.purse.is-hit .purse-count { animation: purse-kick 240ms ease-out; }

@keyframes purse-kick {
  0% { transform: scale(1); }
  35% { transform: scale(1.28); color: #fff6d5; }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .purse.is-hit .purse-count,
  .purse.is-levelled,
  .purse.is-levelled .purse-lv-num { animation: none; }
  .purse-bar i { transition: none; }
}

/* ── The garage ─────────────────────────────────────────────────────────────

   Designed as a WORKSHOP DOCKET, not a store.

   The obvious build is a centred modal of rounded cards with a price badge on
   each, and it is obvious because everything ships it. It is also wrong here:
   this is a garage in a world where the sky, the terrain and the fog are all
   hand-written shaders, and a soft consumer card list belongs to a different
   product entirely.

   So: a job sheet clipped to the right-hand edge. Hairline rules instead of
   card borders, an index number down the left the way a parts list has, prices
   set in tabular monospace and right-aligned like an invoice, and the car you
   are driving marked by a hard orange rule rather than a coloured pill. The
   corner is notched, because technical drawings are.                          */

.shop {
  position: fixed;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  z-index: 20;
  width: min(370px, calc(100vw - 24px));
  padding: 22px 22px 18px;
  color: #e7e5e0;
  font: 400 13.5px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif;
  pointer-events: auto;

  background:
    /* A faint scanline, which is the whole of the texture. Anything heavier
       fights the world behind it. */
    repeating-linear-gradient(
      to bottom,
      rgba(255, 255, 255, 0.016) 0px,
      rgba(255, 255, 255, 0.016) 1px,
      transparent 1px,
      transparent 3px
    ),
    linear-gradient(160deg, rgba(20, 19, 18, 0.95), rgba(11, 11, 12, 0.97));
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  border-left: 1px solid rgba(255, 122, 47, 0.28);
  box-shadow: -28px 0 70px rgba(0, 0, 0, 0.6);

  /* The notch. A cut corner reads as a drawing rather than a dialog, and it is
     the one shape here that no card component would have. */
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 18px), calc(100% - 18px) 100%, 0 100%);

  animation: garage-in 260ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}

.shop[hidden] { display: none; }

@keyframes garage-in {
  from { opacity: 0; transform: translate(22px, -50%); }
  to   { opacity: 1; transform: translate(0, -50%); }
}

/* ── Header ──────────────────────────────────────────────────────────────── */

.shop-head {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  padding-bottom: 13px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.09);
}

.shop-kicker {
  display: block;
  font: 500 9.5px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: #ff7a2f;
  margin-bottom: 7px;
}

.shop-head h2 {
  margin: 0;
  font: 600 20px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #f2f0ec;
}

.shop-close {
  appearance: none;
  background: none;
  border: 0;
  padding: 2px 0 0 12px;
  color: #6f6d69;
  font: 400 18px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  cursor: pointer;
  transition: color 140ms ease;
}
.shop-close:hover { color: #ff7a2f; }

/* ── Balance, as a readout rather than a badge ───────────────────────────── */

.shop-purse {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin: 13px 0 18px;
  padding-bottom: 12px;
  border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
}

.shop-purse-label {
  font: 500 9.5px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #6f6d69;
}

.shop-purse-value {
  font: 600 19px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  color: #ffd873;
  letter-spacing: 0.02em;
}

/* ── The list, as a parts index ─────────────────────────────────────────── */

.shop-list { list-style: none; margin: 0; padding: 0; }

.shop-item {
  position: relative;
  display: grid;
  grid-template-columns: 26px 1fr auto;
  align-items: center;
  gap: 0 12px;
  padding: 12px 0 12px 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.055);
}
.shop-item:last-child { border-bottom: 0; }

/* The marker rule. Transparent by default, wipes in on hover, solid on the one
   you are driving — the same idea a job sheet uses to tick a line. */
.shop-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  bottom: 8px;
  width: 2px;
  background: #ff7a2f;
  transform: scaleY(0);
  transform-origin: 50% 0;
  transition: transform 180ms cubic-bezier(0.2, 0.9, 0.3, 1);
}
.shop-item:hover::before { transform: scaleY(1); }
.shop-item.is-current::before { transform: scaleY(1); }

.shop-index {
  font: 500 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  color: #5a5854;
  letter-spacing: 0.04em;
}
.shop-item.is-current .shop-index { color: #ff7a2f; }

.shop-name {
  display: block;
  font-weight: 500;
  color: #eae8e3;
  letter-spacing: 0.005em;
}
.shop-item.is-locked .shop-name { color: #a5a29c; }

.shop-note {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  line-height: 1.35;
  color: #767470;
}

/* ── Action, set as a price rather than dressed as a button ─────────────── */

.shop-buy {
  appearance: none;
  cursor: pointer;
  background: none;
  border: 0;
  padding: 6px 0 6px 14px;
  font: 600 12.5px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #ff7a2f;
  white-space: nowrap;
  transition: color 140ms ease, opacity 140ms ease;
}

/* Owned but not fitted: the neutral action. */
.shop-buy[data-state="fit"] { color: #cfccc6; }
.shop-buy[data-state="fit"]:hover { color: #ffffff; }

/* Currently driving: a stamp, not a control. */
.shop-buy[data-state="fitted"] {
  color: #ff7a2f;
  cursor: default;
  opacity: 0.85;
}

.shop-buy[data-state="buy"] .shop-coin { opacity: 1; }
.shop-buy:hover:not(:disabled) { color: #ffb37a; }

/* The coin, drawn. It is a circle. */
.shop-coin {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: 6px;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #fff0bd, #ffc63b 46%, #c9860f);
  vertical-align: 0;
}

/* ── Footer ─────────────────────────────────────────────────────────────── */

.shop-hint {
  margin: 16px 0 0;
  padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.055);
  font: 400 11.5px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: #625f5b;
  letter-spacing: 0.02em;
}

/* Cannot afford it. The balance kicks red and the row's rule flashes — no error
   text, because the number is right there and a sentence explaining it would be
   worse than the shake. */
.shop.is-short .shop-purse-value {
  animation: shop-refuse 340ms cubic-bezier(0.36, 0.07, 0.19, 0.97);
}

@keyframes shop-refuse {
  0%, 100% { transform: translateX(0); color: #ffd873; }
  20%      { transform: translateX(-6px); color: #ff6a53; }
  60%      { transform: translateX(5px);  color: #ff6a53; }
}

/* Narrow screens: a bottom sheet, since a right-hand docket on a phone would
   cover the road you are looking at. */
@media (max-width: 560px) {
  .shop {
    right: 0;
    left: 0;
    top: auto;
    bottom: 0;
    transform: none;
    width: auto;
    border-left: 0;
    border-top: 1px solid rgba(255, 122, 47, 0.28);
    box-shadow: 0 -22px 60px rgba(0, 0, 0, 0.6);
    clip-path: polygon(0 18px, 18px 0, 100% 0, 100% 100%, 0 100%);
    animation-name: garage-in-sheet;
  }
  @keyframes garage-in-sheet {
    from { opacity: 0; transform: translateY(18px); }
    to   { opacity: 1; transform: translateY(0); }
  }
}

@media (prefers-reduced-motion: reduce) {
  .shop, .shop-item::before, .shop.is-short .shop-purse-value { animation: none; transition: none; }
}

/* ── Shop sections ──────────────────────────────────────────────────────── */

.shop-section {
  margin: 20px 0 2px;
  font: 500 9.5px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: #ff7a2f;
}
.shop-list + .shop-section { margin-top: 22px; }
.shop-section-note {
  margin: 0 0 10px;
  font: 400 11.5px/1.4 ui-sans-serif, system-ui, sans-serif;
  color: #75757e;
}

/* The list gets long once there are ten cars in it, so it scrolls inside the
   panel rather than growing past the top of the screen. */
.shop-list {
  max-height: min(38vh, 320px);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* A hairline instead of a scrollbar track, so it does not read as a widget. */
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

/* ── Paint, as swatches rather than another list ────────────────────────── */

.shop-paints {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 7px;
}

.shop-swatch {
  appearance: none;
  cursor: pointer;
  width: 100%;
  padding: 8px 9px 7px;
  border-radius: 8px;
  text-align: left;
  /* The colour IS the control. A named swatch with a colour dot beside it is a
     form field; a swatch you read the name off is a paint chip. */
  background: linear-gradient(150deg, var(--swatch), var(--swatch-dark));
  border: 1px solid rgba(255, 255, 255, 0.13);
  color: #fff;
  display: grid;
  gap: 3px;
  /* Dark paints need light text and light paints need dark; one shadow reads
     on both and costs nothing. */
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
  transition: transform 130ms cubic-bezier(0.2, 0.9, 0.3, 1), border-color 130ms ease;
}
.shop-swatch:hover { transform: translateY(-2px); border-color: rgba(255, 255, 255, 0.3); }

.shop-swatch.is-worn {
  border-color: #ff7a2f;
  box-shadow: 0 0 0 1px rgba(255, 122, 47, 0.5);
}

.shop-swatch-name {
  font: 600 11.5px/1.15 ui-sans-serif, system-ui, sans-serif;
  letter-spacing: 0.01em;
}
.shop-swatch-price {
  font: 600 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.82;
}

/* ── Upgrades ───────────────────────────────────────────────────────────── */

/* The level bar. A thin rule rather than a rounded progress widget — this
   panel is a job sheet and a job sheet does not have progress bars on it. */
.shop-xp {
  height: 2px;
  margin: 4px 0 12px;
  background: rgba(255, 255, 255, 0.09);
  overflow: hidden;
}
.shop-xp span {
  display: block;
  height: 100%;
  background: #ff7a2f;
  transition: width 320ms cubic-bezier(0.2, 0.9, 0.3, 1);
}

.shop-perks { list-style: none; margin: 0; padding: 0; }

.shop-perk {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 0 12px;
  padding: 9px 0 9px 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  position: relative;
}
.shop-perk:last-child { border-bottom: 0; }

/* Fitted perks carry the same orange rule the fitted car does, so "this is
   yours" means one thing throughout the panel. */
.shop-perk.is-have::before {
  content: "";
  position: absolute;
  left: 0; top: 8px; bottom: 8px;
  width: 2px;
  background: #ff7a2f;
}

.shop-perk.is-locked { opacity: 0.42; }
.shop-perk.is-poor .shop-buy { color: #6f6d69; }

/* ── Tabs ───────────────────────────────────────────────────────────────────
   Three sections at once overflowed the screen. One at a time, switched by a
   row of labels rather than by chunky pill buttons — a job sheet has tabs cut
   into it, not toggles bolted on. */

.shop-tabs {
  display: flex;
  gap: 18px;
  margin: 14px 0 4px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.09);
}

.shop-tab {
  appearance: none;
  background: none;
  border: 0;
  padding: 0 0 9px;
  cursor: pointer;
  font: 500 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: #6f6d69;
  position: relative;
  transition: color 140ms ease;
}
.shop-tab:hover { color: #cfccc6; }

.shop-tab.is-on { color: #f2f0ec; }
/* The underline sits ON the section rule, so the active tab reads as cut into
   it rather than sitting above it. */
.shop-tab.is-on::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: #ff7a2f;
}

/* The panel can never grow past the screen. Whichever section is showing
   scrolls inside it. */
.shop { max-height: min(78vh, 640px); overflow-y: auto; overscroll-behavior: contain; }
.shop-list { max-height: none; overflow: visible; }

/* ── The race clock ─────────────────────────────────────────────────────────

   Top centre, and hidden entirely when no race is on. The four corners are
   already taken — menu, minimap, purse, condition — and a fifth position for a
   clock is a clock you have to look away from the road to find.

   Set as an instrument rather than a card: no panel behind it, a hard shadow so
   it survives a bright sky, and the digits large enough to read at the edge of
   vision without ever being read on purpose.                                  */

.race {
  position: fixed;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  pointer-events: none;
  user-select: none;
  text-align: center;
}

/* `display: flex` above beats the user-agent's `[hidden] { display: none }`, so
   without this the clock sits on screen at 0:00.0 for the whole game. The same
   line exists on .menu-panel and .fallback for the same reason. */
.race[hidden] { display: none; }

.race-clock {
  display: flex;
  align-items: baseline;
  gap: 9px;
}

.race-time {
  font: 700 30px/1 var(--mono);
  letter-spacing: -0.01em;
  color: #eafff6;
  /* No panel behind it, so the shadow is what keeps it legible over a bright
     sky as well as over a dark hillside. */
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.75), 0 0 3px rgba(0, 0, 0, 0.6);
  /* Stops the whole line jittering left and right as the digits change. */
  font-variant-numeric: tabular-nums;
}

.race-delta {
  font: 600 14px/1 var(--mono);
  font-variant-numeric: tabular-nums;
  color: #63ffc4;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.8);
}

.race-delta.is-behind { color: #ff7a2f; }

/* On the way out of the run. The whole clock goes amber rather than just the
   delta, because the point is to be noticed from the corner of an eye that is
   busy driving. */
.race.is-straying .race-time { color: #ffc98a; }
.race.is-straying .race-delta { animation: race-stray 900ms ease-in-out infinite; }

@keyframes race-stray {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.42; }
}

@media (prefers-reduced-motion: reduce) {
  .race.is-straying .race-delta { animation: none; }
}

.race-meta {
  display: flex;
  gap: 14px;
  font: 500 10px/1 var(--mono);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.62);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.85);
}

/* The verdict. One line, centred, gone in three seconds — the only part of a
   race anyone reads twice, so it gets the screen and then gives it back. */
.race-note {
  position: fixed;
  top: 34%;
  left: 50%;
  z-index: 6;
  padding: 0 1em;
  font: 700 clamp(20px, 4vw, 38px)/1.2 var(--mono);
  letter-spacing: 0.02em;
  text-align: center;
  color: #eafff6;
  text-shadow: 0 3px 20px rgba(0, 0, 0, 0.8);
  pointer-events: none;
  user-select: none;
  white-space: pre-line;
  animation: race-note 3s var(--ease) forwards;
}

.race-note.is-finish { color: #63ffc4; }
.race-note.is-lost { color: var(--muted); }

@keyframes race-note {
  0% { opacity: 0; transform: translate(-50%, 14px) scale(0.94); }
  12% { opacity: 1; transform: translate(-50%, 0) scale(1); }
  75% { opacity: 1; transform: translate(-50%, 0) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -10px) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .race-note { animation: race-note-still 3s steps(1) forwards; }
  @keyframes race-note-still {
    0%, 92% { opacity: 1; transform: translate(-50%, 0); }
    100% { opacity: 0; transform: translate(-50%, 0); }
  }
}

/* ── The job board ──────────────────────────────────────────────────────────

   Docketed like the rest of the garage: hairline rules, no cards, the number
   set in tabular monospace. A job is a line item on a work sheet, not a quest
   scroll — the difference is the whole reason this panel does not look like
   every other in-game shop.                                                   */

.shop-jobs { list-style: none; }

.shop-job {
  display: grid;
  gap: 5px;
  padding: 0.7rem 0;
  border-bottom: 1px solid var(--border);
}

.shop-job:last-child { border-bottom: 0; }

.shop-job-bar {
  display: block;
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.1);
  overflow: hidden;
}

.shop-job-bar i {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width 0.35s var(--ease);
}

.shop-job-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.66rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  color: var(--dim);
}

/* The pay sits on the right in the same colour as a price elsewhere in the
   panel, so "what this is worth" reads the same wherever you meet it. */
.shop-job-foot > span:last-child { color: var(--muted); }
.shop-job-foot .shop-coin { margin-right: 0.3em; }

/* The way back out, now that the game is one room off the arcade rather than
   the whole building. A class rather than a style attribute: `style-src
   'self'` blocks inline styles, and that is deliberate — see _headers. */
.loader-kicker .loader-back { color: inherit; text-decoration: none; }
.loader-kicker .loader-back:hover { color: var(--accent); }
