@layer components {
  /* ---- The falling word ------------------------------------------------
     The single most important element in the game, and it is deliberately not a
     component in the usual sense: no box, no border, no background. Type on
     paper, falling. The moment this grows an outline it becomes Word Race's
     letter tile, and the moment it grows a bevel it becomes Number Clash's
     key face.

     Depth comes from `--y` in [0, 1], set per frame by ui/Lane.js, resolved
     against the sky's height with container query units. That keeps the whole
     fall in the compositor: one transform per word per frame, no layout, no
     paint. Using `top` here instead would reflow the lane sixty times a
     second. */
  .word {
    position: absolute;
    top: 0;
    left: calc(var(--x, 0.5) * 100%);
    font-family: var(--font-fall);
    font-size: var(--text-fall);
    font-weight: 400;
    line-height: 1;
    color: var(--ink);
    white-space: nowrap;
    letter-spacing: var(--track-fall);
    /* The travel is the sky minus one line, so `--y` of 1 puts the word's BOTTOM on
       the floor rather than its top. Without the subtraction a word is entirely
       below the floor line at the instant the simulation calls it missed, so the one
       moment the player most needs to see is the one moment nothing is drawn.
       `1em` is exactly the line box here because line-height is 1. */
    transform: translate(-50%, calc(var(--y, 0) * (100cqh - 1em)));
    will-change: transform;
  }

  /* The prefix you have already typed. Fades rather than highlights, so the word
     visibly erases itself under your fingers and what remains is what you still
     owe. Highlighting the typed part instead would draw the eye backwards, to
     the half you are finished with. */
  .word__done {
    color: var(--ink-faint);
  }

  /* Characters typed wrong and not yet taken back. Type "alones" at "along" and the
     "es" sits here until backspace removes it — the word cannot be completed until it
     is gone.

     Red, underlined, and the loudest thing on the page. This is the one place the design
     abandons its own restraint, deliberately: a mistake the player must notice and undo
     is worthless if it is tasteful enough to miss. The underline carries it for anyone
     who cannot distinguish the colour. */
  .word__wrong {
    color: var(--error);
    text-decoration: underline 2px var(--error);
    text-underline-offset: 0.18em;
  }

  /* The whole word while it is blocked, so the error is legible from peripheral vision
     as well — a player watching the bottom of their lane should not have to read the
     glyphs to know they are stuck. */
  .word--fumbling .word__done,
  .word--fumbling .word__rest {
    opacity: 0.55;
  }

  /* Aimed at. A hairline underline in your own colour and nothing else — no
     glow, no scale, no box. The lock has to be unmistakable without becoming
     the loudest thing in the lane. */
  .word--locked {
    text-decoration: underline 2px var(--lane-ink);
    text-underline-offset: 0.28em;
  }

  .lane--rival .word {
    font-size: var(--text-fall-rival);
    color: var(--ink-soft);
  }

  .lane--rival .word--locked {
    text-decoration-color: var(--lane-rival);
  }

  /* Only the count of a rival's excess crosses the wire, so there is no red text to
     render — the word is tinted instead. Enough to see they are stuck, not enough to
     read what they fumbled. @see Storm.readMirror */
  .lane--rival .word--fumbling {
    color: var(--error);
    opacity: 0.75;
  }

  /* Below the side-by-side breakpoint the rival's words are unreadable at strip
     height, so they are hidden and the strip carries depth and lives instead.
     Hidden rather than shrunk: a 9px serif is not information. */
  @media (width < 52rem) {
    .lane--rival .word {
      display: none;
    }
  }

  /* ---- Cleared, and missed -------------------------------------------- */

  /* One expanding ring in sage, then gone. The word has already been removed
     from the simulation by the time this plays — it is a receipt, not the
     event. */
  .ripple {
    position: absolute;
    top: 0;
    left: calc(var(--x, 0.5) * 100%);
    width: 1.5rem;
    aspect-ratio: 1;
    pointer-events: none;
    border: var(--hair) solid var(--calm);
    border-radius: var(--radius-pill);
    /* Centred on where the word's own centre was: the same travel expression as
       .word, plus half a line to reach its middle. A ripple that does not land on
       the word it is a receipt for reads as an unrelated flicker. */
    transform: translate(-50%, calc(var(--y, 0) * (100cqh - 1em) + 0.5em)) translateY(-50%);
    animation: ripple var(--dur-settle) var(--ease-drift) forwards;
  }

  /* Silt at the floor: one mark per word you let land. Countable, and it
     accumulates, so the cost of missing is visible after the moment has
     passed. Lives are the hard signal; this is the soft one. */
  .silt {
    width: var(--space-4);
    height: var(--puddle-height);
    background: var(--paper-edge);
    border-radius: var(--radius-pill);
    animation: settle var(--dur-slow) var(--ease-drift) both;
  }

  /* ---- Eliminated -----------------------------------------------------
     A lane that is out stops being a lane. The words are already gone from the
     simulation by this point, so what is left is a quiet plate over an empty sky —
     deliberately calm rather than a big red failure state, because in this game
     elimination is how a storm ends rather than something you did wrong. */
  .lane__out {
    position: absolute;
    inset: 0;
    z-index: var(--z-gauge);
    display: grid;
    gap: var(--space-1);
    place-content: center;
    justify-items: center;
    background: rgb(250 246 240 / 72%);
    animation: rise var(--dur-settle) var(--ease-drift) both;
  }

  .lane__out-word {
    font-family: var(--font-fall);
    font-size: var(--text-xl);
    color: var(--error);
  }

  .lane__out-note {
    font-size: var(--text-sm);
    color: var(--ink-soft);
  }

  /* ---- Lives ---------------------------------------------------------- */
  .lives {
    display: flex;
    gap: var(--space-1);
  }

  .lives__pip {
    width: 0.5rem;
    aspect-ratio: 1;
    background: var(--lane-ink);
    border-radius: var(--radius-pill);
    transition: background var(--dur-base) var(--ease-drift);
  }

  .lives__pip--spent {
    background: transparent;
    box-shadow: inset 0 0 0 var(--hair) var(--ink-faint);
  }

  .lane--rival .lives__pip {
    background: var(--lane-rival);
  }

  /* ---- The pressure gauge ---------------------------------------------
     The one element a player must read under time pressure, so it is the one
     element allowed to be emphatic. Water rising in a channel: the level is how
     much is coming, and the fuse wipe is how long you have.

     Vertical on wide screens (a channel between the lanes), horizontal on
     narrow ones (a bar between the strip and your lane). Same element, and the
     JS never knows which. */
  .gauge {
    position: relative;
    display: grid;
    place-items: center;
    overflow: hidden;
    background: var(--paper-deep);
    border-radius: var(--radius-pill);
  }

  .gauge__fill {
    position: absolute;
    inset: auto 0 0;
    height: calc(var(--level, 0) * 100%);
    background: var(--lane-rival);
    opacity: 0.28;
    transition: height var(--dur-base) var(--ease-drift);
  }

  /* The fuse: a line descending to meet the water, reaching it at impact.
     Linear, because a countdown that eases is a countdown that lies about how much
     time is left.

     A line rather than a second filled bar. Drawn as a bar it competed with the
     level for the same channel and usually won — a 2-drop attack one moment from
     landing rendered as a tall fuse over a short fill, so the eye read the fuse
     height as the quantity and the player misjudged what was coming. Two numbers in
     one channel need two different marks. */
  .gauge__fuse {
    position: absolute;
    inset: auto 0 calc((1 - var(--fuse, 0)) * 100%);
    height: 2px;
    background: var(--warn);
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease-drift);
  }

  .gauge[data-active="true"] .gauge__fuse {
    opacity: 1;
  }

  .gauge__count {
    position: relative;
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    color: var(--warn);
    opacity: 0;
    transition: opacity var(--dur-base) var(--ease-drift);
  }

  .gauge[data-active="true"] .gauge__count {
    opacity: 1;
  }

  @media (width < 52rem) {
    .gauge {
      height: 0.75rem;
    }

    .gauge__fill {
      inset: 0 auto 0 0;
      width: calc(var(--level, 0) * 100%);
      height: 100%;
    }

    /* Sideways, the line travels toward your lane instead of down onto it. */
    .gauge__fuse {
      inset: 0 auto 0 calc(var(--fuse, 0) * 100%);
      width: 2px;
      height: 100%;
    }

    .gauge__count {
      display: none;
    }
  }

  /* ---- Labels ---------------------------------------------------------- */
  .eyebrow {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: var(--track-caps);
    color: var(--ink-soft);
  }

  .lane__name {
    font-size: var(--text-sm);
    color: var(--ink-soft);
  }

  .lane--you .lane__name {
    color: var(--lane-you);
  }

  .lane--rival .lane__name {
    color: var(--lane-rival);
  }

  .tally {
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    color: var(--ink-faint);
  }

  /* ---- Buttons --------------------------------------------------------
     Hairline outlines and a fill on hover. No lift, no shadow, no squash: the
     house easing overshoots nothing, and a button that jumps under the cursor
     would be the loudest motion on the page. */
  .btn {
    display: inline-flex;
    gap: var(--space-2);
    align-items: center;
    justify-content: center;
    min-height: 2.75rem;
    padding: var(--space-2) var(--space-5);
    font-size: var(--text-base);
    color: var(--ink);
    background: transparent;
    border: var(--hair) solid var(--ink-hairline);
    border-radius: var(--radius-pill);
    transition:
      background var(--dur-fast) var(--ease-drift),
      border-color var(--dur-fast) var(--ease-drift),
      color var(--dur-fast) var(--ease-drift);
  }

  .btn:hover:not(:disabled) {
    background: var(--paper-deep);
    border-color: var(--ink-faint);
  }

  .btn--primary {
    color: var(--paper);
    background: var(--lane-you);
    border-color: var(--lane-you);
  }

  .btn--primary:hover:not(:disabled) {
    background: var(--ink);
    border-color: var(--ink);
  }

  .btn--quiet {
    min-height: 2rem;
    padding-inline: var(--space-3);
    font-size: var(--text-sm);
    color: var(--ink-soft);
    border-color: transparent;
  }

  .btn:disabled {
    color: var(--ink-faint);
    border-color: var(--ink-hairline);
  }

  /* ---- Cards and fields ----------------------------------------------- */
  .card {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    max-width: var(--measure);
    padding: var(--space-5);
    background: var(--paper);
    border-radius: var(--radius-lg);
    box-shadow: var(--lift);
  }

  .card--raised {
    box-shadow: var(--lift-raised);
  }

  .field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }

  .field__label {
    font-size: var(--text-sm);
    color: var(--ink-soft);
  }

  .input {
    min-height: 2.75rem;
    padding: var(--space-2) var(--space-3);
    color: var(--ink);
    background: var(--paper);
    border: var(--hair) solid var(--ink-hairline);
    border-radius: var(--radius-md);
  }

  .input:focus {
    border-color: var(--lane-you);
    outline: none;
  }

  .input--code {
    max-width: 9rem;
    font-family: var(--font-fall);
    font-size: var(--text-2xl);
    letter-spacing: 0.4em;
    text-align: center;
  }

  .field__error {
    font-size: var(--text-sm);
    color: var(--lane-rival);
  }

  /* ---- Room code ------------------------------------------------------- */
  .code {
    display: flex;
    gap: var(--space-2);
  }

  .code__digit {
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 3rem;
    font-family: var(--font-fall);
    font-size: var(--text-2xl);
    background: var(--paper-deep);
    border-radius: var(--radius-md);
  }

  /* ---- Roster ---------------------------------------------------------- */
  .seat {
    display: flex;
    gap: var(--space-3);
    align-items: center;
    justify-content: space-between;
    padding: var(--space-3) 0;
    border-bottom: var(--hair) solid var(--ink-hairline);
  }

  .seat:last-child {
    border-bottom: 0;
  }

  .badge {
    padding: 2px var(--space-2);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: var(--track-caps);
    color: var(--ink-soft);
    background: var(--paper-deep);
    border-radius: var(--radius-pill);
  }

  .badge--ready {
    color: var(--calm);
  }

  /* ---- Countdown ------------------------------------------------------- */
  .countdown {
    font-family: var(--font-fall);
    font-size: var(--text-count);
    font-weight: 400;
    line-height: 1;
    color: var(--ink);
  }

  /* ---- Result ---------------------------------------------------------- */
  .result {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    align-items: center;
    text-align: center;
  }

  .result__verdict {
    font-family: var(--font-fall);
    font-size: var(--text-2xl);
  }

  .result__stats {
    display: flex;
    gap: var(--space-5);
    font-size: var(--text-sm);
    color: var(--ink-soft);
  }

  .result__stat {
    display: flex;
    flex-direction: column;
    gap: 2px;
    align-items: center;
  }

  .result__figure {
    font-family: var(--font-fall);
    font-size: var(--text-xl);
    color: var(--ink);
  }

  /* ---- Choices --------------------------------------------------------- */
  .choice {
    position: relative;
    display: flex;
    gap: var(--space-3);
    align-items: baseline;
    padding: var(--space-3);
    cursor: pointer;
    border: var(--hair) solid var(--ink-hairline);
    border-radius: var(--radius-md);
    transition: border-color var(--dur-fast) var(--ease-drift);
  }

  .choice:has(:checked) {
    border-color: var(--lane-you);
  }

  .choice__input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
  }

  .choice__label {
    font-size: var(--text-base);
  }

  .choice__sublabel {
    font-size: var(--text-sm);
    color: var(--ink-soft);
  }

  /* ---- Toast ----------------------------------------------------------- */
  .toast {
    padding: var(--space-2) var(--space-4);
    font-size: var(--text-sm);
    color: var(--paper);
    background: var(--ink);
    border-radius: var(--radius-pill);
    box-shadow: var(--lift);
    animation: rise var(--dur-base) var(--ease-drift) both;
  }

  .toast--leaving {
    animation: fade var(--dur-fast) var(--ease-drift) forwards;
  }

  .toast--good {
    background: var(--calm);
  }

  .toast--bad {
    background: var(--lane-rival);
  }

  /* ---- Hint rail ------------------------------------------------------- */
  .hint {
    font-size: var(--text-sm);
    color: var(--ink-faint);
  }

  .hint kbd {
    padding: 1px var(--space-2);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    background: var(--paper-deep);
    border-radius: var(--radius-sm);
  }
}
