/* =========================================================================
   3D PROTAGON PUZZLES — 2D landing page ("Crossword Paper" re-skin)
   -------------------------------------------------------------------------
   Visual layer only. Every selector the markup (index.php) relies on is
   preserved; only the look changes. Fonts + palette come from tokens.css.
   ========================================================================= */

/* Legacy local tokens are re-pointed at the global system so any stray
   references elsewhere still resolve to the new palette. */
:root {
    --text-main: var(--text);
    --tagline-color: var(--text-muted);
    --panel-bg: var(--panel);
    --panel-border: var(--line);
    --panel-shadow: var(--shadow-md);
    --card-bg: var(--bg-1);
    --card-color: var(--text);
    --card-border: var(--line);
    --card-shadow: var(--shadow-md);
    --label-color: var(--text);
    --pill-bg: var(--panel);
    --pill-border: var(--line);
    --pill-color: var(--text);
    --toggle-bg: var(--panel);
    --toggle-border: var(--line);
    --toggle-text: var(--text);
}

* { box-sizing: border-box; }
html, body { height: 100%; }

body {
    margin: 0;
    font-family: var(--font-body);
    color: var(--text);
    -webkit-font-smoothing: antialiased;
}

/* ── Theme toggle ─────────────────────────────────────────────── */
.theme-toggle-wrap {
    max-width: 1300px;
    margin: 16px auto 0;
    display: flex;
    justify-content: flex-end;
}
.theme-toggle {
    border: 1px solid var(--toggle-border);
    background: var(--toggle-bg);
    color: var(--toggle-text);
    border-radius: var(--radius-pill);
    padding: 9px 16px;
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 0.85rem;
    cursor: pointer;
    transition: border-color 0.2s ease, color 0.2s ease;
}
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }

/* ── Page grid: hero (left) + puzzle panel (right) ────────────── */
.page {
    max-width: 1300px;
    margin: 56px auto;
    display: grid;
    grid-template-columns: 1fr minmax(440px, 520px);
    gap: 40px;
    align-items: start;
}
.left {
    padding: 8px 8px 8px 0;
    position: sticky;
    top: 24px;
    align-self: start;
}

/* ── Hero ─────────────────────────────────────────────────────── */
.hero-title {
    position: relative;
    margin: 0 0 32px;
    padding: 8px 0;
    /* Contain the decorative graph-paper bleed horizontally.
       `.hero-title::before` bleeds right by clamp(-40px, -3vw, -12px), which is
       about -31px at a 1024px viewport. `.hero-title` is already the full page
       width, so any outward bleed lands past the page and became a horizontal
       scrollbar: /3d measured 19px of sideways scroll at 1024px and 11px at 768px,
       with documentElement.scrollWidth at 1028 against a 1024px viewport.
       `overflow-x: clip` rather than `hidden`: clip does not force the other axis
       to `auto`, so the -8px vertical bleed keeps painting, and the horizontal
       bleed is cut at the page edge — which is where the rule above already says
       it should stop. */
    overflow-x: clip;
}

/* Faint, slowly-drifting graph-paper grid — the puzzle/craft motif.
   Low contrast, sits behind the headline text, paused for reduced motion. */
.hero-title::before {
    content: "";
    position: absolute;
    /* The decorative graph paper deliberately bleeds past the headline, but not
       past the page: on a narrow viewport the 40px right bleed landed outside
       the viewport and gave the whole document a horizontal scrollbar
       (documentElement.scrollWidth measured 418px against a 390px viewport).
       The bleed is clamped to the page box so the effect survives intact. */
    inset: -8px clamp(-40px, -3vw, -12px) -8px clamp(-16px, -2vw, -8px);
    background-image: var(--grid-texture);
    background-size: var(--grid-size) var(--grid-size);
    opacity: 0.55;
    z-index: 0;
    pointer-events: none;
    animation: gridDrift 32s linear infinite;
    -webkit-mask-image: radial-gradient(120% 120% at 0% 0%, #000 35%, transparent 78%);
            mask-image: radial-gradient(120% 120% at 0% 0%, #000 35%, transparent 78%);
}
.site-eyebrow,
.site-title,
.site-tagline,
.site-lede {
    position: relative;
    z-index: 1;
}

.site-eyebrow {
    margin: 0 0 14px;
    font-family: var(--font-body);
    font-size: var(--fs-eyebrow);
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--accent);
}

.site-title {
    font-family: var(--font-display);
    font-optical-sizing: auto;
    font-size: var(--fs-display);
    font-weight: 700;
    line-height: 0.96;
    letter-spacing: -0.025em;
    margin: 0 0 18px;
    color: var(--text);
}
/* A short vermilion rule under the wordmark — editorial accent. */
.site-title::after {
    content: "";
    display: block;
    width: 64px;
    height: 5px;
    background: var(--accent);
    border-radius: 3px;
    margin-top: 18px;
}

.site-tagline {
    margin: 0;
    font-family: var(--font-display);
    font-style: italic;
    font-weight: 500;
    font-size: clamp(1.2rem, 2.4vw, 1.7rem);
    line-height: 1.2;
    color: var(--text-muted);
}

.site-lede {
    margin: 18px 0 0;
    max-width: 32rem;
    font-size: var(--fs-body-lg);
    line-height: 1.55;
    color: var(--text-muted);
}

@keyframes gridDrift {
    from { background-position: 0 0, 0 0; }
    to   { background-position: var(--grid-size) var(--grid-size), var(--grid-size) var(--grid-size); }
}

/* ── Primary / secondary CTAs ─────────────────────────────────── */
.subheading {
    display: flex;
    gap: 16px;
    flex-direction: column;
    margin-top: 8px;
}


/* ── Puzzle panel + card grid ─────────────────────────────────── */
.panel {
    padding: 24px;
    border-radius: var(--radius);
    background: var(--panel);
    border: 1px solid var(--panel-border);
    box-shadow: var(--panel-shadow);
}

.grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
}

.card {
    min-height: 224px;
    border-radius: var(--radius-sm);
    background: var(--card-bg);
    display: flex;
    flex-direction: column;
    color: var(--card-color);
    text-align: center;
    padding: 12px;
    border: 1px solid var(--card-border);
    position: relative;
    overflow: hidden;
    box-shadow: var(--card-shadow);
    cursor: pointer;
    transition: transform 0.22s cubic-bezier(.22, .68, 0, 1.2),
                box-shadow 0.22s ease,
                border-color 0.22s ease;
}
.card:hover {
    transform: translateY(-5px);
    border-color: var(--accent);
    box-shadow: var(--shadow-lg);
}
.card:hover .card-thumb { box-shadow: 0 12px 24px rgba(0, 0, 0, 0.22); }

/* A slim vermilion top-edge that grows in on hover — replaces the old sheen. */
.card::after {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 3px;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.28s ease;
    z-index: 3;
}
.card:hover::after { transform: scaleX(1); }

.card-thumb {
    display: block;
    width: 100%;
    height: 146px;
    position: relative;
    border-radius: var(--radius-sm);
    overflow: hidden;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
    transition: box-shadow 0.22s ease;
}
/* Stretched-link: extend the anchor over the whole card. */
.card-thumb::after {
    content: "";
    position: absolute;
    top: -200px; right: -200px; bottom: -200px; left: -200px;
    z-index: 1;
}

/* Category pills — colour-coded by data-category (Word/Logic/Spatial). */
.card .pill {
    position: absolute;
    top: 10px; left: 10px;
    z-index: 2;
    background: var(--pill-bg);
    border: 1px solid var(--pill-border);
    color: var(--pill-color);
    padding: 5px 10px;
    border-radius: var(--radius-pill);
    font-family: var(--font-body);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.card[data-category="word"] .pill    { color: var(--cat-word);    border-color: var(--cat-word); }
.card[data-category="logic"] .pill   { color: var(--cat-logic);   border-color: var(--cat-logic); }
.card[data-category="spatial"] .pill { color: var(--cat-spatial); border-color: var(--cat-spatial); }

.label-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 12px;
    padding: 0 4px;
    color: var(--label-color);
}
.label {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 1.05rem;
    letter-spacing: -0.005em;
}

.card.highlight {
    border-color: var(--accent);
    box-shadow: var(--shadow-md);
}

/* small footer (hidden by design) */
.footer { display: none; }

/* ── Hover "blowout" preview ──────────────────────────────────── */
.card-blowout {
    position: fixed;
    z-index: 9999;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.2s ease;
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: var(--shadow-lg);
    background: var(--panel);
    border: 1px solid var(--line);
}
/* The blowout carries a clone of the card's glyph SVG; .puzzle-glyph already
   fills its container, so no extra sizing rule is needed here. */
.card-blowout.active { opacity: 1; }

/* ── Puzzle of the Day (hero co-star — "show, don't tell") ────── */
.daily-card {
    display: flex;
    align-items: center;
    gap: 14px;
    max-width: 460px;
    margin: 6px 0 22px;
    padding: 14px 16px;
    border-radius: var(--radius);
    text-decoration: none;
    background: var(--panel);
    border: 1px solid var(--line);
    border-left: 3px solid var(--accent);
    color: inherit;
    transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.daily-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
    border-left-color: var(--accent);
    border-color: var(--accent);
}
/* The daily card reuses the puzzle-card glyph: a 60px square in which the
   inline SVG (viewBox 120x146) letterboxes itself, centred. */
.daily-card__thumb {
    width: 60px; height: 60px;
    border-radius: var(--radius-sm);
    flex: 0 0 auto;
    background: var(--bg-1);
    overflow: hidden;
}
.daily-card__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.daily-card__eyebrow {
    font-family: var(--font-body);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent);
}
.daily-card__title {
    font-family: var(--font-display);
    font-size: 1.2rem;
    font-weight: 700;
    line-height: 1.2;
}
.daily-card__meta { font-size: 0.8rem; color: var(--text-muted); }
.daily-card__cta { margin-top: 4px; font-size: 0.85rem; font-weight: 700; color: var(--accent); }
/* The return hook: a live tick to the next UTC midnight under the CTA,
   driven by postwin.js (the same ticker the completion modal uses). */
.daily-card__countdown { font-size: 0.75rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.daily-card__countdown [data-postwin-countdown] { font-weight: 700; color: var(--text); }

/* ── Responsive ───────────────────────────────────────────────── */
@media (max-width: 1100px) {
    .page { grid-template-columns: 1fr; }
    .panel { order: 2; }
    .left { position: static; padding: 0; }
    .site-title { font-size: clamp(2.5rem, 9vw, 3.5rem); }
    .btn-text strong { font-size: 1.4rem; }
}

/* Mobile: surface the primary CTA right under the headline so value prop +
   action land in the first viewport (per the brief's mobile-first goal).
   Reorder via flex order — markup untouched.

   `.hero-cube` is listed explicitly, and that is the whole point. It was missing,
   so it kept the default order 0 and — being after `.hero-title` in the DOM, which
   is also 0 — it sorted AHEAD of `.subheading` at order 1. The stated intent was
   therefore not what the browser did. Measured at 390x844 before this:

       .hero-title    order 0   top 183
       .hero-cube     order 0   top 538   <- slips in front of the CTA
       .subheading    order 1   top 743   partially cut off at 844
       .daily-card    order 2   top 887

   Now 0/1/2/3 in the intended sequence, so the CTA sits directly under the
   headline as the comment has always claimed. */
@media (max-width: 768px) {
    .left { display: flex; flex-direction: column; }
    .hero-title { order: 0; }
    .subheading { order: 1; }
    .hero-cube { order: 2; }
    .daily-card { order: 3; }
}

@media (max-width: 600px) {
    .grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
    .site-title { font-size: clamp(2rem, 11vw, 2.8rem); }
    .btn-icon { font-size: 30px; }
    .btn-text strong { font-size: 1.2rem; }
    .big-btn { padding: 14px 18px; gap: 14px; }
    .theme-toggle { padding: 7px 12px; font-size: 0.8rem; }
}

@media (max-width: 400px) {
    .site-title { font-size: clamp(1.8rem, 12vw, 2.4rem); }
    .btn-icon { font-size: 26px; }
    .btn-text strong { font-size: 1.05rem; }
    .btn-text small { letter-spacing: 0.05em; }
    .btn-arrow { font-size: 18px; }
    .big-btn { padding: 12px 14px; gap: 10px; border-radius: 12px; }
    .grid { grid-template-columns: 1fr 1fr; }
    .card { min-height: 180px; }
}

/* =========================================================================
   HERO CUBE — the signature interactive moment
   -------------------------------------------------------------------------
   The single thing that differentiates this site is that its puzzles are not
   only flat: there are genuinely three-dimensional ones you rotate and explore.
   The hero used to describe that in a sentence and show nine flat screenshots,
   so the differentiator was invisible above the fold.

   This is a real, live, draggable 3D object built from CSS transforms alone —
   no WebGL, no library, no image, nothing extra to download. Six divs and one
   requestAnimationFrame loop that stops as soon as the cube is off screen, the
   tab is hidden, or the user is interacting with it.

   Sizing: a cube rotated 45° on Y projects to about 1.45x its own edge, so the
   CONTAINER is sized at 1.45x and the stage is centred inside it. Without that
   the rotated corners spill out of the layout box and collide with whatever
   sits below — which is exactly what happened on the first attempt.

   Accessibility:
   • Focusable and arrow-key rotatable; the idle spin pauses while focused or
     hovered, and stops entirely under prefers-reduced-motion (drag and keys keep
     working, so nothing is taken away).
   • role="img" would hide the interactivity and role="application" would
     overstate it, so it is a labelled group with a described control.
   • The faces are decorative and aria-hidden; a screen-reader description
     carries the meaning.

   The faces read as puzzle surfaces rather than decoration: Sudoku grids and
   word rows, coloured with the 2D (ochre) and 3D (petrol-teal) family accents
   from the design system.
   ========================================================================= */
.hero-cube {
    /* One place to change the cube's edge length. The container is derived from
       it so the two can never drift apart. */
    --hero-cube-size: clamp(146px, 20vw, 208px);
    --hero-cube-outer: calc(var(--hero-cube-size) * 1.45);

    position: relative;
    z-index: 1;
    width: var(--hero-cube-outer);
    height: var(--hero-cube-outer);
    display: grid;
    place-items: center;
    margin: 6px 0 26px;
    perspective: 780px;
    flex: 0 0 auto;
}

.hero-cube__stage {
    width: var(--hero-cube-size);
    height: var(--hero-cube-size);
    position: relative;
    transform-style: preserve-3d;
    /* Tilt + the user's drag/keyboard offset. Only written while interacting.
       --rx / --ry are set by hero-cube.js; the values here are the static pose
       before the script runs and whenever motion is reduced. */
    transform: rotateX(var(--hero-cube-rx, -18deg)) rotateY(var(--hero-cube-ry, 28deg));
}

/* The idle spin lives here, as a CSS animation, so it runs on the compositor
   instead of the main thread. Driving it from requestAnimationFrame meant a
   style recalculation of the whole preserve-3d subtree on every frame and
   measured 510ms of total blocking time / 4.6s of main-thread work; as a
   compositor animation its main-thread cost is zero.

   Composition is Rx(tilt) . Ry(offset) . Ry(spin) = Rx(tilt) . Ry(offset+spin),
   which is exactly the intended result, so splitting it across two elements
   changes nothing visually. */
.hero-cube__spin {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    animation: hero-cube-spin 20s linear infinite;
}

@keyframes hero-cube-spin {
    from { transform: rotateY(0deg); }
    to   { transform: rotateY(-360deg); }
}

/* Pause while the visitor is engaging with it, so the object holds still under
   the pointer and under the keyboard. */
.hero-cube:hover .hero-cube__spin,
.hero-cube:focus-within .hero-cube__spin,
.hero-cube:focus-visible .hero-cube__spin,
.hero-cube[data-dragging="true"] .hero-cube__spin {
    animation-play-state: paused;
}

/* Per-face lighting. Without it all six faces render at identical brightness and
   the object reads as a wireframe diagram rather than something solid. Each face
   casts its own shadow, and the top face catches light. */
.hero-cube__face {
    position: absolute;
    inset: 0;
    display: grid;
    gap: 2px;
    padding: 7px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background-color: var(--bg-1);
    background-image:
        linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0)),
        var(--grid-texture);
    background-size: auto, 11px 11px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
    backface-visibility: hidden;
}

/* Each face is a 3x3 board of blocks — enough to read as a puzzle grid at a
   glance without 81 elements per face. */
.hero-cube__face--sudoku,
.hero-cube__face--word { grid-template-columns: repeat(3, 1fr); }

.hero-cube__cell {
    border-radius: 2px;
    background: var(--bg-2);
    opacity: 0.85;
    /* A hairline of light along the top of every tile, so the grid has relief
       rather than reading as flat rectangles. */
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
}

.hero-cube__cell--on   { background: var(--accent);      opacity: 1; }
.hero-cube__cell--warm { background: var(--cat-word);    opacity: 1; }
.hero-cube__cell--cool { background: var(--cat-spatial); opacity: 1; }

/* Six faces, pushed out along Z by half the cube's edge. */
.hero-cube { --hero-cube-half: calc(var(--hero-cube-size) / 2); }
.hero-cube__face--front  { transform: translateZ(var(--hero-cube-half)); }
.hero-cube__face--back   { transform: rotateY(180deg) translateZ(var(--hero-cube-half)); }
.hero-cube__face--right  { transform: rotateY(90deg)  translateZ(var(--hero-cube-half)); }
.hero-cube__face--left   { transform: rotateY(-90deg) translateZ(var(--hero-cube-half)); }
.hero-cube__face--top    { transform: rotateX(90deg)  translateZ(var(--hero-cube-half)); }
.hero-cube__face--bottom { transform: rotateX(-90deg) translateZ(var(--hero-cube-half)); }

/* --face-shade is how much shadow the face carries; --face-lit adds a highlight.
   Values approximate a key light coming from the upper left. */
.hero-cube__face::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    background:
        linear-gradient(180deg, rgba(255, 255, 255, var(--face-lit, 0)) 0%, transparent 55%),
        rgba(0, 0, 0, var(--face-shade, 0));
}
.hero-cube__face--front  { --face-shade: 0;    --face-lit: 0.05; }
.hero-cube__face--right  { --face-shade: 0.10; --face-lit: 0; }
.hero-cube__face--back   { --face-shade: 0.18; --face-lit: 0; }
.hero-cube__face--left   { --face-shade: 0.24; --face-lit: 0; }
.hero-cube__face--top    { --face-shade: 0;    --face-lit: 0.14; }
.hero-cube__face--bottom { --face-shade: 0.30; --face-lit: 0; }

/* Contact shadow so the cube reads as sitting in space rather than floating. */
.hero-cube::after {
    content: "";
    position: absolute;
    left: 50%;
    bottom: 6%;
    width: calc(var(--hero-cube-size) * 1.05);
    height: calc(var(--hero-cube-size) * 0.16);
    transform: translateX(-50%);
    border-radius: 50%;
    /* No filter: blur() here — a blur sitting under an animating sibling gets
       re-rastered every frame. The radial gradient already fades smoothly. */
    background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.08) 45%, transparent 72%);
    pointer-events: none;
}

.hero-cube[data-dragging="true"] { cursor: grabbing; }
.hero-cube:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
    border-radius: var(--radius-lg);
}

.hero-cube__hint {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    top: 100%;
    margin-top: 2px;
    font-family: var(--font-body);
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--text-muted);
    white-space: nowrap;
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    /* Stop the spin outright and pin the pose, so a reduced-motion visitor never
       sees a single frame of rotation. Drag and the arrow keys still work. */
    .hero-cube__spin { animation: none; }
    .hero-cube__stage { transform: rotateX(-18deg) rotateY(0deg); }
}

/* Mini cube for the daily card — the Puzzle of the Day always gets the site's
   signature rotating cube (the hero object's presentation at stamp size),
   whether the day's puzzle is 2D or 3D. It reuses the hero cube's stage/spin/
   face classes verbatim, so the face transforms, lighting and the
   reduced-motion rules above carry over; only the container differs, which
   also keeps hero-cube.js — it binds .hero-cube — from ever making the daily
   card draggable inside its own link. */
.daily-cube {
    --hero-cube-size: 36px;
    --hero-cube-half: calc(var(--hero-cube-size) / 2);

    width: calc(var(--hero-cube-size) * 1.45);
    height: calc(var(--hero-cube-size) * 1.45);
    display: grid;
    place-items: center;
    perspective: 300px;
}
/* Tighter internals than the hero faces: at 36px the 7px padding and 2px gap
   would leave ~6px cells and the grid would read as noise. */
.daily-cube .hero-cube__face { padding: 4px; gap: 1.5px; border-radius: 3px; }
.daily-cube .hero-cube__cell { border-radius: 1px; }

@media (max-width: 768px) {
    .hero-cube { --hero-cube-size: clamp(118px, 30vw, 150px); }
}

/* =========================================================================
   PUZZLE GLYPHS — bespoke key art for the card thumbnails
   -------------------------------------------------------------------------
   The cards used to show full application screenshots shrunk into a 120x146
   box, which at that size is grey noise; an independent review called it "the
   single biggest craft giveaway — it looks like a placeholder". These are
   drawn instead of photographed (see src/common/PuzzleGlyph.php): one small
   geometric scene per puzzle, showing what the puzzle IS rather than what its
   toolbar looks like.

   Every colour is a token, so light/dark needs no second asset and no swap —
   which also removes the double-themed image fetch the cards used to cause.
   ========================================================================= */
.puzzle-glyph {
    display: block;
    width: 100%;
    height: 100%;
}

.puzzle-glyph .g-surface { fill: var(--bg-2); }
.puzzle-glyph .g-edge    { stroke: var(--line); }
.puzzle-glyph .g-cell    { fill: var(--bg-1); }
.puzzle-glyph .g-rule    { stroke: var(--line); stroke-width: 1; }

/* Family identity — the same three colours the cards' pills use. */
.puzzle-glyph[data-family="word"]    { --g-hi: var(--cat-word); }
.puzzle-glyph[data-family="logic"]   { --g-hi: var(--cat-logic); }
.puzzle-glyph[data-family="spatial"] { --g-hi: var(--cat-spatial); }

.puzzle-glyph .g-cell--on       { fill: var(--g-hi); }
.puzzle-glyph .g-cell--accent   { fill: var(--accent); }
.puzzle-glyph .g-cell--muted    { fill: var(--line); }
.puzzle-glyph .g-accent-soft    { fill: color-mix(in srgb, var(--accent) 12%, transparent); }

.puzzle-glyph .g-letter,
.puzzle-glyph .g-cipher,
.puzzle-glyph .g-tile-letter,
.puzzle-glyph .g-digit,
.puzzle-glyph .g-num {
    font-family: var(--font-mono);
    font-weight: 700;
    font-size: 11px;
    fill: var(--text-muted);
}

.puzzle-glyph .g-tile-letter   { font-size: 9px; fill: var(--text); }
.puzzle-glyph .g-tile-letter--on,
.puzzle-glyph .g-digit--on     { fill: var(--accent-contrast); }
.puzzle-glyph .g-cipher        { font-size: 10px; fill: var(--accent-contrast); }
.puzzle-glyph .g-num           { font-size: 6.5px; fill: var(--text-muted); }
.puzzle-glyph .g-digit         { font-size: 7.5px; fill: var(--text); }

.puzzle-glyph .g-mark          { stroke: var(--g-hi); }
.puzzle-glyph .g-mark--on      { stroke: var(--accent-contrast); }
.puzzle-glyph .g-cross         { stroke: var(--text-muted); opacity: 0.55; }

/* ── Isometric scenes (the 3D collection) ────────────────────────────────
   The projection is defined in src/common/PuzzleGlyph.php. Each box is three
   polygons and the lighting is the same idea as the hero cube: the top face is
   lit, the +x face is mid, the +y face is in shadow. A hairline on each face
   keeps the edges legible where two faces meet. */
.puzzle-glyph .g-iso-top,
.puzzle-glyph .g-iso-right,
.puzzle-glyph .g-iso-front {
    /* A stronger edge than the flat glyphs use: at 120px the isometric scenes are
       mostly pale paper tones, and without a firm join line the blocks dissolve
       into each other. */
    stroke: color-mix(in srgb, var(--text-strong) 20%, transparent);
    stroke-width: 0.9;
    stroke-linejoin: round;
}

/* Shaded faces carry a little of the family colour, so a scene belongs to Word,
   Logic or Spatial at a glance the way the flat glyphs do, without every block
   becoming a saturated slab. */
.puzzle-glyph .g-iso-top   { fill: color-mix(in srgb, var(--bg-1) 86%, var(--g-hi)); }
.puzzle-glyph .g-iso-right { fill: color-mix(in srgb, var(--bg-2) 92%, var(--g-hi)); }
.puzzle-glyph .g-iso-front { fill: color-mix(in srgb, var(--bg-2) 62%, var(--g-hi)); }

/* A block the scene wants to single out — the placed piece, the found word. */
.puzzle-glyph .g-iso--accent.g-iso-top   { fill: var(--g-hi); }
.puzzle-glyph .g-iso--accent.g-iso-right { fill: color-mix(in srgb, var(--g-hi) 84%, var(--text-strong)); }
.puzzle-glyph .g-iso--accent.g-iso-front { fill: color-mix(in srgb, var(--g-hi) 66%, var(--text-strong)); }

.puzzle-glyph .g-iso-cell {
    fill: color-mix(in srgb, var(--bg-1) 30%, transparent);
    stroke: color-mix(in srgb, var(--text-strong) 10%, transparent);
    stroke-width: 0.6;
}

.puzzle-glyph .g-iso-cell--accent { fill: var(--accent); }

.puzzle-glyph .g-iso-num {
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 700;
    fill: var(--text);
    text-anchor: middle;
}

.puzzle-glyph .g-iso-num--on { fill: var(--accent-contrast); }

/* Small variant for faces that carry a whole word (the word-ladder rungs),
   where the default 11px mono would overlap its neighbours. */
.puzzle-glyph .g-iso-num--sm { font-size: 8px; }

.puzzle-glyph .g-piece         { fill: var(--bg-1); stroke: var(--line); }
.puzzle-glyph .g-piece--accent { fill: var(--g-hi); stroke: var(--g-hi); }

/* Crossword and maze boards are drawn as open/blocked cells rather than lines,
   so the pattern itself carries the structure at small sizes. */
.puzzle-glyph .g-cell--paper   { fill: var(--bg-1); }
.puzzle-glyph .g-cell--block   { fill: var(--line); }
.puzzle-glyph .g-cell--wall    { fill: var(--line); }

.puzzle-glyph .g-rail          { fill: var(--line); }
.puzzle-glyph .g-path          { stroke: var(--g-hi); }
.puzzle-glyph .g-node          { fill: var(--text-muted); }
.puzzle-glyph .g-node--accent  { fill: var(--accent); }

/* =========================================================================
   BUTTON ICONS
   -------------------------------------------------------------------------
   These were OS emoji (🧩 / 🧊). Emoji are drawn by the operating system, so
   they render differently on Windows, macOS, Android and Linux, sit apart from
   the Fraunces/Figtree type system, cannot be themed, and were the clearest
   "not finished" tell in the hero. Both are now stroke icons drawn in the same
   language as the puzzle glyphs: currentColor, 1.5px stroke, square joins, so
   they inherit the button's ink in either theme.
   ========================================================================= */



/* 3D puzzle cards: rendered diorama scenes replace the inline SVG glyphs. */
.card-thumb img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

