/* Shared shell.
   Geometry, palette, type scale and grid are taken from the reference's
   published stylesheet rather than estimated from screenshots. Values that
   exist verbatim in the source are marked [ref]. Loaded after styles.css and
   only by index.html, so the case study and about pages are untouched. */

/* Commit Mono — the reference's typeface. SIL OFL, bundled locally from
   fontsource so the page works offline and under file://.
   Licence text in fonts/COMMIT-MONO-LICENSE.txt */
@font-face {
    font-family: commit-mono;
    src: url('fonts/commit-mono-latin-400-normal.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: commit-mono;
    src: url('fonts/commit-mono-latin-700-normal.woff2') format('woff2');
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/* [ref] :root tokens */
.site {
    --base-padding-x: 2em;
    --base-padding-y: 2em;
    --color-primary: #ffffe3;
    --color-background: #10100e;

    /* Derived tiers — the reference only ever uses --color-primary at full
       strength or stepped down through opacity */
    --color-dim: rgba(255, 255, 227, 0.5);
    --color-faint: rgba(255, 255, 227, 0.28);
    --hairline: rgba(255, 255, 227, 0.16);

    /* Shared shell in styles.css reads these */
    --mark: var(--color-primary);
    --accent: var(--color-dim);
}

/* [ref] --base-padding-x: 20vw above 2000px */
@media only screen and (min-width: 2000px) {
    .site { --base-padding-x: 20vw; }
}

/* [ref] --base-padding-x/y: 1em at or below 1024px */
@media only screen and (max-width: 1024px) {
    .site { --base-padding-x: 1em; --base-padding-y: 1em; }
}

/* The site keeps a day mode the reference has no equivalent for; the dark
   values above are the faithful ones */
:root[data-theme="light"] .site {
    --color-primary: #23241a;
    --color-background: #FFF4ED;
    --color-dim: rgba(35, 36, 26, 0.55);
    --color-faint: rgba(35, 36, 26, 0.3);
    --hairline: rgba(35, 36, 26, 0.18);
}

/* [ref] html,body — minus `width:100vw; overflow:hidden`, which the reference
   can afford because it drives scrolling through WebGL. Copying it here would
   make the document unscrollable. */
html,
.site {
    background: var(--color-background);
    color: var(--color-primary);
    overscroll-behavior: none;
    line-height: 1.4;
    font-family: commit-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    text-transform: uppercase;
    font-size: clamp(0.8em, 1.2vw, 1.1em);
    -moz-osx-font-smoothing: grayscale;
    -webkit-font-smoothing: antialiased;
}

.site {
    transition: background-color 0.4s ease, color 0.4s ease;
}

/* [ref] the reference carries a full reset (`font-size:100%; font:inherit`).
   Without it the UA's own h1/h2/h3 scaling leaks through and headings come out
   larger than body copy, which is not how this design works. */
.site h1,
.site h2,
.site h3,
.site h4,
.site p,
.site ul,
.site li,
.site a,
.site dl,
.site dt,
.site dd,
.site figure,
.site figcaption {
    font-size: 100%;
    font-weight: inherit;
    line-height: inherit;
}

/* dd carries a 40px inline start margin from the UA that the reference's own
   reset zeroes out */
.site dd,
.site figure {
    margin: 0;
}

/* [ref] h2,h3 — headings differ from body copy by weight alone, not size */
.site h2,
.site h3 {
    font-weight: 700;
}

/* ---------------------------------------------------------------- Preloader */

.preloader {
    position: fixed;
    inset: 0;
    z-index: 9999;
    background: var(--color-background);
    display: flex;
    align-items: center;
    justify-content: center;
    /* Fallback for a JS failure — the overlay clears itself either way */
    animation: preloader-failsafe 0s linear 6s forwards;
}

.preloader.is-done {
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1), visibility 0s linear 0.7s;
}

@keyframes preloader-failsafe {
    to { opacity: 0; visibility: hidden; }
}

/* An outlined circle that fills from the bottom as the work thumbnails decode,
   landing on the same solid mark the header carries. The ring is the track and
   stays visible throughout: `overflow` clips to the padding box, so the fill is
   masked to the circle while the border sits outside it. */
.preloader-mark {
    position: relative;
    width: clamp(54px, 7vw, 76px);
    aspect-ratio: 1;
    border: 1px solid var(--color-faint);
    border-radius: 50%;
    overflow: hidden;
}

.preloader-fill {
    position: absolute;
    inset: 0;
    background: var(--color-primary);
    transform: scaleY(0);
    transform-origin: bottom center;
    transition: transform 0.45s cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* -------------------------------------------------------------------- Grain */

.grain {
    position: fixed;
    inset: 0;
    z-index: 9998;
    width: 100%;
    height: 100%;
    pointer-events: none;
    opacity: 0.045;
    mix-blend-mode: overlay;
}

:root[data-theme="light"] .grain { opacity: 0.03; }

/* ------------------------------------------------------------------- Header */

/* [ref] #header — absolute rather than fixed, so it scrolls away with the page
   instead of colliding with content beneath it, and difference-blends over the
   hero. mix-blend-mode is dropped in day mode, where it would invert to mud. */
.site-header {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    padding: var(--base-padding-y) var(--base-padding-x);
    z-index: 4;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5em;
    mix-blend-mode: difference;
    color: #ffffe3;
}

:root[data-theme="light"] .site-header {
    mix-blend-mode: normal;
    color: var(--color-primary);
}

.nav-logo,
.nav-item {
    color: inherit;
    text-decoration: none;
    cursor: none;
}

/* The mark itself is styled in styles.css, which also carries its hover pulse.
   Here it only needs to inherit the header's colour — under the difference
   blend a fixed --mark would stop tracking the hero behind it — and to scale
   with the nav type rather than sit at the SVG's own 21px. */
.site .nav-logo {
    color: inherit;
    display: flex;
    align-items: center;
    position: relative;
}

/* The mark is ~20px across, which is well under a comfortable target and made
   it fiddly to hit even with the cursor magnetised onto it. This gives the
   anchor a circular hit area of 3em, centred on the mark — out of
   flow, so nothing in the header moves, and invisible, so the mark keeps its
   size. LOCK_SCALE in cursor.js is set to match this radius: the magnet must
   not claim the cursor from anywhere the click would miss. */
.site .nav-logo::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 3em;
    height: 3em;
    transform: translate(-50%, -50%);
    border-radius: 50%;
}

.site .nav-logo svg {
    width: 1.15em;
    height: 1.15em;
    display: block;
}

.site-nav {
    display: flex;
    align-items: center;
    gap: 2em;
}

.nav-item {
    position: relative;
    padding-bottom: 0.15em;
}

.nav-item::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 1px;
    background: currentColor;
    transform: scaleX(0);
    transform-origin: right center;
    transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}

.nav-item:hover::after {
    transform: scaleX(1);
    transform-origin: left center;
}

.site .theme-toggle {
    width: 1.2em;
    height: 1.2em;
    color: inherit;
}

.site .theme-toggle:hover { color: inherit; opacity: 0.6; }
.site .theme-toggle svg { width: 1em; height: 1em; }

/* --------------------------------------------------------------------- Main */

/* [ref] main padding, and the 12-column section grid */
.site main {
    position: relative;
    z-index: 3;
    padding: calc(var(--base-padding-y) * 2 + 1em) var(--base-padding-x);
}

.site main section {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: 1em;
}

/* [ref] main section:not(:first-child) */
.site main section:not(:first-child) {
    margin-top: 4em;
}

/* [ref] 4-column grid at or below 1024px */
@media only screen and (max-width: 1024px) {
    .site main section {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

/* ------------------------------------------------------------------- Footer */

/* [ref] #footer */
.site-footer {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: 1em;
    padding: 0 var(--base-padding-x) var(--base-padding-y);
    border-top: 1px solid var(--hairline);
    container-type: inline-size;
}

.footer-grid {
    grid-column: span 12 / span 12;
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: 1em;
    padding: 2em 0 4em;
}

.footer-col {
    /* Three columns across the twelve — Contact, Elsewhere, Index */
    grid-column: span 4 / span 4;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35em;
}

.footer-key {
    color: var(--color-faint);
    margin: 0 0 0.3em;
}

.site .footer-link {
    color: var(--color-primary);
    text-decoration: none;
    cursor: none;
    margin: 0;
    transition: opacity 0.3s ease;
    display: inline-block;
}

.site .footer-link:hover { color: var(--color-primary); opacity: 0.5; }

.footer-fine {
    grid-column: span 12 / span 12;
    color: var(--color-faint);
    margin: 0;
}

@media only screen and (max-width: 1024px) {
    .site-footer,
    .footer-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }

    .footer-grid,
    .footer-fine { grid-column: span 4 / span 4; }

    .footer-col { grid-column: span 2 / span 2; }
}

/* -------------------------------------------------------- Scroll distortion */

/* The bend lives in the #scroll-bend SVG filter, which scroll-distort.js builds
   and drives from signed scroll velocity. Applied to media only: distorting
   live text would cost legibility, and a filter or transform on a page-level
   wrapper would re-parent every position:fixed descendant — the grain canvas
   and the work preview would start scrolling with the page.

   Two conditions, both about cost. feDisplacementMap is CPU-rasterised, so the
   filter is attached only while the page is moving (.is-scrolling) and only to
   media currently on screen (.is-bendable, set by an IntersectionObserver).
   At rest the scale is 0, so attaching and detaching is visually silent. */
.is-scrolling .cs-figure img.is-bendable,
.is-scrolling .about-portrait img.is-bendable,
.is-scrolling .preview-item img.is-bendable {
    filter: url(#scroll-bend);
}

/* Filter defs are present in the DOM but never rendered */
.bend-defs {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
    pointer-events: none;
}

/* ------------------------------------------------------------ Scroll reveal */

.reveal {
    opacity: 0;
    transform: translateY(1.2em);
}

.reveal.is-visible {
    opacity: 1;
    transform: translateY(0);
    transition: opacity 0.85s ease, transform 0.85s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Touch devices have no hover, so the follow-preview never applies */
@media (hover: none) {
    .work-preview { display: none; }
}

/* [ref] the reference kills animation and transition outright under
   prefers-reduced-motion; anything parked by a transform needs resetting too */
@media (prefers-reduced-motion: reduce) {
    .site *,
    .site *::before,
    .site *::after {
        animation: none !important;
        transition: none !important;
    }

    .hero-lede .line > span { transform: none; }
    .hero-meta { opacity: 1; transform: none; }
    .reveal { opacity: 1; transform: none; }
    .grain { display: none; }
}
