/* Base styles from the look-locked design-handoff. Per-element styling lives
   inline in app.js (matching the handoff), so this file sets the page canvas,
   the app shell, and a couple of interaction affordances.

   MOBILE FIRST. The handoff was drawn as a 382px phone screen and the first
   build reproduced that literally — a fixed-width phone DRAWING, bezel and all,
   floating in the middle of a desktop page. On an actual phone that renders a
   382px box inside the screen with a fake bezel around it. Here the phone is
   the frame: the app fills the viewport, and only on a wide screen does it
   become a centred card. */

html, body { margin: 0; padding: 0; }

body {
  min-height: 100vh;
  min-height: 100dvh;
  font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
  -webkit-text-size-adjust: 100%;
  transition: background .25s ease;
}
body.theme-light { background: #F5F3EC; }
body.theme-dark  { background: #15140F; }

/* The app shell. Full-bleed on a phone. */
.frame {
  width: 100%;
  max-width: 460px;
  margin: 0 auto;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  /* Respect the notch and the home indicator when installed to a home screen. */
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
  box-sizing: border-box;
}
.frame.light { background: #F5F3EC; }
.frame.dark  { background: #15140F; }

/* DESKTOP SCALE.
   Every size in this app is an absolute px tuned for a phone, where the screen
   is small but the pixel density is high. Shown at 1:1 on a monitor the same
   design is a postage stamp: a 460px card marooned in a 1440px window with 10px
   labels that are genuinely hard to read. Scaling the whole frame keeps the
   proportions the design was drawn at and simply makes it bigger — the phone is
   still the shape of the product, it just stops being a stamp.

   Height is in the condition because a scaled frame needs somewhere to go: on a
   short laptop screen a bigger card would only scroll more. */
:root { --fz: 1; }
@media (min-width: 900px) and (min-height: 720px)  { :root { --fz: 1.2; } }
@media (min-width: 1200px) and (min-height: 860px) { :root { --fz: 1.4; } }
@media (min-width: 1600px) and (min-height: 1000px) { :root { --fz: 1.6; } }

/* While the map is beside it, the phone stops growing before the map becomes
   too small to read. Below this width the two compete for the same pixels and
   the map loses first — its labels are 11-15px in an 880-unit viewBox, so it
   needs roughly 530px on screen before the words go to mush. */
@media (min-width: 900px) and (max-width: 1319px) and (min-height: 560px) {
  :root { --fz: 1; }
}

/* COMPACT MAP.
   Below 1320px the full diagram cannot be readable beside the phone — its
   labels would land around 7px. Rather than hide it (which is what a plain
   breakpoint does, silently, in exactly the windows most people actually use)
   the rail is dropped and the viewBox reframed onto the spine, so the walk
   itself renders large. app.js sets the viewBox; this decides what is in it. */
@media (max-width: 1319px) {
  #map .rail { display: none; }
}

/* Every pixel the gutters give back is a pixel of readable diagram — but ONLY
   where the map is actually beside the phone. Scoped by max-width alone this
   put 20px of padding on phones and shrank the frame from 375 to 335, which is
   precisely the mobile layout that was not supposed to move. */
@media (min-width: 900px) and (max-width: 1319px) {
  #stage { gap: 28px; }
  body { padding: 28px 20px; }
}

/* On anything wider than a phone, sit it in the middle as a card. The dark
   bezel is decoration here, where it costs nothing — never on a phone. */
@media (min-width: 560px) {
  body {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 32px 0;
  }
  body.theme-light { background: #E4E2DC; }
  body.theme-dark  { background: #0C0B08; }

  .frame {
    zoom: var(--fz);
    /* An explicit width, not 100% + max-width: as a flex item inside a zoomed
       coordinate system the percentage resolves short and the card silently
       comes out narrower than it was drawn. flex: none stops it shrinking too. */
    width: 460px;
    flex: none;
    /* A PHONE-SHAPED phone. This was min-height:0, so the card was only as tall
       as whatever the screen happened to render — a stubby letterbox on a
       screen with room to spare. It now takes the height it is given, capped so
       it does not stretch into a ribbon on a tall monitor, and short content
       simply leaves the lower part of the phone empty, which is what a phone
       looks like. The viewport budget is in real pixels, so it is divided back
       down into the frame's own scaled units or the card overflows the window. */
    height: min(calc((100dvh - 64px) / var(--fz)), 940px);
    /* A phone does not show a scrollbar down its screen. The wheel still
       scrolls; the bezel keeps looking like a bezel. */
    scrollbar-width: none;
    min-height: 0;
    overflow-y: auto;
    border: 11px solid #15140f;
    border-radius: 46px;
    box-shadow: 0 26px 60px rgba(0, 0, 0, .26), 0 6px 16px rgba(0, 0, 0, .12);
  }
  .frame.dark {
    box-shadow: 0 26px 60px rgba(0, 0, 0, .4), 0 6px 16px rgba(0, 0, 0, .2);
  }
}

button { font-family: inherit; }
[data-clickable] { cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
[data-clickable]:active { transform: translateY(1px); }

/* Every tap target clears the 44px minimum on touch, without disturbing the
   look-locked spacing on a pointer device. */
@media (hover: none) {
  button[data-clickable] { min-height: 44px; }
}

:focus-visible { outline: 2px solid oklch(0.62 0.16 50); outline-offset: 2px; border-radius: 6px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}

/* --- number pad -------------------------------------------------------------
   Logging a count is the most repeated action in the product. It was a
   window.prompt() dialog, which on a phone is a grey system box with a text
   field — wrong for a number an agent enters after every call block. */
.scrim {
  position: fixed;
  inset: 0;
  background: rgba(12, 11, 8, .42);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease;
  z-index: 20;
}
.scrim.open { opacity: 1; pointer-events: auto; }

/* The sheet is a sibling of the frame, not a child, so it does not inherit the
   desktop scale — it is sized to match instead. Zooming it would fight with the
   fixed positioning, since left/bottom would then resolve in scaled units. */
.sheet {
  position: fixed;
  left: 50%;
  bottom: 0;
  width: min(calc(460px * var(--fz)), 100%);
  transform: translate(-50%, 100%);
  background: #FBFAF6;
  border-radius: 20px 20px 0 0;
  padding: calc(8px * var(--fz)) calc(16px * var(--fz)) max(calc(18px * var(--fz)), env(safe-area-inset-bottom));
  transition: transform .26s cubic-bezier(.32, .72, 0, 1);
  z-index: 21;
  box-shadow: 0 -10px 40px rgba(0, 0, 0, .22);
  box-sizing: border-box;
}
.sheet.open { transform: translate(-50%, 0); }

.grabber { width: calc(38px * var(--fz)); height: calc(4px * var(--fz)); border-radius: 3px; background: #E1DCCF; margin: calc(6px * var(--fz)) auto calc(14px * var(--fz)); }
.sheet-name { text-align: center; font: 600 calc(13px * var(--fz)) 'Space Grotesk'; color: #1B1A15; }
.readout {
  text-align: center;
  margin: calc(14px * var(--fz)) 0 calc(4px * var(--fz));
  font: 700 calc(44px * var(--fz)) 'JetBrains Mono', monospace;
  color: #1B1A15;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.readout span { color: #B0AB9E; font-size: calc(22px * var(--fz)); font-weight: 500; }
.sheet-sub { text-align: center; font: 500 calc(11px * var(--fz)) 'Space Grotesk'; color: #8B867B; min-height: calc(16px * var(--fz)); }

.quick { display: flex; gap: calc(8px * var(--fz)); margin: calc(16px * var(--fz)) 0 calc(10px * var(--fz)); }
.quick button {
  flex: 1;
  min-height: calc(46px * var(--fz));
  border-radius: 12px;
  border: 1px solid #DAD4C6;
  background: #fff;
  font: 600 calc(14px * var(--fz)) 'JetBrains Mono', monospace;
  color: #1B1A15;
  cursor: pointer;
}
.quick button:active { background: #F0ECE1; }

.keys { display: grid; grid-template-columns: repeat(3, 1fr); gap: calc(8px * var(--fz)); }
.keys button {
  min-height: calc(52px * var(--fz));
  border: none;
  border-radius: 12px;
  background: #F0ECE1;
  font: 600 calc(19px * var(--fz)) 'JetBrains Mono', monospace;
  color: #1B1A15;
  cursor: pointer;
}
.keys button:active { background: #E1DCCF; }
.keys button.wide { font-family: 'Space Grotesk', sans-serif; font-size: calc(14px * var(--fz)); }

.sheet-save {
  width: 100%;
  min-height: calc(52px * var(--fz));
  margin-top: calc(10px * var(--fz));
  border: none;
  border-radius: 14px;
  background: #1B1A15;
  color: #F5F3EC;
  font: 600 calc(15px * var(--fz)) 'Space Grotesk';
  cursor: pointer;
}

/* --- THE MAP -----------------------------------------------------------------
   On a desktop the phone sits beside a map of the whole product and the screen
   you are on lights up. It is an orientation aid for a screen with room to
   spare, so it appears only where that room exists and is display:none
   everywhere else — #stage is a plain block on a phone, which is why nothing
   about the mobile layout changes.

   Colours are variables because the app flips the page ground to near-black on
   the two evening screens; the map has to stay legible when it does. */
#map { display: none; }

@media (min-width: 900px) and (min-height: 560px) {
  #stage {
    display: flex;
    /* TOP, NOT CENTRE. The map is far taller than the phone — 1438px against
       836px — so centring pushed the phone 333px down the page and put its
       bottom half below the fold of an ordinary laptop. The first thing you saw
       on opening the app was the middle of a diagram. */
    align-items: flex-start;
    justify-content: center;
    gap: 44px;
    /* Without an explicit width the stage is shrink-to-fit, so the map's
       flex-grow has no free space to claim and it renders at its basis. */
    width: 100%;
    max-width: 1500px;
  }
  body { padding: 32px 30px; }
  #map {
    display: block;
    flex: 1 1 400px;
    min-width: 0;      /* a flex item must be allowed to shrink below its content */
    max-width: 720px;
  }
  /* The phone rides along. Reading down a map two screens tall while the thing
     it describes scrolls away is the wrong way round — the app is the subject,
     the map is the annotation. Sticky keeps the subject on screen.

     ON #root, NOT .frame. A sticky element can only slide within its own
     containing block, and .frame's is #root, which is exactly as tall as the
     frame — zero room, so it simply scrolled away. #root is the flex item, and
     its containing block is the full-height stage. */
  #root {
    position: sticky;
    top: 32px;
  }
}

#map {
  --m-ink: #1B1A15;
  --m-sub: #8B867B;
  --m-card: #FBFAF6;
  --m-rule: #C9C3B4;
  --m-accent: oklch(0.62 0.16 50);
}
body.theme-dark #map {
  --m-ink: #ECE7DC;
  --m-sub: #8B867A;
  --m-card: #1E1C16;
  --m-rule: #3A362B;
  --m-accent: oklch(0.72 0.15 52);
}
#map svg { display: block; width: 100%; height: auto; }

#map .box { fill: var(--m-card); stroke: var(--m-rule); stroke-width: 1.5; }
#map .box-hot { fill: var(--m-accent); stroke: none; }
#map .box-dash { stroke-dasharray: 4 3; }

#map .m-title  { text-anchor: middle; font: 700 15px 'Space Grotesk'; fill: var(--m-ink); }
#map .m-title2 { text-anchor: middle; font: 600 13.5px 'Space Grotesk'; fill: var(--m-ink); }
#map .m-sub    { text-anchor: middle; font: 500 11px 'Space Grotesk'; fill: var(--m-sub); }
#map .m-on-hot { fill: #fff; }
#map .m-eyebrow { font: 600 11px 'JetBrains Mono'; letter-spacing: .14em; fill: var(--m-sub); }
#map .m-note   { font: 500 11.5px 'Space Grotesk'; fill: var(--m-sub); }
#map .m-note-hot { fill: var(--m-accent); font-weight: 600; }
#map .m-foot   { font: 500 11px 'Space Grotesk'; fill: var(--m-sub); opacity: .8; }

#map .m-link { stroke: var(--m-rule); stroke-width: 1.5; fill: none; }
#map .m-link.m-hot { stroke: var(--m-accent); stroke-width: 2; }
#map .m-link.m-faint { opacity: .55; }
#map .m-loop { stroke-dasharray: 5 4; }

/* The indicator. Everything recedes and the current screen alone stays lit —
   a highlight nobody can miss beats a subtle outline nobody notices. */
#map .node { transition: opacity .3s ease, filter .3s ease; }
#map.has-current .node { opacity: .32; }
#map.has-current .node.on { opacity: 1; }
#map .node.on .box { stroke: var(--m-accent); stroke-width: 2.5; }
#map .node.on { filter: drop-shadow(0 0 9px color-mix(in oklab, var(--m-accent) 55%, transparent)); }

@media (prefers-reduced-motion: reduce) {
  #map .node { transition: none; }
}

@media (min-width: 560px) {
  .frame::-webkit-scrollbar { width: 0; height: 0; }
}

/* --- MAP MODES ---------------------------------------------------------------
   FOLLOW is a picture of where you are. JUMP makes the same picture the
   navigation. The ground changes between them because a mode you cannot see you
   are in is a mode people toggle by accident and then fight. */
#map-modes {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 14px 2px;
  flex-wrap: wrap;
}
#map-modes button {
  border: 1px solid var(--m-rule);
  background: var(--m-card);
  color: var(--m-sub);
  border-radius: 8px;
  padding: 6px 10px;
  font: 600 9px 'Space Grotesk', sans-serif;
  letter-spacing: .07em;
  line-height: 1;
  cursor: pointer;
  transition: background .2s ease, color .2s ease, border-color .2s ease;
}
#map-modes button[aria-pressed="true"] {
  background: var(--m-accent);
  border-color: var(--m-accent);
  color: #fff;
}
#map-note {
  font: 500 9.5px 'Space Grotesk', sans-serif;
  color: var(--m-sub);
  opacity: 0;
  transition: opacity .2s ease;
}
#map-note.show { opacity: 1; }

/* JUMP: the ground goes dark, the map lights up, every pill is a door. */
/* Scoped to where the map exists. mapMode persists in localStorage, so an
   agent who chose JUMP at a desk would otherwise carry a dark ground to their
   phone, where there is no map and no way to turn it off. */
@media (min-width: 900px) and (min-height: 560px) {
  body.map-jump { background: #14130E; }
}
body.map-jump #map {
  --m-ink: #ECE7DC;
  --m-sub: #948E80;
  --m-card: #201E17;
  --m-rule: #3A362B;
  --m-accent: oklch(0.74 0.15 52);
}
body.map-jump #map.has-current .node { opacity: .82; }
body.map-jump #map .node { cursor: pointer; }
body.map-jump #map .node .box { stroke-width: 1.5; }
body.map-jump #map .node:hover { opacity: 1; }
body.map-jump #map .node:hover .box {
  stroke: var(--m-accent);
  stroke-width: 2.5;
}
body.map-jump #map .node:hover { filter: drop-shadow(0 0 10px color-mix(in oklab, var(--m-accent) 60%, transparent)); }
body.map-jump #map .node:focus-visible .box { stroke: var(--m-accent); stroke-width: 2.5; }
/* The screen you are on still reads as current, even in JUMP. */
body.map-jump #map .node.on .box { stroke: var(--m-accent); stroke-width: 2.5; }

@media (prefers-reduced-motion: reduce) {
  #map-modes button, #map-note { transition: none; }
}

/* --- WHAT EACH SCREEN IS FOR ------------------------------------------------
   The explanation used to sit in a block of cards under the diagram. It arrives
   beside the pill now, so the answer appears where the question is asked. */
#map { position: relative; }

#map-card {
  position: absolute;
  z-index: 5;
  width: 240px;
  padding: 12px 13px;
  border: 1px solid var(--m-rule);
  border-radius: 12px;
  background: var(--m-card);
  box-shadow: 0 12px 30px rgba(0, 0, 0, .14), 0 3px 8px rgba(0, 0, 0, .07);
  opacity: 0;
  /* Not display:none — the card has to be measurable before it is placed, or it
     lands at the wrong coordinates on the first hover of every session. */
  pointer-events: none;
  transition: opacity .16s ease;
  transform: translateY(2px);
}
#map-card.show { opacity: 1; transform: none; }

.map-card-title {
  font: 700 13px 'Space Grotesk', sans-serif;
  color: var(--m-accent);
  margin-bottom: 5px;
}
.map-card-body {
  font: 500 11px/1.55 'Space Grotesk', sans-serif;
  color: var(--m-sub);
}

body.map-jump #map-card {
  box-shadow: 0 12px 34px rgba(0, 0, 0, .5);
}

@media (prefers-reduced-motion: reduce) {
  #map-card { transition: none; }
}

/* --- PACE -------------------------------------------------------------------
   The mascot. The sheets ship as motion-design concepts with a checkerboard
   painted on instead of an alpha channel; tools/extract-pace.js flood-fills it
   away and writes one horizontal strip per animation.

   SMOOTH, NOT PIXELATED. This looked like pixel art and was first rendered
   with image-rendering:pixelated. Counting the pixels says otherwise: one
   sheet carries 23,298 distinct colours across 45,193 opaque pixels, which is
   continuous rendered shading, near enough a unique colour per pixel. And the
   sprite is drawn smaller than it is stored — 132px of art in a 96px box — so
   pixelated does not preserve crisp edges here, it drops every third row and
   aliases the gradients. The default resampling is the correct one. */
.pace {
  display: block;
  background-repeat: no-repeat;
  background-position: 0 0;
  flex: none;
}

.pace-row {
  display: flex;
  align-items: flex-end;
  gap: 12px;
}

/* What Pace is saying. A speech line rather than a caption, because a mascot
   that stands there silently is decoration and this one is meant to be read. */
.pace-say {
  flex: 1;
  min-width: 0;
  position: relative;
  padding: 11px 13px;
  border-radius: 13px 13px 13px 3px;
  background: #FBFAF6;
  border: 1px solid #E3DECF;
  font: 500 11.5px/1.5 'Space Grotesk', sans-serif;
  color: #2A2820;
}
.pace-say b { font-weight: 700; color: #1B1A15; }

.pace-say-loud {
  background: oklch(0.62 0.16 50);
  border-color: oklch(0.62 0.16 50);
  color: #fff;
}
.pace-say-loud b { color: #fff; }

@media (prefers-reduced-motion: reduce) {
  /* Still shown, just still. The driver checks this too and holds frame one. */
  .pace { animation: none !important; }
}

/* --- THE HERO ---------------------------------------------------------------
   The day's number, at the size the day's number deserves. Mono, because these
   are figures to be read exactly, and because tabular digits do not reflow when
   20 becomes 19. Tight leading and a negative tracking pull the numerals into a
   single mass rather than a row of separate glyphs. */
.hero {
  position: relative;
  overflow: hidden;
  padding: 17px 18px 16px;
  border-radius: 17px;
  background: #1B1A15;
  color: #F5F3EC;
}

.hero-cap {
  font: 600 8.5px 'Space Grotesk';
  letter-spacing: .14em;
  color: rgba(245, 243, 236, .55);
}

.hero-num {
  margin: 2px 0 -2px;
  font: 700 76px/.92 'JetBrains Mono';
  letter-spacing: -.045em;
  font-variant-numeric: tabular-nums;
  color: #F5F3EC;
}

.hero-sub {
  font: 500 11.5px/1.45 'Space Grotesk';
  color: rgba(245, 243, 236, .62);
}

/* The rail is the only thing on the screen that fills, so it is the only thing
   that animates its width. Everything else would just be movement. */
.hero-rail {
  margin-top: 13px;
  height: 5px;
  border-radius: 3px;
  background: rgba(245, 243, 236, .14);
  overflow: hidden;
}

.hero-rail i {
  display: block;
  height: 100%;
  border-radius: 3px;
  background: var(--hero-fill, oklch(0.62 0.16 50));
  transition: width .85s cubic-bezier(.2, .9, .25, 1);
}

/* Met is a different state, not a brighter version of the same one. */
.hero-met { background: oklch(0.62 0.16 50); }
.hero-met .hero-cap,
.hero-met .hero-sub { color: rgba(255, 255, 255, .72); }
.hero-met { --hero-fill: #fff; }
.hero-met .hero-rail { background: rgba(255, 255, 255, .26); }

/* --- ARRIVAL ----------------------------------------------------------------
   Screens replace #root wholesale, so without this every screen change is an
   instantaneous swap — legible, but it reads as a page reload rather than a
   move within one app. A short rise, staggered down the screen, gives the eye
   an order to read in. It is deliberately quick: this is a tool somebody opens
   twenty times a day, and an animation you wait for is one you come to hate. */
@keyframes rise {
  from { opacity: 0; transform: translateY(9px); }
  to   { opacity: 1; transform: none; }
}

.frame > * {
  animation: rise .34s cubic-bezier(.2, .9, .25, 1) both;
}

.frame > *:nth-child(1) { animation-delay: 0ms; }
.frame > *:nth-child(2) { animation-delay: 28ms; }
.frame > *:nth-child(3) { animation-delay: 56ms; }
.frame > *:nth-child(4) { animation-delay: 84ms; }
.frame > *:nth-child(5) { animation-delay: 105ms; }
.frame > *:nth-child(n+6) { animation-delay: 120ms; }

@media (prefers-reduced-motion: reduce) {
  .frame > * { animation: none; }
  .hero-rail i { transition: none; }
}
