/* ===========================================================================
   Depth — the scroll-driven 3D effect.

   The camera flies forward. Your name is not painted behind the page as
   decoration; it is a real object in front of the lens, and scrolling takes you
   through it — the letters part around you, blur as they pass the focal plane,
   and are gone. Behind them the sections resolve out of the depth, each pulling
   into focus as it arrives.

   The whole thing is drawn with light and focus rather than pattern. There is
   no texture behind the text, because six ghost copies of a word behind a
   paragraph is noise, and noise is the opposite of elegant.

   Rules this file keeps:

   1. Almost everything animated is `transform` or `opacity` — GPU-composited,
      so scrolling never triggers layout. `filter: blur()` is the one exception
      and it is deliberately rationed; see the note above `.depth-letter`.
   2. Reading is never disturbed. Article bodies and the admin editor get no
      motion at all — see the `data-depth` modes at the bottom.
   3. `prefers-reduced-motion` disables everything. Z-axis motion and focus
      pulls are a common migraine and vertigo trigger, so this is a hard
      requirement, not a nicety.

   Scroll position arrives from depth.js as two custom properties —
   `--page-progress` and `--hero-fly` — so all the maths lives in one
   rAF-throttled handler and all the look lives here.
   =========================================================================== */

:root {
    --depth-light: rgba(31, 62, 147, .07);
    --depth-vignette: rgba(22, 21, 15, .10);
    --star: rgba(31, 62, 147, .30);
    --star-opacity: .30;
}

@media (prefers-color-scheme: dark) {
    :root {
        --depth-light: rgba(127, 212, 245, .10);
        --depth-vignette: rgba(0, 6, 16, .55);
        --star: rgba(206, 232, 252, .95);
        --star-opacity: .62;
    }
}

/* ---------------------------------------------------------------------------
   The space behind the page.

   Two gradients and nothing else: a light source at the far end of the run that
   swells as you approach it, and a vignette that curves the frame like a lens.
   Depth without a single repeating pattern.
   --------------------------------------------------------------------------- */

.depth-backdrop {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
    contain: strict;
}

.depth-light {
    position: absolute;
    inset: -10%;
    background: radial-gradient(ellipse 48% 38% at 50% 46%, var(--depth-light), transparent 70%);
    /* Growing and brightening together is what sells approach: a light that
       only brightens reads as a lamp being turned up, not as one you are
       walking toward. */
    transform: scale(calc(1 + var(--page-progress, 0) * .55));
    opacity: calc(.3 + var(--page-progress, 0) * .7);
    will-change: transform, opacity;
}

.depth-vignette {
    position: absolute;
    inset: 0;
    background: radial-gradient(ellipse 78% 68% at 50% 50%, transparent 35%, var(--depth-vignette) 100%);
}

/* ---------------------------------------------------------------------------
   Starfield.

   Each layer is a tile of small radial-gradient dots, repeated across the
   viewport. Two layers at different tile sizes and drift rates keeps the
   pattern from reading as a repeat, and gives the field some parallax depth.

   Tuned to be restful rather than busy:

     - The twinkle is a slow breathe between two close opacities over 14-19
       seconds. Anything faster, or with a wider range, starts to flicker in
       peripheral vision, and flicker in the corner of the eye is exactly the
       thing that makes a page tiring to sit with.
     - Dots are sub-pixel-soft and low contrast. They read as texture at a
       glance and resolve into stars only if you look for them.
     - The two layers breathe on different periods, so they never pulse in
       unison — a synchronised field reads as one object blinking.
     - Drift is tied to scroll, not to a clock. Nothing moves while you are
       reading something.
   --------------------------------------------------------------------------- */

.depth-stars {
    position: absolute;
    inset: -15%;
    opacity: var(--star-opacity);
    will-change: transform, opacity;
}

.depth-stars-near {
    background-image:
        radial-gradient(1.4px 1.4px at 12% 18%, var(--star), transparent 100%),
        radial-gradient(1.1px 1.1px at 47% 9%,  var(--star), transparent 100%),
        radial-gradient(1.6px 1.6px at 78% 27%, var(--star), transparent 100%),
        radial-gradient(1px 1px     at 31% 52%, var(--star), transparent 100%),
        radial-gradient(1.3px 1.3px at 66% 68%, var(--star), transparent 100%),
        radial-gradient(1.1px 1.1px at 89% 81%, var(--star), transparent 100%),
        radial-gradient(1.5px 1.5px at 21% 88%, var(--star), transparent 100%);
    background-size: 420px 340px;
    transform: translateY(calc(var(--page-progress, 0) * -52px));
    animation: breathe-near 16s ease-in-out infinite;
}

.depth-stars-far {
    background-image:
        radial-gradient(1px 1px     at 8% 41%,  var(--star), transparent 100%),
        radial-gradient(.9px .9px   at 38% 73%, var(--star), transparent 100%),
        radial-gradient(1.1px 1.1px at 58% 22%, var(--star), transparent 100%),
        radial-gradient(.8px .8px   at 83% 55%, var(--star), transparent 100%),
        radial-gradient(1px 1px     at 71% 94%, var(--star), transparent 100%),
        radial-gradient(.9px .9px   at 15% 63%, var(--star), transparent 100%);
    background-size: 270px 230px;
    opacity: calc(var(--star-opacity) * .6);
    transform: translateY(calc(var(--page-progress, 0) * -22px));
    animation: breathe-far 21s ease-in-out infinite;
}

@keyframes breathe-near {
    0%, 100% { opacity: var(--star-opacity); }
    50%      { opacity: calc(var(--star-opacity) * .68); }
}

@keyframes breathe-far {
    0%, 100% { opacity: calc(var(--star-opacity) * .6); }
    50%      { opacity: calc(var(--star-opacity) * .95); }
}

/* Content rides above the backdrop. */
body > header,
body > main,
body > footer {
    position: relative;
    z-index: 1;
}

/* ---------------------------------------------------------------------------
   The spine — how far into the run you are.

   A hairline in the margin with a single travelling node. It replaces the
   usual bar pinned across the top of the window, which always reads as a
   browser chrome element rather than as part of the page.
   --------------------------------------------------------------------------- */

/* The rail already draws a hairline down the page, so the indicator does not
   need a track of its own — the node simply rides that existing edge. */
.depth-spine {
    position: fixed;
    left: calc(var(--rail) - 1px);
    top: 0;
    bottom: 0;
    width: 1px;
    z-index: 25;
    pointer-events: none;
}

.depth-spine-node {
    position: absolute;
    left: 50%;
    width: 5px;
    height: 5px;
    margin-left: -2.5px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 12px var(--accent);
    top: calc(var(--page-progress, 0) * 100%);
    margin-top: -2.5px;
    will-change: top;
}

@media (max-width: 62rem) {
    .depth-spine { display: none; }
}

/* ---------------------------------------------------------------------------
   Flying through the name.

   The heading gets a `perspective`, and each letter is pushed along Z. That one
   move does the whole trick: perspective projection makes letters near the edge
   of the word swing outward faster than letters near the middle, so the name
   opens around the camera on its own. Faking that spread with `translateX`
   looks wrong because the divergence would not match the growth.

   `--edge` (0 at the centre of the word, 1 at the ends) pushes the outer
   letters slightly further, so the word peels apart from the middle instead of
   advancing as one slab. `--tilt` turns each letter to face the centre line.

   `width: fit-content` shrink-wraps the heading so `perspective-origin` lands
   on the middle of the word rather than the middle of a full-width block. Not
   `display: inline-block`, which shrink-wraps too but drops the heading out of
   block flow and lets the badge above it ride up onto the same line.

   The blur is the one place this file spends a non-composited property. It is
   worth it — passing the focal plane is what makes the move read as a camera
   rather than a zoom — and the cost is bounded: a handful of small glyph spans,
   only while the hero is on screen, and never on a block of body text.
   --------------------------------------------------------------------------- */

.depth-hero-name {
    width: fit-content;
    perspective: 780px;
    transform-style: preserve-3d;
}

/* ---------------------------------------------------------------------------
   Typing the name in.

   `display: none` rather than `visibility: hidden` for the letters not yet
   typed. Hidden letters still occupy their space, which would park the caret at
   the end of the finished word from the very first frame — the one thing that
   gives a typing effect away instantly. Removed from flow, the caret sits
   against the last letter actually on screen and advances with it.

   The gate is scoped to `.is-typing`, a class JS adds and then removes when it
   finishes. Nothing is hidden unless the script is actively driving it, so a
   failure anywhere in here leaves a normal, fully-rendered heading.
   --------------------------------------------------------------------------- */

.depth-hero-name.is-typing .depth-letter { display: none; }
.depth-hero-name.is-typing .depth-letter.typed { display: inline-block; }

.hero-caret {
    display: inline-block;
    width: .055em;
    height: .68em;
    margin-left: .07em;
    vertical-align: baseline;
    background: var(--accent);
    box-shadow: 0 0 18px var(--halo);
    /* steps(1), not a fade. A terminal cursor is on or off — easing it makes
       it a design flourish instead of a cursor. */
    animation: caret-blink 1.06s steps(1) infinite;
    /* Goes as soon as you start moving; it belongs to the resting state. */
    opacity: calc(1 - var(--hero-fly, 0) * 2.4);
}

@keyframes caret-blink {
    0%, 49%   { opacity: 1; }
    50%, 100% { opacity: 0; }
}

.depth-letter {
    display: inline-block;
    white-space: pre;
    transform:
        translateZ(calc(var(--hero-fly, 0) * (390px + var(--edge, 0) * 150px)))
        rotateY(calc(var(--tilt, 0deg) * var(--hero-fly, 0)));
    filter: blur(calc(var(--hero-fly, 0) * 7px));
    opacity: calc(1 - var(--hero-fly, 0) * 1.25);
    will-change: transform, opacity;
}

/* The rest of the hero recedes rather than competing with the name. */
.depth-hero-rest {
    transform:
        perspective(1200px)
        translateZ(calc(var(--hero-fly, 0) * -220px))
        translateY(calc(var(--hero-fly, 0) * 18px));
    opacity: calc(1 - var(--hero-fly, 0) * 1.6);
    will-change: transform, opacity;
}

/* ---------------------------------------------------------------------------
   Sections resolve out of the depth.

   A focus pull, not a slide: each section arrives from further down the Z axis
   soft and settles sharp. The blur runs only for the length of the transition
   and then the element is static, so nothing is being repainted while you read.

   The class is added by depth.js, never by the templates. If the script fails
   to load, nothing is ever hidden — the page is simply flat, which is the right
   failure mode for a site whose job is to be read.
   --------------------------------------------------------------------------- */

.depth-reveal {
    opacity: 0;
    transform: perspective(1500px) translateZ(-150px) translateY(26px);
    filter: blur(5px);
    transition:
        opacity .7s cubic-bezier(.22, 1, .36, 1),
        transform .95s cubic-bezier(.22, 1, .36, 1),
        filter .7s cubic-bezier(.22, 1, .36, 1);
    transition-delay: calc(var(--depth-index, 0) * 65ms);
}

.depth-reveal.in-view {
    opacity: 1;
    transform: perspective(1500px) translateZ(0) translateY(0);
    filter: blur(0);
}

/* Gentler for text-heavy pages: focus resolves, but nothing travels. */
.depth-reveal-soft {
    transform: translateY(16px);
    filter: blur(3px);
}

.depth-reveal-soft.in-view {
    transform: translateY(0);
}

/* ---------------------------------------------------------------------------
   Modes, set per page from the controller as `data-depth` on <body>.

     deep    — home and about: the full run
     cards   — list pages: card depth and a focus pull, no fly-through
     reading — articles: the header resolves, the body is left completely alone
     flat    — admin: nothing moves
   --------------------------------------------------------------------------- */

body[data-depth="reading"] .depth-light,
body[data-depth="cards"] .depth-light {
    opacity: calc(.2 + var(--page-progress, 0) * .4);
}

/* An article body must never move, tilt, blur, or fade while it is being read. */
body[data-depth="reading"] .prose,
body[data-depth="reading"] .prose * {
    transform: none !important;
    filter: none !important;
    opacity: 1 !important;
}

body[data-depth="flat"] .depth-backdrop,
body[data-depth="flat"] .depth-spine {
    display: none;
}

/* ---------------------------------------------------------------------------
   The kill switch.

   Anyone who has asked their system for less motion gets a completely static
   site: no fly-through, no focus pulls, no z-axis travel. Content still
   appears — the reveal resolves to its final state rather than being hidden.
   --------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
    .depth-backdrop,
    .depth-spine {
        display: none;
    }

    /* Stars would still be there, just never breathing - but the whole
       backdrop is hidden above, so this is only belt and braces. */
    .depth-stars {
        animation: none;
        transform: none;
    }

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

    .depth-letter,
    .depth-hero-rest,
    .depth-reveal,
    .depth-reveal.in-view {
        opacity: 1;
        transform: none;
        filter: none;
        transition: none;
        will-change: auto;
    }
}
