/* ==========================================================================
   Vehicle HUD

   One disc, the way a car's instrument binnacle is one disc. The outer ring
   is the tachometer; the two short arcs that fill its open mouth are fuel and
   body damage; road speed is a quieter ring just inside. Nothing is square,
   and there are no corners anywhere.

   Glow is built from paired strokes - a wide, faint one under a crisp one -
   rather than filter: blur(), which is expensive to composite every frame in
   CEF. No external fonts or assets, so the HUD stays inside the
   default-src 'self' policy declared in _headers.
   ========================================================================== */

:root {
    /* One scaling unit drives every dimension, so the HUD keeps its
       proportions from 1366x768 up to 4K without media queries.
       Want the whole HUD bigger or smaller? Change only this line - every
       size below is a multiple of it. Raise the 1.45vw/2.6vh pair to grow it,
       lower them to shrink it; the clamp keeps it sane at the extremes. */
    --u: clamp(14px, min(1.45vw, 2.6vh), 40px);

    --core:  #fff4e6;               /* numerals */
    --beam:  #ff8a1f;               /* live data */
    --amber: #ffc24a;               /* the warm mid-range and warnings */
    --quiet: rgba(255, 224, 194, 0.44);
    --crit:  #ff3b30;
    --go:    #4ce88a;

    --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

* {
    user-select: none;
    -webkit-user-drag: none;
}

html, body {
    margin: 0;
    padding: 0;
    overflow: hidden;
    background: transparent !important;
}

body {
    /* Bahnschrift ships with Windows 10+ - a condensed technical grotesque
       that suits an instrument. The stack degrades gracefully. */
    font-family: "Bahnschrift", "DIN Alternate", "Segoe UI", system-ui, sans-serif;
    font-variant-numeric: tabular-nums;
    color: var(--core);
}

.sprite { position: absolute; width: 0; height: 0; }

/* --- The disc ------------------------------------------------------------ */

.hud {
    position: fixed;
    right: calc(var(--u) * 2.2);
    bottom: calc(var(--u) * 2.2);

    width: calc(var(--u) * 12.5);
    height: calc(var(--u) * 12.5);

    visibility: hidden;
    opacity: 0;
    transition: opacity 0.32s var(--ease);
}

/* .is-live is the only thing that shows the cluster. visibility carries the
   hiding rather than opacity alone, so nothing of it can be seen or hit while
   the opening sequence is running. */
.hud.is-live {
    visibility: visible;
    opacity: 1;
    animation: cluster-in 0.46s cubic-bezier(0.34, 1.26, 0.64, 1);
}

/* Scoped to .is-live on purpose: without it this rule outranks the hidden
   state above, and the cluster would show through the opening sequence -
   which runs with the engine still off. */
.hud.is-live.is-off { opacity: 0.55; }

/* Only transform is animated here. Opacity is left to the transition above so
   the engine-off dimming still wins once the entrance has finished. */
@keyframes cluster-in {
    from { transform: scale(0.8) translateY(calc(var(--u) * 0.8)); }
    to   { transform: none; }
}

.cluster {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}

.face { fill: url(#grad-face); }

.face-rim {
    fill: none;
    stroke: rgba(255, 190, 130, 0.16);
    stroke-width: 1.5;
}

/* --- Graduations --------------------------------------------------------- */

/* A band of marks at the rim. They carry no reading of their own - they are
   the scale the tachometer sweeps across, and they give the face the texture
   of something machined. */
.ticks line {
    stroke: rgba(255, 205, 160, 0.3);
    stroke-width: 1.6;
    stroke-linecap: round;
}

.ticks line.major { stroke: rgba(255, 230, 200, 0.6); stroke-width: 2.4; }
.ticks line.red   { stroke: rgba(255, 59, 48, 0.65); }

/* --- Rings --------------------------------------------------------------- */

.ring, .gauge {
    fill: none;
    stroke-linecap: butt;
    /* The tachometer opens at the bottom; both rings share the same mouth. */
    transform: rotate(145deg);
    transform-origin: 120px 120px;
}

.gauge {
    /* The fuel and body arcs are drawn as explicit paths, already in place. */
    transform: none;
}

.ring--track { stroke: rgba(255, 205, 160, 0.16); stroke-width: 9; }

/* The redline zone is painted onto the track, so you can see where the
   limiter is before you reach it. */
.ring--red { stroke: rgba(255, 59, 48, 0.42); stroke-width: 9; }

/* The halo carries the bloom; the core carries the reading. Two strokes are
   far cheaper than a blur filter and hold their edge at any resolution. */
.ring--glow {
    stroke: var(--beam);
    stroke-width: 20;
    opacity: 0.22;
    transition: stroke-dashoffset 0.13s linear, opacity 0.3s linear, stroke 0.2s linear;
}

.ring--rev {
    stroke: url(#grad-rev);
    stroke-width: 9;
    transition: stroke-dashoffset 0.13s linear;
}

.ring--spdtrack { stroke: rgba(255, 205, 160, 0.1); stroke-width: 3; }

.ring--spd {
    stroke: var(--core);
    stroke-width: 3;
    opacity: 0.85;
    transition: stroke-dashoffset 0.13s linear;
}

/* Past the redline the bloom breathes - the one piece of motion the HUD
   starts on its own, because it is telling you to shift. */
.hud.is-redline .ring--glow {
    stroke: var(--crit);
    animation: redline 0.4s ease-in-out infinite alternate;
}

@keyframes redline { from { opacity: 0.16; } to { opacity: 0.66; } }

.hud.is-off .ring--glow { opacity: 0; }

/* --- Fuel and body ------------------------------------------------------- */

.gauge--track { stroke: rgba(255, 205, 160, 0.14); stroke-width: 7; }

.gauge--fuel, .gauge--body {
    stroke: var(--amber);
    stroke-width: 7;
    transition: stroke-dashoffset 0.35s var(--ease), stroke 0.25s linear;
}

.gauge--body { stroke: rgba(255, 238, 218, 0.72); }

.is-warn .gauge--fuel, .is-warn .gauge--body { stroke: var(--amber); }
.is-crit .gauge--fuel, .is-crit .gauge--body { stroke: var(--crit); }

/* --- Lamps --------------------------------------------------------------- */

/* The lamps follow the face instead of sitting in a straight line: five
   positions on an arc of their own, tucked inside the speed ring so nothing
   overlaps it. Percentages are of the disc, so they hold at any size. */
.lamps {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.lamps > * {
    position: absolute;
    transform: translate(-50%, -50%);
}

.lamps > :nth-child(1) { left: 33.3%; top: 31.4%; }
.lamps > :nth-child(2) { left: 41.0%; top: 26.7%; }
.lamps > :nth-child(3) { left: 50.0%; top: 25.0%; }
.lamps > :nth-child(4) { left: 59.0%; top: 26.7%; }
.lamps > :nth-child(5) { left: 66.7%; top: 31.4%; }

.tell, .turn {
    width: calc(var(--u) * 0.85);
    height: calc(var(--u) * 0.85);
    fill: var(--quiet);
    stroke: var(--quiet);
    opacity: 0.32;
    transition: opacity 0.12s linear, fill 0.14s linear, stroke 0.14s linear;
}

.turn--r { transform: translate(-50%, -50%) scaleX(-1); }

/* Lit states. Each lamp owns one colour, so a glance decodes it. */
.tell.is-on   { opacity: 1; fill: var(--beam); stroke: var(--beam); }
.tell.is-high { opacity: 1; fill: #ffe6c2; stroke: #ffe6c2; }
.tell.is-crit { opacity: 1; fill: var(--crit); stroke: var(--crit); animation: pulse 1.6s ease-in-out infinite; }

.turn.is-on {
    fill: var(--go);
    stroke: var(--go);
    animation: blink 0.9s steps(1, end) infinite;
}

@keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0.06; } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.38; } }

/* --- Speed --------------------------------------------------------------- */

.readout {
    position: absolute;
    top: 38%;
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: calc(var(--u) * 0.1);
    pointer-events: none;
}

.readout__num {
    font-size: calc(var(--u) * 3.5);
    font-weight: 600;
    font-variation-settings: "wght" 600, "wdth" 85;
    line-height: 0.84;
    letter-spacing: -0.03em;
    text-shadow: 0 0 calc(var(--u) * 0.9) rgba(255, 138, 31, 0.45);
}

.readout__unit {
    font-size: calc(var(--u) * 0.68);
    color: var(--quiet);
    letter-spacing: 0.13em;
}

/* --- Footer row ---------------------------------------------------------- */

/* Fuel, gear and body sit on one line across the base of the face, each above
   the arc it belongs to. */
.footer {
    position: absolute;
    top: 70%;
    left: 15%;
    right: 15%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: calc(var(--u) * 0.72);
    line-height: 1;
}

.stat {
    display: flex;
    align-items: center;
    gap: calc(var(--u) * 0.28);
    color: var(--quiet);
    transition: color 0.2s linear;
}

.stat__icon {
    width: calc(var(--u) * 0.78);
    height: calc(var(--u) * 0.78);
    fill: currentColor;
}

.stat__num { color: var(--core); opacity: 0.85; }

.stat.is-warn { color: var(--amber); }
.stat.is-warn .stat__num { color: var(--amber); opacity: 1; }

.stat.is-crit { color: var(--crit); animation: pulse 1.6s ease-in-out infinite; }
.stat.is-crit .stat__num { color: var(--crit); opacity: 1; }

.gear {
    font-size: calc(var(--u) * 1.75);
    line-height: 1;
    color: var(--beam);
    text-shadow: 0 0 calc(var(--u) * 0.6) rgba(255, 138, 31, 0.55);
    transition: color 0.15s linear, text-shadow 0.15s linear;
}

.gear.is-neutral {
    color: var(--quiet);
    text-shadow: none;
}

.gear.is-reverse {
    color: var(--amber);
    text-shadow: 0 0 calc(var(--u) * 0.6) rgba(255, 194, 74, 0.55);
}

/* --- Opening sequence ----------------------------------------------------
   Plays while the player is still climbing into the car. The greeting sits in
   the middle of the screen, then the dial flies into the corner and hands over
   to the cluster - it is the same face, so the handover reads as one object
   moving rather than one thing replacing another.

   Everything here animates transform and opacity only, so it composites on the
   GPU and never touches layout. It runs once per entry and then the element is
   taken out of the document. */

.intro {
    position: fixed;
    inset: 0;
    z-index: 10;

    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: calc(var(--u) * 0.5);

    pointer-events: none;
}

.intro.is-done { display: none; }

.intro__vignette {
    position: absolute;
    inset: 0;
    background: radial-gradient(58% 58% at 50% 50%, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0.6) 100%);
    opacity: 0;
}

/* Larger than the cluster on purpose, so the handover reads as the greeting
   settling down into the corner rather than growing out of it. */
/* The artwork is square, so a round crop ties it to the cluster that follows
   and keeps the greeting from reading as a rectangle dropped on the screen. */
.intro__art {
    width: calc(var(--u) * 11);
    height: calc(var(--u) * 11);
    object-fit: cover;
    border-radius: 50%;
    box-shadow: 0 0 0 2px rgba(255, 138, 31, 0.75),
                0 0 calc(var(--u) * 1.6) rgba(255, 138, 31, 0.35),
                0 calc(var(--u) * 0.4) calc(var(--u) * 1.2) rgba(0, 0, 0, 0.55);
    opacity: 0;
}

.intro__title,
.intro__sub {
    margin: 0;
    opacity: 0;
    text-shadow: 0 2px calc(var(--u) * 0.4) rgba(0, 0, 0, 0.85);
}

.intro__title {
    font-size: calc(var(--u) * 1.5);
    font-variation-settings: "wght" 600, "wdth" 88;
    letter-spacing: 0.02em;
    line-height: 1;
}

.intro__sub {
    font-size: calc(var(--u) * 0.76);
    color: var(--amber);
    letter-spacing: 0.16em;
    line-height: 1;
}

/* Arriving. The vignette only fades in here - the exit below owns fading it
   back out, so how long the greeting is held can change freely without the
   backdrop lifting early. */
.intro.is-running .intro__vignette { animation: intro-vignette-in 0.22s ease both; }
.intro.is-running .intro__art      { animation: intro-wake 0.3s var(--ease) both; }
.intro.is-running .intro__title    { animation: intro-rise 0.3s 0.11s var(--ease) both; }
.intro.is-running .intro__sub      { animation: intro-rise 0.3s 0.19s var(--ease) both; }

@keyframes intro-vignette-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes intro-wake {
    from { opacity: 0; transform: scale(0.88); }
    to   { opacity: 1; transform: none; }
}

@keyframes intro-rise {
    from { opacity: 0; transform: translateY(calc(var(--u) * 0.5)); }
    to   { opacity: 1; transform: none; }
}

/* Leaving.

   These are keyframes, not transitions, on purpose. The elements' own opacity
   is 0 - what makes them visible is the entrance animation's forward fill. A
   transition that cancelled that animation would drop the computed value to 0
   in the same style recalculation and never get a chance to run, which read as
   the greeting blinking out. Starting each exit explicitly from the visible
   state removes the dependency altogether.

   The order is choreographed: the text goes first, having said its piece; the
   artwork then drifts down and to the right, toward the corner the instrument
   is about to occupy; the vignette lifts last. The cluster arrives while the
   artwork is still on its way out, so the two overlap instead of queueing. */

.intro.is-leaving .intro__title { animation: intro-text-out 0.18s var(--ease) both; }
.intro.is-leaving .intro__sub   { animation: intro-text-out 0.18s 0.04s var(--ease) both; }

.intro.is-leaving .intro__art {
    animation: intro-art-out 0.36s 0.04s cubic-bezier(0.4, 0, 0.55, 1) both;
}

.intro.is-leaving .intro__vignette {
    animation: intro-vignette-out 0.3s 0.06s ease both;
}

@keyframes intro-text-out {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: translateY(calc(var(--u) * -0.45)); }
}

@keyframes intro-art-out {
    from { opacity: 1; transform: none; }
    /* The fade is pulled forward of the movement, so the artwork is already
       going before it leaves rather than vanishing at the end of its drift. */
    55%  { opacity: 0.4; }
    to   {
        opacity: 0;
        transform: translate(calc(var(--u) * 0.9), calc(var(--u) * 0.6)) scale(0.74);
    }
}

@keyframes intro-vignette-out {
    from { opacity: 1; }
    to   { opacity: 0; }
}

/* --- Power-on self test --------------------------------------------------
   Turning the key runs the gauges to full and lets them fall back, the way a
   real cluster proves its lamps. It is the only sequence the HUD plays
   unprompted, it runs once per ignition, and it costs nothing afterwards. */

.hud.is-booting .ring--rev,
.hud.is-booting .ring--spd,
.hud.is-booting .ring--glow,
.hud.is-booting .gauge--fuel,
.hud.is-booting .gauge--body {
    transition-duration: 0.45s;
    transition-timing-function: var(--ease);
}

.hud.is-booting .readout__num { animation: wake 0.5s var(--ease) both; }
.hud.is-booting .gear         { animation: wake 0.5s 0.1s var(--ease) both; }

@keyframes wake {
    from { opacity: 0; transform: translateY(calc(var(--u) * 0.3)); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .hud, .ring, .gauge, .tell, .turn { transition-duration: 0s; }
    .intro { display: none; }
    .turn.is-on, .tell.is-crit, .stat.is-crit { animation: none; opacity: 1; }
    .hud.is-redline .ring--glow { animation: none; opacity: 0.5; }
    .hud.is-booting .readout__num, .hud.is-booting .gear { animation: none; }
}
