/* ==========================================================================
   Illustrated Pet — "warm artisan print studio"
   Paper canvas, sienna ink, a serif display over a quiet sans. Every artwork
   sits in a mat-and-hairline frame; that frame is the signature device and is
   reused for the reveal, the gallery and the card preview.

   Self-hosted fonts only: the CSP is default-src 'self', so Google Fonts and
   any CDN are blocked. --font-display falls back through system serifs until
   a woff2 is added to web/fonts/ and the @font-face below is uncommented.
   ========================================================================== */

:root {
  /* ---- colour roles: one warm neutral ramp, one accent ------------------ */
  --paper:       #FAF6F0;   /* canvas */
  --paper-sunk:  #F2EBE1;   /* recessed band */
  --surface:     #FFFCF8;   /* raised card / mat */
  --ink:         #201B15;   /* body + headings (warm near-black, not #000) */
  --ink-soft:    #665C50;   /* secondary text — AA on paper */
  --rule:        #E4DACB;   /* hairlines */
  --rule-firm:   #CFC2AE;   /* emphasised hairlines */
  --accent:      #A24B24;   /* sienna — actions and emphasis only */
  --accent-deep: #83391A;   /* hover/active */
  --accent-wash: #F6EAE1;   /* accent at 8% for tints */
  --danger:      #A32A1F;

  /* ---- type ------------------------------------------------------------- */
  --font-display: "Newsreader", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
  --font-body: ui-sans-serif, system-ui, sans-serif;

  /* 1.25 modular scale */
  --t-xs:   0.75rem;
  --t-sm:   0.8125rem;
  --t-base: 1.0625rem;
  --t-md:   1.25rem;
  --t-lg:   1.5rem;
  --t-xl:   1.875rem;
  --t-2xl:  2.375rem;
  --t-display: clamp(2.375rem, 5.5vw, 3.5rem);

  --measure: 66ch;

  /* ---- space: 4px base --------------------------------------------------- */
  --s-1: 0.25rem; --s-2: 0.5rem;  --s-3: 0.75rem; --s-4: 1rem;
  --s-5: 1.5rem;  --s-6: 2rem;    --s-7: 3rem;    --s-8: 4rem;   --s-9: 6rem;

  /* ---- radius, depth, motion --------------------------------------------- */
  --r-xs: 2px; --r-sm: 4px; --r-md: 8px; --r-lg: 14px;
  --shadow-1: 0 1px 2px rgba(32,27,21,.04), 0 2px 6px rgba(32,27,21,.05);
  --shadow-2: 0 2px 4px rgba(32,27,21,.05), 0 14px 34px rgba(32,27,21,.10);
  --dur-1: 160ms; --dur-2: 300ms;
  --ease: cubic-bezier(.2,.7,.3,1);
}

/* Self-hosted so it survives the CSP (default-src 'self' blocks Google Fonts and every CDN).
   Latin subset of the Newsreader variable face, SIL Open Font License, 129KB.
   unicode-range keeps the browser from reaching for this file to render glyphs it lacks. */
@font-face {
  font-family: "Newsreader";
  src: url("/fonts/newsreader-variable.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

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

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

h1, h2, h3 { font-family: var(--font-display); font-weight: 600; line-height: 1.12; letter-spacing: -0.015em; margin: 0; }
p { margin: 0; }
img { max-width: 100%; height: auto; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-deep); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--r-xs); }

.wrap { width: 100%; max-width: 1120px; margin-inline: auto; padding-inline: var(--s-5); }
.muted { color: var(--ink-soft); font-size: var(--t-sm); }
.eyebrow {
  font-size: var(--t-xs); text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--ink-soft); font-weight: 600;
}

/* ---- masthead ------------------------------------------------------------ */
.masthead {
  border-bottom: 1px solid var(--rule);
  background: color-mix(in srgb, var(--paper) 80%, transparent);
  backdrop-filter: saturate(1.2) blur(6px);
  position: sticky; top: 0; z-index: 20;
}
/* centre, not baseline: the wordmark carries the logo mark now, so the note beside it should
   line up with the lockup as a whole rather than with the text's baseline. */
/* Sam, 2026-09-11: "the logo should be centered on all the pages." A flex space-between put the
   lockup hard left on /style and /create (the only pages carrying a second item) and centred it
   only on home. A 3-column grid with EQUAL 1fr flanks centres the middle track on every page
   regardless of what sits beside it — the "← All styles" link occupies the left flank and the
   empty right flank balances it. The wordmark is pinned to column 2 explicitly, so home's
   single child still lands in the centre track rather than the first one. */
.masthead .wrap {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  gap: var(--s-4); padding-block: var(--s-4);
}
/* The note is taken OUT of flow rather than given a flanking column. A symmetric 3-column grid
   also centres the lockup, but it reserves a track the note's width on BOTH sides, and at 375px
   that left only 327 - 175 - 24 = 128px for two 68px flanks plus gaps — so "← All styles" broke
   across two lines. Out of flow, the lockup centres on the wrap's full inner width and the link
   sits in the space beside it, on one line. `left: var(--s-5)` is .wrap's own padding-inline,
   so it lines up with the content edge (absolute offsets resolve against the padding box). */
.masthead .masthead-note {
  position: absolute; left: var(--s-5); top: 50%; transform: translateY(-50%);
  white-space: nowrap;
}
/* Out of flow, the link no longer pushes the lockup — so it can only ever COLLIDE with it. At
   414px and below it does: the lockup is 175px wide and centred, which leaves (vw - 175)/2 of
   room beside it, and the full "← All styles" needs 68px from the content edge at 24px. Measured,
   the two boxes meet at exactly 375px and overlap by 19px at 320px — and 360px is a mainstream
   Android width. Below 420px the words go visually-hidden (still in the accessible name, so the
   link is announced as "← All styles") and the arrow alone stands for it, padded out to a 44px
   touch target. Above 420px the label returns with ≥34px of clearance. */
@media (max-width: 419px) {
  .masthead .masthead-note {
    /* the padding grows the hit area; the negative offset keeps the ARROW on the content edge */
    left: calc(var(--s-5) - var(--s-4)); padding: var(--s-3) var(--s-4); font-size: var(--t-sm);
  }
  .masthead-note-label {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
}
.wordmark {
  font-family: var(--font-display); font-size: var(--t-md); font-weight: 600; color: var(--ink);
  text-decoration: none; letter-spacing: -0.01em;
  display: inline-flex; align-items: center; gap: var(--s-3);
  /* The mark costs ~42px of the row, which on a narrow phone was enough to break the brand name
     across two lines. The name never wraps; the tagline beside it gives way instead. */
  white-space: nowrap; flex: 0 0 auto;
}
/* The mark is black line art on transparency, so it reads on the paper masthead as drawn.
   Height-driven: it is a wide lockup (aspect 1.247) and the height is what carries the weight.
   Sized well above the 20px wordmark on purpose (Sam, 2026-09-06: "make the mark bigger") — the
   mark leads and the serif name reads as its companion. 54px starts to make a sticky header feel
   heavy; this is the last step before that. */
.wordmark-mark { height: 46px; width: auto; display: block; }
@media (max-width: 719px) { .wordmark-mark { height: 36px; } }
.wordmark:hover { color: var(--accent); }
.masthead-note { font-size: var(--t-xs); color: var(--ink-soft); letter-spacing: 0.02em; }

/* ---- the signature device: a matted, hairline-framed print --------------- */
.frame {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  padding: var(--s-3);
  box-shadow: var(--shadow-1);
}
.frame img { display: block; width: 100%; height: auto; border-radius: var(--r-xs); background: var(--paper-sunk); }

/* ---- post-purchase canvas upsell (Ask 10.1, success page only) ----------- */
.upsell-offer { margin-top: var(--s-6); padding-top: var(--s-5); border-top: 1px solid var(--rule); text-align: center; }
.upsell-offer .frame { max-width: 320px; margin: 0 auto var(--s-4); }
.upsell-offer p { margin-bottom: var(--s-4); font-family: var(--font-display); font-size: var(--t-base); }

/* ---- hero ---------------------------------------------------------------- */
.hero .wrap {
  display: grid; gap: var(--s-8);
  padding-block: var(--s-8) var(--s-7);
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .hero .wrap { grid-template-columns: 1.05fr 0.95fr; align-items: center; padding-block: var(--s-9) var(--s-8); }
}
.hero h1 { font-size: var(--t-display); margin-block: var(--s-3) var(--s-4); max-width: 15ch; }
.hero .lede { font-size: var(--t-md); color: var(--ink-soft); max-width: 46ch; line-height: 1.55; }
.hero-cta { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; margin-top: var(--s-6); }
.hero-fine { font-size: var(--t-sm); color: var(--ink-soft); margin-top: var(--s-3); }

/* three prints, fanned — deliberate asymmetry, not decoration for its own sake */
.hero-gallery { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-3); align-items: start; }
.hero-gallery .frame { transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease); }
.hero-gallery .frame:nth-child(1) { transform: rotate(-2.5deg); }
.hero-gallery .frame:nth-child(2) { transform: translateY(-14px); z-index: 2; }
.hero-gallery .frame:nth-child(3) { transform: rotate(2.5deg); }
.hero-gallery .frame:hover { transform: translateY(-20px) rotate(0deg); box-shadow: var(--shadow-2); z-index: 3; }

/* The UA's [hidden] rule loses to any author `display:` (buttons, .btn, labels…) — so an element
   toggled via the hidden attribute would still render. Make hidden mean hidden, everywhere. */
[hidden] { display: none !important; }

/* ---- buttons ------------------------------------------------------------- */
button, .btn {
  font: inherit; font-size: var(--t-sm); font-weight: 600; letter-spacing: 0.01em;
  border-radius: var(--r-sm); padding: 0.7rem 1.15rem; cursor: pointer;
  border: 1px solid transparent; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  text-decoration: none;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
button:active:not(:disabled), .btn:active { transform: translateY(1px); }

button.primary, .btn-primary { background: var(--accent); color: var(--surface); box-shadow: var(--shadow-1); }
button.primary:hover:not(:disabled), .btn-primary:hover { background: var(--accent-deep); color: var(--surface); box-shadow: var(--shadow-2); }
button.primary:disabled { background: var(--rule-firm); color: var(--surface); cursor: not-allowed; box-shadow: none; }

button.secondary, .btn-secondary {
  background: var(--surface); color: var(--accent); border-color: var(--rule-firm);
}
button.secondary:hover:not(:disabled), .btn-secondary:hover { border-color: var(--accent); background: var(--accent-wash); }
button.secondary:disabled { color: var(--ink-soft); border-color: var(--rule); background: transparent; cursor: not-allowed; }

.btn-lg { font-size: var(--t-base); padding: 0.9rem 1.6rem; }

/* the third, quietest tier — used for anything that is not the main path */
button:not(.primary):not(.secondary) {
  background: transparent; color: var(--ink-soft); border-color: var(--rule);
}
button:not(.primary):not(.secondary):hover { color: var(--ink); border-color: var(--rule-firm); }

/* ---- pricing ------------------------------------------------------------- */
.prices { background: var(--paper-sunk); border-block: 1px solid var(--rule); padding-block: var(--s-7); }
.prices-head { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: baseline; justify-content: space-between; margin-bottom: var(--s-5); }
.prices h2 { font-size: var(--t-lg); }
.price-list { display: grid; gap: var(--s-4); grid-template-columns: 1fr; }
@media (min-width: 720px) { .price-list { grid-template-columns: repeat(3, 1fr); } }
.price {
  background: var(--surface); border: 1px solid var(--rule); border-radius: var(--r-md);
  padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-2);
}
.price.is-value { border-color: var(--accent); box-shadow: var(--shadow-1); }
.price-name { font-family: var(--font-display); font-size: var(--t-md); font-weight: 600; }
.price-fig { font-size: var(--t-xl); font-family: var(--font-display); letter-spacing: -0.02em; }
.price-unit { font-size: var(--t-sm); color: var(--ink-soft); }
.price-tag {
  align-self: flex-start; font-size: var(--t-xs); font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--accent); background: var(--accent-wash);
  border-radius: 999px; padding: 0.2rem 0.6rem;
}

/* ---- how it works -------------------------------------------------------- */
.how .wrap { padding-block: var(--s-7); }
.how-list { display: grid; gap: var(--s-5); grid-template-columns: 1fr; counter-reset: how; margin-top: var(--s-5); }
@media (min-width: 720px) { .how-list { grid-template-columns: repeat(3, 1fr); gap: var(--s-6); } }
.how-item { counter-increment: how; padding-top: var(--s-4); border-top: 2px solid var(--rule-firm); }
.how-item::before {
  content: counter(how); font-family: var(--font-display); font-size: var(--t-md);
  color: var(--accent); display: block; margin-bottom: var(--s-1);
}
.how-item h3 { font-size: var(--t-base); margin-bottom: var(--s-1); }
.how-item p { font-size: var(--t-sm); color: var(--ink-soft); }

/* ---- studio -------------------------------------------------------------- */
.studio { padding-block: var(--s-8) var(--s-9); }
/* One combined headline line (Sam, 2026-09-11): the two headline+subheading pairs merged into
   a single sentence that spans the FULL gallery container (no --measure cap). The vw-clamped
   size keeps it one line across desktop widths of the 1360px-max wrap; it wraps on phones. */
.studio-head { margin-bottom: var(--s-7); }
/* Deliberately NO white-space:nowrap: the vw sizes below are measured to fit one line with
   slack at every width >900, and if font metrics ever jitter the failure mode must be a wrap,
   never horizontal overflow. */
.studio-head h2 { font-size: clamp(15px, 1.85vw, 26px); margin-bottom: 0; }
/* The gallery wrap jumps 1120 -> 1360 past 1280, so one smooth vw size cannot fill both
   regimes; the wide regime gets its own coefficient (measured, not guessed). */
@media (min-width: 1281px) {
  .studio-head h2 { font-size: min(1.9vw, 28px); }
}
@media (max-width: 900px) {
  .studio-head h2 { white-space: normal; font-size: var(--t-md); }
}

.step { border-top: 1px solid var(--rule); padding-block: var(--s-7); }
.step:first-of-type { border-top: none; padding-top: var(--s-2); }
.step-head { display: flex; gap: var(--s-4); align-items: baseline; margin-bottom: var(--s-5); }
.step-n {
  font-family: var(--font-display); font-size: var(--t-sm); font-weight: 700;
  color: var(--accent); background: var(--accent-wash);
  width: 2rem; height: 2rem; flex: none; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
}
.step h2 { font-size: var(--t-xl); }
.step-sub { max-width: var(--measure); color: var(--ink-soft); font-size: var(--t-sm); margin-top: var(--s-2); }

/* ---- template chooser (markup class + JS-generated classes) -------------- */
/* Occasion moved from a section heading onto the tile itself when the chooser became one grid. */
/* SCOPED to .template-choice (2026-09-10). Unscoped, `.tpl-occasion` is (0,1,0) and lost to
   `.template-choice span` (0,1,1) further down this file, so this font-size never applied — the
   caption rendered at 13px, IDENTICAL to the title. That missing hierarchy is a large part of why
   the tile read as cluttered. */
.template-choice .tpl-occasion {
  display: block; font-size: var(--t-xs); text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--ink-soft); padding: 0 var(--s-1) var(--s-1);
}
.template-choice.selected .tpl-occasion { color: var(--accent); }
/* Fixed tracks, not fractions: most occasions hold only 1-2 designs, so `1fr`
   columns would blow a single card up to the full width and make the chooser
   thousands of pixels tall. */
/* Explicit 1fr tracks, NOT auto-fill with a capped max (Sam, 2026-09-10: the cards were "very
   small and cluttered" and "oddly offset left"). `repeat(auto-fill, minmax(132px, 150px))` plus
   `justify-content: start` could never grow a track past its max, so at 1440 the grid filled only
   924px of a 1072px container and dumped 148px of dead space on the right — which read as the
   whole gallery being shoved left. 1fr tracks always consume the full container, so that dead
   space cannot exist, and the column COUNT becomes a deliberate choice per width instead of a
   side effect of tile width.
   2 columns up to 680px (phones and narrow windows — Sam was seeing three), 3 to 1024, 4 above.
   `.templates-row` dropped from the selector: U6.5 removed previous-takes, its only consumer. */
.template-grid {
  display: grid; gap: var(--s-4);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 680px)  { .template-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .template-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* On a wide monitor the gallery was a 1120px strip with ~400px of dead margin either side, so
   even 4 large tiles read small — the specific gap against companionarchive.com, which runs
   essentially full-bleed. The studio section (headline + chooser) widens to 1360 past 1280px,
   taking the tiles from 256px to ~316px. Only THIS section widens: .wrap stays 1120 everywhere
   else so running text keeps a readable measure. */
@media (min-width: 1280px) { .studio > .wrap { max-width: 1360px; } }

/* --- grid density control (2026-09-16) -------------------------------------------------------
   An EXPLICIT column choice must beat the responsive defaults above. `.template-grid[data-cols]`
   is (0,2,0) against the media queries' (0,1,0), so it wins on specificity regardless of source
   order — it does not depend on sitting after them, though it does. No attribute is written until
   the visitor picks one, so the default experience is untouched. */
.template-grid[data-cols="1"] { grid-template-columns: minmax(0, 1fr); }
.template-grid[data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.template-grid[data-cols="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* One-up is a phone-only setting, so this cap is really a safety net: below 680px it exceeds the
   viewport and the tile is simply full width, which is the whole point of the setting there. The
   680px media query further down overrides both declarations, so a stored 1 can never paint a
   single marooned tile down the middle of a 1360px gallery. */
.template-grid[data-cols="1"] { max-width: min(100%, 420px); margin-inline: auto; }

.grid-density { display: inline-flex; gap: 2px; padding: 2px; margin-left: auto;
  border: 1px solid var(--rule); border-radius: var(--r-sm); background: var(--surface); }
.grid-density .density-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 26px; padding: 0; border: 0; border-radius: 3px;
  background: transparent; color: var(--ink-soft); cursor: pointer;
}
.grid-density .density-btn svg { width: 15px; height: 15px; fill: currentColor; display: block; }
.grid-density .density-btn:hover { color: var(--ink); }
/* Scoped past the global `button:not(.primary):not(.secondary)` rule, which is (0,2,1) and forces
   a transparent background — the same specificity trap that flattened U9.5b's split header. */
.grid-density button.density-btn.is-active { background: var(--accent-wash); color: var(--accent); }

/* A tile label is a single unbreakable word often enough ("Monochrome", "Personality") that at
   four-across on a 375px phone it runs 105px out of a 52px track. Measured: 294 labels overflowing
   and 17px of horizontal page scroll. `anywhere` is the general guard — it costs nothing at any
   width where the word already fits, and no future narrow layout can reintroduce the same bug. */
.template-choice .tpl-name, .template-choice .tpl-occasion { overflow-wrap: anywhere; }
/* The offered densities differ by width (Sam, 2026-09-16): 1 or 2 on a phone, 2 or 4 above 680px.
   Both the hiding and the clamping for that live in the two media queries at the end of this
   block, so the rule and the reason it exists sit together. */
/* "Most popular" is a toggle like the seasonal chip, not a theme. It only ever renders when the
   server reports enough attributed sales, so there is no disabled/empty state to style. */
.filter-chip--popular.is-active { border-color: var(--accent); color: var(--accent); }

/* THE CLAMPS LIVE HERE, not only in JS. The script also clamps a stored preference the current
   width does not offer, but that runs on matchMedia/resize events — and narrowing the viewport was
   MEASURED leaving data-cols="4" in place because neither event fired. A layout guarantee must not
   depend on an event arriving, so CSS enforces both outright and the JS clamp is left to keep the
   button highlight honest. */
@media (max-width: 679px) {
  .grid-density .density-btn--wide { display: none; }
  /* On a phone, data-cols="4" simply renders two columns. */
  .template-grid[data-cols="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 680px) {
  .grid-density .density-btn--narrow { display: none; }
  /* And one-up is a phone setting: on anything wider, data-cols="1" renders two columns. The
     centred 420px cap has to be undone with it, or the two columns would be squeezed into the
     width meant for one card. */
  .template-grid[data-cols="1"] {
    grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: none; margin-inline: 0;
  }
}



.template-choice {
  border: 1px solid var(--rule); background: var(--surface); border-radius: var(--r-md);
  padding: var(--s-2); cursor: pointer; text-align: left; display: block; width: 100%;
  box-shadow: var(--shadow-1);
  transition: transform var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.template-choice img {
  width: 100%; aspect-ratio: 3/4; object-fit: cover; display: block; /* U5.2: mockup scene is 3:4 */
  border-radius: var(--r-xs); background: var(--paper-sunk);
}
.template-choice span {
  display: block; font-size: var(--t-sm); color: var(--ink); padding: var(--s-3) var(--s-1) 0;
}
/* Bigger and bolder (Sam, 2026-09-10): set in the display face at 600 so a tile reads as a named
   card rather than a caption, and clearly outranks the uppercase theme label beneath it.
   MUST be scoped to .template-choice — `.template-choice span` above is (0,1,1) and a bare
   `.tpl-name` (0,1,0) would silently lose to it, the same specificity trap that flattened the
   split order header in U9.5b. */
.template-choice .tpl-name {
  font-family: var(--font-display); font-size: var(--t-base); font-weight: 600;
  line-height: 1.2; color: var(--ink); padding: var(--s-3) var(--s-1) 0;
}
@media (min-width: 1024px) { .template-choice .tpl-name { font-size: var(--t-md); } }
.template-choice:hover { transform: translateY(-3px); border-color: var(--rule-firm); box-shadow: var(--shadow-2); }
.template-choice.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), var(--shadow-2); }
.template-choice.selected span { color: var(--accent); font-weight: 600; }

/* ---- chooser filters (U4.1: browse by theme, filter by occasion) --------- */
.chooser-filters { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); margin-bottom: var(--s-5); }
.filter-chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.filter-chip {
  font: inherit; font-size: var(--t-sm); line-height: 1;
  border: 1px solid var(--rule-firm); background: var(--surface); color: var(--ink-soft);
  border-radius: 999px; padding: 0.5rem 0.9rem; cursor: pointer;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease);
}
.filter-chip:hover { color: var(--ink); border-color: var(--accent); }
.filter-chip.is-active { background: var(--accent-wash); border-color: var(--accent); color: var(--accent); font-weight: 600; }
/* Sam, 2026-09-14: the seasonal quick-filter reads BOLD even at rest — accent ink + weight,
   filled like is-active when toggled on. Scoped class; delete with the season. */
/* Scoped to (0,2,1)+ so it outranks the global button:not(.primary):not(.secondary) rule —
   the same specificity trap recorded for U9.5b and the tile titles. */
.chooser-filters button.filter-chip--holiday { font-weight: 700; color: var(--accent); border-color: var(--accent); }
.chooser-filters button.filter-chip--holiday:hover { color: var(--accent-deep); border-color: var(--accent-deep); }
.chooser-filters button.filter-chip--holiday.is-active { background: var(--accent); color: var(--paper); }
.filter-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.filter-occasion {
  font: inherit; font-size: var(--t-sm); color: var(--ink);
  border: 1px solid var(--rule-firm); background: var(--surface); border-radius: var(--r-sm);
  padding: 0.5rem 0.6rem; cursor: pointer;
}
.filter-occasion:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.filter-count { font-size: var(--t-sm); color: var(--ink-soft); margin-left: auto; white-space: nowrap; }
.chooser-empty { color: var(--ink-soft); font-size: var(--t-sm); padding: var(--s-5) 0; }
@media (max-width: 480px) { .filter-count { margin-left: 0; width: 100%; } }

/* ---- U4.4: two-pet badge + upload slots ---------------------------------- */
.template-choice { position: relative; }
.tpl-badge {
  position: absolute; top: var(--s-3); left: var(--s-3);
  background: var(--accent); color: #fff; font-size: var(--t-xs); font-weight: 600;
  padding: 0.15rem 0.5rem; border-radius: 999px; letter-spacing: 0.02em;
}
.upload-slots { display: flex; flex-wrap: wrap; gap: var(--s-5); align-items: flex-start; }
.upload-slot-label { display: block; font-size: var(--t-sm); font-weight: 600; margin-bottom: var(--s-2); color: var(--ink); }

/* ---- the ad band: the product in use ------------------------------------- */
/* U6.2. Copy sits UNDER each image rather than over it: the site's whole register is restrained,
   and overlaid type on a warm photographed interior is a legibility gamble the brand does not need.
   The images are 16:9 and carry width/height so the page cannot reflow as they load. */
.scene-band {
  border-top: 1px solid var(--rule); padding-block: var(--s-7);
  display: grid; gap: var(--s-7); grid-template-columns: 1fr;
}
@media (min-width: 900px) { .scene-band { grid-template-columns: 1fr 1fr; gap: var(--s-6); } }
.scene { margin: 0; }
/* No aspect-ratio and no object-fit: each image element carries its real width and height, so
   the browser reserves the exact box and the whole render shows uncropped. */
.scene img {
  display: block; width: 100%; height: auto;
  border-radius: var(--r-md); background: var(--paper-sunk); box-shadow: var(--shadow-1);
}
.scene figcaption { margin-top: var(--s-4); }
.scene figcaption h2 { font-size: var(--t-md); margin-bottom: var(--s-2); }
.scene figcaption p { color: var(--ink-soft); font-size: var(--t-sm); max-width: 44ch; }

/* ---- upload: the dropzone IS the call to action -------------------------- */
/* U6.1: was a bare <input type=file> behind a step-number badge. The upload is the only thing
   this page asks for, so it gets the weight: one large target, click OR drag, with rotating
   photo guidance inside it. The <label for> makes the whole panel clickable and keeps the real
   input as the accessible control — .sr-only hides it visually but leaves it focusable, and
   :focus-within paints the ring on the panel. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.dropzone {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s-2); text-align: center; cursor: pointer;
  min-height: 210px; padding: var(--s-6) var(--s-5);
  border: 1px dashed var(--rule-firm); border-radius: var(--r-md);
  background: var(--surface); color: var(--ink);
  transition: border-color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease);
}
.dropzone:hover { border-color: var(--accent); background: var(--accent-wash); }
.upload-slot:focus-within .dropzone { outline: 2px solid var(--accent); outline-offset: 3px; }
.dropzone.is-dragging { border-color: var(--accent); border-style: solid; background: var(--accent-wash); box-shadow: var(--shadow-2); }
.dropzone.has-file { border-style: solid; border-color: var(--rule-firm); background: var(--paper-sunk); }
.dropzone-icon { width: 30px; height: 30px; color: var(--accent); }
.dropzone-action { font-family: var(--font-display); font-size: var(--t-md); font-weight: 600; }
.dropzone-sub { font-size: var(--t-sm); color: var(--ink-soft); }
/* The rotating hint. Reserved height so the panel never jumps as the text swaps. */
.dropzone-tip {
  margin-top: var(--s-3); min-height: 2.6em; max-width: 30ch;
  font-size: var(--t-sm); color: var(--ink-soft);
  display: flex; align-items: center; justify-content: center;
  transition: opacity var(--dur-1) var(--ease);
}
.dropzone-tip.is-swapping { opacity: 0; }
.dropzone-chosen { font-size: var(--t-sm); font-weight: 600; color: var(--accent); word-break: break-all; }
.upload-fineprint { margin-top: var(--s-3); }
@media (max-width: 719px) { .dropzone { min-height: 170px; padding: var(--s-5) var(--s-4); } }
/* Rotating text is motion. When it is not wanted, the tips stop cycling — upload.js renders them
   as one static line instead, so none of the guidance is lost. */
@media (prefers-reduced-motion: reduce) { .dropzone-tip { transition: none; min-height: 0; } }

/* ---- upload -------------------------------------------------------------- */
input[type=file] { font: inherit; font-size: var(--t-sm); color: var(--ink-soft); max-width: 100%; }
input[type=file]::file-selector-button {
  font: inherit; font-size: var(--t-sm); font-weight: 600;
  background: var(--surface); color: var(--accent); border: 1px solid var(--rule-firm);
  border-radius: var(--r-sm); padding: 0.6rem 1rem; margin-right: var(--s-3); cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
input[type=file]::file-selector-button:hover { background: var(--accent-wash); border-color: var(--accent); }

/* ---- status -------------------------------------------------------------- */
.status { font-size: var(--t-sm); color: var(--ink-soft); margin-top: var(--s-3); min-height: 1.4em; }
.status.error { color: var(--danger); font-weight: 600; }

/* ---- the reveal: the one moment the whole product turns on ---------------
   U5.5: this is the payoff, so it gets the screen — centred, no competing chrome, and no mat
   frame around it. The image is already a photographed product shot on its own studio ground;
   framing a photo of a card reads as fussy and shrinks the card twice over. */
.reveal-hero { text-align: center; padding: var(--s-6) 0 var(--s-7); }
.reveal-eyebrow {
  font-size: var(--t-xs); font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-soft); margin-bottom: var(--s-3);
}
.reveal-title { font-size: var(--t-2xl); margin-bottom: var(--s-5); }
.reveal-note {
  margin: var(--s-5) auto 0; max-width: 46ch; color: var(--ink-soft); font-size: var(--t-sm);
}
#reveal-wrap { display: flex; justify-content: center; }
#reveal-img {
  width: 100%; max-width: 480px; display: block; border-radius: var(--r-sm);
  background: var(--paper-sunk); aspect-ratio: 3/4; object-fit: cover;
  animation: reveal var(--dur-2) var(--ease) both;
}
@keyframes reveal { from { opacity: 0; transform: scale(.985) translateY(6px); } to { opacity: 1; transform: none; } }

/* ---- change-it options: one quiet line directly under the card ----------- */
/* U6.5 (Sam: put it where the note was, "and make it smaller"). It replaced a full section with a
   heading; now it is a single centred row that does not compete with the reveal above it. */
.refine-row {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: var(--s-3); margin-top: var(--s-5);
}
.refine-label { font-size: var(--t-sm); color: var(--ink-soft); }
.btn-sm { font-size: var(--t-xs); padding: 0.4rem 0.75rem; min-height: 34px; }

/* ---- fix a spot: the drag happens ON the revealed card -------------------- */
/* U6.5 (Sam: "have the fix a spot tool work on the card image ... rather than popping up another
   image"). The reveal is a product shot, so the drag is clamped to the printed panel — a box over
   the table or the fold would map nowhere. Coordinates leave as PANEL fractions; the server turns
   them into master pixels (it alone knows the master size and the cover-fit crop). */
#reveal-stage { position: relative; display: inline-block; line-height: 0; }
/* `touch-action: none` is what makes fix-a-spot WORK ON A PHONE (Sam, 2026-09-17). While the card
   is the drag surface the browser must not claim the gesture for scrolling: it decides that before
   any handler runs, so `preventDefault()` on pointerdown cannot override it — only this can. Scoped
   to `.is-fixing` so the page still scrolls normally past the card the rest of the time. The
   callout/selection suppression stops a long press raising iOS's "Save Image" sheet mid-drag. */
#reveal-stage.is-fixing,
#reveal-stage.is-fixing #reveal-img {
  cursor: crosshair;
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none; user-select: none;
}
#reveal-stage.is-fixing::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  box-shadow: 0 0 0 2px var(--accent); border-radius: var(--r-sm);
}
#reveal-stage #fix-rect {
  position: absolute; display: none; pointer-events: none;
  border: 2px solid var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.fix-hint { margin-top: var(--s-4); }
.fix-row { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; justify-content: center; margin-top: var(--s-3); }
.fix-row input[type=text] { flex: 1 1 22rem; max-width: 34rem; }
#fix-section { text-align: center; }
#fix-section .fix-result-row { justify-content: center; }


/* ---- fix editor ---------------------------------------------------------- */
#fix-canvas-wrap { position: relative; display: inline-block; max-width: 100%; cursor: crosshair; border-radius: var(--r-sm); overflow: hidden; }
#fix-target-img { display: block; max-width: 100%; height: auto; user-select: none; -webkit-user-drag: none; }
#fix-rect { position: absolute; border: 2px dashed var(--accent); background: rgba(162,75,36,.14); display: none; pointer-events: none; }
#fix-note {
  width: 100%; max-width: 480px; margin-top: var(--s-3); padding: 0.6rem 0.75rem;
  font: inherit; font-size: var(--t-sm);
}
.fix-actions { margin-top: var(--s-4); display: flex; gap: var(--s-3); flex-wrap: wrap; }
.fix-result-row { display: flex; gap: var(--s-5); margin-top: var(--s-5); flex-wrap: wrap; }
.fix-result-row figure { margin: 0; }
.fix-result-row img { max-width: 260px; display: block; border-radius: var(--r-sm); border: 1px solid var(--rule); }
.fix-result-row figcaption { font-size: var(--t-xs); text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-soft); margin-top: var(--s-2); }

/* ---- card form ----------------------------------------------------------- */
.card-layout { display: grid; gap: var(--s-7); grid-template-columns: 1fr; align-items: start; }
@media (min-width: 860px) { .card-layout { grid-template-columns: 1fr auto; } }
.card-form label { display: block; font-size: var(--t-sm); font-weight: 600; color: var(--ink); margin-top: var(--s-4); }
.card-form label:first-child { margin-top: 0; }
.card-form input[type=text], .card-form textarea, .card-form select, #fix-note {
  width: 100%; max-width: 460px; margin-top: var(--s-2); padding: 0.6rem 0.75rem;
  font: inherit; font-size: var(--t-sm); font-weight: 400; color: var(--ink);
  background: var(--surface); border: 1px solid var(--rule-firm); border-radius: var(--r-sm);
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.card-form input[type=text]:hover, .card-form textarea:hover, .card-form select:hover { border-color: var(--ink-soft); }
.card-form input[type=text]:focus, .card-form textarea:focus, .card-form select:focus, #fix-note:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash);
}
.card-form textarea { resize: vertical; }
.card-form-row { display: flex; gap: var(--s-4); flex-wrap: wrap; }
.card-form-row label { flex: 1 1 160px; }

#card-preview-wrap { margin-top: 0; }
/* U5.1d: 3:4, matching the card mockup the preview now shows (was 250x350, the bare 5:7 panel). */
#card-preview {
  position: relative; width: 250px; height: 333px; overflow: hidden;
  border-radius: var(--r-sm); background: var(--paper-sunk);
}
#card-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The #card-preview-text overlay is GONE (2026-09-09): front-cover text was removed from the
   product, so the card preview is the product shot alone, with nothing drawn over the panel. */

/* ---- U9.5b: the SPLIT ORDER HEADER --------------------------------------
   Two segments across the top of the buy panel, divided by one hairline. The active segment is
   raised paper carrying the serif title; the inactive one is recessed (paper-sunk) and reads as
   the alternative you can switch to. Deliberately NOT browser-style tabs — Sam asked for the
   split he mocked up, and a split band suits the "print studio" register better: it looks like
   two faces of one object rather than UI chrome. All values from the token scale. */
.order-split {
  display: grid; grid-template-columns: 1fr 1fr;
  border: 1px solid var(--rule-firm); border-radius: var(--r-md);
  overflow: hidden; margin-bottom: var(--s-5);
}
/* Scoped as `.order-split button.order-seg` deliberately: the global catch-all
   `button:not(.primary):not(.secondary)` is specificity (0,2,1) and forces
   `background: transparent`, which silently flattened these segments. Matching its
   specificity and sitting later in the file is what makes the split visible at all. */
.order-split button.order-seg {
  appearance: none; border: 0; margin: 0; cursor: pointer; text-align: left;
  padding: var(--s-4) var(--s-5);
  display: flex; flex-direction: column; gap: var(--s-1);
  background: var(--paper-sunk); color: var(--ink-soft);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.order-split button.order-seg + button.order-seg { border-left: 1px solid var(--rule-firm); }
.order-split button.order-seg:hover { background: color-mix(in srgb, var(--paper-sunk) 60%, var(--surface)); color: var(--ink); }
/* U9.5c (Sam, 2026-09-15): "the selected color looks backward. the darker color appears
   selected" — the raised-vs-recessed paper metaphor lost to the page's own convention: the
   pack cards directly below mark SELECTED with an accent border. The active segment now
   carries the same accent ring (inset box-shadow, so the butted grid never shifts), which is
   the cue a buyer has already learned by the time they reach this control. */
.order-split button.order-seg.is-active {
  background: var(--surface); color: var(--ink); cursor: default;
  box-shadow: inset 0 0 0 2px var(--accent);
}
.order-split button.order-seg:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
.order-seg-title { font-family: var(--font-display); font-size: var(--t-md); font-weight: 600; line-height: 1.15; letter-spacing: -0.01em; }
.order-seg-note { font-size: var(--t-xs); letter-spacing: 0.02em; font-weight: 400; }
.order-split button.order-seg.is-active .order-seg-note { color: var(--ink-soft); }
/* The inactive segment's price should read as an invitation, not fine print. */
.order-split button.order-seg:not(.is-active) .order-seg-note { color: var(--accent); font-weight: 600; }
@media (max-width: 599px) {
  .order-split { grid-template-columns: 1fr; }
  .order-split button.order-seg + button.order-seg { border-left: 0; border-top: 1px solid var(--rule-firm); }
}

.order-body { margin-bottom: var(--s-5); }

/* The price, stated once and unambiguously: amount in the accent, the per-order clarification
   in quiet body text right beside it. "isnt good" was the old one-line version that left
   per-card vs per-order open. */
.inside-price { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2); margin-bottom: var(--s-5); }
.inside-price-amount { font-family: var(--font-display); font-size: var(--t-md); font-weight: 600; color: var(--accent); }
.inside-price-explain { font-size: var(--t-sm); color: var(--ink-soft); max-width: 52ch; }

/* Form fields: the label is a small caps eyebrow ABOVE its control, and the control is a block so
   it can never sit inline beside the label text (which is what made this look sloppy once the
   panel got wider than the old standalone section). */
.card-form .field { display: block; margin-top: var(--s-5); }
.card-form .field:first-child { margin-top: 0; }
.card-form .field-label {
  display: block; margin-bottom: var(--s-2);
  font-size: var(--t-xs); text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--ink-soft); font-weight: 600;
}
.card-form .field > select, .card-form .field > textarea { display: block; }
.card-form .field-narrow > select { max-width: 220px; }
.inside-counter {
  margin-top: var(--s-2); font-size: var(--t-xs); color: var(--ink-soft);
  max-width: 460px; text-align: right; font-variant-numeric: tabular-nums;
}
/* The preview is the REAL printed panel — Ask 14.1 made the upload canvas-native, so this is now
   the vendor's FULL BLEED CANVAS (1687x2325 = 0.7258), not the old 1537x2175 image slot — so the
   box holds that ratio and the image is contained inside it — never cropped, because those are
   the customer's own words. */
#inside-preview {
  position: relative; width: 236px; aspect-ratio: 1687 / 2325; overflow: hidden;
  border-radius: var(--r-xs); background: #fff;
  display: flex; align-items: center; justify-content: center;
}
#inside-preview img { width: 100%; height: 100%; object-fit: contain; display: block; }
.inside-preview-empty {
  margin: 0; padding: 0 var(--s-5); text-align: center;
  font-size: var(--t-xs); color: var(--ink-soft); line-height: 1.5;
}

/* ---- buy ----------------------------------------------------------------- */
.buy-panel {
  margin-top: var(--s-7); background: var(--surface); border: 1px solid var(--rule-firm);
  border-radius: var(--r-md); padding: var(--s-5); box-shadow: var(--shadow-1);
}
/* U5.5: the panel now follows the reveal hero, which already carries its own bottom padding. */
#buy-section .buy-panel { margin-top: 0; }
/* h2, not h3: U5.5 lifted this panel to sit directly under the reveal's h1, so an h3 here would
   skip a heading level and break the document outline for screen readers. */
.buy-panel h2 { font-size: var(--t-md); margin-bottom: var(--s-2); }
.buy-row { display: flex; gap: var(--s-4); flex-wrap: wrap; align-items: flex-end; margin-top: var(--s-4); }
.buy-row label { display: block; font-size: var(--t-sm); font-weight: 600; flex: 1 1 280px; }
.buy-note { font-size: var(--t-xs); color: var(--ink-soft); margin-top: var(--s-4); }

/* ---- pack chooser + sticky buy bar --------------------------------------- */
.packs { border: 0; margin: 0; padding: 0; }
.packs legend { padding: 0; margin-bottom: var(--s-3); }
.pack-list { display: grid; gap: var(--s-3); }
@media (min-width: 720px) { .pack-list { grid-template-columns: repeat(3, 1fr); align-items: stretch; } }

.pack { position: relative; display: block; cursor: pointer; }
/* The input stays focusable and reachable by keyboard — clipped, never display:none. */
.pack input {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.pack-body {
  display: flex; flex-direction: column; gap: var(--s-1); height: 100%;
  border: 1px solid var(--rule-firm); border-radius: var(--r-md);
  padding: var(--s-4); background: var(--paper);
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease),
              background-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.pack:hover .pack-body { border-color: var(--accent); transform: translateY(-2px); }
.pack input:checked + .pack-body {
  border-color: var(--accent); background: var(--surface);
  box-shadow: 0 0 0 1px var(--accent), var(--shadow-1);
}
.pack input:focus-visible + .pack-body { outline: 2px solid var(--accent); outline-offset: 3px; }
.pack-name { font-weight: 600; font-size: var(--t-sm); }
.pack-price { font-family: var(--font-display); font-size: var(--t-lg); letter-spacing: -0.02em; }
.pack-note { font-size: var(--t-xs); color: var(--ink-soft); }
.pack-tag {
  align-self: flex-start; font-size: var(--t-xs); font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--accent); background: var(--accent-wash);
  border-radius: 999px; padding: 0.15rem 0.55rem; margin-bottom: var(--s-1);
}

/* U5.5b: fixed at EVERY width, not just phones. Moving the order panel above the message form
   meant a desktop customer who scrolled down to add a message had to scroll back up to Buy — the
   bar removes that entirely, and keeps "now buy" one click away from any point on the page.
   Full-bleed background, but the CONTENTS align to the same 1120px column as .wrap, so on a wide
   screen the price and button sit under the page's own grid rather than pinned to the far edges. */
.buy-bar {
  display: flex; align-items: center; gap: var(--s-4);
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  margin: 0;
  padding-block: var(--s-3);
  padding-bottom: calc(var(--s-3) + env(safe-area-inset-bottom, 0px));
  padding-inline: max(var(--s-4), calc((100vw - 1120px) / 2 + var(--s-5)));
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  backdrop-filter: saturate(1.3) blur(8px);
  border-top: 1px solid var(--rule-firm);
  box-shadow: 0 -6px 24px rgba(32,27,21,.10);
}
.buy-bar-sum { display: flex; align-items: baseline; gap: var(--s-3); flex: 1 1 auto; }
.buy-bar-label { font-size: var(--t-sm); color: var(--ink-soft); }
.buy-bar-price { font-family: var(--font-display); font-size: var(--t-md); }
.buy-bar #buy-btn { flex: 0 0 auto; min-width: 8.5rem; }

/* ---- studio tools (fulfillment-only, kept out of the customer's way) ----- */
.studio-tools { margin-top: var(--s-6); border-top: 1px dashed var(--rule); padding-top: var(--s-4); }
.studio-tools summary { font-size: var(--t-xs); color: var(--ink-soft); cursor: pointer; letter-spacing: 0.06em; text-transform: uppercase; }
.studio-tools[open] summary { margin-bottom: var(--s-3); }

/* ---- footer -------------------------------------------------------------- */
footer.site {
  border-top: 1px solid var(--rule); background: var(--paper-sunk);
  padding-block: var(--s-6); font-size: var(--t-sm); color: var(--ink-soft);
}
footer.site .wrap { display: flex; flex-wrap: wrap; gap: var(--s-3) var(--s-5); justify-content: space-between; align-items: center; }
footer.site nav { display: flex; gap: var(--s-5); flex-wrap: wrap; }

/* ---- motion preference --------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
  .hero-gallery .frame, .hero-gallery .frame:nth-child(1), .hero-gallery .frame:nth-child(2), .hero-gallery .frame:nth-child(3) { transform: none; }
}
/* Must come after the footer.site rule above: same specificity, so source order decides.
   Without this the fixed buy bar covers the last ~84px of the footer. The bar is fixed at every
   width now (U5.5b), so the clearance is no longer phone-only — but it is scoped to pages that
   actually HAVE a bar, or the gallery and style pages would carry 5.5rem of dead space for
   nothing. .buy-bar exists only in create.html; where :has() is unsupported the bar simply
   overlaps the footer, which is what it did before. */
body:has(.buy-bar) footer.site { padding-bottom: calc(var(--s-6) + 5.5rem); }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* ---- U5.3: style page + link tiles ----------------------------------------- */
a.template-choice { text-decoration: none; color: inherit; }
.style-hero { display: grid; gap: var(--s-6); grid-template-columns: 1fr; align-items: start; margin-bottom: var(--s-7); }
@media (min-width: 760px) { .style-hero { grid-template-columns: minmax(280px, 420px) 1fr; gap: var(--s-8); } }
.style-hero-card img { width: 100%; height: auto; display: block; border-radius: var(--r-md); box-shadow: var(--shadow-2); }
.style-hero h1 { font-size: var(--t-2xl); margin: var(--s-2) 0 var(--s-3); }
/* U6.4: the upload sits in the hero's right column, directly under the card title — Sam wanted the
   tool beside the sample card rather than a scroll below it, and the explainer paragraph gone. It
   is no longer a page-level .step, so it carries none of that chrome. */
.hero-upload { margin-top: var(--s-5); }
.hero-upload h2 { font-size: var(--t-md); margin-bottom: var(--s-4); }
/* In a hero column the zone has less room than it did full-width, so it gets shorter rather than
   pushing the checkmarks off the fold. */
.hero-upload .dropzone { min-height: 168px; padding: var(--s-5) var(--s-4); }
.hero-upload .upload-fineprint { margin-top: var(--s-3); }
.style-trust { list-style: none; padding: 0; margin: var(--s-6) 0 0; display: grid; gap: var(--s-2); color: var(--ink-soft); font-size: var(--t-sm); }
.style-trust li { padding-left: 1.4em; position: relative; }
.style-trust li::before { content: "\2713"; position: absolute; left: 0; color: var(--accent); font-weight: 700; }
.masthead a.masthead-note { text-decoration: none; }

/* ---- U5.4: the creation page (magic wait) ------------------------------------- */
/* U7.4: the ghost photo + breathing opacity is replaced by an honest shimmer skeleton, sized to
   match #reveal-img's own box (480px / 3:4) so the wait and the payoff read as the same object,
   not two different sizes swapped mid-page. The skeleton fully covers the (still-loading, never
   shown) ghost image beneath it — a placeholder should not half-preview the art it stands in for. */
.magic { text-align: center; max-width: 34rem; margin: var(--s-6) auto var(--s-8); }
.magic-card-wrap {
  position: relative; width: min(480px, 88vw); margin: 0 auto var(--s-6);
  border-radius: var(--r-md); overflow: hidden; box-shadow: var(--shadow-2);
}
.magic-card { width: 100%; aspect-ratio: 3/4; object-fit: cover; display: block; }
.magic-card-wrap .card-skeleton { position: absolute; inset: 0; }
/* The wait copy sits ON the placeholder rather than under it (Sam, 2026-09-10). Absolute + inset
   0 so it cannot change the wrap's box — the 3/4 aspect-ratio card stays exactly the same size
   whether the overlay is present or not, which is the invariant gl-u74/gl-u97 defend. */
.magic-overlay {
  position: absolute; inset: 0; z-index: 1;
  /* align-items: STRETCH, not center. With `center`, #generate-status shrink-to-fits its text,
     and the progress bar inside it is width:100% OF THAT — so the bar's length tracked the
     wording and would visibly jump wider at each stage change (measured: 143px on "Painting your
     pet…" vs 253px on "Still painting — this one is taking a little longer…"). Stretch pins the
     bar to the overlay's inner width; `text-align: center` still centres the words. */
  display: flex; flex-direction: column; align-items: stretch; justify-content: center;
  gap: var(--s-2); padding: var(--s-5); text-align: center;
}
/* clamp: the title has to sit inside a min(480px, 88vw) box, so it cannot use the page's --t-xl
   at every width or it wraps to four lines on a phone. */
.magic-title { font-size: clamp(1.25rem, 4.6vw, 1.75rem); margin: 0; line-height: 1.15; }
.magic-status { min-height: 1.6em; }
.magic-note { margin-top: var(--s-4); font-size: var(--t-sm); }
#magic-back { margin-top: var(--s-5); display: inline-block; }

/* ---- U7.4/U9.7: shared card placeholder + honest staged wait copy + progress bar ------------
   Replaces U3.7's generic rotating RITUAL_LINES. U9.7 (Sam, 2026-09-09) dropped the shimmer
   sweep in favour of a real percentage bar carrying the information instead — a flat placeholder
   plus a moving bar reads calmer than two competing animations. The placeholder keeps its
   dimensions and var(--paper-sunk) fill (load-bearing for layout — #reveal-stage's bounding rect
   must stay identical whether the overlay is shown or hidden; see gl-u74/gl-u97). Reused above
   (the pre-first-reveal wait, over the style's ghost photo) and below (the in-place re-roll wait,
   over the already-revealed card in #reveal-stage). */
.card-skeleton {
  background-color: var(--paper-sunk);
}
/* the re-roll skeleton sits INSIDE #reveal-stage, over the already-rendered #reveal-img — the
   box stays #reveal-img's own and never resizes; only this overlay appears and disappears. */
#reveal-skeleton { position: absolute; inset: 0; border-radius: var(--r-sm); }
/* Sam, 2026-09-13: the re-roll wait renders INSIDE the card frame (over the skeleton), like the
   initial build. #reveal-stage is line-height:0, so the overlay restores its own; the inner
   status stretches (never shrink-to-fit — the bar-width-follows-text trap, 2026-09-10), so the
   bar spans the card's width minus padding, stable across stage copy changes. */
.reveal-wait-overlay {
  position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center;
  padding: var(--s-5); line-height: 1.5; text-align: center;
}
.reveal-wait-overlay .status { width: 100%; }
/* Sam, 2026-09-13: "the status bar spreads across the whole page. it should be back in the
   sample area like the initial build." The re-roll and fix waits render into block containers
   in the page column, and .wait-progress is width:100% OF ITS CONTAINER — so the bar spanned
   the full content width. Cap both wait containers to the reveal card's own 480px and centre
   them under it, so the bar reads as belonging to the sample, exactly like the magic-stage
   overlay does on the initial build. */
.reveal-status { min-height: 1.4em; max-width: 480px; margin-inline: auto; }
#fix-status { max-width: 480px; margin-inline: auto; }

/* ---- U9.7: the progress bar + percentage — the only numeric readout in the wait ------------
   Sam: "i wouldn't show seconds, I would just show percentage" / "just show status bar,
   percentage and then the text based encouragements/updates." The elapsed-seconds counter
   (.wait-elapsed) is gone; this bar is what carries "work is underway, and roughly how much of
   it" instead. Track = --paper-sunk, fill = --accent, radii from the existing scale — no new
   colours. The fill transitions over ~900ms so per-second ticks read as motion, not jumps. */
.wait-progress {
  position: relative; width: 100%; height: 6px; margin-top: var(--s-3);
  border-radius: var(--r-xs); overflow: hidden;
  /* --rule, not --paper-sunk: this bar renders on BOTH the page ground AND (since
     2026-09-10) inside the card placeholder, which IS --paper-sunk — an identical track
     colour made the track invisible there and the fill read as a floating dash. --rule is
     the hairline token and contrasts with both grounds. */
  background: var(--rule);
}
.wait-progress-fill {
  /* full-width element REVEALED from the left by clip-path — animating `width` would thrash
     layout every tick (impeccable layout-transition), and `transform: scaleX()` would stretch
     the sheen gradient horizontally. clip-path composites and leaves the gradient undistorted. */
  height: 100%; width: 100%; border-radius: var(--r-xs);
  clip-path: inset(0 100% 0 0);
  background-color: var(--accent);
  /* a slow, subtle sheen travels across the FILLED portion only (never the empty track, which
     would imply progress that has not happened) — the honest replacement for the old card
     shimmer, now attached to something that means something. Proof of life on a very slow draw,
     where the stage text (which also keeps changing, per generationStages/fixStages) is the
     other half of that guarantee. */
  background-image: linear-gradient(100deg,
    transparent 35%, color-mix(in srgb, var(--accent) 55%, white 45%) 50%, transparent 65%);
  background-size: 250% 100%;
  animation: wait-progress-sheen 2s ease-in-out infinite;
  transition: clip-path 900ms var(--ease);
}
@keyframes wait-progress-sheen { 0% { background-position: -135% 0; } 100% { background-position: 135% 0; } }
@media (prefers-reduced-motion: reduce) {
  /* the fill still updates every tick — only the transition/sheen animation is dropped. */
  .wait-progress-fill { transition: none; animation: none; }
}
/* deliberately reuses the retired .wait-elapsed's styling idiom: small, quiet, tabular numerals —
   a truthful readout, not the headline (the stage text still carries the message). */
/* display:block, and the message below it likewise (Sam, 2026-09-11: "the percentage ... should
   be on a separate line [from] the encouraging text. the text should be a line below"). The two
   were inline siblings on one line, which read as a single run of copy and let the wrapping
   point move with the stage wording. Three stacked rows — bar, number, sentence — give each
   one a fixed place, so only the text inside them changes. */
.wait-percent {
  color: var(--ink-soft); font-size: var(--t-xs); font-variant-numeric: tabular-nums;
  display: block; margin-top: var(--s-2);
}
.wait-message { display: block; }
/* the gap belongs to the PAIR, not the message: the re-roll wait (#reveal-status) runs
   startHonestWait with no bar, where .wait-message is the only child — an unconditional
   margin-top there would shift the status box and break the zero-layout-shift invariant. */
.wait-percent + .wait-message { margin-top: var(--s-2); }

/* ---- U12.1: save-for-later — the first modals on the site --------------------------------
   Built from the site's own tokens (paper surface, hairline mat, Newsreader heading), not a
   clone of any competitor's look. A plain <dialog> centred by the UA's top-layer default
   (no manual centring math needed); ::backdrop gets a soft warm scrim instead of pure black so
   it reads as part of this palette rather than generic browser chrome. */
dialog.modal {
  border: 1px solid var(--rule-firm); border-radius: var(--r-lg);
  padding: 0; background: var(--surface); color: var(--ink);
  box-shadow: var(--shadow-2);
  width: min(420px, calc(100vw - var(--s-6)));
  max-height: calc(100vh - var(--s-7));
}
/* The centring above was the UA's top-layer default, which only applies to `showModal()`. Both
   openers fall back to `setAttribute("open")` where showModal is missing (Safari below 15.4), and
   that path renders the dialog as a STATIC BLOCK at its place in the markup — the very bottom of
   <body>, below the fold, with no backdrop. On a phone that is indistinguishable from "the popup
   never appeared". Stating the modal geometry explicitly makes the fallback usable; it is a no-op
   for the showModal path, which already computes exactly this. */
dialog.modal[open] {
  position: fixed; inset: 0; margin: auto; z-index: 50;
}
dialog.modal::backdrop {
  background: color-mix(in srgb, var(--ink) 45%, transparent);
}
.modal-body { padding: var(--s-6); }
.modal-title { font-size: var(--t-lg); margin-bottom: var(--s-3); }
.modal-text { color: var(--ink-soft); font-size: var(--t-sm); margin-bottom: var(--s-5); line-height: 1.55; }
.modal-actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-2); }
.modal-actions button { flex: 1 1 auto; }
/* The quietest tier of all — a plain text link, never competing with the two real buttons above
   it. Deliberately NOT button:not(.primary):not(.secondary) chrome (a bordered pill would read
   as a third button, muddying which action is primary). */
.modal-text-link {
  display: block; margin-top: var(--s-4); background: none; border: none; padding: 0;
  min-height: 0; font-size: var(--t-sm); font-weight: 400; color: var(--ink-soft);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.modal-text-link:hover { color: var(--ink); }

.checkbox-field {
  display: flex; align-items: flex-start; gap: var(--s-2); margin-top: var(--s-5);
  font-size: var(--t-sm); color: var(--ink-soft); cursor: pointer;
}
.checkbox-field input { margin-top: 0.2em; flex: none; }

/* The inline "Save for later" refine-row button and the resume-page friendly line share the
   row's existing quiet styling (.btn-sm / .muted) — nothing new needed here beyond spacing. */
.resume-note { margin-left: var(--s-1); }

/* --- fixed sample gallery (2026-09-16) -------------------------------------------------------
   Sits under the scene band on every style page. Explicit width/height on every <img> in the
   markup plus aspect-ratio here means the grid reserves its boxes before the lazy images arrive,
   so nothing reflows as they load — the same no-layout-shift rule the scene band follows. */
.gallery-band { margin-block: var(--s-7); }
.gallery-head { margin-bottom: var(--s-5); }
.gallery-head h2 { font-family: var(--font-display); font-size: var(--t-lg); margin: 0 0 var(--s-2); }
.gallery-head p { color: var(--ink-soft); margin: 0; max-width: var(--measure); }
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-4);
}
@media (min-width: 720px)  { .gallery-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .gallery-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.gallery-shot {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;   /* v2 tiles are 720x540: the card is portrait, so a 4:3 frame shows it larger than 16:9 did */
  object-fit: cover;
  border-radius: var(--r-xs);
  box-shadow: var(--shadow-1);
  background: var(--paper-sunk);
}
