/* ==========================================================================
   themes/greppo.css — Greppo Winery
   PURPOSE: The world-specific layout and decoration for greppo-winery/.
   Loaded LAST on that page, after motion.css.

   SCOPE DISCIPLINE
   - This file NEVER redefines a --w-* semantic token. tokens.css owns those
     under [data-world="greppo"]. Everything here either consumes them or
     declares a NEW, clearly-prefixed, world-local value.
   - It never invents a parallel to a shared component. The .greppo-* names are
     world constructs layered on top of .card, .media-frame, .route-card and
     the layout primitives in base.css. The shop is not restyled from scratch
     either: commerce.css owns .commerce-card and its parts for every world,
     and section 6 only dresses those same classes from inside .greppo-shop.

   THE FEEL
   Greppo is the slowest world on the site. Transitions run 500-700ms instead
   of 200ms, hovers bloom warm like a candle being carried past rather than
   snapping on, and a 3.5% film grain sits over the whole page. The type stays
   high-contrast parchment on the dark wine field, and the brightest thing on
   the page is the price on a wine — gold, at display scale, over a candlelit
   hairline. That is deliberate: the shop is what this page is for.

   CONTENTS
     1.  World-local values
     2.  Film grain
     3.  Cellar door (hero)
     4.  The name & the slope
     5.  The craft — rail + scrub sequence
     6.  The collection — and the shop: grid, card, 3D stage, price, button
     7.  Visits & tastings
     8.  Journal
     9.  Contact & help
     10. World keyframes
     11. Narrow viewports
     12. Reduced motion — the complete override for everything above
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. WORLD-LOCAL VALUES
   New names only. Prefixed --greppo-* so they can never be mistaken for a
   semantic token that components are allowed to consume.
   -------------------------------------------------------------------------- */

[data-world="greppo"] {
  /* Cellar pacing. Long, soft, never springy. */
  --greppo-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --greppo-dur-slow: 620ms;
  --greppo-dur-slower: 700ms;

  /* Parchment — the collection surfaces, and nothing else. */
  --greppo-parchment: #EDE2CB;
  --greppo-parchment-2: #E3D5B9;
  --greppo-ink: #2A0A15;      /* 13.8:1 on parchment */
  --greppo-ink-dim: #5A3A2E;  /*  7.9:1 on parchment */

  /* Film grain strength. Kept at the low end of the 3-4% brief. */
  --greppo-grain: 0.035;
}

/* Retune the shared components to cellar time. Duration only — the property
   lists in components.css are left exactly as they are, so nothing fights. */
[data-world="greppo"] .btn,
[data-world="greppo"] .btn::after,
[data-world="greppo"] .card,
[data-world="greppo"] .chip,
[data-world="greppo"] .route-card,
[data-world="greppo"] .media-frame {
  transition-duration: var(--greppo-dur-slow);
  transition-timing-function: var(--greppo-ease);
}


/* --------------------------------------------------------------------------
   2. FILM GRAIN
   One fixed, inert layer over the whole page. The texture is a static
   feTurbulence tile carried as a data URI — no network request, rasterised
   once, and never animated, so it costs nothing on a scroll path. It sits at
   --z-sticky: above page content, below the header, panels and dialogs.
   -------------------------------------------------------------------------- */

.greppo-grain {
  position: fixed;
  inset: 0;
  z-index: var(--z-sticky);
  pointer-events: none;
  opacity: var(--greppo-grain);
  background-repeat: repeat;
  background-size: 180px 180px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}


/* --------------------------------------------------------------------------
   3. CELLAR DOOR — the hero
   Near-dark wine field, one authored SVG behind the wordmark. The art bleeds
   past the section on every side so the 12px parallax cap can never expose an
   edge. Everything that moves in here is gated on [data-ambient="on"], which
   greppo.js sets only when motion is permitted.
   -------------------------------------------------------------------------- */

.cellar-door {
  position: relative;
  isolation: isolate;
  display: grid;
  align-content: center;
  min-block-size: 100svh;
  padding-block: calc(var(--header-h) + var(--space-10)) var(--space-12);
  overflow: hidden;
}

.cellar-door__art {
  position: absolute;
  inset: -4% -2%;
  z-index: -1;
}

.cellar-door__svg {
  inline-size: 100%;
  block-size: 100%;
}

/* Vignette + a fade into the next band, so the hero has no hard bottom edge. */
.cellar-door::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(120% 90% at 50% 45%, transparent 35%, color-mix(in srgb, var(--w-base) 72%, transparent) 100%),
    linear-gradient(to bottom, transparent 62%, var(--w-base) 100%);
}

.cellar-door__inner {
  inline-size: 100%;
  max-inline-size: var(--maxw-content);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.cellar-door__lockup {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-block-start: var(--space-6);
}

.cellar-door__wordmark {
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 13vw, 9rem);
  line-height: 0.88;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--w-text);
  text-shadow: 0 0 70px var(--w-glow);
}

.cellar-door__line {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--step-4);
  line-height: var(--lh-snug);
  letter-spacing: var(--track-tight);
  color: var(--w-accent);
}

.cellar-door__standfirst {
  max-inline-size: 44ch;
  margin-block-start: var(--space-6);
  font-size: var(--step-1);
  line-height: var(--lh-loose);
  color: var(--w-text-dim);
}

.cellar-door__actions {
  margin-block-start: var(--space-8);
}

/* The way down. Sits at the foot of the hero, centred, quiet. */
.cellar-door__descend {
  position: absolute;
  inset-block-end: var(--space-6);
  inset-inline: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  min-block-size: 2.75rem;
  padding-block: var(--space-2);
  color: var(--w-text-dim);
  text-decoration: none;
  transition: color var(--greppo-dur-slow) var(--greppo-ease);
}

.cellar-door__descend:hover,
.cellar-door__descend:focus-visible {
  color: var(--w-accent-soft);
}

.cellar-door__descend-label {
  font-family: var(--font-utility);
  font-size: var(--step--1);
  letter-spacing: var(--track-widest);
  text-transform: uppercase;
}

/* --- Ambient motion, only when greppo.js says so ---------------------- */

.cellar-door[data-ambient="on"] .cellar-door__glow {
  animation: apex-candle-flicker 9s var(--ease-in-out-cinema) infinite;
}

.cellar-door[data-ambient="on"] .cellar-door__grain {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: apex-spin 320s linear infinite;
}

.cellar-door[data-ambient="on"] .cellar-door__swell {
  animation: greppo-decant 34s var(--ease-in-out-cinema) infinite;
}

.cellar-door[data-ambient="on"] .cellar-door__swell--mid {
  animation-duration: 44s;
  animation-delay: -7s;
}

.cellar-door[data-ambient="on"] .cellar-door__swell--near {
  animation-duration: 56s;
  animation-delay: -15s;
}

.cellar-door[data-ambient="on"] .cellar-door__descend svg {
  animation: greppo-bob 3.8s var(--ease-in-out-cinema) infinite;
}


/* --------------------------------------------------------------------------
   4. THE NAME & THE SLOPE
   Two columns: the prose runs, the slope drawing holds still beside it.
   -------------------------------------------------------------------------- */

.greppo-name__inner {
  display: grid;
  gap: var(--space-9);
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  align-items: start;
}

.greppo-name__prose p {
  font-size: var(--step-1);
  line-height: var(--lh-loose);
  color: var(--w-text-dim);
}

.greppo-name__prose blockquote {
  color: var(--w-text);
  font-style: italic;
}

.greppo-slope {
  position: sticky;
  inset-block-start: calc(var(--header-h) + var(--space-6));
}

.greppo-slope__svg {
  inline-size: 100%;
  block-size: auto;
  border: 1px solid var(--w-line);
  border-radius: var(--radius-md);
}

/* --- The pending-facts table ------------------------------------------ */

.greppo-facts {
  margin-block-start: var(--space-8);
  border-block-start: 1px solid var(--w-line);
}

.greppo-facts__row {
  display: grid;
  grid-template-columns: minmax(8rem, 13rem) 1fr;
  gap: var(--space-3) var(--space-5);
  align-items: baseline;
  padding-block: var(--space-4);
  border-block-end: 1px solid var(--w-line);
}

.greppo-facts__row dt {
  font-family: var(--font-utility);
  font-size: var(--step--1);
  font-weight: 400;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--w-text-dim);
}

.greppo-facts__row dd {
  color: var(--w-text);
}


/* --------------------------------------------------------------------------
   5. THE CRAFT — rail + scrub sequence

   THE DEFAULT IS STACKED. Five panels, one under the next, fully readable
   with no script, on touch, and under reduced motion. The horizontal scrub is
   an enhancement layered on top inside one media query, and only there does
   the geometry become tall-and-sticky.

   The `.js` prefixes on the scrub rules are not decoration: motion.css styles
   `.js [data-scrub] .scrub__track` at the same specificity, and these have to
   out-rank it in both directions. Source order (this file loads last) settles
   equal-specificity ties in our favour.
   -------------------------------------------------------------------------- */

.craft-scrub {
  position: relative;
  margin-block-start: var(--space-8);
}

/* Stacked truth — no JS, touch, reduced motion. */
.craft-scrub__frame {
  display: block;
}

.craft-scrub .scrub__track,
.js .craft-scrub .scrub__track {
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
  inline-size: 100%;
  max-inline-size: var(--maxw-content);
  margin-inline: auto;
  padding-inline: var(--gutter);
  transform: none;
}

.craft-scrub .scrub__panel {
  inline-size: 100%;
}

/* The rail is a scrub affordance. Where there is no scrub, there is no rail. */
.craft-rail {
  display: none;
}

/* --- Panels ------------------------------------------------------------ */

.craft-panel {
  display: grid;
  gap: var(--space-6);
  align-content: start;
  padding: var(--space-7);
  border: 1px solid var(--w-line);
  border-radius: var(--radius-lg);
  background-color: color-mix(in srgb, var(--w-surface) 60%, transparent);
}

.craft-panel__art svg {
  inline-size: 100%;
  block-size: auto;
  border-radius: var(--radius-md);
}

.craft-panel__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.craft-panel__index {
  font-family: var(--font-utility);
  font-size: var(--step--1);
  letter-spacing: var(--track-widest);
  text-transform: uppercase;
  color: var(--w-accent-soft);
}

.craft-panel__title {
  font-size: var(--step-3);
}

.craft-panel__line {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--step-1);
  line-height: var(--lh-body);
  color: var(--w-text);
}

.craft-panel__note {
  font-size: var(--step--1);
  color: var(--w-text-dim);
}

/* --- The scrub itself: fine pointer, motion allowed, script running ---- */

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {

  /* Tall enough that five panels take a long, deliberate scroll to cross.
     The overflow reset is load-bearing, not tidying: motion.css declares
     `.js [data-scrub] { overflow: hidden }`, and an `overflow: hidden`
     ancestor becomes the scrolling box for any `position: sticky` descendant.
     That box is this 400vh block, which never scrolls — so the frame below
     would silently never stick. Clipping is the frame's job instead. */
  .js .craft-scrub {
    block-size: 400vh;
    overflow: visible;
  }

  .js .craft-scrub__frame {
    position: sticky;
    inset-block-start: 0;
    display: flex;
    align-items: center;
    block-size: 100svh;
    padding-block-start: calc(var(--header-h) + var(--space-8));
    overflow: hidden;
  }

  .js .craft-scrub .scrub__track {
    flex-direction: row;
    align-items: stretch;
    inline-size: max-content;
    max-inline-size: none;
    margin-inline: 0;
    gap: var(--space-6);
    padding-inline: var(--gutter);
  }

  .js .craft-scrub .scrub__panel {
    inline-size: min(84vw, 44rem);
  }

  .js .craft-panel {
    align-content: center;
    block-size: 100%;
  }

  /* The rail sticks under the header for the whole sequence. */
  .js .craft-rail {
    position: sticky;
    inset-block-start: calc(var(--header-h) + var(--space-3));
    z-index: var(--z-sticky);
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-2) var(--space-6);
    margin: 0;
    padding: var(--space-3) var(--gutter);
  }

  .js .craft-rail__item {
    position: relative;
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    padding-block-end: var(--space-2);
    font-family: var(--font-utility);
    font-size: var(--step--1);
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--w-text-dim);
    opacity: 0.45;
    transition:
      opacity var(--greppo-dur-slower) var(--greppo-ease),
      color var(--greppo-dur-slower) var(--greppo-ease);
  }

  .js .craft-rail__item::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    block-size: 1px;
    background-color: var(--w-accent);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--greppo-dur-slower) var(--greppo-ease);
  }

  .js .craft-rail__item.is-current {
    opacity: 1;
    color: var(--w-accent-soft);
  }

  .js .craft-rail__item.is-current::after {
    transform: scaleX(1);
  }

  .js .craft-rail__num {
    color: var(--w-accent);
  }
}


/* --------------------------------------------------------------------------
   6. THE COLLECTION — AND THE SHOP

   This section is the money surface of the whole website: five released wines,
   five prices, five Stripe checkouts. Everything below exists to make that read
   as expensive, calm and trustworthy, in both of the two states the markup can
   be in.

   TWO STATES, ONE LOOK. With JavaScript on, commerce.js empties the mount and
   builds its own cards; with JavaScript off, the cards written into the page
   stand. Both use the SAME class names — .commerce__grid, .commerce-card,
   .commerce-card__stage, .commerce-card__viewer, .commerce-card__foot,
   .commerce-card__price — which commerce.css already styles for every world.
   So every rule here is scoped to `.greppo-shop`, the OUTER block that survives
   the build, and it adds only the Greppo dressing on top: the cellar niche the
   bottle turns in, the candlelit hairline along the top edge of each card, and
   the warm bloom under the pointer. Nothing here is duplicated per state and
   nothing here has to be kept in step with the JavaScript.

   NO PRICE, NO NAME AND NO LINK IS SET IN CSS. The figure, the wine and the
   Stripe address live in the markup and in assets/js/commerce/catalogue.js,
   which is where the client can find and change them.

   WHAT WAS HERE BEFORE. Three decorative "vessel" cards on parchment, drawn as
   SVG silhouettes with a pointer-tracked sheen, plus an order block whose
   buttons were deliberately disabled. The range was unreleased when that was
   written; it is released now, so the silhouettes have been replaced by real
   turnable geometry and the dead buttons by real checkouts. The .bottle-card
   and .greppo-order rules that dressed them are gone with the markup they
   dressed — nothing else on the site used either.
   -------------------------------------------------------------------------- */

/* --- The anchor photograph --------------------------------------------- */
/* 933 x 1400 — a tall portrait. Two things were cutting it: a 3/4 frame, which
   `object-fit: cover` resolved by shaving the top and bottom off the picture,
   and the full content measure, which made a portrait absurdly tall. The frame
   now carries the file's own ratio (declared on the element) so there is
   nothing left to crop, and the figure is held to a portrait-friendly measure
   and centred, so the whole photograph is on screen at a sane size. */

.greppo-collection__anchor {
  max-inline-size: min(100%, 28rem);
  margin-inline: auto;
  text-align: center;
}


/* --------------------------------------------------------------------------
   6a. THE ORDER BLOCK — frame, heading, standing note

   id="order" sits on .greppo-shop, NOT on the mount, because commerce.js
   empties the mount when it builds. The Support Center, the concierge and the
   footer all link to that anchor, so it has to survive the build.
   -------------------------------------------------------------------------- */

.greppo-shop {
  margin-block-start: var(--space-10);
  padding-block-start: var(--space-9);
  border-block-start: 1px solid var(--w-line);
  /* The anchor is a scroll destination. Land under the fixed header, not
     behind it. */
  scroll-margin-block-start: calc(var(--header-h) + var(--space-6));

  /* The four-corner "expand" glyph, shared by the frame affordance and the
     opener button below so there is one shape to change. It is a MASK — the
     stroke is opaque black only so its alpha is 1 where the arrows are; the
     colour is supplied per use with a --w-* token, so no raw hex reaches the
     page. */
  --greppo-expand-icon:
    url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2.2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M9%204H4v5M15%204h5v5M9%2020H4v-5M15%2020h5v-5'/%3E%3C/svg%3E");
}

.greppo-shop__head {
  max-inline-size: var(--maxw-prose);
  margin-inline: auto;
  margin-block-end: var(--space-8);
  text-align: center;
}

.greppo-shop__head .eyebrow--ruled {
  justify-content: center;
}

.greppo-shop__lede {
  margin-block-start: var(--space-4);
  font-size: var(--step-0);
  line-height: var(--lh-loose);
  color: var(--w-text-dim);
  text-wrap: pretty;
}

/* The standing note under the grid. It survives the build too. */
.greppo-shop__note {
  max-inline-size: var(--maxw-prose);
  margin-block-start: var(--space-8);
  margin-inline: auto;
  font-size: var(--step--1);
  line-height: var(--lh-loose);
  color: var(--w-text-dim);
  text-align: center;
}

.greppo-shop__note a {
  white-space: nowrap;
}


/* --------------------------------------------------------------------------
   6b. THE GRID

   Five bottles. The track floor is wide enough that a 3D stage is worth
   dragging in — never so wide that a phone gets a horizontal scrollbar, which
   is what `min(100%, …)` guarantees. At the full content measure that settles
   to three across and two beneath, both at the same width as the row above, so
   the short last row reads as composed rather than left over.
   -------------------------------------------------------------------------- */

.greppo-shop .commerce__grid {
  gap: var(--space-7) var(--space-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}


/* --------------------------------------------------------------------------
   6c. THE CARD

   .card supplies the column, the surface, the border and the hover elevation;
   commerce.css supplies the internal rhythm. This adds the cellar dressing:
   a little depth in the surface, a candlelit hairline along the top edge, and
   a warmer, wider bloom on approach than the shared component gives.

   The hairline is a ::before on the card rather than a border, so it can be a
   gradient that fades out at both ends — a border cannot. .card already
   carries `position: relative` and `overflow: hidden`, so it needs no help to
   place or clip it.
   -------------------------------------------------------------------------- */

.greppo-shop .commerce-card {
  border-color: color-mix(in srgb, var(--w-accent) 24%, var(--w-line));
  background-image: linear-gradient(
    168deg,
    var(--w-surface-2) 0%,
    var(--w-surface) 44%,
    var(--w-base-2) 100%
  );
  box-shadow: var(--shadow-1);
}

.greppo-shop .commerce-card::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 1;
  block-size: 1px;
  background-image: linear-gradient(
    to right,
    transparent 0%,
    var(--w-accent) 50%,
    transparent 100%
  );
  opacity: 0.5;
  pointer-events: none;
}

.greppo-shop .commerce-card:hover,
.greppo-shop .commerce-card:focus-within {
  border-color: color-mix(in srgb, var(--w-accent) 55%, transparent);
  box-shadow: var(--shadow-3), 0 0 72px -20px var(--w-glow);
}


/* --------------------------------------------------------------------------
   6d. THE 3D BOTTLE — the frame it turns in

   THE SIZE. The client asked to be able to inspect these bottles, so the stage
   is generous: 4 / 5 is close to square but still a touch taller than it is
   wide, which suits a bottle without leaving so much dead air that the bottle
   itself shrinks — the fit in bottle-viewer.js is a BOUNDING SPHERE fit, so the
   ratio decides the air around the model, not whether it fits. The floor of
   300px keeps the drag target far past the 44px minimum on every axis.

   ONLY --bv-* TOKENS ARE SET. bottle-viewer.css owns every rule that draws the
   stage, the canvas, the hint and the focus ring; retuning its documented
   custom properties is the supported way to dress one viewer differently, and
   it means this file can never fall out of step with that one.

   THE SCROLL GUARANTEE IS NOT TOUCHED. bottle-viewer.css sets
   `touch-action: pan-y` on the stage until the visitor deliberately engages it,
   so on a phone a vertical swipe over a bottle is a page scroll and always has
   been. Nothing here overrides that. What DOES have to be restated is the size:
   this selector is more specific than bottle-viewer.css's own
   [data-bottle-viewer] media queries, so its coarse-pointer and narrow-screen
   heights are re-declared below rather than silently lost.
   -------------------------------------------------------------------------- */

.greppo-shop .commerce-card__viewer {
  --bv-ratio: 4 / 5;
  --bv-min-height: 300px;
  --bv-max-height: 46vh;
  --bv-radius: var(--radius-md);
  /* A deeper niche than the default: the cellar, not the room. Dark glass
     needs a dark ground to show its rim light against. */
  --bv-surface-2: var(--w-surface);
  --bv-surface: var(--w-base-2);
  --bv-base: var(--w-base);
  --bv-line: color-mix(in srgb, var(--w-accent) 26%, var(--w-line));
  --bv-accent: var(--w-accent);
  --bv-text-dim: var(--w-text-dim);
}

/* A finger, not a mouse: a shorter stage, so the name, the price and the
   button — the parts of the card that carry the meaning — stay on screen. */
@media (pointer: coarse) {
  .greppo-shop .commerce-card__viewer {
    --bv-min-height: 240px;
    --bv-max-height: 42vh;
  }
}

@media (max-width: 30rem) {
  .greppo-shop .commerce-card__viewer {
    --bv-ratio: 4 / 5;
    --bv-min-height: 220px;
    --bv-max-height: 40vh;
  }
}


/* --------------------------------------------------------------------------
   6d-ii. THE INSPECT AFFORDANCE — "you can open this"

   The client asked, in as many words, for a pop-up per product where the bottle
   can be turned freely, and for it to be discoverable — "a feature nobody finds
   is worth nothing". cinema/bottle-showcase.js provides the machinery and one
   always-visible opener button under the frame; this makes the FRAME itself
   read as openable. bottle-viewer.css already fades in its "Drag to turn" hint
   on hover and focus, so between the two the visitor is told, on approach, that
   the bottle both TURNS and EXPANDS.

   It is drawn only when an opener actually exists — :has(.bshow-open) — so it
   never appears in the no-JavaScript state, or where WebGL2 is absent and the
   showcase declined to install, i.e. it never promises a pop-up that cannot
   open. Where :has() is unsupported the glyph simply never shows, which is a
   quieter card, not a broken one. It is a corner glyph, pointer-events: none,
   so it never sits on the surface the visitor has to be able to drag, and it
   touches none of bottle-viewer.js's .bv__* classes.
   -------------------------------------------------------------------------- */

.greppo-shop .commerce-card__stage:has(.bshow-open) {
  position: relative;
}

.greppo-shop .commerce-card__stage:has(.bshow-open)::after {
  content: "";
  position: absolute;
  /* Just inside the top-right corner of the frame: the stage's own side/top
     padding, plus a small margin in from the frame edge. */
  inset-block-start: calc(var(--space-6) + var(--space-3));
  inset-inline-end: calc(var(--space-6) + var(--space-3));
  z-index: 2;
  inline-size: 1.35rem;
  block-size: 1.35rem;
  background-color: var(--w-accent);
  -webkit-mask: var(--greppo-expand-icon) center / contain no-repeat;
  mask: var(--greppo-expand-icon) center / contain no-repeat;
  filter: drop-shadow(0 1px 4px color-mix(in srgb, var(--w-base) 82%, transparent));
  opacity: 0;
  transform: scale(0.86);
  transition:
    opacity var(--dur-micro) var(--ease-out-soft),
    transform var(--dur-micro) var(--ease-out-soft);
  pointer-events: none;
}

.greppo-shop .commerce-card:hover .commerce-card__stage:has(.bshow-open)::after,
.greppo-shop .commerce-card:focus-within .commerce-card__stage:has(.bshow-open)::after {
  opacity: 0.95;
  transform: scale(1);
}

/* The opener button itself carries no glyph: the cinema-system layer owns the
   button's ::before (its engagement ring) and components.css owns its ::after
   (the sweep), so both button pseudo-elements are spoken for. Its real
   "Inspect in 3D — <wine>" label and bottle-showcase.css's skin already say
   what it does; the frame glyph above is where the expand cue lives. */


/* --------------------------------------------------------------------------
   6e. THE PRICE

   MANDATORY, AND THE LOUDEST THING ON THE CARD AFTER THE WINE ITSELF.
   commerce.css already sets the figure in the display face, in the world
   accent, with tabular numerals. Greppo only warms the rule above it and lifts
   the figure a step, because on this near-black field gold at a larger size is
   the most expensive-looking thing the palette can do.

   THE FIGURE PRINTED HERE MUST MATCH WHAT THE MATCHING STRIPE PAYMENT LINK
   CHARGES. CSS cannot check that and neither can the page — see the header of
   greppo-winery/index.html and of assets/js/commerce/catalogue.js.
   -------------------------------------------------------------------------- */

.greppo-shop .commerce-card__foot {
  border-block-start-color: color-mix(in srgb, var(--w-accent) 30%, transparent);
}

.greppo-shop .commerce-card__figure {
  font-size: var(--step-4);
  color: var(--w-accent);
}


/* --------------------------------------------------------------------------
   6f. THE ONE BUTTON

   One control per card, and it takes money, so it is the largest target on the
   card. Everything that matters is already provided: .btn carries the 44px
   floor and the visible focus ring, commerce.css raises it to 3rem, and the
   duration retune at the top of this file has already put it on cellar time.
   All that is left is the tracking, opened up a little so "Buy now" reads as a
   deliberate invitation rather than as a piece of interface.
   -------------------------------------------------------------------------- */

.greppo-shop .commerce-card__cta {
  letter-spacing: var(--track-widest);
}


/* --------------------------------------------------------------------------
   7. VISITS & TASTINGS
   -------------------------------------------------------------------------- */

.greppo-visits__inner {
  display: grid;
  gap: var(--space-9);
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
  align-items: start;
}

.greppo-visits__text .cluster {
  margin-block-start: var(--space-7);
}

.greppo-visits__card {
  border-radius: var(--radius-md);
}


/* --------------------------------------------------------------------------
   8. JOURNAL — a justified row, not a square grid

   THE CROP. The three photographs are two portraits (933x1400 and 934x1400)
   and one landscape (1800x1200). They were being forced into a 1/1 frame with
   `object-fit: cover`, which shaved the top and bottom off both portraits and
   the sides off the landscape. That is the "half cut" the client reported.

   THE FIX. Each <li> declares its own picture's true ratio as --tile-ratio,
   and that single value does two jobs:

     1. It becomes the media frame's --frame-ratio, so the box matches the file
        and `cover` has nothing left to cut. No photograph is cropped.

     2. It sets flex-basis AND flex-grow. With both proportional to the ratio,
        each tile settles at a width proportional to its own ratio — which
        means every tile resolves to the SAME HEIGHT, in growth and in shrink
        alike. Three differently shaped pictures, one flat top edge and one
        flat bottom edge. Composed, not ragged.

   Add or swap a photograph and the only thing to change is the number beside
   it in the markup.
   -------------------------------------------------------------------------- */

.greppo-journal__grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

.greppo-journal__grid > li {
  --tile-ratio: 1;   /* Overridden per tile in the markup. */
  display: flex;
  flex-grow: calc(var(--tile-ratio) * 100);
  flex-shrink: 1;
  flex-basis: calc(var(--tile-ratio) * 18rem);
  min-inline-size: 0;
}

.greppo-journal__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  inline-size: 100%;
}

.greppo-journal__item .media-frame {
  --frame-ratio: var(--tile-ratio);
}

.greppo-journal__actions {
  margin-block-start: var(--space-8);
}


/* --------------------------------------------------------------------------
   9. CONTACT & HELP
   -------------------------------------------------------------------------- */

.greppo-contact__inner {
  display: grid;
  gap: var(--space-9);
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr);
  align-items: start;
}

.greppo-contact__text .cluster {
  margin-block-start: var(--space-7);
}

.greppo-contact__card {
  padding: var(--space-7);
  border: 1px solid var(--w-line);
  border-radius: var(--radius-md);
  background-color: var(--w-surface);
}

.greppo-contact__card-title {
  font-family: var(--font-utility);
  font-size: var(--step--1);
  letter-spacing: var(--track-widest);
  text-transform: uppercase;
  color: var(--w-accent-soft);
}

.greppo-contact__list {
  margin-block-start: var(--space-5);
}

.greppo-contact__row {
  display: grid;
  grid-template-columns: minmax(7rem, 10rem) 1fr;
  gap: var(--space-2) var(--space-5);
  align-items: baseline;
  padding-block: var(--space-4);
  border-block-end: 1px solid var(--w-line);
}

.greppo-contact__row:last-child {
  border-block-end: 0;
}

.greppo-contact__row dt {
  font-family: var(--font-utility);
  font-size: var(--step--1);
  font-weight: 400;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--w-text-dim);
}

.greppo-contact__row dd {
  color: var(--w-text);
}

.greppo-contact__legal {
  margin-block-start: var(--space-5);
  font-family: var(--font-utility);
  font-size: var(--step--1);
  color: var(--w-text-dim);
}


/* --------------------------------------------------------------------------
   10. WORLD KEYFRAMES
   Transform and opacity only. Named greppo-* so they can never collide with
   the shared apex-* library in motion.css.

   greppo-decant translates in SVG USER UNITS, not CSS pixels of the page —
   the hero viewBox is 1600x900, so these values read as a few tenths of a
   percent of the field. That is the whole point: it should look like the wine
   is settling, not sliding.
   -------------------------------------------------------------------------- */

@keyframes greppo-decant {
  0%, 100% { transform: translate(0, -10px); }
  50%      { transform: translate(-18px, 12px); }
}

@keyframes greppo-bob {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, 6px, 0); }
}


/* --------------------------------------------------------------------------
   11. NARROW VIEWPORTS
   -------------------------------------------------------------------------- */

@media (max-width: 62em) {
  .greppo-name__inner,
  .greppo-visits__inner,
  .greppo-contact__inner {
    grid-template-columns: 1fr;
    gap: var(--space-8);
  }

  .greppo-slope {
    position: static;
  }

  /* Three tiles in one justified row need width to stay legible. Below this
     they stack instead, one centred column, each picture still at its own
     ratio — so the fix for the cropping holds here too, and a single column
     cannot be ragged. */
  .greppo-journal__grid {
    display: grid;
    grid-template-columns: 1fr;
    justify-items: center;
  }

  .greppo-journal__grid > li {
    inline-size: 100%;
    max-inline-size: 30rem;
  }
}

@media (max-width: 40em) {
  .cellar-door {
    padding-block-end: var(--space-11);
  }

  .cellar-door__wordmark {
    letter-spacing: 0.04em;
  }

  .cellar-door__line {
    font-size: var(--step-3);
  }

  .craft-panel {
    padding: var(--space-5);
  }

  .greppo-contact__card {
    padding: var(--space-5);
  }

  .greppo-facts__row,
  .greppo-contact__row {
    grid-template-columns: 1fr;
    gap: var(--space-1);
  }
}

/* A pointer that cannot hover cannot carry a bloom, and a highlight stuck on
   after a tap is worse than none. The card keeps its border and its hairline;
   only the approach state is dropped. */
@media (hover: none) {
  .greppo-shop .commerce-card:hover {
    border-color: color-mix(in srgb, var(--w-accent) 24%, var(--w-line));
    box-shadow: var(--shadow-1);
  }

  /* :focus-within is kept. A keyboard or switch user on a touch screen still
     needs to see which card their focus is inside. */
  .greppo-shop .commerce-card:focus-within {
    border-color: color-mix(in srgb, var(--w-accent) 55%, transparent);
    box-shadow: var(--shadow-3), 0 0 72px -20px var(--w-glow);
  }
}


/* --------------------------------------------------------------------------
   12. REDUCED MOTION — the complete override for this file

   motion.css ends with a universal safety net, but it loads BEFORE this file
   and its `*` selector carries zero specificity, so a class-level animation
   declared above would out-rank it. Every animated and transitioned rule in
   this stylesheet is therefore switched off by hand, here, at a specificity
   that actually wins. greppo.js independently refuses to set
   [data-ambient="on"] under the same preference, so the gate is closed from
   both sides.

   What is left is the same page: the same field, the same wordmark, the same
   five panels of craft, the same parchment. Holding still.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {

  /* --- Hero: art present, art still. ---------------------------------- */
  .cellar-door__glow,
  .cellar-door__grain,
  .cellar-door__swell,
  .cellar-door__descend svg,
  .cellar-door[data-ambient="on"] .cellar-door__glow,
  .cellar-door[data-ambient="on"] .cellar-door__grain,
  .cellar-door[data-ambient="on"] .cellar-door__swell,
  .cellar-door[data-ambient="on"] .cellar-door__swell--mid,
  .cellar-door[data-ambient="on"] .cellar-door__swell--near,
  .cellar-door[data-ambient="on"] .cellar-door__descend svg {
    animation: none;
    transform: none;
    opacity: 1;
  }

  .cellar-door__descend {
    transition: none;
  }

  /* --- Craft: stacked, and the rail has nothing left to report. -------- */
  .craft-scrub,
  .js .craft-scrub {
    block-size: auto;
  }

  .craft-scrub__frame,
  .js .craft-scrub__frame {
    position: static;
    display: block;
    block-size: auto;
    padding-block-start: 0;
    overflow: visible;
  }

  .craft-scrub .scrub__track,
  .js .craft-scrub .scrub__track {
    flex-direction: column;
    inline-size: 100%;
    max-inline-size: var(--maxw-content);
    margin-inline: auto;
    transform: none;
  }

  .craft-scrub .scrub__panel,
  .js .craft-scrub .scrub__panel {
    inline-size: 100%;
  }

  .craft-rail,
  .js .craft-rail {
    display: none;
  }

  /* --- The shop: NOTHING IS LOST, only movement. ----------------------
     Every wine, every description, every price and every button is still
     here, at the same size, in the same order, saying the same words. The
     card simply stops travelling and blooming under the pointer, and the
     3D bottle — which bottle-viewer.js has already settled to one static
     frame — stays fully turnable for a visitor who chooses to turn it,
     because motion a visitor asks for is not motion imposed on them. */
  .greppo-shop .commerce-card {
    transition: none;
  }

  .greppo-shop .commerce-card:hover,
  .greppo-shop .commerce-card:focus-within {
    transform: none;
  }

  /* The inspect glyph still appears on hover and focus — it carries meaning —
     but it neither slides nor scales in. */
  .greppo-shop .commerce-card__stage:has(.bshow-open)::after {
    transition: none;
    transform: none;
  }

  .greppo-shop .commerce-card:hover .commerce-card__stage:has(.bshow-open)::after,
  .greppo-shop .commerce-card:focus-within .commerce-card__stage:has(.bshow-open)::after {
    transform: none;
  }

  /* --- The slope stops tracking the scroll. ---------------------------- */
  .greppo-slope {
    position: static;
  }

  /* --- Cellar pacing collapses; state still changes, it just arrives. -- */
  [data-world="greppo"] .btn,
  [data-world="greppo"] .btn::after,
  [data-world="greppo"] .card,
  [data-world="greppo"] .chip,
  [data-world="greppo"] .route-card,
  [data-world="greppo"] .media-frame {
    transition-duration: 1ms;
    transition-delay: 0s;
  }
}
