/* Colours + type come from the brand tokens (/assets/css/9ba413445369.css, source
   ~/projects/wickedwifey.com/brand/brand.css), linked before this file (journal (ggg)). */
/* Wicked Wifey - Atmospheric Goth Theme */
html, body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    background-color: #000;
    font-family: sans-serif;
    overflow: hidden; /* Default lock for landing page */
    overscroll-behavior: none; /* no rubber-band / pull-to-refresh on mobile */
    -webkit-text-size-adjust: 100%; /* stop iOS bumping font sizes in landscape */
}

/* Enable scrolling for hotwife.html */
html.scroll-enabled {
    overflow-y: auto;
    height: auto;
}

html.scroll-enabled body {
    overflow-y: visible;
    height: auto;
    display: block;
}

#background-canvas, #cloud-canvas {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
}

#cloud-canvas { z-index: 5; pointer-events: none; }

.content {

    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    /* dvh tracks the *dynamic* viewport so the mobile address bar showing/hiding
       doesn't jump the layout; vh is the fallback for older browsers. */
    height: 100vh;
    height: 100dvh;
    /* Start her BELOW the moon so she stands on the water instead of floating in the sky:
       moon bottom edge = app.js drawMoon() marginTop clamp(28px,4.5%h,64px) + disc
       clamp(31px,7.7% of min(w,h),70px) — mirrored here 1:1 — plus a small gap.
       Keep in sync with drawMoon() if the moon moves. */
    /* + the SIGILS sky lift (2026-10-28): she sits lower so the sigil has the sky above her
       head (app.js layoutSigil sizes it to the room left above .logo-container). */
    padding-top: calc(clamp(28px, 4.5vh, 64px) + clamp(31px, 7.7vmin, 70px) + 2vh + clamp(36px, 7vh, 96px));
    padding-top: calc(clamp(28px, 4.5dvh, 64px) + clamp(31px, 7.7vmin, 70px) + 2dvh + clamp(36px, 7dvh, 96px));
    /* respect notches / rounded corners on modern phones (viewport-fit=cover) */
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
    z-index: 10;
    width: 100%;
    box-sizing: border-box;
    pointer-events: auto; /* Allow clicks to pass to background if needed */
}

/* Allow interaction and scrolling logic to work properly on scrollable pages */
html.scroll-enabled .content {

    height: auto;
    min-height: 100vh;
    min-height: 100dvh;
    padding: 15vh 0;
    justify-content: flex-start;
    pointer-events: auto; /* Capture touch events for scrolling */
}

/* -- Hero (2026-11-05 yy) ----------------------------------------------------
   The box keeps the old 600x1800 (1:3) aspect: the scene, horizon, sky wheel and section box are
   all laid out in % of it. Her clips are 480x884 master frames (herochar packages: soles on row
   807, tiara to sole 729 px), placed so she stands exactly where the old stills stood (tiara
   to sole = 99% of the box, soles on its bottom edge, centred): a frame is 2.4411 box px per
   master px, so 195.29% of the box wide, 119.88% tall, at left -47.64% / top -9.943%. The look
   (grade, night tone, edge light-wrap, halo) is baked into the pixels by tools/hero-bake.py;
   no CSS filters or masks on her. The reflecting pool hangs below the box (top:100%, 55%). */
.logo-container {
    position: relative;
    aspect-ratio: 600 / 1800;
    height: 58dvh;
    max-height: 62dvh;
    max-width: 92vw;
    display: flex;
    justify-content: center;
    align-items: center;
}

.logo {
    position: absolute;
    inset: 0;
    /* opacity transitions are set from JS (fast to the devil on a plain strike, slow back) */
    transition: opacity 3.2s ease-in-out, filter 3.2s ease-in-out;
}
#logo-wicked { opacity: 0; }
.logo-img, .hero-cv {
    position: absolute; display: block;
    left: -47.64%; width: 195.29%; top: -9.943%; height: 119.88%;
    pointer-events: none;
}
/* the video canvas shows once its first frame is drawn (.vid-on), and the hub still under it
   goes (it is the same frame, so the change can't be seen) */
.hero-cv { opacity: 0; }
.logo-container.vid-on .hero-cv { opacity: 1; }
.logo-container.vid-on #logo-goddess .logo-img { opacity: 0; }

/* --- Reflecting pool: the persona mirrored at her feet (top:100% of the hero), fading downward.
   app.js crossfades it with the hero (.pm main layer, .ps the swap). The webps are only the
   hubs' lower 406 master rows (= 55% of the box), stored PRE-FLIPPED (feet at the top), so the
   pool box is 55% of the hero's height and needs no scaleY(-1).
   Restored 2026-10-27; pre-flipped + pinned at the feet 2026-10-28. ----------------------- */
.hero-reflection {
    position: absolute; top: 100%; left: 0; width: 100%; height: 55%;
    pointer-events: none; z-index: -1;
    /* Refracted-water look: a gentle skew plus a slow skew/scaleX shimmer. Transform-only
       (GPU-composited, no repaint) — deliberately NOT an SVG displacement filter, which is
       what caused the old hero flicker. Pivots at the TOP (her soles) so the contact point
       never slides sideways — the old centre pivot shifted it ~10px and made her float. */
    transform: skewX(-3deg);
    transform-origin: 50% 0;
    animation: waterSway 7.5s ease-in-out infinite alternate;
    will-change: transform;
    /* Barely-there, otherworldly echo (was a fairly readable mirror): drop the whole
       reflection's opacity, fade the gradient sooner, and blur/darken more so she
       reads as a dreamlike shimmer at her feet rather than a literal reflection.
       The container opacity multiplies BOTH persona reflections, preserving the
       crossfade while making the pool faint. */
    opacity: 0.74;
    /* Same fade curve as before (0.5 -> 0.34 -> 0) but eased in over the first 12% — the
       crisp .hero-reflection-sharp copy carries the contact point there. */
    -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.5) 12%, rgba(0,0,0,0.34) 55%, rgba(0,0,0,0) 100%);
            mask-image: linear-gradient(to bottom, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.5) 12%, rgba(0,0,0,0.34) 55%, rgba(0,0,0,0) 100%);
    filter: blur(3.2px) brightness(0.98);
}
.hero-reflection-sharp {
    filter: none;
    -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0) 18%);
            mask-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0) 18%);
}
.hr-img {
    position: absolute; top: 0; height: 100%; left: -47.64%; width: 195.29%;
    transition: opacity 3.2s ease-in-out;
}
/* .pm = the main layer's pool (hub still, then the video's flipped rows), .ps = the swap's */
.hero-pool, .logo-container.vid-on .pool-still { visibility: hidden; }
.logo-container.vid-on .hero-pool { visibility: visible; }

/* Gentle water refraction: the skew angle (and a hair of horizontal spread) wavers
   about the -3deg resting lean. transform-origin (top center) keeps her feet pinned
   to the surface while the deeper end sways. Keeps scaleY(-1) in every frame so the
   mirror flip is never lost. */
/* Same shimmer for the pre-flipped pool (no scaleY) — pivots at the feet. */
@keyframes waterSway {
    0%   { transform: skewX(-3deg)   scaleX(1.000); }
    50%  { transform: skewX(-1.6deg) scaleX(1.015); }
    100% { transform: skewX(-3.6deg) scaleX(1.006); }
}
/* Mirrored variant, still used by .sig-reflection (its SVG copy is flipped in CSS). */
@keyframes waterRefract {
    0%   { transform: scaleY(-1) skewX(-3deg)   scaleX(1.000); }
    50%  { transform: scaleY(-1) skewX(-1.6deg) scaleX(1.015); }
    100% { transform: scaleY(-1) skewX(-3.6deg) scaleX(1.006); }
}
/* Positional water drift (~3-5px) is NOT a fixed keyframe — app.js drives the `translate`
   property of .hero-reflection + .sig-reflection each frame from the STORM WIND (horizontal
   tracks wind strength/direction; gentle independent vertical bob). `translate` composes with
   the skew `transform` above. Under prefers-reduced-motion app.js leaves translate at 0. */
.hr-img.pm { opacity: 0.5; }
.hr-img.ps { opacity: 0; }

.signature-container {

    display: flex;
    flex-direction: column;
    align-items: center;
    /* The new hero is full-length (feet/platforms + gown hem at the bottom edge), so
       only a hair of pull — the SVG's own top padding keeps the ink clear of her feet.
       (Was clamp(-7.2vh,-5.4vh,-1.2rem) for the old hero, whose lower ~12% faded out.) */
    margin-top: clamp(-2vh, -1.2vh, -0.3rem);
    margin-bottom: clamp(0.4rem, 2vh, 2.4rem);
    pointer-events: auto;
}

.signature-top {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* --- Signature + heart: SVG drawn like real handwriting -----------------
   Both signature.png and wickedheart.png are raster line-art, so we derived a
   centreline pen-path from each (skeletonisation) and use it as an animated
   SVG *mask* over the PNG: the neon ink is uncovered stroke-by-stroke in true
   pen order (butt caps — round caps leave stray dots). The heart is timed to
   draw right after the signature finishes. There is NO leading "nib" — the ink
   simply forms. Timing is baked per-stroke (--l / --delay / --dur) so the pen
   moves at constant speed along the arc length.
   2026-11-05 (Drew): ONE mark now. "Wicked Wifey" in La Boutier + the devil heart, both re-drawn
   as the signature's own neon tube by tools/sig-build.py (same material, same red, the heart sized
   off the name's cap height and set after the "y"). Its box keeps the old 717-unit height, baseline
   and x-height scale but is 1274 wide to hold the heart, so every width below is × --sigk (= 1274/1200)
   and the water words (sized off the old 1200-wide box, --sigw) still sit right. */
.sig-svg {
    --sigk: 1.0617;
    width: calc(clamp(226px, 54.6vw, 378px) * var(--sigk));
    height: auto;
    aspect-ratio: 1274 / 717;
    overflow: visible;          /* let the neon glow spill past the box */
    display: block;
}

.sig-ink {
    /* much brighter cherry red: raise opacity, lift + saturate, nudge the hue
       toward cherry, and glow in cherry-pink. */
    opacity: 0.6;
    filter: brightness(1.4) saturate(1.65) hue-rotate(-6deg)
            drop-shadow(0 0 9px rgba(255, 20, 70, 0.85));
}

/* Resting / no-JS fallback: strokes fully drawn (mask solid) so a failed or
   disabled script can NEVER leave the signature/heart hidden. */
.sig-stroke { stroke-dasharray: var(--l); stroke-dashoffset: 0; }

/* JS-armed: wind the strokes back (hidden), ready to be written. */
html.sig-js .sig-stroke { stroke-dashoffset: var(--l); }

/* Trigger (body.writing added by the inline <head> arm script on load). */
html.sig-js .writing .sig-stroke {
    animation: sigDraw var(--dur) linear var(--delay) forwards;
}

@keyframes sigDraw { to { stroke-dashoffset: 0; } }

/* --- Signature reflection: a mirrored, heavily-blurred/frosted, dimmed copy of the
   signature + heart that falls onto the watery reflection — a cloudy neon sign
   glimmering dimly on the pool. Shares the pool's skew + waterRefract shimmer so it
   refracts identically. Renamed mask ids (smmr/hmmr) in the markup avoid id clashes;
   the strokes still carry .sig-stroke so the reflection writes itself in sync. ------- */
.sig-reflection {
    display: flex;
    align-items: center;
    justify-content: center;
    /* Tuck the mirror up so it meets the signature near its baseline — the SVGs carry
       generous empty padding, so a chunk of negative margin closes the gap. (Eyeball-tune.) */
    margin-top: clamp(-7rem, -13vh, -3rem);
    transform-origin: center;                 /* flip in place (see .hero-reflection note) */
    transform: scaleY(-1) skewX(-3deg);       /* same base lean as the pool */
    animation: waterRefract 7.5s ease-in-out infinite alternate;
    /* frosted + dimmed + transparent, a touch brighter. The directional SVG blur
       (#sigSmudge) is a bit more horizontal than vertical → a watery horizontal smudge. */
    filter: url(#sigSmudge) brightness(1.02);
    opacity: 0.6;
    -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.45) 45%, rgba(0,0,0,0) 90%);
            mask-image: linear-gradient(to bottom, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.45) 45%, rgba(0,0,0,0) 90%);
    pointer-events: none;
    z-index: -1;                               /* behind the crisp signature, on the water */
}

/* ===================== SCENE (2026-10-28) ===============================================
   She stands ON a lake (tools/scene-gen.py, seed 7). Geometry, all in % of the hero box
   (.logo-container, her feet = its bottom edge):
     .scene       full viewport width, feet line up, 80% of her height tall
     .sl-l/.sl-r  side forests (2000x1250 masks): shoreline rises from the box bottom at the
                  outer screen edge to the horizon at the inner end — a perspective "chute"
     .sl-ridge    tileable far treeline whose base IS the horizon: 27% of her height above her
                  feet (= 33.75% of .scene), 12% of .scene tall
     .scene-refl  the reflections (mirrored per column about each shoreline by the generator),
                  twice as tall, sharing .scene's top edge; faded, blurred, swaying, drifting
   Near-black at rest. Lightning (app.js on .logo-container): --aura = the lavender cloud
   build-up before the strike, --flash = the white sheet light, --bolt = its side (0 L..1 R).
   .glow = light falling into the canopy (soft noisy raster), .rim = thin top-edge band.
   Opacity-only animation, no animated filters, no mix-blend-mode.
   PATH BACK = delete this block + the SCENE markup/JS (or restore the pre-shore tarball). */
.scene, .scene-refl {
    position: absolute; left: calc(50% - 50vw); width: 100vw;
    z-index: -3; pointer-events: none;
}
.scene { bottom: 0; height: 80%; }
.sl {
    position: absolute; bottom: 0;
    background: #06070a;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;   mask-size: 100% 100%;
}
.sl > i {
    position: absolute; inset: 0;
    -webkit-mask-repeat: inherit; mask-repeat: inherit;
    -webkit-mask-size: inherit;   mask-size: inherit;
}
.sl .glow { background: rgb(168, 158, 228); }            /* dusty lavender */
/* depth (e, reversed f): toward their far (inner) ends the forests dissolve into BLACK — at
   night distance fades to darkness, not haze (grey haze read as a wall behind her). The
   river runs on into nothing. (No blur: a filter on a masked element is applied BEFORE the
   mask, so the silhouette edge stays hard anyway.) */
.sl-l .haze { background: linear-gradient(to right, transparent 50%, #000 94%); }
.sl-r .haze { background: linear-gradient(to left,  transparent 50%, #000 94%); }
.sl .rim  { background: linear-gradient(to bottom, rgb(232, 230, 255), rgba(214, 210, 255, 0.45)); }
/* (f) the ridge is cut away across the river (30-70% of the width, soft edges): the water
   runs on behind her into black instead of ending at a wall — the ridge survives only where
   it meets the side forests. Second mask layer intersected with the treeline shape. */
.sl-ridge {
    left: 0; right: 0; bottom: 33.75%; height: 12%;
    background: #07080d;                                    /* a touch of distance haze */
    -webkit-mask-image: url(/assets/img/2bc3c96e3d52.svg), linear-gradient(to right, #000 0%, #000 30%, transparent 40%, transparent 60%, #000 70%, #000 100%); mask-image: url(/assets/img/2bc3c96e3d52.svg), linear-gradient(to right, #000 0%, #000 30%, transparent 40%, transparent 60%, #000 70%, #000 100%);
    -webkit-mask-repeat: repeat-x, no-repeat; mask-repeat: repeat-x, no-repeat;
    -webkit-mask-size: auto 100%, 100% 100%;  mask-size: auto 100%, 100% 100%;
    -webkit-mask-composite: source-in; mask-composite: intersect;
}
.sl-l, .sl-r { height: 100%; aspect-ratio: 2000 / 1250; }
.sl-l { left: 0;  -webkit-mask-image: url(/assets/img/306ddb71c372.svg); mask-image: url(/assets/img/306ddb71c372.svg); }
.sl-r { right: 0; -webkit-mask-image: url(/assets/img/0ba50a93acce.svg); mask-image: url(/assets/img/0ba50a93acce.svg); }
.sl-ridge .glow { -webkit-mask-image: url(/assets/img/d71e221d9c6e.webp); mask-image: url(/assets/img/d71e221d9c6e.webp);
                  opacity: calc((var(--aura, 0) * 0.3 + var(--flash, 0) * 0.45)); }
.sl-ridge .rim  { -webkit-mask-image: url(/assets/img/4fc052819284.svg); mask-image: url(/assets/img/4fc052819284.svg);
                  opacity: calc(var(--flash, 0) * 0.55); }
.sl-l .glow { -webkit-mask-image: url(/assets/img/e6f7393fd9d3.webp); mask-image: url(/assets/img/e6f7393fd9d3.webp);
              opacity: calc((var(--aura, 0) * 0.35 + var(--flash, 0) * 0.7) * (1.1 - var(--bolt, 0.5) * 0.9)); }
.sl-l .rim  { -webkit-mask-image: url(/assets/img/8d04d714e6db.svg); mask-image: url(/assets/img/8d04d714e6db.svg);
              opacity: calc(var(--flash, 0) * (1.1 - var(--bolt, 0.5) * 0.9)); }
.sl-r .glow { -webkit-mask-image: url(/assets/img/83b0f7fe8bef.webp); mask-image: url(/assets/img/83b0f7fe8bef.webp);
              opacity: calc((var(--aura, 0) * 0.35 + var(--flash, 0) * 0.7) * (0.2 + var(--bolt, 0.5) * 0.9)); }
.sl-r .rim  { -webkit-mask-image: url(/assets/img/0a6c0131c967.svg); mask-image: url(/assets/img/0a6c0131c967.svg);
              opacity: calc(var(--flash, 0) * (0.2 + var(--bolt, 0.5) * 0.9)); }
/* reflections: wrapper spans 2x .scene from the same top edge (horizon at 33.125% of it) */
.scene-refl {
    bottom: -80%; height: 160%;
    opacity: 0.55;
    filter: blur(2px);
    transform-origin: 50% 33.125%;
    animation: waterSway 9s ease-in-out infinite alternate;
    -webkit-mask-image: linear-gradient(to bottom, transparent 30%, rgba(0,0,0,0.8) 33.5%, rgba(0,0,0,0.35) 50%, transparent 72%);
            mask-image: linear-gradient(to bottom, transparent 30%, rgba(0,0,0,0.8) 33.5%, rgba(0,0,0,0.35) 50%, transparent 72%);
}
.scene-refl .sl-l, .scene-refl .sl-r { aspect-ratio: 2000 / 2500; }
.scene-refl .sl-l { -webkit-mask-image: url(/assets/img/2b50b71ca8dd.svg); mask-image: url(/assets/img/2b50b71ca8dd.svg); }
.scene-refl .sl-r { -webkit-mask-image: url(/assets/img/a5cca2e56c8b.svg); mask-image: url(/assets/img/a5cca2e56c8b.svg); }
.scene-refl .sl-ridge { bottom: auto; top: 27.125%; height: 12%;
    -webkit-mask-image: url(/assets/img/c79029da52bc.svg), linear-gradient(to right, #000 0%, #000 30%, transparent 40%, transparent 60%, #000 70%, #000 100%); mask-image: url(/assets/img/c79029da52bc.svg), linear-gradient(to right, #000 0%, #000 30%, transparent 40%, transparent 60%, #000 70%, #000 100%); }
.scene-refl .sl-l .rim { -webkit-mask-image: url(/assets/img/bbad1fd959c8.svg); mask-image: url(/assets/img/bbad1fd959c8.svg); }
.scene-refl .sl-r .rim { -webkit-mask-image: url(/assets/img/74e066ecc647.svg); mask-image: url(/assets/img/74e066ecc647.svg); }
.scene-refl .sl-ridge .rim { -webkit-mask-image: url(/assets/img/6bf1c5aba825.svg); mask-image: url(/assets/img/6bf1c5aba825.svg); }
/* Portrait phones: fixed-aspect side forests would cover the water behind her — pin each
   to 52vw and let the masks squeeze horizontally. */
@media (orientation: portrait) {
    .sl-l, .sl-r, .scene-refl .sl-l, .scene-refl .sl-r { aspect-ratio: auto; width: 52vw; }
}
/* Persona-coloured light pooling on the water at her feet (the deity's contact "shadow").
   app.js setPersona() crossfades these with the hero (goddess .5 / wicked .72). */
.water-glow {
    position: absolute; left: 50%; bottom: 0; width: 170%; height: 8%;
    translate: -50% 50%;
    z-index: -1; pointer-events: none; opacity: 0;
    transition: opacity 3.2s ease-in-out;
}
.glow-goddess { opacity: 0.4;
    background: radial-gradient(closest-side, rgba(160, 188, 255, 0.55), rgba(160, 188, 255, 0.16) 55%, transparent); }
.glow-wicked  {
    background: radial-gradient(closest-side, rgba(255, 36, 70, 0.36), rgba(255, 36, 70, 0.1) 55%, transparent); }
@media (prefers-reduced-motion: reduce) {
    .scene-refl { animation: none; }
}
/* =================== end SCENE ==================================================== */

/* --- Orientation & size adaptation --------------------------------------
   Landscape (phones held sideways + desktops): the viewport is wide and SHORT.
   Both the hero and the signature/heart are driven by viewport HEIGHT here
   so the hero stays dominant and the signature scales down with it (previously
   the hero was height-driven => tiny while the signature was vw-driven => huge). */
@media (orientation: landscape) {
    .logo-container {
        height: 63dvh;
        max-height: 67dvh;
        max-width: 52vw;
    }
    .sig-svg    { width: calc(clamp(142px, 35.7dvh, 307px) * var(--sigk)); }
}

/* Short landscape (phones on their side): tighten everything so it fits
   without scrolling. */
@media (orientation: landscape) and (max-height: 560px) {
    .logo-container {
        height: 54dvh;
        max-height: 58dvh;
    }
    .signature-container { margin-top: 0.2rem; }
    .sig-svg    { width: calc(clamp(113px, 30.5dvh, 210px) * var(--sigk)); }
}

/* Large desktops: don't let the portrait hero grow unbounded on tall windows. */
@media (orientation: landscape) and (min-width: 1200px) {
    .logo-container { max-width: 30vw; }
    .sig-svg { width: calc(clamp(224px, 25dvh, 332px) * var(--sigk)); }
}

/* Hot Wife Page Styles */
.frosted-box {

    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border: 1px solid #ff0028;
    border-radius: 15px;
    padding: 2.5rem;
    width: 75vw; /* Slightly wider on mobile */
    max-width: 800px;
    color: #ffffff;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8), 0 0 15px rgba(255, 0, 40, 0.2);
    line-height: 1.6;
    font-size: 1.1rem;
    text-align: left;
    pointer-events: auto;
    margin: 0 auto;
}

.frosted-box p {
    margin-bottom: 1.5rem;
}

.frosted-box p:last-child {
    margin-bottom: 0;
}

/* Mobile specific tweaks */
@media (max-width: 600px) {
    .frosted-box {
        padding: 1.5rem;
        width: 80vw;
        font-size: 1rem;
    }
}

/* ===================== Site menu (added 2026-09-24) ===================== */
/* Trigger: clean pulsing hamburger, top-right, safe-area aware, morphs to X. */
.menu-trigger{
  position:fixed; z-index:60;
  top:calc(env(safe-area-inset-top,0px) + 14px);
  right:calc(env(safe-area-inset-right,0px) + 16px);
  width:48px; height:48px; padding:0;
  display:flex; align-items:center; justify-content:center;
  background:transparent; border:0; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.mt-bars{ position:relative; width:26px; height:16px; }
.mt-bars i{
  position:absolute; left:0; right:0; height:2px; border-radius:2px;
  background:#ff2c3f; box-shadow:0 0 6px rgba(255,20,45,.55);
  transition:transform .28s ease, opacity .2s ease;
}
.mt-bars i:nth-child(1){ top:0 } .mt-bars i:nth-child(2){ top:7px } .mt-bars i:nth-child(3){ top:14px }
.menu-trigger:hover .mt-bars i{ background:#ff5566; box-shadow:0 0 10px rgba(255,20,45,.9) }
@media (prefers-reduced-motion:no-preference){
  .menu-trigger{ animation:mtPulse 3.2s ease-in-out infinite }
}
@keyframes mtPulse{ 0%,100%{ filter:none } 50%{ filter:drop-shadow(0 0 7px rgba(255,20,45,.75)) } }
/* one-time discoverability hint */
.menu-hint{
  position:fixed; z-index:60; top:calc(env(safe-area-inset-top,0px) + 24px);
  right:calc(env(safe-area-inset-right,0px) + 72px);
  font:italic 400 .8rem/1 var(--ww-serif); letter-spacing:.18em; text-transform:uppercase;
  color:rgba(255,255,255,.55); pointer-events:none; opacity:0; transition:opacity .8s ease;
}
.menu-hint.show{ opacity:1 }

/* Open state: hamburger -> X */
body.menu-open .mt-bars i:nth-child(1){ transform:translateY(7px) rotate(45deg) }
body.menu-open .mt-bars i:nth-child(2){ opacity:0 }
body.menu-open .mt-bars i:nth-child(3){ transform:translateY(-7px) rotate(-45deg) }
body.menu-open .menu-trigger{ animation:none }

/* Overlay */
.site-menu{
  position:fixed; inset:0; z-index:50;
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .35s ease, visibility 0s linear .35s;
}
body.menu-open .site-menu{ opacity:1; visibility:visible; pointer-events:auto; transition:opacity .35s ease }
.sm-veil{
  position:absolute; inset:0; background:radial-gradient(120% 120% at 70% 30%,rgba(20,0,4,.5),rgba(0,0,0,.92));
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
}
.sm-inner{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  padding:max(6dvh,env(safe-area-inset-top)) 6vw max(6dvh,env(safe-area-inset-bottom));
}
/* mobile-first: sigil = faint centred backdrop, list = centred stack */
.sm-sigil{
  position:absolute; top:50%; left:50%; translate:-50% -50%;
  height:86dvh; width:auto; max-width:80vw; object-fit:contain;
  opacity:.16; filter:drop-shadow(0 0 30px rgba(255,20,45,.4)); pointer-events:none;
}
.sm-list{
  position:relative; list-style:none; margin:0; padding:0;
  display:flex; flex-direction:column; align-items:center; gap:clamp(10px,2.4dvh,22px);
  max-height:100dvh;
}
.sm-list a{ display:block; line-height:0; outline:none; }
.sm-list img{
  width:auto; height:auto; max-height:clamp(26px,5vh,50px); max-width:86vw; object-fit:contain;
  /* The PNGs are now recoloured to the signature/heart base red rgb(228,63,68) and
     carry the EXACT same treatment as .sig-ink, so the menu type is an identical
     cherry-red match to the signature + heart. (Base opacity a touch higher than the
     signature's 0.6 purely for nav legibility over the dark veil — same hue.) */
  opacity:0.8;
  filter:brightness(1.4) saturate(1.65) hue-rotate(-6deg)
         drop-shadow(0 0 9px rgba(255,20,70,.85));
  transition:transform .18s ease, filter .18s ease, opacity .18s ease;
}
.sm-list a:hover img, .sm-list a:focus-visible img{
  transform:scale(1.05); opacity:1;
  filter:brightness(1.55) saturate(1.7) hue-rotate(-6deg)
         drop-shadow(0 0 14px rgba(255,20,45,.9));
}
/* staggered reveal */
.sm-list li{ opacity:0; transform:translateY(12px); transition:opacity .32s ease, transform .32s ease; }
body.menu-open .sm-list li{ opacity:1; transform:none; }
body.menu-open .sm-list li:nth-child(1){ transition-delay:.06s }
body.menu-open .sm-list li:nth-child(2){ transition-delay:.12s }
body.menu-open .sm-list li:nth-child(3){ transition-delay:.18s }
body.menu-open .sm-list li:nth-child(4){ transition-delay:.24s }
body.menu-open .sm-list li:nth-child(5){ transition-delay:.30s }
body.menu-open .sm-list li:nth-child(6){ transition-delay:.36s }
body.menu-open .sm-list li:nth-child(7){ transition-delay:.42s }

/* desktop: sigil = solid spine on the left, list mirrored on the right.
   Push both to symmetric side insets (space-between) so the centred hero is
   framed evenly and the list never drifts over it. */
@media (min-width:820px){
  .sm-inner{ justify-content:space-between; gap:0;
             padding-left:clamp(4vw,9vw,190px); padding-right:clamp(4vw,9vw,190px); }
  .sm-sigil{ position:relative; top:auto; left:auto; translate:none; height:74dvh; max-width:24vw; opacity:.9; }
  .sm-list{ align-items:flex-start; gap:clamp(14px,3vh,30px); }
  .sm-list img{ max-height:clamp(30px,6vh,64px); max-width:30vw; }
}
@media (prefers-reduced-motion:reduce){
  .sm-list li{ transition:none; opacity:1; transform:none; }
  .menu-trigger{ animation:none; }
}

/* Users who prefer reduced motion: freeze the water shimmer. The signature reveal,
   canvas storm, wind drift + click ripple honour this in their own scripts. */
@media (prefers-reduced-motion: reduce) {
    /* Freeze the water shimmer but keep the static refraction lean. */
    .hero-reflection { animation: none; transform: skewX(-3deg); }
    .sig-reflection  { animation: none; transform: scaleY(-1) skewX(-3deg); }
}

/* =================== VAPOR + GRAIN (2026-10-28 e) ================================
   .vapor-canvas  app.js draws the goddess's mist "carpet" (thickest at her feet, thin
                  wisps outward), the near-lake mist (feet -> viewer) and, while the devil
                  stands there, the boil + steam that outlives her. IN FRONT of her ankles;
                  full width, 90% of her height above the soles to 35% below, top faded in-canvas. (Far-lake mist,
                  which is BEHIND her in depth, is drawn on the sky canvas instead.)
   .grain         one shared, slowly shifting film grain over the frame (two-tone tile,
                  normal alpha only). Oversized by 64px so the shifts never show an edge.
   PATH BACK = delete this block + the VAPOR/GRAIN markup + the VAPOR JS (or restore the
   pre-vapor tarball). */
.vapor-canvas {
    position: absolute; left: calc(50% - 50vw); width: 100vw;
    bottom: -35%; height: 125%;                 /* (f) up to 90% of her height above the soles */
    pointer-events: none; z-index: 1;
    /* (f) the top fades out softly so a growing steam puff never meets a hard canvas edge —
       done INSIDE the canvas (app.js, destination-out), not a CSS mask: a mask over a
       canvas that repaints every frame was part of the choppiness (g) */
}
.grain {
    position: fixed; inset: -64px; z-index: 45; pointer-events: none;
    background: url(/assets/img/2507a8d0bb24.png) repeat;
    opacity: 0.2;
    animation: grainShift 0.6s steps(1) infinite;
}
@keyframes grainShift {
    0%   { transform: translate(0, 0); }
    20%  { transform: translate(-37px, 21px); }
    40%  { transform: translate(18px, -45px); }
    60%  { transform: translate(-52px, -12px); }
    80%  { transform: translate(29px, 40px); }
}
@media (prefers-reduced-motion: reduce) { .grain { animation: none; } }
/* =================== end VAPOR + GRAIN ============================================ */


/* ===================== NAV phase 1 (2026-10-28): page mode + hidden nav ====================
   Design: ~/projects/SIGILS.md. Content pages are <html class="page">: the same sky, lake and
   forests (the .logo-container keeps its size so the scene and horizon line up exactly with
   the landing page), but no hero / reflection / signature, and app.js PAGE stops the
   lightning. She is replaced by a smoky, semi-transparent box. The soft fringe is a wide
   box-shadow, NOT a backdrop-filter, and there is no mix-blend-mode (../CLAUDE.md §4 white bug).
   Body text sits on a near-opaque backing for contrast over the moving clouds.
   PATH BACK = delete this block. */
.page-box {
    position: fixed; left: 50%; top: 50%; translate: -50% -50%;
    z-index: 20;
    width: min(720px, 88vw);
    color: rgba(var(--ww-mist-rgb), 0.86);
    font-family: var(--ww-serif);
    line-height: 1.65; font-size: clamp(0.98rem, 2.2vmin, 1.1rem);
    text-align: center;
}
/* the smoke: two static blurred layers bleeding past the box (no hard edge anywhere). They
   live on the non-scrolling shell, because a scroll container would clip them. */
.page-box::before, .page-box::after {
    content: ""; position: absolute; z-index: -1; pointer-events: none;
}
.page-box::before {
    inset: -18px; border-radius: 48px;
    background: radial-gradient(120% 110% at 50% 40%, rgba(15, 13, 21, 0.9), rgba(11, 10, 15, 0.84) 70%);
    filter: blur(22px);
}
.page-box::after {
    inset: -70px -90px; border-radius: 50%;
    background: radial-gradient(closest-side, rgba(12, 11, 17, 0.5), rgba(12, 11, 17, 0.18) 70%, transparent);
    filter: blur(30px);
}
.pb-inner {
    box-sizing: border-box;
    max-height: 76vh; max-height: 76dvh;
    overflow: auto; overscroll-behavior: contain;
    padding: clamp(1.4rem, 4.5vmin, 2.8rem);
    scrollbar-width: thin; scrollbar-color: rgba(255, 45, 78, 0.35) transparent;
}
/* section names are set in the brand display face, Grenze Gotisch (journal (ggg)) */
.page-box h1 {
    margin: 0 0 0.2em; font-family: var(--ww-display); font-weight: 500; font-style: normal;
    font-size: clamp(2.1rem, 6.6vmin, 3.3rem); line-height: 1.1; letter-spacing: 0.01em;
    color: var(--ww-cherry-lit); text-shadow: 0 0 14px rgba(255, 45, 78, 0.45);
}
.page-box .pb-sub {
    margin: 0 0 1.6em; font-size: 0.78em; letter-spacing: 0.22em; text-transform: uppercase;
    color: rgba(var(--ww-mist-rgb), 0.45);
}
.page-box .pb-note { margin: 0 auto 1.4em; max-width: 34em; }
.page-box .pb-sigil { margin: 0 0 1.8em; font-style: italic; font-size: 0.86em; color: rgba(var(--ww-mist-rgb), 0.42); }
.page-box .pb-home {
    font-style: italic; color: rgba(255, 70, 92, 0.85); text-decoration: none;
    border-bottom: 1px solid rgba(255, 70, 92, 0.35); padding-bottom: 2px;
}
.page-box .pb-home:hover, .page-box .pb-home:focus-visible { color: var(--ww-cherry-glow); border-color: rgba(255, 92, 116, 0.75); outline: none; }
/* Aurum (gold.html): the retired kiss QR (toolbox/kiss-qr/). INVERTED: the modules are
   transparent cut-outs in neon red, so it only scans on its own dark card, never over the storm.
   Blended into the smoke (journal (aa)): a translucent card, a hairline frame, a soft glow and a
   touch of transparency (it still decodes; the card stays dark over the box's backing). The box
   is a flex column and the QR is the one thing that shrinks, so the fitted box (app.js fitBox)
   only scrolls once the QR is at its 100px floor (it still scans there).
   Tap = BIP21 to a wallet; the address selects whole on one tap for copying. */
.page-box .pb-qr {
    display: block; margin: 0 auto 0.8em;
    flex: 0 1 min(200px, 52vw); min-height: 100px; aspect-ratio: 1; width: auto;
    box-sizing: border-box; padding: 9px; border-radius: 16px;
    background: rgba(8, 6, 11, 0.72);
    border: 1px solid rgba(255, 40, 70, 0.38);
    box-shadow: 0 0 18px rgba(255, 0, 40, 0.26), 0 0 46px rgba(255, 0, 40, 0.1), 0 0 28px 8px rgba(8, 6, 11, 0.45);
    opacity: 0.86;
    transition: opacity 0.3s ease, box-shadow 0.3s ease;
}
.page-box .pb-qr:hover, .page-box .pb-qr:focus-visible {
    opacity: 1; outline: none;
    box-shadow: 0 0 24px rgba(255, 0, 40, 0.45), 0 0 52px rgba(255, 0, 40, 0.16), 0 0 28px 8px rgba(8, 6, 11, 0.45);
}
.pb-inner:has(> .pb-qr) { display: flex; flex-direction: column; align-items: center; }
.pb-inner:has(> .pb-qr) > * { flex-shrink: 0; max-width: 100%; }
.page-box .pb-inner > .pb-qr { flex-shrink: 1; }
.page-box .pb-qr img { display: block; width: 100%; height: 100%; border-radius: 8px; filter: saturate(0.88); }
.page-box .pb-addr {
    margin: 0 auto 0.4em; max-width: 100%; overflow-wrap: anywhere; user-select: all; -webkit-user-select: all;
    font-family: ui-monospace, 'DejaVu Sans Mono', monospace; font-size: min(0.8em, 2.9vw);   /* one line on phones */
    color: rgba(255, 110, 130, 0.85);
}
.page-box .pb-qr-note { margin: 0 0 1.2em; font-size: 0.82em; color: rgba(var(--ww-mist-rgb), 0.5); }
/* short screens: tighter gaps so the QR keeps its size and the box doesn't scroll for a few px */
@media (max-height: 760px) {
    .pb-inner:has(> .pb-qr) { padding-block: 1rem; }
    .pb-inner:has(> .pb-qr) .pb-sub { margin-bottom: 0.9em; }
    .page-box .pb-qr-note { margin-bottom: 0.7em; }
}

/* Site nav: real links for screen readers, keyboards and crawlers. Off-screen until a link
   takes keyboard focus, then it shows as a small panel top-left (a hidden focus target would
   fail keyboard users). The sky sigils replace it visually (nav phase 3). */
.sr-nav {
    position: fixed; top: 12px; left: 12px; z-index: 60;
    clip-path: inset(50%); width: 1px; height: 1px; overflow: hidden; white-space: nowrap;
}
.sr-nav:focus-within {
    clip-path: none; width: auto; height: auto; overflow: visible;
    padding: 0.7rem 1rem; border-radius: 12px;
    background: rgba(11, 10, 15, 0.92); box-shadow: 0 0 24px 8px rgba(11, 10, 15, 0.6);
    font-family: var(--ww-serif); font-size: 0.95rem;
}
.sr-nav ul { list-style: none; margin: 0; padding: 0; }
.sr-nav a { display: block; padding: 0.15rem 0; color: rgba(255, 70, 92, 0.9); text-decoration: none; }
.sr-nav a:focus-visible { outline: 1px solid rgba(255, 92, 116, 0.8); outline-offset: 2px; }
/* Visually hidden, still read by screen readers and search engines (the homepage <header>). */
.sr-only {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* =================== end NAV phase 1 ================================================= */

/* ===================== SIGILS (nav phase 2, 2026-10-28) ===================================
   Active only on <html class="sigils"> (app.js SIGIL_ON; the landing page). app.js draws the
   constellations on the turning sky (top centre + tree-line peeks). Here: the word on the water.
   PATH BACK = delete this block. */
.signature-container { position: relative; }
.signature-top, .sig-reflection { transition: opacity 0.6s ease; }
body.ww-on .signature-top, body.ww-on .sig-reflection { opacity: 0 !important; }

/* The word (v2, journal (u)): a neon tube in the signature's own material (tools/word-build.py),
   sitting on the signature's baseline: x-height and tube width match hers, so the name is
   steady like her signature (no squash, no sway). Its baked reflection hangs below, fades in
   under the writing, sways like .sig-reflection and drifts with the wind (app.js).
   --sigw: the signature's width (same media steps as .sig-svg); the signature box is
   0.5975·sigw tall with its baseline at 0.53 of that = 0.317·sigw. --ww = name width / sigw,
   --bh = name baseline / name width. */
.water-word {
    --sigw: clamp(226px, 54.6vw, 378px);
    position: absolute; left: 50%; z-index: 6;
    top: calc(var(--sigw) * 0.317 - var(--sigw) * var(--ww, 1) * var(--bh, 0.22) + 0.012 * var(--sigw));
    translate: -50% 0;
    width: calc(var(--sigw) * var(--ww, 1)); max-width: 94vw;
    display: block; cursor: pointer;
    opacity: 0; transition: opacity 0.6s ease;
    -webkit-tap-highlight-color: transparent;
}
.water-word.on { opacity: 1; }
.water-word:focus-visible { outline: 1px solid rgba(255, 92, 116, 0.6); outline-offset: 6px; }
.water-word svg { display: block; width: 100%; height: auto; overflow: visible; }
.ww-stroke { stroke-dasharray: var(--l); stroke-dashoffset: var(--l);
             animation: sigDraw var(--dur) linear var(--delay) forwards; }
.ww-refl {
    position: absolute; height: auto; pointer-events: none; z-index: -1;
    opacity: 0; transition: opacity 1.6s ease 0.4s;
    transform-origin: 50% 0;
    animation: waterSway 7.5s ease-in-out infinite alternate;
}
.water-word.on .ww-refl { opacity: 0.6; }
@media (orientation: landscape) { .water-word { --sigw: clamp(142px, 35.7dvh, 307px); } }
@media (orientation: landscape) and (max-height: 560px) { .water-word { --sigw: clamp(113px, 30.5dvh, 210px); } }
@media (orientation: landscape) and (min-width: 1200px) { .water-word { --sigw: clamp(224px, 25dvh, 332px); } }
@media (prefers-reduced-motion: reduce) {
    .ww-refl { animation: none; }
    .ww-stroke { animation: none; stroke-dashoffset: 0; }
}

body.sg-hot { cursor: pointer; }

/* the wheel turns on horizontal touch drags (app.js), so the browser must not claim them;
   vertical pans and pinch-zoom stay native */
html.sigils body { touch-action: pan-y pinch-zoom; }
/* =================== end SIGILS ===================================================== */

/* ===================== PHASE 4 (2026-10-28 (x)): sections open in place =====================
   app.js PHASE 4. The box takes her place, from the top of her head to just above the word on
   the water (--pb-top / --pb-h from app.js fitBox), so the sigil above and the word below stay
   in view. On the landing page she dissolves into the mist (body.in-page) as the smoke gathers
   into the box; body.hero-back holds the slow transition while she returns. The setPersona
   inline transitions are overridden only while those classes are on. Opacity / filter / scale
   only, no mix-blend-mode, no backdrop-filter (../CLAUDE.md §4).
   PATH BACK = delete this block (and app.js PHASE 4). */
html.sigils .page-box {
    top: calc(var(--pb-top, 12dvh) + var(--pb-h, 76dvh) / 2);
    transition: opacity 0.9s ease, filter 0.9s ease, scale 0.9s ease;
}
html.sigils .pb-inner { max-height: var(--pb-h, 76dvh); }
html.sigils .page-box.pb-gather { opacity: 0; filter: blur(16px); scale: 0.92; }
html.sigils .page-box.pb-out { opacity: 0; filter: blur(10px); scale: 1.03; pointer-events: none; transition-duration: 0.55s; }
.page-box h1:focus { outline: none; }
body.in-page .logo { opacity: 0 !important; filter: blur(14px); }
body.in-page .hero-reflection, body.in-page .water-glow { opacity: 0 !important; }
/* the water stays empty under an open page (the word for that page is gone, Drew 2026-11-05),
   so the landing signature must not show through when a page opens in place */
body.in-page .signature-top, body.in-page .sig-reflection { opacity: 0 !important; }
body.in-page .logo, body.hero-back .logo,
body.in-page .hero-reflection, body.hero-back .hero-reflection,
body.in-page .water-glow, body.hero-back .water-glow { transition: opacity 1.1s ease, filter 1.1s ease !important; }
body.in-page .vapor-canvas { opacity: 0; transition: opacity 2.4s ease 0.8s; }
body.hero-back .vapor-canvas { transition: opacity 1.2s ease; }
body.in-page .logo-container { pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
    html.sigils .page-box.pb-gather, html.sigils .page-box.pb-out { filter: none; scale: none; }
    body.in-page .logo { filter: none; }
}
/* =================== end PHASE 4 ===================================================== */

/* ===================== CRUCIBLE (2026-11-02): the contact form =============================
   contact.html's .pb-form (tools/page-skel.py contact_form, driven by app.js CRUCIBLE). Lives in
   the smoky page box, so it's translucent dark fields with a hairline neon edge, Alegreya like the
   rest of the box, two columns that fold to one on phones. Inputs are >= 16px so iOS doesn't zoom.
   No data: URIs (CSP img-src 'self'): the select arrow is two gradients. No mix-blend-mode.
   PATH BACK = delete this block. */
.pb-form {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0.75em 0.9em;
    max-width: 34em; margin: 0 auto 1.4em; text-align: left; color-scheme: dark;
}
.pb-form .pf-wide, .pb-form .pf-status, .pb-form .pf-send { grid-column: 1 / -1; }
@media (max-width: 540px) { .pb-form { grid-template-columns: 1fr; } }
.pf-f { display: flex; flex-direction: column; gap: 0.3em; min-width: 0; }
.pf-f > span {
    font-size: 0.7em; letter-spacing: 0.18em; text-transform: uppercase; color: rgba(var(--ww-mist-rgb), 0.55);
}
.pf-f em { margin-left: 0.5em; font-style: italic; letter-spacing: 0.03em; text-transform: none; color: rgba(var(--ww-mist-rgb), 0.36); }
.pf-f .pf-count { float: right; }
.pf-f .pf-count.over { color: var(--ww-cherry-glow); }
.pb-form input, .pb-form select, .pb-form textarea {
    box-sizing: border-box; width: 100%; margin: 0;
    font: inherit; font-size: max(16px, 0.95em); line-height: 1.4;
    color: rgba(236, 232, 244, 0.92);
    background-color: rgba(8, 6, 11, 0.66);
    border: 1px solid rgba(255, 40, 70, 0.26); border-radius: 8px;
    padding: 0.55em 0.7em;
    transition: border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
}
.pb-form select {
    appearance: none; -webkit-appearance: none; padding-right: 2.2em; cursor: pointer;
    background-image: linear-gradient(45deg, transparent 50%, rgba(255, 70, 92, 0.8) 50%),
                      linear-gradient(135deg, rgba(255, 70, 92, 0.8) 50%, transparent 50%);
    background-position: calc(100% - 1.15em) 55%, calc(100% - 0.8em) 55%;
    background-size: 0.36em 0.36em; background-repeat: no-repeat;
}
.pb-form select:invalid { color: rgba(var(--ww-mist-rgb), 0.45); }
.pb-form option { color: #e6e1ee; background: #0e0c13; }
.pb-form textarea { min-height: 7.5em; resize: vertical; }
.pb-form ::placeholder { color: rgba(var(--ww-mist-rgb), 0.3); font-style: italic; }
.pb-form input:focus, .pb-form select:focus, .pb-form textarea:focus {
    outline: none; border-color: rgba(255, 60, 90, 0.75);
    box-shadow: 0 0 0 1px rgba(255, 40, 70, 0.22), 0 0 14px rgba(255, 0, 40, 0.2);
}
.pb-form [aria-invalid="true"] { border-color: rgba(255, 92, 116, 0.95); box-shadow: 0 0 12px rgba(255, 0, 40, 0.28); }
.pb-form textarea:disabled { opacity: 0.4; cursor: not-allowed; resize: none; }
.pb-form input[type="file"] { font-size: max(15px, 0.85em); padding: 0.45em; color: rgba(var(--ww-mist-rgb), 0.6); cursor: pointer; }
.pb-form input[type="file"]::file-selector-button {
    margin-right: 0.8em; padding: 0.3em 0.9em; cursor: pointer;
    font: inherit; font-style: italic; color: rgba(255, 70, 92, 0.9);
    background: transparent; border: 1px solid rgba(255, 70, 92, 0.4); border-radius: 6px;
}
.pf-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; opacity: 0; }
.pf-status { min-height: 1.3em; margin: 0.1em 0 0; font-size: 0.88em; text-align: center; color: rgba(255, 110, 130, 0.92); }
.pf-status.ok { color: rgba(var(--ww-mist-rgb), 0.6); font-style: italic; }
.pb-form .pf-send {
    justify-self: center; width: auto; padding: 0.5em 2.6em; cursor: pointer;
    font: inherit; font-style: italic; font-size: 1.08em; letter-spacing: 0.06em;
    color: var(--ww-cherry-lit); text-shadow: 0 0 10px rgba(255, 45, 78, 0.45);
    background: rgba(8, 6, 11, 0.6); border: 1px solid rgba(255, 40, 70, 0.5); border-radius: 999px;
    box-shadow: 0 0 16px rgba(255, 0, 40, 0.18);
    transition: color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
}
.pb-form .pf-send:hover:not(:disabled), .pb-form .pf-send:focus-visible {
    outline: none; color: var(--ww-cherry-glow); box-shadow: 0 0 22px rgba(255, 0, 40, 0.38), 0 0 46px rgba(255, 0, 40, 0.12);
}
.pb-form .pf-send:disabled { opacity: 0.32; cursor: default; box-shadow: none; text-shadow: none; }
.page-box .pb-done {
    margin: 0.4em auto 1.6em; max-width: 30em; color: rgba(var(--ww-mist-rgb), 0.86);
}
.page-box .pb-done:focus { outline: none; }
/* =================== end CRUCIBLE ===================================================== */
/* ===================== QUINTESSENCE (2026-11-03): carousel, dialog, inquiry form ============
   quintessence.html (tools/page-skel.py quintessence(), app.js QUINTESSENCE + QFORM). One piece
   at a time, crossfaded; neon-red < > arrows at the sides and a round enlarge button over the
   centre. The stage takes about half the fitted box (--pb-h) so the notes and form scroll under
   it. The <dialog> sits in the top layer (no ancestor filter/opacity reaches it) as a heavily
   tinted box over a dark ::backdrop. drop-shadow/box-shadow only: no mix-blend-mode (../CLAUDE.md §4).
   The form reuses .pb-form. PATH BACK = delete this block. */
.qx { max-width: 30em; margin: 0 auto; }
.qx-stage {
    position: relative; box-sizing: border-box; padding: 0 2.9em; width: fit-content; max-width: 100%; margin: 0 auto;
    height: clamp(150px, calc(var(--pb-h, 76dvh) * 0.5), 360px); touch-action: pan-y;
}
/* the stage shrinks to the widest photo, so the arrows sit just outside it */
.qx-track { list-style: none; margin: 0; padding: 0; height: 100%; display: grid; grid-template: 100% / auto; }
.qx-slide {
    grid-area: 1 / 1; display: flex; justify-content: center; min-height: 0;
    opacity: 0; visibility: hidden; transition: opacity 0.45s ease, visibility 0s linear 0.45s;
}
.qx-slide.on { opacity: 1; visibility: visible; transition: opacity 0.45s ease; }
.qx-slide img {
    display: block; height: 100%; width: auto; max-width: 100%; object-fit: contain; cursor: zoom-in;
    border: 1px solid rgba(255, 40, 70, 0.26); border-radius: 8px; box-shadow: 0 0 18px rgba(255, 0, 40, 0.14);
}
.qx-arr {
    position: absolute; top: 50%; translate: 0 -50%; z-index: 1;
    width: 2.5em; height: 2.5em; padding: 0.3em; margin: 0; cursor: pointer;
    color: #ff2d4e; background: transparent; border: 0; border-radius: 50%;
    filter: drop-shadow(0 0 6px rgba(255, 45, 78, 0.6));
    transition: color 0.25s ease, filter 0.25s ease;
}
.qx-prev { left: 0; }
.qx-next { right: 0; }
.qx-arr svg, .qx-zoom svg { display: block; width: 100%; height: 100%; }
.qx-arr:hover, .qx-arr:focus-visible { outline: none; color: var(--ww-cherry-glow); filter: drop-shadow(0 0 11px rgba(255, 45, 78, 0.95)); }
.qx-zoom {
    position: absolute; left: 50%; top: 50%; translate: -50% -50%; z-index: 1;
    width: 3.1em; height: 3.1em; padding: 0.68em; margin: 0; cursor: zoom-in;
    color: var(--ww-cherry-lit); background: rgba(8, 6, 11, 0.58); border: 1px solid rgba(255, 40, 70, 0.55); border-radius: 50%;
    box-shadow: 0 0 16px rgba(255, 0, 40, 0.25); opacity: 0.78;
    transition: opacity 0.25s ease, box-shadow 0.25s ease;
}
.qx-zoom:hover, .qx-zoom:focus-visible { outline: none; opacity: 1; box-shadow: 0 0 24px rgba(255, 0, 40, 0.45); }
.qx-count { margin: 0.55em 0 1.3em; font-size: 0.78em; letter-spacing: 0.2em; color: rgba(var(--ww-mist-rgb), 0.45); }
.page-box .qx-note { font-size: 0.94em; line-height: 1.55; }
.qx-note a { color: rgba(255, 70, 92, 0.9); text-decoration: none; border-bottom: 1px solid rgba(255, 70, 92, 0.35); }
.qx-note a:hover, .qx-note a:focus-visible { color: var(--ww-cherry-glow); border-color: rgba(255, 92, 116, 0.75); outline: none; }
.qx-pop {
    box-sizing: border-box; width: min(94vw, 760px); max-width: none; max-height: 94vh; max-height: 94dvh;
    margin: auto; padding: clamp(1rem, 3vmin, 1.8rem); overflow: auto; overscroll-behavior: contain;
    color: rgba(var(--ww-mist-rgb), 0.88); text-align: center; color-scheme: dark;
    background: rgba(5, 3, 8, 0.94); border: 1px solid rgba(255, 40, 70, 0.35); border-radius: 14px;
    box-shadow: 0 0 44px rgba(255, 0, 40, 0.2);
}
.qx-pop::backdrop { background: rgba(0, 0, 0, 0.82); }
.qx-pop figure { margin: 0; }
.qx-big { position: relative; padding: 0 2.7em; touch-action: pan-y; }
.qx-big img {
    display: block; margin: 0 auto; width: auto; height: auto; max-width: 100%;
    max-height: calc(94vh - 11em); max-height: calc(94dvh - 11em); object-fit: contain; border-radius: 8px;
}
.qx-desc { margin: 1em auto 0; max-width: 34em; line-height: 1.55; }
.qx-x {
    position: absolute; top: 0.25em; right: 0.45em; z-index: 2; margin: 0; padding: 0.05em 0.3em;
    font: inherit; font-size: 2em; line-height: 1; cursor: pointer;
    color: var(--ww-cherry-lit); background: none; border: 0; text-shadow: 0 0 10px rgba(255, 45, 78, 0.5);
}
.qx-x:hover, .qx-x:focus-visible { outline: none; color: var(--ww-cherry-glow); }
.pb-form .pf-hint { grid-column: 1 / -1; min-height: 1.2em; margin: -0.25em 0 0; font-size: 0.82em; font-style: italic; color: rgba(var(--ww-mist-rgb), 0.45); }
.pb-form .pf-hint.bad { font-style: normal; color: rgba(255, 110, 130, 0.92); }
@media (prefers-reduced-motion: reduce) { .qx-slide, .qx-slide.on { transition: none; } }
/* =================== end QUINTESSENCE ================================================== */
