/* ============================================================
   2BE CLOTHING WORLD — PHASE 1 SITE STYLES
   Derived from styleguide.html (Phase 0). Cite guide sections in comments.
   Production pages use Archivo (display) + Inter (body) only — the
   IBM Plex Mono "spec" voice is guide chrome and never ships (Phase 0 §03).
   ============================================================ */

/* ============ TOKENS (guide §01–02) ============ */
:root{
  --dark:#0D0D0D;            /* editorial surface — premium capsule line */
  --light:#FFFFFF;           /* commerce surface — mainline */
  --accent:#6B2131;          /* deep oxblood [DUMMY: brand color TBD] — <=5% of any viewport */
  --ink-on-dark:#F5F5F5;
  --ink-on-light:#111111;
  --muted:#8A8A8A;

  --t-fast:300ms;
  --t-slow:500ms;
  --ease:cubic-bezier(0.22,1,0.36,1);

  --container:1280px;
  --pad-x:24px;
  --nav-h:72px;

  /* ============ STACKING SCALE (Phase 4) ============
     Before this, every overlay picked its own z-index by hand (2, 60, 61,
     100, 120, 121, 150, 200) and the nav panel had none at all — it merely
     inherited .nav's 100, which put it BELOW the cart drawer. One named
     scale now, so a new overlay picks a layer instead of guessing a number.

     Ordering decision — THE NAV PANEL OUTRANKS THE CART DRAWER. The panel
     is a whole-screen site map: a navigational context switch, not a
     dialog. A bag panel floating over a site map reads as a bug. The two
     are also mutually exclusive in site.js (opening either closes the
     other), so the ambiguous both-visible state cannot occur; this scale
     is the belt to that braces. */
  --z-raised:2;          /* in-flow overlays: tile labels, card badges  */
  --z-sticky:60;         /* PLP filter bar                             */
  --z-sticky-panel:61;   /* its dropdowns                              */
  /* PDP sticky purchase bar (shop.css .buybar), mobile only. Below the
     assistant, whose panel covers the same bottom-right area and which the
     visitor has deliberately opened; below consent and the drawer for the
     reasons each of those states below. Above the PLP filter bar, which it
     never shares a page with anyway. */
  --z-buybar:90;
  --z-nav:100;           /* the fixed nav bar                          */
  /* Phase 9 style assistant — above the page and the sticky filter bar,
     BELOW the cart drawer and the site map. Both of those deliberately own
     the screen; the assistant deliberately does not (it is a side panel you
     keep browsing behind, so it also starts below the nav bar rather than
     covering it). site.js closes it when the site map takes over. */
  --z-assistant:110;
  /* Consent sits ABOVE the page and the assistant, but BELOW any takeover.
     A notice is never more urgent than the thing the visitor just opened:
     at toast level it painted over the cart drawer and made Checkout
     unclickable, which e2e2 caught by hanging on a pay button. */
  --z-consent:115;
  --z-drawer-scrim:120;
  --z-drawer:121;        /* cart drawer                                */
  --z-navpanel:130;      /* site-map overlay — above the drawer        */
  --z-navbar-open:131;   /* the bar itself, so Close stays clickable   */
  --z-toast:150;
  --z-skiplink:200;      /* must outrank everything, always            */

  /* Loading-skeleton fills (see LOADING SKELETONS below). Two steps of one
     neutral: the block, and the tone it pulses to. */
  --sk-a:#E9EAED;
  --sk-b:#DCDEE3;
}
@media (min-width:1024px){ :root{ --pad-x:48px; } }

/* Surface scopes — components read these, never raw hexes (guide §01) */
.s-dark{
  --bg:var(--dark); --ink:var(--ink-on-dark);
  --hairline:rgba(245,245,245,.16);
  --sk-a:#1E1F24; --sk-b:#2A2C33;
  background:var(--bg); color:var(--ink);
}
.s-light{
  --bg:var(--light); --ink:var(--ink-on-light);
  --hairline:rgba(17,17,17,.14);
  background:var(--bg); color:var(--ink);
}

/* ============ BASE ============ */
*,*::before,*::after{ box-sizing:border-box; margin:0; padding:0; }
html{ scroll-behavior:smooth; }
body{
  font-family:'Inter','Helvetica Neue',Arial,sans-serif;
  font-size:16px; line-height:1.6;
  background:var(--dark); color:var(--ink-on-dark);
  -webkit-font-smoothing:antialiased;
}
img,svg,video{ display:block; max-width:100%; height:auto; }
button,input{ font:inherit; color:inherit; border-radius:0; }
a{ color:inherit; text-decoration:none; }
ul{ list-style:none; }
::selection{ background:var(--accent); color:var(--ink-on-dark); }
:focus-visible{ outline:2px solid var(--ink,#F5F5F5); outline-offset:3px; }
/* JOINED CONTROLS. Where a field and its button share an edge — the
   newsletter form, the Gold Card box — a 3px OUTSET ring is drawn on top of
   the neighbour, so focusing the input paints a line through the button.
   Inset the ring for those pairs only: it still reads as a ring, and it
   stays inside the control it belongs to. */
.newsform input:focus-visible,
.newsform button:focus-visible,
.gateform input:focus-visible,
.gateform button:focus-visible{ outline-offset:-2px; }

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{ transition-duration:.01ms!important; animation-duration:.01ms!important; }
  html{ scroll-behavior:auto; }
}

.skiplink{
  position:absolute; left:var(--pad-x); top:-100px; z-index:var(--z-skiplink);
  display:inline-flex; align-items:center; min-height:44px;
  padding:12px 24px; background:var(--light); color:var(--ink-on-light);
  font-size:12px; font-weight:600; letter-spacing:.16em; text-transform:uppercase;
}
.skiplink:focus{ top:12px; }

/* ============ TYPE ROLES (guide §03) ============ */
.display{
  font-family:'Archivo','Arial Narrow','Helvetica Neue',sans-serif;
  font-stretch:66%; font-weight:640;
  text-transform:uppercase; letter-spacing:-0.015em; line-height:.95;
}
.d-96{ font-size:clamp(48px,9vw,96px); letter-spacing:-0.02em; }
.d-64{ font-size:clamp(36px,6vw,64px); }
.d-40{ font-size:clamp(28px,4vw,40px); }
.d-28{ font-size:28px; }
.lead{ font-size:20px; line-height:1.5; }
.small{ font-size:14px; }
.label{
  font-size:12px; font-weight:600; letter-spacing:.16em;
  text-transform:uppercase; line-height:1;
}
.placeholder{ color:var(--muted); } /* renders [PLACEHOLDER-COPY]/[DUMMY] strings quietly */
.muted{ color:var(--muted); }

/* ============ LAYOUT (guide §04 — 8px grid) ============ */
.container{ max-width:var(--container); margin-inline:auto; padding-inline:var(--pad-x); }
.band{ padding-block:64px; }
@media (min-width:1024px){ .band{ padding-block:120px; } }

/* Page-title band. Lives here, not in shop.css: /account/, /find-us/ and
   /2be-trade/ all use it and none of them load shop.css, so defining it
   there left three pages with zero top offset and their first heading
   rendered underneath the fixed nav (found via a Phase 7 screenshot — the
   sign-out button was literally unclickable because the nav covered it).
   The nav is fixed at --nav-h, so anything at the top of <main> has to
   clear it. */
.band-head{ padding-block:calc(var(--nav-h) + 24px) 24px; }

.shead{
  display:flex; flex-wrap:wrap; align-items:flex-end;
  justify-content:space-between; gap:16px; margin-bottom:48px;
}
@media (min-width:1024px){ .shead{ margin-bottom:64px; } }
.shead__eyebrow{ display:block; margin-bottom:16px; color:var(--muted); }

/* ============ NAV (guide §06.2) ============ */
.nav{
  position:fixed; inset:0 0 auto 0; z-index:var(--z-nav);
  display:flex; align-items:center; justify-content:space-between; gap:24px;
  height:var(--nav-h); padding-inline:var(--pad-x);
  color:var(--ink-on-dark);
  background:transparent; border-bottom:1px solid transparent;
  transition:background-color 400ms var(--ease), border-color 400ms var(--ease);
}
.nav.is-solid,.nav.is-open{ background:var(--dark); border-bottom-color:rgba(245,245,245,.16); }
/* Pages with no hero start solid rather than transparent. Lives here, not
   in shop.css: /account/, /find-us/ and /2be-trade/ all set this class and
   none of them load shop.css, so their nav rendered light-on-white and was
   effectively invisible (found alongside the .band-head bug in Phase 7). */
.nav--solid{ background:var(--dark); border-bottom-color:rgba(245,245,245,.16); }

/* Error pages. HERE, not pages.css — 404.html and 500.html are the ONLY
   users of .errpage, and neither loads pages.css, so defining it there
   left both rendering with no vertical centring, no lead spacing and no
   gap between the action buttons ("Home" and "Find us" ran together).
   Same defect as .band-head above, found the same way: by looking at a
   screenshot. Pulling 20KB of editorial CSS onto an error page to fix
   three rules would have been the wrong trade.

   svh, not vh: on a phone `vh` is the viewport with the browser chrome
   subtracted out of the picture entirely, so a vh-sized box is taller than
   the space actually on screen and its height changes as the URL bar hides
   and returns mid-scroll. svh is the small-viewport unit that means what
   this rule intends. (Same reasoning as .hero's 100svh.) */
.errpage{ min-height:52svh; display:flex; align-items:center; }
.errpage .lead{ max-width:52ch; margin:16px 0 32px; }
/* 24px, not 8. This row mixes a PADDED primary (.btn is 0 32px) with
   .btn--ghost, which is deliberately padding:11px 0 — an underlined text
   link, not a box. At 8px the two ghosts abutted and "Home  Find us" read
   as a single phrase. The gap has to clear the narrowest item, not the
   widest. Stays on the 8px grid. */
.errpage__actions{ display:flex; gap:24px; flex-wrap:wrap; align-items:center; }
/* ---------- MINIMUM TAP TARGET ----------
   44x44 is the floor for a DISCRETE CONTROL — a button, an icon control, a
   nav or footer link, a form control, a text CTA that is the whole of its
   own line. Several were well under it: this wordmark was 32x23, the bag
   and menu buttons 31 and 33 wide, the rail arrows 40x40, the filter chips
   40 tall, the currency toggle 32, the staff buttons 37.

   A LINK INSIDE RUNNING TEXT is deliberately exempt. WCAG 2.5.8 carves out
   exactly that case, and padding a link that sits mid-sentence to 44px
   breaks the line box around it — the one place the rule makes the page
   worse. So is a small link whose destination is ALSO reachable from a
   large one on the same page (2.5.8's "Equivalent" exception) — the product
   card's title sits under a card-sized image link to the same product.
   tests/uiaudit.mjs decides both by measurement, not by an allowlist.

   Negative margin below keeps the wordmark's optical position identical
   while its hit box grows to 44. */
.nav__brand{
  font-size:24px; display:inline-flex; align-items:center;
  min-height:44px; min-width:44px; justify-content:center;
  padding-inline:6px; margin-inline:-6px;
}
.nav__links{ display:none; gap:32px; }
/* inline-block rather than a flex box so ::after (the underline) keeps
   sitting on the padding-box bottom, where it always did — only the top
   padding grows, which the underline cannot see. */
.nav__link{
  position:relative; display:inline-block; text-align:center;
  min-width:44px; padding:20px 0 12px; color:var(--ink-on-dark);
}
.nav__link::after{
  /* 6px under the text, which is where it sat before the box grew to 44. */
  content:""; position:absolute; left:0; bottom:6px; width:100%; height:2px;
  background:var(--accent); transform:scaleX(0); transform-origin:left;
  transition:transform var(--t-fast) var(--ease);
}
.nav__link:hover::after,.nav__link[aria-current="page"]::after{ transform:scaleX(1); }
.nav__util{ display:flex; gap:24px; align-items:center; }
.nav__toggle,.nav__bag{
  display:inline-flex; align-items:center; justify-content:center;
  height:48px; min-width:44px;
  background:transparent; border:0; cursor:pointer; color:inherit;
}
/* The top bar keeps only the four primary destinations at every width.
   Everything else lives in the site-map panel below, which is now available
   on desktop too — one canonical IA surface instead of a mobile disclosure
   plus a separate desktop mega-menu to keep in sync. This is also what
   stops the bar overflowing as Phases 5-9 add Sale / Wholesale / Find Us /
   Account: those routes never join the bar. */
@media (min-width:900px){ .nav__links{ display:flex; } }

/* ============ SITE-MAP PANEL (Phase 4) ============
   Was: a short disclosure squeezed between the bar and the page.
   Now: a full-height overlay that reads as its own screen. It is NOT an
   inner scroll container — the grouped grid is sized to fit the viewport
   (guarded by tests/e2e4.mjs check 3), because a nested scrollbar inside a
   fixed overlay is the exact trap this rebuild exists to remove. */
.nav__panel{
  position:fixed; inset:var(--nav-h) 0 0 0; z-index:var(--z-navpanel);
  display:none;
  background:var(--dark); color:var(--ink-on-dark);
  padding:24px var(--pad-x) calc(24px + env(safe-area-inset-bottom,0px));
  overflow:hidden;              /* never a nested scroller — see above */
  overscroll-behavior:contain;
}
.nav.is-open .nav__panel{ display:block; }
/* While open the bar outranks the panel so Close stays reachable, and the
   whole nav outranks the cart drawer (see the stacking scale in TOKENS). */
.nav.is-open{ z-index:var(--z-navbar-open); }

.navmap{
  display:grid; gap:24px 32px;
  grid-template-columns:repeat(2,minmax(0,1fr));
  max-width:var(--container); margin-inline:auto; height:100%;
  align-content:start;
}

/* Desktop resolves to a mega-menu instead of the full-screen takeover:
   at 1280px the takeover left ~600px of empty black under 12 links, which
   is vacancy, not restraint (guide §05 — measured, never busy). Same markup,
   same groups; the panel just stops at its content and dims the page below. */
@media (min-width:900px){
  .navmap{ grid-template-columns:repeat(4,minmax(0,1fr)); gap:32px 48px; height:auto; }
  .nav__panel{
    inset:var(--nav-h) 0 auto 0;
    border-bottom:1px solid rgba(245,245,245,.16);
    padding-block:32px 40px;
  }
  /* Scrim over the page behind the menu. A pseudo-element on .nav keeps it
     out of the markup on all ten pages; site.js closes on a click here. */
  .nav.is-open::after{
    content:""; position:fixed; inset:var(--nav-h) 0 0 0;
    z-index:calc(var(--z-navpanel) - 1);
    background:rgba(13,13,13,.6);
  }
}
.navmap__group{ min-width:0; }
.navmap__title{
  display:block; margin-bottom:8px; padding-bottom:8px;
  color:var(--muted); border-bottom:1px solid rgba(245,245,245,.16);
}
.navmap__list{ display:flex; flex-direction:column; }
/* 44px min target — thumb-friendly per study.md §4.7 (mobile-first). */
.navmap__list a{
  display:flex; align-items:center; gap:8px;
  min-height:44px; color:var(--ink-on-dark);
  font-size:15px; line-height:1.2;
}
.navmap__list a:hover{ color:var(--muted); }
/* Routes Phases 5-9 will build. Marked, not hidden: an unbuilt destination
   that looks identical to a live one is a small lie to the visitor. */
.navmap__soon{
  font-size:10px; letter-spacing:.14em; text-transform:uppercase;
  color:var(--muted); border:1px solid rgba(245,245,245,.24);
  padding:2px 6px; flex:none;
}

/* ============ MEDIA FRAMES ============ */
/* Placeholder <img> stand-ins live in fixed-ratio frames; object-fit keeps
   the swap seamless when brand photography arrives. */
.frame{ position:relative; overflow:hidden; background:#121317; }
.frame > img,.frame > video{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  transform:scale(1);
  transition:transform 400ms var(--ease); /* hover: 1.00 -> 1.04 (guide §05) */
}
.frame--4x5{ aspect-ratio:4/5; }
.frame--16x9{ aspect-ratio:16/9; }

/* ============ LOADING SKELETONS (assets/js/site.js — window.Skeleton) ============
   Every async surface on this site used to render out of an EMPTY container.
   In mock mode that is invisible (the catalog is a local object), but in
   live mode `StorefrontReady` waits on a real round trip, so /shop/ was a
   blank white screen for its whole duration — on the mid-range Android and
   the mobile data that are most of this audience (study.md §4.7).

   THE SKELETON IS BUILT OUT OF THE REAL COMPONENT CLASSES, never a
   hand-measured copy of them. A skeleton card is a real `.card` with a real
   `.card__media.frame--4x5` and a real `.card__body`; only the leaf text and
   the image are swapped for `.sk` blocks. That way the geometry is computed
   by the same rules as the thing it stands in for and cannot drift when
   somebody edits the card — which is the failure mode that makes skeletons
   worse than the blank they replaced, because a skeleton of the wrong height
   IS a layout shift.

   `.sk--text` is `height:1em` inside the real type role, so the line box is
   the strut of that role and matches the real text exactly wherever
   line-height >= 1 (true for every role used here). */
/* OPAQUE and surface-scoped, NOT the translucent --hairline these started
   out using. Two bugs came out of translucency, both found by looking at
   screenshots rather than by any test:
     - the frames a skeleton lands in do not agree on a background
       (`.card__media` is #E9EAED, a bare `.frame` — which is what
       `.pdp__gallery` uses — is #121317), so one wash read as a grey
       placeholder in the grid and as a solid black box on the PDP;
     - pulsing OPACITY reveals whatever is behind, so the same animation was
       invisible over a matching parent and muddy over a dark one.
   Opaque tokens plus a background-color pulse make both predictable, and
   .s-dark restates them so the premium capsule grid gets dark placeholders
   on its dark surface instead of two light slabs. */
.sk{ display:block; background:var(--sk-a); }
/* Media: absolutely filled, because every frame on this site is a
   position:relative ratio box whose children are inset:0. No background of
   its own — it inherits the opaque one from .sk above, which is the whole
   reason that one is opaque. */
.sk--fill{ position:absolute; inset:0; }
.sk--text{ display:inline-block; height:1em; max-width:100%; vertical-align:middle; }
/* Widths are in em and therefore INTRINSIC, which is load-bearing rather
   than a style choice. `.card__body` is a flex row whose items carry
   min-width:0, so a flex item sized only by percentage-width children
   resolves to zero — a percentage contributes nothing to max-content. The
   first version of this used 40/60/80% and every text bar measured 0px
   wide while reporting the correct height, so the card came up a line
   short and the grid still shifted. An em width gives the bar real
   max-content, sized to the type role it sits in; max-width keeps it
   inside the 159px card at 390px.
   Modifier classes rather than inline style="width:…", so the eventual CSP
   (deploy/nginx-2be.conf) never needs 'unsafe-inline' in style-src. */
.sk--w4{ width:4em; }
.sk--w7{ width:7em; }
.sk--w10{ width:10em; }

/* Motion is opt-IN via html.motion, set by the one-line head script only
   when JS is on and prefers-reduced-motion is not "reduce" — so reduced
   motion gets a static block with no second kill switch needed here. */
html.motion .sk{ animation:sk-pulse 1400ms var(--ease) infinite; }
@keyframes sk-pulse{ 0%,100%{ background-color:var(--sk-a); } 50%{ background-color:var(--sk-b); } }

/* STALL, the third state. Not a rejected promise — `boot()` in
   storefront-live.js catches everything and falls back to the mock, so
   products() effectively never rejects. What it cannot catch is a fetch
   that never settles: there is no timeout anywhere in the fetch layer, so
   a stalled connection left `whenReady` pending forever. That used to show
   a blank page; with skeletons it would shimmer forever, which is worse.
   window.Skeleton gives up after its own deadline and says so.
   `grid-column:1/-1` so it spans whatever grid it lands in. */
.skstall{ grid-column:1/-1; text-align:center; padding-block:48px; }
.skstall p{ margin-bottom:24px; color:var(--muted); }

/* ============ FIELD ERRORS (assets/js/site.js — window.FormErrors) ============
   The accent is this site's only red, and §09 caps it at 5% of any
   viewport — a one-line message is nowhere near that, and a refusal is
   exactly the moment the accent is doing real work rather than decoration.

   Colour is NOT the only signal: the field carries aria-invalid, the
   message is tied to it by aria-describedby and spoken via role="alert",
   and the border below changes weight as well as hue. */
.fielderr{ display:block; color:var(--accent); margin-top:4px; }
.fielderr:empty{ display:none; }

/* THE DOUBLED ATTRIBUTE IS LOAD-BEARING, not a typo. Every field rule on
   this site sets the `border` SHORTHAND from a single class — `.wform
   input`, `.acctform input`, `.gateform input`, `.memberform input` — which
   is (0,1,1) and lives in pages.css/premium.css/shop.css, all of which load
   AFTER site.css. A plain [aria-invalid="true"] is (0,1,0) and lost to
   every one of them, so the first version of this rule changed nothing at
   all and the invalid state was carried by the message alone. Repeating the
   attribute makes it (0,2,0), which beats a class+element without needing
   !important and without site.css having to know each form's selector.
   Verified by measuring the computed border, not by reading the cascade. */
[aria-invalid="true"][aria-invalid]{
  border-color:var(--accent);
  /* Weight as well as hue, so the state does not depend on colour alone
     (WCAG 1.4.1). box-sizing:border-box is global, so this costs no shift. */
  border-width:2px;
}
/* On the dark surfaces the oxblood sits too close to the ground to read as
   either text or an edge, so the ink colour carries both there. */
.s-dark .fielderr{ color:var(--ink-on-dark); }
.s-dark [aria-invalid="true"][aria-invalid]{ border-color:var(--ink-on-dark); }

/* ============ BUTTONS (guide §06.1) ============ */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:12px;
  height:48px; padding:0 32px;
  font-family:'Inter',sans-serif; font-size:12px; font-weight:600;
  letter-spacing:.16em; text-transform:uppercase; line-height:1;
  background:transparent; color:var(--ink);
  border:1px solid transparent; border-radius:0; cursor:pointer;
  transition:background-color var(--t-fast) var(--ease),
             color var(--t-fast) var(--ease),
             border-color var(--t-fast) var(--ease),
             filter var(--t-fast) var(--ease);
}
.btn--primary{ background:var(--accent); color:var(--ink-on-dark); }
.btn--primary:hover{ filter:brightness(1.2); }
.btn--secondary{ border-color:var(--ink); }
.btn--secondary:hover{ background:var(--ink); color:var(--bg); }
.btn--ghost{ position:relative; height:auto; min-height:44px; padding:11px 0 19px; }
.btn--ghost::after{
  content:""; position:absolute; left:0; bottom:11px; width:100%; height:1px;
  background:currentColor; transform:scaleX(0); transform-origin:left;
  transition:transform var(--t-fast) var(--ease);
}
.btn--ghost:hover::after{ transform:scaleX(1); }

/* ============ HERO (home + story + collection banner) ============ */
.hero{
  position:relative; min-height:100svh;
  display:flex; flex-direction:column; justify-content:flex-end;
  padding-top:120px; padding-bottom:calc(96px + var(--consent-h,0px));
}
.hero--banner{ min-height:60svh; padding-bottom:calc(64px + var(--consent-h,0px)); } /* collection */
.hero__media{ position:absolute; inset:0; overflow:hidden; }
.hero__media > img,.hero__media > video{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
}
.hero__media > video{ opacity:0; transition:opacity var(--t-slow) var(--ease); }
.hero__media.has-video > video{ opacity:1; }
/* Legibility scrim — bottom third only, keeps the image honest */
.hero__media::after{
  content:""; position:absolute; inset:0;
  background:linear-gradient(180deg,rgba(13,13,13,.25) 0%,transparent 30%,transparent 55%,rgba(13,13,13,.72) 100%);
}
.hero__content{ position:relative; z-index:2; width:100%; }
.hero__inner{ animation:rise var(--t-slow) var(--ease) both; }
@keyframes rise{ from{ opacity:0; transform:translateY(16px); } to{ opacity:1; transform:none; } }
.hero__rule{ width:48px; height:2px; background:var(--accent); margin-bottom:24px; }
.hero__sub{ max-width:560px; margin-block:24px 32px; color:var(--ink-on-dark); }

/* PHONE HERO BUDGET — why these numbers exist.
   .hero is `justify-content:flex-end` inside `min-height:100svh`, so its
   content hangs off the BOTTOM of the hero box. Reserving room for the fixed
   consent bar by growing padding-bottom (see --consent-h below) only works
   while the hero box is exactly 100svh tall: then its bottom edge IS the
   viewport's bottom edge, and padding pushes content up off the bar.
   The moment the content is tall enough that
       content + padding-top + padding-bottom > 100svh
   the box grows instead, its bottom edge goes off-screen, and the reserved
   space goes with it — the padding stops moving the content at all. That is
   exactly what happened at 360x640: the hero grew to 790px and the CTA sat
   under the bar, while 390px and every desktop width were correct.
   So the fix is not a bigger reservation, it is making the mobile hero
   actually fit: desktop padding and a 20px tagline do not leave room for a
   CTA on a 640px screen once the consent bar takes its share. Keep the sum
   of these under 100svh at 360px and the reservation works as designed. */
@media (max-width:600px){
  .hero{ padding-top:88px; padding-bottom:calc(32px + var(--consent-h,0px)); }
  .hero__sub{ font-size:18px; margin-block:16px 24px; }
}

/* ============ PRODUCT RAIL + CARD (guide §06.3; ratio 4:5 per Phase 1 brief) ============ */
.rail{
  display:grid; grid-auto-flow:column; grid-auto-columns:min(72vw,304px);
  gap:24px; overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory; scroll-padding-inline:var(--pad-x);
  padding-inline:var(--pad-x); padding-bottom:16px;
}
.rail > *{ scroll-snap-align:start; }
@media (min-width:1024px){
  .rail{
    grid-auto-columns:calc((min(100vw,var(--container)) - 2*var(--pad-x) - 3*24px)/4);
    padding-inline:max(var(--pad-x),calc((100vw - var(--container))/2 + var(--pad-x)));
  }
}
.card{ width:100%; min-width:0; }
.card__media{ background:#E9EAED; }
.card:hover .frame > img,.card:focus-within .frame > img{ transform:scale(1.04); }
.card__quick{
  position:absolute; left:0; right:0; bottom:0; z-index:2;
  height:48px; width:100%;
  background:var(--bg); color:var(--ink);
  border:0; border-top:1px solid var(--hairline); cursor:pointer;
  font-size:12px; font-weight:600; letter-spacing:.16em; text-transform:uppercase;
  transform:translateY(100%);
  transition:transform var(--t-fast) var(--ease);
}
.card:hover .card__quick,.card:focus-within .card__quick{ transform:translateY(0); }
@media (hover:none){ .card__quick{ transform:none; } }
.card__body{ display:flex; justify-content:space-between; gap:16px; padding-block:16px; }
/* [PLACEHOLDER-COPY:…] tokens are unbreakable strings — allow wrap or narrow
   grid tracks overflow the viewport (found in the Phase 2 390px audit) */
.card__name{ font-size:16px; font-weight:500; overflow-wrap:anywhere; }
.card__price{ white-space:nowrap; color:var(--muted); }

/* Quick-add demo toast (no cart endpoint yet) */
.toast{
  position:fixed; left:50%; bottom:24px; z-index:var(--z-toast);
  transform:translate(-50%,8px); opacity:0; pointer-events:none;
  padding:16px 24px; background:var(--dark); color:var(--ink-on-dark);
  border:1px solid rgba(245,245,245,.16);
  font-size:12px; letter-spacing:.08em; text-transform:uppercase;
  transition:opacity var(--t-fast) var(--ease),transform var(--t-fast) var(--ease);
}
.toast.is-on{ opacity:1; transform:translate(-50%,0); }

/* ============ EDITORIAL SPLIT (guide §06.6) ============ */
.split{ display:grid; grid-template-columns:1fr; }
@media (min-width:900px){ .split{ grid-template-columns:1fr 1fr; min-height:560px; } }
.split__media{ position:relative; overflow:hidden; min-height:320px; }
.split__media > img{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
}
.split__content{
  display:flex; flex-direction:column; align-items:flex-start; justify-content:center;
  gap:24px; padding:64px var(--pad-x);
}
@media (min-width:1024px){ .split__content{ padding:96px; } }
.split__content p{ max-width:44ch; }
@media (min-width:900px){ .split--flip .split__media{ order:2; } }

/* ============ CATEGORY TILES (guide §06.5) ============ */
/* 4 tiles (Suits/Shirts/Bottoms/Shoes) as of the Bottoms addition — 3 even
   columns would orphan the 4th tile alone on its own row. 2-up on tablet,
   4-up once there's room for a 4:5 tile at a legible width. */
.tiles{ display:grid; grid-template-columns:1fr; gap:0; }
@media (min-width:640px){ .tiles{ grid-template-columns:repeat(2,1fr); } }
@media (min-width:1200px){ .tiles{ grid-template-columns:repeat(4,1fr); } }
.tile{ position:relative; display:block; color:var(--ink-on-dark); }
.tile .frame--4x5::after{
  content:""; position:absolute; inset:0; z-index:1;
  background:linear-gradient(180deg,transparent 50%,rgba(13,13,13,.65));
}
.tile:hover .frame > img,.tile:focus-visible .frame > img{ transform:scale(1.04); }
.tile__label{ position:absolute; left:24px; bottom:24px; z-index:2; }
.tile__cta{
  position:absolute; left:24px; bottom:64px; z-index:2; color:var(--ink-on-dark);
  opacity:0; transform:translateY(8px);
  transition:opacity var(--t-fast) var(--ease),transform var(--t-fast) var(--ease);
}
.tile:hover .tile__cta,.tile:focus-visible .tile__cta{ opacity:1; transform:none; }

/* ============ PRESS / UGC STRIP ============ */
.press{ text-align:center; }
.press__row{
  display:flex; flex-wrap:wrap; align-items:center; justify-content:center;
  gap:32px 48px; margin-top:48px;
}
.press__row img{ width:160px; height:auto; opacity:.7; }

/* ============ STORY PAGE ============ */
.bleed{ position:relative; overflow:hidden; }
.bleed > img{ width:100%; height:auto; }
.bleed--tall{ min-height:80svh; }
.bleed--tall > img{ position:absolute; inset:0; height:100%; object-fit:cover; }
.bleed__overlay{
  position:absolute; inset:0; z-index:2;
  display:flex; align-items:flex-end;
  background:linear-gradient(180deg,transparent 40%,rgba(13,13,13,.72));
  padding:64px var(--pad-x);
}
.bleed__overlay > *{ max-width:720px; }
.values{ display:grid; gap:48px; grid-template-columns:1fr; margin-top:64px; }
@media (min-width:900px){ .values{ grid-template-columns:repeat(3,1fr); } }
.value{ border-top:1px solid var(--hairline); padding-top:24px; }
.value h3{ margin-bottom:16px; }

/* ============ COLLECTION PAGE ============ */
.filters{
  display:flex; flex-wrap:wrap; gap:16px; align-items:center;
  padding-block:24px; border-bottom:1px solid var(--hairline); margin-bottom:48px;
}
.filter{
  height:40px; padding:0 24px; cursor:pointer;
  background:transparent; color:var(--ink);
  border:1px solid var(--hairline);
  font-size:12px; font-weight:600; letter-spacing:.16em; text-transform:uppercase;
  transition:background-color var(--t-fast) var(--ease),
             color var(--t-fast) var(--ease),
             border-color var(--t-fast) var(--ease);
}
.filter[aria-pressed="true"]{ background:var(--ink); color:var(--bg); border-color:var(--ink); }
.filters__count{ margin-left:auto; }
.pgrid{ display:grid; gap:48px 24px; grid-template-columns:repeat(2,1fr); }
.pgrid > *{ min-width:0; } /* 1fr track minimum is min-content otherwise */
@media (min-width:768px){ .pgrid{ grid-template-columns:repeat(3,1fr); } }
@media (min-width:1200px){ .pgrid{ grid-template-columns:repeat(4,1fr); } }
.pgrid .card.is-hidden{ display:none; }

/* ============ FOOTER (guide §06.7) ============ */
.footer{ border-top:1px solid var(--hairline); }
.footer__grid{ display:grid; gap:48px; grid-template-columns:1fr; padding-block:64px; }
/* the newsletter form's intrinsic width must not set the track minimum —
   without this the footer overflows small viewports by ~33px */
.footer__grid > *{ min-width:0; }
@media (min-width:900px){ .footer__grid{ grid-template-columns:1.4fr 1fr 1fr 1.6fr; padding-block:96px; } }
.footer__col h2{ margin-bottom:24px; color:var(--muted); }
/* ---------- FOOTER COLUMN RHYTHM ----------
   The Newsletter column sat 2px tighter under its heading than Shop and
   House beside it — 24px against 26px. Not a left-edge problem: all three
   headings and all three bodies already shared an edge, measured at 360,
   768 and 1440. It was the BOTTOM margin, and it was accidental in every
   column: nobody set it, so each one inherited whatever its own content
   happened to produce — a grid gap in two cases, a form's own box in the
   third, and the two answers were never going to agree.

   Stated once here, so the three columns cannot drift apart again. */
.footer__col > .label{ display:block; margin-bottom:24px; }
.footer__list, .newsform{ margin-top:0; }
.footer__list{ display:grid; gap:0; }
/* Footer links are a list of discrete destinations, not prose, so they take
   the 44px floor. The row height replaces the 12px gap rather than adding
   to it, so the footer does not grow. */
.footer__list a{
  display:inline-flex; align-items:center; min-height:44px; min-width:44px;
  transition:color var(--t-fast) var(--ease);
}
.footer__list a:hover{ color:var(--muted); }
/* Current-page marker for the footer and the site-map panel. The top nav's
   underline (.nav__link::after, above) stays exactly as it is — this is a
   deliberately different treatment because the footer is a list of links,
   not a horizontal bar: weight + a static underline reads clearly in a
   list without borrowing the bar's animated reveal, which would look like
   the same control doing two different things. */
.footer__list a[aria-current="page"],
.footer__legal a[aria-current="page"],
.navmap__list a[aria-current="page"]{
  font-weight:600; text-decoration:underline; text-underline-offset:4px;
  color:var(--ink-on-dark);
}
.footer__brandnote{ color:var(--muted); max-width:32ch; margin-top:16px; }
.newsform{ display:flex; width:100%; }
.newsform input{
  flex:1; min-width:0; height:48px; padding:0 16px;
  background:transparent; border:1px solid var(--hairline); border-right:0;
}
.newsform input::placeholder{ color:var(--muted); font-size:12px; letter-spacing:.16em; text-transform:uppercase; }
.newsform .btn{ flex-shrink:0; }
.newsform__status{ margin-top:16px; min-height:1.5em; font-size:14px; color:var(--muted); }
.footer__legal{
  display:flex; flex-wrap:wrap; gap:16px 32px; justify-content:space-between;
  align-items:center;
  padding-block:24px; border-top:1px solid var(--hairline); color:var(--muted);
  font-size:14px;
}
.footer__legal a{
  display:inline-flex; align-items:center; justify-content:center;
  min-height:44px; min-width:44px;
}
/* Review round 5 (P11): moved here from premium.css, which only that
   page's routes load — the footer's new "Need help?" entry point needs
   this everywhere a footer exists, not just on /premium/. premium.css's
   own copy of this rule is gone; this is the only one now. */
.linkbtn{
  background:none; border:0; cursor:pointer;
  display:inline-flex; align-items:center; justify-content:center;
  min-height:44px; min-width:44px; padding:0;
  color:var(--muted); text-decoration:underline; font-size:12px;
}
.linkbtn:hover{ color:var(--ink-on-dark); }
.footer__legal .linkbtn{ font-size:14px; text-decoration:none; }
.footer__legal .linkbtn:hover{ text-decoration:underline; }

/* ============ TRUST CUE MODULE (Phase 5) ============
   Production port of styleguide.html §09.2 — same markup/class names, so
   the guide stays the accurate reference rather than documentation that
   drifts from what ships. Reused wherever a purchase/visit decision needs
   the same three claims: payment, returns, a reachable human. */
.trust{ display:grid; gap:12px; padding:16px; border:1px solid var(--hairline); border-left:2px solid var(--accent); }
.trust__row{ display:flex; align-items:flex-start; gap:12px; font-size:13px; line-height:1.4; }
.trust__mark{
  flex:none; width:24px; height:24px; display:grid; place-items:center;
  border:1px solid var(--hairline); font-size:11px; font-weight:600; margin-top:1px;
}
.trust__row strong{ font-weight:600; }

/* FIND US moved to pages.css — it is a page-specific concern and pages.css
   exists for exactly that. Checked before moving: only /find-us/ uses the
   .location classes and .mapframe (styleguide.html carries its own inline
   copy and loads none of these sheets), and find-us now links pages.css.

   This comment once read ".location*" followed by "/" — which CLOSES a CSS
   comment. Everything after it parsed as garbage and the browser dropped
   the rules that followed, including .consent{position:fixed}, so the
   cookie bar rendered at the bottom of the document instead of the
   viewport. e2e16 caught it. Do not write a star-slash inside a comment.

   Careful: .band-head, .nav--solid and .errpage* stay HERE. They were in shop.css once,
   which /find-us/ does not load, and that put this page's heading under the
   fixed nav for two whole phases. Moving page CSS out is fine; moving shared
   CSS into a page-specific sheet is what caused that. */

/* ============ ANALYTICS CONSENT (assets/js/consent.js) ============
   A bar, not a modal: the page stays readable and usable underneath, so
   nobody is forced to answer before they can see what they are agreeing
   about. It sits under the toast layer and above everything else, because
   a takeover that covered it would leave the visitor unable to answer.
   Decline and Accept are the same size on purpose. */
.consent{
  position:fixed; left:0; right:0; bottom:0; z-index:var(--z-consent);
  background:var(--dark); color:var(--ink-on-dark);
  border-top:1px solid rgba(245,245,245,.16);
}
.consent__inner{
  max-width:var(--container); margin:0 auto; padding:16px var(--pad-x);
  display:flex; gap:24px; align-items:center; flex-wrap:wrap;
}
.consent__copy{ flex:1 1 380px; min-width:0; }
.consent__title{
  font-family:var(--font-display,inherit); text-transform:uppercase;
  letter-spacing:.08em; font-size:12px; font-weight:700; margin:0 0 4px;
}
.consent__body{ margin:0; color:rgba(245,245,245,.78); max-width:70ch; }
.consent__body a{ color:var(--ink-on-dark); text-decoration:underline; }
.consent__actions{ display:flex; gap:8px; flex:0 0 auto; }
/* 44px minimum target — this is the first thing many visitors ever tap. */
.consent__btn{ min-height:44px; }
@media (max-width:600px){
  .consent__actions{ width:100%; }
  .consent__btn{ flex:1 1 0; }
  /* PHONE HEIGHT BUDGET. At desktop widths this bar is one or two lines and
     costs nothing. At 360px the same words wrap to six, and the bar took
     263px — 41% of a 640px screen — which put the PDP's size picker and Add
     to cart underneath it on a first visit, and the home hero's CTA with
     them. Nothing here changes what the notice SAYS: the three things it
     discloses (we want to measure page/product views, only with a yes,
     essential cookies stay either way) are all still on screen. It is the
     same text set tighter.
     The max-height is a backstop, not a scroller for today's copy — the
     current wording fits inside it. It exists so a future edit cannot grow
     this bar without bound on the smallest screen we support. */
  /* 16px, not 12: the gap is the only thing between the inline "How we
     handle data" link at the end of the copy and the Decline/Accept row.
     At 12px the link's inline box ended 7px above the buttons — inside
     thumb-error range for the two controls that matter most on this bar. */
  .consent__inner{ padding-block:12px; gap:16px; }
  .consent__body{ font-size:14px; line-height:1.5; max-height:8.5em; overflow-y:auto; }
}

/* ============ PASSWORD PEEK ============
   The reveal control assets/js/passwordpeek.js wraps around every
   [type=password] on the site. It lives HERE, not in pages.css or
   staff.css, because it appears on both sides: the light customer account
   forms and the dark staff shell. Nothing in it names a colour — `inherit`
   and `currentColor` do the work, so the same rule reads correctly against
   either background without a theme-specific override to keep in sync.

   The 44px minimum matters: on a phone this sits inside a text field, and a
   miss lands in the field and moves the caret instead. */
.pwfield{ position:relative; display:flex; align-items:stretch; }
.pwfield > input{ flex:1 1 auto; min-width:0; padding-right:5.5em; }
.pwpeek{
  position:absolute; top:0; right:0; height:100%; min-width:44px;
  display:inline-flex; align-items:center; justify-content:center;
  padding:0 12px; border:0; background:none; color:inherit;
  font:inherit; font-size:12px; font-weight:600;
  letter-spacing:.16em; text-transform:uppercase;
  opacity:.6; cursor:pointer;
}
.pwpeek:hover{ opacity:1; text-decoration:underline; }
.pwpeek:focus-visible{ opacity:1; outline:2px solid currentColor; outline-offset:-2px; }

/* The Atoli credit — a maker's mark under the client's legal line. Quieter
   than everything above it on purpose: it must not compete with 2Be's own
   name. Text, not a link, until there is a URL to point at — see the note
   in the markup. */
.footer__atoli{
  padding-bottom:24px; margin:0;
  font-size:12px; color:var(--muted); opacity:.72;
}
/* Two CTAs side by side at the end of the story. */
.storycta{ display:flex; flex-wrap:wrap; gap:16px; }
