/* The fitting room — the pattern × garment matrix.
   Ported verbatim from design-system/screens/product-browser.html with the storefront layer's
   porting rules: tokens come from tokens.css (the screen's --mp-line-2 maps to --mp-line-soft,
   its local font vars to --mp-sans/--mp-mono), the header bar lives in header-bar.css, the
   prompt dock in storefront.css, and the screen's loose generic names are scoped fr-*. */

/* ══ Stage — one viewport, no scroll; small screens scroll instead of crushing ══ */
body:has(.fr-stage) { overflow: hidden; }

.fr-stage {
  height: 100dvh;
  min-height: 640px;
  display: flex;
  flex-direction: column;
}

/* ══ Browse stage — 3-column grid ══ */
.fr-browse {
  position: relative;
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(200px, 340px) minmax(280px, 1fr) minmax(200px, 340px);
  align-items: center;
  gap: clamp(14px, 2vw, 24px);
  padding: 0 clamp(18px, 3.9vw, 56px);
}

/* The bloom is data-driven — the cell sets --fr-glow from the pattern's stored primary color. */
.fr-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(38% 55% at 50% 50%,
      var(--fr-glow, rgba(251, 94, 190, .15)), rgba(255, 201, 160, .04) 55%, transparent 78%);
}

.fr-cell { display: contents; }

/* ══ Axis-step transition — the stage slides toward the pressed axis (View Transitions API).
      Scoped to the browse grid so the header and dock never so much as flicker; the JS stamps
      the step on <html> as data-fr-dir before calling startViewTransition, and browsers
      without the API (or readers with reduced motion) keep the instant swap.

      Timing lives here rather than in the module. --fr-step-dur is how one press reads;
      --fr-step-rush is what the runner drops to (data-fr-rush) when more presses are already
      queued behind the animation, so a fast surf stays a slide instead of turning back into a
      cut. Both sit on :root because the ::view-transition tree hangs off the document element —
      a custom property declared on .fr-browse is somewhere the pseudo-elements cannot read. ══ */
:root {
  --fr-step-dur: 460ms;
  --fr-step-rush: 190ms;
  --fr-step-ease: cubic-bezier(.22, .9, .26, 1);
  /* Travel has to be a real fraction of the stage to read as direction — 36px on a 1600px
     stage is 2%, which is why the step used to look like a cut rather than a move. */
  --fr-step-slide: clamp(72px, 9vw, 180px);
  --fr-step-rise: clamp(52px, 6vh, 120px);
}

.fr-browse { view-transition-name: fr-browse; }

::view-transition-old(fr-browse),
::view-transition-new(fr-browse) {
  animation-duration: var(--fr-step-dur);
  animation-timing-function: var(--fr-step-ease);
  /* The UA default is plus-lighter, which washes the outgoing and incoming stages into each
     other; against dark art that reads as one bright dissolve, not as two cells passing. */
  mix-blend-mode: normal;
}

html[data-fr-rush]::view-transition-old(fr-browse),
html[data-fr-rush]::view-transition-new(fr-browse) { animation-duration: var(--fr-step-rush); }

/* Next arrives from where its chevron points; the old cell leaves out the opposite side. */
html[data-fr-dir="next-pattern"]::view-transition-old(fr-browse) { animation-name: fr-slide-out-l; }
html[data-fr-dir="next-pattern"]::view-transition-new(fr-browse) { animation-name: fr-slide-in-r; }
html[data-fr-dir="prev-pattern"]::view-transition-old(fr-browse) { animation-name: fr-slide-out-r; }
html[data-fr-dir="prev-pattern"]::view-transition-new(fr-browse) { animation-name: fr-slide-in-l; }
html[data-fr-dir="next-garment"]::view-transition-old(fr-browse) { animation-name: fr-slide-out-u; }
html[data-fr-dir="next-garment"]::view-transition-new(fr-browse) { animation-name: fr-slide-in-d; }
html[data-fr-dir="prev-garment"]::view-transition-old(fr-browse) { animation-name: fr-slide-out-d; }
html[data-fr-dir="prev-garment"]::view-transition-new(fr-browse) { animation-name: fr-slide-in-u; }

@keyframes fr-slide-out-l { to { opacity: 0; transform: translateX(calc(var(--fr-step-slide) * -1)); } }
@keyframes fr-slide-out-r { to { opacity: 0; transform: translateX(var(--fr-step-slide)); } }
@keyframes fr-slide-out-u { to { opacity: 0; transform: translateY(calc(var(--fr-step-rise) * -1)); } }
@keyframes fr-slide-out-d { to { opacity: 0; transform: translateY(var(--fr-step-rise)); } }

@keyframes fr-slide-in-l { from { opacity: 0; transform: translateX(calc(var(--fr-step-slide) * -1)); } }
@keyframes fr-slide-in-r { from { opacity: 0; transform: translateX(var(--fr-step-slide)); } }
@keyframes fr-slide-in-u { from { opacity: 0; transform: translateY(calc(var(--fr-step-rise) * -1)); } }
@keyframes fr-slide-in-d { from { opacity: 0; transform: translateY(var(--fr-step-rise)); } }

@media (prefers-reduced-motion: reduce) {
  .fr-browse { view-transition-name: none; }
}

.fr-cell .fr-glow { display: none; } /* the page owns one glow; a swapped cell must not stack another */

/* ══ Center column — peek / model / peek ══ */
.fr-center {
  position: relative;
  height: 100%;
  min-height: 0;
  display: grid;
  grid-template-rows: auto 1fr auto;
  justify-items: center;
  padding: 6px 0;
  z-index: 2;
}

.fr-model {
  position: relative;
  min-height: 0;
  width: 100%;
  container-type: size;
  padding: 8px 0;
  margin: 0;
  touch-action: none; /* both swipe axes belong to the stage, not the browser */
}

/* View carousel — selected centered full-size; neighbors shrink with distance (js lays out).
   Transitions arm only after the first layout (.fr-live) so boot paints in place instead of
   assembling from the pre-JS state. */
.fr-cv {
  position: absolute;
  bottom: 2cqh;
  left: 50%;
  height: 60cqh;
  transform: translateX(-50%);
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
}

.fr-model.fr-live .fr-cv {
  transition: left .5s cubic-bezier(.22, .9, .26, 1), height .5s cubic-bezier(.22, .9, .26, 1),
    opacity .35s, filter .35s;
}

/* New mockups are transparent cut-outs (png, the provider's own alpha); legacy renders are
   photos flattened on white. drop-shadow follows the alpha so the cut-out gets a true contour
   shadow, and the rounded corners only ever show on the legacy rectangles. */
.fr-cv img {
  height: 100%;
  width: auto;
  display: block;
  pointer-events: none;
  border-radius: var(--mp-r-card);
  filter: drop-shadow(0 24px 48px rgba(0, 0, 0, .55));
}

.fr-cv:not(.on):hover { opacity: .95 !important; filter: saturate(1) !important; }

.fr-cv.on { cursor: default; }

/* A single view needs no carousel math — it just fills the stage. */
.fr-cv:only-of-type,
.fr-model .fr-cv:first-child:last-child {
  position: absolute;
  left: 50%;
  height: 96cqh;
}

/* The drawn stand-in (Pages/Shared/_GarmentBlank.cshtml) for a cell with no imagery at all — not
   even a blank plate, which is where a garment sits between being ranked and its plates rendering.
   It used to be the pattern thumbnail, which put the same image on the stage twice and showed
   nothing shaped like the product. Sized and seated to land where the real cut-out will: a plate is
   96cqh of image with the garment filling most of it, so a shape drawn tight to its own bounds is a
   little shorter. */
/* An unrendered cell (D59): the plate is a real provider render of the *blank* product — a
   transparent cut-out with the garment on it and nothing else — so the pattern tiles behind it and
   the visitor sees this product against this print without being shown a render that doesn't exist.
   .fr-plain-note says so in words beneath. Falls back to plain black wherever --fr-behind is unset.

   The pattern is the point here, so it is barely held back: it reads as the wallpaper the blank is
   standing in front of, not as a hint. It was at .34 with a tight radial mask, which left the stage
   looking black around a lit garment. Softened to a wide vignette that only takes the very edge,
   so the field ends without a hard rectangle. */
.fr-model.is-unrendered::before {
  content: "";
  position: absolute;
  /* Bounded by the stage column, never wider. The plate is laid out off the container's HEIGHT and
     on a narrow column is wider than the column itself, which is tempting to match — but the rails
     sit right there, and a field sized to the garment paints the pattern under the pattern's own
     name and the price. The garment overhanging its backdrop is the lesser problem. */
  inset: 4%;
  border-radius: var(--mp-r-lg, 16px);
  background-image: var(--fr-behind, none);
  background-size: 26% auto;
  background-repeat: repeat;
  opacity: .88;
  -webkit-mask-image: radial-gradient(ellipse at center, #000 74%, transparent 99%);
  mask-image: radial-gradient(ellipse at center, #000 74%, transparent 99%);
  pointer-events: none;
  z-index: 0;
}

.fr-model.is-unrendered .fr-cv { z-index: 1; }

/* Where the pattern actually goes when a plate exists: on the plate's own box. The plate is a
   transparent cut-out, so a background on the <img> paints through it exactly — the wallpaper the
   product is standing in front of, edge to edge with the photograph and no wider.

   The stage-sized field above cannot do this job. The plate is a square render and the stage is a
   tall narrow column, so a field pinned to the stage is either much taller than the garment (the
   garment marooned in a sea of pattern) or much narrower (the garment hanging over both edges).
   Sizing the plate to fit the stage and letting it carry its own field makes the two the same
   rectangle at every viewport, which is the whole composition. */
.fr-model.is-unrendered:has(.fr-cv)::before { display: none; }

.fr-model.is-unrendered .fr-cv:only-of-type,
.fr-model.is-unrendered .fr-cv:first-child:last-child {
  inset: 3% 0;
  height: auto;
  width: auto;
  transform: none;
  /* Flex, not grid: the plate's `max-height: 100%` has to resolve against this box's definite
     height, and a grid item's percentage max-height resolves against an auto-sized row instead —
     which is no constraint at all, so the plate overflowed the stage and sat on the ↓ chevron. */
  display: flex;
  align-items: center;
  justify-content: center;
}

.fr-model.is-unrendered .fr-cv img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--mp-r-lg, 16px);
  /* A scrim thin enough to keep the pattern reading as itself — it is the point of the cell — but
     enough to stop a bright print fighting the blank garment in front of it. */
  background-image:
    linear-gradient(rgba(8, 8, 10, .14), rgba(8, 8, 10, .14)),
    var(--fr-behind, none);
  background-size: auto, 26% auto;
  background-repeat: no-repeat, repeat;
}

/* ── No imagery at all ──────────────────────────────────────────────────────────────────────
   A garment listed before its blank plate rendered. There is nothing truthful to show, so the
   stage says so front and centre instead of standing in for the product. Deliberately not styled
   as an error — nothing is broken, a photograph is late — but big enough that nobody mistakes the
   pattern field behind it for a picture of the garment. */
.fr-noart {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 10px;
  padding: 0 8%;
  text-align: center;
  pointer-events: none;
}

/* The field behind the words, so the copy survives a busy pattern without hiding it. */
.fr-noart::before {
  content: "";
  position: absolute;
  inset: 22% 6%;
  border-radius: var(--mp-r-card);
  background: radial-gradient(ellipse at center, rgba(8, 8, 10, .9) 40%, rgba(8, 8, 10, 0) 78%);
  z-index: -1;
}

.fr-noart-mark {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
}

.fr-noart-mark svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: var(--mp-peach);
  stroke-width: 1.6;
  stroke-linecap: round;
}

.fr-noart-head {
  margin: 0;
  font-family: var(--mp-display);
  font-size: clamp(17px, 2.4cqw, 24px);
  font-weight: 600;
  color: var(--mp-ink);
}

.fr-noart-sub {
  margin: 0;
  max-width: 46ch;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--mp-dim);
}

/* The stage and the storefront's cards share the `.is-unrendered` hook, and storefront.css hangs a
   small "NOT YET RENDERED" pill off its ::after. That rule says in its own comment that it is for
   everywhere *except* here — the room has the full sentence below — but nothing scoped it, so the
   stage was stacking two pills that said the same thing. The exception lives in the file that owns
   `.fr-model` rather than in the shared one, which has no business knowing this class exists. */
.fr-model.is-unrendered::after { display: none; }

/* Along the foot of the plate, the way a caption sits inside the bottom of a photograph — not
   across the garment's middle, which is where a percentage of the whole centre column put it.
   The offset is mostly fixed because what it has to clear is: the ↓ chevron's row is a constant
   ~81px of chevron, gap and label at every stage height, and the plate stops 3% above it. A pure
   percentage drifts across viewports and lands on the garment on the short ones.

   (It sat at 22% for the same reason it now sits here — at 13% it collided with the chevron's
   label and read as one garbled line on exactly the cells this note exists to explain.) */
.fr-plain-note {
  position: absolute;
  left: 50%;
  bottom: calc(84px + 2%);
  transform: translateX(-50%);
  font-family: var(--mp-mono);
  text-transform: uppercase;
  letter-spacing: .14em;
  font-size: 10px;
  color: var(--mp-mute);
  background: rgba(8, 8, 10, .72);
  border: 1px solid var(--mp-line-soft);
  border-radius: var(--mp-r-pill);
  padding: 8px 14px;
  white-space: nowrap;
  z-index: 8;
}

.fr-peek {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  color: var(--mp-mute);
  font-family: var(--mp-mono);
  text-transform: uppercase;
  letter-spacing: .16em;
  font-size: 11px;
  z-index: 8;
}

.fr-peek-bot { flex-direction: column-reverse; }

/* ══ The round steppers ══ */
.fr-chev {
  position: relative;
  overflow: hidden;
  width: 48px;
  height: 48px;
  border-radius: var(--mp-r-pill);
  display: grid;
  place-items: center;
  cursor: pointer;
  background: rgba(30, 30, 34, .8);
  background-size: cover;
  background-position: center;
  border: 1px solid var(--mp-line-soft);
  color: #fff;
  backdrop-filter: blur(4px);
  transition: border-color .15s;
  padding: 0;
  flex: none;
}

.fr-chev::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(8, 8, 10, .42);
  transition: background .15s;
}

.fr-chev:hover { border-color: rgba(255, 255, 255, .45); }

.fr-chev:hover::before { background: rgba(8, 8, 10, .16); }

/* The garment steppers wear the mock's fabric weave; pattern steppers wear real pattern art. */
.fr-chev-garment {
  background-image: repeating-linear-gradient(55deg, #26262c 0 6px, #45454e 6px 8px);
}

.fr-chev svg {
  position: relative;
  z-index: 1;
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .9));
}

/* ══ Rails ══ */
.fr-rail {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-self: stretch;
  z-index: 7;
  pointer-events: none;
}

.fr-rail > * { pointer-events: auto; }

/* A blurred black scrim keeps rail type readable over a model that slides behind it. */
.fr-rail::after {
  content: "";
  position: absolute;
  top: 24%;
  bottom: 6%;
  left: -32px;
  right: -32px;
  z-index: -1;
  pointer-events: none;
  border-radius: 32px;
  background: radial-gradient(70% 60% at 50% 50%,
      rgba(0, 0, 0, .85), rgba(0, 0, 0, .55) 60%, transparent 100%);
  filter: blur(8px);
}

/* The mock's 32% headroom, but shrinkable: a real size ladder (2XS–6XL) wraps to two rows and
   the commerce column must never spill over the dock. */
.fr-rail::before { content: ""; flex: 0 10 32%; min-height: 40px; pointer-events: none; }

/* With the copy panel open the spacer is the first thing to give — it is whitespace, and
   whitespace is the cheapest thing in the column to spend before body text starts scrolling. */
.fr-rail-l:has(.fr-copy[open])::before { min-height: 0; }

.fr-rail-l {
  align-items: flex-start;
  text-align: left;
  /* Bounded to its grid row: the opened copy panel shrinks to fit rather than pushing the rail
     down past the stage and behind the prompt dock. overflow stays visible on purpose — the
     rail's blurred scrim bleeds 32px past both edges and clipping it is worse than the problem. */
  max-height: 100%;
  min-height: 0;
}

.fr-rail-r { align-items: flex-end; text-align: right; }

.fr-step {
  position: absolute;
  top: 14%;
  display: flex;
  align-items: center;
  gap: 14px;
  cursor: pointer;
  font-family: var(--mp-mono);
  text-transform: uppercase;
  letter-spacing: .16em;
  font-size: 11px;
  color: var(--mp-mute);
  background: none;
  border: 0;
  padding: 0;
}

.fr-rail-l .fr-step { left: 0; }

.fr-rail-r .fr-step { right: 0; }

.fr-step:hover { color: var(--mp-ink); }

.fr-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--mp-mono);
  text-transform: uppercase;
  letter-spacing: .2em;
  font-size: 11px;
  color: var(--mp-pink);
  margin-bottom: 14px;
}

.fr-eyebrow b { color: var(--mp-pink); font-weight: 400; }

.fr-eyebrow .arw { letter-spacing: 0; transition: transform .15s; display: inline-block; }

a.fr-eyebrow:hover, a.fr-eyebrow:hover b { color: var(--mp-pink-soft); }

a.fr-eyebrow:hover .arw { transform: translateX(3px); }

/* The head is a plain block everywhere but the wide room, and full width so the name wraps
   against the rail exactly as it did when it was the rail's own child rather than this one's. */
.fr-head { width: 100%; }

.fr-name {
  font-family: var(--mp-sans);
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.04;
  font-size: clamp(28px, 3.2vw, 50px);
  text-wrap: balance;
  min-height: 1.04em;
  margin: 0;
}

.fr-tagline, .fr-trust {
  font-family: var(--mp-mono);
  text-transform: uppercase;
  letter-spacing: .1em;
  font-size: 10px;
  color: var(--mp-mute);
  line-height: 2;
  margin: 12px 0 0;
  max-width: 250px;
}

.fr-trust { max-width: 280px; }

/* ══ The audience's two controls (left rail) — the heart and the thumbs (D84).

      Both are the shared components — Pages/Shared/_FavoriteButton (the design system's ghost
      `.hbtn.fav` pill, buttons.html) and _PatternRating (rating.css's roomy `.rate` group) — so
      what is here is only their scale in this room: 36px pills carrying 10px mono at .14em, which
      is what .fr-ops-btn below already is, on the raised surface with a hairline edge that .hbtn
      and .fr-size are both made of. Nothing about either control's *behaviour* or its states is
      restated; a room that redrew the heart would be a second heart.

      They sit under the pattern's name because the left rail is the pattern's column — the right
      rail is the garment and the money — and both accents that mean "pressed" here (the heart's
      border, the thumb-up's fill) are the pink this column already wears in its eyebrow and in the
      rule under its name. ══ */
.fr-acts {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 16px;
}

/* The sign-in nudge belongs to the pattern page, not to a stage: the tagline is already using that
   line, and the heart works for this browser with or without an account (§8's visitor capture).
   The browser's own detail panel hides it for the same reason. */
.fr-acts .fav-hint { display: none; }

.fr-acts .hbtn.fav {
  height: 36px;
  padding: 0 14px;
  gap: 7px;
  font-size: 10px;
  letter-spacing: .14em;
}

.fr-acts .hbtn.fav .heart { font-size: 13px; }

.fr-acts .rate:not(.rate-compact) {
  height: 36px;
  padding: 0 3px;
  border-color: var(--mp-line-soft);
  background: var(--mp-raise);
}

.fr-acts .rate:not(.rate-compact) .rate-btn { width: 28px; height: 28px; }

/* ══ Operator chrome (left rail) — the Feature shortcut.

      Peach, like every other "instrumentation, not final storefront" affordance (the pattern
      page's preview note set the precedent): an operator reading the room must never mistake
      their own tooling for something a visitor can see. Quiet at rest — the artwork is still
      the point of this screen. ══ */
.fr-ops { margin-top: 22px; }

.fr-ops-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 15px;
  border: 1px solid rgba(255, 201, 160, .28);
  border-radius: var(--mp-r-pill);
  background: rgba(255, 201, 160, .07);
  color: var(--mp-peach);
  font-family: var(--mp-mono);
  text-transform: uppercase;
  letter-spacing: .14em;
  font-size: 10px;
  line-height: 1;
  text-decoration: none;
  transition: border-color .15s, background .15s;
}

.fr-ops-btn:hover {
  border-color: var(--mp-peach);
  background: rgba(255, 201, 160, .14);
}

/* Already on the hero: the same shortcut becomes its editor, in the accent that means "live". */
.fr-ops-btn.is-featured {
  color: var(--mp-pink);
  border-color: rgba(251, 94, 190, .32);
  background: rgba(251, 94, 190, .08);
}

.fr-ops-btn.is-featured:hover {
  border-color: var(--mp-pink);
  background: rgba(251, 94, 190, .15);
}

.fr-ops-mark { font-size: 11px; letter-spacing: 0; }

/* ══ Commerce (right rail) ══ */
.fr-sizes {
  display: flex;
  gap: clamp(6px, .7vw, 9px);
  margin-top: 20px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.fr-size {
  min-width: clamp(38px, 3.2vw, 44px);
  height: clamp(38px, 3.2vw, 44px);
  display: grid;
  place-items: center;
  border: 1px solid var(--mp-line-soft);
  border-radius: 10px;
  background: var(--mp-raise);
  font-family: var(--mp-mono);
  font-size: 13px;
  font-weight: 700;
  color: var(--mp-ink);
  cursor: pointer;
  transition: border-color .15s, color .15s, background .15s;
  padding: 0 6px;
  white-space: nowrap;
}

.fr-size.active {
  border-color: var(--mp-pink);
  color: var(--mp-pink);
  box-shadow: 0 0 0 1px var(--mp-pink) inset;
}

.fr-size:hover:not(.active) { border-color: rgba(255, 255, 255, .30); background: #26262c; }

.fr-price {
  font-family: var(--mp-sans);
  font-weight: 700;
  letter-spacing: -.02em;
  font-size: clamp(30px, 2.8vw, 40px);
  color: var(--mp-pink);
  margin-top: 18px;
}

.fr-own {
  font-family: var(--mp-mono);
  text-transform: uppercase;
  letter-spacing: .1em;
  font-size: 10px;
  color: var(--mp-mute);
  line-height: 1.8;
  margin-top: 8px;
  max-width: 280px;
}

.fr-own-closed { color: var(--mp-peach); margin-top: 20px; }

.fr-add.hbtn.primary {
  height: 48px;
  padding: 0 28px;
  border-radius: var(--mp-r-pill);
  margin-top: 14px;
  font-weight: 700;
}

.fr-add[disabled] { opacity: .45; cursor: not-allowed; }

.fr-add.added { background: var(--mp-pink-soft); }

/* ══ Dock ══ */
.fr-dock {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 0 34px 24px;
  z-index: 5;
}

/* The shared dock partial brings its own .dock spacing for scrolling pages; pinned to the
   stage's bottom it needs none of it. */
.fr-dock .dock { margin: 0; padding: 0; width: 100%; display: flex; justify-content: center; }

.fr-dock .pbar { width: 100%; max-width: 1000px; }

/* ══ Empty room ══ */
.fr-empty-eyebrow {
  font-family: var(--mp-mono);
  text-transform: uppercase;
  letter-spacing: .2em;
  font-size: 11px;
  color: var(--mp-pink);
}

.fr-empty {
  flex: 1;
  display: grid;
  place-content: center;
  justify-items: center;
  text-align: center;
  gap: 10px;
  padding: 24px;
}

.fr-empty h1 {
  font-family: var(--mp-sans);
  font-weight: 700;
  letter-spacing: -.02em;
  margin: 0;
}

.fr-empty-sub { color: var(--mp-mute); font-size: 14px; margin: 0; }

.fr-empty-links { display: flex; gap: 12px; margin-top: 12px; }

/* ══ Mobile pass — rails become an overlay + bottom sheet (design.md grants the latitude:
      "rails → sheets, prompt bar → floating action"; the swipe gestures carry the axes) ══ */

/* ══ This pairing's own words (T-24).

      The rails are a 10px mono micro-label column by design, so prose cannot simply be dropped
      into them. The blurb gets the smallest real reading size the system has and a measure taken
      from .pd-desc — the one prose style the storefront already owns — narrowed to fit a rail.
      The long half stays folded behind a native <details> so the stage keeps its proportions
      until someone asks for the rest.

      A <summary> is a click target, so it wears the accent on hover the way every other
      interactive word on the site does — and the default marker is removed rather than restyled,
      because it sits on the first line and the blurb is three. ══ */
.fr-copy {
  margin: 14px 0 0;
  max-width: 300px;
  /* A flex column so the panel below can be the part that gives, and min-height:0 so it actually
     may — a flex item's default minimum is its content, which is the whole reason overflowing
     panels refuse to shrink. */
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.fr-copy > summary {
  cursor: pointer;
  list-style: none;
  display: block;
}

.fr-copy > summary::-webkit-details-marker { display: none; }

.fr-copy-blurb {
  display: block;
  font-size: 13px;
  line-height: 1.6;
  color: var(--mp-dim);
}

.fr-copy > summary:hover .fr-copy-blurb,
.fr-copy > summary:focus-visible .fr-copy-blurb { color: var(--mp-ink); }

/* The affordance: the same mono micro-label the rails already speak, in the accent, with a caret
   that turns when the disclosure opens. */
.fr-copy-more {
  display: inline-block;
  margin-top: 6px;
  font-family: var(--mp-mono);
  text-transform: uppercase;
  letter-spacing: .1em;
  font-size: 10px;
  color: var(--mp-pink);
}

.fr-copy-more::after {
  content: " \25BE";
  display: inline-block;
  transition: transform .18s ease;
}

.fr-copy[open] .fr-copy-more::after { transform: rotate(180deg); }

/* The opened half is a bounded, scrollable box — see the note in _FittingCell.cshtml. The cap is
   viewport-relative so a short laptop gets a smaller box rather than a clipped one, and the
   scrollbar is styled explicitly because macOS overlay scrollbars are invisible at rest and
   "there is more text here" has to be discoverable without hovering. */
.fr-copy-panel {
  /* Takes the rail's leftover room, and no more. The cap keeps a tall screen from handing the
     whole column to body copy; the flex basis is what makes a short screen work, because there
     the space that is left is smaller than any cap worth writing. */
  flex: 1 1 auto;
  min-height: 0;
  /* Leaves the dock its own band: the rail ends where the prompt bar begins, and a cap measured
     against the whole viewport does not know that. 28vh clears it at every height tested from
     560px up, and the flex basis above handles anything shorter. */
  max-height: min(28vh, 300px);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--mp-line-soft) transparent;
  padding-right: 8px;
}

.fr-copy-panel:focus-visible {
  outline: 1px solid var(--mp-pink);
  outline-offset: 4px;
}

.fr-copy-panel::-webkit-scrollbar { width: 6px; }

.fr-copy-panel::-webkit-scrollbar-track { background: transparent; }

.fr-copy-panel::-webkit-scrollbar-thumb {
  background: var(--mp-line-soft);
  border-radius: 3px;
}

.fr-copy-body {
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.7;
  color: var(--mp-dim);
}

.fr-copy-specs {
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 4px;
}

.fr-copy-specs li {
  font-size: 12px;
  line-height: 1.55;
  color: var(--mp-mute);
  padding-left: 12px;
  position: relative;
}

.fr-copy-specs li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .62em;
  width: 4px;
  height: 1px;
  background: var(--mp-line-soft);
}

@media (prefers-reduced-motion: reduce) {
  .fr-copy-more::after { transition: none; }
}

/* ══ The phone's two animated custom properties.

      Both are driven from the panel's live position, and both have to be able to *transition*
      when the panel snaps instead of being dragged — a plain custom property cannot, so they are
      registered. The registrations sit at the top level because @property is not allowed inside
      a conditional group rule; only their use is scoped to the phone, and nothing above 600px
      reads either one, so registering them changes no pixel of the finished room. ══ */

/* How present the garment rail is: 1 at rest, 0 by the time the panel has covered 70% of its
   travel — the rail dissolves under the thumb rather than switching off. The prototype spells it
   --l-grail-fade; this is that property under the contract's name, which is the one name
   fitting-room-mobile.js writes. */
@property --fr-m-fade {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

/* How far the artwork rides up as the panel covers it, so the visible band keeps framing the
   torso rather than the model's face. */
@property --fr-m-shift {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}

/* The phone chrome (.fr-m and everything named fr-m-*) is emitted on every visit and assembled
   by fitting-room-mobile.js, so the rule that keeps it out of the finished tablet and desktop
   room lives here rather than in each of their blocks: above the breakpoint, none of it renders.
   No existing class matches — .fr-model is "fr-mo", not "fr-m-".

   The exemption is for the two room elements the phone module *decorates* rather than invents:
   its stage ghosts are .fr-model figures carrying .fr-cv shots, so they wear the room's own
   rules. A ghost still cannot survive up here — it lives inside .fr-m-ghosts, which this hides —
   and the exemption is what stops the same rule from taking the real artwork with it if a phone
   class is ever left on the seated cell. */
@media (min-width: 820.02px), (max-height: 559.98px) {
  /* ══ The column rails ══════════════════════════════════════════════════════
     From design-system/prototypes/fitting-room-desktop/b-column-rails.html, approved 2026-08-02.
     Every size, pitch, colour and duration below is that file's number.

     Everything that depends on the rails EXISTING hangs off .fr-rails-live, which fitting-room.js
     adds once it has actually built them. Without script the room keeps its four chevrons and its
     original gutters — a no-script visitor must still be able to move. */

  .pr-edge {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 96px;
    overflow: hidden;
    cursor: grab;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    z-index: 8;
    /* Fading both ends is what stops a vertical strip reading as a column of equals — it is a
       window onto the whole catalogue and should look like one. */
    -webkit-mask-image: linear-gradient(180deg, transparent, #000 12%, #000 88%, transparent);
    mask-image: linear-gradient(180deg, transparent, #000 12%, #000 88%, transparent);
  }

  .pr-edge-p { left: 0; }

  .pr-edge-g { right: 0; }

  .pr-edge:active { cursor: grabbing; }

  /* A rail needs a surface. Without one the carousel's shoulder shots — laid out from the centre
     and simply continuing — slide under the thumbnails, and two things at two depths occupy the
     same pixels. Opaque rather than glass: a backdrop-filter over a column this tall makes the
     compositor re-blur it every frame of a scrub, for an effect nobody can see behind an opaque
     strip of thumbnails. A sibling rather than a background on .pr-edge because the edge is masked
     at both ends, and a panel that dissolved with it would read as a fault, not a surface. */
  .pr-back {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 96px;
    z-index: 7;
    pointer-events: none;
    background: var(--mp-black);
  }

  .pr-back-p { left: 0; }

  .pr-back-g { right: 0; }

  .pr-track { position: absolute; top: 50%; left: 0; width: 100%; will-change: transform; }

  .pr-cell {
    position: absolute;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 62px;
    height: 62px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    transition: opacity .18s;
    /* Which accent this rail's selection ring is drawn in, and how much of it this tile has. */
    --pr-ring: 251, 94, 190;
    --pr-on: 0;
  }

  .pr-edge-g .pr-cell { --pr-ring: 255, 201, 160; }

  /* While a rail moves, every one of these values is recomputed each frame, so the transitions
     that smooth them at rest would only lag them — a 180ms ease restarted 60 times a second is a
     delay, not a smoothing. They come off for the duration. */
  .pr-live .pr-cell, .pr-live .pr-cell i { transition: none; }

  .pr-cell i {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 12px;
    background-size: cover;
    background-position: center;
    background-color: var(--mp-raise);
    /* One shadow stack with the ring's alpha driven by how close this tile is to the mark, so
       there is no state to switch: selection hands over between two tiles as the strip passes
       rather than strobing on and off once per tile crossed. */
    box-shadow:
      inset 0 0 0 1px var(--mp-line-soft),
      0 0 0 2px rgba(var(--pr-ring), var(--pr-on)),
      0 0 18px rgba(var(--pr-ring), calc(var(--pr-on) * .45));
    transition: box-shadow .18s;
  }

  .pr-garments .pr-cell i {
    background-size: contain;
    background-repeat: no-repeat;
    background-color: transparent;
  }

  /* ══ Direction, on the tiles ══
     The four cardinal chevrons stand down where the rails are live: a rail that shows what is
     either side of you and moves you there was already doing their job. What they carried that the
     rail did not is *which way*, so that goes on the neighbouring tiles. Hidden while a rail moves
     — mid-gesture the pair changes every few pixels, which reads as flicker. */
  .pr-arrow {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 30px;
    height: 30px;
    border-radius: var(--mp-r-pill);
    display: grid;
    place-items: center;
    /* Nearly opaque, because the thing behind it is a full-bleed print and a bare glyph with a
       shadow loses against one. This is the room's own chevron button, at the size a control needs. */
    background: rgba(10, 10, 14, .9);
    border: 1.5px solid rgb(var(--pr-ring));
    color: #fff;
    font-family: var(--mp-mono);
    font-size: 15px;
    font-weight: 700;
    line-height: 1;
    box-shadow: 0 3px 12px rgba(0, 0, 0, .85);
    pointer-events: none;
    /* An animation rather than a transition: a badge that has just come back is a new node, and
       there is no previous value for a transition to run from. */
    animation: pr-arrow-in .2s ease both;
  }

  @keyframes pr-arrow-in {
    from { opacity: 0; transform: translate(-50%, -50%) scale(.8); }
  }

  /* The two reachable neighbours come forward of the rest of the strip and darken just enough
     under the badge to hold it. Applied only at rest — see .pr-arrow above. */
  .pr-cell.pr-prev, .pr-cell.pr-next { opacity: 1 !important; }

  .pr-cell.pr-prev i, .pr-cell.pr-next i {
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .34), inset 0 0 34px rgba(0, 0, 0, .6);
  }

  /* ══ The room, once the rails are in it ══ */

  .fr-rails-live { padding-left: 116px; padding-right: 116px; }

  /* Two 96px rails plus gutters take 232px of width the grid assumed it had, and its tracks have
     hard minimums. Below about 960px they stop fitting and the grid overflows its own padding —
     the right column slides out under the garment rail and the sizes, the price and the ownership
     line print across the thumbnails. The rails give a little and the tracks give a little. */
  @media (max-width: 1040px) {
    .fr-rails-live .pr-edge, .fr-rails-live .pr-back { width: 84px; }

    .fr-rails-live {
      padding-left: 100px;
      padding-right: 100px;
      grid-template-columns: minmax(168px, 340px) minmax(240px, 1fr) minmax(168px, 340px);
    }
  }

  /* The rails are the four axis steppers now, and two controls for one job is one too many. */
  .fr-rails-live .fr-step, .fr-rails-live .fr-peek { display: none; }

  /* The centre column is a three-row grid — peek, stage, peek. With the peeks gone the figure
     falls into the first `auto` row, which sizes to its own content: the carousel's cqh geometry
     then resolves against a 16px box and the garment renders 15px tall at the top of the stage.
     One row, because there is one thing in it. */
  .fr-rails-live .fr-center { grid-template-rows: minmax(0, 1fr); }

  /* ══ Pink is the pattern axis, peach the garment axis ══
     Both eyebrows were pink, so the two axes looked like the same kind of thing. On the phone this
     pairing became the colour language of the whole room. */
  .fr-rail-r .fr-eyebrow, .fr-rail-r .fr-eyebrow b { color: var(--mp-peach); }

  .fr-rail-r a.fr-eyebrow:hover, .fr-rail-r a.fr-eyebrow:hover b { color: #ffd9bb; }

  .fr-rail-l .fr-name { border-bottom: 1px solid var(--mp-pink); padding-bottom: 10px; }

  .fr-rail-r .fr-name { border-bottom: 1px solid var(--mp-peach); padding-bottom: 10px; }

  /* ══ Direction, on the counters ══
     The eyebrow used to end in a single → meaning no more than "this is a link". Bracketing the
     counter with the axis's own keys says something more useful, in the same glyphs the rail
     badges use, so the pairing of an axis with its direction is stated wherever the axis is named.
     The hover nudge follows the arrow it is on rather than the one direction .arw assumes. */
  .fr-eyebrow .arw-pre { margin-right: 3px; }

  .fr-eyebrow .arw-post { margin-left: 3px; }

  a.fr-eyebrow:hover .arw { transform: none; }

  .fr-rail-l a.fr-eyebrow:hover .arw-pre { transform: translateX(-3px); }

  .fr-rail-l a.fr-eyebrow:hover .arw-post { transform: translateX(3px); }

  .fr-rail-r a.fr-eyebrow:hover .arw-pre { transform: translateY(-3px); }

  .fr-rail-r a.fr-eyebrow:hover .arw-post { transform: translateY(3px); }

  /* ══ Vertical alignment ══
     Both columns hang from the coloured rule under the name, not from the top of the type — the
     rule is the one horizontal both sides share and the thing the eye reads as "these two belong
     together". So the eyebrow and the name live in a box of fixed height with their content
     bottom-aligned: the rule lands at lift + head whatever the name does, and a longer name
     overflows out of the TOP of that box, into empty stage.

     The lift falls away faster than the viewport does. A plain vh fraction hands a 660px screen
     two thirds of the whitespace a 1000px screen gets, which is backwards: the tall screen has
     room to spare and the short one is where an eleven-size garment pushes Add to Cart into the
     prompt dock. Subtracting a constant makes it steep — 103px at 1080, 74 at 900, 42 at 700. */
  .fr-browse {
    --fr-lift: clamp(0px, calc(16vh - 70px), 116px);
    --fr-head: clamp(88px, 15.5vh, 150px);
  }

  /* Shrink factor 10, as the room's own spacer had: when a short screen leaves the column short
     of room, the whitespace above the type is what should disappear, not the type. Everything else
     is pinned at 0 so the give lands there and nowhere else — a squashed head would move the rule,
     which is the one thing this block exists to hold still. */
  .fr-rail::before { flex: 0 10 var(--fr-lift); min-height: 0; }

  .fr-rail > * { flex-shrink: 0; }

  /* Except the copy, which is built to scroll and is the second thing that should give. */
  .fr-rail .fr-copy { flex-shrink: 1; }

  .fr-head {
    flex: none;
    height: var(--fr-head);
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
  }

  /* The head's own children must never shrink either. A flex item's default is to give up height
     when its container has a fixed one, so a three-line name was being squeezed to fit the box —
     and since the rule is that name's own border, the rule ended up drawn through the second line
     with the third spilling over the trust text below it. */
  .fr-head > * { flex-shrink: 0; }

  .fr-rail-l .fr-head { align-items: flex-start; }

  .fr-rail-r .fr-head { align-items: flex-end; }

  /* The name is sized off the viewport's width alone, but what it costs is height: at 1333 × 653
     "Men's Cotton Crew Neck T-Shirt" is three 44px lines, and three lines is what pushes the head
     — and with it both rules, and with them Add to Cart — past the bottom of the stage. Capping
     the type against height too only binds on short screens. */
  .fr-name { font-size: clamp(28px, min(3.2vw, 5.6vh), 50px); }

  /* The scrim behind the type followed the old, lower content; it comes up with it. */
  .fr-rail::after { top: 8%; }

  /* ══ The description, always ══
     The room showed a one-line blurb and put the real description behind a Details disclosure —
     but the blurb is a truncation of the description, so the collapsed state was a worse version
     of text there was room for. fitting-room.js opens the <details>; the summary goes. The panel
     was already a bounded scrollable box, so long copy scrolls exactly as it did when opened. */
  .fr-copy > summary { display: none; }

  .fr-copy-panel { max-height: min(34vh, 340px); }

  /* ══ Short and wide — a phone on its side ══
     The wide room is a fixed-height stage because at a desktop height everything fits in one
     screen and a room that scrolled would put the prompt dock below the fold. At 375px of height
     it does not fit: an eleven-size ladder, a price, an ownership line and Add to Cart need more
     room than the header leaves. This is the one idea worth keeping from the stacked layout that
     used to own these viewports — let the page scroll and give the columns a floor to lay out in
     — applied only where the height actually runs out. */
  @media (max-height: 559.98px) {
    body:has(.fr-stage) { overflow: auto; }

    .fr-stage { height: auto; min-height: 100dvh; }

    /* A DEFINITE height, not a minimum. The stage feeds the carousel its geometry and a
       ResizeObserver on this box re-lays the carousel whenever it changes — so a height that
       depends on its own content is a loop, and it hangs the renderer rather than merely looking
       wrong. The stacked layout this replaces avoided it the same way, with a dvh-bounded row. */
    .fr-browse {
      flex: none;
      height: 470px;
      /* No lift down here; every pixel above the type is one the size ladder needs. */
      --fr-lift: 0px;
      --fr-head: 88px;
    }

    .fr-dock { padding: 8px 14px 12px; }
  }

  .fr-m,
  [class*="fr-m-"]:not(.fr-model, .fr-cv) { display: none; }
}

/* The wide room's old 600–880 stacked single column lived here — pattern identity above the
   stage, commerce in a sheet below it, the page scrolling. It was a third design, and Roger's
   call (2026-08-02) is that there are two: the phone room and the wide room. The stack is gone
   and the two share the whole range between them; where the line falls, and why it reads height
   as well as width, is in fitting-room-core.js under PHONE_ROOM. */

/* ══ The phone room — "L-Rails" ═══════════════════════════════════════════════════════════════

   Ported from design-system/prototypes/fitting-room-mobile/g-l-rails.html, approved 2026-08-01.
   Every size, pitch, duration, easing and mask below is that file's number. What is translated is
   only what they are applied to: the prototype is a standalone page with its own names, and the
   room already has markup for most of what it draws.

     prototype           real                           what it is
     .l-room             .fr-browse                     the box every band is positioned inside
     .l-glow             .fr-glow                       the pattern's own bloom
     .l-stage            .fr-center                     the stage, and the surface both axes drag on
     .l-track            .fr-model                      the cell window
     .l-shot             .fr-cv                         one camera of the seated cell
     .l-tile / .l-note   .fr-tile-only / .fr-plain-note the D59 unrendered pairing
     .l-sheet            .fr-rail-r                     the two-state panel
     .l-title            .fr-m-title                    one phrase, two halves (phone-only markup)
     .l-trust / .l-own   .fr-trust / .fr-own
     .l-copy             .fr-copy                       this pairing's own words, no longer folded
     .l-ladder / .l-size .fr-sizes / .fr-size           the size ladder, relocated into a sheet
     .l-actbar           .fr-m-actbar                   price · size chip · Add to Cart
     .l-grail / .l-prail .fr-m-grail / .fr-m-prail      the L

   Two things CSS cannot do, which fitting-room-mobile.js owns: moving nodes, and measuring. It
   moves .fr-copy out of the left rail into the panel, .fr-price and .fr-add into .fr-m-actbar —
   they cannot stay in the panel, because a transformed element is the containing block for its
   own fixed descendants — and .fr-sizes into the size sheet; and it writes --fr-m-rest,
   --fr-m-sheet-h, --fr-m-vis, --fr-m-grail-h, --fr-m-fade and --fr-m-shift from what it measures.
   Every one of those has a resting default here, so the room is laid out correctly before the
   first frame the module paints. ══ */

@media (max-width: 820px) and (min-height: 560px) {

  /* ── A phone-shaped, non-scrolling app surface: the stage owns both axes, the rails own one
        each and the panel owns the rest. A page that could also scroll makes all four ambiguous.
        The 640px floor belongs to the desktop room's three columns; here the room is exactly as
        tall as the phone is. ── */
  body:has(.fr-stage) { overscroll-behavior: none; }

  .fr-stage { height: 100dvh; min-height: 0; }

  :root {
    --fr-m-ax-p: var(--mp-pink);
    --fr-m-ax-g: var(--mp-peach);
    --fr-m-ease: cubic-bezier(.22, .9, .26, 1);
    --fr-m-edge: 14px;
  }

  /* ══ The room ═══════════════════════════════════════════════════════════════
     Every band is positioned off two numbers: --fr-m-vis (how much of the panel is showing) and
     --fr-m-act-h (the pinned action bar). The L is pinned to the panel's *resting* top edge, so
     nothing behind the panel is ever re-laid-out while it is dragged. */
  .fr-browse {
    display: block;
    padding: 0;
    /* The phone animates its own movement and commits instantly, so it never starts a view
       transition — and the name is not free: it makes the room a stacking context, which would
       trap the size sheet's z-index inside the room and let the prompt dock paint over it. */
    view-transition-name: none;
    /* The panel overhangs the room's foot by whatever it is not showing; the room clips it there
       rather than letting it bleed over the prompt dock. */
    overflow: hidden;
    --fr-m-grail-w: 46px;
    --fr-m-prail-h: 48px;
    --fr-m-act-h: calc(64px + env(safe-area-inset-bottom, 0px));
    --fr-m-vis: 44px;
    /* What the room is laid out against — the panel's *resting* height, not its live one. */
    --fr-m-rest: 180px;
    --fr-m-sheet-h: 320px;
    --fr-m-fold: calc(var(--fr-m-act-h) + var(--fr-m-rest));
    --fr-m-corner: calc(var(--fr-m-fold) + var(--fr-m-prail-h));
  }

  .fr-m,
  .fr-m[hidden] { display: block; }

  /* The prototype carries this guard at the head of its own stylesheet and the port needs it for
     the same reason: an author `display` beats the UA's [hidden] rule, and nearly every rule
     below sets one. Everything the module hides by attribute rather than by class — the camera
     dots on a single-camera cell, the size sheet while the ladder is shut, the size chip on a
     cell with no sizes — needs saying again here or it stays on screen wearing whatever display
     its own rule gave it. .fr-m is the one exception, above: the server ships it hidden and the
     phone stylesheet is what un-hides it. */
  .fr-browse [hidden]:not(.fr-m) { display: none !important; }

  /* The prototype's bloom geometry, on the colour the cell already publishes. */
  .fr-glow {
    background: radial-gradient(46% 40% at 50% 40%,
        var(--fr-glow, rgba(251, 94, 190, .15)), rgba(255, 201, 160, .04) 52%, transparent 74%);
    transition: background .45s ease;
  }

  /* ══ The stage — one product, and the surface both axes are dragged on ══
     Full width, so the product is centred in the *screen* rather than in the gap beside the rail:
     the rail floats over the stage instead of taking a column out of it. */
  .fr-center {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: var(--fr-m-corner);
    height: auto;
    padding: 0;
    overflow: hidden;
    /* The module writes real z-indexes on its cells; without a stacking context here those
       numbers would compete with the panel's and the action bar's out in the room. */
    isolation: isolate;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    cursor: grab;
  }

  .fr-browse.is-drag .fr-center,
  .fr-center.is-drag { cursor: grabbing; }

  .fr-model {
    position: absolute;
    inset: 0;
    padding: 0;
    margin: 0;
  }

  /* Not a reflow: the artwork keeps its size and its place and is simply moved, so the promise
     that nothing re-lays-out when the panel opens still holds. Written on whichever box actually
     wraps the art — the module's own cell box, or the seated cell's figure. */
  .fr-m-cell-box,
  .fr-m-cell > .fr-model,
  .fr-center > .fr-model { transform: translateY(var(--fr-m-shift, 0px)); }

  .fr-m-track { display: block; position: absolute; inset: 0; }

  /* Default opacity 1, deliberately: every cell's opacity is written on the frame it is created,
     and if that ever failed the failure should be a visible extra garment, not an empty stage. */
  .fr-m-cell {
    display: block;
    position: absolute;
    inset: 0;
    opacity: 1;
    /* Display only. The stage owns every pointer on this surface. */
    pointer-events: none;
    will-change: transform, opacity;
    transition: opacity .2s ease;
  }

  /* While a gesture owns the surface every frame sets opacity itself; a transition there would
     lag the finger by a fifth of a second. The room wears the flag while anything is in flight. */
  .fr-browse.is-moving .fr-m-cell,
  .fr-browse.is-drag .fr-m-cell,
  .fr-m-track.is-live .fr-m-cell,
  .fr-model.is-live .fr-m-cell { transition: none; }

  .fr-m-cell-box {
    display: grid;
    position: absolute;
    inset: 0;
    /* minmax(0,1fr) rather than an implicit auto track: an auto row sizes itself to the image, and
       a percentage height then measures itself against that row, so the hero would never shrink. */
    grid-template-rows: minmax(0, 1fr);
    grid-template-columns: minmax(0, 1fr);
    place-items: center;
  }

  /* Transparent cut-outs: a contour shadow that follows the alpha, never a card. */
  .fr-m-hero {
    display: block;
    height: 100%;
    width: auto;
    max-width: none;
    filter: drop-shadow(0 20px 38px rgba(0, 0, 0, .72));
    pointer-events: none;
  }

  /* ── The camera carousel at mobile scale. The desktop room fans every camera across the stage,
        each smaller and fainter with distance; that is too much for 390pt, but the *idea* — that
        there is more of this garment just out of frame — is worth keeping, so the phone shows the
        centre and one shoulder each side and animates through them the same way. The shots are
        the cell's own .fr-cv buttons, restyled: the carousel's cqh geometry is the desktop's
        answer to the same question and every part of it has to go.

        Excluding .is-unrendered is load-bearing. A cell with no render carries exactly one .fr-cv
        — the blank garment plate — and the room already has a composition for it that works at
        any width: the plate centred in `inset: 3% 0` with the pattern as its own background. It
        is not a camera and must not be laid out as one, or it takes this rule's
        translate(-50%,-50%) on top of the plate rule's own positioning and lands half off the
        left edge. ── */
  .fr-m-camstrip { display: block; position: absolute; inset: 0; pointer-events: none; }

  .fr-m-shot,
  .fr-model:not(.is-unrendered) .fr-cv,
  .fr-model:not(.is-unrendered) .fr-cv:only-of-type,
  .fr-model:not(.is-unrendered) .fr-cv:first-child:last-child {
    display: block;
    position: absolute;
    left: 50%;
    top: 50%;
    bottom: auto;
    height: 100%;
    width: auto;
    max-width: none;
    transform-origin: 50% 50%;
    filter: drop-shadow(0 20px 38px rgba(0, 0, 0, .72));
    will-change: transform, opacity;
    /* The stage owns every pointer here; a shot that could be hit would also swallow the drag
       that starts on it, because a gesture beginning on a control is not the stage's. */
    pointer-events: none;
  }

  /* The contour shadow moves to the button, which is what actually carries the cut-out's alpha
     here; left on both it is drawn twice. The legacy rectangles keep their corners. */
  .fr-model .fr-cv img { filter: none; }

  /* The desktop carousel's own transition, off — this strip moves on transform, not on left. */
  .fr-model.fr-live .fr-cv { transition: none; }

  .fr-m-camstrip.anim .fr-m-shot,
  .fr-model.anim .fr-cv {
    transition: transform .42s var(--fr-m-ease), opacity .42s var(--fr-m-ease);
  }

  /* D59 — no render exists for this pairing. Filling the cell rather than hugging its content is
     what gives the art tile's max-height a percentage to resolve against, so the tile shrinks with
     the stage instead of pushing its own caption off the bottom of it at the full detent. */
  .fr-tile-only {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    inset: 0;
    height: 100%;
    min-height: 0;
    max-width: 100%;
    max-height: 100%;
  }

  .fr-tile-only img {
    width: min(52vw, 208px);
    /* The desktop cap is 62cqh, which on a phone-height stage bites into the tile before the
       width does and leaves it smaller than the note underneath it. */
    max-width: 100%;
    max-height: 58%;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: var(--mp-r-card);
    box-shadow: 0 22px 58px rgba(0, 0, 0, .66);
    opacity: .84;
  }

  /* The line that says so in words, rather than letting a tile pass for a mockup. */
  .fr-plain-note {
    bottom: 6%;
    max-width: 250px;
    white-space: normal;
    text-align: center;
    letter-spacing: .13em;
    font-size: 9px;
    line-height: 1.7;
    background: rgba(8, 8, 10, .74);
    padding: 7px 12px;
  }

  /* ── Camera views: the dot row, and nothing else. The image itself is the control — a tap on
        the right two thirds advances, a tap on the left third goes back. The row belongs to the
        stage's foot; it lives in .fr-m rather than inside the cell, so it is placed against the
        corner instead of against the stage's own box. ── */
  .fr-m-cams {
    display: flex;
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(var(--fr-m-corner) + 6px);
    padding: 5px 0;
    text-shadow: 0 1px 6px #000;
    /* The prototype's dots are inside the stage, where 25 buys them the top of the cells. Here
       they are a sibling of it, so that number would compete with the panel's instead and the
       readout would float over the copy at the full detent. The cells are already beneath them —
       the stage isolates its own stacking context — so what is left to say is only "above the
       rails, under the panel". */
    z-index: 7;
    justify-content: center;
    align-items: center;
    gap: 5px;
    pointer-events: none;
    transition: opacity .18s ease;
  }

  .fr-m-cams[hidden] { display: none; }

  .fr-browse.is-drag .fr-m-cams { opacity: 0; }

  .fr-m-camdot {
    display: block;
    width: 4px;
    height: 4px;
    border-radius: var(--mp-r-pill);
    background: rgba(255, 255, 255, .26);
    box-shadow: 0 1px 4px rgba(0, 0, 0, .8);
    transition: width .2s, background .2s;
  }

  .fr-m-camdot.on {
    width: 13px;
    background: var(--mp-pink);
  }

  /* ══ The L ══════════════════════════════════════════════════════════════════
     Two rails, one axis each, sized as instruments rather than as galleries: 46px of width on the
     left, 48px of height below, 32px thumbnails inside them. Everything about them is quiet —
     42% opacity off the mark, and a single ring in the axis's colour where the seated cell sits. */

  .fr-m-grail,
  .fr-m-prail {
    display: block;
    position: absolute;
    z-index: 6;
    overflow: hidden;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    cursor: grab;
  }

  .fr-m-grail.is-drag,
  .fr-m-prail.is-drag { cursor: grabbing; }

  .fr-m-grail {
    left: 0;
    width: var(--fr-m-grail-w);
    /* Capped to exactly the garments it holds and centred on the stage, rather than stretched down
       the whole edge — a rail longer than its axis has to either repeat itself or space itself
       out, and both read as a fault. */
    height: min(var(--fr-m-grail-h, 100%), calc(100% - var(--fr-m-corner)));
    top: calc((100% - var(--fr-m-corner)
      - min(var(--fr-m-grail-h, 100%), calc(100% - var(--fr-m-corner)))) / 2);
    border-radius: 0 14px 14px 0;
    background: rgba(0, 0, 0, .55);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 12%, #000 88%, transparent 100%);
    mask-image: linear-gradient(180deg, transparent 0, #000 12%, #000 88%, transparent 100%);
    /* The rail's opacity IS the panel's position, not a state it switches between: the module
       writes --fr-m-fade from the live panel height on every frame of a drag, so the rail
       dissolves under your thumb and comes back as you let the panel down — and because the
       property is registered, the same value transitions when the panel snaps. */
    opacity: var(--fr-m-fade, 1);
  }

  .fr-m-prail {
    left: 0;
    right: 0;
    bottom: var(--fr-m-fold);
    height: var(--fr-m-prail-h);
    background: rgba(0, 0, 0, .55);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 9%, #000 91%, transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0, #000 9%, #000 91%, transparent 100%);
  }

  .fr-browse.is-full .fr-m-grail { pointer-events: none; }

  .fr-browse.is-anim {
    transition: --fr-m-fade .32s var(--fr-m-ease), --fr-m-shift .32s var(--fr-m-ease);
  }

  /* Index space: a cell's left/top is its absolute index times the pitch, and the track is
     translated by −pos·pitch. There is no seam at the wrap because there is no wrap in here. */
  .fr-m-gtrack {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 0;
    will-change: transform;
  }

  .fr-m-ptrack {
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 0;
    will-change: transform;
  }

  .fr-m-gcell,
  .fr-m-pcell {
    display: grid;
    position: absolute;
    place-items: center;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  /* The rail may narrow on a 320pt phone; the thing you put a thumb on may not. */
  .fr-m-gcell {
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--fr-m-grail-w);
    min-width: 44px;
    height: 44px;
  }

  .fr-m-pcell {
    top: 50%;
    transform: translate(-50%, -50%);
    width: 46px;
    height: var(--fr-m-prail-h);
  }

  .fr-m-th {
    display: grid;
    width: 32px;
    height: 32px;
    border-radius: 9px;
    background-color: var(--mp-raise);
    background-size: cover;
    background-position: center;
    box-shadow: inset 0 0 0 1px var(--mp-line-soft);
    overflow: hidden;
    place-items: center;
    opacity: .42;
    transition: opacity .2s var(--fr-m-ease);
  }

  .fr-m-gcell.on .fr-m-th,
  .fr-m-pcell.on .fr-m-th { opacity: 1; }

  /* Garment tiles are the pattern worn — cut-outs, so contained rather than covered. */
  .fr-m-th img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }

  /* An unrendered pairing keeps its rung in the ladder rather than being silently skipped. */
  .fr-m-th.is-bare {
    background-color: transparent;
    box-shadow: none;
    border: 1px dashed var(--mp-line-soft);
  }

  .fr-m-th.is-bare img {
    width: 74%;
    height: 74%;
    object-fit: cover;
    border-radius: 6px;
    opacity: .5;
  }

  /* The stationary mark each rail is scrubbed against — chrome, in the axis's colour. */
  .fr-m-gmark,
  .fr-m-pmark {
    display: block;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 40px;
    height: 40px;
    border-radius: 12px;
    pointer-events: none;
    z-index: 3;
  }

  .fr-m-gmark {
    border: 1.5px solid var(--fr-m-ax-g);
    box-shadow: 0 0 14px rgba(255, 201, 160, .28);
  }

  .fr-m-pmark {
    border: 1.5px solid var(--fr-m-ax-p);
    box-shadow: 0 0 14px rgba(251, 94, 190, .3);
  }

  /* ── The transient name. Shown only while a finger is actually moving an axis, and gone the
        moment it lifts: the name is wanted during the scrub, when the thumbnails are moving too
        fast to read, and not afterwards, when the title underneath already says it. ── */
  .fr-m-tag {
    display: block;
    position: absolute;
    /* 28 in the prototype, for the same reason the dot row is 25 and by the same translation: the
       tag belongs to the artwork, and a tag still fading out when the panel is pulled up must go
       under it rather than across the copy. */
    z-index: 9;
    max-width: 62vw;
    padding: 6px 10px;
    border-radius: var(--mp-r-pill);
    background: rgba(8, 8, 10, .82);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    font-family: var(--mp-mono);
    text-transform: uppercase;
    letter-spacing: .13em;
    font-size: 9.5px;
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    pointer-events: none;
    opacity: 0;
    /* Both directions are stated outright rather than one inheriting the other's timing: it
       arrives almost at once, because a readout that lags the gesture is no readout, and it leaves
       over a slow second with a beat of delay, so letting go reads as the label being done with
       rather than as it being switched off. */
    transition: opacity .9s ease .12s;
  }

  .fr-m-tag.on { opacity: 1; transition: opacity .12s ease 0s; }

  /* Beside the garment mark, just clear of the rail; beside the pattern mark, just above it. Each
     mark sits at the centre of its own rail, so the tag is placed against the same span. */
  .fr-m-tag-g {
    left: calc(var(--fr-m-grail-w) + 8px);
    top: calc((100% - var(--fr-m-corner)) / 2);
    transform: translateY(-50%);
    color: var(--fr-m-ax-g);
    border: 1px solid rgba(255, 201, 160, .3);
  }

  .fr-m-tag-p {
    left: calc(var(--fr-m-grail-w) + (100% - var(--fr-m-grail-w)) / 2);
    transform: translateX(-50%);
    bottom: calc(var(--fr-m-corner) + 8px);
    color: var(--fr-m-ax-p);
    border: 1px solid rgba(251, 94, 190, .3);
  }

  /* ══ The panel — the right rail, on its own lift ══
     Two states, and the difference between them is only how far it has been pulled up. Nothing
     behind it is re-laid-out: the mockup keeps its size and its place, the pattern rail keeps its
     place and is covered as the panel passes over it, and the garment rail — which the panel never
     reaches, because it runs up the side — fades out instead of being resized. */
  .fr-rail-r {
    position: absolute;
    left: 0;
    right: 0;
    bottom: var(--fr-m-act-h);
    height: var(--fr-m-sheet-h);
    z-index: 10;
    align-items: stretch;
    text-align: left;
    padding: 0;
    margin: 0;
    overflow: hidden;
    border-radius: 20px 20px 0 0;
    border-top: 1px solid rgba(255, 255, 255, .16);
    background:
      linear-gradient(180deg, rgba(14, 14, 17, .8) 0%, rgba(9, 9, 11, .965) 22%, #08080a 60%);
    backdrop-filter: blur(22px) saturate(1.15);
    -webkit-backdrop-filter: blur(22px) saturate(1.15);
    box-shadow: 0 -16px 46px rgba(0, 0, 0, .62);
    transform: translate3d(0, calc(var(--fr-m-sheet-h) - var(--fr-m-vis)), 0);
    will-change: transform;
  }

  .fr-rail { pointer-events: auto; }

  .fr-rail::before,
  .fr-rail::after { display: none; }

  /* A snap glides, a drag does not — the module says which by marking the movement, on the room
     (its own hook) or on the panel (the prototype's). Either arms the same transition. */
  .fr-browse.is-anim .fr-rail-r,
  .fr-rail-r.anim { transition: transform .32s var(--fr-m-ease); }

  .fr-m-head {
    display: flex;
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    z-index: 3;
    flex: none;
    min-height: 34px;
    align-items: center;
    justify-content: center;
    touch-action: none;
    cursor: grab;
  }

  .fr-m-head i {
    display: block;
    width: 42px;
    height: 4px;
    border-radius: var(--mp-r-pill);
    background: rgba(255, 255, 255, .28);
  }

  /* The whole panel is the grab handle (Roger: "drag anywhere on it"), so the body takes the
     pointer as well — which means the copy's own scrolling is driven by the same gesture rather
     than by the browser. touch-action: none is deliberate: handing vertical panning to the UA and
     then trying to take it back mid-gesture is the one arrangement that cannot be made reliable. */
  .fr-m-body {
    display: block;
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    padding: 34px var(--fr-m-edge) 22px;
    touch-action: none;
  }

  /* Until the module has wrapped the panel's content, the panel carries the padding itself, so a
     resting phone is never flush against its own edges. */
  .fr-rail-r:not(:has(> .fr-m-body)) { padding: 34px var(--fr-m-edge) 22px; }

  /* Scrolling copy passes *behind* the grip bar and dissolves into it. A hard edge under the
     handle wastes a band of panel and reads as a seam; a 26px fade reads as depth, and it tells
     you there is more above without a scrollbar having to. */
  .fr-browse.is-full .fr-m-body {
    -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 26px);
    mask-image: linear-gradient(180deg, transparent 0, #000 26px);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--mp-line-soft) transparent;
  }

  .fr-browse.is-full .fr-rail-r:not(:has(> .fr-m-body)) {
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--mp-line-soft) transparent;
  }

  /* ══ One title, two halves, two underlines ══
     Both halves stretch to the same height so the two rules and the two micro-labels land on the
     same line; the words sit at the top of their box, which keeps the first line reading straight
     across as "Autumn Ginkgo Bomber Jacket". The columns are not split evenly — "Chromatic
     Carnival" next to "Men's Cotton Crew Neck T-Shirt" is 18 characters against 30 — so the module
     deals them out in proportion to what each name needs and then fits the type to the column it
     got, and the phrase stays on one line until no legible size will do it. */
  .fr-m-title {
    display: grid;
    grid-template-columns: var(--fr-m-col-p, 1fr) var(--fr-m-col-g, 1fr);
    align-items: stretch;
    column-gap: 12px;
    margin: 4px 0 0;
    font-weight: 400;
    font-size: inherit;
  }

  .fr-m-half { display: flex; flex-direction: column; min-width: 0; }

  .fr-m-half-p { --fr-m-ax: var(--fr-m-ax-p); }

  .fr-m-half-g { --fr-m-ax: var(--fr-m-ax-g); }

  .fr-m-word {
    display: block;
    flex: 1;
    min-height: 1.12em;
    font-family: var(--mp-sans);
    font-weight: 700;
    font-size: 19px;
    line-height: 1.12;
    letter-spacing: -.02em;
    color: var(--mp-ink);
    padding-bottom: 7px;
    border-bottom: 2px solid var(--fr-m-ax);
    /* Fitted by the module; wraps only when even the floor size will not do it. */
    white-space: nowrap;
    overflow: hidden;
  }

  .fr-m-word.wrapped { white-space: normal; }

  .fr-m-eye {
    display: inline-flex;
    position: relative;
    align-items: center;
    gap: 6px;
    margin-top: 8px;
    font-family: var(--mp-mono);
    text-transform: uppercase;
    letter-spacing: .14em;
    font-size: 10px;
    line-height: 1;
    color: var(--fr-m-ax);
    white-space: nowrap;
  }

  /* The micro-label is 10px of type because it is a caption; the box you hit is 44. It reaches up
     into the title's own whitespace, so it costs the layout nothing. */
  .fr-m-eye::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: -26px;
    bottom: -8px;
  }

  .fr-m-eye b { font-weight: 700; color: var(--fr-m-ax); }

  .fr-m-eye .arw { letter-spacing: 0; }

  /* ── The heart and the thumbs, lifted out of the clipped left rail and into the panel directly
        under the title (the module does the moving, as it does for .fr-copy). This is the phone's
        "under the pattern name": the row sits below the two words and above the metadata line,
        left-aligned under the pattern half. It costs the resting panel its own height and nothing
        else — measureRest() measures to the fold, so the panel simply rests that much taller.
        14px of clearance, because .fr-m-eye's hit box reaches 8px below the counter it labels. ── */
  .fr-acts { margin: 14px 0 0; gap: 7px; }

  .fr-acts .hbtn.fav,
  .fr-acts .rate:not(.rate-compact) { height: 34px; }

  /* The resting panel's one metadata line: the fact on the left, the way in on the right. Both are
     10px mono, so the row costs a single line where the ownership sentence used to wrap to two and
     push Add to Cart down with it. */
  .fr-m-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-top: 9px;
  }

  .fr-trust {
    margin: 9px 0 0;
    max-width: none;
    min-width: 0;
    letter-spacing: .12em;
    font-size: 9.5px;
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .fr-m-meta .fr-trust { margin-top: 0; }

  /* The way in, without knowing the handle is draggable: a plain visible control in the resting
     state that opens the panel. It sits on the resting side of the fold, which is the point. */
  .fr-m-details {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: none;
    /* The hit box reaches above and below the row rather than around it, so it clears 44px without
       the row itself costing 44px of panel. */
    padding: 15px 0;
    margin: -15px 0;
    background: none;
    border: 0;
    font-family: var(--mp-mono);
    text-transform: uppercase;
    letter-spacing: .13em;
    font-size: 10px;
    line-height: 1;
    color: var(--mp-pink);
    cursor: pointer;
    touch-action: manipulation;
  }

  .fr-m-details::after {
    content: "\25BE";
    display: inline-block;
    transition: transform .22s var(--fr-m-ease);
  }

  .fr-browse.is-full .fr-m-details::after { transform: rotate(180deg); }

  /* ══ Below the resting fold ══ */
  .fr-m-fold {
    display: block;
    margin-top: 18px;
    padding-top: 15px;
    border-top: 1px solid var(--mp-line);
  }

  .fr-own {
    margin: 0 0 14px;
    max-width: none;
    letter-spacing: .12em;
    font-size: 9.5px;
    line-height: 1.7;
  }

  .fr-own b { color: var(--mp-dim); font-weight: 400; }

  /* No disclosure. Once the panel is open the reader has already asked for the detail, and making
     them ask a second time inside it was the round-one mistake — the copy is simply all there. The
     module opens the <details>; this is the belt to that brace on engines that expose the pseudo,
     and the summary stops taking pointers so a tap on the blurb is the panel's, not the UA's. */
  .fr-copy {
    display: block;
    margin: 0;
    max-width: none;
  }

  .fr-copy::details-content { content-visibility: visible; block-size: auto; }

  .fr-copy > summary { pointer-events: none; }

  .fr-copy-more { display: none; }

  /* The resting panel's cut line sits above .fr-m-fold (measureRest() in the phone module reads
     the fold's own top), so the blurb is never seen on its own — it only ever appears stacked
     directly on top of .fr-copy-body, which restates the same sentence at length. One description,
     not two: the expanded panel shows the long body; the short teaser that exists to justify a
     collapsed <details> on desktop has nothing to do on a panel that is never collapsed. */
  .fr-copy-blurb { display: none; }

  /* The panel scrolls as one; an inner scroll box would be two gestures fighting over one finger. */
  .fr-copy-panel {
    max-height: none;
    overflow: visible;
    padding-right: 0;
  }

  .fr-copy-body { margin: 12px 0 0; font-size: 13px; line-height: 1.7; }

  .fr-copy-specs { margin: 12px 0 0; gap: 5px; }

  .fr-copy-specs li { padding-left: 13px; }

  .fr-copy-specs li::before { width: 5px; }

  .fr-m-hint {
    display: block;
    margin: 22px 0 0;
    font-family: var(--mp-mono);
    text-transform: uppercase;
    letter-spacing: .16em;
    font-size: 9px;
    line-height: 1.9;
    color: #55555b;
  }

  /* ══ Commerce — the pinned row ══
     Pinned to the room's foot, above the prompt dock: price, the size chip that opens the ladder,
     and Add to Cart. It cannot live inside the panel — a transformed element is the containing
     block for its own fixed descendants — so the module moves the three of them here. */
  .fr-m-actbar {
    display: flex;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 20;
    align-items: center;
    gap: 8px;
    height: var(--fr-m-act-h);
    padding: 0 var(--fr-m-edge) env(safe-area-inset-bottom, 0px);
    background: rgba(10, 10, 15, .92);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-top: 1px solid var(--mp-line);
  }

  .fr-price {
    margin-top: 0;
    font-size: 20px;
    flex: none;
  }

  .fr-m-sizebtn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    height: 44px;
    flex: none;
    padding: 0 12px;
    border-radius: var(--mp-r-pill);
    background: var(--mp-raise);
    border: 1px solid var(--mp-line-soft);
    color: var(--mp-ink);
    font-family: var(--mp-mono);
    text-transform: uppercase;
    letter-spacing: .1em;
    font-size: 10px;
    cursor: pointer;
    touch-action: manipulation;
  }

  .fr-m-sizebtn .k { color: var(--mp-mute); }

  .fr-m-sizebtn .v { font-weight: 700; font-size: 12px; letter-spacing: .04em; }

  .fr-m-sizebtn .cv { color: var(--mp-mute); font-size: 9px; }

  /* One size is a fact, not a decision. */
  .fr-m-sizebtn[data-single] { cursor: default; }

  .fr-add.hbtn.primary {
    flex: 1;
    min-width: 0;
    width: auto;
    height: 44px;
    margin-top: 0;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--mp-r-pill);
    font-family: var(--mp-mono);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .1em;
    font-size: 11px;
    touch-action: manipulation;
    transition: background .15s;
  }

  /* ══ The size ladder, as a sheet from the foot ══
     The ladder is the room's own .fr-sizes, relocated: four-up, big pills, with a DONE affordance.
     Parked in the panel it would be a row of pills nobody asked for, so it shows only once the
     module has moved it into the sheet. */
  /* Open is the state this rule draws; closed is the `hidden` attribute the module writes when the
     ladder is shut, which the guard at the top of this block is what makes stick. Keyed on the
     attribute rather than on the room's is-sizes flag: the attribute is the one the module has
     always written, it is what the sheet is born with, and it cannot disagree with itself. */
  .fr-m-msheet {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 40;
    flex-direction: column;
    justify-content: flex-end;
  }

  .fr-m-scrim {
    display: block;
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, .6);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
  }

  .fr-m-panel {
    display: block;
    position: relative;
    background: var(--mp-surface);
    border-top: 1px solid var(--mp-line-soft);
    border-radius: var(--mp-r-card) var(--mp-r-card) 0 0;
    padding: 14px var(--fr-m-edge) calc(18px + env(safe-area-inset-bottom, 0px));
    box-shadow: 0 -18px 50px rgba(0, 0, 0, .6);
    animation: fr-m-rise .26s var(--fr-m-ease);
  }

  .fr-m-shead {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-family: var(--mp-mono);
    text-transform: uppercase;
    letter-spacing: .16em;
    font-size: 10px;
    color: var(--mp-mute);
    margin-bottom: 12px;
  }

  .fr-m-shead button {
    background: none;
    border: 0;
    color: var(--mp-pink);
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    padding: 15px 12px;
    margin: -15px -12px;
    cursor: pointer;
  }

  .fr-rail-r .fr-sizes { display: none; }

  .fr-m-msheet .fr-sizes {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
    margin-top: 0;
  }

  .fr-size {
    min-width: 0;
    height: 46px;
    border-radius: 12px;
    font-size: 12px;
    padding: 0 4px;
  }

  .fr-size.wide { grid-column: span 2; }

  /* ══ What the phone does not show ══
     Hidden, never deleted — and the two headings are clipped rather than removed, because they are
     the document's real <h1>/<h2> and a phone reader still has to hear them. The visible title is
     .fr-m-title, which is aria-hidden precisely so these two stay the headings. */
  .fr-rail-l,
  .fr-rail-r .fr-name {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* The four axis steppers are the two rails now, and an invisible button is worse than no
     button: these go rather than being clipped with the rail around them. */
  .fr-rail .fr-step,
  .fr-rail-r .fr-eyebrow,
  .fr-peek { display: none; }

  /* No prompt dock on a phone (Roger, 2026-08-01). design.md makes the prompt bar a fixture of
     every browse screen, and above this breakpoint it still is — but 86px of permanent chrome is
     a different proposition on a 844pt screen than on a desktop, and "Make Your Own" is one tap
     away in the menu. Removing it is also what lets the action bar sit on the foot of the room,
     which is where the approved design puts it. */
  .fr-dock { display: none; }

  /* ══ Narrow and short phones. The rail loses four pixels of width before the garment loses
        anything else, because the garment is the subject. ══ */

  @media (max-width: 374px) {
    .fr-browse { --fr-m-grail-w: 42px; --fr-m-prail-h: 46px; }

    .fr-m-word { font-size: 17px; }

    .fr-price { font-size: 18px; }
  }

  @media (max-height: 700px) {
    .fr-m-word { font-size: 17.5px; }

    .fr-own { margin-top: 10px; }

    .fr-m-fold { margin-top: 14px; padding-top: 12px; }
  }

  @media (prefers-reduced-motion: reduce) {
    .fr-rail-r,
    .fr-rail-r.anim,
    .fr-m-cell,
    .fr-m-track.is-live .fr-m-cell,
    .fr-model.is-live .fr-m-cell,
    .fr-glow,
    .fr-m-th,
    .fr-m-word,
    .fr-m-cams,
    .fr-m-camdot,
    .fr-m-tag,
    .fr-m-grail,
    .fr-m-details::after,
    .fr-m-camstrip.anim .fr-m-shot,
    .fr-model.anim .fr-cv,
    .fr-add.hbtn.primary { transition: none !important; }

    .fr-m-panel { animation: none; }
  }
}

/* The size sheet's arrival. Keyframes are legal inside a media query, but this one is named for
   the phone and read only there, so it sits with the rest of the file's animations instead. */
@keyframes fr-m-rise { from { transform: translateY(24px); opacity: .4; } }
