/* ─────────────────────────────────────────────────────────────────────────────
   northraine.com

   ONE DECISION SETS THE WHOLE PAGE: the page chrome is strictly achromatic.

   The field is a full-spectrum additive point cloud whose default palette sweeps
   the entire hue circle and which the visitor can rotate 360 degrees with the
   palette-shift slider. Any accent colour chosen here would clash with it at some
   setting and, worse, would compete with it at all of them. So the interface holds
   no hue at all — greys from near-black to a cool off-white — and every state that
   would normally be carried by colour (focus, hover, selection) is carried by
   luminance, weight and spacing instead. The only colour on the page comes out of
   the canvas.

   TYPE carries the rest. Two roles:
     * a light grotesque, very large and close-set, for the wordmark — light rather
       than bold because the thing behind it is already loud, and a heavy wordmark
       over a glow reads as a sticker rather than as a plate;
     * a small wide-tracked mono for everything else. That is not decoration: the
       drawer is a shader control panel full of numeric readouts, and the mono is
       the vernacular the subject actually speaks. It also gives tabular figures, so
       a slider readout does not jitter its own row as the value changes.

   No webfont. A woff2 would be another render-blocking request for a page whose
   whole job is to get a canvas on screen quickly, and the treatment above — scale
   contrast, tracking, weight — is where the character lives regardless of which
   grotesque the OS hands over.
   ───────────────────────────────────────────────────────────────────────────── */

/* ── Tokens ─────────────────────────────────────────────────────────────────── */

:root {
    /* Near-black, not black. The blend is additive: a point contributes light on
       top of whatever is already in the framebuffer, so on pure #000 the dimmest
       points have nothing to sit on and the cloud's outer haze quantises into
       visible banding. A few counts of lift, cooled slightly toward blue, gives the
       falloff somewhere to land and reads as depth rather than as a grey screen. */
    --ink:    #06080B;
    --ink-2:  #0B0F14;   /* drawer body — the opaque fallback where there is no backdrop-filter */

    --vellum: #E9EFF3;   /* primary type. Cool off-white, never #FFF: pure white on near-black
                            haloes at large sizes and fights the field's own highlights. */
    --haze:   #A6B2BC;   /* secondary type — tagline, contact, values */
    --dim:    #8D99A4;   /* tertiary — hints and hint-sized labels. Do not darken it. 6.6:1 on
                            --ink-2, and 5.4:1 against the drawer at its worst — that second
                            number is the binding one: the panel is translucent over an
                            additive cloud that reaches pure white, so the surface under a
                            12px description is not --ink-2 but --ink-2 composited over a blown
                            backdrop. The previous #7C8792 measured 1.67:1 there. Raising the
                            drawer's alpha alone could not reach 4.5:1 without making the panel
                            effectively opaque, so the two moved together. */

    --line:   rgba(233, 239, 243, 0.13);   /* hairlines and inert tracks */
    --line-2: rgba(233, 239, 243, 0.26);   /* hairlines that mark something interactive */

    --sans: "Helvetica Neue", Helvetica, Inter, system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
    --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Roboto Mono", "DejaVu Sans Mono", monospace;

    /* The drawer's width is a variable because the rail slides left by exactly this
       much when the drawer opens, so the two can never disagree. */
    --drawer-w: 372px;

    /* The lane on the right edge that the rail occupies. The hero reserves it as
       padding at every width, which is how "the hero must never overlap the toggle"
       is enforced structurally rather than by hoping the copy stays short. */
    --rail-lane: clamp(58px, 9vw, 84px);

    --ease: cubic-bezier(0.22, 0.68, 0.35, 1);

    color-scheme: dark;
}

/* ── Base ───────────────────────────────────────────────────────────────────── */

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

html, body {
    height: 100%;
    margin: 0;
}

body {
    /* The no-WebGL fallback, and the floor the canvas paints over on success. Two
       pools rather than one flat colour so the page still has a light source and a
       direction even when nothing is animating: a cool one low-left, under the copy,
       and a fainter one high-right where the form would be. */
    background-color: var(--ink);
    background-image:
        radial-gradient(120% 80% at 12% 104%, #101822 0%, rgba(16, 24, 34, 0) 62%),
        radial-gradient(90%  70% at 84% -8%,  #0E141C 0%, rgba(14, 20, 28, 0) 58%);
    background-attachment: fixed;

    color: var(--vellum);
    font-family: var(--sans);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;

    /* One page, no document scroll. Everything that scrolls does so inside the
       drawer, which owns its own overflow. */
    overflow: hidden;
    -webkit-tap-highlight-color: transparent;
}

/* Selection is the one place a block of solid light is allowed, and it is inverted
   rather than tinted so it stays inside the achromatic rule. */
::selection { background: var(--vellum); color: var(--ink); }

/* Visible, high-contrast, and only for keyboard users. Offset so the ring never
   sits on top of the glyphs it is marking. */
:focus-visible {
    outline: 2px solid var(--vellum);
    outline-offset: 3px;
    border-radius: 2px;
}
:focus:not(:focus-visible) { outline: none; }

[hidden] { display: none !important; }

/* ── The field ──────────────────────────────────────────────────────────────── */

#field {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
    z-index: 0;

    /* The field is the interactive surface: drag to orbit, wheel to dolly. Touch
       must not be interpreted as a page scroll or the orbit gesture is stolen by
       the browser before the canvas sees a single move event. */
    touch-action: none;
    cursor: grab;
}
#field:active { cursor: grabbing; }

.veil {
    position: fixed;
    inset: 0;
    z-index: 1;
    pointer-events: none;

    /* Anchored to the corner the copy occupies and dying out by two-thirds of the
       way across, so the form in the middle of the frame is never dimmed. A solid
       panel behind the text would have been easier and would have cost the page the
       one thing it is for. The second stop is deliberately not black — it darkens
       toward the same lifted near-black as the page floor, so the falloff never
       shows an edge against the background. */
    background:
        radial-gradient(115% 78% at 6% 100%,
            rgba(4, 6, 9, 0.92) 0%,
            rgba(4, 6, 9, 0.62) 34%,
            rgba(4, 6, 9, 0.00) 72%),
        linear-gradient(to top,
            rgba(4, 6, 9, 0.55) 0%,
            rgba(4, 6, 9, 0.00) 34%);
}

/* ── Hero ───────────────────────────────────────────────────────────────────── */

.hero {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 2;

    display: flex;
    flex-direction: column;
    align-items: flex-start;

    padding-inline-start: clamp(22px, 6vw, 84px);
    padding-inline-end: var(--rail-lane);
    padding-bottom: max(clamp(28px, 7vh, 68px), env(safe-area-inset-bottom));

    /* Bottom-left, not centred. The form occupies the middle of the viewport at
       every default; centred copy sits inside the densest part of the cloud and
       both lose. Down here the copy has the darkest part of the frame to itself and
       the silhouette above it stays unbroken — a plate caption rather than a title
       card. */

    /* Every event belongs to the field underneath, so dragging across the wordmark
       still orbits. The mailto link takes its own back. */
    pointer-events: none;
}

.wordmark {
    margin: 0;
    font-weight: 300;
    font-size: clamp(3.1rem, 12.5vw, 8.75rem);
    line-height: 0.86;

    /* Negative tracking at display size only. Grotesques are spaced for text, and at
       8rem the default gaps read as holes; pulling them in is what makes the word
       read as one mark instead of nine letters. It is scale-dependent — the mono
       below goes the other way for exactly the same reason. */
    letter-spacing: -0.035em;

    color: var(--vellum);
    /* The clamp above is sized so the mark fits at 360px with the rail lane
       reserved; nowrap makes that a hard guarantee rather than a hope if the
       wordmark is ever edited into two words. */
    white-space: nowrap;
}

.tagline {
    margin: clamp(16px, 2.4vh, 26px) 0 0;

    font-family: var(--mono);
    font-size: clamp(0.66rem, 1.5vw, 0.78rem);
    font-weight: 400;
    line-height: 1.55;
    text-transform: uppercase;

    /* The scale contrast against the wordmark is roughly 11:1. Wide tracking is what
       keeps a line that small from reading as a footnote at that ratio — it gives
       the line physical width to answer the wordmark with. */
    letter-spacing: 0.28em;
    color: var(--haze);

    max-width: 34ch;
    /* No text-indent here on purpose. Letter-spacing puts its extra space AFTER the
       last glyph, not before the first, so the left edge is already flush with the
       wordmark's N; indenting to "correct" for it would be the thing that breaks the
       alignment. The rail label below is the opposite case — it is centred, so the
       trailing space there does have to be balanced out. */
}

/* No tagline in the markup, so the address follows the wordmark directly. Its own
   margin was measured against a small uppercase line sitting above it; against the
   wordmark's display size that same gap reads as a crowded second line rather than
   a separate piece of information. Scoped to the adjacent-sibling case so restoring
   the tagline restores the original spacing with no other edit. */
.wordmark + .contact {
    margin-top: clamp(30px, 5.2vh, 56px);
}

.contact {
    margin-top: clamp(22px, 4vh, 44px);

    font-family: var(--mono);
    font-size: clamp(0.7rem, 1.4vw, 0.8rem);
    letter-spacing: 0.05em;
    color: var(--haze);
    text-decoration: none;

    /* The one element in the hero that is clickable, so it is the one element that
       takes pointer events back off the field. */
    pointer-events: auto;

    /* The underline is drawn rather than inherited so it can animate from the left
       and sit a real distance below the baseline instead of cutting the descenders. */
    padding-bottom: 4px;
    background-image: linear-gradient(var(--vellum), var(--vellum));
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 100% 1px;
    transition: color 0.25s var(--ease), background-size 0.35s var(--ease);
}
.contact:hover,
.contact:focus-visible {
    color: var(--vellum);
}
@media (hover: hover) {
    .contact { background-size: 0% 1px; }
    .contact:hover,
    .contact:focus-visible { background-size: 100% 1px; }
}

/* The page's only motion, and it happens once. A short staggered settle on load, in
   the reading order of the block, so the page arrives rather than appearing. Opted
   INTO under no-preference rather than disabled under reduce, which means a reduced-
   motion visitor never has the animation defined at all — no chance of a half-applied
   transform leaving the copy offset. */
@media (prefers-reduced-motion: no-preference) {
    .wordmark, .tagline, .contact {
        animation: settle 900ms var(--ease) both;
    }
    .tagline { animation-delay: 90ms; }
    .contact { animation-delay: 170ms; }
    /* With no tagline between them, the address takes the slot the tagline would
       have had. Holding it at 170ms would leave an audible beat of empty space in
       a two-element sequence. */
    .wordmark + .contact { animation-delay: 90ms; }

    @keyframes settle {
        from { opacity: 0; transform: translateY(14px); }
        to   { opacity: 1; transform: none; }
    }
}

/* ── Drawer toggle: the rail ────────────────────────────────────────────────── */

.rail {
    position: fixed;
    top: 50%;
    right: 0;
    z-index: 4;
    transform: translateY(-50%);

    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;

    padding: 22px 20px;
    margin: 0;
    border: 0;
    background: none;
    color: var(--dim);
    cursor: pointer;

    /* background-color rides along so the open-state plate fades in with the slide
       instead of snapping on at frame one. */
    transition:
        color 0.25s var(--ease),
        background-color 0.42s var(--ease),
        transform 0.42s var(--ease);
}
.rail:hover { color: var(--vellum); }

/* The affordance itself: a hairline, not a box. It lengthens and brightens on hover,
   which is the whole interaction — a rectangle with a border here would have been a
   second piece of chrome on a page that can only afford one. */
.rail__mark {
    display: block;
    width: 1px;
    height: 40px;
    background: currentColor;
    opacity: 0.8;

    /* The line grows from its own centre. Done with scaleY on a full-height box
       rather than by animating height: height is a layout property, so growing it
       would reflow the flex column and nudge the label down on every hover — the one
       thing that must stay still, since it is the label you are reading. */
    transform: scaleY(0.65);
    transform-origin: 50% 50%;
    transition: transform 0.32s var(--ease), opacity 0.25s var(--ease);
}
.rail:hover .rail__mark,
.rail:focus-visible .rail__mark { transform: scaleY(1); opacity: 1; }

.rail__label {
    font-family: var(--mono);
    font-size: 0.62rem;
    text-transform: uppercase;
    letter-spacing: 0.32em;

    /* Vertical, reading bottom-to-top — the orientation a tab on a right edge wants,
       and the reason this reads as an instrument panel tab rather than a button that
       happened to be pushed into the corner. */
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    text-indent: 0.32em;   /* recover the trailing letter-space, so the run is centred */
}

/* Open: the rail rides out with the drawer so it stays reachable as the way back.
   Styled off aria-expanded because that attribute has to be correct anyway — it is
   the one piece of drawer state that cannot silently rot. */
/* Closed, the rail sits on the right edge, which is the darkest part of the frame.
   Open, it has moved a full panel-width inward — at 900px that is the middle of the
   viewport, which is exactly where the form is and where the cloud saturates to
   white. Measured there with the rail itself hidden, the label ran at 2.16:1 over
   its brightest backdrop and 83% of its box was under 4.5:1, so the word CONTROLS
   was not readable at the moment it became the only way back.

   Two changes, both only in the open state. The colour goes UP to --vellum rather
   than down to --haze — a control that has just been moved onto the brightest thing
   on the page is the last one to dim. And the label gets its own plate, so it is
   never read off the field at all: the same near-black and blur as the panel it is
   now attached to, rounded on the outer side only so it reads as a tab growing out
   of the drawer's edge rather than as a floating pill. */
.rail[aria-expanded="true"] {
    transform: translateY(-50%) translateX(calc(-1 * var(--drawer-w)));
    color: var(--vellum);

    background: rgba(6, 8, 11, 0.72);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    border-radius: 3px 0 0 3px;
}

/* ── Drawer ─────────────────────────────────────────────────────────────────── */

.drawer {
    position: fixed;
    top: 0;
    right: 0;
    z-index: 5;

    width: var(--drawer-w);
    max-width: 100vw;
    height: 100%;
    display: flex;
    flex-direction: column;

    /* Opaque enough to read against a bright form, translucent enough that the form
       keeps moving behind it. This value is the fallback and stands on its own; the
       backdrop-filter rule below lowers it where the filter exists, because a blur
       buys legibility that opacity would otherwise have to pay for. */
    background: rgba(11, 15, 20, 0.94);
    border-left: 1px solid var(--line);

    /* Closed. visibility, not just transform: an off-canvas panel that is still
       "visible" stays in the tab order and in the accessibility tree, so a keyboard
       user tabs off the toggle straight into a drawer they cannot see. The delayed
       visibility transition lets the slide finish before it is taken away. */
    transform: translateX(100%);
    visibility: hidden;
    transition:
        transform 0.42s var(--ease),
        visibility 0s linear 0.42s;
}

/* The blur buys some legibility, but far less than it looks like it does, and the
   alpha here is set by the WORST case rather than the typical one: the field is an
   additive point cloud that saturates to pure white over the middle of the frame,
   so a 12px description can end up sitting on a fully blown backdrop. Measured with
   the panel's contents hidden and the composited backdrop sampled, 0.62 put
   .option__desc at 1.67:1 — the one contrast failure on the page and not a marginal
   one. Working back from 4.5:1 for --dim over white: 0.88 gives 4.03, 0.90 gives
   5.41 once --dim is lifted alongside it (see the token). 0.90 is the number, and
   the 10% that still shows through is enough to see the form move behind the panel,
   which is all the translucency was ever for. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .drawer {
        background: rgba(9, 13, 18, 0.90);
        -webkit-backdrop-filter: blur(22px) saturate(135%);
        backdrop-filter: blur(22px) saturate(135%);
    }
}

/* Open. Three hooks for the same state so assets/ui.js can use whichever it already
   reaches for; they are equivalent and setting any one of them opens the drawer. */
.drawer.is-open,
.drawer[data-open="true"],
.drawer[aria-hidden="false"] {
    transform: translateX(0);
    visibility: visible;
    transition:
        transform 0.42s var(--ease),
        visibility 0s linear 0s;
}

.drawer__head {
    flex: 0 0 auto;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;

    padding: max(22px, env(safe-area-inset-top)) 22px 16px;
    border-bottom: 1px solid var(--line);
}

.drawer__title {
    margin: 0;
    font-family: var(--mono);
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.3em;
    color: var(--haze);
}

.drawer__close {
    /* Padded to a 44px-tall target and pulled back out again with an equal negative
       margin, so the word sits exactly where it did optically while the hit area is
       four times the size. This is the ONLY way out of the sheet on a phone: the
       sheet is 100vw so there is no outside for ui.js's outside-pointerdown to fire
       on, the rail is deliberately off-screen at that width, and there is no Esc
       key. It was rendering 45x20 against a 24x24 minimum. */
    padding: 12px 10px;
    margin: -12px -10px;
    border: 0;
    background: none;
    cursor: pointer;

    font-family: var(--mono);
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: var(--dim);
    transition: color 0.2s var(--ease);
}
.drawer__close:hover { color: var(--vellum); }

.drawer__scroll {
    flex: 1 1 auto;
    overflow-y: auto;

    /* Reaching the end of the drawer's own scroll must not start scrolling the page
       behind it, which on touch would drag the field out from under the panel. */
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;

    padding: 4px 22px max(34px, env(safe-area-inset-bottom));
}

/* Thin, achromatic, and only inside the drawer. */
.drawer__scroll { scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
.drawer__scroll::-webkit-scrollbar { width: 8px; }
.drawer__scroll::-webkit-scrollbar-thumb {
    background: var(--line-2);
    border-radius: 4px;
    border: 2px solid transparent;
    background-clip: content-box;
}
.drawer__scroll::-webkit-scrollbar-track { background: transparent; }

/* ── Groups ─────────────────────────────────────────────────────────────────── */

.group { padding: 26px 0; }
.group + .group { border-top: 1px solid var(--line); }

.group__title {
    margin: 0 0 18px;
    font-family: var(--mono);
    font-size: 0.62rem;
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: 0.3em;
    color: var(--dim);
}

/* A hint that belongs to the whole group rather than to one control sits between
   the heading and the body. The heading's 18px is spacing meant for the rows, so it
   is pulled back and the hint carries the gap instead — otherwise the line floats
   between the two and reads as belonging to neither. */
.group__title:has(+ .hint) { margin-bottom: 8px; }
.group__title + .hint { margin: 0 0 16px; }

.group__body {
    display: flex;
    flex-direction: column;
    gap: 22px;
}

/* ── Control primitives ─────────────────────────────────────────────────────────
   Styled on the ELEMENT first and the class second, so a control still looks right
   if assets/ui.js emits a shape slightly different from the one assumed here. The
   classes exist to add layout, not to make the base legible.
   ───────────────────────────────────────────────────────────────────────────── */

.control {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.control__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
}

/* Scoped to slider rows rather than every label in the drawer: an option row may
   itself BE a <label class="option">, and a blanket `.group__body label` would set
   that whole row — description text included — in uppercase mono at a specificity
   the .option rules below could not undo. */
.control__label,
.control label,
.group__body > label:not(.option) {
    font-family: var(--mono);
    font-size: 0.68rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--haze);
}

.control__value {
    font-family: var(--mono);
    font-size: 0.68rem;
    letter-spacing: 0.06em;
    color: var(--vellum);

    /* Tabular figures so a value changing under a drag does not shuffle its own
       digits sideways. This is the readout of a live uniform; it moves constantly. */
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

.control__hint,
.hint {
    margin: 0;
    font-family: var(--sans);
    font-size: 0.75rem;
    line-height: 1.5;
    letter-spacing: 0.005em;
    color: var(--dim);
    max-width: 42ch;
}

/* ── Range ──────────────────────────────────────────────────────────────────────
   A browser-default range would undo the rest of the page on its own, so both
   vendors are styled explicitly.

   The filled portion is drawn as a gradient on the track and driven by an optional
   `--fill` custom property in the 0..1 range, which assets/ui.js may set on the
   input (WebKit has no ::-webkit-slider-progress, so there is no other way to fill
   both engines from one rule). Unset, --fill resolves to 0 and the track is a plain
   hairline — correct-looking either way, never broken.
   ───────────────────────────────────────────────────────────────────────────── */

input[type="range"] {
    --fill: 0;
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    /* 34, not the 20 the visual needs. The track is a 2px hairline and the thumb is
       12px, so 20px was enough to DRAW but under the 24px minimum to HIT, and there
       are sixteen of these once Compose is open. Both engines centre the track in
       the input's box, so the extra height is transparent padding above and below a
       hairline that has not moved. */
    height: 34px;
    margin: 0;
    padding: 0;
    background: transparent;
    cursor: pointer;
    display: block;
}

input[type="range"]::-webkit-slider-runnable-track {
    height: 2px;
    border: 0;
    border-radius: 1px;
    background: linear-gradient(to right,
        var(--vellum) 0 calc(var(--fill) * 100%),
        var(--line-2) calc(var(--fill) * 100%) 100%);
}
input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 12px;
    height: 12px;
    /* (track height - thumb height) / 2 = (2 - 12) / 2 */
    margin-top: -5px;
    border: 0;
    border-radius: 50%;
    background: var(--vellum);
    box-shadow: 0 0 0 0 rgba(233, 239, 243, 0.16);
    transition: box-shadow 0.2s var(--ease), transform 0.15s var(--ease);
}

input[type="range"]::-moz-range-track {
    height: 2px;
    border: 0;
    border-radius: 1px;
    background: linear-gradient(to right,
        var(--vellum) 0 calc(var(--fill) * 100%),
        var(--line-2) calc(var(--fill) * 100%) 100%);
}
input[type="range"]::-moz-range-thumb {
    width: 12px;
    height: 12px;
    border: 0;
    border-radius: 50%;
    background: var(--vellum);
    box-shadow: 0 0 0 0 rgba(233, 239, 243, 0.16);
    transition: box-shadow 0.2s var(--ease), transform 0.15s var(--ease);
}
/* Firefox draws its own filled progress; suppressed so both engines agree that the
   fill comes from --fill and nowhere else. */
input[type="range"]::-moz-range-progress { background: transparent; height: 2px; }

input[type="range"]:hover::-webkit-slider-thumb,
input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 7px rgba(233, 239, 243, 0.14); }
input[type="range"]:hover::-moz-range-thumb,
input[type="range"]:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 7px rgba(233, 239, 243, 0.14); }

input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.15); }
input[type="range"]:active::-moz-range-thumb { transform: scale(1.15); }

/* The thumb is the focus indicator here — the default ring would draw a rounded
   rectangle around a 34px-tall transparent strip and look like a mistake. */
input[type="range"]:focus-visible { outline: none; }

/* ── Select ─────────────────────────────────────────────────────────────────── */

select {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    padding: 9px 34px 9px 12px;

    font-family: var(--mono);
    font-size: 0.72rem;
    letter-spacing: 0.08em;
    color: var(--vellum);

    background-color: rgba(233, 239, 243, 0.05);
    /* Chevron drawn inline: no external host, no icon font, one request fewer. */
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2010%206'%3E%3Cpath%20d='M1%201l4%204%204-4'%20fill='none'%20stroke='%23A6B2BC'%20stroke-width='1.2'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 10px 6px;

    border: 1px solid var(--line);
    border-radius: 2px;
    cursor: pointer;
    transition: border-color 0.2s var(--ease), background-color 0.2s var(--ease);
}
select:hover { border-color: var(--line-2); background-color: rgba(233, 239, 243, 0.08); }

/* The popup list is drawn by the OS and inherits nothing useful; setting it here is
   what stops a white dropdown flashing out of a black page on Windows and Linux. */
select option { background: var(--ink-2); color: var(--vellum); }

/* ── Radio and checkbox groups ──────────────────────────────────────────────────
   The Form and Pointer groups are lists of named options with a line of description
   each, which is a radio group and should be marked up as one. The layout puts the
   control in a fixed first column and lets EVERY other child stack in the second,
   so the row survives whatever ui.js emits into it — a label, a name span and a
   description, in any combination.
   ───────────────────────────────────────────────────────────────────────────── */

fieldset,
.options {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: 0;
    padding: 0;
    border: 0;
}

legend,
.options__legend {
    padding: 0;
    margin-bottom: 10px;
    font-family: var(--mono);
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: var(--haze);
}

.option {
    display: grid;
    grid-template-columns: 13px 1fr;
    column-gap: 12px;
    row-gap: 3px;
    align-items: start;

    padding: 9px 10px 10px;
    margin: 0 -10px;
    border-radius: 3px;
    cursor: pointer;
    transition: background-color 0.18s var(--ease);
}
.option:hover { background: rgba(233, 239, 243, 0.045); }

.option > input[type="radio"],
.option > input[type="checkbox"] {
    grid-column: 1;
    grid-row: 1;
    margin: 4px 0 0;
}
/* Everything that is not the input flows down the second column in source order. */
.option > *:not(input) { grid-column: 2; }

.option__name,
.option > label {
    font-family: var(--mono);
    font-size: 0.72rem;
    letter-spacing: 0.1em;
    color: var(--haze);
    cursor: pointer;
    transition: color 0.18s var(--ease);
    text-transform: none;
}

.option__desc {
    font-family: var(--sans);
    font-size: 0.75rem;
    line-height: 1.5;
    color: var(--dim);
    margin: 0;
}

/* Attribution for the two variants that are ports of someone else's sketch. Set
   apart from the description by size and case so it does not read as more of it. */
.option__credit {
    font-family: var(--mono);
    font-size: 0.62rem;
    letter-spacing: 0.08em;
    color: var(--dim);
    opacity: 0.75;
    margin: 0;
}

/* Only the SELECTED row carries its description.
   Twenty-four two-line descriptions made the Form group 1887px tall in an 841px
   panel and pushed the first Field slider 2006px down — two and a half screens of
   scrolling to reach the point count, which is the control most visitors want
   first. Twenty-three of those lines describe a form that is not on screen. The
   select-shaped controls in this same panel already work this way: buildSelect
   renders one .hint, for the current choice only. This makes the radio lists agree
   with them, and drops Form to roughly a third of its height.
   Browsing by description is not the loss it looks like — selecting a row shows the
   actual form instantly, and the arrow-key shortcut named under the Form heading
   steps through all twenty-four without touching the panel at all. */
.option:not(:has(input:checked)) .option__desc,
.option:not(:has(input:checked)) .option__credit { display: none; }

/* Selection is luminance, not colour, per the palette rule at the top. */
.option:has(input:checked) { background: rgba(233, 239, 243, 0.07); }
.option:has(input:checked) .option__name,
.option:has(input:checked) > label { color: var(--vellum); }

input[type="radio"],
input[type="checkbox"] {
    -webkit-appearance: none;
    appearance: none;
    width: 13px;
    height: 13px;
    flex: 0 0 auto;
    margin: 0;
    border: 1px solid var(--line-2);
    background: transparent;
    cursor: pointer;
    display: inline-grid;
    place-content: center;
    transition: border-color 0.18s var(--ease);
}
input[type="radio"] { border-radius: 50%; }
input[type="checkbox"] { border-radius: 2px; }

input[type="radio"]::before,
input[type="checkbox"]::before {
    content: "";
    width: 7px;
    height: 7px;
    border-radius: inherit;
    background: var(--vellum);
    transform: scale(0);
    transition: transform 0.16s var(--ease);
}
input[type="radio"]:checked,
input[type="checkbox"]:checked { border-color: var(--vellum); }
input[type="radio"]:checked::before,
input[type="checkbox"]:checked::before { transform: scale(1); }

input[type="radio"]:hover,
input[type="checkbox"]:hover { border-color: var(--haze); }

/* Buttons emitted into the drawer (a seed reroll, a reset) get the same hairline
   treatment as the drawer's own close control rather than a filled pill. */
.group__body button {
    align-self: flex-start;
    padding: 8px 14px;
    border: 1px solid var(--line);
    border-radius: 2px;
    background: transparent;
    cursor: pointer;

    font-family: var(--mono);
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--haze);
    transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.group__body button:hover { color: var(--vellum); border-color: var(--line-2); }

/* ── Inert rows ─────────────────────────────────────────────────────────────────
   assets/ui.js puts `is-inactive` on a row whose control does nothing in the current
   configuration — the seed off Improv, the pointer radius and strength on Still — and
   disables its inputs at the same time. It dims rather than hides so the drawer does
   not change height every time the selected form changes, and so the control keeps
   the position people have learned it sits in.

   Opacity alone, not a colour change: the row is built from four different type
   colours and dimming the whole block keeps their relationship intact. 0.38 is low
   enough to read as "not now" at a glance and still high enough that the label can be
   read, which matters — the row is the explanation of why it is off.
   ───────────────────────────────────────────────────────────────────────────── */

.control.is-inactive,
.is-inactive {
    opacity: 0.38;
    transition: opacity 0.2s var(--ease);
}

/* The disabled input keeps the row's cursor honest. Without this the track still
   shows a grab cursor and invites a drag that cannot happen. */
.is-inactive input,
.is-inactive select,
.is-inactive button,
.is-inactive .control__label { cursor: not-allowed; }

/* ── Responsive ─────────────────────────────────────────────────────────────── */

/* Under 900px the drawer is a full-height sheet across the whole viewport.
   Once it is full width the rail cannot ride out with it — it would go off-screen —
   so the rail hides while open and the sheet's own Close button becomes the way
   back, which is why that button exists in the markup.

   900px, not 640px, and the number is the wordmark's. The hero reserves only
   --rail-lane (58-84px) on the right, so between 640px and the width at which a
   372px panel clears the mark there is a band where the panel is a fixed overlay
   and the hero is not moved at all: measured, "Northraine" is cut through the final
   letters at 768px and still touched at 834px. Zero clearance arrives at 900px.
   Shifting the hero instead does not work here — the wordmark is nowrap at
   12.5vw, so at 700px it is wider than what a 372px panel leaves behind it, and
   the only way to make it fit would be to shrink the one piece of type the page is
   built around. So the sheet takes over the whole viewport from 899px down.

   Full width also means there is nothing left to see behind the panel, so the
   translucency is dropped with it: it would be buying a view of a field the sheet
   is already covering, at the cost of the panel's legibility. */
@media (max-width: 899px) {
    :root { --drawer-w: 100vw; }

    .drawer {
        border-left: 0;
        background: var(--ink-2);
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }

    .rail[aria-expanded="true"] {
        transform: translateY(-50%) translateX(100%);
        pointer-events: none;
    }

    .drawer__head { padding-inline: 20px; }
    .drawer__scroll { padding-inline: 20px; }
}

/* Very narrow. The wordmark is set nowrap so it can never break "Northraine" across
   two lines, which means at 360px the clamp has to be the thing that keeps it inside
   the frame; 12.5vw does that with the hero's own padding accounted for. Tracking
   opens back up a little because the negative display tracking is only correct at
   display size. */
@media (max-width: 400px) {
    .wordmark { letter-spacing: -0.025em; }
    .tagline { letter-spacing: 0.22em; }
}

/* Short and wide — a laptop in landscape, or a phone on its side. The hero's bottom
   padding is the first thing to give, since vertical room is what is scarce. */
@media (max-height: 520px) {
    .hero { padding-bottom: max(18px, env(safe-area-inset-bottom)); }
    .wordmark { font-size: clamp(2.4rem, 9vh, 4.6rem); }
    .tagline { margin-top: 12px; }
    .contact { margin-top: 16px; }
}

/* ── Reduced motion ─────────────────────────────────────────────────────────────
   The load sequence is already opted into above and simply does not exist here. What
   remains is the drawer slide and the small hover transitions; the drawer keeps its
   visibility switch (removing it would leave a permanently focusable off-canvas
   panel) but arrives instantly instead of sliding.
   ───────────────────────────────────────────────────────────────────────────── */

@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;
    }
    .drawer { transition: visibility 0s linear 0s !important; }
}
