/* ==========================================================================
   DINKOMART — ATELIER
   The craft layer. Loads after signature.css, last of all.

   ---------------------------------------------------------------------------
   WHAT THIS FILE IS FOR

   Three layers already sit under this one and each did a different job:

     refine.css     scale, rhythm and the responsive floor
     signature.css  components that only THIS business could need — the 9 PM
                    cut-off clock, the cold chain trace, the pin check
     atelier.css    what happens between those two: arrival, response, and the
                    hundred small answers an interface owes the person using it

   The diagnosis this file answers is narrow. The build responds to a POINTER
   well — tilt, magnet, lift, zoom — and responds to SCROLL well — reveal,
   parallax, wipe. What it did not do was respond to an ACTION. You pressed
   Add and a number in the corner silently became a different number. You
   saved something and a heart changed colour. You switched theme and the
   whole page hard-cut. Every one of those is a moment where the interface had
   something to say and said nothing.

   So the rule for everything below, and the reason several candidates were
   cut: an animation earns its place only if removing it would leave a
   QUESTION UNANSWERED. "Did that work?" "Where did it go?" "How much is in
   there now?" "How far down am I?" Motion that only answers "does this look
   expensive" is not in here.

   ---------------------------------------------------------------------------
   RULES OBSERVED (each has already caused a bug in this codebase)

   * SEMANTIC TOKENS ONLY. Never --bone-*, --loam-*, --forest-*. Raw tokens do
     not flip with the theme, so a tint authored in light mode paints that
     same colour across every dark page.
   * NOTHING IS HIDDEN WAITING FOR JS. The entry choreography in §1 is pure
     CSS with `animation-fill-mode: backwards`, which fills only BEFORE the
     animation starts. When it finishes the element returns to its own styles.
     If script never runs the page still arrives; if animations are not
     supported the content was never hidden in the first place.
   * NO NEW TYPE. Not one font-family and not one font-weight is declared in
     this file. The three registers in tokens.css are the ones that were
     there. Size, colour, spacing and rhythm are fair game; the typeface is
     not.
   * NEW CUSTOM PROPERTIES ARE NAMESPACED `--at-*`. This is not tidiness.
     motion.css drives magnetic buttons off `--mx`/`--my` as PIXEL offsets,
     and custom properties inherit — writing a percentage into `--mx` on a
     card would push any magnetic descendant sideways by that percentage. The
     prefix makes that collision impossible.
   * TRANSFORMS ARE COMPOSED, NEVER OVERWRITTEN. micro.css builds .pcard's
     transform out of perspective + rotate + `--lift`, and motion.css builds
     [data-parallax] out of `--pfx/--pfy/--pfs`. Nothing here sets `transform`
     on either. Where a lift is wanted, the existing variable is set.
   * Loads last, so it wins on source order rather than on !important.
   ========================================================================== */

/* ==========================================================================
   0. LOCAL SCALE

   Durations here are multiples of the four in tokens.css rather than new
   numbers, so reduced motion still collapses the whole system from one place.
   ========================================================================== */
:root {
  --at-pop:    260ms;                      /* a confirmation: fast, then done */
  --at-settle: 520ms;                      /* something coming to rest        */
  --at-ring:   0 0 0 3px color-mix(in srgb, var(--brand) 26%, transparent);
}

/* ==========================================================================
   1. ARRIVAL — the page opens rather than appearing.

   Above the fold only. Everything below it already has the scroll reveal
   system, and running both on one element makes it arrive twice.

   `backwards` is load-bearing. With `forwards` or `both` the animation's end
   value STICKS and outranks every later hover rule — .hero-inset:hover has a
   transform, and a filled-forwards animation would silently kill it. With
   `backwards` the fill applies only during the delay; once the animation ends
   the element is governed by its own stylesheet again.
   ========================================================================== */
@keyframes at-enter {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes at-enter-plain {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes at-enter-media {
  from { opacity: 0; transform: translate3d(0, 22px, 0) scale(.985); }
  to   { opacity: 1; transform: none; }
}

.ticker            { animation: at-enter-plain var(--at-settle) var(--ease-out) backwards; }
.site-nav          { animation: at-enter var(--at-settle) var(--ease-soil) 60ms backwards; }

/* The hero title is deliberately absent from this list. hero.css already
   slides each .hero-line out of its own clip box on a transition keyed to
   `.hero-panel.is-on`, with per-line delays. Adding a keyframe animation to
   the same element makes two systems fight over one transform. */
.hero-kicker       { animation: at-enter var(--at-settle) var(--ease-soil) 140ms backwards; }
.hero-lede         { animation: at-enter var(--at-settle) var(--ease-soil) 320ms backwards; }
.hero-cta          { animation: at-enter var(--at-settle) var(--ease-soil) 400ms backwards; }
.hero-ctl          { animation: at-enter var(--at-settle) var(--ease-soil) 470ms backwards; }
.hero-frame        { animation: at-enter-media 760ms var(--ease-soil) 200ms backwards; }
.hero-inset        { animation: at-enter-media 760ms var(--ease-soil) 420ms backwards; }

/* Interior pages open on a page head rather than a hero. */
.page-head .eyebrow  { animation: at-enter var(--at-settle) var(--ease-soil) 120ms backwards; }
.page-head h1        { animation: at-enter var(--at-settle) var(--ease-soil) 180ms backwards; }
.page-head > .wrap > p,
.page-head .lede     { animation: at-enter var(--at-settle) var(--ease-soil) 260ms backwards; }

@media (prefers-reduced-motion: reduce) {
  .ticker, .site-nav, .hero-kicker, .hero-lede, .hero-cta, .hero-ctl,
  .hero-frame, .hero-inset,
  .page-head .eyebrow, .page-head h1, .page-head > .wrap > p, .page-head .lede {
    animation: none;
  }
}

/* ==========================================================================
   2. THE THEME SWITCH — the one hard cut left on the site.

   Every other state change on this site is eased. Theme was instant: the
   entire page swapped colour between one frame and the next, which reads as a
   glitch rather than as a choice you made.

   Two mechanisms, in order of preference:

   a) View Transitions, where supported. atelier.js starts one and writes the
      button's centre into --at-vt-x/y, so the new theme spreads out of the
      control you actually pressed. The old snapshot sits still underneath and
      the new one is clipped open over it — this is why the animation targets
      ::view-transition-new alone and the old layer is frozen.
   b) Everywhere else, a short cross-fade on the colour properties themselves,
      applied only for the length of the switch and then removed, because
      leaving a transition on `background-color` for every element on the page
      permanently would make hover states feel like they were dragging.
   ========================================================================== */
::view-transition-old(root),
::view-transition-new(root) { animation: none; mix-blend-mode: normal; }

::view-transition-old(root) { z-index: 1; }
::view-transition-new(root) { z-index: 2; }

html.at-theming::view-transition-new(root) {
  animation: at-theme-wipe 620ms var(--ease-soil);
}
@keyframes at-theme-wipe {
  from { clip-path: circle(0 at var(--at-vt-x, 50%) var(--at-vt-y, 50%)); }
  to   { clip-path: circle(var(--at-vt-r, 150%) at var(--at-vt-x, 50%) var(--at-vt-y, 50%)); }
}

/* The fallback. `.at-theme-fade` is added for exactly 320ms by atelier.js. */
.at-theme-fade *,
.at-theme-fade *::before,
.at-theme-fade *::after {
  transition: background-color 300ms var(--ease-out),
              border-color 300ms var(--ease-out),
              color 300ms var(--ease-out),
              fill 300ms var(--ease-out),
              box-shadow 300ms var(--ease-out) !important;
}

/* the sun/moon turns over rather than being swapped */
[data-theme-icon] { transition: transform 460ms var(--ease-spring); }
.at-spin [data-theme-icon] { transform: rotate(180deg) scale(.86); }

@media (prefers-reduced-motion: reduce) {
  html.at-theming::view-transition-new(root) { animation: none; }
  .at-theme-fade *, .at-theme-fade *::before, .at-theme-fade *::after { transition: none !important; }
  [data-theme-icon] { transition: none; }
}

/* ==========================================================================
   3. SURFACES — light that comes from somewhere.

   A card on this site is a flat fill with a border. Real paper on a real
   surface has a lit edge and a pointer moving over it moves the light. Both
   effects below are a single custom property write per frame, on one
   delegated listener, and both are clipped by the card's own overflow.
   ========================================================================== */

/* --- the top-edge highlight: free, and it makes the border read as an edge */
.pcard,
.spec-card,
.cutoff,
.fact-chip,
.summary-card,
.cart-line-card { box-shadow: var(--shadow-sm), var(--shadow-inset); }

/* --- the pointer spotlight -------------------------------------------------
   `.pcard::after` is already the brand hairline that draws along the top edge
   (refine.css §3), so this takes ::before, which is free. Painted UNDER the
   content — z-index 0 against the media's own stacking — and only ever at
   opacity, so the card's composed transform in micro.css is untouched. */
.pcard::before,
.cat-card::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
  background: radial-gradient(
    240px circle at var(--at-px, 50%) var(--at-py, 50%),
    color-mix(in srgb, var(--brand) 15%, transparent) 0%,
    transparent 68%);
}
.pcard:hover::before,
.pcard:focus-within::before { opacity: 1; }

/* On a category card the ground is a photograph, so the same idea has to be
   light rather than colour or it does not read at all. */
.cat-card::before {
  z-index: 1;
  background: radial-gradient(
    300px circle at var(--at-px, 50%) var(--at-py, 50%),
    rgba(255, 255, 255, .16) 0%,
    transparent 62%);
}
.cat-card:hover::before,
.cat-card:focus-within::before { opacity: 1; }

@media (pointer: coarse), (prefers-reduced-motion: reduce) {
  .pcard::before, .cat-card::before { display: none; }
}

/* --- photography settles rather than snapping ------------------------------
   A JPEG appears in one frame at full contrast, which is the one moment on
   the page with no easing anywhere in it.

   Written as an ANIMATION rather than a transition, on purpose. Three other
   files already own `transition` on these images — the 900ms hover zoom in
   refine.css, the wipe's counter-scale in micro.css — and this file loads
   last, so declaring `transition` here would silently replace theirs and kill
   the zoom. An animation composes instead of competing.

   `backwards` again: the blur applies only up to the moment the animation
   ends, after which the image is governed by its own styles. Nothing is left
   holding a filter. */
img.at-pending { opacity: 0; }
img.at-loaded  { animation: at-develop 620ms var(--ease-out) backwards; }

@keyframes at-develop {
  from { opacity: 0; filter: blur(10px); }
  to   { opacity: 1; filter: none; }
}

@media (prefers-reduced-motion: reduce) {
  img.at-pending { opacity: 1; }
  img.at-loaded  { animation: none; }
}

/* ==========================================================================
   4. THE PRODUCT CARD — the thing people actually use.

   Four additions, and each answers a question the card was being asked and
   not answering.
   ========================================================================== */

/* --- 4a. (WAS: the Quick look button — REMOVED 2026-09-06.) ---------------
   `.pcard-peek` sat over the photograph at z-index 4, revealed on hover, and
   opened the panel that was §7. Both are gone. It had already been patched
   twice for the same reason — `display: none` under `(hover: none)` after it
   swallowed every tap on a card picture, and a `(pointer: coarse)` rule
   making it permanent and finger-sized — and was still reported freezing on a
   phone. A hover affordance on a catalogue read mostly on phones is a control
   most people never see and some people cannot get past.

   Nothing was lost: components.css carries
   `.pcard-name a::after { position: absolute; inset: 0 }`, so the whole card
   has always been the link to the product. */

/* --- 4b. "did that go in?" -------------------------------------------------
   The Add control is a circle that widens into a labelled button. Pressing it
   used to change a number in the far corner of the screen and nothing else.
   It now goes busy, then confirms, then returns — 1.1s in total, which is
   long enough to be seen and short enough to add three things quickly. */
.add-btn { position: relative; overflow: hidden; }
.add-btn::after {
  content: '';
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: inherit;
  background: var(--success);
  opacity: 0;
  transform: scale(.4);
  transition: opacity var(--at-pop) var(--ease-out), transform var(--at-pop) var(--ease-spring);
  pointer-events: none;
}
.add-btn.is-added::after { opacity: 1; transform: scale(1); }

/* the tick itself is a masked glyph so it inherits the button's own radius
   and needs no extra element in the markup */
.add-btn .at-tick {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  color: #fff;
  opacity: 0;
  transform: scale(.5);
  transition: opacity var(--at-pop) var(--ease-out), transform var(--at-pop) var(--ease-spring);
  pointer-events: none;
}
.add-btn .at-tick .ico { width: 16px; height: 16px; }
.add-btn.is-added .at-tick { opacity: 1; transform: none; }
.add-btn.is-added .ico:not(.at-tick .ico),
.add-btn.is-added .add-btn-text { opacity: 0; }

.add-btn.is-busy { pointer-events: none; }
.add-btn.is-busy .ico:first-child { animation: at-spin 620ms linear infinite; }
@keyframes at-spin { to { transform: rotate(360deg); } }

/* --- 4c. "is it saved?" ----------------------------------------------------
   A heart that changes colour is a state. A heart that answers is an event.
   The burst is six hairlines radiating once, drawn on a pseudo-element with a
   conic mask so it costs one element and no markup. */
.pcard-save { position: relative; }
.pcard-save.is-on .ico { animation: at-heart var(--at-settle) var(--ease-spring); }
@keyframes at-heart {
  0%   { transform: scale(1); }
  32%  { transform: scale(.72); }
  62%  { transform: scale(1.24); }
  100% { transform: scale(1); }
}
.pcard-save.at-burst::before {
  content: '';
  position: absolute;
  inset: -10px;
  border-radius: 50%;
  border: 2px solid var(--danger);
  opacity: .8;
  animation: at-burst 560ms var(--ease-out) forwards;
  pointer-events: none;
}
@keyframes at-burst {
  from { transform: scale(.5); opacity: .8; }
  to   { transform: scale(1.7); opacity: 0; }
}

/* --- 4d. the card's own focus ring -----------------------------------------
   The whole card is a link target via .pcard-name a::after. Keyboard users
   were getting a ring around the invisible 1px anchor text instead of around
   the thing they had selected. */
.pcard:focus-within {
  border-color: var(--brand);
  box-shadow: var(--shadow-lg), var(--at-ring);
}
.pcard-name a:focus-visible { outline: none; }

@media (prefers-reduced-motion: reduce) {
  .pcard-save.is-on .ico { animation: none; }
  .pcard-save.at-burst::before { animation: none; display: none; }
  .add-btn.is-busy .ico:first-child { animation: none; }
}

/* ==========================================================================
   5. WHERE IT WENT — the flight to the cart.

   The cart button lives in the far top-right corner of a 1440px screen. A
   product added from the bottom of a grid changed a badge the eye was nowhere
   near. The flight is a clone of the card's own image travelling the actual
   distance, which is the cheapest possible way to say "it went there".

   The node is created and removed by atelier.js. It is aria-hidden and
   pointer-events: none, so it is invisible to assistive tech and cannot
   intercept a click mid-flight.
   ========================================================================== */
.at-fly {
  position: fixed;
  z-index: var(--z-toast);
  width: 78px;
  height: 78px;
  border-radius: var(--r-ctl);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  border: 1px solid var(--border-strong);
  pointer-events: none;
  will-change: transform, opacity;
}
.at-fly img { width: 100%; height: 100%; object-fit: cover; display: block; }
.at-fly.is-flying {
  transition: transform 720ms cubic-bezier(.5, -0.2, .3, 1),
              opacity 720ms var(--ease-out),
              border-radius 720ms var(--ease-out);
  border-radius: 50%;
  opacity: .25;
}

/* the badge answers the arrival */
.cart-count { transition: transform var(--at-pop) var(--ease-spring); }
.cart-count.at-bump { animation: at-bump 460ms var(--ease-spring); }
@keyframes at-bump {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.5); }
  100% { transform: scale(1); }
}

/* and so does the button it sits on */
.cart-btn.at-bump { animation: at-nudge 460ms var(--ease-spring); }
@keyframes at-nudge {
  0%, 100% { transform: none; }
  30%      { transform: translateY(-3px); }
  60%      { transform: translateY(1px); }
}

@media (prefers-reduced-motion: reduce) {
  .at-fly { display: none; }
  .cart-count.at-bump, .cart-btn.at-bump { animation: none; }
}

/* ==========================================================================
   6. INSTANT SEARCH — the catalogue, one keystroke away.

   The header already had a search field and a `/` shortcut that focused it.
   Typing into it did nothing until you pressed Enter and loaded a new page.
   With 85 products held in memory on every page there is no reason for that
   round trip.

   The panel is built by atelier.js and lives inside the existing <form>, so
   pressing Enter with nothing selected still submits to the shop exactly as
   it did. Everything here is additive to a control that already worked.
   ========================================================================== */
.nav-search { position: relative; }

.at-sug {
  position: absolute;
  top: calc(100% + 10px);
  left: 0;
  right: 0;
  z-index: var(--z-mega);
  min-width: 340px;
  max-height: min(64vh, 520px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-2);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-card);
  background: var(--glass-2);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
  backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
  box-shadow: var(--shadow-xl);
  opacity: 0;
  transform: translateY(-8px) scale(.985);
  transform-origin: 50% 0;
  transition: opacity var(--dur-2) var(--ease-out),
              transform var(--dur-2) var(--ease-soil),
              visibility 0s linear var(--dur-2);
  visibility: hidden;
}
.at-sug.is-open {
  opacity: 1;
  transform: none;
  visibility: visible;
  transition-delay: 0s;
}

.at-sug-group {
  padding: var(--s-3) var(--s-3) 4px;
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: var(--tr-wide);
  text-transform: uppercase;
  color: var(--fg-faint);
}

.at-sug-row {
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  padding: 8px;
  border: 0;
  border-radius: var(--r-ctl);
  background: transparent;
  color: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out);
}
.at-sug-row:hover,
.at-sug-row.is-cursor { background: var(--brand-soft); }
.at-sug-row.is-cursor { outline: none; }

.at-sug-thumb {
  width: 42px;
  height: 42px;
  border-radius: 9px;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--border);
  display: grid;
  place-items: center;
}
.at-sug-thumb img { width: 100%; height: 100%; object-fit: cover; }
.at-sug-thumb .ico { width: 18px; height: 18px; color: var(--fg-faint); }

.at-sug-txt { min-width: 0; }
.at-sug-txt b {
  display: block;
  font-size: var(--t-sm);
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.at-sug-txt span {
  display: block;
  margin-top: 1px;
  font-size: var(--t-xs);
  color: var(--fg-subtle);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* the matched run of characters, so it is obvious WHY a row is here */
.at-sug-txt mark {
  background: color-mix(in srgb, var(--accent) 34%, transparent);
  color: inherit;
  padding: 0 1px;
  border-radius: 3px;
}
.at-sug-price {
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  color: var(--fg-muted);
  white-space: nowrap;
}

.at-sug-empty {
  padding: var(--s-5) var(--s-4);
  font-size: var(--t-sm);
  color: var(--fg-subtle);
  text-align: center;
}
.at-sug-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin-top: 4px;
  padding: var(--s-3);
  border-top: 1px solid var(--border);
  font-size: var(--t-xs);
  color: var(--fg-subtle);
}
.at-sug-foot kbd {
  display: inline-block;
  min-width: 18px;
  padding: 1px 5px;
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--surface-2);
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  color: var(--fg-muted);
}

/* the trailing arrow only appears on the row you are actually on */
.at-sug-row .at-go { opacity: 0; transition: opacity var(--dur-1) var(--ease-out); }
.at-sug-row.is-cursor .at-go { opacity: 1; }

@media (max-width: 1079px) {
  /* the header field is hidden at this width; the panel would have nothing to
     anchor to, so the shortcut falls back to the shop page */
  .at-sug { display: none; }
}

/* ==========================================================================
   7. (WAS: QUICK LOOK — a product without leaving the grid. REMOVED
   2026-09-06 with 4a, which was its only trigger.)

   The scrim, the panel, the variant chips, the fact list and the
   `html.at-locked` scroll lock all went with it. If a quick view is ever
   wanted again, build it from a control that exists on touch.
   ========================================================================== */

/* ==========================================================================
   8. THE WAY BACK UP

   (WAS: a fixed chapter rail down the left margin — a tick per section with
   the section name on hover, synced to scroll position. Built, reviewed and
   REJECTED. It read as chrome bolted onto the page rather than as part of
   it: a column of grey dashes floating in the left margin is the first thing
   the eye lands on and it is not the content. Recorded rather than deleted
   silently, because "add a scroll-spy rail" is a suggestion that will come
   round again and this is the answer.)
   ========================================================================== */

/* --- back to top -----------------------------------------------------------
   Bottom LEFT, deliberately. The assistant's launcher is fixed bottom-right on
   every page; a second circular control beside it is how you get two buttons
   that look like one component and neither of which is pressed on purpose. */
.at-top {
  position: fixed;
  left: var(--s-5);
  bottom: var(--s-5);
  z-index: calc(var(--z-sticky) + 1);
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--glass-2);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: var(--shadow-md);
  color: var(--fg-muted);
  cursor: pointer;
  opacity: 0;
  transform: translateY(14px) scale(.9);
  visibility: hidden;
  transition: opacity var(--dur-2) var(--ease-out),
              transform var(--dur-2) var(--ease-spring),
              color var(--dur-1) var(--ease-out),
              visibility 0s linear var(--dur-2);
}
.at-top.is-on { opacity: 1; transform: none; visibility: visible; transition-delay: 0s; }
.at-top:hover { color: var(--brand-ink); }
.at-top .ico { width: 17px; height: 17px; transform: rotate(-90deg); }

/* the ring IS the read position — the same number the top hairline shows,
   in the place your hand already is */
.at-top-ring { position: absolute; inset: -1px; transform: rotate(-90deg); }
.at-top-ring circle {
  fill: none;
  stroke-width: 2;
  cx: 24px; cy: 24px; r: 22px;
}
.at-top-ring .at-track { stroke: var(--border); }
.at-top-ring .at-fill {
  stroke: var(--brand);
  stroke-linecap: round;
  stroke-dasharray: 138.2;                 /* 2πr at r=22 */
  stroke-dashoffset: calc(138.2 - (138.2 * var(--read, 0)));
  transition: stroke-dashoffset 120ms linear;
}

@media (max-width: 700px) {
  .at-top { left: auto; right: var(--s-4); bottom: calc(var(--s-4) + 64px); width: 42px; height: 42px; }
}
@media (prefers-reduced-motion: reduce) {
  .at-top-ring .at-fill { transition: none; }
}

/* ==========================================================================
   9. PRESS — the physics of a control.

   Every button on this site had a hover and a colour. None of them had a
   press that started WHERE YOU PRESSED. The ripple is a real element created
   at the pointer coordinate and removed on animation end, rather than a
   pseudo-element, because two quick presses should produce two ripples and a
   pseudo-element can only ever produce one.
   ========================================================================== */
/* `.icon-btn` is deliberately absent from this list. The cart and saved
   buttons carry a count badge pinned at `top: 2px; right: 1px` with a 2px
   ring drawn in box-shadow; clipping the button to its own box shaves that
   ring off and cuts the corner of the badge. Icon buttons get the press
   spring below instead, which needs no clipping. */
.btn, .rail-btn, .facet-opt, .fchip, .add-btn, .season-pill {
  position: relative;
  overflow: hidden;
}
.at-rip {
  position: absolute;
  border-radius: 50%;
  background: currentColor;
  opacity: .22;
  transform: scale(0);
  animation: at-rip 560ms var(--ease-out) forwards;
  pointer-events: none;
}
@keyframes at-rip {
  to { transform: scale(2.6); opacity: 0; }
}

/* icon buttons answer the press with a spring rather than a colour */
.icon-btn { transition: transform var(--dur-1) var(--ease-spring), color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out); }
.icon-btn:active { transform: scale(.9); }

/* and the two that carry a count get a hover that reads as "openable" */
.cart-btn:hover .ico, .save-btn:hover .ico { transform: scale(1.08); }
.cart-btn .ico, .save-btn .ico { transition: transform var(--dur-2) var(--ease-spring); }

@media (prefers-reduced-motion: reduce) {
  .at-rip { display: none; }
  .icon-btn:active { transform: none; }
}

/* ==========================================================================
   10. TOASTS — a message with a clock on it.

   The old toast appeared, sat for 3.4s and vanished. Two things were missing
   and both are about trust: you could not tell how long you had, and a
   removal could not be undone. Removing the last item from a basket with no
   account behind it is the most destructive thing a visitor can do here.
   ========================================================================== */
.toast-msg { position: relative; overflow: hidden; }
.toast-msg::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  height: 2px;
  width: 100%;
  background: var(--brand);
  transform-origin: 0 50%;
  transform: scaleX(1);
  animation: at-toast-clock var(--at-toast-life, 3400ms) linear forwards;
}
.toast-msg.is-warn::after { background: var(--accent); }
@keyframes at-toast-clock { to { transform: scaleX(0); } }

/* hovering a toast is a request to read it */
.toast-msg:hover::after,
.toast-msg:focus-within::after { animation-play-state: paused; }

.toast-act {
  margin-left: auto;
  padding: 4px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-chip);
  background: transparent;
  color: inherit;
  font-size: var(--t-xs);
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.toast-act:hover { background: var(--surface-2); border-color: var(--fg-subtle); }

@media (prefers-reduced-motion: reduce) {
  .toast-msg::after { animation: none; transform: scaleX(0); }
}

/* ==========================================================================
   11. NUMBERS THAT CHANGE — steppers and totals.

   micro.css §4 already rolls a PRICE when a variant changes. The same
   reasoning applies everywhere a figure is replaced by a different figure:
   swapping the text reads as a rendering glitch, rolling it reads as a
   consequence of what you just pressed.
   ========================================================================== */
.at-roll { display: inline-block; }
.at-roll.is-up   { animation: at-roll-up   var(--at-pop) var(--ease-soil); }
.at-roll.is-down { animation: at-roll-down var(--at-pop) var(--ease-soil); }
@keyframes at-roll-up {
  0%   { transform: translateY(0); opacity: 1; }
  45%  { transform: translateY(-80%); opacity: 0; }
  46%  { transform: translateY(80%); opacity: 0; }
  100% { transform: translateY(0); opacity: 1; }
}
@keyframes at-roll-down {
  0%   { transform: translateY(0); opacity: 1; }
  45%  { transform: translateY(80%); opacity: 0; }
  46%  { transform: translateY(-80%); opacity: 0; }
  100% { transform: translateY(0); opacity: 1; }
}

/* the stepper's own buttons */
[data-qty-dec], [data-qty-inc], .qty-btn {
  transition: background-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-spring);
}
[data-qty-dec]:active, [data-qty-inc]:active, .qty-btn:active { transform: scale(.86); }

/* a total that has just changed pulses its ground once, so the number you are
   looking for finds you rather than the other way round */
.at-flash { animation: at-flash var(--at-settle) var(--ease-out); }
@keyframes at-flash {
  from { background-color: color-mix(in srgb, var(--brand) 20%, transparent); }
  to   { background-color: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .at-roll.is-up, .at-roll.is-down, .at-flash { animation: none; }
}

/* ==========================================================================
   12. FORMS — the field answers back.

   micro.css §6 lifts the label on focus. What was still missing is everything
   AFTER you have typed: no confirmation that a valid field is valid, no sense
   of how much room is left in a message box, and an error that appeared with
   no motion at all so it was easy to miss on a long form.
   ========================================================================== */
.field { position: relative; }

/* the tick draws itself rather than appearing, which is the difference
   between "this is correct" and "this was always correct" */
.field.is-success .input,
.field.is-success .textarea,
.field.is-success .select { border-color: var(--success); }
.field.is-success::after {
  content: '';
  position: absolute;
  top: calc(50% - 2px);
  right: 14px;
  width: 14px;
  height: 8px;
  border-left: 2px solid var(--success);
  border-bottom: 2px solid var(--success);
  transform: translateY(-50%) rotate(-45deg);
  transform-origin: center;
  animation: at-tick-draw 340ms var(--ease-out) backwards;
  pointer-events: none;
}
@keyframes at-tick-draw {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}
/* a textarea's centre is not where its tick belongs */
.field.is-success:has(.textarea)::after { top: 34px; }

/* an error that shakes once is an error you saw */
.field.is-error .input,
.field.is-error .textarea,
.field.is-error .select { animation: at-shake 400ms var(--ease-out); }
@keyframes at-shake {
  0%, 100% { transform: translateX(0); }
  20%      { transform: translateX(-5px); }
  40%      { transform: translateX(4px); }
  60%      { transform: translateX(-3px); }
  80%      { transform: translateX(2px); }
}
.field-error { animation: at-enter 240ms var(--ease-out) backwards; }

.at-counter {
  position: absolute;
  right: 2px;
  bottom: -18px;
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--fg-faint);
  transition: color var(--dur-1) var(--ease-out);
}
.at-counter.is-near { color: var(--accent-ink); }
.at-counter.is-over { color: var(--danger); }

@media (prefers-reduced-motion: reduce) {
  .field.is-error .input, .field.is-error .textarea, .field.is-error .select,
  .field.is-success::after, .field-error { animation: none; }
}

/* ==========================================================================
   12b. THE CATEGORY RAIL, ON A DEDICATED CATEGORY PAGE

   The four category pages replace the Category FILTER with category
   NAVIGATION: `.filter-opt` as an <a> rather than a <button>, marked with
   `aria-current="page"` instead of `aria-pressed="true"`.

   pages.css §.filter-opt only paints the pressed state, so without this the
   rail on machinery.php would show five identical rows and no indication of
   which one you are standing on. The two selectors are kept as one rule
   rather than two so they cannot drift into looking slightly different.
   ========================================================================== */
.filter-opt[aria-current='page'] {
  background: var(--brand-soft);
  border-color: transparent;
  color: var(--brand);
}
.filter-opt[aria-current='page'] .ico,
.filter-opt[aria-current='page'] .n { color: var(--brand); }

/* An <a> does not inherit the <button> reset, so it needs the row shape
   restated: without this it lays out as an inline link and the count stops
   being pushed to the right edge by the `margin-left: auto` on `.n`. */
a.filter-opt { text-decoration: none; }
a.filter-opt:hover { text-decoration: none; }

/* ==========================================================================
   12b-2. THE CARD IS TAPPABLE ON TOUCH

   Reported as "clicking a product on mobile does nothing at all", and it was
   exactly that. The stretched link was never missing — components.css has
   carried `.pcard-name a::after { position: absolute; inset: 0 }` all along,
   so the whole card has always been the link. What sat on top of it was
   `.pcard-peek`, a hover-only Quick look button at `z-index: 4`, centred over
   the photograph.

   ON A TOUCH SCREEN NOTHING EVER HOVERS, so it stayed invisible at opacity 0
   and still swallowed every tap on the picture. `display: none` under
   `(hover: none)` fixed the interception; it did not fix the control, which
   was still reported freezing, and on 2026-09-06 the whole thing was removed
   (see 4a). The rules below are what remains: everything else on the card
   that sits over the stretched link.
   ========================================================================== */

/* Badges are labels, never controls — they sit over the photograph too. */
.pcard-badge, .pcard-off { pointer-events: none; }

/* The stepper is the one card control that is statically positioned, so it
   needs lifting above the stretched link. `.pcard-save` (absolute, z-index 3)
   and `.pcard-act` (relative, z-index 2) are already above it — an earlier
   attempt restated `position: relative` on both "to be safe", and on the
   Quick look button beside them, and knocked the save heart out of the media
   corner into normal flow. Restating a property you have not checked is how
   you break three working controls to fix a problem none of them had. */
.pcard-media .qty,
.pcard-foot .qty { position: relative; z-index: 2; }

/* ==========================================================================
   12b-3. "PRICE ON REQUEST"

   The bio-input range is quoted rather than priced — the supplier catalogue
   publishes no numbers at all. The card and the product page say so instead of
   printing Rs 0, which would not be a missing price but a wrong one.

   Deliberately NOT styled to look like a price. It is set in the mono face at
   label size so it reads as a state, not as a number somebody could act on.
   ========================================================================== */
.price-ask {
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.pdp-price .now:not(:empty) { line-height: 1.05; }

/* ==========================================================================
   12c. THE PRODUCT GALLERY

   Rebuilt 2026-09-05 after the first version was rejected. Three things were
   wrong with it and all three are answered here:

     * the thumbnails were a column down the LEFT — they are a centred row
       under the stage now, at every width;
     * the stage zoomed on HOVER, so the picture moved under a pointer that
       was only passing through — zoom is on CLICK, in the lightbox, and
       nowhere else;
     * it did not carry itself. The stage was a bordered square with a
       picture in it.

   WHAT MAKES IT AN OBJECT NOW
   Every shot is laid into the stage at once and crossfaded, rather than one
   <img> having its src swapped. Swapping meant a blank frame on every change
   whose length was the network's decision. Stacked, the next picture is
   already decoded and the change is a fade with nothing missing in the middle.

   ADDITIVE. `atelier.js` picks up the frame `catalog.js` already rendered and
   reads the shot list out of the `.pdp-thumbs` strip it already built. Delete
   the atelier tags and the page falls back to that frame and that strip.

   Semantic tokens only — a raw --bone/--loam/--forest here would paint a cream
   stage across every dark product page.
   ========================================================================== */

.ag { display: grid; gap: var(--s-4); }

/* ------------------------------------------------------------------ stage */
.ag-stage {
  position: relative;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  background: var(--surface-2);
  overflow: hidden;
  isolation: isolate;
  cursor: zoom-in;
  touch-action: pan-y;
}

/* Every shot, stacked. Only the current one is opaque. */
.ag-slide {
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity var(--dur-3) var(--ease-out), transform 900ms var(--ease-out);
  transform: scale(1.015);
  pointer-events: none;
}
.ag-slide.is-on { opacity: 1; transform: none; pointer-events: auto; }
.ag-slide img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The pack shots sit on a white sweep; the lifestyle ones fill the frame.
   `contain` on a white-background shot stops the pack being cropped at the
   shoulders by a square stage. atelier.js tags these from the image itself. */
.ag-slide.is-sweep { background: #fff; }
.ag-slide.is-sweep img { object-fit: contain; padding: clamp(10px, 2.4vw, 26px); }

/* SLIDE 0 IS THE ORIGINAL .pdp-frame, moved in rather than replaced, so the
   3D machinery viewer keeps the element it mounts into. It arrives carrying
   its own border, radius, shadow and 1/1 aspect from pages.css and craft.css —
   all of which the stage now provides. Neutralised here rather than edited
   there: those rules are below the design layer and still correct for a page
   this gallery never builds on. */
.ag-slide .pdp-frame {
  position: absolute; inset: 0;
  margin: 0; border: 0; border-radius: 0;
  box-shadow: none; aspect-ratio: auto; background: none;
}
.ag-slide.is-sweep .pdp-frame { background: #fff; }
.ag-slide.is-sweep .pdp-frame img { object-fit: contain; padding: clamp(10px, 2.4vw, 26px); }

/* a hairline of depth so the stage reads as a surface, not a hole */
.ag-stage::after {
  content: ''; position: absolute; inset: 0; z-index: 1;
  border-radius: inherit;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10), inset 0 -40px 60px -50px rgba(0, 0, 0, .5);
  pointer-events: none;
}

/* --------------------------------------------------------------- controls */
.ag-nav {
  position: absolute; top: 50%; z-index: 3;
  width: 44px; height: 44px; margin-top: -22px;
  display: grid; place-items: center;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--glass-2);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
  color: var(--fg);
  cursor: pointer;
  opacity: 0; transform: translateX(var(--at-nudge, 0)) scale(.86);
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-spring),
              background-color var(--dur-1) var(--ease-out);
}
.ag-prev { left: 14px;  --at-nudge: 8px; }
.ag-next { right: 14px; --at-nudge: -8px; }
.ag-stage:hover .ag-nav,
.ag-stage:focus-within .ag-nav { opacity: 1; transform: none; }
.ag-nav:hover { background: var(--surface); }
.ag-nav:focus-visible { opacity: 1; transform: none; outline: 3px solid var(--brand); outline-offset: 2px; }
.ag-nav[disabled] { opacity: 0 !important; pointer-events: none; }
.ag-nav .ico { width: 18px; height: 18px; }
.ag-prev .ico { transform: rotate(180deg); }

/* A touch screen never hovers, so the arrows would never appear. It swipes,
   and the rail under the stage says how many there are. */
@media (hover: none) { .ag-nav { display: none; } }

/* the corner marks */
.ag-badge {
  position: absolute; z-index: 3; bottom: 14px;
  display: inline-flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 13px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--glass-2);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
  color: var(--fg-muted);
  font-family: var(--font-mono); font-size: var(--t-2xs);
  letter-spacing: .08em; font-variant-numeric: tabular-nums;
}
.ag-count { left: 14px; }
.ag-zoomhint {
  right: 14px; padding: 0 13px;
  cursor: pointer; color: var(--fg-muted);
  transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.ag-zoomhint:hover { background: var(--surface); color: var(--fg); }
.ag-zoomhint:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
.ag-zoomhint .ico { width: 15px; height: 15px; }
@media (max-width: 520px) { .ag-zoomhint span { display: none; } .ag-zoomhint { padding: 0 10px; } }

/* --------------------------------------------------------- the autoplay bar
   A carousel that moves on its own owes the reader an explanation. The bar is
   that: it says the change is on a timer and shows how long is left, so the
   next slide is expected rather than startling. It empties when the timer is
   paused, which is also the hover state — so hovering visibly stops it. */
.ag-timer {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  height: 3px; background: color-mix(in srgb, var(--fg) 12%, transparent);
  opacity: 0; transition: opacity var(--dur-2) linear;
}
.ag.is-playing .ag-timer { opacity: 1; }
.ag-timer i {
  display: block; height: 100%; width: 0;
  background: var(--brand);
  border-radius: 0 2px 2px 0;
}
.ag.is-playing .ag-timer i { animation: ag-tick var(--at-ag-every, 5s) linear forwards; }
@keyframes ag-tick { from { width: 0; } to { width: 100%; } }

/* ------------------------------------------------------------------- rail
   UNDER THE STAGE, AT EVERY WIDTH. The first version put it in a left-hand
   column above 900px; it was asked for on the bottom and it reads better there
   anyway — the picture gets the full column width instead of losing 84px of it. */
.ag-rail {
  display: flex; gap: var(--s-3);
  justify-content: center; flex-wrap: nowrap;
  overflow-x: auto; scrollbar-width: none;
  padding: 2px;                     /* room for the focus ring */
  -webkit-overflow-scrolling: touch;
}
.ag-rail::-webkit-scrollbar { display: none; }

.ag-thumb {
  flex: none;
  width: 72px; height: 72px;
  position: relative;
  border: 0; padding: 0;
  border-radius: var(--r-ctl);
  overflow: hidden;
  background: var(--surface-2);
  cursor: pointer;
  opacity: .6;
  transition: opacity var(--dur-2) var(--ease-out),
              transform var(--dur-2) var(--ease-spring);
}
.ag-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ag-thumb::after {
  content: ''; position: absolute; inset: 0;
  border: 1px solid var(--border);
  border-radius: inherit;
  transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out);
}
.ag-thumb:hover { opacity: 1; transform: translateY(-3px); }
.ag-thumb[aria-current='true'] { opacity: 1; }
.ag-thumb[aria-current='true']::after {
  border-color: var(--brand);
  box-shadow: 0 0 0 2px var(--brand-soft);
}
.ag-thumb:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }

@media (max-width: 520px) {
  .ag-rail { justify-content: flex-start; }
  .ag-thumb { width: 62px; height: 62px; }
}

/* ------------------------------------------------------------- the lightbox
   A <dialog>, so the browser supplies the focus trap, Escape and the inertness
   of the page behind it. Zoom lives HERE and only here: click to go to 2.4x,
   move the pointer to pan, click again to come back out. */
.ag-box {
  border: 0; padding: 0; margin: 0;
  width: 100vw; height: 100vh; max-width: none; max-height: none;
  background: transparent; color: var(--fg); overflow: hidden;
}
.ag-box::backdrop { background: rgba(0, 0, 0, .88); }
.ag-box-in {
  position: absolute; inset: 0;
  display: grid; grid-template-rows: auto 1fr auto;
  background: var(--bg);
}
.ag-box-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4); padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--border);
}
.ag-box-title { font-weight: 700; font-size: var(--t-sm); }
.ag-box-title small {
  display: block; font-weight: 400; color: var(--fg-subtle);
  font-family: var(--font-mono); font-size: var(--t-2xs); letter-spacing: .08em;
}

.ag-box-fig {
  position: relative; overflow: hidden;
  min-height: 0;                   /* or the 1fr row grows to the image */
  touch-action: pan-y;
  cursor: zoom-in;
  background: var(--surface-2);
}
.ag-box-fig.is-zoomed { cursor: zoom-out; }

/* ABSOLUTE, and that is the point. Two earlier attempts sized this with
   `max-height:100%` and then `height:100%`; the figure's own grid row
   auto-sizes to its content, so both resolved against the 1035px image rather
   than the 551px box and pushed the rail off the bottom of the screen. Out of
   flow, the percentages resolve against the figure's definite box and the
   image can no longer grow its own parent. */
.ag-box-fig img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: scale-down;          /* contain, minus the upscaling of small art */
  transition: transform var(--dur-3) var(--ease-out), opacity var(--dur-2) linear;
}
.ag-box-fig.is-zoomed img { transition: transform 90ms linear; }

.ag-box-foot { padding: var(--s-4) var(--s-5); border-top: 1px solid var(--border); }
.ag-box .ag-nav { opacity: 1; transform: none; }

.ag-close {
  width: 42px; height: 42px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--border); border-radius: 50%;
  background: var(--surface); color: var(--fg); cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out);
}
.ag-close:hover { background: var(--surface-2); }
.ag-close:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }

@keyframes ag-box-in { from { opacity: 0; transform: scale(.985); } to { opacity: 1; transform: none; } }
.ag-box[open] .ag-box-in { animation: ag-box-in var(--dur-3) var(--ease-out) both; }

@media (prefers-reduced-motion: reduce) {
  .ag-slide, .ag-thumb, .ag-nav, .ag-box-fig img { transition: none; }
  .ag.is-playing .ag-timer i { animation: none; width: 100%; }
  .ag-box[open] .ag-box-in { animation: none; }
}

/* ==========================================================================
   13. NAVIGATION — the header, at rest and in motion.
   ========================================================================== */

/* The header shrinks by 8px once you have left the top. Small enough that
   nobody notices it happening; large enough that the page feels like it is
   moving under a fixed thing rather than the two sliding together. */
.site-nav .nav-in { transition: min-height var(--dur-3) var(--ease-out); }
.site-nav.is-stuck .nav-in { min-height: calc(var(--nav-h) - 8px); }
.site-nav.is-stuck { background: var(--glass-2); -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur)); }

/* the brand lockup answers a hover with the mark, not the words */
.brand img { transition: transform var(--dur-2) var(--ease-spring); }
.brand:hover img { transform: scale(1.06) rotate(-2deg); }

/* the mega panel's columns arrive in sequence rather than all at once */
.mega[data-mega-panel] .mega-col,
.mega[data-mega-panel] .mega-all,
.mega[data-mega-panel] .mega-feature {
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-soil);
}
.mega:not(.is-open) .mega-col,
.mega:not(.is-open) .mega-all,
.mega:not(.is-open) .mega-feature { opacity: 0; transform: translateY(10px); }
.mega.is-open .mega-col:nth-child(1) { transition-delay: 40ms; }
.mega.is-open .mega-col:nth-child(2) { transition-delay: 90ms; }
.mega.is-open .mega-all { transition-delay: 130ms; }
.mega.is-open .mega-feature { transition-delay: 170ms; }
.mega.is-open .mega-col,
.mega.is-open .mega-all,
.mega.is-open .mega-feature { opacity: 1; transform: none; }

/* mega links travel toward what they point at */
.mega-link { transition: background-color var(--dur-1) var(--ease-out), padding-left var(--dur-2) var(--ease-spring); }
.mega-link:hover { padding-left: calc(var(--s-3) + 4px); }

@media (prefers-reduced-motion: reduce) {
  .site-nav .nav-in { transition: none; }
  .brand:hover img { transform: none; }
  .mega .mega-col, .mega .mega-all, .mega .mega-feature { opacity: 1; transform: none; transition: none; }
  .mega-link:hover { padding-left: var(--s-3); }
}

/* ==========================================================================
   14. TYPOGRAPHY DETAIL

   No family and no weight is touched here. These are line-BREAKING and
   optical alignment decisions — the same words, better set.
   ========================================================================== */
@supports (text-wrap: balance) {
  .sec-head h2, .section-title, .hero-title,
  .page-head h1, .mini-head h2 { text-wrap: balance; }
}
@supports (text-wrap: pretty) {
  .sec-head p, .hero-lede, .pcard-meta, .prose p { text-wrap: pretty; }
}
@supports (hanging-punctuation: first) {
  .prose blockquote, .quote, .pull { hanging-punctuation: first last; }
}

/* Every figure on the site lines up in a column. refine.css covered prices;
   this covers the rest of the numbers, which are half of what this business
   talks about — horsepower, litres, degrees, kilos. */
.spec-row dd, .spec-val, .fact b, .chain-stage b, .stat b, .tally,
[data-tally], [data-count] { font-variant-numeric: tabular-nums; }

/* ==========================================================================
   15. THE SMALL PRINT — selection, scrollbar, focus, and the seam.
   ========================================================================== */

/* the scrollbar joins the design system instead of being the one part of the
   page the browser drew */
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border: 3px solid var(--bg);
  border-radius: var(--r-pill);
}
::-webkit-scrollbar-thumb:hover { background: var(--fg-faint); }

/* One focus treatment, with a halo so it survives being drawn over a
   photograph. refine.css set the outline; this adds the ground under it. */
a:focus-visible, button:focus-visible, [tabindex]:focus-visible {
  box-shadow: var(--at-ring);
}
.input:focus-visible, .select:focus-visible, .textarea:focus-visible { box-shadow: var(--at-ring); }

/* skip link becomes a real component rather than a browser default */
.skip-link {
  transition: transform var(--dur-2) var(--ease-spring), box-shadow var(--dur-2) var(--ease-out);
}
.skip-link:focus { box-shadow: var(--shadow-lg); }

/* ==========================================================================
   16. DEPARTURE — the page you are leaving says that it is leaving.

   §1 gives every page an ARRIVAL: content travels up from +14px and fades
   into place. Nothing answered the other half of it. You click a nav link
   and the page sits there, byte for byte identical, for as long as the
   server takes — and on a slow connection the only reasonable reading of
   that is that the click missed.

   This is not a new gesture, it is the same one continued. Arrival travels
   UP to 0; departure keeps going, to -10px, and fades. One movement across
   two documents, which is the whole point — the pages feel like parts of a
   site rather than a sequence of unrelated loads.

   THE FAILURE MODE IS COSMETIC, DELIBERATELY. If the navigation never
   happens the content is left at 55% opacity: dim, and completely readable.
   It is not hidden and it cannot be, which matters — this codebase has been
   bitten six times by content hidden in CSS waiting for script to bring it
   back. `at-going` is additionally cleared by a timeout, by `pageshow`
   (which is what fires on a bfcache back) and by `popstate`.

   `translate` rather than `transform`, because the two compose: micro.css
   and motion.css both build transforms on descendants and one of them would
   otherwise be overwritten. Every `position: fixed` element on this site is
   a direct child of <body> — checked, all seven — so moving <main> cannot
   drag a drawer or the buy bar out of the viewport.
   ========================================================================== */
html.at-going .site-nav,
html.at-going main,
html.at-going .site-footer {
  opacity: .55;
  translate: 0 -10px;
  transition: opacity 200ms var(--ease-soil), translate 200ms var(--ease-soil);
}

/* The furniture is not part of the page leaving. A cart drawer that faded
   out with it would read as having closed itself, and a toast that faded
   would read as having been dismissed unread. */
html.at-going .cart-drawer,
html.at-going .nav-drawer,
html.at-going .offcanvas,
html.at-going .toast-zone,
html.at-going .buy-bar,
html.at-going [data-wa-fab] { opacity: 1; translate: none; }

/* The thing you clicked keeps its weight while everything around it steps
   back, so the answer to "did that land" is on the control itself and not
   somewhere in the periphery. */
html.at-going .at-went {
  opacity: 1;
  position: relative;
  z-index: 2;
}

@media (prefers-reduced-motion: reduce) {
  html.at-going .site-nav,
  html.at-going main,
  html.at-going .site-footer { opacity: 1; translate: none; transition: none; }
}

/* ==========================================================================
   17. RESULTS THAT ANSWER — "how many now, and is this a new list?"

   The busiest interaction on the site had the least to say. You press a
   facet on the shop and `paint()` replaces the grid's innerHTML: thirty-five
   cards become thirteen, instantly, with no transition of any kind — and the
   count beside the search box swaps one number for another in the same
   frame. Nothing marks that the list in front of you is a different list.

   `.pcard` carries no reveal attribute, so the scroll system never touched
   it; `A.motion.scan(grid)` runs after every render and finds nothing to do
   on the cards themselves. That is not a bug in either — the cards were
   never registered — but the result is the one silent moment left on the
   site, on the control people press most.

   TRANSFORM IS NOT TOUCHED. micro.css composes `.pcard`'s transform from
   perspective + rotate + `--lift`, and the hard rule in this file's header
   is that nothing here overwrites it. `translate` is a separate property
   that composes with `transform` rather than replacing it, and it is used
   nowhere else in the tree — checked across all twenty stylesheets.

   The stagger is indexed rather than nth-child because "Show more" APPENDS:
   a positional rule would re-animate the nine cards already on screen every
   time nine more arrived. §21 in atelier.js sets `--at-i` on the cards that
   are actually new.
   ========================================================================== */
@keyframes at-card-in {
  from { opacity: 0; translate: 0 14px; }
  to   { opacity: 1; translate: none; }
}

.pcard.at-in {
  animation: at-card-in 420ms var(--ease-soil) calc(var(--at-i, 0) * 40ms) backwards;
}

/* Nothing matched is a result too, and it arrived with the same silence —
   the grid emptied and a block appeared in the same frame. This needs no
   script: `[data-shop-empty]` is shown by dropping the `hidden` attribute,
   so the element starts matching this rule at exactly the moment it becomes
   visible, and the animation runs from there. The `hidden` attribute is the
   only thing controlling it either way, so a browser that ignores the
   animation still shows the block. */
.empty[data-shop-empty]:not([hidden]) {
  animation: at-card-in 420ms var(--ease-soil) backwards;
}

/* The count is the number being watched while the grid rebuilds under it.
   §13 already flashes the cart totals on change; this is the same mark on
   the same kind of moment, and it reads as one behaviour because it is. */
.shop-count.at-flash,
[data-shop-shown].at-flash {
  border-radius: var(--r-chip);
  padding-inline: 6px;
  margin-inline: -6px;
}

@media (prefers-reduced-motion: reduce) {
  .pcard.at-in,
  .empty[data-shop-empty]:not([hidden]) { animation: none; }
}

/* ==========================================================================
   18. RESPONSIVE

   refine.css §7 set the floor — heading sizes, 44px targets, two-up stats.
   What is here is the layer above that: the things that should behave
   DIFFERENTLY on a phone rather than merely smaller.
   ========================================================================== */

/* notched phones. The WhatsApp button, the back-to-top and the toast zone are all
   fixed to an edge that a home indicator sits on. */
@supports (padding: max(0px)) {
  .at-top { bottom: max(var(--s-5), env(safe-area-inset-bottom)); }
  .toast-zone { padding-bottom: max(0px, env(safe-area-inset-bottom)); }
}

/* Between tablet and desktop the card grid was going to three columns with
   very little in each. Two wider cards read better than three narrow ones. */
@media (min-width: 560px) and (max-width: 900px) {
  .at-sug { min-width: 0; }
}

/* ==========================================================================
   19. PRINT — extends refine.css §8 with the components added here.
   ========================================================================== */
@media print {
  .at-top, .at-sug, .at-fly { display: none !important; }
}

/* ==========================================================================
   19b. A PACKSHOT IN THE HERO INSET.

   hero.css:308 already contains `[src*="products/"]`, because the supplier
   flyer artwork on the machinery slide is white to the edge and must not be
   cropped. The millet range is the same problem arriving by a different
   path: its packs are studio shots on a white sweep but they live in
   `catalogue/`, alongside the food photographs that SHOULD be cropped — so
   the source path cannot decide it and the markup has to.

   Same name and same job as `.pcard-media.is-contain` and
   `.pdp-frame.is-contain`; this is the third surface that needed it.
   ========================================================================== */
.hero-inset.is-contain img { object-fit: contain; }

/* ==========================================================================
   20. THE VERIFICATION HATCH — extends refine.css §9 and signature.css §12.

   `document.documentElement.dataset.flat = '1'` must resolve every animation
   introduced above to its END state, not freeze it mid-flight. Anything that
   starts hidden and animates in has to be forced visible here, or a contrast
   audit reads zero-opacity text and reports a phantom failure.
   ========================================================================== */
[data-flat] .ticker, [data-flat] .site-nav, [data-flat] .hero-kicker,
[data-flat] .hero-lede, [data-flat] .hero-cta, [data-flat] .hero-ctl,
[data-flat] .hero-frame, [data-flat] .hero-inset,
[data-flat] .page-head .eyebrow, [data-flat] .page-head h1,
[data-flat] .mega-col, [data-flat] .mega-all, [data-flat] .mega-feature {
  opacity: 1 !important;
  transform: none !important;
  animation: none !important;
}
[data-flat] img.at-pending { opacity: 1 !important; filter: none !important; }
[data-flat] .toast-msg::after { animation: none !important; }
[data-flat] .at-top-ring .at-fill { transition: none !important; }
