/* ══════════════════════════════════════════
   WORDGRID — style.css

   1.  Variables & Reset
   2.  Grid & Cells
   3.  Cell States
   4.  Scene Overlays (base)
   5.  Home Overlay (hero)
   6.  Section Overlays (+ services offerings)
   7.  Transition Overlay
   8.  Scene Indicator (+ found counter)
   9.  Language Selector
   10. Animations
   11. Utility States
   12. No-JS Fallback
   13. Prefers-reduced-motion
══════════════════════════════════════════ */


/* ──────────────────────────────────────────
   0. FONTS (self-hosted, no external dependency)
   Big Shoulders Display = tall, condensed, industrial-signage display
   (wordmark, scene titles) — high impact, on-brand for "signal".
   Martian Mono = instrument-readout body/data — the site is monospace-native.
────────────────────────────────────────── */
@font-face {
    font-family: 'wus-display';
    src: url('./fonts/wus-display.woff2') format('woff2');
    font-weight: 100 900;
    font-display: swap;
}
@font-face {
    font-family: 'wus-mono';
    src: url('./fonts/wus-mono.woff2') format('woff2');
    font-weight: 100 800;
    font-display: swap;
}


/* ──────────────────────────────────────────
   1. VARIABLES & RESET
────────────────────────────────────────── */

/* ══════════════════════════════════════════
   "SEÑAL" — the skin agrees with the mechanic.
   The grid is a field of NOISE you decode into SIGNAL.

   Colours are stored as space-separated RGB triplets so alpha
   composes with rgb(var(--x) / a). The light theme swaps only the
   primitives + a couple of blend modes; every rule reads a token.

   Monochrome with a single accent:
     signal (violet) = brand, active, glows, ripple, the decode flash
     lock            = a decoded word at MAX contrast (white on dark,
                       ink on light) wearing a violet halo. No second hue.
====================════════════════════════ */
:root {
    /* primitives (the only thing the light theme overrides) */
    --bg-rgb:      7 8 10;
    --ink-rgb:     244 246 251;        /* decoded text */
    --noise-rgb:   255 255 255;        /* undecoded letters, via low alpha */
    --signal-rgb:  192 92 255;         /* #c05cff — electrified violet */
    --danger-rgb:  255 90 90;
    --tint-rgb:    255 255 255;        /* surface/border lift — dark on light */

    /* screen = add light over dark; the light theme flips this to multiply */
    --lens-blend:  screen;

    /* semantic tokens (rules read these, never the primitives) */
    --bg:             rgb(var(--bg-rgb));
    --text:           rgb(var(--ink-rgb));
    --text-dim:       rgb(var(--noise-rgb) / 0.07);
    --text-muted:     rgb(var(--noise-rgb) / 0.32);
    --text-hint:      rgb(var(--noise-rgb) / 0.50);

    --accent:         rgb(var(--signal-rgb));
    --accent-glow:    rgb(var(--signal-rgb) / 0.70);
    --accent-soft:    rgb(var(--signal-rgb) / 0.30);
    --accent-dim:     rgb(var(--signal-rgb) / 0.12);
    /* The "lock" (a decoded word) is expressed as max contrast + a violet halo,
       not a second hue: monochrome with a single accent. lock-rgb is the violet
       so glows/ripple/flash all read violet. */
    --lock-rgb:       var(--signal-rgb);
    --lock:           var(--text);
    --lock-glow:      var(--accent-glow);
    --line-soft:      rgb(var(--noise-rgb) / 0.10);

    --font-display:   'wus-display', 'Arial Narrow', sans-serif;
    --font-mono:      'wus-mono', ui-monospace, monospace;

    --cell-size:      60px;
    --cell-size-mob:  36px;

    --fade-dur:       380ms;

    --ease-out-expo:  cubic-bezier(0.16, 1, 0.3, 1);
    --ease-in-expo:   cubic-bezier(0.7, 0, 0.84, 1);

    color-scheme: dark;
}

/* ── Light theme ("Señal" by day) ──
   A sibling, not an afterthought: only the primitives + the lens blend
   change. glow→ink happens automatically because the glow tokens are alpha
   versions of the (now darker) signal/lock, and screen→multiply turns the
   spotlight and scanline from added light into a shadow lens.
   Colours re-checked for WCAG AA on the light base. */
:root[data-theme="light"] {
    --bg-rgb:      242 243 247;
    --ink-rgb:     13 14 18;
    --noise-rgb:   13 14 18;           /* dark noise letters, via low alpha */
    --signal-rgb:  109 40 217;         /* #6d28d9 — violet-700, AA on light */
    --danger-rgb:  200 40 40;
    --tint-rgb:    13 14 18;           /* surface/border lift is now dark */
    --lens-blend:  multiply;
    /* Dark noise at 0.50 only reaches 3.5:1 on light; body copy needs 0.62. */
    --text-hint:   rgb(var(--noise-rgb) / 0.62);
    color-scheme: light;
}

/* Auto: honour the OS preference until the user makes an explicit choice. */
@media (prefers-color-scheme: light) {
    :root:not([data-theme]) {
        --bg-rgb:      242 243 247;
        --ink-rgb:     13 14 18;
        --noise-rgb:   13 14 18;
        --signal-rgb:  109 40 217;
        --danger-rgb:  200 40 40;
        --tint-rgb:    13 14 18;
        --lens-blend:  multiply;
        --text-hint:   rgb(var(--noise-rgb) / 0.62);
        color-scheme: light;
    }
}

*, *::before, *::after {
    margin: 0; padding: 0; box-sizing: border-box;
}

html, body {
    width: 100%; height: 100%;
    overflow: hidden;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-display);
    -webkit-font-smoothing: antialiased;
}


/* ──────────────────────────────────────────
   2. GRID & CELLS
────────────────────────────────────────── */

#grid-container {
    position: fixed;
    inset: 0;
    z-index: 1;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
}

#grid {
    display: grid;
    user-select: none;
    touch-action: none;
}

.cell {
    width: var(--cell-size);
    height: var(--cell-size);
    display: flex;
    justify-content: center;
    align-items: center;
    font-family: var(--font-mono);
    font-size: 1rem;
    font-weight: 400;
    color: var(--text-dim);
    /* crosshair signals the grid is interactive */
    cursor: crosshair;
    transition:
        color       0.18s ease,
        transform   0.18s ease,
        text-shadow 0.18s ease,
        opacity     0.45s var(--ease-out-expo);
}

@media (max-width: 767px) {
    .cell {
        width: var(--cell-size-mob);
        height: var(--cell-size-mob);
        font-size: 0.72rem;
    }
}


/* ──────────────────────────────────────────
   3. CELL STATES
   Specificity order is intentional:
   word-letter < is-hinted < glitch < glitch-fail < selected < found
────────────────────────────────────────── */

/* All word-letters start identical to the noise.
   HintEngine adds .is-hinted after the delay. */
.cell.word-letter {
    color: var(--text-dim);
}

/* HintEngine: reveal word by word with a soft transition */
.cell.word-letter.is-hinted:not(.found) {
    color: var(--accent-soft);
    transition: color 1.1s ease, text-shadow 1.1s ease;
    text-shadow: 0 0 6px rgb(var(--signal-rgb) / 0.15);
}

/* Glitch: lateral micro-movement — feels like data corruption */
.cell.glitch {
    animation: glitchFlash 0.26s ease forwards;
}

/* Reject glitch: a selection that did NOT form a word.
   Same spirit as .glitch but red — "not here". */
.cell.glitch-fail {
    animation: glitchFail 0.34s ease forwards;
}

/* Ripple: reward wave when a word is found (RippleEngine).
   --rip (1 near the word → ~0 at the edge) scales intensity.
   Only the 30% peak is declared: the 0%/100% frames are implicit,
   so hinted/word-letter cells wave FROM and BACK TO their own color.
   Loses to .selected / .found on purpose (those use !important). */
.cell.ripple {
    animation: rippleWave 0.48s var(--ease-out-expo);
}

/* Selected: the CSS class uses !important on transform to always win
   over any inline transform applied via rAF. */
.cell.selected {
    color: var(--text) !important;
    transform: translateY(-2px) scale(1.06) !important;
    text-shadow: 0 0 14px rgb(var(--tint-rgb) / 0.25) !important;
}

/* Found: a decoded word LOCKS at max contrast (white on dark, ink on light)
   with a violet halo — the payoff of the mechanic. Monochrome, one accent. */
.cell.found {
    color: var(--lock) !important;
    transform: none !important;
    animation: foundPulse 2.8s ease-in-out infinite !important;
}

/* Hover on plain cells: minimal feedback before the drag */
.cell:not(.found):not(.selected):hover {
    color: var(--text-muted);
    transform: scale(1.04);
}


/* ──────────────────────────────────────────
   4. SCENE OVERLAYS — base
────────────────────────────────────────── */

.scene-overlay {
    position: fixed;
    inset: 0;
    z-index: 10;
    /* pointer-events:none by default — the grid stays interactive */
    pointer-events: none;
    opacity: 0;
}

.scene-overlay.is-visible {
    opacity: 1;
}

.scene-overlay.fade-in {
    animation: fadeIn var(--fade-dur) var(--ease-out-expo) forwards;
}

/* A hidden overlay must be fully INERT, not just transparent.
   opacity:0 still hit-tests, and some descendants opt back into clicks
   (.offering, .cta, home .word-item) — so an off-screen SERVICES could
   sit above WORK (equal z-index, later in the DOM) and steal a click meant
   for a project. visibility:hidden is inherited: it removes the overlay and
   every descendant from hit-testing until it is shown again.
   Excludes .fade-in so the entrance animation (which runs before .is-visible
   is added) stays visible. */
.scene-overlay:not(.is-visible):not(.fade-in) {
    visibility: hidden;
}


/* ──────────────────────────────────────────
   5. HOME OVERLAY — hero
   Minimal on purpose: name, tagline, nav, hint.
   Never captures clicks except on interactive items,
   so the grid stays reachable everywhere else.
────────────────────────────────────────── */

#home-overlay {
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 10;
    pointer-events: none !important;
}

.hero {
    text-align: center;
    position: relative;
    padding: 1rem;
}

/* Soft vignette behind the hero so the content reads cleanly over the grid.
   Decorative only → never captures clicks. */
.hero::before {
    content: '';
    position: absolute;
    inset: -10% -14%;
    z-index: -1;
    pointer-events: none;
    background: radial-gradient(ellipse at center,
        rgb(var(--bg-rgb) / 0.92) 32%,
        rgb(var(--bg-rgb) / 0)    74%);
}

/* Big Shoulders is condensed, so it can run much larger than a wide grotesque
   without overflowing — pushed up for real presence. */
.hero-name {
    font-weight: 800;
    font-size: clamp(5rem, 15vw, 13rem);
    line-height: 0.86;
    letter-spacing: 0.005em;
    /* Semi-transparent: the grid is sensed behind the name */
    color: rgb(var(--ink-rgb) / 0.82);
}

/* 30ch broke the longer tagline into three ragged lines; 46ch lands it on two. */
.hero-tagline {
    margin-top: 0.9rem;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 300;
    color: var(--text-hint);
    letter-spacing: 0.03em;
    line-height: 1.6;
    max-width: 46ch;
    margin-inline: auto;
    text-wrap: balance;
}

/* ── Nav (PROJECTS / INFO / CONTACT / SERVICES) ── */
.word-list {
    list-style: none;
    margin-top: 1.6rem;
    display: flex;
    flex-direction: column;
    gap: 0.28rem;
    align-items: center;
}

/* A <button>, so it takes focus: dragging the word out of the grid is the
   fun path, this is the one that works with a keyboard. */
.word-item {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    font-family: var(--font-mono);
    font-size: 0.66rem;
    font-weight: 400;
    color: var(--text-hint);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    transition: color 0.35s ease, text-shadow 0.35s ease;
}
.word-item:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 4px;
}

/* Home nav words are clickable shortcuts (the rest of the overlay is click-through) */
#home-overlay .word-item {
    pointer-events: auto;
    cursor: pointer;
}
#home-overlay .word-item:hover { color: var(--text); }

.word-item.is-found {
    color: var(--lock);
    text-shadow: 0 0 8px var(--lock-glow);
}

/* hero-hint breathes slowly to invite interaction without nagging. */
.hero-hint {
    margin-top: 1.4rem;
    font-family: var(--font-mono);
    font-size: 0.68rem;
    font-weight: 300;
    color: var(--text-hint);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    animation: hintBreathe 3.5s ease-in-out infinite;
    animation-delay: 1.8s;
}

/* Blinking terminal caret after the hint. */
.caret {
    display: inline-block;
    width: 0.5em;
    height: 1em;
    margin-left: 0.5em;
    background: var(--lock);
    vertical-align: -0.15em;
    animation: caretBlink 1.1s steps(1) infinite;
}
@keyframes caretBlink { 50% { opacity: 0; } }

/* ── Hero boot entrance (added once by UICtrl.playHeroIntro) ──
   The name decodes from noise (JS); tagline / nav / hint stagger in.
   fill "both" keeps them hidden during the delay. */
.hero.intro .hero-tagline {
    animation: revealUp 0.55s var(--ease-out-expo) both;
    animation-delay: 0.50s;
}
.hero.intro .word-list {
    animation: revealUp 0.55s var(--ease-out-expo) both;
    animation-delay: 0.68s;
}
.hero.intro .hero-hint {
    /* revealUp once, then the ambient breathe takes over */
    animation:
        revealUp    0.55s var(--ease-out-expo) 0.85s both,
        hintBreathe 3.5s  ease-in-out          1.8s  infinite;
}


/* ──────────────────────────────────────────
   6. SECTION OVERLAYS
   The full overlay is invisible to clicks.
   Only .section-content captures events.
   The grid stays interactive for finding BACK.
────────────────────────────────────────── */

.section-overlay {
    z-index: 15;
    display: flex;
    align-items: flex-end;
    padding: clamp(2rem, 6vw, 5rem);
}

.section-overlay.is-visible .section-content {
    pointer-events: auto;
}

/* The page itself cannot scroll (html/body are overflow:hidden) and content
   grows upward from the bottom edge, so a long section needs its own scroll
   region on a short viewport.

   That region is .section-scroll, NOT .section-content: a scroll container
   clips BOTH axes (setting overflow-y makes overflow-x compute to auto), and
   the display titles are wider than their column on purpose. Scrolling the
   whole column cut "Hablemos" in half. The title stays outside the scroller. */
.section-content {
    width: 100%;
    max-width: 600px;
    max-height: 100%;
    display: flex;
    flex-direction: column;
    min-height: 0;
}

.section-scroll {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* room for the ::marker of .howto, which the old scroller clipped */
    padding-left: 3px;
    margin-left: -3px;
    scrollbar-width: thin;
    scrollbar-color: var(--accent-dim) transparent;
}
.section-scroll::-webkit-scrollbar       { width: 3px; }
.section-scroll::-webkit-scrollbar-track { background: transparent; }
.section-scroll::-webkit-scrollbar-thumb { background: var(--accent-dim); }

/* ── Staggered entrance when a section is revealed ──
   animation-fill-mode: both keeps elements hidden during the delay
   (backwards) and visible at the end (forwards). */
/* The title does NOT use revealUp — its entrance is the decode (UICtrl). */
.section-overlay.fade-in .section-body {
    animation: revealUp 0.38s var(--ease-out-expo) both;
    animation-delay: 150ms;
}
.section-overlay.fade-in .section-offerings {
    animation: revealUp 0.38s var(--ease-out-expo) both;
    animation-delay: 210ms;
}
.section-overlay.fade-in .availability {
    animation: revealUp 0.38s var(--ease-out-expo) both;
    animation-delay: 240ms;
}
.section-overlay.fade-in .cta {
    animation: revealUp 0.38s var(--ease-out-expo) both;
    animation-delay: 270ms;
}
.section-overlay.fade-in .section-word-list {
    animation: revealUp 0.32s var(--ease-out-expo) both;
    animation-delay: 320ms;
}

/* ── Per-scene entrance signatures ──
   Each scene enters with its own motion so they stop reading as clones.
   Timing/easing taken from proven text-animation specs (soft-blur, spring,
   mask-reveal, focus-blur). Reduced-motion resolves them instantly (§13). */

/* PROJECTS: the preview panel "tunes in" with a brief signal channel-split. */
.section-overlay[data-scene="work"].fade-in .project-panel {
    animation: tuneIn 0.55s var(--ease-out-expo) both;
    animation-delay: 0.22s;
}
.section-overlay[data-scene="work"].fade-in .project-list .project {
    animation: revealUp 0.42s var(--ease-out-expo) both;
}
.section-overlay[data-scene="work"].fade-in .project-list li:nth-child(1) .project { animation-delay: 0.16s; }
.section-overlay[data-scene="work"].fade-in .project-list li:nth-child(2) .project { animation-delay: 0.24s; }
.section-overlay[data-scene="work"].fade-in .project-list li:nth-child(3) .project { animation-delay: 0.32s; }

/* INFO: rows reveal top-down like a scanline passing over a document. */
.section-overlay[data-scene="about"].fade-in .section-body { animation: fadeIn 0.4s ease both; }
.section-overlay[data-scene="about"].fade-in .howto-rail li,
.section-overlay[data-scene="about"].fade-in .spec div {
    animation: maskRevealUp 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.section-overlay[data-scene="about"].fade-in .howto-rail li:nth-child(1) { animation-delay: 0.20s; }
.section-overlay[data-scene="about"].fade-in .howto-rail li:nth-child(2) { animation-delay: 0.29s; }
.section-overlay[data-scene="about"].fade-in .howto-rail li:nth-child(3) { animation-delay: 0.38s; }
.section-overlay[data-scene="about"].fade-in .howto-rail li:nth-child(4) { animation-delay: 0.47s; }
.section-overlay[data-scene="about"].fade-in .spec div:nth-child(1) { animation-delay: 0.24s; }
.section-overlay[data-scene="about"].fade-in .spec div:nth-child(2) { animation-delay: 0.31s; }
.section-overlay[data-scene="about"].fade-in .spec div:nth-child(3) { animation-delay: 0.38s; }
.section-overlay[data-scene="about"].fade-in .spec div:nth-child(4) { animation-delay: 0.45s; }
.section-overlay[data-scene="about"].fade-in .spec div:nth-child(5) { animation-delay: 0.52s; }
.section-overlay[data-scene="about"].fade-in .spec div:nth-child(6) { animation-delay: 0.59s; }

/* CONTACT: the money moment — the whole centred block resolves from blur. */
.section-overlay[data-scene="contact"].fade-in .section-body {
    animation: focusBlurResolve 0.76s cubic-bezier(0.22, 1, 0.36, 1) both;
    animation-delay: 0.12s;
}

/* SERVICES: the three cards spring in from noise, one after another. */
.section-overlay[data-scene="services"].fade-in .section-offerings { animation: fadeIn 0.3s ease both; }
.section-overlay[data-scene="services"].fade-in .offering {
    animation: springScaleIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
.section-overlay[data-scene="services"].fade-in .offering:nth-child(1) { animation-delay: 0.18s; }
.section-overlay[data-scene="services"].fade-in .offering:nth-child(2) { animation-delay: 0.27s; }
.section-overlay[data-scene="services"].fade-in .offering:nth-child(3) { animation-delay: 0.36s; }

/* Condensed display runs larger without eating horizontal room; still capped
   so the content below stays in view. */
.section-title {
    font-weight: 800;
    font-size: clamp(3.2rem, 8.5vw, 7rem);
    line-height: 0.88;
    letter-spacing: 0.005em;
    margin-bottom: 1.4rem;
}

/* Title / hero decode (UICtrl._decodeTitle) */
.dc {
    color: var(--accent-soft);
    transition: color 0.28s ease, text-shadow 0.45s ease;
}
.dc.lit   { color: var(--text); }
/* Each letter locks bright the instant it decodes: a brief violet
   channel-split (the signal snapping into register), then settles. */
.dc.flash {
    color: var(--lock);
    text-shadow:
        -1px 0 0 rgb(var(--lock-rgb) / 0.6),
         1px 0 0 rgb(var(--signal-rgb) / 0.6),
         0 0 18px var(--lock-glow);
}

/* The hero name settles back to its own (semi-transparent) color. */
.hero-name .dc.lit { color: inherit; }

/* 1.9 was too loose for mono at this size — the paragraph dissolved into
   separate lines instead of reading as a block. */
.section-body {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 300;
    line-height: 1.7;
    color: var(--text-hint);
    max-width: 46ch;
    margin-bottom: 1.9rem;
}

/* Inline links inside translated copy (kept out of the copy itself). */
.section-body a {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px solid transparent;
    transition: border-color 0.25s ease;
}
.section-body a:hover { border-bottom-color: var(--accent); }

/* "How I build" steps (INFO section) */
.howto-title {
    margin-top: 1.4rem;
    color: var(--text);
    opacity: 0.85;
    letter-spacing: 0.04em;
}
.howto-note { margin-top: 1.2rem; opacity: 0.75; }

/* ══════════════════════════════════════════
   PER-SCENE STRUCTURE
   Four scenes used to be one screen with different text in it, which is what
   made the site feel templated. Each one now has its own shape. The shared
   move — a copy column with a visual anchor beside it — is the one both live
   projects already use; the colours stay this site's own.
══════════════════════════════════════════ */

/* PROJECTS and SERVICES take the full width. */
.section-overlay[data-scene="work"] .section-content,
.section-overlay[data-scene="services"] .section-content { max-width: min(1200px, 100%); }
.section-overlay[data-scene="about"]  .section-content { max-width: min(900px, 100%); }

/* CONTACT is the short one, so it gets the gesture: the only centred,
   full-screen moment, echoing the home scene. */
.section-overlay[data-scene="contact"] {
    align-items: center;
    justify-content: center;
}
.section-overlay[data-scene="contact"] .section-content {
    max-width: 460px;
    text-align: center;
}
.section-overlay[data-scene="contact"] .section-body { max-width: none; }
.section-overlay[data-scene="contact"] .contact-form { margin-inline: auto; text-align: left; }
.section-overlay[data-scene="contact"] .section-word-list { align-items: center; }

/* Scenes whose body is a layout, not a paragraph. */
.section-overlay[data-scene="work"] .section-body,
.section-overlay[data-scene="about"] .section-body { max-width: none; }

/* ── PROJECTS: list left, live preview right ── */
.work-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    gap: clamp(1.4rem, 4vw, 3.2rem);
    align-items: start;
}

.project-list { list-style: none; display: flex; flex-direction: column; }

/* One row per project: index, name, stack. The prose lives in the panel. */
.project {
    appearance: none;
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: 0;
    border-left: 1px solid transparent;
    padding: 0.7rem 0 0.7rem 0.9rem;
    cursor: pointer;
    transition: border-color 0.25s ease, background 0.25s ease;
}
.project:hover  { border-left-color: var(--accent-soft); }
.project.is-active {
    border-left-color: var(--accent);
    background: linear-gradient(90deg, var(--accent-dim), transparent 70%);
}
.project:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.project-name {
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.9rem;
    font-weight: 500;
    letter-spacing: 0.02em;
}
/* Says out loud that a project isn't finished. */
.project-status {
    margin-left: 0.6rem;
    padding: 0.14em 0.5em;
    border: 1px solid var(--accent-soft);
    border-radius: 2px;
    font-size: 0.58rem;
    font-weight: 400;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--accent);
    vertical-align: middle;
    white-space: nowrap;
}

.project-stack {
    display: block;          /* it is a <span> inside the button, so this matters */
    margin-top: 0.3rem;
    font-family: var(--font-mono);
    font-size: 0.62rem;
    font-weight: 300;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-hint);
    opacity: 0.6;
}

/* ── The preview panel (right column) ── */
/* The panel must fit the viewport on its own: it is the tallest thing in the
   scene, and if it overflows the links at the bottom disappear. */
.project-panel {
    display: flex;
    flex-direction: column;
    max-height: 100%;
    min-height: 0;
    border: 1px solid var(--line-soft);
    border-top: 2px solid var(--accent);
    background: rgb(var(--tint-rgb) / 0.02);
    padding: 0;
    overflow: hidden;
}

/* The media well gives up height first when space is tight. */
.panel-media {
    position: relative;
    flex: 0 1 auto;
    min-height: 0;
    aspect-ratio: 16 / 9;
    background: #050505;
    border-bottom: 1px solid rgb(var(--tint-rgb) / 0.08);
    overflow: hidden;
}
.panel-media img,
.panel-media video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: top center;
    display: block;
}
/* Nothing to show yet (wusman.com is the page you are on). */
.panel-media.is-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-mono);
    font-size: 0.62rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-hint);
    opacity: 0.5;
    background: repeating-linear-gradient(-45deg,
        rgb(var(--tint-rgb) / 0.02) 0 8px, transparent 8px 16px);
}

.panel-copy { flex: none; padding: 1rem 1.1rem 1.15rem; }

.panel-desc {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    font-weight: 300;
    line-height: 1.65;
    color: var(--text-hint);
}

.project-links { margin-top: 0.9rem; font-size: 0.72rem; }
.project-links a {
    color: var(--accent);
    text-decoration: none;
    letter-spacing: 0.06em;
    border-bottom: 1px solid var(--accent-soft);
    padding-bottom: 1px;
    transition: border-color 0.25s ease, text-shadow 0.25s ease;
}
.project-links a:hover {
    border-color: var(--accent);
    text-shadow: 0 0 8px var(--accent-glow);
}
.project-links .sep { opacity: 0.25; margin: 0 0.6rem; }

.project-links a svg {
    width: 0.95em;
    height: 0.95em;
    vertical-align: -0.15em;
    margin-right: 0.4em;
    fill: currentColor;
}

/* ── INFO: a spec sheet, not a paragraph ──
   Numbered steps on a hairline rail, with a data column beside them. */
.info-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.6fr) minmax(0, 0.9fr);
    gap: clamp(1.4rem, 4vw, 3rem);
    align-items: start;
}

.howto-rail {
    margin: 0.8rem 0 0;
    /* Wide enough to seat the step number BETWEEN the rail and the text.
       Hanging it outside the border let the scroll container clip it. */
    padding: 0 0 0 2.5rem;
    list-style: none;
    counter-reset: step;
    border-left: 1px solid var(--line-soft);
}
.howto-rail li {
    position: relative;
    counter-increment: step;
    margin-bottom: 0.9rem;
    color: var(--text-hint);
}
.howto-rail li:last-child { margin-bottom: 0; }
.howto-rail li::before {
    content: counter(step, decimal-leading-zero);
    position: absolute;
    left: -2.1rem;
    top: 0.15em;
    font-size: 0.62rem;
    letter-spacing: 0.08em;
    color: var(--accent);
}

/* Data column: label over value, mono, tabular. */
.spec { font-family: var(--font-mono); font-size: 0.7rem; }
.spec div { padding: 0.55rem 0; border-top: 1px solid var(--line-soft, rgb(var(--tint-rgb) / 0.10)); }
.spec div:first-child { border-top: 0; }
.spec dt {
    font-size: 0.58rem;
    font-weight: 400;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--text-hint);
    opacity: 0.65;
}
.spec dd {
    margin: 0.25rem 0 0;
    color: var(--text);
    font-weight: 300;
    font-variant-numeric: tabular-nums;
}

/* ── Offerings inside the SERVICES section ──
   Statements, not buttons: each one carries a link to live proof.
   A row of three, so the scene reads in one glance instead of scrolling. */
.offerings {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(0.9rem, 2.5vw, 1.6rem);
}

/* Availability and CTA share a baseline under the row. */
.services-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1.2rem 2rem;
    margin-top: 1.8rem;
}

.section-offerings {
    align-items: flex-start;
    margin: 0 0 1.8rem;
    pointer-events: none;   /* the overlay rule re-enables it via .section-content */
}

/* Each offering is a card now, so the three read as siblings across the row. */
.offering {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    font-family: var(--font-mono);
    text-align: left;
    color: inherit;
    pointer-events: auto;
    padding: 1rem 1.1rem 1.1rem;
    border: 1px solid var(--line-soft, rgb(var(--tint-rgb) / 0.10));
    border-top: 2px solid var(--accent-soft);
    background: rgb(var(--tint-rgb) / 0.02);
    transition: border-top-color 0.25s ease, background 0.25s ease;
}
.offering:hover {
    border-top-color: var(--accent);
    background: rgb(var(--tint-rgb) / 0.04);
}

.offering-title {
    font-size: 0.8rem;
    font-weight: 500;
    color: var(--text);
    letter-spacing: 0.02em;
}
/* --text-muted (0.32 alpha) only reaches 2.77:1 on this background, which
   fails WCAG AA for copy. Real sentences use --text-hint (5.32:1); the muted
   token stays for decorative states only. */
.offering-desc {
    display: block;
    margin-top: 0.2rem;
    font-size: 0.7rem;
    font-weight: 300;
    color: var(--text-hint);
    line-height: 1.5;
}
/* The proof link is the only interactive part of an offering. */
.offering-proof {
    align-self: flex-start;
    margin-top: auto;
    padding-top: 0.7rem;
    font-size: 0.66rem;
    font-weight: 400;
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px solid transparent;
    transition: border-color 0.25s ease;
}
.offering-proof:hover      { border-bottom-color: var(--accent); }
.offering-proof:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Availability line — the honest note under the offerings. */
.availability {
    flex: 1 1 22ch;
    max-width: 46ch;
    margin: 0;
    font-family: var(--font-mono);
    font-size: 0.68rem;
    font-weight: 300;
    line-height: 1.6;
    color: var(--text-hint);
}

/* ── WhatsApp CTA (SERVICES) ── */
.cta {
    display: inline-block;
    flex: none;
    align-self: flex-start;
    padding: 0.7rem 1.5rem;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--accent);
    border: 1px solid var(--accent-soft);
    border-radius: 2px;
    background: var(--accent-dim);
    pointer-events: auto;
    cursor: pointer;
    transition: border-color 0.25s ease, color 0.25s ease,
                box-shadow 0.25s ease, background 0.25s ease;
}
.cta:hover {
    color: var(--text);
    border-color: var(--accent);
    background: var(--accent-soft);
    box-shadow: 0 0 18px rgb(var(--signal-rgb) / 0.25);
}
.cta:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

/* BACK / VOLVER: visible and clearly pulsing — the user must be able to find it. */
.section-word-list { align-items: flex-start; }

.section-word-list .word-item {
    font-size: 0.68rem;
    font-weight: 500;
    color: rgb(var(--signal-rgb) / 0.85);
    letter-spacing: 0.2em;
}

.section-word-list .word-item.is-found {
    color: var(--lock);
    text-shadow: 0 0 10px var(--lock-glow);
    animation: none;
}

.section-word-list .word-item::before {
    content: '↩ ';
    color: var(--accent);
    opacity: 0.7;
}


/* ──────────────────────────────────────────
   7. TRANSITION OVERLAY
────────────────────────────────────────── */

#transition-overlay {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    justify-content: center;
    align-items: center;
    pointer-events: none;
    opacity: 0;
    background: var(--bg);
    /* Opacity only — cheap and smooth. No animated backdrop-filter. */
    transition: opacity 0.28s ease;
}

#transition-overlay.is-active {
    opacity: 1;
}

#transition-word {
    display: block;
    font-family: var(--font-display);
    font-weight: 800;
    /* Large final size: rasterized at this scale; the animation only scales
       UP TO 1 → the text stays crisp. */
    font-size: clamp(3.5rem, 13vw, 11rem);
    color: var(--accent);
    letter-spacing: -0.02em;
    /* Crisp, not a soft blob: one tight glow, not a 120px halo. */
    text-shadow: 0 0 24px rgb(var(--signal-rgb) / 0.35);
    white-space: nowrap;
    opacity: 0;
    will-change: transform, opacity;
    transform: translateZ(0);
}


/* ──────────────────────────────────────────
   8. SYSTEM CHROME — frame, scanline, corner readouts
   Frames the grid as a signal field. All inert (pointer-events:none);
   every readout is real state (grid size, found count, scene, language).
────────────────────────────────────────── */

/* Thin bezel with two lit corner ticks — the "system" edge. */
#sys-frame {
    position: fixed;
    inset: 12px;
    z-index: 4;
    pointer-events: none;
    border: 1px solid rgb(var(--noise-rgb) / 0.07);
}
#sys-frame::before,
#sys-frame::after {
    content: '';
    position: absolute;
    width: 11px;
    height: 11px;
    border: 1px solid rgb(var(--signal-rgb) / 0.55);
}
#sys-frame::before { top: -1px;    left: -1px;  border-right: 0; border-bottom: 0; }
#sys-frame::after  { bottom: -1px; right: -1px; border-left: 0;  border-top: 0; }

/* A slow band of signal light travelling down the field. */
#scanline {
    position: fixed;
    left: 0; right: 0;
    height: 26vh;
    top: -26vh;
    z-index: 4;
    pointer-events: none;
    mix-blend-mode: var(--lens-blend);
    background: linear-gradient(180deg,
        transparent,
        rgb(var(--signal-rgb) / 0.045) 46%,
        rgb(var(--lock-rgb) / 0.05) 54%,
        transparent);
    animation: scanSweep 9s linear infinite;
}
@keyframes scanSweep { to { top: 100%; } }

/* Corner readouts share one type treatment. */
.sys-readout {
    position: fixed;
    z-index: 200;
    pointer-events: none;
    font-family: var(--font-mono);
    font-size: 0.56rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: rgb(var(--noise-rgb) / 0.32);
    display: flex;
    align-items: center;
    gap: 0.55rem;
}
#sys-loc  { bottom: 1.5rem; left: 1.9rem; }
#scene-indicator { bottom: 1.5rem; right: 1.9rem; align-items: baseline; gap: 0.9rem; }

/* A live violet dot — the "decoding" pulse of the system. */
#sys-loc .dot {
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 8px var(--accent-glow);
    animation: dotPulse 2.4s ease-in-out infinite;
}
@keyframes dotPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* Found counter: real "n/total" of decoded words. */
#found-counter {
    font-size: 0.56rem;
    color: var(--accent);
    letter-spacing: 0.2em;
}
#scene-label {
    font-size: 0.56rem;
    color: rgb(var(--noise-rgb) / 0.24);
    letter-spacing: 0.28em;
    text-transform: uppercase;
}


/* ──────────────────────────────────────────
   9. LANGUAGE SELECTOR — top left
────────────────────────────────────────── */

#lang-indicator {
    position: fixed;
    top: 1.4rem;
    left: 1.8rem;
    z-index: 200;
    display: flex;
    gap: 0.55rem;
}

.lang-opt {
    font-family: var(--font-mono);
    font-size: 0.58rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    /* was 0.22 alpha (1.89:1) — unreadable. The active language is still
       distinguished by hue and glow, not by being the only legible one. */
    color: var(--text-hint);
    background: none;
    border: none;
    padding: 0.1rem 0.2rem;
    cursor: pointer;
    transition: color 0.3s ease, text-shadow 0.3s ease;
}
.lang-opt:hover { color: var(--text); }
.lang-opt.is-active {
    color: var(--accent);
    text-shadow: 0 0 8px var(--accent-glow);
}
.lang-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Theme toggle — sits under the language selector, top-left. */
#theme-toggle {
    position: fixed;
    top: 2.7rem;
    left: 1.8rem;
    z-index: 200;
    padding: 0.1rem 0.2rem;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    line-height: 1;
    color: var(--text-hint);
    background: none;
    border: none;
    cursor: pointer;
    transition: color 0.3s ease, text-shadow 0.3s ease;
}
#theme-toggle:hover { color: var(--accent); text-shadow: 0 0 8px var(--accent-glow); }
#theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }


/* ──────────────────────────────────────────
   10. ANIMATIONS
────────────────────────────────────────── */

@keyframes fadeIn {
    0%   { opacity: 0; }
    26%  { opacity: 1; }
    100% { opacity: 1; }
}

@keyframes revealUp {
    from { opacity: 0; transform: translateY(24px); filter: blur(7px); }
    to   { opacity: 1; transform: translateY(0);    filter: blur(0); }
}

@keyframes foundPulse {
    0%, 100% { text-shadow: 0 0 8px rgb(var(--lock-rgb) / 0.5); }
    50%       { text-shadow: 0 0 20px var(--lock-glow), 0 0 40px rgb(var(--lock-rgb) / 0.3); }
}

/* Glitch: abrupt lateral movement — intentionally unsmoothed */
@keyframes glitchFlash {
    0%   { color: var(--text-dim);           transform: none; }
    22%  { color: rgb(var(--signal-rgb) / 0.55);    transform: translateX(1.5px); }
    44%  { color: rgb(var(--signal-rgb) / 0.18);    transform: translateX(-1px); }
    66%  { color: rgb(var(--signal-rgb) / 0.48);    transform: translateX(0.8px); }
    88%  { color: rgb(var(--signal-rgb) / 0.22);    transform: translateX(0); }
    100% { color: var(--text-dim);           transform: none; }
}

/* Reject glitch: the red twin of glitchFlash */
@keyframes glitchFail {
    0%   { color: var(--text-dim);          transform: none; }
    20%  { color: rgb(var(--danger-rgb) / 0.85);  transform: translateX(2px); }
    40%  { color: rgb(var(--danger-rgb) / 0.35);  transform: translateX(-2px); }
    60%  { color: rgb(var(--danger-rgb) / 0.70);  transform: translateX(1.5px); }
    80%  { color: rgb(var(--danger-rgb) / 0.28);  transform: translateX(0); }
    100% { color: var(--text-dim);          transform: none; }
}

@keyframes hintBreathe {
    0%, 100% { opacity: 1; }
    50%       { opacity: 0.3; }
}

/* Ripple wave: implicit from/to = the cell's current styles.
   Everything scales with --rip so the wave fades with distance. */
@keyframes rippleWave {
    30% {
        color: rgb(var(--lock-rgb) / calc(0.15 + 0.55 * var(--rip, 1)));
        transform: translateY(calc(-4px * var(--rip, 1)))
                   scale(calc(1 + 0.10 * var(--rip, 1)));
        text-shadow: 0 0 calc(10px * var(--rip, 1)) var(--lock-glow);
    }
}

/* ── Per-scene entrance keyframes (Etapa 3) ── */

/* Row reveal, top-down (INFO). mask-reveal-up spec. */
@keyframes maskRevealUp {
    from { opacity: 0; transform: translateY(30px); filter: blur(6px); }
    to   { opacity: 1; transform: translateY(0);    filter: blur(0); }
}

/* Card pop with overshoot (SERVICES). spring-scale-in spec. */
@keyframes springScaleIn {
    from { opacity: 0; transform: scale(0.7); }
    to   { opacity: 1; transform: scale(1); }
}

/* Focus pull from blur (CONTACT). focus-blur-resolve spec. */
@keyframes focusBlurResolve {
    from { opacity: 0; transform: translateY(14px) scale(1.01); filter: blur(14px); }
    to   { opacity: 1; transform: translateY(0)    scale(1);    filter: blur(0); }
}

/* Panel tunes in with a signal channel-split that settles (PROJECTS).
   drop-shadow splits the rendered panel (video included) into bright/violet
   ghosts that converge — the "locking onto signal" moment. */
@keyframes tuneIn {
    0% {
        opacity: 0;
        transform: translateX(-3px);
        filter: drop-shadow(-5px 0 0 var(--lock)) drop-shadow(5px 0 0 var(--accent));
    }
    45% { opacity: 1; }
    70% {
        transform: translateX(2px);
        filter: drop-shadow(2px 0 0 var(--lock)) drop-shadow(-2px 0 0 var(--accent));
    }
    100% {
        opacity: 1;
        transform: none;
        filter: none;
    }
}


/* ──────────────────────────────────────────
   11. UTILITY STATES
────────────────────────────────────────── */

#grid-container.is-locked { pointer-events: none; }

/* Grid dimmed when a section is active. */
#grid-container.is-dimmed .cell:not(.found) {
    opacity: 0.28;
}

/* In a section the grid is a calm backdrop: hovering a letter behind the
   content must not scale or brighten it (that was the "things move" jitter).
   Word-letters (BACK/VOLVER) keep their own accent tint. */
#grid-container.is-dimmed .cell:hover { transform: none; }
#grid-container.is-dimmed .cell:not(.word-letter):hover { color: var(--text-dim); }

/* In sections, grid words (BACK / VOLVER) must stay findable:
   we tint them with the accent instead of hiding them. */
#grid-container.is-dimmed .cell.word-letter:not(.found) {
    opacity: 1;
    color: var(--accent-soft);
    text-shadow: 0 0 6px rgb(var(--signal-rgb) / 0.15);
}

/* Compact the hero stack on mobile so it doesn't overflow. */
@media (max-width: 767px) {
    .hero-tagline   { font-size: 0.74rem; }
    .word-list      { margin-top: 1.2rem; }
    .offerings      { gap: 0.65rem; }
    .offering-title { font-size: 0.76rem; }
    .offering-desc  { font-size: 0.64rem; }

    /* The language selector is fixed at the top-left; without this the
       section title slid underneath it and the two collided. */
    .section-overlay { padding-top: 3.6rem; }

    /* Every two-column layout collapses to one. */
    .work-grid,
    .info-layout,
    .offerings { grid-template-columns: minmax(0, 1fr); }

    /* On one column the preview leads, then the list. */
    .work-grid       { gap: 1.2rem; }
    .project-panel   { order: -1; }
    .section-overlay[data-scene="contact"] .contact-form { max-width: 100%; }
}

/* Between phone and laptop the panel still fits, but the offerings row of
   three gets too narrow to read. */
@media (min-width: 768px) and (max-width: 1023px) {
    .offerings { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}


/* ──────────────────────────────────────────
   11b. CURSOR GLOW (ProximityEngine) + CONTACT FORM
────────────────────────────────────────── */

/* The light that follows the pointer */
#cursor-glow {
    position: fixed;
    top: 0; left: 0;
    width: 460px; height: 460px;
    border-radius: 50%;
    z-index: 5;                 /* over the grid (1), under the overlays (10+) */
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.4s ease;
    mix-blend-mode: var(--lens-blend);  /* screen on dark, multiply on light */
    will-change: transform, opacity;
    background: radial-gradient(circle at center,
        rgb(var(--signal-rgb) / 0.20) 0%,
        rgb(var(--signal-rgb) / 0.08) 38%,
        rgb(var(--signal-rgb) / 0)    70%);
}

/* Contact form (Web3Forms) */
/* Tight gap so a label binds visually to ITS input; the space between
   fields comes from the margin on each label after the first. */
.contact-form {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    max-width: 360px;
    margin-bottom: 2rem;
}
.cf-label:not(:first-child) { margin-top: 0.75rem; }
.cf-submit { margin-top: 1rem; }
.contact-form .hidden { display: none; }

.cf-input {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--text);
    background: rgb(var(--tint-rgb) / 0.03);
    border: 1px solid rgb(var(--tint-rgb) / 0.36);
    border-radius: 2px;
    padding: 0.65rem 0.8rem;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
    resize: vertical;
}
/* Small mono caps above each field — reads as terminal, not as a form. */
.cf-label {
    font-family: var(--font-mono);
    font-size: 0.6rem;
    font-weight: 400;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--text-hint);
}
.cf-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent);
}

.cf-submit {
    align-self: flex-start;
    margin-top: 0.2rem;
    padding: 0.65rem 1.4rem;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--accent);
    background: var(--accent-dim);
    border: 1px solid var(--accent-soft);
    border-radius: 2px;
    cursor: pointer;
    transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.cf-submit:hover:not(:disabled) {
    color: var(--text);
    border-color: var(--accent);
    background: var(--accent-soft);
}
.cf-submit:disabled { opacity: 0.5; cursor: default; }

.cf-status {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    min-height: 1em;
}
.cf-status.is-ok  { color: var(--accent); }
.cf-status.is-err { color: rgba(255, 90, 90, 0.9); }

/* ──────────────────────────────────────────
   12. NO-JS FALLBACK
────────────────────────────────────────── */

.noscript {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 0.8rem;
    text-align: center;
    padding: 2rem;
    background: var(--bg);
}
.noscript h1 { font-size: clamp(3rem, 10vw, 6rem); font-weight: 800; }
.noscript p  { font-family: var(--font-mono); font-size: 0.85rem; color: var(--text-hint); }
.noscript a  { color: var(--accent); text-decoration: none; }


/* ──────────────────────────────────────────
   13. PREFERS-REDUCED-MOTION
   Accessibility for users sensitive to motion
   (vestibular disorders, epilepsy).
────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }

    .hero-hint { animation: none; opacity: 1; }

    /* System chrome: no travelling light, no pulsing — states stay resolved. */
    #scanline { display: none; }
    #sys-loc .dot { animation: none; opacity: 1; }
    .caret { animation: none; }

    /* The hero intro never runs (playHeroIntro bails early),
       but if the class slipped in, everything stays visible. */
    .hero.intro .hero-tagline,
    .hero.intro .word-list,
    .hero.intro .hero-hint {
        animation: none;
        opacity: 1;
        transform: none;
    }

    .section-overlay.fade-in .section-title,
    .section-overlay.fade-in .section-body,
    .section-overlay.fade-in .section-offerings,
    .section-overlay.fade-in .availability,
    .section-overlay.fade-in .cta,
    .section-overlay.fade-in .section-word-list,
    /* Per-scene entrance signatures resolve instantly too. */
    .section-overlay.fade-in .project-panel,
    .section-overlay.fade-in .project-list .project,
    .section-overlay.fade-in .howto-rail li,
    .section-overlay.fade-in .spec div,
    .section-overlay.fade-in .offering {
        animation: none;
        opacity: 1;
        transform: none;
        filter: none;
    }

    .cell.found  { animation: none; text-shadow: 0 0 10px var(--accent-glow); }
    .cell.glitch { animation: none; }
    .cell.ripple { animation: none; }   /* engine bails early, this is belt & suspenders */
    /* .glitch-fail intentionally NOT disabled here: the global 0.01ms rule
       still fires animationend so the class is cleaned up (and the flash
       is invisible to reduced-motion users). */

    .section-word-list .word-item { animation: none; opacity: 1; }
}