/* ============================================================================
   ENTROPY — layout
   ----------------------------------------------------------------------------
   One grid owns the page: three columns (rail / center / rail), three rows
   (masthead / body / footer). Every chrome element is placed into a named area.
   Nothing is positioned against the viewport by hand.

   Two consequences worth knowing before editing:

   1. The center column is defined once, by --rail and --gutter. The gallery,
      the editorial panel and the canvas dot-field all read that one definition
      (the canvas via #fieldGuide + ResizeObserver) instead of each re-deriving
      it per breakpoint.

   2. Anything whose layout depends on the width of the center column uses a
      @container query, not a media query. Center width is a function of the
      rails, not of the viewport, so viewport breakpoints were always the wrong
      instrument. The only media query in this file is the mobile switch.
   ========================================================================== */

:root {
    --fg: #f4f1ea;
    --muted: #8a8a8a;
    --dark: #343434;
    --line: #666;
    --ease: cubic-bezier(0.19, 1, 0.22, 1);

    /* The layout. Change these four and the whole page follows. */
    --rail: clamp(150px, 18vw, 305px);
    --gutter: clamp(18px, 3.2vw, 64px);
    --pad-inline: clamp(20px, 8vw, 152px);
    --pad-block: clamp(16px, 4.2vh, 46px);

    /* Air between the masthead and the rails/center. */
    --head-gap: clamp(24px, 9vh, 116px);
    /* Vertical breathing room for the canvas dot-field inside the center cell. */
    --field-inset: clamp(10px, 3vh, 40px);
}

* {
    box-sizing: border-box;
}
html,
body {
    margin: 0;
    width: 100%;
    height: 100%;
    background: #000;
    color: var(--fg);
    overflow: hidden;
    font-family: "Proxima Nova", "proxima-nova", "Helvetica Neue", Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    text-rendering: geometricPrecision;
}
h1,
h2 {
    margin: 0;
    font-size: inherit;
    font-weight: inherit;
}
button,
input,
a {
    font: inherit;
    color: inherit;
}
button,
a {
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    letter-spacing: 0.19em;
    text-transform: uppercase;
    text-decoration: none;
}
button:focus-visible,
input:focus-visible {
    outline: 1px solid #fff;
    outline-offset: 5px;
}

/* --- canvas ---------------------------------------------------------------
   Full-bleed and behind everything. It is the background, so it is the one
   thing that is *not* in the grid.
   ------------------------------------------------------------------------ */
.app,
.viz {
    position: fixed;
    inset: 0;
}
.viz {
    width: 100%;
    height: 100%;
    display: block;
    background: #000;
    z-index: 0;
}
.introHint {
    position: fixed;
    left: 50%;
    bottom: 7vh;
    transform: translateX(-50%);
    font-size: 10px;
    letter-spacing: 0.48em;
    text-transform: uppercase;
    color: #777;
    opacity: 0;
    animation: hint 2.6s 1.3s forwards;
    pointer-events: none;
    z-index: 3;
}
@keyframes hint {
    to {
        opacity: 0.75;
    }
}
.status {
    display: none;
}

/* --- the grid ------------------------------------------------------------ */
.shell {
    position: fixed;
    inset: 0;
    z-index: 4;
    display: grid;
    grid-template-columns: var(--rail) minmax(0, 1fr) var(--rail);
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    grid-template-areas:
        "head   head    head"
        "modes  center  right"
        "left   center  activity"
        "foot   foot    foot";
    column-gap: var(--gutter);
    padding: var(--pad-block) var(--pad-inline);
    pointer-events: none;
}
/* Both wrappers are dissolved on desktop, so their children are grid items of
   .shell in their own right. They only become boxes on mobile, where they are
   the side menu and the bottom sheet. */
.drawer,
.sheet {
    display: contents;
}

/* Chrome fades in with .live; the rails additionally slide in with .open. */
.masthead,
.footer {
    opacity: 0;
    transition: opacity 1.4s var(--ease);
}
.shell.live .masthead,
.shell.live .footer {
    opacity: 1;
}

/* --- masthead ------------------------------------------------------------ */
.masthead {
    grid-area: head;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: start;
    padding-bottom: var(--head-gap);
}
.topGlyph {
    grid-column: 2;
    justify-self: center;
    width: 59.1px;
    height: 95.85px;
    background: center / contain no-repeat url("../assets/animated-white.gif");
    opacity: 0.95;
    pointer-events: auto;
    cursor: pointer;
}
.menuToggle {
    display: none;
    grid-column: 3;
    justify-self: end;
    width: 22px;
    height: 17px;
    flex-direction: column;
    justify-content: space-between;
    pointer-events: auto;
}
.menuToggle span {
    display: block;
    height: 1px;
    width: 100%;
    background: #fff;
    transition:
        transform 0.42s var(--ease),
        opacity 0.3s var(--ease);
}

/* --- rails ---------------------------------------------------------------
   The activity panel is chrome too — it lives in the drawer in the DOM but
   sits in the right column on desktop, so it fades and slides with the rails.
   ------------------------------------------------------------------------ */
.rail,
.navModes,
.activityLab {
    pointer-events: auto;
    opacity: 0;
    transition:
        transform 1.8s var(--ease),
        opacity 1.4s var(--ease);
}
.rail {
    display: flex;
    flex-direction: column;
    min-height: 0;
}
.navModes {
    grid-area: modes;
    text-align: right;
}
.rail--left {
    grid-area: left;
    align-items: flex-end;
    text-align: right;
    justify-content: space-between;
}
.rail--right {
    grid-area: right;
    align-items: flex-start;
    text-align: left;
    gap: 3.4vh;
}
/* The rails settling in from the page edges is a desktop-only intro beat, so
   it is scoped as one. Left unscoped it would outrank the mobile drawer/sheet
   closed states — media queries add no specificity of their own. */
@media (min-width: 901px) {
    .rail--left,
    .navModes {
        transform: translateX(-70px);
    }
    .rail--right,
    .activityLab {
        transform: translateX(70px);
    }
    .shell.open .rail,
    .shell.open .navModes,
    .shell.open .activityLab {
        opacity: 1;
        transform: translateX(0);
    }
}
.navSection {
    width: 100%;
}
.navTitle {
    font-size: 12px;
    font-weight: 900;
    letter-spacing: 0.52em;
    text-transform: uppercase;
    color: #fff;
    margin-bottom: 28px;
}
.rail--right .navTitle {
    margin-bottom: 16px;
}
.navGroup {
    display: flex;
    flex-direction: column;
    gap: 22px;
    width: 100%;
}
.navGroup--tight {
    gap: 13px;
}
.navItem {
    display: block;
    width: 100%;
    font-size: clamp(11px, 0.82vw, 17px);
    font-weight: 300;
    line-height: 1.05;
    color: #777;
    letter-spacing: 0.2em;
    text-align: inherit;
    transition:
        color 0.5s,
        opacity 0.5s,
        transform 0.5s;
}
.navItem.active {
    font-weight: 850;
    color: #fff;
}
.navItem:hover {
    color: #fff;
}

/* --- wallet -------------------------------------------------------------- */
.walletWrap {
    position: relative;
    width: min(226px, 100%);
    pointer-events: auto;
}
/* "Inspect Wallet / ENS" is 20 characters, and the rail it sits in is a
   clamp() — so the type is fluid too, and the right padding never drops below
   the ✓ button's footprint (right: 13px + 22px wide). */
.walletBox {
    width: 100%;
    border: 1px solid #242424;
    background: transparent;
    padding: 15px clamp(39px, 2.6vw, 44px) 15px clamp(11px, 0.9vw, 16px);
    font-size: clamp(9px, 0.6vw, 10.5px);
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: #ddd;
    border-radius: 0;
}
.walletBox::placeholder {
    color: #b8b8b8;
    opacity: 0.75;
}
.walletBox.busy {
    border-color: #fff;
    color: #fff;
}
.walletSubmit {
    position: absolute;
    right: 13px;
    top: 50%;
    transform: translateY(-50%);
    width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 13px;
    letter-spacing: 0;
    line-height: 1;
    opacity: 0.78;
    transition:
        opacity 0.35s var(--ease),
        transform 0.35s var(--ease);
}
.walletSubmit:hover {
    opacity: 1;
    transform: translateY(-50%) scale(1.08);
}
.walletSubmit.busy {
    opacity: 0.35;
    pointer-events: none;
}
.walletSubmit.clear {
    font-size: 18px;
    opacity: 0.95;
}

/* --- activity ------------------------------------------------------------ */
/* The feed fills the activity cell and scrolls inside it, so it can never
   spill past the row the grid gave it. */
.activityLab {
    grid-area: activity;
    padding-top: 1.5vh;
    margin-top: 2.2vh;
    border-top: 1px solid rgba(255, 255, 255, 0.13);
    display: flex;
    flex-direction: column;
    min-height: 0;
}
.activityFeed {
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
    overflow-y: auto;
    scrollbar-width: none;
    mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent 100%);
}
.activityFeed::-webkit-scrollbar {
    display: none;
}
.activityRow {
    display: block;
    padding: 0 0 10px;
    color: #777;
    font-size: 9px;
    letter-spacing: 0.08em;
    line-height: 1.45;
    text-transform: none;
}
.activityRow a {
    color: #fff;
    letter-spacing: 0.08em;
    text-transform: none;
}
.activityKind {
    color: #aaa;
}
.activityTime {
    color: #4d4d4d;
}
.activityEmpty {
    color: #555;
    font-size: 9px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    line-height: 1.45;
}
/* A live row arrives at the top. It eases open from zero height — the negative
   start margin is the row's own height, measured and handed over by JS — so the
   list below slides down rather than snapping a row. */
.activityRow.isNew {
    animation: activityRowIn 0.46s var(--ease) both;
}
@keyframes activityRowIn {
    from {
        opacity: 0;
        margin-top: calc(-1 * var(--row-h, 22px));
        transform: translateY(-4px);
    }
    to {
        opacity: 1;
        margin-top: 0;
        transform: none;
    }
}

/* The feed dissolves toward the bottom instead of ending on a hard edge. */
.activityRow:nth-child(10) {
    opacity: 0.92;
}
.activityRow:nth-child(11) {
    opacity: 0.82;
}
.activityRow:nth-child(12) {
    opacity: 0.72;
}
.activityRow:nth-child(13) {
    opacity: 0.62;
}
.activityRow:nth-child(14) {
    opacity: 0.52;
}
.activityRow:nth-child(15) {
    opacity: 0.42;
}
.activityRow:nth-child(16) {
    opacity: 0.32;
}
.activityRow:nth-child(17) {
    opacity: 0.23;
}
.activityRow:nth-child(18) {
    opacity: 0.15;
}
.activityRow:nth-child(19) {
    opacity: 0.08;
}
.activityRow:nth-child(20) {
    opacity: 0.035;
}

/* --- center column -------------------------------------------------------
   The gallery and the editorial panel share this cell (both in the "stack"
   area) and swap via .live. #fieldGuide is a zero-paint element the canvas
   measures, so the dot-field's bounds are declared here in CSS, not in JS.
   ------------------------------------------------------------------------ */
.center {
    grid-area: center;
    position: relative;
    min-height: 0;
    display: grid;
    grid-template-areas: "stack";
    pointer-events: none;
    container-type: inline-size;
    container-name: center;
}
.center > * {
    grid-area: stack;
    min-height: 0;
}
/* The dot-field is allowed to rise into the masthead's air, which is why it
   reaches back up by --head-gap. Everything the canvas knows about its own
   bounds is in this one rule. */
.fieldGuide {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(var(--field-inset) - var(--head-gap));
    bottom: var(--field-inset);
    visibility: hidden;
    pointer-events: none;
}

/* --- gallery -------------------------------------------------------------
   auto-fill sizes the columns from the available width, so there are no
   per-breakpoint column counts at all.
   ------------------------------------------------------------------------ */
.gallery {
    display: none;
    grid-template-columns: repeat(auto-fill, minmax(min(230px, 100%), 1fr));
    grid-auto-rows: max-content;
    align-content: start;
    gap: clamp(24px, 4vh, 48px) clamp(20px, 6cqw, 76px);
    overflow-y: auto;
    overflow-x: hidden;
    padding: 24px 0 34px;
    scrollbar-width: none;
    mask-image: linear-gradient(to bottom, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
}
.gallery::-webkit-scrollbar {
    display: none;
}
.gallery.live {
    display: grid;
    pointer-events: auto;
}
.card {
    position: relative;
    aspect-ratio: 1 / 1.22;
    background: #030303;
    overflow: hidden;
    opacity: 0;
    transform: translateY(24px);
    animation: cardIn 1.2s var(--ease) forwards;
    transition: background-color 0.5s var(--ease);
}
/* --- skeleton ------------------------------------------------------------
   An unloaded card is a slightly lighter block than the page with a soft
   highlight passing over it. The sweep is a transform, not an animated
   background-position: a whole grid of cards repainting every frame is real
   work on a phone, whereas transforms stay on the compositor.
   ------------------------------------------------------------------------ */
.card:not(.loaded),
.seriesIntroArtwork:not(.loaded),
.seriesImage:not(.loaded) {
    background: #0d0d0d;
}
.card::after,
.seriesIntroArtwork::after,
.seriesImage::after,
.lightboxStage:not(.loaded)::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: -60%;
    width: 60%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.06), transparent);
    animation: skeletonSweep 1.9s linear infinite;
    transition: opacity 0.5s var(--ease);
    pointer-events: none;
}
/* Paused rather than removed: `animation: none` would snap the highlight back
   to its start position mid-fade. */
.card.loaded::after,
.seriesIntroArtwork.loaded::after,
.seriesImage.loaded::after {
    opacity: 0;
    animation-play-state: paused;
}
@keyframes skeletonSweep {
    to {
        transform: translateX(360%);
    }
}
.card img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    filter: saturate(1.05) contrast(1.04);
}
.cardStill {
    opacity: 0;
    transition: opacity 0.55s var(--ease);
}
.card.loaded .cardStill {
    opacity: 1;
}
.cardMotion {
    opacity: 0;
}
/* These stay last: they share specificity with the .loaded rule above and must
   win when a card is both loaded and hovered. Both are gated on .motionReady —
   the animated file is fetched on first hover, and until it has decoded there
   is nothing to swap to, so hiding the still would just blank the card. */
.card.motionReady.motionLive .cardMotion {
    opacity: 1;
}
.card.motionReady.motionLive .cardStill {
    opacity: 0;
}
.card:hover img {
    filter: saturate(1.15) contrast(1.08) brightness(1.08);
}
@keyframes cardIn {
    to {
        opacity: 1;
        transform: none;
    }
}
@container center (max-width: 560px) {
    .gallery {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* --- editorial panel (The Series / The Artist) ---------------------------- */
.seriesPanel {
    display: none;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 2px 0 10vh;
    scrollbar-width: none;
    mask-image: linear-gradient(to bottom, #000 calc(100% - 8vh), transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 8vh), transparent 100%);
}
.seriesPanel::-webkit-scrollbar {
    display: none;
}
.seriesPanel.live {
    display: block;
    pointer-events: auto;
}
/* Sections resolve the way gallery cards do — same keyframe, same curve, same
   24px rise, staggered down the page. The panel is display:none until .live, so
   the run starts when the page is opened rather than when it is built. */
.seriesIntro,
.seriesBlock {
    display: grid;
    gap: clamp(20px, 5cqw, 64px);
    align-items: start;
    margin: 0 0 8vh;
    opacity: 0;
    transform: translateY(24px);
    animation: cardIn 1.2s var(--ease) forwards;
}
.seriesPanel > article:nth-child(2) {
    animation-delay: 0.09s;
}
.seriesPanel > article:nth-child(3) {
    animation-delay: 0.18s;
}
.seriesPanel > article:nth-child(4) {
    animation-delay: 0.27s;
}
.seriesIntroCopy h1,
.seriesText h2 {
    margin: 0 0 10px;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    font-size: clamp(19px, 1.25vw, 29px);
    line-height: 1.05;
    font-weight: 400;
    color: #fff;
    /* optical alignment: pulls the cap-height onto the rail's title baseline */
    transform: translateY(-0.24em);
}
.seriesText h2 {
    margin-bottom: 7px;
}
.seriesIntroCopy p,
.seriesText p {
    margin: 0 0 1.55em;
    font-size: clamp(11.5px, 0.78vw, 15.3px);
    line-height: 1.48;
    letter-spacing: 0.015em;
    color: #f2f2f2;
}
.seriesIntroArtwork,
.seriesImage {
    /* position + a fixed ratio make the figure its own skeleton host, so these
       need no wrapper the way the lightbox image does. */
    position: relative;
    margin: 0;
    aspect-ratio: 4 / 5;
    background: #050505;
    overflow: hidden;
    transition: background-color 0.5s var(--ease);
}
.seriesIntroArtwork img,
.seriesImage img {
    width: 100%;
    height: 100%;
    display: block;
    object-fit: cover;
    filter: saturate(1.05) contrast(1.06);
    opacity: 0;
    transition: opacity 0.55s var(--ease);
}
.seriesIntroArtwork.loaded img,
.seriesImage.loaded img {
    opacity: 1;
}
.artistPhoto img {
    filter: grayscale(1) contrast(1.08);
}
/* Stacked by default; two-up only when the center column is actually wide
   enough to hold it — which is a property of the column, not the viewport.
   Stacked means the image has a whole row to itself, so it is centred in it.
   Stated rather than left to the default: these carry a definite width, so
   `justify-self: normal` resolves to `start` instead of stretching. */
.seriesIntroArtwork,
.seriesImage {
    width: min(70%, 300px);
    justify-self: center;
}
.seriesImage {
    margin-top: 4vh;
    order: -1;
}
@container center (min-width: 560px) {
    .seriesIntro {
        grid-template-columns: minmax(120px, 26%) minmax(0, 1fr);
    }
    .seriesBlock {
        grid-template-columns: minmax(0, 1.05fr) minmax(180px, 31%);
    }
    .seriesBlock.reverse {
        grid-template-columns: minmax(180px, 31%) minmax(0, 1.05fr);
    }
    .seriesText {
        text-align: right;
    }
    .seriesBlock.reverse .seriesText {
        grid-column: 2;
        grid-row: 1;
        text-align: left;
    }
    .seriesBlock.reverse .seriesImage {
        grid-column: 1;
        grid-row: 1;
    }
    .seriesIntroArtwork,
    .seriesImage {
        width: 100%;
        max-width: 260px;
    }
    /* Two-up: each image hugs the gutter it shares with its copy, so the
       centring above has to be overridden on both. */
    .seriesIntroArtwork {
        justify-self: start;
    }
    .seriesImage {
        margin-top: 0;
        order: 0;
        justify-self: end;
    }
    .seriesBlock.reverse .seriesImage {
        justify-self: start;
    }
}

/* --- footer -------------------------------------------------------------- */
.footer {
    grid-area: foot;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    padding-top: clamp(16px, 3vh, 40px);
}
.brandBottom {
    grid-column: 2;
    justify-self: center;
    width: min(262px, 21vw);
    height: 36px;
    background: center / contain no-repeat url("../assets/logo.png");
    filter: brightness(9) grayscale(1);
    pointer-events: auto;
    cursor: pointer;
    transition:
        opacity 0.34s var(--ease),
        transform 0.34s var(--ease);
}
.fullscreenBtn {
    grid-column: 3;
    justify-self: end;
    width: 34px;
    height: 34px;
    border: 1px solid rgba(255, 255, 255, 0.32);
    border-radius: 50%;
    display: grid;
    place-items: center;
    color: #8a8a8a;
    font-size: 18px;
    line-height: 1;
    letter-spacing: 0;
    pointer-events: auto;
}
.fullscreenBtn:hover {
    color: #fff;
    border-color: #fff;
}
.sheetToggle {
    display: none;
    grid-column: 1;
    justify-self: center;
    color: #aaa;
    font-size: 16px;
    line-height: 1;
    letter-spacing: 0;
    pointer-events: auto;
}

/* --- fullscreen mode -----------------------------------------------------
   Must follow the .shell.open rules: same specificity, later wins.
   ------------------------------------------------------------------------ */
.app.fullscreenMode .brandBottom,
.app.fullscreenMode .topGlyph {
    opacity: 0;
    pointer-events: none;
}
.app.fullscreenMode .rail,
.app.fullscreenMode .navModes,
.app.fullscreenMode .activityLab {
    opacity: 0;
    pointer-events: none;
}
.app.fullscreenMode .rail--left,
.app.fullscreenMode .navModes {
    transform: translateX(-96px);
}
.app.fullscreenMode .rail--right,
.app.fullscreenMode .activityLab {
    transform: translateX(96px);
}
.app.fullscreenMode.fullLeft .rail--left,
.app.fullscreenMode.fullLeft .navModes,
.app.fullscreenMode.fullRight .rail--right,
.app.fullscreenMode.fullRight .activityLab {
    opacity: 1;
    pointer-events: auto;
    transform: translateX(0);
}
.app.fullscreenMode .fullscreenBtn {
    opacity: 0;
    pointer-events: none;
}
.app.fullscreenMode.mouseActive .fullscreenBtn {
    opacity: 0.65;
    pointer-events: auto;
}
.app.cursorIdle,
.app.cursorIdle * {
    cursor: none !important;
}
.app.seriesMode .brandBottom {
    opacity: 0;
    pointer-events: none;
}

/* --- tooltip ------------------------------------------------------------- */
.tooltip {
    position: fixed;
    display: none;
    pointer-events: none;
    z-index: 5;
    background: rgba(0, 0, 0, 0.88);
    border: 1px solid #343434;
    border-radius: 2px;
    padding: 9px 10px;
    min-width: 190px;
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    line-height: 1.45;
    color: #fff;
    opacity: 0;
    transition: opacity 0.25s var(--ease);
}
.tooltip.live {
    opacity: 1;
}
.tooltip span {
    color: #888;
}
.tipRow {
    display: flex;
    gap: 10px;
    align-items: center;
}
.tipThumb {
    width: 42px;
    height: 54px;
    object-fit: cover;
    background: #111;
    flex: 0 0 auto;
}

/* --- lightbox ------------------------------------------------------------ */
/* A column, not a centred box with the caption floated on top: the metadata
   claims its height first and the artwork shrinks into what is left, so a tall
   piece can never run underneath the text. */
.lightbox {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.92);
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: clamp(14px, 2.6vh, 30px);
    z-index: 10;
    padding: 6vh 6vw;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.72s var(--ease);
}
.lightbox.live {
    opacity: 1;
    pointer-events: auto;
}
.lightbox.noImage img,
.lightbox.noImage .lightboxStage {
    display: none;
}
.lightboxStage {
    position: relative;
    align-self: center;
    display: flex;
    /* Lets the stage shrink below its content when the caption is tall; without
       it a flex item refuses to go under its content size. */
    min-height: 0;
    overflow: hidden;
}
/* Until the artwork's dimensions are known the stage would collapse to nothing,
   so the skeleton stands in at the collection's 4:5 proportion. */
.lightboxStage:not(.loaded) {
    width: min(72vw, 62vh);
    aspect-ratio: 4 / 5;
    background: #0d0d0d;
}
.lightbox img {
    max-width: 72vw;
    max-height: 78vh;
    min-height: 0;
    object-fit: contain;
    opacity: 0;
    transition: opacity 0.55s var(--ease);
}
.lightbox img.loaded {
    opacity: 1;
}
.lightboxMeta {
    flex: 0 0 auto;
    font-size: 13px;
    line-height: 1.9;
    letter-spacing: 0.22em;
    text-transform: uppercase;
}
.lightboxMeta span,
.metaMuted {
    color: #8e8e8e;
}
.metaGrid {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 2px 20px;
    max-width: 860px;
}
.close {
    position: absolute;
    right: 6vw;
    top: 5vh;
    color: #ddd;
    font-size: 20px;
    z-index: 11;
}
/* Gallery paging. Touch gets a swipe instead, so these are pointer-only. */
.lightboxRow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: clamp(8px, 1.4vw, 26px);
    min-height: 0;
}
.lightboxNav {
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    color: #ddd;
    font-size: 30px;
    line-height: 1;
    letter-spacing: 0;
    opacity: 0.5;
    transition: opacity 0.35s var(--ease);
    z-index: 11;
}
.lightboxNav:hover {
    opacity: 1;
}
/* No wrapping: at either end the control stays put and goes quiet, so the
   extent of the set is visible rather than something you discover by looping. */
.lightboxNav[disabled] {
    opacity: 0.12;
    pointer-events: none;
}
/* [hidden] is a UA `display: none`, which the display above would beat. */
.lightboxNav[hidden] {
    display: none;
}
/* Paging enters from the direction of travel, so a swipe or an arrow reads as
   movement through the set rather than a replacement in place. The outgoing
   image is not animated out: the stage falls back to its skeleton the moment a
   new source is set, and sliding a shimmer away says nothing. */
.lightboxStage.slideNext {
    animation: stageInFromRight 0.44s var(--ease) both;
}
.lightboxStage.slidePrev {
    animation: stageInFromLeft 0.44s var(--ease) both;
}
@keyframes stageInFromRight {
    from {
        opacity: 0;
        transform: translateX(38px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}
@keyframes stageInFromLeft {
    from {
        opacity: 0;
        transform: translateX(-38px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

/* --- live activity toast (mobile) ---------------------------------------- */
.mobileLiveActivity {
    display: none;
    position: fixed;
    z-index: 8;
    left: 50%;
    top: 69vh;
    transform: translate(-50%, 10px);
    width: 86vw;
    text-align: center;
    opacity: 0;
    pointer-events: none;
    color: #aaa;
    font-size: 11px;
    line-height: 1.35;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}
.mobileLiveActivity a {
    color: #fff;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}
.mobileLiveActivity span {
    color: #aaa;
}
.mobileLiveActivity.live {
    animation: mobileActivityFlash 2s var(--ease) both;
}
@keyframes mobileActivityFlash {
    0% {
        opacity: 0;
        transform: translate(-50%, 10px);
    }
    22% {
        opacity: 0.92;
        transform: translate(-50%, 0);
    }
    72% {
        opacity: 0.92;
        transform: translate(-50%, 0);
    }
    100% {
        opacity: 0;
        transform: translate(-50%, -4px);
    }
}

/* ============================================================================
   MOBILE
   The grid collapses to one column and the chrome becomes two surfaces — the
   same nodes as desktop, re-placed:
     .drawer (left rail + activity) → side menu, slides in from the right
     .rail--right                   → bottom sheet (series + status filters)
   ========================================================================== */
@media (max-width: 900px) {
    :root {
        --pad-inline: 6vw;
        --pad-block: 3.2vh;
        --head-gap: clamp(16px, 5vh, 60px);
    }
    .shell {
        grid-template-columns: minmax(0, 1fr);
        /* Must be respecified alongside the areas: the desktop template has a
           fourth row for the activity cell, and leaving it here would hand the
           1fr to the footer and collapse the center. */
        grid-template-rows: auto minmax(0, 1fr) auto;
        grid-template-areas:
            "head"
            "center"
            "foot";
        column-gap: 0;
    }
    .menuToggle {
        display: flex;
        z-index: 14;
    }
    /* The toggle stacks above the wordmark rather than sharing its row. */
    .footer {
        grid-template-columns: minmax(0, 1fr);
        justify-items: center;
        row-gap: 10px;
    }
    /* Below the drawer (12) so an open menu covers it, above the sheet (9) so
       it stays reachable while the filters are up — the original's stacking. */
    .sheetToggle {
        display: block;
        grid-column: 1;
        grid-row: 1;
        position: relative;
        z-index: 10;
    }
    .brandBottom {
        grid-column: 1;
        grid-row: 2;
    }
    .fullscreenBtn {
        display: none;
    }
    .topGlyph {
        width: 33px;
        height: 54px;
    }
    .brandBottom {
        width: min(138px, 24vw);
        height: 20px;
    }
    .mobileLiveActivity {
        display: block;
    }

    /* The wrapper stops being `display:contents` and becomes the side menu.
       The menu reads links → activity → wallet. The wallet lives inside the
       rail and activity is its sibling, so `display:contents` on the rail
       flattens all three into this flex column where `order` can interleave
       them. Spacing is per-block rather than a shared gap, to keep the
       original's rhythm (18px between links, 20px before the rule). */
    .drawer {
        display: flex;
        flex-direction: column;
        gap: 0;
        text-align: right;
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        width: min(315px, 78vw);
        z-index: 12;
        padding: 14vh 7vw 8vh 6vw;
        background: #000;
        border-left: 1px solid rgba(255, 255, 255, 0.14);
        overflow-y: auto;
        scrollbar-width: none;
        pointer-events: auto;
        transform: translateX(100%);
        transition: transform 0.42s var(--ease);
    }
    .drawer::-webkit-scrollbar {
        display: none;
    }
    .rail,
    .navModes,
    .activityLab {
        opacity: 1;
    }
    .rail--left {
        display: contents;
    }
    .navSection--about {
        order: 1;
    }
    .activityLab {
        order: 2;
    }
    .walletWrap {
        order: 3;
    }

    /* The sheet floats over the canvas: no panel, no background, no divider —
       just three centred lines of words that fade up from 24px below. */
    .sheet {
        display: flex;
        flex-direction: column;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 1.8vh;
        z-index: 9;
        padding: 0 4vw;
        pointer-events: none;
        opacity: 0;
        transform: translateY(24px);
        transition:
            opacity 1.05s var(--ease),
            transform 1.05s var(--ease);
    }
    .shell.sheetOpen .sheet {
        opacity: 1;
        transform: translateY(0);
        pointer-events: auto;
        transition-duration: 1.15s;
    }
    .rail--right {
        display: contents;
    }
    /* Both of these set `pointer-events: auto` for their desktop life as rails.
       Left alone that beats the closed sheet's `none` — pointer-events inherits,
       so a child re-asserting `auto` re-enables itself — and the invisible
       filter rows keep swallowing taps meant for the canvas. */
    .sheet .rail,
    .sheet .navModes {
        pointer-events: inherit;
    }
    .shell.drawerOpen .drawer {
        transform: none;
    }
    /* The editorial pages take the whole screen; the sheet has no place there.
       The footer goes with it — with no filters and the wordmark already
       hidden, it was reserving an empty row, and the panel's bottom fade was
       hinting at controls sitting under it that do not exist. The reading
       column runs to the bottom of the screen instead. */
    .app.seriesMode .sheet,
    .app.seriesMode .sheetToggle,
    .app.seriesMode .footer {
        display: none;
    }
    .app.seriesMode .seriesPanel {
        padding-bottom: 6vh;
        mask-image: none;
        -webkit-mask-image: none;
    }
    .shell.drawerOpen .menuToggle span:nth-child(1) {
        transform: translateY(8px) rotate(45deg);
    }
    .shell.drawerOpen .menuToggle span:nth-child(2) {
        opacity: 0;
    }
    .shell.drawerOpen .menuToggle span:nth-child(3) {
        transform: translateY(-8px) rotate(-45deg);
    }
    /* The wordmark rises 12.2vh as it fades — the same travel and the same
       1.15s/1.25s pairing as the original (bottom 2.2vh → 14.4vh). The toggle
       moves with it and, as before, does not ease: it is repositioned at once
       so the arrow is already waiting where the sheet will appear. */
    .brandBottom {
        transition:
            transform 1.15s var(--ease),
            opacity 1.25s var(--ease);
    }
    .shell.sheetOpen .brandBottom {
        opacity: 0;
        transform: translateY(-12.2vh);
        pointer-events: none;
    }
    .shell.sheetOpen .sheetToggle {
        transform: translateY(calc(-1 * var(--sheet-lift, 0px)));
    }

    .navTitle {
        font-size: 10px;
        letter-spacing: 0.42em;
        margin-bottom: 18px;
    }
    .navItem {
        font-size: 14px;
        letter-spacing: 0.22em;
    }
    /* "About" is redundant when the whole menu is the about section — but
       "Activity" still earns its keep as the feed's label. */
    .navSection--about .navTitle {
        display: none;
    }
    .navSection--about .navGroup {
        gap: 0;
    }
    .navSection--about .navItem {
        color: #ddd;
        margin: 0 0 18px;
    }
    .walletWrap {
        width: 100%;
    }
    .walletBox {
        border-color: rgba(255, 255, 255, 0.25);
        background: #000;
        padding: 11px 34px 11px 12px;
        font-size: 11px;
        letter-spacing: 0.18em;
    }
    .walletSubmit {
        right: 9px;
    }

    /* Three rows of words. The titles are noise here — the words say it. */
    .sheet .navTitle {
        display: none;
    }
    .sheet .navGroup {
        flex-direction: row;
        justify-content: center;
        white-space: nowrap;
        width: auto;
        margin: 18px auto 0;
        gap: min(7vw, 38px);
    }
    .sheet .navItem {
        width: auto;
        font-size: clamp(10px, 2.75vw, 14px);
        letter-spacing: 0.34em;
        color: #555;
    }
    .sheet .navItem.active {
        color: #fff;
        font-weight: 900;
    }
    .sheet .navSection--filter .navGroup {
        gap: min(5vw, 30px);
    }
    .sheet [data-series] {
        letter-spacing: 0.28em;
    }
    .sheet [data-status] {
        letter-spacing: 0.25em;
    }
    /* Activity takes whatever height is left between the links and the wallet,
       which is what pins the wallet to the foot of the menu. The feed scrolls
       inside it rather than pushing the field down. */
    .activityLab {
        flex: 1 1 auto;
        min-height: 0;
        margin: 20px 0 18px;
        padding-top: 18px;
        border-top: 1px solid rgba(255, 255, 255, 0.14);
        text-align: right;
    }
    .activityLab .navTitle {
        margin-bottom: 14px;
    }
    .activityFeed {
        flex: 1 1 auto;
        min-height: 0;
        max-height: none;
    }

    /* --- mobile lightbox ---
       The column and the static caption come from the base rules now; only the
       sizing differs here. */
    .lightbox {
        background: #000;
        padding: 3.5vh 4vw 12vh;
    }
    .lightbox img {
        max-width: 86vw;
        max-height: 52vh;
        flex: 0 1 auto;
    }
    .lightboxStage:not(.loaded) {
        width: min(86vw, 42vh);
    }
    .lightboxMeta {
        width: 82vw;
        max-width: 82vw;
        margin: 0 auto;
        font-size: 8.8px;
        line-height: 1.32;
        letter-spacing: 0.105em;
        overflow: hidden;
    }
    .metaGrid {
        width: 100%;
        grid-template-columns: 94px minmax(0, 1fr);
        gap: 1px 11px;
    }
    .metaGrid > * {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .close {
        top: 1.2vh;
        right: 5vw;
        font-size: 22px;
    }
    /* Swipe replaces them here — arrows sitting over the artwork on a phone
       cost more than they give. */
    .lightboxNav {
        display: none;
    }
    .app.lightboxMode .shell {
        opacity: 0;
        pointer-events: none;
    }
}

/* Small phones: the sheet's rows are `nowrap`, so the tracking has to give a
   little or "All Combustion Diffusion Corruption" runs off the screen. Same
   concession the original made at this width. */
@media (max-width: 520px) {
    .sheet .navGroup {
        gap: 5.2vw;
    }
    .sheet .navItem {
        letter-spacing: 0.24em;
    }
    .sheet .navSection--filter .navGroup {
        gap: 4vw;
    }
    .sheet [data-series],
    .sheet [data-status] {
        letter-spacing: 0.18em;
    }
    .brandBottom {
        width: 28vw;
    }
    .shell.sheetOpen .brandBottom {
        transform: translateY(-11.6vh);
    }
}
