/* index.html — the studio homepage.
   Extracted from the page's <style> block; the <link> replaces it in place.

   The card component lives here too, since the homepage is the only page that
   has cards. It used to sit in common/css/design-system.css, back when
   product-configuration.html showed the same grid — see the note at the foot of
   that file for why it is better off out of there. */

:root {
  --bg:         #000000;
  --purple-mid: #7B2FFF;
  --purple-hi:  #B264FF;
  --pink:       #D44EFF;
  --white:      #FFFFFF;
  --muted:      rgba(255,255,255,0.55);
  --line:       rgba(255,255,255,0.10);
}

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

body {
  min-height: 100vh;
  background: var(--bg);
  color: var(--white);
  font-family: 'Inter', system-ui, sans-serif;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  position: relative;
}

/* Ambient glow — mostly black with a faint blue drift */
body::before {
  content: "";
  position: fixed;
  inset: -30%;
  background:
    radial-gradient(38% 38% at 22% 28%, rgba(46,110,255,0.13), transparent 72%),
    radial-gradient(36% 36% at 82% 78%, rgba(30,80,200,0.11), transparent 72%),
    radial-gradient(28% 28% at 62% 18%, rgba(90,150,255,0.07), transparent 72%);
  pointer-events: none;
  z-index: 0;
  will-change: transform;
  animation: drift 34s ease-in-out infinite alternate;
}

@keyframes drift {
  0%   { transform: translate3d(-2%, -1.5%, 0) scale(1);    }
  50%  { transform: translate3d(2.5%, 2%,  0) scale(1.06); }
  100% { transform: translate3d(-1.5%, 2.5%, 0) scale(1.03); }
}

@media (prefers-reduced-motion: reduce) {
  body::before { animation: none; }
}

main {
  position: relative;
  z-index: 1;
  flex: 1;
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: clamp(3rem, 8vh, 7rem) clamp(1.5rem, 5vw, 4rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* ── Header ── */
header {
  margin-bottom: clamp(3rem, 8vh, 6rem);
  text-align: center;
}
h1 {
  font-size: clamp(2.4rem, 6vw, 4.2rem);
  font-weight: 500;
  line-height: 1.05;
  letter-spacing: -0.02em;
  max-width: 14ch;
  margin: 0 auto;
}
.subtitle {
  margin: 1.4rem auto 0;
  font-size: clamp(1rem, 1.6vw, 1.15rem);
  font-weight: 300;
  color: var(--muted);
  max-width: 46ch;
  line-height: 1.6;
}

/* ── Sections ──
   Each section is a header (title + one-line subtitle) over its own card
   grid. Three-up rather than four: at a quarter width the titles wrap and the
   photos lose their subject. */
.section-head { margin-bottom: 1.75rem; }
.section-head h2 {
  font-size: clamp(1.4rem, 2.6vw, 1.9rem);
  font-weight: 500;
  letter-spacing: -0.01em;
}
.section-subtitle {
  margin-top: 0.5rem;
  font-size: 0.95rem;
  font-weight: 300;
  color: var(--muted);
  max-width: 74ch;
  line-height: 1.55;
}

/* An unlabelled hairline between sections — each section's own header says
   what's below it. */
.grid-divider {
  height: 1px;
  background: var(--line);
  margin: clamp(2.5rem, 6vh, 4.5rem) 0;
}

/* ============================================================
   CARDS — one component, two states
   ============================================================

   Ready:  <a   class="card"             href="/x.html">
   Soon:   <div class="card card--soon"  data-href="/x.html">

   The element type IS the state. A Soon card is not a link at all, so its URL
   can't leak into a copied link and a visitor with JS off can't reach it
   either — which is the whole reason it isn't just a disabled <a>. Dev mode
   swaps it for <a class="card card--soon card--unlocked"> (soon-cards.js) and
   it then renders exactly like a Ready one: the state is about whether the
   card is a way in, and nothing else. It used to carry a Coming Soon badge and
   a frosted photo as well — see the note on `.card--soon` below for why those
   went.

   Readiness used to be written three times over — the element type, a `.soon`
   class, AND a `data-status="ready|soon"` attribute that no CSS and no JS ever
   read. It is now written once. Likewise every card carried a `.card--photo`
   modifier that had no alternative: a photo-less card existed on no page. Both
   are gone, and `.card` is the whole component.

   Each card sets its own `--photo` inline, because that is per-card *content*.
   `--mx`/`--my`/`--px`/`--py` are set by common/js/card-tilt.js on mousemove
   and are *styling*: they carry the cursor's position into the rim glint and
   the photo pan. */

.cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
  /* No `perspective` here on purpose — it lives on `.card` instead. A
     perspective on this grid would put ONE vanishing point at the grid's
     centre and project all nine cards toward it, so a card's depth would
     depend on which column it landed in: measured at rest, the same
     foreground layer sat at -31px, -10px and +11px from its own card's left
     edge across the three columns. Each card owning its own perspective makes
     every card an identical diorama wherever it sits. */
}
/* 1180px, not 1000px: `main` caps at max-width: 1180px, so 3 columns never
   render wider than ~335px regardless of viewport — that's the ceiling, not
   a value that grows with the window. Between 1000px and 1180px viewport
   width the container itself is still narrower than that cap, so 3 columns
   briefly get squeezed to ~275px, tighter than at any other breakpoint
   (tighter even than the single-column mobile layout) — enough to clip a
   card__title/card__desc that fits everywhere else. Holding 2 columns until
   the container can actually afford the 3-column plateau removes that dip
   entirely. */
@media (max-width: 1180px) {
  .cards { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .cards { grid-template-columns: 1fr; }
}

.card {
  /* ── The three knobs the depth effect is tuned with ──
     Every number below is derived from these, so tuning happens here and
     nowhere else. card-tilt.js reads --depth-perspective off the element so
     the CSS and the script can't drift apart. */
  --depth-perspective: 800px;  /* how   strong the projection is; lower = more extreme */
  --depth-text:        40px;   /* how far the foreground plane sits in front of the photo */
  --pan-photo:         70px;   /* how far the photo slides against the cursor */
  --photo-bleed:       50px;   /* photo oversize, so panning never exposes an edge */

  position: relative;
  display: flex;
  flex-direction: column;
  /* Only `.card__body` is left in flow (no CTA), so this is what puts the
     title/description at the bottom of the card instead of the top. */
  justify-content: flex-end;
  min-height: 340px;
  padding: 2rem;
  border-radius: 20px;
  border: 1px solid rgba(255,255,255,0.16);
  background: rgba(255,255,255,0.03);
  text-decoration: none;
  color: inherit;
  will-change: transform;

  /* NO `overflow: hidden` and NO `backdrop-filter` here, and that is the whole
     reason the depth works. Both are "grouping" properties: either one forces
     the *used* value of `transform-style` back to `flat`, which silently kills
     every `translateZ` on the layers below — while `getComputedStyle` happily
     keeps reporting "preserve-3d", so nothing in devtools tells you.

     The clip the photo needs lives on `.card__photo` instead, where an
     `overflow: hidden` flattens only that wrapper's own children and leaves
     the card's 3D context alone. `backdrop-filter` was simply dropped: it
     blurs what is *behind* the card, which here is solid black under a smooth
     low-alpha gradient, and blurring a smooth gradient changes no pixels. It
     cost the entire 3D context for nothing.

     A `clip-path` on the photo pseudo-element was tried first, to avoid the
     extra wrapper — it preserves the 3D context, but a clip-path is defined in
     the element's own space, so it travelled with the pan and dragged the
     window off the card by the full pan distance. See `.card__photo`. */
  transform-style: preserve-3d;

  /* The perspective is part of the card's OWN transform rather than a
     `perspective` property on the grid, so the vanishing point is this card's
     centre — see the note on `.cards`. Declared here rather than left to
     card-tilt.js so the depth still reads when that script doesn't run: no
     GSAP, or reduced motion. The card then sits still and layered instead of
     flattening out, which is the behaviour perfume/docs/adr/0027 asks for —
     reduced motion stops the movement, not the composition.

     card-tilt.js drives rotationX/rotationY/y on top of this via GSAP
     quickTo, which is why `transform` is absent from the transition below: a
     CSS transition and an inline style easing the same property every frame
     just fight each other. */
  transform: perspective(var(--depth-perspective));
  transition: border-color 0.4s, background 0.4s;
}
.card:hover {
  /* Lift deliberately set to 0 for now — matched by LIFT_PX in card-tilt.js,
     so neither path lifts. Kept as a declaration rather than deleted so the
     two stay visibly paired when the lift comes back. The perspective has to
     be repeated: `transform` is one property, so naming it again replaces the
     whole value, and dropping the projection here would flatten the card the
     instant the cursor arrived. */
  transform: perspective(var(--depth-perspective)) translateY(0px);
  border-color: rgba(255,255,255,0.28);
  background: rgba(255,255,255,0.05);
}

/* Vignette effect on card itself */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 0 rgba(0,0,0,0.3);
  transition: box-shadow 0.2s ease;
  z-index: 1; /* Keep this below text */
  pointer-events: none;
}

.card:hover::before {
  box-shadow: inset 0 0 50px 10px rgba(0,0,0,0.3);
}

/* ── The photo, and the window it moves inside ──
   Two elements, and the split is the whole point.

   `.card__photo` is the WINDOW: it never moves, and it owns the clip. The
   photo inside it is what pans. That separation is not optional — it is the
   one arrangement that keeps the frame still while the content slides:

     - `overflow: hidden` on a PARENT clips in the parent's own space, so the
       child slides around behind a window that stays put. What we want.
     - `clip-path` on the MOVING element defines the clip in that element's
       local space, so it is part of what gets transformed. The window travels
       with the photo, overhanging one card edge and pulling clear of the
       other. Tried that; it drifted by the full pan distance.

   `.card` itself must stay free of `overflow`/`backdrop-filter`, or its 3D
   context collapses and every translateZ below dies silently. Putting the
   clip here instead costs nothing: `overflow: hidden` flattens only THIS
   element's children, and the photo inside only ever moves in 2D.

   Coplanar with the card — no translateZ. A layer with real depth cannot stay
   perfectly clipped to the card's rounded rect, because a receded clip
   projects smaller and drifts off the border as the card rotates. At z=0 the
   window is locked to the card's own plane at every angle. The depth you see
   is the foreground pulling away from the photo, not the photo falling back. */
.card__photo {
  position: absolute;
  /* The padding edge — just inside the 1px border, exactly where a background
     would sit. (An absolutely positioned child resolves `inset` against the
     padding box, which INCLUDES the parent's padding, so this is the card's
     full face and not its 2rem content area.) */
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  overflow: hidden;
}
.card__photo::before {
  content: "";
  position: absolute;
  /* Oversized on every side, so the pan below always has photo to bring into
     view and never drags an edge into the window. Must stay larger than half
     of --pan-photo; at 50px against a 35px maximum pan there is 15px spare. */
  inset: calc(var(--photo-bleed) * -1);
  background:
    linear-gradient(0deg,
      rgba(3,6,18,0.8) 0%,
      rgba(3,6,18,0.7) 30%,
      rgba(3,6,18,0.1) 100%),
    var(--photo);
  background-size: cover;
  /* `0deg` (not `180deg`) so the darkest stop lands at the bottom, behind the
     title and description now that they sit there instead of the top. The
     scrim stays centred; only the photo moves. */
  background-position: center, center;
  z-index: 0;
  /* The photo slides the OPPOSITE way to the cursor while the foreground
     plane, being nearer the viewer, is carried the same way as the cursor by
     the card's rotation. The two separating is what reads as depth — perfume
     states the rule as "the product always goes toward the cursor and the
     void always away from it" (perfume/CONTEXT.md), and the reasoning at
     perfume/js/scene/bottle/wobble.js: two things drifting apart read as
     depth, two things drifting together read as the whole frame sliding.

     `--px`/`--py` (card-tilt.js) are the cursor's position in the card,
     centred to -0.5…0.5. No scale: under the "window" reading the photo
     always fills the card, so there is nothing here to shrink. */
  transform: translate(calc(var(--px, 0) * (var(--pan-photo) * -1)),
                       calc(var(--py, 0) * (var(--pan-photo) * -1)));
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}

/* ── The rim glint ── */
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* A specular glint confined to the rim, not the whole face: `padding`
     sets the ring's thickness, and the content-box/border-box mask pair
     below (XOR'd) punches out everything except that ring — the standard
     gradient-border trick, repurposed so the "gradient" is a highlight
     instead of a static border color. `--mx`/`--my` (card-tilt.js) are the
     same cursor position the tilt reacts to, so the glint travels around
     the rim toward wherever the cursor is, the way light catches the edge
     of glass as you turn it. No translateZ: it stays flush with the card's
     own edge regardless of the photo/text depths behind it.

     Plain alpha, no `mix-blend-mode` — overlay blending inside a 3D context
     composites against the accumulated result rather than what's visually
     underneath, which muddied a fully-opaque white center down to a faint
     grey. The gradient's own alpha (opaque middle fading to transparent)
     already gives a crisp white core without needing a blend mode. */
  padding: 2px;
  background: radial-gradient(200px circle at var(--mx, 50%) var(--my, 50%), #fff, transparent 90%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 0;
  transition: opacity 0.2s;
  pointer-events: none;
}
.card:hover::after { opacity: 1; }

/* ── Card contents: the foreground plane ──
   This is the half of the effect that actually has depth. `translateZ` brings
   the text toward the viewer, so the card's rotation carries it further than
   the photo sitting behind it — that difference IS the parallax, and it needs
   no translate of its own.

   No scale correction: the ~5.3% enlargement perspective gives something
   brought 40px closer is left in, deliberately, so the foreground reads
   larger as well as nearer. That size difference is the cue that survives
   with the cursor nowhere near the card, which is what makes the layering
   legible at rest rather than only under the pointer. (A `scale(0.95)` here
   would cancel it exactly and put the type back at its designed size.)

   `drop-shadow` is the other half of the at-rest read: a shadow cast down
   onto the photo is what "this text is a nearer plane" looks like when
   nothing is moving. */
.card__body {
  position: relative;
  z-index: 2; /* Higher than the vignette */
  transform: translateZ(var(--depth-text));
  filter: drop-shadow(0 14px 18px rgba(0,0,0,0.55));
  /* Fixed, not min-height: with `.card` still bottom-anchoring this via
     justify-content: flex-end, a height that shrink-fit its content would
     let a one-line description sit lower (and its title start lower) than a
     two-line one. A fixed height decouples the title's start position from
     how much the description actually wraps, so every card's title lands at
     the same offset from the bottom regardless of copy length. Sized for the
     worst case — 1 title line + the gap + 2 description lines — with a
     little rounding slack so sub-pixel line-height differences across
     browsers never clip the second description line:
       title  1.7rem * 1.2 line-height       = 2.04rem
       gap    margin-bottom below             = 0.7rem
       desc   0.92rem * 1.55 line-height * 2  = 2.852rem
                                          total  5.592rem → 5.7rem
     A short description just leaves blank space under it inside this box,
     rather than being pulled down to meet the bottom — that keeps
     title+desc reading as one grouped block instead of the description
     floating away from its title. */
  height: 5.7rem;
}
.card__title {
  font-size: 1.7rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  /* Explicit rather than the browser default `normal`, so the height math
     above has a fixed, cross-browser-stable number to work from. */
  line-height: 1.2;
  margin-bottom: 0.7rem;
  /* Clamped to 1 line — not just by convention (titles are short category
     names) but enforced, so a future long title can't grow the title's own
     height and push the description's start position off the fixed offset
     the whole scheme depends on. overflow: hidden is scoped to this element
     alone: it has no 3D children, so it can't collapse `.card`'s
     preserve-3d the way overflow on `.card` itself would (see home/docs/adr/0001). */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 1;
  overflow: hidden;
}
.card__desc {
  font-size: 0.92rem;
  font-weight: 300;
  line-height: 1.55;
  color: var(--muted);
  /* Same reasoning as .card__title's clamp: a backstop against copy that
     runs over the character budget, not the primary mechanism — it just
     means an over-length string truncates gracefully instead of spilling
     past the card's edge (no overflow: hidden on `.card` to catch it). */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* ── The grid's coarse gate: what dev mode shows and what it hides ──

   Two attributes, one on either side of the `?dev` switch. They sit on whole
   SECTIONS, on the hairlines between them, and on individual cards, because
   the split does not line up with the grid's structure:

     data-only="dev"   only when dev mode is on.  The whole catalogue.
     data-only="prod"  only when it is off.       The Car card, alone.

   This is a SECOND, coarser gate than Ready/Soon above, and the two are about
   different things. Ready/Soon is whether the card is a way in to the page at
   all. This one is about what a visitor to the public site is shown, which is now one card: everything else, Ready or Soon, is
   the team's own view of the roadmap behind `?dev`.

   Hidden by DEFAULT, revealed by the attribute rather than the other way
   round, so the locked grid is what renders with no CSS variable to restore,
   no JS, and no dev-mode.js at all. Each state only ever ADDS a
   `display: none` — neither rule has to put back a `display` the component
   chose for itself (`.card` is flex, a section is block), which a
   hide-then-unhide pair would.

   NOT A SECRET. A hidden card's href is still in the markup, so this hides the
   way in, not the page — a typed URL still opens whatever isn't gated. The gate
   that actually stops a page rendering is the page's own
   (`data-gate="soon"` on its dev-mode.js tag), and dev mode is
   stage-management either way — see /common/js/dev-mode.js. */
html:not([data-dev="on"]) [data-only="dev"]  { display: none; }
html[data-dev="on"]       [data-only="prod"] { display: none; }

/* ── The Soon state ──
   Locked: an inert <div>. It isn't clickable, so it doesn't get to feel
   clickable either — `pointer-events: none` means a :hover never matches, so
   there's no glow, no tilt and no photo pan (and no cursor override is needed:
   the pointer just shows whatever's beneath).

   THAT IS ALL THAT IS LEFT OF THIS STATE, and it is the only part that was
   ever about behaviour. The Coming Soon badge and the frosted blur on the
   photo are gone. They were a message to a VISITOR — "this exists, it isn't
   ready, don't ask about it yet" — and the visitor can no longer see any of
   these cards: `data-only` above shows the public site one card, so a Soon
   card now appears only in dev mode, to the team. Blurring the shot and
   badging it there told us nothing we didn't already know from the purple Dev
   marker, and it hid the one thing we actually open the grid in dev mode to
   look at: the photo.

   So in dev mode a card looks like the experience. What it still can't do is
   lie about being clickable: a card with no page behind it (no `data-href` —
   Industrial) keeps this rule, so it stays dead to the cursor, with no glint
   and no tilt, while every card that does have a page has been turned into a
   real <a> by soon-cards.js and lost the class's effect via
   `.card--unlocked`. */
.card--soon:not(.card--unlocked) { pointer-events: none; }

/* ── Footer ── */
.site-footer {
  margin-top: clamp(3rem, 8vh, 6rem);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.6rem 2rem;
}
.reserved-link {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  transition: color 0.3s;
}
.reserved-link:hover { color: var(--white); }
