/* ====================================================================
   slavabatov-art — ink-on-paper
   Minimal, editorial, fast. No framework.
   ==================================================================== */

/* ---------- @FONT-FACE (self-hosted) ----------
   Fonts are served from /wwwroot/fonts/ instead of fonts.googleapis.com.
   Benefits: one fewer cross-origin handshake on first paint, no third-party
   dependency (the site still renders if Google is blocked or down), and no
   PII leakage to Google on page load. Files came from google-webfonts-helper
   (gwfh.mranftl.com) — latin subset, woff2 only (every browser we care about
   supports woff2 since 2020).

   font-display: swap matches the previous &display=swap behavior: text shows
   immediately in the fallback (Times/system sans), then swaps in once the
   custom font loads. No invisible-text flash.

   If we ever change the subset/version, bump the filenames (e.g. v22) so
   the immutable cache set in Program.cs doesn't serve stale bytes. */
@font-face {
    font-family: 'Cormorant Garamond';
    font-style: normal;
    font-weight: 300;
    font-display: swap;
    src: url('/fonts/cormorant-garamond-v21-latin-300.woff2') format('woff2');
}
@font-face {
    font-family: 'Cormorant Garamond';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/cormorant-garamond-v21-latin-regular.woff2') format('woff2');
}
@font-face {
    font-family: 'Cormorant Garamond';
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/cormorant-garamond-v21-latin-italic.woff2') format('woff2');
}
@font-face {
    font-family: 'Cormorant Garamond';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('/fonts/cormorant-garamond-v21-latin-500.woff2') format('woff2');
}
@font-face {
    font-family: 'Cormorant Garamond';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('/fonts/cormorant-garamond-v21-latin-600.woff2') format('woff2');
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 300;
    font-display: swap;
    src: url('/fonts/inter-v20-latin-300.woff2') format('woff2');
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/inter-v20-latin-regular.woff2') format('woff2');
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('/fonts/inter-v20-latin-500.woff2') format('woff2');
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('/fonts/inter-v20-latin-600.woff2') format('woff2');
}

:root {
    --paper: #F3EEE6;          /* warm off-white */
    --paper-2: #E9E2D4;        /* slightly darker paper */
    --ink: #1A1714;            /* near-black, warm */
    --ink-soft: #4A4339;       /* mid text */
    --ink-faint: #8B8273;      /* captions */
    --rule: rgba(26, 23, 20, 0.12);
    --accent: #9A3E2D;         /* oxide red, pulled from warm landscapes */
    --accent-soft: #C26A5A;

    /* Status colors — muted so they read as ink, not as warnings. Every
       "success/ok" and "destructive" surface in the app now references
       these instead of drifting hex values. */
    --ok: #2d6a3e;                         /* used by tiles-status--ok, contact-flash--ok */
    --ok-soft: rgba(45, 106, 62, 0.35);
    --danger-bg: #F6DDD6;                  /* pale red surface for destructive pills */
    --danger-bg-hover: #EFC8BE;
    --danger-ink: #7A2B1E;
    --danger-ink-hover: #5E1F14;

    --shadow-art: 0 30px 60px -30px rgba(26, 23, 20, 0.35),
                  0 2px 10px -2px rgba(26, 23, 20, 0.10);
    --shadow-art-hover: 0 40px 80px -30px rgba(26, 23, 20, 0.55),
                        0 4px 14px -2px rgba(26, 23, 20, 0.18);

    --font-display: 'Cormorant Garamond', 'Times New Roman', serif;
    --font-body:    'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

    /* Uppercase letter-spacing scale. --track-ui is the default for
       buttons, labels, pills, and caption meta. --track-wide is reserved
       for very small glyphs (10-11px) where --track-ui reads tight.
       See docs/pre-launch-cleanup.md for the consolidation rationale. */
    --track-ui: 0.2em;
    --track-wide: 0.22em;

    --max: 1280px;
    --pad: clamp(20px, 4vw, 64px);

    --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: 17px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;

    /* Subtle paper grain via layered gradients (no image file needed). */
    background-image:
        radial-gradient(1200px 600px at 0% 0%, rgba(154, 62, 45, 0.05), transparent 70%),
        radial-gradient(1000px 500px at 100% 100%, rgba(154, 62, 45, 0.04), transparent 70%);
    background-attachment: fixed;
}

img { display: block; max-width: 100%; height: auto; }

a { color: inherit; text-decoration: none; }

button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

/* ---------- NAV ---------- */

.nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 18px var(--pad);
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-bottom: 1px solid transparent;
    color: #f8f2e6;
    transition: background-color .35s var(--ease),
                backdrop-filter .35s var(--ease),
                -webkit-backdrop-filter .35s var(--ease),
                border-color .3s var(--ease),
                color .35s var(--ease);
}
.nav.is-scrolled {
    background: rgba(243, 238, 230, 0.92);
    backdrop-filter: saturate(140%) blur(10px);
    -webkit-backdrop-filter: saturate(140%) blur(10px);
    border-bottom-color: var(--rule);
    color: var(--ink);
}

/* Brand text is hidden until the user scrolls — avoids duplicating
   "Slava Batov" with the hero title on first view. */
.nav__brand {
    display: flex;
    align-items: center;
    line-height: 1;
    color: inherit;
    opacity: 0;
    pointer-events: none;
    transition: opacity .35s var(--ease);
}
.nav.is-scrolled .nav__brand {
    opacity: 1;
    pointer-events: auto;
}
.nav__brand-name {
    font-family: var(--font-display);
    font-size: 24px;
    letter-spacing: 0.01em;
}

.nav__right {
    display: flex;
    align-items: center;
    gap: clamp(18px, 3vw, 36px);
}

.nav__links {
    display: flex;
    gap: 28px;
    font-size: 14px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    /* Reveal from the bottom-up in sync with the hero caption strip —
       same 3.6s / 2.0s-delay curve as .hero__bottom, so the nav settles
       in at the exact moment the caption does. */
    opacity: 0;
    transform: translateY(12px);
    animation: hero-text-in 3.6s var(--ease) forwards;
    animation-delay: 2.0s;
}
.nav__links a {
    color: inherit;
    opacity: 0.85;
    position: relative;
    padding-bottom: 2px;
    transition: opacity .2s var(--ease);
}
.nav__links a::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    height: 1px;
    width: 0;
    background: currentColor;
    transition: width .3s var(--ease);
}
.nav__links a:hover { opacity: 1; }
.nav__links a:hover::after { width: 100%; }

.nav__social {
    display: flex;
    align-items: center;
    gap: 14px;
    /* Reveal in sync with .hero__bottom and .nav__links — same 3.6s /
       2.0s-delay curve so the whole top strip and the hero caption
       settle into place at the exact same instant. */
    opacity: 0;
    transform: translateY(12px);
    animation: hero-text-in 3.6s var(--ease) forwards;
    animation-delay: 2.0s;
}
.nav__social a {
    color: inherit;
    opacity: 0.85;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    transition: opacity .2s var(--ease), background-color .2s var(--ease);
}
.nav__social a:hover {
    opacity: 1;
    background: rgba(255,255,255,0.1);
}
.nav.is-scrolled .nav__social a:hover {
    background: rgba(26,23,20,0.06);
}
/* Saatchi logo — full-color PNG flattened to match the mono SVGs next to it.
   brightness(0) wipes color to black; invert(1) flips to white when the nav
   is transparent over the dark hero. */
.nav__social-saatchi img {
    display: block;
    width: 18px;
    height: 18px;
    object-fit: contain;
    filter: brightness(0) invert(1);
    transition: filter .2s var(--ease);
}
.nav.is-scrolled .nav__social-saatchi img {
    filter: brightness(0);
}

@media (max-width: 720px) {
    .nav__right { gap: 14px; }
    .nav__links { gap: 16px; font-size: 12px; }
    .nav__social { gap: 8px; }
    .nav__social a { width: 28px; height: 28px; }
    .nav__brand-name { font-size: 20px; }
}
@media (max-width: 480px) {
    .nav__social { display: none; }
}

/* ---------- HERO (fullscreen fade-in) ---------- */

.hero {
    position: relative;
    width: 100%;
    height: 100vh;
    min-height: 560px;
    overflow: hidden;
    padding: 0;
    margin: 0;
    max-width: none;
    background: #0c0a09;
}
/* The actual image — fades in + scales from 1.04 to 1.00 over 5s on load.
   (Slower reveal reads as more deliberate/editorial, like a gallery
   monograph — gives the eye time to adjust from black to painting.)
   Decorative only: no click handler. */
.hero__bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    background-color: #0c0a09;
    background-size: cover;
    /* Pan/zoom vars set inline by Pages/Index.cshtml from SiteMeta.HeroCrop.
       Defaults (50% / 50% / 1) reproduce the old "cover, centered" framing. */
    background-position: var(--cp-x, 50%) var(--cp-y, 50%);
    background-repeat: no-repeat;
    opacity: 0;
    transform: scale(calc(var(--cp-zoom, 1) * 1.04));
    transform-origin: var(--cp-x, 50%) var(--cp-y, 50%);
    animation: hero-fade-in 5s var(--ease) forwards;
}
/* Gentle gradient top & bottom so overlay text always reads cleanly over
   whatever painting is featured. */
.hero::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(to bottom,
        rgba(0,0,0,0.42) 0%,
        rgba(0,0,0,0) 22%,
        rgba(0,0,0,0) 55%,
        rgba(0,0,0,0.55) 100%);
    opacity: 0;
    animation: hero-fade-in 5s var(--ease) forwards;
    animation-delay: 0.6s;
}

/* Name strip at the top-left of the hero.
   (Moved from centered → left so it doesn't compete with the caption at
   the bottom-center and so it reads like a magazine masthead.)
   NOTE: the `top` value below is the single knob that moves "Slava Batov"
   up/down. Lowering the clamp moves it higher on the page. Dropped another
   5mm (~19px) vs. prior pass, from clamp(45, 8vh, 100) → clamp(27, 6.5vh, 82). */
.hero__top {
    position: absolute;
    left: 0;
    right: auto;
    top: clamp(27px, 6.5vh, 82px);
    padding: 0 var(--pad);
    text-align: left;
    color: #fff;
    opacity: 0;
    transform: translateY(12px);
    animation: hero-text-in 3.6s var(--ease) forwards;
    animation-delay: 1.4s;
    pointer-events: none;
    z-index: 2;
}
/* Caption strip at the bottom (title · medium · Sold?). */
.hero__bottom {
    position: absolute;
    left: 0;
    right: 0;
    bottom: clamp(40px, 8vh, 90px);
    padding: 0 var(--pad);
    text-align: center;
    color: #fff;
    opacity: 0;
    transform: translateY(12px);
    animation: hero-text-in 3.6s var(--ease) forwards;
    animation-delay: 2.0s;
    pointer-events: none;
    z-index: 2;
}

.hero__title {
    font-family: var(--font-display);
    font-weight: 500;
    /* +20% on all three clamp stops (was 48/8vw/120 → 58/9.6vw/144). */
    font-size: clamp(58px, 9.6vw, 144px);
    line-height: 1;
    letter-spacing: -0.005em;
    margin: 0;
    color: #fff;
    text-shadow: 0 2px 24px rgba(0,0,0,0.45);
}
.hero__caption {
    font-family: var(--font-display);
    font-size: clamp(16px, 1.6vw, 22px);
    letter-spacing: 0.02em;
    color: rgba(255,255,255,0.94);
    margin: 0;
    text-shadow: 0 1px 12px rgba(0,0,0,0.5);
}
.hero__caption em { font-style: italic; }
.hero__caption .sep { margin: 0 10px; opacity: 0.55; }
.hero__sold {
    font-family: var(--font-body);
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    padding: 3px 10px;
    border: 1px solid rgba(255,255,255,0.6);
    border-radius: 999px;
    vertical-align: middle;
}

/* Empty-state fallback (no featured artwork configured). Centered dark text. */
.hero__title--dark {
    color: var(--ink);
    text-shadow: none;
}

/* Scroll-down hint — circle with a ↓ centered below the hero.
   Uses its own keyframe (`hero-scroll-in`) because the default
   `hero-text-in` keyframe only animates translateY, which would strip
   the translateX(-50%) centering — causing the badge to visibly slide
   in from the left edge. We want it to reveal bottom-to-top (matching
   every other hero element) while staying horizontally centered. */
.hero__scroll {
    position: absolute;
    bottom: 18px;
    left: 50%;
    transform: translate(-50%, 12px);
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.35);
    font-size: 18px;
    line-height: 1;
    opacity: 0;
    animation: hero-scroll-in 2.4s var(--ease) forwards;
    animation-delay: 2.8s;
    transition: background-color .2s var(--ease), transform .3s var(--ease);
    z-index: 3;
}
.hero__scroll:hover {
    background: rgba(255,255,255,0.2);
    transform: translate(-50%, 3px);
}

@keyframes hero-fade-in {
    /* End state honours the crop zoom so the fade-in settles on the artist's
       chosen framing rather than snapping back to scale(1). */
    to { opacity: 1; transform: scale(var(--cp-zoom, 1)); }
}
@keyframes hero-text-in {
    to { opacity: 1; transform: translateY(0); }
}
/* Scroll-arrow reveal that preserves the -50% X centering. */
@keyframes hero-scroll-in {
    to { opacity: 1; transform: translate(-50%, 0); }
}

@media (max-width: 540px) {
    .hero { min-height: 500px; }
    /* Mobile title: also bumped +20% (was 40/12vw/72 → 48/14.4vw/86). */
    .hero__title { font-size: clamp(48px, 14.4vw, 86px); }
    .hero__top { top: clamp(62px, 14vh, 112px); }
    .hero__caption .sep { display: block; margin: 4px 0; }
    .hero__sold { display: inline-block; margin-top: 6px; }
}

/* ---------- BUTTONS ---------- */

.btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 14px 22px;
    font-size: 13px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    border: 1px solid var(--ink);
    transition: all .25s var(--ease);
}
.btn--primary { background: var(--ink); color: var(--paper); }
.btn--primary:hover { background: var(--accent); border-color: var(--accent); }
.btn--ghost { background: transparent; color: var(--ink); }
.btn--ghost:hover { background: var(--ink); color: var(--paper); }

/* ---------- SECTIONS ---------- */

.section {
    padding: clamp(60px, 8vw, 120px) var(--pad);
    max-width: var(--max);
    margin: 0 auto;
}

.section__head {
    text-align: center;
    margin-bottom: clamp(40px, 6vw, 72px);
}
.section__title {
    font-family: var(--font-display);
    font-weight: 500;
    font-size: clamp(34px, 4.2vw, 54px);
    letter-spacing: -0.01em;
    margin: 0 0 12px;
}
.section__title::after {
    content: "";
    display: block;
    width: 40px;
    height: 1px;
    background: var(--accent);
    margin: 20px auto 0;
}
.section__sub {
    color: var(--ink-faint);
    margin: 0;
    font-family: var(--font-display);
    font-style: italic;
    font-size: clamp(18px, 1.6vw, 22px);
}

/* ---------- WORKS GRID ----------
   All tiles are the same size (no --wide pattern). The Works section fills
   75% of the viewport width, per the artist's request: "dynamic page
   scaling — constant 75% width fill." */

.works {
    /* Override the .section max-width cap so the grid truly scales to 75vw. */
    width: 75vw;
    max-width: none;
}

.works__grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: clamp(16px, 1.6vw, 28px);
}
.works__item {
    /* Now an <a href="/works/{slug}"> rather than a <button>. The anchor
       resets below neutralise the browser's default link styling so the
       caption <em> and meta spans keep their intended look, and the tile
       still behaves like the original clickable card (modal on left-click,
       navigation on middle-click / ctrl-click / right-click "open in new
       tab"). */
    display: block;
    color: inherit;
    text-decoration: none;
    padding: 0;
    position: relative;
    overflow: hidden;
    border-radius: 2px;
    box-shadow: var(--shadow-art);
    background: var(--ink);
    aspect-ratio: 4 / 3;
    transition: transform .5s var(--ease), box-shadow .5s var(--ease);
    opacity: 0;
    transform: translateY(18px);
    animation: fade-up .8s var(--ease) forwards;
    animation-delay: calc(var(--i, 0) * 60ms + 200ms);
}

.works__item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 1.2s var(--ease), filter .4s var(--ease);
}
.works__item:hover { box-shadow: var(--shadow-art-hover); transform: translateY(-3px); }
.works__item:hover img { transform: scale(1.035); }

/* Corner pill shown when an artwork has been sold. Positioned above the
   hover caption so it never gets covered by the gradient.
   Size + tracking match .hero__sold so the same label reads identically
   whether it's on the hero caption or the grid thumbnail; only the
   surround changes (border vs. tinted backdrop). */
.works__sold {
    position: absolute;
    top: 12px;
    left: 12px;
    z-index: 2;
    font-family: var(--font-body);
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: #fff;
    background: rgba(26, 23, 20, 0.78);
    padding: 3px 10px;
    border-radius: 999px;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}

.works__caption {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    padding: 40px 20px 18px;
    color: #fff;
    text-align: left;
    background: linear-gradient(to top, rgba(0,0,0,0.65), transparent);
    display: flex;
    flex-direction: column;
    gap: 2px;
    opacity: 0;
    transform: translateY(8px);
    transition: opacity .35s var(--ease), transform .35s var(--ease);
    pointer-events: none;
}
.works__item:hover .works__caption,
.works__item:focus-visible .works__caption {
    opacity: 1;
    transform: translateY(0);
}
.works__caption em {
    font-family: var(--font-display);
    font-style: italic;
    font-size: 20px;
    letter-spacing: 0.01em;
}
.works__meta {
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    opacity: 0.82;
}

@media (max-width: 900px) {
    .works { width: 88vw; }
    .works__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
    .works { width: 92vw; }
    .works__grid { grid-template-columns: 1fr; gap: 14px; }
    .works__caption { opacity: 1; transform: none; }
}

/* ---------- ABOUT ---------- */

.about {
    display: grid;
    grid-template-columns: 1fr 1.3fr;
    gap: clamp(28px, 5vw, 72px);
    align-items: center;
}
.about__photo {
    margin: 0;
    position: relative;
    padding: 14px;
    background: var(--paper-2);
    box-shadow: var(--shadow-art);
    /* Clip the cropper's scaled background so a zoomed portrait doesn't
       bleed past the paper frame. The ::before border stays inside the
       inset:14px area so it isn't affected. */
    overflow: hidden;
}
.about__photo img { width: 100%; height: auto; }
/* Background-image variant so the admin cropper's pan/zoom vars apply.
   The wrapping .about__photo sets the aspect/size; this element pans and
   zooms inside it. */
.about__photo-bg {
    width: 100%;
    aspect-ratio: 1 / 1;
    background-size: cover;
    background-repeat: no-repeat;
    background-position: var(--cp-x, 50%) var(--cp-y, 50%);
    transform: scale(var(--cp-zoom, 1));
    transform-origin: var(--cp-x, 50%) var(--cp-y, 50%);
    overflow: hidden;
}
.about__photo::before {
    content: "";
    position: absolute;
    inset: 14px;
    border: 1px solid rgba(26,23,20,0.15);
    pointer-events: none;
}
.about__text p {
    font-family: var(--font-display);
    font-size: clamp(20px, 1.55vw, 22px);
    line-height: 1.65;
    color: var(--ink-soft);
    margin: 20px 0 0;
}
.about__text .section__title::after { margin-left: 0; }
.about__text .section__title { text-align: left; }

@media (max-width: 780px) {
    .about { grid-template-columns: 1fr; }
    .about__text .section__title { text-align: center; }
    .about__text .section__title::after { margin: 20px auto 0; }
}

/* ---------- CONTACT FORM ---------- */

.contact { text-align: center; }

.contact-form {
    max-width: 680px;
    margin: 0 auto;
    text-align: left;
    background: var(--paper-2);
    padding: clamp(24px, 4vw, 40px);
    border: 1px solid var(--rule);
}
.contact-form .field-grid { margin-bottom: 0; }

/* Spam honeypot. Not `display:none` — some naive bots skip hidden inputs,
   but most fill every visible form control, including this one. We keep
   the input in the render tree, just move it off-screen and out of the
   tab order so no sighted keyboard user can reach it. Server-side
   handler rejects any submission where `website` is non-empty. */
.hp-field {
    position: absolute;
    left: -10000px;
    top: auto;
    width: 1px;
    height: 1px;
    overflow: hidden;
}
.contact-form input,
.contact-form textarea {
    font-size: 19px;                /* bumped so placeholders / typed text read easily */
}
.contact-form textarea {
    min-height: 140px;
    font-family: var(--font-body);
}
/* Per artist request — the Message field reads at 20px for both the
   placeholder hint and the typed text, so nothing shifts size once
   someone starts writing. The other inputs stay at the 19px base above. */
#contact-message {
    font-size: 20px;
}
#contact-message::placeholder {
    font-size: 20px;
    line-height: 1.2;
}
#contact-message::-webkit-input-placeholder { font-size: 20px; line-height: 1.2; }
#contact-message::-moz-placeholder          { font-size: 20px; line-height: 1.2; }
#contact-message:-ms-input-placeholder      { font-size: 20px; line-height: 1.2; }
.contact-form__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    margin-top: 8px;
}
.contact-form__hint {
    font-size: 18px;
    color: var(--ink-faint);
    font-family: var(--font-display);
    font-style: italic;
}
.contact-form__hint a {
    color: var(--accent);
    border-bottom: 1px solid rgba(154, 62, 45, 0.3);
    transition: border-color .2s var(--ease);
}
.contact-form__hint a:hover { border-bottom-color: var(--accent); }

.contact__flash {
    max-width: 680px;
    margin: 0 auto 20px;
    padding: 14px 18px;
    text-align: left;
    font-family: var(--font-display);
    font-style: italic;
    border-left: 3px solid var(--accent);
    background: var(--paper-2);
}
.contact__flash--ok  { border-left-color: var(--ok); }
.contact__flash--err { border-left-color: var(--accent); }

/* ---------- JS ERROR BANNER ----------
   Injected at the top of <body> by the global error handler in site.js /
   admin.js when an uncaught error or unhandled promise rejection fires.
   Sits at the top of the viewport (sticky — survives scroll), readable-but-
   unobtrusive, dismissable via the × button. The banner is rendered from
   JS rather than present in the DOM, so these styles only hit the page
   when the banner is actually shown; zero cost for normal visitors. */
#js-error-banner {
    position: sticky;
    top: 0;
    left: 0;
    right: 0;
    z-index: 9999;                 /* above the nav and the gallery modal */
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 16px;
    background: var(--danger-bg);
    color: var(--danger-ink);
    border-bottom: 1px solid var(--danger-ink-hover);
    font-family: var(--font-body);
    font-size: 15px;
    line-height: 1.4;
}
.js-error-banner__msg {
    flex: 1 1 auto;
}
.js-error-banner__close {
    flex: 0 0 auto;
    background: transparent;
    border: 0;
    color: inherit;
    font-size: 22px;
    line-height: 1;
    padding: 4px 8px;
    cursor: pointer;
    border-radius: 4px;
}
.js-error-banner__close:hover,
.js-error-banner__close:focus-visible {
    background: var(--danger-bg-hover);
    outline: none;
}

/* ---------- FOOTER ---------- */

.footer {
    border-top: 1px solid var(--rule);
    padding: 28px var(--pad);
    text-align: center;
}
.footer__inner {
    max-width: var(--max);
    margin: 0 auto;
    font-family: var(--font-display);
    font-size: 17px;
    letter-spacing: 0.04em;
    color: var(--ink-soft);
}
.footer__sep {
    margin: 0 10px;
    opacity: 0.5;
}
/* Small, unassuming admin entry point. Deliberately low-contrast so it
   reads as a utility link rather than part of the public nav. */
.footer__admin {
    color: var(--ink-soft);
    text-decoration: none;
    border-bottom: 1px solid transparent;
    transition: color .15s var(--ease), border-color .15s var(--ease);
}
.footer__admin:hover,
.footer__admin:focus-visible {
    color: var(--ink);
    border-bottom-color: var(--ink);
}

/* ---------- WORK DETAIL (per-painting /works/{slug}) ----------
   Standalone canonical page for an individual painting. Exists for SEO
   (each piece gets its own URL, title, description, OG image, JSON-LD
   block) but also functions as a readable public landing page for people
   arriving via search, a shared link, or a link unfurl.

   Design rhythm matches the rest of the site: ink-on-paper palette, Cormorant
   display + Inter body, generous vertical breathing. No modal machinery —
   just the painting, its placard, and a way back into the main site.

   Kept lean: ~70 lines, single responsive breakpoint at 720px. The rest of
   the page inherits from the global nav / footer / button / section rules. */

.nav--inner {
    /* The inner-page nav is solid-on-paper (no hero backdrop to sit on top
       of), so it needs a visible bottom rule to separate it from the content
       the same way the homepage nav blends into the hero fade. */
    background: var(--paper);
    border-bottom: 1px solid var(--rule);
}

.work-detail {
    max-width: var(--max);
    margin: 0 auto;
    padding: 28px var(--pad) 80px;
}

.work-detail__breadcrumb {
    font-family: var(--font-body);
    font-size: 12px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    color: var(--ink-soft);
    margin-bottom: 28px;
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}
.work-detail__breadcrumb a {
    color: var(--ink-soft);
    text-decoration: none;
    border-bottom: 1px solid transparent;
    transition: color .15s var(--ease), border-color .15s var(--ease);
}
.work-detail__breadcrumb a:hover,
.work-detail__breadcrumb a:focus-visible {
    color: var(--ink);
    border-bottom-color: var(--ink);
}

.work-detail__figure {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(260px, 1fr);
    gap: clamp(20px, 3vw, 48px);
    align-items: start;
    margin: 0;
}

.work-detail__image {
    width: 100%;
    height: auto;
    display: block;
    box-shadow: var(--shadow-art);
    background: var(--ink);
}

.work-detail__caption {
    /* Placard on the right of the painting: title, metadata table,
       description, call-to-action. Lines up visually with the top of the
       image so the reading column starts where the art does. */
    display: flex;
    flex-direction: column;
    gap: 20px;
}
.work-detail__title {
    font-family: var(--font-display);
    font-weight: 500;
    font-style: italic;
    font-size: clamp(32px, 3.6vw, 48px);
    line-height: 1.1;
    margin: 0;
    color: var(--ink);
}

.work-detail__meta {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 6px 18px;
    margin: 0;
    font-family: var(--font-body);
    font-size: 14px;
}
.work-detail__meta dt {
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink-soft);
    align-self: center;
}
.work-detail__meta dd {
    margin: 0;
    color: var(--ink);
}

.work-detail__description p {
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.65;
    color: var(--ink);
    margin: 0 0 12px 0;
}

.work-detail__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-top: 8px;
}
.work-detail__sold {
    /* Mirrors the hero sold badge so the visual vocabulary stays consistent
       across the site. */
    font-family: var(--font-body);
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink-soft);
    border: 1px solid var(--rule);
    padding: 10px 16px;
    border-radius: 2px;
    align-self: center;
}

.work-detail__pager {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-top: 64px;
    padding-top: 28px;
    border-top: 1px solid var(--rule);
}
.work-detail__pager-link {
    display: flex;
    flex-direction: column;
    gap: 4px;
    color: var(--ink);
    text-decoration: none;
    padding: 12px 0;
    transition: color .15s var(--ease);
}
.work-detail__pager-link--next { text-align: right; align-items: flex-end; }
.work-detail__pager-link:hover,
.work-detail__pager-link:focus-visible { color: var(--accent); }
.work-detail__pager-hint {
    font-family: var(--font-body);
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink-soft);
}
.work-detail__pager-title {
    font-family: var(--font-display);
    font-style: italic;
    font-size: 20px;
}

@media (max-width: 720px) {
    .work-detail__figure {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ---------- GALLERY MODAL ----------
   Layout goals (per artist feedback):
     - Title strip lives above the image in its own row so it NEVER overlaps
       the painting (previously the Saatchi link sat on top of the art).
     - prev/next buttons occupy a wide invisible click strip the full height
       of the image, with a small visible circle centered inside. Makes the
       click target huge without making the UI heavy.
     - close / prev / next are visually the same size & shape (64px circles).
     - Background click does NOT close — only × and Esc, so users can't
       accidentally dismiss while trying to read the painting.
*/
.gallery {
    position: fixed;
    inset: 0;
    z-index: 100;
    background: rgba(12, 10, 9, 0.96);
    display: none;
    flex-direction: column;
    opacity: 0;
    transition: opacity .35s var(--ease);
}
.gallery.is-open { display: flex; opacity: 1; }

/* ---- Header == museum-style "gallery placard" ----
   Fixed-width card (4.5in on desktop, ≈432 CSS px) floating as an absolute
   overlay in the bottom-right corner of the modal. Dark gray background
   with a very dim outline gives it the feel of an exhibition wall label.
   The image stage fills the entire modal underneath, so the painting uses
   the full viewport and the placard simply overlays the bottom-right.
   Width is capped at 92vw and the mobile media query stretches the card
   full-width (above the centered zoom controls) on phones. ---- */
.gallery__header {
    /* Floats as an absolute overlay in the bottom-right corner so the
       image stage can fill the entire modal. The image (stage) renders
       behind this placard — zooming in expands the image across the full
       viewport including the space behind the card.
       Uniform 5mm screen-edge inset — same mat as the painting, the ×,
       the nav dots, and the zoom controls. */
    position: absolute;
    right: 5mm;
    bottom: 5mm;
    z-index: 3;
    box-sizing: border-box;
    width: 4.5in;                     /* ≈ 432px at standard CSS DPI */
    max-width: 92vw;
    padding: 18px 22px;
    background: #292929;              /* 10% darker than previous #2E2E2E */
    border: 1px solid #2a2a2a;
    color: #F3EEE6;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    pointer-events: none;             /* children re-enable pointer as needed */
}
.gallery__title {
    font-family: var(--font-display);
    font-style: italic;
    font-size: clamp(20px, 2vw, 26px);
    line-height: 1.15;
}
.gallery__meta {
    font-family: var(--font-body);
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    opacity: 0.75;
}
/* Description paragraph — left-aligned prose reads more naturally than a
   centered block of body text. Quieter color keeps the title dominant. */
.gallery__description {
    margin: 4px 0 2px;
    font-family: var(--font-display);
    font-style: italic;
    font-size: 21px;                  /* bumped again per artist request */
    line-height: 1.5;
    color: rgba(243, 238, 230, 0.82);
    text-align: left;
    white-space: pre-wrap;            /* honor intentional paragraph breaks */
    letter-spacing: 0;
    text-transform: none;
}
/* Hide painting descriptions on mobile — both the homepage gallery modal
   and the per-painting detail page. The text still ships in the HTML and
   in the JSON-LD structured data, so SEO is unaffected; this only removes
   the visible block where screen real estate is tight. */
@media (max-width: 720px) {
    .gallery__description,
    .work-detail__description {
        display: none;
    }
}
.gallery__badges {
    display: flex;                    /* parent now uses align-items: stretch, */
    justify-content: center;          /* so we center the badges ourselves */
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin-top: 4px;
    min-height: 0;
}
.gallery__buy {
    pointer-events: auto;
    font-family: var(--font-body);
    font-size: 12px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    color: var(--accent-soft);
    padding: 6px 14px;
    border: 1px solid rgba(194, 106, 90, 0.5);
    border-radius: 999px;
    transition: all .2s var(--ease);
}
.gallery__buy:hover { color: #fff; background: var(--accent); border-color: var(--accent); }
/* Disabled-looking pill in light red. Mirrors the Saatchi pill shape but
   signals "not for sale" — no hover state, cursor stays default. */
.gallery__sold {
    font-family: var(--font-body);
    font-size: 12px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    color: rgba(230, 150, 140, 0.85);
    padding: 6px 14px;
    border: 1px solid rgba(230, 150, 140, 0.35);
    border-radius: 999px;
    background: rgba(230, 150, 140, 0.06);
    cursor: default;
    pointer-events: none;
}

/* ---- Stage (holds OSD viewer + splash image) ---- */
/* The stage is now a positioning context for two stacked layers:
     .gallery__osd   — the OpenSeadragon canvas (museum-grade deep zoom)
     .gallery__image — a plain <img> splash shown until OSD is ready,
                       then faded out. OSD fills the stage; the splash
                       sits on top until its opacity drops to 0.        */
.gallery__stage {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
    overflow: hidden;
    user-select: none;
    /* Per artist request — the painting is always rendered on top of the
       close / nav / placard UI, so the image visually dominates the screen.
       The zoom-controls bar is the exception: it sits ABOVE the image
       (z-index 11) because it auto-hides after idle anyway.
       Pointer-events: none on the stage (and its OSD canvas, via inheritance)
       lets clicks pass through the image to the button underneath — so the
       close / prev / next / Saatchi buttons stay clickable even though they
       render visually below the image. Wheel-to-zoom is forwarded to OSD
       from the gallery-level wheel handler in site.js (see that file). */
    z-index: 10;
    pointer-events: none;
}
/* OpenSeadragon injects its own canvas / container divs inside .gallery__osd
   and sets inline `pointer-events` on some of them, which beats normal
   cascade. Use `!important` on every descendant so clicks are guaranteed
   to pass through to the UI buttons underneath. */
.gallery__stage,
.gallery__stage *,
.gallery__osd,
.gallery__osd * {
    pointer-events: none !important;
}
/* When zoomed in past home, the image needs to receive mouse events so the
   user can drag to pan and wheel/click to zoom around a specific spot.
   The `.gallery--zoomed` class is toggled on the root `.gallery` element
   by the zoom handler in site.js, flipping the stage to interactive. */
.gallery--zoomed .gallery__stage,
.gallery--zoomed .gallery__stage *,
.gallery--zoomed .gallery__osd,
.gallery--zoomed .gallery__osd * {
    pointer-events: auto !important;
}
/* …and `auto !important` on every interactive UI element so none of them
   accidentally inherit `none` from a parent and become unclickable. This
   is the counterpart to the rule above — together they guarantee clicks
   pass through the image to these buttons. */
.gallery__close,
.gallery__close *,
.gallery__nav,
.gallery__nav *,
.gallery__controls,
.gallery__controls *,
.gallery__buy,
.gallery__sold {
    pointer-events: auto !important;
}

/* OSD canvas fills the whole stage; OSD manages its own cursor based
   on pan/zoom state. */
.gallery__osd {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    background: transparent;
}
.gallery__osd .openseadragon-canvas:focus { outline: none; }

/* Splash <img>: 3000px JPG shown instantly while the DZI tile pyramid
   is parsed. Centered via flex on a wrapping pseudo-layer through
   absolute positioning + translate. Faded out by JS once OSD's
   'open' event fires, at which point OSD takes over drawing. */
.gallery__image {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    max-width: 92vw;
    max-height: 100%;
    object-fit: contain;
    transition: opacity .35s var(--ease);
    pointer-events: none;           /* never intercept clicks from OSD */
    box-shadow: 0 40px 80px rgba(0,0,0,0.5);
    user-select: none;
    -webkit-user-drag: none;
}

/* ---- Shared look: close + nav-dot + zoom buttons ---- */
.gallery__close,
.gallery__nav-dot,
.gallery__zoom {
    color: #F3EEE6;
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.2);
    transition: background-color .2s var(--ease), border-color .2s var(--ease);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}

/* ---- Close (× top right) ----
   GALLERY BUTTON EDGE RULE: every edge-anchored button (close / prev-dot /
   next-dot) sits exactly 22px from its screen edge so they line up visually.
   Update this number here AND in .gallery__nav padding below if you change it. */
.gallery__close {
    position: absolute;
    /* Uniform 5mm screen-edge inset — matches placard, nav dots, and the
       painting's top/bottom mat. */
    top: 5mm;
    right: 5mm;
    width: 64px;
    height: 64px;
    border-radius: 50%;
    font-size: 30px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 4;
}
/* The × glyph sits slightly below its em-box center in most fonts, so it
   reads as off-center inside the perfectly-round button. Shift it up 2px. */
.gallery__close > span { transform: translateY(-2px); display: inline-block; }
.gallery__close:hover { background: rgba(255,255,255,0.18); }

/* ---- Nav strips: wide invisible click area, small visible dot ----
   The whole vertical strip is clickable. The visible circle is pinned to
   the outer edge with 22px of padding so it lines up with the close button
   (not centered in the strip — that made the prev/next sit ~13mm in while
   the close sat ~5mm in, and the asymmetry looked off). */
.gallery__nav {
    position: absolute;
    top: 100px;                     /* clear of header strip */
    bottom: 100px;                  /* clear of zoom controls */
    width: clamp(80px, 12vw, 160px);
    background: transparent;
    border: 0;
    /* Uniform 5mm screen-edge inset for the visible dot — matches the ×,
       the placard, and the painting's top/bottom mat. */
    padding: 0 5mm;
    display: flex;
    align-items: center;
    z-index: 3;
    cursor: pointer;
}
.gallery__nav--prev {
    left: 0;
    justify-content: flex-start;    /* dot pinned 22px from left edge */
}
.gallery__nav--next {
    right: 0;
    justify-content: flex-end;      /* dot pinned 22px from right edge */
}
.gallery__nav-dot {
    width: 64px;
    height: 64px;
    border-radius: 50%;
    font-size: 38px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding-bottom: 4px;             /* optical centering for the ‹ / › glyph */
}
.gallery__nav:hover .gallery__nav-dot {
    background: rgba(255,255,255,0.18);
    border-color: rgba(255,255,255,0.35);
}

/* ---- Suppress default focus ring on the custom gallery buttons ----
   After the user presses ArrowLeft/ArrowRight (or Tab) inside the modal,
   Chromium paints its default dashed focus ring around the full
   .gallery__nav strip (which is invisible otherwise). That ring stays
   visible after a subsequent mouse click on the same strip, which looked
   like an unwanted rectangular outline hanging on the screen edge.
   Each of these buttons has its own clear hover/active feedback (the dot
   brightens, the × highlights), so the default ring is redundant.
   For keyboard users we still show a subtle outline on :focus-visible so
   accessibility isn't lost. */
.gallery__close:focus,
.gallery__nav:focus,
.gallery__zoom:focus {
    outline: none;
}
.gallery__close:focus-visible,
.gallery__nav:focus-visible,
.gallery__zoom:focus-visible {
    outline: 2px solid rgba(243, 238, 230, 0.55);
    outline-offset: 3px;
}

/* ---- Clean-picture-viewing fade ----
   When the artist/viewer zooms in on a painting (any amount past the home
   zoom), the surrounding chrome (× close, ‹/› nav, and the entire placard
   header block) fades to transparent over 0.5s so the detail exploration
   experience is uncluttered. Zooming back out brings them back with the
   same timing. The .gallery--zoomed class is toggled by site.js in OSD's
   `zoom` handler. .gallery__controls is intentionally excluded — it has
   its own auto-hide and the artist wants zoom controls to stay available
   while zoomed in. */
.gallery__close,
.gallery__nav,
.gallery__header {
    transition:
        opacity .5s var(--ease),
        visibility .5s var(--ease),
        background-color .2s var(--ease),
        border-color .2s var(--ease);
}
.gallery--zoomed .gallery__close,
.gallery--zoomed .gallery__nav,
.gallery--zoomed .gallery__header {
    opacity: 0;
    visibility: hidden;
    /* Keep them non-interactive while faded so a stray click on a fully-
       transparent button doesn't trigger close/next/prev. */
    pointer-events: none !important;
}

/* ---- Zoom controls (bottom center) ----
   Shown on any mouse/touch/keyboard activity. Hidden after 5s of idleness
   via the .gallery__controls--idle class toggled in site.js. */
.gallery__controls {
    position: absolute;
    /* Bumped 5mm higher than the uniform edge inset so the slider floats
       clear of the painting's bottom mat instead of crowding it. */
    bottom: 10mm;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 10px;
    background: rgba(255,255,255,0.06);
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 999px;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    /* Controls sit ABOVE the image (stage is z:10) so the user can always
       reach the zoom buttons. They auto-hide after 5s idle anyway, so they
       don't obstruct the painting in practice. */
    z-index: 11;
    transition: opacity .35s var(--ease), transform .35s var(--ease);
}
.gallery__controls--idle {
    opacity: 0;
    transform: translate(-50%, 10px);
    pointer-events: none;
}
.gallery__zoom {
    width: 44px;
    height: 44px;
    border-radius: 999px;
    font-size: 22px;
    border: 0;
    background: transparent;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
}
.gallery__zoom:hover { background: rgba(255,255,255,0.14); }
.gallery__zoom:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; }

/* Slider — styled so it reads well on the dark modal. */
.gallery__zoom-slider {
    -webkit-appearance: none;
    appearance: none;
    width: clamp(160px, 22vw, 260px);
    height: 4px;
    border-radius: 999px;
    background: rgba(255,255,255,0.2);
    outline: none;
    margin: 0;
    cursor: pointer;
}
.gallery__zoom-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: #F3EEE6;
    border: 0;
    box-shadow: 0 1px 4px rgba(0,0,0,0.3);
    cursor: pointer;
    transition: transform .15s var(--ease);
}
.gallery__zoom-slider::-moz-range-thumb {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: #F3EEE6;
    border: 0;
    box-shadow: 0 1px 4px rgba(0,0,0,0.3);
    cursor: pointer;
}
.gallery__zoom-slider:active::-webkit-slider-thumb { transform: scale(1.15); }
.gallery__zoom-slider:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--accent-soft); outline-offset: 2px; }

@media (max-width: 640px) {
    /* On narrow phones the placard sits flush to the bottom edges with
       tighter padding — full-width minus small insets since 6in wouldn't
       fit. Stays above the centered zoom-controls bar (bottom: 12px). */
    .gallery__header {
        right: 12px;
        bottom: 68px;                 /* clear of the zoom controls below */
        left: 12px;
        width: auto;
        padding: 12px 14px;
    }
    .gallery__close { top: 12px; right: 12px; width: 48px; height: 48px; font-size: 26px; }
    .gallery__nav { top: 80px; bottom: 80px; width: 60px; padding: 0 12px; }
    .gallery__nav-dot { width: 48px; height: 48px; font-size: 28px; }
    .gallery__controls { bottom: 12px; }
    .gallery__zoom { width: 40px; height: 40px; font-size: 20px; }
    .gallery__zoom-slider { width: clamp(100px, 40vw, 180px); }
}

/* ---------- MOTION (scroll reveal) ---------- */

.reveal {
    opacity: 0;
    transform: translateY(22px);
    transition: opacity .9s var(--ease), transform .9s var(--ease);
    will-change: opacity, transform;
}
.reveal.is-visible {
    opacity: 1;
    transform: none;
}
.reveal--delay.is-visible { transition-delay: 0.15s; }

@keyframes fade-up {
    to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        transition-duration: .01ms !important;
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
    }
    .reveal { opacity: 1; transform: none; }
    .works__item { opacity: 1; transform: none; animation: none; }
    html { scroll-behavior: auto; }
}

/* ---------- ADMIN (carry site aesthetic into the back-end) ---------- */

.admin {
    /* 90vw per artist request — gives editing more breathing room than the
       public-site 75vw Works grid. The previous 1600px cap clamped down to
       ~62% of viewport on ~2500px-wide monitors, which looked identical to
       the old 75vw layout. Bumping the cap to 2400px keeps a sane maximum
       for truly enormous 4K+ displays while letting 90vw apply in practice
       on everything up to ~2660px wide. */
    width: 90vw;
    max-width: 2400px;
    margin: 0 auto;
    padding: clamp(28px, 5vw, 56px) var(--pad);
}
@media (max-width: 1100px) {
    .admin { width: 94vw; }
}
.admin__header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 28px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--rule);
    flex-wrap: wrap;
    gap: 12px;
}
.admin__header h1 {
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 36px;
    margin: 0;
}
.admin__header-actions {
    display: flex;
    gap: 10px;
    align-items: center;
    font-size: 12px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    color: var(--ink-soft);
}
/* Light pill buttons for the admin header — Logs / View site / Sign out.
   Low-chroma surface so they don't fight the "Portfolio Admin" wordmark
   next to them. Border uses the same rgba ink as the page's hairlines. */
.admin__header-btn {
    display: inline-flex;
    align-items: center;
    padding: 7px 14px;
    background: var(--paper-2);
    border: 1px solid var(--rule);
    border-radius: 999px;
    color: var(--ink-soft);
    text-decoration: none;
    line-height: 1;
    /* Uses the shared site easing so the pills' fade-in feels like part
       of the same family as .btn and .btn-small (previously drifted to
       120ms linear-ease). */
    transition: background .2s var(--ease),
                color .2s var(--ease),
                border-color .2s var(--ease);
}
.admin__header-btn:hover,
.admin__header-btn:focus-visible {
    background: var(--paper);
    color: var(--ink);
    border-color: rgba(26, 23, 20, 0.28);
}
.admin__header-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
/* Sign-out button — pale red surface so the destructive action reads
   distinctly from the neutral pills. Hover goes a shade deeper.
   All four colors are palette tokens (:root) so a future tweak (e.g.
   making the whole destructive tree lighter) is a one-line change. */
.admin__header-btn--danger {
    background: var(--danger-bg);
    border-color: rgba(154, 62, 45, 0.25);
    color: var(--danger-ink);
}
.admin__header-btn--danger:hover,
.admin__header-btn--danger:focus-visible {
    background: var(--danger-bg-hover);
    border-color: rgba(154, 62, 45, 0.5);
    color: var(--danger-ink-hover);
}
.admin__section {
    background: var(--paper-2);
    padding: clamp(20px, 3vw, 32px);
    margin-bottom: 24px;
    border: 1px solid var(--rule);
}
.admin__section h2 {
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 24px;
    margin: 0 0 20px;
}
.admin__hint {
    font-size: 13px;
    color: var(--ink-faint);
    margin-top: -12px;
    margin-bottom: 18px;
}

.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.field label {
    font-size: 11px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    color: var(--ink-faint);
}
.field input, .field textarea, .field select {
    font: inherit;
    padding: 12px 14px;
    border: 1px solid var(--rule);
    background: var(--paper);
    color: var(--ink);
    font-size: 16px;
    transition: border-color .2s var(--ease);
}
.field input:focus, .field textarea:focus, .field select:focus {
    outline: none;
    border-color: var(--ink);
}
.field textarea { min-height: 100px; resize: vertical; font-family: var(--font-display); }

.field-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
@media (max-width: 640px) { .field-grid { grid-template-columns: 1fr; } }

/* Four-up variant for the "Add a painting" top row (Title / Dimensions /
   Medium / Year). Collapses to 2 columns on tablets and 1 column on phones
   so the inputs stay wide enough to type into. */
.field-grid--4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) { .field-grid--4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px) { .field-grid--4 { grid-template-columns: 1fr; } }

/* Checkbox rows in admin forms (e.g. "Sold"). Uses label-wraps-input pattern
   so the whole row is clickable — kinder to older users on trackpads. */
.admin__checkbox {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 4px 0 16px;
    font-size: 14px;
    color: var(--ink-soft);
    cursor: pointer;
    user-select: none;
}
.admin__checkbox input[type="checkbox"] {
    width: 18px;
    height: 18px;
    margin: 0;
    accent-color: var(--ink);
    cursor: pointer;
}

/* Paintings grid — TWO-COLUMN CARD:
     Left column  : square thumbnail + "Replace image" + Up/Down/Save/Delete
                    action row (with the 1-indexed order number).
     Right column : title (with Sold checkbox right-aligned) + field grid
                    (title / dims / medium / year across the top row,
                    Description spanning the left half with Saatchi on the
                    right half of the next row).
   The card itself provides the outer padding so the thumbnail has breathing
   room on all four sides — it's the visual anchor of the card and used to
   be jammed against the border. */
.admin__artworks {
    display: grid;
    grid-template-columns: 1fr;
    gap: 18px;
    margin-top: 20px;
}
.admin__card {
    background: var(--paper);
    border: 1px solid var(--rule);
    display: grid;
    grid-template-columns: clamp(220px, 20%, 600px) 1fr;
    gap: 18px;
    padding: 18px;
    min-width: 0;
}

/* --- Left column (image / replace / actions) --- */
.admin__card-left {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
}
.admin__card-left img {
    /* Square thumbnail per artist request. Fills the left column width. */
    width: 100%;
    aspect-ratio: 1 / 1;
    height: auto;
    object-fit: cover;
    background: var(--ink);
    display: block;
}
/* Compact Replace-image input under the thumbnail. */
.admin__card-replace {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 0;
}
.admin__card-replace label {
    font-size: 10px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    color: var(--ink-faint);
}
.admin__card-replace input[type="file"] {
    font: inherit;
    font-size: 12px;
    padding: 6px 8px;
    border: 1px solid var(--rule);
    background: var(--paper);
    color: var(--ink);
    width: 100%;
    min-width: 0;
}
/* 1-indexed order number — prominent marker (22px) so the artist can see
   a painting's public-facing position at a glance without overwhelming the
   action row. */
.admin__card-order {
    /* Tracking stays tight (0.02em, not --track-ui) because this is a
       big numeric chip, not a small-caps label — wide tracking would
       break the digits apart visually. */
    font-family: var(--font-body);
    font-size: 22px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ink-soft);
    display: flex;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
/* Action row stretches its five children — order chip + Up + Down + Save +
   Delete — into equal fifths of the left column width. Each form wrapper
   becomes a flex container so the button inside fills the allocated slot
   rather than collapsing to its text width. */
.admin__card-actions {
    display: flex;
    gap: 4px;
    flex-wrap: nowrap;
    align-items: center;
    margin-top: 4px;
}
.admin__card-actions > .admin__card-order,
.admin__card-actions > form,
.admin__card-actions > button {
    flex: 1 1 0;
    min-width: 0;
}
.admin__card-actions > form {
    display: flex;
}
/* Buttons fill their slot (either the form wrapper or a direct flex cell)
   and center their text — so all four buttons + the order chip are the
   same visual width regardless of label length. */
.admin__card-actions .btn-small {
    width: 100%;
    padding: 6px 4px;
    font-size: 10px;
    letter-spacing: 0.08em;
    gap: 4px;
    justify-content: center;
    /* Keep each label ("↓ DOWN", "↑ UP", "SAVE", "DELETE") on a single
       line. At ~57px per slot, default word-wrap would break "↓ DOWN" at
       the space between the arrow and the word. */
    white-space: nowrap;
}

/* --- Right column (header + form) --- */
.admin__card-body {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
/* Title on the left, Sold checkbox hard right, same line. */
.admin__card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
}
.admin__card-sold {
    margin: 0;
    flex-shrink: 0;
}
.admin__card-title {
    font-family: var(--font-display);
    font-size: 18px;
    font-style: italic;
}

/* Per-card form uses CSS grid with named areas:
     title  dims    medium   year
     desc   desc    saatchi  saatchi
     desc   desc    .        .
     desc   desc    .        .
   Description dominates the bottom-left quadrant so the artist has real
   editing space, and the top-right cells (under Saatchi) are intentional
   blanks that give the card headroom without being cramped. */
.admin__card-form {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr;
    grid-template-areas:
        "title  dims    medium   year"
        "desc   desc    saatchi  saatchi"
        "desc   desc    .        ."
        "desc   desc    .        .";
    gap: 10px 14px;
}
.admin__card-form .field { margin: 0; }
.admin__card-form .field--title   { grid-area: title; }
.admin__card-form .field--dims    { grid-area: dims; }
.admin__card-form .field--medium  { grid-area: medium; }
.admin__card-form .field--year    { grid-area: year; }
.admin__card-form .field--desc    { grid-area: desc; display: flex; flex-direction: column; }
.admin__card-form .field--saatchi { grid-area: saatchi; }

/* Description textarea fills the whole 2×4 desc region, with a much larger
   font so the text — which tends to be the longest thing the artist edits
   — is comfortable to read. Was 14px; 20px is noticeably bigger. */
.admin__card-form .field--desc textarea {
    min-height: 160px;
    height: 100%;
    flex: 1;
    font-size: 20px;
    line-height: 1.45;
}

/* "Add a painting" Description textarea — matches the "Get in touch" Message
   box (and the edit-card description above): 20px for both the typed text
   and the placeholder hint, so nothing shifts size once writing starts.
   Height is 1.5× the global .field textarea baseline (100px → 150px) per
   artist request — the description is where the most writing happens. */
#add-painting textarea[name="description"] {
    font-size: 20px;
    line-height: 1.45;
    min-height: 150px;
}
#add-painting textarea[name="description"]::placeholder {
    font-size: 20px;
    line-height: 1.45;
}
#add-painting textarea[name="description"]::-webkit-input-placeholder { font-size: 20px; line-height: 1.45; }
#add-painting textarea[name="description"]::-moz-placeholder          { font-size: 20px; line-height: 1.45; }
#add-painting textarea[name="description"]:-ms-input-placeholder      { font-size: 20px; line-height: 1.45; }

/* Tighter padding / smaller labels for everything EXCEPT the description,
   which has its own font-size above. */
.admin__card-form .field input,
.admin__card-form .field select {
    padding: 8px 10px;
    font-size: 14px;
}
.admin__card-form .field label {
    font-size: 10px;
}

/* Stack image column above body on narrow viewports; keep the form's
   internal grid, but collapse to 2 columns so fields stay readable. */
@media (max-width: 900px) {
    .admin__card { grid-template-columns: 1fr; }
    .admin__card-form {
        grid-template-columns: 1fr 1fr;
        grid-template-areas:
            "title   title"
            "dims    medium"
            "year    saatchi"
            "desc    desc";
    }
}
@media (max-width: 560px) {
    .admin__card-form {
        grid-template-columns: 1fr;
        grid-template-areas:
            "title"
            "dims"
            "medium"
            "year"
            "desc"
            "saatchi";
    }
}

/* Header row for the Paintings section — heading on the left, bulk action
   button on the right. */
.admin__section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
}
.admin__section-head h2 { margin: 0; }

/* Tile-pyramid status pill + rebuild button inside each artwork card. */
.admin__tiles {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px dashed var(--rule);
    flex-wrap: wrap;
}
.admin__tiles-status {
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid var(--rule);
    background: var(--paper);
}
.admin__tiles-status--ok {
    color: var(--ok);
    border-color: var(--ok-soft);
    background: rgba(45, 106, 62, 0.08);
}
.admin__tiles-status--missing {
    color: var(--ink-faint);
    border-color: rgba(26, 23, 20, 0.18);
    background: transparent;
}

.btn-small {
    padding: 8px 12px;
    font-size: 11px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    border: 1px solid var(--ink);
    background: transparent;
    cursor: pointer;
    transition: all .2s var(--ease);
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.btn-small:hover { background: var(--ink); color: var(--paper); }
.btn-small:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-small:disabled:hover { background: transparent; color: var(--ink); }
.btn-small--danger { border-color: var(--accent); color: var(--accent); }
.btn-small--danger:hover { background: var(--accent); color: var(--paper); }

.admin__login {
    max-width: 380px;
    margin: clamp(60px, 12vw, 120px) auto;
    padding: 36px;
    background: var(--paper-2);
    border: 1px solid var(--rule);
}
.admin__login h1 {
    /* Match .admin__header h1 (36px) so the admin "wordmark" is one
       consistent headline across login and dashboard. */
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 36px;
    margin: 0 0 6px;
    text-align: center;
}
.admin__login p {
    text-align: center;
    color: var(--ink-faint);
    margin: 0 0 24px;
    font-size: 14px;
}
.admin__login button { width: 100%; justify-content: center; }
.admin__login .error {
    color: var(--accent);
    font-size: 14px;
    text-align: center;
    margin-top: 10px;
}

/* ============================================================
   Admin — hero/author library + cropper
   ============================================================ */

/* Subsection heading inside the Hero / Author sections so the
   Library / Upload / Framing blocks have visual hierarchy without
   resorting to another <h2> (which would compete with the section
   header). */
.admin__subhead {
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 18px;
    margin: 24px 0 10px;
    color: var(--ink-soft);
}

/* Row of previously-uploaded images with Select / Delete under each. */
.admin__library {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 12px;
    margin: 8px 0 4px;
}
.admin__library-item {
    background: var(--paper);
    border: 1px solid var(--rule);
    padding: 8px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.admin__library-item--active {
    border-color: var(--ink);
    box-shadow: 0 0 0 2px rgba(26, 23, 20, 0.08);
}
.admin__library-item img {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    background: var(--ink);
}
.admin__library-actions {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    justify-content: center;
}
.admin__library-actions .btn-small {
    flex: 1;
    justify-content: center;
    padding: 6px 8px;
    font-size: 10px;
}

/* Inline "pick a file + Upload" row used under the library. */
.admin__inline-upload {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 8px;
}
.admin__inline-upload input[type="file"] {
    flex: 1;
    min-width: 220px;
    padding: 10px 12px;
    border: 1px solid var(--rule);
    background: var(--paper);
    font: inherit;
    color: var(--ink);
}

/* ----- Cropper ----- */
/* Facebook-style pan+zoom preview. The viewport is a fixed-aspect
   window; the .cropper__image inside fills the viewport at "cover"
   and is panned/zoomed by CSS vars the JS keeps in sync. */
.cropper {
    margin: 10px 0 18px;
    max-width: 720px;
}
/* Hero cropper gets more room so the preview matches the real hero's
   visual footprint (and so the viewport aspect applied by JS has enough
   horizontal canvas to express a wide 16:9-ish ratio). */
.cropper--hero { max-width: 960px; }

/* Doubled About-text textarea per artist request — taller box AND larger
   font. The global .field textarea sets 100px × 16px; 200px × 20px gives
   room to edit a short bio comfortably without the serif body type
   looking cramped. */
#site-info textarea[name="aboutText"] {
    min-height: 200px;
    font-size: 20px;
    line-height: 1.45;
}
.cropper__viewport {
    position: relative;
    width: 100%;
    aspect-ratio: var(--cropper-aspect, 16 / 9);
    background: var(--ink);
    overflow: hidden;
    border: 1px solid var(--rule);
    touch-action: none;                     /* keep pan dragging precise on trackpads/touch */
    cursor: grab;
    user-select: none;
}
.cropper--author .cropper__viewport { aspect-ratio: 1 / 1; max-width: 320px; }
.cropper__viewport:active { cursor: grabbing; }
.cropper__image {
    position: absolute;
    inset: 0;
    background-size: cover;
    background-repeat: no-repeat;
    background-position: var(--cp-x, 50%) var(--cp-y, 50%);
    transform: scale(var(--cp-zoom, 1));
    transform-origin: var(--cp-x, 50%) var(--cp-y, 50%);
    will-change: transform, background-position;
}
.cropper__controls {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 10px;
    flex-wrap: wrap;
}
.cropper__controls label {
    font-size: 11px;
    letter-spacing: var(--track-ui);
    text-transform: uppercase;
    color: var(--ink-faint);
}
.cropper__zoom {
    flex: 1;
    min-width: 180px;
    accent-color: var(--ink);
}

/* ============================================================
   Admin — upload progress overlay
   ============================================================
   Full-screen veil shown while an XHR-driven upload is running so the
   artist gets a clear "something is happening, don't click away" cue.
   - Centered spinner + percentage read-out + determinate bar
   - Dismisses itself when JS flips `.is-open` off on load/abort/error
*/
.upload-overlay {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(12, 10, 9, 0.82);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    color: #F3EEE6;
}
.upload-overlay.is-open { display: flex; }

.upload-overlay__card {
    background: rgba(26, 23, 20, 0.6);
    border: 1px solid rgba(255,255,255,0.2);
    padding: 36px 44px;
    border-radius: 6px;
    min-width: min(420px, 82vw);
    text-align: center;
    font-family: var(--font-body);
    box-shadow: 0 40px 80px rgba(0,0,0,0.45);
}
.upload-overlay__spinner {
    width: 56px;
    height: 56px;
    margin: 0 auto 18px;
    border-radius: 50%;
    border: 4px solid rgba(255,255,255,0.18);
    border-top-color: #F3EEE6;
    animation: upload-spin 0.9s linear infinite;
}
@keyframes upload-spin { to { transform: rotate(360deg); } }
.upload-overlay__title {
    font-family: var(--font-display);
    font-size: 22px;
    margin: 0 0 4px;
    letter-spacing: 0.01em;
}
.upload-overlay__sub {
    font-size: 12px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    opacity: 0.75;
    margin: 0 0 18px;
}
.upload-overlay__bar {
    width: 100%;
    height: 6px;
    border-radius: 999px;
    background: rgba(255,255,255,0.15);
    overflow: hidden;
    margin: 0 auto 10px;
}
.upload-overlay__bar-fill {
    width: 0%;
    height: 100%;
    background: linear-gradient(90deg, var(--accent-soft), #F3EEE6);
    transition: width .15s linear;
}
.upload-overlay__pct {
    font-size: 13px;
    letter-spacing: var(--track-ui);
    opacity: 0.85;
}

/* Delete-mode variant — reused by data-progress="delete" forms.
   No measurable progress (it's a server-side filesystem op), so we
   hide the percent read-out and swap the determinate fill for an
   indeterminate slider animation. Title text ("Deleting…") is swapped
   in JS. */
.upload-overlay--delete .upload-overlay__pct {
    display: none;
}
.upload-overlay--delete .upload-overlay__bar-fill {
    width: 30%;
    transition: none;
    animation: upload-indeterminate 1.4s ease-in-out infinite;
}
@keyframes upload-indeterminate {
    0%   { margin-left: -30%; }
    100% { margin-left: 100%; }
}
