/* Cache Pal v4 — art direction A, "cozy storybook pixel", worn in v4.1 as the FULL Franuka UI kit
   (ART_PASS_PLAN, OWNER OVERRIDE): peach 9-slice panels, octagon slots, ribbon titles, framed
   orbs, bookmark tabs, Franuka bars, buttons and icons, and the Franuka pixel faces for titles and
   labels. Loaded after app.css. Everything here is namespaced (--v4-*, .v4-*) so the old sheet
   cannot fight it; the few element rules below are deliberate overrides of app.css's dark shell.
   The kit's art is adopted from the drive by tools/adopt-art.mjs (tools/art/intake/ui.json) into
   img/v4/ui/ and img/icons/chrome/fr-*.png, and drawn at whole scales: chrome at 2x (one art
   pixel = 2 CSS px), buttons at 3x (so a 15px button is a 45px target), the verb orbs at 1x on
   a phone (in 64px tap targets, five fit 360px) and 2x on desktop (v4.2: no 1.5x art). */

/* ---- the type: two faces, five pixel sizes, whole pixels (v4.1.2) ----
   Cache Pal Mini (Kenney Mini with l/i/k fixed, tools/art/make-fonts.py) is an 8-px pixel face:
   it is crisp only at whole multiples of its design em, so it is set at 16/24/32/40/48 and nowhere
   in between. Young Serif carries the sentences (speech 17/24, help and prose 16/24). Neither face
   has a bold or italic cut, and nothing may fake one (font-synthesis: none). Every line-height is
   a whole px, no letter-spacing, and pixel text starts on a whole pixel (no -50% translates).
   deploy/checks/v4/text-legibility.mjs holds every visible word to this. */
@font-face { font-family: 'Cache Pal Mini'; src: url('../fonts/CachePalMini.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: block; }
@font-face { font-family: 'Young Serif'; src: url('../fonts/YoungSerif-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }

:root {
    /* The warm-wood table's outline ink (tools/art/intake/warm-wood.json: #3d2936 -> #433023). */
    --v4-ink: #433023;
    --v4-ink-soft: #6d4a33;
    --v4-cream: #FFF3DD;
    /* Franuka's peach parchment (BGbox_01A's fill): the paper every panel is made of. */
    --v4-parchment: #ffc2a1;
    --v4-parchment-dark: #daa684;
    --v4-tan: #daa684;
    --v4-peach: #ffc2a1;
    --v4-peach-deep: #ffae70;
    --v4-wood: #ca8455;
    --v4-wood-light: #daa684;
    --v4-wood-dark: #884e28;
    --v4-yellow: #ffee83;
    --v4-gold: #f0b541;
    --v4-green: #63ab3f;
    --v4-green-dark: #3b7d4f;
    /* Green for WORDS: AA on the peach paper (5.07:1) and on cream (7.18:1). --v4-green-dark is
       3.19:1 on peach, so it is for art and fills only (QA 4.2 desktop #20, phone #13). */
    --v4-green-ink: #255c35;
    --v4-red: #e64539;
    --v4-red-deep: #ad2f45;
    --v4-bang: #e64539;
    --v4-sky: #8ED2FF;
    --v4-sky-deep: #4FB6E8;
    --v4-slate: #1b1e2b;
    /* The selected-label red: AA (>= 4.5:1) on the wood dock and on peach. */
    --v4-on: #7a1626;

    /* The two faces. No emoji face in either chain (v4.2 lane E): an icon is pixel art drawn by
       GameIcon, never a glyph, and a character the faces lack is caught by the type gate. */
    --t-mini: 'Cache Pal Mini', monospace;
    --t-voice: 'Young Serif', Georgia, serif;
    /* Cache Pal Mini's only sizes: 2x, 3x, 4x, 5x and 6x its 8-px em. */
    --t-px1: 16px;  /* plates, labels, counts, chips, the HUD name */
    --t-px2: 24px;  /* HUD coins, dock labels, buttons, title plates */
    --t-px3: 32px;  /* ribbon, page and sheet titles */
    --t-px4: 40px;  /* the hero button */
    --t-px5: 48px;  /* splash numbers */
    /* Young Serif's sizes, each on a whole-px line. */
    --t-body: 16px;    /* help, book prose, explanations (line 24px) */
    --t-speech: 17px;  /* what a resident or a Pal says (line 24px) */
    --t-lead: 24px;    /* a big sentence (line 32px) */
    /* Old names, kept so every lane's rules read the same: titles, labels and numbers are Mini. */
    --v4-font-title: var(--t-mini);
    --v4-font-head: var(--t-mini);
    --v4-font-num: var(--t-mini);
    --v4-font-body: var(--t-voice);

    /* The kit's pieces (img/v4/ui, 1x art; a slice is in art pixels). */
    --fr-box: url('../img/v4/ui/box.png');
    --fr-box-gold: url('../img/v4/ui/box-gold.png');
    --fr-paper: url('../img/v4/ui/paper.png');
    --fr-wood: url('../img/v4/ui/wood.png');
    --fr-chip: url('../img/v4/ui/chip.png');
    --fr-sign: url('../img/v4/ui/sign.png');
    --fr-ribbon: url('../img/v4/ui/ribbon.png');
    --fr-ribbon-peach: url('../img/v4/ui/ribbon-peach.png');
    --fr-tick: url('../img/v4/ui/tick.png');
    --fr-divider: url('../img/v4/ui/divider.png');
    --fr-hand-down: url('../img/v4/ui/hand-down.png');

    --v4-hud-h: 70px;
    /* The dock's whole height on a phone, its wooden rim above and below included. */
    --v4-dock-h: 110px;
    --v4-focus: 0 0 0 3px var(--v4-yellow), 0 0 0 6px var(--v4-ink);
    --v4-outline-txt: 2px 0 0 var(--v4-ink), -2px 0 0 var(--v4-ink), 0 2px 0 var(--v4-ink), 0 -2px 0 var(--v4-ink),
        2px 2px 0 var(--v4-ink), -2px 2px 0 var(--v4-ink), 2px -2px 0 var(--v4-ink), -2px -2px 0 var(--v4-ink), 0 4px 0 var(--v4-ink);
}

/* The world behind every screen: a warm wood-plank table. The shell is exactly the window:
   no default body margin to push it past the edge. */
body {
    margin: 0;
    background: #8A552A url('../img/v4/tiles/wood.png') repeat;
    background-size: 488px 240px;
    image-rendering: pixelated;
}

.v4-app {
    position: relative;
    box-sizing: border-box;
    min-height: 100dvh;
    font-family: var(--t-voice);
    font-size: var(--t-body);
    line-height: 24px;
    font-weight: 400;
    font-synthesis: none;
    font-kerning: none;
    font-variant-ligatures: none;
    letter-spacing: 0;
    color: var(--v4-ink);
    image-rendering: auto;
}
/* Form controls do not inherit type by default; here they do, so a field reads like the page.
   Nor do they wear the browser's own face (a button's grey ButtonFace, a field's white): the kit's
   9-slice frames have open corners, and that face showed through them as a white square at each
   corner of every framed button, tile and field (QA 2026-10-06 #9; deploy/checks/v4/frames.mjs). */
:where(.v4-app) :where(button, input, select, textarea) { font: inherit; letter-spacing: 0; background-color: transparent; }
:where(.v4-app) :where(b, strong, h1, h2, h3, h4, h5, h6, th) { font-weight: 400; }
:where(.v4-app) :where(i, em) { font-style: normal; }

/* Franuka's glove: the pointer over everything that answers a tap (Hand01_Up, pre-scaled 2x: a
   CSS cursor cannot be drawn pixelated). */
.v4-app :is(a[href], button, [role="tab"], [role="button"], summary, select, label[for]):not(:disabled) { cursor: url('../img/v4/ui/cursor.png') 6 0, pointer; }

.v4-app canvas { image-rendering: pixelated; }
.v4-e { font-style: normal; line-height: 1; display: inline-block; }
/* An icon (GameIcon): a 16px pixel-art icon at a whole scale. */
.v4-ico { display: inline-block; flex: none; image-rendering: pixelated; vertical-align: middle; }
/* An icon inside a line of words (RichLine): it sits in the line like a letter would. */
.v4-ico.rl-i { vertical-align: -3px; margin: 0 1px; }
/* A chrome icon that names its thing on hover (the title rides this wrapper). */
.v4-ico-tip { display: inline-flex; flex: none; }
.v4-ico-e { width: var(--s); height: var(--s); font-size: calc(var(--s) * .72); display: inline-grid; place-items: center; }
.v4-num { font-family: var(--v4-font-num); letter-spacing: 0; }
.v4-head { font-family: var(--v4-font-head); font-weight: 400; }
.v4-title { font-family: var(--v4-font-title); font-weight: 400; letter-spacing: 0; }
.v4-outline-txt { color: var(--v4-cream); text-shadow: var(--v4-outline-txt); }
.v4-sr { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

.v4-app :is(a, button, [role="tab"], [tabindex]:not([tabindex="-1"])):focus-visible { outline: none; box-shadow: var(--v4-focus); border-radius: 6px; }

/* ---- the Franuka 9-slice frames, at 2x (border-image width = 2 x the art slice) ----
   The old class names stay, so every screen that framed itself with them wears the kit:
   panel and parchment → the peach box (BGbox_01A), chip → the slim HUD plate (BannerSmall_03A),
   bubble → the box (with the continue tick), plate → the wooden signboard (BannerMedium_04A),
   wood → the plank frame (BGbox_05A), tab → the gold selected box (BGbox_02A). New: box, gold,
   paper (the torn note, BGbox_04A). A frame's border may be narrower than its art: the art then
   overlaps the padding's edge, never the layout. The box's rim is 7, 6, 5 and 6 art pixels deep
   (top, right, bottom, left): its 14 px borders sit 16 / 14 / 12 so a word keeps a pixel of air
   off the rim on every side (deploy/checks/v4/frames.mjs F3). */
.v4-frame-panel { border: 18px solid transparent; border-image: var(--fr-box) 9 fill / 18px stretch; image-rendering: pixelated; }
.v4-frame-box { border: 14px solid transparent; border-width: 16px 14px 12px; border-image: var(--fr-box) 9 fill / 18px stretch; image-rendering: pixelated; }
.v4-frame-gold { border: 14px solid transparent; border-image: var(--fr-box-gold) 9 fill / 18px stretch; image-rendering: pixelated; }
.v4-frame-parchment { border: 14px solid transparent; border-width: 16px 14px 12px; border-image: var(--fr-box) 9 fill / 18px stretch; image-rendering: pixelated; }
.v4-frame-paper { border: 16px solid transparent; border-image: var(--fr-paper) 10 fill / 20px stretch; image-rendering: pixelated; }
.v4-frame-chip { border: 9px solid transparent; border-image: var(--fr-chip) 6 fill / 12px stretch; image-rendering: pixelated; }
.v4-frame-bubble { border: 14px solid transparent; border-width: 16px 14px 12px; border-image: var(--fr-box) 9 fill / 18px stretch; image-rendering: pixelated; }
/* The signboard is lettered in ink: a cream title on its tan board would fall under AA. */
.v4-frame-plate { border-style: solid; border-color: transparent; border-width: 10px 12px 14px; border-image: var(--fr-sign) 7 7 9 7 fill / 14px 14px 18px stretch;
    image-rendering: pixelated; color: var(--v4-ink) !important; text-shadow: none !important; }
.v4-frame-wood { border: 14px solid transparent; border-image: var(--fr-wood) 10 fill / 20px stretch; image-rendering: pixelated; }
.v4-frame-tab { border: 12px solid transparent; border-image: var(--fr-box-gold) 9 fill / 18px stretch; image-rendering: pixelated; }
.v4-frame-panel > *, .v4-frame-parchment > *, .v4-frame-chip > *, .v4-frame-tab > *, .v4-frame-box > *, .v4-frame-gold > * { image-rendering: auto; }

/* ---- the kit's other Franuka pieces ---- */
/* A title ribbon (BannerMedium_02C): FantasyRPG Title capitals in ink on gold, its tails hanging. */
.v4-ribbon {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px; box-sizing: border-box;
    min-height: 44px; padding: 0 4px 2px; margin: 0;
    border-style: solid; border-color: transparent; border-width: 12px 26px 16px;
    border-image: var(--fr-ribbon) 7 14 9 14 fill / 14px 28px 18px stretch; image-rendering: pixelated;
    font-family: var(--v4-font-title); font-weight: 400; font-size: var(--t-px3); line-height: var(--t-px3); letter-spacing: 0;
    color: var(--v4-ink); text-shadow: none; white-space: nowrap;
}
.v4-ribbon.peach { border-image-source: var(--fr-ribbon-peach); }
/* A gold filigree rule (Divider_07). */
.v4-divider { display: block; height: 10px; margin: 10px 0; border: 0; background: var(--fr-divider) center / 96px 10px repeat-x; image-rendering: pixelated; }
/* A Franuka bar (Slider01): the groove at 2x, and its fill the adopted strip (img/v4/ui/bar-*.png,
   40×2: a light top row between two shade end pixels, over a shade row) at 2x and any length, in
   whole 2-px steps: the strip's left end at the left, its right end at the right, and between
   them its own two colours (the light row as ::after, the shade row as the fill's colour).
   Stretching the strip would draw its end pixels at a fractional scale (pixel-scale.mjs P3). */
.v4-bar { position: relative; display: block; box-sizing: border-box; height: 18px; min-width: 40px;
    border: 8px solid transparent; border-image: url('../img/v4/ui/bar-box.png') 4 fill / 8px stretch; image-rendering: pixelated; }
.v4-bar > i { --hi: #c8d45d; --lo: #63ab3f; position: absolute; left: -2px; top: -2px; height: 4px;
    width: round(down, calc((100% + 4px) * var(--v, 0)), 2px); max-width: calc(100% + 4px);
    background-color: var(--lo); background-image: url('../img/v4/ui/bar-green.png'), url('../img/v4/ui/bar-green.png');
    background-position: left top, right top; background-size: 80px 4px, 80px 4px; background-repeat: no-repeat; image-rendering: pixelated; }
.v4-bar > i::after { content: ''; position: absolute; left: 2px; right: 2px; top: 0; height: 2px; background: var(--hi); }
/* The fill's strip, by class: the needs (orange hunger, pink happiness, yellow energy, blue
   hygiene, purple smarts), plus red and green; --hi is each strip's light row. */
.v4-bar.orange > i { background-image: url('../img/v4/ui/bar-orange.png'), url('../img/v4/ui/bar-orange.png'); --hi: #ff8933; --lo: #cf752b; }
.v4-bar.pink > i { background-image: url('../img/v4/ui/bar-pink.png'), url('../img/v4/ui/bar-pink.png'); --hi: #ff5277; --lo: #cc2f7b; }
.v4-bar.yellow > i { background-image: url('../img/v4/ui/bar-yellow.png'), url('../img/v4/ui/bar-yellow.png'); --hi: #ffee83; --lo: #f0b541; }
.v4-bar.blue > i { background-image: url('../img/v4/ui/bar-blue.png'), url('../img/v4/ui/bar-blue.png'); --hi: #92e8c0; --lo: #4fa4b8; }
.v4-bar.purple > i { background-image: url('../img/v4/ui/bar-purple.png'), url('../img/v4/ui/bar-purple.png'); --hi: #9c2a70; --lo: #692464; }
.v4-bar.red > i { background-image: url('../img/v4/ui/bar-red.png'), url('../img/v4/ui/bar-red.png'); --hi: #e64539; --lo: #ad2f45; }
/* The continue tick under a speech box (Tick01_Down, 3x). */
.v4-tick { display: block; width: 15px; height: 15px; background: var(--fr-tick) 0 0 / 15px 15px no-repeat; image-rendering: pixelated; }
/* A "something waits here" mark on a scene: Franuka's alert pop (P0095 Emote_alert, its rest
   frame) at 2x, a peach thought bubble with the red "!". The "!" text stays for readers that
   see no image, drawn transparent; each page keeps its own position for the mark. */
.v4-app .v4-alert.v4-alert {
    width: 32px; height: 28px; min-width: 0; min-height: 0; padding: 0; box-sizing: border-box;
    color: transparent; font-size: 0; text-shadow: none;
    background: url('../img/v4/ui/emote-alert.png') -128px 0 / 160px 28px no-repeat;
    border: 0; border-image: none; border-radius: 0; box-shadow: none; image-rendering: pixelated;
    filter: drop-shadow(0 2px 0 rgba(67, 48, 35, .55));
}
.v4-app .v4-alert.v4-alert::before, .v4-app .v4-alert.v4-alert::after { display: none; }
/* The pointing glove of a hint (Hand01_Down, 3x), bobbing over what to tap. */
.v4-hand { display: block; width: 24px; height: 33px; background: var(--fr-hand-down) 0 0 / 24px 33px no-repeat; image-rendering: pixelated;
    filter: drop-shadow(0 3px 0 rgba(67, 48, 35, .45)); }
@media (prefers-reduced-motion: no-preference) {
    .v4-tick { animation: v4-tick-bob .8s steps(2) infinite; }
    .v4-hand { animation: v4-hand-bob .9s steps(3) infinite; }
}
@keyframes v4-tick-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(2px); } }
@keyframes v4-hand-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(6px); } }

/* ---- buttons (Button_02A red, Button_03A gold, Button_01A wood), at 3x: a 45px target ---- */
.v4-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px; box-sizing: border-box;
    min-height: 45px; min-width: 96px; padding: 0 4px; margin: 0;
    border-style: solid; border-color: transparent; border-width: 15px 21px 18px;
    border-image: url('../img/v4/ui/btn-gold.png') 5 7 6 7 fill / 15px 21px 18px stretch; image-rendering: pixelated;
    background: none; color: var(--v4-ink); text-decoration: none; text-shadow: none;
    font-family: var(--v4-font-head); font-weight: 400; font-size: var(--t-px2); line-height: var(--t-px2); white-space: nowrap;
    -webkit-tap-highlight-color: transparent;
}
.v4-btn:active:not(:disabled) { border-image-source: url('../img/v4/ui/btn-gold-down.png'); }
.v4-btn.red { border-image-source: url('../img/v4/ui/btn-red.png'); color: var(--v4-cream); text-shadow: var(--v4-outline-txt); }
.v4-btn.red:active:not(:disabled) { border-image-source: url('../img/v4/ui/btn-red-down.png'); }
.v4-btn.wood { border-image: url('../img/v4/ui/btn-wood.png') 5 7 5 7 fill / 15px 21px 15px stretch; color: var(--v4-cream); text-shadow: var(--v4-outline-txt); }
.v4-btn.wood:active:not(:disabled) { border-image-source: url('../img/v4/ui/btn-wood-down.png'); }
.v4-btn:disabled { filter: saturate(.3) opacity(.6); cursor: not-allowed; }

/* ================================================================== the shell ==== */
.v4-hud {
    position: fixed;
    z-index: 60;
    /* v4.2: a 4px edge, a 4px gap, 44px square boxes 2px apart (below), so the speaker fits
       beside a Pal's face and a six-letter name and a four-digit purse at 390px (QA1 p3);
       desktop keeps its 8px gap and 46px boxes (the media block further down). */
    left: 4px;
    right: 4px;
    top: max(10px, env(safe-area-inset-top));
    height: 60px;
    display: flex;
    align-items: center;
    gap: 4px;
    pointer-events: none;
}
.v4-hud > * { pointer-events: auto; }
/* A page that scrolls (the Book, Help: a .v4-sheetpage) on a phone: the HUD's band is the table's
   own wood, so the page goes under an edge and its words never show between the chips ("Worn
   smooth by water that" stood behind the coins, QA 4.2 phone #11). The band is behind the chips;
   a scene does not scroll and has none. */
@media (max-width: 1023px) {
    .v4-app:has(.v4-sheetpage) .v4-hud::before {
        content: '';
        position: fixed;
        z-index: -1;
        left: 0;
        right: 0;
        top: 0;
        height: var(--v4-hud-h);
        background: #8A552A url('../img/v4/tiles/wood.png') repeat;
        background-size: 488px 240px;
        image-rendering: pixelated;
        box-shadow: 0 2px 0 var(--v4-ink);
        /* It takes the tap itself: nothing the page has scrolled under it can be pressed. */
        pointer-events: auto;
    }
}
.v4-logo { display: none; }

/* The active Pal: their portrait in a framed peach box (the HUD orb), the name on a slim plate. */
.v4-palchip { display: flex; align-items: center; position: relative; text-decoration: none; color: var(--v4-ink); min-width: 0; }
.v4-ava { position: relative; flex: none; width: 58px; height: 58px; box-sizing: border-box; z-index: 2; overflow: hidden;
    border-style: solid; border-color: transparent; border-width: 0; border-image: var(--fr-box) 9 fill / 18px stretch; image-rendering: pixelated; }
/* The portrait canvas is 48 px: drawn at 1×, never squeezed (pixel-scale.mjs P1). */
.v4-ava .v4-face { position: absolute; left: 5px; top: 7px; width: 48px; height: 48px; }
.v4-ava .v4-ico { position: absolute; left: 13px; top: 13px; }
/* The name plate grows with the name and never cuts it: no ellipsis, and no word is ever broken.
   It takes all the room left of the coins (the coins sit at the row's end), and a name of several
   words wraps at its spaces onto a second line, the plate growing to hold it. When the name still
   does not fit, js/hudfit.js steps the row through data-fit (below): the coin count down to Mini
   16; the portrait aside so the plate has its room (coins back at 24, then at 16); and for a
   single word too long for any HUD (a typed name) the portrait alone, the name kept in the
   link's label. */
.v4-palchip .v4-nm { margin-left: -6px; padding: 2px 2px 2px 4px; min-height: 44px; box-sizing: border-box; display: flex; align-items: center;
    font-family: var(--v4-font-head); font-size: var(--t-px1); line-height: var(--t-px1); font-weight: 400; z-index: 1; min-width: 0; flex: 0 1 auto; }
.v4-palchip .v4-nm span { min-width: 0; white-space: normal; overflow-wrap: normal; word-break: normal; hyphens: manual; line-height: var(--t-px1); }
.v4-hud:is([data-fit="1"], [data-fit="3"]) .v4-coins b { font-size: var(--t-px1); line-height: var(--t-px1); }
.v4-hud:is([data-fit="2"], [data-fit="3"]) .v4-ava { display: none; }
.v4-hud:is([data-fit="2"], [data-fit="3"]) .v4-palchip .v4-nm { margin-left: 0; padding-left: 2px; min-height: 46px; }
.v4-hud[data-fit="4"] .v4-palchip .v4-nm { display: none; }
/* The speaker (one-tap mute) has one home at every size (v4.2 lane D2): on desktop it is in the
   HUD's row of boxes between the bell and the gear; on a phone, where the HUD row is the Pal's face
   and name, it stands in the dock's fifth cell, at the dock's right end (below). It never steps
   aside, and it never takes the room a name needs. */
@media (max-width: 1023px) {
    .v4-hud .v4-ibtn.v4-mute { position: fixed; z-index: 61; left: calc(50% + 122px); bottom: calc(35px + env(safe-area-inset-bottom)); }
}

.v4-coins { flex: none; display: flex; align-items: center; gap: 4px; height: 44px; box-sizing: border-box; padding: 0 4px 0 0; margin-left: auto; text-decoration: none; color: var(--v4-ink); }
.v4-coins .v4-ico, .v4-coins .ii { margin: -6px 0 -6px -4px; }
.v4-coins b { font-family: var(--v4-font-num); font-weight: 400; font-size: var(--t-px2); line-height: var(--t-px2); }
/* The bell (a sealed letter: what waits for you) and the gear: square Franuka boxes. They and the
   purse never shrink (a full row squeezed them to 43.83 px, under the touch floor and off the
   pixel grid): the name plate gives way, and js/hudfit.js steps the row. */
.v4-ibtn { flex: none; width: 44px; height: 44px; box-sizing: border-box; display: grid; place-items: center; position: relative; padding: 0; cursor: pointer; color: var(--v4-ink); text-decoration: none; background: none;
    border-style: solid; border-color: transparent; border-width: 0; border-image: var(--fr-box) 9 fill / 18px stretch; image-rendering: pixelated; }
.v4-ibtn + .v4-ibtn { margin-left: -2px; }
.v4-ibtn .v4-dot { position: absolute; z-index: 1; top: -10px; right: -10px; min-width: 22px; height: 22px; box-sizing: border-box; padding: 0 5px; border-radius: 11px; background: var(--v4-red-deep);
    box-shadow: 0 0 0 2px var(--v4-cream), 0 0 0 4px var(--v4-ink); color: #fff; font: 400 var(--t-px1)/22px var(--v4-font-num); font-style: normal; text-align: center; }

.v4-dock {
    position: fixed;
    /* Just under the HUD's layer, so the HUD's speaker draws over the dock's fifth cell. */
    z-index: 59;
    left: 0;
    right: 0;
    bottom: 0;
    box-sizing: border-box;
    height: calc(var(--v4-dock-h) + env(safe-area-inset-bottom));
    /* The labels keep a pixel of air off the plank's bottom rim. */
    padding: 0 6px calc(4px + env(safe-area-inset-bottom));
    /* Four fixed door cells and the speaker's cell (the HUD's mute box, placed over it from the
       HUD: css above), with whole-px gaps (space-around put the labels at x.25). 4×68 + 44 + 4×4 =
       332 px, centred, so the speaker's cell ends 166 px right of the middle. */
    display: grid;
    grid-template-columns: repeat(4, 68px) 44px;
    column-gap: 4px;
    justify-content: center;
    align-items: center;
}
/* A dock door: its Franuka icon (2x) on the plank, lifted into a framed peach box when open, and
   its name in ink lettering (the open one in deep red), both AA on the plank's tan. */
.v4-dk { display: flex; flex-direction: column; align-items: center; gap: 2px; text-decoration: none; color: var(--v4-ink);
    min-width: 64px; min-height: 64px; justify-content: center; }
.v4-dk-ico { width: 56px; height: 56px; box-sizing: border-box; display: grid; place-items: center; border-radius: 0; }
.v4-dk .v4-lbl { font-family: var(--v4-font-head); font-size: var(--t-px2); line-height: var(--t-px2); font-weight: 400; color: var(--v4-ink); text-shadow: none; }
.v4-dk.active .v4-dk-ico { transform: translateY(-4px); }
.v4-dk.active .v4-lbl { color: var(--v4-on); }
.v4-dk:hover .v4-dk-ico { transform: translateY(-2px); }
.v4-dk.active:hover .v4-dk-ico { transform: translateY(-4px); }

/* The page area between the HUD and the dock. */
/* The page area is exactly the band between the HUD and the dock, so "the middle of the page"
   is the middle of what the player can see. */
.v4-main {
    position: relative;
    box-sizing: border-box;
    margin: var(--v4-hud-h) 0 calc(var(--v4-dock-h) + env(safe-area-inset-bottom));
    padding: 10px 6px 14px;
    min-height: calc(100dvh - var(--v4-hud-h) - var(--v4-dock-h) - env(safe-area-inset-bottom));
}
.v4-side { display: none; }

/* Old pages render inside a framed dark page on a parchment mat; their lanes restyle them. */
.v4-main > .cp-shell {
    max-width: 720px;
    margin: 0 auto;
    padding: 0.75rem 0.75rem 5.5rem;   /* the v3 shell's own box: old pages were tuned inside it */
    background: var(--v4-slate);
    color: var(--cp-ink);
    font-family: var(--t-voice);
    border: 10px solid var(--v4-parchment);
    outline: 3px solid var(--v4-ink);
    box-shadow: 0 0 0 9px var(--v4-wood), 0 0 0 12px var(--v4-ink), 0 6px 0 12px var(--v4-ink);
    margin-top: 12px;
    margin-bottom: 12px;
    width: calc(100% - 24px);
}
.v4-main .cp-storage-fault.write { max-width: 720px; margin: 8px auto 0; }
.v4-main > .cp-shell:has(> .cp-page:last-child > .cp-empty:last-child) {
    min-height: calc(100dvh - var(--v4-hud-h) - var(--v4-dock-h) - 168px);
}

/* A scene screen (map, room) takes the whole page; the HUD and dock float over it. The scene is
   its own stacking context (z-index 1): places lay their overlays out against it and the stall
   sheets beside it. The kit's sheets and pops leave it through the top layer (.v4-layer below). */
.v4-app:has(.v4-scene) .v4-main { margin: 0; padding: 0; min-height: 100dvh; }
.v4-app:has(.v4-scene) .v4-main > .cp-shell { max-width: none; margin: 0; padding: 0; background: none; border: 0; outline: 0; box-shadow: none; width: auto; font-family: inherit; color: inherit; }
.v4-scene { position: fixed; inset: 0; z-index: 1; overflow: hidden; }
/* The rise-in moves the page with a transform, and a transformed ancestor would become the
   scene's containing block: the map would collapse to nothing for the first 180ms. */
.v4-app:has(.v4-scene) .cp-page { animation: none; }

/* A v4 page that is not a scene: parchment panels on the table, no dark frame. A machine's page
   is one from its heading on (Components/GameIdentity .gi-head), whatever stands under it: the
   first "Who's playing?" stood in the old dark frame, whose borders made the page 405 px wide on
   a 390 phone (QA 4.2 phone #2). */
.v4-app:has(.v4-sheetpage, .gi-head) .v4-main > .cp-shell { background: none; border: 0; outline: 0; box-shadow: none; padding: 0; color: var(--v4-ink); font-family: var(--v4-font-body); }
/* A machine's round on a phone takes the page's whole width: its stage grows by quarter steps, and
   the old shell's 12 px gutters left a 360 phone 296 px, which stepped the machine down to half
   size, where its 16 px words ran into each other (the legibility gate at 360, v4.2). */
@media (max-width: 1023px) {
    .v4-main > .cp-shell:has(> .cp-page > .ar.stage) { width: 100%; margin-left: 0; margin-right: 0; }
}

/* The top layer for the kit's sheets and pops: js/v4ui.js shows each [data-v4-layer] as a manual
   popover the moment it lands, which lifts it above every stacking context on the page (a scene's,
   a z-indexed place header's), the HUD and the dock, without moving it in the DOM: its styles
   and its place in the page stay where they were. Until it is shown (or where popovers are
   unsupported, or the script never ran) it is display: contents, never hidden: its children
   simply draw in place and their own z-indexes decide, as before. */
.v4-layer { display: contents; }
.v4-layer:popover-open {
    display: block; position: fixed; inset: 0; width: 100%; height: 100%; max-width: none; max-height: none;
    margin: 0; padding: 0; border: 0; background: none; color: inherit; overflow: visible; pointer-events: none;
}
.v4-layer:popover-open > * { pointer-events: auto; }
.v4-layer::backdrop { display: none; }

/* Toasts sit above the dock. */
.v4-app .cp-toast { bottom: calc(var(--v4-dock-h) + 16px + env(safe-area-inset-bottom)); }
/* A toast is a small peach Franuka box: ink words in the body face (a sentence), a drop below it. */
.v4-app .cp-toast {
    box-sizing: border-box; min-height: 48px; padding: 0 4px;
    background: none; color: var(--v4-ink);
    border: 14px solid transparent; border-radius: 0; border-image: var(--fr-box) 9 fill / 18px stretch; image-rendering: pixelated;
    box-shadow: none; filter: drop-shadow(0 4px 0 rgba(67, 48, 35, .45));
    font-family: var(--v4-font-body); font-size: var(--t-speech); line-height: 24px;
}
/* A result card's buttons stand just above the dock, where a toast lands (QA 4.2 phone #4: "Spore
   reached Lv 2!" lay over "Again" and "Back to hall"). While a round's result, a journey's story
   or a fight's result is up, a phone's toast stands under the HUD instead. */
@media (max-width: 1023px) {
    .v4-app:has(.rr, .tr-story, .gl-result-wrap) .cp-toast { bottom: auto; top: calc(var(--v4-hud-h) + 8px + env(safe-area-inset-top)); }
}
.v4-app .cp-toast-x { color: var(--v4-ink-soft); }
.v4-app .cp-toast-again { color: var(--v4-on); font-family: var(--v4-font-num); }

/* ================================================================== desktop ==== */
/* The board on the table (v4.2, DESKTOP_PLAN B1–B8). The window is the wooden table; one board
   lies on it at whole-pixel sizes: the rail, the picture frame and the side panel, the HUD over
   them. js/board.js picks the tier (the largest whole scene scale S whose board fits the window
   on both axes) and writes the measures below on <html>; these defaults are its D3 board, which
   stands if the script never ran. Within a tier nothing moves between screens: the frame is
   200·S by 140·S plus the shelf, inside 20 px of plank, and the side panel always exists.
   deploy/checks/v4/board-layout.mjs holds every screen to it. */
@media (min-width: 1024px) {
    :root {
        --board-s: 3; --rail-w: 96px; --side-w: 256px; --gap: 8px; --hud-h: 60px; --shelf-h: 120px;
        --frame-b: 20px; --scene-w: 600px; --scene-h: 420px; --frame-w: 640px; --frame-h: 580px;
        --board-w: 1008px; --board-h: 648px; --board-x: 8px; --board-y: 8px;
        /* The picture frame's own corner on the table, from the measures above: what a window on
           the board is centred on (a sheet, the Pal card, the Bag, the journey's story). */
        --frame-x: calc(var(--board-x) + var(--rail-w) + var(--gap));
        --frame-y: calc(var(--board-y) + var(--hud-h) + var(--gap));
    }
    body { overflow-x: hidden; }
    .v4-app {
        display: grid;
        grid-template-columns: var(--rail-w) var(--frame-w) var(--side-w);
        grid-template-rows: var(--hud-h) var(--frame-h);
        column-gap: var(--gap);
        row-gap: var(--gap);
        box-sizing: border-box;
        width: var(--board-w);
        height: var(--board-h);
        min-height: 0;
        margin: var(--board-y) 0 0 var(--board-x);
        padding: 0;
    }

    /* The HUD spans the board: the logo over the rail, the purse and the gear ending on the side
       panel's right edge. */
    .v4-hud { position: static; grid-column: 1 / -1; grid-row: 1; height: var(--hud-h); gap: 8px; }
    .v4-ibtn { width: 46px; height: 46px; }
    .v4-ibtn + .v4-ibtn { margin-left: 0; }
    .v4-logo { display: flex; align-items: center; gap: 8px; margin-right: 18px; text-decoration: none; }
    .v4-logo canvas { width: 48px; height: 48px; margin: 0 -6px; }
    .v4-logo span { font-family: var(--v4-font-title); font-size: var(--t-px3); letter-spacing: 0; line-height: var(--t-px3); color: var(--v4-yellow); text-shadow: var(--v4-outline-txt); white-space: nowrap; }

    /* The rail: a plank panel that hugs its four doors, its top on the frame's top. */
    .v4-dock {
        position: static;
        grid-column: 1;
        grid-row: 2;
        align-self: start;
        height: auto;
        margin: 0;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: flex-start;
        gap: 8px;
        padding: 4px 0 6px;
    }
    .v4-dk-ico { width: 72px; height: 72px; }
    .v4-dk-ico .v4-ico { width: 48px; height: 48px; }
    .v4-dk .v4-lbl { font-size: var(--t-px2); line-height: var(--t-px2); }
    /* From D5 the doors are 88 px boxes with their icons at 4×. */
    :root:is([data-board="5"], [data-board="6"]) .v4-dk-ico { width: 88px; height: 88px; }
    :root:is([data-board="5"], [data-board="6"]) .v4-dk-ico .v4-ico { width: 64px; height: 64px; }
    /* From D5 the HUD's Pal chip is bigger (DESKTOP_PLAN B7): the portrait at 60 px in a 68 px
       box (the canvas is drawn at its box's size, js/stage/stage.js data-css-size) and the name
       in Mini 24. */
    :root:is([data-board="5"], [data-board="6"]) .v4-ava { width: 68px; height: 68px; }
    :root:is([data-board="5"], [data-board="6"]) .v4-ava .v4-face { left: 4px; top: 5px; width: 60px; height: 60px; }
    :root:is([data-board="5"], [data-board="6"]) .v4-palchip .v4-nm { min-height: 52px; padding: 2px 6px 2px 8px; font-size: var(--t-px2); line-height: var(--t-px2); }
    :root:is([data-board="5"], [data-board="6"]) .v4-palchip .v4-nm span { line-height: var(--t-px2); }

    /* The picture frame: Franuka's plank box (BGbox_05A) at 2x as a real 20 px border, so the
       frame's box is the board's and the page inside it is exactly the scene and its shelf. */
    .v4-main {
        grid-column: 2;
        grid-row: 2;
        box-sizing: border-box;
        width: var(--frame-w);
        height: var(--frame-h);
        min-height: 0;
        margin: 0;
        padding: 0;
        overflow: auto;
        overscroll-behavior: contain;
        /* Inside the plank only: the plank's art leaves its outer rows open, and a parchment
           clipped to the border box showed through them as a peach rule round the frame. */
        background: var(--v4-parchment) padding-box;
        border: var(--frame-b) solid transparent;
        border-image: var(--fr-wood) 10 / var(--frame-b) stretch;
        image-rendering: pixelated;
    }
    .v4-main > * { image-rendering: auto; }
    .v4-main > .cp-shell { box-shadow: 0 0 0 9px var(--v4-wood), 0 0 0 12px var(--v4-ink); margin: 24px auto; }
    .v4-app:has(.v4-scene) .v4-main { overflow: hidden; min-height: 0; margin: 0; }
    .v4-app:has(.v4-scene) .v4-main > .cp-shell { height: 100%; }
    .v4-scene { position: absolute; }

    /* The side panel: the frame's height, always there. A screen with no cards of its own shows
       the shell's (DesktopFrame's fallback). */
    .v4-side {
        display: flex;
        grid-column: 3;
        grid-row: 2;
        flex-direction: column;
        gap: 12px;
        margin: 0;
        height: var(--frame-h);
        min-height: 0;
        overflow: auto;
    }
    /* A side card hugs its content (DESKTOP_PLAN B4) and never shrinks to fit the panel; a page
       may still let its last card grow (lowest specificity, so the page's rule wins). */
    :where(.v4-side) :where(.v4-side-card) { flex-shrink: 0; }
    /* A card js/sidefit.js has set aside: the small board's panel could not hold it whole. */
    .v4-side .v4-side-card[data-side-aside] { display: none; }
    .v4-side > .v4-side-fallback { display: contents; }
    .v4-side:has(> :not(.v4-side-fallback)) > .v4-side-fallback { display: none; }

    /* A toast lies in the HUD's row, in the bare wood between the Pal's name and the purse, so it
       never covers a button on the board (QA 4.2 desktop #16: it lay over "Again" and "Back to
       hall", the Lawn's verbs and a trail's sign at the frame's foot). */
    .v4-app .cp-toast { bottom: auto; top: calc(var(--board-y) + 6px); max-width: none;
        left: calc(var(--frame-x) + 268px); right: calc(100% - var(--frame-x) - var(--frame-w) + 8px); }

    /* The first screen and the hatch (Pages/Home.razor .fr-welcome, .fr-full): there is no rail
       and no side panel yet, so the picture frame lies alone on the table, in the middle of it,
       under the "Cache Pal" plate (it stood 100 px left of centre beside an empty side panel's
       slot, QA 4.2 desktop #14). The board assembles round it when the room opens. */
    :root:has(.fr-welcome, .fr-full) { --frame-x: calc(var(--board-x) + (var(--board-w) - var(--frame-w)) / 2); }
    .v4-app:has(.fr-welcome, .fr-full) { grid-template-columns: var(--frame-w); width: var(--frame-w); margin-left: var(--frame-x); }
    .v4-app:has(.fr-welcome, .fr-full) .v4-main { grid-column: 1; }
    .v4-app:has(.fr-welcome, .fr-full) .v4-side { display: none; }

    /* A page laid out to the frame itself (.v4-fitpage, v4.2 lane D3: the Book's spread, Help, the
       day book's and the almanac's pages): the shell's wrappers take the frame's whole inside and
       the page, not the frame, decides what scrolls. */
    .v4-app:has(.v4-fitpage) .v4-main { overflow: hidden; }
    .v4-app:has(.v4-fitpage) .v4-main > .cp-shell { margin: 0; width: auto; max-width: none; height: 100%; }
    .v4-app:has(.v4-fitpage) .cp-page { height: 100%; }
    /* The page itself lies on the writing desk: its header, then whatever it lays out below. */
    .v4-app .v4-fitpage { box-sizing: border-box; height: 100%; max-width: none; margin: 0; display: flex; flex-direction: column;
        padding: 10px 14px 12px; background: repeating-linear-gradient(180deg, #BF8348 0 12px, #B77A40 12px 24px); }
    .v4-app .v4-fitpage > header { flex: none; }
}

/* ================================================================ v4 pages ==== */
/* Bag, Book, Settings and the place shells: parchment panels straight on the table. */
.v4-sheetpage { max-width: 760px; margin: 0 auto; padding: 0 4px 8px; color: var(--v4-ink); font-family: var(--v4-font-body); }
.v4-pagehead { display: flex; justify-content: center; margin: 4px 0 10px; }
/* A page's title hangs on the gold ribbon (BannerMedium_02C) in the Franuka title face. */
.v4-pagehead h1 {
    display: inline-flex; align-items: center; justify-content: center; gap: 10px; margin: 0; min-height: 50px; box-sizing: border-box; padding: 0 6px 2px;
    border-style: solid; border-color: transparent; border-width: 12px 26px 16px;
    border-image: var(--fr-ribbon) 7 14 9 14 fill / 14px 28px 18px stretch; image-rendering: pixelated;
    font-family: var(--v4-font-title); font-weight: 400; font-size: var(--t-px3); letter-spacing: 0; line-height: var(--t-px3); color: var(--v4-ink); text-shadow: none;
}
.v4-pagehead h1 .v4-e { font-size: 24px; text-shadow: none; }
.v4-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(86px, 1fr)); gap: 14px 8px; justify-items: center; }
.v4-row-actions { display: flex; flex-wrap: wrap; gap: 16px 10px; justify-content: center; margin-top: 12px; }
.v4-hint { margin: 2px 0 10px; font-size: var(--t-px1); line-height: 24px; text-align: center; }
.v4-tile {
    position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px; width: 100%; max-width: 112px; min-height: 96px; box-sizing: border-box;
    padding: 4px; color: var(--v4-ink); text-decoration: none; font-family: var(--v4-font-head); font-weight: 400; font-size: var(--t-px1); line-height: var(--t-px1); text-align: center;
}
.v4-tile .v4-e { font-size: 32px; }
.v4-tile canvas { width: 48px; height: 48px; }
.v4-tile .v4-count { position: absolute; right: -6px; bottom: -6px; min-width: 24px; height: 24px; padding: 0 5px; box-sizing: border-box; border-radius: 12px; background: var(--v4-ink); color: var(--v4-cream); font: 400 var(--t-px1)/24px var(--v4-font-num); }
.v4-tile.gone { filter: grayscale(.8) opacity(.75); }
.v4-subtitle { margin: 18px 0 10px; font-family: var(--v4-font-head); font-weight: 400; font-size: var(--t-px1); line-height: var(--t-px1); text-align: center; }

.v4-frame-panel .v4-big-lbl, .v4-frame-parchment .v4-big-lbl { color: var(--v4-ink); text-shadow: none; }
.v4-dk.active .v4-dk-ico { border-image: var(--fr-box) 9 fill / 18px stretch; image-rendering: pixelated; }

@media (prefers-reduced-motion: reduce) {
    .v4-app:has(.v4-scene) .cp-page { animation: none; }
    .v4-app *, .v4-app *::before, .v4-app *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

/* ================================================================== turn me upright ==== */
/* Ruling Q37: only a phone on its side (a coarse pointer in a short landscape window). */
.v4-turn { display: none; }
@media (orientation: landscape) and (max-height: 540px) and (pointer: coarse) {
    .v4-turn {
        position: fixed;
        inset: 0;
        z-index: 9500;
        display: grid;
        place-items: center;
        background: linear-gradient(#F7E2B4 0 62%, #D99E5E 62%);
        color: var(--v4-ink);
    }
    .v4-turn-in { display: flex; flex-direction: column; align-items: center; gap: 8px; }
    .v4-turn-pal { width: 144px; height: 144px; image-rendering: pixelated; }
    .v4-turn-egg { font-size: 96px; }
    .v4-turn-hint { margin: 0; font-family: var(--v4-font-head); font-size: var(--t-px2); line-height: var(--t-px2); }
    .v4-turn .v4-bubble { margin-bottom: 18px; }
}

/* ================================================================== the arcade's words ==== */
/* js/arcade-kit.js: counters, hints, count-ins and banners are DOM text over the stage, in Cache
   Pal Mini at whole sizes, placed on whole device pixels by the kit. They never take a tap. */
.cp-arcade-stage { position: relative; }
/* A block, so no line box hangs a descender's gap under the machine's screen. */
.cp-arcade-stage canvas { display: block; }
/* The machine's canvas is exactly its stage's width, which js/arcade-kit.js fitStage sets to a whole
   quarter of the grid in whole pixels. Phaser's own fit can measure its parent mid-layout and write
   a fraction (314.984375 px for 315, under load in the arcade gate); the stage's width wins. */
.cp-arcade-stage[data-stage-scale] canvas { width: 100% !important; height: auto !important; }
.cp-arcade-words { position: absolute; inset: 0; pointer-events: none; overflow: visible; z-index: 1; }
.cp-aw { position: absolute; left: 0; top: 0; white-space: nowrap; font-family: var(--t-mini); font-weight: 400; letter-spacing: 0; }
/* A symbol Mini lacks keeps a box 1.25 em wide exactly (a whole px at 16/24/32/40), so the letters after it start on a whole
   pixel too. An emoji key in a counter ("✓ 0") is its pixel icon instead (v4.2 lane E): 16 px beside 16-24 px words, 32 beside
   32-40, set on the word as --aw-i, top-aligned in the line like the box was. */
.cp-aw .v4-e { font-size: inherit; line-height: inherit; width: 1.25em; text-align: center; vertical-align: top; }
.cp-aw .cp-aw-i { display: inline-block; width: var(--aw-i, 16px); height: var(--aw-i, 16px); vertical-align: top; image-rendering: pixelated; }
/* A machine's screen (js/arcade-kit.js readTokens): the Franuka plum night, its words AA on it. */
:root {
    --ar-screen-bg: #3a2530;
    --ar-screen-panel: #52333f;
    --ar-screen-panel-2: #633d4c;
    --ar-screen-ink: #fff3dd;
    --ar-screen-muted: #daa684;
    --ar-screen-accent: #5ab552;
    /* For words over a machine's art (the lighter plum, #633d4c): the kit's peach 5.86:1 and its
       bright green 4.84:1. The tan and the green above are for paint. */
    --ar-screen-quiet: #ffc2a1;
    --ar-screen-good: #63d54a;
    --ar-screen-danger: #ff8a7a;
    --ar-screen-gold: #f0b541;
    --ar-screen-line: #8f4d57;
}

/* ================================================================== the update banner ==== */
/* js/update.js is byte-identical with the Barn's (BarnCheck W28) and styles its banner inline in
   the system face. In the game the banner speaks in the game's faces, and it is centred by its
   margins rather than a -50% translate (a half pixel on an odd width). */
#cp-update-banner { left: 0 !important; right: 0 !important; width: max-content !important; margin: 0 auto !important; transform: none !important;
    font: 400 var(--t-body)/24px var(--t-voice) !important; font-synthesis: none; letter-spacing: 0; }
#cp-update-banner button { font: 400 var(--t-px1)/var(--t-px1) var(--t-mini) !important; font-synthesis: none; letter-spacing: 0; }
