/* =====================================================================
   Day 8 — The Mouth
   Hand-painted signwriting. Flat enamel on a petrol-ink board.

   Two rules the whole sheet obeys, because they are what makes it read as
   paint rather than as a UI kit:

   SHADOWS ARE OFFSETS, NEVER BLURS. A sign painter's drop shadow is a
   second pass of the brush at a fixed offset. Every shadow here is
   `Npx Npx 0`. A blur radius anywhere in this file is a bug.

   ONE RADIUS FAMILY. Surfaces 26px, controls 18px, nothing pill-shaped.
   Mixed corner systems are the fastest way to make painted work look
   like a component library wearing a costume.
   ===================================================================== */

:root {
  --board: #0c1b26;      /* petrol ink, the board itself */
  --board-deep: #071119; /* the rail, one step down */
  --bone: #f4efe4;       /* the balloon, the lettering */
  --ink: #080d10;        /* every keyline and every shadow */
  --chalk: #b9c9ce;      /* secondary lettering on the board */

  /* One enamel per verdict. Nothing else in the build uses these. */
  --jade: #2fd3a0;       /* you won */
  --vermilion: #f0431c;  /* you lost */
  --ochre: #e8a81c;      /* nobody won */

  --mood: var(--chalk);  /* whichever enamel the last round earned */

  --keyline: var(--ink);
  --r-surface: 26px;
  --r-control: 18px;

  --face: 'Archivo', system-ui, sans-serif;
  --sign: 'Bungee', 'Archivo', system-ui, sans-serif;
  --label: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;

  /* Exponential ease-out, from an already-visible default. */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-snap: cubic-bezier(0.2, 0.9, 0.3, 1);
  /* The one overshoot in the build. The brief asks for a pop on win and
     lose, and a verdict that stamps down and settles is that pop. It is
     deliberately not the house easing: everything else decelerates
     smoothly, so the single element that overshoots is the single
     element the eye is meant to land on. */
  --ease-pop: cubic-bezier(0.2, 1.35, 0.35, 1);
}

body[data-mood='win'] { --mood: var(--jade); }
body[data-mood='lose'] { --mood: var(--vermilion); }
body[data-mood='draw'] { --mood: var(--ochre); }

* { box-sizing: border-box; }

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

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--board);
  color: var(--bone);
  font-family: var(--face);
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
}

/* ---------------------------------------------------------------- *
   Board
   A column that fills the viewport exactly once. The balloon is the
   only flexible row, so a short phone costs balloon height and never
   pushes the throw buttons off the bottom edge.
 * ---------------------------------------------------------------- */

.board {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  max-height: 100dvh;
  gap: clamp(0.75rem, 3vw, 1.25rem);
  padding:
    max(0.85rem, env(safe-area-inset-top))
    max(1rem, env(safe-area-inset-right))
    max(1.5rem, env(safe-area-inset-bottom))
    max(1rem, env(safe-area-inset-left));
  max-width: 34rem;
  margin-inline: auto;
}

/* ---------------------------------------------------------------- *
   Score rail
 * ---------------------------------------------------------------- */

.rail {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
}

.tally {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0;
  width: 100%;
  padding: 0.5rem 0.9rem;
  background: var(--board-deep);
  border: 3px solid var(--ink);
  border-radius: var(--r-control);
}

.tally__side {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
}

.tally__side--them { flex-direction: row; }

.tally__gap {
  flex: 1 1 auto;
  height: 3px;
  background: repeating-linear-gradient(
    90deg, var(--chalk) 0 4px, transparent 4px 9px
  );
  opacity: 0.45;
}

.tally__who {
  font-family: var(--label);
  font-size: 0.625rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--chalk);
}

/* Three slots a side. Empty pips are drawn, not hidden, because the
   count of what is still to play for is the information: two filled and
   one empty says "match point" without a word of copy. */
.pips {
  display: flex;
  gap: 0.3rem;
}

.pips i {
  width: 0.85rem;
  height: 0.85rem;
  border: 2px solid var(--chalk);
  border-radius: 3px;
  background: transparent;
  transition: background-color 200ms var(--ease), border-color 200ms var(--ease);
}

.pips--you i.is-on { background: var(--jade); border-color: var(--jade); }
.pips--ai i.is-on { background: var(--vermilion); border-color: var(--vermilion); }

/* Only the pip that just changed animates. Repainting the row every
   round made the whole rail twitch on a draw, when nothing had moved. */
.pips i.is-landing { animation: pip-land 300ms var(--ease-pop); }

@keyframes pip-land {
  from { transform: scale(2.1); }
  to { transform: scale(1); }
}

.rail__say {
  margin: 0;
  font-family: var(--label);
  font-size: 0.6875rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--chalk);
  text-align: center;
}

.board.is-over .rail__say { color: var(--mood); }

/* One step quieter than the line above it, and deliberately left out of
   the mood colouring: this counts the whole sitting, so it should not
   flash the enamel of a single round. Fixed height, so the balloon — the
   only flexible row on the board — absorbs it once and never reflows. */
.rail__rate {
  margin: 0;
  font-family: var(--label);
  font-size: 0.625rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--chalk);
  opacity: 0.55;
  text-align: center;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- *
   Stage: balloon over clash strip
 * ---------------------------------------------------------------- */

.stage {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(1.5rem, 6vw, 2.2rem);
}

/* --- the balloon ------------------------------------------------- */

.balloon {
  position: relative;
  flex: 0 1 auto;
  min-height: 8.5rem;
  max-height: 44vh;
  display: flex;
}

.balloon__skin {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(1rem, 5vw, 1.75rem);
  background: var(--bone);
  border: 4px solid var(--ink);
  border-radius: var(--r-surface);
  box-shadow: 7px 7px 0 var(--ink);
  transform-origin: 22% 100%;
  position: relative;
  z-index: 1;
}

/* The tail, drawn rather than clipped. It sits behind the balloon skin so
   the skin's own border and background hide the polygon's top edge, which
   is what makes the tail read as part of the same painted shape instead
   of as a triangle parked underneath one. */
.balloon__tail {
  position: absolute;
  z-index: -1;
  top: calc(100% - 12px);
  left: clamp(1.6rem, 9vw, 3rem);
  width: 4.1rem;
  height: 3.1rem;
  overflow: visible;
}

.balloon__tail .tail-shadow { fill: var(--ink); }

.balloon__tail .tail-face {
  fill: var(--bone);
  stroke: var(--ink);
  stroke-width: 5;
  stroke-linejoin: round;
}

.talk {
  margin: 0;
  color: var(--ink);
  font-weight: 900;
  font-size: clamp(1.3rem, 5.9vw, 2.15rem);
  line-height: 1.1;
  letter-spacing: -0.022em;
  text-align: center;
  text-wrap: balance;
  overflow-wrap: break-word;
}

.talk[hidden] { display: none; }

/* Three painted dots. The AI is never silent while it thinks, because a
   blank balloon for a second and a half reads as broken. */
.dots {
  display: flex;
  gap: 0.6rem;
}

.dots[hidden] { display: none; }

.dots i {
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 50%;
  background: var(--ink);
  animation: blip 900ms infinite var(--ease-snap);
}

.dots i:nth-child(2) { animation-delay: 140ms; }
.dots i:nth-child(3) { animation-delay: 280ms; }

@keyframes blip {
  0%, 65%, 100% { transform: translateY(0); opacity: 0.3; }
  32% { transform: translateY(-8px); opacity: 1; }
}

/* The balloon grows out of its own tail, which points at the hands that
   just clashed. The line arrives from where the round happened. */
.balloon.is-saying .balloon__skin { animation: say 280ms var(--ease); }

@keyframes say {
  from { transform: scale(0.93) translateY(7px); }
  to { transform: scale(1) translateY(0); }
}

/* --- the clash strip --------------------------------------------- */

.clash {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-areas:
    'you ai'
    'verdict verdict';
  align-items: center;
  column-gap: clamp(0.6rem, 4vw, 1.4rem);
  row-gap: 0.5rem;
}

.fist {
  width: clamp(78px, 27vw, 124px);
  aspect-ratio: 1;
  color: var(--bone);
  opacity: 0;
}

.fist--you { grid-area: you; justify-self: end; }
.fist--ai { grid-area: ai; justify-self: start; transform: scaleX(-1); }

.clash.is-live .fist { opacity: 1; }

.fist svg { width: 100%; height: 100%; overflow: visible; }

/* Painted in two passes. The first lays a fat ink stroke plus ink fill,
   which welds the separate slabs into one silhouette and leaves a keyline
   round the outside. The second drops the fill colour on top with no
   stroke at all. Stroking the slabs individually would print every
   internal seam, which is what a single-pass version looks like and why
   it reads as clip art instead of as a painted hand. */
.fist .keyline,
.throw__art .keyline {
  fill: var(--keyline);
  stroke: var(--keyline);
  stroke-width: 12;
  stroke-linejoin: round;
}

.fist .body,
.throw__art .body {
  fill: currentColor;
  stroke: none;
}

/* Three quick pumps, then the throw. This is the ritual the game is
   actually made of: nobody plays rock paper scissors without counting
   first, and taking the count away makes the result feel arbitrary. */
.clash.is-counting .fist { animation: pump 150ms 3 var(--ease-snap); }

@keyframes pump {
  0%, 100% { transform: translateY(0); }
  45% { transform: translateY(-17px); }
}

.clash.is-counting .fist--ai { animation-name: pump-flipped; }

@keyframes pump-flipped {
  0%, 100% { transform: scaleX(-1) translateY(0); }
  45% { transform: scaleX(-1) translateY(-17px); }
}

.clash.is-throwing .fist--you { animation: slam-in 190ms var(--ease-snap); }
.clash.is-throwing .fist--ai { animation: slam-in-flipped 190ms var(--ease-snap); }

@keyframes slam-in {
  from { transform: translateX(-52%); }
  to { transform: translateX(0); }
}

@keyframes slam-in-flipped {
  from { transform: scaleX(-1) translateX(-52%); }
  to { transform: scaleX(-1) translateX(0); }
}

.verdict {
  grid-area: verdict;
  margin: 0;
  min-height: 1.6em;
  text-align: center;
  font-family: var(--sign);
  font-size: clamp(1.5rem, 8.4vw, 2.75rem);
  line-height: 1.15;
  color: var(--mood);
  text-shadow: 4px 4px 0 var(--ink);
  opacity: 0;
}

.verdict.is-stamped { animation: stamp 300ms var(--ease-pop) forwards; }

@keyframes stamp {
  from { transform: scale(1.32); opacity: 0; }
  55% { opacity: 1; }
  to { transform: scale(1); opacity: 1; }
}

/* ---------------------------------------------------------------- *
   Throw buttons
   High-frequency controls: pressed dozens of times a session, so the
   feedback is a physical push and nothing else. No entrance animation,
   no hover flourish, nothing that costs the player a frame.
 * ---------------------------------------------------------------- */

.throws {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(0.5rem, 2.5vw, 0.85rem);
}

.throw {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-height: 5.25rem;
  padding: 0.7rem 0.35rem 0.6rem;
  color: var(--bone);
  background: var(--bone);
  border: 3px solid var(--ink);
  border-radius: var(--r-control);
  box-shadow: 5px 5px 0 var(--ink);
  font-family: var(--sign);
  font-size: 0.8125rem;
  cursor: pointer;
  touch-action: manipulation;
  transition:
    transform 130ms var(--ease-snap),
    box-shadow 130ms var(--ease-snap),
    opacity 180ms var(--ease);
}

.throw__art { width: clamp(30px, 9vw, 42px); aspect-ratio: 1; }
.throw__art svg { width: 100%; height: 100%; overflow: visible; }
.throw__name {
  color: var(--ink);
  letter-spacing: 0.02em;
}

/* Pressed into the board: the button travels along its own shadow and
   the shadow shortens by the same amount. */
.throw:active:not(:disabled) {
  transform: translate(4px, 4px);
  box-shadow: 1px 1px 0 var(--ink);
}

.throw:focus-visible {
  outline: 3px solid var(--jade);
  outline-offset: 4px;
}

.throw:disabled {
  opacity: 0.7;
  cursor: default;
}

/* Picking and winning are two different states, half a second apart.
   Colouring the tapped button with the mood immediately meant it went
   chalk on tap and then vermilion on impact, a two-step colour change
   inside one gesture that read as a glitch. So the tap only presses the
   button in, and the enamel arrives with the verdict. */
.throw.is-picked {
  transform: translate(4px, 4px);
  box-shadow: 1px 1px 0 var(--ink);
  opacity: 1;
}

.throw.is-chosen {
  background: var(--mood);
  opacity: 1;
}

/* ---------------------------------------------------------------- *
   Match over

   The closing line gets a treatment the round jabs never get: the
   balloon stops being bone and fills with the verdict's own enamel, the
   keyline thickens, and the type steps up. A round result changes one
   word and three colours; a match result changes the object itself, so
   there is no reading it as just another round.

   Only jade and vermilion can land here, never ochre, because a draw
   cannot end a match. Ink on both clears AA comfortably (10.2:1 and
   5.1:1), which is why the enamel can take the whole panel rather than
   just an edge.
 * ---------------------------------------------------------------- */

.board.is-over .balloon__skin {
  background: var(--mood);
  border-width: 6px;
  box-shadow: 9px 9px 0 var(--ink);
}

.board.is-over .balloon__tail .tail-face {
  fill: var(--mood);
  stroke-width: 7;
}

.board.is-over .talk {
  font-size: clamp(1.45rem, 6.6vw, 2.4rem);
  letter-spacing: -0.03em;
}

.board.is-over .verdict {
  font-size: clamp(1.75rem, 9.6vw, 3.25rem);
}

/* Takes the throw row's place rather than sitting beside it, so the one
   thing left to do is the only thing on screen. */
.rematch {
  flex: 0 0 auto;
  min-height: 5.25rem;
  padding: 0.9rem 1rem;
  color: var(--ink);
  background: var(--mood);
  border: 3px solid var(--ink);
  border-radius: var(--r-control);
  box-shadow: 5px 5px 0 var(--ink);
  font-family: var(--sign);
  font-size: 1.0625rem;
  letter-spacing: 0.02em;
  cursor: pointer;
  touch-action: manipulation;
  transition:
    transform 130ms var(--ease-snap),
    box-shadow 130ms var(--ease-snap);
}

.rematch[hidden] { display: none; }

.rematch:active {
  transform: translate(4px, 4px);
  box-shadow: 1px 1px 0 var(--ink);
}

.rematch:focus-visible {
  outline: 3px solid var(--bone);
  outline-offset: 4px;
}

.throws[hidden] { display: none; }

/* ---------------------------------------------------------------- *
   Full-bleed enamel flash on impact
 * ---------------------------------------------------------------- */

.flash {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: var(--mood);
  opacity: 0;
  pointer-events: none;
}

.flash.is-firing { animation: flash 440ms var(--ease); }

@keyframes flash {
  0% { opacity: 0; }
  12% { opacity: 0.68; }
  100% { opacity: 0; }
}

/* The match ending flashes twice and holds longer. It is the only moment
   in the build that gets a second beat, which is what makes it read as
   an ending rather than as a louder round. */
.board.is-over ~ .flash.is-firing { animation: flash-match 900ms var(--ease); }

@keyframes flash-match {
  0% { opacity: 0; }
  8% { opacity: 0.86; }
  34% { opacity: 0.12; }
  46% { opacity: 0.7; }
  100% { opacity: 0; }
}

/* ---------------------------------------------------------------- *
   Painted grain. Fixed and pointer-inert, so it never repaints on
   scroll and never intercepts a tap.
 * ---------------------------------------------------------------- */

.grain {
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;
  opacity: 0.16;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------------------------------------------------------------- *
   Day mark — same face, size, tracking and corner on all thirty days.
 * ---------------------------------------------------------------- */

.day {
  position: fixed;
  z-index: 55;
  right: max(0.9rem, env(safe-area-inset-right));
  bottom: max(0.5rem, env(safe-area-inset-bottom));
  margin: 0;
  font-family: var(--label);
  font-size: 0.625rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--chalk);
  opacity: 0.65;
  pointer-events: none;
}

/* ---------------------------------------------------------------- *
   Short viewports: landscape phones, and any phone with the browser
   chrome expanded. The balloon gives up height first, then the hands.
 * ---------------------------------------------------------------- */

@media (max-height: 620px) {
  .board {
    gap: 0.5rem;
    padding-top: max(0.5rem, env(safe-area-inset-top));
    padding-bottom: max(0.7rem, env(safe-area-inset-bottom));
  }
  .rail { gap: 0.2rem; }
  .tally { padding: 0.35rem 0.75rem; }
  .tally__n { font-size: 1.1rem; }
  .stage { gap: 0.8rem; }
  .balloon { min-height: 3.25rem; max-height: 38vh; }
  .balloon__skin { padding: 0.65rem 0.9rem; }
  .balloon__tail { width: 2.6rem; height: 2rem; top: calc(100% - 9px); }
  .talk { font-size: clamp(1rem, 3.6vw, 1.45rem); }
  .dots i { width: 0.6rem; height: 0.6rem; }
  .fist { width: clamp(46px, 13vw, 74px); }
  .verdict { font-size: clamp(1.1rem, 4.2vw, 1.75rem); min-height: 1.35em; }
  .throw { min-height: 3.4rem; gap: 0.2rem; padding: 0.4rem 0.3rem; }
  .rematch { min-height: 3.4rem; padding: 0.5rem 0.8rem; font-size: 0.9375rem; }
  .pips i { width: 0.7rem; height: 0.7rem; }
  .throw__art { width: clamp(22px, 4.6vw, 30px); }
  .throw { font-size: 0.75rem; }

  /* The match-over type has to be stepped down here too. `.board.is-over
     .talk` outranks a bare `.talk` inside this block, so the closing line
     kept its full desktop scale on a landscape phone and overflowed the
     balloon. Same cascade trap as the reduced-motion shake: a media query
     buys no specificity, so an override has to match or beat the selector
     it is overriding, not merely come later. */
  .board.is-over .talk { font-size: clamp(0.95rem, 3.4vw, 1.35rem); }
  .board.is-over .verdict { font-size: clamp(1.3rem, 5.2vw, 2.1rem); }
  .board.is-over .balloon__skin { border-width: 4px; box-shadow: 6px 6px 0 var(--ink); }

  /* At match end on a short viewport the hands give up room to the
     closing line. They have already done their job: the thrown pair is
     what the round was about, and the last line is what the match was
     about. Shrunk rather than hidden, so the deciding throw is still
     readable. */
  .board.is-over .fist { width: clamp(34px, 9vw, 48px); }
  .board.is-over .balloon { min-height: 5rem; }
}

@media (min-width: 34rem) {
  .throw { font-size: 0.9375rem; }
}

/* ---------------------------------------------------------------- *
   Impact shake.

   Declared BEFORE the reduced-motion block on purpose. It used to sit
   after it, with a comment claiming that made the override win, which is
   backwards: at equal specificity the later rule wins, so the media
   query was losing and the whole board still shook for anyone who had
   asked it not to. A full-screen shake is exactly the vestibular
   trigger that setting exists to suppress.
 * ---------------------------------------------------------------- */

.board.is-hit { animation: shake 260ms cubic-bezier(0.36, 0.07, 0.19, 0.97); }

@keyframes shake {
  0% { transform: translateX(0); }
  15% { transform: translateX(-8px); }
  35% { transform: translateX(7px); }
  55% { transform: translateX(-5px); }
  75% { transform: translateX(3px); }
  100% { transform: translateX(0); }
}

/* ---------------------------------------------------------------- *
   Reduced motion.
   Motion is the personality here, not the mechanism, so all of it goes
   and none of the game does. The count, the slam, the shake, the stamp
   overshoot and the blinking dots are replaced by state that is simply
   true: the hands are shown, the verdict is shown, the mood colour is
   shown. A player who has asked for stillness still learns everything a
   player who has not does.
 * ---------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .tally__n.is-bumped,
  .clash.is-counting .fist,
  .clash.is-throwing .fist--you,
  .clash.is-throwing .fist--ai,
  .balloon.is-saying .balloon__skin,
  .dots i {
    animation: none;
  }

  .dots i { opacity: 0.55; }

  .pips i.is-landing { animation: none; }

  .board.is-over ~ .flash.is-firing { animation: flash-quiet 260ms linear; }

  .verdict.is-stamped {
    animation: none;
    opacity: 1;
  }

  /* A tint that arrives and leaves rather than a punch. */
  .flash.is-firing { animation: flash-quiet 260ms linear; }

  @keyframes flash-quiet {
    0%, 100% { opacity: 0; }
    40% { opacity: 0.16; }
  }

  .throw { transition: opacity 120ms linear; }
  .throw:active:not(:disabled) { transform: none; box-shadow: 5px 5px 0 var(--ink); }

  .board.is-hit { animation: none; }
}
