/* Pasteled Paw — one stylesheet, three systems: a type scale, a spacing ladder, and a palette.
   Every value on a page comes from a token below. If something needs a size or a gap that isn't
   here, the answer is almost always the nearest token, not a new number.

   The design brief fixes what this must not become: cream + high-contrast serif + terracotta,
   all-caps eyebrows, accented words, arrows on buttons, identical rounded cards with soft grey
   shadows, meta strings joined with middle dots. None of those appear here. */

/* ---- Typefaces ------------------------------------------------------------------------------
   Self-hosted, subset, three files and 59 KB for the set. Built by tools/fonts_build.py, which
   carries the reasoning for each cut; both families are SIL OFL 1.1 and their licences ship
   beside them in /fonts/.

   No third-party font CDN. A <link> to fonts.googleapis.com is a second DNS lookup and a second
   TLS handshake before a single glyph starts downloading — on the connections this site is built
   for, that is the whole budget — and it tells another company who is reading a small artist's
   website. It also could not have rendered a price: Google's "latin" subset has no peso sign.

   `swap`, not `optional`: the fallbacks below are metrically close enough that the swap is a
   change of shape rather than a reflow, and a reader on a slow connection should end up with the
   real face rather than whichever one arrived first. */
@font-face {
  font-family: "Fraunces Display";
  src: url("/fonts/fraunces-display.woff2") format("woff2");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Fraunces Text";
  src: url("/fonts/fraunces-text.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Pasteled Script";
  src: url("/fonts/script.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Archivo";
  src: url("/fonts/archivo.woff2") format("woff2");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* ---- Palette ----------------------------------------------------------------------------
     Design brief v2. The previous palette was a cool grey paper chosen for restraint, and the
     brief's own verdict on it is that it "was wrong for this artist" — her logo is a pastel
     rainbow and she likes animal print and bold colour. So: warm cream ground, deep plum text,
     berry-rose for anything you can act on, and the pastel set living in the scribble.

     Every value here was sampled from the mockups and then, where it had to be, corrected until
     it passes. Two were corrected. The mockup's berry (#c05870) carries a white uppercase label
     at 4.31:1, which is under the 4.5:1 that size of text needs, so it is deepened to #b04a64 —
     5.24:1 with white on it, and 4.67:1 as text on cream, which means the same token can be a
     link as well as a fill. And five of the ten wordmark letter colours failed 3:1 as large text
     on cream; each was darkened in HLS with its hue and saturation held until it cleared 3.4:1,
     so the multicolour title keeps the mockup's colour relationships and is still readable. */
  --paper: #f8f1e7; /* warm cream, with the grain below over it */
  --paper-raised: #fdf8f1; /* a sheet laid on the ground: fields, the selected tier */
  --paper-sunk: #efe5d8; /* the plate an image loads onto */
  --ink: #3d2b33; /* deep plum — 11.8:1 */
  /* Deepened from #6a5560 as the scribble field got stronger. Measured over a glyph-sized
     neighbourhood rather than a single pixel: on the darkest 1% of the finished field the old
     value came to 4.80:1, which passes AA and has no room left in it. This is 5.90:1. */
  --ink-soft: #5c4854;
  --rule: #e3d6c8; /* decorative hairline, no contrast requirement */
  --rule-input: #9c8a83; /* 3.2:1 — a control boundary has to clear 3:1 */
  --alert: #a3312c; /* 5.6:1 — errors only, never decoration */

  /* The one colour you can act on. Fill for buttons, and dark enough to be a link too. */
  --berry: #b04a64;
  --berry-deep: #97374f; /* hover */

  /* The display set: one per letter of a page title, in the mockup's order. Every one clears
     3.4:1 on cream, which is the bar for large text with margin. */
  --hue-1: #c66078;
  --hue-2: #9a7b61;
  --hue-3: #6b555f;
  --hue-4: #a5729e;
  --hue-5: #857ca9;
  --hue-6: #5787a9;
  --hue-7: #497189;
  --hue-8: #ce5f76;
  --hue-9: #bb6a79;
  --hue-10: #5a716e;

  /* ---- Type -------------------------------------------------------------------------------
     Four roles now, per brief v2. Fraunces and Archivo already covered display and body; the
     script is new and does one job — marginalia and the emotional lines. It never sets a price,
     a form label, or anything a customer has to read in order to act. */
  --font-display: "Fraunces Display", "Iowan Old Style", Georgia, serif;
  --font-serif: "Fraunces Text", "Iowan Old Style", Georgia, serif;
  --font-sans: "Archivo", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-script: "Pasteled Script", "Snell Roundhand", "Brush Script MT", cursive;

  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  --t-hero: clamp(2.5rem, 10vw, 4.5rem); /* 40 → 72  the one statement on a page */
  --t-script-lead: clamp(1.75rem, 7.5vw, 3rem); /* 28 → 48  the strapline, which the mockups set big */
  --t-title: clamp(2.75rem, 13vw, 5.5rem); /* 44 → 88  page titles, set in colour */
  --t-head: clamp(1.25rem, 4vw, 1.5rem); /* 20 → 24  section headings */
  --t-lead: clamp(1.125rem, 3.6vw, 1.375rem); /* 18 → 22  her voice */
  --t-script: clamp(1.625rem, 6vw, 2.375rem); /* 26 → 38  straplines */
  --t-body: 1.0625rem; /* 17 */
  --t-small: 0.9375rem; /* 15       meta, hints */
  --t-micro: 0.8125rem; /* 13       labels, captions */
  --t-eyebrow: 0.75rem; /* 12       uppercase, letterspaced */

  --lh-tight: 1.05;
  --lh-snug: 1.22;
  --lh-body: 1.6;

  /* ---- Spacing ----------------------------------------------------------------------------
     One 4px-based ladder. --band is the gap between major sections and is the only thing that
     scales with the viewport; everything inside a section uses the fixed steps, so the rhythm
     stays the same on a phone and a laptop. */
  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.5rem;
  --s6: 2rem;
  --s7: 3rem;
  --s8: 4rem;
  --s9: 6rem;

  --gutter: clamp(1.25rem, 5vw, 2.5rem);
  --band: clamp(2.5rem, 8vw, 4.5rem);

  --measure: 34rem; /* ~66 characters at --t-body */
  --column: 42rem; /* reading column */
  --wide: 76rem; /* the gallery, which wants the room */

  --tap: 2.75rem; /* minimum comfortable target */

  /* Paper grain, held in a token because two places need the same one: the root element, and the
     scroll stage's first ground — which IS this paper, and looked like a different, colder sheet
     for as long as it carried its own texture from the design handoff. */
  /* The drawn heart. Two places use it — the marginalia and the eyebrow — and a data URI
     copied into both is a data URI that drifts. */
  --heart: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 27'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round'%3E%3Cpath d='M13.2 9.9 C 11.3 4.9 7.9 2.6 5.3 4.5 C 2.6 6.5 3.0 10.4 5.3 13.6 C 7.2 16.2 10.3 19.9 13.7 24.2 L 12.0 25.7'/%3E%3Cpath d='M12.3 25.3 C 12.3 25.3 15.4 20.9 18.1 16.7 C 20.9 12.2 22.4 8.1 20.7 5.4 C 19.0 2.7 15.4 3.8 13.8 8.0 L 13.0 11.2'/%3E%3C/g%3E%3C/svg%3E");

  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.22'/%3E%3C/svg%3E");

  /* The same grain, harder. The scroll stage's first ground has to read as a *sheet* the portrait
     is lying on, against a full screen with nothing else in it, and the page's own 22% grain is
     tuned to be invisible unless you look for it. `stitchTiles` is what makes it seamless at any
     size, which the design handoff's paper texture was not. */
  --grain-strong: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.55'/%3E%3C/svg%3E");
}

/* ---- Reset and base ---------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Paper grain. The brief allows this where most sites shouldn't have it — chalk on paper tooth is
   the actual material, not a decoration bolted on top of one.

   It lives on the root element's background, which is the only place that gets all three
   properties this needs: it tiles, it scrolls with the document (a fixed layer would sit still
   while the page moved past it, which reads as a screen artefact rather than a surface), and it
   sits under every image rather than over one. Rasterised once per 200px tile, so the cost does
   not grow with the length of the page.

   The alpha is baked into the SVG rather than applied with opacity + mix-blend-mode. Blending was
   the first attempt and it silently did almost nothing: <body> with a background and a
   transparent <html> propagates that background to the canvas, and a blend against the canvas is
   not the blend you think you asked for. Measured, it moved the paper by one level out of 255.
   This version is ~4 levels with a stddev around 2.5 — visible at arm's length, gone if you look
   for it. Setting it here also means <body> must not carry a background of its own, or it would
   paint over the grain. */
html {
  -webkit-text-size-adjust: 100%;
  background-color: var(--paper);
  background-image: var(--grain);
  background-size: 200px 200px;
}

/* The scribble, as the ground of the whole page rather than a band at the top of it.
   ---------------------------------------------------------------------------------------------
   This is the change that made the site stop looking assembled. Design brief v2 arrived as a
   decoration applied to the first 230px of every page — a scribbled hero, then a hard horizontal
   edge, then several thousand pixels of untouched flat cream from the previous brief. Every page
   had that seam at the same height, and it read exactly like what it was.

   So it is a layer on <body>, sized to the document and scrolling with it. `position: absolute`
   rather than `fixed` for a reason worth keeping: a fixed layer holds still while the page moves
   past it, which reads as a screen artefact rather than as a sheet of paper.

   Two copies of the same tile at different sizes and offsets. One is enough for texture and not
   enough for a page: at 512px a single layer starts to announce its own period on a wide screen,
   and the second layer — a different size, so the two never come back into phase — hides it for
   the cost of one extra paint of a file already in cache.

   The tooth is a mask rather than part of the tile (tools/scribble_derive.py explains why). It
   repeats on a third period, which breaks the pattern once more, and it means the grain is 1:1
   with the device pixel however large the strokes are drawn. */
body::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("/scribble-tile.png"), url("/scribble-tile.png");
  background-size: 512px 512px, 331px 331px;
  background-position: 0 0, 137px 61px;
  -webkit-mask-image: url("/paper-tooth.png");
  mask-image: url("/paper-tooth.png");
  -webkit-mask-size: 148px 148px;
  mask-size: 148px 148px;
  /* The strokes have to read at a glance — that is the device the whole direction rests on, and
     at 0.48 against a tile of pale broad bands it had faded into the paper. What made it safe to
     raise is that the tile itself changed: it is bundles of thin saturated lines now, so the page
     gains colour and drawing without gaining *area*. Body text over the darkest pixel of the
     field still measures 8.5:1. */
  opacity: 0.84;
}

body {
  position: relative;
  margin: 0;
  color: var(--ink);
  font: 400 var(--t-body) / var(--lh-body) var(--font-sans);
  overflow-wrap: break-word;
}

/* The UA stylesheet's `[hidden] { display: none }` is a plain attribute selector, so any rule
   below that sets `display` on a class or an id silently beats it. That is not theoretical here:
   `#login-form { display: flex }` kept the passphrase form on screen after logging in, and
   `.gallery { display: grid }` rendered the empty grid while the artwork was still loading. */
[hidden] {
  display: none !important;
}

img {
  max-width: 100%;
}

h1,
h2,
h3 {
  font-weight: 400;
  text-wrap: balance;
}

p {
  margin: 0 0 var(--s4);
}

p:last-child {
  margin-bottom: 0;
}

a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

a:hover {
  text-decoration-thickness: 2px;
}

/* One focus treatment everywhere: an ink ring on the paper, clear of the element it marks. On a
   filled ink button the ring still lands on the paper around it, so it reads at 13:1 either way. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---- Layout primitives -------------------------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: var(--column);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.wrap-wide {
  width: 100%;
  max-width: var(--wide);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ---- The page grid -------------------------------------------------------------------------
   One container system for every page, replacing a per-page choice between `.wrap` (672px) and
   `.wrap-wide` (1216px) that had drifted into this: the title band was 1216 on all seven pages,
   and the body under it was 672 on five of them and 1216 on the other two. On a laptop that reads
   as some pages being full-width and others being phone-width — which is exactly what it was.

   Five tracks. The middle three are the reading column and the two margins either side of it that
   the marginalia live in; the outer two are the page gutter. So a block picks one of three widths
   by name and every page agrees about where they are:

     grid-column: col   — the reading measure, centred. The default, because most things are prose.
     grid-column: wide  — the full content frame, for grids, images and anything that would look
                          starved at the measure.
     grid-column: full  — edge to edge, for a bleed.

   The measure stays 672px, because that is what 65-odd characters of 17px type needs and a wider
   one would be worse to read. What changes is that everything which is *not* running text can now
   use the frame, so the page fills its width without the paragraphs getting longer. */
/* The column track subtracts the gutters. `min(var(--column), 100%)` was the first version and it
   is the reason phones could be dragged sideways: a percentage in `grid-template-columns` resolves
   against the *grid container*, which is the full viewport, so at 320px the column track was 320px
   wide — and the two gutter tracks have a 20px minimum each, giving a 360px minimum inside a 320px
   viewport. Every block on every page then started 20px in and ran 20px past the right edge.
   `100% - 2 * var(--gutter)` is what leaves room for them. */
.page {
  display: grid;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [wide-start] minmax(0, calc((var(--wide) - var(--column)) / 2 - var(--gutter)))
    [col-start] min(var(--column), 100% - 2 * var(--gutter))
    [col-end] minmax(0, calc((var(--wide) - var(--column)) / 2 - var(--gutter)))
    [wide-end] minmax(var(--gutter), 1fr)
    [full-end];
}

.page > * {
  grid-column: col;
  min-width: 0;
}

.page > .span-wide {
  grid-column: wide;
}

.page > .span-full {
  grid-column: full;
}

/* A centred block inside the column — the closing call to action on every page, which was
   shrink-to-fit and therefore left-adrift in a column it did not fill. */
.page > .cta,
.page > .centred {
  justify-self: center;
}

.measure {
  max-width: var(--measure);
}

.band {
  margin-block: var(--band);
}

/* Escapes the reading column's gutter so a picture reaches the edges of the screen. Done with
   negative margins rather than 100vw, because 100vw includes the scrollbar and overflows the
   page by its width on any desktop browser that reserves one. */
.bleed-gutter {
  margin-inline: calc(var(--gutter) * -1);
}

.more {
  margin-top: var(--s5);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- Masthead --------------------------------------------------------------------------- */

/* Above the scroll stage, which is pulled up underneath it so the first ground runs full-bleed
   (web/src/stage.js, measure). The masthead has no background of its own, so at scroll 0 the nav
   sits on the paper ground and reads normally; by the time a saturated ground has flooded, it has
   scrolled away. */
/* z-index 20, not 1. The marginalia are absolutely positioned with a z-index of their own and
   `.page-head` establishes no stacking context, so they compete with the masthead at the root
   level — and at 2 against 1 they won. On About that put "60 portraits and counting" across the
   open menu panel, which is also why the panel looked transparent: it was opaque, with a script
   line printed on top of it. */
.masthead {
  position: relative;
  z-index: 20;
  padding-block: var(--s3) var(--s4);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s5);
}

/* Wordmark left, nav taking the slack, the call to action hard right — the mockups'
   arrangement. Below 62em the nav and the button collapse behind a disclosure. */
.masthead > picture:first-child,
.masthead > a:first-child {
  margin-right: auto;
}

.masthead-cta {
  min-height: 2.5rem;
  padding-inline: var(--s5);
  font-size: var(--t-eyebrow);
}

.nav-toggle {
  position: relative;
}

/* An opaque control, not letters floating on whatever is behind them. It had no background at
   all, so on a scribbled ground — or over the hero photograph, which reaches the top of the page
   on a phone — "MENU" and its three rules were reading against her work. */
.nav-toggle-button {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  padding: var(--s2) var(--s4);
  background: var(--paper-raised);
  border: 1px solid var(--rule-input);
  border-radius: 999px;
  font-size: var(--t-eyebrow);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}

.nav-toggle[open] .nav-toggle-button {
  border-color: var(--berry);
  color: var(--berry);
}

/* The mark grew and the Menu button gained a border and padding, and at 320px the two together
   came to 315px inside 280 — so the disclosure wrapped to a line of its own again. Both give a
   little back here rather than the mark giving all of it: a header whose brand is 141px wide is
   worse than one whose button is compact. */
/* Top-aligned below the breakpoint where the mark is the tall stacked cut. Centred, the button
   floats halfway down a 140px mark, which is 45px below where every mockup puts it. */
@media (max-width: 61.99em) {
  /* The mark higher up the page, and the Menu button on the paw's line rather than the mark's.
     Top-aligning the two put Menu level with the *top* of the arc, which is a couple of
     centimetres of empty logo above the only other thing in the row. The offset is the distance
     from the top of the stacked lockup to the middle of the paw. */
  .masthead {
    align-items: flex-start;
    padding-block: var(--s2) var(--s2);
  }

  /* The paw's middle sits about 36% down the stacked mark — measured off the asset, where the
     paw occupies rows 185-585 of a 1062-tall crop. This puts the button's middle on that line
     rather than on the top of the arc. */
  .nav-toggle-button {
    margin-top: var(--s6);
  }
}

@media (max-width: 30em) {
  .masthead {
    column-gap: var(--s3);
  }

  .nav-toggle-button {
    padding-inline: var(--s3);
    font-size: 0.6875rem;
    letter-spacing: 0.1em;
  }

  .nav-toggle-bars {
    width: 1rem;
  }
}

.nav-toggle-button::-webkit-details-marker {
  display: none;
}

/* Three rules, drawn rather than a glyph, so it takes the ink token and never renders as a box
   on a device with no emoji for it. */
.nav-toggle-bars {
  display: block;
  width: 1.25rem;
  height: 2px;
  background: currentcolor;
  box-shadow:
    0 -6px 0 currentcolor,
    0 6px 0 currentcolor;
}

.nav-toggle-panel {
  position: absolute;
  right: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--s4);
  min-width: 14rem;
  margin-top: var(--s2);
  padding: var(--s5);
  /* --paper-raised is a sheet laid on the ground and is three levels from it, which is right for
     a field on a form and not nearly enough for a panel floating over content: the edge was
     invisible and the panel read as though the page showed through it. A firmer border and a
     shadow with some weight in it are what say "this is on top". */
  background: var(--paper-raised);
  border: 1px solid var(--rule-input);
  border-radius: 1rem;
  box-shadow: 0 18px 44px rgb(61 43 51 / 0.22);
}

.nav-toggle-panel .site-nav {
  flex-direction: column;
  align-items: flex-end;
  gap: var(--s3);
}

@media (min-width: 62em) {
  /* Unwrapped: the disclosure becomes a plain flex container, its trigger disappears, and the
     panel is just the nav and the button sitting in the header.
     Two mechanisms, because browsers hide a closed <details> two different ways. Newer engines
     use `::details-content { content-visibility: hidden }`, which no amount of `display` will
     override; older ones use `display: none` on the children. Both are unset here, so the nav is
     present on a desktop whichever engine is reading. */
  .nav-toggle::details-content {
    content-visibility: visible;
    block-size: auto;
  }

  .nav-toggle {
    display: contents;
  }

  .nav-toggle-button {
    display: none;
  }

  .nav-toggle-panel,
  .nav-toggle:not([open]) .nav-toggle-panel {
    display: flex;
    position: static;
    flex-direction: row;
    align-items: center;
    gap: var(--s5);
    min-width: 0;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
  }

  .nav-toggle-panel .site-nav {
    flex-direction: row;
    align-items: baseline;
    gap: var(--s5);
  }
}

/* The mark, at the size the mockups draw it: large, top left, the first thing on the page.
   Three steps rather than two, because the nav is six items plus a button and the mark can only
   take the room those leave. 52vw is what keeps the stacked cut and the Menu button on one row at
   320px — at a flat 13rem the two came to 298px inside 280 and the disclosure wrapped onto a line
   of its own, which put a left-aligned MENU under the mark and read as a broken header. */
.lockup {
  display: block;
  width: 100%;
  max-width: min(13rem, 46vw);
  height: auto;
}

@media (min-width: 62em) {
  .lockup {
    max-width: 15rem;
  }
}

@media (min-width: 80em) {
  .lockup {
    max-width: 19rem;
  }
}

/* The masthead carries all three cuts and shows one. Below 62em the stacked pair, which is the
   mobile mockup's composition; above it the inline cut — wordmark and "by Kat" on one line, no
   paw — which is what the desktop mockup draws, because a nav bar is forty pixels tall and the
   paw at that size is a smudge.
   
   `display: none` on the unused cut is deliberate rather than a <picture>: all three files are
   fetched by some part of the page anyway (the hero draws the arc and the lettering, the footer
   draws both again), so there is no request to save by choosing at fetch time, and a <picture>
   cannot switch between "one image" and "two stacked" at a breakpoint. */
.lockup-stack {
  display: block;
  min-width: 0;
}

.lockup-stack img {
  display: block;
  width: 100%;
  height: auto;
}

/* Scoped through .lockup-stack so these match the specificity of the `.lockup-stack img` rule
   above rather than losing to it — which they did, and a phone showed the mark twice. */
.lockup-stack .lockup-inline {
  display: none;
}

@media (min-width: 62em) {
  .lockup-stack .lockup-arc,
  .lockup-stack .lockup-text {
    display: none;
  }

  .lockup-stack .lockup-inline {
    display: block;
  }
}

.masthead a {
  display: inline-block;
  text-decoration: none;
}

/* Brief v2: small, uppercase, wide letterspacing, with an underline on the current page. The
   previous brief banned all-caps labels as a generic tell; v2 supersedes it and asks for exactly
   this, which in the mockups reads as a plate under an artwork rather than as a SaaS nav. */
.site-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s4) var(--s5);
  font-size: var(--t-eyebrow);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

.site-nav a {
  text-decoration: none;
  padding-block: var(--s1);
  border-bottom: 2px solid transparent;
  color: var(--ink);
  white-space: nowrap;
}

.site-nav a:hover {
  border-bottom-color: var(--berry);
}

.site-nav a[aria-current="page"] {
  border-bottom-color: var(--berry);
}

/* The last block on a page does not need to push away from the footer as well: the footer's own
   margin is the gap, and doubling it is where most of the dead space at the bottom came from. */
main > *:last-child,
main > *:last-child > *:last-child {
  margin-bottom: 0;
}

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

/* The full lockup — arc, paw, both lines — signing off the page. Centred, because a signature at
   the foot of a page is the one place symmetry is right, and given real space above it so it
   reads as an ending rather than as another row of furniture. */
/* 160px of nothing used to sit between the end of a page and this mark — 96px of margin plus
   64px of padding — and on top of whatever bottom margin the last section brought with it, which
   on About made about 200px of empty scribble under her story. A signature at the foot of a page
   wants air above it, not a field. */
.site-footer {
  margin-top: var(--s6);
  padding-block: var(--s6) var(--s7);
}

.site-footer-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s4);
  text-align: center;
}

/* The same wash the hero copy gets, for the same reason: the stacked mark has the rainbow arc in
   it, and an arc drawn in these pastels on a field of these pastels is a mark you have to look
   for. */
/* The overshoot is vertical only. It used to be `inset: -30% -22%`, and the horizontal half of
   that is what made every page on the site draggable sideways on a phone: an absolutely
   positioned box contributes to its ancestors' scrollable overflow, so a wash sticking out 22%
   either side of a 320px footer added 70px of scrollable width to the document — 22% of 375 is
   84px, which is exactly what a 375px phone showed. The footer is on every page, so every page
   had it.

   The same fade is achieved by widening the gradient's own ellipse instead, which paints outside
   nothing. */
.site-footer-inner::before {
  content: "";
  position: absolute;
  inset: -30% 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse 74% 50% at 50% 52%, var(--paper) 30%, rgb(248 241 231 / 0) 100%);
}

/* The mark, composed from the arc and the lettering rather than shipped as a third file that is
   the two of them stacked. They keep the source's own proportions because each is trimmed to the
   full 1230px width of the logo, so centring them reproduces the original layout exactly. */
.mark-stack {
  display: block;
  width: 13.125rem;
  max-width: 100%;
}

.mark-stack img {
  display: block;
  width: 100%;
  height: auto;
}

.site-footer-mark {
  display: block;
  text-decoration: none;
}

.site-footer-social {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s5);
  margin: 0;
  font-size: var(--t-small);
  color: var(--ink-soft);
}

.site-footer-social a {
  color: inherit;
}

/* ---- Brief v2: the visual system -------------------------------------------------------------
   Four devices carry the whole direction: the scribble, the eyebrow, the script marginalia, and
   the multicolour title. Everything else on the page is unchanged furniture. */

/* The scribble is the page (see body::before). What is left here is *emphasis*: the mockups run
   heavier at the top of a page and around the mark at the foot of it, and lighter through the
   middle where the work is. A section opts into another coat with .scribbled.

   Not a band with edges. The extra coat fades out top and bottom through the mask, so a section
   gets denser without drawing a line where it starts — which is the specific failure this
   replaces: an opaque scribbled hero over a flat cream page put the same hard horizontal edge at
   the same height on all seven pages. */
.scribbled {
  position: relative;
}

.scribbled::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("/scribble-tile.png");
  background-size: 412px 412px;
  background-position: 71px 23px;
  -webkit-mask-image: url("/paper-tooth.png"), linear-gradient(to bottom, transparent, #000 12%, #000 78%, transparent);
  mask-image: url("/paper-tooth.png"), linear-gradient(to bottom, transparent, #000 12%, #000 78%, transparent);
  -webkit-mask-size: 148px 148px, 100% 100%;
  mask-size: 148px 148px, 100% 100%;
  /* Both masks apply, and where two mask layers overlap the browser takes the union unless told
     otherwise — which would be the tooth OR the fade, i.e. neither. `intersect` is the multiply
     this wants. Where it is unsupported the layer simply keeps its tooth and loses the fade,
     which is a slightly firmer edge and not a broken one. */
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  opacity: 0.5;
}

/* Heavier again: the foot of the page and the 404, where the mockups let it run. */
.scribbled-strong::before {
  opacity: 0.75;
  background-size: 336px 336px;
}

/* The eyebrow: REAL PETS · REAL ART · LASTING MEMORIES. */
.eyebrow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  margin: 0 0 var(--s4);
  font-family: var(--font-sans);
  font-size: var(--t-eyebrow);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-soft);
  text-align: center;
}

/* The same drawn heart as the marginalia, at eyebrow size. It was U+2661, a font glyph, which is
   a different shape on every platform and none of them is the one in the mockups. */
.eyebrow-mark {
  display: inline-block;
  width: 0.85em;
  height: 0.88em;
  background: var(--berry);
  -webkit-mask: var(--heart) center / contain no-repeat;
  mask: var(--heart) center / contain no-repeat;
  translate: 0 0.08em;
}

/* One line at 320px. Thirty-nine tracked capitals do not fit a phone at the desktop setting, and
   an eyebrow that wraps stops being a rule over the page and becomes a sentence under it. */
@media (max-width: 30em) {
  .eyebrow {
    font-size: 0.6875rem;
    letter-spacing: 0.1em;
    gap: var(--s2);
  }
}

@media (max-width: 22.5em) {
  .eyebrow {
    font-size: 0.625rem;
    letter-spacing: 0.06em;
  }
}

/* The script. Marginalia and straplines only — never a price, never a label, never anything a
   customer has to read to act. */
/* Bigger and looser than it was. The face changed too (tools/fonts_build.py has the comparison),
   but half the note that came back was about *setting*: the mockup gives this line a lot of air,
   a line-height near one and a half, and a size that makes it the second-largest thing on the
   page after the mark. Set tight and small, the best script in the world reads as a caption. */
.script {
  font-family: var(--font-script);
  font-weight: 400;
  font-size: var(--t-script);
  line-height: 1.45;
  letter-spacing: 0.01em;
  color: var(--ink);
}

/* Marginalia: short handwritten lines, rotated a few degrees, beside the content.
   ---------------------------------------------------------------------------------------------
   These are always out of the flow and always at an edge. That is the whole idea of the device
   and the previous version gave it up too easily: below 76em it dropped the notes into the
   document, centred, so a phone met two script lines and two hearts stacked down the middle of
   the page before it reached the title. Read top to bottom they looked like the page's opening
   copy, which is the one thing they must not look like — they are an aside, and an aside in the
   middle of the column is not an aside.

   So: never in the flow. Where there is a real outer margin they sit in it, beside the content,
   as the mockups draw them. Where there is not — a phone, a narrow laptop — they tuck into the
   top corners of the section and the section reserves the room, which keeps them at an edge,
   keeps them rotated, and keeps them out of the reading column.

   Rotated a few degrees, never more: past about seven the line stops reading as handwriting in a
   margin and starts reading as a sticker. */
.margin-note {
  position: absolute;
  top: var(--s3);
  z-index: 2;
  width: min(10.5rem, 45vw);
  margin: 0;
  font-family: var(--font-script);
  font-weight: 400;
  font-size: 1.375rem;
  line-height: 1.35;
  color: var(--ink);
}

/* Below the width where there is a real outer margin, a note has nowhere to be *beside* anything
   — so it is not shown, rather than shown somewhere else. Every version that kept it on a phone
   ended up reserving a band across the top of the page for it: the note took the left half and
   the right half stayed empty, which is a dead row where the mockups have none. An aside with no
   margin to sit in is an aside that should wait for a wider screen.

   The home hero is the exception and keeps its own: it has a figure for the note to sit across,
   which is exactly what the mobile mockup draws. */
@media (max-width: 67.99em) {
  .margin-note {
    display: none;
  }

  .hero-v2 .margin-note {
    display: block;
  }
}

.margin-note-left {
  left: var(--s3);
  rotate: -7deg;
  text-align: left;
}

.margin-note-right {
  right: var(--s3);
  rotate: 6deg;
  text-align: right;
}

/* A page can carry two of these; a phone has room for one. The second is named at the call site
   rather than guessed at with :nth-of-type, which would count every sibling paragraph. */
@media (max-width: 47.99em) {
  .margin-note-secondary {
    display: none;
  }
}

.margin-note-heart::after {
  content: "";
  display: block;
  width: 1.35rem;
  height: 1.4rem;
  margin: var(--s2) 0 0;
  background: var(--berry);
  /* A drawn heart, as punctuation — an open outline in one stroke that crosses itself at the
     point and overshoots into the cleft, which is how the mockups draw it. It used to be a solid
     filled glyph, and a solid heart beside a monoline script reads as an icon rather than as
     something somebody drew.

     Still a mask rather than an emoji: it takes the berry token, it does not change shape between
     platforms, and it is not announced by a screen reader. The stroke is the alpha. */
  -webkit-mask: var(--heart) center / contain no-repeat;
  mask: var(--heart) center / contain no-repeat;
}

.margin-note-heart-right::after {
  margin-left: auto;
}

/* Once there is a real margin to put them in: down beside the content rather than up in the
   corner, wider, and a size that reads as her handwriting rather than as a caption. */
@media (min-width: 68em) {
  .margin-note {
    top: 42%;
    width: min(11.5rem, 14vw);
    font-size: 1.625rem;
  }

  .margin-note-left {
    left: var(--gutter);
  }

  .margin-note-right {
    right: var(--gutter);
  }

  .margin-note-heart::after {
    width: 1.35rem;
    height: 1.2rem;
  }
}

/* The multicolour page title. Each letter takes one of ten sampled hues, cycling; spaces are
   emitted outside the spans so a screen reader still hears words rather than letters, and the
   whole thing carries an aria-label with the plain text. */
.title-colour {
  font-family: var(--font-display);
  font-size: var(--t-title);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  margin: 0 0 var(--s3);
  text-align: center;
}

.title-colour span:nth-child(10n + 1) { color: var(--hue-1); }
.title-colour span:nth-child(10n + 2) { color: var(--hue-2); }
.title-colour span:nth-child(10n + 3) { color: var(--hue-3); }
.title-colour span:nth-child(10n + 4) { color: var(--hue-4); }
.title-colour span:nth-child(10n + 5) { color: var(--hue-5); }
.title-colour span:nth-child(10n + 6) { color: var(--hue-6); }
.title-colour span:nth-child(10n + 7) { color: var(--hue-7); }
.title-colour span:nth-child(10n + 8) { color: var(--hue-8); }
.title-colour span:nth-child(10n + 9) { color: var(--hue-9); }
.title-colour span:nth-child(10n) { color: var(--hue-10); }

/* A page's opening block: eyebrow, coloured title, a line of script under it, and the two
   marginalia in the outer margins. The top padding is what gives those somewhere to sit on a
   narrow screen — they are absolutely positioned into it, so it is reserved space rather than
   decoration. */
/* No reservation any more. This used to open with 8.75rem so a margin note could be tucked into
   the top corner on a narrow screen; the notes do not appear below 68em at all now, so that was
   140px of held-open nothing at the top of five pages. */
.page-head {
  position: relative;
  padding-block: var(--s7) var(--s6);
  text-align: center;
}

.page-head .script {
  color: var(--ink);
  margin: 0;
}

/* The count under a gallery title, and its equivalents. Script, because it is her voice about
   the work rather than a label on it. */
.page-head-count {
  margin: 0;
}

/* Filter pills — the same pill as the button, outlined when inactive, filled when active. */
/* Wraps rather than scrolls. Five pills do not fit 375px, and a horizontal scroller would hide
   two facets off the right edge with nothing to say they are there — every facet being visible is
   the whole job of a filter bar, so a second row is the right answer and the row gap is what
   makes it read as one.

   It had none: a superseded v1 rule further down this file set `gap: 0 var(--s4)`, so the two
   rows sat flush against each other with 0px between them. That rule is gone. */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s3);
  margin: 0 0 var(--band);
}

.filter {
  display: inline-flex;
  align-items: baseline;
  gap: 0.45em;
  flex: 0 0 auto;
  min-height: var(--tap);
  padding: var(--s2) var(--s5);
  border: 1px solid var(--berry);
  border-radius: 999px;
  color: var(--berry);
  font-size: var(--t-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  text-decoration: none;
  white-space: nowrap;
}

@media (max-width: 30em) {
  .filter {
    padding-inline: var(--s4);
    letter-spacing: 0.08em;
  }
}

.filter:hover {
  background: var(--paper-raised);
  text-decoration: none;
}

.filter[aria-current="page"] {
  background: var(--berry);
  border-color: var(--berry);
  color: #fff;
}

.filter-count {
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

/* ---- Headings ---------------------------------------------------------------------------- */

/* One display face, three sizes, and nothing else set in it. Negative tracking at the two large
   steps only: Fraunces is drawn tight already, and pulling a 20px heading in as well would close
   the counters. */
.page-title {
  font-family: var(--font-display);
  font-size: var(--t-title);
  font-weight: 800;
  line-height: var(--lh-snug);
  letter-spacing: -0.015em;
  margin: 0 0 var(--s5);
}

.statement {
  font-family: var(--font-display);
  font-size: var(--t-hero);
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: -0.022em;
  margin: 0 0 var(--s5);
  max-width: 15ch;
}

/* Section headings carry the paw. It is the one identity mark that survives being 26px tall, it
   marks the start of a band without a rule or a colour, and it appears at most three times on a
   page — which is the line between a repeating beat and wallpaper. Decorative, so it is a
   background rather than an <img>: nothing for a screen reader to announce, one cached request
   for every use on the site. */
.section-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--font-display);
  font-size: var(--t-head);
  font-weight: 800;
  line-height: var(--lh-snug);
  letter-spacing: -0.006em;
  margin: 0 0 var(--s5);
}

.section-head::before {
  content: "";
  flex: 0 0 auto;
  width: 1.5em;
  height: 1.24em;
  background: url("/brand/paw.png") left center / contain no-repeat;
  /* The paw leans left in its own bounding box; nudging it down sits it on the baseline of the
     word rather than floating above the cap height. */
  translate: 0 0.06em;
}

.lead {
  font-family: var(--font-serif);
  font-size: var(--t-lead);
  line-height: 1.5;
}

.meta {
  color: var(--ink-soft);
  font-size: var(--t-small);
}

.micro {
  color: var(--ink-soft);
  font-size: var(--t-micro);
  line-height: var(--lh-snug);
}

.muted {
  color: var(--ink-soft);
  font-weight: 400;
}

/* ---- Buttons and links -------------------------------------------------------------------- */

/* One filled action per screen. Everything else is an outlined or plain control, so the primary
   action never has to compete with a secondary one wearing the same clothes. */
/* Brief v2: fully rounded, berry fill, white uppercase letterspaced label. The pill shape is
   also what the filter pills use, so the two read as the same family of control. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: var(--s3) var(--s6);
  border: 1px solid var(--berry);
  border-radius: 999px;
  background: var(--berry);
  color: #fff;
  font: 600 var(--t-small) / 1.2 var(--font-sans);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

.btn:hover {
  background: var(--berry-deep);
  border-color: var(--berry-deep);
}

.btn-block {
  display: flex;
  width: 100%;
}

/* A standalone call to action: full width and thumb-height on a phone, as the brief requires,
   but not 672px wide on a laptop. Form buttons keep .btn-block, because a submit button that
   matches the width of the fields above it is right at any size. */
.cta {
  display: flex;
  width: 100%;
}

@media (min-width: 48em) {
  .cta {
    display: inline-flex;
    width: auto;
    min-width: 16rem;
  }
}

/* Secondary: same shape, no fill. Used for "Add photos" and every admin action, so the eye can
   tell in one pass which control commits something. */
.btn-quiet {
  background: none;
  color: var(--ink);
  border-color: var(--rule-input);
  letter-spacing: 0.08em;
}

.btn-quiet:hover {
  background: var(--paper-raised);
  border-color: var(--ink);
  color: var(--ink);
}

.btn-small {
  min-height: 2.25rem;
  padding: var(--s2) var(--s3);
  font-size: var(--t-small);
  font-weight: 400;
}

.btn-danger {
  background: none;
  color: var(--alert);
  border-color: var(--alert);
}

.btn-danger:hover {
  background: none;
  border-color: var(--alert);
  color: var(--alert);
  text-decoration: underline;
}

.link-more {
  display: inline-block;
  font-size: var(--t-small);
  text-decoration-thickness: 1px;
}

/* ---- Forms -------------------------------------------------------------------------------- */

/* Fields are ruled lines on the paper, not boxes floating on it — one hairline underneath at
   3.2:1, which is what a control boundary has to clear when it's the only thing identifying the
   control. Boxes were the previous look and they made every page read like a settings screen. */
.field {
  display: block;
  margin-bottom: var(--s4);
}

.field-label,
.form-section label:not(.checkbox-row):not(.tier-card),
.artwork-form label,
#login-form label,
.setting-field-label {
  display: block;
  font-size: var(--t-micro);
  color: var(--ink-soft);
  margin-bottom: var(--s1);
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
textarea,
select {
  font: inherit;
  font-size: 1rem; /* 16px floor — iOS zooms the page on focus below this */
  width: 100%;
  padding: var(--s2) 0;
  color: var(--ink);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--rule-input);
  border-radius: 0;
  min-height: var(--tap);
}

textarea {
  min-height: 5rem;
  line-height: var(--lh-body);
  resize: vertical;
}

input:focus,
textarea:focus {
  border-bottom-color: var(--ink);
  border-bottom-width: 2px;
  padding-bottom: calc(var(--s2) - 1px);
}

::placeholder {
  color: var(--ink-soft);
  opacity: 1;
}

.field-hint {
  color: var(--ink-soft);
  font-size: var(--t-small);
  margin: 0 0 var(--s4);
}

/* align-items:center put the box against the middle of a wrapped label — at 320px the shipping
   checkbox sat beside line two of three. It aligns to the first line instead. */
.checkbox-row {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  font-size: var(--t-small);
  line-height: var(--lh-snug);
  color: var(--ink);
  cursor: pointer;
}

.checkbox-row input {
  flex: 0 0 auto;
  width: 1.125rem;
  height: 1.125rem;
  margin: 0.1em 0 0;
  accent-color: var(--ink);
}

.form-error {
  color: var(--alert);
  font-size: var(--t-small);
  margin: var(--s3) 0 0;
}

.form-saved {
  color: var(--ink-soft);
  font-size: var(--t-small);
  margin: var(--s3) 0 0;
}

/* ---- Home: the hero (brief v2) --------------------------------------------------------------
   Photograph right, wordmark and paw stacked left, script strapline, call to action, eyebrow at
   the foot. Scribble across the field — and the two columns sit in the middle third of it, which
   is the part the generator leaves clear, so nothing busy ends up behind the photograph. */

/* Tight, because the mockup is tight: mark, figure, strapline and call to action inside one
   screen. This block used to reserve 8.75rem at the top for the margin note, the way .page-head
   still does — but the hero has a figure for the note to sit against, exactly as the mockup draws
   it, so the reservation was 140px of nothing above a photograph. */
.hero-v2 {
  position: relative;
  padding-block: var(--s1) var(--s6);
}

@media (min-width: 68em) {
  .hero-v2 {
    padding-block: 0 var(--s7);
  }
}

/* Every selector here is scoped to .hero-v2 rather than written bare, and that is not decoration:
   this block sits above the base rules it overrides, media queries add no specificity, and a bare
   `.hero-v2-inner` here loses to the identical `.hero-v2-inner` two hundred lines further down on
   source order alone. The gap silently stayed at 24px the first time. */
@media (max-width: 55.99em) {
  /* The figure runs up under the mark, which is where the mockup puts it — the note then sits
     across the top-left of the drawing rather than in a gap of its own. */
  .hero-v2 .hero-v2-figure {
    margin-top: calc(var(--s7) * -1);
  }

  .hero-v2 .hero-v2-copy {
    padding: var(--s2) var(--s4) 0;
  }

  .hero-v2 .hero-v2-inner {
    gap: var(--s2);
  }

  .hero-v2 .hero-v2-trade {
    margin-bottom: var(--s3);
  }

  .hero-v2 .hero-v2-strapline {
    margin-bottom: var(--s2);
  }

  .hero-v2 .hero-v2-cta {
    margin-bottom: var(--s4);
  }

  /* Left, over the clear paper at the drawing's edge — the mobile mockup's placement, and the
     only readable one: on the right of this figure is her hair, and dark script on dark hair is
     not a margin note, it is a smudge. */
  /* Tucked in beside Ceelo rather than up in the corner away from him — the mockup has it
     touching the drawing. */
  .hero-v2 .margin-note-right {
    top: var(--s3);
    right: auto;
    left: var(--s4);
    width: min(6rem, 26vw);
    rotate: -7deg;
    text-align: left;
  }

  .hero-v2 .margin-note-heart-right::after {
    margin-left: 0;
  }

  /* The cream wash is there to stop the field competing with the wordmark, which is itself a
     chalk drawing in the same pastels. Below 62em the hero has no wordmark — the masthead
     carries it — so the wash was protecting nothing and erasing the scribble behind the
     strapline and the button, which is the half of the mockup that has the most of it. */
  .hero-v2 .hero-v2-copy::before {
    display: none;
  }

  /* One line, as the mockup sets it. At the shared size the last word wrapped, and a rule under
     a page that runs to two lines stops being a rule. */
  .hero-v2 .eyebrow {
    font-size: 0.625rem;
    letter-spacing: 0.06em;
  }
}

.hero-v2-inner {
  display: grid;
  gap: var(--s5);
  align-items: center;
}

/* The scribble is heaviest at the edges, and on a wide screen the copy column sits in the left
   quarter — which is one of them. A soft cream wash behind the column clears it, with no edge
   you can see. This is the brief's hardest rule applied to type as well as to paintings: nothing
   busy behind anything anyone has to read. */
.hero-v2-copy {
  position: relative;
  display: grid;
  justify-items: center;
  text-align: center;
  padding: var(--s5) var(--s4);
}

/* A cream wash under the lettering. The scribble is allowed everywhere else — it is pale enough
   that plum type over it still clears 11:1 — but the wordmark is itself a drawing in the same
   pastels, and two chalk drawings on top of each other is the one place the field genuinely
   competes.

   On the element it was a visible rectangle: a radial gradient is inscribed in its box, so it
   reaches transparent at the corners while the middle of each edge is still half opaque, and
   what you see is four straight seams. Painted by a pseudo-element that overshoots the box on
   every side, the fade finishes in open paper and there is no edge to see. */
/* Vertical overshoot only, for the same reason as the footer's — see there. */
.hero-v2-copy::before {
  content: "";
  position: absolute;
  inset: -14% 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse 68% 50% at 50% 48%, var(--paper) 34%, rgb(248 241 231 / 0) 100%);
}

/* The mark, at the size the mockups give it: it is the largest thing on the page after the
   photograph, not a logo sitting above some copy. */
/* The hero's mark, in two pieces. The paw and its arc keep the size they had; the lettering under
   them comes down from 448px to 360px, because at the old size it was the loudest thing on the
   page and the photograph beside it is what should be.

   The mockup makes the same call harder still — its paw is 18% of the wordmark's width where ours
   is 31% — but it is the paw that is wanted at full strength here, so the ratio moves by shrinking
   the lettering rather than by growing the paw.

   Both are hidden below the breakpoint where the masthead switches to the inline cut. Above it
   the masthead is a nav-bar mark and the hero carries the big one; below it the masthead IS the
   big one, and drawing it twice gave a phone two wordmarks in the first screen and a half. */
.hero-v2-arc {
  display: none;
  width: min(100%, 27.5rem);
  height: auto;
  margin-bottom: var(--s1);
}

.hero-v2-wordmark {
  display: none;
  width: min(100%, 22.5rem);
  height: auto;
  margin-bottom: var(--s3);
}

/* Uppercase and letterspaced, with a rule either side, as the mockups draw it. */
.hero-v2-trade {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin: 0 0 var(--s4);
  font-size: var(--t-eyebrow);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-soft);
  text-align: center;
}

.hero-v2-trade::before,
.hero-v2-trade::after {
  content: "";
  flex: 1 1 1.5rem;
  height: 1px;
  background: var(--rule);
}

/* Wide enough for the longest sentence; the break between sentences is put in by
   `straplineLines()` in functions/index.ts, which explains why no measure could do it. */
.hero-v2-strapline {
  max-width: 22ch;
  margin: 0 0 var(--s5);
  font-size: var(--t-script-lead);
}

.hero-v2-cta {
  margin-bottom: var(--s5);
}

.hero-photo {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper-sunk);
}

.hero-photo-empty {
  margin: 0;
  padding: var(--s8) var(--s5);
  text-align: center;
  color: var(--ink-soft);
  background: var(--paper-raised);
}

/* The mockup's phone layout: the mark first, then the photograph under it. The brief's prose
   guessed the other way round — photograph first, "the face is what stops a thumb" — and the
   mockup it was describing does the opposite, for a reason you can see once the two are side by
   side: the masthead's own lockup is directly above, so leading with the photograph puts two
   marks either side of it and the page opens twice. Mockup wins; it is the drawing. */
/* The photograph runs off the outer edge rather than sitting in a box on the page, which is how
   every mockup crops it — on a phone off both edges, on a desktop off the right.

   The negative margin is the gutter plus whatever is left over between the content box and the
   viewport, which `max()` works out at render: 0 on a narrow screen, and half the difference on
   a wide one. Without the `max()` the photograph would pull off the edge of a laptop and stop
   short of it on a monitor. */
/* The mockup's phone order: the mark (which is the masthead's, on a phone), then the photograph,
   then the trade line, the strapline, the button and the eyebrow. The figure has to lead the copy
   block for that, because the copy block is one grid item containing all four of those. */
.hero-v2-figure {
  order: -1;
  margin: 0 calc(var(--gutter) * -1);
}

.hero-v2-figure img,
.hero-v2-figure picture {
  display: block;
}

@media (min-width: 56em) {
  /* Pulled up into the masthead's bottom padding, which is empty space to the right of the nav.
     The mockup starts the figure at y=84 on a 1440-wide page, just under the nav row; top-aligned
     in the grid it landed at 111, which is the masthead's full height. */
  .hero-v2-figure {
    order: 0;
    margin-inline: 0;
    margin-top: calc(var(--s5) * -1);
  }
}

/* A chalk edge rather than a cut one. The mockups do not put a photographic rectangle on the
   paper; the picture fades into the field at its border the way a pastel does. Three percent is
   enough to lose the corner without softening a face. Where masks are unsupported this is a
   plain rectangle, which is where it started.

   This is for an uploaded photograph. The drawn cut-out below has a real matte and overrides it. */
.hero-photo {
  -webkit-mask-image:
    linear-gradient(to right, transparent 0, #000 3%, #000 97%, transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 2%, #000 98%, transparent 100%);
  mask-image:
    linear-gradient(to right, transparent 0, #000 3%, #000 97%, transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 2%, #000 98%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* The drawn portrait: a real matte, so the pencil strokes fade into the page instead of stopping
   at an edge. No plate under it either — a cut-out on a coloured rectangle is a sticker.

   Where `mask-image` is unsupported this shows the JPEG whole, which is the drawing composited
   onto the site's own paper (tools/hero_derive.py mattes it onto --paper for exactly this
   reason). That degrades to a soft-edged rectangle on the right colour, not to a grey box. */
.hero-cutout {
  background: none;
  -webkit-mask-image: url("/hero/kat-ceelo-alpha.png");
  mask-image: url("/hero/kat-ceelo-alpha.png");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

@media (min-width: 62em) {
  .hero-v2-arc,
  .hero-v2-wordmark {
    display: block;
  }
}

@media (min-width: 56em) {
  /* A right margin the note can live in. Without it the figure runs to the frame's edge and
     "Pets make life brighter" is printed over Kat's hair — which is where it was. */
  /* Top-aligned, not centred. Centring the two columns against each other pushed the figure a
     couple of hundred pixels down the page — the mockup starts it just under the nav and lets it
     run off the bottom, and the whole hero sits that much higher for it. */
  .hero-v2-inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr);
    gap: var(--s5);
    align-items: start;
    max-width: calc(var(--wide) - 8rem);
    margin-right: 8rem;
  }

  /* Centred under the mark, not flush left beneath it. Left-aligning put the strapline's optical
     centre 80px to the left of the wordmark's and hung the button off the same edge; the mockup
     centres both under the mark. */
  .hero-v2-copy {
    justify-items: center;
    text-align: center;
    padding-top: var(--s4);
  }

  /* Centred with the rest of the copy column. This used to be flush left, and unscoped, which
     left-aligned the eyebrow at the top of every page on the site while the title under it stayed
     centred — one of the seams. */
  .hero-v2 .hero-v2-trade,
  .hero-v2 .eyebrow {
    justify-content: center;
  }

}

/* ---- Home: the scroll stage ----------------------------------------------------------------
   One portrait, three grounds, then a frame. Ported from the Claude Design handoff; the geometry
   and easing are theirs, the type and palette are ours, and Act 1 (a build sequence over a
   derived graphite stand-in) is not here at all — see docs/decisions.md.

   The whole effect is `transform` and `opacity` driven by scroll position, in web/src/stage.js.
   Nothing below animates on its own and nothing here can trigger layout.

   The default state of this stylesheet is the STILL layout: three stacked full-screen panels,
   server-rendered, no JavaScript required. `stage.js` adds `.stage-js` to <html> and only then
   does the track appear. That ordering is deliberate — if the script fails, or never runs, or
   the reader asked for reduced motion, the page is the still panels and they are a real layout
   rather than a broken one. */

:root {
  /* Must match SOFT in stage.js and the three mask gradients below. */
  --stage-soft: 90px;
  /* Sampled from her own paint, not chosen: the red board and the gold leaf. Near-black type
     tuned per ground — each clears 4.5:1 against the field it sits on. */
  --stage-word-paper: #26241f;
  --stage-word-colour: #1c1512;
  --stage-word-gold: #231a09;
}

.stage-track {
  display: none; /* the motion version; revealed only once stage.js is running */
  position: relative;
  height: 340vh; /* stage.js overwrites this in px against the real viewport height */
}

html.stage-js .stage-track {
  display: block;
}

html.stage-js .stage-still {
  display: none;
}

/* Reduced motion wins over the script in both directions: the stylesheet refuses the track, and
   stage.js refuses to start. Either alone would be enough; both is cheap. */
@media (prefers-reduced-motion: reduce) {
  html.stage-js .stage-track {
    display: none;
  }

  html.stage-js .stage-still {
    display: block;
  }
}

.stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  /* Her paper, in the site's own hue and the site's own grain — not the harder cut. At 0.55 the
     strong grain greys the panel by a few levels, which was invisible against a quiet page and
     became a box the moment the field around it got vivid. */
  background: var(--paper) var(--grain) 0 0 / 200px 200px;
}

/* The field, bleeding into the stage's margins and cleared out of its middle.
   ---------------------------------------------------------------------------------------------
   The stage read as a box, and the reason was not its edges: it was that a full-screen panel of
   plain paper is a different *surface* from the page it interrupts, now that the page is a
   scribbled one. Giving it the same field is what makes it part of the page.

   The mask is the brief's hard rule, made structural rather than hoped for: it is transparent
   across the middle — where the portrait lives for the whole sequence — and only reaches full
   strength at the margins. The scribble cannot get behind the painting because there is no mask
   there to let it.

   First child, so the flooding grounds and the portrait both paint over it: during the red and
   gold acts the field is covered entirely, which is correct — those grounds are the subject. */
.stage::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: url("/scribble-tile.png") 0 0 / 430px 430px;
  -webkit-mask-image: radial-gradient(ellipse 48% 44% at 50% 46%, transparent 52%, #000 100%);
  mask-image: radial-gradient(ellipse 48% 44% at 50% 46%, transparent 52%, #000 100%);
  opacity: 0.85;
}

/* Each ground is extended above the stage by the soft edge and masked at the top, so the
   flood's leading edge is a gradient rather than a line. */
.stage-ground {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--stage-soft) * -1);
  bottom: 0;
  overflow: hidden;
  transform: translate3d(0, 100%, 0);
  will-change: transform;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--stage-soft));
  mask-image: linear-gradient(to bottom, transparent 0, #000 var(--stage-soft));
}

.stage-ground > * {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  will-change: transform;
}

.stage-ground-red > * {
  background-image: url("/stage/field-red.jpg");
}

.stage-ground-gold > * {
  background-image: url("/stage/field-gold.jpg");
}

.stage-pad {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s7) var(--s5) var(--s9);
}

/* Not taste: the mount is sized by percentage insets off this box, making it about 1.34x this
   width, and it must fit the narrowest viewport at rest. 66vw is 248px at 375px, so the mount is
   332px inside 375 with margin either side. The stage is overflow:hidden, so getting this wrong
   clips the mount's borders silently rather than producing a scrollbar. If the width, the insets
   or the 320px floor change, redo the arithmetic. */
.stage-portrait {
  position: relative;
  width: min(66vw, 442px, 50svh);
  transform-origin: 50% 46%;
  will-change: transform;
}

.stage-portrait img {
  display: block;
  width: 100%;
  height: auto;
  /* The cut-out ships as a JPEG flattened onto paper plus a one-colour alpha PNG, rather than a
     photographic PNG with an alpha channel — about half the bytes for the same result. */
  -webkit-mask-image: url("/stage/ceelo-alpha.png");
  mask-image: url("/stage/ceelo-alpha.png");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.stage-mount {
  position: absolute;
  top: -14%;
  right: -17%;
  bottom: -17%;
  left: -17%;
  /* Drawn once at any frame size — an earlier tiled version showed a seam on all four sides. */
  background: url("/stage/mount.jpg") center / cover no-repeat;
  opacity: 0;
  transform: scale(0.94);
  will-change: transform, opacity;
  box-shadow:
    0 20px 48px rgb(28 31 33 / 0.28),
    0 2px 6px rgb(28 31 33 / 0.2);
}

.stage-mount-window {
  position: absolute;
  inset: 16px;
  background: url("/stage/field-gold.jpg") center / cover;
  box-shadow: 0 0 0 1px rgb(44 32 10 / 0.2) inset;
}

/* Four states stacked in one grid cell and cross-faded. The handoff pinned both sizes and said
   neither may wrap; ours has to tolerate wrapping, because the first line is `home_statement`
   from admin and nobody can promise free text fits on one line at 320px. Anchored to the bottom
   of the cell so a second line grows upwards and the ground words never move. */
.stage-lines {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0 var(--s5) var(--s6);
  display: grid;
  justify-items: center;
  align-items: end;
  text-align: center;
  pointer-events: none;
}

.stage-lines > * {
  grid-area: 1 / 1;
  margin: 0;
}

.stage-tag {
  font-family: var(--font-sans);
  font-size: var(--t-lead);
  font-weight: 700;
  line-height: var(--lh-snug);
  letter-spacing: -0.01em;
  max-width: 20ch;
  text-wrap: balance;
  color: var(--stage-word-paper);
}

.stage-word {
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 7vw, 2.5rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.015em;
  opacity: 0;
}

.stage-word-paper {
  color: var(--stage-word-paper);
}

.stage-word-colour {
  color: var(--stage-word-colour);
}

.stage-word-gold {
  color: var(--stage-word-gold);
}

/* ---- Home: the still version ----------------------------------------------------------------
   Three stacked full-screen panels — paper, colour, gold — each with the portrait and its line.
   Same content, same order, no sticky, no cross-fades. This is what the page ships as, and what
   a reader gets with JavaScript off or reduced motion on. */

.stage-still-panel {
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: var(--s7) var(--s5);
}

.stage-still-panel > div {
  display: grid;
  justify-items: center;
  gap: var(--s5);
}

/* The still panels have no cross-fade to drive, so their words are simply on. */
.stage-still-panel .stage-word {
  opacity: 1;
}

.stage-still-panel img {
  display: block;
  width: clamp(15rem, 66vw, 24.5rem);
  height: auto;
  -webkit-mask-image: url("/stage/ceelo-alpha.png");
  mask-image: url("/stage/ceelo-alpha.png");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.stage-still-paper {
  position: relative;
  background: var(--paper) var(--grain) 0 0 / 200px 200px;
}

/* The still version gets the same treatment; it is what the page IS without JavaScript. */
.stage-still-paper::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: url("/scribble-tile.png") 0 0 / 430px 430px;
  -webkit-mask-image: radial-gradient(ellipse 48% 44% at 50% 46%, transparent 52%, #000 100%);
  mask-image: radial-gradient(ellipse 48% 44% at 50% 46%, transparent 52%, #000 100%);
  opacity: 0.85;
}

.stage-still-colour {
  background: url("/stage/field-red.jpg") center / cover;
}

.stage-still-gold {
  background: url("/stage/field-gold.jpg") center / cover;
}

/* ---- Home ---------------------------------------------------------------------------------- */

/* One photograph of one painting. No overlaid headline, no scrim, no rounded corners, no
   carousel — the brief is explicit, and the picture is strong enough to carry the page.
   On a phone it bleeds to all four edges at its real ratio. */
.hero-figure {
  margin: 0;
}

.hero-image {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper-sunk);
}

.hero-empty {
  margin: 0;
  padding: var(--s9) var(--gutter);
  text-align: center;
  color: var(--ink-soft);
  background: var(--paper-raised);
}

/* The statement block under the stage: its button is the page's call to action and belongs on the
   page's axis, centred, not shrink-to-fit against the left edge of a column. `.page > .cta`
   handles the centring; this is the block's own spacing. */
.hero-copy {
  padding-top: var(--band);
  text-align: center;
}

.hero-copy .availability {
  text-align: center;
}

.availability {
  margin: var(--s3) 0 0;
  color: var(--ink-soft);
  font-size: var(--t-small);
}

/* Desktop gets a real composition rather than the phone layout with margins: the painting keeps
   its true ratio beside the statement instead of being cropped into a wide band. */
@media (min-width: 56em) {
  .hero {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    align-items: center;
    gap: var(--s8);
    max-width: var(--wide);
    margin-inline: auto;
    padding-inline: var(--gutter);
  }

  /* Inside the split hero the copy is already in a grid track, so it drops the reading column's
     own width cap and gutter and just fills its half. */
  .hero .hero-copy {
    max-width: none;
    padding: 0;
  }
}

/* Her voice, directly under the Start to finish strip. It used to open with a full --band of
   padding on top of the strip's own bottom margin, which put about 120px of paper between the
   last frame of the sequence and the sentence that explains it. */
.home-intro {
  max-width: var(--measure);
}

.home-voice {
  padding-top: 0;
}

.home-voice .lead {
  margin-top: 0;
}

/* ---- Gallery: the tile grid (brief v2) ------------------------------------------------------
   The brief asks for the mockup's dense packed grid AND keeps the older rule against cropping to
   square, and resolves them itself: the tile is uniform, the artwork is not cropped. So each
   piece sits whole inside a square cell with `object-fit: contain`, on a mat of her own scribble.
   A tall piece and a wide one occupy the same cell and neither loses an ear.

   Ten columns as drawn is too many for work this detailed — six at desktop, three on a phone,
   with close gutters. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s2);
  margin-bottom: var(--band);
}

@media (min-width: 40em) {
  .tile-grid {
    grid-template-columns: repeat(4, 1fr);
    gap: var(--s3);
  }
}

@media (min-width: 62em) {
  .tile-grid {
    grid-template-columns: repeat(6, 1fr);
  }
}

/* The home page's four. Two across on a phone rather than three, because four tiles at a sixth
   of the width is a contact sheet and the point of this strip is that you can see the work. */
.tile-grid-strip {
  grid-template-columns: repeat(2, 1fr);
}

@media (min-width: 40em) {
  .tile-grid-strip {
    grid-template-columns: repeat(4, 1fr);
  }
}

.tile {
  position: relative;
  display: block;
  aspect-ratio: 1;
  min-width: 0;
  border-radius: 0.4rem;
  overflow: hidden;
  background: var(--paper-raised);
  text-decoration: none;
}

/* The mat. This is the one place the scribble comes near a painting, and it is behind the
   letterboxing rather than under the work: the artwork is opaque and sits on top of it.

   The tile's own fill is opaque cream, which is what keeps the brief's hardest rule true by
   construction rather than by z-index — whatever the page's field is doing, it stops at the edge
   of a tile. The scribble is a separate layer here only so it can carry an opacity: at full
   strength against sixty small squares it stopped being a mat and started being a pattern.

   Each mat has to catch a different part of the field — sixty copies of the same square is
   exactly how a texture reads as wallpaper — so the position is cycled below on nth-child.
   `background-attachment: fixed` would do the same job in one line and was the first attempt; it
   is a scroll-linked repaint on sixty elements at once, on a page built for the slowest phones
   this site expects, and a static offset costs nothing. */
.tile::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("/scribble-tile.png") 0 0 / 268px 268px;
  /* Down as the page's field went up. The mat is behind a painting and takes the opposite
     instruction from everywhere else: the brief's one hard rule is that the scribble never
     competes with her work, so a stronger tile has to be a *quieter* mat, not a louder one. */
  opacity: 0.3;
}

/* Eleven offsets against a six-column grid: co-prime, so the pattern does not line up with the
   columns and no two neighbours — across or down — share a mat. */
.tile:nth-child(11n + 1)::before  { background-position: 0 0; }
.tile:nth-child(11n + 2)::before  { background-position: -97px -211px; }
.tile:nth-child(11n + 3)::before  { background-position: -184px -46px; }
.tile:nth-child(11n + 4)::before  { background-position: -31px -139px; }
.tile:nth-child(11n + 5)::before  { background-position: -223px -178px; }
.tile:nth-child(11n + 6)::before  { background-position: -140px -95px; }
.tile:nth-child(11n + 7)::before  { background-position: -62px -244px; }
.tile:nth-child(11n + 8)::before  { background-position: -205px -119px; }
.tile:nth-child(11n + 9)::before  { background-position: -118px -18px; }
.tile:nth-child(11n + 10)::before { background-position: -251px -66px; }
.tile:nth-child(11n)::before      { background-position: -77px -160px; }

/* The home strip's tiles are half as wide again as the gallery's, so the same 268px mat reads at
   half the relative scale and the strokes come out as stripes rather than as a wash. Scaled with
   the tile, not with the page. */
.tile-grid-strip .tile::before {
  background-size: 430px 430px;
  opacity: 0.26;
}

.tile img {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 6%;
}

/* The title is the link's accessible name via the image alt; this is a hover affordance for a
   mouse and is hidden from the accessibility tree. */
.tile-caption {
  position: absolute;
  inset: auto 0 0;
  padding: var(--s4) var(--s2) var(--s2);
  background: linear-gradient(to top, rgb(61 43 51 / 0.72), rgb(61 43 51 / 0));
  color: #fff;
  font-size: var(--t-micro);
  line-height: var(--lh-snug);
  opacity: 0;
  transition: opacity 140ms ease-out;
}

.tile:hover .tile-caption,
.tile:focus-visible .tile-caption {
  opacity: 1;
}

.tile:focus-visible {
  outline: 2px solid var(--berry);
  outline-offset: 3px;
}

/* The block the gallery ends on. */
.order-block {
  position: relative;
  padding-block: 8.75rem var(--band);
  margin-top: var(--band);
}

@media (min-width: 68em) {
  .order-block {
    padding-block: var(--band);
  }
}

.order-block-inner {
  display: grid;
  gap: var(--s5);
  justify-items: center;
  text-align: center;
}

.order-block-figure {
  margin: 0;
  max-width: 18rem;
}

.order-block-figure img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 0.4rem;
}

.order-block-question {
  margin: 0 0 var(--s3);
}

@media (min-width: 56em) {
  .order-block-inner {
    grid-template-columns: auto 1fr;
    align-items: center;
    justify-items: start;
    text-align: left;
    gap: var(--s8);
  }

  .order-block-copy .eyebrow {
    justify-content: flex-start;
  }
}

/* ---- Gallery ------------------------------------------------------------------------------ */

/* The v1 filter bar used to live here — underlined text links in --ink-soft with a 2px rule under
   the current one — and it had been quietly overriding the v2 pills ever since they were written,
   because it is defined further down the same stylesheet at the same specificity. Two of its
   declarations were doing visible damage: `gap: 0 var(--s4)`, a *zero* row gap, which stacked the
   wrapped second row of pills flush against the first with no space between them; and a colour on
   the current filter that fought the filled pill above it.

   Its one argument worth keeping is recorded on .filter-bar above: the bar wraps rather than
   scrolls, because a scroller hides facets off the right edge with nothing to say they are there,
   and every facet being visible is the whole job of a filter bar. */

.gallery-state {
  color: var(--ink-soft);
  padding: var(--s7) 0;
}

/* An empty state is the one moment a page has nothing of hers to show, so it shows the mark
   instead of an apology. The brief's rule for these is "an invitation, not an apology"; the paw
   is what makes the invitation look like it came from somebody. */
.mark-paw {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s4);
  max-width: 26rem;
  margin-inline: auto;
  padding-block: var(--s8);
  text-align: center;
  font-size: var(--t-lead);
  font-family: var(--font-serif);
  color: var(--ink);
}

.mark-paw::before {
  content: "";
  width: 4.5rem;
  height: 3.7rem;
  background: url("/brand/paw.png") center / contain no-repeat;
}

/* A wait, not an empty page: the paw breathes while something is actually in flight. Slow and
   small — 2.4s and 8% — so it reads as alive rather than as a spinner demanding attention. The
   global prefers-reduced-motion rule collapses the duration to nothing, which leaves a still
   paw above the sentence, which is a perfectly good loading state on its own. */
@media (prefers-reduced-motion: no-preference) {
  .mark-loading::before {
    animation: paw-breathe 2.4s ease-in-out infinite;
  }
}

@keyframes paw-breathe {
  0%,
  100% {
    opacity: 0.55;
    scale: 0.96;
  }
  50% {
    opacity: 1;
    scale: 1.04;
  }
}

/* The other end of the same journey. Same mark, no animation, above the thank-you. */
.confirmation-mark {
  display: block;
  width: 4.5rem;
  height: auto;
  margin-bottom: var(--s4);
}

/* A uniform grid makes hand-made work look like inventory, which the brief says in those words.
   The variation is data-driven rather than decorative: anything landscape takes two tracks
   because a 2-up column squashes it, and every seventh piece takes two as well so a long run of
   portraits still breathes. Holes left by a spanning piece are not filled (no grid-auto-flow:
   dense) — the hole is paper, and dense would put the visual order out of step with the reading
   order for a keyboard or a screen reader. */
.gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  align-items: start;
  gap: var(--s6) var(--s4);
}

@media (min-width: 45em) {
  .gallery {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--s7) var(--s5);
  }
}

@media (min-width: 68em) {
  .gallery {
    grid-template-columns: repeat(4, 1fr);
  }
}

.gallery-item {
  display: block;
  width: 100%;
  min-width: 0; /* grid items are min-width:auto by default, which lets an image's intrinsic
                   width blow the track out instead of shrinking into it */
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  color: inherit;
  font: inherit;
  text-decoration: none;
  cursor: pointer;
}

.gallery-item-wide {
  grid-column: span 2;
}

.gallery-item img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 0; /* the paper is the frame */
  background: var(--paper-sunk); /* a paper-coloured plate while the image loads */
}

.gallery-item figcaption,
.gallery-caption {
  margin-top: var(--s2);
  font-size: var(--t-micro);
  line-height: var(--lh-snug);
  color: var(--ink-soft);
}

.gallery-item:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

/* "Start to finish" on the home page: a fixed sequence, not a grid to browse, so it scrolls as a
   filmstrip rather than wrapping — the point is the order, and wrapping would break it.

   The last frame is a different kind of picture from the ones before it: five stills lifted from
   a phone video shot over her shoulder, her hand across the drawing in every one, and then an
   actual photograph of the finished piece, square-on with nothing else in the frame. That change
   of register is the content — the sequence exists to arrive somewhere, and a video still can
   only ever show the finished portrait with a hand over half of it — but a change of register
   reads as a mistake unless the layout owns it. So the last item gets a step up in size and a
   beat of space before it. No caption, no border, no chrome: just "and here it is". */
.buildup-strip {
  display: flex;
  gap: var(--s4);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-bottom: var(--s2); /* room for the scrollbar some browsers draw inside the box */
  -webkit-overflow-scrolling: touch;
}

.buildup-item {
  flex: 0 0 auto;
  width: 78vw;
  max-width: 22rem;
  margin: 0;
  scroll-snap-align: start;
}

.buildup-item:last-child {
  width: 88vw;
  max-width: 27rem;
  margin-left: var(--s4);
}

/* Uniform on a wide screen. The step up in size is a beat you feel when you swipe to the end of
   the strip and the register changes; on a laptop the strip is a static row that shows three of
   its frames, the big one is off the right edge where nobody sees it, and all it does is set the
   flex container's height 150px taller than the frames you *can* see — which put "I paint pets in
   soft and oil pastel" that far below the sequence it belongs to. */
@media (min-width: 62em) {
  .buildup-item:last-child {
    width: 22rem;
    max-width: 22rem;
    margin-left: 0;
  }
}

.buildup-item img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper-sunk);
}

/* Full-screen detail. Native <dialog> brings focus trapping and Escape for free, and showModal
   doesn't scroll the page, so closing returns to the same scroll position by construction. */
.detail {
  border: 0;
  padding: 0;
  margin: 0;
  max-width: 100vw;
  max-height: 100dvh;
  width: 100%;
  height: 100%;
  background: var(--paper);
  color: var(--ink);
}

.detail::backdrop {
  background: var(--paper);
}

.detail[open] {
  display: flex;
  align-items: center;
  justify-content: center;
}

.detail figure {
  margin: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--s7) var(--s5) var(--s6);
  gap: var(--s5);
}

.detail img {
  max-width: 100%;
  max-height: 70dvh;
  width: auto;
  height: auto;
  object-fit: contain;
  transition: opacity 140ms ease-out;
}

/* Stepping to the next piece leaves the previous one on screen until the new file decodes.
   Fading out for that moment is the difference between "loading" and "broken"; the global
   reduced-motion rule collapses the transition, and it becomes an instant swap. */
.detail img.is-loading {
  opacity: 0.25;
}

.detail figcaption {
  text-align: center;
  max-width: var(--measure);
}

.detail figcaption h2 {
  font-family: var(--font-display);
  font-size: var(--t-head);
  font-weight: 800;
  line-height: var(--lh-snug);
  margin: 0 0 var(--s2);
}

/* Medium, then year, on their own lines. The brief bans meta strings joined with middle dots and
   this caption used to be one. */
.detail-meta {
  margin: 0;
  color: var(--ink-soft);
  font-size: var(--t-small);
}

.detail-meta span {
  display: block;
}

.detail-close,
.detail-nav {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap);
  min-height: var(--tap);
  background: none;
  border: 0;
  color: var(--ink);
  font-size: 1.75rem;
  line-height: 1;
  cursor: pointer;
}

.detail-close {
  top: var(--s2);
  right: var(--s2);
}

.detail-prev {
  left: var(--s1);
  top: 50%;
  transform: translateY(-50%);
}

.detail-next {
  right: var(--s1);
  top: 50%;
  transform: translateY(-50%);
}

@media (prefers-reduced-motion: no-preference) {
  .detail[open] {
    animation: detail-in 180ms ease-out;
  }
}

@keyframes detail-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* ---- Not found ---------------------------------------------------------------------------- */

/* The only page with none of her work on it. It gets the paw at a size used nowhere else, which
   is what stops a 404 from being the one screen that forgot whose site it is. */
/* Brief v2: the paw, large, on scribble. One line in script. A way back. */
.notfound {
  padding-block: var(--s9);
}

.notfound-inner {
  display: grid;
  justify-items: center;
  text-align: center;
}

.notfound-mark {
  display: block;
  width: 8rem;
  height: auto;
  margin-bottom: var(--s5);
}

.notfound .page-title {
  font-family: var(--font-display);
  font-size: var(--t-title);
  font-weight: 800;
  margin-bottom: var(--s3);
}

.notfound .script {
  color: var(--ink-soft);
  margin-bottom: var(--s7);
}

.notfound-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s4);
  margin: 0;
}

/* ---- About -------------------------------------------------------------------------------- */

.about-hero-figure {
  margin: 0 0 var(--band);
  text-align: center;
}

.about-hero {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper-sunk);
}

.about-hero-empty {
  margin: 0;
  padding: var(--s8) var(--s5);
  text-align: center;
  color: var(--ink-soft);
  background: var(--paper-raised);
}

@media (min-width: 56em) {
  /* On a phone the hero reaches both edges. On a laptop, at its real portrait ratio, that made
     it about 850px tall and pushed the story — the thing this page exists to deliver — below
     the fold. Capped by height rather than cropped, so the painting keeps its proportions. */
  .about-hero-figure {
    margin-inline: 0;
  }

  /* Centred on the page's own axis. It is capped by height, so it is narrower than its track and
     has to be told where to sit; left-aligned it landed 80px off the axis everything above and
     below it uses, which is what "the About page isn't centred" was. */
  .about-hero {
    width: auto;
    max-height: 68vh;
    margin-inline: auto;
  }
}

.about-section {
  margin-bottom: var(--band);
}

.about-heading,
.about-section h1 {
  font-family: var(--font-display);
  font-size: clamp(2rem, 6vw, 3rem);
  font-weight: 800;
  line-height: var(--lh-snug);
  letter-spacing: -0.015em;
  margin: 0 0 var(--s5);
}

.about-section p {
  max-width: var(--measure);
}

/* The first paragraph of her story carries her voice, so it gets the serif and a step up. The
   rest is body text. */
.about-section .lead {
  margin-bottom: var(--s5);
}

/* A photograph of Kat, not a piece of work. Held to the reading measure and left-aligned with
   her own words so it reads as part of the story rather than a second hero — the painting above
   it is the thing that should be big on this page. */
.about-portrait {
  margin: 0 0 var(--band);
  max-width: var(--measure);
}

.about-portrait img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper-sunk);
}

/* Sized by how many photos there actually are, not by how many the wireframe imagined. A fixed
   three-column grid drew a single process photo at a third of the column — small enough that you
   could not tell it was a reference photo beside a finished piece, which is the entire content of
   the picture. auto-fit gives one photo the full measure, three photos a row, and on a phone
   stacks them rather than shrinking them to 105px each. */
.about-process-photos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
  gap: var(--s3);
  margin-top: var(--s5);
  max-width: var(--measure);
}

.about-process-photos img {
  display: block;
  width: 100%;
  height: auto;
  min-width: 0;
  background: var(--paper-sunk);
}

/* ---- Commissions -------------------------------------------------------------------------- */

.commission-status {
  color: var(--ink-soft);
  margin: 0 0 var(--band);
  font-size: var(--t-small);
}

/* Sections are separated by space first and a hairline second, and the hairline is only there
   where the eye needs a hard stop. Previously every section carried the same rule and the same
   padding, so nothing was more important than anything else. */
.form-section {
  margin-bottom: var(--band);
}

/* The display face, like every other heading on the site. Not the paw: a form's steps are
   labels on the work of filling it in, and marking four of them would make the paw furniture
   (CLAUDE.md — the paw does not appear on form section headings). */
.form-section > h2 {
  font-family: var(--font-display);
  font-size: var(--t-head);
  font-weight: 800;
  line-height: var(--lh-snug);
  letter-spacing: -0.008em;
  margin: 0 0 var(--s5);
}

/* Tier cards, as brief v2 draws them: cream cards with a scribble corner, the chosen one marked.
   They used to be rows separated by hairlines, on the previous brief's reasoning that "selection
   is carried by weight and position rather than by a colour the interface isn't allowed to
   supply". v2 supersedes that in as many words — the interface now has berry-rose for anything
   you can act on — and a rule of hairlines in the middle of a page whose every other block is a
   sheet of paper was one of the seams. */
.tier-list {
  display: grid;
  gap: var(--s3);
}

.tier-card {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: var(--s4);
  padding: var(--s4) var(--s5);
  border: 1px solid var(--rule);
  border-radius: 0.75rem;
  background: var(--paper-raised);
  cursor: pointer;
  overflow: hidden;
}

/* The scribble corner. One corner, clipped by the card's own radius, fading out before it
   reaches the price — a mark on the card rather than a texture behind it. */
.tier-card::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 45%;
  pointer-events: none;
  background: url("/scribble-tile.png") -140px -60px / 300px 300px;
  -webkit-mask-image: linear-gradient(to right, #000, transparent);
  mask-image: linear-gradient(to right, #000, transparent);
  opacity: 0;
  transition: opacity 160ms ease-out;
}

.tier-card:hover {
  border-color: var(--berry);
}

.tier-card:has(input:checked) {
  border-color: var(--berry);
  box-shadow: inset 0 0 0 1px var(--berry);
}

.tier-card:has(input:checked)::after {
  opacity: 0.55;
}

.tier-card-body,
.tier-card-price,
.tier-card input[type="radio"] {
  position: relative;
}

/* appearance:none removes the system blue, which is the interface supplying a saturated colour
   the brief reserves for the work. */
.tier-card input[type="radio"] {
  appearance: none;
  flex: 0 0 auto;
  width: 1.05rem;
  height: 1.05rem;
  margin: 0;
  border: 1px solid var(--rule-input);
  border-radius: 50%;
  background: var(--paper);
  cursor: pointer;
  translate: 0 0.15em;
}

.tier-card input[type="radio"]:checked {
  border-color: var(--berry);
  border-width: 2px;
  background: radial-gradient(circle, var(--berry) 0 44%, transparent 45%);
}

.tier-card-body {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  flex: 1 1 auto;
  min-width: 0;
}

.tier-card-name {
  font-weight: 600;
  line-height: var(--lh-snug);
}

/* Size and price used to be one string joined with a middle dot. They are two different facts
   and now sit in two different places: the size under the name, the price aligned right. */
.tier-card-meta {
  color: var(--ink-soft);
  font-size: var(--t-micro);
}

.tier-card-price {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-size: var(--t-body);
}

.tier-card-closed {
  cursor: default;
  color: var(--ink-soft);
}

.tier-card-closed .tier-card-name {
  font-weight: 400;
}

/* Ground chooser — the site's one motion moment, and the only place it spends any boldness.
   The ground is a full field, not a mat: it fills the panel edge to edge and the sample sits on
   it. On a phone the panel bleeds past the reading column so the colour actually arrives. */
.ground-chooser {
  margin-bottom: var(--s5);
}

.ground-sample {
  position: relative;
  aspect-ratio: 1 / 1;
  max-height: 26rem;
  margin: 0 0 var(--s4);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--paper-sunk);
}

.ground-bg {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 320ms ease;
}

.ground-bg.active {
  opacity: 1;
}

/* Cross-fade, not a slide, so it degrades to an instant change under reduced motion — which the
   global rule above already collapses for it. */
.ground-bg-paper {
  background: var(--paper);
}

.ground-bg-colour {
  background: #b3223b;
}

.ground-bg-gold {
  background: linear-gradient(150deg, #a8842f, #e6cf9a 45%, #b8973f);
}

/* The sample is a cut-out with a real alpha channel, so the ground shows *behind the head*
   rather than around a rectangle — which is the thing the brief actually asks this control to
   do. Sized by height so an irregular silhouette keeps a generous, even margin of ground on all
   four sides; the max-width is only a guard against a future cut-out of a much wider subject.
   No shadow under it: the brief rules those out, and a cut paper edge does not cast one. */
.ground-portrait {
  position: relative;
  display: block;
  width: auto;
  height: 82%;
  max-width: 84%;
  object-fit: contain;
}

@media (min-width: 30em) {
  .ground-sample {
    aspect-ratio: 3 / 2;
  }

  .ground-portrait {
    height: 86%;
    max-width: 62%;
  }
}

/* A colour chooser has to show colour. Three equal tracks rather than a wrapping flex row, so
   "Gold" cannot drop onto a line of its own at 320px — which is exactly what it did. */
.ground-swatches {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s2);
}

.ground-swatch {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
  min-height: var(--tap);
  padding: var(--s2);
  font: inherit;
  font-size: var(--t-small);
  text-align: left;
  border: 1px solid transparent;
  background: none;
  color: var(--ink);
  cursor: pointer;
}

.ground-swatch::before {
  content: "";
  display: block;
  width: 100%;
  height: 2.25rem;
  background: var(--swatch);
  outline: 1px solid rgb(0 0 0 / 0.12);
  outline-offset: -1px;
}

/* The site's own paper, not a grey near it. The brief keeps the swatches' real sampled colours
   and this one is not sampled from anything — it is the ground the portrait is drawn on, which
   is the same warm sheet the page is. It was #f3f3f1, the previous brief's cool grey, and it
   sat in the middle of the commission page contradicting everything around it. */
.ground-swatch[data-ground="paper"] {
  --swatch: var(--paper);
}

.ground-swatch[data-ground="colour"] {
  --swatch: #b3223b;
}

.ground-swatch[data-ground="gold"] {
  --swatch: linear-gradient(150deg, #a8842f, #e6cf9a 45%, #b8973f);
}

.ground-swatch.active {
  border-color: var(--ink);
}

.ground-swatch.active .ground-swatch-name {
  font-weight: 600;
}

.ground-swatch-name {
  line-height: var(--lh-snug);
}

.ground-swatch-price {
  display: block;
  color: var(--ink-soft);
  font-size: var(--t-micro);
}

/* Photos */

.form-section input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.photo-previews {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));
  gap: var(--s2);
  margin-top: var(--s4);
}

.photo-preview {
  position: relative;
  aspect-ratio: 1;
  background: var(--paper-sunk);
}

.photo-preview img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.photo-preview-remove {
  position: absolute;
  top: 0;
  right: 0;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  background: var(--ink);
  color: var(--paper);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

/* Total */

.commission-total {
  border-top: 2px solid var(--ink);
  padding-top: var(--s4);
}

.commission-total-row {
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
  margin: 0 0 var(--s2);
  font-variant-numeric: tabular-nums;
}

.commission-total-deposit {
  font-weight: 600;
  margin-bottom: var(--s5);
}

#commission-confirmation h2 {
  font-family: var(--font-display);
  font-size: var(--t-title);
  font-weight: 800;
  line-height: var(--lh-snug);
  letter-spacing: -0.015em;
  margin: 0 0 var(--s4);
}

/* ---- FAQ and Contact (brief v2) ------------------------------------------------------------ */

.faq-list {
  margin-bottom: var(--band);
  border-top: 1px solid var(--rule);
}

.faq-entry {
  padding-block: var(--s5);
  border-bottom: 1px solid var(--rule);
}

.faq-question {
  font-family: var(--font-display);
  font-size: var(--t-head);
  font-weight: 800;
  line-height: var(--lh-snug);
  margin: 0 0 var(--s3);
  color: var(--ink);
}

.faq-entry p {
  max-width: var(--measure);
  color: var(--ink-soft);
}

.faq-more {
  margin-bottom: var(--s6);
}

.contact-direct {
  margin-bottom: var(--s6);
  font-size: var(--t-lead);
}

.contact-form {
  max-width: 32rem;
  margin-bottom: var(--s6);
}

/* Hidden rather than display:none — a bot reading the DOM sees a normal field, which is the whole
   point, while nobody using the page ever meets it.
   
   Clipped in place rather than parked at left: -9999px. The old technique puts a real box a long
   way outside the document, and while it does not extend `scrollWidth` to the *right* in a
   left-to-right page, it is a box outside the viewport on a page whose whole bug report was
   "this drags sideways on an iPhone" — and it would extend the overflow for real in a
   right-to-left rendering. `clip-path` leaves the element where it is, still in the accessibility
   tree and still submitted, occupying nothing. */
.contact-trap {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.contact-elsewhere {
  color: var(--ink-soft);
}

/* ---- Admin -------------------------------------------------------------------------------- */

/* Utilitarian by design — it only has to work on her phone at midnight. It gets the tokens so it
   stays consistent, and nothing more. */

.admin-body {
  max-width: 46rem;
  margin-inline: auto;
  padding: 0 var(--gutter) var(--s9);
}

/* Admin is utilitarian by the brief's own instruction, so it gets the type system and none of
   the volume: the page title comes back down to a heading, not a poster. */
.admin-masthead {
  align-items: baseline;
}

.admin-masthead .page-title {
  font-family: var(--font-sans);
  font-size: var(--t-lead);
  font-weight: 700;
  letter-spacing: 0;
}

.panel {
  border-top: 1px solid var(--rule);
  padding-top: var(--s5);
  margin-bottom: var(--s7);
}

.panel h2 {
  font-size: var(--t-lead);
  font-weight: 700;
  line-height: var(--lh-snug);
  margin: 0 0 var(--s4);
}

.artwork-form,
#login-form {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  max-width: 28rem;
}

.artwork-form label,
#login-form label {
  margin-top: var(--s3);
}

.artwork-form .btn,
#login-form .btn {
  margin-top: var(--s4);
  align-self: flex-start;
}

.build-panel dl {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s1) var(--s4);
  font-size: var(--t-micro);
}

.build-panel dt {
  color: var(--ink-soft);
}

.build-panel dd {
  margin: 0;
}

.build-panel code {
  font: 400 var(--t-micro) / 1.5 var(--font-mono);
  overflow-wrap: anywhere;
}

.build-panel dd[data-mismatch] code {
  color: var(--alert);
}

.settings-image {
  margin-bottom: var(--s5);
}

.settings-image-preview {
  display: block;
  width: 100%;
  max-width: 16rem;
  height: auto;
  margin-bottom: var(--s2);
  background: var(--paper-sunk);
}

.settings-image-empty {
  margin: 0 0 var(--s2);
  font-size: var(--t-small);
}

.setting-alt {
  font-size: var(--t-small);
  max-width: 24rem;
  margin-bottom: var(--s2);
}

.settings-image-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

.settings-subhead {
  font-size: var(--t-body);
  font-weight: 600;
  margin: var(--s6) 0 var(--s3);
}

.capacity-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s3);
}

.capacity-row input {
  width: 8rem;
}

/* The one thing in this panel that means something is wrong *now*, so it takes the alert colour
   and a rule down its edge instead of sitting in the flow as more grey hint text. Grey hint text
   is how an unset month went unnoticed for the first eighteen days of October 2026. */
.capacity-warning {
  color: var(--alert);
  font-size: var(--t-small);
  line-height: var(--lh-snug);
  border-left: 3px solid var(--alert);
  padding-left: var(--s3);
  margin: 0 0 var(--s4);
}

.capacity-upcoming {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--s4);
  font-size: var(--t-small);
  color: var(--ink-soft);
}

.capacity-upcoming-row {
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--rule);
}

/* Only the months nobody has decided about are picked out. A month she closed on purpose is not
   a problem and must not be coloured like one. */
.capacity-upcoming-row.capacity-state-unconfigured {
  color: var(--alert);
}

.tier-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s2);
  border-top: 1px solid var(--rule);
  padding-top: var(--s3);
}

.tier-row-fields {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s3);
}

.settings-image-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s3);
}

@media (max-width: 30em) {
  .settings-image-row {
    grid-template-columns: 1fr;
  }
}

.settings-image-small .settings-image-preview,
.settings-image-small .settings-image-empty {
  max-width: 100%;
}

/* ---- The request queue (slice 6) ----------------------------------------------------------
   Utilitarian, per the brief: it only has to work on her phone at midnight. Everything here
   comes from the tokens; the status chip is the one place a little colour would have been
   tempting, and it stays ink-on-paper because the interface still supplies none. */

.queue-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-bottom: var(--s4);
}

.queue-filter.active {
  border-color: var(--ink);
  font-weight: 600;
}

.queue-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}

.queue-row {
  border-bottom: 1px solid var(--rule);
}

.queue-summary {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: var(--s1) var(--s3);
  width: 100%;
  padding: var(--s3) 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

@media (min-width: 34em) {
  .queue-summary {
    grid-template-columns: 8.5rem 1fr auto auto;
  }
}

/* A word, not a colour: the status has to be readable in one glance without the interface
   inventing a palette for it. */
.queue-status {
  font-size: var(--t-micro);
  font-weight: 600;
  padding: 0.15em 0.5em;
  border: 1px solid var(--rule-input);
  white-space: nowrap;
}

.queue-status[data-status="new"] {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

.queue-status[data-status="declined"],
.queue-status[data-status="cancelled"],
.queue-status[data-status="complete"] {
  color: var(--ink-soft);
}

.queue-who {
  font-weight: 600;
  min-width: 0;
}

.queue-meta {
  color: var(--ink-soft);
  font-size: var(--t-small);
}

.queue-detail {
  padding: 0 0 var(--s5);
}

.queue-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s1) var(--s4);
  margin: 0 0 var(--s4);
  font-size: var(--t-small);
}

.queue-facts dt {
  color: var(--ink-soft);
}

.queue-facts dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.queue-notes {
  font-size: var(--t-small);
  border-left: 2px solid var(--rule);
  padding-left: var(--s3);
  margin-bottom: var(--s4);
}

.queue-photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr));
  gap: var(--s2);
  margin-bottom: var(--s4);
}

.queue-photos img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  background: var(--paper-sunk);
}

.queue-finished {
  margin: 0 0 var(--s4);
}

.queue-finished img {
  display: block;
  width: 100%;
  max-width: 20rem;
  height: auto;
  background: var(--paper-sunk);
}

.queue-finished figcaption {
  margin-top: var(--s2);
  max-width: 28rem;
}

.queue-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

.queue-link {
  margin-top: var(--s4);
}

.queue-link input {
  font-size: var(--t-small);
  font-family: var(--font-mono);
}

.artwork-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}

.artwork-row {
  display: grid;
  grid-template-columns: 4.5rem 1fr;
  gap: var(--s3);
  border-top: 1px solid var(--rule);
  padding-top: var(--s4);
}

.artwork-thumb {
  width: 4.5rem;
  height: 4.5rem;
  object-fit: cover;
  background: var(--paper-sunk);
}

.artwork-fields {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}

.artwork-fields input,
.artwork-fields textarea,
.artwork-fields select {
  font-size: var(--t-small);
}

/* The note on a piece whose breed was inferred rather than told. Ink on paper with a rule, not a
   warning colour — nothing has gone wrong, there is just something worth her eye. */
.artwork-flag {
  margin: var(--s1) 0 0;
  padding-left: var(--s3);
  border-left: 2px solid var(--ink);
  font-size: var(--t-small);
  line-height: var(--lh-snug);
  color: var(--ink-soft);
}

/* Sits with the row's buttons and only appears when a save actually failed. */
.artwork-saved {
  align-self: center;
  margin: 0;
}

.artwork-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}
