/* ==========================================================================
   FASHION — page styles (de-inlined from fashion.html)

   LOAD ORDER IS A CONTRACT. This file must be linked AFTER
   /common/css/design-system.css: both declare --muted and --line on :root at equal
   specificity, so source order alone decides the winner, and this page needs
   its opaque greys (#767676 / #e2e2e2) rather than the studio's translucent
   whites. Linking it first turns every border and grey label on the page to
   near-invisible white.

   DO NOT SPLIT THIS FILE. 16 selectors are declared twice and rely on source
   order: the header base rules vs their max-width:620px overrides, the AI
   panel base rules vs their max-width:480px overrides, the studio picker vs
   its own 620px override, and `.dw-list a` vs `.dw-nav a` (equal specificity,
   both set font-size).

   CONTRACTS WITH JS (custom properties written from script):
     --cbar-h         the height of the band over the stage. Declared on
                      .hero__stage and read by everything that has to stop short
                      of it — see the note there.
     --sticky-bar-h   written by fashion/js/ui/page-bars.js, read only by the
                      body:has(.sticky-bar.show) rule that lifts the AI launcher
                      off the buy bar. Fallback 106px (the taller, wrapped case,
                      so a failure over-clears rather than collides).
     --is-topbar-h    comes from design-system.css — the studio frame's height,
                      which is also the room it takes (46px; it stopped folding,
                      common/docs/025). Always read with a 0px fallback so this
                      page still stands alone without that stylesheet. .hdr is
                      sticky at this offset; what makes it clear the bar BEFORE
                      the page scrolls is the body padding design-system.css
                      applies to a page carrying the frame.

   Selectors from ~line 340 onward style DOM this page never contains — the AI
   launcher and panel are built at runtime by fashion/js/ai/ai-consultant-panel.js.
   ========================================================================== */

:root {
  --ink:      #1a1a1a;
  --muted:    #767676;
  --line:     #e2e2e2;
  --grey-bg:  #eaeaea;
  --fill:     #f0ede7;
  --fill-2:   #e6e0d6;
  --serif:    'EB Garamond', 'Times New Roman', serif;
  --sans:     "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
  --hdr-h:    62px;

  /* The studio's two secondary-text tokens, re-pointed for a white boutique.
     configurator.css declares them as translucent WHITE — right on the studio's
     dark glass, invisible here, and this page renders that stylesheet's picker,
     its failure card and its dialogs on paper. Three things had already
     disappeared into the background: a product card's summary and its axis
     line, and the detail line under every saved configuration.

     Overridden as TOKENS rather than rule by rule, which is how they came to be
     missed: the .dialog block below restates about forty shared rules, and each
     one that reaches for a colour it does not restate inherits a colour meant
     for the dark theme. Two lines here cover every such rule, including the
     ones this page has yet to show. */
  --bo-dim:   #767676;
  --bo-faint: #9a9a9a;
}
* , *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: #fff; color: var(--ink);
  font-family: var(--sans); font-size: 15px; line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; }
button { font-family: inherit; cursor: pointer; color: inherit; }
h1,h2,h3,h4,p { margin: 0; }

/* ── Header ── */
.hdr {
  position: sticky; top: var(--is-topbar-h, 0px); z-index: 900; background: var(--grey-bg);
  transition: background 0.25s, box-shadow 0.25s;
}
.hdr.scrolled { background: #fff; box-shadow: 0 1px 0 var(--line); }
.hdr__row {
  height: var(--hdr-h); display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; padding: 0 clamp(1rem, 2.5vw, 2.2rem);
}
.hdr__left  { display: flex; align-items: center; gap: 1.6rem; padding-left: 185px; }
.hdr__right { display: flex; align-items: center; justify-content: flex-end; gap: 1.4rem; }
@media (max-width: 900px) { .hdr__left { padding-left: 0; } }
.lnk { display: inline-flex; align-items: center; gap: 0.45rem; background: none; border: 0;
  font-size: 0.72rem; letter-spacing: 0.06em; }
.lnk svg { width: 21px; height: 21px; }
.wordmark {
  grid-column: 2; text-align: center; font-family: var(--serif); font-weight: 500;
  font-size: 1.7rem; letter-spacing: 0.34em; padding-left: 0.34em; text-transform: uppercase;
}
.bag-wrap { position: relative; }
.bag-count { position: absolute; top: -7px; right: -9px; background: var(--ink); color: #fff;
  font-size: 0.56rem; min-width: 15px; height: 15px; border-radius: 100px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center; }
/* Phones: the wordmark and the text labels cannot share the row — at 375px two
   of them already laid out to 528px, scrolling the document sideways and
   pushing search and menu off-screen entirely, and Catalog since made a
   third. Collapse all three to their icons (each carries an aria-label) and
   step the wordmark down. The side
   columns go auto so the row is sized by content rather than two equal 1frs.
   Must stay BELOW the base header rules — same specificity, so source order
   is what makes it win. */
@media (max-width: 620px) {
  .hdr__row { grid-template-columns: auto 1fr auto; padding: 0 0.8rem; }
  .wordmark { font-size: 1.05rem; letter-spacing: 0.16em; padding-left: 0.16em; }
  .hdr__right { gap: 0.85rem; }
  .lnk svg { width: 19px; height: 19px; }
  #catalogBack, #contactBtn, #menuBtn { font-size: 0; gap: 0; }
  /* 44px tap targets. Once the labels collapse, each button is only its 19px icon
     plus the UA's default button padding (1px 6px) — 31x21, under half the 44px
     guideline, on the five controls a thumb reaches for most.
     Grown with a centred pseudo-element rather than padding on purpose: padding
     would widen the two side columns of the `auto 1fr auto` row above, squeezing
     the centre column the wordmark sits in (only ~112px of slack at 375px) and
     shifting it off-centre. This changes no layout at all — the icons, the gaps
     and the row stay exactly as they are, and only the hit area grows, outward
     from each icon's centre.
     44px targets on a 44.6px pitch (31px button + 0.85rem gap), so adjacent
     areas stop just short of overlapping — do not raise this without also
     raising the gap, or a tap near an edge lands on the neighbour. */
  .lnk { position: relative; }
  .lnk::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
  }
}

/* ── Hero — 3D viewer mounts here ── */
/* A sweep, and deliberately WITHOUT a horizon.
   A drawn wall-to-floor seam was tried and rejected, for a reason worth keeping:
   THIS BACKDROP IS PAINTED IN CSS AND THE CAMERA IS NOT. The hero ships five
   camera presets, free orbit, and a voice agent that can move the camera on
   request — at the `top` preset the bag ends up above a drawn seam with its
   contact shadow hanging in mid-air on the wall. A gradient with no defined edge
   claims no geometry, so no camera angle can contradict it. Two more reasons it
   suits a shop specifically: a seam puts a hard horizontal line across the frame
   at the product's widest point, and 14 colourways all have to read true against
   whatever is behind them.
   So: two layers, painted back to front in the order written.
     1. a pool of light behind where the bag stands, so it is lit by its own
        backdrop rather than standing against a flat panel;
     2. the field itself, warming as it falls toward --fill-2 — the beige the
        gallery cards and the related strip already use. It warms without ever
        turning a corner. A neutral grey under a warm product reads as a cut-out.
   It starts ON --grey-bg because the sticky header IS --grey-bg and sits directly
   on this box; any other value at 0% draws a line under the header.
   The other half of the depth is in the render: the viewer casts a real contact
   shadow onto an invisible plane at the bag's base (groundShadow), so the shadow
   turns with the product instead of sitting under it as a fixed oval. Neither
   half works alone — a shadow on a flat field still reads as a sticker, and an
   empty gradient reads as wallpaper. */
.hero {
  position: relative;
  /* The wordmark below is nowrap and, on a narrow phone, wider than the page.
     Without this it pushes the document sideways and the whole page scrolls
     horizontally; clipped, it simply runs off both edges, which is how an
     oversized backdrop word is meant to sit anyway. */
  overflow: hidden;
  background:
    radial-gradient(46% 42% at 50% 40%, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0) 74%),
    linear-gradient(180deg, var(--grey-bg) 0%, #e9e7e3 55%, #e0dbd3 100%),
    var(--grey-bg);
}
/* The house name, set enormous and almost invisible on the wall behind the
   product. Unlike the horizon that was tried here, a word CAN live on a painted
   backdrop: it is a flat graphic on a flat wall, so no camera angle contradicts
   it — turning the bag does not oblige the lettering to move.
   ::before on .hero, so it paints under everything in .hero__stage: the bag, the
   thumbnails, the settings panel and the AI invitation all sit in front of it.
   4% ink. Enough to read as a shape from across a room, never enough to compete
   with the product or with the title below it — on a shop the product is the only
   thing allowed a hard edge. `clamp` ties it to the viewport rather than to the
   stage, so it scales with the page instead of being cropped on a phone. */
.hero::before {
  content: 'AVELINE';
  position: absolute; inset: 0; z-index: 0;
  display: flex; align-items: center; justify-content: center;
  /* Measured, not guessed: at 19vw the word renders about 4.8x the font size, so
     it lands at ~92% of the viewport — edge to edge, but contained, from 360px
     up. Below that the 4.5rem floor takes over and it genuinely crops (345px of
     word in a 320px screen), which is what .hero's overflow:hidden is there for:
     without it the whole document scrolls sideways on the smallest phones.
     The size and the ink are one decision, arrived at by trying the range: 4.5%
     was too loud, 1.8% too faint to be worth having, and a contained 12vw at the
     heavier weight lost the scale that makes the mark work. 4% at near-full
     width is where it landed. */
  font-family: var(--serif); font-size: clamp(4.5rem, 19vw, 17rem);
  letter-spacing: 0.14em; text-indent: 0.14em;   /* balance the trailing track */
  line-height: 1; white-space: nowrap;
  color: rgba(26,26,26,0.04);
  pointer-events: none; user-select: none;
}
/* --cbar-h is the band's share of the stage, and the stage does NOT grow by it:
   the render box shrinks by it from the bottom instead (see #fashion-viewer below).
   So the band gets its own strip of the grey field rather than sitting on the
   product, and the stage stays the height it has always been.
   It was tried the other way — stage height + --cbar-h, to keep the bag the size
   it was before the band existed — and the stage read as too tall on a laptop:
   the bag pushed the title and price below the fold and the band landed in the
   corner where the AI launcher floats. The bag being ~11% smaller is the cheaper
   side of that trade.
   Growing the stage would not have bought clearance on its own, either.
   EmbeddedModelViewer frames the camera off its box's height (dist = radius /
   sin(fov/2) * 1.25), so a taller box renders a PROPORTIONALLY bigger bag — it
   would have grown straight back into the band. Shrinking the render box is the
   only thing here that actually reserves the strip.
   Keep the two in step: every breakpoint that changes the band's height has to
   set --cbar-h with it, or the band starts overlapping the bag again. */
.hero__stage { position: relative; --cbar-h: 86px;
  height: min(78vh, 780px); }
/* Phones: two limits, both taken from the device.
   58svh — 78vh of a tall phone is nearly the whole screen, leaving nothing below
   the bag visible without scrolling; 58% keeps the title and price (and from
   ~700px tall, the swatch row) on screen at the first paint. svh not vh, and both
   declared: vh measures the viewport with the browser toolbar HIDDEN, so 58vh is
   taller than 58% of what you can actually see. Not dvh — that tracks the toolbar
   collapsing as you scroll, which would resize the WebGL canvas mid-scroll. The vh
   line is the fallback where svh is unsupported.
   110vw — what stops the bag being CUT OFF SIDEWAYS. EmbeddedModelViewer frames the
   camera off the vertical fov alone (dist = radius / sin(fov/2) * 1.25, no aspect
   term), so the horizontal field is only `aspect` times the vertical one, and the
   bag's silhouette width in PIXELS scales with the stage's height while ignoring
   its width. Measured on this model: front-on (which the auto-rotation passes
   through twice a turn) is 0.72 * height, and the widest of the five camera presets,
   `bottom` at el -55, is 0.89 — that one clipped even on a 375px phone, and an
   uncapped 58svh clipped the front view on a 344x882 Galaxy Z Fold. 1.1x the
   viewport width clears every azimuth and all five presets. It is sized off the
   front view, not `bottom`: at 1.1 the front view keeps 35px of air either side
   while `bottom` sits ~3px off the edge — flush, but never cut. Sizing for `bottom`
   to have the same air would need ~100vw and cost the default view another 7%,
   which is a poor trade for one auxiliary pose that is meant to fill the frame.
   Retuning the `bottom` elevation does not help either: every angle from -55 to
   -30 spans the full width, because it is the base and the handle arc that spread,
   not the tilt.
   No px constant in either limit: the bag should be the same fraction of every
   screen. This is also the ONLY lever for its on-screen size here — the framing
   padding above is hardcoded in EmbeddedModelViewer, which the splat pages share,
   and its fov option is cancelled out by the distance derived from it. */
/* Phones move the band OUT from over the bag: it drops below the stage, onto the
   hero's own grey field, with the preview strip back in it (see the 620px block
   further down). Three numbers change together here and must stay in step.
     --cbar-h: 0    nothing is overlaying the render box any more, so it reserves
                    nothing — the bag gets the whole stage back.
     46svh / 92vw   the stage itself, cut from 58/110 to pay for the band below.
                    Kept in the SAME PROPORTION as the pair above (46/92 is
                    58/110 scaled by 0.79) so the sideways-clipping guard still
                    holds: the widest camera preset spans 0.89 * height, which at
                    92vw is 82% of the screen — the same air it had at 110.
                    The bag ends up about the size it was WITH the band on top of
                    it, which is the point: nothing is lost, it just moved.
     --cbar-below   the room made for it, on .hero rather than the stage, because
                    .hero is what clips (overflow: hidden) and what the band's
                    top:100% hangs into.
   Net cost is ~26px of page: the old band ate 84px of the stage from the inside,
   the new one is 124px below it, and the stage gave up 98. The title and price
   are still on screen at first paint, which is what the 58svh above was for. */
@media (max-width: 620px) {
  /* 128, against a band that measures 125: the extra is slack for font rounding,
     since the switcher's line box is what sets the top row's height. Too little
     and the pill's last pixels get clipped by .hero's overflow: hidden. */
  .hero { padding-bottom: var(--cbar-below, 128px); }
  .hero__stage { --cbar-h: 0px;
    height: min(46vh, 92vw); height: min(46svh, 92vw); }
}
/* All three stop short of the band: the WebGL canvas, the photo that replaces it
   and the loading ring. inset's bottom, not padding — these are absolutely
   positioned, and the canvas has to be the size the renderer is told about. */
#fashion-viewer { position: absolute; inset: 0 0 var(--cbar-h); }
/* Centred on the grey field at (or below) native resolution so the editorial
   shots never upscale — capped so they stay crisp instead of grainy. */
#heroPhoto { position: absolute; inset: 0 0 var(--cbar-h); margin: auto; z-index: 2;
  width: auto; height: auto; max-width: min(92%, 480px); max-height: min(92%, 500px);
  object-fit: contain; }
/* The 3D stage's loading state. Fades rather than snaps, as the car's overlay does
   — pointer-events off with it, so the faded ring never sits between the visitor
   and the bag. */
.viewer-ph { position: absolute; inset: 0 0 var(--cbar-h); display: flex;
  align-items: center; justify-content: center; transition: opacity 0.3s ease; }
.viewer-ph.hidden { opacity: 0; pointer-events: none; }
.viewer-ph .ring { width: 60px; height: 60px; border-radius: 50%;
  border: 1px solid #cfc8bb; border-top-color: var(--ink); animation: spin 2.4s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ── The bottom band of the stage: colourway dots + thumbnail strip ──
   Three columns, `1fr auto 1fr`, with the dots in the middle and the strip in the
   last: the two outer columns resolve to the same width, so the dots sit on the
   TRUE centre of the stage however wide the strip gets. That is the whole reason
   the band exists — before it, the dots were centred and the strip was pinned
   18px from the right, and the two overlapped on anything under ~1150px.
   Transparent to the pointer, children opaque to it: the gap between the two
   controls is a third of the stage's bottom edge and has to stay draggable. */
.cbar {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  /* minmax(0, auto), not a bare auto, on the middle track: `auto` floors at the
     row's min-content, which for a row of fixed-size dots is all fourteen of
     them — so the track refused to shrink and the dots spilled out over the
     strip instead of scrolling. The 0 floor hands that job back to the row's own
     max-width + overflow-x. Belt to the braces of the 760px stack below, which
     is what actually keeps this readable at tablet widths. */
  display: grid; grid-template-columns: 1fr minmax(0, auto) 1fr;
  /* Two rows: the actions, then the pill. The strip spans both. */
  grid-template-rows: auto auto; align-items: end;
  gap: 6px 12px; padding: 12px clamp(10px, 1.6vw, 16px);
  pointer-events: none;
}
.cbar > * { pointer-events: auto; }
/* The pill: a switcher line, then whichever panel it names. Two rows and no more
   — an earlier version stacked the colourway name, its dots and the thumbnail
   strip and at 149px tall it covered the bottom third of the bag, which on a
   stage this short is the whole product. So each row is kept near 30px, only one
   panel is ever mounted, and the dots scroll rather than wrap.
   max-width AND min-width, both load-bearing: justify-self:center sizes this box
   to its content, i.e. to all fourteen dots, so without the cap it grew past its
   column and hung off both edges of a phone with the end dots simply unreachable
   rather than scrolled. The cap is what .cbar__row's max-width resolves against.
   Frosted rather than solid: the backdrop behind it is a soft gradient the bag is
   lit by, and a white slab across it reads as a second, closer wall. Radius 20,
   not 999: a pill this tall bows its sides out into a lozenge. */
/* grid-row: 1 on this and on .thumbs, explicitly. The strip sits in column 1 and
   comes SECOND in the markup — the pill is the primary control and reads first —
   and auto-placement will not step backwards into a column it has already passed,
   so it dropped the strip onto a second row and doubled the band's height. */
/* ONE SIZE, whatever is inside it — otherwise the box jumped on every switch, and
   grew wider again the moment a gated tab appeared.
   460 = the widest thing inside, plus the 18px of padding. MEASURE THE CONTENT,
   not the boxes: an earlier constant came from reading the tab row's scrollWidth,
   which for a stretched flex child reports its own width and not its content, so
   the number was circular and 24px too generous. Summing the tabs' own rects
   instead: the row is 309, and the widest panel is the colourway's fourteen dots
   at 442 — so it is the DOTS that set the pill now, not the tabs, which is a
   reversal from when there were six tabs and two of them were long words.
   Re-measure whenever a tab is renamed or a picker gains an entry.
   A PLAIN 460px, not min(460px, 100%). That percentage was circular and quietly
   defeated the whole point: above 1000px the pill sits in an `auto` grid track
   sized FROM ITS OWN CONTENT, so `100%` resolved to the content width and min()
   picked that — the box was 460 on the colour panel and 329 on the other three,
   which is exactly the jumping this constant exists to stop. It only looked fixed
   while the tab row happened to be the widest thing in every panel.
   max-width: 100% still caps it below 1000px, where the track is a 1fr and the
   percentage has a definite size to resolve against. */
.cbar__pick {
  /* Row 2: the build actions share this column and sit on the row above. */
  grid-column: 2; grid-row: 2; justify-self: center;
  width: 240px; min-width: 0; max-width: 100%;
  transition: width 0.25s ease;
  /* align-items:stretch, not center. Width is the CROSS axis in a column, so a
     centred child sizes to its own content and min-width:0 does nothing — the dot
     row stayed 498px wide and painted straight out of the pill instead of
     scrolling inside it. Stretched, each row takes the pill's width (which is
     itself capped at the column, above) and the dot row's overflow-x can do its
     job; the rows centre their own contents instead. At wide widths this changes
     nothing: the pill is already exactly as wide as its widest row. */
  display: flex; flex-direction: column; align-items: stretch; gap: 8px;
  padding: 4px 9px 7px; overflow: hidden;
  background: rgba(255,255,255,0.78); backdrop-filter: blur(8px);
  border: 1px solid rgba(26,26,26,0.07); border-radius: 20px;
}
/* TEMPORARY. Two widths, not one: with the placement tab present the tab row is
   316px of content in a 220px box, so the row and the chips under it both had to
   be scrolled. 340 clears the widest of them; 240 is kept for every state before
   it so the opening pill is unchanged. data-i="3" is the placement panel's index
   in the PANELS table in personalization-ui.js, and the script sets [hidden] on
   the tabs it gates out — so this reacts to the tab appearing with no JS.
   Both numbers are measured off the current labels. Re-measure if a tab is
   renamed; .ptabs/.cbar__row keep their overflow-x as the fallback. */
.cbar__pick:has(.ptab[data-i="3"]:not([hidden])) { width: 340px; }
@media (prefers-reduced-motion: reduce) { .cbar__pick { transition-duration: 0.001ms; } }
/* The tab row: every destination on screen, one click each. It replaced two
   arrows and a caption, which for six panels was a carousel — no sense of place,
   no sense of what else there was, four clicks from the colour to the motif.
   Scrolls rather than wraps. Six words come to about 440px, so on a phone this is
   the one row in the bar that cannot fit; a second line of tabs would put the
   band back on the product, which is the thing the whole band is arranged to
   avoid. The active tab is scrolled into view from the script. */
/* position:relative so the row is its children's offsetParent. The script scrolls
   the active tab into view using offsetLeft, and without this that is measured
   from some ancestor further out — it happened to land near enough to look right,
   which is the worst kind of working. */
/* Centred, now that the pill is a fixed width and a reduced set of tabs would
   otherwise sit off to the left of it. `safe center` and not plain center: this is
   an overflow container, and on a phone the full row is wider than the pill —
   where plain centring can put the first tab out of reach past the start edge.
   Browsers that do not know `safe` keep the `center` above it. */
.ptabs { position: relative; display: flex; align-items: center; gap: 2px;
  justify-content: center; justify-content: safe center;
  max-width: 100%; overflow-x: auto; scrollbar-width: none; }
.ptabs::-webkit-scrollbar { display: none; }
/* The active tab is the only one with ink. Underlined and not filled: a filled
   pill on top of a frosted pill reads as a button inside a button, and this row
   is navigation, not a control. */
.ptab { flex: none; padding: 3px 7px 4px; border: 0; background: transparent;
  border-bottom: 1px solid transparent;
  font-size: 0.58rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: #a9a29a; white-space: nowrap; }
.ptab:hover { color: var(--muted); }
.ptab.is-active { color: var(--ink); border-bottom-color: var(--ink); }
.ptab[hidden] { display: none; }
/* One row per panel, and only the named one is in the flow. The [hidden] rule is
   not redundant: display:flex would override the attribute's own display:none. */
/* min-height so the pill's HEIGHT is as fixed as its width. The rows are not the
   same height by nature — 26px for a colour dot or a chip, 30px for a metal or a
   motif swatch — and left alone the box grew and shrank by 4px on every switch. */
.ppanel { display: flex; align-items: center; justify-content: center; gap: 9px;
  min-height: 30px; max-width: 100%; min-width: 0; }
.ppanel[hidden] { display: none; }
/* Scrolls sideways rather than wrapping: 14 dots outgrow the middle column on a
   laptop, and a second row of dots would climb into the bag. */
.cbar__row { display: flex; align-items: center; gap: 6px;
  min-width: 0; overflow-x: auto; scrollbar-width: none; }
.cbar__row::-webkit-scrollbar { display: none; }

/* ── The initials panel ──
   The pencil is a label, not a button: the field beside it is the control, and
   there is no longer a drawer for it to open. It is there because a bare
   underlined field on a product shot does not say what it is for. */
.ibar__icon { flex: none; width: 22px; height: 22px; color: var(--muted);
  display: grid; place-items: center; }
.ibar__icon svg { width: 14px; height: 14px; }
/* Underline, not a box: a bordered field in here read as a form dropped on the
   product shot. Set in the page's serif and tracked out, because what it holds is
   the engraving itself, not a form value. */
.ibar__input { width: 64px; padding: 2px 0; color: var(--ink);
  border: 0; border-bottom: 1px solid rgba(26,26,26,0.2); background: transparent;
  font-family: var(--serif); font-size: 0.84rem; letter-spacing: 0.16em;
  text-transform: uppercase; text-align: center; }
.ibar__input:focus { outline: none; border-bottom-color: var(--ink); }
.ibar__input::placeholder { color: #bdb6aa; }
/* ── The hand the initials are set in ──
   The chips are the letters "Aa" in the face they offer, not a label: at 20px the
   specimen says serif / sans / roman instantly, in any language, and costs a
   third of the width the words would. */
.ibar__faces { display: flex; align-items: center; gap: 3px; }
.fchip { flex: none; min-width: 26px; height: 26px; padding: 0 5px;
  border: 1px solid transparent; border-radius: 6px; background: transparent;
  color: var(--muted); font-size: 0.92rem; line-height: 1; }
.fchip:hover { color: var(--ink); border-color: rgba(26,26,26,0.18); background: #fff; }
.fchip.is-active { color: var(--ink); border-color: var(--ink); background: #fff; }

/* ── Where it is struck ──
   Words, not swatches: a place on a bag has no colour to stand for it. Its own
   segmented control rather than .stg-seg, which is styled for the opaque settings
   panel and would read as a slab on the frosted pill (and the file header warns
   what happens when two rules fight over one selector). */
.pseg { display: flex; align-items: center; gap: 3px; }
.pchip { flex: none; height: 26px; padding: 0 9px;
  border: 1px solid transparent; border-radius: 999px; background: transparent;
  color: var(--muted); font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase; }
.pchip:hover { color: var(--ink); border-color: rgba(26,26,26,0.18); background: #fff; }
.pchip.is-active { color: #fff; border-color: var(--ink); background: var(--ink); }

/* The active ring is the BORDER over the button's own white, not an outer
   box-shadow: the row is an overflow container, which would clip a shadow on the
   first and last dot. */
.cdot { flex: none; width: 26px; height: 26px; border-radius: 50%; padding: 2px;
  border: 1px solid transparent; background: #fff; }
.cdot i { display: block; width: 100%; height: 100%; border-radius: 50%; }
.cdot:hover { border-color: rgba(26,26,26,0.35); }
.cdot.is-active { border-color: var(--ink); }
/* Metals get a little more room — there are three of them, not fourteen — and an
   inner rim, because a metal painted as a flat disc reads as paint. The gradient
   itself carries the highlight; this is only the edge. */
.cdot--metal { width: 30px; height: 30px; }
.cdot--metal i { box-shadow: inset 0 0 0 1px rgba(0,0,0,0.16), inset 0 1px 2px rgba(255,255,255,0.5); }
/* Thumbnail strip, right end of the band. Watch it against the AI launcher: that
   pill is fixed to the bottom-right corner of the VIEWPORT, so when the stage's
   bottom edge happens to land near the window's it can come down on the last
   thumbs. The stage no longer grows for the band (see .hero__stage), which is
   what keeps the two apart at ordinary window heights. */
/* Thumbnail strip — vertical column on the right of the stage.
   top:0 / bottom:var(--cbar-h) keeps the strip inside the visible render area
   so it never slides under the colour pill; justify-content:center visually
   centres the thumbs in that safe zone. */
.thumbs { position: absolute; right: clamp(10px, 1.6vw, 20px);
  top: 0; bottom: calc(var(--cbar-h) + 6px); z-index: 3;
  display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 6px; }
.thumb { width: 40px; height: 40px; border: 1px solid transparent;
  /* fallback shown before real renders exist / if an image 404s */
  background: radial-gradient(120% 120% at 50% 30%, var(--fill), var(--fill-2));
  display: grid; place-items: center; overflow: hidden; padding: 0; }
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb--3d { background: #fff; color: var(--ink); }
.thumb--3d svg { width: 20px; height: 20px; }
.thumb.is-active { border-color: var(--ink); }
.arrow { width: 38px; height: 38px; border-radius: 50%; border: 1px solid #cfc8bb;
  background: rgba(255,255,255,0.7); display: grid; place-items: center; }
.arrow svg { width: 16px; height: 16px; }
.arrow:disabled { opacity: 0.35; }
/* 1000px, and measured off the controls rather than off any device: the strip is
   312px, the dots 455px, and the true-centre layout needs the strip's width
   MIRRORED in the empty right column, so three of those widths plus the gaps is
   what it costs. Below that the middle column starts losing, and two things give
   at once.
   The empty spacer goes, and with it the pretence of centring: at 969px the
   mirror could no longer be paid for, the right column collapsed to 131px against
   the strip's 312 and the pill sat 94px right of centre — drifting, which reads
   worse than being frankly left-aligned. So the strip takes its own column and
   the pill stretches across the rest.
   The colourway NAME goes too. It is ~120px, and it is what pushes the dots into
   scrolling; the switcher line above already says COLOUR, and the details
   column's "Colour <b>…</b>" line spells the finish out in full below.
   Must stay BELOW the base .cbar/.cbar__pick/.thumbs rules — same specificity, so
   source order is what makes it win. */
@media (max-width: 999px) {
  .cbar { grid-template-columns: minmax(0, 1fr) auto 1fr; }
  /* Pill in its own centred auto column; thumbs are now outside .cbar. */
  .cbar__pick { grid-column: 2; }
}
/* The strip loses its arrows and shrinks to 34px thumbs: they only step the hero
   and every thumb is directly tappable, so nothing becomes unreachable, and
   5 x 34 + gaps = 194px is what leaves the pill a usable width. */
@media (max-width: 760px) {
  .thumb { width: 34px; height: 34px; }
  .thumb--3d svg { width: 17px; height: 17px; }
  .arrow { display: none; }
}
/* PHONES: THE BAND IS NOT OVER THE BAG. top:100% with bottom:auto hangs it off
   the bottom edge of the stage instead, into the padding .hero was given for it —
   see the 620px block up at .hero__stage, which is the other half of this and
   does not work without it.
   THE STRIP STAYS AT THE SIDE, as it is on every other width — it keeps the base
   rule and this block says nothing about it. It was laid horizontally under the
   stage for a while, on the reasoning that below the product it costs the product
   nothing. What that actually cost was the SWITCHER: the strip and the band are
   both absolutely positioned at `top: 100%` of the stage with nothing between
   them, so at 375px the strip (453→497) landed exactly on the row of tabs
   (468→487) and covered them. `elementFromPoint` on 'Metal' answered a thumbnail.
   With the arrows hidden below 760px the tabs are the ONLY way to reach the metal
   and the initials, so a phone could configure the colour and nothing else — and
   nothing looked broken, which is why it lasted.
   Vertical at the side it is 5 x 34 plus gaps = 194px inside a 345px stage, and
   the band below is the pill and the actions, the two rows it was designed for.
   The dots go UP to 30px on the width that buys — still under the 44px ideal, but
   fourteen at 44 would be a 600px scroller in a 300px window. */
@media (max-width: 620px) {
  .cbar { top: 100%; bottom: auto;
    grid-template-columns: minmax(0, 1fr); grid-template-rows: auto;
    padding: 10px 8px 9px; gap: 8px; }
  .cbar__pick { grid-column: 1; grid-row: 1; }
  .cdot { width: 30px; height: 30px; }
}

/* ── The opening invitation to the consultant (hero, centred) ──
   car.html's #ai-center-cta, ported: pinging rings round a sparkle, a headline,
   a line of copy, gone after five seconds. Two things are deliberately not the
   car's.
   FIRST, it covers the STAGE, not the viewport. The car is one fullscreen canvas
   with nothing else to click; this is a shop — the header, the swatches and Add to
   bag are real controls, and a full-page catcher would eat five seconds of them.
   SECOND, the layer does not take the pointer: only the button inside does. In the
   car the whole overlay is clickable while visible, so any click opens the AI. Here
   a shopper who grabs the bag in the first five seconds gets the bag, and the
   invitation quietly goes away (ai-invitation.js dismisses on canvas pointerdown).
   Colours are the launcher's, not the car's blue glow: ink on the grey field. */
.ai-invite {
  position: absolute; inset: 0; z-index: 5; display: grid; place-items: center;
  /* Matched to .viewer-ph's fade: the ring going and the invitation arriving are
     one crossfade, so they have to run for the same length of time. */
  pointer-events: none; opacity: 0; transition: opacity 0.3s ease;
}
.ai-invite.show { opacity: 1; }
/* A veil, because this page is LIGHT. The car sets white text with a blue glow on
   a dark scene and needs nothing behind it; here the copy is ink and lands on a
   beige canvas bag, where it is simply unreadable. Radial and in the hero's own
   --grey-bg, so it reads as the product stepping back for a moment rather than as
   a modal: near-opaque under the words, gone by the edges, and it fades out with
   the invitation because it is the same layer. */
.ai-invite::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  /* Keyed to the wall behind it, not to --grey-bg: on the set below, a veil in
     the header's grey reads as a lighter patch hanging in the room. */
  background: radial-gradient(56% 50% at 50% 50%,
    rgba(240,238,235,0.92) 0%, rgba(240,238,235,0.82) 42%,
    rgba(240,238,235,0.48) 72%, rgba(240,238,235,0) 100%);
}
.ai-invite__btn {
  position: relative;                   /* above the veil */
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
  background: none; border: 0; padding: 1rem; text-align: center;
  pointer-events: none;                 /* only while shown — see below */
}
.ai-invite.show .ai-invite__btn { pointer-events: auto; }
.ai-invite__rings { position: relative; width: 72px; height: 72px;
  display: grid; place-items: center; margin-bottom: 0.2rem; }
.ai-invite__ring {
  position: absolute; border-radius: 50%; border: 1px solid rgba(26,26,26,0.35);
  animation: ai-invite-ping 2.4s ease-out infinite;
}
.ai-invite__ring:nth-child(2) { animation-delay: 1.2s; }
@keyframes ai-invite-ping {
  0%   { width: 48px; height: 48px; opacity: 0.7; }
  100% { width: 128px; height: 128px; opacity: 0; }
}
.ai-invite__icon {
  width: 48px; height: 48px; border-radius: 50%; display: grid; place-items: center;
  background: var(--ink); color: #fff; box-shadow: 0 6px 22px rgba(0,0,0,0.28);
}
.ai-invite__icon svg { width: 20px; height: 20px; }
.ai-invite__headline {
  font-family: var(--serif); font-size: clamp(1.3rem, 3vw, 1.8rem);
  letter-spacing: 0.02em; color: var(--ink);
}
.ai-invite__sub {
  font-size: 0.6rem; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--muted);
}
@media (prefers-reduced-motion: reduce) { .ai-invite__ring { animation: none; } }

/* ── Viewer settings: gear + panel (hero, top-right) ──
   The car configurator's settings gear (car.html #settings-btn / #settings-panel)
   in this page's clothes: the same interaction — a round button, a card that opens
   under it, click-outside or Escape to close — but a white card in the page's
   greys rather than the car's dark glass.
   Top-RIGHT of the STAGE, not fixed to the viewport as the car's is: that page is
   one fullscreen canvas, this one scrolls, and a gear pinned to the window would
   sail over the product copy. The other corners are taken — the thumbnail strip
   owns the bottom-right and goes full-width under 620px, with the AI launcher on
   top of it. The gear is [hidden] in the markup until the module half publishes
   the seam, so it can never open an empty panel. */
.stg-btn {
  position: absolute; right: 18px; top: 18px; z-index: 4;
  width: 38px; height: 38px; border-radius: 50%; border: 1px solid #cfc8bb;
  /* Translucent like .arrow, for the same reason: it sits over a live WebGL
     canvas whose colour changes with the colourway. */
  background: rgba(255,255,255,0.7); display: grid; place-items: center;
  color: var(--muted); transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.stg-btn[hidden] { display: none; }
.stg-btn svg { width: 17px; height: 17px; }
.stg-btn:hover, .stg-btn.is-open { color: var(--ink); border-color: var(--ink); background: #fff; }
.stg-panel {
  /* 244px is not a round number: it is 3 buttons wide enough for the longest label
     (Spotlight measures 62px at this size) plus their gaps and this padding. Check
     it again if a label grows — the buttons clip rather than wrap. */
  position: absolute; right: 18px; top: 64px; z-index: 4; width: 244px;
  background: #fff; border: 1px solid var(--line); border-radius: 4px;
  padding: 1rem; box-shadow: 0 10px 30px rgba(0,0,0,0.10);
  /* Closed by default, and animated from under the gear. visibility rather than
     display so the transition runs at all, and so the buttons inside leave the
     tab order while it is shut. */
  opacity: 0; visibility: hidden; transform: translateY(-6px);
  transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
}
.stg-panel.open { opacity: 1; visibility: visible; transform: translateY(0); }
.stg-title {
  font-size: 0.6rem; font-weight: 600; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 0.9rem;
}
.stg-row { display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem; }
/* A switch sits beside its label; a segment sits under it. */
.stg-row--inline { flex-direction: row; align-items: center; justify-content: space-between;
  gap: 1rem; margin-bottom: 1.1rem; cursor: pointer; }
.stg-label { font-size: 0.8rem; color: var(--ink); }
/* Toggle — the car's .switch/.track in this page's colours. The real checkbox is
   kept in the DOM and only visually hidden, so the label, the keyboard and the
   :checked state all keep working without a line of JS to fake them. */
.stg-switch { position: relative; flex: none; width: 34px; height: 20px; }
.stg-switch input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
.stg-track { position: absolute; inset: 0; border-radius: 100px; background: var(--line);
  transition: background 0.2s; pointer-events: none; }
.stg-track::after { content: ''; position: absolute; top: 3px; left: 3px;
  width: 14px; height: 14px; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 2px rgba(0,0,0,0.25); transition: transform 0.2s; }
.stg-switch input:checked + .stg-track { background: var(--ink); }
.stg-switch input:checked + .stg-track::after { transform: translateX(14px); }
.stg-switch input:focus-visible + .stg-track { outline: 2px solid var(--ink); outline-offset: 2px; }
/* The segment: the car's .seg in this page's colours, injected by
   fashion/js/ui/viewer-settings.js from the STUDIOS table. It WRAPS — the car has
   exactly three environments and can assume one row; this list is meant to grow
   while a studio is being chosen, so the buttons flow onto as many rows as they
   need at a fixed three per row rather than squeezing thinner each time one is
   added. */
.stg-seg { display: flex; flex-wrap: wrap; gap: 4px; }
.stg-seg button {
  flex: 1 1 calc(33.333% - 4px); min-width: 0;
  font-family: var(--sans); font-size: 0.56rem; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
  background: #fff; border: 1px solid var(--line); border-radius: 2px;
  padding: 0.5rem 0.2rem; transition: color 0.2s, background 0.2s, border-color 0.2s;
}
.stg-seg button:hover { color: var(--ink); border-color: var(--ink); }
.stg-seg button.is-active { background: var(--ink); border-color: var(--ink); color: #fff; }
/* Waiting on a ~1.5 MB HDR. Also takes the pointer off the row, so a second pick
   cannot queue another download while this one is still in flight. */
.stg-seg.is-loading { opacity: 0.55; pointer-events: none; }
/* Phones: tighter into the corner, and the panel is bounded by the viewport
   rather than given a width that could hang off the right edge. */
@media (max-width: 620px) {
  .stg-btn { right: 10px; top: 10px; }
  /* Same 44px tap target as the header's .lnk, by the same centred ::after so the
     drawn button stays 38px. Safe here where it is not on the segment inside: the
     gear has no neighbour within 44px, so nothing can steal its tap. */
  .stg-btn::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
  }
  .stg-panel { right: 10px; top: 54px; width: min(244px, calc(100vw - 20px)); }
}

/* ── Floating add-to-cart bar (over hero) ── */
.sticky-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 850;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  /* Opaque: this sits over live page content, so a transparent bar would leave the
     info text unreadable against whatever scrolled behind it. */
  padding: 0.9rem clamp(1rem, 3vw, 2.2rem); background: #fff; border-top: 1px solid var(--line);
  transform: translateY(120%); transition: transform 0.3s ease; pointer-events: none;
}
.sticky-bar.show { transform: translateY(0); pointer-events: auto; }
/* While the buy bar is up it owns the bottom edge, so the AI launcher has to step
   aside: at z-index 860 against the bar's 850 the pill lands directly on the bar's
   Add to shopping bag button (both are flush right) and swallows the tap. Lift it
   by the bar's own height, which page-bars.js measures into --sticky-bar-h because
   the bar is ~81px wide-screen and ~106px once the info text wraps. */
body:has(.sticky-bar.show) { --launcher-bottom: calc(var(--sticky-bar-h, 106px) + 1rem); }
.sticky-bar__info { font-size: 0.85rem; }
/* Phones: side by side there is no room for both — at 329px the info wrapped to
   three lines AND the button label wrapped to three, making the bar 134px, over a
   fifth of the screen. The bar exists for one reason, to keep a way to buy within
   reach, so drop the info (the title and price are both still in the page above)
   and give the button the full width. Same specificity as the base rules, so this
   must stay below them. */
@media (max-width: 620px) {
  .sticky-bar { padding: 0.7rem 0.9rem; }
  .sticky-bar__info { display: none; }
  .sticky-bar .btn-cart { flex: 1; justify-content: center; padding: 1rem; }
}
.sticky-bar__info .eb { font-size: 0.6rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); display: block; }
.btn-cart {
  background: var(--ink); color: #fff; border: 1px solid var(--ink);
  padding: 1rem 2.4rem; font-size: 0.72rem; letter-spacing: 0.16em; text-transform: uppercase;
  display: inline-flex; align-items: center; gap: 0.7rem; transition: background 0.2s, color 0.2s;
}
.btn-cart:hover { background: #333; }
.btn-cart svg { width: 17px; height: 17px; }

/* ── Details two-column ── */
.details { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: clamp(2rem, 6vw, 6rem);
  max-width: 1360px; margin: 0 auto; padding: clamp(2rem, 5vw, 3.4rem) clamp(1rem, 3vw, 2.2rem) 4rem; }
@media (max-width: 900px) { .details { grid-template-columns: 1fr; gap: 2.4rem; } }
.eyebrow { font-size: 0.72rem; color: var(--muted); margin-bottom: 0.7rem; }
.title { font-size: 1.4rem; font-weight: 400; letter-spacing: 0.01em; }
.price { font-size: 1.15rem; margin: 0.7rem 0 1.4rem; }

/* ── A piece that is not configured ───────────────────────────────────────
   `configurable: false` in the catalog. The stage shows the photograph and
   nothing else: no colour band, no thumbnail strip, no viewer gear, and no
   "Colour beige and brown canvas" line in the details column — there is no
   colourway to name, because nobody is choosing one.

   Hidden in CSS rather than left out of the markup because the markup is one
   page serving both kinds of product: what it can do is the catalog's
   answer, not the document's. The class is set by product-behaviour.js before
   anything paints. */
body.is-showcase .cbar,
body.is-showcase .thumbs,
body.is-showcase .stg-btn,
body.is-showcase .stg-panel,
body.is-showcase .variant-label { display: none; }
/* …except when the piece has more than one photograph of it. The strip comes
   back as a plain gallery — same control, photographs instead of camera
   views. `has-gallery` is set by product-behaviour.js only when there are
   frames to move between, so a single-photograph piece keeps the clean stage
   rather than a strip with one button on it. */
body.is-showcase.has-gallery .thumbs { display: flex; }
/* The stage takes the photograph's size, and the viewer's height is the CEILING
   on it.

   Three things were wanted here and only two of them fit together, so this rule
   is where the third was given up:
     no band       no grey strip above or below the picture;
     whole frame   nothing cropped, and nothing painted outside the box;
     one height    every piece opening at the height the 3D bag gets.
   The frames are 16:9 (1407x768) and the stage is 1.28:1 — so holding the
   height at min(78vh, 780px) either letterboxes (a band) or covers (a frame
   scaled to 1338px wide inside a 936px box, i.e. spilling out of it). ONE
   HEIGHT is the one that goes: the picture sets the height, within a cap.

   So `position: static` takes the image out of the absolute fill it shares with
   the canvas, and the stage — which declares no height of its own here — ends up
   exactly as tall as the picture. `width: auto` with the two caps below is what
   keeps it whole: `max-width: 100%` stops a wide frame at the stage's edge (511px
   tall at 936 wide, no spill), `max-height` stops a portrait one at the height
   the 3D bag would have had (no frame taller than the viewer, which is the half
   of "one height" worth keeping), and whichever binds first, the other stays
   proportional. `margin: 0 auto` centres a frame that the height cap has made
   narrower than the stage.

   KEEP THE CAP IN STEP with `.hero__stage`'s own height — both the 780 here and
   the phone pair in the media query below are copies of it. The `vh`/`svh` pair
   is declared twice for the same reason it is up there.

   `#heroPhoto`'s own cap — 480x500, at or below native resolution — was for
   the editorial shots that join the 3D bag in the thumbnail strip: small
   frames beside a model, where upscaling would have shown. It is lifted to the
   stage's full size here, because the photograph is the subject and not a
   companion to it — the stage is its only limit. */
body.is-showcase .hero__stage { height: auto; }
body.is-showcase #heroPhoto {
  position: static;
  display: block;
  width: auto;
  height: auto;
  margin: 0 auto;
  max-width: 100%;
  max-height: min(78vh, 780px);
}
@media (max-width: 620px) {
  body.is-showcase #heroPhoto {
    max-height: min(46vh, 92vw); max-height: min(46svh, 92vw);
  }
}
/* The band's height is a custom property everything on the stage stops short
   of. With no band there is nothing to stop short of, so the photograph gets
   the whole stage. */
body.is-showcase .hero__stage { --cbar-h: 0px; }

/* Names the colourway; does not offer it. The swatch grid that used to follow
   this line lives in the hero now (see .cbar) — this is the reading half of that
   pair, and the only place the colourway is spelt out below 1000px. */
.variant-label { font-size: 0.8rem; margin-bottom: 1.6rem; }
.variant-label b { font-weight: 600; }

/* All that is left of personalisation in the details column. The read-back row
   above it went the same way as the drawer before it: the bar over the bag names
   every choice as it is made, and the bag itself shows them. */
.gift-fine { font-size: 0.74rem; color: var(--muted); line-height: 1.5;
  margin: 0.7rem 0 2.2rem; max-width: 52ch; }

.sect-title { font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 700; margin-bottom: 0.7rem; }
.style-ref { font-size: 0.76rem; color: var(--muted); margin-bottom: 1.1rem; }
.desc { font-size: 0.95rem; line-height: 1.65; max-width: 52ch; margin-bottom: 2.2rem; }

.acc { border-top: 1px solid var(--line); }
.acc:last-of-type { border-bottom: 1px solid var(--line); }
.acc__head { width: 100%; background: none; border: 0; text-align: left; padding: 1.15rem 0;
  display: flex; justify-content: space-between; align-items: center; font-size: 0.98rem; }
.acc__head .chev { transition: transform 0.25s; color: var(--muted); }
.acc[open] .acc__head .chev { transform: rotate(180deg); }
.acc__body { padding: 0 0 1.3rem; font-size: 0.9rem; line-height: 1.6; color: #3a3a3a; max-width: 54ch; }
.acc__body ul { margin: 0; padding-left: 1.1rem; } .acc__body li { margin-bottom: 0.4rem; }

/* Right purchase panel */
/* ── Size ──
   The rows are squares rather than pills, because one holds "XS" and the next
   "10.5" and a pill would be a different width for each: a rack reads as a rack
   only when the boxes line up. Fixed min-width with the label centred inside
   does that without stretching "XS" to the width of "10.5".

   The systems switcher is deliberately quieter than the sizes themselves — it
   sets how to READ the row, and a client who already knows their size should
   see the row first. */
.size { margin-bottom: 1.4rem; }
.size__head { display: flex; align-items: baseline; gap: 0.9rem; margin-bottom: 0.7rem; }
.size__label { font-size: 0.72rem; letter-spacing: 0.16em; text-transform: uppercase; }
.size__systems { display: flex; gap: 2px; }
.size__systems button { background: none; border: 0; padding: 2px 5px;
  font-size: 0.64rem; letter-spacing: 0.1em; color: var(--muted); cursor: pointer; }
.size__systems button:hover { color: var(--ink); }
.size__systems button.is-active { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.size__row { display: flex; flex-wrap: wrap; gap: 6px; }
.size__row button { min-width: 46px; height: 42px; padding: 0 0.6rem;
  background: #fff; border: 1px solid var(--line); border-radius: 2px;
  font-family: var(--sans); font-size: 0.82rem; color: var(--ink); cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s; }
.size__row button:hover { border-color: var(--ink); }
.size__row button.is-active { background: var(--ink); border-color: var(--ink); color: #fff; }
.size__row button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* Says what is missing, not what went wrong: it is showing before the client
   has done anything, so it has to read as an instruction rather than an error. */
.size__hint { font-size: 0.74rem; color: var(--muted); margin: 0.7rem 0 0; }
.size.is-chosen .size__hint { display: none; }
/* A disabled Add stays legible — greying it to near-invisible reads as a page
   that failed to load rather than as a step still to take. */
.btn-cart[disabled] { opacity: 0.38; cursor: not-allowed; }

.buy { align-self: start; }
@media (min-width: 901px) { .buy { position: sticky; top: calc(var(--is-topbar-h, 0px) + var(--hdr-h) + 24px); } }
.buy .btn-cart { width: 100%; justify-content: center; padding: 1.1rem; margin-bottom: 1.2rem; }
.delivery { display: flex; gap: 0.6rem; font-size: 0.82rem; color: #3a3a3a; margin-bottom: 1.4rem; }
.delivery svg { width: 18px; height: 18px; flex: none; color: var(--ink); }
.buy-links { display: flex; flex-direction: column; gap: 1rem; margin-bottom: 1.4rem; }
.buy-links a { display: inline-flex; align-items: center; gap: 0.6rem; font-size: 0.85rem; }
.buy-links a:hover { text-decoration: underline; }
.buy-links svg { width: 17px; height: 17px; }
.fineprint { font-size: 0.78rem; color: var(--muted); line-height: 1.55; }

/* ── Related carousel ── */
.related { padding: 1rem clamp(1rem, 3vw, 2.2rem) 2rem; }
.rel-track { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(230px, 1fr);
  gap: 1rem; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; padding-bottom: 0.5rem; }
.rel-track::-webkit-scrollbar { display: none; }
@media (min-width: 901px) { .rel-track { grid-auto-columns: calc(25% - 0.75rem); } }
.rel-card { scroll-snap-align: start; }
.rel-card .im { aspect-ratio: 1/1; margin-bottom: 0.8rem; transition: opacity 0.2s;
  background-color: var(--fill); background-size: cover; background-position: center;
  /* branded placeholder (subtle diagonal on Aveline tones) until a real photo is set */
  background-image:
    repeating-linear-gradient(45deg, rgba(0,0,0,0.035) 0 10px, transparent 10px 20px),
    radial-gradient(120% 120% at 50% 30%, var(--fill), var(--fill-2)); }
.rel-card:hover .im { opacity: 0.85; }
.rel-card .eb { font-size: 0.68rem; color: var(--muted); }
.rel-card .nm { font-size: 0.9rem; margin: 0.2rem 0; }
.rel-card .pr { font-size: 0.85rem; }
.rel-progress { height: 2px; background: var(--line); max-width: 340px; margin: 1.4rem auto 0; position: relative; }
.rel-progress i { position: absolute; top: 0; left: 0; height: 100%; width: 40%; background: var(--ink); transition: left 0.2s, width 0.2s; }
.rel-crumb { text-align: center; font-size: 0.74rem; color: var(--muted); padding: 1.8rem 1rem 0.5rem; }
.rel-crumb a:hover { text-decoration: underline; } .rel-crumb span { margin: 0 0.35rem; }

/* ── Footer ── */
.foot { background: #000; color: #fff; margin-top: 2rem; }
.foot__services { padding: 3rem clamp(1rem, 3vw, 2.2rem) 1rem; }
.foot__services .h { font-size: 0.66rem; letter-spacing: 0.18em; color: #8a8a8a; text-transform: uppercase; margin-bottom: 1.6rem; }
.foot__services a { display: block; font-size: 0.95rem; margin-bottom: 1.5rem; text-decoration: underline; text-underline-offset: 3px; }
.foot__brands { display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; padding: 2rem clamp(1rem, 3vw, 2.2rem);
  font-family: var(--serif); letter-spacing: 0.16em; color: #cfcfcf; }
.foot__brands .mark { width: 34px; height: 34px; border-radius: 8px; background: #2a2a2a; display: grid; place-items: center; font-size: 1rem; }
.foot__copy { padding: 0 clamp(1rem, 3vw, 2.2rem) 1.5rem; font-size: 0.76rem; color: #9a9a9a; }
.foot__cols { display: grid; grid-template-columns: 1fr 1fr 1.4fr; gap: 2rem;
  border-top: 1px solid #222; padding: 2.6rem clamp(1rem, 3vw, 2.2rem); }
@media (max-width: 760px) { .foot__cols { grid-template-columns: 1fr; } }
.foot__cols ul { list-style: none; margin: 0; padding: 0; }
.foot__cols li { margin-bottom: 1rem; }
.foot__cols a { font-size: 0.85rem; color: #cfcfcf; text-decoration: underline; text-underline-offset: 3px; }
.foot__cols a:hover { color: #fff; }
.foot__form .h { font-size: 0.66rem; letter-spacing: 0.14em; color: #8a8a8a; text-transform: uppercase; margin: 1.6rem 0 0.8rem; }
.foot__form p { font-size: 0.85rem; color: #cfcfcf; margin-bottom: 1rem; max-width: 42ch; }
.foot-input { display: flex; align-items: center; border-bottom: 1px solid #555; padding: 0.5rem 0; }
.foot-input input { flex: 1; background: none; border: 0; color: #fff; font-size: 0.9rem; }
.foot-input input::placeholder { color: #8a8a8a; }
.foot-input button { background: none; border: 0; color: #fff; }
.foot__wordmark { font-family: var(--serif); font-weight: 500; text-transform: uppercase;
  font-size: clamp(4rem, 22vw, 20rem); line-height: 0.8; letter-spacing: 0.04em;
  white-space: nowrap; overflow: hidden; padding: 1rem clamp(1rem, 3vw, 2.2rem) 0.2rem; }

/* ── Header drawers (bag, account, contact, menu, search) ── */
/* Sit below the studio topbar (z-index 3000, and it must stay reachable).
   Without this the panels start at y=0 and their ✕ lands inside the
   topbar's band, where the topbar wins the hit test and the button is dead. */
.dw-overlay { position: fixed; inset: 0; top: var(--is-topbar-h, 0px); z-index: 1250; display: none; background: rgba(0,0,0,0.4); }
.dw-overlay.show { display: block; }
.dw { position: absolute; top: 0; right: 0; height: 100%; width: min(400px, 100%);
  background: #fff; color: var(--ink); overflow-y: auto; padding: 3rem 2rem 2rem;
  /* Reaching the end of a drawer must not start scrolling the page behind it —
     on a phone that silently moves the page you return to when it closes. */
  overscroll-behavior: contain;
  box-shadow: -20px 0 60px rgba(0,0,0,0.22); animation: pz-in 0.3s ease; }
.dw--left { right: auto; left: 0; box-shadow: 20px 0 60px rgba(0,0,0,0.22); }
/* search and the catalog drop from the top instead */
/* max-height is load-bearing, not cosmetic. This overrides .dw's height:100% with
   height:auto so the panel is only as tall as its content — but an auto-height box
   never overflows, so .dw's overflow-y:auto produced no scrollbar, and the panel
   simply grew past the bottom of the position:fixed overlay. The catalog's six
   cards fit a desktop grid but stack one per row on a phone, which put four of
   them permanently out of reach. Capping the height at the overlay lets
   overflow-y:auto do its job. */
.dw--top { position: absolute; top: 0; left: 0; right: 0; width: auto; height: auto;
  max-height: 100%;
  padding: 2.4rem clamp(1rem, 4vw, 3rem) 2rem; box-shadow: 0 20px 50px rgba(0,0,0,0.2); }
.dw-close { position: absolute; top: 1.2rem; right: 1.4rem; width: 34px; height: 34px;
  border-radius: 50%; border: 0; background: var(--ink); color: #fff; font-size: 0.9rem; }
.dw-title { font-family: var(--serif); font-size: 1.4rem; letter-spacing: 0.06em;
  text-transform: uppercase; margin-bottom: 1.4rem; }
.dw p { font-size: 0.88rem; line-height: 1.6; color: #3a3a3a; }
.dw-list { list-style: none; margin: 0; padding: 0; }
.dw-list li { border-bottom: 1px solid var(--line); }
.dw-list a, .dw-list button { display: flex; align-items: center; gap: 0.7rem; width: 100%;
  background: none; border: 0; text-align: left; padding: 1rem 0; font-size: 0.95rem; color: inherit; }
.dw-list a:hover, .dw-list button:hover { color: var(--muted); }
.dw-list svg { width: 18px; height: 18px; flex: none; }
.dw-nav a { font-family: var(--serif); font-size: 1.15rem; letter-spacing: 0.04em; }
.dw-field { display: flex; align-items: center; border-bottom: 1px solid var(--ink); padding: 0.5rem 0; margin-bottom: 1.2rem; }
.dw-field input { flex: 1; border: 0; background: none; font-family: var(--sans); font-size: 0.95rem; color: var(--ink); }
.dw-field input:focus { outline: none; }
.dw-btn { width: 100%; background: var(--ink); color: #fff; border: 1px solid var(--ink);
  padding: 0.95rem; font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase; }
.dw-note { font-size: 0.74rem; color: var(--muted); margin-top: 1rem; line-height: 1.5; }
/* Cart */
.cart-item { display: flex; gap: 0.9rem; padding: 1.1rem 0; border-bottom: 1px solid var(--line); }
.cart-item .im { width: 74px; height: 90px; flex: none; background: var(--fill) center/cover; }
.cart-item .nm { font-size: 0.9rem; margin-bottom: 0.2rem; }
.cart-item .mt { font-size: 0.76rem; color: var(--muted); line-height: 1.5; }
.cart-item .pr { font-size: 0.85rem; margin-top: 0.35rem; }
.cart-item .info { flex: 1; min-width: 0; }
/* ✕ at the row's edge, in a 40px box so it stays a thumb-sized target. The
   glyph is larger and darker than it first was — at 0.8rem in mid-grey it read
   as decoration and got missed entirely. */
.cart-rm { align-self: flex-start; flex: none; background: none; border: 0;
  width: 40px; height: 40px; margin: -8px -12px 0 0; display: grid; place-items: center;
  color: var(--ink); font-size: 1rem; line-height: 1; cursor: pointer;
  opacity: 0.55; transition: opacity 0.15s; }
.cart-rm:hover, .cart-rm:focus-visible { opacity: 1; }
.cart-empty { font-size: 0.9rem; color: var(--muted); padding: 1.4rem 0; }
.cart-total { display: flex; justify-content: space-between; align-items: baseline;
  padding: 1.2rem 0; font-size: 0.95rem; }
.cart-total b { font-size: 1.05rem; }
/* Search results */
.sr-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 1.2rem; margin-top: 1.6rem; }
.sr-card .im { aspect-ratio: 1/1; background: var(--fill) center/cover; margin-bottom: 0.5rem; }
.sr-card .nm { font-size: 0.85rem; } .sr-card .pr { font-size: 0.8rem; color: var(--muted); }
.sr-card .mt { font-size: 0.72rem; color: var(--muted); margin-top: 0.15rem; }
.sr-empty { font-size: 0.88rem; color: var(--muted); margin-top: 1.4rem; }

/* ── AI assistant — Aveline skin ── */
@keyframes ai-pulse {
  0%,100% { box-shadow: 0 6px 24px rgba(0,0,0,0.22); }
  50%     { box-shadow: 0 8px 30px rgba(0,0,0,0.34), 0 0 0 6px rgba(26,26,26,0.06); }
}
.ai-launcher {
  /* --launcher-bottom is raised while the buy bar is up; see the .sticky-bar rules. */
  position: fixed; bottom: var(--launcher-bottom, 2rem); right: 2rem; z-index: 860;
  display: inline-flex; align-items: center; gap: 0.6rem;
  height: 50px; padding: 0 1.5rem; border-radius: 100px; border: 1px solid var(--ink);
  background: var(--ink); color: #fff; cursor: pointer;
  animation: ai-pulse 2.6s ease-in-out infinite;
  transition: transform 0.2s, background 0.2s, bottom 0.3s ease;
}
.ai-launcher:hover { transform: translateY(-2px); background: #333; }
.ai-launcher svg { width: 18px; height: 18px; flex: none; }
.ai-launcher span {
  font-family: var(--serif); font-size: 0.92rem; letter-spacing: 0.06em; white-space: nowrap;
}
/* Phones: the labelled pill is 167px wide — a quarter of a 329px screen — and it
   landed on the hero thumbnail strip. Collapse it to a circular icon button; the
   aria-label carries the name for screen readers, and tapping it opens a panel
   that says "AI consultant" at the top, so the label is not load-bearing.
   Must stay BELOW the base .ai-launcher rules — same specificity, source order
   decides. */
@media (max-width: 620px) {
  .ai-launcher { width: 46px; height: 46px; padding: 0; border-radius: 50%; justify-content: center; }
  .ai-launcher span { display: none; }
  .ai-launcher svg { width: 20px; height: 20px; }
}

.ai-panel {
  position: fixed; bottom: 2rem; right: 2rem; z-index: 1400;
  width: 320px;
  display: flex; flex-direction: column; overflow: hidden;
  background: #fff; color: var(--ink); border: 1px solid var(--line);
  box-shadow: 0 24px 60px rgba(0,0,0,0.24);
}
.ai-panel-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1rem 1.2rem; background: var(--ink); color: #fff;
}
.ai-panel-header .panel-label {
  font-family: var(--serif); font-size: 1.1rem; letter-spacing: 0.08em; text-transform: uppercase;
}
.ai-panel-header .panel-close {
  background: none; border: 0; color: rgba(255,255,255,0.7); font-size: 1rem; line-height: 1;
}
.ai-panel-header .panel-close:hover { color: #fff; }

.ai-messages {
  flex: 1; overflow-y: auto; padding: 0.9rem 1.2rem; display: flex; flex-direction: column; gap: 0.6rem;
  max-height: 120px; min-height: 40px; background: #fbfaf8;
}
.ai-messages::-webkit-scrollbar { width: 5px; }
.ai-messages::-webkit-scrollbar-thumb { background: #d9d3c8; border-radius: 4px; }
.ai-bubble { max-width: 85%; }
.ai-bubble.agent { align-self: flex-start; }
.ai-bubble.user  { align-self: flex-end; text-align: right; }
.ai-bubble-role {
  font-size: 0.6rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.3rem;
}
.ai-bubble-text {
  font-size: 0.9rem; line-height: 1.5; padding: 0.6rem 0.85rem; border: 1px solid var(--line);
}
.ai-bubble.agent .ai-bubble-text { background: var(--fill); }
.ai-bubble.user  .ai-bubble-text { background: var(--ink); color: #fff; border-color: var(--ink); }

.ai-footer { padding: 0.9rem 1.2rem 1.1rem; border-top: 1px solid var(--line); background: #fff; }
.ai-status-row { display: flex; align-items: center; gap: 0.7rem; margin-bottom: 0.8rem; }
.ai-status {
  font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted);
}
.ai-status.connected { color: #2f5a3a; }
.ai-agent-state { display: flex; align-items: center; gap: 0.4rem; font-size: 0.62rem;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.ai-state-dot { width: 7px; height: 7px; border-radius: 50%; background: #cfc8bb; }
.ai-state-dot.speaking { background: var(--ink); animation: ai-pulse 1s ease-in-out infinite; }

.ai-chat-input-row { display: none; gap: 0.5rem; margin-bottom: 0.8rem; }
.ai-text-input {
  flex: 1; border: 1px solid var(--line); background: #fff; color: var(--ink);
  padding: 0.6rem 0.75rem; font-family: var(--sans); font-size: 0.85rem;
}
.ai-text-input:focus { outline: none; border-color: var(--ink); }
.ai-send-btn {
  background: var(--ink); color: #fff; border: 1px solid var(--ink);
  padding: 0 1rem; font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase;
}

.ai-controls { display: flex; gap: 0.6rem; }
.ai-call-btn {
  flex: 1; background: var(--ink); color: #fff; border: 1px solid var(--ink);
  padding: 0.85rem; font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
  transition: background 0.2s;
}
.ai-call-btn:hover { background: #333; }
.ai-call-btn.connecting { background: var(--muted); border-color: var(--muted); cursor: default; }
.ai-call-btn.end { background: #fff; color: var(--ink); }
.ai-mute-btn {
  background: #fff; color: var(--ink); border: 1px solid var(--line);
  padding: 0.85rem 1.1rem; font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
}
.ai-mute-btn:disabled { opacity: 0.4; cursor: default; }

@media (max-width: 480px) {
  /* A compact sheet, not a half-screen takeover: on a phone the bag is what
     the page is for, so the assistant keeps its footprint small and leaves
     the model visible behind it. The transcript scrolls instead of growing. */
  .ai-panel { width: 100vw; right: 0; bottom: 0; max-height: 42vh; }
  .ai-panel-header { padding: 0.7rem 1rem; }
  .ai-messages { max-height: 74px; padding: 0.7rem 1rem; gap: 0.5rem; }
  /* The role label costs a whole line per bubble in a 2-line transcript, and
     left/right alignment already says who is speaking. */
  .ai-bubble-role { display: none; }
  .ai-bubble-text { font-size: 0.82rem; padding: 0.5rem 0.7rem; }
  .ai-footer { padding: 0.7rem 1rem 0.85rem; }
  .ai-status-row { margin-bottom: 0.55rem; }
  .ai-call-btn, .ai-mute-btn { padding: 0.7rem 0.9rem; }
  .ai-launcher { right: 1rem; bottom: var(--launcher-bottom, 1rem); }
}

/* ── Toast ── */
/* pointer-events:none because the toast is never interactive — and because
   opacity:0 does NOT remove an element from hit testing. Parked at z-index 1500
   it sat right on top of the AI launcher and ate the tap, which is why the
   assistant would not open on phones (where the launcher moves to bottom:1rem). */
.toast { position: fixed; right: 20px; bottom: 90px; z-index: 1500; pointer-events: none;
  background: var(--ink); color: #fff;
  padding: 1rem 1.3rem; font-size: 0.78rem; transform: translateY(160%); opacity: 0;
  transition: transform 0.35s, opacity 0.35s; }
.toast.show { transform: translateY(0); opacity: 1; }

/* ==========================================================================
   THE CATALOG SCREEN, AND THE STUDIO'S DIALOGS

   Both come from common/css/configurator.css, linked in this page's <head>.
   The picker, the "no catalog" card and the Save / Send / Configure-together
   dialogs are deliberately the studio's and not the boutique's: choosing a
   piece, and saving or sharing a build, are the same acts on every
   catalog-driven page, and they should look it.

   An earlier version copied those rules in here to avoid linking the
   stylesheet. That was the wrong trade — 100 lines of duplication with a
   "keep in step by hand" note on them — and it was unnecessary: the only two
   rules in configurator.css this page cannot live with are the two undone
   below. Every class name the two files share is one the studio owns.
   ========================================================================== */

/* configurator.css is written for a full-window app, so it pins the document
   to the viewport and locks scrolling:

     html, body { height: 100%; }
     body       { overflow: hidden; }

   On a product page that froze everything below the hero — the details column,
   the related bags and the footer were all still in the document and
   unreachable. Its other body declarations (background, colour, family, size)
   are already overridden by this file's own `body` rule further up; these two
   are not, because that rule never had a reason to mention them. */
html, body { height: auto; }
body { overflow: visible; }

/* While the catalog screen is up, the page behind it must not scroll under
   it. Scoped to the class fashion-catalog.js adds and removes, so it cannot
   outlive the screen. */
body.is-picking { overflow: hidden; }

/* The header stays visible above the catalog screen (the picker's top inset
   clears it), so the Catalog link would sit there offering to take you to
   the screen you are already on. Hidden while picking, by the same class.

   `visibility`, not `display`: below 620px the header row is `auto 1fr auto`
   and sized by its content, so removing this link would narrow the left column
   and slide the centred wordmark off centre — the exact problem the media
   query above that line was written to avoid. Hiding it in place changes no
   layout at all. */
body.is-picking #catalogBack { visibility: hidden; }

/* The picker sits over this page's own furniture, which the studio's z-indexes
   know nothing about: the header is 900 and the AI launcher higher still. */
#picker, #failure { z-index: 2000; }

/* The consultant has to sit OVER the catalog screen, because on the catalog
   screen it is the point: the copy invites the client to ask for a bag instead
   of picking one. The launcher is 860 and the panel 1400 against the picker's
   2000, so without this the invitation is a button behind an overlay. Both stay
   below the studio topbar's 3000, which must stay reachable. */
body.is-picking .ai-launcher { z-index: 2010; }
body.is-picking .ai-panel    { z-index: 2020; }

/* And so do the six drawers, for the same reason and with a worse symptom.
   The bar's buttons — bag, account, search, menu, contact — are at 900 and
   reachable on the catalog screen, but their overlay is 1250 against the
   picker's 2000: pressing one dimmed the screen, showed the close cross over
   the header, and opened the panel BEHIND the catalog. Nothing appeared, so
   it read as a dead button rather than as a panel in the wrong place.

   One rule covers all six because they share `.dw-overlay` and the panels are
   its children. Above the consultant's panel, because a drawer is a modal and
   should be the top thing while it is open; still under the studio topbar's
   3000, which must stay reachable. */
body.is-picking .dw-overlay  { z-index: 2040; }

/* ==========================================================================
   THE CATALOG SCREEN, IN THIS PAGE'S CLOTHES

   It was the studio's — dark radial field, purple eyebrow, Inter — on the
   argument that choosing a piece is the same act on every catalog-driven
   page. Seen next to the boutique it is a different site for one screen, and
   the screen a client meets FIRST. So the picker and the "no catalog" card
   now read as the maison, and the studio's look survives only in the shared
   markup's structure.

   Only colour and type change. The markup is still renderPicker's and the
   layout is still configurator.css's grid, so a card gained or lost in the
   JSON needs nothing here.
   ========================================================================== */
#picker, #failure {
  color: var(--ink);
  font-family: var(--sans);
  /* The page's own soft field, not a near-black scrim. */
  background: linear-gradient(180deg, var(--fill) 0%, #fff 62%);
  backdrop-filter: none;
}

/* The department bar. A boutique's collection nav: tracked-out small caps in
   a centred row, the open one inked and underlined, the rest quiet. Not the
   studio's pill chips — those are for a product's facts, and two rows of
   pills on one screen would make the departments look like another spec. */
/* The picker stacks three blocks — the department bar, the SAVED row, the
   grid — and the studio's 1.6rem between each put 83px between the open
   department and the first card it names. On the studio's own pages that gap
   separates a heading from a catalog; here the bar IS the catalog's heading,
   and the cards want to read as hanging off the department rather than as a
   second screen below it. 0.6rem closes the span to ~51px with the SAVED row
   still inside it.

   Not `.picker__sections { margin-bottom }`: the gap either side of the SAVED
   row is one measurement, and setting half of it here would leave the other
   half at 1.6rem and the row visibly off-centre in its own span. */
#picker { gap: 0.6rem; }

#picker .picker__sections {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(1.1rem, 3vw, 2.2rem);
  /* No negative pull any more: it used to tuck the bar under the head, and
     the head is gone. The picker's own column gap above is the whole
     spacing now. */
}
#picker .section-chip {
  padding: 0 0 0.35rem;
  border: 0;
  /* A hairline that is always there and only changes colour, so choosing a
     department doesn't move the row by a pixel. */
  border-bottom: 1px solid transparent;
  border-radius: 0;
  background: none;
  color: var(--muted);
  font-family: var(--sans);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.25s, border-color 0.25s;
}
#picker .section-chip:hover { color: var(--ink); }
#picker .section-chip[aria-pressed="true"] {
  color: var(--ink);
  border-bottom-color: var(--ink);
}
/* The consultant's recommendation, sitting after the four departments as the
   fifth thing the bar can be showing — and the way back out of it. A pill
   rather than a fifth underlined department, because it is not a room in the
   shop: it is a moment in a conversation, and it disappears when the client
   presses it. The ✕ is in the label rather than a nested button, so the whole
   chip is the one target and there is no small cross to miss. */
#picker .section-chip--selection {
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--ink);
  border-radius: 999px;
  color: var(--ink);
  font-size: 0.58rem;
  letter-spacing: 0.16em;
}
#picker .section-chip--selection:hover { background: var(--fill); }
/* Nothing published at all. One line of the maison's serif where the grid
   would be — the same voice the rest of the boutique speaks in, rather than a
   dashed box apologising. A department that is merely empty never reaches
   this: it has no chip to be reached by. */
#picker .picker__empty {
  grid-column: 1 / -1;
  padding: clamp(3rem, 10vh, 6rem) 0;
  color: var(--muted);
  font-family: var(--serif);
  font-size: 1.05rem;
  font-style: italic;
  text-align: center;
}

/* Every card the same height, whatever its copy runs to.

   A grid stretches the cards in ONE row to the tallest of them, so a row
   matched itself and disagreed with the row under it. Measured at three
   columns, back when the card still carried the studio's axes line: in Purse
   the Lily is the only configurable piece, so its line ("5 colour · 2 metal ·
   3 lettering · 4 initials placement") wrapped and stood its whole row 40px
   taller than the row below; in Tops a single two-line name ("Nathalie
   Cashmere Turtleneck") did the same by 22px, and that one still would.

   `1fr` rows make every row as tall as the tallest, which is what a
   boutique's grid does — the slack lands under the shortest card's copy
   rather than under an entire row. Nothing is truncated to achieve it, so a
   department whose copy grows still shows all of it and simply sets a taller
   row for everything. */
#picker .picker__cards { grid-auto-rows: 1fr; }

/* ── The collection card ────────────────────────────────── */

/* The boutique's own card, built by `renderPieces` in
   fashion/js/experience/catalog-sections.js and styled here from nothing.

   It used to be the studio's `.product-card` with this file turning half of it
   back off — the chips hidden, the axes line hidden, and the lift-and-glow
   neutralised one property at a time, which is how a card on an ink-and-paper
   page went on lifting a violet glow out of configurator.css for a while: the
   fourth hover property was missed. Nothing here inherits from that card now,
   so there is nothing to miss.

   White, hairline-bordered, square — the language the details column and the
   drawers already use. The hover is a border darkening, which is what every
   other surface on this page does. */
.piece-card {
  display: flex;
  flex-direction: column;
  text-align: left;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: #fff;
  overflow: hidden;
  cursor: pointer;
  transition: border-color 0.25s;
}
.piece-card:hover { border-color: rgba(26, 26, 26, 0.34); }
/* A card here is a button, and taking the ring away would leave the keyboard
   with nothing to follow. Ink is what every other focusable thing on this page
   rings in. */
.piece-card:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}
.piece-card__art {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--fill);
  overflow: hidden;
}
.piece-card__art img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The configurable tag, in the boutique's hand: paper and a hairline, and
   top-RIGHT — the bags are shot left-of-centre on their plinths, and top-left
   sat on the product. */
.piece-card__tag {
  position: absolute;
  top: 10px;
  right: 10px;
  padding: 3px 7px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.92);
  color: var(--ink);
  font-size: 0.54rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.piece-card__body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.85rem;
}
.piece-card__name {
  color: var(--ink);
  font-family: var(--serif);
  font-size: 1.1rem;
  font-weight: 400;
  /* Two lines, reserved and capped. Reserved so a one-line name leaves the
     room a two-line one takes, capped so a third line cannot appear: the
     summary then begins at the same height on every card. Without it, the one
     name in Tops that wraps pushed its own summary a line below its
     neighbours' while the card around it was being stretched to match them —
     equal boxes with the copy inside them out of step, which reads worse than
     unequal boxes. */
  line-height: 1.3;
  min-height: 2.6em;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
/* Two lines, and then an ellipsis. The catalog's `summary` is the product
   page's description — five or six clauses naming the cut, the cloth, the
   pockets and the workshop — and printed whole on a card it buried the name
   under a paragraph and left the grid with rows of wildly different heights.
   Clamped rather than shortened in the catalog, because the long version is
   right where it is read: the card's job is to say which piece this is, and
   the page's is to describe it. Every summary here opens by naming the piece
   and its cloth, so two lines is the sentence that matters. */
.piece-card__summary {
  color: var(--muted);
  font-size: 0.82rem;
  line-height: 1.45;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* No price on a collection card. The studio's configurators print "Starting
   from €96,400" under the summary, which is right for a showroom of eight
   cars; the maison names the piece and lets the product page carry the figure,
   and this page has never had a price in the grid. */

/* The menu button, in the boutique's language: a quiet outline like the
   saved-designs button beside it, not the studio's filled glass pill. The
   sheet it opens is the shared one and inherits the .dialog rules above. */
#picker .menu__button {
  padding: 6px 11px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: transparent;
  color: var(--ink);
  font-family: var(--sans);
  font-size: 0.6rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
#picker .menu__button:hover { border-color: var(--ink); background: #fff; }
#picker .menu__sheet {
  border: 1px solid var(--line);
  border-radius: 2px;
  background: #fff;
  color: var(--ink);
}
#picker .menu__item { color: var(--ink); }
#picker .menu__item:hover:not(:disabled) { background: var(--fill); }
#picker .menu__hint { color: var(--muted); }
#picker .menu__glyph { color: var(--ink); }

/* The saved-designs row. `SAVED:` in the same small caps the config bar uses,
   and the resume pills as quiet outlines rather than filled studio chips. */
#picker .picker__saved { align-items: center; }
/* The two catalog actions — saved designs, Configure together — sitting where
   the overflow menu used to. Flex here because the container was a menu root
   before, which brought its own layout; now it holds the buttons directly. */
#picker #picker-menu { display: flex; gap: 0.4rem; }

/* The "no catalog" card, on the same field. */
#failure .failure__card {
  border: 1px solid var(--line);
  border-radius: 2px;
  background: #fff;
  backdrop-filter: none;
}
#failure .failure__card h2 { font-family: var(--serif); font-weight: 400; color: var(--ink); }
#failure .failure__card p { color: var(--muted); }
#failure .failure__card label { color: var(--muted); }
#failure .failure__card input {
  border: 1px solid var(--line);
  border-radius: 2px;
  background: #fff;
  color: var(--ink);
}
#failure .failure__card input:focus { border-color: var(--ink); }
#failure .failure__card .btn--primary {
  border: 1px solid var(--ink);
  border-radius: 2px;
  background: var(--ink);
  color: #fff;
  font-family: var(--sans);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* ── Save / Send / Invite ────────────────────────────────────────
   In the band over the bag, column 1 — the one the pill (column 2) and the
   preview strip (column 3) leave empty. Third position for these: the buy
   column was below the fold and nobody found them, and the stage's top-left
   corner sat them on the product beside the header. The band is where this
   page already puts controls that must not cover the bag.

   Dressed as this page's own quiet actions rather than the studio's buttons:
   frosted pills matching the one beside them, tracked-out small caps. Not
   `.btn` — that class belongs to configurator.css now, and borrowing it would
   drag the studio's radius and purple into the boutique. */
/* Directly over the pill and centred on it, not pinned to the band's left
   edge: the two are one control stack — what to configure, and what to do with
   it once configured — so they share a centre line. The preview strip keeps the
   right column and spans both rows so it stays bottom-right. */
.build-actions {
  grid-column: 1;
  grid-row: 1 / -1;
  justify-self: start;
  align-self: end;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px;
  min-width: 0;
}
.build-action {
  padding: 4px 9px;
  border: 1px solid rgba(26, 26, 26, 0.08);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.72);
  backdrop-filter: blur(8px);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.build-action:hover:not(:disabled) {
  background: #fff;
  border-color: rgba(26, 26, 26, 0.28);
}
.build-action--quiet { color: var(--muted); }
/* Disabled until a piece is chosen. Stated rather than hidden: the actions are
   part of what this page offers, and a control that only appears once you have
   done something else is harder to find than one that was always there. */
.build-action:disabled {
  color: #bdb6aa;
  cursor: default;
  background: rgba(255, 255, 255, 0.45);
  border-color: rgba(26, 26, 26, 0.05);
}

/* The band's own breakpoints, which these have to follow — see the three
   @media blocks under .cbar. The pill drops to the row below in every one of
   them; the actions stay centred on whatever column it lands in. */
@media (max-width: 999px) {
  .build-actions { grid-column: 1; }
}
/* On phones the band hangs below the stage and stacks, so the actions are a
   second row rather than a first one.
   ALL THREE are here. Invite used to be dropped on this width, on the worry that
   a fourth pill would wrap over a 375px screen — but there is no fourth, and the
   three measure 166px of the 359px the band has inside its padding. Dropping it
   also took away the only way to start a shared session from a phone, which is
   the device a client is most likely to be holding when they want someone else
   to see the bag. */
@media (max-width: 620px) {
  .build-actions { grid-column: 1; grid-row: 2; }
}

/* ── Notices, and the session badge ─────────────────────────────────────────
   `notify()` in common/js/configurator/ui.js writes into #notices, and its
   `.notice` is the studio's: a near-black pill. Left alone it inherited THIS
   page's ink for its text colour — dark on dark, which is why the bar over the
   band read as a black slab with nothing in it.

   This page already has a toast, `.toast` above, for "Added to your shopping
   bag". A second notification with its own look would be one idea in two
   costumes, so the notice takes the toast's: ink, white text, square, bottom
   right, clear of the band and of the sticky buy bar. */
/* Bottom LEFT, not right. Right is where the preview strip ends up (measured:
   it runs to y 798-838, and a notice at bottom:90px lands 816-868), and the
   centre is the pill — so the band's empty left column is the one place a
   notice can appear without covering a control. */
.notices {
  left: 20px;
  right: auto;
  bottom: 90px;
  transform: none;
  z-index: 1500;
  align-items: flex-start;
  max-width: min(360px, calc(100vw - 40px));
}
.notices .notice {
  border: 0;
  border-left: 2px solid rgba(255, 255, 255, 0.28);
  border-radius: 0;
  background: var(--ink);
  color: #fff;
  padding: 0.85rem 1.1rem;
  font-family: var(--sans);
  font-size: 0.78rem;
  line-height: 1.45;
  backdrop-filter: none;
}
/* The three kinds keep their meaning, restated against ink rather than glass. */
.notices .notice--info { border-left-color: rgba(255, 255, 255, 0.4); }
.notices .notice--warn { border-left-color: #e0a558; }
.notices .notice--blocked { border-left-color: #e08a8a; }

/* Both have to clear the buy bar once it slides up. --sticky-bar-h is written
   by fashion/js/ui/page-bars.js; the fallback is the taller wrapped case, so a
   failure over-clears rather than collides. */
body:has(.sticky-bar.show) .notices { bottom: calc(var(--sticky-bar-h, 106px) + 1rem); }

/* ── The session badge, under the viewer ────────────────────────────────────
   configurator.css pins this to the top centre of the WINDOW, which on this
   page is the middle of the AVELINE masthead. It now sits in the hero, after the
   stage, and flows in the document — so it reads as a line about the bag above
   it, and the invite link inside it can be selected and copied by hand. That
   matters more than it sounds: navigator.clipboard needs a secure context, and
   this page is served over plain HTTP from a laptop on the Wi-Fi as often as
   not. */
.session-badge {
  position: static;
  inset: auto;
  transform: none;
  z-index: auto;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin: 0 auto;
  padding: 0.7rem clamp(1rem, 3vw, 2.2rem);
  max-width: 1280px;
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
  background: none;
  backdrop-filter: none;
  color: var(--muted);
  font-family: var(--sans);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.session-badge[hidden] { display: none; }
/* The link itself: a real field, readable and selectable, in the mono this
   page uses nowhere else because nothing else on it is a URL. */
.session-badge input {
  flex: 1;
  min-width: 0;
  padding: 6px 9px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--fill);
  color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
  font-size: 0.72rem;
  letter-spacing: 0;
  text-transform: none;
}
.session-badge input:focus { outline: none; border-color: var(--ink); }

/* ==========================================================================
   THE SAVE / SEND / SAVED-DESIGNS DIALOGS, IN THIS PAGE'S CLOTHES

   These are built by common/js/configurator/designs-ui.js and come dressed as
   the studio: near-black glass, Inter, a purple primary. The catalog screen
   above KEEPS that look on purpose — choosing a piece is the same act on every
   catalog-driven page. A dialog is not: it opens over the boutique, about
   this bag, with this bag's name and price in it, and a dark sheet across a
   white product page reads as a different site.

   So the markup and the behaviour stay the studio's and only the surface
   changes. Scoped under `.dialog`, which is a <dialog> in the top layer, so
   none of it can reach the page or the picker.
   ========================================================================== */

.dialog {
  width: min(460px, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: #fff;
  color: var(--ink);
  font-family: var(--sans);
  box-shadow: 0 24px 60px rgba(26, 26, 26, 0.14);
}
.dialog--wide { width: min(620px, calc(100vw - 2rem)); }
/* A light scrim. The studio's is near-opaque black, which on this page hides
   the product the dialog is about. */
.dialog::backdrop {
  background: rgba(240, 237, 231, 0.72);
  backdrop-filter: blur(3px);
}

.dialog__head {
  align-items: baseline;
  padding: 1.2rem 1.4rem 0.9rem;
  border-bottom: 1px solid var(--line);
  background: none;
}
/* The maison's voice: the serif, at the weight the details column uses for a
   product title, because that is what these dialogs are naming. */
.dialog__title {
  color: var(--ink);
  font-family: var(--serif);
  font-size: 1.15rem;
  font-weight: 400;
  letter-spacing: 0;
}
.dialog__head .btn--icon {
  padding: 2px 6px;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 0.9rem;
}
.dialog__head .btn--icon:hover { background: none; color: var(--ink); }

.dialog__body { padding: 1.2rem 1.4rem; }
.dialog__note { color: var(--muted); font-size: 0.8rem; line-height: 1.5; }
.dialog__note--bad { color: #a4303f; }

/* Field labels in the page's own small-caps, the same line the config bar and
   the footer headings use. */
.dialog .field label {
  color: var(--muted);
  font-size: 0.58rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.dialog .field input {
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: #fff;
  color: var(--ink);
  font-family: var(--sans);
  font-size: 0.86rem;
}
.dialog .field input:focus { outline: none; border-color: var(--ink); }

.dialog__actions {
  padding: 0 1.4rem 1.2rem;
  border-top: 0;
}
/* Ink, not purple. One filled action per dialog and the rest quiet, which is
   how the buy column reads too. */
.dialog .btn {
  padding: 9px 16px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: transparent;
  color: var(--ink);
  font-family: var(--sans);
  font-size: 0.64rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.dialog .btn:hover:not(:disabled) { border-color: var(--ink); background: transparent; }
.dialog .btn--primary {
  border-color: var(--ink);
  background: var(--ink);
  color: #fff;
}
.dialog .btn--primary:hover:not(:disabled) { background: #000; border-color: #000; }
.dialog .btn--small { padding: 6px 11px; font-size: 0.6rem; }
.dialog .btn--danger { color: #a4303f; border-color: rgba(164, 48, 63, 0.3); }
.dialog .btn--danger:hover:not(:disabled) { border-color: #a4303f; }

/* ── The send sheet ── */
/* The hero sits on the page's own soft field rather than on black: it is a
   photograph of a bag on a white ground, and a black mat around it reads as a
   different studio than the one it was shot in. */
.dialog .send__hero {
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--fill);
}
/* The rows. These were written against `.send__action` / `.send__label` /
   `.send__hint`, which the shared sheet renamed to `.sheet-action*` when the
   send panel and the session panel became one component — so every rule below
   stopped matching and the panel fell back to the studio's dark theme on white
   paper: labels at `color: #fff` (contrast 1.0 against this dialog — the titles
   were simply not there), hints at --bo-faint, glyphs in the studio's purple,
   and the row dividers in white-at-12%. Same failure as the picker's
   `.boat-card`; the names are the only thing that had to change.

   There is no `--primary` variant any more. The old block inked the
   recommended row the way a primary button is inked, and nothing in the sheet
   sets such a class today, so it went rather than being aimed at a guess. */
.dialog .sheet-group__card {
  border: 1px solid var(--line);
  border-radius: 2px;
  background: #fff;
}
.dialog .sheet-action {
  border-top-color: var(--line);
  background: transparent;
  color: var(--ink);
  text-align: left;
}
.dialog .sheet-action:hover:not(:disabled) { background: var(--fill); }
.dialog .sheet-action__label {
  color: var(--ink);
  font-size: 0.82rem;
  font-weight: 500;
}
.dialog .sheet-action__hint { color: var(--muted); font-size: 0.74rem; }
/* Ink, not the studio's purple — the same correction the dialog's buttons
   above already make, and the reason the focus ring is restated too: a violet
   rectangle round a row is the one thing on this panel that was visibly not
   the maison's. */
.dialog .sheet-action__glyph { color: var(--ink); }
.dialog .sheet-action:disabled .sheet-action__glyph { color: var(--bo-faint); }
.dialog .sheet-action:focus-visible { outline-color: var(--ink); }
.dialog .sheet-action b { font-weight: 500; }

/* ── Saved designs ── */
.dialog .designs__row {
  border: 1px solid var(--line);
  border-radius: 2px;
  background: #fff;
}
.dialog .designs__row:hover { background: var(--fill); }
.dialog .designs__name { font-family: var(--serif); font-size: 1rem; font-weight: 400; }
.dialog .designs__meta { color: var(--muted); font-size: 0.74rem; }
.dialog .designs__badge {
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.dialog .designs__badge--warn { border-color: rgba(164, 48, 63, 0.4); color: #a4303f; }

/* The link, and anything else read back rather than typed. */
.dialog input[readonly] {
  border: 1px solid var(--line);
  background: var(--fill);
  color: var(--muted);
  font-family: var(--bo-mono, ui-monospace, Menlo, monospace);
  font-size: 0.72rem;
}
