/* ═══════════════════════════════════════════════════════════════
   AFROTUNDA — Label 2026 component styles
   Component classes are .lb-*; responsive structure classes are .af-*
   and live in layout.css. Values come from the design system's label/
   React components unchanged — those components are the specification,
   this is the shipping CSS.

   Loaded last, after design-tokens → editorial → layout. See style.css.
   ══════════════════════════════════════════════════════════════ */

/* The rem base.

   Astra's typography settings put `font-size: 17px` on <html>. Every --ed-*
   token in editorial.css is expressed in rem and was authored against a 16px
   root, so the whole type scale rendered 6.25% larger than the design system
   specifies — the hero cleared its own clamp ceiling and fell through to the
   4.4vw arm, and every label, lead and body size drifted with it.

   Pinned back to 16px so the tokens mean what they say. Scoped with :has()
   rather than a bare `html` rule because <html> is outside the .at-editorial
   scope: a surface exempted via `afl_editorial_excluded` must keep Astra's
   base. Browsers without :has() keep the 17px root — larger type, identical
   layout, which is exactly today's behaviour rather than a regression. */
:root:has(body.at-editorial) { font-size: 16px; }

body.at-editorial {
  background: var(--ed-surface);
  font-family: var(--ed-font-body);
  color: var(--ed-text-title);
  -webkit-font-smoothing: antialiased;
  /* Body leading is the design system's own token, not Astra's. It matters
     for CMS prose that carries no class of ours — the product description
     inside a detail panel, the paragraph in the oxblood band — which would
     otherwise fall back to `normal` and set solid. */
  line-height: var(--ed-leading-body);
}

/* The small label roles take the browser default instead, which is what the
   prototype's own markup renders at: its <body> sets no leading and these
   components set none either. Inheriting a reading leading here made each of
   them roughly twice as tall as specified — the collection eyebrow rendered
   26px instead of 12.5px and pushed the headline 15px down the page, and the
   category rail stood 15px taller than the prototype's. */
.at-editorial .lb-label,
.at-editorial .lb-maker,
.at-editorial .lb-tile-price,
.at-editorial .lb-filter,
.at-editorial .lb-sort-title,
.at-editorial .lb-count { line-height: normal; }
.at-editorial img { max-width: 100%; }
.at-editorial ::selection { background: var(--ed-accent); color: #fff; }

/* ── Form controls take the label's faces ──────────────────────
   A control does NOT inherit `font-family`. Astra's inline CSS says
   `body,button,input,select,textarea,.ast-button,.ast-custom-button
   { font-family:'Roboto',sans-serif }` and `body.at-editorial` above only
   answers the `body` half of that, so a control with no rule of its own has
   been rendering in Roboto — a face this design system does not use anywhere.

   WHICH controls, measured rather than assumed. Most of the site was already
   covered, each by a different accident: WPForms declares `font-family:
   inherit` on every field type inside `.wpforms-container`, so the Contact
   form was fine; `.lb-news-form input` sets the body face, so the footer
   newsletter was fine; design-tokens.css scopes a fix to `.woocommerce-Reviews`
   and `#review_form`, so the review form was fine. What was left in Roboto is
   everything Woo renders raw: My Account's login and register fields
   (`.woocommerce-Input`), checkout's fields, the cart's coupon and quantity,
   and the comment form. Commerce, in other words — the pages where a stranger
   is asked to type their name and card details.

   Two roles, not one. A field a reader TYPES INTO is prose and takes the body
   face; a button is a label and takes the tracked display face — the same
   division `.lb-btn` and `.lb-acc-btn` already make by hand.

   THE SPECIFICITY IS THE POINT and it is deliberately as low as it can go.
   These are element selectors at (0,0,1), which is exactly Astra's weight —
   they win on ORDER, because label.css is enqueued long after Astra's inline
   block. Keeping them there is what lets every component rule that names a face
   for a specific control keep winning without being restated: `.lb-btn`
   (0,1,0), `.lb-news-form input` (0,1,1), `.lb-acc--faq .lb-acc-btn` (0,2,0)
   and its Lora question, the add-to-cart button. Scope this up to
   `.at-editorial` and every one of them would have to be re-asserted, which is
   the trap the heading reset at the end of this file had to be dug out of
   twice.

   `:where()` gives the scope without the weight: it contributes ZERO
   specificity, so this stays a base rule that applies only inside the label
   layer. A surface opted out via `afl_editorial_excluded` keeps Astra's. */
:where(body.at-editorial) :is(input, select, textarea, optgroup) {
  font-family: var(--ed-font-body);
}
:where(body.at-editorial) :is(
  button,
  .ast-button,
  .ast-custom-button,
  input[type="submit"],
  input[type="button"],
  input[type="reset"]
) {
  font-family: var(--ed-font-label);
}

/* WooCommerce's own buttons need naming, because Woo declares `font-family:
   inherit` on them twice over — `.woocommerce button.button` / `input.button` /
   `a.button` at (0,2,1) for anything inside its container, and
   `.woocommerce-js a.button` at (0,1,1) for a link anywhere on a Woo-enabled
   page — and both sit above the rule just written. Inheriting sent every Woo
   button to the body face: "Proceed to checkout", "Place order", "Update
   cart", "Apply coupon", "Log in". They are label roles like every other
   button here. Both of Woo's scopes are answered, or a bare `a.button` outside
   the container is missed.

   Named by Woo's own selectors rather than raised to a blanket `.at-editorial`
   weight, which would score (0,2,1) and out-rank `.lb-acc--faq .lb-acc-btn`
   (0,2,0) — turning every FAQ question from Lora into tracked Barlow.
   `.button` is Woo's class and appears on nothing of ours, so this cannot
   reach a label component. */
.at-editorial .woocommerce :is(button, input, a).button,
.at-editorial.woocommerce :is(button, input, a).button,
.at-editorial.woocommerce-js :is(button, input, a).button,
.at-editorial.woocommerce-page :is(button, input, a).button {
  font-family: var(--ed-font-label);
}

/* TranslatePress ships the ONE remaining off-system face. Its v2 switcher
   stylesheet is enqueued after label.css and sets `.trp-language-item-name` to
   the system stack at (0,1,0), so order cannot answer it — `.at-editorial`
   scoping (0,1,1) can. Currently only reachable inside Astra's hidden mobile
   popup drawer, which nothing on this site opens, but it is one rule and a
   drawer that becomes visible should not bring a fourth typeface with it.

   Found by sweeping every element on nine pages for a computed face outside
   Lora / DM Sans / Barlow Semi Condensed. It was the only hit left. */
.at-editorial .trp-language-item-name { font-family: var(--ed-font-label); }

/* Belt and braces on texture. The grain is deleted from this theme's
   design-tokens.css, but Astra, a plugin or a stale cached stylesheet could
   still paint one, so the label scope refuses it outright. */
body.at-editorial::after { content: none !important; }

/* ── Shared primitives ───────────────────────────────────────── */
.lb-wrap { max-width: var(--ed-max); margin: 0 auto; padding-inline: var(--ed-gutter); }
.lb-section { padding-block: var(--ed-section-y); }
.lb-section-tight { padding-block: var(--ed-section-y-tight); }
.lb-alt { background: var(--ed-surface-alt); }
/* Section shell — the blocks' version.

   `--flush` is the design's `space="flush"`; the stack is its gap between a
   section's children. The templates space their sections by hand — a `.lb-head`
   margin here, a grid margin there — so the stack only goes on the wrap the
   afrotunda/section block builds, and the head's own margin is released inside
   it so the two do not add up. */
.lb-section--flush { padding-block: 0; }
.lb-section-stack {
  /* The gap is a token so the paragraph rule below can subtract it back out
     and stay correct if this number ever changes. */
  --afl-stack-gap: clamp(28px, 3.5vw, 56px);
  display: flex; flex-direction: column; gap: var(--afl-stack-gap);
}
.lb-section-stack > .lb-head { margin-bottom: 0; }
/* The section's other arrangement: two half-width children side by side.
   `.af-two` in layout.css supplies the grid and the 900px collapse; this only
   tightens the gutter, because `.af-two`’s clamp(28px, 5vw, 88px) is sized for
   a sticky sidebar against a column of prose, and 88px between two PANELS that
   already carry 52px of padding each reads as two unrelated things. */
.lb-section-pair { gap: var(--ed-grid-gap); align-items: stretch; }
/* Prose written as core blocks directly in a section, the way the split half
   already allows. Same declarations as `.lb-split .af-split-text > p` and the
   same reason: an editor typing a core paragraph cannot wrap it in `.lb-body`,
   so the measure and the body face are given to the children. Without this a
   paragraph in a section inherits Astra's defaults across the full 1440.

   Consecutive paragraphs close up to a reading rhythm rather than taking the
   stack's section-sized gap — that gap is for a head against a grid, not for
   one sentence against the next. Scoped to `>` so a paragraph inside a nested
   `.lb-body` keeps that rule's spacing. */
.lb-section-stack > p {
  font-family: var(--ed-font-body); font-size: var(--ed-size-body);
  line-height: var(--ed-leading-body); color: var(--ed-text-body);
  max-width: var(--ed-max-text); margin: 0;
}
/* The stack's gap is sized for a head against a grid, not for one sentence
   against the next, so consecutive paragraphs pull it back to the 1.1em the
   split's prose already uses. Subtracting the token rather than guessing a
   negative number keeps the two in step. */
.lb-section-stack > p + p { margin-top: calc(1.1em - var(--afl-stack-gap)); }

.lb-label {
  font-family: var(--ed-font-label);
  font-weight: 600;
  font-size: var(--ed-size-label);
  text-transform: uppercase;
  letter-spacing: var(--ed-track-label);
  color: var(--ed-text-meta);
}
.lb-label-accent { color: var(--ed-accent); letter-spacing: var(--ed-track-label-wide); }

h1, h2, h3, .lb-title {
  font-family: var(--ed-font-title);
  font-weight: var(--ed-weight-title);
  letter-spacing: var(--ed-track-title);
  line-height: var(--ed-leading-title);
  color: var(--ed-text-title);
  margin: 0;
  text-wrap: balance;
}
h1 { font-size: var(--ed-size-hero); line-height: var(--ed-leading-hero); }
h2 { font-size: var(--ed-size-h2); }
h3 { font-size: var(--ed-size-h3); }

.lb-body {
  font-family: var(--ed-font-body);
  font-size: var(--ed-size-body);
  line-height: var(--ed-leading-body);
  color: var(--ed-text-body);
  max-width: var(--ed-max-text);
}
.lb-lead { font-size: var(--ed-size-lead); }
.lb-meta { font-family: var(--ed-font-body); font-size: var(--ed-size-meta); color: var(--ed-text-meta); line-height: 1.7; }

.lb-media { position: relative; overflow: hidden; background: var(--ed-media); display: block; }
.lb-media > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }

/* WooCommerce's own grid stylesheet sets `height: auto` on
   `.woocommerce-js ul.products li.product a img` — (0,3,4), which beats
   `.lb-media > img` at (0,1,1). The well kept its 3:4 box but the image sat at
   its natural ratio inside it, so every square source (much of this catalogue)
   rendered as a square with the well's grey showing beneath it, and only the
   3:4 sources happened to look right. Won on specificity at (0,4,3), not
   `!important`, and scoped to the label's own well so Woo's default images
   elsewhere keep their intrinsic sizing. */
.woocommerce ul.products li.product .lb-media > img,
.woocommerce-js ul.products li.product .lb-media > img { height: 100%; }

/* ── Section head ────────────────────────────────────────────── */
.lb-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 24px; flex-wrap: wrap; margin-bottom: clamp(16px, 2vw, 28px);
}
.lb-head > div:first-child { display: flex; flex-direction: column; gap: 8px; }
.lb-head :is(h1, h2) { max-width: 520px; }
.lb-head--center { justify-content: center; text-align: center; }
.lb-head--center > div:first-child { align-items: center; margin-inline: auto; }
.lb-head--h3 h2 { font-size: var(--ed-size-h3); }

/* The section head's link, as the design system specifies it: tracked caps on a
   hairline. It replaced the outlined small button outright in phase 8, when the
   last template that rendered one was seeded — see afl_section_head().
   Hover follows `.lb-btn--text`, the only other underlined link in the layer. */
.lb-head-link {
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: var(--ed-track-label);
  color: var(--ed-text-title); text-decoration: none;
  border-bottom: 1px solid var(--ed-line-strong); padding-bottom: 4px;
  white-space: nowrap;
  transition: color var(--ed-dur-fast) var(--ed-ease), border-color var(--ed-dur-fast) var(--ed-ease);
}
.lb-head-link:hover { color: var(--ed-accent); border-bottom-color: var(--ed-accent); }

/* Product search: compact, label-native, and usable before the category rail. */
.lb-product-search {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  max-width: 620px; margin: 0 0 clamp(18px, 2vw, 28px);
  border-bottom: 1px solid var(--ed-line);
}
.screen-reader-text {
  position: absolute !important; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0);
  white-space: nowrap; border: 0;
}
.lb-product-search input[type="search"] {
  min-width: 0; padding: 12px 0; border: 0; background: transparent;
  font-family: var(--ed-font-body); color: var(--ed-text-body);
}
.lb-product-search button {
  padding: 12px 0 12px 24px; border: 0; background: transparent;
  font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); letter-spacing: var(--ed-track-label);
  text-transform: uppercase; color: var(--ed-accent); cursor: pointer;
}

.afrotunda-withdrawal form { max-width: var(--ed-max-text); }
.afrotunda-withdrawal .form-row { margin-bottom: 18px; }
.afrotunda-withdrawal label { display: block; margin-bottom: 6px; }
.afrotunda-withdrawal input:not([type="checkbox"]),
.afrotunda-withdrawal textarea { width: 100%; }
.afrotunda-withdrawal button[type="submit"] { min-height: var(--ed-control-h); }
.afrotunda-hp { position: absolute !important; left: -10000px !important; width: 1px; height: 1px; overflow: hidden; }
.afrotunda-product-compliance {
  display: grid; grid-template-columns: minmax(130px, .7fr) minmax(0, 1.3fr);
  gap: 10px 20px; margin: 0 0 18px;
}
.afrotunda-product-compliance dt { font-weight: 600; }
.afrotunda-product-compliance dd { margin: 0; overflow-wrap: anywhere; }
@media (max-width: 520px) {
  .afrotunda-product-compliance { grid-template-columns: 1fr; gap: 4px; }
  .afrotunda-product-compliance dd + dt { margin-top: 10px; }
}

/* ── LabelButton ─────────────────────────────────────────────── */
.lb-btn {
  font-family: var(--ed-font-label);
  font-weight: 600;
  font-size: var(--ed-size-label-lg);
  text-transform: uppercase;
  letter-spacing: var(--ed-track-label);
  text-decoration: none;
  padding: var(--ed-control-pad);
  border: 1px solid var(--ed-accent);
  border-radius: 0;
  background: var(--ed-accent);
  color: var(--at-white);
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  line-height: 1.1; white-space: nowrap;
  transition: background var(--ed-dur-fast) var(--ed-ease),
              color var(--ed-dur-fast) var(--ed-ease),
              border-color var(--ed-dur-fast) var(--ed-ease);
}
/* Hover is gated on the enabled state. It used to be unconditional, with a
   second rule painting every disabled button back to solid oxblood on hover —
   which was right for a disabled `Sold out` and wrong for every other variant,
   since a disabled `--line` or `--onImage` button turned red under the pointer. */
.lb-btn:not([disabled]):not(.is-disabled):hover { background: var(--ed-ground-deep); border-color: var(--ed-ground-deep); color: var(--at-white); }
.lb-btn--sm { font-size: var(--ed-size-label); padding: var(--ed-control-pad-sm); }
.lb-btn--full { width: 100%; }
.lb-btn--line { background: transparent; color: var(--ed-text-title); border-color: var(--ed-line-strong); }
.lb-btn--line:not([disabled]):not(.is-disabled):hover { background: var(--ed-text-title); border-color: var(--ed-text-title); color: var(--at-canvas); }
.lb-btn--onImage { background: var(--at-white); color: var(--ed-text-title); border-color: var(--at-white); }
.lb-btn--onImage:not([disabled]):not(.is-disabled):hover { background: var(--ed-accent); border-color: var(--ed-accent); color: var(--at-white); }
.lb-btn--text {
  background: none; border: none; border-bottom: 1px solid var(--ed-line-strong);
  color: var(--ed-text-title); padding: 0;
}
.lb-btn--text:not([disabled]):not(.is-disabled):hover { background: none; color: var(--ed-accent); border-bottom-color: var(--ed-accent); }
.lb-btn[disabled], .lb-btn.is-disabled { opacity: .4; cursor: not-allowed; }

/* The not-found template uses the regular page-head and button vocabulary;
   this is the only layout it needs beyond those shared components. */
.lb-404-actions { display: flex; flex-wrap: wrap; gap: 12px; margin: 12px 0 0; }
.lb-pagehead--center .lb-404-actions { justify-content: center; }
/* Every container the label composes with is a flex column, so a bare button
   would stretch to the full measure. The templates wrap theirs in a plain
   `<div>` for that reason; this is that div with a name and an alignment. */
.lb-btn-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.lb-btn-row--center { justify-content: center; }
.lb-btn-row--end { justify-content: flex-end; }

/* ── MakerTag ────────────────────────────────────────────────── */
.lb-maker {
  font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); text-transform: uppercase;
  letter-spacing: var(--ed-track-label); color: var(--ed-text-meta);
  display: inline-flex; align-items: center; gap: 7px;
}
.lb-maker::before { content: ''; display: block; width: 5px; height: 5px; background: var(--ed-accent-soft); flex-shrink: 0; }
.lb-maker--accent { color: var(--ed-accent); }

/* ── AnnouncementBar ─────────────────────────────────────────── */
.lb-announce {
  background: var(--ed-text-title); color: var(--at-canvas);
  padding: 9px 16px; display: flex; align-items: center; justify-content: center;
  gap: 16px; position: relative;
  font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); text-transform: uppercase;
  letter-spacing: var(--ed-track-label);
}
.lb-announce button {
  position: absolute; right: 4px; width: 40px; height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none;
  color: inherit; cursor: pointer; font-size: 0.875rem; opacity: .6; padding: 0; line-height: 1;
  transition: opacity var(--ed-dur-fast) var(--ed-ease);
}
.lb-announce button:hover, .lb-announce button:focus-visible { opacity: 1; }

/* The rotator. Three messages share one line, so the lines are stacked in a
   grid cell rather than absolutely positioned — the bar then sizes itself to
   the tallest message and never reflows mid-rotation.

   Crossfade only, no movement: the same rule the hero slider follows. The
   600ms here must stay equal to FADE in afl_print_announce_script(). */
.lb-announce-lines { display: grid; }
.lb-announce-line {
  grid-area: 1 / 1;
  opacity: 0; visibility: hidden;
  transition: opacity 600ms var(--ed-ease), visibility 0s linear 600ms;
}
.lb-announce-line.is-on {
  opacity: 1; visibility: visible;
  transition: opacity 600ms var(--ed-ease), visibility 0s;
}
@media (prefers-reduced-motion: reduce) {
  /* The script never starts, so the first line is the whole message. */
  .lb-announce-line { transition: none; }
}

/* ── Header ──────────────────────────────────────────────────── */
.lb-header { position: sticky; top: 0; z-index: 40; background: var(--ed-surface); border-bottom: 1px solid var(--ed-line); }
.lb-header .af-header-bar {
  max-width: var(--ed-max); margin: 0 auto; padding: 0 var(--ed-gutter);
  height: 88px; display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 24px;
}
.af-header-nav { display: flex; gap: 30px; }
/* Nav and logo sit above the system's 0.875–0.9375rem label range on purpose —
   at spec size the nav read as tiny and the wordmark as tucked away.
   Logged as a deliberate deviation in HANDOFF.md. The gap the deviation buys
   is preserved across the 14px floor: the nav stays one step over the subnav
   and utilities beside it, which are now on the floor itself. */
.lb-navlink {
  font-family: var(--ed-font-label); font-weight: 600; font-size: 0.9375rem;
  text-transform: uppercase; letter-spacing: 0.14em;
  text-decoration: none; color: var(--ed-text-body);
  text-underline-offset: 5px; text-decoration-thickness: 1px;
  transition: color var(--ed-dur-fast) var(--ed-ease);
}
.lb-navlink:hover, .lb-navlink.is-active { color: var(--ed-text-title); text-decoration: underline; }

/* Collections dropdown. A top-level item with children becomes a panel rather
   than a second row of links. No shadow and no radius: --ed-shadow is none and
   the label layer has no elevation, so the panel is held by its hairline and
   the canvas behind it. Under 860px layout.css hides the nav outright. */
.lb-navitem { position: relative; display: inline-flex; align-items: center; }
.lb-navlink.has-children { display: inline-flex; align-items: center; gap: 7px; }
/* The one deliberate exception to the 14px floor: this is a ▾ disclosure glyph,
   not text, and it is sized in em so it tracks the nav link it hangs off. */
.lb-caret {
  font-size: 0.62em; line-height: 1; display: inline-block;
  transition: transform var(--ed-dur-fast) var(--ed-ease);
}
.lb-navitem:hover .lb-caret,
.lb-navitem:focus-within .lb-caret { transform: rotate(180deg); }

.lb-subnav {
  position: absolute; top: 100%; left: -24px; z-index: 50;
  display: flex; flex-direction: column; gap: 15px;
  min-width: 200px; padding: 24px;
  margin-top: 18px; /* clears the header's own hairline before the panel starts */
  background: var(--ed-surface);
  border: 1px solid var(--ed-line);
  /* Hidden with visibility, never display:none — the links inside must be able
     to enter the tab order as soon as the parent anchor takes focus. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(-5px);
  transition: opacity var(--ed-dur-fast) var(--ed-ease),
              transform var(--ed-dur-fast) var(--ed-ease),
              visibility 0s linear var(--ed-dur-fast);
}
/* The hover target has to span the gap between the link and the panel, or the
   pointer leaves the item on the way down and the panel closes under it. */
.lb-navitem::after {
  content: ""; position: absolute; top: 100%; left: 0; right: 0; height: 18px;
}
.lb-navitem:hover .lb-subnav,
.lb-navitem:focus-within .lb-subnav {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s, 0s, 0s;
}
.lb-subnav-link {
  font-family: var(--ed-font-label); font-weight: 600; font-size: 0.875rem;
  text-transform: uppercase; letter-spacing: 0.14em;
  text-decoration: none; color: var(--ed-text-body); white-space: nowrap;
  text-underline-offset: 5px; text-decoration-thickness: 1px;
  transition: color var(--ed-dur-fast) var(--ed-ease);
}
.lb-subnav-link:hover,
.lb-subnav-link.is-active { color: var(--ed-text-title); text-decoration: underline; }
.lb-logo { display: block; justify-self: center; }
.lb-logo img { height: 46px; display: block; }

/* Never size a wordmark by its height alone.
   ------------------------------------------
   Both wordmarks set only `height`, and let `width: auto` derive the rest from
   the file's intrinsic ratio. That is one HTML attribute away from breaking,
   and LiteSpeed Cache breaks it: for logged-out visitors only, it rewrites the
   img to a 1x1 GIF placeholder and adds `width="1000" height="329"`. The width
   attribute is a presentational hint that sets the used width, our CSS was not
   setting width at all, so it won — 1000px wide at 46px tall, and
   `object-fit: fill` (the default) stretched the wordmark flat. The header grid
   then squeezed the nav and the utilities around a wordmark column ~4.5x its
   proper size, which is what made the nav wrap at 1500px.

   Invisible to anyone signed in: LiteSpeed does not optimise for logged-in
   users, so every check ever run against this header was on the good path.

   `data-no-lazy` on the markup stops it at source (see chrome.php); this rule
   is the guard that makes the theme right regardless of what any optimiser,
   plugin or CMS field does to the attributes later. `aspect-ratio` keeps the
   box correct even while the src is still a 1x1 placeholder, and
   `object-fit: contain` means the worst case is letterboxing rather than
   distortion.

   The ratio is logo-dark.png's own — update it if that file is replaced. */
.lb-logo img,
.lb-footer-brand img {
  width: auto;
  aspect-ratio: 1000 / 329;
  object-fit: contain;
}
.lb-utils { display: flex; gap: 22px; justify-self: end; align-items: center; }
.lb-utils a {
  font-family: var(--ed-font-label); font-weight: 600; font-size: 0.875rem;
  letter-spacing: 0.14em; text-decoration: none; color: var(--ed-text-meta);
  text-transform: uppercase; min-height: 44px; min-width: 24px;
  display: inline-flex; align-items: center; justify-content: center;
}
.lb-utils a.is-on, .lb-utils a.lb-account, .lb-utils a.lb-bag { color: var(--ed-text-title); }
/* The utilities had no hover while the nav links beside them underline. Same
   role, so the same response: colour to ink, 1px underline at 5px offset. */
.lb-utils a {
  text-underline-offset: 5px; text-decoration-thickness: 1px;
  transition: color var(--ed-dur-fast) var(--ed-ease);
}
.lb-utils a:hover { color: var(--ed-text-title); text-decoration: underline; }

/* ── The burger and the drawer ─────────────────────────────────
   Below 860px `layout.css` has hidden `.af-header-nav` outright since this
   theme was written and nothing replaced it: on a phone the site had NO
   navigation, and the seven collection categories were unreachable. This is
   what replaces it. Both halves are hidden above 860px in layout.css, where
   every other responsive collapse lives.

   The mark is drawn, not loaded. Two hairlines plus the element's own top
   border would be three rules; one border and one ::before is two, and the box
   is 44px square because that is the smallest comfortable touch target and the
   bar has the height for it. */
/* `display: flex` here and hidden above 861px in layout.css, not the other way
   round. label.css is enqueued AFTER layout.css, so a `display: none` base
   rule here would out-order the media query that turns it on and the burger
   would never appear — which is exactly what happened the first time. The
   responsive OFF switch carries `!important` and wins from either direction. */
.lb-burger {
  display: flex;
  width: 44px; height: 44px; padding: 0; margin-left: -10px;
  background: none; border: 0; cursor: pointer;
  align-items: center; justify-content: flex-start;
  color: var(--ed-text-title);
}
.lb-burger-bars, .lb-burger-bars::before {
  content: ''; display: block; width: 22px; height: 1px;
  background: currentColor;
  transition: transform var(--ed-dur-fast) var(--ed-ease),
              opacity var(--ed-dur-fast) var(--ed-ease);
}
.lb-burger-bars { position: relative; }
.lb-burger-bars::before { position: absolute; top: -7px; }
/* Open: the top bar drops onto the bottom one and they cross. Two transforms,
   which is the whole animation — the label layer's motion rule is that if you
   can clearly see it happen it is too much. */
.lb-burger.is-open .lb-burger-bars { transform: rotate(45deg); }
.lb-burger.is-open .lb-burger-bars::before { top: 0; transform: rotate(-90deg); }

/* The panel. It hangs off the sticky header, so `top: 100%` puts it directly
   under the bar and the announcement strip above it stays put.

   The height is the viewport less everything above the panel, and CSS cannot
   work that out. `calc(100dvh - 100%)` subtracts the HEADER's height, which is
   right only when the header is at the top of the screen — with the
   announcement strip still on screen the panel starts 66px lower and its last
   row is cut off by exactly that much. So the script measures
   `innerHeight - header.bottom` when it opens the panel and writes the number
   here. The calc stays as the fallback: without JavaScript the panel is
   `hidden` and never seen, so the fallback only has to be sane, not exact.
   `100dvh` rather than `100vh` so an iOS address bar collapsing does not leave
   the last link underneath it. */
.lb-drawer {
  position: absolute; top: 100%; left: 0; right: 0;
  background: var(--ed-surface);
  border-top: 1px solid var(--ed-line);
  max-height: var(--lb-drawer-max, calc(100dvh - 100%));
  overflow-y: auto;
  /* Scroll chaining, not a scroll lock. Reaching the bottom of the panel must
     not start scrolling the page underneath it — that is the annoyance a lock
     is usually reached for. A LOCK is the wrong tool here and was tried first:
     `overflow: hidden` on <html> collapses the document's scroll range, so
     opening the panel 1500px down the page jumped it to 87 and closing it did
     not put it back. Measured, not guessed.
     Nothing else needs locking, because the panel hangs off a STICKY header —
     scroll the page behind it and the header pins to the top and the panel
     rides with it, still under the bar, still open. */
  overscroll-behavior: contain;
  padding: 8px var(--ed-gutter) 32px;
  z-index: 39; /* Under .lb-header's 40, so the bar stays above its own panel. */
}
.lb-drawer-nav { display: flex; flex-direction: column; }
.lb-drawer-item { display: grid; grid-template-columns: 1fr auto; align-items: center; }
.lb-drawer-link {
  display: block; padding: 16px 0;
  border-bottom: 1px solid var(--ed-line);
  font-family: var(--ed-font-title); font-weight: var(--ed-weight-title);
  font-size: var(--ed-size-h3); line-height: var(--ed-leading-title);
  letter-spacing: var(--ed-track-title);
  color: var(--ed-text-title); text-decoration: none;
}
/* The link and the toggle sit in one row and must share one rule, or the
   hairline breaks in the middle of the row it is meant to close. */
.lb-drawer-item > .lb-drawer-link,
.lb-drawer-item > .lb-drawer-more { border-bottom: 1px solid var(--ed-line); }
.lb-drawer-link.is-active { color: var(--ed-accent); }
.lb-drawer-more {
  display: flex; align-items: center; justify-content: flex-end;
  width: 44px; align-self: stretch; padding: 0;
  background: none; border: 0; cursor: pointer;
  font-family: var(--ed-font-label); font-size: 1.15rem; line-height: 1;
  color: var(--ed-text-meta);
  transition: color var(--ed-dur-fast) var(--ed-ease);
}
/* The GLYPH turns, not the button. The button carries the row's hairline —
   rotating it took the hairline with it and left a diagonal streak across the
   row, which is what the first version did. */
.lb-drawer-more > span {
  display: block;
  transition: transform var(--ed-dur-fast) var(--ed-ease);
}
.lb-drawer-more.is-open { color: var(--ed-text-title); }
.lb-drawer-more.is-open > span { transform: rotate(45deg); }
/* The categories, in the micro-type the collection rail uses for the same
   list — they are a level down from the four destinations above them, and the
   size difference is what says so. */
.lb-drawer-sub { display: flex; flex-direction: column; padding: 4px 0 12px; }
.lb-drawer-sub[hidden] { display: none; }
.lb-drawer-sublink {
  padding: 11px 0; text-decoration: none;
  font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); text-transform: uppercase;
  letter-spacing: var(--ed-track-label); color: var(--ed-text-body);
}
.lb-drawer-sublink.is-active { color: var(--ed-accent); }

/* Account, cart and the language pair, given up by the bar so the burger fits
   — see the note in layout.css. */
/* No rule of its own: the last nav link already closes the list with one, and
   a second hairline 14px under it reads as a mistake rather than a division.
   The air is the division. */
.lb-drawer-utils {
  display: flex; align-items: center; flex-wrap: wrap; gap: 22px;
  padding-top: 30px;
}
.lb-drawer-util, .lb-drawer-locales a {
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: 0.14em;
  text-decoration: none; color: var(--ed-text-meta);
}
.lb-drawer-util { color: var(--ed-text-title); }
.lb-drawer-locales a.is-on { color: var(--ed-text-title); }

/* ── The header at laptop widths ──────────────────────────────
   `1fr auto 1fr` centres the wordmark by making the side columns EQUAL, so
   the whole bar has to be wide enough for two of the WIDER one. The nav needs
   453px and the utilities 214, and below about 1190px the nav no longer fits
   its column and breaks onto a second line — "Contact us" and "Cart (0)"
   stacking. The nav does not hide until 860, so every viewport from 861 to
   1190 rendered a broken header: a 330px band that is exactly a laptop window
   that is not maximised.

   This band cannot be closed by tightening. At 861px the bar has 792px of
   content width; the nav, the wordmark and the utilities measure 807 before a
   single gap. Something has to give, and the alternatives were worse: raising
   the 860 cut-off removes navigation entirely (there is no mobile menu — the
   footer carries it), shrinking the nav to fit needs ~0.62rem, below the
   design system's label floor, and dropping "Contact us" undoes a call you
   made deliberately.

   So the bar becomes two rows instead: the wordmark centred over nav-left /
   utilities-right. Nothing is hidden, nothing shrinks, the wordmark stays
   centred, and there is ~100px of slack at the narrow end. The header grows
   from 88px to about 125px in this band only.

   Worth knowing: `1fr auto 1fr` with a hard 860 cut-off is the DESIGN SYSTEM's
   own header (`ui_kits/label/Chrome.jsx`, `readme.md`: "nav hides ≤860px"), so
   this band is a gap in the system rather than in this build — the same
   situation as the reveal-observer bug. Worth carrying back. */
@media (min-width: 861px) and (max-width: 1190px) {
  .lb-header .af-header-bar {
    height: auto;
    padding-block: 18px;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "mark mark"
      "nav  utils";
    row-gap: 16px;
  }
  .af-header-nav { grid-area: nav; gap: 24px; }
  .lb-logo { grid-area: mark; justify-self: center; }
  .lb-utils { grid-area: utils; }
}

/* ── CollectionHero ──────────────────────────────────────────── */
.lb-hero {
  position: relative; background: var(--ed-media); aspect-ratio: var(--ed-ratio-hero);
  min-height: min(420px, 62vh); display: flex; align-items: flex-end; overflow: hidden;
}
.lb-hero > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.lb-hero .lb-scrim {
  position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(23,21,15,.55) 0%, rgba(23,21,15,.1) 45%, transparent 70%);
}
.lb-hero-inner {
  position: relative; width: 100%; max-width: var(--ed-max); margin: 0 auto;
  padding: var(--ed-gutter); display: flex; flex-direction: column;
  align-items: flex-start; gap: 18px;
}
.lb-hero--onImage .lb-hero-inner .lb-label { color: rgba(255,255,255,.85); letter-spacing: var(--ed-track-label-wide); font-size: var(--ed-size-label-lg); }
.lb-hero--onImage h1 { color: var(--at-white); max-width: 620px; }
.lb-hero--onImage .lb-body { color: rgba(255,255,255,.78); }
/* No photograph behind it — reversed-out white type would be illegible on the
   stone well, so type falls back to ink. Mirrors CollectionHero's own rule. */
.lb-hero--onStone .lb-hero-inner .lb-label { color: var(--ed-accent); letter-spacing: var(--ed-track-label-wide); font-size: var(--ed-size-label-lg); }
/* The hero's two other heights. 21:9 is the system's own, and the one the
   photography argues with — see HANDOFF deviation 1. These are the dials that
   walk it back without leaving the component. */
.lb-hero--wide { aspect-ratio: var(--ed-ratio-wide); }
.lb-hero--tall { aspect-ratio: var(--ed-ratio-editorial); }
.lb-hero--center .lb-hero-inner { align-items: center; text-align: center; }

/* The tick markers, which replace the slider's dots and arrows on the hero —
   an adopted design change. They keep `.lb-slider-dot` so the footer script
   drives them unchanged, so these have to out-specify it: hence the compound
   selector rather than relying on source order. */
.lb-hero-ticks {
  position: absolute; z-index: 4;
  top: var(--ed-gutter); right: var(--ed-gutter);
  display: flex; align-items: center; gap: 10px;
}
.lb-slider-dot.lb-hero-tick {
  width: 26px; height: 24px; padding: 11px 0;
  background: rgba(255,255,255,.38); background-clip: content-box;
}
.lb-slider-dot.lb-hero-tick.is-on { background: var(--at-white, #fff); }
.lb-slider-dot.lb-hero-tick:hover { background: rgba(255,255,255,.7); }
.lb-hero--onStone .lb-slider-dot.lb-hero-tick { background: var(--ed-line-strong); }
.lb-hero--onStone .lb-slider-dot.lb-hero-tick.is-on { background: var(--ed-text-title); }

/* ── Page head (text-only, used by shop/about/journal) ─────────
   The deep top padding is the design system's, adopted in phase 8. It lives on
   `.lb-pagehead` rather than on a modifier because the shop and category heads
   reuse this class and were always meant to take the change with it — it was
   only held on `--deep` through phases 6 and 7 so the pages could be seeded in
   one move rather than drifting apart first. */
.lb-pagehead { padding: clamp(88px, 11vw, 172px) 0 var(--ed-section-y-tight); }
.lb-pagehead h1 { max-width: 760px; }
.lb-pagehead .lb-label { display: block; margin-bottom: 18px; }
.lb-pagehead .lb-body { margin-top: 20px; }
.lb-pagehead--divider { border-bottom: 1px solid var(--ed-line); }
/* The blocks' page head stacks its parts with one gap, so the two hand-set
   margins above are turned off here rather than deleted — the Woo heads have no
   `.lb-pagehead-inner` and still need them. Declared after those two rules and
   at equal weight, which is what lets it win. */
.lb-pagehead-inner { display: flex; flex-direction: column; gap: 20px; align-items: flex-start; }
.lb-pagehead--center .lb-pagehead-inner { align-items: center; text-align: center; }
.lb-pagehead--center h1, .lb-pagehead--center .lb-body { margin-inline: auto; }
.lb-pagehead-inner .lb-label { margin-bottom: 0; }
.lb-pagehead-inner .lb-label-accent { font-size: var(--ed-size-label-lg); }
.lb-pagehead-inner .lb-body { margin-top: 0; }

/* ── ProductTile ─────────────────────────────────────────────── */
.lb-tile { display: block; text-decoration: none; color: inherit; cursor: pointer; }
.lb-tile .lb-media { aspect-ratio: var(--ed-ratio-product); overflow: hidden; }
.lb-tile .lb-media img {
  transition: transform var(--ed-dur) var(--ed-ease);
}
.lb-tile:hover .lb-media > img.lb-tile-main,
.lb-tile:focus-visible .lb-media > img.lb-tile-main { transform: scale(1.025); }
.lb-tile.is-soldout .lb-tile-main { filter: saturate(0.45); }
.lb-soldout-flag {
  position: absolute; left: 0; bottom: 0; z-index: 2;
  background: var(--at-white); color: var(--ed-text-title);
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: var(--ed-track-label); padding: 7px 12px;
}
.lb-tile-info { position: relative; padding: 14px 0 13px; display: flex; flex-direction: column; gap: 5px; align-items: flex-start; }
.lb-tile-info::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: var(--ed-accent); opacity: 0; transform: scaleX(.18);
  transform-origin: left; transition: opacity var(--ed-dur-fast) var(--ed-ease), transform var(--ed-dur) var(--ed-ease);
}
.lb-tile-name {
  font-family: var(--ed-font-title); font-weight: var(--ed-weight-title);
  font-size: 1.02rem; line-height: 1.3; color: var(--ed-text-title);
  text-underline-offset: 3px; text-decoration-thickness: 1px;
}
.lb-tile:hover .lb-tile-name,
.lb-tile:focus-visible .lb-tile-name { color: var(--ed-accent); }
.lb-tile:hover .lb-tile-info::after,
.lb-tile:focus-visible .lb-tile-info::after { opacity: 1; transform: scaleX(1); }
.lb-tile-price { font-family: var(--ed-font-body); font-size: var(--ed-size-meta); color: var(--ed-text-body); display: flex; gap: 8px; }
.lb-tile-price del { color: var(--ed-text-meta); }

/* Product cards stay legible as a collection: hovering one no longer washes
   every sibling out. The interaction now belongs to the card itself. */
.at-editorial ul.products.af-quiet-siblings:hover > * { opacity: 1; }

/* ── EditorialSplit ──────────────────────────────────────────── */
.lb-split { background: var(--ed-surface); }
.lb-split.lb-alt { background: var(--ed-surface-alt); }
/* Prose left, photograph right — the About page alternates its two splits so
   the page does not read as a column of identical rows. Deliberately confined
   to the two-column width: below 900px layout.css puts every split's
   photograph first with `order: -1`, and an unscoped `order` here would beat it
   (same specificity, later file) and strand one split text-first on a phone. */
@media (min-width: 901px) {
  .lb-split--reverse > .af-split-media { order: 2; }
}
/* Square, and it grows with the row. `.af-split` is `align-items: stretch`, so
   a media well left at `height: auto` takes the row height whenever the prose
   beside it is the taller of the two — the 1:1 is the FLOOR, not a cap, and
   there is never a band of ground above or below the photograph.

   `width: 100%` is what keeps that safe. With a stretched height and an auto
   width the aspect-ratio transfers the other way and sizes the WIDTH from the
   row: a 1100px-tall row gave a 1100px-wide well in a 756px column, straight
   out of the page. A definite width leaves the ratio only one axis to act on.
   See the decorative-media note near the end of this file. */
.lb-split .af-split-media { aspect-ratio: 1 / 1; width: 100%; }
.lb-split--media-landscape .af-split-media { aspect-ratio: 4 / 3; }
.lb-split .af-split-text {
  display: flex; flex-direction: column; justify-content: center;
  padding: clamp(24px, 4vw, 72px); gap: 20px; min-width: 0;
}
.lb-split .af-split-text h2 { max-width: 460px; }
.lb-split .af-split-text .lb-body { display: flex; flex-direction: column; gap: 1.1em; }
.lb-split .af-split-text p { margin: 0; }
/* Prose written as core blocks in the text half. The templates wrap their
   paragraphs in `.lb-body`, which an editor typing a core paragraph cannot do,
   so the measure and the body face are given to the children directly. Scoped
   to `>` so a paragraph inside a nested `.lb-body` keeps that rule's spacing. */
.lb-split .af-split-text > p {
  font-family: var(--ed-font-body); font-size: var(--ed-size-body);
  line-height: var(--ed-leading-body); color: var(--ed-text-body);
  max-width: var(--ed-max-text); margin: 0;
}
.lb-split .af-split-text > .lb-head { margin-bottom: 0; }

/* The page opener. The headline takes the hero size the homepage template used
   to set inline — it belongs in the cascade, not on the element — and the
   measure keeps it to about a dozen characters a line so it breaks where the
   design breaks it. Phase 8: this arrangement replaced the full-bleed
   CollectionHero on the homepage, because a portrait photograph fits a fold at
   half the width and not at full. See HANDOFF.md. */
.lb-split--opener .af-split-text :is(h1, h2) {
  font-size: var(--ed-size-hero);
  line-height: var(--ed-leading-hero);
  max-width: 12ch;
}

/* ── Reel ────────────────────────────────────────────────────── */
/* The centre-focused horizontal track, shared by the lookbook and the
   collaborator strip. Added 2026-09-02.

   NOT called .lb-rail: that name is already the collection page's filter bar
   (woo-archive.php, and the rules at the foot of this file), whose hairlines
   and flex-wrap this would have inherited on sight.

   This is the one component in the label layer that MOVES something sideways.
   The system has no carousel and its two rotators are both "crossfade only,
   no movement"; that rule is broken here once, deliberately, and the price
   paid for it is that nothing moves on its own — no autoplay, and no drag.

   Two states, and the script picks between them by measuring:
   - default (no .is-live): a static centred row. Every card at full size and
     full opacity, controls hidden. This is also what a reader with no
     JavaScript keeps, which is why it is the state written in the markup.
   - .is-live: the reel proper. One card centred, its neighbours dimmed and
     scaled down, arrows and dots shown. */

.lb-reel {
  position: relative;
  /* HOW MANY CARDS ARE IN VIEW is the component's one dimension, and the card
     width is derived from it rather than the other way round — three whole
     cards always fill the reel, at any width, and the breakpoints below change
     the count rather than letting the cards shrink towards illegibility.

     The script READS this to know where its window starts, so a breakpoint can
     change the arrangement without the script carrying any of the numbers. */
  --afl-reel-per: 3;
  --afl-reel-card: calc(
    (100% - (var(--afl-reel-per) - 1) * var(--afl-reel-gap)) / var(--afl-reel-per)
  );
  --afl-reel-gap: var(--ed-grid-gap);
  /* How far a dimmed neighbour shrinks. The reference scales the centre card
     UP; scaling the neighbours DOWN reaches the same relationship while only
     ever downsampling a photograph, and it leaves the track's height fixed by
     the full-size card so nothing below the reel shifts on a step. */
  --afl-reel-rest: 0.86;
  --afl-reel-dim: 0.34;
  /* The step's own duration, longer than an interface hover for the same
     reason the hero slider's crossfade is: this is a composition changing,
     not a control answering a pointer. */
  --afl-reel-dur: 600ms;
  --afl-reel-ease: cubic-bezier(0.37, 0, 0.24, 1);
}
.lb-reel--airy { --afl-reel-gap: calc(var(--ed-grid-gap) * 1.6); }

/* Fewer cards in view rather than narrower cards. The steps match `.af-grid`'s
   in layout.css so a reel and a grid collapse on the same lines. `--narrow`
   holds one more card per row than `--wide` at every step, which is what the
   collaborator strip's shorter cards are for. */
.lb-reel--narrow { --afl-reel-per: 4; }
@media (max-width: 1100px) {
  .lb-reel--narrow { --afl-reel-per: 3; }
}
@media (max-width: 860px) {
  .lb-reel { --afl-reel-per: 2; }
  .lb-reel--narrow { --afl-reel-per: 2; }
}
@media (max-width: 560px) {
  .lb-reel { --afl-reel-per: 1; }
  .lb-reel--narrow { --afl-reel-per: 1; }
}

/* Out to the viewport edge, leaving the section head behind inside the
   measure. `100vw` rather than `100%` because the reel's parent carries the
   measure; the negative margins re-centre the wider box on it. */
.lb-reel--bleed {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
.lb-reel-viewport { overflow: hidden; }
/* A focusable div needs a focus ring that is not the browser's default box
   drawn round the whole page width. */
.lb-reel-viewport:focus-visible { outline: 2px solid var(--ed-accent); outline-offset: -2px; }

.lb-reel-track {
  display: flex;
  gap: var(--afl-reel-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.lb-reel-card {
  flex: 0 0 var(--afl-reel-card);
  width: var(--afl-reel-card);
  min-width: 0;
}

/* The static row — a reel with no more cards than the window holds. Centred,
   and given the gutter back, because with nothing to scroll a row pinned to
   the viewport edges is just a row with no margin. */
.lb-reel:not(.is-live) .lb-reel-track {
  justify-content: center;
  padding-inline: var(--ed-gutter);
}

/* The live reel. Everything below this point is inert until the script has
   found more cards than the window holds.

   `--w` is the window's FIRST card, not the highlighted one. The highlight
   moves inside the window — middle slot normally, edge slot at the two ends —
   so that three whole cards are in view everywhere including the first and
   last, which is what centring the active card could not do. */
.lb-reel.is-live .lb-reel-track {
  transform: translateX(calc(var(--w, 0) * (var(--afl-reel-card) + var(--afl-reel-gap)) * -1));
  transition: transform var(--afl-reel-dur) var(--afl-reel-ease);
}
.lb-reel.is-live .lb-reel-card {
  opacity: var(--afl-reel-dim);
  transform: scale(var(--afl-reel-rest));
  transition: opacity var(--afl-reel-dur) var(--afl-reel-ease),
              transform var(--afl-reel-dur) var(--afl-reel-ease);
}
.lb-reel.is-live .lb-reel-card.is-on { opacity: 1; transform: none; }

/* A dimmed card is scenery: it must not be a tab stop, or the keyboard walks
   through links nobody can read. */
.lb-reel.is-live .lb-reel-card:not(.is-on) a { pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  /* It still changes card — a carousel that cannot is broken, not considerate
     — but it cuts rather than glides. */
  .lb-reel.is-live .lb-reel-track,
  .lb-reel.is-live .lb-reel-card { transition: none; }
}

/* ── Reel controls ───────────────────────────────────────────── */
/* The one place a circle is allowed in this layer. `--ed-radius` is 0 by rule
   and every other control obeys it; an arrow floating in the gutter beside a
   photograph has no box to be square with, and a square there reads as a crop
   mark. Written as an explicit 50% rather than a token so the exception stays
   local to this component and nothing else picks it up. */
.lb-reel-nav { position: absolute; inset: 0; pointer-events: none; }
.lb-reel-arrow {
  position: absolute;
  /* Centred on the PHOTOGRAPH, not on the card — a card is a square well plus
     one or two lines of caption, so the card's own midpoint sits low, in the
     type. The value is measured and written by the script because it cannot be
     computed here: `--afl-reel-card` is a width built from a percentage, and a
     percentage in `top` resolves against height instead. The 50% fallback only
     applies before the script has run, and the arrows are `hidden` until it
     has. */
  top: var(--afl-reel-mid, 50%);
  transform: translateY(-50%);
  pointer-events: auto;
  width: 46px; height: 46px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--ed-line-strong);
  border-radius: 50%;
  background: var(--ed-surface);
  color: var(--ed-text-title);
  font-size: 1.05rem; line-height: 1; cursor: pointer;
  transition: background var(--ed-dur-fast) var(--ed-ease),
              border-color var(--ed-dur-fast) var(--ed-ease);
}
.lb-reel-arrow:hover { background: var(--ed-surface-alt); border-color: var(--ed-text-title); }
.lb-reel-arrow:focus-visible { outline: 2px solid var(--ed-accent); outline-offset: 2px; }
/* In the two gutters of the window — one card in from each edge, centred on
   the seam — which is where the reference puts them. Derived from the card
   width, so they follow the breakpoints without being restated. */
.lb-reel-arrow--prev { left: calc(var(--afl-reel-card) + var(--afl-reel-gap) / 2 - 23px); }
.lb-reel-arrow--next { right: calc(var(--afl-reel-card) + var(--afl-reel-gap) / 2 - 23px); }
/* With two cards in view there is one seam, and with one there is none, so
   below 860px the arrows leave the seams and sit at the outer edges over the
   photographs. That is the only place left for them once the window stops
   having a middle. */
@media (max-width: 860px) {
  .lb-reel-arrow--prev { left: 12px; }
  .lb-reel-arrow--next { right: 12px; }
}

.lb-reel-dots {
  display: flex; justify-content: center; gap: 9px;
  margin-top: clamp(20px, 2.4vw, 34px);
}
/* The script hides the controls on a static reel with the `hidden` attribute,
   and `hidden`'s display:none is a UA rule that a class loses to. The arrows
   never showed the seam because `.lb-reel-nav` declares no display of its own;
   the dots did, so a reel with nothing to step carried three marks under it.
   Restated here rather than dropping the flex, because `hidden` is the state
   the script already speaks. */
.lb-reel-dots[hidden] { display: none; }
/* The MakerTag's 5px square, reused rather than reinvented — the same mark
   the hero slider uses, on a light ground instead of a photograph. */
.lb-reel-dot {
  width: 24px; height: 24px; padding: 9px; border: 0; cursor: pointer;
  background-clip: content-box;
  background: var(--ed-line-strong);
  transition: background var(--ed-dur) var(--ed-ease);
}
.lb-reel-dot.is-on { background: var(--ed-text-title); }
.lb-reel-dot:hover { background: var(--ed-accent-soft); }
.lb-reel-dot:focus-visible { outline: 2px solid var(--ed-accent); outline-offset: 3px; }

/* ── Lookbook ────────────────────────────────────────────────── */
/* A frame in the reel. Square, per the one-shape standard every decorative
   photograph on this site follows — and here it is a flat 1:1 rather than a
   floor, because a reel has no row for a frame to stretch to the height of.
   The mixed-span rule this replaces went with the grid on 2026-09-02. */
.lb-reel-frame { margin: 0; display: flex; flex-direction: column; }
.lb-reel-frame .lb-media { aspect-ratio: 1 / 1; width: 100%; }
/* Set as PROSE, not as a label. These captions carry whole sentences — three
   and four lines of them on the About page — and the label treatment they used
   to wear was built for two or three words: condensed, 600, uppercase, on the
   widest tracking in the system. Uppercase is what makes that tracking
   necessary, so dropping one drops the other; and a sentence at 600 in a
   condensed face still reads as shouting whatever its case. The whole role
   changes together, to the same meta treatment `.lb-event-place` and every
   other small line of prose on the site already uses. */
.lb-lookbook figcaption {
  margin-top: 10px; font-family: var(--ed-font-body); font-weight: 400;
  font-size: var(--ed-size-meta); line-height: 1.6;
  letter-spacing: normal; color: var(--ed-text-meta);
}

/* A linked frame. The anchor wraps the photograph AND the caption, because a
   caption under a clickable image gets clicked whether or not it is a link.
   The hover treatment is the hairline underline `.lb-head-link` already uses,
   so a card that can be clicked says so in the vocabulary the rest of the page
   already speaks — no radius, no elevation, nothing borrowed from a button. A
   frame with no destination gets none of this and stays inert, which is the
   same rule as the catalogue well that says why it is empty. */
.lb-reel-link { display: flex; flex-direction: column; text-decoration: none; color: inherit; }
.lb-reel-link .lb-media img { transition: transform var(--ed-dur) var(--ed-ease); }
.lb-reel-link:hover .lb-media img { transform: scale(var(--ed-hover-image-scale)); }
.lb-reel-link:hover figcaption,
.lb-reel-link:focus-visible figcaption {
  color: var(--ed-text-title);
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}
.lb-reel-link:focus-visible { outline: 2px solid var(--ed-accent); outline-offset: 3px; }

/* Collaborator cards lead to Afrotunda's own catalogue filtered by Maker —
   never to a retired vendor storefront. */
.lb-collab-card { display: block; color: inherit; text-decoration: none; }
.lb-collab-card > .lb-media { width: 100%; overflow: hidden; }
.lb-collab-card > .lb-media img { transition: transform var(--ed-dur) var(--ed-ease); }
.lb-collab-card:hover > .lb-media img,
.lb-collab-card:focus-visible > .lb-media img { transform: scale(1.025); }
.lb-collab-card:hover .lb-maker,
.lb-collab-card:focus-visible .lb-maker { text-decoration: underline; text-underline-offset: 4px; }
.lb-collab-card:focus-visible { outline: 2px solid var(--ed-accent); outline-offset: 3px; }

/* A post card in a reel. `.lb-post` is already the whole card as one anchor
   with a 1:1 well and an underline on hover, so it needs none of the linking
   the lookbook frame does — it arrives clickable. What it does need is to stop
   being a grid item: `.lb-journal` sets a generous row-gap for a grid that no
   longer exists here, and the reel owns the spacing between cards. */
.lb-journal-reel .lb-post { height: 100%; }
.lb-journal-reel .lb-post > .lb-media { width: 100%; }
/* And nothing else. The window, the card width, the 1:1 well and the crop bias
   all stay the component's and the base card's, so this is the home page's
   lookbook reel with post cards in it — same size, same rhythm, one carousel
   in the library rather than two. The cost is the reel's documented degrade:
   three posts against a window of three draw as a static centred row and the
   track starts moving when a fourth post is written. */

/* ── Standalone figure ───────────────────────────────────────── */
/* One picked photograph, on its own in a section rather than inside a tile,
   a lookbook frame or a journal card. Those three give their well its
   aspect-ratio; nothing gives this one, so the block sets it inline from the
   shape chosen in the editor — `.lb-media` has none of its own and positions
   its image absolutely, so a well without one has no height at all.

   The caption is the lookbook's, restated rather than shared: the lookbook's
   rule is scoped under `.lb-lookbook` and widening it would move bytes on
   every page that renders a lookbook, for nothing. */
.lb-figure { margin: 0; display: flex; flex-direction: column; }
/* And the well needs a width as well as a shape. It is a flex item whose only
   in-flow content is nothing — the image is absolute, and in the editor the
   placeholder is too — so with an aspect-ratio and no definite size in either
   axis it resolves to 0x0 and the whole figure disappears. It survives inside
   a section only because `.lb-section-stack` stretches the figure to a
   definite width first; at the top level of a page, where a freshly inserted
   block lands, there is nothing to stretch it and the block is invisible. */
.lb-figure > .lb-media { width: 100%; }
.lb-figure figcaption {
  margin-top: 10px; font-family: var(--ed-font-body); font-weight: 400;
  font-size: var(--ed-size-meta); line-height: 1.6;
  letter-spacing: normal; color: var(--ed-text-meta);
}

/* ── SpecList ────────────────────────────────────────────────── */
.lb-specs { margin: 0; display: grid; border-top: 1px solid var(--ed-line); }
.lb-specs > div {
  display: grid; grid-template-columns: 130px 1fr; gap: 16px;
  padding: 14px 0; border-bottom: 1px solid var(--ed-line);
}
.lb-specs dt {
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: var(--ed-track-label);
  color: var(--ed-text-meta); padding-top: 2px;
}
.lb-specs dd { margin: 0; font-family: var(--ed-font-body); font-size: var(--ed-size-meta); line-height: 1.65; color: var(--ed-text-body); }

/* ── DetailsAccordion ────────────────────────────────────────── */
.lb-acc { border-top: 1px solid var(--ed-line); max-width: var(--ed-max-text); }
.lb-acc-item { border-bottom: 1px solid var(--ed-line); }

/* Two-up accordion — the Contact FAQ, which has enough entries that a single
   620px column leaves most of the page empty. Runs full width and drops to one
   column at 760px, where two columns would squeeze every question onto three
   lines.

   The rules are written to survive any number of items. The container loses its
   top border and the FIRST ITEM OF EACH COLUMN carries one instead — in a
   two-column row-major grid those are always :nth-child(1) and :nth-child(2),
   whatever the total. Items keep their own bottom border, so each column closes
   itself and an odd count cannot leave a stray line hanging across the gutter. */
.lb-acc--2col {
  max-width: none;
  border-top: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(28px, 5vw, 72px);
  align-items: start;
}
.lb-acc--2col > .lb-acc-item:nth-child(-n+2) { border-top: 1px solid var(--ed-line); }
.lb-acc--2col .lb-acc-body { max-width: none; }

@media (max-width: 760px) {
  .lb-acc--2col { grid-template-columns: minmax(0, 1fr); }
  /* Stacked, the second item's top border would double the first item's
     bottom one. The first item's top border stands in for the container's. */
  .lb-acc--2col > .lb-acc-item:nth-child(2) { border-top: 0; }
}
.lb-acc-btn {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 17px 0; background: none; border: none; cursor: pointer;
  text-align: left; font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label-lg); text-transform: uppercase;
  letter-spacing: var(--ed-track-label); color: var(--ed-text-title);
}
.lb-acc-btn .lb-acc-title { text-underline-offset: 4px; text-decoration-thickness: 1px; }
/* The panel headers had no hover state — neither here nor in the system's own
   DetailsAccordion.jsx, which styles a button with `cursor: pointer` and then
   never responds to the pointer. Built from the label layer's existing
   vocabulary rather than a new one: the title underlines exactly as a nav link,
   a tile name and a journal headline do, and the + comes up from meta to ink.
   Nothing lifts and nothing shadows. The title is wrapped so the underline
   cannot be dragged through the +. */
.lb-acc-btn:hover .lb-acc-title { text-decoration: underline; }
.lb-acc-btn:hover .lb-acc-mark { color: var(--ed-text-title); }
.lb-acc-btn .lb-acc-mark {
  font-size: 15px; line-height: 1; color: var(--ed-text-meta); display: block;
  transition: transform var(--ed-dur-fast) var(--ed-ease),
              color var(--ed-dur-fast) var(--ed-ease);
}
.lb-acc-item.is-open .lb-acc-btn .lb-acc-mark { transform: rotate(45deg); }
.lb-acc-body { display: none; padding: 0 0 20px; font-family: var(--ed-font-body); font-size: var(--ed-size-meta); line-height: 1.75; color: var(--ed-text-body); max-width: var(--ed-max-text); }
.lb-acc-item.is-open .lb-acc-body { display: block; }

/* ── FaqAccordion ─────────────────────────────────────────────
   The design system's FAQ sibling of the DetailsAccordion above. Same panel
   primitive; what changes is the register. The question is set in Lora at h3
   in sentence case rather than in tracked uppercase micro-type, which is what
   lets a FAQ read as a page of questions rather than as a spec table, and the
   answer comes up from meta size to body size to match.

   The hover state is this build's, not the system's — DetailsAccordion styles
   a button with `cursor: pointer` and then never responds to the pointer. It
   is inherited from .lb-acc-btn above and deliberately kept. */
.lb-acc--faq { max-width: 860px; }
.lb-acc--faq .lb-acc-btn {
  align-items: baseline; gap: 24px; padding: 22px 0;
  font-family: var(--ed-font-title); font-weight: var(--ed-weight-title);
  font-size: var(--ed-size-h3); line-height: var(--ed-leading-title);
  letter-spacing: var(--ed-track-title); text-transform: none;
}
/* The + stays in the label face. Without this it inherits the button's Lora
   and the mark reads as a serif plus sign next to the question. */
.lb-acc--faq .lb-acc-mark {
  font-family: var(--ed-font-label); font-size: 1rem;
  transition: transform var(--ed-dur) var(--ed-ease), color var(--ed-dur) var(--ed-ease);
}
.lb-acc--faq .lb-acc-body {
  padding: 0 0 26px;
  font-size: var(--ed-size-body); line-height: var(--ed-leading-body);
}
/* The component lays the answer out as a flex column with a 1em gap; as block
   flow that is the same measure without changing the display type the toggle
   switches. */
.lb-acc--faq .lb-acc-body > * { margin: 0; }
.lb-acc--faq .lb-acc-body > * + * { margin-top: 1em; }

/* Two-up. af-two carries the grid and its own 900px collapse; the list is
   split down the DOM so it reads column-major — first half left, second half
   right — where .lb-acc--2col above flows row-major across the gutter. */
.lb-acc--split { max-width: none; }
.lb-acc--split > .lb-acc-col > .lb-acc-item:last-child { border-bottom: 1px solid var(--ed-line); }

/* ── SizeSelector ────────────────────────────────────────────── */
.lb-sizes-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 10px; }
.lb-sizes { display: flex; gap: 8px; flex-wrap: wrap; }
.lb-size {
  min-width: 52px; height: 44px; padding: 0 14px;
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label-lg);
  text-transform: uppercase; letter-spacing: .08em;
  border: 1px solid var(--ed-line-strong); background: transparent;
  color: var(--ed-text-title); border-radius: 0; cursor: pointer;
  transition: background var(--ed-dur-fast) var(--ed-ease),
              border-color var(--ed-dur-fast) var(--ed-ease),
              color var(--ed-dur-fast) var(--ed-ease);
}
/* Selected fills near-black, deliberately not oxblood, so red stays reserved
   for the add-to-cart action. */
.lb-size.is-on { background: var(--ed-text-title); border-color: var(--ed-text-title); color: var(--at-canvas); }
/* An unselected size had no hover, though SizeSelector.jsx declares a
   transition on border-color and then never changes it. The border firms from
   --ed-line-strong to ink — the same near-black the selected state fills with,
   so hovering reads as a preview of selecting. Red stays reserved for
   add-to-cart. A selected or a sold-out size does not move. */
.lb-size:not([disabled]):not(.is-on):hover { border-color: var(--ed-text-title); }
.lb-size[disabled] { opacity: .45; cursor: not-allowed; text-decoration: line-through; color: var(--ed-text-meta); }

/* ── Filter rail ─────────────────────────────────────────────── */
.lb-rail { display: flex; gap: 22px; flex-wrap: wrap; align-items: center; padding: 18px 0; border-block: 1px solid var(--ed-line); }
.lb-filter {
  background: none; border: none; padding: 7px 0; min-height: 32px; cursor: pointer;
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: var(--ed-track-label);
  color: var(--ed-text-meta); text-underline-offset: 5px; text-decoration-thickness: 1px;
  transition: color var(--ed-dur-fast) var(--ed-ease);
}
/* The rail's filters are anchors here, not the prototype's <button>s — real
   archive links rather than a client-side filter, so they need the button's
   box and none of an anchor's underline. Leading comes from the body rule. */
.lb-filter,
.lb-rail .lb-count { display: inline-block; }
.lb-filter { text-decoration: none; }
.lb-filter:hover { color: var(--ed-text-title); }
.lb-filter.is-on { color: var(--ed-text-title); text-decoration: underline; }
.lb-count { margin-left: auto; }

/* The sort row sits under the rail and reads as its quieter sibling: the same
   .lb-filter type, no border of its own, and the whole row pushed right so the
   rail's category list stays the first thing scanned. The row's own title is a
   label, not a control — hence .lb-sort-title rather than a fifth .lb-filter. */
.lb-sort {
  display: flex; gap: 22px; flex-wrap: wrap; align-items: center;
  justify-content: flex-end; padding: 14px 0 0;
}
.lb-sort-title { color: var(--ed-text-meta); margin-right: -8px; }
@media (max-width: 560px) {
  /* Right-aligned, the row wraps into a ragged left edge on a phone. */
  .lb-sort { justify-content: flex-start; gap: 16px; }
  .lb-sort-title { flex-basis: 100%; margin-right: 0; }
}

/* ── The empty collection ────────────────────────────────────── */
/* Woo prints this inside wc_print_notice(); the override in
   woocommerce/loop/no-products-found.php prints these three instead. Centred
   and given real height so an empty category reads as a finished page rather
   than as a grid that failed to load. */
.lb-empty {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  text-align: center; padding: clamp(56px, 9vw, 120px) 0;
}
.lb-empty-title {
  margin: 0; font-family: var(--ed-font-title);
  font-size: clamp(1.25rem, 2.2vw, 1.6rem); color: var(--ed-text-title);
}
.lb-empty .lb-meta { margin: 0; max-width: 42ch; }
.lb-empty .lb-btn { margin-top: 8px; }

/* ── Footer ──────────────────────────────────────────────────── */
.lb-footer { background: var(--ed-footer); border-top: 1px solid var(--ed-line); }
.lb-footer-inner { max-width: var(--ed-max); margin: 0 auto; padding: clamp(48px, 6vw, 80px) var(--ed-gutter) 0; }
.lb-footer-main { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(340px, .9fr); gap: clamp(48px, 6vw, 100px); align-items: start; }
.lb-footer-main--no-newsletter { grid-template-columns: minmax(0, 1fr); }
.lb-footer-main--no-newsletter .lb-footer-left { max-width: 840px; }
.lb-footer-left { min-width: 0; }
.lb-footer-newsletter { min-width: 0; max-width: 520px; padding-top: 4px; }
/* Width and ratio are pinned beside .lb-logo img above — see the note there. */
.lb-footer-brand img { height: 52px; display: block; margin-bottom: 22px; }
.lb-footer-place {
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: var(--ed-track-label-wide);
  color: var(--ed-accent); margin-bottom: 14px;
}
.lb-footer-brand p { margin: 0; font-family: var(--ed-font-body); font-size: var(--ed-size-meta); line-height: 1.75; color: var(--ed-text-body); max-width: 340px; }
.lb-news-title { font-family: var(--ed-font-title); font-weight: 400; font-size: 1.3rem; letter-spacing: -0.015em; color: var(--ed-text-title); margin-bottom: 16px; }
.lb-news-form { display: flex; align-items: flex-end; gap: 16px; border-bottom: 1px solid var(--ed-line-strong); padding-bottom: 9px; max-width: 420px; }
.lb-news-form input {
  flex: 1; min-width: 0; border: none; background: none; outline: none;
  font-family: var(--ed-font-body); font-size: .95rem; color: var(--ed-text-title); padding: 4px 0;
}
.lb-news-form button {
  background: none; border: none; cursor: pointer; padding: 4px 0;
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: var(--ed-track-label);
  color: var(--ed-accent-soft); white-space: nowrap;
  transition: color var(--ed-dur-fast) var(--ed-ease);
}
/* Amber to oxblood, which is what .lb-btn--text does — the only text-button
   hover the label layer defines. */
.lb-news-form button:hover { color: var(--ed-accent); }
.lb-news-note { margin: 12px 0 0; font-family: var(--ed-font-body); font-size: 0.875rem; color: var(--ed-text-meta); line-height: 1.6; }
.lb-news-embed { width: 100%; max-width: 520px; min-height: var(--ed-control-h); }
.af-footer-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(16px, 2vw, 32px); margin-top: clamp(38px, 4vw, 56px);
}
.af-fcol-head {
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: var(--ed-track-label-wide);
  color: var(--ed-accent); margin-bottom: 16px;
}
.af-fcol-toggle {
  width: 100%; align-items: center; justify-content: space-between; gap: 12px;
  padding: 16px 0; background: none; border: none; cursor: pointer;
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: var(--ed-track-label-wide);
  color: var(--ed-text-title);
}
.af-fcol-toggle span { font-size: 15px; line-height: 1; color: var(--ed-text-meta); display: block; transition: transform var(--ed-dur-fast) var(--ed-ease), color var(--ed-dur-fast) var(--ed-ease); }
.af-fcol-toggle:hover span { color: var(--ed-text-title); }
.af-fcol.is-open .af-fcol-toggle span { transform: rotate(45deg); }
.af-fcol-body { list-style: none; padding: 0 0 4px; margin: 0; display: grid; gap: 11px; }
.af-fcol-body a { font-family: var(--ed-font-body); font-size: var(--ed-size-meta); color: var(--ed-text-body); text-decoration: none; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.af-fcol-body a:hover { text-decoration: underline; color: var(--ed-text-title); }
.af-fcol-body a:focus-visible { outline: 2px solid var(--ed-accent); outline-offset: 3px; }
.lb-legal {
  margin-top: clamp(32px, 4vw, 56px); border-top: 1px solid var(--ed-line);
  padding: 18px 0 clamp(28px, 4vw, 44px); display: flex; align-items: center;
  justify-content: space-between; flex-wrap: wrap; gap: 14px;
  font-family: var(--ed-font-body); font-size: 0.875rem; color: var(--ed-text-meta);
}
/* These underlined on hover but kept their meta grey, while the column links
   directly above them go to ink. One rule for both. */
.lb-legal a { color: var(--ed-text-meta); text-decoration: none; text-underline-offset: 3px; text-decoration-thickness: 1px; transition: color var(--ed-dur-fast) var(--ed-ease); }
.lb-legal a:hover { text-decoration: underline; color: var(--ed-text-title); }
.lb-legal-right { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.lb-locales { display: flex; gap: 10px; font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label); letter-spacing: var(--ed-track-label); }
.lb-locales a { min-width: 32px; min-height: 32px; display: inline-flex; align-items: center; justify-content: center; }
.lb-locales a.is-on { color: var(--ed-text-title); }
.lb-legal-links { display: flex; gap: 16px; flex-wrap: wrap; }
@media (max-width: 900px) {
  .lb-footer-main { grid-template-columns: minmax(0, 1fr); gap: 46px; }
  .lb-footer-newsletter { max-width: 560px; }
}

/* ── Product page ────────────────────────────────────────────── */
.lb-gallery-main { aspect-ratio: var(--ed-ratio-editorial); }
/* Columns follow the actual image count so the strip stays flush instead of
   wrapping into a ragged second row. */
.lb-thumbs { display: grid; grid-template-columns: repeat(var(--n, 4), 1fr); gap: var(--ed-grid-gap-tight); margin-top: var(--ed-grid-gap-tight); }
.lb-thumb { aspect-ratio: 1 / 1; border: none; padding: 0; cursor: pointer; background: var(--ed-media); position: relative; overflow: hidden; }

/* Woo's password-eye button is otherwise a 16px glyph with a 17px hitbox. */
.at-editorial .password-input .show-password-input {
  width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
}
/* --ed-dur-fast, not --ed-dur: LabelProductScreen.jsx sets 0.2s on this
   control and the strip felt sluggish at 0.4s. */
.lb-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; transition: opacity var(--ed-dur-fast) var(--ed-ease); }
.lb-thumb.is-on img { opacity: 1; }
.lb-thumb:not(.is-on) img { opacity: .55; }
/* An unselected thumb gave no sign it was clickable. It lifts partway toward
   the selected 1, so hover and selected stay tellable apart. */
.lb-thumb:not(.is-on):hover img, .lb-thumb:not(.is-on):focus-visible img { opacity: .8; }
.lb-buy { display: flex; flex-direction: column; gap: 26px; }
.lb-buy-price { font-family: var(--ed-font-body); font-size: 1.05rem; color: var(--ed-text-body); }
.af-sticky { position: sticky; top: 96px; }

/* ── Cart bar (the .lb-bag* class names are unchanged on purpose) ── */
.lb-bagbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  background: var(--ed-text-title); color: var(--at-canvas);
  transform: translateY(100%); transition: transform .3s var(--ed-ease);
}
.lb-bagbar.is-on { transform: translateY(0); }
.lb-bagbar-inner {
  max-width: var(--ed-max); margin: 0 auto; padding: 16px var(--ed-gutter);
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  font-family: var(--ed-font-label); font-weight: 600; font-size: var(--ed-size-label);
  text-transform: uppercase; letter-spacing: var(--ed-track-label);
}
.lb-bagbar button { background: none; border: none; color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; opacity: .7; padding: 4px; transition: opacity var(--ed-dur-fast) var(--ed-ease); }
.lb-bagbar button:hover, .lb-bagbar button:focus-visible { opacity: 1; }

/* ── Journal ─────────────────────────────────────────────────── */
.lb-post { display: block; text-decoration: none; color: inherit; }
.lb-post .lb-media { aspect-ratio: 1 / 1; }
/* The lead is the exception, and only because it spans the whole row: a square
   at the full measure is a 1440px-tall photograph, which is a page rather than
   a card. Every card that sits in a column is square. */
.lb-post.lb-post--lead .lb-media { aspect-ratio: var(--ed-ratio-wide); }
.lb-post-info { padding-top: 16px; display: flex; flex-direction: column; gap: 9px; }
.lb-post h3 { max-width: 30ch; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.lb-post:hover h3 { text-decoration: underline; }
/* The Journal grid, as the design system's JournalGrid has it: `.af-grid` with
   a row gap of its own, and the lead post spanning the whole row.

   The full-row lead is what retires the template's 3/7/11 arithmetic — a lead
   that takes the entire row cannot leave a hole beside itself, so there is
   nothing left for the rule to protect against. It interacts with HANDOFF
   deviation 4, which sized the grid to three posts for exactly that reason. */
.lb-journal { row-gap: clamp(36px, 4vw, 72px); }
.lb-journal .lb-post--lead { grid-column: 1 / -1; }

/* ── Prototype-only note ribbon ──────────────────────────────── */
.lb-note {
  font-family: var(--ed-font-body); font-size: 0.875rem; line-height: 1.6;
  color: var(--ed-text-meta); border-left: 2px solid var(--ed-accent-soft);
  padding-left: 12px; max-width: 560px; margin-top: 28px;
}

/* ── Square-source crop bias ─────────────────────────────────
   Much of the real catalogue is shot square with the object sitting low in
   the frame. A centred 3:4 cover crop clips the subject and keeps the dead
   white above it, so square sources bias downward. Deviation logged in
   HANDOFF.md — the ratio itself stays 3:4 as the system specifies. */
/* This is a POSITIVE list, and deliberately so. The bias belongs to frames
   that are TALLER than a square source, where the crop is vertical and the
   subject sits low. On a frame that is WIDER than the source — the 16:9
   collaborator wells, the journal's lead post, the shop's page-head frame —
   the crop is a letterbox that already keeps only 56% of the height, and
   pushing it to 62% took the subject's head off. The rule used to be a bare
   `.lb-media img.is-square`, so it reached every well on the site.

   Listing the frames that opt in means a wide frame added later centres by
   default and can only be wrong on purpose; the reverse list would have made
   every new frame wrong by inheritance. */
.lb-tile .lb-media img.is-square,
.lb-figure--editorial .lb-media img.is-square,
.lb-figure--product .lb-media img.is-square,
.lb-post:not(.lb-post--lead) .lb-media img.is-square,
.lb-split .af-split-media img.is-square,
.lb-reel-frame .lb-media img.is-square,
.lb-collab-card > .lb-media img.is-square,
.lb-gallery-main img.is-square { object-position: center 62%; }
/* The split and the lookbook left this list on 2026-08-28 and came back on
   2026-08-31, and the list's own rule is what moved them both ways. They were
   4:5 — taller than a square source, so the bias was right. The height caps
   made them 756x420 and 330x245, both WIDER than a square, where the crop is a
   letterbox and biasing it downward takes the subject's head off. At 1:1 they
   are never wider than a square source again: over a square the frame crops
   nothing and the bias is inert, and over a portrait — or a split stretched
   past a square by the prose beside it — the crop is vertical and the bias is
   right. Not hypothetical: the Contact split and two homepage lookbook frames
   are square sources today. */

/* ── CollaboratorStrip ────────────────────────────────────────
   The system's own component uses `--ed-ratio-wide`, which is right for the
   detail photography it was drawn against and wrong for this catalogue: these
   are full-length figures and square studio shots, and a 16:9 window over one
   keeps 56% of its height and cuts the head off. It was 4:5 here from the
   start and 1:1 since 2026-08-31, when the square became the site's one
   decorative shape — over a square studio shot a square frame crops nothing
   whatsoever, which is the same argument that put 4:5 here, finished. The
   ratio is still set inline in `render/collaborators.php`, because that is a
   renderer rather than a block and `.lb-media` carries no ratio of its own.

   `.lb-collab-grid { max-width: 960px }` lived here until 2026-09-02 and is
   gone with the grid it capped. Its job was to stop an uncapped `--cols` grid
   giving two credited makers a 683px-tall frame each; the reel has one card
   width for any number of cards, so there is no count-dependent blow-up left
   to cap. `.lb-reel--narrow` is what sets that width now. */

/* ── Hero scrim ───────────────────────────────────────────────
   The opener's photography is shot on light backdrops, and against the
   canvas beside it the well read as the lit half of the page. This darkens
   it. Deliberately NOT named `.lb-scrim`: that class already belongs to
   CollectionHero's scrim element above, and the last name collided this way
   (see HANDOFF.md on `.lb-hero`).

   It is a tint rather than a scrim in the design system's sense — nothing is
   reversed out over the opener, so it is doing tonal work, not legibility
   work.

   Two layers, and the order matters. The system's own `--ed-scrim` sits on
   top, so the slider's white marks and arrows keep exactly the wash the
   system already specifies for controls over a photograph; the even tint
   underneath takes the brightness down across the whole frame, which
   `--ed-scrim` alone does not do (it is transparent above 70%). Reusing the
   token rather than deriving a second gradient means a change to it still
   reaches here.

   `--afl-tint` is the one number to turn if this wants to be lighter or
   heavier. */
.lb-media-scrim { --afl-tint: 0.28; }
.lb-media-scrim::after {
  content: ""; position: absolute; inset: 0; z-index: 3;
  /* The well still has to receive hover, focus and swipe. */
  pointer-events: none;
  background-image:
    var(--ed-scrim),
    linear-gradient(rgba(23, 21, 15, var(--afl-tint)), rgba(23, 21, 15, var(--afl-tint)));
}
/* Slides top out at z-index 2, so the tint covers them; the controls have to
   clear it in turn. */
.lb-media-scrim .lb-slider-nav { z-index: 4; }

/* ── MediaSlider ──────────────────────────────────────────────
   Several images in one media well, used by the homepage opener.

   NOT called .lb-hero: that name is already the design system's
   CollectionHero above, whose `min-height: min(420px, 62vh)` and
   `display: flex` were being applied to the split's media well and changing
   its box. This is its own component and leaves CollectionHero alone.

   The system has no carousel at all, so this is built to its rules rather
   than borrowed: no elevation, no radius, no movement — a crossfade only —
   and controls in the same tracked micro-type as everything else. The marks
   are the MakerTag's 5px square, reused rather than reinvented. */
.lb-slider {
  /* The slider's own motion, deliberately slower than the interface's.
     `--ed-dur` (0.4s) is the right length for a hover — a control answering a
     pointer — and the wrong one for a photograph replacing a photograph, which
     reads as a cut rather than a change. Kept as local custom properties so the
     value lives beside the rule that spends it; the script's FADE must match.
     The ease is symmetrical (in-out) rather than `--ed-ease`'s ease-out: an
     ease-out crossfade dumps most of its opacity in the first third, which is
     precisely the flicker. */
  --afl-slide-fade: 1200ms;
  --afl-slide-ease: cubic-bezier(0.37, 0, 0.24, 1);
}
.lb-slider .lb-slider-slide {
  opacity: 0;
  z-index: 0;
  transition: opacity var(--afl-slide-fade) var(--afl-slide-ease);
  /* layout.css fades every .af-media img in on decode with `both` fill, which
     would otherwise hold every slide at opacity 1 and stack them all.

     The `.lb-slider` ancestor is load-bearing: `.af-media img` is (0,1,1), so a
     bare `.lb-slider-slide` at (0,1,0) loses to it and the animation's fill
     takes the opacity back off this rule entirely. */
  animation: none;
}
.lb-slider .lb-slider-slide.is-on { opacity: 1; z-index: 2; }

/* The dissolve, and the reason it stopped flickering.

   Fading two slides against each other at once is not a crossfade: at the
   midpoint both sit near 0.5 and the well's `--ed-media` ground shows straight
   through the pair, so every change carried a grey pulse. The outgoing slide is
   therefore *held* at full opacity underneath the incoming one for the length
   of the fade — the new photograph resolves over a solid old one and the ground
   is never visible. The class is dropped once the fade is over; the slide fades
   out from beneath an opaque neighbour, where it cannot be seen. */
.lb-slider .lb-slider-slide.is-prev { opacity: 1; z-index: 1; transition: none; }

.lb-slider-nav {
  position: absolute; z-index: 2; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; gap: 14px;
  padding: clamp(16px, 2vw, 28px);
}
.lb-slider-dots { display: flex; gap: 9px; margin-right: auto; }
.lb-slider-dot {
  width: 24px; height: 24px; padding: 9px; border: 0; cursor: pointer;
  background-clip: content-box;
  background: rgba(255, 255, 255, .55);
  /* The mark travels with the photograph rather than ahead of it: at
     `--ed-dur-fast` it snapped white a full second before the image behind it
     had finished changing, which read as two separate events. */
  transition: background var(--afl-slide-fade) var(--afl-slide-ease);
}
.lb-slider-dot.is-on { background: var(--at-white, #fff); }
.lb-slider-dot:hover { background: var(--ed-accent-soft); }

/* Arrows stay out of the photograph until asked for. */
.lb-slider-arrow {
  width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: none; padding: 0; cursor: pointer;
  font-family: var(--ed-font-label); font-size: 1.15rem; line-height: 1;
  color: var(--at-white, #fff); opacity: 0;
  transition: opacity var(--ed-dur-fast) var(--ed-ease);
}
.lb-slider:hover .lb-slider-arrow,
.lb-slider:focus-within .lb-slider-arrow { opacity: .8; }
.lb-slider-arrow:hover { opacity: 1; }
/* Never hide a control from the keyboard. */
.lb-slider-arrow:focus-visible { opacity: 1; outline: 1px solid var(--at-white, #fff); outline-offset: 3px; }
.lb-slider-dot:focus-visible { outline: 1px solid var(--at-white, #fff); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .lb-slider .lb-slider-slide,
  .lb-slider-dot { transition: none; }
}

/* ── Footer accordion: restore layout.css's mobile collapse ──
   layout.css sets `.af-fcol-body { display: none }` under 760px, but this
   file loads after it and sets `display: grid` at equal specificity, which
   silently defeats the collapse. Re-assert it here. Anything porting these
   component styles into the child theme must keep this pairing. */
@media (max-width: 760px) {
  .af-fcol-body { display: none; }
  .af-fcol-body.is-open { display: grid; }
}


/* ── Page head with media (shop) ──────────────────────────────
   The text-only head left the right half empty at desktop widths. The frame
   is desktop-only: on mobile it would just push the grid below the fold. */
.lb-pagehead-split {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(32px, 5vw, 80px); align-items: end;
}
.lb-pagehead-split > .lb-pagehead { padding-bottom: 0; }
/* Landscape, and one of the four frames 2026-08-31 deliberately left out of
   the square standard — see the decorative-media note near the end of this
   file. Its partner column is a two-line head, and a square here is 660px
   against 350px of type: it opens exactly the band of empty canvas above the
   eyebrow that `--compact` below exists to close. The compact variant already
   does what the square standard is for — the frame takes the head's height,
   flush top and bottom — by matching the column rather than naming a shape. */
.lb-pagehead-media { aspect-ratio: var(--ed-ratio-wide); }

/* Compact variant — category and tag archives.
   The collection head is bottom-aligned so the lead's last line settles onto
   the frame's bottom edge, which only reads when the text column is as tall as
   the frame. A category headline is one word, so the frame is let go of its
   own ratio and matched to the head instead. The floor keeps a one-word title
   with a short lead from squeezing the photograph into a letterbox. */
.lb-pagehead-split--compact { align-items: stretch; }
.lb-pagehead-split--compact > .lb-pagehead { padding-bottom: 0; align-self: end; }
.lb-pagehead-split--compact .lb-pagehead-media {
  aspect-ratio: auto;
  height: 100%;
  min-height: clamp(200px, 18vw, 280px);
}
@media (max-width: 900px) {
  .lb-pagehead-split { grid-template-columns: minmax(0, 1fr); }
  .lb-pagehead-media { display: none; }
}

/* ── The one oxblood band ─────────────────────────────────────
   `--ed-ground` is specified as the single oxblood CTA band a page may carry.
   It was unused, which is a large part of why the pages read as all-neutral. */
.lb-band {
  background: var(--ed-ground); color: var(--at-white);
  padding-block: var(--ed-section-y-tight);
}
.lb-band .lb-label { color: var(--ed-accent-soft); letter-spacing: var(--ed-track-label-wide); }
.lb-band h2 { color: var(--at-white); max-width: 16ch; }
.lb-band p { color: rgba(255,255,255,.78); margin: 0; }
.lb-band .lb-band-inner {
  max-width: var(--ed-max); margin: 0 auto; padding-inline: var(--ed-gutter);
  display: grid; grid-template-columns: 1.1fr 1fr;
  gap: clamp(28px, 5vw, 80px); align-items: end;
}
.lb-band .lb-btn--onImage { background: var(--at-white); border-color: var(--at-white); color: var(--ed-text-title); }
.lb-band .lb-btn--onImage:hover { background: var(--ed-accent-soft); border-color: var(--ed-accent-soft); color: var(--ed-text-title); }
@media (max-width: 900px) {
  .lb-band .lb-band-inner { grid-template-columns: minmax(0, 1fr); align-items: start; gap: 24px; }
}
/* The band's two arrangements. `.lb-band-inner` above is what the homepage
   renders today — headline left, lead and button right. `.lb-band-stack` is the
   design system's CtaBand: one column, left or centred. The design has no
   split and the homepage has no stack, so both stand until phase 8 seeds the
   homepage and picks one. */
.lb-band-stack { display: flex; flex-direction: column; gap: 22px; align-items: flex-start; }
.lb-band-stack--center { align-items: center; text-align: center; }
.lb-band-stack h2 { max-width: 700px; }
.lb-band .lb-band-aside { display: flex; flex-direction: column; gap: 22px; align-items: flex-start; }

/* `tone="alt"` — the neutral band, for a page that has already spent its one
   oxblood. Every colour the oxblood ground forces has to be handed back. */
.lb-band--alt { background: var(--ed-surface-alt); color: var(--ed-text-body); border-top: 1px solid var(--ed-line); }
.lb-band--alt .lb-label { color: var(--ed-accent); }
.lb-band--alt h2 { color: var(--ed-text-title); }
.lb-band--alt p { color: var(--ed-text-body); }

/* ═══════════════════════════════════════════════════════════════
   WOOCOMMERCE + ASTRA INTEGRATION
   Everything below exists only because Woo, Astra and Dokan ship their
   own opinions. Scoped to .at-editorial so it can never reach a page
   that hasn't opted in. The theme already inherits the parent's
   `border-radius: 0 !important` convention on Woo controls — kept,
   because the label layer is square too.
   ══════════════════════════════════════════════════════════════ */

/* ── Astra's global button hover ──────────────────────────────
   Astra prints one rule inline in the head covering `button:hover`,
   `button:focus` and a long tail of Woo and block-editor buttons, and paints
   them all `color:#fff; background-color:#3e1e1e; border-color:#3e1e1e` — the
   marketplace era's button hover, from the Customizer.

   Every label control that is a bare <button> was inheriting it. Hovering a
   product panel header filled the whole row with oxblood; the same happened to
   a size, a gallery thumb, the announcement dismiss, the newsletter submit,
   the slider controls and the footer's mobile toggles. None of these are
   buttons in the label layer's sense — they are labels, marks and photographs
   that happen to be focusable — and the editorial layer has no filled hover at
   all outside `.lb-btn`, which sets its own.

   So the ground is held and each component's own rule, above, is left as the
   only thing that speaks. `:focus` is included because Astra's selector list
   carries it too and it painted the same block on keyboard focus;
   `:focus-visible` is where the keyboard affordance belongs, and it is
   restored below.

   Specificity, not !important: Astra's `button:hover` scores (0,1,1) and
   `.at-editorial .lb-acc-btn:hover` scores (0,2,1). */
.at-editorial .lb-acc-btn:hover,      .at-editorial .lb-acc-btn:focus,
.at-editorial .af-fcol-toggle:hover,  .at-editorial .af-fcol-toggle:focus,
.at-editorial .lb-size:hover,         .at-editorial .lb-size:focus,
.at-editorial .lb-thumb:hover,        .at-editorial .lb-thumb:focus,
.at-editorial .lb-announce button:hover, .at-editorial .lb-announce button:focus,
.at-editorial .lb-news-form button:hover, .at-editorial .lb-news-form button:focus,
.at-editorial .lb-slider-arrow:hover, .at-editorial .lb-slider-arrow:focus,
.at-editorial .lb-slider-dot:hover,   .at-editorial .lb-slider-dot:focus {
  background-color: transparent;
  border-color: transparent;
  color: inherit;
  box-shadow: none;
}
/* Each of these carries a ground or a border of its own that `transparent`
   above would otherwise strip. Restated at the same weight, after it. */
.at-editorial .lb-thumb:hover, .at-editorial .lb-thumb:focus { background-color: var(--ed-media); }
.at-editorial .lb-size:hover, .at-editorial .lb-size:focus { color: var(--ed-text-title); border-color: var(--ed-line-strong); }
.at-editorial .lb-size:not([disabled]):not(.is-on):hover { border-color: var(--ed-text-title); }
.at-editorial .lb-size.is-on:hover, .at-editorial .lb-size.is-on:focus { background-color: var(--ed-text-title); border-color: var(--ed-text-title); color: var(--at-canvas); }
.at-editorial .lb-size[disabled]:hover { color: var(--ed-text-meta); }
.at-editorial .lb-acc-btn:hover, .at-editorial .lb-acc-btn:focus { color: var(--ed-text-title); }
.at-editorial .af-fcol-toggle:hover, .at-editorial .af-fcol-toggle:focus { color: var(--ed-text-title); }
.at-editorial .lb-news-form button:hover { color: var(--ed-accent); }
.at-editorial .lb-slider-arrow:hover, .at-editorial .lb-slider-arrow:focus,
.at-editorial .lb-slider-dot:hover, .at-editorial .lb-slider-dot:focus { color: var(--at-white, #fff); }
.at-editorial .lb-slider-dot:hover { background-color: var(--ed-accent-soft); }
.at-editorial .lb-slider-dot.is-on { background-color: var(--at-white, #fff); }

/* Keyboard affordance. Astra's rule doubled as the only visible focus state on
   these controls; with it held back they need a real one. A 1px ink outline at
   2px offset — the label layer has no radius and no shadow, so an outline is
   the whole vocabulary available. Only :focus-visible, so a mouse click does
   not leave a ring behind. */
.at-editorial .lb-acc-btn:focus-visible,
.at-editorial .af-fcol-toggle:focus-visible,
.at-editorial .lb-size:focus-visible,
.at-editorial .lb-thumb:focus-visible,
.at-editorial .lb-filter:focus-visible,
.at-editorial .lb-announce button:focus-visible,
.at-editorial .lb-news-form button:focus-visible,
.at-editorial .lb-btn:focus-visible,
.at-editorial .lb-burger:focus-visible,
.at-editorial .lb-drawer-link:focus-visible,
.at-editorial .lb-drawer-more:focus-visible,
.at-editorial .lb-drawer-sublink:focus-visible,
.at-editorial .lb-drawer-util:focus-visible,
.at-editorial .lb-drawer-locales a:focus-visible,
.at-editorial .lb-navlink:focus-visible,
.at-editorial .lb-subnav-link:focus-visible,
.at-editorial .lb-utils a:focus-visible,
.at-editorial .lb-tile:focus-visible,
.at-editorial .lb-post:focus-visible {
  outline: 1px solid var(--ed-text-title);
  outline-offset: 2px;
}

/* Stripe mounts an empty disabled express-checkout frame until a variable
   product has a size. Do not present that grey strip as a control. Woo removes
   the disabled class after a valid variation is chosen, which reveals the
   wallet button again without JavaScript of our own. */
.at-editorial.single-product .woocommerce-variation-add-to-cart-disabled .wc-stripe-product-checkout-container {
  display: none !important;
}

/* ── Product loop ─────────────────────────────────────────────
   A label tile is a photograph plus a caption. No card, no border,
   no shadow, no in-grid buying. */
.at-editorial.woocommerce ul.products,
.at-editorial .woocommerce ul.products {
  display: grid;
  grid-template-columns: repeat(var(--cols, 4), minmax(0, 1fr));
  gap: var(--ed-grid-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.at-editorial ul.products::before,
.at-editorial ul.products::after { content: none !important; }

/* The collection grid steps 4 → 3 → 2 and STOPS. `ul.products` is not an
   `.af-grid`, so it was falling back to Astra's own Woo breakpoints: still 4
   across at 1100 where the prototype is already at 3, and never stepping down
   at all below that.

   It used to follow `.af-grid`'s 4 → 3 → 2 → 1 exactly, and the single-column
   step is dropped on the user's decision of 2026-09-03: a phone should show two
   pieces side by side, not one. It is the right call for a catalogue — one tile
   per screen makes a collection of twelve read as a scroll with no end, and the
   pair is what lets a shopper compare two colourways without scrolling. At
   375px that is two tiles in a 335px measure, ~157px each; the tile is a
   photograph, a name and a price, all of which hold at that width.

   Deliberately not applied to `.af-grid`: the journal grid's tiles carry an
   excerpt, and two columns of excerpt on a phone is where the measure gives
   out.

   `!important` is needed, not lazy. Astra prints its Woo grid CSS inline in
   the document head — after this stylesheet — at
   `.ast-container .woocommerce ul.products:not(.elementor-grid)`, which is one
   class more specific than anything scoped to `.at-editorial`. It holds the
   grid at 3 columns from 921px all the way down, so neither step fired.
   Raising specificity cannot win against a rule that is both later and
   heavier; this is the one lever left. */
@media (max-width: 1100px) {
  .at-editorial.woocommerce ul.products,
  .at-editorial .woocommerce ul.products { grid-template-columns: repeat(min(var(--cols, 4), 3), minmax(0, 1fr)) !important; }
}
@media (max-width: 860px) {
  .at-editorial.woocommerce ul.products,
  .at-editorial .woocommerce ul.products { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
}

.at-editorial ul.products li.product {
  width: auto !important;
  margin: 0 !important;
  float: none !important;
  clear: none !important;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  text-align: left;
}
.at-editorial ul.products li.product::before,
.at-editorial ul.products li.product::after { content: none !important; }

/* Woo/Astra loop furniture the label grid does not use. */
.at-editorial ul.products li.product .button,
.at-editorial ul.products li.product .added_to_cart,
.at-editorial ul.products li.product .ast-shop-product-out-of-stock,
.at-editorial ul.products li.product .star-rating,
.at-editorial ul.products li.product .ast-woo-product-category { display: none !important; }

/* The price stays. It was hidden here originally on the assumption the tile
   would print its own, which it never did — leaving the grid priceless. It is
   Woo's own markup, restyled to the tile's meta role: body face, quiet weight,
   sitting under the title rather than shouting next to it. */
.at-editorial ul.products li.product .astra-shop-summary-wrap > .price {
  display: block;
  font-family: var(--ed-font-body);
  font-size: var(--ed-size-meta);
  font-weight: 400;
  color: var(--ed-text-body);
  margin: 0;
}
/* A sale price prints <del> then <ins>; the struck original stays subordinate. */
.at-editorial ul.products li.product .price del {
  opacity: .5;
  margin-right: 8px;
  text-decoration-thickness: 1px;
}
.at-editorial ul.products li.product .price ins {
  text-decoration: none;
  color: var(--ed-text-title);
}

.at-editorial ul.products li.product .woocommerce-loop-product__title {
  font-family: var(--ed-font-title) !important;
  font-weight: var(--ed-weight-title) !important;
  font-size: 1.02rem !important;
  line-height: 1.3;
  letter-spacing: var(--ed-track-title);
  text-transform: none !important;
  color: var(--ed-text-title);
  padding: 0;
  margin: 0;
}

/* Astra wraps loop content in .astra-shop-summary-wrap; flatten it. */
.at-editorial ul.products li.product .astra-shop-summary-wrap {
  padding: 14px 0 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
  align-items: flex-start;
}

/* ── Woo controls ─────────────────────────────────────────────
   Ordering dropdown, result count and pagination reduced to label text.
   The journal archive reuses the pagination rules via .lb-pagination —
   it is the same control in the same role, so it is one rule set, not two. */
.at-editorial .woocommerce-ordering select,
.at-editorial .woocommerce-result-count {
  font-family: var(--ed-font-label);
  font-weight: 600;
  font-size: var(--ed-size-label);
  text-transform: uppercase;
  letter-spacing: var(--ed-track-label);
  color: var(--ed-text-meta);
  border: 0;
  background: none;
  border-radius: 0 !important;
  padding: 0;
}
/* The Journal's pagination is chrome printed by home.php after the page's own
   block content, so it sits outside the section that holds the grid and needs
   its own foot of air. The gap above it is that section's bottom padding. */
.lb-pagination-wrap { padding-bottom: var(--ed-section-y-tight); }
.at-editorial .woocommerce-pagination ul.page-numbers,
.at-editorial .lb-pagination ul.page-numbers {
  border: 0;
  display: flex;
  gap: 6px;
}
.at-editorial .woocommerce-pagination ul.page-numbers li,
.at-editorial .lb-pagination ul.page-numbers li { border: 0 !important; }
.at-editorial .woocommerce-pagination .page-numbers,
.at-editorial .lb-pagination .page-numbers {
  font-family: var(--ed-font-label);
  font-weight: 600;
  font-size: var(--ed-size-label);
  letter-spacing: var(--ed-track-label);
  border-radius: 0 !important;
  background: none !important;
  color: var(--ed-text-meta);
  min-width: 34px;
  padding: 8px 4px;
  text-align: center;
}
.at-editorial .woocommerce-pagination .page-numbers.current,
.at-editorial .woocommerce-pagination .page-numbers:hover,
.at-editorial .lb-pagination .page-numbers.current,
.at-editorial .lb-pagination .page-numbers:hover { color: var(--ed-text-title) !important; }

/* ── Single product ───────────────────────────────────────────
   Woo's own layout is replaced by .af-two in the template override;
   these rules only neutralise what Woo and Astra add on top. */
.at-editorial.single-product div.product .woocommerce-product-gallery {
  margin: 0;
  /* Woo hard-codes `.images { width: 48% }` and Astra layers its own column
     width on top. Both are meant for Woo's side-by-side layout, which the
     .af-two split has already replaced — left in place they shrank the
     gallery to half of an already-halved column. */
  width: 100% !important;
  float: none !important;
  max-width: none;
}
.at-editorial.single-product div.product .woocommerce-product-gallery__wrapper { margin: 0; }

/* Same story on the other half: Woo hard-codes `div.summary { width: 48%;
   float: right }` for its own side-by-side layout. Inside the .af-two cell
   that already IS the right-hand column, it shrank the buy column to just
   over a third of its cell and pushed it against the gutter — which is why
   the product title wrapped to three lines. */
.at-editorial.single-product div.product div.summary {
  width: 100% !important;
  float: none !important;
  margin: 0;
  max-width: none;
}

/* The product title is a page headline, not a hero. At hero size it wrapped
   to three lines inside the buy column and dwarfed the piece it names. */
.at-editorial.single-product div.product .product_title {
  font-size: var(--ed-size-h2);
  /* h1 keeps hero leading even at h2 size — the tighter setting is what makes
     a two-line product name read as one block rather than two lines. */
  line-height: var(--ed-leading-hero);
  margin: 0 0 18px;
}
.at-editorial.single-product div.product .woocommerce-product-gallery__image { border-radius: 0; }
.at-editorial.single-product div.product .woocommerce-product-gallery__trigger { display: none; }
.at-editorial.single-product div.product p.price,
.at-editorial.single-product div.product span.price {
  font-family: var(--ed-font-body);
  font-size: 1.05rem;
  font-weight: 400;
  color: var(--ed-text-body);
}
.at-editorial.single-product div.product .single_add_to_cart_button {
  border-radius: 0 !important;
  background: var(--ed-accent) !important;
  color: var(--at-white) !important;
  font-family: var(--ed-font-label);
  font-weight: 600;
  font-size: var(--ed-size-label-lg);
  text-transform: uppercase;
  letter-spacing: var(--ed-track-label);
  padding: var(--ed-control-pad);
  width: 100%;
  border: 1px solid var(--ed-accent) !important;
  transition: background var(--ed-dur-fast) var(--ed-ease);
}
/* Woo's own `.woocommerce-js div.product form.cart .button.single_add_to_cart_button`
   scores (0,5,2) — one class and one element more than the rule above — so it
   kept its 10px/20px padding and let the button shrink to its text. Matching
   its shape and adding one class takes it on specificity rather than with
   !important. The float is Woo's too, for a layout the .af-two split replaced. */
.at-editorial.single-product div.product form.cart .button.single_add_to_cart_button {
  width: 100%;
  float: none;
  margin: 0;
  padding: var(--ed-control-pad);
}
.at-editorial.single-product div.product .single_add_to_cart_button:hover {
  background: var(--ed-ground-deep) !important;
  border-color: var(--ed-ground-deep) !important;
  transform: none !important;
}
.at-editorial.single-product div.product form.cart .quantity.buttons_added {
  display: inline-grid;
  grid-template-columns: 44px 58px 44px;
  align-items: stretch;
  width: auto;
  float: none;
  margin: 0 0 14px;
  border: 1px solid var(--ed-line-strong);
  background: var(--ed-surface);
}
.at-editorial.single-product div.product form.cart .quantity.buttons_added .minus,
.at-editorial.single-product div.product form.cart .quantity.buttons_added .plus {
  position: static;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--ed-text-title);
  font-family: var(--ed-font-body);
  font-size: 1rem;
  text-decoration: none;
}
.at-editorial.single-product div.product form.cart .quantity.buttons_added .minus:hover,
.at-editorial.single-product div.product form.cart .quantity.buttons_added .plus:hover,
.at-editorial.single-product div.product form.cart .quantity.buttons_added .minus:focus-visible,
.at-editorial.single-product div.product form.cart .quantity.buttons_added .plus:focus-visible {
  background: var(--ed-surface-alt);
}
.at-editorial.single-product div.product form.cart .quantity.buttons_added input.qty {
  width: 100% !important;
  max-width: none;
  min-width: 0;
  height: 44px;
  min-height: 44px;
  margin: 0;
  padding: 0 4px;
  border: 0;
  border-inline: 1px solid var(--ed-line-strong);
  border-radius: 0;
  background: transparent;
  color: var(--ed-text-title);
  font-family: var(--ed-font-body);
  font-size: .95rem;
  text-align: center;
  appearance: textfield;
}
.at-editorial.single-product div.product form.cart .quantity.buttons_added input.qty::-webkit-inner-spin-button,
.at-editorial.single-product div.product form.cart .quantity.buttons_added input.qty::-webkit-outer-spin-button {
  margin: 0;
  appearance: none;
}

/* Attributes table → hairline spec rows. */
.at-editorial .woocommerce-product-attributes,
.at-editorial table.shop_attributes {
  border: 0 !important;
  border-top: 1px solid var(--ed-line) !important;
  width: 100%;
}
.at-editorial table.shop_attributes th,
.at-editorial table.shop_attributes td {
  border: 0 !important;
  border-bottom: 1px solid var(--ed-line) !important;
  padding: 14px 0 !important;
  background: none !important;
  font-style: normal !important;
}
.at-editorial table.shop_attributes th {
  font-family: var(--ed-font-label);
  font-weight: 600;
  font-size: var(--ed-size-label);
  text-transform: uppercase;
  letter-spacing: var(--ed-track-label);
  color: var(--ed-text-meta);
  width: 130px;
}
.at-editorial table.shop_attributes td {
  font-family: var(--ed-font-body);
  font-size: var(--ed-size-meta);
  line-height: 1.65;
  color: var(--ed-text-body);
}

/* ── Variation swatches ───────────────────────────────────────
   Sold-out variants must stay visible and struck through, never hidden.

   `.afl-swatches` is added by script once the button row is driving the
   select, so the native control is only hidden where something has replaced
   it — with JavaScript off the table is the visible control and the buttons
   never appear. */
.at-editorial form.cart.afl-swatches table.variations,
.at-editorial form.cart.afl-swatches .reset_variations_alert { display: none; }
.at-editorial .lb-sizes-wrap { margin-bottom: 26px; }
.at-editorial .variations { border: 0; margin: 0; }
.at-editorial .variations td, .at-editorial .variations th { padding: 0 0 10px; border: 0; }
.at-editorial .variations select { border-radius: 0 !important; }

/* ── Type floor: what the tokens cannot reach ─────────────────
   Cart and checkout are Woo's and the artisan plugin's markup, so the --ed-*
   scale never touches them and two roles were still setting under 14px:

   - `small.includes_tax` takes the UA's 0.8em, which put the "(includes €X
     Tax)" line and the two price spans nested inside it at 12.8px. Pinning the
     `small` alone lifts all three, since they only inherit.
   - The plugin's multi-vendor shipping note sets 0.85rem (13.6px).

   Scoped to .at-editorial so the marketplace theme keeps its own sizes, and
   sized in rem so they move with the scale rather than pinning to px. */
.at-editorial small.includes_tax,
.at-editorial .afrotunda-maker-shipping-note {
  font-size: var(--ed-size-meta);
}

/* ── Forms ────────────────────────────────────────────────────
   Underline only; focus darkens the underline. That is the whole system. */
.at-editorial input[type="text"], .at-editorial input[type="email"],
.at-editorial input[type="search"], .at-editorial input[type="tel"],
.at-editorial textarea {
  border: 0;
  border-bottom: 1px solid var(--ed-line-strong);
  border-radius: 0 !important;
  background: none;
  font-family: var(--ed-font-body);
  font-size: 0.95rem;
  color: var(--ed-text-title);
  padding: 8px 0;
  box-shadow: none;
}
.at-editorial input[type="text"]:focus, .at-editorial input[type="email"]:focus,
.at-editorial input[type="search"]:focus, .at-editorial textarea:focus {
  border-bottom-color: var(--ed-text-title);
  outline: 0;
  box-shadow: none;
}

/* Fields read at 0.95rem (15.2px) on the desktop, which is above the type floor
   but below the 16px iOS Safari demands: any smaller and tapping a field zooms
   the viewport and leaves it zoomed. Only the size moves — the underline-only
   treatment above is untouched. Woo's checkout and account fields are caught
   too, since they inherit this scope. */
@media (max-width: 782px) {
  .at-editorial input[type="text"], .at-editorial input[type="email"],
  .at-editorial input[type="search"], .at-editorial input[type="tel"],
  .at-editorial input[type="number"], .at-editorial input[type="password"],
  .at-editorial textarea, .at-editorial select {
    font-size: 16px;
  }
}

/* ── Astra chrome ─────────────────────────────────────────────
   The label header and footer replace Astra's, but Astra still emits
   its page wrapper. Let full-bleed sections escape the container. */
.at-editorial .site-content .ast-container,
.at-editorial #primary { max-width: none; padding: 0; margin: 0; }

/* ── Plain content pages ──────────────────────────────────────
   The legal four, the three commerce pages and every single post render
   through Astra's article markup and have no `.lb-wrap` of their own — and the
   rule directly above strips Astra's container, so that the label's own
   full-bleed sections can escape it. The consequence is that anything *not*
   composed of label blocks runs edge to edge with its text against the
   viewport.

   Elementor's container had been standing in for the measure on cart and
   checkout, which is why phase 9 is where this surfaced; on the legal pages and
   the posts nothing had ever supplied one.

   `.at-plain` is set in afl_body_class() on the content, not the template: a
   page the blocks compose already carries its own measure and must not get a
   second one. */
.at-editorial.at-plain .entry-header,
.at-editorial.at-plain .entry-content {
  max-width: var(--ed-max);
  margin-inline: auto;
  padding-inline: var(--ed-gutter);
}
.at-editorial.at-plain .entry-header { padding-block: var(--ed-section-y-tight) 0; }
.at-editorial.at-plain .entry-content { padding-bottom: var(--ed-section-y); }

/* Prose wants a reading measure. A checkout form wants the whole width, so the
   commerce pages take the wrap above and stop there.

   `margin-inline: 0` is load-bearing: Astra sets `margin-left/right: auto` on
   `.entry-content[data-ast-blocks-layout] > *`, which against a measure centres
   every paragraph in the 1440 wrap while the heading — outside that container —
   stays left. The label sets prose left inside a wide wrap, as `.lb-pagehead`
   does, so both hang together. */
.at-editorial.at-plain:not(.woocommerce-page) .entry-header > *,
.at-editorial.at-plain:not(.woocommerce-page) .entry-content > * {
  max-width: var(--ed-max-text);
  margin-inline: 0;
}

/* Astra's own rhythm does not survive `.ast-page-builder-template`, so the
   prose sets its own: one line between paragraphs, more before a heading. */
.at-editorial.at-plain .entry-content > * + * { margin-block-start: 1.15em; }
.at-editorial.at-plain .entry-content > :is(h2, h3, h4, h5, h6) { margin-block-start: 1.9em; }
.at-editorial.at-plain .entry-content > :is(ul, ol) { padding-left: 1.4em; }
.at-editorial.at-plain .entry-content > :is(ul, ol) li { margin-block: 0.35em; }
/* Scoped away from `.at-plain` below: this exists so the label's own sections
   sit flush against each other, and on a page of prose it removes every
   paragraph break. */
.at-editorial:not(.at-plain) .entry-content > * { margin-block: 0; }
.at-editorial .ast-separate-container .ast-article-single,
.at-editorial .ast-separate-container #primary {
  background: none;
  padding: 0;
  border: 0;
}
.at-editorial .site-main > .woocommerce-notices-wrapper { padding-inline: var(--ed-gutter); }


/* Product teaser — first sentence of the description, italic Lora.
   Anything human in this brand is Lora italic. */
.at-editorial .lb-teaser {
  font-family: var(--ed-font-title);
  font-style: italic;
  font-size: var(--ed-size-lead);
  line-height: var(--ed-leading-body);
  color: var(--ed-text-body);
  margin: 0;
  max-width: var(--ed-max-text);
}
/* ── Buy column head ──────────────────────────────────────────
   Eyebrow, title, price and MakerTag read as one block. `.lb-buy`'s 26px gap
   belongs BETWEEN the blocks of the column, not inside this one — and the
   title's own bottom margin has to come off, or it reopens the same gap. */
.at-editorial .lb-buy-head { display: flex; flex-direction: column; gap: 12px; }
.at-editorial.single-product div.product .lb-buy-head .product_title { margin: 0; }
/* Woo's `div.product p.price` carries a 0.2em bottom margin at (0,3,2); the
   head's own 12px gap is the spacing here, so it has to be outscored. */
.at-editorial.single-product div.product .lb-buy-head p.price { margin: 0; line-height: normal; }

/* ── Gallery ──────────────────────────────────────────────────
   The label gallery is a still 3:4 frame plus a flush thumb strip, rendered
   in PHP — not Woo's FlexSlider, which starts at opacity 0 and depends on its
   own script to ever become visible. */
.at-editorial .lb-gallery-main { aspect-ratio: var(--ed-ratio-editorial); }

/* Related products and upsells sit on the alternating band. */
.at-editorial .related.products,
.at-editorial .upsells.products {
  background: var(--ed-surface-alt);
  padding: var(--ed-section-y) var(--ed-gutter);
  margin-top: var(--ed-section-y-tight);
}
/* The related band opens with the same eyebrow + headline head as every other
   band on the site; upsells keep Woo's bare h2. Both need the measure. */
.at-editorial .related.products > .lb-head,
.at-editorial .upsells.products > h2 {
  max-width: var(--ed-max);
  margin: 0 auto clamp(16px, 2vw, 28px);
}
.at-editorial .upsells.products > h2 { font-size: var(--ed-size-h2); }
.at-editorial .related.products ul.products,
.at-editorial .upsells.products ul.products { max-width: var(--ed-max); margin-inline: auto; }
/* The head already carries the space below it; the loop's own top margin
   (set !important further down, against Astra's inline grid CSS) would
   double it. */
.at-editorial .related.products > .lb-head + ul.products { margin-top: 0 !important; }

/* ==========================================================================
   ASTRA DYNAMIC CSS OVERRIDES

   Astra prints a <style id="astra-theme-dynamic-css"> block INLINE in <head>,
   generated from the Customizer's global typography settings. Inline styles
   are parsed after every enqueued stylesheet, so at equal specificity Astra
   wins no matter where label.css sits in the cascade — this is why headlines
   arrived as tracked uppercase Lora instead of sentence-case Lora.

   The winning move is specificity, not order. Astra's selector is
   `h1, .entry-content :where(h1)` — the `:where()` contributes nothing, so
   both halves score (0,0,1) and (0,1,0). Scoping ours to `.at-editorial`
   scores (0,1,1) and takes it cleanly, while leaving the marketplace theme's
   typography untouched on any surface we opt out of.

   Sentence case is not a preference here: it is the first of the three moves
   that define the label layer. Uppercase headlines read as marketplace
   signage; the roman sentence-case Lora headline is what makes the page read
   as a house rather than a shop.
   ========================================================================== */

.at-editorial h1,
.at-editorial h2,
.at-editorial h3,
.at-editorial h4,
.at-editorial h5,
.at-editorial h6 {
  font-family: var(--ed-font-title);
  font-weight: var(--ed-weight-title);
  text-transform: none;
  letter-spacing: var(--ed-track-title);
  line-height: var(--ed-leading-title);
  color: var(--ed-text-title);
}

.at-editorial h1 { font-size: var(--ed-size-hero); line-height: var(--ed-leading-hero); }
.at-editorial h2 { font-size: var(--ed-size-h2); }
.at-editorial h3 { font-size: var(--ed-size-h3); }

/* And the same problem as the label role, for the same reason.

   Two components reverse a headline out of a dark ground — the oxblood band and
   CollectionHero over its scrim — and say so at (0,1,1). The blanket above is
   also (0,1,1) and comes later, so it has been winning: `.lb-band h2` has been
   rendering near-black on oxblood on the live homepage ever since this reset
   was added, and the hero would have done the same the first time it was used.
   Neither was noticed because the band's own rule looks correct where it is
   written, 500 lines above, and the hero had no caller until the block landed.

   Re-asserted at `.at-editorial` weight, in the same order as the originals so
   the neutral band still takes its ink back. */
.at-editorial .lb-band h2 { color: var(--at-white); }
.at-editorial .lb-band--alt h2 { color: var(--ed-text-title); }
.at-editorial .lb-hero--onImage h1 { color: var(--at-white); }

/* The label role is the ONE place uppercase survives, so it has to be
   re-asserted at the same weight after the blanket reset above. */
.at-editorial .lb-label,
.at-editorial .lb-eyebrow,
.at-editorial .lb-navlink,
.at-editorial .lb-subnav-link,
.at-editorial .lb-btn,
.at-editorial .lb-acc-head,
.at-editorial .lb-fcol-head { text-transform: uppercase; }

/* Astra's page wrapper carries its own max-width and padding even under
   `page-builder`. It is released to full width here so that full-bleed
   sections (the oxblood band, the lookbook) can actually reach the viewport
   edge — the measure is then re-imposed one level in, on the content rail
   below, rather than on the wrapper. */
.at-editorial #primary,
.at-editorial #content .ast-container,
.at-editorial .site-content > .ast-container {
  max-width: none;
  width: 100%;
  padding-left: 0;
  padding-right: 0;
  margin: 0;
  display: block;
}

/* The content rail.

   `.ast-woocommerce-container` is Astra's wrapper around everything Woo
   prints on an archive — result count, ordering select, the product grid and
   pagination. Releasing #primary above left all of it flush against the
   viewport edge, so the measure and gutter are restored here, matched to the
   pagehead above it so the grid's left edge lines up with the headline's. */
.at-editorial .ast-woocommerce-container,
.at-editorial .woocommerce-notices-wrapper {
  max-width: var(--ed-max);
  margin-inline: auto;
  padding-inline: var(--ed-gutter);
  box-sizing: border-box;
}

/* The collection head prints INSIDE `.ast-woocommerce-container`, which already
   carries the measure and the gutter above. It must not carry them again —
   doing so applied the gutter twice and pulled the collection page 121px
   narrower than the prototype at 1440. The templates that render a pagehead
   outside that wrapper put `.lb-wrap` on it instead. */
.at-editorial .ast-woocommerce-container > .lb-pagehead-split {
  max-width: none;
  margin-inline: 0;
  padding-inline: 0;
}

/* Collection page rhythm, matched to the prototype: the rail sits directly
   under the head, and the grid opens 56px below it. */
.at-editorial .ast-woocommerce-container > .lb-rail { margin-top: 0; }
.at-editorial ul.products { margin-top: clamp(32px, 4vw, 56px) !important; }
.at-editorial .lb-note { margin-top: clamp(28px, 3vw, 44px); }

/* The collection lead comes through wpautop as `.lb-body > p`, where the
   prototype writes a bare `<p class="lb-body">` — one element, not two.

   Only the top margin is zeroed. In the prototype the paragraph IS the
   .lb-body, so it takes that rule's `margin-top: 20px` and keeps its default
   bottom margin; the generated inner <p> would otherwise add its own top
   margin on top of the wrapper's 20px. The bottom margin is deliberately left
   alone: it collapses out of the wrapper and gives the pagehead the extra
   ~16px that makes it taller than the media frame, which is what decides
   which of the two the bottom-aligned split takes its height from. It is
   pinned to the browser default 1em rather than left to inherit: Astra sets
   paragraph margins to about 1.75em, which overshot the prototype by 11px. */
.at-editorial .lb-pagehead .lb-body > p { margin-top: 0; margin-bottom: 1em; }

/* Detail-panel bodies are CMS HTML — the product description arrives as its
   own <p> elements carrying Astra's font-size. The prototype's panels held
   plain text, so the panel's type is the one that should win. */
.at-editorial .lb-acc-body p,
.at-editorial .lb-acc-body li { font-size: inherit; line-height: inherit; }

.at-editorial #content { background: var(--ed-surface); }

/* ═══════════════════════════════════════════════════════════════
   THE JOURNAL REWORK — 2026-08-28
   Components from "Afrotunda Journal.dc.html" and the changes handoff
   that came with it. Everything here is a page component, reachable
   from any page; nothing is scoped to the Journal.
   ══════════════════════════════════════════════════════════════ */

/* ── Decorative media is SQUARE, and grows with its section ───
   2026-08-28 capped these frames by height — `clamp(280px, 30vw, 420px)`
   on the split, `clamp(180px, 17vw, 280px)` on a lookbook cell — because
   a 4:5 photograph at half of a 1440px row came out 900px tall. It fixed
   the height and broke the picture: a 756px column under a 420px cap is a
   1.8:1 letterbox, and `object-fit: cover` threw away well over half of
   every portrait. Worse, a cap is a fixed height, so a split whose prose
   ran to 518px left 98px of bare ground above and below the photograph —
   the one thing a full-height media column exists not to do.

   2026-08-31 replaced the caps with one shape. Every decorative frame is
   1 / 1, and the 1:1 is a FLOOR rather than a cap: the frames that share
   a row with prose sit in `align-items: stretch` containers at
   `height: auto`, so they take the section's height whenever the section
   is taller than a square. A square is also the shape that costs this
   photography the least — much of it is shot square, and a square window
   over a square source crops nothing whatsoever.

   The rules themselves are on the components now, not here, because a
   shape is what a component IS rather than a deviation from it: see
   `.lb-split .af-split-media`, `.lb-reel-frame .lb-media`,
   `.lb-post .lb-media`, `.lb-pagehead-media`, and the strip's inline
   ratio in `render/collaborators.php`. What stays here is the list of
   frames that are deliberately NOT square, each for the same reason.

   NOT square — product tiles. A tile is content, not decoration, and
   `--ed-ratio-product` is the catalogue's own shape.

   NOT square — the frames that run the full measure: `.lb-hero`,
   `.lb-figure--band` (the Journal's opener strip, which is a HEIGHT and
   not a ratio by design), and `.lb-post--lead`, which spans its whole
   grid row. Squareness belongs to a frame in a column.

   NOT square — `.lb-pagehead-media`, the archive head's frame, for the
   reason written beside it: a square opposite a two-line head opens the
   band of canvas its compact variant was built to close.

   NOT square — the bento tiles in `.lb-stories`. Their height is the
   grid row's, and they already fill it edge to edge; giving them a ratio
   is what would put holes in that grid.

   `.lb-hero` is also the one this build has ruled on twice: capping the
   4:5 CollectionHero crops it back to about 2.2:1 at the fold, which is
   the 21:9 problem the deviation was raised to escape (HANDOFF, phase 8).
   No seeded page renders it anyway — the homepage opener is a split. */
/* The caption sits ON its picture, not a hand's width below it. */
.at-editorial .lb-lookbook figcaption { margin-top: 10px; }

/* ── The rhythm stays tight ──────────────────────────────────
   Landed on 2026-08-28 beside the height caps, so that capping the
   pictures would not just re-spend the saving as whitespace. Kept when
   the caps went on 2026-08-31 and the pictures grew back: a tighter
   scale is now what pays for the squares, which is the same trade in
   the other direction. `.lb-pagehead` keeps its own deeper clamp — an
   opener is the one place the air is the point.

   THIS IS THE VALUE THE SITE USES. editorial.css declares the same two
   tokens and is never read for them; this file loads after it, and a
   later declaration on the same element wins.

   `:root`, not `.at-editorial`, and that is the whole point. It was on
   the scope class until 2026-09-04, which put it on `<body>` while
   editorial.css declared it on `<html>`. Two different elements, so
   specificity never entered into it — everything on the page simply
   resolved the token from the nearer ancestor, and reading the token
   file told you 140px when the site rendered 96px. Worse, the scope
   class is never on the block editor's canvas body, so the canvas kept
   inheriting the untightened `<html>` value and `.lb-section-tight` and
   `.lb-pagehead` drifted from the front end by 24px.

   Declared here on the same element as the value it replaces, both are
   fixed at once: one lookup path, and the canvas — which is handed all
   four sheets by afb_editor_styles(), with `:root` rewritten to
   `.editor-styles-wrapper` on the way in — now agrees with the site.

   The rule for anything added below: a token that is the SITE's value
   belongs at `:root`. A token that genuinely differs by surface belongs
   on the class that names that surface — `.afl-about` further down is
   the real example, and it still wins here because `<body>` is nearer
   to the content than `<html>`. */
:root {
  --ed-section-y: clamp(56px, 6vw, 96px);
  --ed-section-y-tight: clamp(40px, 4.5vw, 64px);
}
.at-editorial .lb-head { margin-bottom: clamp(14px, 1.75vw, 24px); }

/* ── About runs tighter still ────────────────────────────────
   About is the longest page on the site — a page head, five splits, an
   essay, a lookbook, the events panel, the journal reel and a band — and
   the normal 96px band of air between its sections was reading as a
   scroll rather than a rhythm. Its sections take the tight scale, so
   `space: normal` and `space: tight` are the same thing on this page.

   Set on `.afl-about`, which `page-about-us.php` adds to the body. The
   template applies by filename, so there is no `page-template-*` class
   to hang this on and `.page-id-4484` would be an id in a stylesheet.

   Scope of the change: `.lb-section` is the only consumer of
   `--ed-section-y` that this page renders. `.lb-pagehead` keeps its own
   deeper clamp, `.lb-band` reads `--ed-section-y-tight` and is already
   at 64, and `.lb-split` carries no vertical padding above 900px — its
   height is the 1:1 photograph's. So this moves the three section blocks
   and the essay, and nothing else. */
.afl-about {
  --ed-section-y: clamp(40px, 4.5vw, 64px);
}

/* ── Page head, compact ──────────────────────────────────────
   For a page whose opener is followed straight away by a photograph.
   `.lb-pagehead`'s clamp(88px, 11vw, 172px) is right for a page that
   opens on a headline alone and pushes the picture off the fold here. */
.lb-pagehead--compact { padding: clamp(40px, 5vw, 72px) 0 clamp(28px, 3vw, 44px); }

/* ── Section head: the right-hand line, and the rule ─────────
   `.lb-head` is already a flex row with the head on the left and the
   link on the right; the meta line takes the same baseline. Both may
   be present — a count and a link — and the row wraps as it always has. */
.lb-head-meta { white-space: nowrap; align-self: flex-end; }
.lb-head--divider { border-bottom: 1px solid var(--ed-line); padding-bottom: 18px; }

/* ── Band figure ─────────────────────────────────────────────
   The one media shape that is a height rather than a ratio: a landscape
   strip that stays the same depth however wide the page is. Set through
   the class alone — afl_media_ratio('band') deliberately returns ''
   rather than an inline aspect-ratio, so this is the only rule. */
.lb-figure--band > .lb-media { height: clamp(240px, 32vw, 420px); aspect-ratio: auto; }

/* ── Essay ───────────────────────────────────────────────────
   A narrow column that names the section against a wide column of
   prose. Not a split: no photograph, and the prose keeps its reading
   measure instead of filling its half — which is the whole point, and
   why the column widths are a choice rather than 50/50.

   The paragraph rules are `.lb-section-stack`'s, restated for this
   container: an editor typing a core paragraph cannot wrap it in
   `.lb-body`, so the measure and the body face go to the children. */
.lb-essay-inner { align-items: start; }
.lb-essay-head { display: flex; flex-direction: column; gap: 14px; }
.lb-essay-head h2 { max-width: 420px; }
.lb-essay-body { display: flex; flex-direction: column; gap: 1.1em; max-width: var(--ed-max-text); }
.lb-essay-body > p {
  font-family: var(--ed-font-body); font-size: var(--ed-size-body);
  line-height: var(--ed-leading-body); color: var(--ed-text-body); margin: 0;
}
/* The set phrase that closes the column. Italic Lora at lead size is
   the design's, and is why this is an attribute rather than one more
   paragraph — a core paragraph in this cascade cannot ask for it. */
.lb-essay-close {
  font-family: var(--ed-font-title); font-style: italic;
  font-size: var(--ed-size-lead); line-height: 1.6;
  color: var(--ed-text-title); margin: 0;
}

/* ── Visual essay — the bento ────────────────────────────────
   The cell is a SQUARE, and that is the whole arrangement: a 1x1 tile
   is a square photograph, a 2x2 is a bigger one, and a 2x1 is the only
   landscape in the grid. It replaced `auto-fit` + a fixed row height on
   2026-09-02, because those two cannot agree on a shape — the column
   was fluid and the row was a clamp, so a 1x1 tile landed at 318x190
   (5:3) on a desktop and a 2x1 at 3.4:1, and a photograph put in either
   was cropped to a letterbox strip.

   The column COUNT is therefore explicit, and the row height is derived
   from it: `100cqi` is the container's own inline size, so the row is
   whatever the column works out to at that width, gap subtracted. A
   container query and not a media query, for the reason the span
   release below already records — the editor canvas is 1032px wide and
   the grid inside it is 537px, and only the container knows that.

   `dense` still does the back-filling, so the tile COUNT is as free as
   it was: add or remove a tile and the grid re-tiles itself. */
.lb-stories-head { align-items: end; border-bottom: 1px solid var(--ed-line-strong); padding-bottom: 22px; }
.lb-stories-head > div:first-child { display: flex; flex-direction: column; gap: 12px; }
.lb-stories-head h2 { max-width: 460px; }
.lb-bento {
  display: grid;
  --afl-bento-gap: clamp(8px, 1vw, 16px);
  --afl-bento-cols: 4;
  grid-template-columns: repeat(var(--afl-bento-cols), minmax(0, 1fr));
  grid-auto-rows: calc(
    (100cqi - (var(--afl-bento-cols) - 1) * var(--afl-bento-gap)) / var(--afl-bento-cols)
  );
  grid-auto-flow: dense;
  gap: var(--afl-bento-gap);
  margin-top: clamp(24px, 3vw, 40px);
}
.lb-bento > * { grid-column: span var(--w, 1); grid-row: span var(--h, 1); min-width: 0; }
/* The ladder the rest of the system uses, 4 -> 3 -> 2 -> 1. The widths are
   where a 280px tile stops fitting, which is the floor the old `auto-fit`
   minmax carried and the narrowest a captioned photograph reads at. */
.lb-stories .lb-wrap { container: lb-stories / inline-size; }
@container lb-stories (max-width: 1160px) { .lb-bento { --afl-bento-cols: 3; } }
@container lb-stories (max-width: 860px)  { .lb-bento { --afl-bento-cols: 2; } }
/* Below the width where the grid can hold two tracks, a `span 2` tile has
   nowhere to go: the second column does not exist, the span forces an implicit
   one, and an implicit column here has no width — so a 1x1 tile placed in it
   renders 0px wide and disappears off the side of the page. Measured twice
   before this rule: a 350px container on a phone gave `342px 0px`, and the
   editor canvas gave the same at 537px.

   The ROW span is released with it now that the cell is square. It was kept
   before because a row span cannot overflow, which is still true — but a 2-row
   tile in a one-column grid is a 1:2 tower twice the width of the phone, and
   the point of the square was that a photograph fits. One column, one shape. */
@container lb-stories (max-width: 580px) {
  .lb-bento { --afl-bento-cols: 1; }
  .lb-bento > * { grid-column: span 1; grid-row: span 1; }
}
.lb-bento figure { margin: 0; position: relative; }
/* The well fills its cell rather than carrying a shape: the grid row
   already decided how tall this tile is. `.lb-media` positions its
   image absolutely and has no height of its own, so it is given one. */
.lb-bento figure > .lb-media { height: 100%; width: 100%; }
.lb-bento figcaption {
  position: absolute; inset: auto 0 0 0; z-index: 2;
  padding: 52px 18px 16px; pointer-events: none;
  background: linear-gradient(to top, rgba(23,21,15,.74) 0%, rgba(23,21,15,.26) 55%, transparent 100%);
}
.lb-bento figcaption b {
  display: block; font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); text-transform: uppercase;
  letter-spacing: var(--ed-track-label); color: var(--at-white, #fff);
}
.lb-bento figcaption i {
  display: block; margin-top: 5px; font-family: var(--ed-font-title);
  font-style: italic; font-size: var(--ed-size-meta); line-height: 1.5;
  color: rgba(255,255,255,.82);
}
/* The copy tile, on the page's own ground. Bottom-aligned so its last
   line sits on the same baseline as the scrim captions beside it. */
.lb-bento-text {
  background: var(--ed-surface); display: flex; flex-direction: column;
  justify-content: flex-end; gap: 8px; padding: clamp(18px, 2vw, 28px);
}
.lb-alt .lb-bento-text { background: var(--ed-surface-alt); }
.lb-bento-text p {
  margin: 0; font-family: var(--ed-font-title); font-style: italic;
  font-size: var(--ed-size-lead); line-height: 1.5;
  letter-spacing: var(--ed-track-title); color: var(--ed-text-title);
}
.lb-stories-close {
  margin: clamp(22px, 2.6vw, 36px) 0 0; max-width: var(--ed-max-text);
  font-family: var(--ed-font-title); font-style: italic;
  font-size: var(--ed-size-lead); line-height: 1.6; color: var(--ed-text-title);
}
.lb-bento .lb-media img { transition: transform var(--ed-dur) var(--ed-ease); }
.lb-bento figure:hover .lb-media img { transform: scale(var(--ed-hover-image-scale)); }

/* ── Journal index ───────────────────────────────────────────
   The same `.lb-post` card. It carried a 3:2 thumbnail when it landed on
   2026-08-28 — the biggest whitespace saving available on a long list —
   and 2026-08-31 took it back to the square every other card on the site
   now uses, because a 3:2 window over this photography is a letterbox.
   The saving comes out of the section rhythm instead, which is where the
   rest of it already comes from.

   With the frame back to 1:1 the square-source crop bias applies here as
   it does everywhere else, so the override that turned it off is gone:
   a square frame over a square source crops nothing at all, and over a
   portrait source it crops horizontally, where a vertical bias is inert. */
.lb-journal--index .lb-post-info { padding-top: 14px; gap: 8px; }
.lb-journal--index .lb-post h3 { max-width: 420px; }
/* Category and date are coloured differently, so they are two spans on
   one line rather than one string with a middot in it. */
.lb-post-meta { display: flex; flex-wrap: wrap; gap: 14px; align-items: baseline; }

/* ── Pull quote ──────────────────────────────────────────────
   The plainest thing in the layer: two rules and one italic line. It is
   how a page changes pace without changing colour. */
.lb-quote { padding: clamp(48px, 5vw, 80px) var(--ed-gutter); }
.lb-quote--ruled { border-top: 1px solid var(--ed-line); border-bottom: 1px solid var(--ed-line); }
/* Astra dresses every `blockquote` with a 5px left rule and 20px of padding —
   `.entry-content blockquote` at (0,1,1), which a single class cannot reach.
   Stated at (0,3,0) instead. The component's whole idea is two hairlines and
   nothing else, so the inherited third rule is not a small difference. */
.at-editorial .lb-quote .lb-quote-inner {
  max-width: 900px; margin: 0 auto;
  border: 0; padding: 0; background: none;
}
.lb-quote-inner p {
  margin: 0; font-family: var(--ed-font-title); font-style: italic; font-weight: 400;
  font-size: clamp(1.35rem, 2.2vw, 2rem); line-height: 1.4;
  letter-spacing: var(--ed-track-title); color: var(--ed-text-title);
  text-wrap: pretty;
}
.lb-quote-inner footer { margin-top: 20px; }
.lb-quote--center .lb-quote-inner { text-align: center; }

/* ── Post spotlight ──────────────────────────────────────────
   An editorial split whose right half is a POST — so the headline is a
   link and carries the underline every post title in this layer does,
   and the meta row is the index's, reused. Everything else is
   `.lb-split`'s already. */
.lb-spotlight .af-split-text { gap: 16px; }
.lb-spotlight-title { display: block; text-decoration: none; color: inherit; }
.lb-spotlight-title h2 { text-underline-offset: 4px; text-decoration-thickness: 1px; }
.lb-spotlight-title:hover h2 { text-decoration: underline; }
.lb-spotlight .lb-btn-row { margin-top: 8px; }
/* The link names its destination — "Read “<the post's title>” →" — so its
   length is a post title's, not a label's. `.lb-btn`'s nowrap is right for the
   short verbs everywhere else and runs this one clean out of the column.

   Wrapping alone is not enough: `.lb-btn--text` draws its rule as a
   border-bottom, which on a two-line box becomes a full-measure line under the
   second line and reads as a divider. A real underline hugs both lines, at the
   3px offset the layer already uses on every post title. */
.lb-spotlight .lb-btn--text {
  white-space: normal; max-width: var(--ed-max-text);
  line-height: 1.6; text-align: left;
  border-bottom: 0;
  text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1px;
}
.lb-spotlight .lb-btn--text:not([disabled]):not(.is-disabled):hover { border-bottom: 0; }

/* ── Where to find us, and the voices beside it ──────────────
   Adapted from studionadee.com's community-events panel, rebuilt in the
   label's own tokens: ink and canvas where the reference is earth and cream,
   Lora where it sets a display serif, Barlow SC where it sets a mono.

   Two separate components, not one composite. `.af-two` in layout.css already
   splits a row in half and stacks it at 900px, so a page puts one in each
   half and the arrangement is the editor's rather than a third block's. Both
   are panels — a ground, a padding and their own type — so either can also
   stand alone across a full measure. */
.lb-events,
.lb-voices {
  padding: clamp(32px, 4vw, 52px);
  display: flex; flex-direction: column;
  background: var(--ed-surface-alt);
  color: var(--ed-text-body);
  height: 100%;
}
/* The dark ground is the label's ink, not a new colour. The oxblood is spoken
   for — one CTA band a page — and a second saturated panel beside it would be
   two things shouting. Ink is what the footer and the scrim already use. */
/* The two wrappers are inert until the wide query below fires: a flex column
   inside a flex column inside a flex column is, for four flush children, the
   same box the panel drew when they were its own children. `flex: 1` and
   `min-height: 0` on both are what carry the note's `margin-top: auto` all the
   way down from `.lb-events`'s `height: 100%`. */
.lb-events-inner,
.lb-events-body {
  display: flex; flex-direction: column;
  flex: 1 1 auto; min-height: 0;
}

.lb-events--dark,
.lb-voices--dark {
  background: var(--at-ink);
  color: rgba(255, 255, 255, .72);
}
.lb-events--dark .lb-label,
.lb-voices--dark .lb-label { color: var(--ed-accent-soft); }

/* The amber ground. The brand's own secondary — Astra global-color-1, already
   the accent on every ink panel on the site — used here as a FIELD rather than
   as a highlight. No new colour enters the system for it.

   It is the tone for a diary standing ALONE across a full measure, where ink
   reads as a hole punched in the page. Beside the quote panel keep ink: two
   saturated panels side by side is the thing the oxblood rule already forbids.

   The type reverses back to ink rather than to white, because white on amber
   is 2.5:1 and unreadable. Ink on amber is 6.7:1; the softened inks below are
   held at .82 and .88 alpha, the point where the meta line still clears 4.5:1
   over this ground. Nothing here goes softer than that. */
.lb-events--amber {
  background: var(--at-amber);
  color: rgba(23, 21, 15, .88);
}
.lb-events--amber .lb-events-title,
.lb-events--amber .lb-event-title { color: var(--at-ink); }
.lb-events--amber .lb-events-intro { color: rgba(23, 21, 15, .88); }
.lb-events--amber .lb-event { border-left-color: rgba(23, 21, 15, .3); }
.lb-events--amber .lb-event + .lb-event { border-top-color: rgba(23, 21, 15, .14); }
.lb-events--amber .lb-event-date,
.lb-events--amber .lb-event-place { color: rgba(23, 21, 15, .82); }
.lb-events--amber .lb-events-cta { color: rgba(23, 21, 15, .88); }
.lb-events--amber .lb-events-note { color: rgba(23, 21, 15, .72); }
/* Oxblood on amber is 4.8:1 and is the pairing the brand already owns, so the
   eyebrow, the "happening now" dot and the link colour all keep the accent
   they carry on canvas — no amber-specific values needed. */
.lb-events--amber .lb-events-cta a,
.lb-events--amber .lb-event-title a:hover { color: var(--ed-accent); }
.lb-events--amber .lb-event-title a:focus-visible { outline-color: var(--at-ink); }

.lb-events-head { margin-bottom: clamp(20px, 3vw, 32px); }
.lb-events-title {
  margin: 10px 0 0;
  font-family: var(--ed-font-title); font-style: italic; font-weight: 400;
  font-size: clamp(1.5rem, 2.4vw, 2.1rem); line-height: 1.25;
  letter-spacing: var(--ed-track-title);
  color: var(--ed-text-title); text-wrap: balance;
}
.lb-events--dark .lb-events-title { color: var(--at-white); }
/* The paragraph under the headline. Held to the reading measure rather than the
   panel's full width — the panel can be a whole page wide when it is not beside
   the quotes, and a 1440-wide line of body copy is not a line anybody reads.

   A shift+enter in the field is a PARAGRAPH break. The renderer turns it into
   real <p>s rather than styling the <br>, because Chrome ignores `display` on a
   <br> and the CSS route silently does nothing — hence the <p> selector here.
   In the editor the same break still draws as a plain line break; that is the
   one place in this block where the canvas is not literally the page. */
.lb-events-intro {
  margin: 14px 0 0; max-width: var(--ed-max-text);
  font-family: var(--ed-font-body); font-size: var(--ed-size-body);
  line-height: 1.75; color: var(--ed-text-body);
}
.lb-events-intro p { margin: 0; }
.lb-events-intro p + p { margin-top: 0.85em; }
.lb-events-intro a {
  color: inherit;
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
}
.lb-events--dark .lb-events-intro { color: rgba(255, 255, 255, .72); }

/* The rows are ruled on the LEFT, not between. A stack of full-width rules
   reads as a table; a single continuous left rule reads as a timeline, which
   is what a diary of dates is. The hairline between rows is a quarter the
   weight of the left one and only separates — it does not enclose. */
.lb-events-list { list-style: none; margin: 0; padding: 0; }
.lb-event {
  position: relative;
  padding: 18px 0 18px 22px;
  border-left: 1px solid var(--ed-line-strong);
}
.lb-event + .lb-event { border-top: 1px solid var(--ed-line); }
.lb-events--dark .lb-event { border-left-color: rgba(255, 255, 255, .22); }
.lb-events--dark .lb-event + .lb-event { border-top-color: rgba(255, 255, 255, .1); }

.lb-event-date {
  display: block; margin-bottom: 4px;
  font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); text-transform: uppercase;
  letter-spacing: var(--ed-track-label-wide); color: var(--ed-text-meta);
}
.lb-events--dark .lb-event-date { color: rgba(255, 255, 255, .55); }
.lb-event-title {
  margin: 0 0 4px;
  font-family: var(--ed-font-title); font-style: italic; font-weight: 400;
  font-size: var(--ed-size-h3); line-height: 1.3;
  letter-spacing: var(--ed-track-title); color: var(--ed-text-title);
}
.lb-events--dark .lb-event-title { color: var(--at-white); }
/* The name is the link, all of it. The underline is the layer's own — 1px at
   4px offset, the same one every post title and nav link carries — appearing
   on hover rather than sitting there, because a diary of five links underlined
   at rest is a page of underlines. */
.lb-event-title a {
  color: inherit; text-decoration: none;
  text-underline-offset: 4px; text-decoration-thickness: 1px;
  transition: color var(--ed-dur-fast) var(--ed-ease);
}
.lb-event-title a:hover { text-decoration: underline; }
.lb-events--dark .lb-event-title a:hover { color: var(--ed-accent-soft); }
.lb-event-place {
  display: block;
  font-family: var(--ed-font-body); font-size: var(--ed-size-meta);
  line-height: 1.6; color: var(--ed-text-meta);
}
.lb-events--dark .lb-event-place { color: rgba(255, 255, 255, .55); }

/* "Happening now" — the only self-starting movement on a label page outside
   the two rotators, and it earns its place: it is the difference between a
   diary and a reason to leave the house today. A ring expanding out of a 6px
   dot; no bounce, no colour cycling. */
.lb-event-now {
  display: inline-flex; align-items: center; gap: 8px;
  margin-bottom: 8px;
  font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); text-transform: uppercase;
  letter-spacing: var(--ed-track-label-wide); color: var(--ed-accent);
}
.lb-events--dark .lb-event-now { color: var(--ed-accent-soft); }
.lb-event-now::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: 0 0 auto;
  animation: lb-pulse 2s infinite;
}
@keyframes lb-pulse {
  0%   { box-shadow: 0 0 0 0 currentColor; opacity: 1; }
  70%  { box-shadow: 0 0 0 8px transparent; opacity: 1; }
  100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
}

/* ── An event that has been and gone ──
   Set by an optional "Ends on" date in the editor, never inferred from the
   dates a reader sees — those are typed, carry no year, and are written in two
   languages.

   The row is DEMOTED, not dimmed: every line drops to the colour that tone
   already gives its meta, and the left rule drops to the weight that tone
   already gives the hairline between rows. No value here is softer than
   something the panel was already showing, so a past row cannot be less
   readable than a present row's date and place — which is the trap in "grey it
   out", and the reason this is not an opacity.

   The word "Past" above it is what actually carries the meaning; see the
   renderer. Colour on its own is not a differentiator a reader with low vision,
   a monochrome display, or a diary where everything has passed can use.

   The link stays live. A finished event's page is still the answer to "what was
   that?", and there is nothing to gain by breaking it. */
.lb-event-flag {
  display: block; margin-bottom: 8px;
  font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); text-transform: uppercase;
  letter-spacing: var(--ed-track-label-wide); color: var(--ed-text-meta);
}
.lb-event--past .lb-event-title,
.lb-event--past .lb-event-title a { color: var(--ed-text-meta); }
.lb-event--past { border-left-color: var(--ed-line); }

.lb-events--dark .lb-event-flag,
.lb-events--dark .lb-event--past .lb-event-title,
.lb-events--dark .lb-event--past .lb-event-title a { color: rgba(255, 255, 255, .55); }
.lb-events--dark .lb-event--past { border-left-color: rgba(255, 255, 255, .1); }

.lb-events--amber .lb-event-flag,
.lb-events--amber .lb-event--past .lb-event-title,
.lb-events--amber .lb-event--past .lb-event-title a { color: rgba(23, 21, 15, .82); }
.lb-events--amber .lb-event--past { border-left-color: rgba(23, 21, 15, .14); }

/* The two closing lines. The first asks for something and carries a link; the
   second is the quiet admission that the list is not the whole year. */
.lb-events-cta {
  margin: clamp(22px, 3vw, 32px) 0 0;
  font-family: var(--ed-font-body); font-size: var(--ed-size-meta);
  line-height: 1.7; color: var(--ed-text-body);
}
.lb-events--dark .lb-events-cta { color: rgba(255, 255, 255, .72); }
.lb-events-cta a {
  color: var(--ed-text-title);
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
}
.lb-events--dark .lb-events-cta a { color: var(--at-white); }
.lb-events--dark .lb-events-cta a:hover { color: var(--ed-accent-soft); }
/* margin-top: auto pins it to the bottom of the panel, so a short diary beside
   a tall quote panel still closes level with it rather than floating. */
.lb-events-note {
  margin: 14px 0 0; padding-top: 0;
  font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); text-transform: uppercase;
  letter-spacing: var(--ed-track-label); color: var(--ed-text-meta);
}
.lb-events-list ~ .lb-events-note:last-child { margin-top: auto; padding-top: 22px; }
.lb-events--dark .lb-events-note { color: rgba(255, 255, 255, .4); }

/* The wide panel.

   The diary was drawn for the half-column it occupies beside the quotes. Stood
   alone across a full measure it keeps stacking, and three dates in a 1300px
   column leave two thirds of the panel empty with every name stranded at the
   left edge — which is what "it does not look right" was about.

   Wide, the stack turns sideways: the same rows, the same left rule, read as
   columns instead. The rule was already doing the separating, so only the
   hairline BETWEEN rows has to go — three cards each with a top hairline would
   be three boxes, and this is still one timeline.

   A CONTAINER query, not a viewport one, and for the reason the essay grid
   found on 2026-08-28: this panel is half a row on the home page and a whole
   one here at the same viewport width, so the viewport cannot tell them apart.
   The container is `.lb-events` itself and only its CHILDREN are restyled —
   nothing inside the query touches the element the query measures. */
.lb-events { container: lb-events / inline-size; }

@container lb-events (min-width: 760px) {
  .lb-events-list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    column-gap: clamp(28px, 3vw, 56px);
  }
  /* The vertical padding was the gap between stacked rows; side by side it is
     dead air above the first date and below the last place. */
  .lb-event { padding-top: 0; padding-bottom: 0; }
  .lb-event + .lb-event { border-top: 0; }
}

/* Wider still — the panel running the whole width of the screen — the head
   stops sitting ON TOP of the diary and moves BESIDE it.

   Stacked at 1500px the panel is a 620px paragraph with a thousand pixels of
   empty ground to its right and everything below pushed down by it; the height
   is the intro's height plus the diary's, and only the first third of the width
   is doing any work. Turned sideways the panel is as tall as the TALLER of the
   two halves instead of their sum, which on the About page is a third off, and
   the empty right-hand side is where the diary now lives.

   Same arrangement as the oxblood band's `.lb-band-inner` — prose left, the
   thing being announced right — because it is the same problem, and the band
   is the precedent already on the page. 1100px is where the head's measure plus
   three 220px event columns first fit; below it, stack.

   `align-items: start` so the two halves both hang from the top rule rather
   than the shorter one centring itself against the taller. */
@container lb-events (min-width: 1100px) {
  .lb-events-inner {
    display: grid;
    grid-template-columns: minmax(0, var(--ed-max-text)) minmax(0, 1fr);
    column-gap: clamp(48px, 6vw, 112px);
    align-items: start;
  }
  /* The gap is the grid's now. */
  .lb-events-head { margin-bottom: 0; }
  /* The intro no longer needs its own cap — the column IS the measure. */
  .lb-events-intro { max-width: none; }
  /* The diary starts level with the eyebrow, not below the paragraph. */
  .lb-events-body { display: block; }
  /* And it is a STACK again, not the sideways grid the 760px query asked for.
     That query exists because three dates strung across a 1300px panel strand
     every name at the left edge — but in this arrangement the diary has a
     ~600px half, not the panel, and three 220px columns do not fit in it. What
     they do instead is wrap to a second row, where the hairline the sideways
     grid removed is exactly what was telling the reader that Sfinks Mixed is a
     different event from the one above it. Half the width, so: half the rules
     back. Restated rather than scoped out of the 760px query, because the two
     queries both match here and this one has to be the one that lands. */
  .lb-events-list { display: block; }
  .lb-event { padding-top: 18px; padding-bottom: 18px; }
  .lb-event + .lb-event { border-top: 1px solid var(--ed-line); }
  .lb-events--dark .lb-event + .lb-event { border-top-color: rgba(255, 255, 255, .1); }
  .lb-events--amber .lb-event + .lb-event { border-top-color: rgba(23, 21, 15, .14); }
  /* `margin-top: auto` pinned the note to the bottom of a column that ran the
     whole panel. In its own half there is nothing below it to be pinned away
     from, and the auto margin would drop it a paragraph's worth of nothing. */
  .lb-events-list ~ .lb-events-note:last-child { margin-top: 14px; padding-top: 0; }
}

/* A panel standing in a full-bleed section has no `.lb-wrap` around it, so its
   own padding is the only thing deciding where its type starts — and 52px is
   not where the rest of the page starts. This puts the panel's inline padding
   back on the page's measure: the gutter when the viewport is narrower than
   `--ed-max`, and the gutter plus whatever `.lb-wrap` would have left at the
   sides when it is wider. The ground still runs edge to edge; only the words
   line up. */
.lb-section--bleed > .lb-events {
  padding-inline: max(
    var(--ed-gutter),
    calc((100% - var(--ed-max)) / 2 + var(--ed-gutter))
  );
}

/* ── The voices ──
   One quote visible, the rest stacked underneath it at opacity 0 and taken out
   of the accessibility tree. Every quote is in the markup, so no-JS, a
   printout and a crawler all get the whole panel; the script only ever changes
   which one carries `is-on`.

   The stage is a grid with ONE cell that every figure shares, rather than
   `position: absolute`. Absolute would collapse the stage to nothing and need
   a hand-set min-height that the longest quote eventually outgrows; a shared
   grid cell sizes itself to the tallest quote and stops the panel jumping as
   it rotates. */
.lb-voices-stage { display: grid; flex: 1; align-items: center; }
.lb-voice {
  grid-area: 1 / 1;
  margin: 0; text-align: center;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.lb-voices--rotates .lb-voice {
  opacity: 0; pointer-events: none;
  transition: opacity var(--ed-dur) var(--ed-ease);
}
.lb-voices--rotates .lb-voice.is-on { opacity: 1; pointer-events: auto; }
.lb-voice-mark {
  font-family: var(--ed-font-title); font-size: 2.4rem; line-height: 1;
  color: var(--ed-line-strong); margin-bottom: 14px;
}
.lb-voices--dark .lb-voice-mark { color: rgba(255, 255, 255, .3); }
/* Astra dresses every blockquote with a 5px left rule and 20px of padding at
   `.entry-content blockquote` — (0,1,1), unreachable by one class. Same trap
   the pull quote had to be dug out of; stated at the same weight here. */
.at-editorial .lb-voice .lb-voice-text {
  margin: 0 0 22px; border: 0; padding: 0; background: none;
  font-family: var(--ed-font-title); font-style: italic; font-weight: 400;
  font-size: clamp(1.1rem, 1.7vw, 1.4rem); line-height: 1.5;
  letter-spacing: var(--ed-track-title); color: var(--ed-text-title);
  max-width: 26ch; text-wrap: pretty;
}
.at-editorial .lb-voices--dark .lb-voice .lb-voice-text { color: var(--at-white); }
.lb-voice-by {
  font-family: var(--ed-font-body); font-size: var(--ed-size-meta);
  line-height: 1.6; color: var(--ed-text-meta);
}
.lb-voice-by b {
  font-family: var(--ed-font-label); font-weight: 600;
  font-size: var(--ed-size-label); text-transform: uppercase;
  letter-spacing: var(--ed-track-label); color: var(--ed-accent);
}
.lb-voices--dark .lb-voice-by { color: rgba(255, 255, 255, .55); }
.lb-voices--dark .lb-voice-by b { color: var(--ed-accent-soft); }
.lb-voice-handle {
  display: block; margin-top: 4px;
  font-size: 0.8125rem; color: var(--ed-text-meta);
}
.lb-voices--dark .lb-voice-handle { color: rgba(255, 255, 255, .4); }

.lb-voices-dots { display: flex; gap: 8px; justify-content: center; margin-top: 26px; }
.lb-voices-dot {
  width: 24px; height: 24px; padding: 9px; border: 0; border-radius: 50%;
  background-clip: content-box;
  background: var(--ed-line-strong); cursor: pointer;
  transition: background var(--ed-dur-fast) var(--ed-ease),
              transform var(--ed-dur-fast) var(--ed-ease);
}
.lb-voices-dot.is-on { background: var(--ed-accent); }
.lb-voices--dark .lb-voices-dot { background: rgba(255, 255, 255, .28); }
.lb-voices--dark .lb-voices-dot.is-on { background: var(--ed-accent-soft); }
.at-editorial .lb-voices-dot:focus-visible,
.at-editorial .lb-event-title a:focus-visible {
  outline: 1px solid var(--ed-text-title); outline-offset: 3px;
}
.lb-voices--dark .lb-voices-dot:focus-visible,
.lb-events--dark .lb-event-title a:focus-visible { outline-color: var(--at-white); }

/* A rotator nobody can pause is worse than a list. Under reduced motion the
   script never starts, so every quote must be visible — the stage becomes a
   column and the dots, which now control nothing, go. Same rule as the
   announcement bar's and the hero slider's. */
@media (prefers-reduced-motion: reduce) {
  .lb-event-now::before { animation: none; box-shadow: none; }
  .lb-voices-stage { display: flex; flex-direction: column; gap: 32px; }
  .lb-voices--rotates .lb-voice,
  .lb-voices--rotates .lb-voice.is-on { opacity: 1; pointer-events: auto; }
  .lb-voices-dots { display: none; }
}

/* ── Third-party consent UI, brought into the label system ────
   Complianz owns the consent behaviour; the theme only supplies the same
   typography, square geometry and quiet focus language as every other form. */
body.at-editorial .cmplz-cookiebanner {
  border: 1px solid var(--ed-line);
  border-radius: 0;
  box-shadow: none;
  font-family: var(--ed-font-body);
}
body.at-editorial .cmplz-cookiebanner .cmplz-title {
  font-family: var(--ed-font-title);
  font-weight: var(--ed-weight-title);
  color: var(--ed-text-title);
}
body.at-editorial .cmplz-cookiebanner .cmplz-message {
  font-size: max(14px, 0.875rem);
  font-family: var(--ed-font-body);
  color: var(--ed-text-body);
}
body.at-editorial .cmplz-cookiebanner .cmplz-btn {
  min-height: var(--ed-control-h);
  border-radius: 0;
  box-shadow: none;
  font-family: var(--ed-font-label);
  font-size: var(--ed-size-label);
  font-weight: 600;
  letter-spacing: var(--ed-track-label);
  text-transform: uppercase;
}
body.at-editorial .cmplz-cookiebanner .cmplz-accept {
  border-color: var(--ed-accent);
  background: var(--ed-accent);
  color: var(--at-white);
}
body.at-editorial .cmplz-cookiebanner .cmplz-deny,
body.at-editorial .cmplz-cookiebanner .cmplz-view-preferences,
body.at-editorial .cmplz-cookiebanner .cmplz-save-preferences {
  border-color: var(--ed-line-strong);
  background: transparent;
  color: var(--ed-text-title);
}
body.at-editorial .cmplz-cookiebanner .cmplz-btn:hover { box-shadow: none; }
body.at-editorial .cmplz-cookiebanner .cmplz-btn:focus-visible,
body.at-editorial #cmplz-manage-consent .cmplz-manage-consent:focus-visible {
  outline: 1px solid var(--ed-text-title);
  outline-offset: 2px;
}
body.at-editorial #cmplz-manage-consent .cmplz-manage-consent {
  border: 1px solid var(--ed-line);
  border-radius: 0;
  box-shadow: none;
  font-family: var(--ed-font-label);
  font-size: var(--ed-size-label);
  letter-spacing: var(--ed-track-label);
  text-transform: uppercase;
}

@media (max-width: 760px) {
  /* The opener has no copy over the photograph. A lighter mobile wash keeps
     its square crop from turning into a solid grey lower half. */
  .lb-split--opener .lb-media-scrim {
    --afl-tint: 0.12;
    --ed-scrim: linear-gradient(to top, rgba(23,21,15,.3) 0%, rgba(23,21,15,.05) 48%, transparent 72%);
  }
}
