* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html, body { height: 100%; }

body {
    font-family: 'Arial', sans-serif;
    background: linear-gradient(135deg, #1a1a2e, #16213e);
    color: white;
    user-select: none;
    margin: 0;
    /* Fill the viewport as a column; no page scroll */
    height: 100vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* Brand on the left, title block centred in the remaining space. The logo is
   taken out of the centring by giving the text block flex:1 and reserving the
   same width on the right, so the title stays optically centred on the page. */
.game-header {
    display: flex;
    align-items: center;
    gap: 20px;
    padding: 8px 20px;
    background: rgba(0, 0, 0, 0.3);
    backdrop-filter: blur(10px);
    flex-shrink: 0;
}

.brand-link {
    flex-shrink: 0;
    width: 180px;
    display: block;
    line-height: 0;
}

.brand-logo {
    width: 100%;
    height: auto;
    display: block;
}

.header-text {
    flex: 1;
    text-align: center;
}

/* Balances the logo so the title block sits centred on the page, not on the
   space left over beside it. */
.game-header::after {
    content: '';
    flex-shrink: 0;
    width: 180px;
}

.game-title {
    font-size: 1.6rem;
    line-height: 1.1;
    background: linear-gradient(45deg, #ffd700, #ffeb3b);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    margin-bottom: 4px;
}

.game-subtitle {
    font-size: 0.95rem;
    opacity: 0.9;
    margin-bottom: 6px;
}

.instructions {
    max-width: 900px;
    margin: 0 auto;
    font-size: 0.85rem;
    line-height: 1.35;
    opacity: 0.8;
}

.game-container {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    max-width: 1600px;
    width: 100%;
    margin: 0 auto;
    padding: 12px 20px;
    /* Fill remaining viewport height under the header */
    flex: 1;
    min-height: 0;
    box-sizing: border-box;
}

.info-panel.left-panel {
    width: 300px;
    display: flex;
    flex-direction: column;
    gap: 15px;
    min-height: 0;
    overflow-y: auto;
}

/* Circuit Section — the workspace is its only child, so its height no longer
   depends on how long the current feedback message happens to be. */
.circuit-section {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 600px;
    min-height: 0;
    /* The start veil, the rotate prompt and the exit button all lay themselves
       over the workspace from here. */
    position: relative;
}

/* Educational feedback, a card of its own in the left column — the colour is
   the heading, so it carries no title. Sized for a 300px column: left-aligned
   and free to wrap over several lines without disturbing anything else. */
.feedback-box {
    background: rgba(52, 152, 219, 0.95);
    color: white;
    padding: 14px 16px;
    border-radius: 8px;
    font-size: 0.78rem;
    line-height: 1.45;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    border: 1px solid rgba(255, 255, 255, 0.3);
    text-align: left;
    transition: background 0.3s ease, border-color 0.3s ease;
    font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
    border-left: 4px solid rgba(255, 255, 255, 0.8);
}

.feedback-box.success {
    background: rgba(46, 204, 113, 0.95);
    border-left-color: #2ecc71;
}

.feedback-box.warning {
    background: rgba(230, 126, 34, 0.95);
    border-left-color: #f39c12;
}

.feedback-box.error {
    background: rgba(231, 76, 60, 0.95);
    border-left-color: #e74c3c;
}

.feedback-box.info {
    background: rgba(52, 152, 219, 0.95);
    border-left-color: #3498db;
}

/* Circuit Area */
.circuit-area {
    flex: 1;
    background: #2c3e50;
    border-radius: 15px;
    border: 3px solid #34495e;
    position: relative;
    min-height: 0; /* fill the section height instead of a fixed 550px */
    box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.3);
    overflow: hidden;
}

.info-panel {
    width: 350px;
    display: flex;
    flex-direction: column;
    gap: 15px;
    min-height: 0;
    overflow-y: auto;
}

.panel {
    width: 100%;
    background: rgba(0, 0, 0, 0.4);
    border-radius: 12px;
    padding: 20px;
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.1);
}

.panel h3 {
    color: #ffd700;
    margin-bottom: 15px;
    font-size: 1.1rem;
}

/* Link out to the build instructions. Card-shaped like its neighbours, but
   gold-on-dark so it reads as an action rather than another readout. */
.instructions-link {
    display: block;
    padding: 14px 16px;
    background: rgba(0, 0, 0, 0.4);
    border: 1px solid rgba(255, 215, 0, 0.35);
    border-radius: 12px;
    backdrop-filter: blur(10px);
    color: #ffd700;
    font-size: 0.95rem;
    font-weight: bold;
    text-align: center;
    text-decoration: none;
    transition: background 0.2s ease, border-color 0.2s ease;
}

.instructions-link:hover {
    background: rgba(255, 215, 0, 0.12);
    border-color: rgba(255, 215, 0, 0.75);
}

/* Buy This Kit card */
.kit-link { display: block; }
.kit-image {
    width: 100%;
    height: auto;
    display: block;
    border-radius: 8px;
    border: 1px solid rgba(255, 255, 255, 0.15);
}

.status-item {
    display: flex;
    align-items: center;
    margin: 8px 0;
    font-size: 0.9rem;
}

.status-dot {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    margin-right: 10px;
    background: #e74c3c;
    transition: all 0.3s ease;
}

.status-dot.active {
    background: #2ecc71;
    box-shadow: 0 0 12px rgba(46, 204, 113, 0.6);
}

/* The track is the whole stage floor now, not a framed band: no background,
   border or radius of its own — the asphalt lives on .road, which is the layer
   that gets the 3D perspective. The track itself stays untransformed so the car
   inside it is not tilted along with the ground. */
.car-track {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 96%;
    height: 88%;
}

/* Horizon fade: dissolves the far edge of the road into the backdrop instead of
   cutting it off, which is what sells the vanishing point. */
.car-track::after {
    content: '';
    position: absolute;
    inset: 0 0 42% 0;
    background: linear-gradient(to bottom,
        #2c3e50 0%,
        rgba(44, 62, 80, 0.85) 45%,
        rgba(44, 62, 80, 0) 100%);
    pointer-events: none;
    z-index: 1;
}

/* Ground plane. Tipping the top edge away in Z gives the trapezoid (short side
   far) and makes the surface stripes converge toward the vanishing point — all
   for free, and without touching the JS: updateRoadScroll() still scrolls along
   the plane's local X axis.
   Pattern period = 40px, so it loops seamlessly under the JS modulo. */
.road {
    position: absolute;
    /* The plane runs well past the top of the track: the extra length is what
       recedes into the distance, and the horizon fade above swallows its end. */
    top: -70%;
    right: 0;
    bottom: 0;
    left: 0;
    transform: perspective(700px) rotateX(45deg);
    transform-origin: center bottom;
    backface-visibility: hidden;
    background-color: #2f4054;
    background-image: repeating-linear-gradient(
        to right,
        #3a4b5e 0px,
        #3a4b5e 20px,
        #33445673 20px,
        #334456 40px
    );
    background-size: 40px 100%;
}

/* Single knob for the whole car: every mounted part sizes itself as a
   percentage of its container, so changing this width rescales the lot. */
.car-chassis {
    position: absolute;
    width: 648px; /* +20% over the previous 540 — the one knob that scales the
                     whole car (deck, motor, propeller, battery, switch, wheels),
                     since every part is sized in % of the chassis */
    aspect-ratio: 360 / 230;
    /* Centred rather than offset from the bottom: the track is itself centred
       in the work area, so this centres the car in the area exactly, at any
       window size, without a percentage that would drift as the area resizes. */
    top: 50%;
    bottom: auto;
    left: 50%;
    transform: translate(-50%, -50%); /* car stays put; the road scrolls */
    z-index: 2;
}

/* The chassis is now the photographed part: plywood deck + vertical mast
   (parts/base.png, 903x544). aspect-ratio keeps it undistorted at any size. */
.car-body {
    position: absolute;
    width: 88.89%;
    aspect-ratio: 903 / 544;
    top: 4.35%;
    left: 50%;
    transform: translateX(-50%);
    background: url('parts/base.png') no-repeat center / 100% 100%;

    /* Mounting anchors, measured on base.png (see plan). Children position
       themselves against these instead of hardcoded pixels. */
    --mast-x: 21%;          /* mast centre line */
    --mast-top-y: 6.4%;     /* top face of the mast — motor + propeller */
    --deck-y: 76%;          /* usable strip of the deck surface */
    /* The battery is anchored by its centre and the switch by its base, so the
       same --deck-y would not line their terminals up. This value equalises the
       two on the NEGATIVE line — the row the black cable runs along (battery −
       at fy 0.559 of the pack, motor − across from it) — so the switch now sits
       in the black run instead of the red one, and that wire passes dead level
       through its posts. Derived from the current part sizes: pack 32.66% wide
       (h = 0.1909W), switch 22.77% (h = 0.2651W, posts at fy 0.777, box
       bottom-anchored) => 0.5282W / 0.6024W of the car. Re-derive it if either
       part is resized. A percentage, so it holds at any car scale. */
    --deck-switch-y: 87.68%;
    --deck-switch-x: 48%;
    --deck-battery-x: 80%;
    --motor-y: 12%;         /* motor straddles the top of the mast */
}

/* Car Wheels — four photographed parts, two per side of the car. Sized off the
   chassis so they scale with it, and each keeps its own image ratio so none of
   them is stretched. The spoke and hubcap that used to be drawn with
   pseudo-elements are in the photographs now.

   Depth is done with document order, not z-index: .car-chassis is a stacking
   context of its own, so the two far-side wheels (…-SP.png) are written BEFORE
   .car-body and the deck — with everything mounted on it — paints over them,
   while the two near-side ones (…-FP.png) come last and paint over the lot. */
.car-wheel {
    position: absolute;
    width: 16.67%;
    bottom: -4.35%;
    background: no-repeat center / 100% 100%;
    transition: transform 0.1s linear;
}

/* Which end is which: the mast carries the motor and the propeller, and it
   stands at --mast-x (21%), so the LEFT end of the deck is the back of the car.
   The rear wheels go there and the front ones at the other end — the two spots
   themselves have not moved, only which wheel sits in each. */
.rear-wheel {
    left: 2.78%;
}

.front-wheel {
    right: 5.56%;
}

.wheel-fp.front-wheel {
    aspect-ratio: 992 / 1011;
    background-image: url('parts/Wheel-Front-FP.png');
}

/* The back pair runs 20% bigger than the front, like a rear-drive kit. Growing
   them from a fixed `left` would walk the axle across the deck, so the left edge
   is pulled back by half the extra width and the wheel grows about its own
   centre; `bottom` already keeps the tyre on the same ground line. */
.wheel-fp.rear-wheel {
    width: 20%;        /* 16.67% × 1.2 */
    left: 1.11%;       /* 2.78% − half of the 3.33% it gained */
    aspect-ratio: 982 / 954;
    background-image: url('parts/Wheel-Back-FP.png');
}

/* The far pair, placed by the scene's own perspective rather than by eye. The
   view is a cone with the vanishing point above the middle of the car, which
   the deck itself measures out: its far edge is 0.9 the width of its near edge
   and sits 120px higher on a 347px-tall deck, and those two facts put the
   vanishing point at x 50%, y −203% of the chassis. Everything one car-width
   further away is therefore its near counterpart pulled 10% of the way toward
   that point — smaller, higher, and drawn in toward the centre line.

   So: 0.9 the width, both resting on the far ground line (the near ground line
   mapped through the same projection, 26.4% up the chassis), and each centred
   on its own near wheel's centre mapped the same way — then the gap between the
   two adjusted by eye from there (closed 20%, reopened 10%, closed 5%), which
   reads better than the bare projection at this focal length. The front wheel
   took half of the first adjustment and none of the later ones, so the two
   `left`/`right` values below no longer sit symmetrically about the centre. */
.wheel-sp {
    width: 15%;        /* 16.67% × 0.9 */
    bottom: 26.4%;     /* the near ground line (−4.35%) taken to the far one */
}

.wheel-sp.rear-wheel {
    width: 18%;        /* 15% × 1.2, matching the near back wheel */
    left: 10.3%;       /* the far gap reopened 10% then closed 5%, all at this end */
    aspect-ratio: 1009 / 1032;
    background-image: url('parts/Wheel-Back-SP.png');
}

.wheel-sp.front-wheel {
    right: 16.77%;     /* 10.02% + the same 6.75% from the other end */
    aspect-ratio: 1015 / 1036;
    background-image: url('parts/Wheel-Front-SP.png');
}

/* DC Motor - the photographed part (parts/motor.png, 355x154 => ratio 2.305),
   mounted at the top of the mast with its shaft pointing forward, like the kit.
   Anchors below are measured on the image and drive both the propeller and the
   terminal positions in connections.js. */
.car-body .dc-motor {
    position: absolute;
    width: 31.25%;
    aspect-ratio: 355 / 154;
    left: var(--mast-x);
    top: var(--motor-y);
    /* -56.6% puts the motor *body* centre (not the image centre, which the
       shaft skews left) on the mast centre line. */
    transform: translate(-56.6%, -50%);
    background: url('parts/motor.png') no-repeat center / 100% 100%;
    cursor: default; /* Not draggable */

    /* Where the propeller hub sits on the output shaft. The shaft spans 0-24.8%
       of motor.png's width, so this rides its first third rather than floating
       at the very tip. The slipstream anchors here too, so it follows along. */
    --shaft-x: 8%;
    --shaft-y: 49.7%;
}

/* Body and shaft are painted into motor.png now. */
.motor-body,
.motor-shaft {
    display: none;
}

/* Slipstream off the propeller. Anchored to the shaft tip with the motor's own
   variables, so it follows and scales with the car without any JS positioning.
   parts/wind.gif is pure white on transparency, which reads straight over the
   dark asphalt. motor.js only toggles .blowing. */
.dc-motor .wind-stream {
    position: absolute;
    left: var(--shaft-x);
    top: var(--shaft-y);
    width: 220%; /* of the motor */
    aspect-ratio: 420 / 228;
    transform: translate(-100%, -50%); /* streams away to the left */
    background: url('parts/wind.gif') no-repeat center / 100% 100%;
    opacity: 0;
    transition: opacity 0.25s ease;
    pointer-events: none;
}

.dc-motor .wind-stream.blowing {
    opacity: 0.85;
}

/* Motor running backwards: the stream mirrors and swaps to the far side of the
   shaft, so the air is pushed over the car instead of ahead of it. The translate
   has to come first — put the scale first and it would mirror the offset too. */
.dc-motor .wind-stream.reversed {
    transform: translate(0, -50%) scaleX(-1);
}

/* Battery Pack - the photographed part (parts/batteryholder.png, 1052x615 =>
   ratio 1.711), a top-down view sitting flat on the deck right of the mast.
   No rotation needed: the image is already in its resting orientation. */
.car-body .battery-pack {
    position: absolute;
    width: 32.66%;   /* 29.69% + 10% */
    aspect-ratio: 1052 / 615;
    left: var(--deck-battery-x);
    top: var(--deck-y);
    right: auto;
    cursor: default;
    transform: translate(-50%, -50%);
    background: url('parts/batteryholder.png') no-repeat center / 100% 100%;
}

/* Holder shell and cells are painted into batteryholder.png now. */
.car-body .battery-holder,
.car-body .battery-cell {
    display: none;
}

/* Short circuit: the pack runs red hot and shudders. Driven by
   batteryComponent.setOverheated(). The transform repeats the anchoring
   translate, or the shake would tear the pack off its mount. */
.car-body .battery-pack.overheating {
    animation: batteryOverheat 0.45s ease-in-out infinite;
    filter: saturate(1.5) hue-rotate(-14deg) brightness(1.15);
}

@keyframes batteryOverheat {
    0%, 100% {
        transform: translate(-50%, -50%);
        box-shadow: 0 0 18px 4px rgba(255, 76, 30, 0.55);
    }
    50% {
        transform: translate(calc(-50% + 1.5px), calc(-50% - 1px));
        box-shadow: 0 0 34px 10px rgba(255, 106, 30, 0.95);
    }
}

/* Sparks jumping at the contacts, on the pack's own free pseudo-elements. */
.car-body .battery-pack.overheating::before,
.car-body .battery-pack.overheating::after {
    content: '';
    position: absolute;
    left: 7.3%;
    width: 14%;
    aspect-ratio: 1;
    border-radius: 50%;
    background: radial-gradient(circle,
        #fffbe6 0%, #ffd84a 35%, rgba(255, 138, 30, 0.6) 65%, transparent 72%);
    pointer-events: none;
}

.car-body .battery-pack.overheating::before {
    top: 22.1%;
    transform: translate(-50%, -50%);
    animation: spark 0.28s steps(2, end) infinite;
}

.car-body .battery-pack.overheating::after {
    top: 55.9%;
    transform: translate(-50%, -50%);
    animation: spark 0.34s steps(2, end) infinite 0.12s;
}

@keyframes spark {
    0%, 100% { opacity: 0; }
    50%      { opacity: 1; }
}

/* Sits on the holder's bottom third — the only genuinely dark part of the
   photograph (luminance ~31, against 142-179 over the cells), so light type
   reads there without needing a chip of its own. */
.car-body .battery-label {
    position: absolute;
    bottom: auto;
    top: 84%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 0.6rem;
    font-weight: bold;
    color: #e3eaf1;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
    white-space: nowrap;
}

/* Switch on Car - two photographed states swapped by the .closed class that
   switch.js already toggles (parts/switch_open.png and switch_close.png, both
   900x1048).

   The two shots were taken from different angles, so they are pre-aligned on a
   shared canvas rather than compensated for here: the brass screw posts land on
   the same pixel in both files (fx 0.314 / 0.698, fy 0.777) and the base plate
   is centred with its bottom edge on the canvas bottom. So both states use the
   same box and the same background rules — only the image swaps, and only the
   lever appears to move. Originals kept in parts/originals/.

   The plate is 69.8% of the canvas width, so 22.77% here renders it at ~15.9%
   of the car; the rest of the box is headroom for the raised lever, which is
   why it no longer takes pointer events (see ::after). */
.car-body .switch {
    position: absolute;
    width: 22.77%;   /* 20.7% + 10% */
    aspect-ratio: 900 / 1048; /* the shared canvas: plate at the bottom, lever above */
    left: var(--deck-switch-x);
    top: var(--deck-switch-y);
    /* -100% rests the plate on the deck line; the lever rises above it. */
    transform: translate(-50%, -100%);
    /* The box is mostly transparent headroom now — only the ::after patch over
       the plate takes clicks, so the empty area doesn't swallow taps meant for
       the deck or the parts behind it. */
    pointer-events: none;
}

/* The toggle target: the bare strip of plate between the two brass posts. Kept
   clear of the connector circles, which sit on the posts themselves. */
.car-body .switch::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 83%;
    width: 22%;
    height: 30%;
    transform: translate(-50%, -50%);
    pointer-events: auto;
    cursor: pointer;
}

/* Second target: the lever, which is the other place a player naturally taps.
   It moves between the two states, so the patch tracks it. Measured off the
   900x1048 canvas the two photographs share — then mirrored, because ::before
   paints the photo with scaleX(-1), so everything lands on the opposite side:

     open   blade + black handle stand up from the hinge, fx 0.60-0.83 in the
            file => fx 0.17-0.40 on screen, from the top edge down to fy ~0.62.
     closed the lever lies flat: the gold bar spans fx 0-0.77 in the file =>
            fx 0.23-1.00 on screen, at fy 0.55-0.71 (the black handle is the
            part that overhangs the plate).

   Both rects stop above fy 0.742, where the connector circles on the posts
   begin, so a tap meant for a terminal is never caught by the lever. The rest
   of the box keeps pointer-events: none, so the empty headroom over the deck
   still lets taps through to the parts behind it. */
.car-body .switch-lever-hit {
    position: absolute;
    left: 14%;
    top: 0;
    width: 30%;
    height: 62%;
    pointer-events: auto;
    cursor: pointer;
}

.car-body .switch.closed .switch-lever-hit {
    left: 23%;
    top: 55%;
    width: 77%;
    height: 16%;
}

/* The photograph lives on a pseudo-element so it can be mirrored on its own.
   Flipping the switch itself would reverse the "Power Switch" caption it
   contains, and would overwrite the translate that anchors it. The plate is
   centred, so mirroring leaves it exactly where it was. */
.car-body .switch::before {
    content: '';
    position: absolute;
    inset: 0;
    transform: scaleX(-1);
    background: url('parts/switch_open.png') no-repeat center center;
    background-size: 100% 100%;
}

/* Same box, same size: the alignment is baked into the two files. */
.car-body .switch.closed::before {
    background-image: url('parts/switch_close.png');
}

/* Hover feedback via filter, not transform — the transform is doing the
   anchoring now, and switch.js used to overwrite it inline. */
.car-body .switch:hover {
    filter: brightness(1.25);
}

/* The lever again, repainted above the cables so the black wire runs BEHIND the
   switch. It has to be a second element out here: the real switch lives inside
   .car-chassis, which is a stacking context of its own (z-index + transform), so
   no amount of z-index on a cable in #circuitArea can slide it between the deck
   and one of the car's parts.
   Only the top of the image is shown — the lever, not the base plate — so the
   red cables still land visibly on the posts underneath. Position, size and the
   .closed state are copied from the real switch by syncSwitchOverlay(); the
   background rules mirror .car-body .switch::before exactly, or the copy would
   not line up with the original. */
.switch-lever-overlay {
    position: absolute;
    background-image: url('parts/switch_open.png');
    background-repeat: no-repeat;
    background-position: top center;
    background-size: 100% auto;   /* width of the switch box => its full height */
    transform: scaleX(-1);
    pointer-events: none;
    z-index: 12;                  /* over the cables (10/11), under the contacts (20) */
}

.switch-lever-overlay.closed {
    background-image: url('parts/switch_close.png');
}

/* The copy has to take the hover brightening too, or the lever would be the one
   part of the switch that stays dull while the rest lights up. */
.circuit-area:has(.car-body .switch:hover) .switch-lever-overlay {
    filter: brightness(1.25);
}

/* Lever and terminals are painted into the two switch images now. */
.car-body .switch-lever {
    display: none;
}

/* Crystal-dark chip (shared rule above) so it reads against the pale deck.
   Hung under the first section of the switch: its top edge is the base of the
   switch photograph (the box is bottom-anchored, so 100% is the image base),
   which puts the chip on the deck rather than over the part. Left-aligned with
   the plate, which is 69.8% of the canvas and centred, so its edge falls at
   ~15%. */
.car-body .switch-label {
    position: absolute;
    bottom: auto;
    top: 100%;
    left: 15%;
    transform: none;
    padding: 2px 6px;
    font-size: 0.62rem;
    font-weight: bold;
    color: #e8eef5;
}

.motor-body {
    width: 70px; /* Made bigger */
    height: 40px; /* Made bigger */
    background: linear-gradient(to bottom, #95a5a6, #7f8c8d, #34495e);
    border: 2px solid #2c3e50;
    border-radius: 8px; /* Slightly more rounded */
    position: relative;
    box-shadow: 
        inset 0 2px 4px rgba(255, 255, 255, 0.3),
        inset 0 -2px 4px rgba(0, 0, 0, 0.5),
        0 2px 6px rgba(0, 0, 0, 0.4);
}

.motor-shaft {
    position: absolute;
    width: 25px; /* Made bigger */
    height: 6px; /* Made longer */
    background: linear-gradient(to bottom, #ecf0f1, #bdc3c7);
    border: 1px solid #95a5a6;
    border-radius: 3px;
    left: -15%;
    top: 15px; /* Adjusted for bigger size */
    transform: translateX(-50%);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

/* Printed straight onto the motor can, like a spec label on the real part.
   The body of motor.png is bright everywhere (luminance 160-248) and brightest
   in the 25-42% band, so dark type reads cleanly there. 56.6% is the body
   centre — the shaft skews the image centre to the left. */
.motor-label {
    position: absolute;
    top: 33%;
    left: 56.6%;
    transform: translate(-50%, -50%);
    font-size: 0.62rem;
    font-weight: bold;
    letter-spacing: 0.03em;
    color: #1c2b38;
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.45);
    white-space: nowrap;
}

/* Propeller - the photographed part (parts/propeller.png, 185x707 => ratio
   3.822 tall). Its hub sits at the centroid of the image, so centring the
   element on the shaft tip also puts the axis of rotation there. */
.propeller {
    position: absolute;
    width: 34.8%; /* of the motor, so it tracks the shaft length */
    aspect-ratio: 185 / 707;
    left: var(--shaft-x);
    top: var(--shaft-y);
    transform: translate(-50%, -50%);
    background: url('parts/propeller.png') no-repeat center / 100% 100%;
    animation-duration: 0.22s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
}

.propeller.spinning {
    animation-name: propellerSpin;
}

/* Spin is faked as a mirror flip about the horizontal axis, not a rotation:
   the part is a flat photo in 3/4 perspective, and rotating it in the page
   plane would read as the whole propeller tipping over. Interpolating scaleY
   from 1 to -1 passes through 0 — the blades swap top for bottom, which is
   what a real propeller does turning on its shaft.
   The centring translate has to be repeated here, otherwise the animation
   would drop it and the propeller would jump off the shaft. */
@keyframes propellerSpin {
    0%   { transform: translate(-50%, -50%) scaleY(1); }
    50%  { transform: translate(-50%, -50%) scaleY(-1); }
    100% { transform: translate(-50%, -50%) scaleY(1); }
}

/* Blades are painted into propeller.png now. */
.propeller-blade {
    display: none;
}




/* Dotted demo of the first cable's run, played on load to show the gesture.
   Built from the same route the real cable takes, so it teaches the actual path
   rather than a sketch of one — but drawn as white dashes rather than in the
   cable's own colour, because it is a gesture being demonstrated, not a
   connection that exists yet. (startDemo passes 'red'; the dashes override it.) */
.circuit-wire-group.wire-demo .wire-casing,
.circuit-wire-group.wire-demo .wire-sheen,
.circuit-wire-group.wire-demo .wire-ferrule {
    display: none;
}

/* Dashes travelling along the path itself, so they follow the route round its
   corners instead of restarting at each one. */
.circuit-wire-group.wire-demo .wire-core {
    stroke: rgba(255, 255, 255, 0.85);
    stroke-width: 4;
    stroke-dasharray: 9 9;
    animation: demoDashes 1.8s linear infinite;
}

@keyframes demoDashes {
    to { stroke-dashoffset: -18; }
}

/* The travelling dot — a finger tracing the path. */
.wire-demo-dot {
    position: absolute;
    width: 16px;
    height: 16px;
    margin: -8px 0 0 -8px;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 35%, #fff 0%, #ffe9a8 45%, rgba(255, 208, 66, 0.2) 75%, transparent 78%);
    box-shadow: 0 0 12px 3px rgba(255, 208, 66, 0.8);
    pointer-events: none;
}

/* Caption for the demo. Same chip as the drag hint, so the two read as one
   voice, but parked under the route instead of chasing the pointer. */
.wire-demo-tip {
    position: absolute;
    z-index: 41;
    transform: translate(-50%, 14px);
    padding: 6px 10px;
    white-space: nowrap;
    border-radius: 8px;
    background: rgba(12, 18, 28, 0.92);
    border: 1px solid rgba(255, 215, 0, 0.45);
    backdrop-filter: blur(6px);
    color: #ffe9a8;
    font-size: 0.72rem;
    font-weight: bold;
    pointer-events: none;
    animation: demoTip 1.8s ease-in-out infinite;
}

@keyframes demoTip {
    0%, 100% { opacity: 0.75; }
    50%      { opacity: 1; }
}

/* Connection Points — small rings inside a gold bezel. The two fills are
   swapped from the usual convention on the battery and the motor: their
   negative terminals are the RED ones, their positive ones the BLACK ones (the
   default below). The switch is the exception — both its posts stay red, see
   the rule after this one — so the part reads the same whichever line it is
   wired into.
   Note this is colour only: analyzeCircuit()/validateCircuit() still work by
   topology and still ask the player for a red cable on the + side and a black
   one on the − side, so the cable colours and these dots no longer agree.
   connections.js hands us the exact terminal coordinate; the centring is done
   here, which is why every other transform below has to repeat the translate
   instead of replacing it. */
.connection-point {
    position: absolute;
    width: 13px;
    height: 13px;
    box-sizing: border-box;
    background: #14181e;
    border: 2px solid #ffd042;
    border-radius: 50%;
    cursor: pointer;
    z-index: 20;
    transform: translate(-50%, -50%);
    transition: background 0.15s ease, border-color 0.15s ease,
                box-shadow 0.15s ease, transform 0.15s ease;
    box-shadow:
        0 0 0 1px rgba(0, 0, 0, 0.55),
        0 0 6px rgba(255, 208, 66, 0.5);
}

.connection-point[data-terminal$="negative"] {
    background: #e03a2f;
}

/* The switch posts: red, both of them. They carry no polarity of their own —
   the names are -input / -output, not +/− — so they are not part of the swap
   above and must not inherit the black default. */
.connection-point[data-terminal^="switch-"] {
    background: #e03a2f;
}

/* Without this a touch drag starting on a terminal scrolls the page instead of
   pulling the cable — the workspace is scrollable on small screens. */
.connection-point {
    touch-action: none;
}

/* The terminal the cable would land on if released now. */
.connection-point.drop-target {
    transform: translate(-50%, -50%) scale(1.5);
    border-color: #fff3c4;
    box-shadow:
        0 0 0 1px rgba(0, 0, 0, 0.55),
        0 0 14px 3px rgba(255, 215, 0, 1);
}

/* The first contact tapped on touch. Bright and enlarged so it reads clearly
   over the finger while the user reaches for the second contact. */
.connection-point.selected {
    transform: translate(-50%, -50%) scale(1.5);
    border-color: #ffffff;
    box-shadow:
        0 0 0 2px rgba(0, 0, 0, 0.5),
        0 0 14px 4px rgba(120, 220, 255, 1);
    z-index: 46;
}

/* Dark glass chip for the switch caption, so the text carries its own surface
   and stays legible over the pale deck. */
.car-body .switch-label {
    background: rgba(12, 18, 28, 0.78);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid rgba(255, 255, 255, 0.22);
    border-radius: 4px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
    line-height: 1;
    white-space: nowrap;
    pointer-events: none;
}

/* Polarity is carried by colour now, so the +/- glyphs in the markup are no
   longer drawn. Six captions over the parts read as clutter at this scale. */
.connection-point > strong {
    display: none;
}

/* Hover brightens the bezel, never the fill — the polarity colour has to stay
   readable at all times. */
.connection-point:hover {
    transform: translate(-50%, -50%) scale(1.35);
    border-color: #fff3c4;
    box-shadow:
        0 0 0 1px rgba(0, 0, 0, 0.55),
        0 0 10px rgba(255, 215, 0, 0.9);
}

/* Connected: the fill keeps its polarity colour and the gold bezel thickens,
   so a fully wired circuit still shows which terminal is which. */
.connection-point.connected {
    border-width: 3px;
    box-shadow:
        0 0 0 1px rgba(0, 0, 0, 0.5),
        0 0 12px 2px rgba(255, 208, 66, 0.95);
}

/* Applied by wires.js while a wire is selected, so the small rings announce
   themselves exactly when the user is looking for somewhere to plug in. */
.connection-point.ready-for-connection {
    animation: readyPulse 1.1s ease-in-out infinite;
}

/* Glow only, no scale — a ring that changes size while a wire is held reads as
   jitter next to the parts it sits on. */
@keyframes readyPulse {
    0%, 100% {
        border-color: #ffd042;
        box-shadow:
            0 0 0 1px rgba(0, 0, 0, 0.55),
            0 0 6px rgba(255, 208, 66, 0.6);
    }
    50% {
        border-color: #fff3c4;
        box-shadow:
            0 0 0 1px rgba(0, 0, 0, 0.55),
            0 0 16px 3px rgba(255, 208, 66, 0.95);
    }
}

/* The cable being pulled: same construction as a real one, just translucent so
   it reads as "not committed yet". */
.circuit-wire-group.wire-preview {
    opacity: 0.65;
    pointer-events: none;
}

/* Hint bubble that follows the cable end. A channel of its own, separate from
   the Guide card: that one reports the circuit, this one narrates the gesture. */
.drag-hint {
    position: absolute;
    z-index: 40;
    margin: -38px 0 0 14px;
    padding: 6px 10px;
    max-width: 190px;
    border-radius: 8px;
    background: rgba(12, 18, 28, 0.92);
    border: 1px solid rgba(255, 215, 0, 0.45);
    backdrop-filter: blur(6px);
    color: #ffe9a8;
    font-size: 0.72rem;
    font-weight: bold;
    line-height: 1.25;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s ease;
}

.drag-hint.visible {
    opacity: 1;
}

/* First beat: scissors nibbling at the insulation. */
.drag-hint.stripping::before {
    content: '✂️ ';
    display: inline-block;
    animation: snip 0.4s ease-in-out infinite;
}

@keyframes snip {
    0%, 100% { transform: rotate(-12deg); }
    50%      { transform: rotate(8deg); }
}

/* Circuit Wires — one group per connected wire, holding a single SVG path
   traced along the whole route (see buildWireGroup in wires.js for why it is
   one path and not a rotated div per leg). */
.circuit-wire-group {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

/* No viewBox on the element, so one SVG user unit is one CSS pixel and the
   route coordinates from connections.js can be written straight into the path.
   overflow visible because a stroke is centred on its path: half of it hangs
   outside the box wherever a cable runs along the edge of the board. */
.wire-svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.45));
    transition: filter 0.3s ease;
}

/* Round join AND round cap, on all three strokes: the join is what removes the
   corner seam, the cap is what lets the cable end flush on its terminal. */
.wire-svg path {
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Stacked in the order they are appended: casing widest and darkest at the
   bottom, the cable colour over it, a thin bright core on top. Together they
   give a round cable whose shading follows the run rather than the leg. */
.wire-casing { stroke-width: 10; }
.wire-core   { stroke-width: 8; }
.wire-sheen  { stroke-width: 2.4; opacity: 0.45; }

/* Three live colours here, unlike the other labs: pickWireFor paints a cable
   when it is picked up, and a second red cable takes the lighter shade so two
   lying side by side stay tellable apart. Both still read as "Red Wire" to the
   player, and isRedWire() in game-logic.js accepts either. */
.red-circuit .wire-casing { stroke: #a93226; }
.red-circuit .wire-core   { stroke: #e74c3c; }
.red-circuit .wire-sheen  { stroke: #ff9d93; }

.white-circuit .wire-casing { stroke: #c9483c; }
.white-circuit .wire-core   { stroke: #f4776a; }
.white-circuit .wire-sheen  { stroke: #ffc4bc; }

.black-circuit .wire-casing { stroke: #11181f; }
.black-circuit .wire-core   { stroke: #2c3e50; }
.black-circuit .wire-sheen  { stroke: #7c8b9b; }

/* The stripped copper end, where the cable meets a terminal. Mostly hidden
   under the connection point itself (which is wider than this) — what shows is
   a brass rim around it, which is the point: it says the cable is landed. */
.wire-ferrule {
    fill: #e3a83a;
    stroke: #8b5a17;
    stroke-width: 1;
}

/* Lit by game-logic.js marking the group, so the whole run glows as one. The
   black cast shadow goes while it is lit — the glow stands in for it.
   Deliberately restrained: a drop-shadow traces the cable's outline exactly,
   where the old per-segment box-shadow blurred a rectangle around it, so the
   same numbers now read far stronger. */
.circuit-wire-group.active .wire-svg {
    filter: drop-shadow(0 0 3px rgba(46, 204, 113, 0.85))
            drop-shadow(0 0 8px rgba(46, 204, 113, 0.35));
}

/* The wire (or wires) forming a short circuit flash white hot. An animation, so
   it beats the plain .active declaration above whatever the source order: a
   shorted wire always reads as the alarm, never as running. */
.circuit-wire-group.shorted .wire-svg {
    animation: wireShort 0.32s ease-in-out infinite;
}

@keyframes wireShort {
    0%, 100% {
        filter: brightness(1.3) drop-shadow(0 0 8px rgba(255, 90, 30, 0.85));
    }
    50% {
        filter: brightness(2.6) saturate(0.4)
                drop-shadow(0 0 18px rgba(255, 220, 120, 1));
    }
}


/* Electron Animation */
.electron {
    position: absolute;
    width: 6px;
    height: 6px;
    background: radial-gradient(circle, #ffffff 0%, #00ffff 40%, #0099ff 100%);
    border: 1px solid #00ccff;
    border-radius: 50%;
    z-index: 15;
    box-shadow: 
        0 0 8px rgba(0, 255, 255, 0.9),
        0 0 16px rgba(0, 153, 255, 0.6),
        inset 0 0 4px rgba(255, 255, 255, 0.8);
    transition: transform 0.1s ease-out;
    animation: electronGlow 1s ease-in-out infinite alternate;
}

@keyframes electronGlow {
    0% { 
        opacity: 0.8;
        box-shadow: 
            0 0 4px rgba(0, 255, 255, 0.7),
            0 0 8px rgba(0, 153, 255, 0.4),
            inset 0 0 2px rgba(255, 255, 255, 0.8);
    }
    100% { 
        opacity: 1;
        box-shadow: 
            0 0 8px rgba(0, 255, 255, 1),
            0 0 16px rgba(0, 153, 255, 0.8),
            inset 0 0 4px rgba(255, 255, 255, 1);
    }
}

/* Reset Button */
.reset-button {
    position: absolute;
    top: 15px;
    right: 15px;
    padding: 8px 16px;
    background: #e74c3c;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 0.9rem;
    z-index: 50;
    transition: background 0.3s ease;
}

.reset-button:hover {
    background: #c0392b;
}

/* Educational Info */
.concept-explanation {
    font-size: 0.85rem;
    line-height: 1.4;
    color: #ecf0f1;
}

.concept-explanation strong {
    color: #f39c12;
}

/* Mobile Responsive */
@media (max-width: 1200px) {
    /* Stacked layout can't fit one viewport — allow normal page scroll */
    body {
        height: auto;
        overflow: auto;
    }

    /* Header stacks: brand on top, then the title block, both full width. */
    .game-header {
        flex-direction: column;
        gap: 10px;
    }

    /* The spacer exists to keep the title optically centred beside the logo.
       Stacked there is nothing to balance, and it would just add dead height. */
    .game-header::after {
        display: none;
    }

    .brand-link {
        width: min(200px, 55%);
    }

    .game-container {
        flex-direction: column;
        flex: none;
        min-height: 0;
    }

    /* The left column dissolves so its three cards become items of the
       container itself — only then can `order` interleave them with the
       workspace, which sits between them in the reading order below. */
    .info-panel.left-panel {
        display: contents;
    }

    .circuit-section             { order: 1; }  /* the lab */
    .feedback-box                { order: 2; }
    .status-panel                { order: 3; }
    .instructions-link           { order: 4; }  /* stays right under Car Status */
    .info-panel:not(.left-panel) { order: 5; }  /* the reference panels */
    .kit-panel                   { order: 6; }  /* shop link, last */

    /* Everything full width and stacked; nothing scrolls sideways. */
    .info-panel {
        width: 100%;
        flex-direction: column;
        overflow: visible;
    }

    .panel,
    .feedback-box,
    .instructions-link {
        width: 100%;
        min-width: 0;
    }

    .feedback-box {
        font-size: 0.8rem;
    }

    .circuit-section {
        min-width: 0;
    }

    .circuit-area {
        height: 450px;
        min-height: 400px;
        min-width: 0;
        width: 100%;
        overflow-y: auto;
        padding: 20px;
    }

    /* One knob. Every part sizes and anchors itself in percentages, so the
       whole car — deck, motor, propeller, battery, switch, wheels — follows.
       Capped by the available width so it never spills on a narrow phone. */
    .car-chassis {
        width: min(418px, 95%); /* +10% over 380; 95% still caps narrow phones */
    }

    .connection-point {
        width: 12px;
        height: 12px;
    }

    .reset-button {
        top: 10px;
        right: 10px;
        padding: 6px 12px;
        font-size: 0.8rem;
    }
}

/* ==========================================================================
   Immersive mode — the lab, full screen and sideways, on touch devices.

   The stacked mobile layout squeezes the workspace into a 450px band between
   the header and the reference panels, and makes it scroll internally. That is
   enough to read, not enough to build in. So on touch the lab is veiled until
   the player opts in, and opting in lifts the whole workspace out to fill the
   screen.

   One `.is-immersive` class drives both paths. On Android the class rides on
   top of a real Fullscreen API request plus an orientation lock; on iOS Safari,
   where neither exists for non-<video> elements, the same class alone produces
   a fixed, viewport-filling overlay and the rotate prompt asks for the turn the
   browser will not do itself. Nothing here rotates the lab with a transform:
   wire geometry is read straight off getBoundingClientRect() (connections.js),
   and a rotated ancestor would put every terminal and every pointer coordinate
   in the wrong place.
   ========================================================================== */

/* Hidden by default; the touch media query below opts them in. */
.lab-start-veil,
.rotate-prompt,
.lab-exit-btn {
    display: none;
}

/* --- The veil -------------------------------------------------------- */
.lab-start-veil {
    position: absolute;
    inset: 0;
    z-index: 60;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    border-radius: 15px;                 /* matches .circuit-area */
    background: rgba(20, 26, 36, 0.45);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    text-align: center;
    padding: 20px;
}

.lab-start-btn {
    min-height: 52px;
    padding: 14px 28px;
    border: 2px solid #ffd042;
    border-radius: 999px;
    background: linear-gradient(135deg, #f6b60c, #e07b00);
    color: #1a1206;
    font-size: 1.05rem;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45), 0 0 0 6px rgba(255, 208, 66, 0.12);
    animation: labStartPulse 2.2s ease-in-out infinite;
    touch-action: manipulation;
}

@keyframes labStartPulse {
    0%, 100% { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45), 0 0 0 6px rgba(255, 208, 66, 0.12); }
    50%      { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45), 0 0 0 14px rgba(255, 208, 66, 0.03); }
}

.lab-start-note {
    color: #ffe9a8;
    font-size: 0.8rem;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}

/* --- The immersive shell --------------------------------------------- */
.circuit-section.is-immersive {
    position: fixed;
    inset: 0;
    z-index: 9999;
    width: 100vw;
    height: 100vh;
    height: 100dvh;            /* so Safari's chrome does not crop the board */
    margin: 0;
    padding: 0;
    border-radius: 0;
    background: #0e1117;
}

/* The stacked layout lets the area scroll internally; here it fills the screen
   instead, so there is nothing to scroll to. */
.circuit-section.is-immersive .circuit-area {
    width: 100%;
    height: 100%;
    min-height: 0;
    padding: 0;
    border-radius: 0;
    overflow: hidden;
}

/* Stop the page underneath from scrolling behind the overlay. */
body.lab-immersive {
    overflow: hidden;
}

.lab-exit-btn {
    position: absolute;
    top: 10px;
    left: 10px;
    /* Above the rotate prompt (80): a player who does not want to turn the
       phone still has to be able to get out. */
    z-index: 90;
    width: 38px;
    height: 38px;
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 50%;
    background: rgba(12, 18, 28, 0.85);
    color: #ffe9a8;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    touch-action: manipulation;
}

.circuit-section.is-immersive .lab-exit-btn {
    display: block;
}

/* --- "Turn your phone" ----------------------------------------------- */
/* Only while immersive AND upright. The car is a wide scene, so rather than
   show it squeezed the prompt covers it outright. */
@media (orientation: portrait) {
    .circuit-section.is-immersive .rotate-prompt {
        display: flex;
        position: absolute;
        inset: 0;
        z-index: 80;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 22px;
        background: #0e1117;
        color: #ffe9a8;
        text-align: center;
        padding: 24px;
    }
}

.rotate-phone {
    width: 54px;
    height: 92px;
    border: 3px solid #ffd042;
    border-radius: 10px;
    box-shadow: inset 0 0 0 2px rgba(255, 208, 66, 0.2);
    animation: rotatePhone 2.4s ease-in-out infinite;
}

@keyframes rotatePhone {
    0%, 25%  { transform: rotate(0deg); }
    55%, 85% { transform: rotate(-90deg); }
    100%     { transform: rotate(0deg); }
}

/* ==========================================================================
   Touch-only wiring for the above, plus the immersive scene scale.
   ========================================================================== */
@media (max-width: 1200px) and (pointer: coarse) {
    .lab-start-veil {
        display: flex;
    }

    /* Once the player is inside, the veil has done its job. */
    .circuit-section.is-immersive .lab-start-veil {
        display: none;
    }

    /* Still one knob. The chassis is 360/230, so on a landscape phone height is
       what binds, not width — hence the vh cap ahead of the vw one. */
    .circuit-section.is-immersive .car-chassis {
        width: min(92vw, 130vh);
    }

    /* A finger-sized target, now that there is room for one. */
    .circuit-section.is-immersive .connection-point {
        width: 18px;
        height: 18px;
    }

    .circuit-section.is-immersive .reset-button {
        top: 12px;
        right: 12px;
    }
}

/* ==========================================================================
   Tap guide — the opening demo on touch devices.

   The dotted trace shows a drag, but a drag is not the gesture here: wires.js
   switches to tap-one-contact-then-the-other on coarse pointers. So on touch
   the trace is replaced by a hand that taps the first contact, travels to the
   second and taps again — the gesture, performed.
   ========================================================================== */
.tap-demo {
    position: absolute;
    inset: 0;
    z-index: 41;
    pointer-events: none;
}

.tap-finger {
    position: absolute;
    left: 0;
    top: 0;
    width: 34px;
    height: 34px;
    margin: 2px 0 0 -5px;     /* the fingertip, not the box, sits on the contact */
    opacity: 0;
    transform-origin: 50% 0;
    /* Positional moves are the "travel" beat; opacity and scale are the taps. */
    transition: left 600ms cubic-bezier(0.4, 0, 0.2, 1),
                top 600ms cubic-bezier(0.4, 0, 0.2, 1),
                opacity 220ms ease,
                transform 140ms ease;
    filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.7));
}

.tap-finger.is-visible  { opacity: 1; }
.tap-finger.is-pressing { transform: scale(0.82); }

.tap-ripple {
    position: absolute;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    border: 2px solid rgba(120, 220, 255, 0.95);
    border-radius: 50%;
    opacity: 0;
    animation: tapRipple 620ms ease-out forwards;
}

@keyframes tapRipple {
    0%   { transform: scale(0.4); opacity: 1; }
    100% { transform: scale(3.4); opacity: 0; }
}