/* ===========================================================================
   bikinis.com shared design system.
   Every page links this instead of carrying its own copy of the chrome.
   =========================================================================== */
:root{
  --white:#FFFFFF; --off:#F8F9FA; --mist:#F1F3F5;
  /* --muted was #64748B, which clears AA on white (4.76) and on --off (4.51)
     and MISSES it on --mist: 4.28 against 4.5. --mist is the ground under every
     card body on the homepage rail, so the miss showed up on the line a shopper
     reads to compare categories — "26 styles - from $25.99".
     #5E6C85 clears it on all three grounds (4.77 / 5.03 / 5.30) with room, so a
     future card on a slightly different grey does not reopen this. It is still
     visibly a muted grey rather than body ink. Measured 2026-08-20. */
  --slate:#1E293B; --ink:#0E1117; --muted:#5E6C85; --line:#E6EAEF;
  /* Dark SURFACES are a soft blue-slate, not black.
     Every dark band and hero used #090A0F — near-pure black. Against white
     product photography that reads as a hole punched in the page, and it made
     the whole site feel heavy and severe. This is still unmistakably dark but
     has a color temperature, so garment shots sit on it instead of floating. */
  --dark:#151B26; --dark-2:#1C2331; --dark-3:#252E3E; --dark-line:#2E3849;
  /* Brand teal, sampled from the official logo lockup. --orange is kept as the
     variable name because it is referenced in dozens of places across every
     page; renaming it would be a site-wide find-and-replace for no gain. */
  --teal:#0C4C4A; --teal-lift:#12706C;
  --cyan:#12706C; --orange:#0C4C4A;
  --bg:var(--white); --bg-2:var(--off); --bg-3:var(--mist);
  --fg:var(--ink); --fg-2:var(--slate); --fg-3:var(--muted);
  /* Corner radius. Was 6px everywhere, which on big surfaces — cards, tiles,
     panels, images — reads as square, and the whole page came across as a stack
     of boxes. One step up softens the geometry without going bubbly, and --r-lg
     handles the large surfaces where 12px still looks tight. */
  --r:12px; --r-lg:18px;
  --ff:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
  --fm:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
/* Dark is OPT-IN, not automatic.
   This used to key off prefers-color-scheme, so anyone whose phone was in dark
   mode — which is most phones — got a near-black storefront by default. A
   clothing store is photography on white; the dark ground made every garment
   shot look like it was floating in a hole, and the whole site read as sombre
   rather than clean. Dark stays available through the header toggle, which now
   persists the choice (see site.js), and only that toggle turns it on. */
:root[data-theme="dark"]{
  --bg:var(--dark); --bg-2:var(--dark-2); --bg-3:var(--dark-3);
  --fg:#F5F7FA; --fg-2:#CBD5E1; --fg-3:#94A2B6; --line:var(--dark-line);
  --orange:#3FBFB6; --cyan:#3FBFB6;
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--fg);font:400 16px/1.6 var(--ff);
     letter-spacing:-.011em;overflow-x:hidden}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
img{max-width:100%;display:block}
/* `img{display:block}` above is an AUTHOR rule, and author rules beat the
   browser's built-in [hidden]{display:none}. So every <img hidden> on the site
   stayed visible. That is what broke the Studio: the mockup layer is
   position:absolute, inset:0 with a white background, so despite carrying
   `hidden` it painted a solid white sheet over the garment and the stage looked
   empty. Restore the intended meaning of the attribute, for every element. */
[hidden]{display:none!important}
.wrap{max-width:1280px;margin:0 auto;padding:0 28px}
@media(max-width:640px){.wrap{padding:0 18px}}
.mono{font-family:var(--fm);font-size:.68rem;letter-spacing:.14em;text-transform:uppercase}

/* --- nav --- */
header{position:sticky;top:0;z-index:60;background:var(--bg);border-bottom:1px solid var(--line)}
/* The lockup is wider than the old wordmark, so the gaps tighten to stop the
   nav links wrapping onto two lines. */
.nav{display:flex;align-items:center;gap:16px;height:88px}
.links a{white-space:nowrap}
/* --- wordmark ---------------------------------------------------------------
   Pure type. The old lockup was an unrelated box-and-collar icon sitting beside
   an all-caps wordmark; now the mark IS the "t" of bikinis.com, set inline where
   the letter belongs. Lowercase throughout — all-caps fought the domain, which
   nobody types or says in capitals. */
/* Official lockup. Fixed height, width auto — the source is 866x288, so the
   aspect ratio must not be forced or the tee glyph distorts. */
.brand{display:flex;align-items:center;flex:none}
/* The lockup is 866x288 and the tagline occupies only the bottom ~18% of that,
   so the strapline needs real height to be readable. 64px puts "WHERE SHIRTS
   BEGIN!" at roughly 8px — legible. Below ~56px it degrades to a grey smear. */
.brandmark{height:64px;width:auto;display:block}
.brandmark--foot{height:44px;margin-bottom:14px}
/* The lockup is dark ink on transparent; on a dark ground it vanishes.
   Keyed to the explicit dark theme ONLY. While this also matched
   prefers-color-scheme, a phone in dark mode inverted the logo on top of the
   now-light page — white-on-white, so the brand mark disappeared entirely. */
:root[data-theme="dark"] .brandmark{
  filter:invert(1) hue-rotate(180deg) saturate(1.4) brightness(1.15)}
.band--dark .brandmark,.hero .brandmark{
  filter:invert(1) hue-rotate(180deg) saturate(1.4) brightness(1.15)}
@media(max-width:640px){.brandmark{height:28px}}

/* HOLD THE HEADER'S HEIGHT WHILE IT IS STILL EMPTY.
   ==========================================================================
   mountChrome() fills <div id="nav"></div> with innerHTML after the page has
   painted. Until it does, the element is 0px tall — so every page renders with
   its content at the top, then everything drops 89px the instant the header
   lands. Measured 2026-09-24: cumulative layout shift 0.38 on /shop/ and 0.75
   on /design/, against 0.1 for "good". The tell was every shifted element
   moving by exactly the same 89px.

   It is the cheapest kind of expensive-looking: a page that does not jump
   while you are reading it.

   `:empty` rather than a plain min-height, so the reserve exists ONLY until
   the header mounts and can never leave a gap under a shorter header later.
   The placeholders are written `<div id="nav"></div>` with no whitespace, which
   is what makes them match — a newline between the tags would not.

   65 / 89 measured, not guessed: the promo ticker is hidden at and below 640px
   and that is worth exactly the 24px difference. 640 is the same breakpoint the
   rest of this file uses.

   The FOOTER needs the same treatment and cannot use an exact number: its
   height moves continuously with width, because the link columns rewrap rather
   than switching at breakpoints. Measured: 502px at 1000+, 915 at 820-900,
   951 at 700-768, 935 at 600, 974 at 390-480, 1340 at 360.

   So each band reserves the SMALLEST height seen in it. Under-reserving leaves
   some shift; over-reserving would open a gap and then snap shut, which trades
   a downward jump for an upward one. Smallest-in-band can only ever help.

   Why the footer shifts at all, when it is below the fold: before the chrome
   mounts the page is nearly empty, so the footer sits at the TOP of the
   viewport. It is on screen at exactly the moment it moves, which is why it
   scored as the single largest shift on /shop/ (0.377) and /design/ (0.527).

   Reserving heights is the cheap fix. The proper one is to render this markup
   into the pages instead of mounting it from script. */
#nav:empty{min-height:89px}
#foot:empty{min-height:502px}
@media(max-width:999px){#foot:empty{min-height:915px}}
@media(max-width:640px){#nav:empty{min-height:65px}#foot:empty{min-height:935px}}

/* AND HOLD THE PRODUCT GRID OPEN, WHICH IS THE ONE THAT ACTUALLY COSTS.
   ==========================================================================
   Reserving the header and footer removed the obvious jumps and barely moved
   /shop/. Reading the shift records instead of guessing showed why:

     div#foot   561,339  ->  0,0

   The footer is not collapsing. It starts 561px down a nearly empty page —
   ON SCREEN — and when the grid fills in, the page grows from about 900px to
   11,315px and the footer is shoved far below the fold. A visible element
   leaving the viewport is a layout shift, and at 0.377 it was almost the whole
   of that page's 0.382.

   So the fix is not to hold the footer still; it is to make sure the footer was
   never on screen to begin with. One viewport of reserved grid does that, and
   `:empty` means it costs nothing the moment the first card lands.

   100vh and not a card-grid skeleton on purpose: the real height depends on how
   many products come back, which is unknowable before the fetch. A skeleton
   guessing 12 cards would be wrong for 67 and shift again. */
#grid:empty{min-height:100vh}
/* /design/ ships a "Loading designs…" line inside the grid, so it is never
   :empty and the rule above cannot reach it. Same job, matched on the loading
   placeholder itself — and only that one, so a genuine "nothing matches your
   filters" message stays the size of a message. */
#grid>.gridloading{min-height:100vh}
.brandtext{font-weight:800;font-size:1.45rem;letter-spacing:-.045em;line-height:1;
           display:inline-flex;align-items:baseline}
.brandtext i{font-style:normal;color:var(--fg-3);font-weight:700}
.brand:hover .brandtext i{color:var(--orange)}

/* The tee-glyph replaces the letter t, so it has to sit on the baseline and
   match the optical weight of the surrounding letterforms — hence em units and
   a nudge rather than a fixed pixel size. */
.tee{display:inline-block;width:.46em;height:.78em;position:relative;top:.035em;
     margin:0 .015em}
.tee svg{width:100%;height:100%;display:block;color:var(--orange);
         transition:color .2s,transform .35s cubic-bezier(.2,.7,.3,1)}
.brand:hover .tee svg{transform:translateY(-2px)}
/* Legacy: some pages still ship the old .mk icon markup. */
.mk{height:26px;width:26px;color:var(--fg);flex:none}
.links{display:flex;gap:19px;flex:1;height:100%;align-items:center}
.links a{font-size:.9rem;font-weight:600;color:var(--fg-2)}
.links a:hover,.links a.on{color:var(--fg)}
.navact{display:flex;align-items:center;gap:8px;flex:none}
.ibtn{display:flex;align-items:center;gap:7px;padding:9px 14px;border:1px solid var(--line);
      border-radius:var(--r);font-size:.8125rem;font-weight:600;white-space:nowrap}
.ibtn:hover{border-color:var(--fg-3)}
.ibtn.solid{background:var(--ink);color:#fff;border-color:var(--ink)}
.count{background:var(--orange);color:#fff;border-radius:99px;min-width:19px;height:19px;
       display:grid;place-items:center;font-size:.68rem;font-weight:800;padding:0 5px}
/* Mobile navigation.
   `.links{display:none}` used to be the whole mobile story — Shop, Studio,
   Bulk, Creators, Archive and Guides simply vanished below 900px with nothing
   to replace them, so a phone visitor could reach the homepage, the cart and
   nothing else. The links now collapse into a real toggleable panel. */
#menuBtn{display:none}
/* 1180, NOT 900. Measured 2026-09-29: the full row (logo, six links, search,
   theme, Contact, Account, Cart) needs ~1290px. Between 901 and 1289 it ran
   off the right edge and body{overflow-x:hidden} sliced it off, so on a
   1024px laptop or iPad the Cart button sat 217px past the screen edge and
   could not be reached at all. The row now collapses to the menu below 1181
   and is tightened above that (see the visual-pass block near the end). */
@media(max-width:1180px){
  #menuBtn{display:inline-flex;align-items:center;justify-content:center;font-size:1.1rem}
  .links{position:absolute;top:100%;left:0;right:0;display:none;flex-direction:column;
         align-items:stretch;height:auto;gap:0;padding:6px 0 10px;background:var(--bg);
         border-bottom:1px solid var(--line);
         box-shadow:0 18px 34px rgba(0,0,0,.28)}
  .links.open{display:flex}
  .links a{padding:14px 28px;font-size:1rem;font-weight:600;
           border-bottom:1px solid var(--line)}
  .links a:last-child{border-bottom:0}
  /* The panel hangs off the nav row, so it tracks the sticky header. */
  .nav{position:relative}
}
@media(max-width:640px){
  .links a{padding:14px 18px}
  /* A logo, a menu button, a theme toggle, Contact, Account AND a cart do not
     fit a 360px row — the header overflowed and forced a horizontal scroll.
     Contact and Account move into the menu panel (.navonly-mobile), the theme
     toggle goes with them, and the row keeps only what a phone needs: brand,
     menu, cart. */
  .nav{gap:8px;height:64px}
  #themeBtn,.navact a.ibtn{display:none}
  .navact{gap:6px}
  .ibtn{padding:8px 11px;font-size:.78rem}
}
/* Duplicated nav entries that exist only for the mobile panel. */
.navonly-mobile{display:none}
@media(max-width:1180px){.links .navonly-mobile{display:block}}

/* --- page header --- */
.pagehead{background:var(--bg-2);border-bottom:1px solid var(--line);padding:44px 0 40px}
.crumbs{font-size:.8125rem;color:var(--fg-3);margin-bottom:14px}
.crumbs a:hover{color:var(--orange)}
.crumbs span{margin:0 7px}
.ph1{font-size:clamp(2rem,4.4vw,3rem);letter-spacing:-.045em;font-weight:900;margin:0 0 12px}
.phl{margin:0;color:var(--fg-2);font-size:1.02rem;max-width:62ch;line-height:1.55}

/* --- filters --- */
.filters{display:flex;gap:8px;align-items:center;margin-bottom:26px;flex-wrap:wrap}
.fspacer{flex:1}
.chip{border:1px solid var(--line);padding:9px 15px;font-size:.8125rem;border-radius:var(--r);font-weight:600}
.chip:hover,.chip.on{background:var(--ink);color:#fff;border-color:var(--ink)}
select{padding:9px 12px;background:var(--bg);color:var(--fg);border:1px solid var(--line);
       border-radius:var(--r);font:inherit;font-size:.8125rem;font-weight:600}

/* --- product grid --- */
/* .grid shares the centred track definition with .agrid (see below). The old
   version used gap:1px over a --line background to fake hairline separators,
   which meant any row that was not completely full rendered as a large grey
   slab where the empty cells were. */
.pcard{background:var(--bg);display:flex;flex-direction:column;
       border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.pcard:hover{border-color:var(--fg-3)}
.pimg{aspect-ratio:4/5;background:var(--bg-3);position:relative;display:grid;place-items:center;overflow:hidden}
/* contain, not cover: these are full-body model shots, and cover crops the
   model's head off at this aspect ratio. */
.pimg img{width:100%;height:100%;object-fit:contain;background:#fff;
          transition:transform .5s cubic-bezier(.2,.7,.3,1)}
.pcard:hover .pimg img{transform:scale(1.045)}
.pimg .ph{font-family:var(--fm);font-size:.62rem;letter-spacing:.14em;color:var(--fg-3)}
/* BULK OK SITS AT THE BOTTOM, BECAUSE HEADS ARE AT THE TOP.
 *
 * Owner: "the bulk ok tab cuts into a lot of models heads etc".
 *
 * It was pinned top-left, which is the worst corner on this catalogue: most of
 * the photography is a model shot framed head-and-torso, so the one thing
 * reliably in the top-left is a face. Bottom-left is the safe corner on both
 * kinds of photo here — on a model shot it falls on the hem or the background,
 * and on a flat lay the garment has already tapered in by then.
 *
 * The tile's title and price live in .tile-body BELOW the image, so nothing is
 * competing for this corner.
 *
 * Smaller on narrow screens: at 11px inset on a ~170px-wide phone tile the badge
 * spanned a third of the picture. */
/* WHITE ON THE TEAL, NOT INK. Another value written for the old bright accent
   and left unchanged through the teal rebrand: near-black on #12706C measures
   3.20:1, under AA for a badge this small. White measures 5.90:1 on the same
   ground — and it is what every other element sitting on this accent already
   uses (.count, .sz:hover, .navsearch-go:hover, .dtab .new). Measured
   2026-08-20 on the "BULK OK" badge, /shop/. */
.flag{position:absolute;bottom:11px;left:11px;background:var(--cyan);color:#fff;
      font-family:var(--fm);font-size:.56rem;letter-spacing:.1em;padding:3px 7px;border-radius:3px;font-weight:800;z-index:2}
@media (max-width:640px){
  .flag{bottom:7px;left:7px;font-size:.5rem;letter-spacing:.08em;padding:2px 5px}
}
.pbody{padding:16px;display:flex;flex-direction:column;gap:5px;flex:1}
.pbody h3{margin:0;font-size:.95rem;font-weight:700;letter-spacing:-.025em;line-height:1.35;
          display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
          overflow:hidden;min-height:2.7em}
.pmeta{font-size:.8125rem;color:var(--fg-3)}
.swatches{display:flex;gap:5px;margin-top:9px}
.sw{width:15px;height:15px;border-radius:99px;border:1px solid rgba(0,0,0,.18);display:inline-block}
.sizes{display:flex;flex-wrap:wrap;gap:5px;margin-top:12px}
.sz{border:1px solid var(--line);padding:6px 10px;font-size:.75rem;border-radius:4px;
    font-family:var(--fm);letter-spacing:.05em;font-weight:600;transition:all .12s}
.sz:hover{background:var(--orange);color:#fff;border-color:var(--orange)}
.price{margin-top:auto;padding-top:14px;font-size:1.02rem;font-weight:800;letter-spacing:-.02em}
.price small{font-weight:500;color:var(--fg-3);font-size:.75rem;letter-spacing:0}
/* THE QUALIFIER GETS ITS OWN LINE ONCE THE CARD IS TOO NARROW TO HOLD IT.
   Inline it reads well on a desktop card — "$39.99 and down at volume" on one
   line. In the two-up grid on a phone the card is about 170px, so it wrapped
   as "$39.99 and down at" / "volume", breaking the phrase at the worst
   possible point and leaving one orphaned word under the price. Photographed
   at 390px. Below 700px it becomes its own line, which is predictable at any
   name length instead of depending on how long the price happens to be. */
@media(max-width:700px){
  .price small{display:block;margin-top:2px}
}
.empty{text-align:center;color:var(--fg-3);padding:70px 20px;font-size:.9rem}

.btn{display:inline-flex;align-items:center;gap:8px;padding:14px 26px;border-radius:var(--r);
     font-size:.95rem;font-weight:700;background:var(--ink);color:#fff;border:1px solid var(--ink)}
.btn:hover{opacity:.88}

/* --- footer --- */
footer{padding:60px 0 44px;color:var(--fg-3);font-size:.85rem;border-top:1px solid var(--line)}
.fgrid{display:grid;grid-template-columns:1.6fr repeat(4,1fr);gap:34px}
/* A `1fr` track is `minmax(auto,1fr)`, and that `auto` minimum is the item's
   min-content width — so a track REFUSES to shrink below its widest child no
   matter how little room there is. In this footer the first cell (blurb,
   signup form, small print) floored at 263px, which forced the two-column grid
   to 354px inside a 284px page. Every page on the site inherits this footer, so
   at 320px — iPhone SE, Galaxy Fold cover screen — 21 of 23 pages ran 52px
   wide, and `body{overflow-x:hidden}` turned that into content sliced off at
   the right edge rather than a scrollbar. `min-width:0` is what removes the
   automatic minimum for a grid item. */
.fgrid>*{min-width:0}
@media(max-width:900px){
  .fgrid{grid-template-columns:1fr 1fr;gap:28px}
  /* THE FIRST CELL TAKES THE WHOLE ROW, WHICH FIXES TWO THINGS AT ONCE.
     There are five cells and two columns, so the fifth — Help, six links —
     ran down the left on a row of its own with about 200px of nothing beside
     it. And the first cell holds the brand line, the newsletter form and the
     small print inside one 163px column at 390px, which squeezed the email
     field to **106px**: its own placeholder, "Email address", did not fit and
     rendered cut off as "Email addres". A truncated input is the sort of
     thing that reads as broken rather than tight.

     Spanning the first cell gives that block the full width, and it leaves
     exactly four link cells to fill two rows of two — so the orphan is not
     patched, it stops existing. Below 380px the rule above collapses
     everything to one column anyway and this is a no-op. */
  .fgrid > :first-child{grid-column:1 / -1}
}
/* Below this, two columns leave ~128px each and the link text stacks one word
   per line. One column is narrower to read but not broken. */
@media(max-width:380px){.fgrid{grid-template-columns:1fr;gap:22px}}
footer h5{margin:0 0 12px;font-family:var(--fm);font-size:.62rem;letter-spacing:.13em;
          color:var(--fg-2);font-weight:700}
footer ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:9px}
footer a:hover{color:var(--orange)}
.fbot{margin-top:42px;padding-top:24px;border-top:1px solid var(--line);
      display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;font-size:.8rem}

/* --- cart drawer --- */
.scrim{position:fixed;inset:0;background:rgba(21,27,38,.55);opacity:0;pointer-events:none;
       transition:opacity .22s;z-index:90}
.scrim.on{opacity:1;pointer-events:auto}
.drawer{position:fixed;top:0;right:0;bottom:0;width:min(440px,100%);background:var(--bg);
        border-left:1px solid var(--line);transform:translateX(100%);
        transition:transform .28s cubic-bezier(.4,0,.2,1);z-index:100;display:flex;flex-direction:column}
.drawer.on{transform:none}
.dh{display:flex;justify-content:space-between;align-items:center;padding:22px;border-bottom:1px solid var(--line)}
.dh h3{margin:0;font-size:1.05rem;font-weight:800;letter-spacing:-.03em}
.db{flex:1;overflow-y:auto;padding:6px 22px}
.ci{display:flex;gap:14px;padding:18px 0;border-bottom:1px solid var(--line)}
.ciimg{width:62px;height:78px;background:var(--bg-3);flex:none;border-radius:4px;overflow:hidden}
.ciimg img{width:100%;height:100%;object-fit:contain;background:#fff}
/* The line's artwork, laid on the garment thumbnail by placeCartArt(). Those
   left/top/width values are placeholders it overwrites after measuring the
   letterboxed photo. */
.ciimg{position:relative}
.ciimg .ci-art{position:absolute;z-index:2;width:28%;height:auto;left:50%;top:36%;
               background:none;transform:translateX(-50%);object-fit:contain}

/* HOVER TO SEE IT PROPERLY.
   A 62x78 thumbnail is enough to recognise a line and not enough to APPROVE
   one, and the cart is the last place the design is visible before paying.
   Owner: "it should be hover over viewable larger in the cart view and
   checkout". So the render pops out at a size a print can actually be judged
   at, without a modal or a navigation.

   Only where a real render arrived (.hasrender). Enlarging the CSS composite
   would magnify an approximation, which is worse than leaving it small — the
   bigger it is the more it reads as a promise about where the print goes. */
.ciimg.hasrender{cursor:zoom-in}
.ciimg.hasrender::after{content:"⤢";position:absolute;right:3px;bottom:2px;z-index:3;
  font-size:.6rem;line-height:1;padding:2px 3px;border-radius:3px;
  background:rgba(11,13,18,.72);color:#fff;pointer-events:none}
.ciimg.hasrender .ci-render{transition:none}
.ciimg.hasrender:hover{overflow:visible;z-index:40}
.ciimg.hasrender:hover .ci-render{position:absolute;left:0;top:50%;
  width:min(310px,74vw);height:auto;transform:translateY(-50%);
  border-radius:8px;background:#fff;box-shadow:0 18px 48px rgba(0,0,0,.45);
  border:1px solid var(--line);z-index:41}
/* A phone has no hover, and a pop-out that needs one is a control that does not
   exist there. Left at the thumbnail rather than half-implemented. */
@media(hover:none){.ciimg.hasrender:hover .ci-render{position:static;width:100%;
  transform:none;box-shadow:none;border:0;border-radius:0}
  .ciimg.hasrender:hover{overflow:hidden}}
.cib{flex:1;min-width:0}
.cib h4{margin:0 0 3px;font-size:.9rem;font-weight:700}
.cq{display:flex;align-items:center;gap:9px;margin-top:9px}
.cq button{border:1px solid var(--line);width:27px;height:27px;border-radius:4px;line-height:1;font-weight:700}
.cq span{font-family:var(--fm);font-size:.82rem;min-width:22px;text-align:center}
.cip{text-align:right;font-weight:800;font-size:.9rem;font-variant-numeric:tabular-nums;white-space:nowrap}
/* Remove-line control. Deliberately quiet until hovered — it sits next to the
   price and must never be mistaken for the close button of the drawer. */
.cirm{flex:none;align-self:flex-start;width:24px;height:24px;margin-left:2px;border:0;
      background:none;color:var(--fg-3);font-size:.8rem;line-height:1;cursor:pointer;
      border-radius:4px;transition:color .15s,background .15s}
.cirm:hover{color:#fff;background:#c2410c}
.cirm:focus-visible{outline:2px solid var(--orange);outline-offset:2px}
.tflag{color:var(--orange);font-size:.66rem;font-family:var(--fm);letter-spacing:.08em;margin-top:4px;font-weight:700}
.df{border-top:1px solid var(--line);padding:22px}
.ups{background:var(--bg-2);border:1px dashed var(--line);padding:13px 15px;font-size:.83rem;
     margin-bottom:15px;border-radius:4px}
.ups b{color:var(--orange)}
.ql{display:flex;justify-content:space-between;padding:12px 0;font-size:.9375rem}
/* Made-to-order notice in the cart. Deliberately quiet but legible — this is a
   term of sale, not fine print to be hidden, and it goes directly above the
   Checkout button so it cannot be scrolled past. */
.mto{margin:0 0 13px;font-size:.78rem;line-height:1.5;color:var(--fg-3)}
.mto b{color:var(--fg-2);font-weight:700}
.mto a{text-decoration:underline}
.ql b{font-variant-numeric:tabular-nums;font-weight:700}

/* ===========================================================================
   ART DIRECTION LAYER
   Added in the 2026-08 rebuild. The rules above are the utilitarian chrome;
   everything below is the editorial surface — full-bleed heroes, alternating
   light/dark bands, asymmetric grids and lifestyle breaks.
   =========================================================================== */

/* --- bands -----------------------------------------------------------------
   A band owns its own palette. Redefining the tokens (rather than hardcoding
   colors on children) means cards, rules and muted text all invert correctly
   inside a dark band with no per-element overrides. */
/* THE GAP BETWEEN TWO BANDS IS THE SUM OF BOTH, AND THAT IS WHAT WENT WRONG.
   Bands sit flush — measured gap between them is 0 — so a band's padding is
   only ever seen added to its neighbour's. At 120px each that is 240px of
   uninterrupted nothing at every seam, and with seven bands it came to 1,584px
   of the 7,989px desktop page: a fifth of the homepage, empty. Two white bands
   in a row were the worst of it, because there is not even a change of colour
   to explain the distance.

   It reads as generous in a screenshot of one section and as an unfinished
   page when you scroll it. 88px a side gives 176px at a seam, which is still
   unmistakably a new section and no longer a void. The floor comes down too:
   a phone was getting 64px a side, 128px a seam, on a page already too long.

   Deliberately NOT expressed as a spacing scale. There is one number here and
   one exception below it, which is easier to reason about than tokens that
   have to be chosen correctly at every call site. */
.band{padding:clamp(52px,5.4vw,88px) 0;background:var(--bg);color:var(--fg);position:relative}
.band--tight{padding:clamp(36px,3.6vw,60px) 0}
.band--alt{background:var(--bg-2)}
.band--dark{
  --bg:var(--dark); --bg-2:var(--dark-2); --bg-3:var(--dark-3);
  --fg:#F5F7FA; --fg-2:#CBD5E1; --fg-3:#94A2B6; --line:var(--dark-line);
  /* THE ACCENT HAS TO LIFT HERE TOO, AND IT WAS THE ONE TOKEN LEFT BEHIND.
     This block reassigned the greys and stopped, so --orange stayed at the
     light-theme brand teal #0C4C4A — which on this near-black band is barely
     visible. Measured 2026-08-20 on /studio/: the step numbers 01-04 rendered
     rgb(12,76,74) on rgb(21,27,38), 1.77:1. Same colour, same cause as the
     "Find it" button the owner reported: a value chosen for a light surface,
     used unchanged on a dark one after the teal rebrand.
     #3FBFB6 is not a new colour — it is exactly what :root[data-theme="dark"]
     already assigns to these two tokens, so a dark BAND now agrees with a dark
     PAGE instead of contradicting it. */
  --orange:#3FBFB6; --cyan:#3FBFB6;
  background:var(--bg);color:var(--fg);
}
.band--dark .ibtn.solid{background:#fff;color:var(--dark);border-color:#fff}
.band--dark .btn{background:#fff;color:var(--dark);border-color:#fff}

/* --- section headers -------------------------------------------------------
   An eyebrow + oversized title + one line of standfirst. Repeated on every
   band so the page reads as one rhythm rather than a stack of components.

   THE DEAD RIGHT HALF. Reported three times and each time as missing content:
   "main page still missign tons on right side bottom, take a look yourself!"
   and "you will not leave that main page missing all to the right".

   Nothing was missing. The head was `display:flex` with `space-between`, and
   everything real — eyebrow, title and standfirst — lived in `.shead-l`, capped
   at `max-width:64ch`. Measured on a 1536px viewport that is 646px of content
   and ~890px of nothing, with a single small "See all →" link pushed to the far
   edge. A wide empty band with one stranded link at the end of it does not read
   as whitespace; it reads as a grid whose images failed to load. The owner was
   describing the layout accurately — it looked broken because it looked unfinished.

   So the head is now a real two-column editorial masthead: the title holds the
   left, and the standfirst and its link move into the right column and sit on
   the title's baseline. The space is filled with the copy that was already
   written, rather than by inventing something to put there.

   `display:contents` on `.shead-l` is what allows this without touching the
   markup on eight pages — its children become grid items of `.shead` directly,
   so the same HTML lays out in two columns here and stacks on a phone. */
.shead{display:grid;
       grid-template-columns:minmax(0,1.06fr) minmax(0,.94fr);
       grid-template-areas:"eyebrow eyebrow"
                           "title   sub"
                           "title   link";
       column-gap:clamp(30px,5vw,86px);
       align-items:end;
       margin-bottom:clamp(28px,3.4vw,52px)}
.shead-l{display:contents}
.shead .eyebrow{grid-area:eyebrow}
.shead .stitle{grid-area:title;align-self:start}
.shead .sub{grid-area:sub;align-self:end}
.shead .slink{grid-area:link;justify-self:start;margin-top:20px}

/* A head with no standfirst has nothing to put in the second column, and a
   half-width title with a void beside it is the very thing being fixed. Those
   revert to a single column and let the title use the full measure. */
.shead:not(:has(.sub)){grid-template-columns:minmax(0,1fr);
                       grid-template-areas:"eyebrow" "title" "link"}
.shead:not(:has(.sub)) .stitle{max-width:22ch}

/* One column on anything narrower than a laptop — two 45%-width columns of
   type is worse than one full-width one. */
@media(max-width:900px){
  .shead{grid-template-columns:minmax(0,1fr);
         grid-template-areas:"eyebrow" "title" "sub" "link"}
  .shead .stitle{align-self:auto}
  .shead .sub{align-self:auto}
  .shead .slink{margin-top:16px}
}
.eyebrow{font-family:var(--fm);font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;
         color:var(--fg-3);display:flex;align-items:center;gap:10px;margin-bottom:16px}
.eyebrow::before{content:"";width:26px;height:1px;background:var(--orange);flex:none}
.stitle{font-size:clamp(1.9rem,4vw,3.1rem);line-height:1.03;letter-spacing:-.045em;
        font-weight:900;margin:0}
.sub{margin:14px 0 0;color:var(--fg-2);font-size:1.03rem;line-height:1.6;max-width:60ch}
.slink{font-size:.875rem;font-weight:700;white-space:nowrap;border-bottom:2px solid var(--orange);
       padding-bottom:3px;position:relative}
/* A 44px-TALL HIT AREA THAT CHANGES NOTHING ON SCREEN.
   These read 27px tall on a phone. Padding would work but this link wears its
   own underline — `border-bottom` three pixels under the text — so vertical
   padding would shove that rule away from the words and change the design.
   An invisible overlay centred on the link grows only what a thumb lands on;
   the type, the underline and the layout are untouched. */
.slink::after{content:"";position:absolute;left:0;right:0;top:50%;
              transform:translateY(-50%);height:44px}
.slink:hover{color:var(--orange)}

/* --- full-bleed hero -------------------------------------------------------
   The image is a background layer so text never reflows around it, and a
   scrim guarantees contrast whatever photograph is dropped in. */
.hero{position:relative;min-height:min(88vh,760px);display:flex;align-items:flex-end;
      /* Same token lift as .band--dark, for the same reason. This set a dark
         background and white text but left the ACCENT at its light-surface
         value, so anything inside using var(--cyan) or var(--orange) painted
         brand teal onto near-black. Measured 2026-08-20: the hero marquee proof
         points — "Free shipping over $75" and the rest, via .strip-in b —
         rendered rgb(18,112,108) on rgb(14,17,23), 3.2:1, under AA at that size.
         Third surface to need this after .band--dark and the designs gallery.
         The rule: a dark surface reassigns its TOKENS, not just its colours. */
      --orange:#3FBFB6; --cyan:#3FBFB6;
      background:var(--dark);color:#fff;overflow:hidden;isolation:isolate}
.hero-media{position:absolute;inset:0;z-index:-2;background-size:cover;background-position:center}
.hero-media img{width:100%;height:100%;object-fit:cover}
/* Fallback texture, used only until the product mosaic loads. On its own a
   gradient behind the scrim reads as an empty black box, so the hero fills
   itself with real garment photography as soon as the catalogue resolves. */
.hero-media--fallback{
  background:
    radial-gradient(120% 90% at 78% 18%, rgba(0,240,255,.16), transparent 58%),
    radial-gradient(90% 80% at 12% 88%, rgba(255,94,43,.20), transparent 60%),
    linear-gradient(155deg,#12151D 0%,#151B26 55%,#252E3E 100%);
}
/* Mosaic of real product shots behind the hero copy. */
.hero-mosaic{position:absolute;inset:0;display:grid;
             grid-template-columns:repeat(6,1fr);grid-auto-rows:1fr}
.hero-cell{position:relative;display:block;overflow:hidden}
/* contain, not cover.
   Printful mixes full-body model shots with flat-lay garment shots, so NO
   single crop suits both: a centred cover-crop stacked one model's head under
   the next one's hips, and shifting it to the top then pushed every flat-lay
   off-centre and low. Containing the image crops nothing — each garment sits
   centred and whole in its cell, and the adjacency problem disappears with it. */
.hero-mosaic img{width:100%;height:100%;object-fit:contain;object-position:center;
                 padding:6px;opacity:.6;filter:grayscale(.2) contrast(1.04);
                 transition:opacity .3s,filter .3s,transform .5s}
/* Each tile links to its product; lifting the dimming on hover is the cue
   that the hero is interactive at all. */
a.hero-cell:hover img{opacity:1;filter:none;transform:scale(1.04)}
.hero-tip{position:absolute;left:0;right:0;bottom:0;padding:8px 10px;font-size:.68rem;
          font-weight:700;letter-spacing:-.01em;color:#fff;background:linear-gradient(
          to top,rgba(21,27,38,.9),transparent);opacity:0;transition:opacity .25s;
          pointer-events:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
a.hero-cell:hover .hero-tip{opacity:1}
/* The hero copy must stay readable and clickable over the linked tiles — but
   its BOX is full-width and mostly empty space, and that empty space was
   blocking the tiles underneath it. Only the actual text and controls take
   clicks now; everywhere else falls through to the product links. */
.hero-in{position:relative;z-index:2;pointer-events:none}
.hero-in a,.hero-in button,.hero-in input,.hero-in .hero-stats{pointer-events:auto}
@media(max-width:640px){.hero-tip{display:none}}
@media(max-width:900px){.hero-mosaic{grid-template-columns:repeat(3,1fr)}}
@media(max-width:560px){.hero-mosaic{grid-template-columns:repeat(2,1fr)}}
/* pointer-events:none is load-bearing.
   This scrim sits at z-index -1, ABOVE the mosaic at -2, and covers the whole
   hero. Pseudo-elements capture clicks like any other box, so it intercepted
   every one before it could reach a tile — the product links were all present
   and none of them were reachable. */
/* AND IT HAS TO DARKEN WHERE THE WORDS ARE, NOT JUST WHERE THE PAGE ENDS.
   The scrim was a single bottom-to-top gradient: .92 at the foot, .55 at 38%,
   .12 at 72%, nothing above that. The copy does not live at the foot. The
   eyebrow and the headline sit in the upper half, which is to say in the part
   with almost no scrim at all, directly over a mosaic of product photographs
   that includes a light blue sweatshirt, a lilac tee and a bright grey
   backdrop.

   Photographed at 1440px on /bulk/: "BULK & BUSINESS" in 70% white over a
   pale grey cell was close to invisible, and the teal word "honestly" — the
   payoff of the whole headline — sat over a light green tee. check-contrast
   reports 0 problems on this page because it skips text over art, so nothing
   was ever going to report it.

   A SECOND GRADIENT ACROSS RATHER THAN A DARKER ONE UP. Raising the vertical
   scrim would dim the mosaic everywhere, and the mosaic is the point of this
   hero — it is real garments, and every cell is a link to one. Darkening only
   the column the copy occupies leaves the right-hand cells at full strength.
   It is the same move as the scrimmed corner on the editorial break. */
.hero::after{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:
    linear-gradient(to right,rgba(21,27,38,.88) 0%,rgba(21,27,38,.66) 30%,
                             rgba(21,27,38,.18) 52%,rgba(21,27,38,0) 66%),
    linear-gradient(to top,rgba(21,27,38,.92) 0%,rgba(21,27,38,.55) 38%,rgba(21,27,38,.12) 72%)}
/* On a narrow screen the copy is the full width, so a gradient that clears by
   two thirds across would leave the right-hand end of every line unscrimmed. */
@media(max-width:900px){
  .hero::after{background:
    linear-gradient(to right,rgba(21,27,38,.88) 0%,rgba(21,27,38,.78) 55%,rgba(21,27,38,.6) 100%),
    linear-gradient(to top,rgba(21,27,38,.92) 0%,rgba(21,27,38,.55) 38%,rgba(21,27,38,.12) 72%)}
}
/* Bottom padding ONLY. This was `padding:0 0 clamp(...)`, which is shorthand —
   it reset the left/right padding .wrap sets, so every hero on the site ran its
   headline, paragraph and buttons flush into both screen edges. Invisible on a
   wide desktop where the 1280px max-width leaves slack; on a phone the copy
   started at x=0 and the paragraph was clipped mid-word. */
.hero-in{padding-bottom:clamp(48px,6vw,88px);width:100%}
.hero h1{font-size:clamp(2.6rem,7.4vw,6rem);line-height:.94;letter-spacing:-.055em;
         font-weight:900;margin:0;max-width:16ch}
/* Deep brand teal disappears against the dark hero scrim, so the accent word
   uses a bright tint that actually pops off the photography. */
.hero h1 em{font-style:normal;color:#3FE0D0}
.hero p{margin:22px 0 0;font-size:clamp(1rem,1.5vw,1.2rem);max-width:52ch;line-height:1.55;
        color:rgba(255,255,255,.8)}
.hero .eyebrow{color:rgba(255,255,255,.7)}
.hero-cta{display:flex;gap:12px;margin-top:34px;flex-wrap:wrap}
.hero-cta .btn{background:#fff;color:var(--ink);border-color:#fff;padding:16px 30px}
.hero-cta .btn.ghost{background:transparent;color:#fff;border-color:rgba(255,255,255,.42)}
.hero-cta .btn.ghost:hover{border-color:#fff;opacity:1}
/* Marquee of proof points pinned to the hero foot. */
.hero-stats{display:flex;gap:clamp(20px,4vw,56px);margin-top:clamp(36px,5vw,60px);
            padding-top:26px;border-top:1px solid rgba(255,255,255,.16);flex-wrap:wrap}
.hero-stats div{min-width:0}
/* Four stats in a row do not fit a phone: they either squash to unreadable or
   push the hero wider than the viewport. Two-up grid below 560px. */
@media(max-width:560px){
  .hero-stats{display:grid;grid-template-columns:1fr 1fr;gap:18px 20px}
}
.hero-stats b{display:block;font-size:clamp(1.2rem,2vw,1.6rem);letter-spacing:-.03em;font-weight:800}
.hero-stats span{font-family:var(--fm);font-size:.62rem;letter-spacing:.15em;text-transform:uppercase;
                 color:rgba(255,255,255,.6)}

/* --- product grid ----------------------------------------------------------
   Deliberately EVEN. This started out as a 12-column asymmetric grid with hero
   tiles spanning 7 and others 5 — it read as cluttered and misaligned once real
   products of differing image heights filled it, so every tile is now the same
   size on a uniform track. The .t-* classes are kept so existing markup keeps
   working; they no longer change the span except .t-full. */
/* auto-FIT + justify-content:center, not auto-fill: a final row holding one or
   two cards centres under the grid instead of hugging the left edge with a
   wide dead gap beside it. max-width on the track stops 1-2 cards stretching
   to fill the whole row. */
/* FLEX, not grid, specifically so a partial final row centres.
   `justify-content:center` on a GRID centres the whole track grid inside the
   container — it cannot centre the items of an incomplete last row, which still
   pack into the leading tracks and leave a wide dead gap to their right. A
   catalogue almost never divides evenly by the column count, so that ragged
   last row showed on nearly every view. flex-wrap has no fixed tracks, so the
   final line centres naturally.
   flex-basis 230px with a max-width cap reproduces the old minmax(230px,280px)
   sizing, so full rows look identical to before. */
.agrid,.grid{display:flex;flex-wrap:wrap;justify-content:center;
       gap:clamp(12px,1.4vw,20px);align-items:stretch}
.agrid > *,.grid > *{flex:1 1 230px;max-width:var(--tile-max,280px);min-width:0}
/* Full-bleed children (empty states, editorial blocks) still span the row. */
.agrid .t-full,.grid .t-full,.agrid > .empty,.grid > .empty{
       flex:1 1 100%;max-width:100%}
@media(max-width:560px){
  .agrid > *,.grid > *{flex:1 1 150px;max-width:none}
}

/* --- simplified product tile -----------------------------------------------
   Image, name, price. Nothing else. Swatches, sizes, style codes and volume
   copy all moved to the PDP — a grid that tries to sell every attribute at
   once sells none of them. */
.tile{display:flex;flex-direction:column;background:var(--bg);position:relative;
      overflow:hidden;border-radius:var(--r);border:1px solid var(--line)}
.tile:hover{border-color:var(--fg-3)}
/* A SOFT NEUTRAL, NOT WHITE — a white garment shot on a white backdrop and
   contain-fitted onto a white tile is an invisible product. Reported as "lots
   of blocks main page in white that are empty"; the images were all loading.
   Measuring luminance per photo (as the gallery does) is impossible here —
   these come from Printful's CDN, so a canvas read taints and throws. */
.tile-img{position:relative;aspect-ratio:4/5;overflow:hidden;background:#F1F2F4}
/* contain, not cover — Printful supplies full-body model shots and cover
   guillotines the model's head at this aspect ratio. */
.tile-img img{width:100%;height:100%;object-fit:contain;
              transition:transform .6s cubic-bezier(.2,.7,.3,1)}
.tile:hover .tile-img img{transform:scale(1.05)}
.tile-ph{position:absolute;inset:0;display:grid;place-items:center;font-family:var(--fm);
         font-size:.62rem;letter-spacing:.15em;color:var(--fg-3)}
.tile-body{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
           padding:14px 16px 16px;border-top:1px solid var(--line)}
/* Clamp to two lines with a reserved two-line box, so titles of differing
   length do not leave ragged gaps and every card's price sits on one baseline. */
.tile-body h3{margin:0;font-size:.95rem;font-weight:700;letter-spacing:-.025em;line-height:1.35;
              display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
              overflow:hidden;min-height:2.7em}
.tile-price{font-weight:800;font-size:1rem;letter-spacing:-.02em;white-space:nowrap;
            font-variant-numeric:tabular-nums}
/* WHO MADE IT — on a creator storefront, the whole reason the visitor is here.
   .tile-body is a baseline row of title and price, so a third child would become
   a column and squeeze both. This takes a full-width line of its own under them
   instead, which also keeps every card's price on the same baseline. */
.tile-body{flex-wrap:wrap}
.tile-credit{flex:1 0 100%;order:3;font-size:.72rem;color:var(--fg-3);
             letter-spacing:.01em;margin-top:2px}
.tile .flag{background:var(--ink);color:#fff}
.band--dark .tile .flag{background:var(--cyan);color:var(--ink)}

/* --- editorial lifestyle break ---------------------------------------------
   Sits between product rows to break the catalogue cadence. Text sits in a
   scrimmed corner so it stays legible over any photograph. */
.edit{position:relative;min-height:clamp(340px,44vw,560px);display:flex;align-items:flex-end;
      overflow:hidden;border-radius:var(--r);isolation:isolate;background:var(--ink);color:#fff}
.edit--tall{min-height:clamp(420px,54vw,680px)}
.edit-media{position:absolute;inset:0;z-index:-2}
.edit-media img{width:100%;height:100%;object-fit:cover}
.edit::after{content:"";position:absolute;inset:0;z-index:-1;
  background:linear-gradient(to top,rgba(21,27,38,.9),rgba(21,27,38,.28) 55%,transparent)}
.edit-in{padding:clamp(26px,3.4vw,46px);max-width:56ch}
.edit h3{margin:0;font-size:clamp(1.5rem,2.9vw,2.4rem);letter-spacing:-.04em;line-height:1.08;
         font-weight:900}
.edit p{margin:14px 0 0;color:rgba(255,255,255,.82);line-height:1.6;font-size:.98rem}
.edit .eyebrow{color:rgba(255,255,255,.72)}
.edit-link{display:inline-block;margin-top:22px;font-weight:700;font-size:.9rem;
           border-bottom:2px solid var(--orange);padding-bottom:3px}
/* Editorial fallbacks — distinct enough that two breaks on one page don't twin. */
.ed-f1{background:radial-gradient(100% 100% at 20% 0%,#1B2230,#151B26 70%)}
.ed-f2{background:radial-gradient(100% 100% at 80% 10%,#2A1710,#151B26 68%)}
.ed-f3{background:radial-gradient(100% 100% at 50% 100%,#0C2126,#151B26 66%)}

/* --- feature split ---------------------------------------------------------
   Two-up: copy on one side, visual on the other. Reversible per instance. */
.split{display:grid;grid-template-columns:1fr 1fr;gap:clamp(28px,4vw,72px);align-items:center}
.split--rev .split-copy{order:2}
@media(max-width:860px){.split{grid-template-columns:1fr}.split--rev .split-copy{order:0}}
.split-visual{aspect-ratio:4/3;border-radius:var(--r);overflow:hidden;background:var(--bg-3);
              position:relative}
.split-visual img{width:100%;height:100%;object-fit:cover}

/* --- generic bits ----------------------------------------------------------*/
.ticks{list-style:none;margin:26px 0 0;padding:0;display:flex;flex-direction:column;gap:13px}
.ticks li{padding-left:28px;position:relative;color:var(--fg-2);line-height:1.55}
.ticks li::before{content:"";position:absolute;left:0;top:.5em;width:13px;height:2px;
                  background:var(--orange)}
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
      gap:clamp(20px,2.4vw,36px)}
.cardy{border:1px solid var(--line);border-radius:var(--r);padding:26px;background:var(--bg)}
.cardy h4{margin:0 0 9px;font-size:1.02rem;font-weight:800;letter-spacing:-.025em}
.cardy p{margin:0;color:var(--fg-3);font-size:.9rem;line-height:1.6}
.num{font-family:var(--fm);font-size:.66rem;letter-spacing:.15em;color:var(--orange);
     margin-bottom:14px;display:block}
.btn.ghost{background:transparent;color:var(--fg);border-color:var(--line)}
.btn.ghost:hover{border-color:var(--fg-3);opacity:1}

/* --- toast notifications ---------------------------------------------------
   Add-to-cart / subscribe / error feedback. Live region so screen readers
   announce it; auto-dismissing so it never needs a close affordance. */
.toasts{position:fixed;bottom:22px;left:22px;z-index:120;display:flex;flex-direction:column;
        gap:10px;pointer-events:none;max-width:min(380px,calc(100vw - 44px))}
.toast{background:var(--ink);color:#fff;border-radius:var(--r);padding:14px 18px;
       font-size:.875rem;font-weight:600;box-shadow:0 12px 34px rgba(0,0,0,.3);
       display:flex;align-items:center;gap:11px;pointer-events:auto;
       animation:toastin .26s cubic-bezier(.2,.8,.3,1)}
.toast.err{background:#B42318}
.toast.ok::before{content:"✓";color:var(--cyan);font-weight:900}
.toast.err::before{content:"!";font-weight:900}
@keyframes toastin{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){
  .toast{animation:none}
  .tile-img img,.pimg img{transition:none}
  html{scroll-behavior:auto}
}

/* --- nav search ------------------------------------------------------------*/
.navsearch{display:flex;align-items:center;gap:7px;border:1px solid var(--line);
           border-radius:var(--r);padding:0 11px;height:38px;background:var(--bg-2);
           transition:border-color .18s}
.navsearch:focus-within{border-color:var(--orange);background:var(--bg)}
.navsearch svg{width:15px;height:15px;color:var(--fg-3);flex:none}
.navsearch input{border:0;background:none;color:var(--fg);font:inherit;font-size:.85rem;
                 width:150px;outline:none}
.navsearch input::-webkit-search-cancel-button{filter:grayscale(1)}
/* Visible submit arrow — the box used to accept only the Enter key, with
   nothing to click. Sits inside the field so it reads as part of the control
   rather than a separate button. */
.navsearch-go{flex:none;display:grid;place-items:center;width:24px;height:24px;padding:0;
              border:0;border-radius:6px;background:transparent;color:var(--fg-3);
              cursor:pointer;transition:background .16s,color .16s}
.navsearch-go:hover{background:var(--orange);color:#fff}
.navsearch:focus-within .navsearch-go{color:var(--orange)}
.navsearch:focus-within .navsearch-go:hover{color:#fff}
.navsearch-go svg{width:14px;height:14px;color:inherit}
@media(max-width:1100px){.navsearch input{width:104px}}
/* Only the HEADER copy hides on a narrow row — scoped to .navact so it does not
   also hide the copy inside the mobile menu panel, which is the whole point of
   that one existing. */
@media(max-width:960px){.navact .navsearch{display:none}}

/* Search inside the mobile menu panel. Hidden on desktop, where the header
   search is already visible. */
.navsearch--panel{display:none}
@media(max-width:1180px){
  .links .navsearch--panel{display:flex;margin:8px 18px 12px;height:44px}
  .links .navsearch--panel input{width:100%;flex:1;font-size:1rem}
  /* The panel's links carry a bottom border; the form is not a link and must
     not inherit that separator treatment. */
  .links .navsearch--panel{border:1px solid var(--line)}
}

/* --- social / share --------------------------------------------------------*/
.social{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:38px;
        padding-top:26px;border-top:1px solid var(--line)}
.social a,.social button{display:grid;place-items:center;width:34px;height:34px;
        border:1px solid var(--line);border-radius:99px;color:var(--fg-2);
        transition:color .18s,border-color .18s,transform .18s;background:none;cursor:pointer}
.social a:hover,.social button:hover{color:var(--orange);border-color:var(--orange);
        transform:translateY(-2px)}
.social svg{width:16px;height:16px}
.sl-lab{font-family:var(--fm);font-size:.6rem;letter-spacing:.15em;text-transform:uppercase;
        color:var(--fg-3);margin-right:2px}
.sl-sep{width:1px;height:20px;background:var(--line);margin:0 6px}

/* --- option pickers --------------------------------------------------------
   Color swatches and size/option chips. These lived only inside the Designs
   page's <style> block, so the Studio's picker rendered its size buttons and
   placement chips as unstyled bare text — the "dropdowns not working" bug.
   Shared here so every picker on the site looks and behaves the same. */
.chips{display:flex;gap:7px;flex-wrap:wrap}
.chipb{border:1px solid var(--line);border-radius:var(--r);padding:9px 13px;font-size:.82rem;
       cursor:pointer;background:var(--bg);color:var(--fg);transition:all .15s}
.chipb:hover{border-color:var(--fg-3)}
.chipb.on{background:var(--ink);color:#fff;border-color:var(--ink)}
.chipb:disabled{opacity:.32;cursor:not-allowed;text-decoration:line-through}
.swb{width:30px;height:30px;border-radius:99px;border:1px solid rgba(0,0,0,.22);cursor:pointer;
     padding:0;flex:none;transition:box-shadow .15s}
/* .swb is a COLOUR SWATCH: fixed 30px, no padding. Never combine it with .chipb
   on a chip that carries a text label — the label has nowhere to go and spills
   out of the circle. The Studio's placement chips did exactly that. */
/* A multi-print line, broken down. An unexplained higher unit price on a
   garment the shopper knows the sticker price of reads as an overcharge. */
.ciextra{margin-top:5px;padding:6px 8px;border-radius:7px;background:rgba(9,10,15,.045)}
.ciextrah{font-size:.68rem;font-weight:600;opacity:.85;margin-bottom:3px}
.ciextral{display:flex;justify-content:space-between;gap:9px;font-size:.66rem;opacity:.7;
          line-height:1.5}
.swb.on{box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--ink)}
/* Native selects need to match the chips, or a picker looks half-finished. */
.selct{width:100%;padding:12px 14px;border:1px solid var(--line);border-radius:10px;
       background:var(--bg-2);color:var(--fg);font:inherit;font-size:.93rem;cursor:pointer}
.selct:focus{outline:none;border-color:var(--orange)}

/* --- active design bar -----------------------------------------------------
   Follows the visitor from the Studio to whichever garment they pick, so the
   artwork they just made is visibly still in play. */
.designbar{position:fixed;left:22px;bottom:22px;z-index:115;display:flex;align-items:center;
           gap:13px;padding:10px 12px 10px 10px;background:var(--bg);border:1px solid var(--line);
           border-radius:12px;box-shadow:0 14px 40px rgba(0,0,0,.22);
           max-width:min(430px,calc(100vw - 44px))}
.designbar img{width:46px;height:46px;object-fit:contain;background:#fff;border-radius:7px;flex:none}
.db-txt{min-width:0;flex:1}
.db-txt b{display:block;font-size:.85rem;letter-spacing:-.02em}
.db-txt span{display:block;font-size:.75rem;color:var(--fg-3);overflow:hidden;
             text-overflow:ellipsis;white-space:nowrap}
/* PRODUCT NAMES ARE LONG AND THE CARDS GET NARROW.
   "All-Over Print Recycled String Bikini" needs three lines in a half-width
   phone card, and the 2-line clamp cut it mid-word — so several swimwear
   products were indistinguishable from each other in the grid. The clamp still
   exists (ragged card heights look broken) but it is loosened where the column
   is narrow, and min-height goes with it or the rows stop lining up. */
@media(max-width:640px){
  .pbody h3,.tile-body h3{-webkit-line-clamp:3;min-height:4.05em;font-size:.9rem}
}
@media(max-width:640px){
  .designbar{left:12px;right:12px;bottom:12px;max-width:none}
  .cbtn,.chat{bottom:88px}
  /* THE PANEL'S HEIGHT HAS TO KNOW IT WAS MOVED UP.
     The base rule is `bottom:22px; height:calc(100vh - 60px)`. This override
     raises it to bottom:88px but left the height alone, so the top of the panel
     sat 100vh + 28px above the bottom — i.e. 28px OFF THE TOP OF THE SCREEN,
     taking the header and the close button with it. It filled the display and
     could not be shut.
     dvh, not vh: on a phone `vh` is the tallest the viewport ever gets, with
     the address bar retracted, so `100vh` overflows a screen that still shows
     one. `dvh` tracks what is actually visible. vh first as the fallback. */
  .chat{right:12px;left:12px;width:auto;
        height:min(540px,calc(100vh - 108px));
        height:min(540px,calc(100dvh - 108px))}
  /* Nothing important should end up underneath the launcher. */
  body{padding-bottom:76px}
}
/* AND NOTHING AT ALL WHILE THE CART IS OPEN.
   ==========================================================================
   `body{padding-bottom:76px}` keeps the launcher off the end of the PAGE. The
   cart drawer is `position:fixed`, so page padding does nothing for it, and its
   Checkout button sits at the bottom of the panel — exactly where the launcher
   is. Seen on a phone: the chat bubble sitting on top of the primary action of
   the whole site, on the one screen where the shopper is deciding to pay.

   Hidden rather than moved: the drawer covers the page it would be offering
   help with, and a button that has to dodge another button will collide again
   the next time either moves. The drawer has its own close, and the launcher
   comes straight back when it shuts. */
body:has(.drawer.on) .cbtn,
body:has(.drawer.on) .chat{display:none}


/* --- support chat ----------------------------------------------------------
   Moved out of the homepage and into shared chrome so support is reachable
   from the PDP, bulk quote and archive too — not only the front page. */
.cbtn{position:fixed;right:22px;bottom:22px;z-index:110;width:54px;height:54px;border-radius:99px;
      background:var(--ink);color:#fff;display:grid;place-items:center;font-size:1.3rem;
      box-shadow:0 10px 30px rgba(0,0,0,.26)}
.cbtn:hover{transform:translateY(-2px)}
.chat{position:fixed;right:22px;bottom:22px;z-index:111;width:min(374px,calc(100vw - 44px));
      height:min(540px,calc(100vh - 60px));background:var(--bg);border:1px solid var(--line);
      border-radius:10px;box-shadow:0 24px 60px rgba(0,0,0,.3);display:none;flex-direction:column;
      overflow:hidden}
.chat.on{display:flex}
.chh{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:15px 17px;
     border-bottom:1px solid var(--line);background:var(--bg-2)}
.chh b{display:block;font-size:.92rem;letter-spacing:-.02em}
.chh span{font-size:.74rem;color:var(--fg-3)}
/* Brand lockup in the chat header.
   `flex:none` so the title block keeps the remaining width rather than the logo
   stretching. 26px keeps the header to a single 60px row — at 30px the subtitle
   wrapped and the header grew to 105px, which read as broken rather than
   branded. The subtitle is clamped to one line for the same reason. */
.chh .chlogo{height:26px;width:auto;flex:none;display:block}
.chh > div{flex:1;min-width:0}
.chh span{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chb{flex:1;overflow-y:auto;padding:17px;display:flex;flex-direction:column;gap:11px}
.msg{max-width:84%;padding:11px 14px;border-radius:12px;font-size:.875rem;line-height:1.5}
.msg.a{background:var(--bg-2);border:1px solid var(--line);align-self:flex-start;
       border-bottom-left-radius:3px}
.msg.u{background:var(--ink);color:#fff;align-self:flex-end;border-bottom-right-radius:3px}
.chf{display:flex;gap:8px;padding:13px;border-top:1px solid var(--line)}
.chf input{flex:1;min-width:0;padding:11px 13px;border:1px solid var(--line);border-radius:var(--r);
           background:var(--bg);color:var(--fg);font:inherit;font-size:.875rem}
.chf button{padding:0 17px;background:var(--ink);color:#fff;border-radius:var(--r);
            font-weight:700;font-size:.85rem}

/* ===========================================================================
   THE INSTALL BAR — see the block at the end of site.js for why it exists.

   One line, pinned to the bottom, above the fold of nothing. It must never
   cover the thing somebody came for: on the Studio that is the garment and the
   Design it button, both of which sit low on a phone. So it is a slim bar that
   pushes nothing and animates in from below rather than a modal.
   =========================================================================== */
#shInstallBar{
  position:fixed; left:50%; bottom:calc(12px + env(safe-area-inset-bottom));
  transform:translate(-50%,140%);
  z-index:9998;                       /* under toasts, over the page */
  display:flex; align-items:center; gap:12px;
  width:min(560px, calc(100vw - 24px));
  padding:10px 12px;
  background:var(--bg); color:var(--fg);
  border:1px solid var(--line); border-radius:12px;
  box-shadow:0 8px 30px rgba(0,0,0,.22);
  transition:transform .26s cubic-bezier(.2,.7,.3,1), opacity .26s;
  opacity:0;
}
#shInstallBar.on{ transform:translate(-50%,0); opacity:1 }
#shInstallBar img{ flex:0 0 auto; border-radius:8px }
#shInstallBar .sh-ib-t{ flex:1 1 auto; min-width:0; display:block; line-height:1.3 }
#shInstallBar .sh-ib-t b{ display:block; font-size:.86rem }
#shInstallBar .sh-ib-t small{ display:block; color:var(--fg-3); font-size:.74rem }
#shInstallBar button{ font:inherit; cursor:pointer; flex:0 0 auto }
#shInstallBar .sh-ib-go{
  padding:9px 16px; border-radius:8px; border:0;
  background:var(--fg); color:var(--bg); font-weight:600; font-size:.82rem;
}
#shInstallBar .sh-ib-go[disabled]{ opacity:.6; cursor:default }
/* 44x44, because this is the DISMISS on a bar that covers the page.
   Measured at 390px: 28 wide by 35 tall. A close button is the one control a
   person stabs at without looking, and 28px is narrower than a fingertip — miss
   it and you hit whatever the bar is sitting over. The glyph is unchanged; only
   the area around it grows, so nothing looks different. */
#shInstallBar .sh-ib-x{
  min-width:44px; min-height:44px;
  display:inline-flex; align-items:center; justify-content:center;
  padding:6px 8px; border:0; background:none; color:var(--fg-3); font-size:.9rem;
}
#shInstallBar .sh-ib-x:hover{ color:var(--fg) }
/* On a narrow phone the sentence is what goes, not the button — the button is
   the entire point of the bar. */
@media (max-width:400px){
  #shInstallBar .sh-ib-t small{ display:none }
  #shInstallBar{ gap:9px; padding:9px 10px }
}
/* Desktop keeps the full sentence; the short label is for phones. */
#shInstallBar .sh-ib-short{ display:none }
/* A PILL, NOT A BAR, ON A PHONE (live QA 2026-09-29). Full width at 390px it
   hid the bottom row of tiles on every page. Now it hugs the bottom-left
   corner, one short line, and leaves the right-hand side (where the chat
   bubble and most tiles' price lines sit) clear. layoutBottomStack() still
   sets its bottom, so it stacks above any design bar. */
@media (max-width:640px){
  #shInstallBar{ left:12px; width:auto; max-width:calc(100vw - 96px);
    transform:translate(0,140%); gap:8px; padding:0 0 0 8px; border-radius:99px }
  #shInstallBar.on{ transform:translate(0,0) }
  #shInstallBar img{ width:24px; height:24px; border-radius:6px }
  #shInstallBar .sh-ib-long{ display:none }
  #shInstallBar .sh-ib-short{ display:inline }
  #shInstallBar .sh-ib-t b{ font-size:.78rem; white-space:nowrap }
  #shInstallBar:not(.ios) .sh-ib-t small{ display:none }
  #shInstallBar.ios .sh-ib-t small{ font-size:.68rem }
  #shInstallBar .sh-ib-go{ padding:6px 11px; font-size:.76rem; border-radius:99px }
}
/* Nobody should be surprised by a bar sliding up under their thumb. */
@media (prefers-reduced-motion:reduce){
  #shInstallBar{ transition:opacity .2s }
}

/* The chat bubble while a phone scrolls: see the note beside layoutBottomStack
   in site.js. Faded, not moved, and not clickable while faded. */
.cbtn{transition:opacity .2s ease,transform .2s ease}
.cbtn.away{opacity:0;pointer-events:none;transform:translateY(8px)}
@media (prefers-reduced-motion:reduce){ .cbtn.away{transform:none} }
@media (max-width:640px){ .cbtn{width:46px;height:46px;font-size:1.1rem} }

/* ===========================================================================
   PER-DESIGNER ROWS on the homepage — see loadStaffPicks() in site.js.

   Horizontal so a designer with thirty pieces does not push the rest of the
   front page off the screen, and so each person's body of work reads as theirs
   rather than as part of one anonymous pile.
   =========================================================================== */
/* A ROW IS NOT A TILE. #staffPicks carries .agrid, whose `> *` rule caps every
   direct child at `--tile-max` (280px) and centres the line — sizing meant for
   product cards. The designer rows are full-width blocks, so each one was
   squeezed into a 280px column floating in the middle of the band, with the
   horizontal strip inside it clipping the second tile at the container edge.
   Reported 2026-09-18 with a screenshot: "our staff designs did not show good
   or in full". Same shape as the existing .t-full escape hatch above. */
.agrid > .crow{ flex:1 1 100%; max-width:100% }
.crow{ margin:0 0 26px }
.crow:last-child{ margin-bottom:0 }
.crow-head{
  display:flex; align-items:baseline; justify-content:space-between; gap:14px;
  margin:0 0 12px;
}
.crow-head b{
  font-family:var(--fm,inherit); font-size:.72rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--fg-2);
}
.crow-in{
  display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x proximity;
  /* Bleed to the edge on a phone so a half-visible tile signals "there is more
     this way" — a row that stops flush at the margin reads as complete. */
  padding:2px 16px 6px; margin:0 -16px;
  scrollbar-width:none; -webkit-overflow-scrolling:touch;
}
.crow-in::-webkit-scrollbar{ display:none }
.crow-in > .tile{
  flex:0 0 auto; width:clamp(150px, 42vw, 216px); scroll-snap-align:start;
}
@media (min-width:900px){
  .crow-in{ padding-left:0; padding-right:0; margin:0 }
  /* A designer with three pieces should not read as three stamps in the corner
     of an empty band. 216px was sized for a strip that scrolls; on a desktop
     row that fits everything, the artwork is the point — so the tiles take the
     width they would have in any other grid on this page. */
  /* AND A FIXED WIDTH CANNOT DELIVER THAT, WHICH IS WHY IT STILL LOOKED WRONG.
     Photographed at 1440px with the only three staff designs that exist: the
     row ended at 911px inside a 1225px band, leaving 420px — a full quarter of
     the row — empty beside it, which reads as a fourth card that failed to
     load. Widening the tile from 216 to 276 moved that number and could never
     close it, because three fixed-width tiles leave whatever they leave.

     Letting them GROW closes it, and the same rule still behaves for the case
     the strip was built for. `flex-grow:1` only has an effect when there is
     free space, so a designer with three pieces fills the row, while a
     designer with thirty has no free space, keeps the basis, and scrolls
     exactly as before. `flex-shrink` stays 0 for that second case — shrinking
     a scrolling strip to fit would defeat the point of it scrolling.

     The cap stops two pieces becoming billboards. */
  .crow-in > .tile{ flex:1 0 clamp(200px, 18vw, 276px); width:auto; max-width:400px }
}

/* "Staff designed" under the initials, never appended to them. Owner:
   "can say staff designed under it". */
.crow-head b i{
  display:block; font-style:normal; font-weight:500;
  letter-spacing:.08em; font-size:.62rem; color:var(--fg-3); margin-top:3px;
}

/* ==========================================================================
   MOTION: REVEAL ON SCROLL, AND THE THINGS THAT MOVE UNDER THE POINTER
   ==========================================================================

   Why the hidden state is applied by JAVASCRIPT and not by this stylesheet.

   The obvious way to write this is to hide everything here and let a script
   reveal it. That is a content-destroying bug waiting for the first time the
   script does not run: a parse error anywhere in site.js, a blocked CDN, a
   browser with scripting off, or the file simply arriving late — and the page
   is blank prose-free furniture, indexed and screenshotted that way.

   So nothing below hides anything on its own. The reveal class is NEVER
   written into the HTML; it is added, element by element, by revealOnScroll()
   in site.js, and only after that function has confirmed it holds an
   IntersectionObserver to bring the content back with. If it never runs, no
   element ever carries the class, every rule here matches nothing, and the
   page renders exactly as it did before any of this was written.

   THE RULES HANG OFF THE ELEMENT'S OWN CLASSES AND NOTHING ELSE, and that is
   a correction rather than a preference. The first version gated them on a
   class set on the document element. It read well and it measured wrong: a
   freshly created div picked the hidden state up correctly, while the real
   section heads — matching the identical selector, with that class confirmed
   present on the document element — computed to opacity 1 and never animated.
   A rule that needs only the element in front of it is also a rule that
   cannot be broken from somewhere else in the document.

   Everything stops entirely under prefers-reduced-motion, which is enforced in
   the script as well as here — the script refuses to arm, so the class is
   never applied in the first place. */

.reveal{
  opacity:0;
  transform:translateY(18px);
  will-change:opacity,transform;
  transition:opacity .7s cubic-bezier(.2,.7,.3,1),
             transform .7s cubic-bezier(.2,.7,.3,1);
}
.reveal.shown{opacity:1;transform:none}

/* Children of a revealed row come in one after another rather than as a slab.
   The delay is small on purpose: past about 90ms per item a six-tile row takes
   longer to assemble than a visitor will wait before scrolling past it. */
.reveal-stagger > *{
  opacity:0;
  transform:translateY(14px);
  transition:opacity .55s cubic-bezier(.2,.7,.3,1),
             transform .55s cubic-bezier(.2,.7,.3,1);
}
.reveal-stagger.shown > *{opacity:1;transform:none}
.reveal-stagger.shown > *:nth-child(1){transition-delay:0ms}
.reveal-stagger.shown > *:nth-child(2){transition-delay:70ms}
.reveal-stagger.shown > *:nth-child(3){transition-delay:140ms}
.reveal-stagger.shown > *:nth-child(4){transition-delay:210ms}
.reveal-stagger.shown > *:nth-child(5){transition-delay:280ms}
.reveal-stagger.shown > *:nth-child(6){transition-delay:350ms}
/* Past six, everything shares the last step. A row of twenty tiles staggered
   all the way out would still be arriving after the visitor had scrolled on. */
.reveal-stagger.shown > *:nth-child(n+7){transition-delay:420ms}

/* ==========================================================================
   THE GARMENT SHOULD FILL ITS TILE
   ==========================================================================

   The photograph is the product. Every tile on this site was drawing it at
   roughly two thirds of the space available: object-fit:contain letterboxes a
   4:5 photo inside a 4:5 panel and then the shot's own studio margin takes
   another slice, so a tee ended up a small object floating in a large pale
   rectangle. That reads as a placeholder, not as something to want.

   SCALED, NOT RE-FITTED, AND THE DIFFERENCE MATTERS HERE.
   The obvious change is cover instead of contain. It is the wrong one on this
   catalogue and it has already been tried and reverted once on the hero: these
   are portrait studio shots, so filling the box centre-crops them, and what
   gets cropped is the part that sells — the hoodie lost the model's face, and
   a tank top read "SUNBURNT AND" with the punchline cut off. contain keeps the
   whole garment; a transform then spends the letterbox margin that contain
   leaves behind. Nothing is cropped that the photographer framed.

   1.12 rather than the 1.2+ that looks better on the tidiest shots. The
   catalogue is mixed — some are cut-outs with generous margin, some are
   full-bleed lifestyle frames with the garment already near the edge — and the
   scale has to be safe on the tightest of them, not flattering on the loosest.

   The panel colour stays. It is a soft neutral rather than white on purpose,
   because a white garment on a white backdrop on a white card is an invisible
   product, which is what "lots of blocks main page in white that are empty"
   turned out to mean. */
/* LET THE PHOTOGRAPH BE THE EDGE.
   A white card, hairline border, rounded corner and a divider rule above the
   price is the default shape of every store template built since about 2015,
   and on a white band the border is the only thing distinguishing the card
   from the page — a line drawn around nothing. The garment panel is already a
   soft grey rectangle with a radius; that is a boundary, and a better one,
   because it is the product making it.

   The border is made TRANSPARENT rather than removed, and the width is left
   alone. Taking it out would pull every tile 1.6px wider and reflow rows that
   are tuned to fit four across; this changes what is drawn and nothing that
   is measured. Same reasoning for the divider above the price.

   Dark bands keep their card surface, which still reads as a panel against
   the near-black. Only the outline goes. */
/* .pcard IS THE SHOP PAGE'S OWN CARD, AND IT HAD BEEN LEFT BEHIND.
   The homepage draws products as .tile/.tile-img; /shop/ and the category
   pages draw the same products as .pcard/.pimg — a parallel implementation.
   Every change above was written against the homepage's class names, so the
   catalogue page that 67 products actually live on kept the old boxed-in
   look. Whatever is done to one has to be done to the other or the site
   disagrees with itself one click in. */
.tile,.rcard,.pcard{border-color:transparent}
.tile-body{border-top-color:transparent}

/* THE PRICE GETS ITS OWN LINE, ALWAYS — THE RESERVED TITLE BOX WAS NOT ENOUGH.
   ==========================================================================
   `.tile-body h3` already clamps to two lines and reserves 2.7em for them,
   and the note beside it says the point is that "every card's price sits on
   one baseline". Photographed at 1440px, they did not. In one row of four:

     Men's Premium Tank Top          $37.99   <- same line as the name
     Unisex Garment-Dyed Pocket
     T-Shirt
     $44.99                                   <- third line
     Women's Micro-Rib Tank Top
     $36.99                                   <- second line

   Reserving height for the title does not stop a SHORT title sharing the
   first flex line with the price. `.tile-body` wraps, so whether the price
   sits beside the name or beneath it depends on whether the two happen to fit
   — which means it depends on the product's name, and changes row to row. The
   result is a row of prices at three different heights, which reads as
   carelessness more than any single thing on the page.

   Giving the price a full-width line of its own makes the answer the same
   every time, whatever the name is. It is exactly what `.tile-credit`
   immediately above already does, and for the same reason. Left under the
   name rather than pushed right, because with the outline gone there is no
   longer a card edge for it to be pushed against. */
.tile-price{flex:1 0 100%;order:2;margin-top:2px}

.tile-img img,.rcard .rimg img{transform:scale(1.12)}
/* LESS ON THE SHOP GRID, AND THE REASON IS IN THE RULE ABOVE IT.
   `.pimg img` carries a note explaining that these are full-body model shots
   and that `cover` was rejected because it crops the model's head off at 4/5.
   A scale does the same thing more gently — 1.12 takes 6% off each edge, and
   on a shot framed tightly at the top that is a forehead. 1.08 is 4%, which
   the whole grid survives; it was checked against all 67 photographs rather
   than the four that happen to be on the homepage. */
.pimg img{transform:scale(1.08)}
@media(hover:hover){
  .tile:hover .tile-img img,.rcard:hover .rimg img{transform:scale(1.2)}
  .pcard:hover .pimg img{transform:scale(1.14)}
}
@media(prefers-reduced-motion:reduce){
  /* The enlargement is composition, not motion, so it stays — only the change
     on hover goes. */
  .tile:hover .tile-img img,.rcard:hover .rimg img{transform:scale(1.12)}
  .pcard:hover .pimg img{transform:scale(1.08)}
}

/* ==========================================================================
   LINE BREAKS THAT A TYPESETTER WOULD ACCEPT
   ==========================================================================
   Two lines of CSS that do more for how considered this site looks than any
   amount of spacing work. The headlines here are very large and very tightly
   tracked, so the browser's default greedy line breaking regularly left one
   short word stranded on its own line — worst on the section titles, which run
   to three lines on a phone. `balance` evens the lines instead. `pretty` does
   the narrower job of refusing to leave a single word alone on the last line
   of a paragraph. Both degrade to ordinary breaking where unsupported. */
h1,h2,.stitle,.hero2-h1,.shead .stitle{text-wrap:balance}
p,.sub,.hero2-sub,.tile-body,.rcard .rbody{text-wrap:pretty}

/* A product tile should feel picked up, not nudged. The shadow grows with the
   lift because a card that rises without its shadow following reads as flat
   artwork sliding, rather than an object leaving the page. */
.tile{transition:transform .35s cubic-bezier(.2,.7,.3,1),
                 box-shadow .35s cubic-bezier(.2,.7,.3,1)}
@media(hover:hover){
  .tile:hover{transform:translateY(-6px);
              box-shadow:0 18px 38px rgba(0,0,0,.18)}
}

/* The one accent button gets a light that crosses it once on hover. The sweep
   is a pseudo-element clipped by the button, so it costs no markup and cannot
   be clicked through. transform is the only property animated — moving a
   gradient's background-position repaints the whole button every frame. */
.btn-primary,.h2b--go{position:relative;overflow:hidden;isolation:isolate}
.btn-primary::after,.h2b--go::after{
  content:"";position:absolute;top:0;bottom:0;left:0;width:45%;
  z-index:-1;pointer-events:none;
  transform:translateX(-160%) skewX(-18deg);
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.22),transparent);
}
@media(hover:hover){
  .btn-primary:hover::after,.h2b--go:hover::after{
    transform:translateX(320%) skewX(-18deg);
    transition:transform .75s cubic-bezier(.3,.7,.3,1);
  }
}

@media(prefers-reduced-motion:reduce){
  /* Belt and braces. revealOnScroll() already refuses to arm, so no element
     carries the class and the rules above match nothing — but a visitor who
     turns the preference on mid-session has a page that is already partly
     hidden, and this is what brings it back. */
  .reveal,
  .reveal.shown,
  .reveal-stagger > *,
  .reveal-stagger.shown > *{
    opacity:1;transform:none;transition:none;transition-delay:0ms;
  }
  .tile{transition:none}
  .tile:hover{transform:none}
  .btn-primary::after,.h2b--go::after{display:none}
}

/* ==========================================================================
   VISUAL PASS 2026-09-29 — owner: "make the site look far better".
   ==========================================================================
   Everything here was chosen from screenshots at 390px and 1280px (agents-1,
   headless Chrome against the local tree). CSS only, no new requests, nothing
   that changes a box's size before its content arrives — so CLS is untouched.
   Selectors that reach into one page (.hero2, .gallery, .rail) carry an extra
   class so they outrank that page's own <style>, which loads after this file. */

/* --- header: make the full row FIT between 1181 and 1366 ------------------
   Measured before: 7px too wide at 1280, 106px at 1181 (the cart was the part
   cut off). Tracking and gaps pay for it; nothing is removed. */
@media(min-width:1181px) and (max-width:1366px){
  .nav{gap:12px}
  .links{gap:14px}
  .links a{font-size:.875rem}
  .brandmark{height:56px}
  /* Smaller type rather than a narrower box: at 112px the placeholder read
     "Search appare". */
  .navsearch input{width:106px;font-size:.78rem}
  .ibtn{padding:9px 11px}
}
/* Collapsed row (961-1180) has room to spare; the 1100px rule above squeezed
   the box to 104px and cut its placeholder. */
@media(min-width:961px) and (max-width:1180px){.navsearch input{width:150px}}

/* Page titles inherited the body's 1.6 line-height, so a two-line title on a
   phone ("Ready-made / shirt designs") had a gap between its lines as tall
   as a line of body text. Section titles already use 1.03. */
.ph1{line-height:1.05}
/* The current section, marked. `.on` was set by site.js all along and only
   changed the colour from slate to ink, which nobody can see. */
@media(min-width:1181px){
  .links a{position:relative;transition:color .15s}
  .links a::after{content:"";position:absolute;left:0;right:0;bottom:-7px;height:2px;
    border-radius:2px;background:var(--orange);transform:scaleX(0);transform-origin:left;
    transition:transform .22s cubic-bezier(.2,.7,.3,1)}
  .links a.on::after,.links a:hover::after{transform:scaleX(1)}
}
.ibtn{transition:border-color .15s,background-color .15s,color .15s}
.ibtn.solid:hover{background:var(--slate);border-color:var(--slate)}

/* --- one focus ring, everywhere -------------------------------------------
   Keyboard users had the browser default on some controls and nothing on
   others (several inputs set outline:none). :where() keeps this at zero
   specificity, so any control that already draws its own focus keeps it. */
:where(a,button,input,select,textarea,summary,[tabindex]):focus-visible{
  outline:2px solid var(--orange);outline-offset:2px}

/* --- buttons: one behaviour ------------------------------------------------
   Colours are left exactly as each variant sets them (solid, ghost, white on
   dark); only the response to the pointer is made the same. */
.btn{justify-content:center;line-height:1.2;
     transition:transform .18s cubic-bezier(.2,.7,.3,1),box-shadow .18s,opacity .18s,
                border-color .18s,background-color .18s}
@media(hover:hover){
  .btn:hover{transform:translateY(-1px);box-shadow:0 8px 22px rgba(14,17,23,.16)}
}
.btn:active{transform:none;box-shadow:none}
.chip,.chipb{transition:background-color .15s,border-color .15s,color .15s}

/* --- section heads: the eyebrow carries the brand colour -------------------
   It was muted grey beside a teal rule, so every band opened with the quietest
   thing on the page. Teal on white is 9.6:1; on the dark bands --orange is
   already lifted to #3FBFB6. Hero and editorial eyebrows keep their own. */
.eyebrow{color:var(--orange);font-weight:600}
.band--dark .eyebrow{color:var(--orange)}

/* --- two grey bands in a row read as one void ------------------------------
   "Four ways" and the guides are both .band--alt, so the seam between them
   was 176px of the same grey with nothing to mark a new section. The second
   one starts at its own heading; the first one's bottom padding is the gap. */
.band--alt + .band--alt{padding-top:0}

/* --- cards: one lift for every clickable card ------------------------------
   .tile had a lift and shadow, .pcard (the /shop/ grid, where the products
   actually live) only showed a grey outline on hover. Same motion now. */
.pcard{transition:transform .35s cubic-bezier(.2,.7,.3,1),box-shadow .35s cubic-bezier(.2,.7,.3,1)}
@media(hover:hover){
  .tile:hover,.pcard:hover{transform:translateY(-5px);
    box-shadow:0 16px 36px rgba(14,17,23,.13);border-color:transparent}
  .band--dark .tile:hover{box-shadow:0 18px 40px rgba(0,0,0,.45)}
}
@media(prefers-reduced-motion:reduce){.pcard{transition:none}.pcard:hover{transform:none}}

/* THE PHOTO'S WHITE BACKDROP BECOMES THE PANEL.
   Printful shoots on white; the panels are a soft grey on purpose (a white
   garment on a white card is invisible). contain-fitting a square photo into
   a 4:5 panel therefore drew a white square with grey bars above and below —
   seen on every best-seller and shop card. multiply turns the photo's white
   into the panel's grey, so the garment sits on one continuous ground, and a
   white garment still shows as its folds and shadow. Direct children only:
   artwork laid over a garment lives in its own layer and is never blended. */
.pimg > img,.tile-img > img{mix-blend-mode:multiply}
.rail .rcard .rimg > img{mix-blend-mode:multiply;background:#fff}

/* --- phone rails: the first card lines up with the heading above it --------
   The rails bleed to the screen edge with 18px of padding, but snap-to-start
   snapped the first card to the scroll edge, not the padding: every rail on
   the phone homepage started at x=0 under headings at x=18. */
@media(max-width:768px){
  .rail,#featTop,#guideGrid,.cols{scroll-padding-inline:18px}
}
.crow-in{scroll-padding-inline:16px}
@media(max-width:640px){
  .crow-in{padding-left:18px;padding-right:18px;margin-left:-18px;margin-right:-18px;
           scroll-padding-inline:18px}
}

/* --- homepage hero ---------------------------------------------------------
   Radius agrees with every other control on the site (was 4px here, 12px
   everywhere else). The secondary button's border was #2A3342 on #0B0D12 —
   it read as disabled, next to the one thing the page most wants pressed. */
.hero2 .hero2-box input,.hero2 .h2b{border-radius:var(--r)}
.hero2 .h2b{border-color:rgba(255,255,255,.28)}
.hero2 .h2b:hover{background:rgba(255,255,255,.07);border-color:rgba(255,255,255,.55)}
/* The accent button was the deep brand teal, which on near-black is a dark
   shape on a darker one. The lifted teal is the same hue, white text 5.9:1. */
.hero2 .h2b--go{background:var(--teal-lift);border-color:var(--teal-lift);color:#fff}
.hero2 .h2b--go:hover{background:#0F807B;border-color:#0F807B}
.hero2 .hero2-sub{color:#B7C1CF;max-width:40ch}
.hero2 .hero2-sub b{color:#fff;font-weight:700}
/* The proof points become a trust row: ticks, not dots, in the lifted teal —
   the dots were #0C4C4A on #0B0D12, about 1.5:1, i.e. invisible. Two columns
   from a large phone up, which is SHORTER than the old single column. */
.hero2 .hero2-pts{color:#C3CCD8;gap:9px}
.hero2 .hero2-pts li{padding-left:24px}
.hero2 .hero2-pts li::before{width:10px;height:5px;top:.42em;left:2px;border-radius:0;
  background:none;border:solid #3FBFB6;border-width:0 0 2px 2px;transform:rotate(-45deg)}
@media(min-width:561px){
  .hero2 .hero2-pts{display:grid;grid-template-columns:1fr 1fr;gap:10px 22px;max-width:560px}
}

/* --- /designs/ -------------------------------------------------------------
   THE HOVER CARD ON EVERY DESIGN WAS DEAD. The page names its slide-up panel
   .reveal, and so does the scroll-reveal system above — whose `.reveal` rule
   sets opacity:0 and is only undone by `.shown`, which nothing ever adds to a
   tile. So "From $42.99 · Pick your garment" existed on every tile and was
   never visible. Restored on devices that can hover; on a phone the label
   under the tile already says it. */
@media(hover:hover){.dtile .reveal{opacity:1;will-change:auto}}
/* The "Make it yours" and Search buttons were .btn — ink — on the gallery's
   near-black wall: a black button on a black band. White, like .band--dark. */
.gallery .btn:not(.ghost){background:#fff;color:#0B0D12;border-color:#fff}
.gallery .diyband{border-radius:var(--r)}
/* And the wall lifts its ACCENT like every other dark surface here: it
   reassigned the greys only, so the teal eyebrow on its empty states and the
   "Pick your garment" line were #0C4C4A / #12706C on near-black. */
.gallery{--orange:#3FBFB6;--cyan:#3FBFB6}

/* --- /product/ -------------------------------------------------------------
   TWO FLEX RULES THAT PULLED SENTENCES APART. Photographed at 1280px:
   "How this      shirt      is printed" and "A   bikinis.com   original".
   `.acc summary` is display:flex with space-between, and its text contains a
   <span data-noun>, so the sentence became three flex items spread across the
   row. `.trust div{display:flex}` matched the INNER text div as well as the
   row, so the bold brand name became a flex item with gaps either side.
   The summary keeps its +/- on the right by positioning it instead. */
details.acc > summary{display:block;position:relative;padding-right:32px}
details.acc > summary::after{position:absolute;right:0;top:50%;transform:translateY(-50%)}
.trust > div > div{display:block}

/* --- footer ----------------------------------------------------------------
   Its own quiet ground so the page visibly ends, headings that look like
   headings, and the logo back to a readable size on a phone (the 28px header
   rule was also shrinking the footer's copy to a smudge). */
footer{background:var(--bg-2)}
footer h5{text-transform:uppercase;font-size:.64rem;letter-spacing:.16em;color:var(--fg);
          margin-bottom:16px}
footer ul{gap:10px}
footer a{transition:color .15s}
@media(max-width:640px){
  .brandmark--foot{height:38px}
  /* body carries 76px of bottom padding to keep the chat launcher off the
     end of the page; with the footer now on its own ground that showed as a
     white strip under it. The footer takes the same room instead. */
  body:has(> #foot footer){padding-bottom:0}
  footer{padding-bottom:120px}
}

/* THE CHAT LAUNCHER'S LIFT ABOVE THE STICKY BAR — LAST IN THE FILE, WHICH IS
   THE WHOLE POINT.
   ==========================================================================
   `@media(max-width:640px){.cbtn,.chat{bottom:88px}}` is written around line
   750, and `.cbtn{...bottom:22px}` is written after it, around line 800. Same
   specificity, so the later one wins and the media query has never done
   anything: measured on a 430px screen the launcher sat at bottom 22px — on top
   of the right-hand end of the sticky "$44.99 · Pick a size" bar, on every page
   that has one.

   A media query does not outrank a plain rule. Only order or specificity does.

   Written once more here, at the end of the file, where nothing can follow it.
   It is here rather than 20 lines up because the first attempt was ALSO placed
   above the base rule and measured unchanged at 22px — the position in the file
   IS the fix, so it has to be checked in a browser rather than reasoned about.
   check-launcher-clear.mjs measures it. */
@media(max-width:640px){
  .cbtn{bottom:88px}
  .chat{bottom:88px;
        height:min(540px,calc(100vh - 108px));
        height:min(540px,calc(100dvh - 108px))}
}

/* NET-6 lane H 10-17: the full link row (shown above 1180px) must fit without a sideways scroll.
   Between 1181 and 1599px: tighter link gaps, a narrower search box, and the header Contact button
   gives way (Contact stays in the footer and on /contact/). Phone icon buttons get an 8px gap. */
@media(min-width:1181px) and (max-width:1599px){
  .links{gap:13px}
  .nav{gap:12px}
  .navact .navsearch input{width:96px}
  .navact > a.ibtn[href="/contact/"]{display:none}
}
.navact > .ibtn{flex-shrink:0}
@media(max-width:640px){.navact{gap:8px}}

@media(min-width:1181px) and (max-width:1279px){
  .links{gap:10px}
  .navact .navsearch input{width:72px}
  .brandmark{height:56px}
}
@media(min-width:1600px) and (max-width:1759px){
  .links{gap:13px}
  .nav{gap:12px}
  .navact .navsearch input{width:96px}
  .navact > a.ibtn[href="/contact/"]{display:none}
}

/* STUDIO-2a 10-17: the chat bubble sat on the footer's last row ("Contact us", bottom right) at the end of
   every page. The footer now keeps the bubble's corner clear: desktop bubble = 54px at bottom 22px (top edge
   76px up), phone = 46px at bottom 88px (top edge 134px up). LAST IN THE FILE on purpose (see above). */
footer{padding-bottom:96px}
@media(max-width:640px){
  footer{padding-bottom:150px}
}

/* ===== MOB-2 lane H (10-17): phone design polish. Desktop untouched. ===== */
@media (max-width:640px){
  body{font-size:16px;line-height:1.6}
  .wrap{padding:0 20px}
  /* type scale: readable labels, calmer headlines */
  .eyebrow{font-size:.74rem;letter-spacing:.12em;gap:10px}
  .eyebrow::before{width:18px}
  .crumbs{font-size:.86rem;margin-bottom:12px}
  .ph1{font-size:2.15rem;line-height:1.06;letter-spacing:-.035em}
  .phl{font-size:1.02rem;line-height:1.6}
  .stitle{font-size:1.9rem;line-height:1.08;letter-spacing:-.035em}
  .shead .sub,.sub{font-size:1rem;line-height:1.55}
  .hint,.note,small{font-size:.88rem;line-height:1.5}
  /* rhythm */
  .band{padding:48px 0}
  .band--tight{padding:32px 0}
  /* controls: thumb-sized */
  .btn{min-height:48px;padding:13px 22px;font-size:1rem;border-radius:12px}
  .chip{min-height:40px;padding:9px 14px;font-size:.88rem;border-radius:999px}
  input,select,textarea{font-size:16px!important}   /* 16px stops iOS zooming into fields */
  /* cards: soft surfaces instead of hard outlines */
  .tile,.rcard,.pcard,.dtile,.fd-tile{border-radius:14px;overflow:hidden;
    box-shadow:0 1px 2px rgba(16,24,40,.06),0 6px 18px rgba(16,24,40,.07)}
  .tile:hover{transform:none}
  /* footer: real tap targets and readable text */
  footer{font-size:.95rem;padding-top:44px}
  footer h5{font-size:.74rem;letter-spacing:.12em;margin-bottom:10px}
  footer ul{gap:4px}
  footer ul a{display:inline-block;padding:7px 0}
}

/* ===== MOB-3 lane H (10-17): sweep flags MOB-2 missed. Desktop untouched. ===== */
@media (max-width:640px){
  /* body copy: 16px everywhere people read */
  .engine p,.steps li,.hero2-pts li,.hero2-sub,.tile p,.anyline,.ways-h p,.diyband p,.anyd p,.aud-sec .sub,
  .cardy p,.hero-slim p,.howto-n{font-size:16px!important;line-height:1.55}
  /* headings: H1 28-34, H2 22-26 */
  .hero-slim h1{font-size:1.9rem!important;line-height:1.08}
  .stitle{font-size:1.6rem!important;line-height:1.1}
  .ways-h h2{font-size:1.4rem!important;line-height:1.15}
  /* labels: nothing under 12px */
  .eyebrow,footer h5,.strip-in span,.strip-in b,.engine .num,.frame .reveal span,.anyd small,.sway span,.howto-h,
  .howto-n b,.howto-n i,.howto-n code,.sl-lab{font-size:12.5px!important}
  .annc span,.annc b{font-size:12px!important}
  .wallfall{font-size:12px!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
  /* tap targets: 44px minimum for icons/chips/toggles (.btn is 48 from MOB-2) */
  .ibtn{min-height:44px!important;min-width:44px}
  .h2loc,.chip,.gm-view button,.howto-ex button{min-height:44px!important}
  /* inner padding so text sits ~20px from the screen edge inside studio cards */
  .howto-d,.cardy{padding-left:16px!important;padding-right:16px!important}
  /* page-level !important rules (studio inline style) need a stronger selector */
  p.howto-n,details p.howto-n{font-size:15px!important}
  .hf-shop span,.hf-shop b,.imgfall{font-size:12.5px!important}
  /* engine cards: 30px inner padding pushed copy 51px from the edge; 18px keeps it ~38 inside a card */
  .engine{padding:22px 18px!important}
  .engine .steps{font-size:15px}
  .engine .steps i{font-size:12px}
  .ideas-lab{font-size:12.5px!important}
  /* empty / full-width states must span the whole grid (was squeezed into one of two columns) */
  .t-full,.empty.t-full{grid-column:1/-1!important;max-width:none}
}
