/* Violet and the Grumpy Nisse — game-local styles only.
 *
 * The header, start screen, AI badge, tool panels and dialog shell all come from
 * ../core/deckades.css, which is linked BEFORE this file. Nothing here restyles them.
 *
 * Where the colours came from
 * ---------------------------
 * --dk-accent   #d064d8  Agie Games' own spot violet, sampled off the section banners in
 *                        rules_EN.pdf (median #7d568a, hue 285) and lifted for legibility on
 *                        a near-black panel — 5.8:1 at 12px. Rotated 285 -> 296 to open the
 *                        gap against Doppelkopf's pale lilac #ab7ce0 (hue 268).
 *                        Honest note: 28 deg from Doppelkopf and 33 from Hearts #e0559c, both
 *                        just under the house 35 deg rule. There is no >=35 deg slot left
 *                        outside the green band (60-140), and this game — a purple night
 *                        forest named "Violet" — has no green in it. Distinct in saturation
 *                        and weight rather than in hue alone.
 * --dk-tint     the night sky of the cover art, #2c2448.
 * card colours  sampled off the printed diamonds in assets/board.jpg, so the cards in hand
 *               match the areas on the board exactly. They differ in LIGHTNESS as well as
 *               hue (39/47/29/48), and every one also carries its own printed icon, so no
 *               game state is ever carried by colour alone.
 */

:root {
    --dk-accent:      #d064d8;
    --dk-accent-soft: rgba(208, 100, 216, 0.34);
    --dk-tint:        rgba(44, 36, 72, 0.28);

    /* == THE START SCREEN IS A LID AND A CARD ====================================
     *
     * Tokens only — not one .start-* rule lives in this file (the grep at the top of it
     * stays empty). Read the --dk-start-* contract in ../core/deckades.css.
     *
     * WHY it is skinned at all: assets/cover.webp IS the title of this screen. Karina
     * Duncan's painting carries "Violet and the Grumpy Nisse" in hand-lettered gold across
     * its middle, and the screen used to bury it under an 0.86 scrim, a 2px backdrop blur
     * and a 1.18 zoom that cropped the letters off both ends — and then printed a SECOND
     * title, "VIOLET" in Cormorant, on the card in front of it. Two titles in the same
     * 200 pixels, and the worse one won because it was the lit one.
     *
     * So the painting stands up behind and IS the title; what lies in front of it is the
     * card you set the game up on. Everything below serves that.
     *
     * HOW FAR THE PAINTING IS LIFTED, and why it is one expression rather than a media
     * query per shape. The art is SQUARE and its lettering sits at 34%-51% of its own
     * height — almost the middle — so on a landscape screen, where `cover` scales the
     * square to the full viewport WIDTH, the gold lands exactly where the card is going to
     * be. The lift is therefore derived from the one thing that has to be true:
     *
     *     bottom of the lettering  <=  top of the card
     *     0.515 * 100vw - lift     <=  100vh - (card 348px + inset 6vh + 38px of air)
     *
     * solved for the lift, clamped at 0 (portrait needs none — the square already fits the
     * width and the whole painting shows, moon included) and capped at the overflow
     * 100vw - 100vh, past which the art would lift off the bottom edge of the screen and
     * leave a strip of nothing under the card. Every landscape shape from 1024x768 to
     * 2560x1440 therefore lands the lettering ~40px clear of the card, and no shape
     * letterboxes. The price on landscape is the moon, which the lift carries off the top;
     * portrait keeps it. */
    --vi-lid-y: calc(-1 * clamp(0px, 51.5vw - 94vh + 386px, 100vw - 100vh));

    /* The backdrop is a GRADE, not a wash: near-clear through the band the lettering is in,
       closing at the foot so the card has something to sit on. The numbers are low because
       three layers multiply here — the felt's own wash under the painting, body::after's
       vignette, and this — and because the painting is a NIGHT SCENE that arrives dark. */
    --dk-start-scrim: linear-gradient(180deg,
                          rgba(7, 4, 16, 0.12) 0,
                          rgba(7, 4, 16, 0.08) 34%,
                          rgba(7, 4, 16, 0.20) 70%,
                          rgba(5, 3, 12, 0.30) 100%);
    --dk-start-blur:  none;             /* the art has to stay sharp to be a title */
    /* The header goes with it. It is unreachable behind the start screen anyway (bar at
       z-index 300, screen at 1200), so lighting it only puts a second, inert wordmark
       across the top of a full-bleed painting. */
    --dk-start-header: none;
    --dk-start-place:  flex-end;        /* the card lies at the near edge of the table */
    /* VERTICAL ONLY. A single value is padding on all four sides, and 51px of it down each
       side of a 390px phone left the 420px card 288px to live in — the fitter then scaled it,
       and the Play button with it, below the 44px tap floor. The card's own `max-width: 92vw`
       already keeps it off the side edges. */
    --dk-start-inset:  clamp(22px, 6vh, 64px) 0px;
    /* Bottom-aligned, so fitCard()'s scale has to grow from the bottom too. */
    --dk-start-origin: bottom center;

    /* The card is the night sky the painting is set in, near-opaque so 13px of hint copy
       never has a tree behind it. Same construction as the platform's dialog shell. */
    --dk-start-surface: linear-gradient(176deg,
                            rgba(32, 22, 54, 0.965), rgba(15, 10, 28, 0.975));
    /* A HAIRLINE, not the accent at full strength. The default draws the card's border in
       raw --dk-accent, which over near-black reads as an edge and over a painting reads as
       a magenta outline traced round a rectangle — and it put a fourth magenta object on a
       screen that only wants one. The system's own Hairline Rule: 1px, low alpha,
       hue-tinted. The Play button keeps the accent, and is then the only thing wearing it. */
    --dk-start-edge:   var(--dk-accent-soft);
    /* ...which the language picker cannot inherit: with the card's rule softened, the
       CURRENT language would be outlined fainter than the two beside it. This token exists
       for exactly that, and the accent is right here — the picker sits ON the card. */
    --dk-start-lang-on: var(--dk-accent);
    --dk-start-radius: var(--dk-radius-lg);
    /* Lamplight on the card's edge, and a real cast shadow with offset and blur under it —
       the long one because the card is lying on a forest floor, not floating in a halo. */
    --dk-start-glow:
        0 0 26px rgba(208, 100, 216, 0.20),
        0 14px 30px rgba(4, 2, 10, 0.62),
        0 40px 74px rgba(4, 2, 10, 0.46);
    /* The default's 52px top reserves room under the language corner for a 42px nowrap
       title, and its 62px foot reserves an achievements corner. This card has neither. */
    --dk-start-pad:        40px 32px 30px;
    /* 48, not 38: on the stacked card the subtitle is the FIRST row and it is centred across
       the full width, so it ran under the language flag once the title stopped holding it
       down. The corner occupies y = 12..44; clear it. See the short-screen block for the
       viewport where there is no subtitle and this can go back to being tight. */
    --dk-start-pad-narrow: 48px 20px 22px;

    /* The four suits, straight off the board scan. Named as the printed cards name them
       (assets/original/<Suit>-card-N.pdf), not by hue: the game speaks of the lake, the
       forest, the mountains and the crystals, and so does the artwork. */
    --vi-lake:     #15b4a7;
    --vi-forest:   #cf601f;
    --vi-mountain: #6b3f9e; /* the board diamond is #5d256e; lifted so a value reads on it */
    --vi-crystal:  #b83f87;
    --vi-yellow: #dda542;   /* neutral spaces */
    --vi-fire:   #de4534;   /* Ordep's fire */

    /* THE RAIL. The lantern track across the top of the board is a gold double rule with
       rounded caps over the night sky, holding teal roundels -- it is the one shape this
       painting uses for "a strip that tells you something", and the help banner is drawn in
       it. Sampled off the scan, not picked beside it. */
    --vi-rail:      #c9a24a;
    --vi-rail-soft: rgba(201, 162, 74, .42);
    --vi-night-hi:  rgba(40, 30, 72, .95);
    --vi-night-lo:  rgba(21, 14, 43, .96);

    --vi-ink:    #f4ecdd;
    --vi-plate:  rgba(16, 11, 26, 0.90);
    --vi-plate-line: rgba(208, 100, 216, 0.26);

    /* The glide easing, named once because all three pieces share it. */
    --vi-move-ease: cubic-bezier(.22, .75, .3, 1);
    --vi-speed: 1;              /* game.js overrides from ?speed=N */

    --card-scale: 1;
    --opp-scale: 0.66;
    --play-scale: 0.92;
    --card-w: calc(clamp(38px, 8.6vw, 72px) * var(--card-scale));
    --card-h: calc(var(--card-w) * 1.496);   /* the printed card is 172.8 x 258 pt */
}

/* The cover art belongs behind the START SCREEN only — in play the table is the shared felt.
 *
 * The fallback colour goes on <html>, NOT on <body>: body::before carries the art at a
 * negative z-index, and an opaque `body { background }` can hide it outright. */
html { background: #0a0712; }
body { margin: 0; overflow: hidden; }

/* == The table under the start screen ========================================
 * The painting is square and a landscape screen is not, so in portrait there is a band of
 * screen below it and on a very wide one there is a little beside it. That void is not a
 * gap to be closed, it is the TABLE — the same felt every other Deckades game is played on,
 * under Violet's own night-sky tint — and the two objects on this screen, the painting
 * propped at the back and the card lying in front of it, finally have something to lie on.
 *
 * Scoped with :has() so it lasts exactly as long as the start screen does; once the game is
 * up, #game carries its own cover/felt blend over the whole viewport.
 *
 * Written out rather than reaching for --dk-table because the wash is one notch LIGHTER
 * than the shared one (0.55/0.62 against 0.70/0.84): two more dimming layers are laid over
 * this screen — body::after's vignette and --dk-start-scrim — and three coats of the
 * platform's own weight took the foot of the screen to near-black. */
html:has(#start-screen) {
    background:
        radial-gradient(ellipse at 50% 34%, var(--dk-tint), transparent 66%),
        linear-gradient(rgba(11, 8, 20, 0.55), rgba(7, 5, 15, 0.62)),
        url('../table.avif') center / cover no-repeat fixed,
        #0a0712;
}

/* == The lid =================================================================
 * assets/cover.webp IS the title of this screen (see --vi-lid-y up in :root for the whole
 * argument, and for how far it is lifted), so it gets the whole width: sized to 100vw and
 * pinned to the top, which is `cover` on a landscape screen and the complete square on a
 * portrait one. Longhands, not the shorthand, because the vertical offset is a token and a
 * three-part `center top var(--x) / 100vw auto` position inside `background` is a parse
 * hazard for no gain.
 *
 * The 1.18 scale that used to be here is gone. It was a zoom on a full-bleed `cover`, which
 * on a landscape screen threw away another 9% of the width off each side — and what lives
 * in the outer ninth of this painting is the first and last letters of its own title. */
body::before {
    content: ''; position: fixed; inset: 0; z-index: -2;
    /* .webp, not the 2362px .png this used to load. The painting is now the first thing
       drawn and the largest, and 1.68 MB of PNG in front of a start screen is 1.68 MB the
       player waits on; the same picture at 2048 square is 265 KB with nothing visible lost
       off the gold. cover.png stays in the folder as the source — build_android.py already
       excludes it from a packaged build and ships the .webp. */
    background-image: url('assets/cover.webp');
    background-repeat: no-repeat;
    background-size: 100vw auto;
    background-position: center top var(--vi-lid-y);

}

/* PORTRAIT ONLY: the square painting cannot reach the bottom of a taller-than-wide screen at
   all, so it ends on a printed edge with the felt directly under it — a seam, plainly
   visible in the strips of table either side of the card. Its last 130px dissolve instead,
   so the forest recedes into the table rather than stopping on a cut.

   NOT in landscape, where the painting does reach the bottom edge: masking it there uncovers
   the felt along the foot of a full-bleed picture, which reads as a leak rather than as a
   table. body::after's floor gradient does that job instead — see the note on it above. */
@media (orientation: portrait) {
    body::before {
        -webkit-mask-image: linear-gradient(180deg,
            #000 0, #000 calc(100vw - 130px), transparent 100vw);
        mask-image: linear-gradient(180deg,
            #000 0, #000 calc(100vw - 130px), transparent 100vw);
    }
}

/* Deliberately LIGHT — it only vignettes, so the corners fall away and the two lit objects
   sit forward of the table. It composes multiplicatively with --dk-start-scrim, and there
   was a revision of this file that carried NO vignette at all because the scrim was 0.86
   and the picture was already gone. The scrim is a grade now, so this can do its own job. */
body::after {
    content: ''; position: fixed; inset: 0; z-index: -1;
    background:
        /* THE FOREST FLOOR. In LANDSCAPE the painting reaches the bottom edge of the screen
           and what is down there is its own signature — the designer's and illustrator's
           names at the left, the publisher's mark at the right — with the viewport cutting
           through the middle of that line, which reads as stray text rather than as a
           credit. So the night closes over it: near-opaque at the very edge and gone by
           165px, which is the card's own cast shadow pooling on the forest floor rather than
           a band, and which is mostly BEHIND the card anyway — only the two corners of it
           are ever on show. See the note on the mask below for why this is not a mask here:
           masking the picture out in landscape uncovers the felt, and a strip of tabletop
           along the bottom of a full-bleed painting reads as a leak, not as a table. */
        linear-gradient(0deg,
            rgba(8, 5, 16, 0.96) 0, rgba(8, 5, 16, 0.82) 44px, rgba(8, 5, 16, 0.52) 92px,
            rgba(8, 5, 16, 0.22) 145px, rgba(8, 5, 16, 0) 205px),
        /* THE CANOPY. The lift carries the moon's face off the top of the screen and, on a
           landscape shape between about 1.3 and 1.7, leaves 30-50px of its chin behind — a
           face cut through by the screen edge, which reads as a rendering fault rather than
           as a crop. Rather than lose the moon everywhere (portrait keeps it whole) the top
           of the night closes over it: steep, short, and gone by 130px. */
        linear-gradient(180deg,
            rgba(6, 4, 14, 0.50) 0, rgba(6, 4, 14, 0.22) 54px, rgba(6, 4, 14, 0) 130px),
        radial-gradient(ellipse at 50% 32%,
            rgba(6, 4, 14, 0) 0%, rgba(6, 4, 14, 0.11) 54%, rgba(4, 2, 10, 0.38) 100%);
}

/* Once the table is up the painting has done its job — #game blends its own copy of the art
   into the felt — and two layers of it would only fight through #game's own translucency. */
body:not(:has(#start-screen))::before,
body:not(:has(#start-screen))::after { opacity: 0; }

/* Short screens — landscape phones, the platform's 640x360 floor. There is no composition
   to make here: the shared sheet stacks the card's columns below 540px tall and the card
   then owns almost the whole viewport, so the painting is behind it whatever the lift says.
   It gives up the lift, the card goes back to centre with a small inset, and the backdrop
   goes back to a flat wash — a grade needs room to grade in. */
@media (max-height: 620px) {
    :root {
        --vi-lid-y:        calc(-1 * clamp(0px, 8vw, 100vw - 100vh));
        --dk-start-place:  center;
        --dk-start-origin: center center;
        --dk-start-inset:  6px;
        --dk-start-scrim:  linear-gradient(180deg,
                               rgba(7, 4, 16, 0.44) 0,
                               rgba(6, 4, 14, 0.52) 100%);
    }
}

/* 429px, matching start.js's own `roomy` threshold exactly rather than the 620 above: below
   it there is NO subtitle, so the card's top pad no longer has to duck the language corner —
   it only has to coexist with the settings label, which shares the row rather than sitting
   under the flag. Every pixel back is a pixel of Play button, which the shared sheet's stack
   -and-scale is already squeezing at the 640x360 floor (36px before this pass, 46px after). */
@media (max-height: 429px) {
    :root { --dk-start-pad-narrow: 24px 20px 16px; }
}

/* The table: the game's own night sky blended ONTO the shared felt, not laid over it.
 *   1. a LIFT, where there used to be a wash. It was a dark gradient at 0.62/0.80, halved to
 *      0.30/0.48, and thinning it further stopped helping: measured off a screenshot, the
 *      felt around the board sat at luminance ~9 and going to 0.18/0.32 moved it about four
 *      per cent, because what is dark here is the shared felt itself and --dk-table is not a
 *      token a game may re-declare. So this layer is now a warm near-white at 0.075/0.035
 *      instead, which roughly doubles the margin (~9 to ~19-28) and lets the moon and the
 *      mountains in the picture below actually read. It only ever shows in the MARGIN -- the
 *      board is its own element on top -- and the cards, at 100+, stay far and away the
 *      brightest thing on screen, which is what the dark coats were protecting
 *   2. assets/table.jpg — the cover's upper part: the moon, the mountains, the treetops and
 *      Ordep's fire on the right. Cropped above the wordmark, so no lettering ghosts onto
 *      the felt.
 *   3. var(--dk-table) — the shared felt, from the token, never a re-typed URL.
 * soft-light takes the light and shade of one layer and the hue of the other, so the picture
 * and the wood read as ONE surface; `normal` would look like a photo propped up behind the
 * cards. The wash is a near-black in the palette's own hue, never pure black. */
#game {
    position: absolute; inset: 0;
    background:
        linear-gradient(rgba(255, 244, 222, 0.075), rgba(255, 240, 214, 0.035)),
        url('assets/table.jpg') center top / cover no-repeat fixed,
        var(--dk-table);
    background-blend-mode: normal, soft-light, normal;
    /* `manipulation`, NOT `none`. `none` here killed PINCH-ZOOM for the whole match: #game
       covers the viewport, and touch-action on the element the gesture starts over decides
       whether the browser gets it at all. Nothing in here is dragged except the tray grip,
       which declares its own `none`, so the page had nothing to gain and a zoomed-in player
       no way back out. `manipulation` still drops the double-tap-zoom delay on every tap. */
    touch-action: manipulation;
    display: none;                       /* revealed by init() */
}
#game.live { display: block; }

/* ── the board ─────────────────────────────────────────────────────────────── */

#board { position: absolute; border-radius: 10px; overflow: hidden;
         box-shadow: 0 10px 32px rgba(0, 0, 0, 0.62), 0 0 0 1px rgba(208, 100, 216, 0.16); }
#board-img { width: 100%; height: 100%; display: block; user-select: none; }
#board-marks, #board-pawns { position: absolute; inset: 0; pointer-events: none; }

/* The publisher's mark. Sized as a percentage of the board's WIDTH so it holds its place on
   the painting at every board size, and squared off its own 150x150 art rather than left to
   the board's 1.98 aspect. 6% keeps it clear of the first journey diamond at 8.7% across:
   the logo spans 1.2%..7.2%, the diamond starts at about 6.5%, and the two only graze.
   At .7 it is 30% transparent -- present as a credit, not competing with the board. */
#board-logo {
    position: absolute; left: 0.4%; bottom: 1.6%;
    width: 6%; height: auto; aspect-ratio: 1;
    opacity: .45; pointer-events: none; user-select: none;
}

/* pawns and overlays are placed by JS in board-image percentage coordinates */
/* HOW LONG A PIECE TAKES TO GET THERE.
 *
 * All three used to glide in 380ms, which is a UI transition rather than a move — fast enough
 * that on a board this size the piece is simply somewhere else, and the one thing the glide
 * exists to show is the DISTANCE it covered. The lantern is the clearest case: losing two
 * units of fuel and losing five look identical if the marker teleports, and that difference
 * is the whole state of the game.
 *
 * Three different speeds, because the three pieces are doing three different things:
 *   Violet WALKS — a step or two, deliberate, and it is the player's own move.
 *   Torech's fear CREEPS — the slowest thing on the board, and it is closing on her.
 *   The lantern DRAINS — the longest, because it is the only one that regularly crosses
 *   several spaces at once and the length of that slide IS the reading.
 *
 * All three are divided by --vi-speed, which game.js sets from ?speed=N, so the Playwright
 * driver is unaffected by any of this. The easing is unchanged: a heavy ease-out, so a piece
 * leaves quickly and arrives slowly, which is what makes it land somewhere rather than stop. */
.pawn { position: absolute; transform: translate(-50%, -50%); pointer-events: none;
        transition: left var(--vi-move) var(--vi-move-ease),
                    top  var(--vi-move) var(--vi-move-ease); }
.pawn-violet  { --vi-move: calc(860ms  / var(--vi-speed, 1)); }
.pawn-fear    { --vi-move: calc(1040ms / var(--vi-speed, 1)); }
.pawn-lantern { --vi-move: calc(980ms  / var(--vi-speed, 1)); }
@media (prefers-reduced-motion: reduce) { .pawn { transition: none; } }
/* THE THREE PIECES. Drawn in game.js (PAWN_ART) and coloured here; read the note above that
   map for why they are silhouettes rather than the three coloured lozenges they used to be.
   The glow is a `drop-shadow` on the SVG, not a `box-shadow` on the box, so the light comes
   off the FIGURE — a halo round an invisible rectangle was most of what made the old pieces
   read as markers dropped on the art rather than as things standing on it.

   Widths are of the BOARD, and the board is 640px wide at the platform's 1024x768 floor, so
   these are ~22, ~20 and ~19 CSS pixels tall. Every decision in the art follows from that. */
.pawn svg { width: 100%; height: 100%; display: block; overflow: visible; }
/* The scanned tokens are ROUND and fill their frame, so the board pieces are squares now
   rather than the three standing-figure ratios above. The widths went up with them: a
   3.3%-wide meeple stood 4.4% of the board TALL, and a 1:1 token at the same width would
   have been the smaller mark on the same space. */
.pawn img { width: 100%; height: 100%; display: block; object-fit: contain;
            user-select: none; -webkit-user-drag: none; }
.pawn-violet  { width: 4.4%; aspect-ratio: 1;
                filter: drop-shadow(0 0 7px rgba(255, 214, 130, .55))
                        drop-shadow(0 2px 3px rgba(0, 0, 0, .75)); }
.pawn-lantern { width: 4.2%; aspect-ratio: 1;
                filter: drop-shadow(0 0 8px rgba(255, 210, 120, .7))
                        drop-shadow(0 2px 3px rgba(0, 0, 0, .75)); }
.pawn-fear    { width: 4.2%; aspect-ratio: 1;
                filter: drop-shadow(0 0 7px rgba(224, 60, 40, .55))
                        drop-shadow(0 2px 3px rgba(0, 0, 0, .8)); }
/* The end panel's hero is still the DRAWN figure (see PAWN_TOKEN in game.js) and needs no
   override here: .vi-hero states its own width AND height in px further down, so neither the
   percentage widths nor the 1:1 above can reach it. */

/* The rim every piece carries. The board runs from a near-white snowfield on the left to a
   near-black forest in the middle, and a figure with no rim disappears into one of them —
   which one depends on where it is standing, so neither a light nor a dark piece is safe. */
.pawn path, .pawn circle { stroke: #1b0f26; stroke-width: 1.1; stroke-linejoin: round;
                           paint-order: stroke fill; }

/* Violet: a lilac meeple, lit down her left arm, with the lantern at her right hand. */
.pawn-violet .pw-body   { fill: #a259c4; }
.pawn-violet .pw-lit    { fill: #e8b6f2; stroke: none; }
.pawn-violet .pw-flame      { fill: #f5b944; }
.pawn-violet .pw-flame-core { fill: #fff6d4; stroke: none; }

/* Torech: the same meeple under the fear track's own red, worn as a nisse's cap. The cap is
   drawn ABOVE the head and clipped by the viewBox — a nisse's cap is longer than his head,
   and letting it run off the top is what stops the piece being a meeple in a party hat. */
.pawn-fear .pw-body  { fill: #7d3226; }
.pawn-fear .pw-cap   { fill: #c02718; }
.pawn-fear .pw-brim  { fill: #ff8f7a; }
.pawn-fear .pw-beard { fill: #e6dcd2; }

/* THE LANTERN IS A HURRICANE LAMP, drawn off the one the board already prints at the full
   end of the top rail: wire bail, vented dome, two side guards round a bulbous globe, fuel
   font, footed base. It was a plain amber box before, which is the shape of a lantern ICON
   and not of the object this game is about — the whole rail is a lamp burning down.

   Copper for the metal, because the printed one is copper and because amber frame + amber
   flame gave a token with no lit part: what makes a lamp read as BURNING is the contrast
   between a dark body and a hot centre. */
.pawn-lantern .pw-metal  { fill: #9a6038; }
.pawn-lantern .pw-metal-hi { fill: #c8874f; stroke: none; }
.pawn-lantern .pw-wire   { fill: none; stroke: #9a6038; stroke-width: 1.5; stroke-linecap: round; }
.pawn-lantern .pw-glass  { fill: #3d2a14; }
/* The flame: three nested bodies, coolest outside. The halo is drawn INSIDE the svg rather
   than as a filter so it is part of the piece and scales with it. */
.pawn-lantern .pw-halo       { opacity: .85; stroke: none; }
.pawn-lantern .pw-flame      { fill: #ffa317; stroke: none; }
.pawn-lantern .pw-flame-mid  { fill: #ffd36a; stroke: none; }
.pawn-lantern .pw-flame-core { fill: #fffbe8; stroke: none; }
.pawn-lantern .pw-wick       { fill: none; stroke: #4a3118; stroke-width: 1.1; stroke-linecap: round; }

/* A FLAME IS NEVER STILL, and it is the one thing on this board that may move quickly —
   it is a point of light, not an object, so it cannot be mistaken for the game asking a
   question the way a moving RING would be. Two rates, deliberately not multiples of each
   other, so the flicker never settles into a beat: the body dances, the halo breathes.
   `transform-box: fill-box` so the origin is the flame's own box and not the whole svg. */
.pawn-lantern .pw-flame,
.pawn-lantern .pw-flame-mid,
.pawn-lantern .pw-flame-core {
    transform-box: fill-box; transform-origin: 50% 100%;
    animation: vi-flicker 2.3s ease-in-out infinite;
}
.pawn-lantern .pw-flame-mid  { animation-duration: 1.9s; animation-delay: -.4s; }
.pawn-lantern .pw-flame-core { animation-duration: 1.5s; animation-delay: -.9s; }
.pawn-lantern .pw-halo {
    transform-box: fill-box; transform-origin: 50% 50%;
    animation: vi-breathe 3.7s ease-in-out infinite;
}
/* The BOARD's lantern is a photographed token now, so there is no `.pw-flame` inside it to
   dance — and a still lamp on a board whose whole subject is a lamp burning down was the one
   thing the piece could not be. The light moves instead of the flame: the token's own glow
   breathes on the same unmatched periods, which is the same idea carried by the only handle
   a flat image leaves. The rules above still drive the DRAWN lantern wherever it is used. */
@keyframes vi-lampGlow {
    0%, 100% { filter: drop-shadow(0 0 6px rgba(255, 210, 120, .55))
                       drop-shadow(0 2px 3px rgba(0, 0, 0, .75)); }
    43%      { filter: drop-shadow(0 0 11px rgba(255, 216, 138, .92))
                       drop-shadow(0 2px 3px rgba(0, 0, 0, .75)); }
}
.pawn-lantern:has(img) { animation: vi-lampGlow 3.1s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .pawn-lantern:has(img) { animation: none; } }
@keyframes vi-flicker {
    0%, 100% { transform: scaleY(1)    scaleX(1);    opacity: 1; }
    28%      { transform: scaleY(1.09) scaleX(.95);  opacity: .93; }
    54%      { transform: scaleY(.95)  scaleX(1.04); opacity: 1; }
    76%      { transform: scaleY(1.05) scaleX(.98);  opacity: .96; }
}
@keyframes vi-breathe {
    0%, 100% { transform: scale(1);    opacity: .72; }
    50%      { transform: scale(1.13); opacity: 1; }
}
/* Violet is carrying the same lamp; hers is one warm dot, and it breathes with the track's. */
.pawn-violet .pw-flame      { fill: #ffa317; }
.pawn-violet .pw-flame-core { fill: #fffbe8; stroke: none;
    transform-box: fill-box; transform-origin: 50% 50%;
    animation: vi-breathe 3.7s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
    .pawn-lantern .pw-flame, .pawn-lantern .pw-flame-mid, .pawn-lantern .pw-flame-core,
    .pawn-lantern .pw-halo, .pawn-violet .pw-flame-core { animation: none; }
}

/* ── THE BOARD'S WEATHER ──────────────────────────────────────────────────────────────
 * Built by life.js; read the note at the top of that file for the four rules this layer is
 * held to and for why it is not allowed to be faster or louder than it is.
 *
 * Every colour here is sampled off the painting itself, not off the chrome palette: the
 * moon's cold white, the water's pale lavender, woodsmoke, and the fire's amber. They are
 * written as literals rather than as tokens because they belong to the SCAN — a light on
 * Karina Duncan's board is not the same object as --vi-fire, which is a game state.
 * Nothing is blended: a blend mode re-rasterises everything behind it, and this sits over a
 * photograph inside a board the layout scales. Everything is a soft radial over the art.
 */
.life { position: absolute; inset: 0; pointer-events: none; overflow: hidden;
        border-radius: inherit; }
.life > * { position: absolute; transform: translate(-50%, -50%); pointer-events: none;
            will-change: transform, opacity; }

/* THE MOON. A core that holds still inside a halo that swells — haze crossing its face,
   rather than a light being turned up and down. 19s, which is seven times the slowest cue
   on this board. */
.life-moon-core {
    width: 3.4%; aspect-ratio: 1; border-radius: 50%;
    background: radial-gradient(circle, rgba(255,253,242,.62) 0%, rgba(255,250,232,.27) 52%, transparent 74%);
    animation: vi-moon-core 8s ease-in-out infinite;
}
.life-moon-halo {
    width: 14%; aspect-ratio: 1; border-radius: 50%;
    background: radial-gradient(circle, rgba(232,238,255,.44) 0%, rgba(200,210,248,.19) 42%, transparent 72%);
    animation: vi-moon-halo 8s ease-in-out infinite;
}
/* THE MOON, THIRD TIME. Twice this was tuned and twice it was reported as not moving, and
   both times the mistake was the same: treating "ambience must be slow and quiet" as one rule
   when it is two. SLOW is what stops it being mistaken for a cue, and that part was always
   right. QUIET was never the requirement -- it was a habit -- and a 16% swell between 60 and
   100% opacity on an already-pale glow is not quiet, it is invisible.
   The swing was then taken most of the way back, and that sequence is the useful part of the
   record: it was pushed from invisible to obvious, LOOKED AT, and settled between the two.
   The halo breathes about half again rather than doubling, the core never quite reaches full,
   and the 8-second tempo -- five times the slowest cue on this board -- stayed exactly where
   it was through all three cuts. Amplitude is the knob to hunt on; tempo is the one that
   decides whether ambience can be mistaken for a question, and it was never in play. */
@keyframes vi-moon-core {
    0%,100% { opacity: .40; transform: translate(-50%,-50%) scale(.92); }
    46%     { opacity: .92; transform: translate(-50%,-50%) scale(1.12); }
}
@keyframes vi-moon-halo {
    0%,100% { opacity: .30; transform: translate(-50%,-50%) scale(.86); }
    46%     { opacity: .88; transform: translate(-50%,-50%) scale(1.34); }
}

/* THE WATER. Each glint is a four-point star, not a dot: a dot at this size is a dead pixel
   and a star is a highlight. --s carries its own size so the fifteen are not one stamp
   repeated, and the duration varies as well as the delay -- one duration with staggered
   delays is a chase, and a chase has a direction the river does not. */
.life-glint {
    width: calc(1.9% * var(--s, 1)); aspect-ratio: 1;
    background: radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(226,236,255,.5) 26%, transparent 62%);
    clip-path: polygon(50% 0, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0 50%, 40% 40%);
    opacity: 0;
    animation: vi-glint 6s ease-in-out infinite;
}
@keyframes vi-glint {
    0%, 52%, 100% { opacity: 0;   transform: translate(-50%,-50%) scale(.45) rotate(0deg); }
    68%           { opacity: 1;   transform: translate(-50%,-50%) scale(1.1) rotate(20deg); }
    82%           { opacity: .35; transform: translate(-50%,-50%) scale(.8)  rotate(30deg); }
}

/* THE SPIDERS ON THE FEAR TRACK. The one thing in this layer that is DRAWN rather than cut
   from the board, and the one that replaces printed art rather than lighting it — see the
   note above SPIDERS in life.js for why eight articulating legs leave no other option.

   Only the red field is replaced. The circle stops at 99 of the 100 units that are the
   printed field's radius, and the gold ring around it is still the scan's, so the edge that
   would give the substitution away is never drawn. The ink and the field colours are sampled
   off the 4967px board master, not picked beside it.

   The shadow is a second instance of the same geometry, offset down-right AND scaled up six
   per cent. The scale is standing in for a blur: the printed spider's shadow is soft and
   spreads a little all the way round it, an offset copy alone only darkens one side, and a
   real feGaussianBlur would re-rasterise a moving subtree every frame for something that is
   one pixel across. It is a <use> of a single <g>, so the legs are only transformed once. */
/* Invisible until it walks: the PRINTED disc is the resting state and this is only laid over
   it for the few seconds a leg is moving. life.js sets the opacity; the 0 here is what a
   browser that never runs the driver is left with, which is the artwork. */
.life-spider { aspect-ratio: 1; opacity: 0; }
.life-spider svg { display: block; width: 100%; height: 100%; overflow: visible; }
.life-spider .sp path { fill: currentColor; }
.life-spider .ik { color: #161111; }
.life-spider .sh { color: #802014; opacity: .42; }

/* THE CRYSTAL FACES CATCHING THE MOON. Deliberately NOT the river's glint.
   The water's is a symmetric rosette that breathes; this is a specular flash — a small hot
   core with one long spike and one short one, lying along the facet's own plane. The spikes
   are what make it read as a hard surface catching the moon rather than as a soft one
   carrying it, and they are the whole difference at fifteen pixels.
   Near-achromatic and cool, but a touch of the crystals' own lilac in the falloff rather than
   snow's flat blue-white: the light leaving a coloured stone carries the stone. */
.life-facet {
    width: 1.3%; aspect-ratio: 1;
    background: radial-gradient(circle,
        rgba(255,255,255,.98) 0%, rgba(232,222,255,.55) 30%, transparent 66%);
    opacity: 0;
    animation: vi-facet 60s linear infinite;   /* duration set per facet in life.js */
}
.life-facet::before, .life-facet::after {
    content: ''; position: absolute; left: 50%; top: 50%;
}
/* the long spike, along the facet's plane */
.life-facet::before {
    width: 360%; height: 1.4px; transform: translate(-50%, -50%);
    background: linear-gradient(90deg, transparent,
        rgba(236, 247, 255, .95) 50%, transparent);
}
/* and the short one across it */
.life-facet::after {
    width: 1.4px; height: 170%; transform: translate(-50%, -50%);
    background: linear-gradient(180deg, transparent,
        rgba(236, 247, 255, .8) 50%, transparent);
}
/* A FLASH, not a twinkle: visible for about a fifth of a second out of a minute, so the
   percentages here are deliberately tiny. Rise fast, fall slower — that is the shape of a
   specular highlight passing, and it is the opposite of the water's slow swell. */
@keyframes vi-facet {
    0%, 100% { opacity: 0; transform: translate(-50%, -50%) rotate(var(--tilt, 0deg)) scale(.25); }
    0.14%    { opacity: 1; transform: translate(-50%, -50%) rotate(var(--tilt, 0deg)) scale(1.15); }
    0.30%    { opacity: .55; transform: translate(-50%, -50%) rotate(var(--tilt, 0deg)) scale(.9); }
    0.62%    { opacity: 0; transform: translate(-50%, -50%) rotate(var(--tilt, 0deg)) scale(.55); }
}

/* THE STARS. A point with a cross of light through it, not a dot: a dot at two pixels is a
   dead pixel, and the cross is what the eye reads as a star. Points of light, so like the
   flame and the fire they are allowed to be quicker than the rest of the layer -- but they
   are never in step, because each one's rate AND phase come off its own coordinates.
   The dimmest they go is a quarter, never nothing: a star that switches off is a fault. */
.life-star {
    width: calc(1.15% * var(--s, 1)); aspect-ratio: 1;
    background:
        radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(214,226,255,.4) 34%, transparent 68%),
        linear-gradient(0deg,  transparent 42%, rgba(255,255,255,.55) 50%, transparent 58%),
        linear-gradient(90deg, transparent 42%, rgba(255,255,255,.55) 50%, transparent 58%);
    animation: vi-star 4.6s ease-in-out infinite;
}
@keyframes vi-star {
    0%, 100% { opacity: .26; transform: translate(-50%,-50%) scale(.7); }
    50%      { opacity: 1;   transform: translate(-50%,-50%) scale(1.25); }
}

/* A FALLING STAR. The box is the verified corridor (see life.js) — laid along the segment,
   turned to its angle, and clipped — so the streak cannot leave the sky it was cleared for
   however long its tail gets. What travels is a child, which means the parent's rotation and
   the child's motion never fight each other for the same transform.

   Head and tail, not a uniform dash: a meteor is a bright point with light trailing BEHIND
   it, and a line of even brightness is a scratch on the lens. The tail fades to nothing over
   its whole length and the head carries a small bloom.

   The visible window is under a second inside a cycle of twenty-three to fifty-three, which
   is the only reason something this quick belongs on the board at all: it is a point of light
   and it is over before it could be read as the game asking for anything. */
.life-meteor {
    height: max(3px, .62%);
    transform: translate(-50%, -50%) rotate(var(--ang));
    overflow: hidden;
    pointer-events: none;
}
.life-meteor > i {
    position: absolute; inset: 0;
    background: linear-gradient(90deg,
        rgba(226, 236, 255, 0) 0%,
        rgba(226, 236, 255, .30) 46%,
        rgba(240, 246, 255, .78) 82%,
        #ffffff 100%);
    border-radius: 999px;
    opacity: 0;
    animation: vi-meteor 31s linear infinite;      /* life.js sets the real period */
}
/* The head, which is what actually makes it read at this size. A tail alone is a smear on the
   sky; what the eye recognises as a falling star is a bright POINT with the smear behind it. */
.life-meteor > i::after {
    content: ''; position: absolute; right: -2px; top: 50%;
    width: 7px; height: 7px; transform: translateY(-50%);
    border-radius: 50%; background: #fff;
    box-shadow: 0 0 7px 2px rgba(226, 238, 255, .85), 0 0 16px 5px rgba(180, 205, 255, .38);
}
/* The whole flight is the first 3.6% of the timeline; the rest of the cycle is empty sky. */
@keyframes vi-meteor {
    0%    { transform: translateX(-130%); opacity: 0; }
    0.5%  { opacity: .9; }
    3.0%  { opacity: .9; }
    3.6%  { transform: translateX(230%);  opacity: 0; }
    100%  { transform: translateX(230%);  opacity: 0; }
}

/* THE WATERFALL. A clipping box over the painted fall with two runs of highlight travelling
   down inside it (see the note in life.js for why one was not enough). Each is translated by
   exactly one tile of its own repeat, so every loop closes on itself with nothing to hand over.

   AND IT IS QUIET. The cut before this one ran the coarse run at 62% white, which on water
   the painting has already drawn as near-white read as a lit panel laid over the fall rather
   than as the fall catching the moon. The motion was right and the weight was wrong; the
   weight is now about a third of it. What a real fall gives you at this size is a suggestion
   that the surface is not still, and that is all this should ever be.

   THREE THINGS MAKE IT A STREAM RATHER THAN A MOVING PATTERN, and the first cut had none:

   1. IT IS FAST. 26px in 0.62s is about 42 pixels a second, which crosses the whole visible
      fall in a second. At the 14px/s it ran at before, the eye can track one band the entire
      way down and count it; past roughly 30px/s it cannot, and the bands become flow.
   2. THE HIGHLIGHTS ARE THREADS, not rungs. Each run carries a second gradient at 90deg --
      fine vertical stripes -- so what travels is a bundle of threads of differing brightness
      rather than a horizontal bar sliding down a column. Falling water is threads.
   3. THE FADE IS AT THE ENDS ONLY. An ellipse mask took the middle out of the fall along with
      its edges. The water has to be solid where the water is and go quiet only where it leaves
      the lip and where it lands.

   Linear and continuous throughout: the one thing on this board with no beat at all, which is
   exactly what lets something this quick sit next to cues without becoming one. */
/* THE FEATHER IS THE WHOLE MASK. An ellipse that is solid to 76% and clear at 100% has only
   a quarter of its radius to fade in, which on a 22px-wide box is five pixels -- and five
   pixels of falloff on a bright overlay is not a fade, it is an edge, reported as a visible
   border round the water. Solid to a QUARTER and gone by the corner: most of the box is
   ramp, so there is nowhere for a rectangle to show itself. */
.life-fall { overflow: hidden;
    -webkit-mask-image: radial-gradient(ellipse 76% 74% at 50% 46%, #000 24%, transparent 98%);
    mask-image: radial-gradient(ellipse 76% 74% at 50% 46%, #000 24%, transparent 98%); }
.life-fall > i {
    position: absolute; left: -6%; right: -6%; top: -100%; height: 300%;
    background-repeat: repeat;
}
/* The coarse run: the body of the water. */
.life-fall > i:nth-child(1) {
    background-image:
        repeating-linear-gradient(90deg,
            rgba(255,255,255,0) 0px, rgba(255,255,255,.05) 1px,
            rgba(255,255,255,0) 3px, rgba(255,255,255,0)   5px),
        repeating-linear-gradient(180deg,
            rgba(255,255,255,0)   0px,
            rgba(255,255,255,.24) 6px,
            rgba(232,242,255,.11) 13px,
            rgba(255,255,255,0)   21px,
            rgba(255,255,255,0)   26px);
    animation: vi-fall-a .62s linear infinite;
}
/* The fine one, and it never falls into step: 15 does not divide 26. */
.life-fall > i:nth-child(2) {
    background-image:
        repeating-linear-gradient(90deg,
            rgba(255,255,255,0) 0px, rgba(255,255,255,.04) 1px,
            rgba(255,255,255,0) 2px, rgba(255,255,255,0)   4px),
        repeating-linear-gradient(180deg,
            rgba(255,255,255,0)   0px,
            rgba(244,250,255,.13) 3px,
            rgba(255,255,255,0)   9px,
            rgba(255,255,255,0)   15px);
    animation: vi-fall-b .41s linear infinite;
}
@keyframes vi-fall-a { to { transform: translateY(26px); } }
@keyframes vi-fall-b { to { transform: translateY(15px); } }

/* ...and the pool it lands in. The fall itself is steady; what breathes is the spray coming
   off the bottom of it, which is where a real one is never the same twice. */
.life-mist {
    aspect-ratio: 1.5; border-radius: 50%;
    background: radial-gradient(ellipse, rgba(240,246,255,.24) 0%, rgba(214,226,250,.10) 46%, transparent 74%);
    animation: vi-mist 6.5s ease-in-out infinite;
}
@keyframes vi-mist {
    0%,100% { opacity: .45; transform: translate(-50%,-50%) scale(.9)  translateY(0); }
    50%     { opacity: 1;   transform: translate(-50%,-50%) scale(1.2) translateY(-6%); }
}

/* WOODSMOKE. It rises, widens, leans off the chimney and is gone --
   drawn as ONE finite pass per puff rather than a loop that re-dresses itself mid-flight,
   which is how an ambient layer ends up visibly handing over. Three puffs a third of a cycle
   apart make a broken column. */
.life-smoke {
    width: 4.4%; aspect-ratio: 1; border-radius: 50%;
    background: radial-gradient(circle, rgba(232,230,244,.66) 0%, rgba(210,208,228,.30) 46%, transparent 72%);
    opacity: 0;
    animation: vi-smoke 10.8s linear infinite;
}
@keyframes vi-smoke {
    0%   { opacity: 0;   transform: translate(-50%,-50%) translate(0,0)        scale(.34); }
    12%  { opacity: 1; }
    58%  { opacity: .70; }
    100% { opacity: 0;   transform: translate(-50%,-50%) translate(150%,-470%) scale(2.9); }
}

/* ORDEP'S FIRE, the thing Violet is walking toward. Allowed to be quick for the same reason
   the lantern token is: a point of light cannot be mistaken for a ring round a space.

   THE ONLY THING ON THIS BOARD THAT IS NOT A KEYFRAME ANIMATION, and it took three goes to
   accept why. One slow pulse is a lamp breathing. Three pulses on coprime periods stop the
   composite REPEATING but do not stop it having a BEAT — the eye locks onto the fastest layer
   and hears a heartbeat. Making the keyframes uneven inside each cycle does not fix it
   either: an uneven cycle is a MOTIF, and a motif played every 1.63 seconds is more obviously
   a loop than an even wave was. "It always flickers twice" is what a motif sounds like.

   A fire has no cycle at all, so neither does this. life.js drives these four custom
   properties off smoothed value noise — deterministic, no die rolled, and with no period an
   eye will ever find. The CSS keeps the shape, the colour and the geometry; the only thing
   that comes from script is how bright each of the four lights is this instant.

   The MOVEMENT is in the core; the glow mostly changes brightness. A big disc that visibly
   grows and shrinks reads as a pulsing marker, and this board's rings already own that.
   The fallbacks in the var() calls are the still fire: a browser that never runs the driver,
   or reduced motion (which stands the whole layer down), gets a lit hearth rather than a
   black hole where one should be. */

/* The GLOW is the quiet one. It is the largest lit thing on a dark board, so it carries the
   fire's presence and nothing else: a narrow brightness band, and barely any change of size. */
.life-fire {
    width: 7.4%; aspect-ratio: 1; border-radius: 50%;
    background: radial-gradient(circle,
        rgba(255,214,130,.34) 0%, rgba(255,166,70,.19) 32%,
        rgba(238,116,40,.09) 58%, transparent 78%);
    opacity: var(--glow-o, .66);
    transform: translate(-50%, -50%) scale(var(--glow-s, 1));
}
/* the core: the fast one, and the only one that moves rather than merely brightening */
.life-fire::before {
    content: ''; position: absolute; inset: 27%;
    border-radius: 50%;
    background: radial-gradient(circle,
        rgba(255,241,196,.66) 0%, rgba(255,184,88,.34) 44%, transparent 74%);
    opacity: var(--core-o, .74);
    transform: scale(var(--core-s, 1)) translate(var(--core-x, 0%), var(--core-y, 0%));
}
/* the lick: off-centre, so the light is never symmetric about the logs */
.life-fire::after {
    content: ''; position: absolute; inset: 30% 34% 34% 26%;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(255,226,152,.6) 0%, transparent 70%);
    opacity: var(--lick-o, .26);
    transform: scale(var(--lick-s, .95)) translate(var(--lick-x, 0%), var(--lick-y, 0%));
}
/* THE GUST. Much slower and wider than the other three, so the brightness the licks land on
   is itself always moving: the same flicker over a rising glow and over a dying one does not
   read as the same flicker. Drawn under the glow, and never bright on its own. */
.life-ember {
    width: 10.6%; aspect-ratio: 1; border-radius: 50%;
    background: radial-gradient(circle,
        rgba(255,186,96,.16) 0%, rgba(240,126,44,.08) 46%, transparent 72%);
    opacity: var(--gust-o, .4);
    transform: translate(-50%, -50%) scale(var(--gust-s, 1));
}

/* ORDEP ASLEEP. The one thing on this board that is a letter rather than a shape, and the
   only mark that says the figure by the fire is doing something rather than merely sitting
   there. Cream, the same warmth as his firelight, and small: he is a long way off. */
.life-z {
    position: absolute; width: 2.6%; aspect-ratio: 1;
    color: rgba(255, 244, 214, .92);
    filter: drop-shadow(0 1px 3px rgba(4, 2, 10, .8));
    opacity: 0; pointer-events: none;
    animation: vi-zzz 43s ease-out infinite;
}
.life-z svg { display: block; width: 100%; height: 100%; }
/* Up and a little to the right, growing as it goes, and gone for the rest of the cycle.
   The DRIFT is unchanged at about 2.8 seconds; only the waiting is longer — 19s put him
   snoring four times a minute, which is a character trait rather than a joke. The stops are
   percentages, so lengthening the cycle without shrinking them would have stretched the
   drift to six seconds instead of leaving more silence around it. */
@keyframes vi-zzz {
    0%   { opacity: 0;   transform: translate(-50%, -50%) scale(.5) rotate(-8deg); }
    1.3% { opacity: .9;  transform: translate(-46%, -84%) scale(.72) rotate(-4deg); }
    6.6% { opacity: 0;   transform: translate(-14%, -236%) scale(1.16) rotate(7deg); }
    100% { opacity: 0;   transform: translate(-14%, -236%) scale(1.16) rotate(7deg); }
}

/* THE PLANK BRIDGE on the crystal face. Same sprite trick as the tree, one difference that
   matters: it HANGS, so it turns about the anchor at its top rather than about a root at its
   bottom. And it is slow and small -- a rope bridge in a night breeze moves about a degree,
   and this one is 60 pixels wide, so a degree is all there is room for anyway. */
.life-bridge {
    aspect-ratio: 1;
    background-image: url('assets/board.jpg');
    background-repeat: no-repeat;
    transform-origin: 50% 4%;
    -webkit-mask-image: radial-gradient(ellipse at 50% 50%, #000 30%, transparent 74%);
    mask-image: radial-gradient(ellipse at 50% 50%, #000 30%, transparent 74%);
    animation: vi-swing 8.4s ease-in-out infinite;
}
@keyframes vi-swing {
    0%, 100% { transform: translate(-50%,-50%) rotate(calc(var(--lean) * -1)); }
    50%      { transform: translate(-50%,-50%) rotate(var(--lean)); }
}

/* NO STAND-DOWN RULE, and that is a finding rather than an omission. Orleans' weather has one
   because that board DIMS to 62% while a Place is being chosen, and ambience during a question
   is noise. Violet asks its questions somewhere else entirely -- the draft row, the hand, and
   the fear-slot overlay -- and the board is never dimmed, never picked from, and never the
   thing being read. A `#game.resolving .life { opacity: 0 }` was written here first and
   removed: nothing in this game sets that class, so it was a rule that could only ever look
   like a decision. Reduced motion is the one case that is real. */
@media (prefers-reduced-motion: reduce) { .life { display: none; } }

/* One of Ordep's helpers, waiting on the first space of a coloured area: the rulebook's
   little yellow stone, and nothing else. It was drawn as a ring with the stone inside, and
   the ring was furniture -- the space is already a painted diamond, so circling it only
   added a second outline around art that draws itself. */
.helper-ring {
    position: absolute; transform: translate(-50%, -50%);
    width: 2.35%; aspect-ratio: 1; border-radius: 50%;
    background: radial-gradient(circle at 34% 28%, #ffeeb4, #f2c65c 46%, #b8862c);
    box-shadow: 0 1px 4px rgba(18, 9, 0, .65), 0 0 9px rgba(255, 214, 96, .35),
                inset 0 -2px 3px rgba(118, 82, 18, .5);
    pointer-events: none;
}

.faery-ring { position: absolute; transform: translate(-50%, -50%);
              width: 4.2%; aspect-ratio: 1; border-radius: 50%;
              border: 2px dashed rgba(150, 240, 255, .9);
              box-shadow: 0 0 12px rgba(120, 220, 255, .5); pointer-events: none;
              animation: faery 2.6s ease-in-out infinite; }
@keyframes faery { 0%,100% { opacity: .55; } 50% { opacity: 1; } }
/* The Scent of Fear, sitting on the space it was laid on: the tile's own art, small, rather
   than a purple dot that says nothing about which trap it is. */
.scent-mark {
    position: absolute; transform: translate(-50%, -50%);
    width: 3.6%; aspect-ratio: 1.41; border-radius: 3px; overflow: hidden;
    box-shadow: 0 0 0 1px rgba(240, 140, 255, .85), 0 2px 8px rgba(4, 2, 10, .6);
    pointer-events: auto; cursor: help;
}
.scent-mark img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* An advantage token, lying face up beside its coloured area. The face is the rulebook's own
   (p10) and it is drawn rather than tinted, because the piece has to be legible as a THING
   before it is legible as a state: it was a gold disc for a while, which made it a dead
   ringer for one of Ordep's helper stones sitting a little way off its space.
   Who owns it is carried by the RIM, which is the only part that survives at ~14px. */
.adv-token {
    position: absolute; transform: translate(-50%, -50%) rotate(-7deg);
    width: 4.4%; aspect-ratio: 4 / 3; border-radius: 2px;
    box-shadow: 0 0 0 1px rgba(238, 214, 158, .5), 0 3px 8px rgba(4, 2, 10, .65);
    pointer-events: auto; cursor: help;
}
.adv-token svg { display: block; width: 100%; height: 100%; border-radius: 2px; }
.adv-token.claim-fly {
    position: fixed; z-index: 1200; pointer-events: none;
    transform: none;                       /* placed by its own rect, not centred on a point */
}

.trap-mark {
    position: absolute; transform: translate(-50%, -50%);
    /* sized to the bracketed slot the board prints for it */
    width: 8.3%; aspect-ratio: 1.41; border-radius: 4px;
    /* Face down, this is the PRINTED BACK of the event tile — the moon over the night sky,
       from assets/original/Back_x4.png, whose 413x295 is the same 1.41 the board's bracketed
       slots are drawn at. It used to be a dark box with a `!` in it, which was scaffolding
       for the same reason the card faces were. The red rule and glow stay: they are what
       says the tile is TORECH'S and is still waiting to go off. */
    background: #241536 url('assets/cards/tile-back.webp') center / cover no-repeat;
    border: 1px solid rgba(255, 150, 120, .75);
    box-shadow: 0 0 8px rgba(220, 80, 60, .45);
    pointer-events: none; overflow: hidden;
}
/* Face up, so it answers a pointer -- #board-marks turns events off for the whole layer and
   this is the one thing in it a player has any reason to interrogate. Face DOWN it must stay
   inert: a cursor that reacts over a hidden tile is a way of finding the traps. */
.trap-mark.open { width: 8.3%; aspect-ratio: 1.41; border-radius: 3px;
                  pointer-events: auto; cursor: help;
                  transition: transform .14s ease, box-shadow .14s ease; }
.trap-mark.open:hover {
    transform: translate(-50%, -50%) scale(1.14);
    box-shadow: 0 0 0 1px rgba(255, 226, 160, .8), 0 4px 14px rgba(0, 0, 0, .6);
    z-index: 3;
}
@media (prefers-reduced-motion: reduce) {
    .trap-mark.open, .trap-mark.open:hover { transition: none; transform: translate(-50%, -50%); }
}
.trap-mark.open img { width: 100%; height: 100%; object-fit: cover; display: block; }
.trap-mark.spent { opacity: .55; filter: grayscale(.5); border-color: rgba(255,255,255,.35); }

/* A TRAP GOING OFF. Its one moment: it turns face up on the slot it was laid on, lifts off the
   board so it is the biggest thing on it while its sentence is read, and is then thrown onto
   the heap with the spent tokens. Scaled about its BOTTOM edge, so it grows upward out of its
   own slot rather than swelling symmetrically over the two neighbouring spaces.
   The glow is the board's own alarm red, not the chrome's — this is the painting's object. */
.trap-mark.firing {
    z-index: 6;
    transition: transform .34s cubic-bezier(.16, 1, .3, 1), box-shadow .34s ease;
    transform-origin: 50% 100%;
}
.trap-mark.firing.lifted {
    transform: translate(-50%, -50%) scale(2.5) translateY(-11%);
    box-shadow: 0 0 0 1px rgba(255, 226, 160, .5),
                0 0 22px rgba(220, 80, 60, .75),
                0 10px 26px rgba(4, 2, 10, .7);
}
/* The copy that outlives the render which takes the original away is placed from the lifted
   mark's MEASURED rect and carries no transform of its own — the rect already includes the
   scale, so re-applying it would square it. Its own flight overrides the transition above. */
@media (prefers-reduced-motion: reduce) {
    .trap-mark.firing { transition: box-shadow .34s ease; }
    .trap-mark.firing.lifted { transform: translate(-50%, -50%) scale(1.7); }
}

/* The night laid over the end of the lantern track after a storm. Painted in the board's own
   sky rather than in a UI colour, and feathered at the left edge so it reads as more of the
   same night rather than as a panel stuck on top of a painting. */
.lantern-cover {
    position: absolute; pointer-events: none; border-radius: 3px;
    background:
        linear-gradient(90deg, rgba(26, 18, 54, 0) 0%, rgba(26, 18, 54, .96) 9%,
                        rgba(22, 15, 48, .99) 100%);
}
.lantern-cover::after {                 /* a few stars, so it is sky and not a rectangle */
    content: ''; position: absolute; inset: 0; border-radius: 3px;
    background-image:
        radial-gradient(1.2px 1.2px at 22% 34%, rgba(255,255,255,.75), transparent),
        radial-gradient(1px 1px at 55% 68%, rgba(255,255,255,.55), transparent),
        radial-gradient(1.4px 1.4px at 78% 26%, rgba(255,255,255,.65), transparent);
}
/* The track's end marker, CUT FROM THE BOARD and set down on the space the track now ends
   at. A sprite of the scan rather than a drawing of it: nothing can disagree with the art,
   and the icon that moves is the icon that was printed. Square in PIXELS — the width is a
   percentage of the board and the aspect ratio makes the height match. */
.lantern-end {
    position: absolute; transform: translate(-50%, -50%);
    aspect-ratio: 1; border-radius: 50%;
    background-image: url('assets/board.jpg');
    background-repeat: no-repeat;
    box-shadow: 0 0 10px rgba(255, 214, 96, .4);
    pointer-events: auto; cursor: help;
}

/* THE OFF TRACK TILE, lying on the space it covers: two slots in that space's own colour,
   angled along the path. It is the picture of the rule — one space became two — and without
   it the tile changed the track and left nothing on the board to say so. */
.offtrack-tile {
    position: absolute; transform: translate(-50%, -50%) rotate(var(--tilt, 0deg));
    display: flex; gap: 2px; padding: 2px;
    border-radius: 3px;
    background: rgba(22, 12, 34, .92);
    box-shadow: 0 0 0 1px rgba(255, 244, 214, .55), 0 2px 8px rgba(4, 2, 10, .6);
    pointer-events: auto; cursor: help;
}
.offtrack-slot {
    width: 1.5%; aspect-ratio: 1; border-radius: 2px;
    box-shadow: inset 0 0 0 1px rgba(4, 2, 10, .35);
}
/* sized against the BOARD, not the tile: percentages inside a flex row would resolve against
   the row's own width, which is what they are setting */
.offtrack-tile { width: 4.2%; }
.offtrack-slot { width: 50%; height: auto; aspect-ratio: 1; }

/* a space Adapt or Off Track changed, so the board never lies. Adapt lays the printed AREA
   token; Off Track can leave a colour that has no token (fire), and that keeps the plain disc. */
.recoloured { position: absolute; transform: translate(-50%,-50%); width: 2.2%; aspect-ratio: 1;
              border-radius: 50%; border: 2px solid rgba(255,255,255,.85); pointer-events: none; }
.recoloured.token { width: 3.1%; border: 0; border-radius: 0; background: none;
                    filter: drop-shadow(0 1px 3px rgba(4, 2, 10, .75)); }
.recoloured.token img { display: block; width: 100%; height: 100%; object-fit: contain; }

/* ── cards ─────────────────────────────────────────────────────────────────── */

.card {
    position: absolute; width: var(--card-w); height: var(--card-h);
    border-radius: calc(var(--card-w) * 0.09);
    background: #16101f;                 /* the FACE ground — never transparent */
    box-shadow: 0 3px 9px rgba(0, 0, 0, 0.55);
    /* Cards are TAPPED, never dragged (every one is wired with onclick), so they only need
       the double-tap delay gone — `none` would have blocked a pinch that happens to start
       on the fan, which on a phone is most of the lower screen. */
    user-select: none; touch-action: manipulation;
    transition: filter .13s ease, box-shadow .13s ease;
    overflow: hidden;
}
/* THE PRINTED FACE. The value in both corners, the suit badge and the power symbol are all
   painted on it, so nothing is drawn over the artwork any more -- see makeCard(). */
.card .cf {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; display: block;
    pointer-events: none; user-select: none;
}
/* The suit ground, still declared: it is what shows for the frame before the face decodes,
   and a card that flashes BLACK on a fan of nine is worse than one that flashes its colour. */
.card.lake     { background: linear-gradient(160deg, #1ed3c3, var(--vi-lake) 62%, #0d7c73); }
.card.forest   { background: linear-gradient(160deg, #ef7f37, var(--vi-forest) 62%, #92400f); }
.card.mountain { background: linear-gradient(160deg, #8b55c2, var(--vi-mountain) 62%, #43206b); }
.card.crystal  { background: linear-gradient(160deg, #d9629f, var(--vi-crystal) 62%, #7d2258); }

.card.back {
    background: #241536 url('assets/cards/back.webp') center / cover no-repeat;
}
/* a card already on screen slides to its new place; a brand-new one gets the class only
   after its first frame, so it does not transition in from the corner */
.card.slide { transition: left .34s cubic-bezier(.22,.75,.3,1),
                          top .34s cubic-bezier(.22,.75,.3,1),
                          filter .13s ease, box-shadow .13s ease; }
.card.playable { cursor: pointer; }
.card.playable:hover { filter: brightness(1.16); box-shadow: 0 5px 16px rgba(0,0,0,.6); }
/* readable but plainly not clickable: at 0.55 the tableau could not be planned from */
.card.dim { filter: brightness(0.80) saturate(0.85); }
.card.selected { box-shadow: 0 0 0 2px var(--dk-accent), 0 8px 20px rgba(0,0,0,.6); }
/* Every card answers a hover or a long-press, so every card can explain itself — including
   the dimmed tableau, the pair on the table and the face-down backs. Only `.playable` ones
   carry a click handler, and they sit above the rest, so a dimmed card can never swallow the
   tap meant for a live one. */
.card { pointer-events: auto; }
.card.playable { z-index: 3; }
.card.dim { z-index: 1; }

/* THE ONE THING THE PAINTING CANNOT SAY. A card's power is silent while Violet stands in
   that card's own area, and a printed card has no way to show it -- so the printed power
   plate gets a scrim. Measured off assets/cards/: the plate is drawn from 11% to 88% across
   and 63% to 96% down on every numbered card, so the scrim covers it and nothing else.
   Grey FIRST and dark second. The old faint wash read as a printing flaw, and a heavy one
   reads as a redaction -- neither says "this symbol, but silent". Draining the colour out of
   the plate does, and the dark is only enough to sit it back behind the live cards. */
.card .fx-mute {
    position: absolute; left: 10%; right: 10%; top: 62%; bottom: 3%;
    border-radius: calc(var(--card-w) * 0.04);
    background: rgba(12, 8, 22, .34);
    backdrop-filter: grayscale(1) brightness(.66) contrast(.9);
    -webkit-backdrop-filter: grayscale(1) brightness(.66) contrast(.9);
    box-shadow: inset 0 0 0 1px rgba(226, 210, 245, .14);
    pointer-events: none;
    opacity: 0; transition: opacity .16s ease;
}
.card .fx-mute.on { opacity: 1; }

/* ── zones ─────────────────────────────────────────────────────────────────── */

#tableau, #trick, #hand-top, #hand-bottom, #fear-slots { position: absolute; }

.zone-label {
    position: absolute; transform: translateX(-50%);
    font-family: var(--dk-font-ui); font-size: 11px; letter-spacing: .08em;
    text-transform: uppercase; color: rgba(226, 210, 245, .48); white-space: nowrap;
    pointer-events: none;
}

/* THE TABLEAU IS A PLACE, not four cards that happen to be in a row. The draft is the one
   decision this game repeats every single turn, and until now the cards for it were laid
   straight onto the painting with nothing saying where the offer began and ended -- the
   player had to infer the boundary of the most-used area on the screen from the cards
   currently in it, which changes size as the pool does.
   So it gets a bed: the same night ground and gold double rule as the help banner, because
   they are the two places on this board where the GAME speaks rather than the painting, and
   they should be recognisably the same furniture. Drawn behind the cards, never over them. */
.tableau-bed {
    position: absolute; pointer-events: none;
    border-radius: calc(var(--card-w) * 0.16);
    background: linear-gradient(180deg, rgba(30, 22, 56, .55), rgba(18, 12, 38, .62));
    box-shadow:
        0 0 0 1px rgba(201, 162, 74, .55),
        inset 0 0 0 3px rgba(30, 22, 56, .55),
        inset 0 0 0 4px rgba(201, 162, 74, .22),
        inset 0 1px 0 rgba(255, 244, 214, .07),
        0 10px 26px rgba(4, 2, 10, .45);
}
/* The word sits ON the rule, the way a printed frame carries its own caption: a label
   floating above the box reads as a heading for the whole screen. */
.tableau-bed > span {
    position: absolute; top: -7px; left: 50%; transform: translateX(-50%);
    padding: 0 8px;
    background: linear-gradient(180deg, rgba(30, 22, 56, .95), rgba(24, 17, 46, .95));
    font-family: var(--dk-font-ui); font-size: 10px; letter-spacing: .16em;
    text-transform: uppercase; color: rgba(226, 208, 160, .72); white-space: nowrap;
}

.slot-frame {
    position: absolute; border-radius: calc(var(--card-w) * 0.09);
    border: 1px dashed rgba(208, 100, 216, 0.28); pointer-events: none;
}

/* THE RULEBOOK SCREEN.
 *
 * The shared `.overlay` gives the scrim and the centring; what sits in it is NOT the shared
 * `.panel`, which is `min(440px, 94vw)` and built for a question with three buttons under
 * it. A page of A4 at 440px is unreadable, so this is a game-owned sheet sized to a document
 * instead — the platform sheet is left alone rather than overridden out from under the
 * other things that use it.
 *
 * ABOVE THE START SCREEN. The overlay token is 1100 and the start screen is 1200, so an
 * untouched `.overlay` opened from the menu would render BEHIND the menu that opened it —
 * a click that appears to do nothing. Ten over the start screen, and it is over both.
 *
 * Sized in dynamic viewport units: on a phone the browser's own chrome grows and shrinks as
 * you scroll, and `vh` would put the bottom of the rulebook underneath it. */
#rules-screen { z-index: calc(var(--dk-z-start) + 10); padding: 12px; }

.rules-sheet {
    display: flex; flex-direction: column;
    width: min(1000px, 96vw); height: min(880px, 92dvh);
    background: linear-gradient(180deg, #241640, #150d2c);
    border: 1px solid rgba(150, 100, 220, .45);
    border-radius: 16px; overflow: hidden;
    box-shadow: 0 12px 44px rgba(0, 0, 0, .7);
}

/* Title, the way out to a tab, and the close — one row, because the frame below it should
   get every pixel that is not this. */
.rules-bar {
    display: flex; align-items: center; gap: 12px;
    padding: 10px 12px 10px 16px;
    border-bottom: 1px solid rgba(150, 100, 220, .28);
    background: rgba(10, 6, 22, .35);
}
.rules-bar h2 {
    flex: 1; margin: 0; text-align: left;
    font-family: var(--dk-font-display); font-weight: 700; font-size: 20px;
    color: var(--dk-accent); line-height: 1.2;
}
.rules-tab {
    font-family: var(--dk-font-ui); font-size: 12px; letter-spacing: .04em;
    color: rgba(226, 208, 160, .78); text-decoration: none;
    padding: 6px 10px; border-radius: 8px;
    border: 1px solid rgba(201, 162, 74, .32);
}
.rules-tab:hover { color: #f0d79a; border-color: rgba(201, 162, 74, .6); }
/* 44px of tap target, which is the floor everything clickable in this game is held to. */
.rules-x {
    width: 34px; height: 34px; flex: none;
    display: grid; place-items: center;
    background: transparent; border: 1px solid rgba(150, 100, 220, .35);
    border-radius: 9px; cursor: pointer;
    color: rgba(226, 208, 250, .85); font-size: 14px; line-height: 1;
}
.rules-x:hover { color: #fff; border-color: rgba(150, 100, 220, .7); }

/* The browser's own PDF viewer, on a light ground: a PDF page is white, and a white page
   arriving inside a dark frame flashes if the frame under it is dark too. */
.rules-frame { flex: 1; width: 100%; border: 0; background: #2a2a30; }

@media (max-width: 560px) {
    .rules-bar { padding: 8px 8px 8px 12px; gap: 8px; }
    .rules-bar h2 { font-size: 17px; }
    .rules-tab { font-size: 11px; padding: 5px 8px; }
}

/* THE TWO PLAYER PLATES: the lantern reading, the fear reading, the tokens.
 *
 * Same family as the banner, the tableau bed and the buttons -- night ground, gold rule --
 * because all four are the GAME talking over the painting and there is no reason for them to
 * be four different objects. What is new here is hierarchy: the name was 14px display type
 * sitting on top of a 12px sentence, and the two numbers that decide every turn (how much
 * lantern is left, how close the fear is) were the same weight as the words around them. The
 * name is now a small tracked label, and the readings are the biggest thing on the plate --
 * a plate is a gauge with a caption, not a caption with numbers in it.
 *
 * The reading is set in TABULAR figures so it does not jitter as it counts down. */
.plate {
    position: absolute;
    background: linear-gradient(180deg, rgba(30, 22, 56, .93), rgba(17, 11, 36, .95));
    border: 0; border-radius: var(--dk-radius);
    box-shadow:
        0 0 0 1px rgba(201, 162, 74, .40),
        inset 0 1px 0 rgba(255, 244, 214, .08),
        0 8px 20px rgba(4, 2, 10, .52);
    padding: 8px 13px 9px; font-family: var(--dk-font-ui); font-size: 12.5px;
    line-height: 1.45;
    color: rgba(236, 226, 250, .88); white-space: nowrap; pointer-events: none;
    font-variant-numeric: tabular-nums;
}
/* The player's OWN plate is the one they read under time pressure; it is a step brighter and
   its rule is a step warmer, which is the whole of the distinction. Two different shapes
   would have been a second component to no purpose. */
.plate.mine {
    background: linear-gradient(180deg, rgba(38, 28, 68, .95), rgba(22, 14, 44, .96));
    box-shadow:
        0 0 0 1px rgba(201, 162, 74, .62),
        inset 0 1px 0 rgba(255, 244, 214, .12),
        0 10px 24px rgba(4, 2, 10, .58);
    color: rgba(244, 236, 221, .96);
}
.plate b { color: #f0d79a; font-weight: 600; font-size: 15px; }
.plate .warn { color: #ffb27a; }
.plate .bad  { color: #ff8a72; }
.plate .who {
    display: block; margin-bottom: 3px;
    font-family: var(--dk-font-ui); font-size: 10px; font-weight: 600;
    letter-spacing: .15em; text-transform: uppercase;
    color: rgba(226, 208, 160, .70);
}
.plate.mine .who { color: rgba(240, 215, 154, .92); }
.plate .ai-tag { opacity: .6; font-size: 11px; }

/* the lantern gauge — the reading the whole game turns on */
.gauge { display: block; width: 100%; height: 7px; margin-top: 6px;
         border-radius: 4px; background: rgba(0, 0, 0, .45);
         box-shadow: inset 0 1px 2px rgba(0, 0, 0, .55), 0 1px 0 rgba(255, 244, 214, .06);
         overflow: hidden; }
/* Lit from inside, like the lamp it is reading. */
.gauge > i { display: block; height: 100%; border-radius: 4px;
             background: linear-gradient(90deg, #ffe9b0, #ffd98a 40%, #e8952f);
             box-shadow: 0 0 8px rgba(255, 200, 110, .5);
             transition: width .35s ease; }
.gauge.fear > i { background: linear-gradient(90deg, #ff9c86, #c9291a); }

/* the human's own advantage tokens, as pieces beside their hand */
/* The heap of used-up pieces under the draft row. The patch itself is NOT drawn — nothing
   in it lines up, and a frame would promise an order it does not have. The pieces are dimmed
   and desaturated: they are a record, not a resource, and they must never compete with the
   rack of live ones a few inches away. */
#spent-pile { position: absolute; pointer-events: none; }
/* A spent piece is OPAQUE, in the air and on the heap alike. It used to fade to .5 and
   desaturate on landing while flying at .82, so the piece visibly dimmed and lost its colour
   at the exact moment it arrived — which reads as the token going somewhere other than where
   it was thrown, even though the flight lands pixel-for-pixel on the drawn position (the
   ghost is `fixed` and the heap is `absolute` inside #game, which is `inset: 0`, so the two
   coordinate systems coincide). The heap already reads as discarded from its scatter, its
   rotation and its place on the board; it does not need the pieces to be ghosts as well. */
.spent-piece {
    position: absolute; border-radius: 2px; overflow: hidden;
    box-shadow: 0 0 0 1px rgba(238, 214, 158, .3), 0 2px 5px rgba(4, 2, 10, .55);
}
.spent-piece svg, .spent-piece img { display: block; width: 100%; height: 100%; }
.spent-piece img { object-fit: cover; }
/* Same opacity as the landed piece: any difference here is a pop at the handover. */
.spent-piece.flying { position: fixed; z-index: 1200; }

/* A rack of won tokens, beside its owner's hand -- both seats have one, because a token in
   Torech's hands is a thing that can go off at any moment and you should be able to see how
   many he is holding. */
#my-tokens, #opp-tokens { position: absolute; display: grid; gap: 6px; }
/* The piece keeps the face it wore on the board. It was a gold disc here, which is the
   HELPER's shape -- the one confusion this game has already been through once. */
.held-token {
    display: block; padding: 0; border: 0; background: none; cursor: default;
    border-radius: 2px; overflow: hidden;
    box-shadow: 0 0 0 1px rgba(238, 214, 158, .55), 0 2px 6px rgba(4, 2, 10, .6);
    touch-action: manipulation;
}
.held-token svg { display: block; width: 100%; height: 100%; }
/* A trap Torech pocketed. Same footprint as a token so the column reads as one rack of held
   pieces, and his own red rim so the two are never confused. Face up only when it is YOURS or
   the game is open -- see .face-down below. */
.held-tile {
    display: block; padding: 0; border: 0; background: rgba(28, 10, 34, .92); cursor: default;
    border-radius: 2px; overflow: hidden;
    box-shadow: 0 0 0 1px rgba(255, 150, 120, .7), 0 2px 6px rgba(4, 2, 10, .6);
}
.held-tile img { display: block; width: 100%; height: 100%; object-fit: cover; }
.held-tile.live { cursor: pointer; animation: tilePulse 2.4s ease-in-out infinite; }
.held-tile.live:hover { filter: brightness(1.15); }
@keyframes tilePulse {
    0%, 100% { box-shadow: 0 0 0 1px rgba(255,150,120,.7), 0 2px 6px rgba(4,2,10,.6); }
    50%      { box-shadow: 0 0 0 1.5px #ffb49a, 0 0 16px rgba(230,120,90,.8), 0 2px 6px rgba(4,2,10,.6); }
}
/* the copy that travels from the board to that rack */
.trap-mark.pocket-fly { position: fixed; z-index: 1200; pointer-events: none; transform: none; }
/* A kept trap the player is not allowed to identify: the same printed back the board gives it,
   so his rack and his slots say the same thing about the same object. */
.held-tile.face-down {
    background: #241536 url('assets/cards/tile-back.webp') center / cover no-repeat;
    box-shadow: inset 0 0 0 1px rgba(255, 150, 120, .55), 0 0 7px rgba(220, 80, 60, .35);
}
.held-token.live { cursor: pointer; animation: tokenPulse 2.4s ease-in-out infinite; }
.held-token.live:hover { filter: brightness(1.15); }
/* The rim, not the disc: the piece is a tile now, and the pulse says "this one is spendable
   right now" without repainting its face. */
@keyframes tokenPulse {
    0%, 100% { box-shadow: 0 0 0 1px rgba(238,214,158,.55), 0 2px 6px rgba(4,2,10,.6); }
    50%      { box-shadow: 0 0 0 1.5px #ffe9a8, 0 0 16px rgba(255,225,140,.8), 0 2px 6px rgba(4,2,10,.6); }
}

/* ── action bar ────────────────────────────────────────────────────────────── */

#action-bar { position: absolute; transform: translateX(-50%);
              display: flex; gap: 10px; align-items: center; justify-content: center; }
/* THE ACTION BUTTONS. They were flat plates with a hairline that changed colour on hover --
   correct, and completely without weight on a painted board where everything else is either
   a lit object or a printed one. They are objects now: a lit top edge, a body that falls away
   downwards, a real cast shadow, and a press that actually travels. Nothing here is a new
   colour -- the primary is the accent it always was, and the secondary is the same night
   ground and gold rule as the help banner and the tableau bed, so the three things on this
   board that are UI rather than painting are recognisably one family. */
.act-btn {
    position: relative;
    min-height: 44px; min-width: 44px; padding: 9px 20px;
    font-family: var(--dk-font-ui); font-size: 14px; font-weight: 600;
    letter-spacing: .012em;
    color: var(--vi-ink);
    background: linear-gradient(180deg, rgba(38, 27, 68, .96), rgba(22, 14, 44, .97));
    border: 0; border-radius: var(--dk-radius);
    box-shadow:
        0 0 0 1px rgba(201, 162, 74, .38),
        inset 0 1px 0 rgba(255, 244, 214, .10),
        inset 0 -1px 0 rgba(0, 0, 0, .35),
        0 3px 0 rgba(8, 4, 18, .55),
        0 8px 18px rgba(4, 2, 10, .48);
    cursor: pointer; touch-action: manipulation;
    transition: transform .12s ease, box-shadow .12s ease, filter .12s ease;
}
.act-btn:hover {
    transform: translateY(-1px);
    filter: brightness(1.14);
    box-shadow:
        0 0 0 1px rgba(201, 162, 74, .68),
        inset 0 1px 0 rgba(255, 244, 214, .14),
        inset 0 -1px 0 rgba(0, 0, 0, .35),
        0 4px 0 rgba(8, 4, 18, .55),
        0 11px 22px rgba(4, 2, 10, .55);
}
/* A press that travels the full height of its own shadow, so the button lands on the board
   instead of dimming in place. */
.act-btn:active {
    transform: translateY(3px);
    box-shadow:
        0 0 0 1px rgba(201, 162, 74, .68),
        inset 0 2px 5px rgba(0, 0, 0, .45),
        0 0 0 rgba(8, 4, 18, .55),
        0 3px 8px rgba(4, 2, 10, .5);
}
.act-btn:focus-visible { outline: 2px solid var(--dk-accent); outline-offset: 3px; }
/* The primary carries the accent, and is the only thing in the row that does. */
.act-btn.primary {
    background: linear-gradient(180deg, #c661cf, #8c3a9d);
    color: #fff;
    text-shadow: 0 1px 2px rgba(40, 8, 48, .55);
    box-shadow:
        0 0 0 1px rgba(238, 168, 245, .55),
        inset 0 1px 0 rgba(255, 226, 255, .38),
        inset 0 -1px 0 rgba(60, 12, 72, .5),
        0 3px 0 rgba(58, 14, 70, .7),
        0 8px 20px rgba(120, 30, 140, .38);
}
.act-btn.primary:hover {
    box-shadow:
        0 0 0 1px rgba(245, 190, 250, .75),
        inset 0 1px 0 rgba(255, 232, 255, .46),
        inset 0 -1px 0 rgba(60, 12, 72, .5),
        0 4px 0 rgba(58, 14, 70, .7),
        0 12px 26px rgba(150, 40, 175, .48);
}
.act-btn.primary:active {
    box-shadow:
        0 0 0 1px rgba(245, 190, 250, .75),
        inset 0 2px 6px rgba(50, 10, 60, .55),
        0 0 0 rgba(58, 14, 70, .7),
        0 3px 9px rgba(120, 30, 140, .35);
}
.act-btn small { display: block; font-size: 11px; opacity: .7; font-weight: 400;
                 letter-spacing: 0; margin-top: 1px; }
@media (prefers-reduced-motion: reduce) {
    .act-btn, .act-btn:hover, .act-btn:active { transition: none; transform: none; }
}

/* == THE HELP BANNER =================================================================
 * The one sentence the game says to the player while they are deciding, laid on the board.
 * #status-line belongs to the shared sheet, so everything here hangs off the game's own
 * `.on-board` class -- the contract's child-safe route, not a rule on a shared selector.
 *
 * IT IS THE BOARD'S OWN RAIL. It used to be a dark rounded rectangle with a magenta hairline,
 * a backdrop blur and a glow that breathed every 3.2 seconds, and there are three separate
 * things wrong with that on this particular board:
 *
 *   - It was the shape of a WEB NOTIFICATION, dropped on a painting. This board already has
 *     a vocabulary for "a strip that tells you something": the lantern track across its top
 *     is a gold double rule with rounded caps over the night sky. The sentence is set on that
 *     same rail, so it reads as part of the printed object rather than as UI standing on it.
 *   - The magenta is the ACCENT, and the accent is not in the painting. On chrome it is the
 *     game's signature; over Karina Duncan's art it is the one foreign colour on screen, and
 *     it was outlining the largest thing laid over the board.
 *   - The glow LOOPED. On a Deckades board a pulse means "you may act here" -- it is what the
 *     helper ring and the faery ring say -- so a banner that pulses forever is a permanent
 *     false cue, and it now also competes with the ambient layer in life.js. What replaces it
 *     is finite and tied to something real: the banner settles once, when the sentence
 *     CHANGES (header.js retriggers it), which is the only moment there is news to notice.
 *
 * And it WRAPS. The shared sheet sets `white-space: nowrap` with an ellipsis, which is right
 * for a status line squeezed beside a hand and wrong for a full sentence in the middle of a
 * board: German runs about a third longer than English here, and "Nimm eine Karte, um zuerst
 * zu waehlen -- oder lass Torech beginnen" was one truncation away at the 1024px floor. */
.on-board #status-msg.show {
    /* The night sky the rail is printed on, lit from above like the rail itself. */
    background: linear-gradient(180deg, var(--vi-night-hi), var(--vi-night-lo));
    /* THE DOUBLE RULE, which is what makes it the board's rail and not a pill: an outer gold
       line, a gap of ground, and an inner line. Both are drawn as ring shadows, so the gap
       between them is real ground rather than a third colour invented to sit there. */
    border: 0;
    border-radius: 999px;
    box-shadow:
        0 0 0 1px var(--vi-rail),
        inset 0 0 0 3px var(--vi-night-hi),
        inset 0 0 0 4px var(--vi-rail-soft),
        inset 0 1px 0 rgba(255, 244, 214, .10),
        0 8px 26px rgba(4, 2, 10, .55);
    padding: 10px 28px;
    /* Wrapping, and a measure that stops at a readable line rather than at a share of the
       viewport: 46ch is about eleven words, which is every string this game shows here. */
    white-space: normal;
    max-width: min(46ch, 52vw);
    /* ...and it must be ABLE to honour a narrower cap. positionStatusLine() measures the pill
       after setting a max-width and places it from that; a line that cannot wrap below its
       own longest word ignores the cap, comes back wider than measured, and lands on the
       played cards. German compounds are what makes this real rather than theoretical. */
    overflow-wrap: break-word;
    hyphens: auto;
    font-size: 19px;
    line-height: 1.38;
    letter-spacing: .01em;
    text-shadow: 0 2px 10px rgba(4, 2, 10, .9);
}
/* The journey's two answers, under the question, inside the rail. #status-line is
   pointer-events:none for the whole layer (it is a caption), so the row has to opt back in --
   and only the row, so the rest of the banner stays a caption and never eats a click meant
   for the board behind it. */
.banner-acts {
    display: flex; justify-content: center; flex-wrap: wrap; gap: 8px;
    margin: 9px 0 1px; pointer-events: auto;
}
.banner-acts .act-btn { margin: 0; }

/* Cream, not the platform's 78% parchment -- this sits on a lit night ground rather than on
   the felt. The two state colours must still win, so they are excluded by name rather than
   by hoping the cascade lands the right way round: .on-board #status-msg.show is (1,2,1) and
   would otherwise beat #status-msg.msg-alert at (1,1,0). */
.on-board #status-msg.show:not(.msg-alert):not(.msg-good) { color: var(--vi-ink); }

/* One settle when the sentence changes. Rise and land, 340ms, and then nothing -- a gesture,
   not a state. header.js adds the class only when the text is actually different, so the
   banner does not twitch on every render that happens to repaint the same words. */
.on-board #status-msg.settle { animation: vi-banner-settle .34s cubic-bezier(.16, 1, .3, 1); }
@keyframes vi-banner-settle {
    from { transform: translateY(-5px); opacity: .35; }
    to   { transform: none;             opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .on-board #status-msg.settle { animation: none; }
}

/* ── flying cards ──────────────────────────────────────────────────────────── */

.fly { position: fixed; z-index: 900; pointer-events: none; transform-origin: center center; }

/* ── the rules link on the start screen ────────────────────────────────────── */
/* its own class, NOT .start-play — those are launch buttons and the shared sheet gives
   them width:100%, which would make a one-word link the widest control on the card */
.rules-link {
    align-self: center; display: inline-block; width: auto;
    margin-top: 2px; padding: 7px 16px; text-align: center; white-space: nowrap;
    font-family: var(--dk-font-ui); font-size: 13px; text-decoration: none;
    color: rgba(232, 220, 250, .82);
    border: 1px solid var(--dk-accent-soft); border-radius: var(--dk-radius);
}
.rules-link:hover { color: var(--vi-ink); border-color: var(--dk-accent); }

/* ── the side badge Torech's tiles get when they fire ──────────────────────── */
/* One column for all of them, so two sentences in the same beat stack instead of landing on
   top of each other. `top` is set in JS, from the header's own height. */
/* Positioned by placeToasts() onto the lantern rail; the declarations here are only what it
   does not set. */
#vi-toasts {
    position: fixed; left: 50%; transform: translateX(-50%);
    display: flex; flex-direction: column; align-items: center; gap: 6px;
    z-index: 950; pointer-events: none; max-width: 92vw;
}
/* Set like the game's other sentences, not like a notification: the same night ground and
   gold double rule as the banner on the board, at a size you read rather than notice. It was
   13px UI type in a red-edged box, which made "You show your hand to Torech" -- a thing that
   just happened TO you -- look like a log line that had escaped. */
.tile-toast {
    background: linear-gradient(180deg, var(--vi-night-hi), var(--vi-night-lo));
    border: 0; border-radius: 999px; padding: 9px 22px;
    box-shadow:
        0 0 0 1px var(--vi-rail),
        inset 0 0 0 3px var(--vi-night-hi),
        inset 0 0 0 4px var(--vi-rail-soft),
        0 8px 26px rgba(4, 2, 10, .55);
    font-family: var(--dk-font-display); font-size: 17px; line-height: 1.35;
    letter-spacing: .01em; color: var(--vi-ink);
    text-align: center; text-shadow: 0 2px 10px rgba(4, 2, 10, .9);
    max-width: min(46ch, 82vw);
    pointer-events: none;
    opacity: 0; transform: translateY(-6px);
    transition: opacity .25s ease, transform .25s ease;
}
@media (max-width: 620px), (max-height: 560px) { .tile-toast { font-size: 14px; padding: 7px 16px; } }
.tile-toast.show { transform: translateY(0); }
.tile-toast.show { opacity: 1; }
.tile-toast b { color: #ffb27a; }


/* the little count chip on the deck and the discard — the house pattern, in this game's pink */
.pile-count {
    position: absolute; transform: translate(-50%, -50%);
    min-width: 18px; height: 18px; padding: 0 5px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 9px;
    background: var(--dk-accent); color: #1b0d20;
    font-family: var(--dk-font-ui); font-size: 11px; font-weight: 600;
    font-variant-numeric: tabular-nums;
    box-shadow: 0 2px 6px rgba(0,0,0,.6);
    pointer-events: none; z-index: 4;
}
.pile-count.empty { background: rgba(208,100,216,.35); color: rgba(255,255,255,.75); }

/* ── the zones around the board ─────────────────────────────────────────────── */

/* The fear slots are printed ON the board's right edge — a rect two card-heights tall,
   because a slot holds the PAIR from one turn Torech won. Sized in board percentages so
   they track the art at every board size. */
.fear-slot { position: absolute; pointer-events: none; }
.fear-card {
    transform-origin: center center;
    box-shadow: 0 3px 10px rgba(0, 0, 0, .7);
    z-index: 3;
}

/* the pair pushed off the second slot, turned face up so both players see the difference */
.fear-reveal {
    opacity: 0; transform: scale(.7) rotate(-6deg);
    transition: opacity .26s ease, transform .34s cubic-bezier(.2,1.5,.4,1);
    box-shadow: 0 10px 30px rgba(0,0,0,.7), 0 0 0 2px rgba(255,140,110,.75);
}
.fear-reveal.shown { opacity: 1; transform: scale(1) rotate(0deg); }

/* ── tooltips ───────────────────────────────────────────────────────────────── */

.card-tip {
    position: fixed; z-index: 10001; max-width: 250px;
    padding: 9px 12px; border-radius: var(--dk-radius);
    background: rgba(20, 12, 34, .98);
    border: 1px solid var(--dk-accent-soft);
    box-shadow: 0 10px 30px rgba(0, 0, 0, .66);
    font-family: var(--dk-font-ui); font-size: 12.5px; line-height: 1.45;
    color: rgba(232, 220, 250, .94);
    opacity: 0; pointer-events: none;
    transition: opacity .13s ease;
}
.card-tip.on { opacity: 1; }
.card-tip b { display: block; color: var(--dk-accent); font-size: 13.5px; margin-bottom: 3px; }
.card-tip span { display: block; }
.card-tip .tip-trump { color: #ffd489; font-size: 11.5px; margin-bottom: 3px; }
.card-tip .tip-mute { color: #ff9a86; font-size: 11.5px; margin-top: 4px; }
/* "and you can spend it right now" -- the same warm cue the trump line uses */
.card-tip .tip-cue { color: #ffd489; font-size: 11.5px; margin-top: 4px; }

/* ── a 9 torn by a 1 ────────────────────────────────────────────

   The wrapper is a card-shaped BOX and nothing else: the ground, the corner and the shadow
   all move onto the two halves, or the gap the tear opens would be filled with card-coloured
   nothing instead of with the table. */
.card.torn {
    background: none; box-shadow: none; overflow: visible;
    transition: none;
}
.card.torn .tear-half {
    position: absolute; left: 0; top: 0;
    /* The raw edge, and the piece's own weight. Two shadows: a tight pale one for the fibres
       the tear has just exposed, and an offset, blurred dark one because each half is now a
       separate scrap lying on the table at its own angle. */
    filter: drop-shadow(0 3px 7px rgba(4, 2, 10, .7))
            drop-shadow(0 0 .5px rgba(255, 244, 214, .5));
    transform: translate(var(--tx), var(--ty)) rotate(var(--tr));
    transition: transform 620ms cubic-bezier(.12, .82, .24, 1);
    will-change: transform;
}
/* Closed. The card is whole for exactly one frame, so the tear is something that HAPPENS
   rather than a state the board was already in when you looked. */
.card.torn.sealed .tear-half { transform: none; }

/* The fibres, lit along the line the tear is running down. It follows the same points the
   clip does, so the light is on the actual edge and not near it. */
.card.torn .tear-spark {
    position: absolute; inset: 0; width: 100%; height: 100%;
    pointer-events: none; overflow: visible;
}
.card.torn .tear-spark polyline {
    fill: none;
    stroke: rgba(255, 246, 224, .95);
    stroke-width: 1.8;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
    filter: drop-shadow(0 0 4px rgba(255, 214, 130, .9));
}
/* The tear RUNS, top to bottom, before anything comes apart — the seam is revealed along its
   own length rather than appearing all at once. A clip, not a dash offset: the stroke is
   non-scaling inside a non-uniform 100x100 viewBox, so a dash pattern measured in user units
   would not match the line it is dashing. */
.card.torn .tear-spark {
    clip-path: inset(0 0 0 0);
    opacity: 1;
    transition: clip-path 300ms cubic-bezier(.45, .05, .6, .95), opacity 400ms ease;
}
.card.torn.drawing .tear-spark { clip-path: inset(0 0 100% 0); }
.card.torn.ripped .tear-spark { opacity: 0; }

/* ...and the paper that stays with neither half. */
.tear-fleck {
    position: absolute; width: 3px; height: 7px; border-radius: 1px;
    background: linear-gradient(180deg, rgba(255, 246, 224, .92), rgba(206, 190, 160, .5));
    pointer-events: none;
    animation: tearFleck 720ms cubic-bezier(.2, .6, .35, 1) both;
}
@keyframes tearFleck {
    0%   { opacity: 0; transform: none; }
    12%  { opacity: .95; }
    100% { opacity: 0; transform: translate(var(--fx), var(--fy)) rotate(var(--fr)); }
}

/* The card that did it. ONE pass of light across the 1 — the tear is the performance and a
   second one beside it in the same 200ms means neither is read. */
.card.struck { position: absolute; overflow: hidden; }
.card.struck::after {
    content: ''; position: absolute; inset: -20% -60%;
    background: linear-gradient(104deg, rgba(255, 246, 224, 0) 42%,
        rgba(255, 246, 224, .55) 50%, rgba(255, 246, 224, 0) 58%);
    animation: cardStruck 460ms cubic-bezier(.3, .7, .35, 1) both;
    pointer-events: none;
}
@keyframes cardStruck {
    from { transform: translateX(-58%); }
    to   { transform: translateX(58%); }
}

/* Reduced motion keeps the RESULT — the 9 is in two pieces, which is the whole point — and
   drops the flight: the halves are simply apart, the flecks never leave, and the fibres light
   once without the stroke growing. */
@media (prefers-reduced-motion: reduce) {
    .card.torn .tear-half { transition: opacity 200ms ease; }
    .card.torn.sealed .tear-half { transform: translate(var(--tx), var(--ty)) rotate(var(--tr)); }
    .tear-fleck { display: none; }
    .card.torn .tear-spark { transition: clip-path 120ms linear, opacity 220ms ease; }
    .card.struck::after { animation: none; opacity: 0; }
}

/* The back of a card coming out of a flip — the second half of the turn the faces began. */
.card.flip-up { animation: cardFlipUp 170ms ease-out both; }
@keyframes cardFlipUp { from { transform: scaleX(0.04); } to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) {
    .card.flip-up { animation: cardFadeUp 170ms ease-out both; }
    @keyframes cardFadeUp { from { opacity: 0; } to { opacity: 1; } }
}

/* A CARD LAID ON THE BOARD ITSELF, rather than on the felt. The felt is one flat colour and
   a card only has to be lighter than it; the board is a painting of a moonlit forest, and a
   card dropped on it has no single ground to stand out from -- pale sky in one place, black
   trees in another. So the separation is carried by the card's own edge, not by contrast
   with what is behind it:

     a dark keyline hard against the art, then a warm rim the colour of a printed card's
     white border, then a two-stage pool underneath -- a tight one for the height of the
     card off the board, a wide soft one for the shade it casts on the painting.

   The rim is what does the work. A shadow alone vanishes where the art is already dark,
   which is exactly where the played pair lands. */
.card.on-table {
    box-shadow:
        0 0 0 1px rgba(10, 5, 20, .92),
        0 0 0 3px rgba(252, 244, 226, .34),
        0 3px 8px rgba(0, 0, 0, .60),
        0 12px 26px rgba(0, 0, 0, .70),
        0 26px 52px rgba(0, 0, 0, .42);
    cursor: help;                        /* it answers, so it should say that it does */
    transition: transform .16s cubic-bezier(.16, 1, .3, 1), filter .16s ease;
}
/* The pair on the board is the one thing on this screen you can interrogate but not click,
   and until now nothing under the pointer said so. A card that lifts a little is the whole
   answer — TRANSFORM only, so it does not have to fight the winner's ring or the leader's
   outline for the box-shadow both of those are written in. The tooltip is the payload and it
   has its own long-press path on touch, so this adds an affordance and never a dependency. */
.card.on-table:hover { transform: translateY(-3px) scale(1.025); filter: brightness(1.06); }
@media (prefers-reduced-motion: reduce) {
    .card.on-table { transition: filter .16s ease; }
    .card.on-table:hover { transform: none; }
}
/* The card that took the turn, while the pair waits on the table for it to finish -- and the
   leader's, while the turn is still open. BOTH are box-shadow marks on a card that is also
   `.on-table`, at the same specificity, so they have to come AFTER it or the rim above simply
   overwrites them and the mark never appears. It did not: this rule used to sit above and the
   winner's ring has been invisible on the board the whole time. */
.card.on-table.selected {
    box-shadow: 0 0 0 1px rgba(10, 5, 20, .92), 0 0 0 3px var(--dk-accent),
                0 12px 26px rgba(0, 0, 0, .70), 0 26px 52px rgba(0, 0, 0, .42);
}
.card.took-it, .card.on-table.took-it {
    box-shadow: 0 0 0 1px rgba(10, 5, 20, .92), 0 0 0 3px var(--dk-accent),
                0 0 18px rgba(208, 100, 216, .6),
                0 12px 26px rgba(0, 0, 0, .70), 0 26px 52px rgba(0, 0, 0, .42);
}


/* ── the trap draft ─────────────────────────────────────────────────────── */

/* A tray, not a modal: the drop targets are the slots printed on the BOARD, so the board has
   to stay visible and reachable behind it. */
.trap-tray-panel {
    position: fixed; left: 50%; transform: translateX(-50%);
    bottom: 14px;                          /* the fallback; placeTrapTray() sets `top` instead */
    z-index: 1150; max-width: 96vw;
    padding: 12px 16px 14px; border-radius: var(--dk-radius-lg);
    background: rgba(18, 10, 34, .97);
    border: 1px solid var(--dk-accent-soft);
    box-shadow: 0 14px 44px rgba(0, 0, 0, .66);
}
.trap-tray-panel.over { border-color: var(--dk-accent); background: rgba(40, 18, 62, .97); }
/* Hanging above the slots: the panel points at them, so it wears a small nose on that edge
   the way a tooltip does — the one thing that tells you the two are one gesture. */
.trap-tray-panel.above::after {
    content: ''; position: absolute; left: 50%; bottom: -7px;
    width: 14px; height: 14px; transform: translateX(-50%) rotate(45deg);
    background: linear-gradient(135deg, rgba(30, 16, 52, 0) 48%, rgba(14, 8, 26, .98) 48%);
    border-right: 1px solid var(--dk-accent-soft);
    border-bottom: 1px solid var(--dk-accent-soft);
}
/* One step down when the band above the slots is shorter than the tray. */
.trap-tray-panel.dense { padding: 8px 12px 10px; }
.trap-tray-panel.dense .trap-head { font-size: 16px; }
.trap-tray-panel.dense .trap-sub { margin: 1px 0 7px; font-size: 11px; }
.trap-tray-panel.dense .trap-row { gap: 7px; }
.trap-tray-panel.dense .trap-row + .trap-row { margin-top: 7px; }
.trap-tray-panel.dense .trap-tile { width: 78px; padding: 4px; }
.trap-tray-panel.dense .trap-tile span { font-size: 10px; margin-top: 3px; }
.trap-head {
    font-family: var(--dk-font-display); font-size: 20px; font-weight: 600;
    color: var(--vi-rail); text-align: center;
}
.trap-sub {
    margin: 2px 0 10px; text-align: center;
    font-family: var(--dk-font-ui); font-size: 12.5px; line-height: 1.45;
    color: rgba(226, 210, 245, .70);
}
/* Three over two: five in a row is a strip you scan, 3/2 is a shape you take in at once.
   Two flex rows, each centred on its own — the grid version put the pair of two around the
   boundary between columns one and two, a third of a tile left of the middle. */
.trap-row { display: flex; justify-content: center; gap: 10px; }
.trap-row + .trap-row { margin-top: 10px; }
.trap-tile {
    width: 118px; padding: 6px; border-radius: 8px; cursor: grab;
    background: linear-gradient(180deg, var(--vi-night-hi), var(--vi-night-lo));
    border: 0;
    box-shadow: 0 0 0 1px var(--vi-rail-soft), inset 0 1px 0 rgba(255, 244, 214, .07);
    transition: transform .14s ease, box-shadow .14s ease, opacity .14s ease;
    touch-action: manipulation;
}
/* The tile art is 154x109 on disk. Declaring that ratio means the panel has its final height
   from the first frame, before a single image has loaded -- without it the tray measured
   ~200px short, placed itself against that, and then grew back down over the very slots it
   was supposed to be clear of. */
.trap-tile img { width: 100%; aspect-ratio: 154 / 109; display: block; border-radius: 4px; }
.trap-tile span {
    display: block; margin-top: 5px; text-align: center;
    font-family: var(--dk-font-ui); font-size: 11px; line-height: 1.3;
    color: var(--vi-ink); opacity: .88;
}
.trap-tile:hover { transform: translateY(-2px); box-shadow: 0 0 0 1px var(--vi-rail), 0 6px 18px rgba(4, 2, 10, .55); }
.trap-tile.selected {
    transform: translateY(-2px);
    box-shadow: 0 0 0 2px var(--vi-rail), 0 0 18px rgba(201, 162, 74, .45);
}
.trap-tile.picked { opacity: .3; }
.trap-tile.dragging { opacity: .5; }

/* A journey space that will take the tile Torech is holding. Breathing, like the trap slots
   during the draft — the two are the same gesture at two moments, so they look the same. */
.place-target {
    position: absolute; transform: translate(-50%, -50%);
    width: 5%; aspect-ratio: 1; border-radius: 50%;
    /* A CREAM RING ON A DARK SCRIM. The first version was a 35%-alpha dashed accent border
       over a 12% wash, which is invisible on this painting -- measured 51px across, opacity
       1, and not findable in a screenshot. A drop target the player has to hunt for is not a
       drop target. The scrim is what makes it work on both the snowfield and the forest. */
    border: 2px solid rgba(255, 244, 214, .92);
    background: radial-gradient(circle, rgba(208, 100, 216, .34) 0%, rgba(20, 8, 34, .52) 72%);
    box-shadow: 0 0 0 1px rgba(20, 8, 34, .85), 0 2px 10px rgba(4, 2, 10, .6);
    pointer-events: auto; cursor: pointer;
    /* The pulse is LIGHT, never geometry. A target that grows and shrinks moves under the
       pointer — harder to hit on a phone, and an automated driver will not click a thing that
       never comes to rest. The trap slots' own breathe does box-shadow only for the same
       reason; this one had to learn it the same way. */
    animation: placePulse 2.2s ease-in-out infinite;
}
@keyframes placePulse {
    0%, 100% { box-shadow: 0 0 0 1px rgba(20, 8, 34, .85), 0 2px 10px rgba(4, 2, 10, .6); }
    50%      { box-shadow: 0 0 0 1px rgba(20, 8, 34, .85), 0 0 18px rgba(226, 128, 232, .65),
                           0 2px 10px rgba(4, 2, 10, .6); }
}
.place-target:hover, .place-target.picked {
    border-color: #fff8e2;
    background: radial-gradient(circle, rgba(226, 128, 232, .62) 0%, rgba(40, 14, 60, .6) 74%);
    box-shadow: 0 0 0 1px rgba(20, 8, 34, .85), 0 0 20px rgba(226, 128, 232, .7),
                0 2px 10px rgba(4, 2, 10, .6);
    animation: none;
}
@media (prefers-reduced-motion: reduce) { .place-target { animation: none; } }
/* Adapt asks a second question — which colour — and asks it ON the space, because that is
   what the answer is about. */
.place-colours {
    position: absolute; left: 50%; bottom: 116%; transform: translateX(-50%);
    display: flex; gap: 5px; padding: 5px 6px; border-radius: var(--dk-radius);
    background: linear-gradient(180deg, var(--vi-night-hi), var(--vi-night-lo));
    box-shadow: 0 0 0 1px var(--vi-rail), 0 8px 22px rgba(4, 2, 10, .7);
}
.place-swatch {
    width: 26px; height: 26px; padding: 0; cursor: pointer;
    border: 0; background: none; border-radius: 0;
    filter: drop-shadow(0 1px 3px rgba(4, 2, 10, .6));
    transition: transform .12s ease;
}
.place-swatch img { display: block; width: 100%; height: 100%; object-fit: contain; }
.place-swatch:focus-visible { outline: 2px solid var(--vi-rail); outline-offset: 2px; }
.place-swatch:hover { transform: scale(1.18); }

/* the printed slots, while they are live drop targets */
.trap-mark.drop {
    pointer-events: auto; cursor: pointer;
    border-style: dashed; border-color: var(--dk-accent-soft);
    background: rgba(208, 100, 216, .10);
    animation: dropBreathe 2.6s ease-in-out infinite;
}
.trap-mark.drop.over, .trap-mark.drop:hover {
    border-color: var(--dk-accent); background: rgba(208, 100, 216, .26);
}
.trap-mark.drop.open { border-style: solid; animation: none; }
@keyframes dropBreathe {
    0%, 100% { box-shadow: 0 0 0 rgba(208,100,216,0); }
    50%      { box-shadow: 0 0 18px rgba(208,100,216,.5); }
}

@media (max-width: 620px), (max-height: 560px) {
    .trap-tile { width: 84px; }
    .trap-tile span { font-size: 10px; }
    .trap-head { font-size: 16px; }
}


/* ── the end of the story ────────────────────────────────────
 *
 * The game ends with somebody standing in the wood at night, so the panel shows that rather
 * than announcing it over a flat ground. The figure is the SAME drawn piece that walked the
 * track — Violet with her lantern lit, or the nisse alone — which is what makes the picture
 * a result and not decoration. Everything in it is CSS: a gradient sky, a moon, two clipped
 * treelines for depth, a snowline, and a drifting layer that is snow when Violet wins and
 * embers when she does not.
 *
 * Only the game's own classes are styled. `.overlay` and `.panel` belong to the shared
 * sheet and are left exactly as they are. */

#over-overlay { animation: viOvIn .34s ease both; }
@keyframes viOvIn { from { opacity: 0; } }

/* No `overflow: hidden` here: the shared panel scrolls when it is taller than the viewport,
   and clipping it takes the last button out of reach on a short screen. The scene rounds its
   own top corners instead — 15px, the panel's 16px radius less its 1px border. */
.panel.vi-end { width: min(470px, 94vw); padding-top: 0; }

.vi-scene {
    position: relative; overflow: hidden;
    margin: 0 -24px 16px; height: 156px;
    border-radius: 15px 15px 0 0;
    background: linear-gradient(180deg, #0b0722 0%, #1b1140 46%, #3a2064 74%, #6b3f7e 100%);
    box-shadow: inset 0 -1px 0 var(--vi-rail-soft);
}
/* THE LOSING SKY IS A LIGHT GOING OUT. It ran to #6d2a26 at the horizon, which is a salmon
   sunset — a warm, open, rather pretty sky under which to be told the lantern has failed. The
   ramp now dies into a dull ember instead: the same hue, two thirds of the way down, so the
   embers rising off it still read while the picture as a whole is plainly darker than the
   one you get for winning. That difference is the only thing this gradient has to say. */
.vi-scene.dark {
    background: linear-gradient(180deg, #08040c 0%, #170a15 48%, #2c0f17 76%, #48191b 100%);
}

/* the moon, with a halo the same warmth as the lantern flame */
.vi-moon {
    position: absolute; right: 15%; top: 16%; width: 34px; height: 34px;
    border-radius: 50%;
    background: radial-gradient(circle at 38% 34%, #fffdf2, #ffe9b8 58%, #e8c98a);
    box-shadow: 0 0 26px 10px rgba(255, 232, 180, .22), 0 0 60px 26px rgba(255, 232, 180, .10);
}
.vi-scene.dark .vi-moon {
    background: radial-gradient(circle at 38% 34%, #ffe6d8, #f0a58c 58%, #b96a55);
    box-shadow: 0 0 26px 10px rgba(240, 130, 100, .22), 0 0 60px 26px rgba(240, 130, 100, .10);
}

/* Snow, as three repeating tiles at different scales drifting at one speed — parallax comes
   from the tile sizes, not from three separate animations. */
.vi-fall {
    position: absolute; inset: -30% 0 0 0; pointer-events: none;
    background-image:
        radial-gradient(2.4px 2.4px at 14% 22%, rgba(255,255,255,.95), transparent 58%),
        radial-gradient(1.9px 1.9px at 62% 48%, rgba(255,255,255,.80), transparent 58%),
        radial-gradient(1.5px 1.5px at 33% 78%, rgba(255,255,255,.62), transparent 58%);
    background-size: 74px 62px, 104px 88px, 53px 47px;
    animation: viFall 13s linear infinite;
}
.vi-scene.dark .vi-fall {
    background-image:
        radial-gradient(2.4px 2.4px at 14% 22%, rgba(255,186,120,.95), transparent 58%),
        radial-gradient(1.9px 1.9px at 62% 48%, rgba(255,132,84,.80), transparent 58%),
        radial-gradient(1.5px 1.5px at 33% 78%, rgba(255,96,62,.62), transparent 58%);
    animation-direction: reverse;              /* embers rise */
}
@keyframes viFall {
    to { background-position: -16px 62px, 22px 88px, -9px 47px; }
}

/* TWO TREELINES, and they are DRAWN TREES.
 *
 * They were clip-path sawteeth — isosceles triangles of one width, evenly spaced — which at
 * any size reads as a graph line rather than as a wood, and which on a game whose entire
 * identity is a painted forest is the cheapest possible forest. A conifer has a trunk, tiers
 * that step out as they descend, and a neighbour that is never the same height; the strips
 * below are generated with all three (tools/trees.mjs in the scratchpad, seeded so it is the
 * same wood every build) and used as a MASK rather than as a picture, so each ridge keeps its
 * own colour and the dark variant still works by changing one value.
 *
 * The strips are 340 and 300 wide against a 470px panel, so the repeat lands roughly once and
 * is not readable as a repeat. They are `repeat-x` at a FIXED width rather than stretched:
 * `preserveAspectRatio: none` plus a percentage width would squash the trees on a narrow
 * viewport, and a squashed conifer is a sawtooth again. */
.vi-ridge { position: absolute; left: -2%; right: -2%; bottom: 26px;
            -webkit-mask-repeat: repeat-x; mask-repeat: repeat-x;
            -webkit-mask-position: bottom left; mask-position: bottom left; }
.vi-ridge-far {
    bottom: 34px;                  /* set back and up just enough to show through the gaps */
    height: 62px; background: #3a2a5c; opacity: .55; filter: blur(.6px);
    -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='340' height='70' viewBox='0 0 340 70' preserveAspectRatio='none'><path d='M-6.0,18.9L-10.2,36.4L-8.4,36.4L-13.4,50.2L-10.7,50.2L-16.6,62.7L-7.4,62.7L-7.4,65.0L-4.6,65.0L-4.6,62.7L4.6,62.7L-1.3,50.2L1.4,50.2L-3.6,36.4L-1.8,36.4ZM5.4,32.5L-0.4,44.9L2.0,44.9L-4.8,54.6L-1.0,54.6L-9.1,63.4L3.5,63.4L3.5,65.0L7.2,65.0L7.2,63.4L19.8,63.4L11.7,54.6L15.5,54.6L8.7,44.9L11.1,44.9ZM25.7,36.2L19.8,47.1L22.3,47.1L15.4,55.8L19.2,55.8L11.1,63.6L23.8,63.6L23.8,65.0L27.6,65.0L27.6,63.6L40.3,63.6L32.1,55.8L35.9,55.8L29.0,47.1L31.5,47.1ZM47.8,24.0L42.6,39.6L44.8,39.6L38.8,51.9L42.1,51.9L35.0,63.0L46.1,63.0L46.1,65.0L49.4,65.0L49.4,63.0L60.6,63.0L53.4,51.9L56.7,51.9L50.7,39.6L52.9,39.6ZM61.3,12.7L57.7,32.5L59.2,32.5L55.1,48.2L57.4,48.2L52.5,62.4L60.1,62.4L60.1,65.0L62.4,65.0L62.4,62.4L70.0,62.4L65.1,48.2L67.4,48.2L63.3,32.5L64.8,32.5ZM74.4,39.6L70.8,49.3L72.3,49.3L68.0,56.9L70.4,56.9L65.3,63.7L73.2,63.7L73.2,65.0L75.6,65.0L75.6,63.7L83.4,63.7L78.4,56.9L80.7,56.9L76.5,49.3L78.0,49.3ZM85.0,30.7L81.6,43.7L83.1,43.7L79.1,54.0L81.3,54.0L76.6,63.3L83.9,63.3L83.9,65.0L86.1,65.0L86.1,63.3L93.4,63.3L88.7,54.0L90.9,54.0L87.0,43.7L88.4,43.7ZM97.3,29.8L93.1,43.2L94.9,43.2L89.9,53.7L92.6,53.7L86.7,63.2L95.9,63.2L95.9,65.0L98.7,65.0L98.7,63.2L107.9,63.2L102.0,53.7L104.7,53.7L99.7,43.2L101.5,43.2ZM111.2,35.9L107.6,47.0L109.1,47.0L104.8,55.7L107.2,55.7L102.1,63.5L110.0,63.5L110.0,65.0L112.4,65.0L112.4,63.5L120.4,63.5L115.3,55.7L117.6,55.7L113.3,47.0L114.9,47.0ZM124.6,33.3L119.7,45.3L121.8,45.3L116.0,54.8L119.2,54.8L112.3,63.4L123.0,63.4L123.0,65.0L126.2,65.0L126.2,63.4L136.9,63.4L130.0,54.8L133.2,54.8L127.5,45.3L129.6,45.3ZM137.8,22.6L132.5,38.7L134.8,38.7L128.6,51.4L132.0,51.4L124.7,62.9L136.1,62.9L136.1,65.0L139.5,65.0L139.5,62.9L150.8,62.9L143.5,51.4L146.9,51.4L140.8,38.7L143.0,38.7ZM155.7,33.5L152.1,45.5L153.6,45.5L149.3,54.9L151.7,54.9L146.6,63.4L154.5,63.4L154.5,65.0L156.9,65.0L156.9,63.4L164.9,63.4L159.8,54.9L162.2,54.9L157.8,45.5L159.4,45.5ZM166.7,34.5L161.4,46.1L163.6,46.1L157.4,55.2L160.8,55.2L153.4,63.5L165.0,63.5L165.0,65.0L168.5,65.0L168.5,63.5L180.1,63.5L172.6,55.2L176.1,55.2L169.8,46.1L172.1,46.1ZM186.6,17.8L181.1,35.7L183.4,35.7L177.0,49.9L180.5,49.9L172.9,62.6L184.8,62.6L184.8,65.0L188.3,65.0L188.3,62.6L200.3,62.6L192.6,49.9L196.1,49.9L189.7,35.7L192.0,35.7ZM201.8,35.8L197.2,46.9L199.1,46.9L193.8,55.6L196.7,55.6L190.3,63.5L200.3,63.5L200.3,65.0L203.3,65.0L203.3,63.5L213.2,63.5L206.8,55.6L209.8,55.6L204.4,46.9L206.3,46.9ZM214.5,12.9L209.8,32.7L211.8,32.7L206.3,48.3L209.3,48.3L202.7,62.4L213.0,62.4L213.0,65.0L216.1,65.0L216.1,62.4L226.3,62.4L219.7,48.3L222.8,48.3L217.2,32.7L219.2,32.7ZM229.1,36.9L223.2,47.6L225.7,47.6L218.8,56.0L222.6,56.0L214.4,63.6L227.2,63.6L227.2,65.0L231.0,65.0L231.0,63.6L243.7,63.6L235.5,56.0L239.3,56.0L232.4,47.6L234.9,47.6ZM250.8,21.7L245.2,38.1L247.6,38.1L240.9,51.1L244.6,51.1L236.7,62.8L249.0,62.8L249.0,65.0L252.7,65.0L252.7,62.8L265.0,62.8L257.0,51.1L260.7,51.1L254.1,38.1L256.5,38.1ZM271.2,31.4L266.2,44.2L268.3,44.2L262.5,54.3L265.7,54.3L258.7,63.3L269.6,63.3L269.6,65.0L272.8,65.0L272.8,63.3L283.7,63.3L276.7,54.3L279.9,54.3L274.1,44.2L276.2,44.2ZM289.3,30.8L283.5,43.8L286.0,43.8L279.2,54.1L282.9,54.1L274.9,63.3L287.4,63.3L287.4,65.0L291.1,65.0L291.1,63.3L303.7,63.3L295.6,54.1L299.4,54.1L292.6,43.8L295.0,43.8ZM309.7,16.1L305.2,34.7L307.1,34.7L301.8,49.3L304.8,49.3L298.5,62.6L308.2,62.6L308.2,65.0L311.1,65.0L311.1,62.6L320.8,62.6L314.6,49.3L317.5,49.3L312.2,34.7L314.1,34.7ZM324.8,30.4L321.3,43.5L322.8,43.5L318.6,53.9L320.9,53.9L316.0,63.3L323.6,63.3L323.6,65.0L325.9,65.0L325.9,63.3L333.6,63.3L328.7,53.9L330.9,53.9L326.8,43.5L328.3,43.5ZM335.1,18.8L329.7,36.4L332.0,36.4L325.6,50.2L329.1,50.2L321.6,62.7L333.3,62.7L333.3,65.0L336.8,65.0L336.8,62.7L348.6,62.7L341.0,50.2L344.5,50.2L338.2,36.4L340.5,36.4ZM0,65H340V70H0Z' fill='%23000'/></svg>"); mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='340' height='70' viewBox='0 0 340 70' preserveAspectRatio='none'><path d='M-6.0,18.9L-10.2,36.4L-8.4,36.4L-13.4,50.2L-10.7,50.2L-16.6,62.7L-7.4,62.7L-7.4,65.0L-4.6,65.0L-4.6,62.7L4.6,62.7L-1.3,50.2L1.4,50.2L-3.6,36.4L-1.8,36.4ZM5.4,32.5L-0.4,44.9L2.0,44.9L-4.8,54.6L-1.0,54.6L-9.1,63.4L3.5,63.4L3.5,65.0L7.2,65.0L7.2,63.4L19.8,63.4L11.7,54.6L15.5,54.6L8.7,44.9L11.1,44.9ZM25.7,36.2L19.8,47.1L22.3,47.1L15.4,55.8L19.2,55.8L11.1,63.6L23.8,63.6L23.8,65.0L27.6,65.0L27.6,63.6L40.3,63.6L32.1,55.8L35.9,55.8L29.0,47.1L31.5,47.1ZM47.8,24.0L42.6,39.6L44.8,39.6L38.8,51.9L42.1,51.9L35.0,63.0L46.1,63.0L46.1,65.0L49.4,65.0L49.4,63.0L60.6,63.0L53.4,51.9L56.7,51.9L50.7,39.6L52.9,39.6ZM61.3,12.7L57.7,32.5L59.2,32.5L55.1,48.2L57.4,48.2L52.5,62.4L60.1,62.4L60.1,65.0L62.4,65.0L62.4,62.4L70.0,62.4L65.1,48.2L67.4,48.2L63.3,32.5L64.8,32.5ZM74.4,39.6L70.8,49.3L72.3,49.3L68.0,56.9L70.4,56.9L65.3,63.7L73.2,63.7L73.2,65.0L75.6,65.0L75.6,63.7L83.4,63.7L78.4,56.9L80.7,56.9L76.5,49.3L78.0,49.3ZM85.0,30.7L81.6,43.7L83.1,43.7L79.1,54.0L81.3,54.0L76.6,63.3L83.9,63.3L83.9,65.0L86.1,65.0L86.1,63.3L93.4,63.3L88.7,54.0L90.9,54.0L87.0,43.7L88.4,43.7ZM97.3,29.8L93.1,43.2L94.9,43.2L89.9,53.7L92.6,53.7L86.7,63.2L95.9,63.2L95.9,65.0L98.7,65.0L98.7,63.2L107.9,63.2L102.0,53.7L104.7,53.7L99.7,43.2L101.5,43.2ZM111.2,35.9L107.6,47.0L109.1,47.0L104.8,55.7L107.2,55.7L102.1,63.5L110.0,63.5L110.0,65.0L112.4,65.0L112.4,63.5L120.4,63.5L115.3,55.7L117.6,55.7L113.3,47.0L114.9,47.0ZM124.6,33.3L119.7,45.3L121.8,45.3L116.0,54.8L119.2,54.8L112.3,63.4L123.0,63.4L123.0,65.0L126.2,65.0L126.2,63.4L136.9,63.4L130.0,54.8L133.2,54.8L127.5,45.3L129.6,45.3ZM137.8,22.6L132.5,38.7L134.8,38.7L128.6,51.4L132.0,51.4L124.7,62.9L136.1,62.9L136.1,65.0L139.5,65.0L139.5,62.9L150.8,62.9L143.5,51.4L146.9,51.4L140.8,38.7L143.0,38.7ZM155.7,33.5L152.1,45.5L153.6,45.5L149.3,54.9L151.7,54.9L146.6,63.4L154.5,63.4L154.5,65.0L156.9,65.0L156.9,63.4L164.9,63.4L159.8,54.9L162.2,54.9L157.8,45.5L159.4,45.5ZM166.7,34.5L161.4,46.1L163.6,46.1L157.4,55.2L160.8,55.2L153.4,63.5L165.0,63.5L165.0,65.0L168.5,65.0L168.5,63.5L180.1,63.5L172.6,55.2L176.1,55.2L169.8,46.1L172.1,46.1ZM186.6,17.8L181.1,35.7L183.4,35.7L177.0,49.9L180.5,49.9L172.9,62.6L184.8,62.6L184.8,65.0L188.3,65.0L188.3,62.6L200.3,62.6L192.6,49.9L196.1,49.9L189.7,35.7L192.0,35.7ZM201.8,35.8L197.2,46.9L199.1,46.9L193.8,55.6L196.7,55.6L190.3,63.5L200.3,63.5L200.3,65.0L203.3,65.0L203.3,63.5L213.2,63.5L206.8,55.6L209.8,55.6L204.4,46.9L206.3,46.9ZM214.5,12.9L209.8,32.7L211.8,32.7L206.3,48.3L209.3,48.3L202.7,62.4L213.0,62.4L213.0,65.0L216.1,65.0L216.1,62.4L226.3,62.4L219.7,48.3L222.8,48.3L217.2,32.7L219.2,32.7ZM229.1,36.9L223.2,47.6L225.7,47.6L218.8,56.0L222.6,56.0L214.4,63.6L227.2,63.6L227.2,65.0L231.0,65.0L231.0,63.6L243.7,63.6L235.5,56.0L239.3,56.0L232.4,47.6L234.9,47.6ZM250.8,21.7L245.2,38.1L247.6,38.1L240.9,51.1L244.6,51.1L236.7,62.8L249.0,62.8L249.0,65.0L252.7,65.0L252.7,62.8L265.0,62.8L257.0,51.1L260.7,51.1L254.1,38.1L256.5,38.1ZM271.2,31.4L266.2,44.2L268.3,44.2L262.5,54.3L265.7,54.3L258.7,63.3L269.6,63.3L269.6,65.0L272.8,65.0L272.8,63.3L283.7,63.3L276.7,54.3L279.9,54.3L274.1,44.2L276.2,44.2ZM289.3,30.8L283.5,43.8L286.0,43.8L279.2,54.1L282.9,54.1L274.9,63.3L287.4,63.3L287.4,65.0L291.1,65.0L291.1,63.3L303.7,63.3L295.6,54.1L299.4,54.1L292.6,43.8L295.0,43.8ZM309.7,16.1L305.2,34.7L307.1,34.7L301.8,49.3L304.8,49.3L298.5,62.6L308.2,62.6L308.2,65.0L311.1,65.0L311.1,62.6L320.8,62.6L314.6,49.3L317.5,49.3L312.2,34.7L314.1,34.7ZM324.8,30.4L321.3,43.5L322.8,43.5L318.6,53.9L320.9,53.9L316.0,63.3L323.6,63.3L323.6,65.0L325.9,65.0L325.9,63.3L333.6,63.3L328.7,53.9L330.9,53.9L326.8,43.5L328.3,43.5ZM335.1,18.8L329.7,36.4L332.0,36.4L325.6,50.2L329.1,50.2L321.6,62.7L333.3,62.7L333.3,65.0L336.8,65.0L336.8,62.7L348.6,62.7L341.0,50.2L344.5,50.2L338.2,36.4L340.5,36.4ZM0,65H340V70H0Z' fill='%23000'/></svg>");
    -webkit-mask-size: 340px 100%; mask-size: 340px 100%;
}
.vi-scene.dark .vi-ridge-far { background: #46283a; }
.vi-ridge-near {
    height: 92px; background: #0a0616;
    -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='100' viewBox='0 0 300 100' preserveAspectRatio='none'><path d='M-6.0,24.7L-10.5,51.1L-8.6,51.1L-13.9,71.8L-11.0,71.8L-17.3,90.5L-7.5,90.5L-7.5,94.0L-4.5,94.0L-4.5,90.5L5.3,90.5L-1.0,71.8L1.9,71.8L-3.4,51.1L-1.5,51.1ZM9.4,42.9L3.5,62.3L6.0,62.3L-1.0,77.7L2.9,77.7L-5.4,91.4L7.5,91.4L7.5,94.0L11.3,94.0L11.3,91.4L24.2,91.4L15.9,77.7L19.7,77.7L12.8,62.3L15.3,62.3ZM27.6,50.8L22.7,67.2L24.8,67.2L19.1,80.2L22.3,80.2L15.5,91.8L26.0,91.8L26.0,94.0L29.2,94.0L29.2,91.8L39.7,91.8L32.9,80.2L36.1,80.2L30.4,67.2L32.4,67.2ZM41.3,32.0L36.6,55.6L38.6,55.6L33.0,74.2L36.1,74.2L29.5,90.9L39.8,90.9L39.8,94.0L42.8,94.0L42.8,90.9L53.1,90.9L46.5,74.2L49.6,74.2L44.0,55.6L46.0,55.6ZM57.0,10.2L52.7,42.1L54.5,42.1L49.4,67.2L52.2,67.2L46.2,89.8L55.6,89.8L55.6,94.0L58.4,94.0L58.4,89.8L67.8,89.8L61.8,67.2L64.6,67.2L59.5,42.1L61.3,42.1ZM69.8,35.0L65.1,57.4L67.1,57.4L61.6,75.1L64.7,75.1L58.1,91.0L68.3,91.0L68.3,94.0L71.4,94.0L71.4,91.0L81.6,91.0L75.0,75.1L78.1,75.1L72.5,57.4L74.5,57.4ZM86.1,14.3L81.7,44.6L83.6,44.6L78.4,68.5L81.3,68.5L75.2,90.0L84.7,90.0L84.7,94.0L87.5,94.0L87.5,90.0L97.0,90.0L90.9,68.5L93.8,68.5L88.6,44.6L90.5,44.6ZM99.0,18.2L92.6,47.0L95.3,47.0L87.8,69.7L92.0,69.7L83.1,90.2L96.9,90.2L96.9,94.0L101.0,94.0L101.0,90.2L114.9,90.2L106.0,69.7L110.1,69.7L102.6,47.0L105.3,47.0ZM121.7,52.9L117.4,68.5L119.2,68.5L114.2,80.8L117.0,80.8L110.9,91.9L120.3,91.9L120.3,94.0L123.1,94.0L123.1,91.9L132.5,91.9L126.4,80.8L129.2,80.8L124.2,68.5L126.0,68.5ZM138.2,18.6L131.2,47.2L134.2,47.2L126.0,69.9L130.5,69.9L120.7,90.2L135.9,90.2L135.9,94.0L140.5,94.0L140.5,90.2L155.7,90.2L145.9,69.9L150.5,69.9L142.3,47.2L145.2,47.2ZM157.6,30.0L153.2,54.3L155.0,54.3L149.8,73.5L152.7,73.5L146.5,90.8L156.2,90.8L156.2,94.0L159.0,94.0L159.0,90.8L168.7,90.8L162.5,73.5L165.4,73.5L160.1,54.3L162.0,54.3ZM174.8,22.8L170.1,49.8L172.1,49.8L166.5,71.2L169.6,71.2L163.0,90.4L173.3,90.4L173.3,94.0L176.3,94.0L176.3,90.4L186.6,90.4L180.0,71.2L183.1,71.2L177.5,49.8L179.5,49.8ZM192.4,36.6L187.8,58.4L189.7,58.4L184.3,75.6L187.3,75.6L180.8,91.1L190.9,91.1L190.9,94.0L193.9,94.0L193.9,91.1L204.1,91.1L197.5,75.6L200.6,75.6L195.1,58.4L197.1,58.4ZM210.4,9.5L204.7,41.6L207.1,41.6L200.4,67.0L204.1,67.0L196.2,89.8L208.5,89.8L208.5,94.0L212.2,94.0L212.2,89.8L224.5,89.8L216.6,67.0L220.3,67.0L213.6,41.6L216.0,41.6ZM232.2,52.6L226.4,68.3L228.9,68.3L222.1,80.8L225.9,80.8L217.8,91.9L230.3,91.9L230.3,94.0L234.1,94.0L234.1,91.9L246.6,91.9L238.5,80.8L242.3,80.8L235.5,68.3L238.0,68.3ZM248.4,50.7L242.7,67.1L245.1,67.1L238.4,80.1L242.1,80.1L234.1,91.8L246.5,91.8L246.5,94.0L250.2,94.0L250.2,91.8L262.6,91.8L254.6,80.1L258.3,80.1L251.6,67.1L254.1,67.1ZM268.1,16.3L261.5,45.8L264.3,45.8L256.5,69.1L260.8,69.1L251.5,90.1L265.9,90.1L265.9,94.0L270.2,94.0L270.2,90.1L284.7,90.1L275.4,69.1L279.7,69.1L271.9,45.8L274.7,45.8ZM289.6,8.8L283.5,41.1L286.1,41.1L278.9,66.7L282.9,66.7L274.3,89.7L287.6,89.7L287.6,94.0L291.6,94.0L291.6,89.7L304.9,89.7L296.3,66.7L300.3,66.7L293.1,41.1L295.7,41.1ZM0,94H300V100H0Z' fill='%23000'/></svg>"); mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='100' viewBox='0 0 300 100' preserveAspectRatio='none'><path d='M-6.0,24.7L-10.5,51.1L-8.6,51.1L-13.9,71.8L-11.0,71.8L-17.3,90.5L-7.5,90.5L-7.5,94.0L-4.5,94.0L-4.5,90.5L5.3,90.5L-1.0,71.8L1.9,71.8L-3.4,51.1L-1.5,51.1ZM9.4,42.9L3.5,62.3L6.0,62.3L-1.0,77.7L2.9,77.7L-5.4,91.4L7.5,91.4L7.5,94.0L11.3,94.0L11.3,91.4L24.2,91.4L15.9,77.7L19.7,77.7L12.8,62.3L15.3,62.3ZM27.6,50.8L22.7,67.2L24.8,67.2L19.1,80.2L22.3,80.2L15.5,91.8L26.0,91.8L26.0,94.0L29.2,94.0L29.2,91.8L39.7,91.8L32.9,80.2L36.1,80.2L30.4,67.2L32.4,67.2ZM41.3,32.0L36.6,55.6L38.6,55.6L33.0,74.2L36.1,74.2L29.5,90.9L39.8,90.9L39.8,94.0L42.8,94.0L42.8,90.9L53.1,90.9L46.5,74.2L49.6,74.2L44.0,55.6L46.0,55.6ZM57.0,10.2L52.7,42.1L54.5,42.1L49.4,67.2L52.2,67.2L46.2,89.8L55.6,89.8L55.6,94.0L58.4,94.0L58.4,89.8L67.8,89.8L61.8,67.2L64.6,67.2L59.5,42.1L61.3,42.1ZM69.8,35.0L65.1,57.4L67.1,57.4L61.6,75.1L64.7,75.1L58.1,91.0L68.3,91.0L68.3,94.0L71.4,94.0L71.4,91.0L81.6,91.0L75.0,75.1L78.1,75.1L72.5,57.4L74.5,57.4ZM86.1,14.3L81.7,44.6L83.6,44.6L78.4,68.5L81.3,68.5L75.2,90.0L84.7,90.0L84.7,94.0L87.5,94.0L87.5,90.0L97.0,90.0L90.9,68.5L93.8,68.5L88.6,44.6L90.5,44.6ZM99.0,18.2L92.6,47.0L95.3,47.0L87.8,69.7L92.0,69.7L83.1,90.2L96.9,90.2L96.9,94.0L101.0,94.0L101.0,90.2L114.9,90.2L106.0,69.7L110.1,69.7L102.6,47.0L105.3,47.0ZM121.7,52.9L117.4,68.5L119.2,68.5L114.2,80.8L117.0,80.8L110.9,91.9L120.3,91.9L120.3,94.0L123.1,94.0L123.1,91.9L132.5,91.9L126.4,80.8L129.2,80.8L124.2,68.5L126.0,68.5ZM138.2,18.6L131.2,47.2L134.2,47.2L126.0,69.9L130.5,69.9L120.7,90.2L135.9,90.2L135.9,94.0L140.5,94.0L140.5,90.2L155.7,90.2L145.9,69.9L150.5,69.9L142.3,47.2L145.2,47.2ZM157.6,30.0L153.2,54.3L155.0,54.3L149.8,73.5L152.7,73.5L146.5,90.8L156.2,90.8L156.2,94.0L159.0,94.0L159.0,90.8L168.7,90.8L162.5,73.5L165.4,73.5L160.1,54.3L162.0,54.3ZM174.8,22.8L170.1,49.8L172.1,49.8L166.5,71.2L169.6,71.2L163.0,90.4L173.3,90.4L173.3,94.0L176.3,94.0L176.3,90.4L186.6,90.4L180.0,71.2L183.1,71.2L177.5,49.8L179.5,49.8ZM192.4,36.6L187.8,58.4L189.7,58.4L184.3,75.6L187.3,75.6L180.8,91.1L190.9,91.1L190.9,94.0L193.9,94.0L193.9,91.1L204.1,91.1L197.5,75.6L200.6,75.6L195.1,58.4L197.1,58.4ZM210.4,9.5L204.7,41.6L207.1,41.6L200.4,67.0L204.1,67.0L196.2,89.8L208.5,89.8L208.5,94.0L212.2,94.0L212.2,89.8L224.5,89.8L216.6,67.0L220.3,67.0L213.6,41.6L216.0,41.6ZM232.2,52.6L226.4,68.3L228.9,68.3L222.1,80.8L225.9,80.8L217.8,91.9L230.3,91.9L230.3,94.0L234.1,94.0L234.1,91.9L246.6,91.9L238.5,80.8L242.3,80.8L235.5,68.3L238.0,68.3ZM248.4,50.7L242.7,67.1L245.1,67.1L238.4,80.1L242.1,80.1L234.1,91.8L246.5,91.8L246.5,94.0L250.2,94.0L250.2,91.8L262.6,91.8L254.6,80.1L258.3,80.1L251.6,67.1L254.1,67.1ZM268.1,16.3L261.5,45.8L264.3,45.8L256.5,69.1L260.8,69.1L251.5,90.1L265.9,90.1L265.9,94.0L270.2,94.0L270.2,90.1L284.7,90.1L275.4,69.1L279.7,69.1L271.9,45.8L274.7,45.8ZM289.6,8.8L283.5,41.1L286.1,41.1L278.9,66.7L282.9,66.7L274.3,89.7L287.6,89.7L287.6,94.0L291.6,94.0L291.6,89.7L304.9,89.7L296.3,66.7L300.3,66.7L293.1,41.1L295.7,41.1ZM0,94H300V100H0Z' fill='%23000'/></svg>");
    -webkit-mask-size: 300px 100%; mask-size: 300px 100%;
}
.vi-scene.dark .vi-ridge-near { background: #0d0510; }

/* the ground the figure is standing on */
/* THE GROUND, and it must not be the brightest thing in the picture. Both grounds used to
   finish near-white at the bottom edge — a lit band running the full width, directly under a
   56px figure, so the eye landed on the snow and not on whoever was standing in it. The
   platform's own rule is that nothing in the chrome may out-luminance the subject; here the
   subject is the winner. Both ramps are pulled down about 20%, and the top edge is longer and
   softer so the snow arrives out of the dark rather than starting on a line. */
.vi-snowline {
    position: absolute; left: 0; right: 0; bottom: 0; height: 44px;
    background: linear-gradient(180deg,
        rgba(96, 82, 132, 0) 0%, rgba(118, 104, 156, .55) 40%, #9083b0 74%, #a99cc4 100%);
}
.vi-scene.dark .vi-snowline {
    background: linear-gradient(180deg,
        rgba(78, 48, 48, 0) 0%, rgba(104, 70, 68, .55) 40%, #7d5f5c 74%, #937470 100%);
}

/* The winner, standing on the snow. The glow under the feet is the lantern's, so it goes
   warm-amber for Violet and low-red for the nisse — light is the thing this game is about. */
/* The winner, standing IN the snow rather than on top of it: the figure is set 4px into the
   ground plane and carries a contact shadow (see .vi-shadow), because the one thing that made
   it read as a sticker was that nothing beneath it acknowledged its weight. Larger, too — at
   56px on a 470px panel the piece that the whole screen is about was the smallest object on
   it. */
.vi-hero {
    position: absolute; left: 50%; bottom: 22px; width: 70px; height: 78px;
    transform: translateX(-50%);
    filter: drop-shadow(0 3px 5px rgba(6, 2, 12, .55));
    animation: viHeroIn .5s cubic-bezier(.2, 1.3, .4, 1) both .12s;
}
/* The dark ellipse where the figure meets the ground. Under the glow, over the snow. */
.vi-shadow {
    position: absolute; left: 50%; bottom: 20px; transform: translateX(-50%);
    width: 62px; height: 13px; border-radius: 50%; pointer-events: none;
    background: radial-gradient(ellipse at center, rgba(18, 8, 26, .62), rgba(18, 8, 26, 0) 72%);
    animation: viGlowIn .6s ease both .2s;
}
.vi-scene.dark .vi-shadow {
    background: radial-gradient(ellipse at center, rgba(26, 8, 10, .68), rgba(26, 8, 10, 0) 72%);
}
.vi-glow {
    position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%);
    width: 132px; height: 42px; border-radius: 50%; pointer-events: none;
    background: radial-gradient(ellipse at center, rgba(255,190,96,.62), rgba(255,190,96,0) 70%);
    animation: viGlowIn .6s ease both .2s;
}
.vi-scene.dark .vi-glow {
    background: radial-gradient(ellipse at center, rgba(226,78,54,.55), rgba(226,78,54,0) 70%);
}
@keyframes viGlowIn { from { opacity: 0; } }
.vi-hero svg { position: relative; width: 100%; height: 100%; display: block; overflow: visible; }
@keyframes viHeroIn { from { opacity: 0; transform: translate(-50%, 14px) scale(.82); } }

.panel.vi-end h2 { margin-bottom: 6px; }
.vi-why { font-size: 15px; line-height: 1.55; color: var(--vi-ink); opacity: .92; }

/* the three numbers a player actually asks about, as figures rather than a grey run-on line */
.vi-stats { display: flex; gap: 8px; justify-content: center; margin-top: 14px; }
.vi-stats > div {
    flex: 1 1 0; min-width: 0; padding: 9px 4px 8px;
    border-radius: var(--dk-radius);
    background: linear-gradient(180deg, var(--vi-night-hi), var(--vi-night-lo));
    box-shadow: 0 0 0 1px var(--vi-rail-soft), inset 0 1px 0 rgba(255, 244, 214, .07);
}
.vi-stats b {
    display: block; font-family: var(--dk-font-display);
    font-size: 23px; font-weight: 700; color: var(--vi-rail); line-height: 1.05;
}
.vi-stats b i { font-style: normal; font-size: 14px; opacity: .55; }
.vi-stats span {
    display: block; margin-top: 3px; font-size: 11px; letter-spacing: .04em;
    color: var(--vi-ink); opacity: .58;
}

@media (max-height: 620px) {
    .vi-scene { height: 112px; }
    .vi-ridge { bottom: 20px; }
    .vi-ridge-far { bottom: 26px; height: 46px; }
    .vi-ridge-near { height: 66px; }
    .vi-snowline { height: 30px; }
    .vi-hero { width: 44px; height: 49px; bottom: 15px; }
    .vi-glow { width: 104px; height: 32px; bottom: 7px; }
}
@media (prefers-reduced-motion: reduce) {
    #over-overlay, .vi-fall, .vi-hero { animation: none; }
}

/* the trap tray gets the same night ground rather than a flat plate */
.trap-tray-panel {
    background:
        radial-gradient(120% 90% at 50% -20%, rgba(208, 100, 216, .16), rgba(0, 0, 0, 0) 62%),
        linear-gradient(180deg, rgba(30, 16, 52, .98), rgba(14, 8, 26, .98));
}
.trap-tray-panel::before {
    content: ''; position: absolute; left: 14%; right: 14%; top: 0; height: 1px;
    background: linear-gradient(90deg, transparent, var(--dk-accent), transparent);
    opacity: .7;
}

/* Two identical bright buttons make the player read both to find out which is which. Play
   again is what almost everyone wants; the menu is the way out, and it says so quietly. */
.primary-btn.vi-ghost {
    background: transparent;
    color: var(--vi-ink);
    box-shadow: inset 0 0 0 1px var(--vi-rail-soft);
    opacity: .85;
}
.primary-btn.vi-ghost:hover { background: rgba(255, 244, 214, .07); opacity: 1; }

/* The two ways out of the end panel are alternatives, not steps, so they sit side by side —
   which is also the only way both of them fit under the scene at 360px of viewport height. */
.vi-endrow { display: flex; gap: 10px; margin-top: 16px; }
.vi-endrow .primary-btn { flex: 1 1 0; width: auto; min-width: 0; margin-top: 0; }

@media (max-height: 620px) {
    .panel.vi-end { padding-bottom: 16px; }
    .panel.vi-end h2 { font-size: 20px; margin-bottom: 4px; }
    .vi-why { font-size: 13.5px; line-height: 1.4; }
    .vi-scene { margin-bottom: 12px; }
    .vi-stats { margin-top: 10px; }
    .vi-stats > div { padding: 6px 4px 5px; }
    .vi-stats b { font-size: 19px; }
    .vi-endrow { margin-top: 12px; }
    .vi-endrow .primary-btn { min-height: 42px; font-size: 15px; }
}

/* The sound button says which state it is IN, not which state it would move to. Both halves
   live in one SVG and one of them is always hidden, so the button never changes width. */
#music-btn .snd-off { display: none; }
#music-btn.is-off .snd-on { display: none; }
#music-btn.is-off .snd-off { display: inline; }
#music-btn.is-off { opacity: .55; }

/* Compact mode has no board, so the plates sit where it would be and their height is taken
   straight off the card size. Same information, tighter setting. */
.plate.dense { padding: 6px 10px 7px; font-size: 11px; line-height: 1.3; }
.plate.dense b { font-size: 13px; }
.plate.dense .who { font-size: 9px; margin-bottom: 1px; letter-spacing: .12em; }
.plate.dense .gauge { height: 5px; margin-top: 4px; }

/* The unit on a plate figure — quiet, because it is read once and then known, but present,
   because "Fear 13" and "Fear 13 behind" say opposite things about how the game is going. */
.plate .unit { margin-left: 3px; font-size: 10.5px; opacity: .58; }
.plate.dense .unit { font-size: 9.5px; }

/* ── the top five, after a played 3 ──────────────────────────────── */

/* The scrim covers the BOARD only. The hand below it stays lit, because the choice being
   made is against the hand and a modal that hides it would be answering the wrong question. */
.peek-scrim {
    position: fixed; left: 0; right: 0; z-index: 1130;
    background: rgba(6, 3, 14, .74);
    backdrop-filter: blur(1.5px);
    animation: peekFade .22s ease both;
}
@keyframes peekFade { from { opacity: 0; } }

.peek-tray-panel {
    position: fixed; left: 50%; transform: translateX(-50%);
    z-index: 1140; max-width: 96vw;
    padding: 14px 22px 18px; border-radius: var(--dk-radius-lg);
    background:
        radial-gradient(120% 90% at 50% -20%, rgba(201, 162, 74, .18), rgba(0, 0, 0, 0) 62%),
        linear-gradient(180deg, var(--vi-night-hi), var(--vi-night-lo));
    box-shadow: 0 0 0 1px var(--vi-rail), 0 20px 56px rgba(4, 2, 10, .78);
    animation: peekIn .3s cubic-bezier(.2, 1.2, .4, 1) both;
}
@keyframes peekIn { from { opacity: 0; transform: translate(-50%, -10px) scale(.97); } }
/* The grip. A bar rather than a corner: it is the whole top edge of the panel, so it is
   findable without hunting and big enough for a thumb on a tablet. */
.tray-grip {
    display: block; width: 62px; height: 18px; margin: -4px auto 4px;
    padding: 0; border: 0; background: none; cursor: grab;
    touch-action: none;                      /* the drag is ours, not the page's */
}
.tray-grip::before {
    content: ''; display: block; width: 100%; height: 4px; margin-top: 7px;
    border-radius: 2px; background: var(--vi-rail); opacity: .5;
    transition: opacity .14s ease, transform .14s ease;
}
.tray-grip:hover::before { opacity: .9; }
.tray-grip.dragging { cursor: grabbing; }
.tray-grip.dragging::before { opacity: 1; transform: scaleY(1.4); }

.peek-head {
    font-family: var(--dk-font-display); font-size: 22px; font-weight: 600;
    color: var(--vi-rail); text-align: center;
}
.peek-sub {
    margin: 3px 0 12px; text-align: center;
    font-family: var(--dk-font-ui); font-size: 13px; line-height: 1.45;
    color: var(--vi-ink); opacity: .72;
}
.peek-row { display: flex; gap: 12px; justify-content: center; align-items: flex-end; }
.peek-slot { display: flex; flex-direction: column; align-items: center; gap: 5px; }
/* The box holds the space a `position: absolute` card cannot hold for itself. */
.peek-box { position: relative; cursor: pointer; border-radius: 6px; }
.peek-box > .card { position: absolute; left: 0; top: 0; }
.peek-box { transition: transform .14s ease, box-shadow .14s ease; }
.peek-box:hover { transform: translateY(-7px); box-shadow: 0 0 0 2px var(--vi-rail), 0 12px 28px rgba(4, 2, 10, .65); }

/* The deck order is the point — leftmost is the top, and it is also the card the next draw
   turns over if you leave it — so the position is labelled, not merely implied. */
.peek-flag {
    font-family: var(--dk-font-ui); font-size: 10px; letter-spacing: .12em;
    text-transform: uppercase; color: var(--vi-ink); opacity: .40;
}
.peek-flag.is-top { opacity: 1; color: var(--vi-rail); }

@media (max-width: 620px), (max-height: 560px) {
    .peek-head { font-size: 17px; }
    .peek-sub { font-size: 11.5px; margin-bottom: 8px; }
    .peek-row { gap: 7px; }
    .peek-tray-panel { padding: 10px 12px 12px; }
}
@media (prefers-reduced-motion: reduce) {
    .peek-tray-panel, .peek-scrim { animation: none; }
}

/* The discard pile, browsable, after a played 4. Same overlay as the peek — board scrimmed,
   hand left lit — but the pile grows all game, so it wraps and scrolls inside its own band. */
.swap-tray-panel { overflow-y: auto; -webkit-overflow-scrolling: touch; }
.swap-row { flex-wrap: wrap; gap: 9px; max-width: 78vw; }
.swap-box.picked {
    box-shadow: 0 0 0 3px var(--vi-rail), 0 0 22px rgba(201, 162, 74, .55);
    transform: translateY(-7px);
}
.swap-foot { display: flex; justify-content: center; margin-top: 14px; }
/* the hand card you are about to give, once a take has been chosen */
.card.give { cursor: pointer; box-shadow: 0 0 0 2px var(--dk-accent), 0 6px 18px rgba(0,0,0,.6); }
