/* The two home sections rebuilt after the owner's review of 2026-09-10.
   Pairs with app/hub_sections.py. Loaded after the hub sheet, so the few rules
   that share a selector with it are overrides on purpose.

   No colour value in this file. Money is --px-*, the world is --w-*, and the
   two never cross (S section 4.3). */

/* =============================================================== world plate
   The brand's shop window: its strongest pair at a size worth looking at, the
   pack priced and buyable, what is inside it, and the rest of its work.

   Source order is aperture, pack, strip. On a phone that puts the price one
   frame below the top of the plate instead of three; on a laptop the areas
   below put the strip back under the aperture and give the pack both rows. */

.wplate{ padding-block:var(--px-12); }
@media (min-width:1024px){ .wplate{ padding-block:var(--px-16); } }
.wplate>.wrap>h2{ font-size:var(--px-t-h2); }
@media (min-width:1024px){ .wplate>.wrap>h2{ font-size:44px; line-height:1.06; } }
.wplate p.body{ color:var(--w-dim); max-width:56ch; margin-top:var(--px-3); }

.wgrid{ display:grid; gap:var(--px-5); margin-top:var(--px-6);
  padding-top:var(--px-6); border-top:var(--px-hair) solid var(--w-rule-soft); }
@media (min-width:1024px){
  .wgrid{ grid-template-columns:minmax(0,430px) minmax(0,1fr);
          grid-template-areas:"art buy" "strip buy";
          grid-template-rows:auto 1fr;
          column-gap:var(--px-14); row-gap:var(--px-3);
          margin-top:var(--px-8); padding-top:var(--px-8); }
  .wart{ grid-area:art; } .wbuy{ grid-area:buy; } .wstrip{ grid-area:strip; }
}

/* the aperture. One frame, the brand's strongest built pair, never a pair
   under the card gate. */
.wart .cmp[data-variant="detail"]{
  width:100%; border-color:var(--w-rule);
  box-shadow:0 18px 40px color-mix(in srgb, var(--px-ink) 45%, transparent);
}

/* the strip: two more of the brand's photographs beside the aperture.

   A GRID THAT WRAPS, never a horizontal scroller. A before and after is
   dragged along x and a scroller is swiped along x, so on a touch device the
   two fight for the same axis and the scroller wins: the seam does not move
   and the component looks broken. That is the report behind "the sliders do
   not work on mobile" (orchestrator, 2026-09-10). Two columns at both widths,
   which is also what keeps each frame at 171 px on a phone instead of the
   110 px three across would leave. */
.wstrip{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
  gap:var(--px-3); margin-top:var(--px-1); }
.wshot .cmp{ border-color:var(--w-rule); }
.wshot .cmp-tag.before{ display:none; }
.wcap{ margin-top:var(--px-2); font-size:var(--px-t-micro); line-height:1.35;
  color:var(--w-dim); }
.wart .wcap{ font-family:var(--w-display); font-size:var(--px-t-ui);
  line-height:1.35; margin-top:var(--px-3); }

/* the pack: what it is, what it costs, one way to buy, one way to explore */
.wbuy{ background:var(--w-surface-2); border:var(--px-hair) solid var(--w-rule);
  border-radius:var(--px-r-card); padding:var(--px-5);
  box-shadow:0 12px 34px color-mix(in srgb, var(--px-ink) 30%, transparent); }
/* align-self:start, not stretch. Stretched to the art column the panel held a
   200 px hole between its last line and its floor, and a hole inside a bordered
   card reads as a bug. A card ends where its content ends. */
@media (min-width:1024px){ .wbuy{ padding:var(--px-8); align-self:start; } }
.wbuy .wpack{ font-family:var(--w-display); text-transform:var(--w-case);
  font-size:var(--px-t-h2); line-height:1.1; color:var(--w-text); }
.wbuy .wmeta{ margin-top:var(--px-2); font-size:var(--px-t-ui); color:var(--w-dim-2); }
.wbuy .price{ margin-top:var(--px-4); }
/* the price is the point of the panel, so it is set at heading size */
.wbuy .price .now{ font-size:var(--px-t-h2); letter-spacing:-.02em; }
.wbuy .trust{ color:var(--w-dim-2); }
.wbuy .own{ margin-top:var(--px-4); font-size:var(--px-t-h3); font-weight:700;
  color:var(--px-owned); }
/* a buy control the width of a 620 px panel reads as a banner, not a button */
@media (min-width:1024px){
  .wbuy .cta,.wbuy .btn2,.wbuy .express{ max-width:340px; }
}

/* what is inside. Real preset names and their real intent, from the brand's
   own index. This is the "more info" the owner asked for, and it is data. */
.winside{ margin-top:var(--px-6); padding-top:var(--px-5);
  border-top:var(--px-hair) solid var(--w-rule-soft); }
.winside h3{ font-size:var(--px-t-micro); font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:var(--w-dim-2); }
.winside ul{ list-style:none; padding:0; margin-top:var(--px-3); }
.winside li{ padding-block:var(--px-2); display:grid; gap:2px; }
.winside li+li{ border-top:var(--px-hair) solid var(--w-rule-soft); }
.winside b{ font-family:var(--w-display); text-transform:var(--w-case);
  font-weight:500; font-size:var(--px-t-h3); color:var(--w-text); }
.winside span{ font-size:var(--px-t-micro); line-height:1.45; color:var(--w-dim-2); }
.wmore{ margin-top:var(--px-3); font-size:var(--px-t-micro); color:var(--w-dim); }

.wplate .exit{ display:inline-flex; min-height:44px; align-items:center;
  margin-top:var(--px-5); color:var(--w-text);
  border-bottom:var(--px-hair) solid var(--w-accent); }
.wplate .exit:focus-visible{ outline:2px solid var(--px-focus); outline-offset:3px; }
/* the measured heights, not a guess. Under-reserving here buys back the CLS
   the reservation exists to save (R1 5.5): plate 1710 at 390, 1213 at 1440. */
.wplate{ contain-intrinsic-size:auto 1710px; }
@media (min-width:1024px){ .wplate{ contain-intrinsic-size:auto 1213px; } }

/* ========================================================== find your light
   The taxonomy entry point, as photographs. Tiles are ranked by how many
   makers work in the territory.

   Two across at both widths, and the renderer keeps the count even, so a row
   always closes. Two across is also what keeps the frame sharp: at three
   across on a laptop the card ladder's 540 rung is asked to cover 564 px,
   which it does; stretched to a full 1152 px row it is visibly soft. */

#discover .trust{ font-size:var(--px-t-ui); margin-bottom:var(--px-5); }
.tgrid{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--px-4); }
/* a fixed height, not the 4:5 crop: these are territory bands, not products */
.tile{ background:var(--px-ink-2); border:var(--px-hair) solid var(--px-rule);
  border-radius:var(--px-r-card); overflow:hidden; position:relative; }
.tile .cmp{ aspect-ratio:auto; height:190px; border:0; border-radius:0; }
@media (min-width:1024px){
  .tgrid{ gap:var(--px-6); }
  .tile .cmp{ height:210px; }
  .tile{ transition:transform var(--px-t-enter) var(--px-e-enter),
    border-color var(--px-t-enter) var(--px-e-enter); }
  .tile:hover{ transform:translateY(-3px); border-color:var(--px-dim); }
}
.tile:focus-visible{ outline:2px solid var(--px-focus); outline-offset:3px; }
.tbody{ padding:10px 12px 12px; }
.tbody h3{ font-size:var(--px-t-h3); font-weight:700; line-height:1.25; }
.tbody .tmeta{ margin-top:2px; font-size:var(--px-t-micro); color:var(--px-dim);
  font-variant-numeric:tabular-nums; }

/* the tail: every territory whose brands have no built pair yet, as links with
   their maker count. Nothing falls out of the index for want of a photograph. */
.tmore{ margin-top:var(--px-8); padding-top:var(--px-5);
  border-top:var(--px-hair) solid var(--px-rule); }
.tmore h3{ font-size:var(--px-t-micro); font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:var(--px-dim); }
.tlinks{ display:grid; grid-template-columns:minmax(0,1fr);
  gap:0 var(--px-8); margin-top:var(--px-2); }
@media (min-width:640px){ .tlinks{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (min-width:1024px){ .tlinks{ grid-template-columns:repeat(3,minmax(0,1fr)); } }
.tlinks a{ display:flex; align-items:center; justify-content:space-between;
  gap:var(--px-3); min-height:48px; font-size:var(--px-t-ui);
  border-bottom:var(--px-hair) solid var(--px-rule); }
.tlinks a:hover{ color:var(--px-chalk); }
.tlinks a:focus-visible{ outline:2px solid var(--px-focus); outline-offset:2px; }
.tlinks .n{ color:var(--px-dim); font-variant-numeric:tabular-nums;
  white-space:nowrap; }
#discover{ contain-intrinsic-size:auto 1305px; }
@media (min-width:1024px){ #discover{ contain-intrinsic-size:auto 1022px; } }

/* ==================================================== the brand page (D2)
   2026-09-10. The owner on the old /<brand>: "this page is so NOT OUR BRAND,
   its heavy, full of content, and there's no flow or no brain, just data",
   and what he wants instead: the hand written storefront's rhythm, its
   specimen rows, its popup and its sticky bar, on the hub's own terms.

   Everything inside <main data-world> wears the brand: --w-ground, --w-text,
   --w-display. Nothing you can spend money on does: a price, a struck price,
   a save chip, a cart count and a buy control are --px-* here as everywhere
   (S section 4.3). No colour value is written in this file. */

/* .woffer and .buybar are display:flex, which beats the user agent's own
   [hidden]{display:none}. Without this line the offer's scrim paints over the
   whole page from the first frame: that is exactly what round 1 photographed.
   hub_shop_pages.SHOP_CSS carries the same line for the five shop pages; the
   hub's own pages had nothing. */
[hidden]{ display:none !important; }

.bw{ background:var(--w-ground); color:var(--w-text); }
/* Sections inside a world page own their own rhythm; the hub sheet's generic
   `section{padding-block:var(--px-12)}` is the chrome's, not the world's. */
.bsec{ padding-block:var(--px-12); border-top:var(--px-hair) solid var(--w-rule-soft); }
/* Round 1: at the platform's 1296 the section head's fact sat a thousand
   pixels from its heading and the FAQ's + sat off on its own. A page that is
   read wants a column; the ten storefronts are 980 wide for the same reason. */
.bsec>.wrap,.bhero>.wrap{ max-width:1120px; }
@media (min-width:1024px){ .bsec{ padding-block:var(--px-16); } }

/* the section head: the storefront's signature. A display heading on the left
   and one small caps fact on the right, on the same baseline. */
.sechead{ display:flex; align-items:baseline; justify-content:space-between;
  gap:var(--px-3) var(--px-6); flex-wrap:wrap; }
.sechead h2{ font-size:var(--px-t-h2); line-height:1.1; }
@media (min-width:1024px){ .sechead h2{ font-size:40px; } }
.label{ font-size:var(--px-t-micro); letter-spacing:.16em; text-transform:uppercase;
  color:var(--w-dim); font-family:var(--px-font); font-weight:600; }
.subline{ color:var(--w-dim); max-width:62ch; margin-top:var(--px-4);
  line-height:1.65; }

/* ------------------------------------------------------------------ open */
/* One left edge down the whole page. Round 2 ran the hero at the platform's
   1296 and every section below it at 1120, so the headline and the first
   heading did not line up, which is the one thing an editorial page cannot
   get wrong. */
.bhero{ padding-block:var(--px-6) var(--px-12); }
.bhero .kick{ font-family:var(--px-font); font-size:var(--px-t-micro);
  letter-spacing:.16em; text-transform:uppercase; color:var(--w-dim); }
.bhero h1{ margin-top:var(--px-4); }
.bhero .lede{ color:var(--w-dim); max-width:54ch; margin-top:var(--px-5);
  line-height:1.65; }
.bhero .deal{ margin-top:var(--px-8); padding-top:var(--px-6);
  border-top:var(--px-hair) solid var(--w-rule-soft); max-width:420px; }
.bhero .pack{ font-size:var(--px-t-ui); color:var(--w-dim); }
.bhero .price .now{ font-size:var(--px-t-price-hero);
  line-height:var(--px-lh-price-hero); letter-spacing:-.03em; }
@media (max-width:1023.98px){
  .bhero .cmp{ margin-inline:calc(50% - 50vw); width:100vw;
    border-radius:0; border-inline:0; }
}
@media (min-width:1024px){
  .bhero .wrap{ display:grid; grid-template-columns:minmax(0,1fr) minmax(0,520px);
    gap:var(--px-14); align-items:center; padding-block:var(--px-10); }
  .bhero .cmp{ width:100%; aspect-ratio:4/5; height:auto; justify-self:end;
    border-radius:var(--px-r-card); }
}

/* ----------------------------------------------------------------- prove */
/* auto-fit, never a fixed count: three pairs in two columns leaves the third
   alone on a phone, which is the ragged last row D1 already ruled against.
   One column under about 700, three above it, and the row always closes. */
.pairs{ display:grid; gap:var(--px-4); margin-top:var(--px-8);
  grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr)); }
@media (min-width:1024px){ .pairs{ gap:var(--px-6); } }
.pairs figure{ margin:0; min-width:0; }
.pairs .cmp{ --cmp-ar:4/5; }
.pairs figcaption{ font-family:var(--w-display); text-transform:var(--w-case);
  color:var(--w-dim); font-size:var(--px-t-ui); margin-top:var(--px-3);
  line-height:1.4; }
/* the hub's figure() ships a visually hidden caption inside the frame; the
   editorial one below it is a sibling and must not inherit that. */
.pairs>figure>figcaption{ position:static; width:auto; height:auto;
  clip-path:none; overflow:visible; white-space:normal; }

/* -------------------------------------------------------- the whole set */
.specs{ list-style:none; padding:0; margin-top:var(--px-8);
  border-top:var(--px-hair) solid var(--w-rule-soft); }
.specs>li{ border-bottom:var(--px-hair) solid var(--w-rule-soft); }
.specs .row{ display:grid; gap:2px var(--px-5); align-items:baseline;
  padding-block:var(--px-4); grid-template-columns:minmax(0,1fr); }
@media (min-width:760px){
  .specs .row{ grid-template-columns:104px minmax(0,15em) minmax(0,1fr); }
}
.specs .no{ font-family:var(--px-font); font-size:var(--px-t-micro);
  letter-spacing:.14em; text-transform:uppercase; color:var(--w-dim);
  font-variant-numeric:tabular-nums; }
.specs .name{ font-family:var(--w-display); text-transform:var(--w-case);
  font-size:var(--px-t-h3); color:var(--w-text); }
.specs .intent{ color:var(--w-dim); font-size:var(--px-t-ui); line-height:1.55; }
/* The <details> row itself. brands_ui/specimens.py ships a CSS string for
   the ten storefronts; these are the same rules in the hub's tokens, so the
   hub does not inline a second sheet on every brand page. The two must stay
   in step: platform/tests/test_hub_brand.py asserts the class names. */
.specimen{ min-width:0; }
.specimen>summary{ position:relative; list-style:none; cursor:pointer;
  padding-right:84px; }
.specimen>summary::-webkit-details-marker{ display:none; }
.specs .see{ position:absolute; right:0; top:var(--px-4); color:var(--w-dim);
  font-family:var(--px-font); font-size:11px; letter-spacing:.14em;
  text-transform:uppercase; }
.specs .see::after{ content:" +"; }
.specimen[open] .see::after{ content:" \2212"; }
.specimen>summary:hover .name{ text-decoration:underline;
  text-underline-offset:3px; }
.specimen[open]>summary{ background:var(--w-tint-2, transparent); }
.specimen>summary:focus-visible{ outline:2px solid var(--px-focus);
  outline-offset:-2px; }
/* four frames, side by side, the way the storefront shows them. Tall crop
   because the renders are 440x789. */
.shots{ display:grid; gap:var(--px-3); padding-block:var(--px-2) var(--px-4);
  grid-template-columns:repeat(auto-fit, minmax(min(150px,100%),1fr)); }
.shots .cmp{ --cmp-ar:440/789; --cmp-handle:28px; }
.shots .cmp .cmp-tag{ font-size:11px; padding:2px 5px; }
.shot{ margin:0; min-width:0; }
.shot figcaption{ font-family:var(--px-font); font-size:11px; letter-spacing:.12em;
  text-transform:uppercase; color:var(--w-dim); margin-top:var(--px-2); }
.shot-note{ color:var(--w-dim); font-size:var(--px-t-micro); line-height:1.6;
  max-width:62ch; margin:0 0 var(--px-4); }

/* ------------------------------------------------------------ the maker */
.maker{ padding-block:var(--px-12); border-top:var(--px-hair) solid var(--w-rule-soft); }
@media (min-width:1024px){ .maker{ padding-block:var(--px-16); } }
.maker>.wrap{ max-width:1120px; }
.maker-grid{ display:grid; gap:var(--px-6); align-items:start; }
@media (min-width:760px){
  .maker-grid{ grid-template-columns:300px minmax(0,1fr); gap:var(--px-12); }
}
.maker-photo{ margin:0; max-width:300px; }
.maker-photo img{ display:block; width:100%; height:auto;
  border-radius:var(--px-r-card); }
.maker .maker-kick{ font-family:var(--px-font); font-size:var(--px-t-micro);
  letter-spacing:.16em; text-transform:uppercase; margin-bottom:var(--px-4); }
.maker h2{ font-size:var(--px-t-h2); line-height:1.1; margin-bottom:var(--px-5); }
@media (min-width:1024px){ .maker h2{ font-size:40px; } }
.maker p{ line-height:1.7; max-width:62ch; }
.maker p+p{ margin-top:var(--px-4); }
.maker .maker-sign{ margin-top:var(--px-6); padding-top:var(--px-5);
  border-top:var(--px-hair) solid var(--w-rule-soft); font-size:var(--px-t-ui); }

/* about.section() carries its own layout and no colour; these three lines
   give it the world's face and rule rather than the platform's. */
.bw .maker{ border-top-color:var(--w-rule-soft); }
.bw .maker .maker-kick,.bw .maker p,.bw .maker .maker-sign{ color:var(--w-dim); }
.bw .maker .maker-sign{ border-top-color:var(--w-rule-soft); }
.bw .maker .maker-sign b{ color:var(--w-text); }

/* --------------------------------------------------------------- the buy
   Round 1 put the one pack most brands sell into a four column grid, where it
   was a quarter-width card stranded on the left: the same "just data" shape
   the owner rejected, at the one point on the page where a decision is made.
   One pack gets a panel. Two or more keep the grid, where a grid is the
   honest layout. */
.bpanel{ display:grid; gap:var(--px-6); margin-top:var(--px-8);
  padding:var(--px-6); border-radius:var(--px-r-card);
  background:var(--w-surface-2); border:var(--px-hair) solid var(--w-rule);
  box-shadow:0 10px 30px color-mix(in srgb, var(--px-ink) 30%, transparent); }
@media (min-width:760px){
  .bpanel{ grid-template-columns:minmax(0,1fr) minmax(0,340px);
    gap:var(--px-12); align-items:center; padding:var(--px-8); }
}
.bpanel h3{ font-family:var(--w-display); text-transform:var(--w-case);
  font-size:var(--px-t-h2); line-height:1.1; font-weight:500; }
.bpanel .meta{ color:var(--w-dim-2); margin-top:var(--px-2); }
.bpanel .incl{ list-style:none; padding:0; margin-top:var(--px-5); }
.bpanel .incl li{ color:var(--w-dim-2); font-size:var(--px-t-ui);
  padding-block:var(--px-2); display:flex; gap:var(--px-3); }
.bpanel .incl li::before{ content:"\2713"; color:var(--w-accent); flex:none; }
.bpanel .price{ margin-top:0; }
.bpanel .price .now{ font-size:var(--px-t-price-hero);
  line-height:var(--px-lh-price-hero); letter-spacing:-.03em; }
.bpanel .trust{ color:var(--w-dim-2); }

/* --------------------------------------------------------- the objections */
.faq{ margin-top:var(--px-6); max-width:860px;
  border-top:var(--px-hair) solid var(--w-rule-soft); }
.faq details{ border-bottom:var(--px-hair) solid var(--w-rule-soft); }
.faq summary{ cursor:pointer; list-style:none; padding-block:var(--px-4);
  min-height:56px; display:flex; align-items:center; justify-content:space-between;
  gap:var(--px-4); font-family:var(--w-display); text-transform:var(--w-case);
  font-size:var(--px-t-h3); }
.faq summary::-webkit-details-marker{ display:none; }
.faq summary::after{ content:"+"; color:var(--w-dim); font-family:var(--px-font);
  flex:none; }
.faq details[open] summary::after{ content:"\2212"; }
.faq summary:focus-visible{ outline:2px solid var(--px-focus); outline-offset:-2px; }
.faq p{ color:var(--w-dim); line-height:1.7; max-width:70ch;
  padding-bottom:var(--px-5); }
.faq a{ border-bottom:var(--px-hair) solid currentColor; }

/* ============================================== the welcome offer (D2 sec 3)
   A dialog, dismissible three ways, carrying the SAME price the page and the
   sticky bar already show, from the server row. No timer, no percentage, no
   invented discount: it renders at all only when the row's own save is real. */
.woffer{ position:fixed; inset:0; z-index:var(--px-z-scrim);
  background:color-mix(in srgb, var(--px-ink) 78%, transparent);
  display:flex; align-items:flex-end; justify-content:center;
  padding:var(--px-4); padding-bottom:calc(var(--px-4) + env(safe-area-inset-bottom)); }
@media (min-width:640px){ .woffer{ align-items:center; } }
.wo-card{ position:relative; width:min(100%,420px); background:var(--w-surface-2);
  color:var(--w-text); border:var(--px-hair) solid var(--w-rule);
  border-radius:var(--px-r-sheet); padding:var(--px-6);
  box-shadow:0 18px 48px color-mix(in srgb, var(--px-ink) 55%, transparent); }
.wo-card h2{ font-size:var(--px-t-h2); margin-top:var(--px-3); }
.wo-card p{ color:var(--w-dim-2); margin-top:var(--px-3); line-height:1.6; }
.wo-x{ position:absolute; top:6px; right:6px; width:44px; height:44px;
  background:none; border:0; color:var(--w-dim); font-size:22px; cursor:pointer;
  font-family:var(--px-font); line-height:1; }
.wo-x:focus-visible,.wo-no:focus-visible{ outline:2px solid var(--px-focus);
  outline-offset:2px; }
.wo-card .price{ margin-top:var(--px-5); }
.wo-no{ display:block; width:100%; margin-top:var(--px-3); padding:var(--px-3);
  background:none; border:0; color:var(--w-dim); font:inherit;
  font-size:var(--px-t-ui); cursor:pointer; text-decoration:underline; }

/* ============================================= the cart affordance (D2 sec 4)
   The owner, 2026-09-10: "cart should be a floating bubble on mobile and
   sticky to the header on desktop". One affordance per device, one count.
   These rules used to live in hub_shop_pages.SHOP_CSS, which only the five
   shop pages load; the cart is on every hub page, so they live here. */
.bar .cartn{ display:none; align-items:center; min-height:44px; gap:6px;
  font-size:var(--px-t-ui); color:var(--px-chalk); margin-left:var(--px-4); }
@media (min-width:1024px){ .bar .cartn{ display:inline-flex; } }
.bar .cartn b{ min-width:18px; height:18px; padding-inline:5px;
  border-radius:var(--px-r-pill); background:var(--px-signal);
  color:var(--px-signal-ink); font-size:11.5px; display:inline-flex;
  align-items:center; justify-content:center; font-variant-numeric:tabular-nums; }
.bar .cartn b:empty,.bar .cartn b[data-n="0"]{ display:none; }
.bar .cartn:focus-visible{ outline:2px solid var(--px-focus); outline-offset:2px; }

/* the phone's bubble. Fixed, above the safe area, and it steps up over the
   sticky buy bar rather than under it: cart.js puts .buybar-on on <html> the
   moment the bar slides in, so the two are never stacked on one another. */
.cartbub{ position:fixed; z-index:var(--px-z-buybar);
  right:var(--px-4); bottom:calc(var(--px-4) + env(safe-area-inset-bottom));
  width:56px; height:56px; border-radius:var(--px-r-pill);
  display:flex; align-items:center; justify-content:center;
  background:var(--px-ink-3); color:var(--px-chalk);
  border:var(--px-hair) solid var(--px-rule);
  box-shadow:0 6px 20px color-mix(in srgb, var(--px-ink) 60%, transparent); }
.cartbub b{ position:absolute; top:-4px; right:-4px; min-width:20px; height:20px;
  padding-inline:5px; border-radius:var(--px-r-pill); background:var(--px-signal);
  color:var(--px-signal-ink); font-size:11.5px; font-weight:700;
  display:inline-flex; align-items:center; justify-content:center;
  font-variant-numeric:tabular-nums; }
.cartbub b:empty,.cartbub b[data-n="0"]{ display:none; }
.cartbub:focus-visible{ outline:2px solid var(--px-focus); outline-offset:3px; }
/* Above the bar, never on it. Round 2 measured the bar at 170 px, not the 96
   this guessed, and the bubble sat squarely on the bar's Buy with card. The
   height is no longer a guess: cart.js measures the bar when it slides in and
   writes --buybar-h, so this is exact whatever the bar ends up carrying. The
   fallback is the measured 170 for the half second before JS runs. */
.buybar-on .cartbub{
  bottom:calc(var(--buybar-h,170px) + var(--px-4) + env(safe-area-inset-bottom)); }
@media (min-width:1024px){ .cartbub{ display:none; } }

/* the sticky buy bar. Phone only: a laptop has the rail or the hero control
   in view for most of the page and a fixed bar there is a banner. */
.buybar{ position:fixed; inset-inline:0; bottom:0; z-index:var(--px-z-buybar);
  background:var(--px-ink-2); border-top:var(--px-hair) solid var(--px-rule);
  padding:var(--px-3) var(--px-4) calc(var(--px-3) + env(safe-area-inset-bottom));
  transform:translateY(110%); transition:transform var(--px-t-enter) var(--px-e-enter);
  display:flex; flex-direction:column; align-items:stretch; gap:var(--px-2); }
.buybar.on{ transform:none; }
.buybar .bbrow{ display:flex; align-items:center; gap:var(--px-4); }
/* One line of money. At 390 a 40 per cent column wrapped the struck price, the
   price and the chip onto three lines and made the bar 170 px tall. The struck
   price already states the saving; a chip repeating it inside a bar this size
   is the badge soup R2 5.3 refuses. */
.buybar .price{ flex:0 0 auto; margin:0; flex-wrap:nowrap; gap:var(--px-1); }
.buybar .price .now{ font-size:var(--px-t-price); }
.buybar .price .reg{ font-size:var(--px-t-micro); }
.buybar .price .save{ display:none; }
.buybar .bbrow form{ flex:1 1 auto; min-width:0; }
.buybar .cta,.buybar .btn2{ margin:0; }
.buybar .express{ margin-top:0; }
@media (min-width:1024px){ .buybar{ display:none; } }
/* the last line of content clears the bar AND the bubble stacked above it */
.hasbar{ padding-bottom:calc(var(--buybar-h,170px) + 88px); }
@media (min-width:1024px){ .hasbar{ padding-bottom:0; } }

.toast{ position:fixed; inset-inline:var(--px-4);
  bottom:calc(var(--buybar-h,170px) + 88px);
  z-index:var(--px-z-toast); background:var(--px-ink-3); color:var(--px-chalk);
  border:var(--px-hair) solid var(--px-rule); border-radius:var(--px-r-card);
  padding:var(--px-3) var(--px-4); font-size:var(--px-t-ui);
  display:flex; align-items:center; justify-content:space-between; gap:var(--px-4);
  opacity:0; transform:translateY(8px);
  transition:opacity var(--px-t-enter) var(--px-e-enter),
             transform var(--px-t-enter) var(--px-e-enter); }
.toast.on{ opacity:1; transform:none; }
@media (min-width:1024px){
  .toast{ inset-inline:auto var(--px-14); bottom:auto; top:88px; width:360px; }
}

/* ================================================ N4, the accessibility audit
   docs/research/2026-09-10-presets-hub/N4-a11y.md. Findings 7, 10 and 11.
   CSS only: everything here that needed markup or script is an edit-op. */

/* Finding 7. hub_pages.py:350 gives .trust the INSTRUMENT dim, --px-dim,
   which is designed to sit on --px-ink. Inside a world it lands on the
   brand's own ground, and 33 of the 60 grounds are light: measured 2.30:1 on
   roamhue's brand hero. Secondary copy inside a world takes the world's own
   dim, which clears 4.5:1 on every one of the 60 (worst rainpane 4.76:1).
   The panel cases below take --w-dim-2 instead and are more specific, so this
   does not reach them whatever the source order. */
[data-world] .trust{ color:var(--w-dim); }
[data-world] .wbuy .trust,[data-world] .bpanel .trust,
[data-world] .wo-card .trust,[data-world] .winside .wmore{
  color:var(--w-dim-2); }
/* The struck price fails the same way, at 4.24:1 on reeltint's panel, and it
   is deliberately NOT fixed here. hub_pages.py:340 is what gives it
   var(--w-dim), and S section 4.3's lint in test_hub_sections.py forbids a
   --w-* token in any money rule in this sheet. Fixing it here would mean
   loosening that lint to allow the very thing it exists to catch, so it is
   N4 finding 21 and edit-op hub_pages.py.price_pair.md op 2 instead. */

/* Finding 10. The toast fades to opacity 0 at 3,200 ms and stayed hit-testable
   and focusable: its "View cart" link was an invisible tab stop, which is
   2.4.7 and 1.4.13 both. Fading OUT of visibility is the visibility
   transition; fading in must not delay it, hence the two durations. */
.toast{ visibility:hidden; pointer-events:none;
  transition:opacity var(--px-t-enter) var(--px-e-enter),
             transform var(--px-t-enter) var(--px-e-enter),
             visibility 0s linear var(--px-t-enter); }
.toast.on{ visibility:visible; pointer-events:auto; transition-delay:0s; }

/* Finding 11. O section 1.1: "minimum tap target 44 x 44, no exception".
   Measured at 390 before this block: the wordmark 94x29, the back control
   36x44, a breadcrumb link 85x18, a footer link 47x17, a section exit 99x23.
   min-height plus inline-flex, so the box grows and the type does not. */
.bar .mark,.back,.railhead .close{ display:inline-flex; align-items:center;
  justify-content:center; min-height:44px; min-width:44px; }
.crumb a,.head a{ display:inline-flex; align-items:center; min-height:44px; }
/* An inline link inside a sentence cannot be 44 px tall without opening the
   line box, so it grows its HIT AREA instead: padding expands the inline box
   for pointer and touch, and an inline element's vertical padding does not
   move the line. 17 px of type plus 14 above and below is 45. */
.fine a,.faq p a,.obj p a{ padding-block:14px; }
