/* Self-hosted: the CSP allows font-src 'self' data: only, so no external font host.
   Paths are relative to this stylesheet (/LOLOTRON6/assets/site.css). */
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("fonts/Inter/Inter-Regular.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("fonts/Inter/Inter-Bold.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 900; font-display: swap; src: url("fonts/Inter/Inter-Black.woff2") format("woff2"); }
/* Display faces for nav and calls to action only. Single weight, no italic cut,
   so the slant stays geometric. SIL OFL. Unused families are never fetched,
   so keeping Luckiest Guy declared costs nothing but makes swapping back a
   one-token change on --display. */
@font-face { font-family: "Boogaloo"; font-style: normal; font-weight: 400; font-display: swap; src: url("fonts/Boogaloo/Boogaloo-Regular.woff2") format("woff2"); }
@font-face { font-family: "Luckiest Guy"; font-style: normal; font-weight: 400; font-display: swap; src: url("fonts/LuckiestGuy/LuckiestGuy-Regular.woff2") format("woff2"); }

:root {
    color-scheme: dark;
    --ink: #070404;
    --surface: #120706;
    --surface-raised: #1c0b08;
    --surface-soft: #280e09;
    --line: rgba(255, 177, 138, 0.18);
    --line-strong: rgba(255, 120, 50, 0.42);
    --text: #fff8f3;
    --muted: #cdb5aa;
    --ember: #ff2b05;
    --amber: #fa7800;
    --blood: #530a03;
    --splash: #ff4d00;
    --splash-hot: #ff6a1a;
    /* The one colour on the site that is not in the fire family. It is reserved
       for taking something away with you - the opposite errand from every
       orange call to action, which sends a reader further in - and it is the
       only reason it is allowed to sit next to the splash. */
    --verdant: #1fa94a;
    --verdant-hot: #27c758;
    --paper: #ffffff;
    --focus: #ffd29a;
    --shadow: 0 28px 80px rgba(44, 5, 0, 0.48);
    /* The dot halftone: the site's screen-print texture, and the thing that
       keeps a large dark field from reading as flat paint. */
    --halftone: radial-gradient(rgba(255, 77, 0, 0.28) 1.4px, transparent 1.6px);
    /* The mascot at texture scale, filled in --ink: the page's own ground, so
       the heads sit on the footer's lighter --surface as a tone rather than as a
       drawing. Recognition is not the job and the silhouette could not do it
       anyway - head, hair and hand merge without the linework. Dense and dim,
       that is exactly right: the eye reads texture and never asks what one shape
       is. It was tried on the dialog backdrops first, sparse and in full colour,
       and there it read as wallpaper over the one surface that has to stay out
       of the way; the footer is where a signature belongs.
       Two heads staggered inside a 150px tile, so the offset repeat is one
       background layer. The ink is carried at half alpha, which lands the shapes
       roughly midway between --ink and --surface: at full strength the heads
       read as cut-outs punched through the footer rather than as a grain in it.
       Alpha is baked into the file rather than set here - a CSS opacity would
       need its own layer to sit on, and the footer's background is the layer. */
    --footer-tile: url("website/footer-tile-150.webp");
    /* Twelve offsets, not -webkit-text-stroke: an inward stroke closes over
       Boogaloo's stems at label size. Every light label on an orange face wears
       it, so it lives here rather than being written out at each one. */
    --ink-ring:
        1.5px 0 0 var(--ink), -1.5px 0 0 var(--ink), 0 1.5px 0 var(--ink), 0 -1.5px 0 var(--ink),
        1.3px .75px 0 var(--ink), -1.3px .75px 0 var(--ink),
        1.3px -.75px 0 var(--ink), -1.3px -.75px 0 var(--ink),
        .75px 1.3px 0 var(--ink), -.75px 1.3px 0 var(--ink),
        .75px -1.3px 0 var(--ink), -.75px -1.3px 0 var(--ink);
    /* The ring, emptied. Same twelve offsets in the same order so text-shadow can
       interpolate between the two lists shadow for shadow; `none` cannot be a
       transition endpoint, which is the whole reason this exists rather than the
       obvious text-shadow: none. Transparent black premultiplies out cleanly, so
       the ring thins to nothing instead of passing through grey. */
    --ink-ring-clear:
        1.5px 0 0 transparent, -1.5px 0 0 transparent, 0 1.5px 0 transparent, 0 -1.5px 0 transparent,
        1.3px .75px 0 transparent, -1.3px .75px 0 transparent,
        1.3px -.75px 0 transparent, -1.3px -.75px 0 transparent,
        .75px 1.3px 0 transparent, -.75px 1.3px 0 transparent,
        .75px -1.3px 0 transparent, -.75px -1.3px 0 transparent;
    --display: "Boogaloo", Impact, "Arial Black", sans-serif;
    font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }
/* .site-header is sticky at 96px (72px under 900px), so an anchor target would
   land underneath it. scroll-padding-top offsets every in-page jump by the
   header height plus a little breathing room. */
html { scroll-behavior: smooth; scroll-padding-top: 112px; background: var(--ink); }
/* Standard properties first: Firefox has only ever had these, and Chromium 121+
   honours them too, so the ::-webkit- block below is the legacy leg rather than
   the primary one. Declaring both is not a duplicate — where both are supported
   the standard properties win, and thin is the closer match to the 12px bar. */
html { scrollbar-width: thin; scrollbar-color: var(--splash) var(--surface); }
/* Legacy WebKit. Square thumb on a flat track: a rounded pill would be the one
   soft edge left on a page built out of hard offsets. The transparent border
   plus background-clip is what insets the thumb without a radius — padding does
   not apply to scrollbar parts. */
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--surface); border-left: 1px solid var(--line); }
::-webkit-scrollbar-thumb {
    background: var(--splash);
    background-clip: padding-box;
    border: 3px solid transparent;
}
::-webkit-scrollbar-thumb:hover { background: var(--splash-hot); background-clip: padding-box; }
/* Where the two bars meet. Left as the track colour, the default is a light
   system square that reads as a hole in the corner. */
::-webkit-scrollbar-corner { background: var(--surface); }
body {
    margin: 0;
    min-width: 320px;
    min-height: 100vh;
    overflow-x: hidden;
    color: var(--text);
    background: var(--ink);
}
a { color: inherit; text-decoration: none; }
button, input, select { font: inherit; }
button, a, input, select { -webkit-tap-highlight-color: transparent; }
:focus-visible { outline: 3px solid var(--focus); outline-offset: 4px; }
img { max-width: 100%; }
.shell { width: min(1320px, calc(100% - 40px)); margin-inline: auto; }
.skip-link { position: fixed; z-index: 300; top: 12px; left: 12px; transform: translateY(-160%); padding: 10px 14px; color: #150603; background: var(--focus); border-radius: 999px; font-weight: 800; }
.skip-link:focus { transform: none; }
/* The skip link's target. <main> is not focusable by default, so a browser that
   does not move focus on a fragment jump (Safari, and Chromium before it was
   fixed) would scroll the page while leaving focus on the link — the next Tab
   lands back in the header. tabindex="-1" makes it a focus target; the outline
   is suppressed because the whole page is the thing focused and a ring around
   it reads as a rendering fault, not as feedback. */
#main:focus { outline: none; }
.halftone { background-image: var(--halftone); background-size: 12px 12px; }

/* No overflow clip: the logo is meant to hang past the bottom rule and sit over
   the hero. z-index keeps the whole header, overhang included, above the page. */
.site-header { position: sticky; top: 0; z-index: 80; border-bottom: 1px solid var(--line); background: var(--ink); }
.nav { height: 96px; display: flex; align-items: center; gap: 18px; }
.brand { display: inline-flex; align-items: center; }
/* Wide lockup, not a square mark: 2172x724 art, so the box keeps 3:1. Taller
   than the 84px nav row and pushed lower still, so it breaks the header's bottom
   rule and overhangs the hero. The nudge is a transform, not a margin, so
   pushing it down and left does not drag the nav links with it. */
.brand img { width: 288px; height: 96px; flex: none; object-fit: contain; transform: translateX(-16px); }
/* Carries the same 12px drop as the logo. Without it the row stays centred on
   the nav box while the logo sits lower, so the links read as floating high. */
.nav-links { display: flex; align-items: center; gap: 30px; margin-right: auto; margin-left: 14px; }
/* Upright on purpose. An earlier skewX() slant sheared the rasterised glyph
   layer and cost the text its hinting; keeping the type untransformed is what
   makes it render crisply. */
.nav-links a {
    position: relative;
    padding: 7px 2px;
    color: var(--paper);
    font-family: var(--display);
    font-weight: 400;
    font-size: 1.3rem;
    letter-spacing: .055em;
    text-transform: uppercase;
    transition: color .18s ease;
}
.nav-links a:hover { color: var(--splash-hot); }
/* An anchor link taking the mark has to take it from the current-page link too,
   or Home keeps its stroke while Releases draws a second one. */
.nav-links.is-anchor-marked a[aria-current="page"] { color: var(--paper); }
.nav-links.is-anchor-marked a[aria-current="page"]:not(.is-marked)::after { content: none; }
.nav-links a[aria-current="page"], .nav-links a.is-marked { color: var(--splash); }
/* One stroke at a time: it marks the current page, or - for the in-page anchor
   links, which are never the current page - whichever one was last clicked.
   .is-marked is set by site.js and replays the draw on every click. Absolutely
   positioned, so it adds no height: padding stays symmetric and the label keeps
   its place. */
.nav-links a[aria-current="page"]::after, .nav-links a.is-marked::after {
    position: absolute;
    /* Measured from the link box, which carries 7px of bottom padding, so 100%
       lands well clear of the glyphs. Pull back into that padding to sit the
       stroke against the text without adding height. */
    top: calc(100% - 2.5px);
    left: -3px;
    right: -3px;
    height: 11px;
    content: "";
    /* Marker swipe: tapered to a point at both ends, fattest just past centre. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 14' preserveAspectRatio='none'%3E%3Cpath d='M1.2 8.6 C16 5.1 38 3.2 63 2.9 C84 2.7 103 3.4 118.8 4.8 C104 6.9 84 8.4 62 8.9 C40 9.4 19 9.4 1.2 8.6 Z' fill='%23ff4d00'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-size: 100% 100%;
    /* scaleX draws on the compositor instead of repainting the background. */
    transform-origin: left center;
    animation: marker-draw .52s cubic-bezier(.35, .9, .4, 1) .12s both;
}
/* Comic panel button: three concentric bands on the hero buttons' skewed panel. */
.nav-cta {
    position: relative;
    /* Rounded on the skewed panel: the shear turns each corner into a slightly
       asymmetric ellipse rather than a circular arc, which reads as intentional
       at this angle. Inner layers step the radius down by their own inset, or
       the bands stop running parallel around the corners. */
    border-radius: 10px;
    display: inline-flex;
    align-items: center;
    /* Clearance for both bands beneath the label. */
    padding: 16px 34px;
    border: none;
    color: var(--paper);
    --btn-face: var(--splash);
    font-family: var(--display);
    font-weight: 400;
    /* Deliberately a step above .nav-links a (1.3rem): the CTA is the primary
       action in the header, so it leads rather than matches. Tracking stays at
       the links' .055em — the earlier .08em was what made it read as mismatched
       rather than larger. */
    font-size: 1.42rem;
    letter-spacing: .055em;
    text-transform: uppercase;
    transform: skewX(-9deg);
    /* Outermost of three bands: white, then black, then the orange face. The
       pseudo-elements inherit the skew from this element, so all three stay in
       register without repeating the transform.
       Stacking comes from paint order, not z-index: ::before then ::after paint
       in sequence, which gives outer to inner for free. */
    background: var(--paper);
    box-shadow: 6px 7px 0 rgba(0, 0, 0, .72);
    /* Label ring sits outside the glyph. An inward stroke was tried and is not
       viable at this size: any visible thickness is a large share of Boogaloo's
       stem width at 1.3rem, so the black closed over the letterforms. */
    text-shadow: var(--ink-ring);
    transition: box-shadow .2s ease;
}
/* Middle band: the black edge. */
.nav-cta::before {
    position: absolute;
    inset: 2px;
    border-radius: 8px;
    content: "";
    background: var(--ink);
}
/* Inner band: the orange face. */
.nav-cta::after {
    position: absolute;
    inset: 7px;
    border-radius: 3px;
    content: "";
    background: var(--btn-face);
    transition: background .2s ease;
}
/* Counter-skew returns the label to a net identity transform so the glyphs
   rasterise unsheared; z-index lifts it above the two band layers. */
.nav-cta span { position: relative; z-index: 1; display: inline-block; transform: skewX(9deg); }
.nav-cta:hover { --btn-face: var(--splash-hot); box-shadow: 9px 10px 0 rgba(0, 0, 0, .72); }
/* Staff-only, so it leaves the nav row entirely and hangs off the header's top
   right corner as a small tab. That keeps it out of the visitor CTA's rhythm
   without hiding it. .site-header is position: sticky, which already makes it
   the containing block for absolute children, so no extra context is needed.
   The 28px height and the CTA's 10px drop below it are one measurement, not
   two: the CTA is 55px tall in a 96px row, so centred it starts at 20.5px and
   the tab would clip its corner. Dropped, it starts at 30.5px and clears. */
.nav-admin {
    position: absolute;
    top: 0;
    right: 0;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    height: 28px;
    padding: 0 16px;
    /* Only the inner corner is cut, and only the two inner edges are drawn, so
       the tab reads as part of the header's edge rather than a floating chip. */
    border: 1px solid var(--line);
    border-top: 0;
    border-right: 0;
    border-radius: 0 0 0 12px;
    background: var(--surface);
    color: var(--muted);
    font-weight: 900;
    font-size: .8rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    /* Upright: the CTA's skew would lift the tab off the header's square corner. */
    transform: none;
}
.nav-admin:hover { color: white; background: var(--surface-raised); }
/* Buys the tab its clearance. The row keeps its 96px, and the CTA's 6px/7px
   drop shadow still lands inside it. */
.nav-cta { margin-top: 10px; }
/* The burger wears the CTA's comic panel: same three concentric bands on the
   same skew, so the two controls sharing the mobile row read as one family.
   Shadow and hover growth are scaled down from the CTA's - it is the smaller
   sibling, not a second primary action. */
.menu-toggle {
    display: none;
    position: relative;
    width: 56px;
    height: 44px;
    padding: 0;
    border: none;
    border-radius: 8px;
    --btn-face: var(--splash);
    background: var(--paper);
    box-shadow: 5px 6px 0 rgba(0, 0, 0, .72);
    transform: skewX(-9deg);
    cursor: pointer;
    transition: box-shadow .2s ease;
}
.menu-toggle::before { position: absolute; inset: 2px; border-radius: 6px; content: ""; background: var(--ink); }
.menu-toggle::after { position: absolute; inset: 6px; border-radius: 2px; content: ""; background: var(--btn-face); transition: background .2s ease; }
.menu-toggle:hover { --btn-face: var(--splash-hot); box-shadow: 7px 8px 0 rgba(0, 0, 0, .72); }
/* Counter-skewed like the CTA's label, and lifted over the band pseudos the
   same way: z-index on the content, none on the bands. */
.menu-toggle-box { position: relative; z-index: 1; display: flex; flex-direction: column; gap: 4px; transform: skewX(9deg); }
.menu-toggle-line {
    width: 18px;
    height: 3px;
    border-radius: 2px;
    background: var(--paper);
    box-shadow: 1px 1px 0 rgba(0, 0, 0, .5);
    transition: transform .3s cubic-bezier(.16, 1, .3, 1), opacity .2s ease;
}
/* Open state folds the lines into a cross. 7px is one line height plus the 4px
   gap - the travel that lands the outer lines on the middle one's row. */
.menu-toggle[aria-expanded="true"] .menu-toggle-line:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.menu-toggle[aria-expanded="true"] .menu-toggle-line:nth-child(2) { opacity: 0; transform: scaleX(.4); }
.menu-toggle[aria-expanded="true"] .menu-toggle-line:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
/* The dropdown's twin of the header CTA exists only where the dropdown does. */
.nav-cta--menu { display: none; }

/* stretch, not center: the shell has to reach the hero's own edges so the stage
   inside it can, too. The text stays centred via .hero-grid below. */
.hero { position: relative; min-height: 680px; display: grid; align-items: stretch; padding: 88px 0 76px; overflow: hidden; border-bottom: 1px solid var(--line); }
/* Comic speed lines thrown out of the splash. A conic repeat is the right tool:
   the wedges keep a fixed angle, so they widen with distance the way drawn
   impact lines do. Absolute, so it stays out of the hero's grid flow.
   Two elements because the mask must hold still while the lines turn: this one
   owns the mask, keeping the fan on the flat left side and cutting the middle
   where converging wedges would pool into a solid blob. */
.hero-rays {
    position: absolute;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    -webkit-mask-image: radial-gradient(ellipse 62% 70% at 28% 52%, #000 22%, transparent 80%);
    mask-image: radial-gradient(ellipse 62% 70% at 28% 52%, #000 22%, transparent 80%);
    pointer-events: none;
}
/* The turning layer. Only ever rotates 5deg, because that is the gradient's
   repeat period — at 5deg the pattern lands exactly on itself, so a short loop
   reads as one continuous slow revolution. That keeps the overdraw needed to
   cover the swept corners to ~14% instead of the 300%-wide layer a true 360deg
   spin would need. transform animates on the compositor, so no per-frame
   repaint: animating the conic's own angle would re-rasterise ~1.1M pixels
   every frame instead. Origin is the splash's mass, restated in this box's
   own coordinates. */
.hero-rays::before {
    position: absolute;
    inset: -14%;
    content: "";
    background-image: repeating-conic-gradient(
        from 0deg at 73.4% 53.9%,
        rgba(255, 77, 0, .14) 0deg .7deg,
        transparent .7deg 5deg
    );
    transform-origin: 73.4% 53.9%;
    animation: hero-rays-spin 2.4s linear infinite;
}
/* 5deg every 2.4s is one full turn every ~2.9 minutes. */
@keyframes hero-rays-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(5deg); }
}
/* Lifts the copy above the halftone layer. */
.hero > .shell { position: relative; z-index: 1; height: 100%; }
.hero-grid { display: grid; height: 100%; grid-template-columns: minmax(0, 1.15fr) minmax(320px, .85fr); gap: clamp(44px, 8vw, 110px); align-items: center; }
.eyebrow { display: inline-flex; align-items: center; gap: 9px; color: #ffd3bd; font-size: .76rem; font-weight: 900; letter-spacing: .19em; text-transform: uppercase; }
.eyebrow::before { width: 34px; height: 2px; content: ""; background: var(--splash); }
h1, h2, h3, p { margin-top: 0; }
/* Display face needs positive tracking; the offset shadows are the comic depth.
   Uppercase is presentational so the CMS keeps its sentence-case string. */
.hero h1 {
    max-width: 15ch;
    /* line-height .92 crops the glyph box tight and the drop shadow eats 8px of
       what is left, so the measured gap reads smaller than it is. */
    /* Tight to the copy below: an even gap on both sides left the subhead
       floating between the headline and the buttons instead of belonging to
       either. The space now falls after it, not around it. */
    margin: 0 0 24px;
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(3.25rem, 8.4vw, 7rem);
    line-height: .92;
    letter-spacing: .012em;
    text-transform: uppercase;
    text-wrap: balance;
    display: flex;
    flex-direction: column;
}
/* Two CMS-bound lines, matching the sibling sites' hero-title pattern.
   Each takes its own depth: orange under white, black under orange. */
/* One offset each, same distance, solid — the buttons cast a single hard shadow
   and the headline now speaks the same way. The lead previously stacked an
   orange offset and a translucent black one, which read as a third device beside
   the buttons' bands. Colour is the only variable left between the two lines. */
.hero-title-lead { color: var(--paper); text-shadow: 5px 5px 0 var(--splash); }
.hero-title-accent { color: var(--splash); text-shadow: 5px 5px 0 var(--ink); }
/* The only body-type line in a hero of display faces, and the one that says what
   the site is — sized up so it carries rather than reads as placeholder. */
.hero-copy { max-width: 42ch; color: var(--text); font-size: clamp(1.15rem, 2.2vw, 1.35rem); line-height: 1.55; }
.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; }
.button { display: inline-flex; min-height: 52px; align-items: center; justify-content: center; gap: 10px; padding: 0 24px; border: 2px solid var(--line-strong); border-radius: 999px; color: var(--text); background: var(--surface-raised); font-weight: 850; cursor: pointer; transition: transform .2s ease, border-color .2s ease, background .2s ease; }
.button:hover { transform: translateY(-2px); border-color: var(--splash); background: var(--surface-soft); }
.button-primary { border-color: var(--splash); color: white; background: var(--splash); }
.button-primary:hover { border-color: var(--splash-hot); background: var(--splash-hot); }
/* The header CTA's panel-plus-offset-frame vocabulary, shared by the three
   contexts enumerated below rather than promoted to a public class: each one has
   to opt in, and each supplies its own scale. :is() takes its highest-specificity
   argument, so the compound still weighs exactly what the original hero-only
   selector did and nothing in the cascade re-orders.
   Metrics are NOT shared — 74px/1.42rem is a full-bleed hero measurement. The
   modal and the cookie banner override size and shadow in their own blocks.
   The tier cards are the fourth context, and they are prepended to each rule
   rather than added inside the :is(). The contract test pins that argument list
   verbatim, so growing it is a failing change and appending after .button would
   turn the pinned brace into a comma. Prepending leaves the pinned string
   untouched, and `.actions--panel .button` weighs the same (0,3,0) as the
   hero compound, so nothing in the cascade re-orders either. */
/* Gap must clear both frames: each overhangs its button by 8px. Scoped margin
   too — the shared .actions rule also lays out the age-gate modal's buttons. */
.hero .actions { gap: 30px; margin-top: 52px; }
.actions--panel .button,
:is(.hero .actions, .modal .actions, .cookie-banner) .button {
    position: relative;
    min-height: 74px;
    padding: 0 36px;
    border: none;
    border-radius: 10px;
    color: var(--paper);
    /* Lifted off --surface-raised: at the card surfaces' darkness the secondary
       button lost its edge against the hero and read as a hole. */
    --btn-face: #3a1610;
    font-family: var(--display);
    font-weight: 400;
    /* Matched to .nav-cta so every panel button on the page reads at one size. */
    font-size: 1.42rem;
    letter-spacing: .055em;
    text-transform: uppercase;
    transform: skewX(-9deg);
    /* Same three concentric bands as the header CTA: white outermost, then
       black, then the face. The pseudo-elements inherit the skew from here, so
       all three stay in register. Stacking is paint order, not z-index —
       ::before then ::after gives outer to inner for free. */
    background: var(--paper);
    box-shadow: 6px 7px 0 rgba(0, 0, 0, .72);
    text-shadow: var(--ink-ring);
    transition: transform .2s ease, box-shadow .2s ease;
}
/* Middle band: the black edge. */
.actions--panel .button::before,
:is(.hero .actions, .modal .actions, .cookie-banner) .button::before {
    position: absolute;
    inset: 2px;
    border-radius: 8px;
    content: "";
    background: var(--ink);
    pointer-events: none;
}
/* Inner band: the face. */
.actions--panel .button::after,
:is(.hero .actions, .modal .actions, .cookie-banner) .button::after {
    position: absolute;
    inset: 7px;
    border-radius: 3px;
    content: "";
    background: var(--btn-face);
    pointer-events: none;
    transition: background .2s ease;
}
/* Counter-skew returns label and icon to a net identity transform, so the type
   rasterises unsheared and the arrow keeps square corners inside the slanted
   panel. z-index lifts them above the two band layers. Safe with the CMS: its
   editor rewrites the text node, not the markup. */
.actions--panel .button > span,
.actions--panel .button > .button-icon,
:is(.hero .actions, .modal .actions, .cookie-banner) .button > span,
:is(.hero .actions, .modal .actions, .cookie-banner) .button > .button-icon { position: relative; z-index: 1; display: inline-block; transform: skewX(9deg); }
/* Drawn, not a text glyph: the display face has no arrow, so a character would
   fall back to another font and break weight and baseline against the label. */
/* Narrower box than the old shafted arrow: a lone chevron in a square would
   leave dead space either side and read as an over-wide gap after the label. */
.button-icon { flex: none; width: 13px; height: 20px; }
/* Hover grows the shadow and leaves the panel where it is, which is what
   .nav-cta has always done. It used to travel up-left as well, and that had two
   costs: the skewed label re-rasterised off the pixel grid mid-move, and a
   pointer near the bottom or right edge fell outside the button as it left,
   un-hovering it, snapping it back, and re-hovering - a loop that reads as a
   twitch rather than as a lift. Press still travels: the pointer is held down
   by then, so it cannot be shaken off, and the movement is the whole point of a
   press. The skew is repeated in both - transform is one property, and dropping
   the skewX here would snap the panel square for the length of the transition. */
.actions--panel .button:hover,
.actions--panel .button:focus-visible,
:is(.hero .actions, .modal .actions, .cookie-banner) .button:hover,
:is(.hero .actions, .modal .actions, .cookie-banner) .button:focus-visible { --btn-face: #4b1e16; transform: skewX(-9deg); box-shadow: 9px 10px 0 rgba(0, 0, 0, .72); }
.actions--panel .button:active,
:is(.hero .actions, .modal .actions, .cookie-banner) .button:active { transform: translate(3px, 4px) skewX(-9deg); box-shadow: 2px 2px 0 rgba(0, 0, 0, .72); }
.actions--panel .button-primary,
:is(.hero .actions, .modal .actions, .cookie-banner) .button-primary { --btn-face: var(--splash); }
.actions--panel .button-primary:hover,
.actions--panel .button-primary:focus-visible,
:is(.hero .actions, .modal .actions, .cookie-banner) .button-primary:hover,
:is(.hero .actions, .modal .actions, .cookie-banner) .button-primary:focus-visible { --btn-face: var(--splash-hot); }
/* Secondary keeps the primary's white-then-black edge and drops only the fill,
   so the two read as one family: edge is the constant, face is the variable.
   Both bands are borders rather than stacked fills — a filled band would paint
   across the middle and there would be nothing to see through. The element's own
   border is the white; ::before sits on the padding box, just inside it, and
   carries the black. box-shadow still draws off the border box, so the panel
   keeps its offset with no fill behind it. */
.actions--panel .button:not(.button-primary),
:is(.hero .actions, .modal .actions, .cookie-banner) .button:not(.button-primary) { background: transparent; border: 2px solid var(--paper); }
.actions--panel .button:not(.button-primary)::before,
:is(.hero .actions, .modal .actions, .cookie-banner) .button:not(.button-primary)::before { inset: 0; border-radius: 8px; background: transparent; border: 5px solid var(--ink); transition: background .2s ease; }
.actions--panel .button:not(.button-primary)::after,
:is(.hero .actions, .modal .actions, .cookie-banner) .button:not(.button-primary)::after { display: none; }
/* Secondary has no ::after to tint, so the fill lands on the band element itself:
   the ink border stays, the hole inside it takes paper. Hovered it inverts — white
   panel, black label — which is the one face the primary can never wear, so the two
   stay legible as different ranks no matter which one the pointer is over. Filling
   it with splash instead made it a second primary.
   The element background stays transparent so the white edge is still a border,
   not a fill.
   The ring goes out with the flip rather than staying on: at rest it holds the white
   label off the dark hero, but over the white fill it is a black outline on black
   type and does nothing except thicken every stem. It fades rather than switching —
   text-shadow: none is not an interpolable endpoint, so it would snap back the
   instant the pointer left and smear a black ring across a still-black label over a
   still-draining fill. --ink-ring-clear is the same list with the colour taken out,
   which transitions. All three — fill, label, ring — run the same .2s, so the ring
   is still there while the label is light and gone by the time it is dark. */
.actions--panel .button:not(.button-primary),
:is(.hero .actions, .modal .actions, .cookie-banner) .button:not(.button-primary) { transition: transform .2s ease, box-shadow .2s ease, color .2s ease, text-shadow .2s ease; }
.actions--panel .button:not(.button-primary):hover,
.actions--panel .button:not(.button-primary):focus-visible,
:is(.hero .actions, .modal .actions, .cookie-banner) .button:not(.button-primary):hover,
:is(.hero .actions, .modal .actions, .cookie-banner) .button:not(.button-primary):focus-visible { background: transparent; color: var(--ink); text-shadow: var(--ink-ring-clear); }
.actions--panel .button:not(.button-primary):hover::before,
.actions--panel .button:not(.button-primary):focus-visible::before,
:is(.hero .actions, .modal .actions, .cookie-banner) .button:not(.button-primary):hover::before,
:is(.hero .actions, .modal .actions, .cookie-banner) .button:not(.button-primary):focus-visible::before { background: var(--paper); }
/* Anchored toward the left edge of the hero artwork area so the splash stays
   on the right side of the page without being pushed off-screen.
   max-width: none is required: the global `img { max-width: 100% }` would other-
   wise cap the artwork at the stage width. */
/* Spans the hero edge to edge vertically: stretch fills the content box, and the
   negative margins cancel the hero's own padding so the box reaches the border.
   That makes `bottom: 0` on the artwork below mean the hero's bottom, not some
   offset inside a centred row — the offset is what every previous magic number
   here was compensating for. Height comes from the stretch, so no aspect-ratio. */
.brand-stage {
    position: relative;
    width: 128%;
    align-self: stretch;
    margin: -88px 0 -76px;
}
.splash-burst {
    position: absolute;
    left: -32%;
    bottom: 0;
    width: 205%;
    max-width: none;
    height: auto;
    pointer-events: none;
}
/* Centred in the splash, sitting on top of it. */
/* Keep the mascot toward the right side of the artwork area. */
/* Bottom-anchored like the splash: the torso art runs off its own frame, so it
   is meant to be cut by the hero rather than sit whole inside it. 50% is the
   middle of the part of the stage actually on screen, not the stage's centre.
   The width is what makes her head match the old head-crop on screen — the head
   is ~60% of this frame against ~95% of the last one — and it needs max-width
   because the global `img { max-width: 100% }` would clamp it to the stage. */
/* icon_torso.png reads ~6% too wide against the creator's original art. Landmark
   measurement could not confirm it (per-feature scale ratios scattered 0.89-1.07,
   as wide as the effect), so this is corrected by eye. The box is narrowed while
   aspect-ratio holds the rendered height at the old 100%-of-stage, and object-fit
   fill squeezes the bitmap into it rather than letterboxing. Drop all three
   properties to revert. */
.brand-portrait { position: absolute; z-index: 2; left: 50%; bottom: 0; width: 94%; aspect-ratio: 94 / 100; object-fit: fill; max-width: none; height: auto; transform: translateX(-50%);
    /* icon_torso.png carries 23 transparent rows below the art (last opaque row
       is 1230 of 1254), which reads as her floating. Pull them back off. A
       percentage margin resolves against the stage's width — the same thing the
       square image scales with — so the correction holds at any viewport. */
    margin-bottom: -1.83%; }

/* One connected gallery. Filter chips borrow the button language at a smaller
   weight: skewed panel, white edge, hard shadow, no inner bands — three bands on
   a 36px chip collapses into mush.
   The panel is reserved for the filters. A heading's "view all" is not a fourth
   filter — it leaves the page rather than narrowing it — so it is a link, below. */
.gallery-filters { display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 26px; }
.gallery-pill {
    position: relative;
    display: inline-flex;
    align-items: center;
    padding: 9px 20px;
    border: 2px solid var(--paper);
    border-radius: 8px;
    color: var(--paper);
    background: transparent;
    /* The band slot, held open and empty. A shadow list only interpolates against
       another of the same length, so a chip that grows from one shadow to two would
       jump — taking the offset with it, since that is the same property. Every pill
       state below therefore declares both, and colours the inset in or out. */
    box-shadow: inset 0 0 0 2px transparent, 4px 5px 0 rgba(0, 0, 0, .72);
    font-family: var(--display);
    font-weight: 400;
    font-size: 1.06rem;
    letter-spacing: .055em;
    text-transform: uppercase;
    cursor: pointer;
    transform: skewX(-9deg);
    transition: background .18s ease, color .18s ease, transform .2s ease, box-shadow .2s ease;
}
/* Counter-skew keeps the glyphs unsheared inside the slanted chip. */
.gallery-pill span { display: inline-block; transform: skewX(9deg); }
/* Same pop and press as the hero panels at chip scale, so the filter row and the
   hero read as one family. Movement and shadow are the part every chip shares,
   selected or not — the fill is what splits below. */
.gallery-pill:hover,
.gallery-pill:focus-visible { transform: translate(-2px, -2px) skewX(-9deg); box-shadow: inset 0 0 0 2px transparent, 6px 7px 0 rgba(0, 0, 0, .72); }
/* The hero's inversion at chip scale: white fill, black label. The row splits the
   same way the hero's two buttons do — the selected chip is this row's primary and
   keeps its orange, everything else inverts — so hovering an unselected filter can
   never be mistaken for selecting it. An orange tint was what it did before and it
   read as a dimmer version of selected, which is the one thing a filter row must
   not blur.
   No ring to fade here: unlike the hero panels, an unselected chip carries no
   --ink-ring at rest, so fill and label are the whole flip.
   The band is what keeps this the hero's inversion rather than a plain white block.
   The chip's own 2px paper border vanishes into a paper fill, so without it the
   three-band reading Membership has — paper edge, ink, paper face — collapses to
   one flat slab. It is an inset shadow rather than a third element for the reason
   the selected chip already gives below: at 38px a real band element turns to mush.
   Inset paints inside the border, so the order comes out edge-then-band-then-face
   with nothing extra in the box. */
.gallery-pill:not(.is-active):hover,
.gallery-pill:not(.is-active):focus-visible { color: var(--ink); background: var(--paper); box-shadow: inset 0 0 0 2px var(--ink), 6px 7px 0 rgba(0, 0, 0, .72); }
.gallery-pill:active { transform: translate(2px, 3px) skewX(-9deg); box-shadow: inset 0 0 0 2px transparent, 1px 2px 0 rgba(0, 0, 0, .72); }
/* Pressed is still hovered, so the band has to be restated here or the chip loses
   it on the way down — :active and :hover carry equal weight and this one is later. */
.gallery-pill:not(.is-active):active { box-shadow: inset 0 0 0 2px var(--ink), 1px 2px 0 rgba(0, 0, 0, .72); }
/* A filter waiting on its response. The outgoing cards stay in place and stay
   readable — emptying the grid first would collapse the section and yank the page
   up under the pointer that is still on the filter row. Dimming is the whole of it:
   pointer-events go too, so a second click cannot queue a request against cards
   that are already being replaced. */
.gallery-grid[aria-busy="true"], .media-grid[aria-busy="true"] { opacity: .45; pointer-events: none; transition: opacity .18s ease; }
/* Deliberately not a panel. Sitting a line above the filter row, anything with an
   edge and an offset shadow reads as one more filter; this one leaves the page,
   so it drops the panel entirely and keeps only the type. Orange rather than
   paper is what marks it as the outbound one. The arrow is the hero button's,
   ink outline and all, at heading scale. */
.section-link {
    display: inline-flex;
    align-items: center;
    gap: 11px;
    padding-bottom: 5px;
    color: var(--splash);
    font-family: var(--display);
    font-weight: 400;
    font-size: 1.16rem;
    letter-spacing: .055em;
    text-transform: uppercase;
    white-space: nowrap;
    transition: color .18s ease;
}
.section-link-icon { flex: none; width: 11px; height: 17px; transition: transform .18s ease; }
.section-link:hover { color: var(--splash-hot); }
/* Moves in the direction it sends you. */
.section-link:hover .section-link-icon { transform: translateX(3px); }
/* Selected reads as the family's primary rather than a flat orange fill: paper
   edge, then the ink band, then the face. The band is an inset shadow because a
   third element on a 38px chip is what turned to mush — inset gets the same
   outer-to-inner order out of the box the chip already has. The label takes the
   ring every light-on-orange label wears. */
.gallery-pill.is-active {
    color: var(--paper);
    background: var(--splash);
    box-shadow: inset 0 0 0 2px var(--ink), 4px 5px 0 rgba(0, 0, 0, .72);
    text-shadow: var(--ink-ring);
}
.gallery-pill.is-active:hover,
.gallery-pill.is-active:focus-visible {
    background: var(--splash-hot);
    transform: translate(-2px, -2px) skewX(-9deg);
    box-shadow: inset 0 0 0 2px var(--ink), 6px 7px 0 rgba(0, 0, 0, .72);
}
/* The inset band is the chip's face and travels with it; only the outer offset
   collapses on press. */
.gallery-pill.is-active:active { box-shadow: inset 0 0 0 2px var(--ink), 1px 2px 0 rgba(0, 0, 0, .72); }

/* Uniform grid, deliberately. Masonry by columns closed every gap but flowed
   items down each column, so newest-first content read scrambled left to right.
   A fixed cell keeps the order honest; the crop it forces is the same trade
   every gallery makes, and the full piece is what a lightbox is for.
   auto-fill rather than auto-fit: empty tracks stay reserved, so a short filtered
   set keeps the gallery's rhythm instead of stretching to fill the row. */
/* 300px floor caps the row at four inside the 1320px shell (4x300+3x16 = 1248,
   a fifth would need 1564). Five columns made the cells too small to read art in. */
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
/* One cell, both grids. The landing gallery and Browse show the same object in
   the same rhythm, so the portrait cell is declared here once rather than copied
   into the browse block further down and left to drift out of step. .card's own
   min-height is a list-view measurement and is cleared here; the list view takes
   it back, and the ratio with it, in .is-list .card. */
.gallery-grid .card, .media-grid .card { min-height: 0; aspect-ratio: 4 / 5; border-width: 2px; border-radius: 12px; }
/* 4:5, not 4:3. Aspects across the sample library run 0.56 to 1.84, and the tall
   cluster (0.70-0.93) outnumbers the square one -- character art and comic pages.
   A landscape cell would crop the bulk of the work hardest. */
.gallery-tile { position: relative; overflow: hidden; margin: 0; border: 2px solid var(--line); border-radius: 12px; background: var(--surface); aspect-ratio: 4 / 5; }
/* Sets are the norm, so the page count sits on the cover rather than hiding
   behind a click. Bottom-right keeps it clear of the kind badge. */
.gallery-tile-count {
    position: absolute;
    right: 10px;
    bottom: 10px;
    padding: 4px 10px;
    border-radius: 999px;
    color: var(--text);
    background: rgba(7, 4, 4, .82);
    font-size: .66rem;
    font-weight: 900;
    letter-spacing: .1em;
    text-transform: uppercase;
}
/* Halftone marks these as deliberately empty rather than broken artwork. */
.gallery-tile-fill { position: absolute; inset: 0; opacity: .5; }
.gallery-tile-tag {
    position: absolute;
    top: 12px;
    left: 12px;
    padding: 5px 12px;
    border-radius: 999px;
    color: var(--ink);
    background: var(--splash);
    font-weight: 900;
    font-size: .68rem;
    letter-spacing: .13em;
    text-transform: uppercase;
}

.section { padding: 90px 0; }
.section-compact { padding: 46px 0 78px; border-bottom: 1px solid var(--line); }
/* The divider separates one section from the next, so the last one on the page
   has nothing to divide itself from: its rule and the footer's would land as a
   doubled line at the end of the page, 60px apart on the same ground. :last-child
   rather than a modifier class because which section is last is decided at render
   time - the gallery drops out on an empty collection and the membership band
   drops out on an empty tier catalog, so either one can end up holding the bottom
   of the page. */
#main > .section-compact:last-child { border-bottom: none; }
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 30px; }
/* The .post-title treatment, so every heading in a section-heading row —
   Latest Releases, Membership, Connect Membership, Privacy policy — speaks in
   the same display voice as Browse and the post pages. They carried a plain
   Inter setting before, which split the site's page and section titles into
   two type systems for no reason a reader could see. */
.section-heading h1, .section-heading h2 {
    margin: 0;
    color: var(--paper);
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(2.1rem, 4.4vw, 3.4rem);
    line-height: .96;
    letter-spacing: .012em;
    text-transform: uppercase;
    text-wrap: balance;
    text-shadow: 4px 4px 0 var(--splash);
}
.section-heading p { max-width: 530px; margin: 0; color: var(--muted); line-height: 1.65; }
/* Browse's h1 also wears .post-title; the two rules now agree on everything
   but the margin, which .post-title carries for a header that stands alone
   and would stack on the row's own 30px here. */
.section-heading .post-title { margin-bottom: 0; }
/* The gallery's template, for the gallery's reasons - see .gallery-grid above.
   A fixed three-column track was what made Browse read as a different page from
   the one that links to it: the same eight cards sat wider here, in three
   columns, against the homepage's four. */
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.media-grid[hidden] { display: none; }
/* Radius steps down to the buttons' 12: at 20 the cards were the roundest thing
   on a page whose vocabulary is hard corners. */
.card { position: relative; min-height: 390px; display: flex; overflow: hidden; border: 2px solid var(--line); border-radius: 12px; background: var(--surface); transition: border-color .25s ease, box-shadow .25s ease; }
/* Hover is the paper edge alone. The card used to throw an orange offset with
   it - a black one would have been invisible against --ink - but an offset that
   only ever appears down-right reads as a stray border rather than as depth, and
   the tile is already carrying artwork, a shade, a play control and a tier badge.
   The border going from --line to --paper is a big enough state change on its own.
   box-shadow stays in the transition list: the tilt below still needs it. */
.card:hover { border-color: var(--paper); }
/* Tilt-to-cursor. Gated on a fine pointer so it never arms on touch, where there
   is no hover to leave and the card would stay stuck at its last angle. The
   angles come from JS as --tilt-x/--tilt-y; everything else, including the whole
   effect being absent, is decided here.
   perspective() is inside the transform rather than on the grid: a perspective on
   the parent would share one vanishing point across every card in the row, so
   cards at the edges would lean away from a centre they do not have. */
@media (hover: hover) and (pointer: fine) {
    .card {
        --tilt-x: 0deg;
        --tilt-y: 0deg;
        --tilt-scale: 1;
        transform: perspective(900px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y)) scale(var(--tilt-scale));
        /* Handed to the card's single transition list at the foot of this file
           rather than declared here. A transition shorthand replaces the whole
           list rather than adding to it, and the card is transitioning three
           separate things - the reveal, the tilt, and the border and shadow on
           hover - so all three have to be written in one place. This variable is
           the half of that list the tilt owns. */
        --card-tilt-transition: transform .5s cubic-bezier(.22, .68, .3, 1);
    }
    /* The longer ease is the settle on the way out. Tracking gets its own timing,
       but not an instant one: at .09s linear the card sat exactly on the cursor
       and every small hand movement showed up as a twitch. A quarter second of
       ease-out lets it trail the pointer slightly, which is what reads as weight.
       will-change is scoped to the card being touched — on every card at once it
       would hand the compositor a layer per card for a grid that mostly sits
       still. */
    .card.is-tilting { --card-tilt-transition: transform .26s cubic-bezier(.19, .66, .35, 1); will-change: transform; }
    .card:hover { --tilt-scale: 1.03; }
}
.card-media { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.card-shade { position: absolute; inset: 0; background: linear-gradient(to top, rgba(8,3,2,.98) 5%, rgba(8,3,2,.45) 60%, rgba(8,3,2,.08)); }
.card-body { position: relative; z-index: 2; align-self: end; width: 100%; padding: 24px 24px 30px; }
.card-meta { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; color: var(--splash-hot); font-size: .74rem; letter-spacing: .12em; text-transform: uppercase; font-weight: 850; }
/* Tracked in like the section headings rather than left at Inter's default. */
.card h3 { margin-bottom: 8px; font-size: 1.38rem; line-height: 1.15; letter-spacing: -.02em; }
/* The hero headline's hard down-right offset, scaled to card type. The hero is
   5px on a ~5rem face; the tier card name runs the same device at 3px on ~2rem.
   The title takes 3px at 1.38rem and the meta 2px at .74rem, so the offset stays
   proportional to the type rather than constant. Both were a step lighter and
   the pair went thin over busy artwork — the offset has to be thick enough to
   hold the letter off the frame behind it, which is the whole job.
   Both offset in ink. The meta has to: it is already splash-hot, and an orange
   shadow under orange type is invisible. The title could carry the hero's splash
   offset instead, but over artwork it made the two lines read as two different
   colours of type — one card's title orange-edged against a warm frame, the next
   against a cold one. Ink is the constant the artwork cannot argue with, and it
   keeps the pair reading as one block. */
.card-meta { text-shadow: 2px 2px 0 var(--ink); }
.card h3 { text-shadow: 3px 3px 0 var(--ink); }
.card p { margin: 0; color: var(--muted); line-height: 1.55; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
/* Was the last glass surface on the page: a 999px pill on a blurred maroon wash.
   Now the gallery chip at badge scale — skewed panel, paper edge, splash face,
   and the ink ring every light-on-orange label wears. No backdrop-filter: it sits
   on artwork, so blur bought nothing but paint cost. */
.tier {
    position: absolute;
    z-index: 3;
    top: 14px;
    right: 14px;
    padding: 5px 11px;
    border: 2px solid var(--paper);
    border-radius: 6px;
    color: var(--paper);
    background: var(--splash);
    box-shadow: 3px 4px 0 rgba(0, 0, 0, .72);
    font-family: var(--display);
    font-weight: 400;
    font-size: .82rem;
    letter-spacing: .05em;
    text-transform: uppercase;
    text-shadow: var(--ink-ring);
    transform: skewX(-9deg);
}
.tier span { display: inline-block; transform: skewX(9deg); }
/* A surface, so it takes .panel's edge and offset rather than the dashed hairline
   and 24px radius it used to wear - the one box left on the site that still read
   as a placeholder outline. The halftone stays: it is the site's own texture and
   was already saying "deliberately empty" rather than "broken".
   No offset with it, though. The chips carry one because a 2px paper edge sits
   between the box and the shadow and that boundary is what reads; this box's
   edge is translucent orange, so a black offset on --ink registers against
   nothing - the objection .card:hover already records.
   The homepage's filter notice is this same markup, built in site.js, so both
   empty states move together. */
.empty { min-height: 280px; display: grid; place-items: center; padding: 44px; border: 2px solid var(--line-strong); border-radius: 14px; text-align: center; background-color: rgba(255,77,0,.03); background-image: radial-gradient(rgba(255, 77, 0, 0.14) 1.4px, transparent 1.6px); background-size: 12px 12px; }
/* Round on purpose: a mark is one of the few shapes
   read before anything is read, and skewing it costs that. It takes the panel's
   other two moves instead - paper edge, hard offset. */
.empty-mark { width: 62px; height: 62px; display: grid; place-items: center; margin: 0 auto 18px; border: 2px solid var(--paper); border-radius: 50%; color: var(--paper); background: var(--splash); box-shadow: 4px 5px 0 rgba(0, 0, 0, .72); font-size: 1.6rem; }
/* .post-title's device at card-heading scale - the same proportion .card h3
   keeps, the offset sized to the letter rather than held constant. */
.empty h3 { margin-bottom: 10px; color: var(--paper); font-family: var(--display); font-weight: 400; font-size: 1.6rem; letter-spacing: .012em; text-transform: uppercase; text-shadow: 3px 3px 0 var(--splash); }
.empty p { max-width: 490px; margin: 0; color: var(--muted); line-height: 1.6; }
/* Centred under the copy, and reined in: .actions--panel arrives at a 74px
   hero measurement, which inside a notice reads as the page's main call to
   action rather than a way out of a dead end. */
.empty-actions { justify-content: center; margin-top: 26px; }
.empty-actions .button { min-height: 52px; padding: 0 24px; font-size: 1.1rem; }

.carousel { position: relative; }
.carousel-track { display: grid; grid-auto-flow: column; grid-auto-columns: clamp(240px, 28vw, 320px); gap: 16px; overflow-x: auto; padding: 4px 4px 14px; scroll-snap-type: x mandatory; overscroll-behavior-x: contain; scrollbar-width: thin; scrollbar-color: var(--splash) transparent; }
.carousel-track .card { min-height: 320px; scroll-snap-align: start; }
.carousel-next { position: absolute; z-index: 5; top: 50%; right: -8px; width: 52px; height: 52px; display: grid; place-items: center; transform: translateY(-50%); border: 2px solid var(--splash); border-radius: 50%; color: var(--splash); background: rgba(7,4,4,.85); cursor: pointer; transition: background .2s ease, color .2s ease; }
.carousel-next:hover { color: var(--pill-ink); background: var(--splash); }
.carousel-next[hidden] { display: none; }

/* -- Featured wall --------------------------------------------------------
   One poster and a 2x2 rail beside it, the shape HRA's spotlight takes with a
   carousel and a thumbnail rail - minus the carousel on desktop. Nothing
   autoplays: a rail whose links keep moving under the cursor is a rail nobody
   can aim at. On phones the same cards become a swipe slider (see the 900px
   block). The poster is a gallery card at a bigger size rather than its own
   component, so it tilts, blurs and badges exactly as the tiles under it do;
   only the type steps up. 4:5 on the poster because the work is portrait - a
   landscape hero of the kind the audio sites run would crop every drawing at
   the neck.
   All cards are direct children of one track, because the phone slider needs
   its slides that way, so the rail is columns of the track's grid rather than
   a grid of its own. The poster column is sized as the old 1.1fr : 1fr split
   of the width left after one gap, and the rail columns share what remains. */
.featured-track { display: grid; grid-template-columns: calc((100% - 16px) * 1.1 / 2.1) repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr)); height: min(66vh, 560px); gap: 16px; }
.featured-track > .card--lead { grid-row: 1 / -1; }
/* The row owns the height and the cards fill it. No aspect-ratio on the poster
   here: a ratio under a height cap becomes a width cap, and the poster shrank
   away from its column and left a gap beside the rail. At the tiles' 4:5 it ran
   near 800px tall on a desktop shell, a second hero under the hero, so the row
   is capped instead and the cover is cropped to fit. The rail cards stretch to
   their rows for the same reason, rather than carrying their own ratio. */
.featured-wall .card { min-height: 0; aspect-ratio: auto; height: 100%; }
/* The rail shapes itself to what it holds. Two or three cards stack in one
   column as landscape strips, which is what a creator featuring "about three"
   gets; four is the 2x2. One class on the wall rather than :has() so the
   count is decided where the cards are counted. */
.featured-wall[data-featured-rail="2"] .featured-track { grid-template-columns: calc((100% - 16px) * 1.1 / 2.1) minmax(0, 1fr); }
.featured-wall[data-featured-rail="3"] .featured-track { grid-template-columns: calc((100% - 16px) * 1.1 / 2.1) minmax(0, 1fr); grid-template-rows: repeat(3, minmax(0, 1fr)); }
.featured-wall[data-featured-rail="3"] .card:not(.card--lead) p { display: none; }
/* Slider dots exist only while the phone slider runs. */
.featured-dots { display: none; }
/* The poster speaks in the section heading's display voice, at a size between
   the tile title and the heading. Ink offset like every card title: it sits on
   artwork, and ink is the one colour the artwork cannot argue with. */
.card--lead .card-body { padding: 32px 32px 40px; }
.card--lead .card-meta { font-size: .84rem; }
.card--lead h3 {
    margin-bottom: 12px;
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(1.9rem, 3.2vw, 2.9rem);
    line-height: .98;
    letter-spacing: .02em;
    text-transform: uppercase;
    text-wrap: balance;
    text-shadow: 4px 4px 0 var(--ink);
}
.card--lead p { max-width: 48ch; font-size: 1.05rem; -webkit-line-clamp: 3; }
/* A bigger tile carries a bigger shade: the tile gradient reaches 60% up a
   390px card, which on a 700px poster leaves the title over bare artwork. */
.card--lead .card-shade { background: linear-gradient(to top, rgba(8,3,2,.98) 8%, rgba(8,3,2,.5) 48%, rgba(8,3,2,.06)); }

/* -- Browse controls -----------------------------------------------------
   The search row, the type filters, the layout toggle and the pager. This was
   the last corner of the site still speaking the pre-redesign language - 999px
   pills, 1px --line edges, a translucent glass field - so a reader who took the
   hero's own "Browse releases" button landed somewhere that looked like a
   different site. Every control below now makes the two moves the rest of the
   page makes: a 2px paper edge and a hard offset, on a -9deg panel whose label
   counter-skews back to upright.
   The type filters themselves are not here at all. posts.php prints
   .gallery-pill, the homepage's chip, so the site has one filter chip rather
   than two that have to be kept in step by hand; only the row holding them is
   local, and it takes .gallery-filters' metrics.
   Every state below declares both the inset band slot and the outer offset, for
   the reason .gallery-pill records: a shadow list only interpolates against
   another of the same length, so a control that grows from one shadow to two
   jumps rather than eases. */
.filter-pills { display: flex; flex-wrap: wrap; gap: 18px; margin: 0 0 26px; }

/* Layout only - no edge, no fill, nothing of its own to see. Every item in this
   row already carries a frame, so a frame around them made a box holding boxes
   and read as chrome the page does not otherwise have: the filter row below it
   is three bare chips on the page ground, and this row is the same kind of thing.
   Losing the padding costs nothing. It was there to keep the skewed submit off
   the frame, and with no frame the overhang - ~4.4px a side at -9deg, plus the
   6px shadow - runs into .shell's 20px gutter instead. */
.search-panel { display: grid; grid-template-columns: 1fr auto auto; gap: 12px; margin: 30px 0 22px; }

/* The Filters dropdown. The toggle is a .field like the sort select beside it,
   so the search row reads as one control strip; the panel is a card dropped
   from its right edge, on the raised surface with the strong line, so it sits
   above the grid rather than in it. Options are checkbox rows: a box that
   fills with the splash and a display-face label, the same two moves the
   pills make, in a column instead of a row. */
.filter-drawer { position: relative; }
.filter-drawer > summary { list-style: none; display: inline-flex; align-items: center; gap: 10px; width: auto; cursor: pointer; user-select: none; white-space: nowrap; font-weight: 600; }
.filter-drawer > summary::-webkit-details-marker { display: none; }
.filter-drawer > summary:hover,
.filter-drawer[open] > summary { border-color: var(--splash); }
.filter-drawer > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.filter-drawer-icon { width: 18px; height: 18px; color: var(--splash); flex: none; }
.filter-drawer-count { min-width: 22px; height: 22px; padding: 0 7px; border-radius: 11px; background: var(--splash); color: var(--paper); font-size: .8rem; font-weight: 700; line-height: 22px; text-align: center; }
.filter-drawer-count[hidden] { display: none; }
.filter-drawer-panel { position: absolute; right: 0; top: calc(100% + 10px); z-index: 30; min-width: 250px; padding: 14px; display: grid; gap: 4px; background: var(--surface-raised); border: 2px solid var(--line-strong); border-radius: 10px; box-shadow: var(--shadow); transform-origin: top right; }
/* Open drops the panel in from the button with a small overshoot, the same
   spring the pills have on hover; close is shorter and plain, since a reader
   dismissing something should not watch it leave. A <details> cannot animate
   its own close - the content is gone the instant open is removed - so the
   script holds `open` through .is-closing until animationend and clears it
   then. The button's icon turns with the panel so the two read as one thing. */
.filter-drawer[open] > .filter-drawer-panel { animation: filter-drawer-in .24s cubic-bezier(.2, .9, .3, 1.25) both; }
.filter-drawer.is-closing > .filter-drawer-panel { animation: filter-drawer-out .16s ease-in both; pointer-events: none; }
.filter-drawer-icon { transition: transform .24s cubic-bezier(.2, .9, .3, 1.25); }
.filter-drawer[open]:not(.is-closing) > summary .filter-drawer-icon { transform: rotate(180deg); }
@keyframes filter-drawer-in { from { opacity: 0; transform: translateY(-10px) scale(.95); } to { opacity: 1; transform: none; } }
@keyframes filter-drawer-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(-8px) scale(.97); } }
@media (prefers-reduced-motion: reduce) {
    .filter-drawer[open] > .filter-drawer-panel,
    .filter-drawer.is-closing > .filter-drawer-panel { animation-duration: .01s; }
    .filter-drawer-icon { transition: none; }
}
.filter-drawer-heading { margin: 0 0 6px; padding: 0 12px; font-size: .76rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.filter-option { display: flex; align-items: center; gap: 12px; padding: 9px 12px; border-radius: 8px; color: var(--text); transition: background .15s ease; }
.filter-option:hover,
.filter-option:focus-visible { background: var(--surface-soft); outline: none; }
.filter-option-box { position: relative; width: 20px; height: 20px; flex: none; border: 2px solid var(--paper); border-radius: 5px; background: transparent; box-shadow: 3px 3px 0 rgba(0, 0, 0, .72); transition: background .15s ease; }
.filter-option.is-active .filter-option-box { background: var(--splash); }
/* Drawn rather than typed: the display face has no check glyph, and this site
   draws its arrows for the same reason. */
.filter-option.is-active .filter-option-box::after { content: ''; position: absolute; inset: 0; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23ffffff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5 6.5 11.5 12.5 4.5'/%3E%3C/svg%3E") center / 13px no-repeat; }
.filter-option-label { font-family: var(--display); font-size: 1.06rem; letter-spacing: .055em; text-transform: uppercase; }
.filter-drawer-clear { margin-top: 6px; padding: 10px 12px 2px; border-top: 1px solid var(--line); font-size: .86rem; color: var(--muted); }
.filter-drawer-clear:hover,
.filter-drawer-clear:focus-visible { color: var(--text); outline: none; }
.filter-drawer-clear[hidden] { display: none; }
/* One step off the page ground rather than a well cut into a panel: with the
   container gone the field sits on --ink, and matching it would leave the input
   defined by nothing but a faint border. --surface is the same lift .card takes
   against the same ground. */
.field { width: 100%; min-height: 48px; padding: 0 16px; border: 2px solid var(--line); border-radius: 8px; color: var(--text); background: var(--surface); transition: border-color .18s ease; }
.field::placeholder { color: #947e75; }
.field:focus { border-color: var(--splash); outline: none; }
/* The native select arrow is a system widget: it ignores the page's palette and
   sits at the platform's own size, which left it the one control on this row
   still looking borrowed. appearance: none takes it away and the chevron is
   drawn back. A data URI rather than a pseudo-element because a <select> cannot
   carry one, and the stroke is written out rather than read from --splash: a
   custom property does not resolve inside url(). Keep the two in step. */
select.field {
    padding-right: 42px;
    cursor: pointer;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%23ff4d00' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 16px center;
}

/* The open menu, where a <select> stops being ours. appearance: base-select
   takes the popup out of the platform and puts it in the page, so ::picker() and
   the options become ordinary boxes that can wear the panel vocabulary. It stays
   a real <select> throughout - same element, same options, same change event the
   browse controller listens on - so nothing here is load-bearing for behaviour.
   Guarded, because this is Chromium's today. Anywhere it is missing the block is
   skipped whole and the native popup stands, over the closed control drawn
   above, which is why the appearance: none rule stays where it is rather than
   being replaced by this one. */
@supports (appearance: base-select) {
    /* Both halves have to opt in. Setting base-select on the select alone gets
       the closed control and the picker icon but leaves the popup rendering as
       the platform's, silently - it does not fail, it just ignores every rule
       below. The picker needs the property in its own right. */
    select.field,
    select.field::picker(select) { appearance: base-select; }
    select.field {
        /* The arrow changes hands: it was a background image because a <select>
           had no element to hang it on, and ::picker-icon is that element. Real
           box, in flow, so the padding that was holding a space for it goes. */
        padding-right: 16px;
        background-image: none;
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
    }
    select.field::picker-icon {
        color: var(--splash);
        transition: rotate .18s ease;
    }
    /* Turning over is the one thing the background image could never do. */
    select.field:open::picker-icon { rotate: 180deg; }
    select.field:open { border-color: var(--splash); }
    /* The panel is the site's, not the platform's: paper edge, hard offset, ink
       ground. It sits in the top layer, so the offset lands on whatever is
       behind it and the edge is what has to carry the shape - hence paper here
       where the closed field is content with --line. */
    select.field::picker(select) {
        padding: 6px;
        border: 2px solid var(--paper);
        border-radius: 8px;
        background: var(--surface);
        box-shadow: 5px 6px 0 rgba(0, 0, 0, .72);
        /* Drops out of the control rather than appearing on top of it. The two
           discrete properties are the whole trick: a popover in the top layer
           goes display:none and leaves the overlay the instant it closes, so
           without allow-discrete on both there is nothing left on screen to
           transition and only the opening would ever animate. */
        opacity: 0;
        translate: 0 -6px;
        transition:
            opacity .16s ease,
            translate .16s ease,
            display .16s allow-discrete,
            overlay .16s allow-discrete;
    }
    select.field:open::picker(select) { opacity: 1; translate: 0 0; }
    /* The state it animates FROM. A popover has no previous computed style on
       the frame it opens - it was not rendered - so without this the open rule
       is simply where it starts and nothing moves. */
    @starting-style {
        select.field:open::picker(select) { opacity: 0; translate: 0 -6px; }
    }
    /* The blanket reduced-motion rule at the foot of this file reaches *,
       ::before and ::after, which does not include a picker. Stated here so the
       menu stands still for a reader who asked for that. Duration rather than
       `none`: the discrete pair still has to run for the popup to be taken off
       screen at all. */
    @media (prefers-reduced-motion: reduce) {
        select.field::picker(select),
        select.field::picker-icon { transition-duration: .001ms; }
    }
    select.field option {
        display: flex;
        align-items: center;
        padding: 10px 14px;
        border-radius: 6px;
        color: var(--text);
        background: transparent;
        font: inherit;
        cursor: pointer;
        transition: background .14s ease, color .14s ease;
    }
    /* :hover for the pointer, :focus for the keyboard - arrow keys move focus
       through the options rather than hovering them, and an unlit row under the
       caret is the usual way a custom menu breaks. */
    select.field option:hover,
    select.field option:focus { color: var(--paper); background: var(--surface-soft); }
    /* Selected reads the way a selected pill does: the splash face and the ring
       every light-on-orange label wears. */
    select.field option:checked {
        color: var(--paper);
        background: var(--splash);
        text-shadow: var(--ink-ring);
    }
    /* The fill already says which one is chosen. The UA's tick beside it says it
       twice and pushes the labels off the field's own left edge. */
    select.field option::checkmark { display: none; }
}
/* The chip at control scale, with the filter row's three states: an outline at
   rest, the inversion on hover - paper fill, ink mark - and the splash face with
   its ink band for the layout actually in use. aria-pressed is the selector
   because it is already the truth site.js maintains; a parallel class would be a
   second thing to keep correct. */
.view-toggle { display: inline-flex; gap: 10px; }
.view-toggle[hidden] { display: none; }
.view-toggle button {
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    border: 2px solid var(--paper);
    border-radius: 8px;
    color: var(--paper);
    background: transparent;
    box-shadow: inset 0 0 0 2px transparent, 3px 4px 0 rgba(0, 0, 0, .72);
    cursor: pointer;
    transform: skewX(-9deg);
    transition: background .18s ease, color .18s ease, transform .2s ease, box-shadow .2s ease;
}
/* Counter-skew keeps the drawn glyph square inside the slanted panel. */
.view-toggle button svg { transform: skewX(9deg); }
.view-toggle button[aria-pressed="true"] { background: var(--splash); box-shadow: inset 0 0 0 2px var(--ink), 3px 4px 0 rgba(0, 0, 0, .72); }
/* Movement and offset are what every button shares, pressed or not; the fill is
   what splits below. Written first so the two fills can override it. */
.view-toggle button:hover,
.view-toggle button:focus-visible { transform: translate(-2px, -2px) skewX(-9deg); box-shadow: inset 0 0 0 2px transparent, 5px 6px 0 rgba(0, 0, 0, .72); }
.view-toggle button[aria-pressed="false"]:hover,
.view-toggle button[aria-pressed="false"]:focus-visible { color: var(--ink); background: var(--paper); box-shadow: inset 0 0 0 2px var(--ink), 5px 6px 0 rgba(0, 0, 0, .72); }
.view-toggle button[aria-pressed="true"]:hover,
.view-toggle button[aria-pressed="true"]:focus-visible { background: var(--splash-hot); box-shadow: inset 0 0 0 2px var(--ink), 5px 6px 0 rgba(0, 0, 0, .72); }
.view-toggle button:active { transform: translate(2px, 3px) skewX(-9deg); box-shadow: inset 0 0 0 2px transparent, 1px 2px 0 rgba(0, 0, 0, .72); }
/* Pressed is still hovered, so each fill restates its band on the way down or it
   is dropped - :active and :hover weigh the same and this one is later. */
.view-toggle button[aria-pressed="false"]:active { box-shadow: inset 0 0 0 2px var(--ink), 1px 2px 0 rgba(0, 0, 0, .72); }
.view-toggle button[aria-pressed="true"]:active { box-shadow: inset 0 0 0 2px var(--ink), 1px 2px 0 rgba(0, 0, 0, .72); }

.media-grid.is-list { grid-template-columns: 1fr; gap: 12px; }
/* The list row takes back both halves of the cell the grids set above: the
   height, and the ratio that would otherwise hold every row at 4:5 and turn the
   list into a second, wider grid. */
.is-list .card { min-height: 128px; aspect-ratio: auto; }
.is-list .card-media { inset: 0 auto 0 0; width: 200px; }
.is-list .card-shade { background: linear-gradient(to right, transparent 160px, rgba(8,3,2,.96) 220px); }
.is-list .card-body { align-self: center; padding: 20px 20px 20px 220px; }

/* The chip once more, at digit scale. The current page wears the filter row's
   selected face - splash, ink band, and the ring every light-on-orange label on
   this site carries.
   .pagination span is deliberately not styled: the span is now the counter-skew
   wrapper inside each link, not a non-link page number. */
.pagination { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 40px; }
.pagination:empty { display: none; }
.pagination a {
    min-width: 44px;
    min-height: 44px;
    display: grid;
    place-items: center;
    padding: 0 12px;
    border: 2px solid var(--paper);
    border-radius: 8px;
    color: var(--paper);
    background: transparent;
    box-shadow: inset 0 0 0 2px transparent, 3px 4px 0 rgba(0, 0, 0, .72);
    font-family: var(--display);
    font-weight: 400;
    font-size: 1.06rem;
    letter-spacing: .055em;
    transform: skewX(-9deg);
    transition: background .18s ease, color .18s ease, transform .2s ease, box-shadow .2s ease;
}
.pagination a span { display: inline-block; transform: skewX(9deg); }
/* The ellipsis between two runs of pages and the previous/next steps. The gap
   is a span, not a link: same footprint as a number so the row stays on one
   rhythm, no edge or shadow so it reads as a pause rather than a button. The
   steps are ordinary pager links carrying a drawn chevron - the display face
   sets ‹ › on the baseline, so a typed glyph sat low in the box - and the
   chevron is a block so it centres on the grid like a number does. The
   ellipsis is typed and lifted a little for the same reason. */
.pagination-gap { min-width: 44px; min-height: 44px; display: grid; place-items: center; font-family: var(--display); font-size: 18px; color: var(--paper); opacity: .7; letter-spacing: 2px; }
.pagination-gap { transform: translateY(-4px); }
.pagination a.pagination-step span { display: grid; place-items: center; }
.pagination-step-icon { display: block; width: 12px; height: 18px; }
.pagination [aria-current="page"] { background: var(--splash); box-shadow: inset 0 0 0 2px var(--ink), 3px 4px 0 rgba(0, 0, 0, .72); text-shadow: var(--ink-ring); }
.pagination a:hover,
.pagination a:focus-visible { transform: translate(-2px, -2px) skewX(-9deg); box-shadow: inset 0 0 0 2px transparent, 5px 6px 0 rgba(0, 0, 0, .72); }
.pagination a:not([aria-current="page"]):hover,
.pagination a:not([aria-current="page"]):focus-visible { color: var(--ink); background: var(--paper); box-shadow: inset 0 0 0 2px var(--ink), 5px 6px 0 rgba(0, 0, 0, .72); }
.pagination [aria-current="page"]:hover,
.pagination [aria-current="page"]:focus-visible { background: var(--splash-hot); box-shadow: inset 0 0 0 2px var(--ink), 5px 6px 0 rgba(0, 0, 0, .72); }
.pagination a:active { transform: translate(2px, 3px) skewX(-9deg); box-shadow: inset 0 0 0 2px transparent, 1px 2px 0 rgba(0, 0, 0, .72); }
.pagination a:not([aria-current="page"]):active { box-shadow: inset 0 0 0 2px var(--ink), 1px 2px 0 rgba(0, 0, 0, .72); }
.pagination [aria-current="page"]:active { box-shadow: inset 0 0 0 2px var(--ink), 1px 2px 0 rgba(0, 0, 0, .72); }

/* ── Post pages ───────────────────────────────────────────────────────────
   Everything below dresses /post/{key} (image galleries and video) and the
   artist page in the landing page's vocabulary: hard corners,
   2px edges, offset shadows and the display face. It used to be the one corner
   of the site still speaking the pre-redesign language — 28px radii, 1px
   borders, translucent glass and a soft blurred --shadow — so a reader who
   clicked a card on the homepage arrived somewhere that looked like a
   different site. --shadow survives in the token list for the surfaces that
   still want a lift; none of them are here. */
.creator-hero { display: grid; grid-template-columns: 340px minmax(0,1fr); gap: clamp(32px, 6vw, 80px); align-items: start; }
/* The gallery tile's frame at hero scale: paper edge and a hard offset, the
   same two moves the panel buttons make. Paper rather than
   --line-strong because the artwork behind it is arbitrary — an orange-tinted
   edge disappears into warm art, and white never does. */
.creator-image, .media-art { width: 100%; aspect-ratio: 1; object-fit: cover; border: 2px solid var(--paper); border-radius: 12px; box-shadow: 8px 9px 0 rgba(0, 0, 0, .72); background: var(--surface); }
/* 4:5, the same crop the gallery grid uses, so a cover reads as the enlarged
   version of the card the reader just clicked. */
.image-post-cover { aspect-ratio: 4 / 5; }
/* The way back out, at the top where a reader looks for it rather than only at
   the foot of a long gallery. .section-link is the site's existing outbound
   type — no panel, no edge, orange, and the hero's arrow — and this only turns
   the chevron round. Both post pages carry one, and the unlocked gallery repeats
   it under the set. */
.post-back .section-link-icon { transform: rotate(180deg); }
.post-back:hover .section-link-icon { transform: rotate(180deg) translateX(3px); }
/* The only h1 on the site that used to inherit the browser default. The hero's
   rule at page-heading scale: display face, positive tracking, uppercase for
   presentation only, one hard offset in splash under paper. The offset is 4px
   against the hero's 5px because the type is a third smaller — the shadow has
   to stay proportional to the letter, the same call .card h3 makes at 3px. */
.post-title {
    margin: 0 0 18px;
    color: var(--paper);
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(2.1rem, 4.4vw, 3.4rem);
    line-height: .96;
    letter-spacing: .012em;
    text-transform: uppercase;
    text-wrap: balance;
    text-shadow: 4px 4px 0 var(--splash);
}
/* The unlocked page's header: the gate's column without the gate. No .panel -
   an edge and an offset around the title of a page whose subject is the artwork
   below it frames the wrong thing, and the locked half already made that call.
   One solid hairline over the count is the whole separation. */
.post-head { display: grid; justify-items: start; margin-bottom: clamp(28px, 4vw, 44px); }
/* Shares the column's left edge with the title, the way the gate panel's does:
   every line of the header starts at the same x. */
.post-head .post-back { margin-bottom: 18px; }
/* Title and count on one baseline row, closed by a single hairline - the
   section-heading pattern from the homepage rather than a stack. The count is a
   fact about the set, not a step in reading it, so it sits at the far end of the
   line instead of interrupting the reading column. align-items: end puts the
   figure on the title's baseline however many lines the title runs to. */
.post-head-row { width: 100%; display: flex; align-items: end; justify-content: space-between; gap: clamp(24px, 5vw, 64px); padding-bottom: 24px; border-bottom: 1px solid var(--line); }
.post-head-copy { min-width: 0; }
.post-lede { max-width: 62ch; margin: 0; color: var(--text); font-size: clamp(1.02rem, 1.6vw, 1.16rem); line-height: 1.6; }
/* No rule of its own now - the row carries it - and it must not shrink: the
   label wraps to two short lines by design, and a flex item allowed to shrink
   would wrap it to four. */
.post-count { flex: none; }
/* Two lines, "images in / this set", beside the figure: the pair reads as one
   block with it, where a single long line turns the count into a sentence
   trailing off the end of the header row. The break is a pair of block spans
   rather than a max-width in ch - ch measures the zero glyph, and this label is
   uppercase and tracked out, so the character measure and the rendered width
   disagree and the line broke a word late. */
.post-head-row .post-count-label > span { display: block; }

/* Natural ratios, no crop. The 4:5 cell the browse grid uses is right for a
   card - forty of them have to share a rhythm - and wrong here: this page is the
   artwork itself, and cropping a member's piece to make a tidy grid is the one
   thing a paid set must not do. The pieces keep their own shapes and the layout
   packs around them.
   Not CSS columns. The objection .gallery-grid records above binds harder here:
   items flow down each column, so an authored sequence reads scrambled, and the
   lightbox counter - which numbers by DOM order - would disagree with the screen
   on every set. */
.post-gallery { display: grid; gap: 16px; margin-top: clamp(32px, 5vw, 56px); }
/* The state the page ships in, and the state it stays in if the script never
   arrives: an ordinary auto-fill grid, every piece at its own height, rows
   ending ragged. Nothing cropped and nothing hidden - a masonry that needs JS to
   be legible is not a fallback, it is a dependency.
   align-items: start is load-bearing. A stretched grid item fills its row span
   and stops taking its height from aspect-ratio, which makes the measurement
   below circular. */
/* One floor, fixed, chosen at 420 so a 1320px shell packs three. It used to be
   rewritten by site.js once the set's median shape was known - 520 for landscape
   work, 300 for portrait - and that was a whole-grid reflow landing after first
   paint: three columns at 429x537 became two at 652x367 exactly as the pieces
   were arriving, which reads as the page flinching. A column width that is
   merely well-chosen is worth less than one that never moves. */
.post-gallery--masonry { grid-template-columns: repeat(auto-fill, minmax(var(--piece-floor, 420px), 1fr)); align-items: start; }
/* site.js has measured, so the rows become an 8px ruler and each piece spans as
   many of them as its own height needs. 8px is fine enough that the rounding
   error never exceeds half a row and coarse enough that the spans stay small.
   The class is added by the script rather than printed by PHP: the ruler is only
   correct once something is assigning spans. */
.post-gallery--masonry.is-masonry { grid-auto-rows: 8px; }
.post-gallery--masonry.is-masonry .post-piece { grid-row: span var(--piece-span, 40); }
/* A button, so the full piece is reachable from the keyboard and announces
   itself as opening something. Reset to the tile: a button brings its own font,
   padding and background, none of which survive here.
   The ratio is the reservation. The server writes the verified preview ratio
   before the request begins; the library's commonest 4:5 shape remains the
   narrow migration-window fallback. `contain` never distorts the art. */
.post-piece {
    display: block;
    width: 100%;
    margin: 0;
    padding: 0;
    border: 2px solid var(--line);
    border-radius: 12px;
    background: var(--surface);
    aspect-ratio: var(--piece-ratio, 4 / 5);
    overflow: hidden;
    cursor: pointer;
    /* One declaration for the whole list: a second transition shorthand would
       replace this one rather than extend it, and a piece transitions its
       entrance, its tilt, and its border and shadow. Each protected preview
       enters independently; --piece-tilt-transition is the half the tilt owns
       inside the fine-pointer block below. */
    transition:
        opacity .18s ease-out,
        translate .18s ease-out,
        var(--piece-tilt-transition, transform 0s),
        border-color .25s ease,
        box-shadow .25s ease;
}
/* The entrance rests here, in the stylesheet, so it is painted before site.js
   runs and the browser has a state to animate from - set from script it was
   applied and cleared inside one frame on a cached load and nothing moved.
   Gated on html.js, the class the inline script in lolotron6_head() sets before
   first paint, so a reader without scripting is never handed a hidden set.
   translate rather than transform: a piece spends transform on the tilt, and a
   travel written there would land as `transform: none` and take the tilt with
   it. translate is applied before transform, so the two compose. */
html.js .post-piece { opacity: 0; translate: 0 28px; }
html.js .post-piece.is-shown { opacity: 1; translate: none; }
.post-piece img { display: block; width: 100%; height: 100%; object-fit: contain; }
/* The one shape that cannot be reserved honestly. contain is right for every
   other piece - it never distorts the art and the frame is the art's own shape -
   but a strip held at its own ratio is a tile four screens tall, and a strip
   letterboxed into a 4:5 frame is a hairline of picture between two fields of
   nothing. Cropped from the top instead: the first panel is what a reader
   recognises the piece by, and the rest is one press away. */
.post-piece--tall img { object-fit: cover; object-position: top center; }
.post-piece:hover, .post-piece:focus-visible { border-color: var(--paper); box-shadow: 5px 6px 0 rgba(0, 0, 0, .72); }
/* The release card's tilt-to-cursor, on the pieces. Same angles, same easings
   and the same JS that drives the cards - a piece is the same object as the card
   the reader clicked to get here, so it should answer a pointer the same way.
   The card's version is written against its own reveal transition list; a piece
   carries no data-reveal, so it can state its transitions plainly.
   perspective() is inside the transform rather than on the grid: a perspective
   on the container would share one vanishing point across the row, so pieces at
   the edges would lean away from a centre they do not have. */
@media (hover: hover) and (pointer: fine) {
    .post-piece {
        --tilt-x: 0deg;
        --tilt-y: 0deg;
        --tilt-scale: 1;
        transform: perspective(900px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y)) scale(var(--tilt-scale));
        /* Handed to the piece's single transition list rather than declared as a
           shorthand here: a piece transitions the reveal, the tilt, and its
           border and shadow, and a second shorthand would replace the list
           instead of extending it. This variable is the half the tilt owns. */
        --piece-tilt-transition: transform .5s cubic-bezier(.22, .68, .3, 1);
    }
    /* The longer ease is the settle on the way out; tracking gets a quarter
       second of ease-out so the piece trails the pointer slightly, which is what
       reads as weight. will-change is scoped to the piece being touched - on
       every piece at once it would hand the compositor a layer per piece for a
       gallery that mostly sits still. */
    .post-piece.is-tilting { --piece-tilt-transition: transform .26s cubic-bezier(.19, .66, .35, 1); will-change: transform; }
    .post-piece:hover { --tilt-scale: 1.03; }
}
.prose { color: var(--muted); font-size: 1.03rem; line-height: 1.8; }
.prose h2 { margin-top: 2em; color: white; letter-spacing: -.03em; }
.prose h3 { margin-top: 1.4em; color: white; letter-spacing: -.02em; font-size: 1.08rem; }
.prose ul { margin: .8em 0 1.2em; padding-left: 1.4em; list-style: disc; }
.prose li { margin: .35em 0; }
.prose li strong { color: var(--text); }
.prose a { color: #ff9c61; text-decoration: underline; text-underline-offset: 3px; }
/* Opaque ground and a hard offset, matching every other surface on the site.
   The translucent wash it replaces was the last glass panel outside the modal,
   and over the page's dark ground it bought nothing a solid --surface does not:
   what separated it from the page was the blurred shadow, which is the one
   depth idiom the redesign dropped. Radius steps to the cards' 12-14 band.
   Also worn by /privacy, which takes the same correction. */
.panel { padding: clamp(22px,4vw,38px); border: 2px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: 6px 7px 0 rgba(0, 0, 0, .72); }
/* Squared off to the page's corner vocabulary, and opaque: the 7%-alpha wash
   read as a smudge over --surface rather than as a marked-out block. */
.notice { margin: 18px 0; padding: 14px 16px; border-left: 4px solid var(--amber); color: #e8cec2; background: rgba(58, 22, 16, .9); border-radius: 4px 12px 12px 4px; }

/* ── The locked gate ──────────────────────────────────────────────────────
   Shown in place of the hero and the gallery when a reader does not hold the
   tier a set requires. Two columns: the artwork and the frames for the rest of
   the set on the left, the offer on the right.

   Deliberately unboxed. The gate used to sit inside a .panel, which put an edge
   and an offset shadow around the one thing the page exists for and made the
   pitch read as a card dropped onto the page rather than as the page itself.
   The hairlines below do the separating instead, and they are solid --line
   rather than dashes: the cover already carries a dim, the placeholder
   frames carry an edge, and a third dotted texture on the dividers turned the
   column into three competing patterns. */
.section--gate { padding: 44px 0 56px; }
.image-post-gate { display: grid; grid-template-columns: minmax(300px, 460px) minmax(0, 1fr); gap: clamp(28px, 5vw, 60px); align-items: start; max-width: 1180px; margin: 0 auto; }
/* Artwork and placeholders are one column, so the frames read as the rest of
   the piece above them rather than as a separate band under the page.
   Height first, ratio second: the column is measured off the viewport so the
   whole offer lands above the fold, and the cover crops to whatever is left
   after the placeholder row. A 4:5 cover at this width ran ~575px on its own and
   put the button under the fold on a laptop. Cropping costs nothing here - the
   artwork is a blurred derivative, so there is no detail to lose. */
.gate-media { display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: 16px; height: min(calc(100vh - 230px), 720px); min-height: 380px; }
.gate-cover { position: relative; display: grid; min-height: 0; overflow: hidden; border-radius: 12px; }
.gate-cover .media-art { height: 100%; min-height: 0; max-height: 100%; aspect-ratio: auto; }
/* Dim over the blur, light enough that the artwork's colour and masses still
   read: the tease only works if there is something to almost see. The blur
   itself is baked into the derivative the page is served, so this is the only
   half of it the stylesheet controls. */
.gate-cover-veil { position: absolute; inset: 0; border-radius: 12px; background-color: rgba(7, 4, 4, .18); pointer-events: none; }
/* The badge the card in the grid was wearing when it was clicked, in the same
   corner, so the gate reads as the continuation of that click. .tier supplies
   the skewed panel; only the position changes. */
.gate-cover-badge { top: 12px; left: 12px; right: auto; }
.gate-panel { display: grid; justify-items: start; }
/* Shares the column's left edge with the title rather than sitting out beside
   the artwork: on the gate every line of the offer starts at the same x. */
.gate-panel .post-back { margin-bottom: 18px; }
/* The one line selling the set, so it is set at the hero subhead's weight
   rather than at the muted body size the rest of the prose on this page uses. */
.gate-lede { max-width: 46ch; margin: 0; color: var(--text); font-size: clamp(1.02rem, 1.6vw, 1.16rem); line-height: 1.6; }
/* One hairline between each part of the offer. Full width so the rules run the
   measure of the column rather than the width of whatever they sit above. */
.gate-count,
.gate-panel .membership-perks,
.gate-panel .actions { width: 100%; margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--line); }
/* The count is the only hard fact the gate holds about what is behind it, and
   as a line of body copy it read as a footnote to the description above it.
   Set as the display face at figure scale, it is the first thing read after the
   title. The label is measured rather than run out flat: two short lines beside
   the figure keep the pair reading as one block. */
.gate-count,
.post-count { display: flex; align-items: center; gap: 14px; color: var(--muted); font-weight: 850; font-size: .92rem; letter-spacing: .04em; text-transform: uppercase; }
.gate-count-label,
.post-count-label { max-width: 24ch; line-height: 1.35; }
.gate-count-number,
.post-count-number { color: var(--splash); font-family: var(--display); font-weight: 400; font-size: clamp(2.6rem, 6vw, 3.6rem); line-height: .8; letter-spacing: .02em; text-shadow: 3px 3px 0 var(--ink); }
/* Capped rather than run to the column's full width. The panel button is a
   skewed parallelogram, and past about this measure the slant stops reading as
   a comic panel and starts reading as a rendering fault. */
.gate-panel .actions .button { flex: 1 1 100%; max-width: 420px; }
/* The way in for someone who already pays: type, not a second panel button.
   A pair of equal-weight buttons split a decision that has one side worth
   drawing, and an existing member is the rarer case on a gate. */
.gate-note { margin: 18px 0 0; color: var(--muted); font-size: .92rem; line-height: 1.55; }
.gate-note a { color: var(--splash-hot); text-decoration: underline; text-underline-offset: 3px; }
/* The set's shape, drawn rather than described: one frame per piece, carrying no
   artwork to leak - the locked model returns no image URLs at all, which is the
   point of it. Same 4:5 crop as a real gallery cell, because a square frame
   would misdescribe the set it stands for. One row under the cover; the rest
   arrives as a count on the last frame. */
.locked-gallery { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.locked-tile {
    display: grid;
    place-items: center;
    /* A band, not a crop: the frames stand for pieces rather than showing them,
       so the height they are worth is whatever keeps the cover large and the
       button above the fold. */
    height: clamp(92px, 13vh, 140px);
    border: 2px solid var(--line);
    border-radius: 12px;
    background: var(--surface);
}
.locked-tile-mark { display: grid; place-items: center; width: 46px; height: 46px; color: var(--splash); opacity: .5; }
.locked-tile-mark svg { width: 30px; height: 30px; }
.locked-tile-more { color: var(--splash); font-family: var(--display); font-weight: 400; font-size: clamp(1.2rem, 2.4vw, 1.6rem); letter-spacing: .05em; text-transform: uppercase; }

/* Fullscreen viewer for a protected gallery. Built on .modal-backdrop rather
   than a second overlay system: the dim, the brand tile, the blur, the exit
   transition and the reduced-motion handling are already written there, and
   the age gate is the only other thing on the site that covers the page.
   HRA's image viewer first presents a neutral framed surface, then reveals the
   requested image only after it is ready. The same surface here prevents a
   compact preview from being enlarged and visibly replaced. */
/* Clipped to itself. A thrown plate travels a viewport and a half sideways,
   and while it is out there it is content overflowing this box - visible, it
   becomes a horizontal scrollbar on the way past. Nothing in this dialog is
   meant to be seen outside it: the arrows sit inside the padding box and the
   entrance ghost is on <body>, not in here. */
.lightbox { z-index: 200; overflow: hidden; }
/* Wide enough not to clip the plate site.js sizes inside it. The arrows hang
   outside this box on purpose and nothing here is allowed to hide them. */
.lightbox-frame { position: relative; display: grid; place-items: center; width: min(1520px, 100%); transition: transform .32s cubic-bezier(.32, 0, .67, 0), opacity .32s ease-in; }
/* The modal's exit, one step gentler: the panel collapses to .8 because it
   is small, and artwork at full viewport height reads as a slam at that
   distance. site.js watches for this transform to know the close finished. */
.lightbox.is-closing .lightbox-frame { transform: scale(.94); opacity: 0; }
.lightbox-frame:focus { outline: none; }
.lightbox-media {
    position: relative;
    display: grid;
    place-items: center;
    /* The state before site.js has measured anything, and the one a reader
       without scripting would see. It only has to be a sane plate; the moment
       the viewer opens, sizeLightbox replaces both of these with the artwork's
       own shape at the size this screen can actually give it. */
    width: min(1200px, 100%);
    height: min(900px, calc(100vh - 108px));
    overflow: hidden;
    border: 3px solid var(--paper);
    border-radius: 12px;
    background: var(--ink);
    box-shadow: 10px 12px 0 rgba(0, 0, 0, .72);
    /* No overscroll-behavior here. This box is not the scroller - the dialog
       is - and containment on a box that has nothing to scroll swallows the
       wheel instead of passing it up, so a tall piece would only scroll while
       the cursor was off the artwork. Containment belongs on .lightbox.is-tall,
       which is what a flick could otherwise hand to the page behind. */
    transition: width .22s ease, height .22s ease;
    /* Horizontal drags belong to site.js's swipe, not to the browser's own
       pan or to the back-navigation gesture at a viewport edge. Vertical is
       left alone so a stuck gesture can still be flicked out of. */
    touch-action: pan-y;
    /* The same gesture is on a mouse, so the surface says so and neither the
       cursor's own drag-select nor the image's native drag competes with it. */
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
}
/* Only the way home is animated. A drag that turned the page is centred again
   on the same frame the next piece arrives; an abandoned one has to be seen
   travelling back, or a reader cannot tell a gesture that missed from one that
   was never noticed. */
.lightbox-stage.is-settling {
    transition:
        translate .26s cubic-bezier(.22, .61, .36, 1),
        scale .26s cubic-bezier(.22, .61, .36, 1),
        opacity .26s ease;
}
/* The turn, in two legs of the same travel: the thrown panel carries on out of
   frame, and the next piece enters from the far side. Eased out of the throw
   and into the arrival - a symmetric curve would decelerate the exit, which
   reads as the picture changing its mind on the way off.
   The box's size is on the list too. The next piece is usually a warmed
   neighbour, so it decodes and takes the frame while the panel is still
   leaving; an untransitioned resize at that moment is a jolt on something the
   reader can still see, where the same change carried on this curve reads as
   the plate reshaping on its way out. */
.lightbox-stage.is-turning {
    transition:
        translate .24s cubic-bezier(.32, 0, .3, 1),
        scale .24s ease,
        opacity .24s ease;
}
/* The box's own reshaping rides the same leg. The next piece is usually a
   warmed neighbour, so it decodes and takes the frame while the plate is still
   leaving; an untransitioned resize at that moment is a jolt on something the
   reader can still see. */
.lightbox-stage.is-turning .lightbox-media {
    transition:
        width .24s cubic-bezier(.32, 0, .3, 1),
        height .24s cubic-bezier(.32, 0, .3, 1);
}
.lightbox-media:active { cursor: grabbing; }
.lightbox-loading {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: grid;
    place-content: center;
    justify-items: center;
    gap: 16px;
    padding: 24px;
    color: rgba(255, 248, 241, .3);
    background-color: var(--ink);
    background-image: radial-gradient(rgba(255, 248, 241, .07) 1.2px, transparent 1.4px);
    background-size: 14px 14px;
    opacity: 1;
    visibility: visible;
    transition: opacity .3s ease, visibility 0s linear 0s;
}
.lightbox-loading svg { width: 74px; height: 74px; animation: lightbox-loading-pulse 1.15s ease-in-out infinite alternate; }
.lightbox-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
.lightbox-media.is-ready .lightbox-loading { opacity: 0; visibility: hidden; transition: opacity .3s ease, visibility 0s linear .3s; }
.lightbox-media.is-error .lightbox-loading svg { animation: none; opacity: .55; }
.lightbox-media.is-error .lightbox-status { position: static; width: auto; height: auto; overflow: visible; clip: auto; clip-path: none; color: var(--paper); font-weight: 850; font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; white-space: normal; }
@keyframes lightbox-loading-pulse {
    from { opacity: .32; transform: scale(.94); }
    to { opacity: .72; transform: scale(1); }
}
.lightbox-image {
    width: 100%;
    height: 100%;
    object-fit: contain;
    opacity: 0;
    visibility: hidden;
    transition: opacity .3s ease;
    /* Safari and Firefox still start a native image drag from a mouse press
       even with the pointerdown default prevented, and that drag swallows the
       pointerup the swipe is waiting for. */
    -webkit-user-drag: none;
}
.lightbox-media.is-ready .lightbox-image { opacity: 1; visibility: visible; }
/* A strip of stacked panels, held at the width it is read at and scrolled past
   rather than shrunk to fit a landscape screen. The plate keeps its whole
   height and the dialog is what moves, so the frame, its border and the artwork
   travel together the way a page does.
   place-items goes to start: a centred grid item taller than its container has
   its top pushed above the scroll origin, where no amount of scrolling reaches
   it. The scrollbar takes the site's own two colours - the default light one on
   an ink ground reads as a piece of another site. */
.lightbox.is-tall {
    place-items: start center;
    /* Spelled out rather than left to the shorthand above: naming one axis
       makes the other compute to auto if it is visible, and the turn would put
       a scrollbar under every tall piece. */
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-bottom: 40px;
    scrollbar-width: thin;
    scrollbar-color: var(--splash) rgba(255, 248, 241, .08);
    /* The blur has to go, and this is the only reason it does. A backdrop-filter
       makes its element the containing block for every fixed descendant, so the
       controls below resolved against this box instead of the viewport and rode
       the scroll away with the artwork - fixed in name and absolute in fact.
       The dim underneath it is rgba(4, 1, 1, .88) over the site's halftone, so
       what the blur was separating is very nearly hidden already; the controls
       staying where the reader left them is worth more than the last of it. */
    backdrop-filter: none;
}
/* The controls stop belonging to the plate and start belonging to the screen:
   the plate is now taller than the viewport, so a close button on its corner
   and arrows at its middle would both be somewhere the reader has to go looking
   for. Fixed works here only because .lightbox-stage declares its travel behind
   a class - at rest it is transform-free, so it is not their containing block. */
.lightbox.is-tall :is(.lightbox-nav, .lightbox-close) { position: fixed; }
.lightbox.is-tall .lightbox-close { top: 20px; right: 20px; }
/* 566 is half the widest column plus the gutter the arrows sit in, so on a wide
   screen they stay exactly where they are on a short piece; the max floors them
   at the viewport edge when there is no room for that. */
.lightbox.is-tall .lightbox-prev { left: max(16px, calc(50% - 566px)); }
.lightbox.is-tall .lightbox-next { right: max(16px, calc(50% - 566px)); }
/* The hint is over the artwork, and on a tall piece "over the artwork" is
   wherever the reader happens to be looking. */
.lightbox.is-tall .lightbox-hint { position: fixed; top: 50%; left: 50%; }
/* The count follows the plate down rather than sitting under a strip the reader
   may be four screens above the end of. */
.lightbox.is-tall .lightbox-count { position: fixed; left: 0; right: 0; bottom: 14px; margin: 0; }
/* The stage exists only to give the three controls a box to hang off. It is a
   grid whose single item is centred, so it shrink-wraps the media element and
   its edges are the artwork's edges at every aspect ratio. */
.lightbox-stage {
    position: relative;
    display: grid;
    place-items: center;
}
/* The drag travels here rather than on the picture: the stage is the plate and
   the close button pinned to its corner, and moving one without the other
   leaves that button hanging over the backdrop. translate and scale rather than
   one transform, for the reason a piece travels on translate: they compose,
   where a second transform shorthand would replace the first. Untransitioned by
   default so it tracks the pointer exactly; an eased follow lags the hand and
   reads as weight the artwork has not got.
   Behind a class rather than declared always: a translate of 0px is still a
   translate, and any value but none makes this a containing block for the fixed
   controls a tall piece needs. */
.lightbox-stage.is-travelling {
    translate: var(--lightbox-drag, 0px) 0;
    scale: var(--lightbox-drag-scale, 1);
    opacity: var(--lightbox-drag-fade, 1);
}
/* Round on purpose: prev, next and close are shapes a
   reader recognises before reading anything, and skewing them costs that. */
.lightbox-button { width: 52px; height: 52px; display: grid; place-items: center; border: 3px solid var(--paper); border-radius: 50%; color: var(--paper); background: var(--splash); box-shadow: 4px 5px 0 rgba(0, 0, 0, .72); cursor: pointer; transition: transform .2s ease, background .2s ease, box-shadow .2s ease, opacity .18s ease; }
.lightbox-button:hover, .lightbox-button:focus-visible { background: var(--splash-hot); transform: scale(1.08); box-shadow: 6px 7px 0 rgba(0, 0, 0, .72); }
.lightbox-button:active { transform: scale(1); box-shadow: 2px 2px 0 rgba(0, 0, 0, .72); }
/* Beside the picture rather than stacked under it: a reader's hand is already
   at the left and right edges of the artwork, and a centred row put both
   directions in one place away from either. The nav keeps its own transform
   chain because the centring translate has to survive hover and press. */
.lightbox-nav { position: absolute; z-index: 2; top: 50%; transform: translateY(-50%); }
.lightbox-nav:hover, .lightbox-nav:focus-visible { transform: translateY(-50%) scale(1.08); }
.lightbox-nav:active { transform: translateY(-50%) scale(1); }
.lightbox-prev { left: -76px; }
.lightbox-next { right: -76px; }
/* Stood down while the reader is moving the plate by hand, the count with them:
   it is a fact about the plate, and a place marker left hanging under a picture
   that is halfway off the screen belongs to neither piece. It also spares the
   reader watching the number change - show() rewrites it mid-turn, while it is
   invisible, so it simply comes back saying where the set has got to.
   The close button is not in this group. It is pinned to the plate's own corner,
   so it rides the stage's travel and stays where it has always been relative to
   the artwork - the one control that has somewhere to go.
   Three classes deep on purpose: .lightbox-button[disabled] is two, and an arrow
   that had reached the end of the set would otherwise keep its .35 through the
   gesture and reappear as the only thing on screen that had not gone. Inert with
   it, so a press cannot land on a control that is not there. */
.lightbox.is-handling :is(.lightbox-nav, .lightbox-count) { opacity: 0; pointer-events: none; }
/* On a tall piece the close button joins them. It is pinned to the viewport
   there rather than to the plate's corner, and the two things that hide the
   others - a travelling stage and a scaling frame - are both containing blocks
   it would be re-measured against the moment either one is declared. */
.lightbox.is-tall.is-handling .lightbox-close { opacity: 0; pointer-events: none; }
/* And they go instantly, not over .18s. The whole point of standing them down
   is that they are invisible before the containing block moves under them, and
   a fade leaves them on screen for a fifth of a second of exactly the jump it
   was added to hide. */
.lightbox.is-tall.is-handling :is(.lightbox-nav, .lightbox-close, .lightbox-count) { transition: none; }
/* The three states that put a containing block back around a tall piece's
   controls, each declared away.
   blur(0px) is not none: a backdrop-filter of any value makes this element the
   containing block for everything fixed inside it, so the entrance and the exit
   were both re-anchoring the controls to a box that scrolls. The blur is
   already gone on a tall piece; these two only have to agree with it. */
.lightbox.is-tall.is-opening,
.lightbox.is-tall.is-closing { backdrop-filter: none; }
/* The exit's scale, likewise. It is 6% of a frame that is leaving anyway, and
   the cost of keeping it is that every fixed control inside jumps by the length
   of the scroll for the length of the transition. fadeOut waits on a transform
   that now never changes, so its own timer closes the dialog instead - the case
   that timer exists for. The opacity leg still runs, so it still fades out. */
.lightbox.is-tall.is-closing .lightbox-frame { transform: none; }
.lightbox-button[disabled] { opacity: .35; pointer-events: none; }
.lightbox-button svg { width: 18px; height: 18px; }
/* Its own line under the artwork: the count is read, not pressed, and beside
   the arrows it read as a third button. */
.lightbox-count { margin: 14px 0 0; color: var(--splash-hot); font-weight: 850; font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; text-align: center; text-shadow: 2px 2px 0 var(--ink); }
/* Pinned to the artwork's corner, clear of its own top edge. */
.lightbox-close { position: absolute; z-index: 2; top: -18px; right: -18px; }
/* The first-run affordance, over the artwork and inside its rounded frame. It
   is the only thing on the page that says the picture answers a drag, and it
   is worth exactly one showing - after that it is clutter over the work. Inert
   at every moment: it sits directly on top of the surface whose swipe it is
   advertising, and a hint that swallowed that gesture would be self-defeating.
   Out of the accessibility tree too (aria-hidden in the markup): a keyboard
   reader is already on the arrow keys and a screen reader has the labelled
   buttons. */
.lightbox-hint {
    position: absolute;
    z-index: 4;
    /* Centred by hand rather than by the media grid: an absolutely positioned
       grid child is centred by alignment in some engines and cornered in
       others, and this one carries a skew that has to compose with whatever
       does the centring. */
    top: 50%;
    left: 50%;
    display: grid;
    place-items: center;
    padding: 14px 20px;
    /* A caption plate, in the vocabulary the rest of the page is drawn in: the
       viewer's own paper edge and hard offset shadow, the loading surface's
       halftone, and the panel skew the header CTA wears. The art inside takes
       the skew back off, the same way that CTA's label stays upright. */
    border: 3px solid var(--paper);
    border-radius: 14px;
    color: var(--paper);
    background-color: var(--ink);
    background-image: radial-gradient(rgba(255, 248, 241, .09) 1.2px, transparent 1.4px);
    background-size: 14px 14px;
    box-shadow: 5px 6px 0 rgba(0, 0, 0, .72);
    transform: translate(-50%, -50%) skewX(-9deg);
    pointer-events: none;
    transition: opacity .3s ease, visibility 0s linear 0s;
}
/* display: grid above would otherwise beat the attribute, and the hint would
   paint on every set for readers who have already dismissed it. */
.lightbox-hint[hidden] { display: none; }
/* Dismissed, not removed: the first swipe is still travelling under it, and a
   panel that vanished on the same frame reads as a glitch rather than an
   answer. visibility trails the fade so it cannot be a target in between. */
.lightbox-hint.is-dismissed { opacity: 0; visibility: hidden; transition: opacity .3s ease, visibility 0s linear .3s; }
.lightbox-hint-art { display: block; width: 92px; height: 48px; transform: skewX(9deg); }
/* The hand travels and the speed lines answer it: each set is drawn only while
   the hand is moving away from it, which is what makes the pair read as one
   gesture rather than as two decorations either side of a glyph. */
.lightbox-hint-hand { animation: lightbox-hint-sweep 2.2s ease-in-out infinite; }
.lightbox-hint-trail { opacity: 0; }
.lightbox-hint-trail--left { animation: lightbox-hint-trail-out 2.2s ease-in-out infinite; }
.lightbox-hint-trail--right { animation: lightbox-hint-trail-back 2.2s ease-in-out infinite; }
@keyframes lightbox-hint-sweep {
    0%, 100% { transform: translateX(-9px); }
    50% { transform: translateX(9px); }
}
@keyframes lightbox-hint-trail-out {
    0%, 50%, 100% { opacity: 0; }
    22% { opacity: .8; }
}
@keyframes lightbox-hint-trail-back {
    0%, 50%, 100% { opacity: 0; }
    72% { opacity: .8; }
}
/* The travelling copy of the tile. It lives on <body>, not inside the dialog:
   the backdrop is fading in underneath it, and a child of that backdrop would
   fade in with it instead of flying over it. 210 clears the backdrop's 200.
   Inert - the reader is pressing the viewer behind it, not the ghost. */
.lightbox-ghost {
    position: fixed;
    z-index: 210;
    overflow: hidden;
    border-radius: 12px;
    transform-origin: top left;
    pointer-events: none;
    transition: transform .34s cubic-bezier(.22, .61, .36, 1), opacity .16s ease;
}
.lightbox-ghost img { display: block; width: 100%; height: 100%; object-fit: contain; }
/* Dissolved at the moment it lands and never resting at full size: the ghost is
   a compact preview, and the viewer's own neutral surface is already painted
   underneath it, still waiting on the full file. */
.lightbox-ghost.is-landing { opacity: 0; }
/* The dialog is unhidden before the ghost is built, so the backdrop starts
   clear and fades in through .modal-backdrop's transition while the ghost
   travels. The frame is the half that waits for the landing. */
.lightbox.is-opening { opacity: 0; backdrop-filter: blur(0px); }
.lightbox-stage, .lightbox-count { transition: opacity .16s ease; }
.lightbox.is-flying :is(.lightbox-stage, .lightbox-count) { opacity: 0; }

/* The set's download, under the gallery. It borrows the header CTA's
   construction rather than its class: three bands - paper, ink, then the face -
   built from the element and its two pseudo-elements, skewed as one so they
   stay in register, with the label counter-skewed back to upright. The same
   object in a different room, and the contract test keeps the two apart so
   restyling the header cannot restyle this.
   Green, where every other call to action on the site is orange, because this
   one is the only one that sends a reader away with something rather than
   further in. */
/* Left, on the gallery's own edge. The set is read from that edge - the title,
   the lede and the first column all start there - and an action centred under
   it belongs to the page rather than to the work above it.
   The reveal rides the wrapper, never the button: [data-reveal] travels on
   transform, and the button spends its own transform on the skew that makes
   the three bands shear together. */
.post-actions { display: flex; justify-content: flex-start; margin-top: clamp(28px, 4vw, 40px); }
.post-download {
    position: relative;
    /* Rounded on the skewed panel, and the inner bands step their radius down by
       their own inset or the three stop running parallel around the corners. */
    border-radius: 9px;
    display: inline-flex;
    align-items: center;
    /* A step under the header CTA on every measure - padding, type and the
       shadow's throw. It is built like that button because it is the same kind
       of object, but it is a utility on one set rather than the site's primary
       action, and at the CTA's own size it was competing with it from the far
       corner of the same screen. */
    padding: 11px 22px;
    border: none;
    color: var(--paper);
    --btn-face: var(--verdant);
    font-family: var(--display);
    font-weight: 400;
    font-size: 1.12rem;
    letter-spacing: .055em;
    text-transform: uppercase;
    background: var(--paper);
    box-shadow: 4px 5px 0 rgba(0, 0, 0, .72);
    /* The same ring the header CTA's label wears, from the same token: a light
       label on a coloured face carries it everywhere on this site, and the icon
       beside it is drawn twice in the markup to match. It is written for
       label-sized type, which this still is. */
    text-shadow: var(--ink-ring);
    transform: skewX(-9deg);
    cursor: pointer;
    transition: box-shadow .2s ease, transform .2s ease;
}
/* Middle band: the ink gap that separates the paper edge from the face. */
.post-download::before {
    position: absolute;
    inset: 2px;
    border-radius: 7px;
    content: "";
    background: var(--ink);
}
/* Inner band: the green face. Its inset comes in with the button: held at the
   CTA's 7px on a plate this size the face was a sliver and the black band read
   as the button. */
.post-download::after {
    position: absolute;
    inset: 5px;
    border-radius: 3px;
    content: "";
    background: var(--btn-face);
    transition: background .2s ease;
}
/* Counter-skewed back to a net identity transform so the glyphs and the icon
   rasterise unsheared, and lifted above the two band layers. */
.post-download span {
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    gap: 9px;
    transform: skewX(9deg);
}
/* Sized against the cap height rather than the font size: the label is display
   type and sits taller than its em box, and an icon matched to that box reads
   small beside it. */
.post-download svg { width: 17px; height: 17px; }
.post-download:hover, .post-download:focus-visible { --btn-face: var(--verdant-hot); box-shadow: 6px 7px 0 rgba(0, 0, 0, .72); }
.post-download:active { box-shadow: 2px 3px 0 rgba(0, 0, 0, .72); transform: skewX(-9deg) translate(2px, 2px); }
/* The same plate at a card-foot size. Every measure steps down together -
   padding, type, icon, throw - so it reads as the gallery button seen from
   further away rather than a different control. */
.post-download--version { padding: 7px 15px; font-size: .95rem; box-shadow: 3px 4px 0 rgba(0, 0, 0, .72); }
.post-download--version svg { width: 14px; height: 14px; }
.post-download--version:hover, .post-download--version:focus-visible { box-shadow: 5px 6px 0 rgba(0, 0, 0, .72); }
.post-download--version:active { box-shadow: 1px 2px 0 rgba(0, 0, 0, .72); }

/* The provider connect cards on /membership. Deliberately NOT .tier-card: that
   class is the home page's pricing panel, and the contract test pins the two
   apart so restyling one cannot redesign the other. They wear the same drawn
   vocabulary — 2px line, the shared halftone, a hard offset shadow — because
   they are the page a visitor arrives on straight from the home CTA, but they
   are a different subject and keep their own selectors. */
/* Gap is 22px rather than the grid's usual 18px: each card's shadow overhangs it
   by 7px, and at 18px the two columns' shadows nearly met. */
.membership-cards { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 22px; }
/* A centred column: mark, then name, then the CTA under both. The row this
   replaced set the mark beside the copy, which left the CTA measuring whatever
   width was left over — so the two cards' buttons came out different lengths and
   the pair stopped reading as one choice. Stacked, each card is one object on one
   axis and the two stay symmetrical whatever their labels measure.
   overflow is kept for the halftone ground, which is painted to the border box;
   the shadow is drawn outside it and is unaffected. */
.membership-card {
    position: relative;
    overflow: hidden;
    display: grid;
    justify-items: center;
    gap: 18px;
    padding: 34px clamp(24px, 2.6vw, 32px) 30px;
    border: 2px solid var(--line);
    border-radius: 23px;
    background-color: var(--surface);
    background-image: var(--halftone);
    background-size: 12px 12px;
    box-shadow: 6px 7px 0 rgba(0, 0, 0, .72);
    text-align: center;
    transition: transform .2s ease, border-color .2s ease, background-color .2s ease, box-shadow .2s ease;
}
/* .version-card's hover, verbatim: these are the same object doing the same job
   - a card that stands for a choice the reader is about to make - and a locked
   version card is the last one most readers touch before they land here. The
   resting border is the quiet --line rather than --line-strong so the lift has
   somewhere to go; --splash is left to .is-playing, which is a state rather
   than a pointer. :focus-within is doing the keyboard work: the card is not
   itself the control, the button inside it is, so tabbing to that button is
   what must light the card. */
.membership-card:hover, .membership-card:focus-visible, .membership-card:focus-within {
    border-color: var(--line-strong);
    transform: translate(-3px, -3px);
    box-shadow: 9px 10px 0 rgba(0, 0, 0, .72);
}
.membership-card:active { transform: translate(2px, 3px); box-shadow: 3px 4px 0 rgba(0, 0, 0, .72); }
/* The perk mark's three concentric bands — paper, ink, face — turned round: a
   circle rather than the skewed plate, because both provider marks are round
   logos and a slanted panel cropped their silhouettes. The face is per-card so
   the two are told apart by colour without importing either brand's full
   palette; the glyphs themselves stay paper on both. */
.membership-card-mark {
    --mark-face: var(--splash);
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    width: clamp(74px, 8vw, 92px);
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--paper);
    box-shadow: 4px 5px 0 rgba(0, 0, 0, .72);
}
.membership-card-mark::before { position: absolute; inset: 3px; border-radius: 50%; content: ""; background: var(--ink); }
.membership-card-mark::after { position: absolute; inset: 7px; border-radius: 50%; content: ""; background: var(--mark-face); }
.membership-card-mark svg { position: relative; z-index: 1; width: 46%; height: 46%; }
/* Teal is the one hue on this palette that is neither the ember nor a neutral,
   so the second card is distinguishable at a glance without a second orange
   competing with the CTA sitting right under it. */
.membership-card--subscribestar .membership-card-mark { --mark-face: #12908f; }
/* Full width so the CTA below can be measured against the card rather than
   against its own label. */
.membership-card-body { position: relative; z-index: 1; width: 100%; }
/* The tier card's name treatment: display face wearing the hero headline's
   paper-on-splash offset, so a provider name reads as a title rather than as a
   form label. */
.membership-card-name { margin: 0; font-family: var(--display); font-weight: 400; font-size: clamp(1.6rem, 2.1vw, 2rem); line-height: 1.05; letter-spacing: .05em; text-transform: uppercase; color: var(--paper); text-shadow: 3px 3px 0 var(--splash); }
/* Capped and centred rather than stretched: .actions--panel gives the button
   width: 100% of this box, so without a ceiling the CTA would run the whole card
   and read as a banner at wide viewports and as a stripe at narrow ones. The
   ceiling clears the longer of the two labels at the desktop button size. */
.membership-card .actions { width: min(100%, 470px); margin: 14px auto 0; }
/* OAuth initiation is refused while a temporary membership is active, and until
   now that state left a button that looked live and went to '#'. The panel keeps
   its skew through the hover so it does not square up under a pointer it will
   not respond to. */
.membership-card .button[aria-disabled="true"] { cursor: not-allowed; opacity: .5; }
.membership-card .button[aria-disabled="true"]:hover,
.membership-card .button[aria-disabled="true"]:focus-visible { --btn-face: var(--splash); transform: skewX(-9deg); box-shadow: 6px 7px 0 rgba(0, 0, 0, .72); }

/* The membership pitch on the home page. Deliberately NOT .membership-card:
   that class is the provider connect card on /membership and restyling it here
   would redesign that page by accident. Same reason the badge below is not
   .tier, which is the absolutely-positioned gallery flag.
   auto-fit rather than a fixed three: the catalog is admin-configured, so the
   row has to look deliberate at one tier and at four. The track is capped rather
   than 1fr for exactly that reason — the site currently sells one tier, and a
   single 1fr card would stretch the full 1320px shell and read as a banner. With
   a ceiling and a centred row, one tier is a card and three still fill the shell. */
.tier-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 420px)); justify-content: center; gap: 18px; }
/* A drawn panel, not a soft card: 2px of line, the halftone the empty state and
   the buttons already use, and a hard offset shadow instead of a blur. The dots
   are a custom property holding a radial-gradient, which is inert without the
   background-size that sets the pitch. The mascot tile is deliberately not here:
   at card size one head is artwork behind the copy, not texture under it. */
.tier-card { display: flex; flex-direction: column; padding: 32px; border: 2px solid var(--line); border-radius: 23px; background-color: var(--surface); background-image: var(--halftone); background-size: 12px 12px; box-shadow: 6px 7px 0 rgba(0, 0, 0, .72); transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease; }
/* The same fixed light source as the buttons: the card lifts up-left and the
   shadow grows, rather than the shadow moving. */
.tier-card:hover { transform: translate(-3px, -3px); border-color: var(--line-strong); box-shadow: 9px 10px 0 rgba(0, 0, 0, .72); }
/* The top tier is marked by its edge alone. A ribbon would have to claim
   something — most popular, best value — that the catalog does not record. */
.tier-card--top { border-color: var(--splash); }
/* Sized up, and wearing the hero headline's paper-on-splash offset. The site has
   sold one tier for four years, so the card is not one option among several to be
   compared — it is the offer, and the name and the number are what carry it. The
   offset is 3px rather than the hero's 5px: same device, proportional to type a
   third the size. */
.tier-card-name { font-family: var(--display); font-weight: 400; font-size: clamp(1.9rem, 2.6vw, 2.3rem); line-height: 1.05; letter-spacing: .05em; text-transform: uppercase; color: var(--paper); text-shadow: 3px 3px 0 var(--splash); }
/* Priced in the display face and outlined with the same ink ring the hero
   headline and the panel buttons wear, so the number reads as artwork rather
   than as a table cell. The cadence stays body type: it is not part of the
   figure and should not compete with it. */
.tier-price { display: flex; align-items: baseline; gap: 8px; margin: 18px 0 0; font-family: var(--display); font-weight: 400; font-size: clamp(3.2rem, 5vw, 4.4rem); line-height: 1; color: var(--splash); text-shadow: var(--ink-ring); }
.tier-price small { font-family: inherit; font-size: 1.15rem; color: var(--muted); text-shadow: none; }
/* flex: 1 is what keeps the CTAs on one line across cards with unequal perk
   counts: the list absorbs the difference instead of the button drifting. */
.tier-perks { flex: 1; display: grid; gap: 12px; margin: 24px 0 0; padding: 0; list-style: none; }
.tier-perks li { display: flex; gap: 10px; color: var(--muted); line-height: 1.55; }
/* A skewed chip rather than a check glyph: the display face has no tick, and a
   fallback font would sit off the baseline against the copy. The same -9deg the
   buttons and the filter pills are cut at. */
.tier-tick { flex: none; width: 14px; height: 14px; margin-top: 5px; border-radius: 3px; background: var(--splash); transform: skewX(-9deg); }
.tier-card .actions { margin-top: 26px; }
.actions--panel .button { width: 100%; }

/* The single-offer band. The site has sold one tier for four years, so there is
   nothing to compare and a row of pricing cards was answering a question nobody
   asked. This is a pitch instead: the offer stated once, at full width, with the
   price carried as artwork rather than as a table figure. The card grid above is
   still here and still correct the day a second tier appears. */
.membership-band { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; gap: clamp(28px, 5vw, 64px); }
/* One measure for the whole column. The CTA is width: 100% of this, and
   fit-content is what makes the column — and with it the button — end where the
   widest perk line ends instead of running out to the 34rem cap; the cap still
   wraps a perk that would otherwise run long. */
.membership-pitch { width: fit-content; max-width: 34rem; }
.membership-tier-name { margin: 0; font-family: var(--display); font-weight: 400; font-size: clamp(1.8rem, 3.4vw, 2.6rem); line-height: 1; letter-spacing: .05em; text-transform: uppercase; color: var(--splash); }
.membership-perks { display: grid; gap: 20px; margin: 26px 0 0; padding: 0; list-style: none; }
/* Top-aligned, not centred: each perk is a heading with a line under it, so
   centring would float the mark against the middle of a two-line block. */
.membership-perks li { display: flex; align-items: flex-start; gap: 16px; }
/* The tier card's flat tick will not carry a row this size, so the mark is the
   button recipe in miniature, and that means all three of its concentric bands:
   paper outermost, then the ink edge, then the splash face. Dropping the middle
   band is what made an earlier pass read as a flat orange chip rather than as a
   drawn panel - the ink is what the other two are separated by. Paint order does
   the stacking, outer to inner, so no z-index is needed between them; the glyph
   is lifted over both and counter-skewed so it rasterises square. */
.membership-perk-mark { position: relative; flex: none; display: grid; place-items: center; width: 46px; height: 46px; margin-top: 1px; border-radius: 11px; background: var(--paper); box-shadow: 3px 4px 0 rgba(0, 0, 0, .72); transform: skewX(-9deg); }
.membership-perk-mark::before { position: absolute; inset: 2px; border-radius: 9px; content: ""; background: var(--ink); }
.membership-perk-mark::after { position: absolute; inset: 5px; border-radius: 6px; content: ""; background: var(--splash); }
.membership-perk-mark svg { position: relative; z-index: 1; width: 27px; height: 27px; transform: skewX(9deg); }
.membership-perk-copy { display: grid; gap: 3px; }
.membership-perk-title { font-weight: 850; font-size: 1.12rem; line-height: 1.3; color: var(--paper); }
.membership-perk-note { font-size: .98rem; line-height: 1.5; color: var(--muted); }
.membership-band .actions { margin-top: 34px; }
/* The slot the price sits in, opposite the pitch. */
.membership-flash { display: grid; place-items: center; min-height: clamp(280px, 30vw, 420px); }
/* The card IS the splash. A ray burst behind a rounded plate was two objects
   competing - a backdrop and a sign - and the backdrop kept winning; the shape
   the price sits on is the burst now, so there is one.
   The shape itself is inline SVG in the template, not a clip-path here. A polygon
   is straight edges and evenly spaced spikes, which is a star; paint is uneven
   lobes, curves between them and droplets thrown clear of the body. */
.membership-flash-price {
    position: relative;
    display: grid;
    justify-items: center;
    /* The splash is sized from this box, so padding is what opens the middle: the
       box grows, the shape grows with it, and the type stays where it is. Equal on
       all four sides, which is what centres the type in the shape - the earlier
       taller-top value was leaning against a blob whose mass sat high, and on a
       shape that fills its own box it just drops the type below centre. */
    padding: clamp(82px, 9.5vw, 116px);
    /* Rotation only. The rectangle skewed -6deg and counter-skewed its children;
       skewing a radial shape drags the lobes into a lean the eye reads as a
       rendering fault rather than as a slant. */
    transform: rotate(-3deg);
    font-family: var(--display);
    color: var(--paper);
    text-shadow: var(--ink-ring);
}
/* Inset in from the text box rather than out past it. The negative value this
   replaces existed to give flung droplets room to clear the box; artwork without
   them just read large against the type.
   overflow: visible because the drop-shadow below is a filter on this element and
   Safari has clipped filtered SVG to the viewBox without it. */
.membership-flash-splash {
    position: absolute;
    inset: 2%;
    z-index: -1;
    overflow: visible;
    /* Nudged up and right against this trace, which is drawn a little low and left
       of its own viewBox centre. Per-shape by nature: it is a property of the
       artwork, not of the layout, so it comes back to zero on a shape drawn true. */
    transform: translate(4px, -4px);
    filter: drop-shadow(8px 9px 0 rgba(0, 0, 0, .72));
    pointer-events: none;
}
/* paint-order: stroke, so the rim sits wholly outside the fill. A centred stroke
   puts half its width inside the shape, which on a blob this size eats the lobes
   back and thins the tips.
   Styled here rather than as fill=/stroke= attributes on the markup because a
   presentation attribute does not resolve var(), and these are the palette's own
   two colours rather than a second copy of them. */
/* The rim, in the perk mark's order: white outside, black between the white and
   the orange. Each layer is the same shape under a wider stroke, and with
   paint-order: stroke every layer shows only its outer half - the inner half is
   covered by whatever is painted over it. So the visible bands are half the
   difference between successive widths: 14 and 7 give 3.5px of white and 3.5px of
   black, against the perk mark's 2px and 3px on a 46px box.
   Each layer is filled as well as stroked. A stroke alone leaves its own middle
   transparent, and any seam the layer above does not perfectly cover would flash
   the page through the paint. */
.membership-flash-splash-paper {
    fill: var(--paper);
    stroke: var(--paper);
    stroke-width: 14;
    stroke-linejoin: round;
    paint-order: stroke;
}
.membership-flash-splash-ink {
    fill: var(--ink);
    stroke: var(--ink);
    stroke-width: 7;
    stroke-linejoin: round;
    paint-order: stroke;
}
/* No stroke: the two layers under it draw the border, so a stroke here would only
   push the orange back out over the black. */
.membership-flash-splash-body {
    fill: var(--splash);
    stroke: none;
}
/* The figure is the whole message now, so it takes the room the old
   "Only ... !" wrapper's two lines were using and then some.
   Its own ring rather than --ink-ring. That token is 1.5px because it is written
   for label-sized type; carried onto a 10rem figure the same 1.5px reads as a
   hairline and the number goes soft exactly where it should hit hardest. Same
   twelve offsets in the same order, scaled up, so the outline keeps its weight
   relative to the stems. */
.membership-flash-figure {
    font-size: clamp(4.2rem, 10vw, 8rem);
    line-height: .86;
    letter-spacing: -.01em;
    /* The ring comes down with the type. Held at 4px against a figure a quarter
       smaller it stops being an outline and starts being a second colour. */
    text-shadow:
        3.2px 0 0 var(--ink), -3.2px 0 0 var(--ink), 0 3.2px 0 var(--ink), 0 -3.2px 0 var(--ink),
        2.8px 1.6px 0 var(--ink), -2.8px 1.6px 0 var(--ink),
        2.8px -1.6px 0 var(--ink), -2.8px -1.6px 0 var(--ink),
        1.6px 2.8px 0 var(--ink), -1.6px 2.8px 0 var(--ink),
        1.6px -2.8px 0 var(--ink), -1.6px -2.8px 0 var(--ink);
}
/* The gap is margin here rather than the grid's gap because the figure's own
   ring adds ~4px of drawn outline below its baseline: measured from the type the
   two lines looked spaced, and measured by eye they were touching. */
.membership-flash-cadence { margin-top: 13px; font-size: 1.3rem; letter-spacing: .1em; text-transform: uppercase; }

/* The page closes on the same hairline every section is divided by, not on a
   drawn splash edge. That edge was written when the page ran hero into gallery
   and the orange had somewhere to be loud; the membership band now ends on an
   orange CTA, an orange price and orange perk plates, so a fourth orange stroke
   underneath read as one more of them rather than as a full stop. --surface is
   what actually separates the block - the rule only has to agree with it.
   Structure follows the sibling sites: a brand column carrying the mark and the
   one-line pitch, heading-topped link columns beside it, and a divider bar with
   the copyright and the attribution at opposite ends. */
/* The mark as the footer's ground. Nothing here competes with it: the columns
   are muted type on a surface that otherwise carries no texture at all, and the
   page ends on the signature rather than on flat paint. */
.site-footer { margin-top: 60px; padding: 56px 0 26px; border-top: 1px solid var(--line); color: var(--muted); background-color: var(--surface); background-image: var(--footer-tile); background-size: 150px; }
/* 5/3/4, the siblings' proportions: the brand column has to hold a 288px lockup
   plus a line of copy, so it takes the larger share. */
.footer-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 3fr) minmax(0, 4fr); gap: clamp(32px, 5vw, 64px); margin-bottom: 44px; }
/* The header's lockup at the header's size, both here and at the 900px step.
   No extra request — the nav has already fetched this srcset — so the mark reads
   identically at both ends of the page. The nav's translateX is not copied: that
   nudge hangs the logo over the hero, and here it would pull the mark off the
   shell's left edge. */
.footer-brand { display: inline-flex; align-items: center; }
.footer-brand img { width: 288px; height: 96px; flex: none; object-fit: contain; }
.footer-tagline { max-width: 38ch; margin: 14px 0 0; font-size: .98rem; line-height: 1.6; }
.footer-socials { display: flex; gap: 16px; margin-top: 24px; }
.footer-socials a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border: 1px solid var(--line);
    border-radius: 50%;
    color: var(--muted);
    background: var(--surface);
    transition: color .18s ease, border-color .18s ease;
}
.footer-socials a:hover,
.footer-socials a:focus-visible { color: var(--splash-hot); border-color: var(--splash-hot); }
.footer-socials a:focus-visible { outline: 2px solid var(--splash-hot); outline-offset: 3px; }
/* Column headings sit in the display face so they read as signage rather than
   as another link. Paper against the muted list below them does the ranking. */
.footer-heading {
    margin: 0 0 18px;
    color: var(--paper);
    font-family: var(--display);
    font-weight: 400;
    font-size: 1.16rem;
    letter-spacing: .075em;
    text-transform: uppercase;
}
.footer-list { display: grid; gap: 13px; margin: 0; padding: 0; list-style: none; }
/* The button is a real button for the consent API, so it needs the anchor's
   appearance spelled out rather than inherited. */
.footer-list a,
.footer-list button {
    padding: 0;
    border: 0;
    color: var(--muted);
    background: none;
    font: inherit;
    font-size: .98rem;
    text-align: left;
    cursor: pointer;
    transition: color .18s ease;
}
.footer-list a:hover,
.footer-list button:hover { color: var(--splash-hot); }
/* Below the rule and small. The copyright and the attribution are the one pair
   here that should not compete, so both keep the body face. */
.footer-legal { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; padding-top: 20px; border-top: 1px solid var(--line); font-size: .82rem; letter-spacing: .04em; }
.footer-legal p { margin: 0; }
.footer-legal a { transition: color .18s ease; }
.footer-legal a:hover { color: var(--splash-hot); }

/* Halftone over the blur rather than a flat wash: the dot grid is the site's
   own texture, so the page behind reads as a printed screen instead of frosted
   glass. Blur drops to 10px because the dots now carry the separation.
   The dots stay on the backdrop only — carried onto the panels too they read as
   a screen-printing fault rather than a texture behind the page. */
.modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 180;
    display: grid;
    /* safe, not plain, centring: a panel taller than the viewport centres to a
       negative offset, and the overflow above the top edge is unreachable - no
       scrollbar runs in that direction. safe drops the alignment to start once
       the content stops fitting, so the panel begins at the top and the scroll
       below reaches the rest of it. */
    place-items: safe center;
    padding: 20px;
    /* The backdrop is the scroller, not the panel: .membership-unlock-close is
       hung 18px outside the panel's own corner, and an overflow on the panel
       would clip it off. The page behind is already locked by the controller;
       contain stops a flick at the end of this list from chaining into it. */
    overflow-y: auto;
    overscroll-behavior: contain;
    background-color: rgba(4, 1, 1, .88);
    background-image: var(--halftone);
    background-size: 12px 12px;
    backdrop-filter: blur(10px);
    /* Exit only. The gate is in the markup before first paint, so there is
       nothing to animate in; leaving is the moment worth showing.
       Held back 60ms: the dim is what the shrinking panel is seen against, so it
       has to outlast the panel rather than clear with it. */
    transition: opacity .3s ease .06s, backdrop-filter .3s ease .06s;
}
.modal-backdrop[hidden] { display: none; }
/* Dim, dots and blur all live on the backdrop, so one opacity fade takes the
   whole screen treatment with it. pointer-events keeps a second click from
   landing while the panel is on its way out. */
.modal-backdrop.is-closing { opacity: 0; backdrop-filter: blur(0px); pointer-events: none; }
.modal-backdrop.is-closing .modal { transform: scale(.8); opacity: 0; }
/* Paper edge and one hard offset, no ink ring: at panel size the buttons' third
   band read as a heavy outline rather than depth. */
.modal {
    position: relative;
    /* 440, not 520: one headline, two lines of copy and one button never filled
       the wider box, so its bottom right sat empty and the panel read unbalanced. */
    width: min(440px, 100%);
    /* One token so the three gaps below can be measured against it. The bottom
       carries 8px more: the button's offset shadow falls into that space, so a
       symmetric padding reads short at the bottom. */
    --pad: clamp(28px, 5vw, 40px);
    padding: var(--pad) var(--pad) calc(var(--pad) + 8px);
    border: 3px solid var(--paper);
    border-radius: 12px;
    color: var(--text);
    text-align: center;
    /* The page's own ground, not a raised surface: the panel is a cut-out of the
       site rather than a card floating over it. The paper edge and the hard
       offset are what separate it from the backdrop, so it needs no lift. */
    background: var(--ink);
    box-shadow: 10px 12px 0 rgba(0, 0, 0, .72);
    /* Ahead of the backdrop so the panel leads. Both legs run the full duration:
       an opacity leg shorter than the transform emptied the panel before the
       scale had travelled, which read as no animation at all. Ease-in, so it
       accelerates away rather than crawling off. */
    transition: transform .32s cubic-bezier(.32, 0, .67, 0), opacity .32s ease-in;
}
/* The panel takes focus on open so the dialog is entered, but it is a container,
   not a control: the ring would read as a pressed button. The buttons keep
   theirs, so keyboard users still see where they are once they Tab. */
.modal:focus { outline: none; }
/* The hero headline's rule at dialog scale: display face, positive tracking, one
   hard offset. Single tone on purpose — the hero's two-colour split needs the
   artwork behind it to carry the second word's darker offset; on a flat panel
   that half just goes quiet next to the first. */
/* One line, always: the two words break to a ragged stack the moment the box
   gets tight, and clamp() already scales the type down instead. The 2.7rem cap
   is what keeps all 16 characters inside the 360px content box. */
.modal h2 {
    margin: 0 0 16px;
    white-space: nowrap;
    color: var(--paper);
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(2.1rem, 6.4vw, 2.7rem);
    line-height: .94;
    letter-spacing: .012em;
    text-transform: uppercase;
    text-shadow: 4px 4px 0 #000;
}
/* Fills the content box rather than balancing to its own narrower measure. With
   balance on, the copy set ~270px inside a 360px box, so headline, copy and
   button each centred at a different width and the stack read as a pyramid.
   Sharing the button's measure gives the panel one edge to align to. */
.modal p { margin: 0; color: var(--muted); font-size: 1rem; line-height: 1.6; }
/* Gap clears both the skew overhang and each panel's offset shadow. */
.modal .actions { justify-content: center; gap: 22px; margin-top: 28px; }
/* Full width: a lone primary action parked at the left edge read as the survivor
   of a two-button row. */
.modal .actions .button { flex: 1 1 100%; }
/* Dialog scale for the shared panel button. The hero's 74px/1.42rem pair needs
   ~380px and wraps inside a 440px content box; this needs ~340px and does not.
   Source order beats the shared block at equal specificity, so no !important. */
.modal .actions .button {
    min-height: 64px;
    padding: 0 30px;
    font-size: 1.26rem;
    box-shadow: 5px 6px 0 rgba(0, 0, 0, .72);
}
.modal .actions .button:hover { box-shadow: 7px 8px 0 rgba(0, 0, 0, .72); }

/* Same panel, one step down. No backdrop-filter: the surface is opaque, so the
   blur painted nothing — a hard edge is the whole point here. */
.cookie-banner {
    position: fixed;
    z-index: 150;
    /* 22px, not 20: the offset shadow needs clearance from the viewport edge. */
    right: 22px;
    bottom: 22px;
    /* Sized by the button row and nothing else: width: max-content asks the
       widest child, and the paragraph is taken out of that measurement below, so
       the box comes out exactly as wide as the two buttons need to sit side by
       side. The max-width is only a small-screen ceiling. */
    width: max-content;
    max-width: calc(100% - 44px);
    padding: 18px 20px;
    border: 3px solid var(--paper);
    border-radius: 12px;
    color: var(--text);
    background: var(--surface-raised);
    box-shadow: 8px 9px 0 rgba(0, 0, 0, .72);
    /* Collapses into its own corner rather than the middle of itself: the banner
       is pinned bottom right, so that is the point it should read as leaving
       towards. */
    transform-origin: 100% 100%;
    transition: transform .32s cubic-bezier(.32, 0, .67, 0), opacity .32s ease-in;
}
.cookie-banner[hidden] { display: none; }
.cookie-banner.is-closing { transform: scale(.86); opacity: 0; pointer-events: none; }
/* width: 0 with min-width: 100% is the shrink-to-fit escape hatch: the copy
   contributes nothing to the parent's max-content measurement but still fills
   whatever width the buttons settle on, so it wraps to them instead of setting
   the box. */
.cookie-banner p { width: 0; min-width: 100%; margin: 0 0 16px; color: var(--muted); font-size: .96rem; line-height: 1.55; }
.cookie-banner a { color: var(--splash-hot); text-decoration: underline; text-underline-offset: 3px; }
/* The shared .actions rule's 34px top margin is hero spacing; the paragraph
   above already carries the gap here. nowrap because this row is what sizes the
   banner — allowed to wrap, it would measure one button wide and the second
   would drop under it at every viewport. */
.cookie-banner .actions { flex-wrap: nowrap; gap: 12px; margin-top: 0; }
/* Smallest of the three panel scales, still clear of the ~36px floor where the
   bands collapse into each other (see .gallery-pill). Ties the shared recipe's
   (0,3,0) and wins on source order, same as the modal's block. */
.cookie-banner .actions .button {
    min-height: 50px;
    padding: 0 24px;
    font-size: 1.1rem;
    box-shadow: 4px 5px 0 rgba(0, 0, 0, .72);
}
.cookie-banner .actions .button:hover { box-shadow: 6px 7px 0 rgba(0, 0, 0, .72); }

.status-page { min-height: 100vh; display: grid; place-items: center; padding: 24px; text-align: center; }
.status-dot { width: 16px; height: 16px; margin: 0 auto 18px; border-radius: 50%; background: #45dd82; box-shadow: 0 0 24px #45dd82; }

/* ── Scroll motion ────────────────────────────────────────────────────────
   The behaviour BP and HRA get from GSAP + ScrollTrigger, without the 150KB.
   An IntersectionObserver in site.js adds .is-revealed; everything below is
   the resting and arrived state it moves between. Only opacity and transform
   are animated, so a reveal never leaves the compositor and a grid of forty
   cards costs no layout.

   The easing is the curve GSAP ships as power3.out. Matching it by number
   rather than by eye is what keeps this site's motion a sibling of the other
   two rather than a near miss.

   Delay arrives as --reveal-delay from the markup (data-reveal-delay, in ms),
   so the stagger is authored in PHP beside the loop that emits the elements —
   the same division of labour BP uses. */
[data-reveal] {
    opacity: 0;
    transition: opacity .65s cubic-bezier(.16, 1, .3, 1), transform .65s cubic-bezier(.16, 1, .3, 1), translate .65s cubic-bezier(.16, 1, .3, 1);
    transition-delay: var(--reveal-delay, 0ms);
}
/* Every rule below excludes .card, and the card pair at the foot of the block
   picks it back up on `translate` instead. A card spends its `transform` on the
   tilt-to-cursor effect further up this file, so a reveal written in transform
   lands as `transform: none` on arrival and takes the tilt and the 1.03 hover
   scale down with it - the card is left answering the pointer with its border
   alone. `translate` is applied before `transform` rather than replacing it, so
   the two compose and the tilt survives the landing. */
[data-reveal="fade-up"]:not(.card):not(.post-piece) { transform: translateY(28px); }
[data-reveal="fade-down"]:not(.card):not(.post-piece) { transform: translateY(-28px); }
/* The name says where the element comes *from*, matching BP's markup: a
   fade-right enters travelling rightward, so it starts to the left. */
[data-reveal="fade-right"]:not(.card):not(.post-piece) { transform: translateX(-28px); }
[data-reveal="fade-left"]:not(.card):not(.post-piece) { transform: translateX(28px); }
[data-reveal="zoom-in"]:not(.card):not(.post-piece) { transform: translateY(12px) scale(.88); }
[data-reveal].is-revealed { opacity: 1; }
[data-reveal]:not(.card):not(.post-piece).is-revealed { transform: none; }
/* The card half of the same reveal, on `translate` so the tilt keeps transform.
   Only fade-up is emitted on a card today; the reset is written against
   [data-reveal] so a future card carrying another direction still lands. */
.card[data-reveal="fade-up"] { translate: 0 28px; }
.card[data-reveal="fade-down"] { translate: 0 -28px; }
.card[data-reveal="fade-right"] { translate: -28px 0; }
.card[data-reveal="fade-left"] { translate: 28px 0; }
.card[data-reveal].is-revealed { translate: none; }
/* The card's whole transition list, in one declaration because a second
   shorthand would replace this one instead of extending it - which is exactly
   what the reveal rule above was doing to the card's border and shadow, leaving
   the paper edge to snap on hover and again on the way out.
   The reveal delay is written per property rather than as transition-delay, so
   a card staggered 180ms into its row still answers the pointer immediately.
   Outside the fine-pointer block there is no tilt to time and the fallback
   stands in for it. */
.card[data-reveal] {
    transition:
        opacity .65s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms),
        translate .65s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms),
        var(--card-tilt-transition, transform 0s),
        border-color .25s ease,
        box-shadow .25s ease;
}

/* Header entrance. The header is sticky and painted before site.js runs, so it
   is hidden here and released by .is-entered rather than animated from JS —
   a script-side fromTo would flash the header at its resting position first.
   Everything that starts hidden is gated on html.js, which a one-line inline
   script in lolotron6_head() sets before the first paint. A reader with
   scripting off therefore never has anything hidden from them - which is the
   failure mode BP and HRA both ship, where a script that does not load leaves
   the page permanently blank. */
html.js .site-header {
    opacity: 0;
    transform: translateY(-60px);
    transition: opacity .5s cubic-bezier(.16, 1, .3, 1), transform .5s cubic-bezier(.16, 1, .3, 1);
}
html.js .site-header.is-entered { opacity: 1; transform: none; }
/* The nav-link and CTA entrance is desktop-only and lives in the min-width
   block further down: below 900px .nav-links is a closed dropdown and the
   responsive block deliberately nulls the transform on both, so staggering
   them there would fight the layout rather than decorate it.

   The sibling sites also shrink the bar on scroll. That is deliberately not
   ported: at this header size the useful range is a handful of pixels, and
   anything wide enough to read as motion moves the overhanging logo out of
   the row it breaks. The bar simply stays put. */

/* Hero words. site.js wraps each word of the h1 in .hero-word, inside a
   .hero-line whose overflow clips them, so the words rise into view from
   behind the line above instead of fading in place.
   The 3D tip is desktop-only and lives in the media query at the foot of this
   file: on a phone the rotation costs a full-width layer promotion for an
   effect a 6in screen barely shows. */
/* The clip is what makes a word rise from behind the line above rather than
   fade in place. It has to be paid for: both title lines cast a hard 5px
   text-shadow, and a flush overflow:hidden would shear it off. The padding
   buys exactly that 5px back and the matching negative margin keeps the
   headline's metrics unchanged, so the clip costs no layout.
   6px of bottom padding is nowhere near the 110%-of-a-word travel below, so a
   resting word is still fully hidden. */
.hero-line { display: block; overflow: hidden; padding: 0 6px 6px 0; margin: 0 -6px -6px 0; }
.hero-word {
    display: inline-block;
    opacity: 0;
    transform: translateY(110%);
    transition: opacity .85s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms), transform .85s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms);
}
.hero-word.is-revealed { opacity: 1; transform: none; }
/* Everything under the h1 - the copy line and the button row - arrives as one
   staggered group once the headline is under way. */
html.js .hero-anim-element {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity .9s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms), transform .9s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms);
}
html.js .hero-anim-element.is-revealed { opacity: 1; transform: none; }

/* Hero parallax. site.js writes --hero-scroll as 0->1 across the hero's exit
   and nothing else; the mapping to pixels lives here so the JS never touches a
   style property directly and the whole effect can be dropped by a media
   query alone. Desktop-only, applied in the (hover: hover) block below.
   .hero-rays is a sibling of .hero-grid, not a child, so its own infinite spin
   is untouched by this transform - keep it that way. */
.hero-grid { will-change: auto; }

@keyframes marker-draw {
    from { transform: scaleX(0); }
    to { transform: scaleX(1); }
}

/* ---- The video post page -------------------------------------------------
   Written in the gallery page's language: no .panel around the copy, hairlines
   where a frame would have gone, and one priced button rather than a pair. What
   is new here is the stage and its chrome, because a drawn control bar has no
   precedent on this site to borrow from.
   The transport controls are round rather than skewed, for the same reason
   .lightbox-button is: a play triangle and a volume horn are recognised as
   shapes before they are read, and skewing them costs that recognition for a
   consistency nobody is looking for at 40px. */
.media-stage {
    position: relative;
    display: block;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    margin: 0 0 clamp(28px, 4vw, 44px);
    border: 2px solid var(--paper);
    border-radius: 12px;
    background: var(--ink);
    box-shadow: 8px 9px 0 rgba(0, 0, 0, .72);
}
.media-frame { display: block; width: 100%; height: 100%; object-fit: contain; background: var(--ink); }
.media-status-notice {
    position: absolute;
    z-index: 12;
    top: 14px;
    left: 14px;
    max-width: calc(100% - 28px);
    margin: 0;
    padding: 10px 14px;
    border: 1px solid var(--amber);
    border-radius: 8px;
    background: rgba(7, 4, 4, .9);
    color: var(--paper);
    font-size: .875rem;
    line-height: 1.4;
    pointer-events: none;
}
.media-download-error { flex-basis: 100%; color: var(--amber); font-size: .85rem; line-height: 1.4; }
[data-media-reload][hidden] { display: none; }
/* The picture recedes behind the offer rather than competing with it: the same
   move the locked gallery cover makes with its blurred artwork. */
.media-stage.is-blocked .media-frame { filter: blur(10px) saturate(.7); }
/* A locked video is already serving the gallery's baked heavy-blur derivative.
   Keep the blocked-state colour treatment, but do not blur that derivative a
   second time. Preparing and failed stages retain the CSS blur above. */
.media-stage.is-privacy-blurred .media-frame { filter: saturate(.7); }
.media-stage.is-fullscreen-safari {
    position: fixed;
    inset: 0;
    z-index: 220;
    aspect-ratio: auto;
    margin: 0;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}
/* The same undressing for the real thing. The stage's paper edge, its corners
   and its offset shadow are how a video sits on this page; taken full screen it
   is not on the page any more, and a 2px white line all the way round the
   monitor is a frame around nothing. The iOS fallback above had this from the
   start and native fullscreen never did.
   Two rules, not one selector list: an engine that does not know one of these
   pseudo-classes throws out the whole list it appears in, and each of these is
   the only one its own engine knows. */
.media-stage:fullscreen {
    aspect-ratio: auto;
    margin: 0;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}
.media-stage:-webkit-full-screen {
    aspect-ratio: auto;
    margin: 0;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

.media-controls {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    display: grid;
    gap: 10px;
    padding: 22px 18px 16px;
    background: linear-gradient(to top, rgba(7, 4, 4, .92), rgba(7, 4, 4, 0));
    opacity: 0;
    transition: opacity .3s ease;
}
.media-stage.is-controls-visible .media-controls { opacity: 1; }
/* The bar is inert while hidden, so a stray click near the bottom edge plays
   the video rather than pressing a control the reader cannot see. */
.media-stage:not(.is-controls-visible) .media-controls { pointer-events: none; }
.media-stage.is-blocked .media-controls { display: none; }
/* The mobile mute button lives outside the control bar, so the blocked stage
   must silence it separately or a locked page shows a working-looking control. */
.media-stage.is-blocked .media-mute--mobile { display: none; }
.media-controls-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-width: 0; }
.media-controls-group { display: flex; align-items: center; gap: 12px; min-width: 0; }

.media-button {
    flex: none;
    width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    padding: 0;
    border: 2px solid var(--paper);
    border-radius: 50%;
    color: var(--paper);
    background: var(--splash);
    box-shadow: 3px 4px 0 rgba(0, 0, 0, .72);
    cursor: pointer;
    transition: transform .2s ease, background .2s ease, box-shadow .2s ease;
}
.media-button:hover, .media-button:focus-visible { background: var(--splash-hot); transform: scale(1.08); box-shadow: 5px 6px 0 rgba(0, 0, 0, .72); }
.media-button:active { transform: scale(1); box-shadow: 1px 2px 0 rgba(0, 0, 0, .72); }
.media-button svg { width: 19px; height: 19px; }
[data-media-wave] { transition: opacity .2s ease; }

/* Both sliders are the same object at two sizes. The fill is painted by
   player.js as a gradient on the track: a range input has no styleable
   "before the thumb" box, and a faked one drifts out of register on zoom. */
.media-scrub, .media-volume-slider {
    -webkit-appearance: none;
    appearance: none;
    height: 6px;
    margin: 0;
    border-radius: 999px;
    background: rgba(255, 255, 255, .2);
    cursor: pointer;
}
.media-scrub { width: 100%; }
.media-scrub:hover { height: 8px; }
.media-scrub::-webkit-slider-thumb, .media-volume-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    border: 2px solid var(--paper);
    border-radius: 50%;
    background: var(--splash);
    box-shadow: 2px 3px 0 rgba(0, 0, 0, .72);
}
.media-scrub::-moz-range-thumb, .media-volume-slider::-moz-range-thumb {
    width: 16px;
    height: 16px;
    border: 2px solid var(--paper);
    border-radius: 50%;
    background: var(--splash);
    box-shadow: 2px 3px 0 rgba(0, 0, 0, .72);
}
/* The slider is a hover affordance: at rest the row is a mute button, and the
   track unrolls out of it. Width rather than opacity, so the collapsed state
   takes no space in a row that is tight on a phone. */
/* The control is one object that opens, not a button with a slider appearing
   beside it. The pill wears the chrome every other control in this row wears -
   the paper edge, the splash face, the hard offset - and the button inside
   gives all three up, so what a reader sees is the mute button itself growing
   sideways to make room for the slider. Collapsed it is 36 + 2 + 2, which is
   exactly the 40px circle its neighbours are.
   The sibling sites' player does the same thing; this is that pattern in this
   site's vocabulary rather than a second idea about what a volume control is. */
/* One object that opens. The capsule wears the chrome every control in this row
   wears - the paper edge, the splash face, the hard offset - and the mute button
   inside gives all three up, so what a reader sees is that button growing
   sideways to make room for the slider. Collapsed it is 36 + 2 + 2, which is
   exactly the 40px circle its neighbours are.
   The sibling sites' player does the same thing; this is that pattern in this
   site's vocabulary rather than a second idea about what a volume control is. */
.media-volume {
    display: flex;
    align-items: center;
    height: 40px;
    border: 2px solid var(--paper);
    border-radius: 999px;
    background: var(--splash);
    box-shadow: 3px 4px 0 rgba(0, 0, 0, .72);
    /* What makes it read as opening rather than appearing: the slider is
       already there at zero width and the capsule is what reveals it. */
    overflow: hidden;
    transition: background .2s ease, box-shadow .2s ease;
}
.media-volume:hover, .media-volume:focus-within { background: var(--splash-hot); box-shadow: 5px 6px 0 rgba(0, 0, 0, .72); }
/* Stripped to its icon. The capsule is the button now, and a second border,
   face and shadow inside it would draw the seam this is meant to remove. The
   scale goes with them: a control that grew inside a box with overflow hidden
   would be clipped by its own hover. */
.media-volume .media-button {
    width: 36px;
    height: 36px;
    border: 0;
    background: none;
    box-shadow: none;
}
.media-volume .media-button:hover,
.media-volume .media-button:focus-visible,
.media-volume .media-button:active { background: none; box-shadow: none; transform: none; }
.media-volume-slider { width: 0; margin-right: 0; opacity: 0; transition: width .24s ease, margin-right .24s ease, opacity .2s ease; }
.media-volume:hover .media-volume-slider,
.media-volume:focus-within .media-volume-slider { width: 84px; margin-right: 14px; opacity: 1; }
/* The track is inside the orange face, so it cannot wear the scrubber's
   colours: an orange fill on an orange face is not a fill. Paper for the part
   that is set and ink for the part that is not, read by player.js when it
   paints the gradient. */
.media-volume-slider {
    --range-fill: var(--paper);
    --range-rest: rgba(7, 4, 4, .32);
    background: rgba(7, 4, 4, .32);
}
/* A plain paper disc. The scrubber's thumb carries a ring and a hard offset
   because it sits on the dark panel and has to hold its own against the
   artwork behind it; in here it is a small white mark on an orange face at
   arm's length from anything else, and the ring and the shadow between them
   were more black than the mark was white. */
.media-volume-slider::-webkit-slider-thumb {
    width: 13px;
    height: 13px;
    border: 0;
    background: var(--paper);
    box-shadow: none;
}
.media-volume-slider::-moz-range-thumb {
    width: 13px;
    height: 13px;
    border: 0;
    background: var(--paper);
    box-shadow: none;
}

.media-time {
    display: flex;
    align-items: baseline;
    gap: 6px;
    margin: 0;
    color: var(--paper);
    font-family: var(--display);
    font-weight: 400;
    font-size: .96rem;
    font-variant-numeric: tabular-nums;
    letter-spacing: .04em;
    text-shadow: 2px 2px 0 var(--ink);
}
.media-time-sep { color: var(--muted); }

.media-mute--mobile { position: absolute; z-index: 2; top: 14px; left: 14px; display: none; }

/* Buffering and the click receipt. Neither is a control, so neither takes
   pointer events; both sit over the picture and under the offer. */
.media-spinner {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 54px;
    height: 54px;
    margin: -27px 0 0 -27px;
    border: 4px solid var(--line);
    border-top-color: var(--splash);
    border-radius: 50%;
    pointer-events: none;
    animation: lolo-media-spin .9s linear infinite;
}
.media-flash { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none; }
/* The glyph is the mark's own background rather than an inner element: one box
   means one animation, and the shape only ever says "play" or "pause". */
.media-flash-mark {
    width: 72px;
    height: 72px;
    border: 3px solid var(--paper);
    border-radius: 50%;
    background: var(--splash) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 5.2 19 12 8 18.8z' fill='%23ffffff'/%3E%3C/svg%3E") no-repeat center / 26px 26px;
    box-shadow: 5px 6px 0 rgba(0, 0, 0, .72);
    opacity: 0;
}
.media-flash-mark.is-paused {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.4 5h2.9v14H8.4zM12.7 5h2.9v14h-2.9z' fill='%23ffffff'/%3E%3C/svg%3E");
}
.media-flash.is-running .media-flash-mark { animation: lolo-media-flash .42s ease-out; }
@keyframes lolo-media-spin { to { transform: rotate(360deg); } }
@keyframes lolo-media-flash {
    0% { opacity: 0; transform: scale(.6); }
    35% { opacity: 1; transform: scale(1.12); }
    100% { opacity: 0; transform: scale(1.32); }
}

/* The offer, drawn over the picture. The veil is the locked gallery cover's,
   at full strength: halftone over dim, so a held-back video reads as artwork
   deliberately withheld rather than as a failed load. */
.media-lock { position: absolute; inset: 0; display: grid; place-items: center; padding: clamp(20px, 4vw, 40px); }
.media-lock .gate-cover-veil { background-color: rgba(7, 4, 4, .74); background-image: var(--halftone); background-size: 12px 12px; border-radius: 0; }
.media-lock-panel { position: relative; z-index: 1; width: min(460px, 100%); text-align: center; }
.media-lock-panel h2 {
    margin: 0 0 12px;
    color: var(--paper);
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(1.8rem, 4vw, 2.5rem);
    line-height: 1;
    text-transform: uppercase;
    text-shadow: 3px 3px 0 var(--splash);
}
.media-lock-panel .prose { margin: 0; color: var(--text); }
.media-lock-panel .actions { margin-top: 22px; justify-content: center; }

/* The live version label, in the slot the gallery page gives its image count.
   Same kind of fact about the thing being shown, and the only one on this page
   that changes while the reader is on it. */
.media-now { flex: none; display: grid; justify-items: end; gap: 4px; margin: 0; text-align: right; }
/* An explicit display beats the user agent's [hidden] rule, so anything laid
   out here and toggled from script has to restate it. */
.media-now[hidden], .media-lock[hidden], .media-flash[hidden] { display: none; }
.media-now-title {
    color: var(--splash);
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(1.4rem, 3vw, 2rem);
    line-height: 1;
    text-shadow: 3px 3px 0 var(--ink);
}
.media-now-label { color: var(--muted); font-weight: 850; font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; }

/* Version cards. A version is something you choose, not a line you read: the
   old list said "Members only" and left the reader to guess at both the tier
   and its price. Gap is 22px rather than 18px for the same reason the
   membership cards' is - each card's shadow overhangs it by 7px. */
/* No rule of its own: .post-head-row already closes the header with one, and a
   second hairline a gap below it read as a double line rather than as two
   sections. The space is the separation here. */
.version-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 22px;
    margin: clamp(28px, 4vw, 44px) 0 0;
}
/* The panel recipe the homepage's pricing cards wear, because this is the same
   kind of object: a choice on a surface. It is deliberately upright. The site
   skews buttons, pills, badges and ticks - things you press or read at a glance
   - and leaves every card it has square, so a leaning card here read as a
   different site rather than as the same one. */
.version-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: clamp(20px, 2.4vw, 26px);
    border: 2px solid var(--line);
    border-radius: 23px;
    color: var(--text);
    text-align: left;
    text-decoration: none;
    background-color: var(--surface);
    background-image: var(--halftone);
    background-size: 12px 12px;
    box-shadow: 6px 7px 0 rgba(0, 0, 0, .72);
    transition: transform .2s ease, border-color .2s ease, background-color .2s ease, box-shadow .2s ease;
}
/* The same fixed light source as the tier cards and the buttons: the card lifts
   up-left and the shadow grows, rather than the shadow moving. */
.version-card:hover, .version-card:focus-visible, .version-card:focus-within {
    border-color: var(--line-strong);
    transform: translate(-3px, -3px);
    box-shadow: 9px 10px 0 rgba(0, 0, 0, .72);
}
.version-card:active { transform: translate(2px, 3px); box-shadow: 3px 4px 0 rgba(0, 0, 0, .72); }
.version-card.is-playing { border-color: var(--splash); background-color: rgba(255, 43, 5, .14); }
.version-card.is-failed { opacity: .55; }
.version-card.is-failed:hover { transform: none; box-shadow: 6px 7px 0 rgba(0, 0, 0, .72); }
/* The card is the switch, so the button fills it rather than sitting in it. */
/* The tier card's name, a size down: display face, uppercase, and the paper-on-
   splash offset the hero headline wears. It was set in the display face already
   but sentence case and unshadowed, which made it the only display-face heading
   on the site that read as a label. */
.version-name { color: var(--paper); font-family: var(--display); font-weight: 400; font-size: clamp(1.35rem, 2vw, 1.6rem); line-height: 1.05; letter-spacing: .04em; text-transform: uppercase; text-shadow: 2px 2px 0 var(--splash); }
/* The card's bottom edge, and the sibling sites' own row: what this version is
   at one end, what you can do with it at the other. margin-top: auto is what
   keeps the row on the bottom edge of every card in a stretched row - the same
   job .tier-perks does with flex: 1, without needing a filler element. */
.version-foot { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: auto; }
.version-foot .version-download,
.version-foot .version-reverify { margin-left: auto; }
/* The cover that makes the whole card the switch. Its label is for screen
   readers only: the card already says the version's name in type, and a second
   visible copy under it would be the same word twice. */
.version-choose {
    position: absolute;
    inset: 0;
    z-index: 0;
    width: 100%;
    padding: 0;
    border: 0;
    border-radius: 23px;
    background: none;
    cursor: pointer;
}
.version-choose[disabled] { cursor: default; }
.version-choose-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
/* Above the cover, so the download is clickable and the badge is not part of
   the switch's own hit area. */
.version-card > .version-name,
.version-card > .version-note,
.version-card > .version-foot { position: relative; z-index: 1; pointer-events: none; }
/* The visible copy sits above the full-card .version-choose button for paint,
   but must not sit above it for hit testing. Otherwise only the gaps between
   the title and footer reach the button. Real controls inside the footer opt
   back in so downloads and membership actions keep their own targets. */
.version-foot :is(.version-download, .version-reverify, .button) { pointer-events: auto; }
.version-card .tier { position: static; }
.version-note { color: var(--muted); font-size: .94rem; line-height: 1.45; }
.version-mark { flex: none; display: grid; place-items: center; width: 26px; height: 26px; color: var(--splash); }
.version-mark svg { width: 22px; height: 22px; }
.version-mark--lock { opacity: .8; }
.version-state {
    justify-self: start;
    padding: 4px 10px;
    border: 2px solid var(--amber);
    border-radius: 6px;
    color: var(--amber);
    font-weight: 850;
    font-size: .74rem;
    letter-spacing: .1em;
    text-transform: uppercase;
}
/* Now playing wears the badge shape rather than the state pill: it is a fact
   about this card, the way the tier badge is, not a warning about it. */
.version-now {
    justify-self: start;
    padding: 4px 10px;
    border: 2px solid var(--paper);
    border-radius: 6px;
    color: var(--paper);
    background: var(--splash);
    box-shadow: 2px 3px 0 rgba(0, 0, 0, .72);
    font-family: var(--display);
    font-weight: 400;
    font-size: .76rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-shadow: var(--ink-ring);
}
.version-download { justify-self: start; }
.version-download[aria-busy="true"] { cursor: wait; opacity: .6; }
/* Failure recolours the plate's face rather than a border it no longer has:
   the button is the gallery download plate now, and amber on the inner band is
   how that object can say "Try again" without changing shape. */
.version-download.is-failed { --btn-face: var(--amber); }
/* The offer card is built exactly like its neighbours: same name on the top
   edge, same row on the bottom, same border at rest and on hover. It carried a
   splash edge borrowed from .tier-card--top, which made the one card in the row
   the reader cannot play look like the one the page wanted pressed; the price
   in the display face is loud enough to mark the offer on its own. */
/* Card scale for the panel button: the hero's 74px/1.42rem pair wraps the
   label to two lines inside a version card's column. Same step-down the
   download button takes at phone width. */
.version-card--membership .actions--panel .button { min-height: 48px; padding: 0 18px; font-size: 1.02rem; box-shadow: 4px 5px 0 rgba(0, 0, 0, .72); }
/* No divider over the foot: the locked card sharing this grid row has no
   interior rule, and the line made the pair read as two components rather
   than one card family. */
.version-card--membership .button { flex: 1 1 100%; }
/* The homepage sets the price in the display face with the ink ring, so the
   number reads as artwork rather than as a table cell. It is the same number and
   it gets the same treatment; the button underneath is then free to name the
   action instead of repeating the figure. */
.version-price { display: flex; align-items: baseline; gap: 8px; margin: 4px 0 0; color: var(--splash); font-family: var(--display); font-weight: 400; font-size: clamp(2.4rem, 4vw, 3.2rem); line-height: 1; text-shadow: var(--ink-ring); }
.version-price small { color: var(--muted); font-family: inherit; font-size: 1rem; text-shadow: none; }
/* Nothing to un-accent any more, and no permanent --line-strong edge either:
   an already-verified card wearing the hover border reads as stuck mid-hover.
   The card says "Membership active" and prints the tier - that is the state. */
.version-reverify { color: var(--splash-hot); text-decoration: underline; text-underline-offset: 3px; }
.media-script { margin-top: clamp(28px, 4vw, 44px); }

/* Credits. Hairlines between groups rather than cards around them: a credit is
   a line of the page's own copy, not a thing to click through a frame to. */
.media-credits { margin-top: clamp(28px, 4vw, 44px); }
.media-credits-group { padding-top: 24px; margin-top: 24px; border-top: 1px solid var(--line); }
.media-credits-group h2 { margin: 0 0 18px; }
.media-credit-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; margin: 0; padding: 0; list-style: none; }
.media-credit { display: flex; align-items: center; gap: 14px; text-decoration: none; }
/* The perks mark's plate, carrying a portrait. The image is counter-skewed so
   a face is never sheared - the frame leans, the person does not. */
.media-credit-avatar {
    position: relative;
    flex: none;
    display: grid;
    place-items: center;
    overflow: hidden;
    width: 48px;
    height: 48px;
    border: 2px solid var(--paper);
    border-radius: 11px;
    background: var(--surface-raised);
    box-shadow: 3px 4px 0 rgba(0, 0, 0, .72);
    transform: skewX(-9deg);
    transition: box-shadow .2s ease, transform .2s ease;
}
.media-credit-avatar img { width: 118%; height: 118%; object-fit: cover; transform: skewX(9deg); }
.media-credit-initial { color: var(--splash); font-family: var(--display); font-weight: 400; font-size: 1.4rem; transform: skewX(9deg); }
.media-credit:hover .media-credit-avatar, .media-credit:focus-visible .media-credit-avatar {
    transform: skewX(-9deg) translate(-2px, -2px);
    box-shadow: 5px 6px 0 rgba(0, 0, 0, .72);
}
.media-credit-copy { display: grid; gap: 2px; min-width: 0; }
.media-credit-name { color: var(--paper); font-weight: 850; font-size: 1.04rem; line-height: 1.3; }
.media-credit:hover .media-credit-name { color: var(--splash-hot); }
.media-credit-role { color: var(--muted); font-size: .9rem; line-height: 1.4; }

/* The unlock dialog and the download consent, both on .modal-backdrop so the
   dim, the exit and the reduced-motion behaviour are the age gate's. Above the
   gate (180) because they are opened by a click the gate has already cleared;
   below the lightbox (200), which is another page's overlay entirely. */
/* The image post's terms sit between: raised the moment the gate is answered
   rather than by a press, so it has to clear the gate on the way in - but it is
   the last thing a reader answers before the page is theirs, so everything they
   can then open belongs over it. */
.image-privacy { z-index: 185; }
.membership-unlock { z-index: 190; }
/* The one dialog on the site that is opened rather than answered. The age gate
   and the cookie banner are in the markup before first paint and only have a
   leaving to show; this one arrives on a press, so it has an entrance too, and
   the two want opposite curves - out of nothing on the way in, into nothing on
   the way out. The exit is still the shared one fadeOut drives. */
.media-consent { z-index: 195; transition: opacity .26s ease, backdrop-filter .26s ease; }
/* The dim leads on the way in. It is what the panel is seen against, so a panel
   that arrived first would land on the page rather than in the dialog; on the
   way out .modal-backdrop's own delay makes the dim outlast it instead. */
.media-consent.is-opening { opacity: 0; backdrop-filter: blur(0px); }
.media-consent .modal { transition: transform .3s cubic-bezier(.22, .61, .36, 1), opacity .24s ease-out; }
/* Not scale(.8) like the exit: a panel that grows from four fifths reads as
   arriving, where the same distance on the way out reads as being dismissed.
   Entrances are the shorter journey of the two. */
.media-consent.is-opening .modal { transform: scale(.94); opacity: 0; }
/* The exit curve, back on the way out. Ease-in so it accelerates away rather
   than crawling off - the same reasoning .modal's own rule carries. */
.media-consent.is-closing .modal { transition: transform .32s cubic-bezier(.32, 0, .67, 0), opacity .32s ease-in; }
.membership-unlock-panel, .media-consent-panel, .image-privacy-panel { width: min(480px, 100%); text-align: left; }
/* No .is-opening pair here, unlike .media-consent. This dialog is printed into
   the page before first paint like the age gate, so it has only a leaving to
   show and .modal-backdrop's own exit is already it.
   The consequence carries the paper the prose above it does not: it is the one
   line in the panel a reader has to have read, and at body colour it sat level
   with the sentence explaining the watermark. */
/* .modal p zeroes its margins for the single-paragraph panels; this one stacks
   three, so the gap is reinstated between them rather than on each. */
.image-privacy-panel p + p { margin-top: 14px; }
.image-privacy-consequence { color: var(--paper); font-weight: 700; }
.image-privacy-fine { font-size: .9rem; color: var(--muted); }
/* No overflow: hidden here — it would clip the close button hung off the
   corner below. The artwork rounds its own top corners instead. */
.membership-unlock-panel { padding: 0 0 calc(var(--pad) + 8px); }
.membership-unlock-panel h2, .media-consent-panel h2 { white-space: normal; }
.membership-unlock-body { padding: 0 var(--pad); }
/* The poster runs to the panel's own edge: it is the thing being withheld, not
   an illustration of it, and an inset image would read as a thumbnail. */
/* The clip lives on the artwork, not the panel: the image has to follow the
   panel's rounded top corners, but a panel-level overflow would also eat the
   close button hung off the corner. 9px is the panel's 12px radius minus its
   3px border. */
.membership-unlock-art { position: relative; margin-bottom: var(--pad); border-radius: 9px 9px 0 0; overflow: hidden; border-bottom: 3px solid var(--paper); background: var(--surface); }
.membership-unlock-art img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; opacity: 0; transition: opacity .3s ease; }
.membership-unlock-art::after { position: absolute; inset: 0; content: ""; background: linear-gradient(to top, rgba(7, 4, 4, .82), rgba(7, 4, 4, 0) 62%); pointer-events: none; }
/* The lightbox close's own overhang: pinned off the panel's corner. The
   artwork carries the clip that used to eat this — see .membership-unlock-art. */
.membership-unlock-close { position: absolute; z-index: 2; top: -18px; right: -18px; }
.membership-unlock-version { margin: 0 0 6px; color: var(--paper); font-weight: 850; font-size: 1.06rem; }
.membership-unlock-duration { color: var(--muted); font-weight: 400; }
.membership-unlock-duration::before { content: " \00b7  "; }
.membership-unlock-tier { margin: 0; color: var(--muted); font-size: .96rem; }
.membership-unlock-figure { display: flex; align-items: baseline; gap: 6px; margin: 10px 0 0; }
.membership-unlock-price { color: var(--splash); font-family: var(--display); font-weight: 400; font-size: clamp(2.2rem, 5vw, 3rem); line-height: .9; text-shadow: 3px 3px 0 var(--ink); }
.membership-unlock-period { color: var(--muted); font-weight: 850; font-size: .9rem; letter-spacing: .06em; text-transform: uppercase; }
.membership-unlock-panel .membership-perks { gap: 14px; margin-top: 22px; }
.membership-unlock-error { margin: 18px 0 0; }
/* Its own rule rather than a fourth term in the shared :is() list: this panel's
   buttons stack full width, and folding it in would have set the same rule for
   the age gate and the cookie banner. */
.membership-unlock-actions { margin-top: 24px; }
.membership-unlock-actions .button { flex: 1 1 100%; }
.membership-unlock-current { margin: 14px 0 0; }

.media-consent-panel .prose { margin: 0 0 18px; color: var(--text); }
.media-consent-check { display: flex; align-items: flex-start; gap: 12px; color: var(--muted); font-size: .96rem; line-height: 1.5; cursor: pointer; }
.media-consent-check input {
    flex: none;
    width: 22px;
    height: 22px;
    margin: 2px 0 0;
    appearance: none;
    border: 2px solid var(--paper);
    border-radius: 4px;
    background: var(--ink);
    box-shadow: 2px 3px 0 rgba(0, 0, 0, .72);
    transform: skewX(-9deg);
    cursor: pointer;
}
.media-consent-check input:checked { background: var(--splash) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12.5 9.5 18 20 6'/%3E%3C/svg%3E") no-repeat center / 14px 14px; }
.media-consent-check a { color: var(--splash-hot); text-decoration: underline; text-underline-offset: 3px; }
.media-consent-status { margin: 14px 0 0; color: var(--muted); font-size: .9rem; line-height: 1.4; }
/* The one thing this line says that is the reader's to fix rather than the
   site's to report. The box it points at wears the same colour, so the sentence
   and its subject are found together. */
.media-consent-check input[aria-invalid="true"] { border-color: var(--splash); }
.media-consent-check:has(input[aria-invalid="true"]) ~ .media-consent-status { color: var(--splash-hot); }
.media-consent-panel .actions { margin-top: 24px; }
/* The same corner the viewer's and the unlock dialog's close buttons take, at
   the same overhang: a reader who has met one of them has met all three. */
.media-consent-close { position: absolute; z-index: 2; top: -18px; right: -18px; }
/* The Download button's working state. The label goes invisible rather than
   away so the panel keeps its width, and the ring takes its place: the same
   ring the player shows while it buffers, sized to the type. A busy button is
   disabled, but :hover and :active would still lift and press it - held at
   rest here so a control that cannot be pressed does not act as if it could. */
/* Weighed to beat the panel button's own `> span` rule above, which would
   otherwise flow the ring inline beside the hidden label and skew it with the
   panel. The counter-skew is baked into the spin so the ring stays round while
   it turns inside the slanted button. */
.actions--panel .button > .button-spinner,
:is(.hero .actions, .modal .actions, .cookie-banner) .button > .button-spinner { position: absolute; z-index: 1; top: 50%; left: 50%; display: block; width: 26px; height: 26px; margin: -13px 0 0 -13px; border: 3px solid rgba(255, 248, 243, .3); border-top-color: var(--paper); border-radius: 50%; transform: skewX(9deg); opacity: 0; pointer-events: none; }
.button[aria-busy="true"] { cursor: wait; }
.button[aria-busy="true"] > span:not(.button-spinner) { visibility: hidden; }
.actions--panel .button[aria-busy="true"] > .button-spinner,
:is(.hero .actions, .modal .actions, .cookie-banner) .button[aria-busy="true"] > .button-spinner { opacity: 1; animation: button-spinner-turn .9s linear infinite; }
@keyframes button-spinner-turn { from { transform: skewX(9deg) rotate(0deg); } to { transform: skewX(9deg) rotate(360deg); } }
.actions--panel .button[aria-busy="true"]:hover,
.actions--panel .button[aria-busy="true"]:focus-visible { --btn-face: var(--splash); transform: skewX(-9deg); box-shadow: 6px 7px 0 rgba(0, 0, 0, .72); }
.actions--panel .button[aria-busy="true"]:active { transform: skewX(-9deg); box-shadow: 6px 7px 0 rgba(0, 0, 0, .72); }

/* Desktop hero motion. Both effects are gated on width rather than on pointer:
   they are about how much of the hero a reader can see at once, which is a
   question about the viewport, not about the input device.
   The 3D tip is what gives the headline its comic-panel flip; transform-origin
   at the baseline is what makes the word pivot up rather than tumble. */
@media (min-width: 901px) {
    /* Nav links arrive after the bar itself, 60ms apart, so the row assembles
       rather than appearing whole. Indices are set as --reveal-delay by
       site.js: the link count is fixed in layout.php but the admin link is
       conditional, so counting them in CSS with :nth-child would drift. */
    html.js .site-header .nav-links a,
    html.js .site-header .nav-cta {
        opacity: 0;
        transform: translateY(-16px);
        transition: opacity .6s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms), transform .6s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms);
    }
    html.js .site-header.is-entered .nav-links a { opacity: 1; transform: none; }
    /* Keep the panel slanted after its entrance animation. The label carries
       the matching counter-skew below, so resetting this to none makes Join
       Now look italic while the hero CTA remains upright. */
    html.js .site-header.is-entered .nav-cta { opacity: 1; transform: skewX(-9deg); }
    /* The marker stroke under a nav link is a transform on ::after; it is not
       part of the entrance and must not inherit the delay above. */
    .site-header.is-entered .nav-links a::after { transition-delay: 0ms; }

    .hero-line { perspective: 800px; }
    .hero-word { transform: translateY(110%) rotateX(-55deg); transform-origin: 50% 100%; }
    .hero-word.is-revealed { transform: none; }
    /* --hero-scroll is written by site.js, 0 at rest and 1 once the hero has
       fully left. calc() does the mapping so the script only ever publishes a
       progress number and never a pixel value. */
    .hero-grid {
        transform: translateY(calc(var(--hero-scroll, 0) * 120px)) scale(calc(1 - var(--hero-scroll, 0) * .05));
        opacity: calc(1 - var(--hero-scroll, 0));
    }
    /* Promoted only while it is actually moving: a permanent will-change on a
       full-width hero holds a large layer for the whole session. */
    .hero-grid.is-parallaxing { will-change: transform, opacity; }
}
/* The mobile dropdown's entrance; used only inside the block below. */
@keyframes nav-drop {
    from { opacity: 0; transform: translateY(-10px); }
}
@media (max-width: 900px) {
    /* Most of a screenful, not all of it: the next section's heading peeking
       over the fold is what invites the scroll. 72px is the mobile nav row. */
    .hero { min-height: calc(88svh - 72px); }
    .hero-grid, .creator-hero { grid-template-columns: 1fr; }
    /* Stacked, the mascot band pushed the pitch and CTAs down the page, so the
       hero is text-only here. The imgs still download (display:none does not
       cancel a fetch); acceptable for now. */
    .brand-stage { display: none; }
    /* auto-fill already lands on two here; stated anyway, because two-up at this
       width is a decision and not a coincidence of the 300px floor. */
    .media-grid { grid-template-columns: repeat(2,minmax(0,1fr)); }
    /* The tiles keep two-up once the cover has stacked above them: at the
       desktop 300px floor a 900px viewport fits three, which reads busier than
       the page the reader came from. */
    /* The whole template, not the floor variable: site.js writes that variable
       as an inline style, which would outrank anything this query said about it.
       Overriding the property it feeds leaves the script's measurement intact
       and still wins where it has to. */
    .post-gallery--masonry { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
    /* Stacked: a display-face title and a figure-scale count stop sharing a
       line long before the phone breakpoint. */
    .post-head-row { flex-direction: column; align-items: flex-start; gap: 22px; }
    .image-post-gate { grid-template-columns: 1fr; }
    /* Keep portrait teases inside the viewport after the gate stacks. The old
       auto height restored the 4:5 cover's full width-derived height and let it
       run below the fold before the locked-set row had even been counted. */
    .gate-media { max-width: 420px; height: min(calc(100svh - 150px), 680px); min-height: 0; }
    .gate-cover .media-art { aspect-ratio: auto; }
    .section--gate { padding: 44px 0 66px; }
    .carousel-track { grid-auto-columns: clamp(220px, 42vw, 280px); }
    .carousel-next { right: 0; }
    /* The wall becomes one snap strip: the rail wrapper dissolves and its cards
       line up after the poster. One column width for all of them, and one 4:5
       ratio: a wider lead column was tried and read as a mistake beside the
       rest, and mixed sizes are also what made a ratio on stretched cards
       overflow their tracks. Equal tracks and equal ratios give equal boxes,
       so nothing is stretched and nothing overflows. The poster keeps only its
       type. Same track treatment as .carousel-track, not shared with it: that
       one has a next button to position, this one does not.
       overflow is auto hidden, not overflow-x alone: a non-visible overflow-x
       makes overflow-y compute to auto, and the cards' pre-reveal
       translate: 0 28px (see .card[data-reveal]) then gave the strip 14px of
       vertical scroll. Cards off the right edge never intersect the viewport,
       so they stay translated and that overflow never went away. A vertically
       scrollable box under an x-mandatory snap swallows vertical swipes on
       iOS, and the page stopped scrolling with a finger on the strip. */
    /* Attribute selector for the specificity of the rail-count rules above. */
    .featured-wall[data-featured-rail] .featured-track { grid-template-columns: none; grid-template-rows: auto; height: auto; grid-auto-flow: column; grid-auto-columns: clamp(250px, 70vw, 330px); overflow: auto hidden; padding: 4px 4px 14px; scroll-snap-type: x mandatory; overscroll-behavior-x: contain; scrollbar-width: thin; scrollbar-color: var(--splash) transparent; }
    .featured-track > .card--lead { grid-row: auto; }
    .featured-wall .card { aspect-ratio: 4 / 5; height: auto; scroll-snap-align: start; }
    /* The strip above is the fallback. Once site.js starts the slider (the
       same Swiper build and swipe behaviour as HRA and BP), the track is a
       flex row that Swiper moves, and the wall clips it. Swiper's own
       stylesheet is not loaded; these are the few rules it needs. The slide
       width leaves the next card peeking, as HRA's 1.15 slides per view do,
       capped so a tablet shows more than one card rather than one huge one.
       pan-y hands vertical swipes to the page and keeps horizontal ones. */
    .featured-wall.swiper-initialized { overflow: hidden; padding: 4px 4px 0; touch-action: pan-y; }
    .featured-wall.swiper-initialized .featured-track { display: flex; width: 100%; height: auto; padding: 0; overflow: visible; scroll-snap-type: none; box-sizing: content-box; transition-property: transform; }
    .featured-wall.swiper-initialized .card { flex-shrink: 0; width: clamp(220px, 86%, 330px); }
    .featured-wall.swiper-initialized .featured-dots { display: flex; justify-content: center; margin-top: 10px; }
    /* A 24px button around a 10px dot: the dot is what reads, the button is
       what a thumb can actually hit. */
    .featured-dots .swiper-pagination-bullet { display: grid; place-items: center; width: 24px; height: 24px; margin: 0; padding: 0; border: 0; background: none; cursor: pointer; }
    .featured-dots .swiper-pagination-bullet::before { content: ""; width: 10px; height: 10px; border-radius: 50%; background: rgba(255, 255, 255, .28); transition: background .2s ease, transform .2s ease; }
    .featured-dots .swiper-pagination-bullet-active::before { background: var(--splash); transform: scale(1.2); }
    .featured-dots .swiper-pagination-bullet:focus-visible { outline: 2px solid var(--splash); outline-offset: 2px; border-radius: 50%; }
    .card--lead .card-body { padding: 22px 22px 28px; }
    /* No descriptions in the strip, on any card: the poster dropped its own for
       room, and the rail cards carrying theirs made the row read as two kinds
       of tile. Title and meta are the strip. */
    .featured-wall .card p { display: none; }
    .nav-links { position: absolute; top: calc(100% + 1px); left: 0; right: 0; display: none; padding: 14px 20px 20px; flex-direction: column; align-items: stretch; border-bottom: 1px solid var(--line); background: rgba(7,4,4,.98); }
    /* An animation rather than a transition: the panel goes display:none when
       closed, so there is no resting state to transition from - the keyframes
       play fresh each time the open class re-renders it. */
    .nav-links.is-open { display: flex; animation: nav-drop .32s cubic-bezier(.16, 1, .3, 1); }
    /* The dropdown's Join Now. Doubled class outranks the .nav-links a row
       styling above without leaning on !important; the panel look itself comes
       from .nav-cta. */
    .nav-links a.nav-cta--menu {
        display: inline-flex;
        align-self: flex-start;
        margin: 14px 2px 6px;
        padding: 12px 24px;
        font-size: 1.05rem;
        color: var(--paper);
        transform: skewX(-9deg);
    }
    /* Upright in the dropdown: stacked slanted rows read as misaligned. */
    .nav-links { gap: 2px; margin-left: 0; }
    .nav-links a { padding: 12px 2px; }
    /* Rows are full-width here, so a stroke spanning the row would look wrong. */
    .nav-links a[aria-current="page"]::after, .nav-links a.is-marked::after { right: auto; width: 92px; top: calc(100% - 5px); animation: none; }
    .menu-toggle { display: inline-grid; place-items: center; margin-left: auto; }
    .nav { height: 72px; }
    /* Matches the shorter header row. */
    html { scroll-padding-top: 88px; }
    /* No bleed here: the row is shorter and the dropdown opens beneath it, so an
       overhanging logo would collide with the open menu. */
    .brand img { width: 165px; height: 55px; transform: none; }
    /* The logo does not drop below 900px, so nothing else should either. */
    .nav-links { transform: none; }
    /* The row's Join Now retires here: the burger holds its own copy, and two
       primary panels in a 72px row crowded the burger out. The doubled-class
       .nav-links a.nav-cta--menu rule above outranks this and keeps the menu
       twin visible. */
    .nav-cta { display: none; }
    /* The row is 72px here and the CTA 37px, so the same kind of drop leaves it
       starting at 25.5px, which is what sets the tab's height. */
    .nav-admin { height: 24px; padding: 0 12px; font-size: .74rem; }
    .footer-brand img { width: 150px; height: 50px; }
    /* The 288px lockup is what the 5fr column was sized for; at 150px the three
       columns would sit in a lot of air, so the brand takes the full row. */
    .footer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 34px; margin-bottom: 34px; }
    .footer-brand-col { grid-column: 1 / -1; }
    /* The hero measured 961px stacked, most of it spacing scaled for a two-column
       desktop layout. These pull it back toward a screenful. */
    /* Text-only hero here (the mascot band is hidden below 900px), so the
       spacing that balanced the old image row reads as dead air — pulled in. */
    .hero { padding: 32px 0 36px; }
    .hero-grid { gap: 34px; }
    .hero h1 { margin-bottom: 18px; }
    .hero .actions { gap: 14px; margin-top: 24px; }
    /* Desktop button metrics are far too heavy here: at 1.42rem with 36px of
       padding the pair wrapped to two full-width rows 178px tall. */
    .hero .actions .button { min-height: 56px; padding: 0 24px; font-size: 1.15rem; }
    .tier-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 420px)); }
    /* One column, and no splash in it. The price panel earned its place as the
       band's second column; stacked it is most of a screenful repeating a figure
       the CTA right above it already states — "Become a member — $7/mo" — and
       it pushes the perks it exists to sell off the screen. Nothing is lost to a
       screen reader: the CTA label carries the price as text. */
    .membership-band { grid-template-columns: 1fr; gap: 34px; }
    .membership-flash { display: none; }
    /* Two connect cards side by side leave each CTA narrower than its own label
       at this width, so the pair stacks. The cards keep their inner column. */
    .membership-cards { grid-template-columns: 1fr; }
    /* Same reason as the hero pair above: 74px at 1.42rem does not fit a card
       column at this width and wraps the label. */
    .actions--panel .button { min-height: 56px; padding: 0 24px; font-size: 1.15rem; }
    .hero-copy { font-size: 1.1rem; }
    /* The version label under the title rather than at the far end of the
       row: .post-head-row stacks here, and a right-aligned figure under a
       left-aligned title reads as a stray. */
    .media-now { justify-items: start; text-align: left; }
    .version-grid { grid-template-columns: 1fr; }
}
@media (max-width: 620px) {
    /* One card per row on a phone. Two-up was tried and each card came out too
       narrow to wear its own dressing: the Members Only ribbon overlapped the
       title and the titles wrapped to four lines. A full-width card holds the
       ribbon, the title and the excerpt on their own rows. */
    .gallery-grid { grid-template-columns: 1fr; gap: 14px; }
    .gallery-filters, .filter-pills { gap: 12px; margin-bottom: 20px; }
    .gallery-pill { padding: 7px 14px; font-size: .94rem; }

    .shell { width: min(100% - 26px,1320px); }
    .hero { padding-top: 40px; }
    .hero h1 { font-size: clamp(3rem,17vw,5rem); }
    .tier-grid { grid-template-columns: 1fr; }
    /* Same single column as .gallery-grid above, for the same reason. */
    .media-grid { grid-template-columns: 1fr; gap: 14px; }
    .carousel-track { grid-auto-columns: min(78vw, 280px); }
    .section { padding: 66px 0; }
    .section-heading { align-items: flex-start; flex-direction: column; }
    .footer-grid { grid-template-columns: 1fr; gap: 30px; }
    .footer-legal { justify-content: flex-start; }
    .search-panel { grid-template-columns: 1fr 1fr; }
    .search-panel .field[data-browse-input] { grid-column: 1 / -1; }
    .filter-drawer > summary { width: 100%; justify-content: center; }
    .filter-drawer-panel { left: 0; right: auto; min-width: 100%; }
    .creator-image, .media-art { max-width: 360px; }
    .post-gallery { gap: 10px; }
    .post-gallery--masonry { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    /* Two-up whatever the pieces are: one landscape panel per row on a phone
       is larger than the screen can use, and four rows of one is a scroll. */

    .locked-gallery { gap: 10px; }
    /* Nothing overhangs a phone viewport: the close button moves inside the
       frame rather than sitting on its corner, and the artwork gives back the
       height the control row needs. */
    /* One more step down, and the shadow with it: at this width the offset it
       wears on a desktop reads as the panel sitting off-centre rather than as
       depth. */
    .post-download { padding: 10px 18px; font-size: 1.02rem; box-shadow: 3px 4px 0 rgba(0, 0, 0, .72); }
    .post-download svg { width: 15px; height: 15px; }
    /* Nothing overhangs a phone viewport: the backdrop's own padding is 20px
       and the panel fills what is left, so a corner mark hung outside it would
       sit off the screen. Same move the viewer's close makes. */
    .media-consent-close { top: 8px; right: 8px; }
    .lightbox-close { top: 8px; right: 8px; }
    /* No room for a 76px gutter either side on a phone, so the arrows move
       inside the artwork's edges. They keep the solid splash fill and the hard
       shadow, which is what holds them apart from the picture behind. */
    .lightbox-prev { left: 8px; }
    .lightbox-next { right: 8px; }
    .lightbox-media { height: calc(100vh - 118px); }
    .lightbox-button { width: 46px; height: 46px; }
    /* One step down together: the section-heading titles wear .post-title's
       treatment now, so they scale with it. */
    .post-title, .section-heading h1, .section-heading h2 { font-size: clamp(1.9rem, 8.4vw, 2.6rem); text-shadow: 3px 3px 0 var(--splash); }
    .is-list .card-media { width: 120px; }
    .is-list .card-body { padding-left: 136px; }

    /* The dialog had no responsive rules at all. At 360px its content box is
       ~268px and the desktop button pair needs ~340px, so it wrapped into a
       ragged near-miss that the skew read as a mistake. Stack it on purpose. */
    .modal { padding: 24px 20px 30px; border-width: 2px; box-shadow: 7px 8px 0 rgba(0, 0, 0, .72); }
    .modal h2 { font-size: clamp(1.9rem, 8.4vw, 2.4rem); text-shadow: 3px 3px 0 #000; }
    .modal .actions { gap: 14px; margin-top: 24px; }
    /* Full-width rows. A 56px panel skewed 9deg overhangs ~4.4px a side plus a
       4px shadow, against 20px of padding — nothing clips. */
    .modal .actions .button { flex: 1 1 100%; min-height: 56px; padding: 0 20px; font-size: 1.12rem; box-shadow: 4px 5px 0 rgba(0, 0, 0, .72); }
    .modal .actions .button:hover { box-shadow: 5px 6px 0 rgba(0, 0, 0, .72); }
    /* Shallower offsets, matching the dialog above: at this width the desktop
       6px/7px shadow eats into the 26px gutter. */
    .membership-perk-mark { width: 40px; height: 40px; border-radius: 10px; }
    .membership-perk-mark svg { width: 23px; height: 23px; }
    .tier-card { padding: 22px; box-shadow: 4px 5px 0 rgba(0, 0, 0, .72); }
    .tier-card:hover { box-shadow: 5px 6px 0 rgba(0, 0, 0, .72); }
    .actions--panel .button { box-shadow: 4px 5px 0 rgba(0, 0, 0, .72); }
    .cookie-banner { right: 14px; bottom: 14px; max-width: calc(100% - 28px); padding: 16px 18px; }
    .cookie-banner .actions .button { flex: 1 1 100%; }
    /* No hover to unroll the slider with, so the row loses it and the mute
       moves to where a thumb already is. */
    .media-volume { display: none; }
    .media-mute--mobile { display: grid; }
    .media-controls { padding: 18px 12px 12px; }
    .media-credit-grid { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    /* The dropdown appears in place and the burger cuts straight to its open
       cross; the states themselves are information, only the travel is motion. */
    .nav-links.is-open { animation: none; }
    .menu-toggle-line { transition: none; }
    /* The skew is the button's shape, not its motion, so it survives; only the
       press travel is dropped. */
    .post-download:active { transform: skewX(-9deg); }
    /* No spinning ring; the dimmed label says the button is working instead. */
    .actions--panel .button[aria-busy="true"] > .button-spinner { animation: none; opacity: 0; }
    .button[aria-busy="true"] > span:not(.button-spinner) { visibility: visible; opacity: .55; }
    /* site.js already skips the flight for this reader; the rule is the second
       line of defence, so a ghost can never be left painted over the page. */
    .lightbox-ghost { display: none; }
    /* The plate still appears and the hand still reads as a swipe; only the
       travel is dropped, so the speed lines are held on rather than animated. */
    .lightbox-hint-hand, .lightbox-hint-trail { animation: none; }
    .lightbox-hint-trail { opacity: .7; }
    /* The drag itself stays - it is the reader's own hand moving the picture,
       not an effect played at them - but an abandoned one snaps home rather
       than travelling there. */
    .lightbox-stage.is-settling { transition: none; }
    /* The turn is skipped outright for this reader - site.js steps straight to
       the next piece - so the class never lands; the rule is the second line of
       defence behind that guard. */
    .lightbox-stage.is-turning, .lightbox-stage.is-turning .lightbox-media { transition: none; }
    /* The controls still stand down for the drag - that is not decoration, it
       is what stops them flanking a gap - but they do it instantly. */
    .lightbox-button { transition: none; }
    /* The dialog still fades - fadeOut waits on a transform that has to exist,
       and its timer is the backstop rather than the plan - but the panel stops
       travelling to get there. */
    .media-consent.is-opening .modal { transform: none; }
    .media-consent.is-closing .modal { transform: none; }
    /* Zeroing the duration below does not stop a transform from applying — it makes
       it instant, which is the jump this query exists to prevent. Colour and shadow
       still answer the pointer; only the travel is dropped. */
    :is(.hero .actions, .modal .actions, .cookie-banner) .button:hover,
    :is(.hero .actions, .modal .actions, .cookie-banner) .button:focus-visible,
    :is(.hero .actions, .modal .actions, .cookie-banner) .button:active { transform: skewX(-9deg); }
    .actions--panel .button:hover,
    .actions--panel .button:focus-visible,
    .actions--panel .button:active { transform: skewX(-9deg); }
    /* The card has no skew to preserve, so it stands down to no transform at
       all. Its border and shadow still answer the pointer. */
    .tier-card:hover { transform: none; }
    .membership-card:hover, .membership-card:focus-visible, .membership-card:focus-within,
    .membership-card:active { transform: none; }
    .gallery-pill:hover,
    .gallery-pill:focus-visible,
    .gallery-pill:active,
    .view-toggle button:hover,
    .view-toggle button:focus-visible,
    .view-toggle button:active,
    .pagination a:hover,
    .pagination a:focus-visible,
    .pagination a:active { transform: skewX(-9deg); }
    /* Scroll motion stands down entirely rather than playing instantly. The
       blanket rule below collapses the duration but not the starting
       transform, so without these an element would still be laid out 28px
       adrift and merely snap into place on reveal - the jump this query
       exists to prevent. site.js makes the same call for itself and reveals
       everything up front rather than observing; this is the half that has to
       hold if the script never runs. */
    [data-reveal] { opacity: 1; transform: none; translate: none; }
    html.js .site-header,
    html.js .site-header .nav-links a,
    html.js .hero-anim-element { opacity: 1; transform: none; }
    /* Same specificity as the pre-paint rule above, later in the file, so the
       set is simply there for a reader who asked for less motion. */
    html.js .post-piece { opacity: 1; translate: none; }
    html.js .site-header .nav-cta { opacity: 1; transform: skewX(-9deg); }
    .hero-word { opacity: 1; transform: none; }
    /* The parallax is travel, not emphasis, so the hero simply holds still. */
    .hero-grid { transform: none; opacity: 1; }
    .version-card:hover, .version-card:focus-visible, .version-card:focus-within,
    .version-card:active { transform: none; }
    .media-credit:hover .media-credit-avatar,
    .media-credit:focus-visible .media-credit-avatar { transform: skewX(-9deg); }
    .media-button:hover, .media-button:focus-visible, .media-button:active { transform: none; }
    /* The flash is decoration rather than information - the control bar has
       already said what state the video is in - so it is absent here rather
       than merely fast. */
    .media-flash { display: none; }
    .media-controls { transition: none; }
    *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}
