/*
 * Valve & Vessel — site.css
 *
 * Single stylesheet for the public site. No framework. The file is split
 * into commented sections that map to the plan:
 *
 *   §0   tokens / reset / base typography
 *   §1   the typewriter system (Special Elite, ribbon-jitter SVG filter,
 *        per-character ink jitter, paper-grain noise tile, valve + vessel
 *        SVG motif, canvas reservoir at 6% opacity)
 *   §8.4 View Transitions API for stream ↔ poem nav
 *   §8.5 word-level reading effects + atmosphere presets + pace toggle
 *   §8.6 ambient music + per-poem audio surfaces (default-off)
 *   §10  /search and /timeline view-specific styling
 *   §11  reduced-motion + print stylesheets
 *
 * Strict rules from §1 enforced everywhere below:
 *   - paper-and-ink only: --paper, --ink, --accent
 *   - no raster images anywhere; only SVG, CSS, and the canvas
 *   - hairline rules instead of borders
 *   - no shadows on word-containing surfaces
 *   - no rounded corners on the public side
 */

:root {
    color-scheme: light;          /* opt out of system dark inversion */
    /* ---- Core palette ----
     * One paper, one ink, one accent. The semantic colours below
     * (honey / sage / clay) are the ONLY warm-status hues used
     * anywhere in the app — so a flash banner in the journal, a
     * "published" pill in the admin, and an inbox badge on the hive
     * all read as the same family. */
    --paper: #f5eed9;            /* warm cream — slightly lighter than
                                    the old #ede4d3 so the texture
                                    layer feels airy rather than dense.
                                    Matches the rgba(245,238,217,X)
                                    translucent surfaces already used
                                    throughout journal.css/admin.css. */
    --paper-rgb: 245 238 217;    /* RGB triplet so any surface can write
                                    rgb(var(--paper-rgb) / 0.X) for a
                                    translucent paper tint that always
                                    tracks --paper. */
    --ink:   #3a2e22;            /* warm walnut, deliberately lighter than
                                    pure black so the typewritten text
                                    reads as airy rather than stamped. */
    --ink-soft: rgba(58, 46, 34, 0.55);
    --accent: #4f6f6b;           /* default muted teal — overridden per-poem */

    /* Semantic status hues — used identically across hive, journal,
       admin, and public surfaces so a "warn" flash never shifts hue
       between the dashboard and the editor. */
    --honey: #c79a3a;            /* warm gold — drafts, hints, badges */
    --sage:  #3a6b4f;            /* forest green — ok / published */
    --clay:  #8a4f3a;            /* warm clay — danger / error */

    --hairline: rgba(58, 46, 34, 0.18);
    --type-stack: "Special Elite", "Courier Prime", "Courier New", Courier, ui-monospace, SFMono-Regular, monospace;
    --measure: 36rem;
}

/*
 * The "valve & vessel" aesthetic is paper-and-ink. We do NOT auto-flip
 * to a dark palette under prefers-color-scheme:dark — the typewritten
 * poems are designed to be read on cream paper with dark walnut ink,
 * and dark mode breaks the metaphor (the paper grain, the ink-bleed
 * filter, the ribbon-strike jitter all assume a light background).
 *
 * If we ever want a "night reading" mode, that's a deliberate user
 * choice via the .vv-pace toggle, not a system-driven inversion.
 */

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

/* ---- Accessibility helpers ---- */

.vv-skip {
    position: absolute;
    top: 0;
    left: 0;
    transform: translateY(-110%);
    padding: 0.5rem 1rem;
    background: var(--paper);
    color: var(--ink);
    border: 1px solid var(--accent);
    text-decoration: none;
    z-index: 100;
    transition: transform 160ms ease;
}
.vv-skip:focus, .vv-skip:focus-visible {
    transform: translateY(0);
    outline: none;
}

/* Universal focus ring — never erased anywhere by the rest of the
   stylesheet. We *replace* the underline-on-focus pattern with a more
   visible 2px outline for keyboard navigation, but leave hover/visit
   styles unchanged. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
:focus:not(:focus-visible) { outline: none; }

html {
    /* Paper background lives on the html element only so we can place
       fixed-position decorative layers (.vv-grain, .vv-reservoir,
       .vv-rail) behind the body content without their negative
       z-indexes vanishing under an opaque body background. */
    background: var(--paper);
}
html, body {
    margin: 0;
    padding: 0;
    color: var(--ink);
    font-family: var(--type-stack);
    font-size: 16px;        /* iOS Safari floor — no auto-zoom on focus */
    line-height: 1.65;
    /* Special Elite ships at a single weight; setting 300 here only
       affects fallback fonts and any synthetic-light path the browser
       chooses, but it nudges everything in the "thin / light" direction
       the way the brief asks for. */
    font-weight: 300;
    letter-spacing: 0.01em;
    -webkit-font-smoothing: antialiased;
    text-rendering: geometricPrecision;
}

body {
    min-height: 100dvh;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
             env(safe-area-inset-bottom) env(safe-area-inset-left);
    overflow-x: hidden;
    /* Transparent so the html-level paper background shows through;
       the fixed decorative layers (z=-1/-2/-3) sit above the html
       background and below the body's text content. */
    background: transparent;
}

a {
    color: var(--ink);
    text-decoration: none;
}
a:hover, a:focus-visible {
    color: var(--accent);
}

/* ---- Layout ---- */

.vv-main {
    flex: 1 1 auto;
    width: 100%;
    max-width: var(--measure);
    margin: 0 auto;
    padding: 4rem 1.25rem;
}

@media (min-width: 720px) {
    .vv-main { padding: 6rem 2rem; }
}

.vv-title {
    font-family: var(--type-stack);
    font-weight: 300;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    font-size: 1.05rem;
    margin: 0 0 1.5rem;
    color: var(--ink-soft);
}

/* ---- Valve / drop / vessel rail ----
 *
 * Centered behind the page content. The valve is two crossed lines
 * at the very top; the drop falls down the page's vertical center;
 * the vessel is a single curved line at the bottom of the viewport
 * (visually "in the footer"). z-index: -1 so every poem reads OVER
 * the motif rather than beside it.
 *
 * Reading the markup top-to-bottom in CSS:
 *
 *   1. .vv-rail            — full-viewport fixed container, ink colour
 *   2. .vv-rail__valve     — absolute, top: 0,  centered horizontally
 *   3. .vv-rail__drop      — absolute, animated translateY from 0
 *                            down to (100dvh - 80px), where the
 *                            bowl rim sits
 *   4. .vv-rail__vessel    — absolute, bottom: 0, centered horizontally
 *
 * The drop's keyframes are paired with the vessel's bottom-offset so
 * the impact frame lands ON the lip of the bowl. Both share the same
 * left: 50% / translateX(-50%) baseline. */

.vv-rail {
    position: fixed;
    inset: 0;
    pointer-events: none;
    color: var(--ink);
    z-index: -1;
    /* The whole motif sits well below the type contrast so the eye
       isn't pulled away from the poem. Hover bumps it up slightly
       for moments of curiosity. */
    opacity: 0.22;
    transition: opacity 600ms ease;
}
.vv-rail:hover { opacity: 0.4; }

/* Two crossed lines at the very top, centered — a faucet stub, not
   a piece of plumbing. */
.vv-rail__valve {
    display: block;
    position: absolute;
    top: clamp(0.6rem, 1.4vh, 1.4rem);
    left: 50%;
    transform: translateX(-50%);
    width: 44px;
    height: auto;
    color: var(--ink);
    animation: vv-valve-breath 9s ease-in-out infinite;
}

/* The drop bead. position:fixed (not absolute inside the rail) so
   we can give it a translateY value that's a fraction of the
   ACTUAL viewport, regardless of how tall the rail container is.
   Starts directly below the spout and ends just inside the bowl
   curve. */
.vv-rail__drop {
    position: fixed;
    top: calc(clamp(0.6rem, 1.4vh, 1.4rem) + 22px); /* spout exit y */
    left: 50%;
    width: 7px;
    height: 9px;
    margin-left: -3.5px;
    background: var(--ink);
    border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
    transform-origin: 50% 100%;
    opacity: 0;
    pointer-events: none;
}
.vv-rail__drop.is-falling {
    animation: vv-rail-drop 1.9s cubic-bezier(0.45, 0, 0.55, 1) forwards;
}
@keyframes vv-rail-drop {
    /* Form at the spout, fall through the rim, and land at the
       BOTTOM of the bowl (its inside floor).
       Geometry:
         - Vessel SVG viewBox is 0 0 80 28; CSS width 96px → height
           auto = 96 × 28/80 = 33.6 px.
         - Path is `M 4,2 Q 40,38 76,2` — a quadratic curve. The
           deepest point of a quadratic Bezier is at t=0.5, y =
           0.25·2 + 0.5·38 + 0.25·2 = 20 (SVG units). In CSS px
           that's 20/28 × 33.6 = 24 px below the vessel top.
         - Vessel top in viewport-from-top coords:
             100dvh − (0.8rem + 33.6 px) ≈ 100dvh − 46 px.
         - Floor (deepest curve point) is therefore at:
             100dvh − 46 + 24 = 100dvh − 22 px.
         - The bead's bottom starts ~42 px from the viewport top
           (33 px starting top + 9 px bead height), so to put the
           bead bottom on the floor:
             translateY ≈ 100dvh − 22 − 42 = 100dvh − 64 px
                         ≈ calc(100dvh − 0.8rem − 52 px).
       The narrow-screen bowl (64 px wide) shrinks the floor up by
       ~4 px; the same translateY just dips the bead a couple of
       pixels past the bowl floor on phones, which still reads as
       "lands inside the bowl" rather than the previous "stops at
       the rim and never enters". transform-origin: 50% 100% pins
       the squash to that bottom y. */
    0%   { transform: translateY(0)                                          scale(0.5, 0.5); opacity: 0;   }
    6%   { transform: translateY(0)                                          scale(1.0, 1.1); opacity: 1;   }
    12%  { transform: translateY(3px)                                        scale(0.85, 1.5); opacity: 1; }
    78%  { transform: translateY(calc(100dvh - 0.8rem - 56px))               scale(0.75, 1.6); opacity: 1; }
    88%  { transform: translateY(calc(100dvh - 0.8rem - 52px))               scale(1.4, 0.45); opacity: 0.7; }
    100% { transform: translateY(calc(100dvh - 0.8rem - 50px))               scale(1.7, 0.15); opacity: 0;   }
}

/* Bowl — single curved line, visually in the footer position
   (bottom of viewport, centered horizontally). */
.vv-rail__vessel {
    display: block;
    position: absolute;
    bottom: 0.8rem;
    left: 50%;
    transform: translateX(-50%);
    width: 96px;
    height: auto;
    color: var(--ink);
    animation: vv-rail-vessel-rock 12s ease-in-out infinite;
}
.vv-rail__vessel.is-bobbing {
    animation: vv-rail-vessel-bob 800ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes vv-rail-vessel-rock {
    0%, 100% { transform: translateX(-50%) rotate(-0.6deg); }
    50%      { transform: translateX(-50%) rotate( 0.6deg); }
}
@keyframes vv-rail-vessel-bob {
    0%   { transform: translateX(-50%) translateY(0)     rotate(0); }
    30%  { transform: translateX(-50%) translateY(2px)   rotate(-1.5deg); }
    60%  { transform: translateX(-50%) translateY(-1px)  rotate(1.5deg); }
    100% { transform: translateX(-50%) translateY(0)     rotate(0); }
}

/* On narrow phones the motif scales down a touch but stays
   centered. On admin / login pages we hide it entirely. */
@media (max-width: 30rem) {
    .vv-rail { opacity: 0.18; }
    .vv-rail__valve  { width: 32px; }
    .vv-rail__vessel { width: 64px; }
}
.vv-admin .vv-rail,
.vv-admin--login .vv-rail { display: none; }

/* The reservoir sits behind the rail (z=-1) and above the grain
   (z=-2). The cascading is: paper texture → ripple canvas →
   valve/vessel motif → poem text. */
.vv-reservoir {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    opacity: 0.1;
    z-index: -2;
}

/* Screen-reader-only helper used by .vv-poem__permalink etc. */
.vv-sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap; border: 0;
}

/* ---- Stream view ---- */

.vv-stream__header {
    text-align: center;
    margin-bottom: 3rem;
}

.vv-stream__eyebrow {
    font-size: 0.75rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-soft);
    margin: 0 0 0.5rem;
}

.vv-stream__meta {
    font-size: 0.85rem;
    color: var(--ink-soft);
    margin: 0;
    text-align: center;
}

.vv-search {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0.75rem;
    margin: 1rem auto 0;
    max-width: 28rem;
}

.vv-search__label {
    font-size: 0.75rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-soft);
    text-align: left;
}

.vv-search__input {
    font: inherit;
    background: color-mix(in oklch, var(--ink) 5%, transparent);
    border: 0;
    border-radius: 0;
    color: var(--ink);
    padding: 0.6rem 0.75rem;
    font-size: 16px;
    text-align: center;
}
.vv-search__input:focus { outline: none; background: color-mix(in oklch, var(--accent) 10%, transparent); }

.vv-search__button {
    font: inherit;
    background: transparent;
    color: var(--ink-soft);
    border: 0;
    padding: 0.6rem 1rem;
    cursor: pointer;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 0.85rem;
    min-height: 44px;
    align-self: center;
}
.vv-search__button:hover, .vv-search__button:focus-visible {
    color: var(--accent); outline: none;
}

/* ---- Site nav (small wayfinding row above the vessel) ---- */

.vv-nav {
    text-align: center;
    margin: 1.5rem auto 0;
    font-size: 0.75rem;
    letter-spacing: 0.16em;
    text-transform: lowercase;
    color: var(--ink-soft);
    display: inline-flex;
    gap: 0.65rem;
    align-self: center;
    justify-self: center;
    width: 100%;
    justify-content: center;
}
.vv-nav a {
    color: var(--ink-soft);
    text-decoration: none;
    border-bottom: 1px dotted transparent;
    padding: 0.25rem 0.1rem;
}
.vv-nav a:hover, .vv-nav a:focus-visible {
    color: var(--accent);
    border-bottom-color: var(--accent);
    outline: none;
}

/* The login page hides the public nav completely so it doesn't
   compete with the password field for attention. */
.vv-admin--login .vv-nav { display: none; }

/* ---- Tag cloud (public /tags index) ---- */

.vv-tags-cloud {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.2rem;
    justify-content: center;
    align-items: baseline;
}

.vv-tags-cloud__tag {
    /* --weight is 0..1 from the controller. Map it to font-size and
       opacity so heavier tags read first without ever screaming. */
    --base: 0.95rem;
    font-size: calc(var(--base) + var(--weight, 0) * 0.7rem);
    opacity: calc(0.6 + var(--weight, 0) * 0.4);
    text-decoration: none;
    color: var(--ink);
    padding: 0.1rem 0.05rem;
    line-height: 1.3;
    display: inline-flex;
    align-items: baseline;
    gap: 0.3em;
    transition: color 200ms ease;
}

.vv-tags-cloud__tag:hover,
.vv-tags-cloud__tag:focus-visible {
    color: var(--accent);
    outline: none;
}

.vv-tags-cloud__count {
    font-size: 0.7em;
    color: var(--ink-soft);
    letter-spacing: 0.06em;
}

/* ---- Timeline (vertical scrubber + year sections) ---- */

.vv-timeline {
    display: grid;
    grid-template-columns: 1fr;
    gap: 2rem;
    margin-top: 1rem;
}

.vv-timeline__scrubber {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.8rem;
    justify-content: center;
    padding: 0.5rem 0;
    font-size: 0.78rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.vv-timeline__year-link {
    color: var(--ink-soft);
    text-decoration: none;
    border-bottom: 1px dotted transparent;
    padding: 0.2rem 0.1rem;
    transition: color 200ms ease, border-color 200ms ease;
}
.vv-timeline__year-link:hover,
.vv-timeline__year-link:focus-visible {
    color: var(--accent);
    border-bottom-color: var(--accent);
    outline: none;
}
.vv-timeline__year-link.is-active {
    color: var(--ink);
    border-bottom-color: var(--accent);
}

.vv-timeline__years {
    display: flex;
    flex-direction: column;
    gap: 4rem;
}

.vv-timeline__year {
    scroll-margin-top: 4rem;
}

.vv-timeline__year-label {
    font-size: 1rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-soft);
    text-align: center;
    margin: 0 0 1.5rem;
    font-weight: normal;
    position: sticky;
    top: 0;
    background: var(--paper);
    padding: 0.5rem 0;
    z-index: 1;
}

@media (min-width: 920px) {
    /* On wider screens the scrubber slides to the right side as a true
       vertical scrubbable rail. We use a grid layout so the year sections
       take the full main width and the rail is sticky beside them. */
    .vv-main:has(.vv-timeline) {
        max-width: calc(var(--measure) + 9rem);
    }
    .vv-timeline {
        grid-template-columns: minmax(0, 1fr) 6.5rem;
    }
    .vv-timeline__years    { grid-column: 1; }
    .vv-timeline__scrubber {
        grid-column: 2;
        flex-direction: column;
        gap: 0.5rem;
        align-items: flex-start;
        position: sticky;
        top: 1.5rem;
        align-self: start;
        max-height: calc(100dvh - 3rem);
        max-height: calc(100vh - 3rem);
        overflow-y: auto;
        border: 0;
        padding-left: 1rem;
    }
    .vv-timeline__year-link { padding: 0.25rem 0; }
}

.vv-stream__list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.vv-stream__item + .vv-stream__item {
    margin-top: 3rem;
}

.vv-stream__link {
    display: block;
    color: var(--ink);
    text-decoration: none;
}

.vv-stream__title {
    font-size: 1.2rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 0 0 0.5rem;
    font-weight: normal;
}

.vv-stream__date {
    display: block;
    font-size: 0.85rem;
    color: var(--ink-soft);
    letter-spacing: 0.08em;
    margin-bottom: 0.75rem;
}

.vv-stream__preview {
    margin: 0;
    color: var(--ink-soft);
    font-size: 0.95rem;
}

.vv-stream__empty {
    text-align: center;
    color: var(--ink-soft);
}

/* ---- Single poem ---- */

.vv-poem {
    --poem-accent: var(--accent);
}

.vv-poem__title {
    text-align: center;
    margin-bottom: 0.5rem;
}

.vv-poem__date {
    display: block;
    text-align: center;
    color: var(--ink-soft);
    font-size: 0.85rem;
    letter-spacing: 0.08em;
    margin-bottom: 2.5rem;
}

.vv-poem__body p {
    margin: 0 0 1.25rem;
    white-space: pre-wrap;
}

.vv-poem__body em {
    color: var(--poem-accent, var(--accent));
}

.vv-poem__audio {
    margin: 2rem 0;
    text-align: center;
}

.vv-poem__audio audio { width: 100%; max-width: 28rem; }

.vv-poem__tags {
    list-style: none;
    /* Sit directly under the last stanza, hard left, almost a margin
       note. Light + small so the eye treats them as metadata, not
       another reading line. */
    margin: 0.4rem 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.9rem;
    justify-content: flex-start;
    font-size: 0.7rem;
    font-weight: 300;
    letter-spacing: 0.02em;
    color: var(--ink-soft);
    opacity: 0.6;
    text-transform: lowercase;
}
.vv-poem__tags li { margin: 0; padding: 0; }
.vv-poem__tags a {
    color: inherit;
    text-decoration: none;
    border-bottom: 0;
}
.vv-poem__tags a:hover,
.vv-poem__tags a:focus-visible {
    color: var(--ink);
    opacity: 1;
}

/* Share button — sits as the last item in the tag row so it reads
   as another piece of poem metadata. Same scale/weight/casing as
   the tags so it doesn't shout "BUTTON" — the typewriter restraint
   is the point. */
.vv-poem__share-item { margin-left: 0.4rem; }
.vv-poem__share {
    font: inherit;
    font-size: inherit;
    font-weight: inherit;
    letter-spacing: inherit;
    color: inherit;
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
    text-transform: lowercase;
    opacity: 1;
    transition: color 200ms ease;
}
.vv-poem__share::before {
    /* Tiny chain glyph so the action reads at a glance even in
       tags that already contain the word "share" (none in the
       current vocab, but cheap insurance). */
    content: "↗ ";
    margin-right: 0.05em;
    color: var(--accent);
}
.vv-poem__share:hover,
.vv-poem__share:focus-visible {
    color: var(--ink);
    outline: none;
}
.vv-poem__share.is-shared::before { content: "✓ "; }
.vv-poem__share.is-shared        { color: var(--accent); opacity: 1; }

.vv-poem__back {
    margin-top: 3rem;
    text-align: center;
    font-size: 0.85rem;
    letter-spacing: 0.06em;
    color: var(--ink-soft);
}

/* ---- Prev / next poem navigation ----
 *
 * Chevrons sit centered at the bottom of every poem. They walk the
 * date-ordered timeline so reading the site cover-to-cover is one
 * smooth right-to-left drift. The home page anchors at the most
 * recent poem and never shows a "next" link.
 *
 * On wide screens the chevrons float at the inline edges of the
 * reading column; on narrow screens they stack centered. The arrow
 * glyph fades in slightly when hovered so the touch target is
 * obvious without breaking the paper-and-ink restraint. */
.vv-poem__nav {
    /* Two-column grid pins prev to the left and next to the right
       regardless of HTML order or whether one of them is missing.
       The previous flex+margin-auto layout could collapse both links
       to the right when only one was rendered. */
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5rem;
    margin: 4rem auto 1rem;
    max-width: var(--measure);
    padding-top: 2rem;
    font-size: 0.9rem;
    letter-spacing: 0.04em;
    color: var(--ink-soft);
}
.vv-poem__nav-link {
    text-decoration: none;
    color: var(--ink-soft);
    display: inline-flex;
    align-items: baseline;
    gap: 0.5em;
    transition: color 200ms ease, transform 220ms cubic-bezier(0.4,0,0.2,1);
}
.vv-poem__nav-link:hover,
.vv-poem__nav-link:focus-visible {
    color: var(--ink);
}
/* Pin BOTH links to row 1 explicitly. Without this, the HTML order
   in poem.twig (next first, prev second) combined with default
   grid-auto-flow: row would push prev down to row 2 — the visible
   misalignment from round 7. */
.vv-poem__nav-link--prev { grid-column: 1; grid-row: 1; justify-self: start; }
.vv-poem__nav-link--next { grid-column: 2; grid-row: 1; justify-self: end;   }
.vv-poem__nav-link--prev:hover { transform: translateX(-0.25rem); }
.vv-poem__nav-link--next:hover { transform: translateX( 0.25rem); }
.vv-poem__nav-arrow {
    font-size: 1.4em;
    line-height: 1;
    color: var(--accent);
}
@media (max-width: 30rem) {
    /* Keep prev / next on the SAME row on phones — stacking them
       made the user say "they're on top of each other". The
       2-column grid still fits two short labels (← previous /
       → next) at this font-size on a 320 px-wide screen. */
    .vv-poem__nav {
        gap: 0.75rem;
        font-size: 0.78rem;
        margin: 2.5rem auto 0.75rem;
        padding-top: 1.25rem;
    }
}

/* The poem itself sits inside a tall reading column so each stanza
   gets breathing room; the scroll-driven word reveal benefits from
   a bit of vertical travel. We DON'T pad the column out absurdly —
   short poems should still fit on one screen — but we do cap the
   width so eye-flick distance stays comfortable. */
.vv-poem {
    max-width: var(--measure);
    margin: 0 auto;
    padding: 1rem 0 6rem;
}
.vv-poem__title {
    text-align: center;
    margin: 0 0 1rem;
}
.vv-poem__date {
    display: block;
    text-align: center;
    margin: 0 0 3rem;
    color: var(--ink-soft);
    letter-spacing: 0.18em;
    font-size: 0.85rem;
}
@media (min-width: 48rem) {
    .vv-poem__date { margin-bottom: 4rem; }
}

/* ---- Sweetener (one-off) ---- */
/* The Sweetener SVG already contains the poem text as hand-drawn paths;
   it is the only poem rendered as artwork. Every other poem is plain
   typewritten text per the chapbook aesthetic. */

.vv-poem--sweetener .vv-sweetener {
    margin: 0 auto;
    max-width: 32rem;
    display: block;
}

.vv-poem--sweetener .vv-sweetener svg {
    width: 100%;
    height: auto;
    display: block;
}

/* Recolour the SVG strokes to inherit --ink (the artwork was exported
   with stroke="#000"; in dark mode we want it to read as ink-on-paper). */
.vv-poem--sweetener .vv-sweetener svg [stroke="#000"],
.vv-poem--sweetener .vv-sweetener svg [stroke="#000000"],
.vv-poem--sweetener .vv-sweetener svg .cls-1 {
    stroke: var(--ink) !important;
}

/* Visually hide the title text since it's already drawn into the artwork. */
.vv-poem__title--visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ---- Login (§7.6) ---- */

.vv-admin--login .vv-main { max-width: 24rem; }

.vv-login__title {
    text-align: center;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 0 0 2rem;
    font-weight: normal;
    font-size: 1.2rem;
}

.vv-login__form { display: flex; flex-direction: column; gap: 1.25rem; }

.vv-field { display: flex; flex-direction: column; gap: 0.4rem; }

.vv-field--check {
    flex-direction: row;
    gap: 0.5rem;
    align-items: center;
    color: var(--ink-soft);
    font-size: 0.9rem;
}

.vv-field__label {
    font-size: 0.75rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.vv-field__input {
    font: inherit;
    background: transparent;
    border: 0;
    border-bottom: 1px solid var(--hairline);
    color: var(--ink);
    padding: 0.5rem 0.25rem;
    font-size: 16px; /* lock against iOS auto-zoom */
}
.vv-field__input:focus {
    outline: none;
    border-bottom-color: var(--accent);
}

.vv-login__button {
    margin-top: 1rem;
    font: inherit;
    background: transparent;
    color: var(--ink);
    border: 1px solid var(--hairline);
    padding: 0.85rem 1rem;
    cursor: pointer;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 0.9rem;
    min-height: 44px; /* HIG floor */
}
.vv-login__button:hover, .vv-login__button:focus-visible {
    border-color: var(--accent);
    color: var(--accent);
    outline: none;
}

.vv-login__error {
    text-align: center;
    color: var(--clay);
    margin: 0 0 1rem;
    font-size: 0.9rem;
}

.vv-login__flash {
    text-align: center;
    color: var(--ink-soft);
    margin: 0 0 1rem;
    font-size: 0.85rem;
}

.vv-login__hint {
    text-align: center;
    color: var(--ink-soft);
    font-size: 0.8rem;
    margin: 1rem 0 0;
}

/* ---- Errors ---- */

.vv-error { text-align: center; color: var(--ink-soft); }
.vv-error__path { font-size: 0.85rem; }

/* ---- §8.6.1 Ambient music toggle (default-off; only rendered when enabled) ---- */

.vv-ambient {
    position: fixed;
    right: 1rem;
    bottom: max(1rem, env(safe-area-inset-bottom));
    z-index: 5;
}

.vv-ambient__toggle {
    font: inherit;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    background: var(--paper);
    color: var(--ink-soft);
    border: 1px solid var(--hairline);
    padding: 0.55rem 0.8rem;
    cursor: pointer;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 0.7rem;
    min-height: 36px;
}

.vv-ambient__toggle:hover,
.vv-ambient__toggle:focus-visible,
.vv-ambient__toggle[aria-pressed="true"] {
    color: var(--accent);
    border-color: var(--accent);
    outline: none;
}

.vv-ambient__icon { font-size: 1rem; }

.vv-ambient__player { display: none; }

/* ================================================================
 * §1 Typewriter system + §8.5 word-level reading effects.
 *
 * The whole reading surface is essentially three layers:
 *
 *   1. A paper layer (--paper) with a fixed-position SVG noise tile
 *      on top of it (.vv-grain) — the static "page".
 *   2. An ink layer (every word/stanza) with a sub-pixel ribbon-
 *      displacement filter, per-character opacity jitter, and
 *      atmospheric reveal animations driven by `animation-timeline:
 *      view()`.
 *   3. A canvas reservoir (.vv-reservoir, 6% opacity, fixed) that
 *      site.js paints with cursor-following ink blooms and the
 *      occasional drop ripple.
 *
 * Words come pre-wrapped from Sanitizer::plainTextToBodyHtml() as
 * <section class="stanza"><span class="word">…</span> ··· </section>
 * so we can address them granularly without touching the markup.
 * ================================================================ */

/* ---- Paper grain (the static "page" backdrop) ---- */

.vv-grain {
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: -3;
    /* Reference the inline <filter id="vv-grain"> in base.twig.
       Apply via background filter so we never repaint anything. */
    background: var(--paper);
    /* Tile a Perlin SVG into a CSS background. The data: URL is a tiny
       inline 240x240 fractal-noise tile; we keep one copy here so old
       browsers without filter() support still get the texture. */
    /* The colour-matrix alpha here is the ONLY knob for how heavy the
       paper grain reads. 0.08 keeps the noise as a barely-there fibre
       that catches the eye on first paint and then disappears into
       the reading. Was 0.14 — Clara found it too dense once it sat on
       every page of the site at once. */
    background-image:
        url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' seed='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0'/></filter><rect width='240' height='240' filter='url(%23n)'/></svg>");
    background-size: 240px 240px;
    background-repeat: repeat;
}

/* ---- Word + stanza wrappers (typewritten) ---- */

.vv-poem__body {
    font-family: var(--type-stack);
    font-size: 1.02rem;
    line-height: 1.7;
    /* The SVG ribbon-displacement filter was previously applied here,
       but on long pages with lots of poems it forces the browser to
       paint each stanza through an offscreen surface, which was a
       big chunk of the scroll jank. The visible result of the
       filter was very subtle anyway — the jitter is now provided by
       per-word --ink-jitter / --ink-shift alone, set once at load
       and not re-evaluated every frame. */
    letter-spacing: 0.01em;
    color: var(--ink);
    font-weight: 300;
}

.vv-poem__body .stanza {
    display: block;
    margin: 0 0 1.6rem;
}
.vv-poem__body .stanza:last-child { margin-bottom: 0; }

.vv-poem__body .word {
    /* Each word is its own inline-block so the per-character ink
       jitter doesn't shift adjacent words around. Transitions used
       to live here for hover effects but the cost of style-invalidation
       across thousands of words was a meaningful chunk of the scroll
       jank, so we leave them off. */
    display: inline-block;
}

/* Per-word strike-pressure jitter. site.js sets two custom props on
   each .word at load time:
     --ink-jitter  — opacity 0.78..1.0 (was 0.86..1.0; we widened the
                     range so the variance is actually visible)
     --ink-shift   — a sub-pixel vertical drift -0.5..0.5 px so the
                     baseline isn't perfectly flat, the way real
                     typewriter ribbons aren't. */
.vv-poem__body .word {
    opacity: var(--ink-jitter, 1);
    transform: translateY(var(--ink-shift, 0));
}

/* ---- §8.5.1 drip-reveal — words fade in on scroll-into-view ----
 *
 * Each .word is its own scroll-driven element. As the reader scrolls
 * down, words materialise individually: opacity 0 + a half-line drop
 * → opacity 1 + settled. The animation is GPU-cheap (transform +
 * opacity only) and finishes well before the word fully crosses
 * the viewport, so reading speed isn't bottlenecked by it.
 *
 * Stanzas separately receive a slower bloom so the *block* still
 * feels like it lands as a unit even while individual words pop.
 *
 * Both rules live behind `@supports (animation-timeline: view())`
 * — Firefox + Safari fall through and the page renders the static
 * end state (opacity 1, no transform).
 */
/* §8.5.1 — atmosphere-driven word reveal.
 *
 * Words inside a poem are hidden until that poem first enters the
 * viewport ("revealed"). On reveal, every word fades in with a
 * staggered animation-delay, so the reader sees the poem
 * MATERIALISE word-by-word — that's the "cool" effect the user
 * asked for.
 *
 * Cost: each poem reveals exactly once. The animations finish in
 * ~600–900 ms after which the words are static (animation-fill: forwards
 * locks them at the end state). No infinite per-word work, no
 * scroll-timeline computations. Single-poem pages reveal on load;
 * stream poems reveal via IntersectionObserver in site.js.
 *
 * Atmosphere variants live below (drift / vessel / pulse / pour /
 * still). Each picks its own keyframes + stagger so the mood is
 * tactile, not just decorative.
 */
html.vv-js .vv-poem:not(.has-revealed) .vv-poem__body .word {
    opacity: 0;
}
html.vv-js .vv-poem:not(.has-revealed) .vv-poem__title,
html.vv-js .vv-poem:not(.has-revealed) .vv-poem__date {
    opacity: 0;
}

/* Default reveal — used by atmospheres that don't override (and as the
   fallback for poems with no atmosphere set). Words fade up with a
   gentle stagger driven by --word-index (set by site.js). */
.vv-poem.has-revealed .vv-poem__title,
.vv-poem.has-revealed .vv-poem__date {
    animation: vv-poem-meta-in 600ms ease both;
}
.vv-poem.has-revealed .vv-poem__date { animation-delay: 80ms; }

.vv-poem.has-revealed .vv-poem__body .word {
    animation: vv-word-fade 540ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: calc(var(--word-index, 0) * 28ms + 80ms);
    will-change: opacity, transform, filter;
}

@keyframes vv-poem-meta-in {
    from { opacity: 0; transform: translateY(0.3rem); }
    to   { opacity: 1; transform: translateY(0);     }
}
@keyframes vv-word-fade {
    from {
        opacity: 0;
        transform: translateY(0.5em);
        filter: blur(2px);
    }
    to {
        opacity: var(--ink-jitter, 1);
        transform: translateY(var(--ink-shift, 0));
        filter: blur(0);
    }
}

/* drift — words drift in from a subtle horizontal direction, like
   ink sliding across the page. Alternates left/right per word so the
   stanza shimmers rather than slides as a block. */
.vv-poem[data-atmosphere="drift"].has-revealed .vv-poem__body .word {
    animation: vv-word-drift 660ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: calc(var(--word-index, 0) * 26ms + 80ms);
}
.vv-poem[data-atmosphere="drift"].has-revealed .vv-poem__body .word:nth-child(odd)  { animation-name: vv-word-drift-l; }
.vv-poem[data-atmosphere="drift"].has-revealed .vv-poem__body .word:nth-child(even) { animation-name: vv-word-drift-r; }
@keyframes vv-word-drift-l {
    from { opacity: 0; transform: translate(-0.4em, 0.15em); filter: blur(1.5px); }
    to   { opacity: var(--ink-jitter, 1); transform: translateY(var(--ink-shift, 0)); filter: blur(0); }
}
@keyframes vv-word-drift-r {
    from { opacity: 0; transform: translate(0.4em, 0.15em);  filter: blur(1.5px); }
    to   { opacity: var(--ink-jitter, 1); transform: translateY(var(--ink-shift, 0)); filter: blur(0); }
}

/* vessel — words pour down from above, line by line. The stagger is
   strongly tied to vertical position so it reads as falling water. */
.vv-poem[data-atmosphere="vessel"].has-revealed .vv-poem__body .word {
    animation: vv-word-pour 680ms cubic-bezier(0.34, 1.4, 0.64, 1) both;
    animation-delay: calc(var(--word-index, 0) * 22ms + 80ms);
}
@keyframes vv-word-pour {
    from { opacity: 0; transform: translateY(-0.6em); filter: blur(1px); }
    to   { opacity: var(--ink-jitter, 1); transform: translateY(var(--ink-shift, 0)); filter: blur(0); }
}

/* pulse — words throb in (scale + opacity), giving a heartbeat feel.
   Faster cadence so the rhythm reads as deliberate, not lethargic. */
.vv-poem[data-atmosphere="pulse"].has-revealed .vv-poem__body .word {
    animation: vv-word-pulse 620ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
    animation-delay: calc(var(--word-index, 0) * 18ms + 80ms);
    transform-origin: center;
}
@keyframes vv-word-pulse {
    0%   { opacity: 0;                            transform: scale(0.85); }
    55%  { opacity: 1;                            transform: scale(1.06); }
    100% { opacity: var(--ink-jitter, 1);         transform: scale(1) translateY(var(--ink-shift, 0)); }
}

/* pour — STANZAS pour as a unit (slow, viscous), with their words
   already revealed inside. Use this for atmospheric, narrative poems
   where individual word stagger would feel too busy. */
.vv-poem[data-atmosphere="pour"].has-revealed .vv-poem__body .word {
    /* Words ride along with the stanza; no per-word stagger. */
    animation: vv-word-still 220ms ease both;
    animation-delay: 0ms;
}
.vv-poem[data-atmosphere="pour"].has-revealed .vv-poem__body .stanza {
    animation: vv-stanza-pour 1100ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: calc(var(--stanza-index, 0) * 240ms + 80ms);
}
@keyframes vv-stanza-pour {
    from { opacity: 0; transform: translateY(-0.8em); filter: blur(2.5px); }
    60%  { opacity: 0.9; transform: translateY(0.2em); filter: blur(0.4px); }
    to   { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@keyframes vv-word-still {
    from { opacity: 0; }
    to   { opacity: var(--ink-jitter, 1); transform: translateY(var(--ink-shift, 0)); }
}

/* still — quiet, almost no motion. Tiny opacity stagger so it doesn't
   feel like a hard cut, but no movement / blur. For solemn poems. */
.vv-poem[data-atmosphere="still"].has-revealed .vv-poem__body .word {
    animation: vv-word-still 360ms ease both;
    animation-delay: calc(var(--word-index, 0) * 9ms + 60ms);
}

/* Long poems / very high word index: cap the stagger so the LAST word
   in a 200-word poem isn't waiting 6+ seconds. After ~80 words we hold
   the delay flat — readers scrolling will just see the rest revealed
   already by the time they get there. */
.vv-poem.has-revealed .vv-poem__body .word { animation-delay: clamp(0ms, calc(var(--word-index, 0) * 28ms + 80ms), 2400ms); }
.vv-poem[data-atmosphere="drift"].has-revealed  .vv-poem__body .word { animation-delay: clamp(0ms, calc(var(--word-index, 0) * 26ms + 80ms), 2400ms); }
.vv-poem[data-atmosphere="vessel"].has-revealed .vv-poem__body .word { animation-delay: clamp(0ms, calc(var(--word-index, 0) * 22ms + 80ms), 2200ms); }
.vv-poem[data-atmosphere="pulse"].has-revealed  .vv-poem__body .word { animation-delay: clamp(0ms, calc(var(--word-index, 0) * 18ms + 80ms), 2000ms); }
.vv-poem[data-atmosphere="still"].has-revealed  .vv-poem__body .word { animation-delay: clamp(0ms, calc(var(--word-index, 0) *  9ms + 60ms), 1500ms); }

/* The first stanza of long poems gets a faintly slower reveal —
   the reader's eye has just landed there, give it room to breathe. */
.vv-poem.has-revealed .vv-poem__body .stanza--first .word {
    animation-duration: 720ms;
}

/* @starting-style entrance for the entire poem container — a single
   slow ink-bloom on first paint after a View Transition. */
@supports (transition-behavior: allow-discrete) {
    .vv-poem {
        opacity: 1;
        transition: opacity 600ms ease 0ms, transform 600ms ease 0ms;
    }
    @starting-style {
        .vv-poem { opacity: 0; transform: translateY(0.25rem); }
    }
}

/* ---- §8.5.2 stanza-tide — adjacent stanzas slip slightly when one is hovered ---- */

.vv-poem__body:has(.stanza:hover) .stanza        { opacity: 0.55; }
.vv-poem__body:has(.stanza:hover) .stanza:hover  { opacity: 1; transform: translateX(0); }

/* ---- §8.5.3 reading focus mode (CSS :has + tap-to-focus) ----
 *
 * Tapping a stanza adds the .is-focused class via site.js (because
 * :focus-within doesn't work on non-interactive elements on touch).
 * Every other stanza dims; nothing blurs. */
.vv-poem__body:has(.stanza.is-focused) .stanza:not(.is-focused) {
    opacity: 0.32;
    color: var(--ink-soft);
}
.vv-poem__body .stanza.is-focused {
    opacity: 1;
    color: var(--ink);
}

/* ---- §8.5.4 selection ripple — applied by site.js to selected words ---- */

.vv-poem__body .word.is-rippled {
    color: var(--poem-accent, var(--accent));
    text-shadow: 0 0 8px color-mix(in oklch, var(--poem-accent, var(--accent)) 35%, transparent);
}

/* The first-stanza SVG-filter "wet ink" bleed was visually pretty
   but expensive to composite when many poems are on screen at once
   (the home stream renders 70). Drop it for the listing view; the
   drop-cap drain effect below already announces the first stanza. */

/* ---- §8.5.6 drop-cap drain (the first letter "drains" into ink) ---- */

.vv-poem__body .stanza--first .word:first-child::first-letter {
    font-size: 1.6em;
    line-height: 0.9;
    margin-right: 0.05em;
    color: var(--poem-accent, var(--accent));
}

/* ---- §8.5.7 end-mark drip — a slow drip emoji-equivalent on the last word ---- */

/* Real teardrop shape: a square with three rounded corners + one sharp
   corner, rotated -45° so the sharp corner lands at the top.

   Subtlety: per CSS Transforms Module Level 2 the final matrix is
       translate × rotate × scale × transform
   which when applied to a point means the `transform` property is
   applied INNERMOST (its translation vector gets rotated by the
   `rotate` property), and the `translate` individual property is
   applied OUTERMOST (in world coords, after rotation).

   So: the static teardrop shape lives on `rotate`, and the falling
   motion lives on `translate` (individual property), guaranteeing the
   bead drops straight down regardless of the rotation. */
.vv-poem__body .stanza--last .word:last-child::after {
    content: "";
    display: inline-block;
    width: 0.42em;
    height: 0.42em;
    margin-left: 0.28em;
    vertical-align: -0.18em;
    /* Three rounded corners + one sharp (top-right). After
       rotate(-45deg) the top-right corner pivots to point straight
       up, giving a teardrop with a sharp top and a rounded fat
       bottom. */
    border-radius: 50% 0 50% 50%;
    rotate: -45deg;
    background: var(--poem-accent, var(--accent));
    opacity: 0.75;
    animation: vv-drip-fall 6.4s ease-in infinite;
}
@keyframes vv-drip-fall {
    0%, 50%   { translate: 0 0;       opacity: 0.75; }
    65%       { translate: 0 0.45em;  opacity: 0.55; }
    80%       { translate: 0 0.85em;  opacity: 0;    }
    81%, 100% { translate: 0 0;       opacity: 0;    }
}

/* The previous "pace toggle" (Quiet / Calm / Lively) was removed. The
   "calm" baseline values it used to apply at default are now built
   into .vv-poem__body directly above; the toggle wasn't earning its
   complexity. */

/* ---- Atmosphere presets (per-poem mood) ---- */

[data-atmosphere="still"]  { --atmo-x: 0;     --atmo-y: 0;     --atmo-blur: 0; }
[data-atmosphere="drift"]  { --atmo-x: 0.4em; --atmo-y: 0.2em; --atmo-blur: 0.2px; }
[data-atmosphere="vessel"] { --atmo-x: 0;     --atmo-y: 0.5em; --atmo-blur: 0; }
[data-atmosphere="pulse"]  { --atmo-x: 0;     --atmo-y: 0;     --atmo-blur: 0; }
[data-atmosphere="pour"]   { --atmo-x: 0;     --atmo-y: 1em;   --atmo-blur: 0.4px; }

/* The atmosphere data attributes drive the per-word reveal animations
   defined further up. Those reveals are ONE-SHOT (animation-fill:
   forwards) and only fire when a poem first enters the viewport, so
   total per-frame work is proportional to the number of revealing
   words — never to the total number of words on the page. */

/* The cursor wake / trail dots were removed: the per-pointer DOM
   spawn rate was creating a measurable scroll/cursor jitter and
   the user found the visual effect "too extreme" anyway. The
   reservoir canvas below still responds to pointer movement,
   which is the calmer, more abstract version of the same idea. */
.vv-cursor-trail { display: none !important; }

/* ================================================================
 * §1 Valve / drop / vessel — keyframes shared by the rail
 * ================================================================ */

@keyframes vv-valve-breath {
    0%, 100% { opacity: 0.85; }
    50%      { opacity: 1;    }
}

/* ================================================================
 * Home stream — every published poem stacked vertically with soft
 * scroll-snap. The intent is "one poem at a time" without forcing
 * a hard snap that can fight the user on long poems or short
 * gestures. Each panel is at least one viewport tall (modulo
 * safe-area insets) so the next poem only enters as the current
 * one is finished.
 *
 * On dedicated `/p/{slug}` pages the .vv-poem class is reused but
 * WITHOUT this stream wrapper, so direct deep links remain a single
 * focused page with prev/next chevrons.
 * ================================================================ */

/* No scroll-snap on the home stream — proximity-snap on a 70-poem
   list ended up fighting the user's scroll gestures and made the
   page feel "glitchy". The is-active dim/undim is enough of a
   per-poem cue without forcing the scroller to land anywhere. */
.vv-public--stream { scroll-behavior: auto; }

.vv-stream {
    display: block;
    margin: 0 auto;
    max-width: var(--measure);
}

.vv-poem--stream {
    min-height: 88vh;
    min-height: 88dvh;
    padding: 6vh 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    transition: opacity 600ms ease, filter 600ms ease;
    position: relative;
}

/* Stream-focus dimming: the scroll-position-active poem reads at full
   ink; siblings dim to ~32%. site.js toggles .is-active via an
   IntersectionObserver. The transition is slow on purpose — abrupt
   dim/undim feels frantic; slow feels like attention shifting. */
.vv-stream:has(.is-active) .vv-poem--stream:not(.is-active) {
    opacity: 0.34;
}
.vv-poem--stream.is-active {
    opacity: 1;
}

/* No dividers between stream poems — the dim/undim attention shift
   is the only "change of scene" cue we need. */
.vv-poem--stream::after { content: none; }

.vv-stream__end {
    text-align: center;
    margin: 4rem 0 6rem;
    color: var(--ink-soft);
    letter-spacing: 0.16em;
    font-size: 0.8rem;
    text-transform: uppercase;
}
.vv-stream__end > span { margin: 0 0.6rem; }

/* ================================================================
 * §8.4 View Transitions — fade between stream and poem.
 *
 * Browsers without ::view-transition-* selectors silently fall back
 * to a no-op page navigation. There's no JS shim required because
 * we use the document.startViewTransition() API only when present.
 * ================================================================ */

@supports (view-transition-name: vv) {
    .vv-poem        { view-transition-name: vv-poem-content; }
    .vv-stream__title:has(+ .vv-stream__date),
    .vv-stream__link { view-transition-name: vv-poem-link; }
}

::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: 380ms;
    animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

::view-transition-old(vv-poem-content) {
    animation: vv-fade-out 280ms ease both;
}
::view-transition-new(vv-poem-content) {
    animation: vv-fade-in 420ms ease both;
}
@keyframes vv-fade-out { from { opacity: 1; } to { opacity: 0; transform: translateY(-0.25rem); } }
@keyframes vv-fade-in  { from { opacity: 0; transform: translateY(0.25rem); } to { opacity: 1; } }

/* ================================================================
 * Reduced motion — disable everything that moves, but keep colour /
 * focus / opacity-on-hover working so the page is still legible.
 * ================================================================ */

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
    .vv-poem__body { filter: none; }
    .vv-grain { display: none; }
    .vv-reservoir { display: none; }
}

/* ================================================================
 * Hamburger button + poem-index drawer
 *
 * The button lives fixed at top-left so it never collides with the
 * right-side rail. The drawer slides in from the left as a full-
 * height panel with a click-to-dismiss scrim filling the rest of
 * the viewport. Year-grouped list inside; one click → /p/{slug}.
 * Hidden on /admin* pages.
 * ================================================================ */

.vv-burger {
    position: fixed;
    top: max(0.75rem, env(safe-area-inset-top));
    left: max(0.75rem, env(safe-area-inset-left));
    z-index: 8;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;
    background: color-mix(in oklch, var(--paper) 80%, transparent);
    border: 0;
    color: var(--ink);
    cursor: pointer;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    transition: opacity 200ms ease, left 320ms cubic-bezier(0.4, 0, 0.2, 1);
    opacity: 0.55;
}
.vv-burger:hover, .vv-burger:focus-visible { opacity: 1; }

/* When the drawer is open, slide the burger to the right edge of
   the drawer panel so it sits inside the panel as the close X.
   It's already morphed into an X via the existing `aria-expanded`
   keyframes; this just gets it out from over the "All poems"
   title without introducing a second button. The math is
   `panel-width - burger-width - inset` so the button hugs the
   panel's right edge. */
.vv-burger[aria-expanded="true"] {
    left: calc(min(86vw, 22rem) - 36px - 0.75rem);
    opacity: 1;
}
.vv-burger__bars {
    display: inline-grid;
    grid-template-rows: repeat(3, 1.5px);
    gap: 4px;
    width: 16px;
}
.vv-burger__bars > span {
    display: block;
    height: 100%;
    background: currentColor;
    transition: transform 240ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms ease;
    transform-origin: center;
}
.vv-burger[aria-expanded="true"] .vv-burger__bars > span:nth-child(1) {
    transform: translateY(5px) rotate(45deg);
}
.vv-burger[aria-expanded="true"] .vv-burger__bars > span:nth-child(2) {
    opacity: 0;
}
.vv-burger[aria-expanded="true"] .vv-burger__bars > span:nth-child(3) {
    transform: translateY(-5px) rotate(-45deg);
}

.vv-drawer {
    position: fixed;
    inset: 0;
    z-index: 7;
    display: grid;
    grid-template-columns: min(86vw, 22rem) 1fr;
    pointer-events: none;
    visibility: hidden;
}
.vv-drawer[aria-hidden="false"],
.vv-drawer.is-open {
    pointer-events: auto;
    visibility: visible;
}

.vv-drawer__panel {
    background: var(--paper);
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: translateX(-100%);
    transition: transform 320ms cubic-bezier(0.4, 0, 0.2, 1);
    display: flex;
    flex-direction: column;
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
    box-shadow: 0 0 0 transparent;
}
.vv-drawer[aria-hidden="false"] .vv-drawer__panel,
.vv-drawer.is-open .vv-drawer__panel {
    transform: translateX(0);
    box-shadow: 4px 0 12px color-mix(in oklch, var(--ink) 18%, transparent);
}

.vv-drawer__scrim {
    background: color-mix(in oklch, var(--ink) 40%, transparent);
    border: 0;
    cursor: pointer;
    opacity: 0;
    transition: opacity 320ms ease;
}
.vv-drawer[aria-hidden="false"] .vv-drawer__scrim,
.vv-drawer.is-open .vv-drawer__scrim {
    opacity: 1;
}

.vv-drawer__header {
    /* Reserve space on the right for the hamburger button which
       slides into the panel's top-right when the drawer is open
       (see `.vv-burger[aria-expanded="true"]`). Without this
       reserve the title would visually collide with the X. */
    display: flex;
    align-items: baseline;
    justify-content: flex-start;
    padding: 1.25rem 3.5rem 0.75rem 1.25rem;
}
.vv-drawer__title {
    margin: 0;
    font-size: 0.9rem;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--ink);
}

.vv-drawer__body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 1rem 0;
}

.vv-drawer__year {
    margin-bottom: 1.5rem;
}
.vv-drawer__year-label {
    margin: 0 1.25rem 0.4rem;
    font-size: 0.75rem;
    letter-spacing: 0.3em;
    text-transform: uppercase;
    color: var(--ink-soft);
}
.vv-drawer__list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.vv-drawer__item { margin: 0; }
.vv-drawer__link {
    display: grid;
    grid-template-columns: 4.5rem 1fr;
    align-items: baseline;
    gap: 0.6rem;
    padding: 0.55rem 1.25rem;
    text-decoration: none;
    color: var(--ink);
    transition: background 160ms ease, color 160ms ease;
}
.vv-drawer__link:hover,
.vv-drawer__link:focus-visible {
    background: color-mix(in oklch, var(--accent) 10%, transparent);
    color: var(--ink);
}
.vv-drawer__date {
    font-size: 0.72rem;
    letter-spacing: 0.14em;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}
.vv-drawer__title-text {
    font-size: 0.95rem;
    line-height: 1.35;
}

.vv-drawer__footer {
    padding: 0.85rem 1.25rem 1.5rem;
    color: var(--ink-soft);
    font-size: 0.75rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
}

/* When the drawer is open, lock the document scroll so dragging on
   the scrim doesn't scroll the page underneath. site.js sets this
   class on <body>. */
body.vv-drawer-open { overflow: hidden; }

/* Hide the burger + drawer on admin / login. The admin has its own
   navigation. */
.vv-admin .vv-burger,
.vv-admin .vv-drawer,
.vv-admin--login .vv-burger,
.vv-admin--login .vv-drawer { display: none; }

@media (max-width: 30rem) {
    .vv-drawer { grid-template-columns: 88vw 1fr; }
    /* Burger lands at the (wider) phone-drawer right edge. */
    .vv-burger[aria-expanded="true"] {
        left: calc(88vw - 36px - 0.5rem);
    }
}

/* ================================================================
 * Print: a clean chapbook page.
 * ================================================================ */

@media print {
    .vv-grain, .vv-reservoir, .vv-ambient,
    .vv-rail, .vv-burger, .vv-drawer { display: none; }
    .vv-poem__body { filter: none; color: #000; }
    body { background: #fff; }
    a { color: #000; text-decoration: none; }
}
