/* Grimberg Studio — the home page's parallax hero and the chrome that sits over it.
   Pairs with styles/tokens.css (canonical palette, type, notch) and site/pilot.js.
   Sub-pages use styles/page.css instead: with no stage, their nav and cards differ. */

/* The hero's own two colours; everything else is a canonical token from styles/tokens.css.
   This page used to alias the palette locally (--ink, --amber) to values tokens.css already names. */
:root {
    --cream: #F0E3C4;      /* the wordmark's warm off-white, sampled from the painted mark's face */
    /* The mark's size and seat once it is docked. On :root because BOTH ends read these and they
       are not related: nav sizes its own height from them, and .title -- a fixed sibling of nav,
       not a child -- lerps toward them. Declared on nav, as --navw first was, .title could not
       see it, the width calc was invalid, and the mark silently kept its intrinsic 340px while
       its position animated perfectly.

       --nav-pad is both the bar's padding and the mark's distance from the top of it. That is
       not a coincidence and it is why the mark ends up centred: the bar reserves exactly the
       mark's height as CONTENT and adds the same padding above and below. An earlier version
       folded the padding into min-height as well, which content-box then added a second time --
       the bar came out 24px taller than intended and the mark sat 12px high inside it. */
    --navw: 176px;
    --mark-h: calc(var(--navw) * 92 / 340);
    --nav-pad: 12px;
    --sky-bottom: #4685a8; /* the horizon the stage is painted on, and the .fill below it */
    --sky-top: #051530;    /* sky.png's own top row: what shows above the stage when it cannot cover */
}
html,
body {
    margin: 0;
    background: var(--bg-0);
    color: var(--cream);
    font-family: var(--font-body);
}

/* ---- The scene ---------------------------------------------------------
Design canvas is 2048 x 1400. The stage covers the viewport (plus 8% overscan) and keeps that
aspect; every layer is placed in percentages of the stage, so nothing is computed at runtime.

--hero is how tall the hero actually is. A full viewport wherever the stage can cover one; in
portrait it is the floored stage height instead, so the scene ends where the art ends and the
content starts right underneath rather than after a band of empty sky. Everything that used to
say 100vh -- the scroll distance, the spacer, where the content sheet sits -- reads this.

   In svh, not vh. On a phone 100vh is the LARGE viewport, the one you get once the URL bar has
   scrolled away, so at load -- bar still showing -- the hero is taller than the screen and its
   foot is cut off; scroll an inch, the bar collapses, the viewport grows, and the whole scene
   jumps. svh is the SMALL viewport, the smallest it ever gets, so the scene fits from the first
   paint and nothing moves when the bar does. Each plain-vh line below is the fallback for
   browsers without svh, which skip the declaration they cannot parse. */
:root { --hero: 100vh; }
:root { --hero: 100svh; }

/* --art-px is how big ONE art pixel lands, in CSS px.
   art/sky/site_layers.py exports every layer at REF_STAGE_W, so the scene's pixel size is just how
   far the stage has been scaled away from that number -- stage / 1036.8. It is the same max()
   the stage itself uses, repeated here because .title is a sibling of .stage, not a child, and
   cannot read --w. If REF_STAGE_W moves, this moves with it. */
:root { --art-px: calc(max(108vw, 158vh) / 1036.8); }
:root { --art-px: calc(max(108vw, 158svh) / 1036.8); }
.hero {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--hero);
    overflow: hidden;
    background: var(--sky-top);
    z-index: 0;
}
.stage {
    --w: max(108vw, 158vh);
    --w: max(108vw, 158svh); /* 158svh = 108svh * 2048/1400: whichever covers */
    --h: calc(var(--w) * 1400 / 2048);
    position: absolute;
    left: 50%;
    width: var(--w);
    height: var(--h);
    transform: translateX(-50%);
    bottom: min(
        0px,
        calc((var(--hero) - var(--h)) / 2)
    ); /* split vertical overflow between summit and cliff base */
}
.layer {
    position: absolute;
    left: 0;
    width: 100%;
    image-rendering: pixelated;
    display: block;
}
.fill {
    position: absolute;
    left: 0;
    right: 0;
    top: 73.14%;
    bottom: 0;
    background: var(--sky-bottom);
}

/* Placements: width / left = % of stage width, bottom = % of stage height. Hand-tuned here and
   nowhere else -- art/sky/compose.py renders the same plates but does not model this page, so when
   the two disagree, this file is right. Keep every value a percentage so it scales with the stage. */
/* The sky is drawn here rather than loaded from layers/sky.webp, and it is drawn WITHOUT the
   plate's dither. The plate held eight colours with an ordered dither between each pair, which
   hid the banding beautifully at its native size -- but the stage scales every layer by --art-px,
   and that is 1.5 at 1440, 2.0 at 1920, a fraction on a phone. A baked 2x2 checkerboard survives
   an integer factor and falls apart at every other one: some cells double, their neighbours do
   not, and the weave goes lumpy differently at each width. That is what the dither looked wrong
   for.

   A gradient has no pattern to alias, so it is correct at every resolution by construction. The
   eight colours are the plate's own, sampled from it, placed where each was dominant; the browser
   interpolates between them instead of dithering, which is the other way to hide a band and the
   one that does not care what size it is drawn at.

   The other layers keep their plates. They carry drawing, not just a ramp, and their dithering
   sits inside shapes where uneven scaling reads as texture rather than as a fault.

   aspect-ratio replaces what the <img> supplied: the plate was 1024x512, and .layer sets only
   the width. */
#sky {
    bottom: 26.857%;
    --d: 0.04;
    aspect-ratio: 2 / 1;
    background: linear-gradient(
        to bottom,
        #01081e 0%,
        #051530 40%,
        #0c2547 58%,
        #173a60 72.5%,
        #1e476e 79.7%,
        #26547b 84.4%,
        #32688e 91%,
        #417ca0 100%
    );
}
#farclouds {
    bottom: 45.714%;
    height: 40.214%;
    --d: 0.12;
    opacity: 0.35;
    mask-image: linear-gradient(#000 45%, transparent 95%);
    -webkit-mask-image: linear-gradient(#000 45%, transparent 95%);
}
#mountain {
    width: 62.012%;
    left: 18.994%;
    bottom: 38.286%;
    --d: 0.18;
}
#clouds {
    bottom: 33.571%;
    height: 73.143%;
    --d: 0.26;
}
#forestA {
    bottom: 14%;
    --d: 0.32;
    width: 63.7%;
    left: 14%;
}
#forestB {
    bottom: 1.714%;
    --d: 0.42;
    width: 96%; /* the plate is spliced wider for this in art/sky/site_layers.py; 79% left its cut edge in open sky */
    left: 4%;
}
#forestC {
    bottom: -12.857%;
    --d: 0.55;
    width: 95%;
    left: 4%;
    filter: hue-rotate(327deg);
}
/* The nearest layers are pinned (below) rather than placed, so they carry no bottom of their own.
   The cliff is wider than it was to stay prominent now that pinning sits it lower. */
#neartrees {
    --d: 0.7;
    width: 62%;
    left: 13%;
}
#foreground {
    width: 62%;
    left: 36%;
    --d: 1;
}
#frontleft {
    width: 35.936%;
    left: 0.244%;
    bottom: -9.571%;
    --d: 1.1;
}
#frontright {
    width: 33.936%;
    left: 75%;
    bottom: -5%;
    --d: 1.1;
    scale: -1 1;
}

/* The foot of the scene belongs to the SCREEN, not to the stage. Everything else is placed as a
   share of the stage, but the stage is sized to cover the viewport and then hangs half its overflow
   below the fold -- so a layer placed a fixed % above the stage's bottom floats clear of the screen
   at any aspect narrower than about 2.1:1, which is every ordinary display. Cancelling exactly that
   overhang lands the layer's foot on the fold instead, at every size. (When the stage is shorter
   than the viewport its bottom already IS the fold, and max() holds the layer there.) */
.pinned { bottom: max(0px, calc((var(--h) - var(--hero)) / 2)); }

/* cloud strips: a child three tile-periods wide slides right by one period and loops */
.tile {
    left: -100%;
    width: 300%;
}
.tile > i {
    position: absolute;
    inset: 0;
    background-repeat: repeat-x;
    background-size: 33.3333% auto;
    background-position: 0 0;
    animation: drift linear infinite;
}
#farclouds > i {
    background-image: url(../layers/farclouds.webp);
    animation-duration: 560s;
} /* 4 px/s at 2253 px period */
#clouds > i {
    background-image: url(../layers/clouds.webp);
    animation-duration: 186s;
} /* 11 px/s at 2048 px period */
@keyframes drift {
    to {
        transform: translateX(33.3333%);
    }
}

/* scroll parallax: over the first viewport of scrolling, each layer rises by its depth share.
Without scripts this is a pure scroll-driven animation (steps with the wheel). With scripts, --sy is an
eased copy of the scroll position and the same formula runs off it, so wheel notches glide. */
.layer {
    will-change: transform;
}
@supports (animation-timeline: scroll()) {
    html:not(.js) .layer {
        animation: lift linear both;
        animation-timeline: scroll(root);
        animation-range: 0 var(--hero);
    }
    @keyframes lift {
        to {
            transform: translateY(calc(var(--d) * -90svh));
        }
    }
}
html.js .layer {
    transform: translateY(
        calc(var(--d) * min(var(--sy, 0px), var(--hero)) * -0.9)
    );
}
/* With scripts the content sheet is fixed and slid by the same eased value, and the spacer
   supplies the scroll track -- but only where the pointer is a mouse. Touch scrolls with momentum
   the browser composites off the main thread, so a sheet moved by JS every frame can never keep up
   with it: it reads as lag however small the easing, and no amount of tuning fixes it.

   The test is the input, not the shape of the screen. Orientation was the wrong proxy in both
   directions -- it left a phone in landscape dragging the fixed sheet, and took the easing away
   from a narrow window on a desktop. Anything without a hovering, fine pointer falls through to
   the no-script arrangement: sheet in normal flow, scrolled by the browser. A browser too old to
   know these queries also falls through, which is the safe way round.

   site/scroll.js tests the exact negation of this and drops its easing to match. The layers
   parallax off --sy either way; they sit inside the fixed hero and are not racing the scroller. */
@media (hover: hover) and (pointer: fine) {
    html.js .content {
        position: fixed;
        top: var(--hero);
        left: 0;
        right: 0;
        transform: translateY(calc(-1 * var(--sy, 0px)));
        will-change: transform;
    }
    html.js .spacer {
        height: calc(var(--hero) + var(--ch, var(--hero)));
    }
}
@media (prefers-reduced-motion: reduce) {
    .layer,
    .tile > i {
        animation: none !important;
    }
}

/* ---- Portrait ------------------------------------------------------------
   A tall screen cannot show this scene the way a wide one does. The art is one
   1.46:1 picture, so covering a 0.46 viewport takes 3.4x of overscan and you end up
   looking through a keyhole -- the cabin off-screen, the trees enormous.

   So portrait stops treating the stage as a fixed-aspect picture. The stage becomes
   viewport-tall, and every band is re-placed as a share of THAT height, which makes the
   scene read bottom-to-top instead of left-to-right: cliff on the floor, forest climbing
   away behind it, mountain and clouds above that, sky over everything. Same layers, same
   depth order, same parallax -- stacked up the screen instead of strung across it. */
@media (max-aspect-ratio: 6/5) {
    /* ---- Portrait ---------------------------------------------------------
       Every placement below is written as a pair: the value on a phone, and the value the wide
       layout uses. --t walks from 0 to 1 as the viewport goes from the phone ratio (390:844) to
       where portrait meets wide (6:5), and each property lerps between its pair, so there is no
       step anywhere in between and none at the 6:5 boundary either -- at --t: 1 the stage and
       every layer in it are arithmetically identical to the wide layout.

       --t comes from site/scroll.js, because CSS cannot produce it: calc() refuses to divide one
       length by another, so vw/vh can never become a plain number. The fallback below keeps a
       usable composition when scripts are off.

       To retune, edit the two ends of a pair. The first is the phone, the second is desktop. */
    :root {
        --hero: min(100vh, calc(100vw * 844 / 390));
        --hero: min(100svh, calc(100vw * 844 / 390));
        --t: 0;
        /* A second ramp, flat until 906:1305 (--t 0.3147) and climbing to 1 at the wide end, for
           the few layers that need to keep changing after the rest have settled. Division by a
           plain number is allowed in calc; dividing by a LENGTH is not, which is why --t itself
           has to come from the script. */
        --t2: clamp(0, calc((var(--t) - 0.3147) / 0.6853), 1);
        /* Portrait sizes the stage by a different rule, so the art pixel follows that one instead.
           Same expression as .stage's --w below. */
        --art-px: calc((118vw + 13.67vw * var(--t)) / 1036.8);
    }
    /* Without scripts --t never moves, so one breakpoint stands in for the sweep: tall screens
       keep the phone composition, wider portrait ones get most of the way to the wide layout. */
    @media (min-aspect-ratio: 4/5) { :root { --t: 0.6; } }

    .stage {
        --w: calc(118vw + 13.67vw * var(--t));        /* -> 131.67vw, which is max(108vw,158vh) at 6:5 */
        --h: calc(var(--hero) + 8.01svh * var(--t));   /* -> 108.01vh, the wide stage's height there */
        /* Centred at the far end, but as a PERCENTAGE of the hero rather than in vw: vw counts the
           scrollbar and a percentage does not, so a vw-based centre lands half a scrollbar (7px
           here) away from where the wide layout's left: 50% puts it. */
        left: calc(-9vw * (1 - var(--t)) + (50% - var(--w) / 2) * var(--t));
        right: auto;
        transform: none;
        /* bottom is left to the base rule: it already reads (--hero - --h) / 2, which is 0 while
           the stage is hero-tall and becomes the wide overflow split as --h grows past it. */
    }

    /* One formula for every layer's foot; each --ba/--bb pair below is phone -> wide. Listed by id
       rather than on .layer: the wide rules set `bottom` on the ids, so a class selector here
       loses to them on specificity and every pair below would be silently ignored. */
    #sky, #farclouds, #mountain, #clouds, #forestA, #forestB, #forestC,
    #neartrees, #foreground, #frontleft, #frontright {
        bottom: calc(var(--ba) + (var(--bb) - var(--ba)) * var(--t));
    }

    .fill {
        top: auto;
        bottom: calc(26% - 26% * var(--t));
        height: calc(26% + 0.86% * var(--t));
        background: linear-gradient(to bottom, transparent, var(--sky-bottom));
    }

    #sky       { --ba: 28%; --bb: 26.857%; height: calc(72% + 1.14% * var(--t)); }
    #farclouds { --ba: 54%; --bb: 45.714%; height: calc(13% + 27.214% * var(--t));
                 opacity: calc(0.5 - 0.15 * var(--t)); }
    #clouds    { --ba: 36%; --bb: 33.571%; height: calc(24% + 49.143% * var(--t)); }

    /* The peak is the subject on a tall screen and merely scenery on a wide one, so it shrinks
       from half again the stage to the wide layout's 62%. It is centred at both ends -- the wide
       rule's 18.994% + 62.012%/2 is exactly 50% -- so only the width has to travel. The vh cap
       stops the summit growing off the top; the margin re-centres whichever term wins, since
       translateX is unavailable (the parallax owns `transform`). */
    #mountain {
        --mp: min(150%, calc(var(--hero) * 1.05));
        --mw: calc(var(--mp) + (62.012% - var(--mp)) * var(--t));
        --ba: 40%;
        --bb: 38.286%;
        width: var(--mw);
        left: 50%;
        margin-left: calc(var(--mw) / -2);
    }

    /* Bands span the stage on a phone and narrow to the wide layout's staggered widths. */
    #forestA   { --ba: 25%; --bb: 14%;
                 width: calc(100% - 36.3% * var(--t)); left: calc(14% * var(--t)); }
    /* forestB's plate was spliced 96/79 wider for the wide layout; on a phone the extra runs off
       the right edge unseen, so the composition there is exactly what it was at 100%. */
    #forestB   { --ba: 16%; --bb: 1.714%;
                 width: calc(121.519% - 25.519% * var(--t)); left: calc(4% * var(--t)); }
    #forestC   { --ba: 7%;  --bb: -12.857%;
                 width: calc(100% - 5% * var(--t));    left: calc(4% * var(--t)); }

    /* The two nearest layers are pinned in the wide layout, and (--h - --hero) / 2 is what .pinned
       resolves to there -- 0 while the stage is hero-tall, so the pair starts where they sit now. */
    #neartrees { --ba: 6%; --bb: calc((var(--h) - var(--hero)) / 2);
                 width: calc(98% - 36% * var(--t)); left: calc(2% + 11% * var(--t)); }
    #foreground {
        /* From 906:1305 up the cliff runs wider, which is also what lifts the cabin: the plate
           grows upward off an anchored base, so widening raises it without lifting it clear of the
           floor. Adding to `bottom` instead floats it -- the plate's bottom is a hard edge, and
           anything above zero opens a gap under the cliff. */
        --fp: min(110%, calc(var(--hero) * 0.6));
        --fw: calc(var(--fp) + (62% - var(--fp)) * var(--t) + 16% * var(--t2));
        --ba: 0%;
        --bb: calc((var(--h) - var(--hero)) / 2);
        width: var(--fw);
        /* Positioned by where the cabin lands rather than by the plate's edge, since the plate's
           left third is empty. 66% of the stage on a phone -> 84.36%, which with a 62% plate is
           the wide rule's left: 36% exactly. */
        left: calc(66% + 18.36% * var(--t) - var(--fw) * 0.78);
    }

    /* Tall pines that the screen edge cuts lengthwise, so on a phone they sit well below the fold
       and rise back to the wide layout's framing. */
    /* Their height follows viewport WIDTH while the offset is a share of HEIGHT, so past 906:1305
       they ride up far enough to put their inner edge in frame as a vertical cut. --t2 carries them
       back down over that same span and is worth nothing below it, so the phone and the sizes
       either side of the threshold keep exactly what they had. */
    #frontleft  { --ba: -7%; --bb: -9.571%;
                  width: calc(48% - 12.064% * var(--t)); left: calc(-8% + 8.244% * var(--t));
                  bottom: calc(var(--ba) + (var(--bb) - var(--ba)) * var(--t) - 10% * var(--t2)); }
    #frontright { --ba: -12%; --bb: -5%;
                  width: calc(46% - 12.064% * var(--t)); left: calc(64% + 11% * var(--t));
                  bottom: calc(var(--ba) + (var(--bb) - var(--ba)) * var(--t) - 10% * var(--t2)); }

    /* Each band plate ends in a flat cut with ground-fill streaks beneath it. Spread up a tall
       screen that edge lands in the gaps between trunks and the trees visibly stop dead; in the
       wide layout the next band buries it. So the dissolve fades out as --t rises -- at 1 both
       stops are at 100%, which is no mask at all. */
    #forestA, #forestB, #forestC {
        mask-image: linear-gradient(#000 calc(74% + 26% * var(--t)), transparent calc(96% + 4% * var(--t)));
        -webkit-mask-image: linear-gradient(#000 calc(74% + 26% * var(--t)), transparent calc(96% + 4% * var(--t)));
    }
}

/* ---- Page chrome ---------------------------------------------------- */
/* One mark does both jobs. It used to be two -- a big one over the stage and a small one in the
   nav -- swapped at a threshold by a view transition. Even morphed, a swap is a cut: it happens
   at one scroll position whether or not you are moving, and on a page where everything else
   tracks the scroll continuously it read as a jump.

   So it is a single element now, fixed, interpolated from where it sits over the scene to where
   it sits in the nav by --dock, which scroll.js ramps 0 -> 1 across the stage. calc() can lerp
   two lengths with a unitless factor, which is all this needs: a + (b - a) * t, once for left,
   once for top, once for width.

   z-index 3 puts it over the nav (2) so it lands on top of the bar rather than behind its plate.
   It is out of .hero entirely -- that box is overflow:hidden at z-index 0, and both would have
   fought this. */
.title {
    position: fixed;
    z-index: 3;
    display: block;
    text-decoration: none;
    --hx: calc(var(--gutter) + 7vw);          /* over the stage */
    --hy: 9svh;
    --nx: calc(var(--gutter) + 12px);         /* in the nav, matching its padding */
    --ny: var(--nav-pad);
    left: calc(var(--hx) + (var(--nx) - var(--hx)) * var(--dock, 0));
    top: calc(var(--hy) + (var(--ny) - var(--hy)) * var(--dock, 0));
    white-space: nowrap;

    /* The mark's width at rest. The grid term keeps it in proportion to the stage; the floor
       takes over on phones and tablets, where the stage is driven by viewport width and that
       term collapses. Screens over 1024 use this; the rule further down replaces it below that. */
    --lockup: max(calc(259 * var(--art-px)), min(340px, 88vw));
}
.title h1 {
    margin: 0;
    line-height: 0;
}
/* The stroke is drawn, not baked. text-shadow used to sit on .title and did nothing at all once
   the mark became an image -- text-shadow does not apply to replaced content. drop-shadow does,
   and it follows the alpha channel rather than the box, so four of them at one pixel each in the
   four directions outline the letterforms exactly.

   --stroke is one art pixel, not one CSS pixel, so the outline is the same weight as every line
   in the scene behind it and thickens with the stage instead of going hairline on a big display.
   The fifth shadow is the old 4px 4px hard offset, kept because it is what lifts the mark off a
   busy sky. */
.title h1 img {
    display: block;
    width: calc(var(--lockup) + (var(--navw) - var(--lockup)) * var(--dock, 0));
    height: auto;
    image-rendering: pixelated;
    --stroke: max(1px, calc(var(--art-px) * 1));
    filter:
        drop-shadow(var(--stroke) 0 0 var(--bg-0))
        drop-shadow(calc(-1 * var(--stroke)) 0 0 var(--bg-0))
        drop-shadow(0 var(--stroke) 0 var(--bg-0))
        drop-shadow(0 calc(-1 * var(--stroke)) 0 var(--bg-0))
        drop-shadow(calc(var(--stroke) * 2) calc(var(--stroke) * 2) 0 var(--bg-0));
}
/* STUDIO used to be a second image here, set solid under the left of the name and scaled from
   --lockup so the two could never drift apart. The painted lockup draws it as part of the
   artwork, on the line below GRIMBERG and in the same hand, so there is nothing left to keep in
   proportion -- the rule and its <p> are both gone rather than left pointing at an unused asset. */
.scroll-hint {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 24px;
    text-align: center;
    font:
        400 18px/1 var(--font-pixel);
    letter-spacing: 0.3em;
    color: var(--cream);
    -webkit-text-stroke: 3px var(--bg-0);
    paint-order: stroke fill;
    animation: blink 1.6s steps(1) infinite;
}
@keyframes blink {
    0%,
    55% {
        opacity: 0.7;
    }
    56%,
    100% {
        opacity: 0;
    }
}

.spacer {
    height: var(--hero);
}
.content {
    position: relative;
    z-index: 1;
    background: var(--bg-0);
    box-shadow: 0 -6px 0 0 var(--bg-2);
}
.wrap {
    max-width: 64rem;
    margin: 0 auto;
    padding: 5rem var(--gutter);
}
.content h2 {
    font:
        400 clamp(40px, 6vw, 72px)/1 var(--font-display);
    margin: 0 0 1rem;
    color: var(--cream);
}
.content p {
    font-size: 1.125rem;
    line-height: 1.6;
    max-width: 48ch;
    color: var(--snow-1);
}
/* ---- Work cards + devlog list -------------------------------------
Same notch and hard shadow as the button; art is pixel-scaled like every other layer. */
.cards {
    display: grid;
    grid-template-columns: repeat(
        auto-fit,
        minmax(min(100%, 22rem), 1fr)
    );
    gap: 12px;
    margin-top: 2.5rem;
}
.cards .card {
    display: block;
    background: var(--bg-1);
    color: inherit;
    text-decoration: none;
    clip-path: var(--notch);
    transition: background 100ms steps(2, end);
}
.cards .card:hover {
    background: var(--bg-2);
}
.cards .card:hover h3 {
    color: var(--ember-0);
}
.cards .card img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
}

/* ---- Mobs ---------------------------------------------------------
Sprites off the notnoita creature LoRA, taken back to their 16px paint grid by a
per-block majority vote and keyed at the border. ~40x50 native; every width here is a
whole multiple of that, so the pixel grid never lands on a half. */
.cards .body {
    padding: 1.1rem 1.35rem 1.5rem;
}
.cards .k {
    display: block;
    font:
        400 15px/1 var(--font-pixel);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ember-1);
    margin-bottom: 0.5rem;
}
.cards h3 {
    font:
        400 1.9rem/1.1 var(--font-display);
    margin: 0 0 0.3rem;
    color: var(--cream);
}
.cards p {
    font-size: 0.975rem;
    margin: 0;
    max-width: none;
}

.log {
    list-style: none;
    margin: 2rem 0 0;
    padding: 0;
}
.log li {
    box-shadow: 0 -3px 0 0 var(--rock-1);
}
.log a {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1.25rem;
    align-items: baseline;
    justify-content: space-between;
    padding: 1.1rem 0;
    color: var(--cream);
    text-decoration: none;
}
.log a:hover b {
    color: var(--ember-1);
}
.log b {
    font:
        400 1.5rem/1.15 var(--font-display);
    font-weight: 400;
}
.log span {
    font:
        400 16px/1 var(--font-pixel);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--snow-2);
}

nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    padding: var(--nav-pad) calc(var(--gutter) + 12px);
    /* Held at the docked height so the links do not move. Without it the bar is only as tall as
       the link text until the mark arrives, then grows by the mark's height and drops the links
       14px mid-transition -- a jump the eye reads as a glitch in the morph. 92/340 is the
       wordmark's aspect. */
    min-height: var(--mark-h);
    font:
        400 20px/1 var(--font-pixel);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    text-shadow: 2px 2px 0 var(--bg-0);
}
.links { margin-left: auto; }
/* The plate runs on --plate, not --dock, because they want different timing. The mark travels
   across the stage and is seated by the end of it; the bar has no reason to exist until the
   stage is gone, so --plate does not start until --dock has finished and then fades over a short
   run of the content. Sharing one factor put a half-lit bar over the scene the whole way down. */
nav {
    background: rgba(11, 13, 17, calc(0.93 * var(--plate, 0)));
    box-shadow: 0 var(--px) 0 0 rgba(47, 55, 66, var(--plate, 0));
}
nav a {
    color: var(--cream);
    text-decoration: none;
    margin-left: 28px;
}
nav a:hover {
    color: var(--ember-1);
}

/* ---- The pilot ------------------------------------------------------
One canvas over the stage, under the title so the wordmark stays readable. */
#pilot {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    pointer-events: none;
    image-rendering: pixelated;
}
@media (prefers-reduced-motion: reduce) {
    #pilot {
        display: none;
    }
}

/* ---- The ground crowd ------------------------------------------------
   Its own strip, in normal flow after the footer, so the walkers are along the foot of the PAGE.
   They used to share the pilot's canvas, which is inset:0 inside .hero -- so they walked the foot
   of the STAGE and were gone as soon as you scrolled past it.

   position and z-index for the same reason the footer needs them: this is below .content in the
   source and unpositioned blocks paint under .hero, which is fixed at z-index 0. Height comes
   from pilot.js, since it follows the sprite scale, which follows the stage. */
#crowd {
    position: relative;
    z-index: 1;
    display: block;
    width: 100%;
    background: var(--bg-0);
    pointer-events: none;
    image-rendering: pixelated;
}

/* ---- Narrow chrome -------------------------------------------------------
   The nav is one row of four links plus the wordmark, which stops fitting around 560px.
   Tightening the type and the gaps keeps it one row down to phone widths. */
@media (max-width: 560px) {
    nav {
        padding: 12px var(--gutter);
        font-size: 16px;
        letter-spacing: 0.06em;
    }
    nav a { margin-left: 14px; }
    nav { min-height: 0; }      /* the mark is fixed over the bar, not laid out inside it */

    .title { top: 12%; }        /* of the hero, not the viewport, so a floored hero keeps it in proportion */
}

/* On a phone and a tablet the mark is sized by the SCREEN, not by the grid. --art-px follows the
   stage and the stage follows viewport width in portrait, so the grid term lands around 115px on
   a 390 phone and 250px on an 820 tablet -- a third of the screen and under half of it. The
   fixed 340px floor fixed the phone and left the tablet, because a fixed floor cannot scale.

   min(88vw, 600px) scales instead: 343px on a 390 phone, 600 on a tablet either way up. The cap
   stops it running away on the widest screens this rule still covers.

   Worth knowing: this does not meet the desktop rule smoothly. Crossing 1024 the mark steps from
   600 to about 300, because the grid term only catches up with a 600px floor somewhere past a
   2000px viewport. No device sits at that seam -- it is only visible while dragging a window
   across it. */
@media (max-width: 1024px) {
    .title { --lockup: min(88vw, 600px); }
}

/* ---- Portrait chrome -----------------------------------------------------
   Must sit AFTER the page-chrome rules above: a media query adds no specificity, so an
   override placed before them would lose on source order.

   The wordmark centres over the stacked composition instead of hugging the left edge. Both lines
   are images now, so they centre with auto margins -- the old text-indent nudge is gone with the
   letter-spacing that made it necessary. */
@media (max-aspect-ratio: 6/5) {
    /* Centred over the stacked composition. --hx is the mark's left at rest, so centring is
       just (screen - mark) / 2; it still travels to --nx on the same factor. */
    .title { --hx: calc((100vw - var(--lockup)) / 2); }
}

/* ---- Footer ------------------------------------------------------------
   The home page links tokens, base and this file only -- page.css is sub-page
   chrome -- so its footer had no rules of its own and sat on browser defaults.
   That went unnoticed while the mark was a line of text. It stops being
   survivable the moment the mark is an image, which renders at its intrinsic
   640px unless something says otherwise. These mirror page.css so the two
   footers agree. */
/* position + z-index are what make it visible at all. The footer is a sibling of .content, not a
   child, so it never inherited the sheet's z-index: 1 -- and as the only unpositioned block on the
   page it painted BELOW .hero, which is fixed at z-index: 0. Scroll to the bottom on main and the
   footer is there in the layout, underneath the stage and the scroll hint. It has been invisible
   as long as the page has existed; a line of grey text is easy to not miss. */
footer { position: relative; z-index: 1; background: var(--bg-0);
         padding: 3rem var(--gutter) 4rem; box-shadow: 0 calc(-1*var(--px)) 0 0 var(--rock-1); }
footer .bar { max-width: 68rem; margin: 0 auto; display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; align-items: center; }
footer, footer a { font: 400 18px/1.4 var(--font-pixel); letter-spacing: .12em; text-transform: uppercase; color: var(--snow-3); }
footer a { color: var(--snow-2); }
footer .mark { display: flex; align-items: center; gap: .9rem; }
footer .mark .footmark { width: 260px; height: auto; display: block;
                         image-rendering: pixelated; }
