/* Trickbox storefront styles.
   Hand-written and not generated. Only the .html files are rebuilt from data.
   No web fonts, no CDN, no third-party requests: the privacy policy promises
   that, so keep it true. */

:root {
  --paper: #faf6f0;
  --paper-2: #f3ece2;
  --card: #fffdfa;
  --ink: #2a221d;
  --ink-2: #5f524a;
  --muted: #756557;
  --line: #e5dacb;
  --accent: #9a5a25;
  --accent-soft: #f0e2d2;
  --accent-ink: #7b451a;
  /* The dark band, and the two colours that stay readable on it. Kept here as well
     as in admin/lib/theme.mjs because theme.css is generated and a build that has
     not run yet would otherwise leave the band transparent. */
  --band: #1c1714;
  --band-ink: #f3ece2;
  --band-accent: #dd9933;
  --shadow: 0 1px 2px rgba(42, 34, 29, .05), 0 12px 32px -18px rgba(42, 34, 29, .28);
  /* What a photograph is laid on. Not a palette colour and not in theme.mjs on
     purpose: it has to match the paper the boxes were photographed on, which is
     pure white in every shot we have, sampled corner by corner. Anything else and
     a photograph that is not exactly 4/3 shows two hard seams where the file ends
     and the frame begins, which is what "the pictures don't sit well in the box"
     looks like. It is also why the dark block below does not override it: the
     photographs stay white in the dark, so the frame has to as well. Reshoot on
     something other than white and this is the one line to change. */
  --photo-ground: #fff;
  --radius: 18px;
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --measure: 68ch;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #17130f;
    --paper-2: #1f1913;
    --card: #201a15;
    --ink: #f0e7dc;
    --ink-2: #c3b3a4;
    --muted: #9b8b7c;
    --line: #3a2f26;
    --accent: #d99a5e;
    --accent-soft: #34271c;
    --accent-ink: #eec08c;
    --band: #221b15;
    --band-ink: #f0e7dc;
    --band-accent: #e0a75a;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 16px 40px -20px rgba(0, 0, 0, .7);
  }
}

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

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { font-family: var(--serif); font-weight: 600; line-height: 1.15; letter-spacing: -.01em; margin: 0; }
p { margin: 0 0 1em; }
a { color: var(--accent-ink); text-decoration-thickness: 1px; text-underline-offset: 3px; }
img, svg { max-width: 100%; height: auto; display: block; }

.wrap { width: 100%; max-width: 1080px; margin: 0 auto; padding: 0 24px; }
.eyebrow {
  font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 14px; font-weight: 600;
}

/* ---------- header ---------- */

.site-header {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: saturate(160%) blur(10px);
  border-bottom: 1px solid var(--line);
}
.site-header .wrap { display: flex; align-items: center; gap: 28px; height: 66px; }
.brand {
  font-family: var(--serif); font-size: 20px; font-weight: 600;
  color: var(--ink); text-decoration: none; letter-spacing: -.01em;
  display: inline-flex; align-items: center; gap: 10px;
}
.brand-mark {
  width: 24px; height: 24px; border-radius: 7px; flex: none;
  background: linear-gradient(135deg, #b9793c, #7d4519);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .25);
}
.site-nav { margin-left: auto; display: flex; gap: 22px; align-items: center; }
.site-nav a { color: var(--ink-2); text-decoration: none; font-size: 15px; }
.site-nav a:hover { color: var(--accent-ink); }
.lang-pill {
  border: 1px solid var(--line); border-radius: 999px; padding: 4px 12px;
  font-size: 13px; color: var(--ink-2); text-decoration: none;
}
.lang-pill:hover { border-color: var(--accent); color: var(--accent-ink); }
@media (max-width: 620px) {
  .site-nav a.nav-link { display: none; }
}

/* ---------- hero ---------- */

/* The front page of the shop this replaced, rebuilt: a full-bleed photograph of dark
   charred wood, one big rounded cream panel of words laid on it, and a cut-out of the
   box half open lying across the panel's right edge, half on the cream and half on the
   wood. Three parts, and the third is the one that matters. Without the overlap it is
   a card next to a picture; with it the panel reads as something set down on a
   workbench, which is the whole effect the old page had.

   What is deliberately not taken is the wood tiled behind every section below this one,
   which is what made the old page hard to read.

   Two files, and they are the only design assets this site fetches, so both are sized
   against what they are: 1600px of grain at 139 KB for something nobody looks at, and
   1000px of box at 124 KB for the thing the page is about. A build refuses if either is
   missing. auditStylesheet in admin/lib/seo.mjs covers the ground, because a url() is
   not markup and nothing else was reading it, and the ordinary <img> check covers the
   box. Renaming one would otherwise leave a flat brown rectangle and no error anywhere. */
.hero {
  /* Two lengths, named because the one-column overlap is built out of both and has to
     stay built out of both. See .hero-box. */
  --hero-gap: clamp(18px, 3vw, 30px);
  --hero-pad: clamp(26px, 4.4vw, 54px);
  padding: 64px 0 76px; position: relative; overflow: hidden;
  background: var(--band) url("wood.jpg") center / cover no-repeat;
  color: var(--band-ink);
}
/* A light scrim, and much lighter than it was. Every word in this block sits inside the
   cream panel now, so the ground is not carrying any text and does not need to be pushed
   down to a contrast figure: it only needs to stop the brightest knot in the planks from
   competing with the panel, and to darken the top edge where the sticky header crosses
   it. Pulled towards --band rather than towards black, so a recoloured band takes the
   wood with it instead of leaving a grey hole. */
.hero::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(to bottom, color-mix(in srgb, var(--band) 34%, transparent), color-mix(in srgb, var(--band) 12%, transparent) 30%, color-mix(in srgb, var(--band) 30%, transparent));
}
.hero-inner {
  position: relative; z-index: 1;
  display: grid; gap: var(--hero-gap); justify-items: center;
}

/* The panel. `--card` rather than a literal cream, so the dark palette gets a dark panel
   on dark wood instead of the one bright rectangle on the site. The radius is the old
   shop's 32px, much rounder than anything else here, and it is what makes the panel read
   as laid on top rather than cut out of the page. */
.hero-card {
  background: var(--card); color: var(--ink);
  border-radius: 32px; padding: var(--hero-pad);
  box-shadow: 0 34px 70px -36px rgba(0, 0, 0, .85);
  width: 100%;
}
.hero-card .eyebrow { color: var(--accent-ink); }
.hero h1 { font-size: clamp(31px, 4.6vw, 50px); margin-bottom: 18px; }
.hero .lede { font-size: clamp(17px, 2vw, 20px); color: var(--ink-2); max-width: 46ch; }
/* Was an inline style attribute, which our own CSP (style-src 'self') blocks. */
.hero-cta { margin-top: 26px; }

/* The box. A cut-out with a transparent ground, so no frame, no border and no shadow on
   the picture itself: a drop shadow on a photograph of an object is a shadow on the
   object, and it lands in the wrong place the moment the box is not where the shadow
   says. The art fills its frame to all four edges, so the element's overlap is the
   overlap you see.

   Below the wide layout it lies across the panel's bottom right corner, which is the
   same gesture in one column that the overhang is in two. The overlap it needs is the
   thing the old fixed offset got wrong: -6vw against an 18px row gap left 6px of it on
   a phone, which is not an overlap, it is a picture resting on the card's edge, and it
   was centred, so it did not reach a corner either.

   So the overlap is the panel's own bottom padding, and the margin cancels the row gap
   and then takes back exactly that. Why that length: the padding is by definition the
   empty strip below the last line of the panel, so a box lying over that much of it can
   never reach the words or the button, at any width, however long the German runs. Any
   larger number would have to be checked against wherever the button happens to end. */
.hero-box {
  display: block; width: min(82%, 380px); height: auto;
  justify-self: end;
  margin-top: calc(-1 * (var(--hero-gap) + var(--hero-pad)));
}

/* Side by side, and overlapping, once there is room for it.

   Both sit in one grid row: the panel spans the row, the box sits in the last column on
   top of it. The overhang is the panel being narrower than the row rather than the box
   being pulled out of it with a negative margin, which is how the old page did it too
   (its panel was ten columns of a twelve-column row and the box was right-aligned across
   the full twelve). The difference matters at 900px, where the page's side margin is
   24px: a box pulled out by a fixed amount is cut off by the `overflow: hidden` above,
   and a box that stops at the row's own edge cannot be, at any width.

   Three numbers, and they are related. The panel's right padding has to be wider than the
   part of the panel the box lies over, or a long German headline runs under the box, so
   whenever the box column grows that padding has to grow with it. It is checked in
   scripts/selftest.mjs, which does that arithmetic rather than restating the numbers. */
@media (min-width: 900px) {
  .hero { padding: 80px 0 92px; }
  .hero-inner { grid-template-columns: minmax(0, 1fr) minmax(0, .7fr); align-items: center; }
  .hero-card { grid-area: 1 / 1 / 2 / 3; justify-self: start; width: 86%; padding-right: 38%; }
  /* `align-self: center` against the panel's height rather than a fixed offset, so the box
     stays centred on the words when the copy changes length. */
  .hero-box {
    grid-area: 1 / 2 / 2 / 3; align-self: center; justify-self: end;
    width: 100%; max-width: none; margin: 0;
  }
}

/* Only ever emitted into the staging build. Loud on purpose: the whole point is
   that nobody mistakes that site for the shop. */
.test-banner {
  margin: 0;
  padding: 10px 16px;
  background: #7a1f1f;
  color: #fff;
  font: 600 14px/1.4 system-ui, sans-serif;
  letter-spacing: 0.02em;
  text-align: center;
}

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 13px 22px; border-radius: 999px; border: 1px solid transparent;
  background: var(--accent); color: #fff8ef; font: inherit; font-size: 16px;
  font-weight: 550; text-decoration: none; cursor: pointer;
  transition: transform .12s ease, filter .12s ease;
}
.btn:hover { filter: brightness(1.06); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }
.btn-block { width: 100%; }
.btn-ghost { background: transparent; color: var(--accent-ink); border-color: var(--line); }
.btn[aria-disabled="true"] {
  background: var(--paper-2); color: var(--muted); border-color: var(--line);
  cursor: default; pointer-events: none;
}

/* ---------- sections ---------- */

/* The margin is the sticky header's 66px and a little air. Every link in the nav is a
   fragment pointing at one of these, and without it following one put the section's
   eyebrow underneath the header: the heading you asked for was the first thing
   covered. Same number as .gallery-slide, for the same reason. */
section { padding: 64px 0; scroll-margin-top: 82px; }
.section-head { max-width: 60ch; margin-bottom: 40px; }
.section-head h2 { font-size: clamp(26px, 3.4vw, 34px); margin-bottom: 12px; }
.section-head p { color: var(--ink-2); margin: 0; }

.tinted { background: var(--paper-2); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }

/* A centred label with a rule running out to each side, as the old shop set
   "Variations". The rules are pseudo-elements on the heading rather than markup,
   and they take their colour from currentColor, so the one class works on the paper
   sections and on the dark band without a second set of rules for the inverted case. */
.section-head.ruled { max-width: none; text-align: center; }
.section-head.ruled h2 {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(14px, 4vw, 30px);
}
.section-head.ruled h2::before, .section-head.ruled h2::after {
  content: ""; height: 1px; flex: 1 1 0; max-width: 110px;
  background: currentColor; opacity: .3;
}
.section-head.ruled p { max-width: 58ch; margin-left: auto; margin-right: auto; }
/* On a phone the rules would keep their 110px each while the heading wrapped around
   what was left, which turns a five-word line into four. The label is still centred
   without them, so they go. */
@media (max-width: 620px) {
  .section-head.ruled h2::before, .section-head.ruled h2::after { display: none; }
}

/* ---------- the dark band ---------- */

/* One section inverts the page: near-black, cream panels floating on it, the small
   marks in gold. The old shop did this with a section of its own and it is the single
   most effective thing on that page, because a full-width change of ground reads as a
   change of subject in a way a 5% tint never does.

   Exactly one band on the site, and that is the point of it. Its three colours are
   their own tokens in admin/lib/theme.mjs rather than the page's, because `ink` on
   `band` is unreadable and `accent` measures 3.3:1 on it, so a model asked to darken
   the band cannot take the text on it along by accident: CONTRAST_PAIRS holds
   band-ink and band-accent against band at 4.5:1 and refuses a palette that does. */
.band { background: var(--band); color: var(--band-ink); padding: 76px 0; }
.band h2, .band h3 { color: var(--band-ink); }
.band .eyebrow { color: var(--band-accent); }
/* `:not(.eyebrow)` because the eyebrow is a <p> in the same block, and `.band
   .section-head p` outweighs `.band .eyebrow` on specificity: without this the gold
   label above the heading was quietly the same cream as the prose. */
.band .section-head p:not(.eyebrow) { color: color-mix(in srgb, var(--band-ink) 80%, transparent); }
.band a { color: var(--band-accent); }

/* The panels. `--card` again rather than a literal cream, for the same reason as the
   hero card: on the dark palette the whole page is dark and a cream panel would be
   the one thing glowing on it. The heading is the warm brown the rest of the site
   uses for a link, which is close to the olive-gold the old shop set these in and,
   unlike that colour, is measured against the panel it sits on. */
.band .feature {
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 26px 26px 28px;
  box-shadow: 0 22px 44px -30px rgba(0, 0, 0, .7);
}
.band .feature h3 { color: var(--accent-ink); font-size: 19px; }
.band .feature p { color: var(--ink-2); }

/* ---------- product grid ---------- */

.grid { display: grid; gap: 26px; grid-template-columns: repeat(auto-fit, minmax(288px, 1fr)); }

.card {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); overflow: hidden; display: flex; flex-direction: column;
}
/* The picture is a link to the product's own page, so it needs to be a block for
   the aspect ratio to apply, and it must not pick up the underline that every
   other link in the copy gets.

   The ratio is on the image, not only on the link around it. It used to be on the
   link with `height: 100%` on the image, and that quietly did nothing: a percentage
   height against a parent whose own height comes from a ratio is cyclic, so the
   image fell back to its natural ratio and then stretched the link to fit. Two boxes
   photographed at different ratios therefore sat at different heights, and the cards
   beside each other started their text at different lines. An `aspect-ratio` on a
   replaced element with `height: auto` overrides the file's own ratio, so every card
   is the same height whatever shape the photograph arrives in. `overflow: hidden` is
   what keeps the hover scale inside the frame. */
.card-media { display: block; background: var(--paper-2); border-bottom: 1px solid var(--line); overflow: hidden; }
.card-media img {
  display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover;
  transition: transform .5s ease;
}
.card-media:hover img { transform: scale(1.03); }
.card-media:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: -2px; }
.card-body { padding: 22px 22px 24px; display: flex; flex-direction: column; flex: 1; }
.card-body h3 { font-size: 21px; margin-bottom: 6px; }
.card-tagline { color: var(--accent-ink); font-size: 14.5px; margin: 0 0 12px; }
.card-desc { color: var(--ink-2); font-size: 15px; margin: 0 0 18px; }

/* Stripe's marketing feature list, on a product card. Named apart from .features
   below, which is the wide three-column block on the craft section. */
.card-features { list-style: none; margin: 0 0 18px; padding: 0; font-size: 14.5px; color: var(--ink-2); }
.card-features li { position: relative; padding: 3px 0 3px 18px; }
.card-features li::before {
  content: ""; position: absolute; left: 2px; top: 11px;
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
}

.spec { list-style: none; margin: 0 0 18px; padding: 0; font-size: 14px; border-top: 1px solid var(--line); }
.spec li { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--line); }
.spec dt, .spec .k { color: var(--muted); flex: none; }
/* A row can come from Stripe metadata, so its value may be a sentence rather
   than a number. It takes the remaining width and wraps instead of pushing the
   label out of the card. */
.spec .v { color: var(--ink); font-variant-numeric: tabular-nums; min-width: 0; text-align: right; overflow-wrap: break-word; }

.price-row { display: flex; align-items: baseline; gap: 10px; margin: 0 0 16px; }
/* The amount must never break across lines, so it claims its width first and
   the note takes whatever is left. */
.price { font-family: var(--serif); font-size: 27px; font-variant-numeric: tabular-nums; white-space: nowrap; flex: none; }
.price-note { font-size: 13px; color: var(--muted); flex: 1; min-width: 0; }

.note {
  background: var(--accent-soft); color: var(--accent-ink); border-radius: 12px;
  padding: 10px 13px; font-size: 14px; margin: 0 0 16px;
}
.stock-low { color: var(--accent-ink); font-weight: 550; }
.stock-out { color: #b91c1c; font-weight: 600; }
/* One thing in here now: the button. The small print under it and the second link to the
   product page were both taken out, so the styles for them went with them rather than
   waiting in the sheet for a class nothing writes. */
.card-cta { margin-top: auto; }

.card-title-link { color: inherit; text-decoration: none; }
.card-title-link:hover { color: var(--accent-ink); }

/* ---------- one product, on a page of its own ---------- */

.product-page { padding: 40px 0 56px; }
.back { margin: 0 0 22px; font-size: 14.5px; }
.back a { color: var(--muted); text-decoration: none; }
.back a:hover { color: var(--accent-ink); text-decoration: underline; }

/* Photographs on the left, everything that is read on the right. The picture
   column is given the larger share because a photograph of a box is the reason
   the page exists, and the text column has a measure it should not exceed. */
.product-grid { display: grid; gap: 34px; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); align-items: start; }
/* The photographs, as one frame you move through rather than a column of files.
   See the `gallery` function in admin/templates/site.mjs for why there is no
   JavaScript behind this.

   Every slide is the same 4/3 frame whatever shape the file is, so moving between
   them does not move the page under the reader. `contain` rather than `cover`: on a
   card a crop is an improvement, but this is the page somebody opened to look at the
   box properly, and cutting the top off a tall photograph here would be the opposite
   of the point.

   Which leaves the bars that `contain` puts beside a photograph that is not 4/3, and
   those are the whole reason this frame looked wrong. The files run from 0.75 to 1.94,
   so most slides have them. They were beige, on photographs shot against white, so
   each one drew two hard vertical seams and a white block floating inside a warm
   frame. They are `--photo-ground` now, the same white the boxes were shot on, so the
   bar and the photograph are one surface and there is nothing to see: a tall box is a
   tall box on a white card, a wide one is a wide one, and neither reads as a mistake.
   The frame keeps its own edge from the border and the shadow rather than from a
   change of colour. */
.gallery { min-width: 0; }
.gallery-track {
  display: flex; overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scroll-behavior: smooth;
  background: var(--photo-ground); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  /* The pager and a swipe are the way through, so the bar is only a second scrollbar
     under the page's own. Firefox and WebKit spell hiding it differently. */
  scrollbar-width: none; -ms-overflow-style: none;
}
.gallery-track::-webkit-scrollbar { display: none; }
/* It is a tab stop, because a scroll region reachable only by pointer is not
   reachable. Focused, the arrow keys move it, so the ring has to be visible. */
.gallery-track:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }
/* Following a thumbnail scrolls the page as well as the track, and it aligns the
   slide to the top of the viewport, where the sticky header would cover the top of
   the photograph. The margin is the header's 66px and a little air. */
.gallery-slide { flex: 0 0 100%; min-width: 0; scroll-snap-align: center; scroll-margin-top: 82px; }
.gallery-slide img {
  display: block; width: 100%; height: auto; aspect-ratio: 4 / 3;
  object-fit: contain; background: var(--photo-ground);
}

/* The strip of handles under the frame, as one row that divides the width rather
   than a fixed size that wraps. Eight at a fixed 84px do not fit across the picture
   column, and the 5 + 3 that fell out of that read as a second gallery instead of as
   a pager for the first. Shrinking is the trade, and it is the right way round: the
   basis is the size a short strip gets, `flex-shrink` takes it down to about 60px
   when there are eight, and a row of eight small handles still says "eight
   photographs, you are on this one" in a way two ragged rows do not.

   Not scrolled, still: a second scroller inside a scroller is worse than either. */
.gallery-thumbs { display: flex; gap: 8px; margin: 14px 0 0; }
.gallery-thumb {
  display: block; flex: 0 1 96px; min-width: 0; padding: 0; line-height: 0;
  background: var(--photo-ground); border: 1px solid var(--line);
  border-radius: 10px; overflow: hidden; text-decoration: none;
  transition: border-color .15s ease, transform .15s ease, box-shadow .15s ease;
}
/* `cover` here and `contain` in the frame, on purpose: a thumbnail this size is a
   handle rather than a picture, and a crop that fills it is easier to aim at than
   a letterboxed one. The ratio is on the image with `height: auto`, which is what
   makes every handle the same size whatever shape the file is. */
.gallery-thumb img {
  display: block; width: 100%; height: auto; aspect-ratio: 4 / 3;
  object-fit: cover;
}
.gallery-thumb:hover { border-color: var(--accent); transform: translateY(-1px); }
.gallery-thumb:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }

/* Below this the one row runs out of width: on a 390px phone eight handles come out
   at 36px each, and a 36px target is not a target. So the strip wraps here, into as
   many equal columns of at least 64px as the width holds, which is four on a phone
   and eight by the time this rule stops applying. That is why 64 and not some other
   floor: at 620px the grid and the row above arrive at the same handle, so there is
   no jump in size as the breakpoint is crossed. Grid rather than flex-wrap because
   wrapping and shrinking do not combine, and because a grid cannot round its way
   into a stray last row. */
@media (max-width: 620px) {
  .gallery-thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); }
}

/* Which photograph you are on, without a script to track it.

   `:focus` on the thumbnail is not the answer, which took measuring to find out:
   following a fragment link hands focus to the target, so the handle loses it at
   the moment it does its job, and Chrome leaves document.activeElement on the body.
   What does survive is `:target` on the slide. `:has()` turns that into a statement
   about the handle in the same position, matched by `nth-child` rather than by id,
   so these rules are about the shape of a gallery and not about any product.

   Eight of them, because Stripe allows eight pictures per product and
   PHOTO_MAX_COUNT in admin/lib/art.mjs is the same eight. That is a bounded list
   rather than a pattern needing a generator, and the selftest fails if the ceiling
   there ever moves past what is written here. */
.gallery-track:has(.gallery-slide:nth-child(1):target) + .gallery-thumbs .gallery-thumb:nth-child(1),
.gallery-track:has(.gallery-slide:nth-child(2):target) + .gallery-thumbs .gallery-thumb:nth-child(2),
.gallery-track:has(.gallery-slide:nth-child(3):target) + .gallery-thumbs .gallery-thumb:nth-child(3),
.gallery-track:has(.gallery-slide:nth-child(4):target) + .gallery-thumbs .gallery-thumb:nth-child(4),
.gallery-track:has(.gallery-slide:nth-child(5):target) + .gallery-thumbs .gallery-thumb:nth-child(5),
.gallery-track:has(.gallery-slide:nth-child(6):target) + .gallery-thumbs .gallery-thumb:nth-child(6),
.gallery-track:has(.gallery-slide:nth-child(7):target) + .gallery-thumbs .gallery-thumb:nth-child(7),
.gallery-track:has(.gallery-slide:nth-child(8):target) + .gallery-thumbs .gallery-thumb:nth-child(8),
/* Nothing is targeted on a fresh page load, and the frame is showing the first
   photograph, so that is the handle to mark. Saying so beats a strip where none of
   them is current until you have already pressed one. */
.gallery:not(:has(.gallery-slide:target)) .gallery-thumb:nth-child(1) {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent);
}

/* Somebody who has asked their system to stop animating things did not mean the
   gallery: the jump is still instant, it just is not slid. */
@media (prefers-reduced-motion: reduce) {
  .gallery-track { scroll-behavior: auto; }
}

.product-detail h1 { font-size: clamp(26px, 3.6vw, 36px); margin-bottom: 8px; }
.product-tagline { color: var(--accent-ink); font-size: 16px; margin: 0 0 16px; }
.product-desc { color: var(--ink-2); margin: 0 0 20px; max-width: var(--measure); }
.product-detail .card-features, .product-detail .spec, .product-detail .note { margin-bottom: 20px; }
.product-detail .card-cta { margin-top: 24px; max-width: 340px; }

/* The long piece about a box, below the grid so the price and the buy button stay
   at the top of the page. One column at reading width, because it is prose. */
.product-story { margin: 44px 0 0; max-width: var(--measure); border-top: 1px solid var(--line); padding-top: 28px; }
.product-story h2 { font-size: 20px; font-weight: 600; margin: 0 0 16px; }
.product-story p { color: var(--ink-2); margin: 0 0 16px; }
.product-story p:last-child { margin-bottom: 0; }

/* On a phone the two columns become one, and the gallery stays two across so the
   pictures below the first stay thumbnails rather than a very long scroll. */
@media (max-width: 780px) {
  .product-grid { grid-template-columns: 1fr; gap: 26px; }
  .product-detail .card-cta { max-width: none; }
}

/* ---------- feature list / shipping ---------- */

.features { display: grid; gap: 30px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.feature h3 { font-size: 18px; margin-bottom: 8px; }
.feature p { color: var(--ink-2); font-size: 15px; margin: 0; }

.ticks { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; max-width: var(--measure); }
.ticks li { display: flex; gap: 12px; color: var(--ink-2); }
.ticks li::before {
  content: ""; flex: none; margin-top: 9px; width: 7px; height: 7px;
  border-radius: 2px; background: var(--accent);
}

/* ---------- added sections and written pages ---------- */

/* A section the operator asked for, rendered from data/custom.json. It gets the
   same rhythm as a built-in section so an addition does not read as an addition.
   Elements inside it are styled generically, because the markup was written
   rather than designed and cannot be relied on to carry a class. */
.added { padding: 48px 0; }
.added > .wrap > :first-child { margin-top: 0; }
.added h2 { font-size: clamp(24px, 3.2vw, 31px); margin-bottom: 12px; }
.added h3 { font-size: 19px; margin: 26px 0 8px; }
.added p, .added li { color: var(--ink-2); }
.added p { max-width: var(--measure); }
.added ul, .added ol { max-width: var(--measure); padding-left: 20px; }
.added li { margin: 6px 0; }
.added figure { margin: 24px 0; }
.added figcaption { color: var(--muted); font-size: 14px; margin-top: 8px; }
.added img, .added svg { border-radius: var(--radius); }
.added blockquote {
  margin: 24px 0; padding: 4px 0 4px 20px;
  border-left: 3px solid var(--accent-soft); color: var(--ink-2); font-style: italic;
}
.added table { border-collapse: collapse; width: 100%; max-width: var(--measure); font-size: 15px; }
.added th, .added td { text-align: left; padding: 8px 12px 8px 0; border-bottom: 1px solid var(--line); }
.added th { color: var(--muted); font-weight: 600; }

.written { padding: 60px 0 40px; }
.written-inner { max-width: var(--measure); }
.written h1 { font-size: clamp(28px, 4vw, 38px); margin-bottom: 18px; }
.written h2 { font-size: 21px; margin: 32px 0 10px; }
.written h3 { font-size: 18px; margin: 24px 0 8px; }
.written p, .written li { color: var(--ink-2); }
/* The opening paragraph of a written page, set larger because it is the one a
   reader decides on. Also what the meta description is taken from. */
.written .lead { font-size: 18px; line-height: 1.6; color: var(--ink); }
.written ul, .written ol { padding-left: 20px; }
.written li { margin: 6px 0; }
.written figure { margin: 24px 0; }
.written figcaption { color: var(--muted); font-size: 14px; margin-top: 8px; }
.written img, .written svg { border-radius: var(--radius); }

/* ---------- legal pages ---------- */

.legal { padding: 60px 0 40px; }
.legal-inner { max-width: var(--measure); }
.legal h1 { font-size: clamp(28px, 4vw, 38px); margin-bottom: 12px; }
.legal .intro { color: var(--ink-2); font-size: 17px; margin-bottom: 8px; }
.legal-section { padding-top: 28px; }
.legal-section h2 { font-size: 19px; margin-bottom: 10px; }
.legal-section p { color: var(--ink-2); margin: 0 0 .55em; }
.legal-section p:last-child { margin-bottom: 0; }
.updated { color: var(--muted); font-size: 14px; margin-top: 40px; }

/* ---------- footer ---------- */

.site-footer { border-top: 1px solid var(--line); background: var(--paper-2); padding: 44px 0 56px; margin-top: 24px; }
.footer-grid { display: flex; flex-wrap: wrap; gap: 24px 40px; align-items: flex-start; }
.footer-brand { font-family: var(--serif); font-size: 18px; }
.footer-tagline { color: var(--muted); font-size: 14.5px; margin: 6px 0 0; max-width: 34ch; }
.footer-links { display: flex; flex-wrap: wrap; gap: 8px 22px; margin-left: auto; }
.footer-links a { color: var(--ink-2); font-size: 14.5px; text-decoration: none; }
.footer-links a:hover { color: var(--accent-ink); text-decoration: underline; }
.footer-legal { color: var(--muted); font-size: 13px; margin: 30px 0 0; }
.footer-legal a { color: var(--muted); }

/* ---------- one-message pages ---------- */

/* The shop closed for a while, and an address with no page behind it. Both are a
   line of text in the middle of an otherwise ordinary page, and both used to carry
   `style` attributes, which the published CSP drops: style-src is 'self' and an
   attribute is inline, so the centring silently never happened and the notice came
   out flush left. Classes, so the rules are in a file the policy allows. */
.notice { padding: 72px 0 56px; text-align: center; }
.notice-inner { max-width: 60ch; margin: 0 auto; }
.notice h1 { font-size: clamp(26px, 4vw, 36px); margin: 10px 0 0; }
.notice .lede { font-size: clamp(16px, 2vw, 18.5px); color: var(--ink-2); margin: 18px auto 0; }
.notice-contact { margin-top: 28px; font-size: 16.5px; }
.notice-links { display: flex; flex-wrap: wrap; gap: 10px 20px; justify-content: center; margin: 30px 0 0; }
.notice-links a { color: var(--accent-ink); font-size: 15px; }

.skip-link {
  position: absolute; left: -9999px; top: 0; background: var(--card);
  padding: 10px 16px; border-radius: 0 0 10px 0; z-index: 50;
}
.skip-link:focus { left: 0; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .btn { transition: none; }
}
