/* wissotaart.com — design system.
   Warm gallery wall, quiet chrome, paintings carry all the colour. Accent is a hairline and a
   link colour, never a large surface: a saturated field next to a painting competes with it.
   No dark mode by decision (SPEC.md) — near-black chrome measurably distorts perceived colour
   of the bright florals and marine work, which is why real galleries paint walls off-white. */

/* Newsreader, SIL OFL 1.1 — see fonts/newsreader-LICENSE.txt. Self-hosted from the
   @fontsource-variable/newsreader npm package (lockfile-pinned, not a CDN pull), latin subset,
   weight axis only: the optical-size axis costs 129 KB and nothing here sets it. Roman only —
   the serif is used for headings, the brand and prices, none of which are italic.
   font-display:swap + the preload in base.njk means Georgia paints first and never blocks. */
@font-face{
  font-family:"Newsreader";
  src:url("/assets/fonts/newsreader-var.woff2") format("woff2-variations");
  font-weight:200 800;
  font-style:normal;
  font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* Fragment Mono, SIL OFL 1.1 — see fonts/fragment-mono-LICENSE.txt. Self-hosted from the
   @fontsource/fragment-mono npm package, latin subset, one weight, roman only: 24 KB.
   It is Helvetica-derived rather than a coding face, which is exactly the voice of a printed
   museum wall label — and that is its whole job here. Every piece of METADATA on this site is
   set in it (eyebrows, mediums, dimensions, prices, dates, badges, catalogue numbers), so that
   the facts about a painting read as catalogued rather than as small grey text. Prose is never
   set in it. */
@font-face{
  font-family:"Fragment Mono";
  src:url("/assets/fonts/fragment-mono-400.woff2") format("woff2");
  font-weight:400;
  font-style:normal;
  font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

:root{
  color-scheme:dark;

  /* ==========================================================================================
     GALLERY NIGHT.
     The site is a dark room and the paintings are the lit objects in it. Chrome is charcoal,
     every canvas floats in a halo of its own pigment, and section headlines are set in the
     colour of the painting the section is about — colour lives in the TYPE, not in buttons or
     panels. The previous cream-and-hairline scheme is gone; it read as a template, which is
     precisely what it was.

     Three grounds, so depth is a token rather than an improvisation:
       --ink0  the page
       --ink1  raised surfaces — cards, footer, form inputs
       --ink2  the artwork mount. Deliberately a NEUTRAL warm grey rather than black: a canvas
               on pure black reads with distorted colour, which is why real galleries light work
               against a grey wall. This is the mitigation for SPEC's colour-fidelity objection.

     Every value here that carries text is measured, and scripts/check.mjs RECOMPUTES all of it
     on every build (TOKEN_PAIRS) — a token edit that breaks contrast now fails the build rather
     than shipping. Measured on ink0 / ink1 / ink2:
       --text    #f4f1ec  16.71 / 15.52 / 14.00
       --muted   #b3aca2   8.37 /  7.78 /  7.02
       --accent  #7fc6cf   9.76 /  9.06 /  8.18   Gulf teal, lightened for dark
       --accent2 #e59a90   8.39 /  7.79 /  7.03   hibiscus, lightened for dark
       --sold    #a59c91   6.96 /  6.47 /  5.83   (badge stays #6f685f, white on it 5.50)
       --ring    #ffc46b  11.98 / 11.13 / 10.04   the old ochre ring was invisible on charcoal
       --ok      #7fd39a  10.48   --err #f2a099  9.20
     And the two inverted pairs: --ink0 on --text 16.71 (button fill), --ink0 on --accent2 8.39
     (commission badge). White on --accent-deep #0f474d is 10.35 (announcement bar). */
  --ink0:#131110; --ink1:#1c1917; --ink2:#26221f;
  --text:#f4f1ec; --muted:#b3aca2;
  --line:rgba(244,241,236,.14);
  --accent:#7fc6cf; --accent2:#e59a90; --sold:#a59c91; --ring:#ffc46b;
  --ok:#7fd39a; --err:#f2a099; --accent-deep:#0f474d;
  --maxw:1180px;

  /* Per-piece pigment, overridden by the generated /assets/art.css. These fallbacks are what a
     piece with no extracted colour gets, so a newly added painting is chip-less rather than
     broken. --pen is the only one of the three ever allowed to carry text; it is now derived by
     LIGHTENING toward AA against the dark grounds, the reverse of the cream site. */
  --chip:var(--line); --wash:var(--ink1); --pen:var(--muted);
  /* The halo. Not stored per piece — computed from the chip, so it costs no data. */
  --glow:color-mix(in srgb, var(--chip) 45%, transparent);

  /* Three faces. Serif carries the display voice, the system stack carries body copy, and the
     mono is the museum wall label — it is what makes metadata read as catalogued rather than as
     small text. --font-serif and --font-mono are the single swap points for the two webfonts. */
  --font-serif:"Newsreader",Georgia,"Iowan Old Style","Times New Roman",serif;
  --font-ui:-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-mono:"Fragment Mono",ui-monospace,"SFMono-Regular","Cascadia Mono","Consolas",monospace;

  /* Fluid scale, 360px → 1180px viewport. Deliberately wide-ranging at the top end: display
     type is most of what separates a gallery site from a small-business template, and the old
     top step topped out at 3.11rem, which reads as a heading rather than as a statement.
     --step-5 exists for the one typographic statement per page and nothing else. */
  --step--1:clamp(.78rem,.76rem + .1vw,.84rem);
  --step-0:clamp(1rem,.97rem + .16vw,1.09rem);
  --step-1:clamp(1.19rem,1.11rem + .38vw,1.42rem);
  --step-2:clamp(1.42rem,1.27rem + .74vw,1.85rem);
  --step-3:clamp(1.75rem,1.4rem + 1.7vw,2.75rem);
  --step-4:clamp(2.4rem,1.5rem + 4.2vw,4.75rem);
  --step-5:clamp(2.9rem,1.5rem + 6.2vw,6.5rem);

  /* Spacing. There was no scale before this — every gap was a literal, so nothing was in
     rhythm with anything else. The top three steps are fluid because section air is what
     separates a gallery from a brochure at desktop width. */
  --space-1:.25rem; --space-2:.5rem; --space-3:.75rem; --space-4:1rem;
  --space-5:1.5rem; --space-6:2rem;
  --space-7:clamp(2.5rem,1.8rem + 2.6vw,4rem);
  --space-8:clamp(3.5rem,2.2rem + 4.4vw,6rem);
  --space-9:clamp(4.5rem,2.5rem + 7vw,8.5rem);

  /* Motion. Slow, long easing reads as a gallery; 150ms snap reads as a SaaS dashboard. The
     budget: micro-interactions at --dur-fast, reveals and morphs at --dur-slow, nothing loops. */
  --ease-out:cubic-bezier(.16,1,.3,1);
  --dur-fast:150ms; --dur-med:400ms; --dur-slow:800ms;
  --radius:8px;
}

*{box-sizing:border-box}
/* The background belongs on <html>, not just <body>. Without it the browser paints its own
   white canvas for the instant between documents, so every navigation on a dark site flashes
   white — the single most common way a dark theme is given away as an afterthought. */
html{scroll-behavior:smooth;background:var(--ink0)}
body{margin:0;background:var(--ink0);color:var(--text);font:var(--step-0)/1.65 var(--font-ui);-webkit-text-size-adjust:100%}
h1,h2,h3,.brand,.price,.fbig{font-family:var(--font-serif);font-weight:600;line-height:1.15}
/* Display headings get a tighter line and slight negative tracking. At 4.75rem the default
   1.15 leading and 0 tracking read as loose and cheap; this is what makes large serif type
   look set rather than merely enlarged. */
h1{font-size:var(--step-4);margin:0 0 .35em;line-height:1.02;letter-spacing:-.02em;font-weight:500;text-wrap:balance}
h2{line-height:1.08;letter-spacing:-.01em;text-wrap:balance}
h2{font-size:var(--step-3);margin:0 0 .5em}
h3{font-size:var(--step-1);margin:0 0 .3em}
img{max-width:100%;display:block}
a{color:var(--accent);text-underline-offset:2px}

.wrap{max-width:var(--maxw);margin-inline:auto;padding-inline:20px}
.section{padding-block:clamp(2.5rem,1.5rem + 4vw,4.5rem)}
.section.alt{background:var(--ink1);border-block:1px solid var(--line)}
.lede{font-size:var(--step-1);color:var(--muted);max-width:60ch;margin:0 0 1.5em}
.prose{max-width:66ch}
.prose p{margin:0 0 1em}
/* The eyebrow is the wall-label line. Mono, so it sits apart from the prose it introduces;
   tracking is lower than the old 600-weight sans because a monospace face is already wide. */
.eyebrow{font:400 var(--step--1)/1.3 var(--font-mono);letter-spacing:.13em;text-transform:uppercase;color:var(--accent2);margin:0 0 .9em}

/* Accessibility. The skip link is visible on focus, not merely present. */
.skip{position:absolute;left:-9999px;top:0;z-index:100}
.skip:focus{left:0;padding:12px 20px;background:var(--text);color:var(--ink0);text-decoration:none}
/* Amber, not the accent. A focus ring has to be visible against every ground on the site, and
   the accents are mid-tone — the old ochre ring simply disappeared into charcoal. 11.98:1. */
:focus-visible{outline:2px solid var(--ring);outline-offset:2px;border-radius:2px}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,::before,::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important}
}

/* THE PAINT STRIP. Every painting as one sliver of its own pigment. See paint-strip.njk for
   why the site-wide version carries no links. */
.strip{display:flex;width:100%;line-height:0}
.strip>i,.strip>a{flex:1 1 0;min-width:1px;background:var(--chip);display:block}
.strip--head{height:4px}
.strip--foot{height:6px}
/* The interactive one on /works/. A sliver grows on hover or keyboard focus and names itself —
   content:attr() reads an attribute already in the markup, so there is no inline style and
   nothing for the CSP to block. */
.strip--index{height:76px;border-radius:2px;overflow:visible;position:relative;margin:var(--space-6) 0 var(--space-7)}
.strip--index>a{position:relative;transition:flex-grow var(--dur-med) var(--ease-out),transform var(--dur-fast) var(--ease-out)}
.strip--index>a:hover,.strip--index>a:focus-visible{flex-grow:7;z-index:2}
.strip--index>a:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}
.strip--index>a::after{content:attr(aria-label);position:absolute;left:50%;bottom:calc(100% + 10px);
  transform:translateX(-50%);white-space:nowrap;background:var(--ink1);color:var(--text);
  font:400 var(--step--1)/1 var(--font-mono);padding:7px 10px;border-radius:4px;
  border:1px solid var(--line);opacity:0;pointer-events:none;transition:opacity var(--dur-fast)}
.strip--index>a:hover::after,.strip--index>a:focus-visible::after{opacity:1}
@media (prefers-reduced-motion:reduce){
  .strip--index>a,.strip--index>a::after{transition:none}
}
.skip--strip{top:auto}

/* Announcement bar. Above the sticky header and NOT sticky itself — it scrolls away, because a
   permanent band eating vertical space on every screen is what makes a promo feel like an ad.
   Deep accent field with white text (7.42:1); this is the one place a saturated surface is
   allowed, because it sits above the chrome rather than beside a painting. */
.announce{background:var(--accent-deep);color:#fff;display:flex;align-items:center;gap:12px;
  padding:9px clamp(16px,3vw,28px)}
.announce[hidden]{display:none}
.announce p{margin:0 auto 0 0;font:400 var(--step--1)/1.45 var(--font-mono);letter-spacing:.02em}
.announce a{color:#fff;text-underline-offset:3px}
.announce__x{flex:none;background:none;border:0;color:#fff;font-size:1.3rem;line-height:1;
  cursor:pointer;padding:0 4px;border-radius:3px}
.announce__x[hidden]{display:none}
.announce__x:hover{opacity:.75}
.announce__x:focus-visible{outline:2px solid #fff;outline-offset:2px}

/* Header — sticky, translucent. Nav is a plain wrapping row; at 8 links it needs no burger,
   and a burger would cost JS the rest of this site does not need. */
.site-header{position:sticky;top:0;z-index:30;background:color-mix(in srgb,var(--ink0) 88%,transparent);border-bottom:1px solid var(--line);backdrop-filter:saturate(160%) blur(10px)}
.header-inner{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:4px 24px;padding-block:12px}
.brand{font-size:1.22rem;color:var(--text);text-decoration:none;line-height:1.1;letter-spacing:.01em;font-weight:600}
.brand span{display:block;font:400 .64rem/1.4 var(--font-ui);letter-spacing:.2em;text-transform:uppercase;color:var(--muted)}
/* Quieter nav: small, wide-tracked, uppercase. Chrome should recede so the paintings are the
   loudest thing on the page — at .94rem sentence case it competed with the work. */
.site-nav{display:flex;flex-wrap:wrap;gap:4px 26px}
.site-nav a{color:var(--muted);text-decoration:none;font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;padding-block:6px;border-bottom:1px solid transparent}
/* Below 640px the seven links are ~575px of text against ~320px of usable width, so they wrap to
   a second and third line and the STICKY header eats 100-115px of a 640px-tall phone screen,
   permanently. One swipeable row fixes that without a burger, and therefore without the JS this
   site otherwise does not need. It must stay a real scroll container — not overflow:hidden with a
   transformed track — or it stops being keyboard-scrollable and drops out of the a11y tree. */
@media (max-width:640px){
  .header-inner{flex-wrap:nowrap;gap:12px}
  /* min-width:0 is load-bearing, not tidying. A flex item defaults to min-width:auto, i.e. its
     CONTENT width — so the scrolling nav demanded the full width of all eight links and crushed
     the brand into three wrapped lines, making the sticky header ~140px tall on a 390px screen.
     overflow-x:auto cannot shrink the item on its own; this is what lets it. */
  .site-nav{flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x proximity;gap:16px;min-width:0;
    scrollbar-width:none;-webkit-overflow-scrolling:touch;
    /* Fade the right edge so it reads as "more this way" rather than "that is all of it". */
    mask-image:linear-gradient(to right,#000 calc(100% - 20px),transparent)}
  .site-nav::-webkit-scrollbar{display:none}
  .site-nav a{white-space:nowrap;scroll-snap-align:end}
  .brand{font-size:1.05rem;white-space:nowrap}
  .brand span{white-space:nowrap}
}
.site-nav a:hover{color:var(--accent)}
.site-nav a[aria-current="page"]{border-bottom-color:var(--accent2);color:var(--accent)}

/* Buttons */
/* Pure contrast rather than a coloured fill — the Kordansky move. On a dark site the loudest
   available button is simply light-on-dark, and it leaves every saturated colour on the page
   belonging to the paintings. 16.71:1. */
.btn{display:inline-block;background:var(--text);color:var(--ink0);border:1px solid var(--text);padding:12px 22px;border-radius:var(--radius);font:inherit;font-size:.95rem;font-weight:600;text-decoration:none;cursor:pointer;transition:background var(--dur-fast),color var(--dur-fast),border-color var(--dur-fast)}
.btn:hover{background:#fff;border-color:#fff}
.btn-outline{background:transparent;color:var(--text);border-color:rgba(244,241,236,.42);font-weight:400}
.btn-outline:hover{background:var(--text);color:var(--ink0);border-color:var(--text)}
/* The whole site is on dark now, so these modifiers are no-ops kept so the templates that use
   them need no edit. Do not add new ones. */
.btn--on-dark,.btn-outline--on-dark{}
.btn-sm{padding:8px 14px;font-size:.85rem}
.btn-row{display:flex;flex-wrap:wrap;gap:12px;margin-top:1.5em}

/* Art grid. auto-fill so a category with three pieces does not stretch them to full width.
   Tiles preserve each painting's real aspect-ratio — object-fit:cover would crop the work,
   which is not an acceptable thing to do to an artist's portfolio. */
/* align-items:start is load-bearing. Grid items stretch to the tallest card in their row by
   default, and .art-card figcaption{flex:1} absorbs the slack — so with aspect ratios spanning
   0.33 to 3.69 in this collection, a panorama sitting beside a tall vertical grows an inch of
   empty caption. Letting each card size to its own content gives ragged bottoms, which is how a
   salon hang looks anyway, and costs nothing. */
/* Larger tiles and much more air: three across at full width instead of four, so each painting
   is ~370px rather than 267px. The work is the content; it should not be thumbnail-sized. */
.art-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(330px,100%),1fr));gap:clamp(20px,3.2vw,44px);margin-top:var(--space-6);padding:0;list-style:none;align-items:start;
  /* Long grids only. Skips rendering work for off-screen rows; contain-intrinsic-size is not
     optional with it — without a placeholder height the scrollbar jumps and in-page find
     breaks. The value is one average card: a ~355px column at the collection's median ratio,
     plus caption. */
  content-visibility:auto;contain-intrinsic-size:auto 420px}
/* Panoramas do NOT span two columns, and this is a decision rather than an omission — it was
   built, measured and reverted. A span-2 tile that reaches the last column cannot fit, wraps to
   the next row, and leaves the cell behind it EMPTY: with the real data, beach-boats knocked a
   hole in row one of /gallery/. Closing that hole needs grid-auto-flow:dense, which back-fills
   from later in the DOM and so decouples the visual order from the focus order for every tile
   on the page. Four pieces out of eighty-nine are wide enough to benefit. A ragged bottom edge
   is a salon hang; a hole in the middle of the grid just reads as broken.
   data-ratio="wide" is still emitted — it costs nothing and the detail page uses the same idea
   to give panoramas their own full-width treatment, where nothing can wrap. */
/* The pigment chip. Decorative square of the painting's own dominant colour, set from
   /assets/art.css via [data-piece]. Falls back to --line when a piece has no extracted entry. */
/* The keel hairline is load-bearing on dark: a piece whose dominant pigment is a deep brown or
   near-black would otherwise dissolve into the page and read as a missing chip. */
.chip{display:inline-block;width:.62em;height:.62em;margin-right:.5em;border-radius:2px;background:var(--chip);vertical-align:baseline;flex:none;box-shadow:0 0 0 1px rgba(244,241,236,.2)}
/* No card chrome. The border and filled panel framed every painting in a box and made the page
   read as a product grid; on a gallery wall the work sits against the wall itself. */
.art-card{margin:0;display:flex;flex-direction:column}
/* THE PIGMENT GLOW. Each canvas floats in a halo of its own dominant colour, so the grid reads
   as lit work on a dark wall rather than as tiles on a page. The first box-shadow is a plain
   dark fallback for anything without color-mix; the second overrides it where supported.
   Transform and shadow only — nothing here touches opacity, so no text ever crosses the
   contrast floor mid-animation. */
.art-card__link{display:block;background:var(--ink2);border-radius:2px;overflow:hidden;
  box-shadow:0 12px 34px -18px rgba(0,0,0,.85);
  box-shadow:0 12px 34px -16px var(--glow), 0 0 0 1px rgba(244,241,236,.07);
  transition:transform var(--dur-med) var(--ease-out),box-shadow var(--dur-med) var(--ease-out)}
.art-card__link:hover{transform:translateY(-5px);
  box-shadow:0 22px 60px -14px color-mix(in srgb,var(--chip) 68%,transparent), 0 0 0 1px rgba(244,241,236,.14)}
.art-card img{width:100%;height:auto;background:var(--ink2)}
.art-card figcaption{padding:14px 2px 0;display:flex;flex-direction:column;gap:3px;flex:1}
.art-card .t{font-family:var(--font-serif);font-size:1.18rem;line-height:1.25}
/* Medium and dimensions are catalogue facts, not prose — mono, like a wall label. */
.art-card .m{font:400 var(--step--1)/1.5 var(--font-mono);color:var(--muted)}
.price{font-family:var(--font-mono);color:var(--accent);font-size:.95rem}
/* No opacity here. It used to be .9, which dimmed EVERY child — the title, the medium, the
   dimensions and the white-on-grey SOLD badge — and pushed all four under the contrast
   threshold at once. Sold work is Jenny's proof of sales and has to stay legible; the greyed
   title and the badge already say "sold" without making the text harder to read. */
.art-card--sold .t{color:var(--sold)}

/* SOLD is a badge, never a deletion — it is Jenny's proof of sales (SPEC.md hard rule). */
/* The label is INK, not white. --sold is a light tone now because its main job is titling sold
   work against charcoal — but this badge uses it as a BACKGROUND, and white on it measures
   2.2:1. One token cannot be both a light foreground and a light background with light text.
   Ink on --sold is 6.96:1. axe caught this; the stale white-on-#6f685f row in check.mjs did
   not, because it was still describing the old cream site. */
.badge{align-self:flex-start;margin-top:6px;font:400 .68rem/1 var(--font-mono);letter-spacing:.1em;text-transform:uppercase;padding:5px 9px;border-radius:3px;background:var(--sold);color:var(--ink0)}
/* Both badge backgrounds are light on dark now, so the label flips to ink. 8.39 and 7.02. */
.badge--commission{background:var(--accent2);color:var(--ink0)}
.badge--portfolio{background:var(--muted);color:var(--ink0)}

/* Availability filter on /gallery/ — no JavaScript involved.
   The radios stay in the DOM and stay focusable (clip, not display:none — a display:none input
   is unreachable by keyboard and invisible to assistive tech), and the label is the visible chip.
   :has() on the wrapper does the filtering. With CSS unsupported the radios simply render as
   radios and every piece stays visible, which is the correct failure. */
.vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
.filters{display:flex;flex-wrap:wrap;gap:8px;border:0;padding:0;margin:1.5rem 0 0}
.filters input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.filters label{cursor:pointer;border:1px solid var(--line);border-radius:999px;padding:7px 15px;font-size:.88rem;color:var(--muted);background:var(--ink0);transition:border-color var(--dur-fast),color var(--dur-fast)}
.filters label:hover{border-color:var(--accent);color:var(--accent)}
.filters input:checked + label{border-color:var(--accent);color:var(--accent);background:var(--ink1)}
.filters input:focus-visible + label{outline:2px solid var(--accent2);outline-offset:2px}
.filters + .filters{margin-top:10px}
.gallery-filter:has(#av-yes:checked) .art-grid>li:not([data-status="for-sale"]){display:none}
.gallery-filter:has(#av-sold:checked) .art-grid>li:not([data-status="sold"]){display:none}

/* CSS-only sort. Four rules, not four per piece: the rank for each mode is a custom property on
   the <li> (generated into art.css from src/_data/pieces.mjs), and each rule reads the matching
   one into `order`. Default is no rule at all, so every item keeps order:0 and the grid renders
   in document order — which is forSaleFirst, the order the page is built in.

   `order` is visual only: it does NOT move the tab sequence, so a keyboard user would tab
   through the old order. That is a real cost and the reason the control is a preference rather
   than navigation — the pieces themselves are unchanged and every one is still reachable, in the
   order the document declares. Reordering the DOM instead would need JavaScript, and the whole
   point of this mechanism is that sorting survives with scripting blocked.

   Without :has() support nothing matches and the grid stays in its default order: the correct
   failure, same as the availability filter above. */
.gallery-filter:has(#sort-price-up:checked)   .art-grid>li{order:var(--ord-price-up)}
.gallery-filter:has(#sort-price-down:checked) .art-grid>li{order:var(--ord-price-down)}
.gallery-filter:has(#sort-size:checked)       .art-grid>li{order:var(--ord-size)}
.gallery-filter:has(#sort-year:checked)       .art-grid>li{order:var(--ord-year)}
/* The eight per-subject :has() rules that used to live here are gone. Subject is now a set of
   real pages under /collections/, which are shareable, indexable and linkable; availability is
   still a CSS filter above, because "available" is a state rather than a destination.
   data-tags is still emitted on each li — it costs nothing and documents the grouping in the
   markup. */

/* Collections ------------------------------------------------------------------------------ */
/* A row of links on /gallery/, a stacked list at the foot of a collection page. Each carries the
   pigment chip of that collection's hero painting, so the set reads as six colours before it
   reads as six words. */
.collection-links{list-style:none;padding:0;margin:var(--space-5) 0 0;display:flex;flex-wrap:wrap;gap:10px}
.collection-links a{display:inline-flex;align-items:center;gap:.5em;text-decoration:none;color:var(--text);
  border:1px solid var(--line);border-radius:999px;padding:8px 16px;font-size:.92rem;background:var(--ink0);
  transition:border-color var(--dur-fast),background var(--dur-fast)}
.collection-links a:hover{border-color:var(--accent);background:var(--ink1)}
.collection-links a .chip{margin-right:0}
.collection-links__n{font-family:var(--font-mono);font-size:.78rem;color:var(--muted)}
.collection-nav{margin-top:var(--space-8);padding-top:var(--space-5);border-top:1px solid var(--line)}
.collection-count{color:var(--muted)}
.collection-essay{margin:var(--space-5) 0 0}

/* Cards — used for class listings and the pricing blurbs on the commission pages. */
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:16px;margin-top:2rem}
.card{background:var(--ink1);border:1px solid var(--line);border-left:4px solid var(--accent);border-radius:var(--radius);padding:20px 22px}
.card.alt{background:var(--ink1)}
.card p:last-child{margin-bottom:0}
.card .meta{font:400 var(--step--1)/1.5 var(--font-mono);color:var(--muted)}

/* Forms — markup mirrors realistate's so wireForm() ports unchanged in T9. */
.form{margin-top:2rem;max-width:720px}
.form .row{display:flex;gap:16px;flex-wrap:wrap}
.form label{display:block;flex:1;min-width:240px;font-size:.82rem;color:var(--muted);margin-bottom:14px}
.form input,.form textarea,.form select{width:100%;margin-top:5px;padding:11px 12px;border:1px solid var(--line);border-radius:6px;font:inherit;font-size:1rem;color:var(--text);background:var(--ink1)}
.form input::placeholder,.form textarea::placeholder{color:var(--muted);opacity:1}
.form textarea{min-height:130px;resize:vertical}
.form input:focus,.form textarea:focus,.form select:focus{outline:2px solid var(--ring);outline-offset:0;border-color:var(--ring)}
.form-msg{font-size:.9rem;margin-top:12px;min-height:20px}
.form-msg.ok{color:var(--ok)}
.form-msg.err{color:var(--err)}
/* Honeypot. Off-screen rather than display:none — some bots skip hidden fields. */
.hp{position:absolute;left:-9999px;width:1px;height:1px;opacity:0}

/* "Regarding: <piece> ×" chip. Visible and dismissible by design — silently attaching the
   subject to an inquiry via a hidden field only would be a dark pattern (SPEC.md). */
.re-chip{display:none;align-items:center;gap:10px;margin-bottom:1rem;padding:8px 10px 8px 14px;border:1px solid var(--accent);border-radius:999px;background:var(--ink1);font-size:.9rem;width:fit-content;max-width:100%}
.re-chip[data-active]{display:inline-flex}
.re-chip button{background:none;border:none;color:var(--muted);font-size:1.2rem;line-height:1;cursor:pointer;padding:0 4px}
.re-chip button:hover{color:var(--text)}

/* Footer */
/* The saturated teal field is gone. A large accent surface competes with the paintings, and on
   a dark site it also fought the charcoal. The footer is now a raised panel wearing the paint
   strip as a crown, so the last thing on every page is her whole body of work as one object. */
.site-footer{background:var(--ink1);color:var(--text);padding-block:var(--space-7);margin-top:auto;border-top:1px solid var(--line)}
.site-footer a{color:var(--accent)}
.site-footer p{margin:.4em 0;font-size:.92rem}
.fbig{font-size:1.25rem;color:var(--text);margin:0 0 .3em}
/* .fine is .78rem — small text, so it needs the full 4.5:1. --muted on --ink1 is 7.78. */
.fine{color:var(--muted);font-size:.78rem;max-width:60ch;margin-top:1.2em}
.f-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(240px,100%),1fr));gap:24px}
.f-origin{margin:0 0 .8em;font-family:var(--font-mono);letter-spacing:.08em;text-transform:uppercase}

body{min-height:100vh;display:flex;flex-direction:column}
main{flex:1}

/* KINETIC MASKED TYPE.
   Headline words rise out of a clipping mask on entry. This is the one motion device that made
   the site feel authored rather than generated, and it is the reason the "never animate text"
   rule needed re-reading rather than obeying: that rule exists because FADING text is genuinely
   below AA at every intermediate frame and axe flags it. Sliding a fully-opaque word out from
   behind a mask never changes its colour at all — the contrast is the final contrast on frame
   one. Transform only, no opacity, ever.

   The stagger is static nth-child, so no inline styles and no JavaScript. The authored default
   is the END state: with the media query unmatched, or animations disabled, the spans simply
   sit at translate zero and the headline is a headline. */
.kin{display:inline-block;overflow:clip;vertical-align:bottom}
.kin>span{display:inline-block}
@media (prefers-reduced-motion:no-preference){
  @keyframes kin-up{from{transform:translateY(112%)}to{transform:none}}
  .kin>span{animation:kin-up var(--dur-slow) var(--ease-out) both}
  .kin:nth-child(2)>span{animation-delay:70ms}
  .kin:nth-child(3)>span{animation-delay:140ms}
  .kin:nth-child(4)>span{animation-delay:210ms}
  .kin:nth-child(5)>span{animation-delay:280ms}
  .kin:nth-child(6)>span{animation-delay:350ms}
  .kin:nth-child(7)>span{animation-delay:420ms}
  .kin:nth-child(8)>span{animation-delay:490ms}
  .kin:nth-child(n+9)>span{animation-delay:560ms}
}

/* Cross-document view transitions — two lines, progressive enhancement. Now they morph over
   charcoal, so a painting glides through a dark room between pages. */
@view-transition{navigation:auto}
::view-transition-old(root),::view-transition-new(root){animation-duration:var(--dur-med)}

/* Scroll-driven reveal. Paintings rise and fade in as they enter the viewport — the single
   cheapest thing that makes a static page feel current, and it costs no JavaScript at all.
   Guarded three ways: @supports so unsupporting browsers never apply the start state and hide
   content, prefers-reduced-motion so it is opt-out, and animation-fill-mode both so an element
   cannot be left mid-animation. Content is always in the DOM and readable regardless. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    @keyframes rise{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:none}}
    /* Images only — never anything containing text. Fading text in means that, at any moment
       mid-animation, the copy is genuinely below AA contrast; axe measures it and flags it, and
       a fast scroller really does see washed-out type. Revealing just the artwork keeps the
       effect and leaves every caption, price and heading at full contrast throughout. */
    .art-card__link,.about-figure,.pair figure,.hero__media{
      animation:rise linear both;
      animation-timeline:view();
      animation-range:entry 0% entry 42%;
    }
  }
}

/* Lightbox.
   This had NO styling at all — the markup existed, main.js drove it, and every audit checked
   focus trapping and keyboard paging rather than what it looked like. Unstyled, #lightbox is a
   plain block at the end of <body>, so "opening" it simply scrolled the page down to a
   full-bleed image with the caption below the fold. It has never worked visually.
   [hidden] needs an explicit display:none because display:flex below would otherwise override
   the attribute and leave the viewer permanently open. */
.lightbox{position:fixed;inset:0;z-index:100;background:rgba(19,17,16,.97);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;
  padding:clamp(56px,8vh,76px) clamp(56px,8vw,92px)}
.lightbox[hidden]{display:none}
/* max-height on the image is what keeps the whole painting on screen. No object-fit: with
   width and height auto the browser preserves the true aspect ratio, so nothing is cropped.
   dvh, not vh, or mobile browser chrome hides the bottom of tall work. */
.lb-img{max-width:100%;max-height:calc(100dvh - 190px);width:auto;height:auto;border-radius:3px}
.lb-cap{margin:0;color:#ece8e3;font-size:.95rem;text-align:center;max-width:70ch}
.lb-close,.lb-nav{position:absolute;display:grid;place-items:center;min-width:46px;min-height:46px;
  padding:0;background:rgba(255,255,255,.10);color:#fff;border:1px solid rgba(255,255,255,.28);
  border-radius:8px;font:inherit;font-size:1.7rem;line-height:1;cursor:pointer}
.lb-close:hover,.lb-nav:hover{background:rgba(255,255,255,.22)}
/* The global focus ring is a dark ochre — invisible on this backdrop. */
.lb-close:focus-visible,.lb-nav:focus-visible{outline:2px solid #fff;outline-offset:2px}
.lb-close{top:12px;right:14px}
.lb-prev{left:10px;top:50%;transform:translateY(-50%)}
.lb-next{right:10px;top:50%;transform:translateY(-50%)}
.lb-nav[hidden]{display:none}

/* Artwork detail page.
   Two columns above 1000px: the work on the left, and everything a buyer needs — facts, price,
   the story, both CTAs — in a sticky rail beside it. Previously all of this was capped at 820px
   and stacked inside an 1180px wrap, so a wide screen showed a tall left column against 360px of
   nothing, and "Inquire" sat below the fold on a phone for any tall painting. */
/* The stage: a blurred, scaled copy of the painting filling the band behind the sharp one. This
   is how a 480px original gets a full-width presence without being stretched or cropped — the
   real image on top is untouched. The backdrop is decorative (alt="", aria-hidden), so cover and
   upscaling are both fine: it is blurred well past the point where resolution is legible.
   isolation:isolate keeps the blur from bleeding over the sticky header. */
/* The stage is the neutral mount — the lit gallery wall. The blurred self-backdrop stays but
   drops to .3, so it reads as ambient spill from the canvas rather than a bright band. */
.art-stage{position:relative;overflow:hidden;isolation:isolate;background:var(--ink2);
  border-bottom:1px solid var(--line);padding-block:clamp(2rem,1rem + 3vw,4.5rem)}
.art-stage__bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  filter:blur(56px) saturate(1.3);transform:scale(1.22);opacity:.3;z-index:-1;pointer-events:none}
.art-stage__inner{position:relative}
.art-detail__body{padding-block:var(--space-7)}
.art-detail__lead .eyebrow a{color:inherit}
/* The chip beside the title is larger here than in a grid caption — on the piece's own page it
   is the one place the pigment is worth stating outright. */
.chip--lg{width:.5em;height:.5em;border-radius:3px;margin-right:.38em}
/* Catalogue number. A work belongs to a body of work, not to a shopping feed. */
.cat-no{margin:.2em 0 0;font:400 var(--step--1)/1.4 var(--font-mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted)}
.art-detail__desc{margin-top:var(--space-5);max-width:62ch}
/* In-room photograph. Absent from every record today; styled for the day one arrives. */
.wall-shot{margin:var(--space-6) 0 0}
.wall-shot img{width:100%;height:auto;border:1px solid var(--line);border-radius:var(--radius)}
.wall-shot figcaption{margin-top:.5rem;font:400 var(--step--1)/1.5 var(--font-mono);color:var(--muted)}
/* VIEW IN A ROOM. The scene is a 168-inch wall — the sofa spans half of it (84in), and the
   painting's width is its real inches as a fraction of 168. Everything is proportional, so it
   stays honest at any screen size. --art-w-in comes from the generated art.css; the fallback of
   24 never actually applies because the whole block only renders when dimensions exist. */
.room{margin:var(--space-5) 0 0;border:1px solid var(--line);border-radius:var(--radius);background:var(--ink1)}
.room>summary{cursor:pointer;padding:12px 16px;font:400 var(--step--1)/1.4 var(--font-mono);
  letter-spacing:.08em;text-transform:uppercase;color:var(--text);list-style-position:inside}
.room>summary:hover{color:var(--accent)}
.room>summary:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}
.room[open]>summary{border-bottom:1px solid var(--line)}
.room__scene{position:relative;aspect-ratio:2/1;overflow:hidden;
  background:linear-gradient(180deg,var(--ink2) 0%,var(--ink2) 62%,var(--ink1) 62%,var(--ink1) 100%)}
.room__art{position:absolute;left:50%;bottom:44%;transform:translateX(-50%);
  width:calc(var(--art-w-in,24) / 168 * 100%);line-height:0}
.room__art img{width:100%;height:auto;
  box-shadow:0 10px 26px -10px rgba(0,0,0,.9);
  box-shadow:0 10px 30px -8px var(--glow),0 0 0 1px rgba(244,241,236,.12)}
/* The sofa is scenery: 50% of a 168in wall = 84in, the reference the caption names. */
.room__sofa{position:absolute;left:25%;bottom:2%;width:50%;height:auto;fill:rgba(244,241,236,.13)}
.room__note{padding:10px 16px 14px;margin:0;color:var(--muted)}

.related{margin-top:var(--space-8);padding-top:var(--space-6);border-top:1px solid var(--line)}
.related .art-grid{margin-top:var(--space-4)}

.art-detail__cols{display:grid;gap:clamp(1.5rem,3vw,3rem);align-items:start}
@media (min-width:1000px){
  .art-detail__cols{grid-template-columns:minmax(0,1.55fr) minmax(0,1fr)}
  /* 6rem clears the sticky header with room to breathe. */
  .art-detail__rail{position:sticky;top:6rem}
}
.art-detail__figure{margin:0 auto;max-width:min(100%,760px)}
/* No object-fit and no crop: width 100% / height auto shows the whole painting. The white mount
   and shadow give it the presence a frame would, without touching the work. */
/* The canvas, lit. No mount colour behind it — the painting IS the surface — and the halo is
   its own dominant pigment, so the light in the room comes from the work. */
.art-detail__figure img{border-radius:2px;width:100%;height:auto;
  box-shadow:0 30px 70px -28px rgba(0,0,0,.9),0 0 0 1px rgba(244,241,236,.1);
  box-shadow:0 30px 90px -26px color-mix(in srgb,var(--chip) 62%,transparent),0 0 0 1px rgba(244,241,236,.1)}
.art-detail__figure figcaption{margin-top:.6rem}
.art-detail__desc{margin:1.25rem 0 0}
.art-detail__desc p{margin:0 0 .8em}
.art-detail__desc p:last-child{margin-bottom:0}

/* Flex, not grid, and width:fit-content — because 30 of the 88 pieces carry exactly ONE fact
   ("Status: Portfolio piece"). As a filling grid that single cell stretched into a full-width
   bordered slab. The 1px gap over a --line background is what draws the hairlines between
   cells, so tracks must always fill the box: any layout that leaves dead space inside it bleeds
   the divider colour. flex:1 1 180px guarantees the last row grows to close that gap. */
.facts{display:flex;flex-wrap:wrap;gap:1px;background:var(--line);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;margin:1.5rem 0 0;width:fit-content;max-width:100%}
.facts>div{background:var(--ink1);padding:14px 16px;flex:1 1 180px;min-width:min(180px,100%)}
.facts dt{font:400 .68rem/1 var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:.5em}
.facts dd{margin:0;font-family:var(--font-mono);font-size:.92rem;line-height:1.45}
.art-nav{display:flex;flex-wrap:wrap;gap:1.5rem;margin-top:3rem;padding-top:1.5rem;border-top:1px solid var(--line);font-size:.94rem}

/* Editorial index at /works/. A catalogue line per piece: number, title, medium, size, price.
   The hover/focus preview is CSS only — :focus-within is what makes it reachable by keyboard,
   which is the whole reason this is a list of links and not a JS widget. */
.works{list-style:none;padding:0;margin:var(--space-6) 0 0;counter-reset:none;
  border-top:1px solid var(--line)}
.works__row{border-bottom:1px solid var(--line);position:relative}
.works__row a{display:grid;grid-template-columns:3.5rem minmax(0,2fr) minmax(0,1.6fr) 7.5rem 6.5rem;
  gap:1rem;align-items:baseline;padding:.85rem .5rem;text-decoration:none;color:var(--text);
  transition:background var(--dur-fast)}
.works__row a:hover,.works__row a:focus-visible{background:var(--wash)}
.works__n,.works__m,.works__d,.works__p{font:400 var(--step--1)/1.4 var(--font-mono);color:var(--muted)}
.works__n{letter-spacing:.06em}
.works__t{font-family:var(--font-serif);font-size:1.06rem;line-height:1.3}
.works__p{text-align:right;color:var(--pen)}
/* The preview. Absolutely positioned so it cannot affect layout, and pointer-events:none so it
   can never sit between the cursor and the link it belongs to. */
.works__peek{position:absolute;right:8.5rem;top:50%;width:220px;z-index:5;
  transform:translateY(-50%) scale(.96);opacity:0;pointer-events:none;
  transition:opacity var(--dur-fast) var(--ease-out),transform var(--dur-fast) var(--ease-out);
  box-shadow:0 18px 44px -20px rgba(34,32,29,.6);border-radius:2px;overflow:hidden;line-height:0}
.works__peek-bg{width:100%;height:auto;max-height:260px;object-fit:contain;background:var(--wash)}
.works__row a:hover .works__peek,.works__row a:focus-visible .works__peek{opacity:1;transform:translateY(-50%) scale(1)}
/* Below the two-column threshold the row cannot carry five columns, and a floating preview over
   a narrow screen is in the way rather than helpful. Medium and size stack under the title; the
   preview is suppressed entirely — on a touch screen there is no hover to reveal it with. */
@media (max-width:820px){
  .works__row a{grid-template-columns:2.75rem minmax(0,1fr) auto;gap:.5rem .8rem}
  .works__t{grid-column:2}
  .works__m{grid-column:2;grid-row:2}
  .works__d{grid-column:2;grid-row:3}
  .works__p{grid-column:3;grid-row:1}
  .works__peek{display:none}
}
@media (prefers-reduced-motion:reduce){
  .works__peek{transition:none}
}

/* Before/after reference-photo pairs on /pets/ */
.pairs{list-style:none;padding:0;margin:2rem 0 0;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(320px,100%),1fr));gap:2rem}
.pair{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:.75rem}
.pair figure{margin:0}
.pair img{width:100%;height:auto;border:1px solid var(--line);border-radius:var(--radius)}
.pair figcaption{margin-top:.4rem;text-align:center}
.pair__arrow{color:var(--accent2);font-size:1.5rem}

/* Classes.
   The numbered steps are numbered because booking really is a sequence — 01/02/03 markers on
   content that is not ordered would be decoration pretending to be structure. */
.steps{list-style:none;padding:0;margin:var(--space-7) 0 0;display:grid;gap:clamp(20px,3vw,36px);
  grid-template-columns:repeat(auto-fit,minmax(min(240px,100%),1fr));counter-reset:none}
.steps li{border-top:2px solid var(--accent);padding-top:var(--space-4)}
.steps__n{display:block;font:400 var(--step--1)/1 var(--font-mono);letter-spacing:.14em;color:var(--accent2);margin-bottom:.7em}
.steps h3{margin-bottom:.25em}
.steps p{margin:0;color:var(--muted)}
/* A class card's date is the thing being scanned for, so it gets the mono treatment and the
   accent rule rather than sitting in the body text. */
.card time{font-family:var(--font-mono);font-size:.9rem;color:var(--text)}
.card--full{opacity:.75;border-left-color:var(--sold)}
.past-classes summary{cursor:pointer;font-family:var(--font-serif);font-size:var(--step-1);padding-block:.4em}
.past-list{list-style:none;padding:0;margin:1rem 0;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(320px,100%),1fr));gap:.4rem 2rem}
.past-list li{color:var(--muted);font-size:.94rem;border-bottom:1px solid var(--line);padding-block:.4em}
.past-list time{color:var(--text);font-family:var(--font-mono);font-size:.88rem}

/* Studio notes — /notes/. A reading column with the illustrating painting alongside it, not
   behind it: text over artwork needs a scrim and a scrim is a dark surface over the work.
   The rule down the left is --pen, which is that painting's own pigment walked to AA, so a page
   of notes reads as a run of different colours without a single coloured panel anywhere. A note
   with no piece inherits the neutral fallback from :root and the rule is simply grey. */
.notes{list-style:none;padding:0;margin:var(--space-7) 0 0;display:grid;gap:var(--space-7)}
.note{border-top:1px solid var(--line);padding-top:var(--space-5)}
.note h2{margin:.1em 0 .6em;font-size:var(--step-2)}
.note article{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:var(--space-3) var(--space-6);align-items:start}
/* The eyebrow and heading span both columns so the painting starts level with the body text
   rather than pushing the date sideways. */
.note article > .eyebrow,.note article > h2{grid-column:1 / -1}
/* Both children are placed EXPLICITLY into row 3. The figure comes first in source order (so it
   is the first thing after the heading for a screen reader and on a phone), and pinning only the
   figure would leave the body to auto-place into row 4 — a column of text sitting under an empty
   cell. Naming both is what keeps them side by side. */
.note__body{border-left:2px solid var(--pen);padding-left:var(--space-4);grid-column:1;grid-row:3}
.note__body p:last-child{margin-bottom:0}
.note__art{margin:0;grid-column:2;grid-row:3}
/* Never cover — the aspect ratios here run 0.33 to 3.69 and cropping artwork is banned. */
.note__art img{width:100%;height:auto;border-radius:var(--radius);background:var(--ink2)}
.note__art figcaption{margin-top:.5em;font-family:var(--font-mono);font-size:var(--step--1);color:var(--muted)}
@media (max-width:760px){
  /* One column: the painting moves ABOVE the words rather than below them, because on a phone a
     note that opens with its picture is the thing worth scrolling into. */
  .note article{grid-template-columns:1fr}
  .note__art{grid-column:1;grid-row:3}
  .note__body{grid-column:1;grid-row:4}
}

/* Contact */
.contact-cols{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:clamp(2rem,4vw,4rem);margin-top:2rem;align-items:start}
@media (max-width:760px){.contact-cols{grid-template-columns:1fr}}
.contact-aside{background:var(--ink1);border:1px solid var(--line);border-radius:var(--radius);padding:1.5rem 1.75rem}
.contact-aside h2{font-size:var(--step-1);margin-top:1.5rem}
.contact-aside h2:first-child{margin-top:0}
.contact-aside p{margin:0 0 1em}
.contact-aside .m{color:var(--muted);font-size:.88rem}
.fine-print{color:var(--muted);font-size:.85rem;margin-top:1em}

/* The newsletter box. A raised surface rather than an accent field: a saturated panel here would
   be the largest coloured area on a page whose job is to show paintings, and the palette rules
   exist to stop exactly that. The accent appears as a top rule only. */
.subscribe{background:var(--ink1);border:1px solid var(--line);border-top:3px solid var(--accent);
  border-radius:var(--radius);padding:clamp(1.5rem,4vw,2.5rem);margin-top:var(--space-7)}
.subscribe__in{max-width:56ch}
.subscribe h2{margin:.1em 0 .5em;font-size:var(--step-2)}
.subscribe .lede{font-size:var(--step-0);color:var(--muted)}
/* One field and one button on a line at desktop width, stacked on a phone. align-items:end
   keeps the button's baseline level with the input rather than with the label above it. */
.form--inline{margin-top:1.5rem;display:flex;flex-wrap:wrap;gap:12px;align-items:end}
.form--inline .subscribe__field{flex:1 1 260px;margin-bottom:0}
.form--inline button{flex:0 0 auto}
/* Both are full-width block children of the flex row, so they sit on their own lines under it
   instead of being squeezed alongside the input. */
.form--inline .form-msg,.form--inline .fine-print,.form--inline .cf-turnstile{flex:1 0 100%;margin-top:0}
@media (max-width:520px){.form--inline .subscribe__field,.form--inline button{flex:1 0 100%}}

/* The opt-in on the contact form. A real checkbox, never pre-ticked — see /privacy/. The label
   overrides .form label's block display so the box and its text sit on one line. */
.optin{display:flex;align-items:flex-start;gap:10px;margin:0 0 14px;font-size:.9rem;color:var(--text)}
.optin input{width:auto;margin:2px 0 0;flex:0 0 auto}

/* Preview-build banner. Never present in a production build at all — base.njk only emits it when
   SITE_ENV is not production, so this rule is dead CSS on the live site. Deliberately the one
   loud, unbranded thing on the site: it has to be impossible to mistake for chrome. Ochre on
   near-black is the --ring pair, measured at 11.98:1. */
.prevbar{margin:0;padding:6px 16px;background:var(--ring);color:#131110;text-align:center;
  font:400 var(--step--1)/1.4 var(--font-mono);letter-spacing:.04em}

/* Catalogue search on /works/. Ships hidden and is revealed by main.js. */
.works-search{margin:var(--space-6) 0 0;max-width:32rem}
.works-search label{display:block;font-family:var(--font-mono);font-size:var(--step--1);
  letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:.5em}
.works-search input{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:6px;
  font:inherit;font-size:1rem;color:var(--text);background:var(--ink1)}
.works-search input:focus{outline:2px solid var(--ring);outline-offset:0;border-color:var(--ring)}
.works-search input::placeholder{color:var(--muted);opacity:1}
/* min-height reserves the line so the list below does not jump on the first keystroke. */
.works-search__count{font-family:var(--font-mono);font-size:var(--step--1);color:var(--muted);
  margin:.6em 0 0;min-height:1.2em}
.works-empty{margin-top:var(--space-5);color:var(--muted)}
/* A button that reads as a link. It IS a button — it performs an action rather than navigating,
   and making it an <a href="#"> would be a dead control for anyone without JavaScript. */
.linkish{background:none;border:0;padding:0;font:inherit;color:var(--accent);
  text-decoration:underline;cursor:pointer}
.linkish:hover{color:var(--text)}

/* The saved shortlist on the contact form, and the toggle on an artwork page. */
.btn-save[aria-pressed="true"]{border-color:var(--accent);color:var(--accent)}
.saved-box{background:var(--ink1);border:1px solid var(--line);border-left:3px solid var(--accent);
  border-radius:var(--radius);padding:1rem 1.25rem;margin-bottom:1.25rem}
.saved-box__h{font-family:var(--font-mono);font-size:var(--step--1);letter-spacing:.08em;
  text-transform:uppercase;color:var(--muted);margin:0 0 .6em}
.saved-list{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:8px}
.saved-list li{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);
  border-radius:999px;padding:5px 6px 5px 14px;font-size:.9rem;background:var(--ink0)}
.saved-list__x{background:none;border:0;color:var(--muted);font-size:1.15rem;line-height:1;
  cursor:pointer;padding:0 5px}
.saved-list__x:hover{color:var(--text)}

/* Home hero — the painting runs edge to edge and the headline sits under it.
   NOT overlaid on the image: text over artwork needs a scrim to stay legible, a scrim is a
   large dark surface over the work, and that is the exact thing the palette rules forbid. Full
   width plus large type gets the same impact with none of that, and keeps contrast at AA.
   No crop either — the image is width:100%, height:auto, so the whole painting is shown. */
/* ============================================================================================
   FULL-BLEED COLOUR. The site used to be cream with hairline accents; the paintings are the
   loudest thing on any wall Jenny hangs them on, and the page should behave the same way.
   Every band below takes its colour from a painting's own extracted pigment (--pen), which is
   machine-verified to clear 5:1 against white across all 89 pieces — so a saturated field with
   white type is safe whatever painting drives it.
   The two rules that protect the work still hold: nothing is cropped, and no text is ever set
   over a painting. Type sits on a colour field NEXT to the work, never on top of it.
   ============================================================================================ */

/* Full-viewport split hero. Colour field one side, painting the other, both edge to edge. */
.hero-full{display:grid;min-height:100svh;background:var(--wash)}
@media (min-width:900px){.hero-full{grid-template-columns:minmax(0,1fr) minmax(0,1.2fr)}}
/* The panel is the hero painting's own colour breathed into the charcoal — an ember, not a
   slab. --pen would be a light surface now; it belongs on the type, not behind it. */
.hero-full__panel{display:flex;align-items:center;background:var(--wash);color:var(--text);
  padding:clamp(2.5rem,5vw,4.5rem) clamp(1.25rem,3.5vw,3.5rem)}
.hero-full__panel-in{max-width:32ch}
/* Sized for a COLUMN, not for the page. --step-4 and --step-5 are both scaled to the full
   1140px measure; inside a half-width panel either one set eight words over six lines and
   pushed the lede and the buttons off the bottom of the viewport. A headline in a column is a
   different instrument from a headline across a page, and needs its own step. */
/* The headline is set in the painting's own pigment — the Henry Art move: colour lives in the
   type, not in buttons or panels. --pen is machine-verified to clear AA against all three
   grounds and its own wash, so this is safe for every one of the 89 pieces. */
.hero-full__panel h1{font-size:clamp(2.25rem,1.4rem + 2.4vw,3.6rem);color:var(--pen);
  margin-bottom:.4em;text-wrap:balance;line-height:1.04}
.hero-full__credit{margin:var(--space-6) 0 0;font:400 var(--step--1)/1.5 var(--font-mono);color:var(--muted)}
.hero-full__credit a{color:var(--text);text-underline-offset:3px}
/* The painting sits on its own wash, contained — the whole work, never a crop. */
/* The mount: a neutral warm grey, not black. A canvas on pure black reads with distorted
   colour — this is the lit-gallery-wall condition SPEC was right to worry about. */
.hero-full__art{margin:0;background:var(--ink2);display:grid;place-items:center;
  padding:clamp(.75rem,1.5vw,1.75rem);min-height:52svh}
.hero-full__art a{display:block;line-height:0;max-width:100%}
/* Fill the half. The painting is the reason anyone is here, so it takes the space — contained,
   never cropped, so the whole work is always shown whatever its ratio. */
.hero-full__art img{width:auto;max-width:100%;max-height:88svh;height:auto;
  box-shadow:0 30px 80px -34px rgba(0,0,0,.75);
  box-shadow:0 30px 90px -30px var(--glow), 0 0 0 1px rgba(244,241,236,.08)}

/* On-dark variants. Kept as explicit modifiers rather than context selectors so it is obvious at
   the call site that a colour field is in play and the contrast has been thought about. */
/* No-ops. The whole site is on dark now, so these modifiers do nothing — kept only so the
   templates referencing them need no edit. Do not add new ones. */
.eyebrow--on-dark,.lede--on-dark{}

/* Full-bleed collection band: a saturated field carrying a horizontal rail of work. */
/* Six bands, six pigments. Each collection's ground is its hero painting's colour breathed into
   the charcoal, and its headline is that same pigment at full strength. Scrolling the page
   moves through the palette of the work itself. */
.band{background:var(--wash);color:var(--text);padding-block:var(--space-8);border-block:1px solid var(--line)}
.band__head{display:flex;flex-wrap:wrap;align-items:end;justify-content:space-between;gap:var(--space-5)}
.band__head h2{color:var(--pen);margin:0;font-size:var(--step-4)}
.band__head h2 a{color:inherit;text-decoration:none}
.band__head h2 a:hover{text-decoration:underline;text-underline-offset:6px}
.band__head .lede{margin:.5em 0 0;max-width:46ch}

/* The rail. A REAL scroll container — labelled, focusable, and scrolled by the browser. The
   accessibility problem with horizontal sections is faking them with transforms or hijacking
   the wheel; neither happens here, and it degrades to a plain scrolling list everywhere. */
/* scroll-padding-inline is not decoration: without it the snap engine ignores the rail's own
   padding and parks the first item hard against the viewport edge, so the opening painting is
   sliced in half. The two values have to stay equal — the padding sets the resting position,
   scroll-padding tells the snap engine to respect it. */
.rail-wrap{margin:var(--space-6) 0 0}
.rail-wrap:focus-visible{outline:2px solid var(--ring);outline-offset:4px}
.rail{display:flex;gap:clamp(14px,2vw,28px);list-style:none;margin:0;
  padding:0 max(calc((100vw - var(--maxw)) / 2), 20px) var(--space-2);
  scroll-padding-inline:max(calc((100vw - var(--maxw)) / 2), 20px);
  overflow-x:auto;scroll-snap-type:x proximity;overscroll-behavior-x:contain;
  scrollbar-width:thin;scrollbar-color:rgba(244,241,236,.4) transparent}
.rail::-webkit-scrollbar{height:8px}
.rail::-webkit-scrollbar-thumb{background:rgba(244,241,236,.4);border-radius:4px}
.rail__item{flex:0 0 clamp(230px,30vw,420px);scroll-snap-align:start}
.rail__item a{display:block;text-decoration:none;color:var(--text)}
.rail__item img{width:100%;height:auto;background:var(--ink2);border-radius:2px;
  box-shadow:0 14px 40px -20px rgba(0,0,0,.85);
  box-shadow:0 14px 40px -18px var(--glow), 0 0 0 1px rgba(244,241,236,.07);
  transition:transform var(--dur-med) var(--ease-out),box-shadow var(--dur-med) var(--ease-out)}
.rail__item a:hover img,.rail__item a:focus-visible img{transform:translateY(-6px);
  box-shadow:0 28px 64px -18px color-mix(in srgb,var(--chip) 70%,transparent), 0 0 0 1px rgba(244,241,236,.16)}
.rail__cap{display:flex;justify-content:space-between;gap:1rem;align-items:baseline;padding-top:.7rem}
.rail__t{font-family:var(--font-serif);font-size:1.06rem}
.rail__p{font:400 var(--step--1)/1.4 var(--font-mono);color:var(--muted);white-space:nowrap}

/* SCROLL-DRIVEN PIGMENT. Bands warm from charcoal into their own colour as they enter view.
   Backgrounds only — never text — and the authored default is the END state, so a browser
   without scroll timelines simply shows the tinted band with no animation at all. */
@supports (animation-timeline: view()){
  @media (prefers-reduced-motion:no-preference){
    @keyframes band-tint{from{background-color:var(--ink0)}to{background-color:var(--wash)}}
    .band,.story{animation:band-tint linear both;animation-timeline:view();animation-range:entry 0% entry 55%}
  }
}

/* The statement leads, the painting follows. --wash tints the band with the hero painting's own
   colour, so the top of the site is already in its palette before the image loads. */
.hero{padding:0;background:var(--wash);border-bottom:1px solid var(--line)}
.hero__body{padding-block:clamp(2.5rem,1.5rem + 5vw,5.5rem)}
.hero__body h1{font-size:var(--step-5);max-width:16ch}
.hero__body .lede{max-width:52ch;margin-bottom:0}
.hero__body .btn-row{margin-top:var(--space-6)}
.hero__media{margin:0;line-height:0}
/* svh, not vh — on a phone, vh is the ADDRESS-BAR-HIDDEN height, so the painting would be taller
   than the screen on load and the band below it would never peek.
   The cap is 82svh rather than something safer because max-height plus object-fit:contain
   letterboxes: at 62svh the 2400x1207 hero was capped at 558px tall, so contain shrank it to
   1110px wide and the "full-bleed" painting sat in a box with wash down both sides. The cap has
   to clear the natural height of a wide hero at desktop width (1440/1.99 = 723px) or it stops
   being full-bleed. Genuinely short viewports still letterbox, which is the correct behaviour —
   the alternative is cropping the work. */
.hero__media img{width:100%;height:auto;max-height:82svh;object-fit:contain;background:var(--wash)}
.hero__credit{margin:0;padding-block:.9rem;font:400 var(--step--1)/1.5 var(--font-mono);color:var(--muted)}
.hero__credit a{color:var(--muted);text-decoration-color:var(--line)}
.hero__credit a:hover{color:var(--accent)}

/* Collection cards on the home page. The painting is the label — a name plus a count under a
   representative piece, on that piece's own wash. */
.coll-cards{list-style:none;padding:0;margin:var(--space-6) 0 0;display:grid;gap:clamp(16px,2.4vw,28px);
  grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr))}
.coll-card a{display:block;text-decoration:none;color:var(--text);background:var(--wash);
  border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;height:100%;
  transition:transform var(--dur-med) var(--ease-out),box-shadow var(--dur-med) var(--ease-out)}
.coll-card a:hover{transform:translateY(-3px);
  box-shadow:0 20px 48px -20px color-mix(in srgb,var(--chip) 60%,transparent)}
/* Fixed-ratio window with object-fit:cover — permitted here and ONLY here, because this is a
   navigational thumbnail standing for a whole collection, not the presentation of a work. Every
   painting is still shown uncropped on the collection page and on its own page. */
.coll-card__media{display:block;aspect-ratio:4/3;overflow:hidden}
.coll-card__media img{width:100%;height:100%;object-fit:cover}
.coll-card__body{display:flex;flex-direction:column;gap:3px;padding:14px 16px 16px}
.coll-card__t{font-family:var(--font-serif);font-size:1.1rem;line-height:1.25}

/* Classes teaser on the home page. */
.classes-teaser{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr);gap:clamp(2rem,4vw,4rem);align-items:start}
@media (max-width:820px){.classes-teaser{grid-template-columns:1fr}}
.classes-teaser .facts{flex-direction:column;width:100%;margin-top:0}

/* Home */
.about-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:clamp(2rem,4vw,3.5rem);align-items:center}
@media (max-width:820px){.about-grid{grid-template-columns:1fr}}
.about-figure{margin:0}
.about-figure img{width:100%;height:auto;border:1px solid var(--line);border-radius:var(--radius)}
.about-figure figcaption{margin-top:.6rem;color:var(--muted);font:400 var(--step--1)/1.5 var(--font-mono)}

/* PRINT. Without this the whole site prints as near-white text on unprinted white paper — a
   dark theme's most easily-missed failure, because nobody prints a page they just designed.
   Flipping the ground tokens is enough: everything else is expressed in terms of them. */
@media print{
  :root{
    color-scheme:light;
    --ink0:#fff; --ink1:#fff; --ink2:#f4f2ef;
    --text:#000; --muted:#444; --line:#ccc;
    --accent:#0b5560; --accent2:#8a2f27; --sold:#555; --pen:#333; --wash:#fff;
    --glow:transparent;
  }
  .site-header,.strip,.announce,.rail,.works__peek{display:none !important}
  .art-card__link,.art-detail__figure img{box-shadow:none !important}
  a{text-decoration:underline}
}

/* About page ------------------------------------------------------------------------------- */
.about-head__grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:clamp(2rem,4vw,4rem);align-items:start;margin-top:var(--space-7)}
@media (max-width:860px){.about-head__grid{grid-template-columns:1fr}}
/* Full measure. Constrained to 14ch first, which broke the statement over four lines against a
   column of empty space — at --step-5 a headline needs the width it asks for. */
.about-head h1{font-size:var(--step-5);margin-bottom:0}
/* The fact list is the .facts hairline grid, but stacked rather than side by side: these are
   label/value pairs to be read down, not a row of measurements. */
.about-facts{flex-direction:column;width:100%;margin-top:var(--space-6)}
.about-facts>div{flex:1 1 auto}
/* A slightly larger measure for the bio — it is the one long read on the site. */
.prose--lead{max-width:64ch;font-size:var(--step-1);line-height:1.6}
.prose--lead p{margin:0 0 .9em}

/* The story band. A painting holds its column while the text scrolls past it. position:sticky
   is universally supported and needs no animation at all, so this behaves identically in every
   browser, at every motion preference — the scroll-driven rule further down only draws a
   hairline. Below 900px it stacks and the sticky is switched off, because a sticky image on a
   phone eats the screen the text needs. */
.story{background:var(--wash);border-block:1px solid var(--line);padding-block:var(--space-8)}
.story__grid{display:grid;gap:clamp(2rem,4vw,4rem)}
.story__media{margin:0}
.story__media img{width:100%;height:auto;border-radius:2px}
.story__media figcaption{margin-top:.6rem;color:var(--muted);font:400 var(--step--1)/1.5 var(--font-mono)}
.story__panels{display:flex;flex-direction:column;gap:var(--space-8);justify-content:center}
.pull{margin:0;font-family:var(--font-serif);font-size:var(--step-3);line-height:1.25;letter-spacing:-.01em;
  text-wrap:pretty;border-left:3px solid var(--pen);padding-left:clamp(1rem,2vw,1.75rem)}
@media (min-width:900px){
  .story__grid{grid-template-columns:minmax(0,.85fr) minmax(0,1fr);align-items:start}
  .story__media{position:sticky;top:6rem}
}
