/* ==========================================================================
   Vehicle HUD - v2, analog

   One dial with a real needle. The needle reads speed off the numbered scale,
   and the neon arc outside the scale is its trail, always ending where the
   needle points. In the scale's open mouth: a small digital speed, a
   segmented rev bar, and fuel and body arcs.

   Pink and violet neon, held back on purpose: glow is a wide, faint stroke
   under a crisp one, never above ~0.22 opacity, so it reads as lit rather
   than as something shining in your eyes. That also keeps it cheap - paired
   strokes, not filter: blur(), which is expensive to composite every frame in
   CEF. No external fonts or assets: everything is served from one origin.
   ========================================================================== */

: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:   #f6eeff;              /* numerals, lavender-white */
    --pink:   #ff5ec8;              /* the needle and live accents */
    --violet: #9b6bff;              /* the low end of the rev range, structure */
    --quiet:  rgba(226, 208, 255, 0.46);
    --warn:   #ffc46b;
    --crit:   #ff3d7f;
    --go:     #6ff5c8;              /* turn signals - their own colour, on purpose */

    --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 instrument ------------------------------------------------------ */

.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; }
}

.dial,
.needle-layer {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
    overflow: visible;
}

/* Above the lamps and the readout, so the needle passes over them. */
.needle-layer { pointer-events: none; }

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

.face-rim {
    fill: none;
    stroke: rgba(196, 150, 255, 0.22);
    stroke-width: 1.4;
}

/* --- Scale --------------------------------------------------------------- */

.scale line {
    stroke: rgba(226, 208, 255, 0.34);
    stroke-width: 1.3;
    stroke-linecap: round;
}

.scale line.major {
    stroke: rgba(246, 238, 255, 0.78);
    stroke-width: 2.2;
}

.scale text {
    fill: rgba(246, 238, 255, 0.78);
    font-size: 11px;
    font-weight: 600;
    text-anchor: middle;
    dominant-baseline: central;
}

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

/* The arc outside the scale is the needle's trail, and it must end exactly
   where the needle points - not just at rest but while moving. The needle
   turns linearly in angle and the arc grows linearly in length, which is
   proportional to angle, so the two stay aligned in every frame as long as
   they share one duration and one timing function: --follow. Change it in
   one place only. */
:root { --follow: 0.14s linear; }

.trail {
    fill: none;
    stroke-linecap: butt;
    /* Zero on the scale is at 150deg; the trail starts there too. */
    transform: rotate(150deg);
    transform-origin: 120px 120px;
}

.trail--track { stroke: rgba(196, 150, 255, 0.14); stroke-width: 3.5; }

/* The halo carries the bloom; the core carries the reading. */
.trail--glow {
    stroke: var(--pink);
    stroke-width: 10;
    opacity: 0.18;
    transition: stroke-dashoffset var(--follow), opacity 0.3s linear;
}

.trail--fill {
    stroke: url(#grad-trail);
    stroke-width: 3.5;
    transition: stroke-dashoffset var(--follow);
}

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

/* --- Needle -------------------------------------------------------------- */

/* The needle is drawn pointing at 3 o'clock and rotated into place, so its
   angle is one transform. The short linear transition smooths the steps
   between server updates without making the needle feel late. */
.needle {
    transform: rotate(150deg);
    transform-origin: 120px 120px;
    transform-box: view-box;
    transition: transform var(--follow);
}

.needle__halo {
    stroke: var(--pink);
    stroke-width: 7;
    stroke-linecap: round;
    opacity: 0.2;
}

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

.hub {
    fill: #120a1f;
    stroke: rgba(255, 94, 200, 0.7);
    stroke-width: 1.6;
}

.hub__dot { fill: var(--pink); }

.hud.is-off .needle__halo { opacity: 0; }

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

.gauge { fill: none; stroke-linecap: butt; }

.gauge--track { stroke: rgba(196, 150, 255, 0.14); stroke-width: 6; }

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

.gauge--body { stroke: rgba(246, 238, 255, 0.7); }

/* The warning classes go on the arcs themselves. In v1 they sat on the text
   beside them, which is not an ancestor of the arc, so the arc never changed
   colour however low the tank got. */
.gauge.is-warn { stroke: var(--warn); }
.gauge.is-crit { stroke: var(--crit); }

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

/* Five positions on an arc of their own inside the numbers, so nothing
   overlaps the scale. Percentages are of the dial, 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: 34.9%; top: 34.4%; }
.lamps > :nth-child(2) { left: 41.9%; top: 29.9%; }
.lamps > :nth-child(3) { left: 50.0%; top: 28.3%; }
.lamps > :nth-child(4) { left: 58.1%; top: 29.9%; }
.lamps > :nth-child(5) { left: 65.1%; top: 34.4%; }

.tell, .turn {
    width: calc(var(--u) * 0.78);
    height: calc(var(--u) * 0.78);
    fill: var(--quiet);
    stroke: var(--quiet);
    opacity: 0.3;
    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(--pink); stroke: var(--pink); }
.tell.is-high { opacity: 1; fill: #ffe3f6; stroke: #ffe3f6; }
.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; } }

/* --- Digital readout ----------------------------------------------------- */

/* Sits in the mouth of the scale, below the hub - the one place the needle
   never points, so the number is never covered. */
.readout {
    position: absolute;
    top: 55%;
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: calc(var(--u) * 0.05);
    pointer-events: none;
}

.readout__num {
    font-size: calc(var(--u) * 1.55);
    font-weight: 600;
    font-variation-settings: "wght" 600, "wdth" 88;
    line-height: 0.9;
    letter-spacing: -0.01em;
    text-shadow: 0 0 calc(var(--u) * 0.5) rgba(255, 94, 200, 0.35);
}

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

/* --- Tachometer bar ------------------------------------------------------ */

/* A segmented bar in the scale's open mouth, under the number. Like the
   number, it sits where the needle never points, so it is never covered. */
.rpm {
    position: absolute;
    top: 72%;
    left: 50%;
    transform: translateX(-50%);
    width: 34%;
    display: flex;
    align-items: center;
}

.rpm__track {
    position: relative;
    flex: 1;
    height: calc(var(--u) * 0.32);
    overflow: hidden;
    /* The last 14% of the track is the redline, tinted before you get there. */
    background: linear-gradient(90deg,
        rgba(196, 150, 255, 0.14) 0 86%,
        rgba(255, 61, 127, 0.4) 86% 100%);
}

.rpm__fill {
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, var(--violet), #d45cff 60%, var(--pink));
    box-shadow: 0 0 calc(var(--u) * 0.35) rgba(255, 94, 200, 0.35);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.13s linear;
}

/* Thin gaps cut into both track and fill, so it reads as ten segments. */
.rpm__segments {
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(90deg,
        transparent 0 calc(10% - 1.5px),
        rgba(14, 8, 26, 0.95) calc(10% - 1.5px) 10%);
}

/* Hangs off the right end, so the bar itself stays centred under the number. */
.rpm__label {
    position: absolute;
    left: calc(100% + var(--u) * 0.28);
    font-size: calc(var(--u) * 0.5);
    color: var(--quiet);
    letter-spacing: 0.12em;
}

/* Past the redline the bar breathes - the one piece of motion the HUD
   starts on its own, because it is telling you to shift. */
.hud.is-redline .rpm__fill {
    background: var(--crit);
    box-shadow: 0 0 calc(var(--u) * 0.4) rgba(255, 61, 127, 0.5);
    animation: redline 0.4s ease-in-out infinite alternate;
}

@keyframes redline { from { opacity: 1; } to { opacity: 0.55; } }

.hud.is-off .rpm__fill { box-shadow: none; }

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

/* Fuel, gear and body on one line across the base. Fuel and body sit on the
   inner, concave side of their own arcs, clear of the stroke rather than on
   top of it. The mouth is tight - number, rev bar, this row and the arcs all
   share it - so the row is placed by measurement: it clears the rev bar above
   and both arcs below, and still fits "100 · 8 · 100" across. */
.footer {
    position: absolute;
    top: 77%;
    left: 29%;
    right: 29%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: calc(var(--u) * 0.66);
    line-height: 1;
}

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

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

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

.stat.is-warn { color: var(--warn); }
.stat.is-warn .stat__num { color: var(--warn); 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.1);
    line-height: 1;
    color: var(--pink);
    text-shadow: 0 0 calc(var(--u) * 0.5) rgba(255, 94, 200, 0.45);
    transition: color 0.15s linear, text-shadow 0.15s linear;
}

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

.gear.is-reverse {
    color: var(--warn);
    text-shadow: 0 0 calc(var(--u) * 0.5) rgba(255, 196, 107, 0.45);
}

/* --- Opening sequence ----------------------------------------------------
   Plays while the player is still climbing into the car: a portrait in a
   round frame, a neon ring drawing itself around it, one sweep of light
   across the picture, and the greeting beneath. Then it stands down and the
   cluster makes its own entrance in the corner.

   Everything here animates transform, opacity or a stroke offset only, so it
   composites cheaply and never touches layout. It runs once per entry and the
   element then leaves 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(12, 4, 24, 0) 40%, rgba(12, 4, 24, 0.62) 100%);
    opacity: 0;
}

.intro__portrait {
    position: relative;
    width: calc(var(--u) * 11);
    height: calc(var(--u) * 11);
    opacity: 0;
}

.intro__frame {
    position: absolute;
    inset: calc(var(--u) * 0.55);
    border-radius: 50%;
    overflow: hidden;
    box-shadow: 0 calc(var(--u) * 0.4) calc(var(--u) * 1.4) rgba(0, 0, 0, 0.55),
                0 0 calc(var(--u) * 1.4) rgba(255, 94, 200, 0.22);
}

.intro__art {
    width: 100%;
    height: 100%;
    display: block;
    object-fit: cover;
}

/* One diagonal band of light that crosses the picture once. */
.intro__glint {
    position: absolute;
    inset: -20%;
    background: linear-gradient(115deg,
        rgba(255, 255, 255, 0) 38%,
        rgba(255, 214, 244, 0.32) 50%,
        rgba(255, 255, 255, 0) 62%);
    transform: translateX(-100%);
}

.intro__ring {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    transform: rotate(-90deg);     /* the ring starts drawing at 12 o'clock */
    overflow: visible;
}

.intro__ring circle {
    fill: none;
    stroke-dasharray: 100;
    stroke-dashoffset: 100;
}

.intro__ring-halo { stroke: var(--pink); stroke-width: 3.6; opacity: 0.22; }
.intro__ring-core { stroke: url(#grad-ring); stroke-width: 1.1; }

.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(--pink);
    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. */
.intro.is-running .intro__vignette  { animation: fade-in 0.22s ease both; }
.intro.is-running .intro__portrait  { animation: intro-wake 0.34s var(--ease) both; }
.intro.is-running .intro__art       { animation: ken-burns 1.4s var(--ease) both; }
.intro.is-running .intro__ring circle { animation: ring-draw 0.7s 0.08s var(--ease) both; }
.intro.is-running .intro__glint     { animation: glint 0.75s 0.3s ease-in-out both; }
.intro.is-running .intro__title     { animation: intro-rise 0.3s 0.14s var(--ease) both; }
.intro.is-running .intro__sub       { animation: intro-rise 0.3s 0.22s var(--ease) both; }

@keyframes fade-in    { from { opacity: 0; } to { opacity: 1; } }
@keyframes ken-burns  { from { transform: scale(1.14); } to { transform: scale(1); } }
@keyframes ring-draw  { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
@keyframes glint      { from { transform: translateX(-100%); } to { transform: translateX(100%); } }

@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.

   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 run, which reads as the greeting
   blinking out. Starting each exit explicitly from the visible state removes
   that dependency.

   The text goes first; the portrait then drifts down and right, toward the
   corner the instrument is about to occupy; the vignette lifts last. The
   cluster arrives while the portrait is still leaving, so the two overlap. */

.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__portrait { animation: intro-art-out 0.36s 0.04s cubic-bezier(0.4, 0, 0.55, 1) both; }
.intro.is-leaving .intro__vignette { animation: fade-out 0.3s 0.06s ease both; }

/* The entrance animations stay filled at their end state while leaving. */
.intro.is-leaving .intro__art         { animation: none; }
.intro.is-leaving .intro__ring circle { animation: none; stroke-dashoffset: 0; }
.intro.is-leaving .intro__glint       { animation: none; transform: translateX(100%); }

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

@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 portrait 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);
    }
}

/* --- Power-on self test --------------------------------------------------
   Turning the key sweeps the needle and every gauge to full and lets them
   fall back, the way a real cluster proves itself. It runs once per ignition
   and costs nothing afterwards. */

/* The needle and its trail are listed together and given one duration and
   one curve, so they stay aligned through the sweep as well. */
.hud.is-booting .needle,
.hud.is-booting .trail--fill,
.hud.is-booting .trail--glow,
.hud.is-booting .rpm__fill,
.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, .needle, .trail, .rpm__fill, .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 .rpm__fill { animation: none; }
    .hud.is-booting .readout__num, .hud.is-booting .gear { animation: none; }
}

/* --- Call probe ----------------------------------------------------------
   Only ever on screen with ?debug=1 on the URL. Deliberately plain: it is a
   diagnostic readout, not part of the instrument. */

.probe {
    position: fixed;
    top: 10px;
    left: 10px;
    z-index: 100;

    padding: 8px 10px;
    background: rgba(10, 6, 18, 0.9);
    border: 1px solid rgba(255, 94, 200, 0.35);
    border-radius: 6px;

    font-family: Consolas, "Cascadia Mono", monospace;
    font-size: 12px;
    line-height: 1.45;
    color: #f6eeff;
}

.probe table { border-collapse: collapse; margin-top: 6px; }
.probe th, .probe td { padding: 1px 10px 1px 0; text-align: left; font-weight: normal; }
.probe th { color: rgba(226, 208, 255, 0.5); }

/* Green while the calls are still arriving, amber once they have stopped,
   red for a function the server has never called at all. */
.probe tr.live  td { color: #4ce88a; }
.probe tr.stale td { color: #ffc24a; }
.probe tr.never td { color: #ff6b6b; }
