/*
 * Cross-document view transitions — opt-in via the platform.
 * Browsers that support the View Transitions API
 * (Chrome 126+, Edge 126+) crossfade navigation between same-origin
 * documents. Browsers without support navigate normally — there is
 * no JS dependency. We keep the default fade subtle (180ms) so the
 * transition reads as polish without distracting from content.
 */
@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 180ms;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

/* Every reduced-motion rule in this contract is guarded by
 * `html:not([data-f2b-force-motion])`: a tenant that deliberately opts out
 * of `prefers-reduced-motion` (round 3 item 9 — goban) stamps
 * `data-f2b-force-motion` on its source `<html>` (preserved by the import
 * shell sanitizer), and every animation then plays even when the OS
 * requests reduced motion. Tenants without the attribute are untouched. The
 * runtime's `prefersReducedMotion()` consults the same attribute, so JS and
 * CSS agree from first paint. */
@media (prefers-reduced-motion: reduce) {
  html:not([data-f2b-force-motion])::view-transition-old(root),
  html:not([data-f2b-force-motion])::view-transition-new(root) {
    animation-duration: 0.001ms;
  }
}

.f2b-widget {
  --f2b-widget-font: inherit;
  --f2b-widget-text: inherit;
  --f2b-widget-muted: color-mix(in srgb, currentColor 72%, transparent);
  --f2b-widget-accent: var(--brand-accent, var(--color-accent, var(--color-ember, #6f2f19)));
  /* Secondary editorial accent — the tenant's "highlight" voice (goban's gold
   * rules / prices / eyebrows) when it exposes one, else the primary accent.
   * Lets a two-accent brand set rules + prices apart from hot CTAs/badges
   * without forking the contract; single-accent tenants fall back gracefully. */
  --f2b-widget-accent-alt: var(--brand-highlight-100, var(--f2b-widget-accent));
  --f2b-widget-surface: var(--surface-1, transparent);
  --f2b-widget-border: color-mix(in srgb, currentColor 18%, transparent);
  --f2b-widget-radius: 10px;
  --f2b-widget-control-height: var(--brand-control-height, 2.75rem);
  --f2b-widget-transition-duration: var(--brand-transition-duration, 200ms);
  --f2b-widget-transition-easing: var(--brand-transition-easing, ease);
  --f2b-widget-focus-ring: 2px solid color-mix(in srgb, var(--f2b-widget-accent) 60%, transparent);
  --f2b-widget-error: color-mix(in srgb, var(--f2b-widget-accent) 78%, currentColor);
  --f2b-widget-success: color-mix(in srgb, var(--f2b-widget-accent) 72%, currentColor);
  --f2b-widget-disabled-opacity: 0.55;
  --f2b-widget-shadow: 0 14px 36px color-mix(in srgb, currentColor 12%, transparent);

  /* Type scale (brief 09 § C). Each step is a token defaulting to its own
   * value, so a tenant can retune any step without forking the contract.
   * --f2b-widget-display-font carries the persona voice on the panel title
   * (goban themes it to Playfair). --f2b-widget-cjk-font is the seam for a
   * future unicode-range-sliced webfont; the default `none` is a deliberate
   * no-op (an invalid font-family value, so the declaration is ignored and
   * the inherited stack stands) — no CJK woff2 ships today (goban is en-only;
   * the sliced build is a named platform follow-up). */
  --f2b-widget-display-font: inherit;
  /* Condensed/label voice for uppercase chrome (menu eyebrows, badges, meta).
   * Default inherit; a tenant binds it to its label face (goban → Barlow
   * Condensed) the same way it themes the display font. */
  --f2b-widget-label-font: inherit;
  --f2b-widget-cjk-font: none;
  --f2b-widget-title-size: 1.375rem;
  --f2b-widget-subtitle-size: 0.875rem;
  --f2b-widget-message-size: 1rem;
  --f2b-widget-message-line: 1.5;
  --f2b-widget-chip-size: 0.875rem;

  box-sizing: border-box;
  color: var(--f2b-widget-text);
  font: var(--f2b-widget-font);
  line-height: inherit;
}

.f2b-widget *,
.f2b-widget *::before,
.f2b-widget *::after {
  box-sizing: border-box;
}

/* A widget that hides itself (e.g. admin-link after an anonymous whoami
 * probe) must STAY hidden even when adopted tenant classes set `display`
 * — any author-level display rule beats the UA's `[hidden]` default, so
 * without this a hidden widget paints as an empty themed box. */
.f2b-widget[hidden] {
  display: none !important;
}

.f2b-widget :where(button, input, select, textarea) {
  min-height: var(--f2b-widget-control-height);
  border: 1px solid var(--f2b-widget-border);
  border-radius: var(--f2b-widget-radius);
  background: var(--f2b-widget-surface);
  color: inherit;
  font: inherit;
  line-height: 1.2;
  transition-property: background-color, border-color, color, outline-color,
    box-shadow, opacity, transform;
  transition-duration: var(--f2b-widget-transition-duration);
  transition-timing-function: var(--f2b-widget-transition-easing);
}

.f2b-widget :where(input, select, textarea) {
  width: 100%;
  padding: 0.65rem 0.75rem;
}

.f2b-widget :where(button) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  padding: 0.65rem 1rem;
  cursor: pointer;
  text-decoration: none;
}

.f2b-widget :where(button, input, select, textarea, a):focus-visible {
  outline: var(--f2b-widget-focus-ring);
  outline-offset: 3px;
}

.f2b-widget :where(button:disabled, input:disabled, select:disabled, textarea:disabled) {
  cursor: not-allowed;
  opacity: var(--f2b-widget-disabled-opacity);
}

.f2b-field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-block: 0 0.8rem;
}

.f2b-field label {
  color: var(--f2b-widget-muted);
  font: inherit;
  font-size: 0.9em;
}

.f2b-submit {
  border-color: color-mix(in srgb, var(--f2b-widget-accent) 40%, var(--f2b-widget-border));
  background: color-mix(in srgb, var(--f2b-widget-accent) 10%, transparent);
  color: var(--f2b-widget-accent);
}

.f2b-status {
  min-height: 1.4em;
  margin: 0.8rem 0 0;
  color: var(--f2b-widget-muted);
}

.f2b-disclosure,
.f2b-check {
  margin: 0 0 0.8rem;
  color: var(--f2b-widget-muted);
  font-size: 0.9em;
}

.f2b-check {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
}

.f2b-check input {
  width: var(--f2b-widget-control-height);
  min-width: var(--f2b-widget-control-height);
  height: var(--f2b-widget-control-height);
  min-height: var(--f2b-widget-control-height);
  margin: 0;
  accent-color: var(--f2b-widget-accent);
}

.f2b-widget[data-f2b-state="error"] .f2b-status,
.f2b-widget [aria-invalid="true"] {
  color: var(--f2b-widget-error);
}

.f2b-waitlist-form,
.f2b-email-form {
  display: block;
  max-width: min(100%, 34rem);
}

.f2b-email-success {
  max-width: 38rem;
  color: inherit;
}

.f2b-email-success-title {
  margin: 0 0 0.35rem;
  color: var(--f2b-widget-success);
  font-weight: 700;
}

.f2b-email-success-body {
  margin: 0;
  color: var(--f2b-widget-muted);
}

.f2b-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  margin: -1px;
  clip: rect(0, 0, 0, 0);
  overflow: hidden;
  white-space: nowrap;
}

.f2b-admin-link-anchor {
  display: inline-flex;
  min-height: var(--f2b-widget-control-height);
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.8rem;
  border: 1px solid color-mix(in srgb, var(--f2b-widget-accent) 35%, var(--f2b-widget-border));
  border-radius: var(--f2b-widget-radius);
  background: color-mix(in srgb, var(--f2b-widget-accent) 10%, transparent);
  color: var(--f2b-widget-accent);
  text-decoration: none;
}

.f2b-admin-link-avatar {
  display: inline-flex;
  width: 1.85rem;
  height: 1.85rem;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--f2b-widget-surface);
  color: var(--f2b-widget-accent);
  font-size: 0.78em;
  font-weight: 700;
}

.f2b-admin-link-avatar img {
  width: 100%;
  height: 100%;
  border-radius: inherit;
  object-fit: cover;
}

.f2b-admin-link-text {
  display: inline-flex;
  min-width: 0;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem;
}

.f2b-admin-link-name,
.f2b-admin-link-role {
  overflow-wrap: anywhere;
}

@media (prefers-reduced-motion: reduce) {
  html:not([data-f2b-force-motion]) .f2b-widget *,
  html:not([data-f2b-force-motion]) .f2b-widget *::before,
  html:not([data-f2b-force-motion]) .f2b-widget *::after {
    transition-duration: 0.001ms !important;
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

/* === Widget presentation modes (brief 09) === */

/* Presentation mode: full_page_route — editorial layout with hero +
 * form panel, expands the existing data-f2b placeholder via CSS so
 * import-time route_synthesis stays untouched. */
.f2b-widget--full-page {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  padding: clamp(1.5rem, 4vw, 3rem);
  align-content: start;
}

/* The full-viewport height belongs to the STANDALONE synthesized widget route
 * (/email-signup), where the canvas IS the whole page. When the same canvas
 * widget is mounted INLINE in a content section (e.g. the home email-signup
 * widget-slot), it must size to its content — reserving a screen-height there
 * left a large empty gap below the form. */
[data-f2b-route-kind="synthesized-widget"] .f2b-widget--full-page {
  min-height: calc(100dvh - var(--brand-nav-height, var(--nav-h, 4rem)));
}

.f2b-widget--full-page .f2b-widget-hero {
  display: grid;
  gap: 0.75rem;
}

.f2b-widget--full-page .f2b-widget-hero h1 {
  font: inherit;
  font-size: clamp(1.75rem, 4vw, 2.75rem);
  line-height: 1.1;
  font-weight: 700;
  margin: 0;
}

.f2b-widget--full-page .f2b-widget-form-panel {
  display: grid;
  gap: 1rem;
  max-width: 38rem;
}

@media (min-width: 768px) {
  .f2b-widget--full-page {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    grid-template-areas: "hero form";
    align-items: start;
  }
  .f2b-widget--full-page .f2b-widget-hero    { grid-area: hero; }
  .f2b-widget--full-page .f2b-widget-form-panel { grid-area: form; }
}

/* Presentation mode: sidebar_overlay — the data-f2b marker is the
 * click trigger; the actual overlay (backdrop + panel) is appended at
 * <body> level to avoid stacking-context / position:fixed-ancestor
 * traps in the imported page chrome.
 *
 * The marker carries no children of its own (the panel content renders
 * into the body-level overlay), so the style contract must give the
 * trigger a visible, clickable affordance — otherwise it collapses to a
 * 0-height block and cannot be clicked. Render it as a floating launcher
 * pill pinned bottom-right, labelled from the marker's aria-label and
 * tinted with the tenant brand accent so it reads as native. */
.f2b-widget--sidebar {
  position: fixed;
  --f2b-widget-launcher-gap: clamp(1rem, 4vw, 1.75rem);
  --f2b-widget-launcher-lift: 0px;
  /* Offsets clear the mobile safe area (home indicator / rounded corners /
   * notch) by adding the UA-reported inset to the base gap, so the pill never
   * lands under the safe area at supported viewports. `env(...,0px)` degrades
   * to the plain gap on browsers without safe-area insets. */
  right: calc(var(--f2b-widget-launcher-gap) + env(safe-area-inset-right, 0px));
  bottom: calc(
    var(--f2b-widget-launcher-gap) + env(safe-area-inset-bottom, 0px) +
      var(--f2b-widget-launcher-lift)
  );
  z-index: 999; /* below the open overlay (1000) */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* ≥44×44 CSS-px target (WCAG 2.5.5 / brief item 3): height is 3rem = 48px,
   * and min-width guarantees the floor even if the label were ever empty; the
   * text label makes it wider in practice. */
  min-height: 3rem;
  min-width: 44px;
  padding: 0 1.25rem;
  border-radius: 999px;
  background: var(--f2b-widget-launcher-bg, var(--f2b-widget-accent));
  color: var(--f2b-widget-launcher-text, var(--brand-on-accent, var(--cream, #fff)));
  font: inherit;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--f2b-widget-shadow);
  transition:
    transform var(--f2b-widget-transition-duration) var(--f2b-widget-transition-easing),
    box-shadow var(--f2b-widget-transition-duration) var(--f2b-widget-transition-easing),
    opacity var(--f2b-widget-transition-duration) var(--f2b-widget-transition-easing);
}

/* The accessible name doubles as the visible pill label. */
.f2b-widget--sidebar::after {
  content: attr(aria-label);
  white-space: nowrap;
}

.f2b-widget--sidebar[data-f2b-floating-dock^='compact'] {
  width: 3rem;
  max-width: 3rem;
  padding: 0;
  overflow: hidden;
}

.f2b-widget--sidebar[data-f2b-floating-dock='compact-left'] {
  right: auto;
  left: calc(var(--f2b-widget-launcher-gap) + env(safe-area-inset-left, 0px));
}

.f2b-widget--sidebar[data-f2b-floating-lift='1'] {
  --f2b-widget-launcher-lift: 4rem;
}

.f2b-widget--sidebar[data-f2b-floating-lift='2'] {
  --f2b-widget-launcher-lift: 8rem;
}

.f2b-widget--sidebar[data-f2b-floating-lift='3'] {
  --f2b-widget-launcher-lift: 12rem;
}

.f2b-widget--sidebar[data-f2b-floating-lift='4'] {
  --f2b-widget-launcher-lift: 16rem;
}

.f2b-widget--sidebar[data-f2b-floating-dock^='compact']::before {
  content: '';
  width: 1.125rem;
  height: 0.875rem;
  border: 2px solid currentColor;
  border-radius: 0.35rem;
  box-sizing: border-box;
}

.f2b-widget--sidebar[data-f2b-floating-dock^='compact']::after {
  content: '';
  position: absolute;
  width: 0.375rem;
  height: 0.375rem;
  right: 0.95rem;
  bottom: 0.8rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(32deg);
  white-space: normal;
}

.f2b-widget--sidebar:hover {
  transform: translateY(-2px);
  box-shadow: 0 18px 44px color-mix(in srgb, currentColor 16%, transparent);
}

.f2b-widget--sidebar:focus-visible {
  outline: var(--f2b-widget-focus-ring);
  outline-offset: 3px;
}

.f2b-widget--sidebar:active {
  transform: scale(0.96);
}

/* Trigger ↔ panel handoff: while the overlay is open (any mode) the
 * launcher pill yields the corner. Presence selector, not `="true"`, so it
 * fires for every mode value (companion / focus / modal). In companion the
 * page stays interactive, so the faded pill also drops pointer-events —
 * otherwise its invisible corner would still swallow clicks. */
body[data-f2b-sidebar-open] .f2b-widget--sidebar {
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  html:not([data-f2b-force-motion]) .f2b-widget--sidebar {
    transition: none;
  }
  html:not([data-f2b-force-motion]) .f2b-widget--sidebar:hover,
  html:not([data-f2b-force-motion]) .f2b-widget--sidebar:active {
    transform: none;
  }
}

/* Nav-anchored sidebar trigger (`data-f2b-placement-kind="nav_item"` on
 * a sidebar_overlay feature): compose injects it as an `<a>` among the
 * tenant's own nav entries, adopting their class — fidelity-first, the
 * tenant nav CSS owns typography/spacing/hover. The contract only
 * guarantees the pointer affordance and the label (the marker's text
 * content; no `::after attr(aria-label)` — unlike the floating pill the
 * nav trigger has real text children). Notably NOT `position: fixed`
 * and NOT hidden while the overlay is open. */
.f2b-widget--sidebar-nav {
  /* The only style-contract guarantee. The trigger intentionally does
   * not carry `.f2b-widget` (the dispatcher skips it for nav triggers),
   * so the widget font/color tokens never override the tenant nav
   * styles the marker adopted — the tenant sheet owns the look. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  cursor: pointer;
  touch-action: manipulation;
}

.f2b-widget--sidebar-nav:hover,
a.f2b-widget--sidebar-nav[data-f2b='chat'][data-f2b-placement-kind='nav_item']:hover {
  background: var(
    --f2b-widget-nav-hover-bg,
    var(--brand-accent, var(--color-accent, var(--color-ember, #6f2f19)))
  );
  color: var(
    --f2b-widget-nav-hover-text,
    var(--brand-on-accent, var(--cream, #fff))
  );
  border-color: var(
    --f2b-widget-nav-hover-border,
    var(--f2b-widget-nav-hover-bg, var(--brand-accent, var(--color-accent, var(--color-ember, #6f2f19))))
  );
}

/* Overflow yield: the nav trigger is chrome the source layout never
 * budgeted for. When the runtime measures the nav row overflowing with
 * the trigger participating (goban at phone widths: the extra pill
 * pushed the tenant's own hamburger out of the viewport), the trigger
 * yields — the drawer/mobile-menu copy is the access path there.
 * `!important` outranks tenant `display: flex !important` nav rules. */
.f2b-widget--sidebar-nav[data-f2b-nav-yield='true'] {
  display: none !important;
}

/* Open/close gating (brief 05): visibility instead of display so the
 * slide/fade transitions actually render in both directions — a
 * display toggle cancels them and `closeSidebar`'s transitionend
 * cleanup never fires (it always fell through to the 800 ms safety
 * timeout). The 240 ms visibility delay holds the overlay through the
 * panel's exit slide; pointer-events drops immediately so the fading
 * overlay never swallows clicks. Hidden visibility keeps the closed
 * panel out of the a11y tree and tab order exactly like display:none. */
.f2b-sidebar-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  visibility: hidden;
  pointer-events: none;
  transition: visibility 0s linear 240ms;
}

.f2b-sidebar-overlay[data-open="true"] {
  visibility: visible;
  pointer-events: auto;
  transition: none;
}

/* Companion (brief 08 § A): non-modal co-browsing dock. The full-viewport
 * overlay must NOT capture pointer events — the page behind stays clickable
 * and scrollable (occlusion, not push). Only the panel itself is
 * interactive. Take-over modes (focus / modal) keep the overlay capturing
 * so the backdrop can absorb click-to-close. */
.f2b-sidebar-overlay[data-f2b-sidebar-mode="companion"][data-open="true"] {
  pointer-events: none;
}

.f2b-sidebar-panel {
  pointer-events: auto;
}

.f2b-sidebar-backdrop {
  position: absolute;
  inset: 0;
  background: var(
    --brand-sidebar-backdrop,
    color-mix(in srgb, var(--ink, currentColor) 60%, transparent)
  );
  backdrop-filter: blur(2px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--f2b-widget-transition-duration, 240ms)
    var(--f2b-widget-transition-easing, ease);
}

/* Backdrop appears only in the take-over modes; companion has no scrim. */
.f2b-sidebar-overlay[data-open="true"]:is(
    [data-f2b-sidebar-mode="modal"],
    [data-f2b-sidebar-mode="focus"]
  )
  .f2b-sidebar-backdrop {
  opacity: 1;
  pointer-events: auto;
}

/* Open/close motion (brief 05): asymmetric slide on the platform
 * ease-out curve — the same cubic-bezier(0.16, 1, 0.3, 1) the
 * view-transition fade uses (and goban v7's own `--ease-out`).
 * Enter decelerates over 320 ms; exit accelerates away over 240 ms
 * (the base rule below is the closed state, so it carries the exit
 * timing). Tenants overriding the contract vars still win. */
.f2b-sidebar-panel {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  /* Round 3 item 1: the default rail grew ≥33% (420px → 560px) so the
   * conversation + all four quick-prompt chips breathe at rest. */
  width: min(560px, 100vw);
  max-width: 100vw;
  background: var(--surface-1, var(--cream, white));
  color: var(--ink, currentColor);
  font: inherit;
  display: grid;
  grid-template-rows: auto 1fr;
  transform: translateX(100%);
  transition: transform var(--f2b-widget-transition-duration, 240ms)
    var(--f2b-widget-transition-easing, cubic-bezier(0.55, 0, 0.85, 0.4));
  box-shadow: -8px 0 32px var(--f2b-widget-shadow, rgba(0, 0, 0, 0.18));
}

.f2b-sidebar-overlay[data-open="true"] .f2b-sidebar-panel {
  transform: translateX(0);
  /* Slide in on the platform ease-out; the companion↔focus width tween
   * (brief 08 § A) rides the same curve at ~280 ms. Width is animated
   * deliberately here (a fixed right-anchored single element) — the brief
   * specifies a width tween, not a transform, so the page edge the panel
   * occludes grows/shrinks rather than the panel sliding. */
  transition:
    transform var(--f2b-widget-transition-duration, 320ms)
      var(--f2b-widget-transition-easing, cubic-bezier(0.16, 1, 0.3, 1)),
    width 280ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* Per-mode geometry (brief 08 § A; widths retuned round 3 items 1 + 4).
 * Companion is the 560px right rail; focus is a deep-reading takeover whose
 * width is spent on content (the reading column below grows with it — no
 * dead gutters); modal (phone) is fullscreen. */
.f2b-sidebar-overlay[data-f2b-sidebar-mode="companion"] .f2b-sidebar-panel {
  width: min(560px, 100vw);
}

.f2b-sidebar-overlay[data-f2b-sidebar-mode="focus"] .f2b-sidebar-panel {
  width: min(900px, 86vw);
}

.f2b-sidebar-overlay[data-f2b-sidebar-mode="modal"] .f2b-sidebar-panel {
  width: 100vw;
}

/* Content crossfade on the companion↔focus toggle: the transient
 * `data-f2b-mode-shift` flag (cleared by JS after ~320 ms) re-runs the
 * opacity settle so the reflowed reading column fades rather than snaps.
 * Reuses the open settle keyframe (opacity only — reduced-motion already
 * nulls it below). */
.f2b-sidebar-overlay[data-f2b-mode-shift] .f2b-sidebar-body {
  animation: f2b-sidebar-content-settle 280ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* Panel content settles in just behind the slide (opacity only — a
 * transform here would break the sticky chat form and create scroll
 * traps). `backwards` fill keeps the body invisible through its
 * 60 ms stagger. */
.f2b-sidebar-overlay[data-open="true"] .f2b-sidebar-header,
.f2b-sidebar-overlay[data-open="true"] .f2b-sidebar-body {
  animation: f2b-sidebar-content-settle 360ms
    cubic-bezier(0.16, 1, 0.3, 1) backwards;
}

.f2b-sidebar-overlay[data-open="true"] .f2b-sidebar-body {
  animation-delay: 60ms;
}

@keyframes f2b-sidebar-content-settle {
  from {
    opacity: 0;
  }
}

.f2b-sidebar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--rule, var(--f2b-widget-border));
  font-size: 1rem;
  font-weight: 600;
}

.f2b-sidebar-header-actions {
  display: flex;
  gap: 0.5rem;
}

.f2b-sidebar-close,
.f2b-sidebar-expand {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--rule, var(--f2b-widget-border));
  border-radius: var(--f2b-widget-radius, 10px);
  padding: 0.4rem 0.6rem;
  cursor: pointer;
  font: inherit;
  color: inherit;
}

/* The expand control is hidden in modal (phone) and on bundles without
 * server-emitted expand/collapse labels — `hidden` must win over the
 * inline-flex above. */
.f2b-sidebar-expand[hidden] {
  display: none;
}

.f2b-sidebar-expand-icon {
  display: block;
  width: 1.05rem;
  height: 1.05rem;
  transition: transform var(--f2b-widget-transition-duration, 200ms)
    var(--f2b-widget-transition-easing, ease);
}

/* Companion (collapsed): chevrons point « toward the page edge that grows.
 * Focus (expanded): flip to » — the collapse direction. */
.f2b-sidebar-expand[aria-expanded="true"] .f2b-sidebar-expand-icon {
  transform: rotate(180deg);
}

.f2b-sidebar-expand:hover,
.f2b-sidebar-expand:focus-visible {
  border-color: color-mix(in srgb, var(--f2b-widget-accent) 55%, transparent);
}

.f2b-sidebar-expand:focus-visible {
  outline: var(--f2b-widget-focus-ring);
  outline-offset: 2px;
}

/* Sound toggle (brief 10 § D): a header control that opts the visit into the
 * muted-by-default audio cues. Matches the close/expand action chrome and is
 * visible in every mode (unlike the desktop-only expand control). The icon
 * swaps waves ↔ a mute slash off the aria-pressed / data-f2b-sound-on state;
 * enabled tints accent so the on-state reads at a glance. */
.f2b-widget__chat-sound {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--rule, var(--f2b-widget-border));
  border-radius: var(--f2b-widget-radius, 10px);
  padding: 0.4rem 0.6rem;
  cursor: pointer;
  font: inherit;
  color: inherit;
}

.f2b-widget__chat-sound-icon {
  display: block;
  width: 1.05rem;
  height: 1.05rem;
}

.f2b-widget__chat-sound:hover,
.f2b-widget__chat-sound:focus-visible {
  border-color: color-mix(in srgb, var(--f2b-widget-accent) 55%, transparent);
}

.f2b-widget__chat-sound:focus-visible {
  outline: var(--f2b-widget-focus-ring);
  outline-offset: 2px;
}

.f2b-widget__chat-sound[aria-pressed="true"] {
  color: var(--f2b-widget-accent);
  border-color: color-mix(in srgb, var(--f2b-widget-accent) 45%, transparent);
  background: color-mix(in srgb, var(--f2b-widget-accent) 8%, transparent);
}

.f2b-widget__chat-sound[data-f2b-sound-on="true"] .f2b-widget__chat-sound-slash {
  display: none;
}

.f2b-widget__chat-sound[data-f2b-sound-on="false"] .f2b-widget__chat-sound-waves {
  opacity: 0.2;
}

.f2b-sidebar-body {
  position: relative; /* containing block for the § D jump-to-latest pill */
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 1rem 1.25rem 1.5rem;
  /* The panel scrolls without a visible scrollbar (round 4): wheel,
   * trackpad, touch, and the jump-to-latest pill are the affordances —
   * a persistent classic scrollbar reads as desktop-app chrome inside
   * the premium panel. Scrolling itself is untouched. */
  scrollbar-width: none;
}

.f2b-sidebar-body::-webkit-scrollbar {
  display: none;
}

/* Scroll lock only in the take-over modes (brief 08 § A). Companion leaves
 * the page scrollable so the reader can browse the menu while asking about
 * it — the whole point of the non-modal dock. The lock rides on BOTH html
 * and body: with body alone, iOS Safari keeps the document scroller alive
 * and a touch-drag on the panel's non-scrollable chrome (header, composer,
 * chips) pans the page behind the overlay. */
html:has(body[data-f2b-sidebar-open="modal"]),
html:has(body[data-f2b-sidebar-open="focus"]),
body[data-f2b-sidebar-open="modal"],
body[data-f2b-sidebar-open="focus"] {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  html:not([data-f2b-force-motion]) .f2b-sidebar-overlay,
  html:not([data-f2b-force-motion]) .f2b-sidebar-panel,
  /* must outweigh the open-state transition override */
  html:not([data-f2b-force-motion])
    .f2b-sidebar-overlay[data-open="true"]
    .f2b-sidebar-panel,
  html:not([data-f2b-force-motion]) .f2b-sidebar-backdrop {
    transition: none;
  }
  html:not([data-f2b-force-motion]) .f2b-sidebar-backdrop {
    backdrop-filter: none;
  }
  html:not([data-f2b-force-motion])
    .f2b-sidebar-overlay[data-open="true"]
    .f2b-sidebar-header,
  html:not([data-f2b-force-motion])
    .f2b-sidebar-overlay[data-open="true"]
    .f2b-sidebar-body,
  /* companion↔focus width tween crossfade snaps */
  html:not([data-f2b-force-motion])
    .f2b-sidebar-overlay[data-f2b-mode-shift]
    .f2b-sidebar-body {
    animation: none;
  }
  /* The expand chevron flips instantly rather than rotating (brief 08 § A;
   * brief-05 reduced-motion grammar — final state at t=0). */
  html:not([data-f2b-force-motion]) .f2b-sidebar-expand-icon {
    transition: none;
  }
}

/* === Widget canvas sidebar chat (brief 12) === */

/* Chat panel inner content is `renderSidebarCanvas`-emitted. The
 * editorial chrome (header + subtitle + quick-prompt chips + message
 * stream + sticky form) lives inside the slot-09 `.f2b-sidebar-body`.
 * The slot-09 `.f2b-sidebar-title` carries the `.f2b-widget__chat-header`
 * (canvas-driven persona title + scope subtitle).
 *
 * Brand surfacing reuses the slot-08 `--brand-*` tokens and the slot-11
 * `--f2b-canvas-*` cascade (declared below for editorial chrome). No
 * new `FBrandTokenAlias`. */

.f2b-widget__chat-header {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  flex: 1 1 auto;
  min-width: 0;
}

.f2b-widget__chat-title {
  font: inherit;
  /* Persona voice (brief 09 § C): the panel title takes the tenant's display
   * face (goban → Playfair) at ~22px, so "The pitmaster" reads as a maître d'
   * rather than a form label. */
  font-family: var(--f2b-widget-display-font, inherit);
  font-size: var(--f2b-widget-title-size, 1.375rem);
  font-weight: 600;
  line-height: 1.15;
  color: currentColor;
  margin: 0;
}

.f2b-widget__chat-subtitle {
  font: inherit;
  font-size: var(--f2b-widget-subtitle-size, 0.875rem);
  line-height: 1.4;
  color: color-mix(in srgb, currentColor 70%, transparent);
  margin: 0;
}

.f2b-sidebar-body:has(> .f2b-widget__chat-empty),
.f2b-sidebar-body:has(> .f2b-widget__chat-messages),
.f2b-sidebar-body:has(> .f2b-widget__chat-form) {
  /* Stack welcome/chips → messages → sticky form within the slot-09 body
   * so the message stream takes the flex remainder and the form
   * anchors to the panel bottom. */
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding-bottom: 0;
}

/* First-open welcome composition (brief 08 § B). Centered in the empty
 * transcript: persona title + subtitle + quick-prompt grid over a faint
 * tenant ornament. The chip grid stays inside it; after the first turn the
 * §-C rules below morph this whole block into a compact chip bar. */
.f2b-widget__chat-empty {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  text-align: center;
  padding: 1.25rem 0.5rem;
  min-height: 0;
}

/* Decorative brand-mark slot — tenant theme CSS fills the custom property
 * (goban: the seal, faint). Sits behind the copy; default is no mark. */
.f2b-widget__chat-empty-ornament {
  position: absolute;
  inset: 0;
  background-image: var(--f2b-chat-empty-ornament, none);
  background-repeat: no-repeat;
  background-position: center;
  background-size: var(--f2b-chat-empty-ornament-size, contain);
  opacity: var(--f2b-chat-empty-ornament-opacity, 0.1);
  pointer-events: none;
}

.f2b-widget__chat-empty-title {
  position: relative;
  margin: 0;
  font: inherit;
  font-family: var(--f2b-widget-display-font, inherit);
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.15;
}

.f2b-widget__chat-empty-subtitle {
  position: relative;
  margin: 0 auto;
  max-width: 32ch;
  font: inherit;
  font-size: 0.95rem;
  line-height: 1.45;
  color: color-mix(in srgb, currentColor 72%, transparent);
}

/* The welcome chip grid sits relative so it layers above the ornament. */
.f2b-widget__chat-empty .f2b-widget__chat-quick-prompts {
  position: relative;
  width: 100%;
  margin-top: 0.25rem;
}

/* Empty state: the transcript collapses so the welcome owns the space. The
 * compact header keeps the persistent persona (title + subtitle + controls);
 * the welcome adds the centered greeting + chips below it. */
.f2b-sidebar-body:not(:has(.f2b-widget__chat-message))
  .f2b-widget__chat-messages {
  flex: 0 0 auto;
  min-height: 0;
}

.f2b-widget__chat-quick-prompts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

@media (min-width: 720px) {
  .f2b-widget__chat-quick-prompts {
    /* Stay 2-column on wider panels — the sidebar is single column at
     * a max of ~420px so a 2x2 grid keeps each chip touch-target-sized
     * and reads as a chat-app suggestion row. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.f2b-widget__chat-quick-prompt {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 44px;
  padding: 0.55rem 0.75rem;
  border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 4%, transparent);
  color: inherit;
  font: inherit;
  font-size: var(--f2b-widget-chip-size, 0.875rem);
  line-height: 1.2;
  cursor: pointer;
  transition-property: background-color, border-color, color, box-shadow,
    transform;
  transition-duration: var(--f2b-widget-transition-duration, 200ms);
  transition-timing-function: var(--f2b-widget-transition-easing, ease);
}

.f2b-widget__chat-quick-prompt:hover,
.f2b-widget__chat-quick-prompt:focus-visible {
  border-color: color-mix(in srgb, var(--f2b-widget-accent) 60%, transparent);
  background: color-mix(in srgb, var(--f2b-widget-accent) 8%, transparent);
}

.f2b-widget__chat-quick-prompt:hover {
  transform: translateY(-1px);
}

.f2b-widget__chat-quick-prompt:active {
  transform: translateY(0) scale(0.97);
  background: color-mix(in srgb, var(--f2b-widget-accent) 14%, transparent);
}

/* After the first user message (brief 08 § C; reworked round 3 items 1 + 2):
 * the welcome folds to one compact chip row — phones reclaim the ~⅓ viewport
 * the 2-col grid held mid-conversation. Title / subtitle / ornament retire;
 * the chip row (still inside the welcome, so insert-without-send is
 * unchanged) becomes a CENTERED, WRAPPING row: every chip stays fully
 * visible — overflow wraps to a second row, never a horizontal scroller.
 * The fold is a layout change — instant by construction, hence
 * reduced-motion-safe with no extra rule. */
.f2b-sidebar-body:has(.f2b-widget__chat-message) .f2b-widget__chat-empty {
  flex: 0 0 auto;
  justify-content: flex-start;
  gap: 0;
  padding: 0;
}

.f2b-sidebar-body:has(.f2b-widget__chat-message) .f2b-widget__chat-empty-title,
.f2b-sidebar-body:has(.f2b-widget__chat-message)
  .f2b-widget__chat-empty-subtitle,
.f2b-sidebar-body:has(.f2b-widget__chat-message)
  .f2b-widget__chat-empty-ornament {
  display: none;
}

.f2b-sidebar-body:has(.f2b-widget__chat-message)
  .f2b-widget__chat-quick-prompts {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem;
  margin-top: 0;
  padding-bottom: 0.15rem;
}

.f2b-sidebar-body:has(.f2b-widget__chat-message)
  .f2b-widget__chat-quick-prompt {
  flex: 0 0 auto;
  min-height: 38px;
}

.f2b-widget__chat-quick-prompt-emoji {
  display: inline-block;
  flex: 0 0 auto;
  font-size: 1.05em;
  line-height: 1;
}

.f2b-widget__chat-quick-prompt-label {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.f2b-widget__chat-messages {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 6rem;
  gap: 0.45rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-block: 0.25rem;
  /* Scrollbar-less like its `.f2b-sidebar-body` host (round 4) — the
   * message rail is the inner chat scroller. */
  scrollbar-width: none;
}

.f2b-widget__chat-messages::-webkit-scrollbar {
  display: none;
}

.f2b-widget__chat-message {
  width: fit-content;
  max-width: min(85%, 22rem);
  padding: 0.55rem 0.8rem;
  border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
  border-radius: 14px;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  font: inherit;
  font-size: var(--f2b-widget-message-size, 1rem);
  line-height: var(--f2b-widget-message-line, 1.5);
  /* Entrance (brief 05): each bubble rises in once on append. Streamed
   * deltas mutate the same node's text, so the animation never replays
   * on chunk boundaries by construction. No overshoot — calm register. */
  animation: f2b-widget-chat-message-enter 240ms cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes f2b-widget-chat-message-enter {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Rehydrated history (brief 10 § C): a transcript restored on soft-nav is the
 * resting state, not a fresh arrival — it renders with no entrance rise.
 * Declared after the base rule so it wins on equal specificity. */
.f2b-widget__chat-message--no-entrance {
  animation: none;
}

/* Focus mode (brief 08 § A; reworked round 3 item 4 — the expanded panel
 * must EARN its width). The expand control grew the panel to a deep-reading
 * takeover, so the extra width becomes reading room, not gutters: the
 * column tracks the panel (~780px of content inside the 900px panel), the
 * message measure and body size step up, the composer spans the full
 * column, and the chip row sits on one line. Companion + modal keep the
 * compact full-width rail. */
.f2b-sidebar-overlay[data-f2b-sidebar-mode="focus"] .f2b-sidebar-body {
  align-items: center;
  padding-inline: 2rem;
}

.f2b-sidebar-overlay[data-f2b-sidebar-mode="focus"] .f2b-widget__chat-empty,
.f2b-sidebar-overlay[data-f2b-sidebar-mode="focus"] .f2b-widget__chat-messages,
.f2b-sidebar-overlay[data-f2b-sidebar-mode="focus"] .f2b-widget__chat-form,
.f2b-sidebar-overlay[data-f2b-sidebar-mode="focus"]
  .f2b-widget__chat-quick-prompts {
  width: 100%;
  max-width: 780px;
}

/* Expanded welcome: the four chips ride one line (4-up grid); the compact
 * row (post-first-message) already centers + wraps and fits one line at
 * this column width. */
.f2b-sidebar-overlay[data-f2b-sidebar-mode="focus"]
  .f2b-widget__chat-empty
  .f2b-widget__chat-quick-prompts {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.f2b-sidebar-overlay[data-f2b-sidebar-mode="focus"] .f2b-widget__chat-message {
  font-size: 17px;
  max-width: min(88%, 620px);
}

.f2b-widget__chat-message--host {
  background: color-mix(in srgb, currentColor 5%, transparent);
  /* Host replies render sanitized Markdown (brief 09 § B): block children
   * (p / ul / li) carry the line structure, so the user-message `pre-wrap`
   * would surface marked's inter-tag newlines as stray whitespace. Use
   * normal flow and let the elements space themselves. */
  white-space: normal;
}

/* Markdown structure inside a host reply — tight chat-density spacing.
 * The allowlist (brief constraint 2) means only p / ul / ol / li / strong /
 * em / a / br / code ever appear here. First/last margins collapse so the
 * bubble padding stays even. */
.f2b-widget__chat-message--host > :first-child {
  margin-top: 0;
}

.f2b-widget__chat-message--host > :last-child {
  margin-bottom: 0;
}

.f2b-widget__chat-message--host p {
  margin: 0 0 0.5em;
}

.f2b-widget__chat-message--host ul,
.f2b-widget__chat-message--host ol {
  margin: 0.35em 0 0.5em;
  padding-inline-start: 1.3em;
}

.f2b-widget__chat-message--host li {
  margin: 0.15em 0;
}

.f2b-widget__chat-message--host li::marker {
  color: color-mix(in srgb, var(--f2b-widget-accent) 70%, currentColor);
}

.f2b-widget__chat-message--host a {
  color: var(--f2b-widget-accent);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  overflow-wrap: anywhere;
}

.f2b-widget__chat-message--host a:hover,
.f2b-widget__chat-message--host a:focus-visible {
  text-decoration-thickness: 2px;
}

.f2b-widget__chat-message--host code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  padding: 0.05em 0.3em;
  border-radius: 4px;
  background: color-mix(in srgb, currentColor 8%, transparent);
}

/* While a reply is streaming (the bubble's aria-live="off" in-flight
 * state), an accent caret marks the writing edge — now on the LAST block
 * (`> :last-child::after`) so it rides the end of the growing Markdown,
 * not a container edge below it (brief 09 § B). The `complete` handler
 * lifts the attribute, which retires the caret with no JS. */
.f2b-widget__chat-message--host[aria-live="off"] > :last-child::after {
  content: "";
  display: inline-block;
  width: 0.45em;
  height: 0.95em;
  margin-inline-start: 0.12em;
  vertical-align: -0.12em;
  border-radius: 1px;
  background: color-mix(in srgb, var(--f2b-widget-accent) 75%, transparent);
  animation: f2b-widget-chat-caret-pulse 950ms ease-in-out infinite;
}

@keyframes f2b-widget-chat-caret-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.2;
  }
}

.f2b-widget__chat-message--user {
  align-self: flex-end;
  border-color: color-mix(in srgb, var(--f2b-widget-accent) 35%, transparent);
  background: color-mix(in srgb, var(--f2b-widget-accent) 12%, transparent);
  color: var(--f2b-widget-accent);
}

/* Per-script CJK metrics axis (brief 09 § C). The served page sets
 * <html lang="ko|zh|ja">, which propagates so these match every widget
 * descendant on a CJK page. Korean / Japanese / Chinese body text wants
 * looser leading than the Latin 1.5 and no tracking. --f2b-widget-cjk-font
 * is the seam for a tenant-shipped face; its default `none` is an invalid
 * font-family value, so the declaration is ignored and the inherited stack
 * stands (no woff2 ships today — goban is en-only, ajahn-jayasaro zh is the
 * next consumer of this seam). */
.f2b-widget :lang(ko),
.f2b-widget :lang(zh),
.f2b-widget :lang(ja) {
  line-height: 1.6;
  letter-spacing: 0;
  font-family: var(--f2b-widget-cjk-font, inherit);
}

/* Retry chip beneath a retryable streamed error (brief 09 § E). Sits on the
 * host side under the error bubble; accent-outlined like the follow-up chips
 * so it reads as a sanctioned next action, not an alarm. */
.f2b-widget__chat-retry {
  display: flex;
  align-self: flex-start;
}

.f2b-widget__chat-retry-button {
  min-height: 36px;
  padding: 0.4rem 0.9rem;
  border: 1px solid
    color-mix(in srgb, var(--f2b-widget-accent) 45%, var(--f2b-widget-border));
  border-radius: 999px;
  background: color-mix(in srgb, var(--f2b-widget-accent) 8%, transparent);
  color: var(--f2b-widget-accent);
  font: inherit;
  font-size: var(--f2b-widget-chip-size, 0.875rem);
  cursor: pointer;
  transition-property: background-color, border-color, transform;
  transition-duration: var(--f2b-widget-transition-duration, 200ms);
  transition-timing-function: var(--f2b-widget-transition-easing, ease);
}

.f2b-widget__chat-retry-button:hover,
.f2b-widget__chat-retry-button:focus-visible {
  border-color: color-mix(in srgb, var(--f2b-widget-accent) 70%, transparent);
  background: color-mix(in srgb, var(--f2b-widget-accent) 14%, transparent);
}

.f2b-widget__chat-retry-button:active {
  transform: scale(0.97);
}

.f2b-widget__chat-retry-button:focus-visible {
  outline: var(--f2b-widget-focus-ring);
}

.f2b-widget__chat-typing {
  display: inline-flex;
  align-self: flex-start;
  gap: 0.3rem;
  padding: 0.6rem 0.8rem;
  border-radius: 14px;
  background: color-mix(in srgb, currentColor 6%, transparent);
}

/* Thinking indicator — glow_dots variant (brief 05, catalogued brief 10 § A):
 * the three dots glow like embers — accent-derived fill + a warm halo that
 * breathes on a slow rise. On goban the accent is the v7 ember red, so the
 * motif reads as coals on the grill; on any other tenant it stays that
 * tenant's accent (the shared contract carries no tenant branches). Scoped to
 * the variant attribute so the birds variant's slot <span>s never inherit it. */
.f2b-widget__chat-typing[data-f2b-thinking="glow_dots"] span {
  width: 0.42rem;
  height: 0.42rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--f2b-widget-accent) 80%, currentColor);
  opacity: 0.45;
  animation: f2b-widget-chat-ember-pulse 1.5s ease-in-out infinite;
}

/* Dots run 120° out of phase: with the narrow peak below, at most one
 * ember is hot at any instant and the wave reads unmistakably — even
 * in a still frame (operator review 2026-06-10: the first cut's broad
 * plateau + small offsets left all three dots co-lit most of the
 * time, reading as static). NEGATIVE delays (-2T/3, -T/3) start every
 * dot mid-cycle the moment the indicator appears — a positive stagger
 * leaves dots 2-3 frozen at base opacity through their first delay,
 * which is a large share of a typical model wait. */
.f2b-widget__chat-typing[data-f2b-thinking="glow_dots"] span:nth-child(2) {
  animation-delay: -1s;
}

.f2b-widget__chat-typing[data-f2b-thinking="glow_dots"] span:nth-child(3) {
  animation-delay: -0.5s;
}

@keyframes f2b-widget-chat-ember-pulse {
  0%,
  60%,
  100% {
    opacity: 0.22;
    transform: translateY(0) scale(0.8);
    box-shadow: 0 0 0 0 transparent;
  }
  30% {
    opacity: 1;
    transform: translateY(-0.2rem) scale(1.1);
    box-shadow: 0 0 0.5rem 0
      color-mix(in srgb, var(--f2b-widget-accent) 50%, transparent);
  }
}

/* Reduced-motion shimmer for the thinking dots: opacity only, no
 * transform, no glow. Declared next to the full pulse so the pair
 * reads as one design. */
@keyframes f2b-widget-chat-ember-shimmer {
  0%,
  100% {
    opacity: 0.3;
  }
  50% {
    opacity: 0.85;
  }
}

/* Stall affordance (brief 02's mid-stream gap timer) — same embers,
 * banked: a slower cycle reads as patient rather than busy. Calm,
 * non-spinner by construction. Delays stay one third of the slower
 * cycle so the wave keeps its phase separation. */
.f2b-widget__chat-typing--stall[data-f2b-thinking="glow_dots"] span {
  animation-duration: 2.6s;
}

.f2b-widget__chat-typing--stall[data-f2b-thinking="glow_dots"] span:nth-child(2) {
  animation-delay: -1.73s;
}

.f2b-widget__chat-typing--stall[data-f2b-thinking="glow_dots"] span:nth-child(3) {
  animation-delay: -0.87s;
}

/* Thinking indicator — typing_dots variant (round 3 item 10; the catalog
 * default, replacing the retired birds). Three muted dots ride a gentle
 * rise + opacity wave, 120° out of phase — the same negative-delay phase
 * trick as the embers, with no glow: calm, neutral, premium. The dots take
 * currentColor (not the accent) so the default reads quiet on any tenant.
 * The three <span>s + the variant attribute are the tenant theme slot —
 * goban's own stylesheet restyles them into rising grill-steam wisps. */
.f2b-widget__chat-typing[data-f2b-thinking="typing_dots"] span {
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 60%, transparent);
  opacity: 0.35;
  animation: f2b-widget-chat-typing-wave 1.4s ease-in-out infinite;
}

.f2b-widget__chat-typing[data-f2b-thinking="typing_dots"] span:nth-child(2) {
  animation-delay: -0.933s;
}

.f2b-widget__chat-typing[data-f2b-thinking="typing_dots"] span:nth-child(3) {
  animation-delay: -0.467s;
}

@keyframes f2b-widget-chat-typing-wave {
  0%,
  60%,
  100% {
    opacity: 0.3;
    transform: translateY(0) scale(0.92);
  }
  30% {
    opacity: 1;
    transform: translateY(-0.18rem) scale(1);
  }
}

/* Stall banks the cycle the same way the embers slow — patient, not busy
 * (the indicator contract holds across variants). */
.f2b-widget__chat-typing--stall[data-f2b-thinking="typing_dots"] span {
  animation-duration: 2.6s;
}

.f2b-widget__chat-typing--stall[data-f2b-thinking="typing_dots"] span:nth-child(2) {
  animation-delay: -1.73s;
}

.f2b-widget__chat-typing--stall[data-f2b-thinking="typing_dots"] span:nth-child(3) {
  animation-delay: -0.87s;
}

.f2b-widget__chat-breadcrumbs {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin: 0;
  padding: 0 0 0 1rem;
  color: color-mix(in srgb, currentColor 60%, transparent);
  font-size: var(--f2b-widget-chip-size, 0.875rem);
  list-style: disc;
}

/* Settle-collapse (brief 10 § B; reworked round 4): when a reply lands, the
 * runtime stamps this attribute and the list fades out before it's removed —
 * so multi-turn conversations stop accumulating ✓-line pileups. Opacity-only
 * ON PURPOSE: the previous height tween reflowed the whole transcript column
 * every frame for 400ms, a visible stutter on phone GPUs exactly when the
 * follow-up chips entered. The column now reflows once, at removal.
 * Reduced motion removes the node instantly (the runtime skips this path), so
 * the !important global animation kill never needs to reach here. */
.f2b-widget__chat-breadcrumbs[data-f2b-crumbs-collapsing] {
  opacity: 0;
  /* Fixed 240ms keeps the CSS in lockstep with the runtime's
   * BREADCRUMB_COLLAPSE_MS safety-net timeout (a tenant retune of the generic
   * transition token must not strand the node). */
  transition: opacity 240ms ease;
}

/* Streamed-turn breadcrumb progression (brief 02): items carry
 * data-f2b-crumb-state while phase events stream in — active pulses,
 * done shows a check. The non-streaming static list (no data attr)
 * keeps its plain disc rendering. */
.f2b-widget__chat-breadcrumbs li[data-f2b-crumb-state] {
  list-style: none;
  position: relative;
  margin-left: -1rem;
  padding-left: 1.2rem;
  /* Each phase rises in like a message and the active→done flip
   * crossfades instead of snapping (brief 05 § B smoothing). */
  animation: f2b-widget-chat-message-enter 200ms cubic-bezier(0.16, 1, 0.3, 1);
  transition: color var(--f2b-widget-transition-duration, 240ms)
    var(--f2b-widget-transition-easing, ease);
}

.f2b-widget__chat-breadcrumbs li[data-f2b-crumb-state]::before {
  position: absolute;
  left: 0;
  top: 0;
}

.f2b-widget__chat-breadcrumbs li[data-f2b-crumb-state="active"]::before {
  content: "•";
  animation: f2b-widget-chat-crumb-pulse 1.1s ease-in-out infinite;
}

/* Settled phases recede so the live one carries the eye. */
.f2b-widget__chat-breadcrumbs li[data-f2b-crumb-state="done"] {
  color: color-mix(in srgb, currentColor 62%, transparent);
}

.f2b-widget__chat-breadcrumbs li[data-f2b-crumb-state="done"]::before {
  content: "✓";
  color: color-mix(in srgb, var(--f2b-widget-accent) 80%, currentColor);
  animation: f2b-widget-chat-crumb-check-pop 240ms cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes f2b-widget-chat-crumb-pulse {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 0.95; }
}

@keyframes f2b-widget-chat-crumb-check-pop {
  from {
    opacity: 0.3;
    transform: scale(0.55);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Follow-up suggestion chips (brief 03): one server-emitted row per
 * assistant turn, rendered beneath the latest host bubble inside the
 * message stream and replaced on the next turn. Click SENDS (quick
 * prompts above prime the input instead — the asymmetry matches the
 * legacy sidebar). Pill recipe mirrors .f2b-widget__chat-quick-prompt;
 * the accent-tinted border reads as "tap to ask". */
.f2b-widget__chat-followups {
  display: flex;
  flex-wrap: wrap;
  align-self: flex-start;
  gap: 0.4rem;
  max-width: 100%;
}

.f2b-widget__chat-followup {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0.45rem 0.75rem;
  border: 1px solid color-mix(in srgb, var(--f2b-widget-accent) 30%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--f2b-widget-accent) 5%, transparent);
  color: inherit;
  font: inherit;
  font-size: 0.85rem;
  line-height: 1.25;
  text-align: left;
  overflow-wrap: anywhere;
  cursor: pointer;
  transition-property: background-color, border-color, color, box-shadow,
    transform;
  transition-duration: var(--f2b-widget-transition-duration, 200ms);
  transition-timing-function: var(--f2b-widget-transition-easing, ease);
  /* Staggered entrance: the row swaps in beneath the fresh reply with
   * each chip following 50 ms behind the last (server caps the row at
   * four). `backwards` holds the delayed chips invisible. */
  animation: f2b-widget-chat-chip-enter 260ms cubic-bezier(0.16, 1, 0.3, 1)
    backwards;
}

.f2b-widget__chat-followup:nth-child(2) {
  animation-delay: 50ms;
}

.f2b-widget__chat-followup:nth-child(3) {
  animation-delay: 100ms;
}

.f2b-widget__chat-followup:nth-child(4) {
  animation-delay: 150ms;
}

@keyframes f2b-widget-chat-chip-enter {
  from {
    opacity: 0;
    transform: translateY(5px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.f2b-widget__chat-followup:hover,
.f2b-widget__chat-followup:focus-visible {
  border-color: color-mix(in srgb, var(--f2b-widget-accent) 60%, transparent);
  background: color-mix(in srgb, var(--f2b-widget-accent) 10%, transparent);
}

.f2b-widget__chat-followup:hover {
  transform: translateY(-1px);
}

.f2b-widget__chat-followup:active {
  transform: translateY(0) scale(0.97);
  background: color-mix(in srgb, var(--f2b-widget-accent) 16%, transparent);
}

/* Jump-to-latest pill (brief 08 § D). Floats above the sticky composer when
 * the reader scrolls away from the bottom during a streamed reply; click (or
 * scrolling back near the bottom) re-pins the follow. The label is
 * server-emitted (`jump_to_latest_label`) — absent label, no pill (the
 * runtime never creates it; older bundles keep unconditional follow). */
.f2b-widget__chat-jump {
  position: absolute;
  left: 50%;
  bottom: var(--f2b-chat-jump-offset, 4.75rem);
  transform: translateX(-50%);
  z-index: 3;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  min-height: 36px;
  padding: 0.35rem 0.85rem;
  border: 1px solid color-mix(in srgb, var(--f2b-widget-accent) 45%, transparent);
  border-radius: 999px;
  background: var(--f2b-widget-accent);
  color: var(--f2b-canvas-accent-ink, var(--cream, #fff));
  font: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--f2b-widget-shadow);
  animation: f2b-widget-chat-jump-in 220ms cubic-bezier(0.16, 1, 0.3, 1);
}

.f2b-widget__chat-jump[hidden] {
  display: none;
}

.f2b-widget__chat-jump-arrow {
  font-size: 0.95em;
  line-height: 1;
}

.f2b-widget__chat-jump:hover,
.f2b-widget__chat-jump:focus-visible {
  filter: brightness(1.05);
}

.f2b-widget__chat-jump:focus-visible {
  outline: var(--f2b-widget-focus-ring);
  outline-offset: 2px;
}

@keyframes f2b-widget-chat-jump-in {
  from {
    opacity: 0;
    transform: translate(-50%, 6px);
  }
  to {
    opacity: 1;
    transform: translate(-50%, 0);
  }
}

.f2b-widget__chat-form {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.5rem;
  align-items: stretch;
  margin: 0;
  padding-top: 0.75rem;
  border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent);
  position: sticky;
  bottom: 0;
  background: inherit;
  padding-bottom: 1rem;
}

.f2b-widget__chat-input {
  width: 100%;
  min-height: var(--f2b-widget-control-height, 2.75rem);
  max-height: 12rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
  border-radius: 12px;
  background: color-mix(in srgb, currentColor 3%, transparent);
  color: inherit;
  font: inherit;
  line-height: 1.4;
  resize: vertical;
  overflow-y: auto;
}

.f2b-widget__chat-input:focus-visible {
  border-color: color-mix(in srgb, var(--f2b-widget-accent) 60%, transparent);
  outline: var(--f2b-widget-focus-ring);
  outline-offset: 2px;
}

.f2b-widget__chat-send {
  align-self: stretch;
  min-height: var(--f2b-widget-control-height, 2.75rem);
  padding: 0.55rem 1.1rem;
  border: 1px solid color-mix(in srgb, var(--f2b-widget-accent) 50%, transparent);
  border-radius: 12px;
  background: var(--f2b-widget-accent);
  /* On-accent ink (cream/white). The sidebar overlay is body-appended,
   * outside `.f2b-widget`, so the prior `--brand-accent-ink ->
   * --f2b-widget-surface -> transparent` chain rendered the label
   * invisible on the accent fill. `--f2b-canvas-accent-ink` is defined
   * on `:root` (so it reaches the overlay) and is the same on-accent ink
   * the full-page submit button uses. */
  color: var(--f2b-canvas-accent-ink, var(--cream, #fff));
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.f2b-widget__chat-send:hover:not(:disabled),
.f2b-widget__chat-send:focus-visible {
  filter: brightness(1.05);
}

/* Visible accent focus ring on the Send button (a11y parity with the input
 * + chips). The base outline-color is transparent so the ring is purely the
 * accent below — the on-accent ink is cream, which would otherwise read as a
 * pale default outline. */
.f2b-widget__chat-send {
  outline-color: transparent;
}

.f2b-widget__chat-send:focus-visible {
  outline: var(--f2b-widget-focus-ring);
  outline-offset: 2px;
}

.f2b-widget__chat-send:active:not(:disabled) {
  transform: scale(0.96);
}

.f2b-widget__chat-send:disabled {
  opacity: var(--f2b-widget-disabled-opacity, 0.55);
  cursor: not-allowed;
}

@media (max-width: 480px) {
  .f2b-widget__chat-quick-prompts {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Reduced motion: every brief-05 animation falls back to its final
 * state at t=0 — content is never motion-gated. Two deliberate
 * survivors: the streaming caret stays visible (steady, no blink),
 * and the thinking dots keep a gentle opacity-only shimmer — a frozen
 * "thinking" indicator reads as a hang (operator review 2026-06-10),
 * and a 7 px opacity fade carries none of the translate/scale/glow
 * motion WCAG 2.3.3 targets. The !important matches the global
 * `.f2b-widget *` animation kill so this exemption wins on the dots. */
@media (prefers-reduced-motion: reduce) {
  html:not([data-f2b-force-motion]) .f2b-widget__chat-quick-prompt,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-followup,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-send,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-input,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-retry-button,
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-breadcrumbs
    li[data-f2b-crumb-state] {
    transition: none;
  }
  html:not([data-f2b-force-motion]) .f2b-widget__chat-quick-prompt:hover,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-quick-prompt:active,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-followup:hover,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-followup:active,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-retry-button:active,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-send:active:not(:disabled) {
    transform: none;
  }
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-typing[data-f2b-thinking="glow_dots"]
    span {
    animation: f2b-widget-chat-ember-shimmer 1.8s ease-in-out infinite !important;
    transform: none;
    box-shadow: none;
  }
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-typing[data-f2b-thinking="glow_dots"]
    span:nth-child(2) {
    animation-delay: -1.2s !important;
  }
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-typing[data-f2b-thinking="glow_dots"]
    span:nth-child(3) {
    animation-delay: -0.6s !important;
  }
  /* typing_dots under reduced motion: the same opacity-only shimmer as the
   * embers (a frozen "thinking" indicator reads as a hang), translate/scale
   * stripped. */
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-typing[data-f2b-thinking="typing_dots"]
    span {
    animation: f2b-widget-chat-ember-shimmer 1.8s ease-in-out infinite !important;
    transform: none;
  }
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-typing[data-f2b-thinking="typing_dots"]
    span:nth-child(2) {
    animation-delay: -1.2s !important;
  }
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-typing[data-f2b-thinking="typing_dots"]
    span:nth-child(3) {
    animation-delay: -0.6s !important;
  }
  html:not([data-f2b-force-motion]) .f2b-widget__chat-message,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-followup,
  html:not([data-f2b-force-motion]) .f2b-widget__chat-jump,
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-breadcrumbs
    li[data-f2b-crumb-state],
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-breadcrumbs
    li[data-f2b-crumb-state="done"]::before {
    animation: none;
  }
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-message--host[aria-live="off"]
    > :last-child::after {
    animation: none;
    opacity: 0.6;
  }
  html:not([data-f2b-force-motion])
    .f2b-widget__chat-breadcrumbs
    li[data-f2b-crumb-state="active"]::before {
    animation: none;
    opacity: 0.8;
  }
}

/* === Widget canvas editorial (brief 11) === */

/* Canvas brand-token cascade. Resolves the tenant's accent / surface /
 * ink colors across the two known naming conventions:
 *  - `--brand-*` / `--ember` / `--cream` / `--ink` (Goban convention)
 *  - `--color-*` (Maru convention, shadcn-style)
 * Falls back to concrete defaults so the canvas never renders invisible
 * even when neither convention is defined.
 *
 * Centralizing here avoids the cross-property `currentColor` collision
 * (setting `background: currentColor` and `color: <value>` in the same
 * rule resolves both to `<value>` because `currentColor` reads the new
 * `color` from the same computed-style pass). */
:root,
[data-f2b-route-kind="synthesized-widget"],
.f2b-widget--full-page {
  --f2b-canvas-accent: var(
    --brand-accent,
    var(--color-accent, var(--ember, var(--color-ember, #b3261e)))
  );
  --f2b-canvas-accent-ink: var(
    --brand-accent-ink,
    var(--cream, var(--color-cream, #ffffff))
  );
  --f2b-canvas-ink: var(--ink, var(--color-ink, #1a1a1a));
  --f2b-canvas-surface: var(
    --surface-1,
    var(--cream, var(--color-cream, #faf6ec))
  );
  --f2b-canvas-error: var(
    --brand-error,
    var(--ember, var(--color-ember, #b3261e))
  );
}

/* Editorial chrome rendered into `.f2b-widget-hero` by
 * `renderFullPageCanvas`. All brand surfacing via the `--f2b-canvas-*`
 * cascade above. */

.f2b-widget__eyebrow {
  font: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--f2b-canvas-accent);
  margin: 0;
}

.f2b-widget__title {
  font: inherit;
  font-size: clamp(1.875rem, 4vw, 3.25rem);
  font-weight: 600;
  line-height: 1.05;
  /* Inherit from the synthesized wrapper / tenant body so the title
   * picks up the page's foreground color (cream on Goban's dark
   * theme, dark on Maru's cream theme). Using `--ink` here would
   * lock to the card-surface text color which kills contrast when
   * the title renders against the page background. */
  color: currentColor;
  margin: 0.75rem 0 0;
}

.f2b-widget__body {
  font: inherit;
  font-size: 1rem;
  line-height: 1.55;
  /* Same inheritance reasoning as the title; the muted alpha keeps
   * the body subordinate to the title without flipping to --ink. */
  color: color-mix(in srgb, currentColor 75%, transparent);
  margin: 1.25rem 0 0;
  max-width: 36rem;
}

.f2b-widget__stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
  margin: 2rem 0 0;
  max-width: 24rem;
  padding: 0;
}

.f2b-widget__stat-tile {
  display: grid;
  gap: 0.25rem;
  padding: 0.85rem 1rem;
  border-radius: var(--brand-radius-md, 6px);
  /* Hero-column tiles ride on the page background (cream on Maru,
   * deep ink on Goban). Use `currentColor`-derived chrome so the
   * tile reads as a subtle bit of page elevation on both themes
   * instead of a cream-card lift dropped on a dark page (which
   * looked like loose Post-it notes). */
  border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
  background: color-mix(in srgb, currentColor 4%, transparent);
}

.f2b-widget__stat-tile-label {
  font: inherit;
  font-size: 0.75rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  /* Shared muted token (72%), not a bespoke 60% — the lighter mix dropped
   * this label to ~3.9:1 on a light theme (sub-AA); every other muted label
   * already rides --f2b-widget-muted, which clears 4.5:1 on both themes. */
  color: var(--f2b-widget-muted);
  margin: 0;
}

.f2b-widget__stat-tile-value {
  font: inherit;
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.1;
  color: currentColor;
  margin: 0;
}

.f2b-widget__stat-tile-caption {
  font: inherit;
  font-size: 0.85rem;
  color: color-mix(in srgb, currentColor 60%, transparent);
  margin: 0.25rem 0 0;
}

/* Form card surface — rendered into `.f2b-widget-form-panel`. */

.f2b-widget__form-card {
  display: grid;
  gap: 1.25rem;
  padding: clamp(1.25rem, 3vw, 1.75rem);
  border: 1px solid
    color-mix(in srgb, var(--f2b-canvas-ink) 12%, transparent);
  border-radius: var(--brand-radius-lg, 8px);
  background: var(--f2b-canvas-surface);
  color: var(--f2b-canvas-ink);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--f2b-canvas-ink) 6%, transparent);
}

.f2b-widget__form {
  display: grid;
  gap: 1.25rem;
}

.f2b-widget__form-fields {
  display: grid;
  gap: 1.25rem;
}

.f2b-widget__field {
  display: grid;
  gap: 0.5rem;
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.f2b-widget__field-label {
  font: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--f2b-canvas-ink);
}

.f2b-widget__field-label-secondary {
  font: inherit;
  font-size: 0.85rem;
  color: color-mix(in srgb, var(--f2b-canvas-ink) 65%, transparent);
}

/* Optional badge — brief 13 cluster C2. Emitted as a sibling span on
 * .f2b-widget__field-label when `field.optional === true` and the
 * field is not a consent checkbox. The badge text comes from
 * `canvas.form.optional_text` (server-emitted, locale-aware) so the
 * runtime never hard-codes the English string. Smaller, lighter,
 * muted vs. the field label proper. */
.f2b-widget__field-label-optional {
  font: inherit;
  font-size: 0.75rem;
  font-weight: 400;
  margin-inline-start: 0.4rem;
  /* Shared muted token (72%), not a bespoke 55% — the lighter mix dropped the
   * "(optional)" badge to ~3.8:1 on a light theme (sub-AA). */
  color: var(--f2b-widget-muted);
}

.f2b-widget__field-input {
  font: inherit;
  display: block;
  width: 100%;
  min-height: 2.75rem;
  padding: 0.65rem 0.85rem;
  border-radius: var(--brand-radius-md, 6px);
  border: 1px solid
    color-mix(in srgb, currentColor 12%, transparent);
  /* Slightly recessed surface so the input reads as carved out of
   * the form card (cream-on-cream-shade on Maru; same effect on
   * Goban's cream form-card on dark page). Uses `currentColor`
   * derivation so the recess is consistent across themes. */
  background: color-mix(in srgb, currentColor 3%, var(--f2b-canvas-surface));
  color: var(--f2b-canvas-ink);
  transition: border-color 150ms ease, box-shadow 150ms ease;
}

.f2b-widget__field-input::placeholder {
  color: color-mix(in srgb, currentColor 45%, transparent);
}

.f2b-widget__field-input:focus-visible {
  outline: none;
  border-color: var(--f2b-canvas-accent);
  box-shadow: 0 0 0 3px
    color-mix(in srgb, var(--f2b-canvas-accent) 20%, transparent);
}

.f2b-widget__field-input--textarea {
  min-height: auto;
  resize: vertical;
}

/* Party-size pills — option (a) native radios + sr-only inputs. */

.f2b-widget__pill-group {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem;
  min-width: 0;
}

@media (min-width: 480px) {
  .f2b-widget__pill-group {
    grid-template-columns: repeat(7, minmax(0, 1fr));
  }
}

.f2b-widget__pill {
  position: relative;
  display: block;
  min-width: 0;
}

.f2b-widget__pill-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.f2b-widget__pill-label {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 0.5rem;
  border-radius: var(--brand-radius-md, 6px);
  border: 1px solid
    color-mix(in srgb, var(--f2b-canvas-ink) 15%, transparent);
  background: color-mix(
    in srgb,
    var(--f2b-canvas-surface) 80%,
    transparent
  );
  color: var(--f2b-canvas-ink);
  font: inherit;
  font-size: 1rem;
  font-weight: 500;
  cursor: pointer;
  transition: background 150ms ease, border-color 150ms ease, color 150ms ease;
}

.f2b-widget__pill-input:checked + .f2b-widget__pill-label {
  background: var(--f2b-canvas-accent);
  border-color: var(--f2b-canvas-accent);
  color: var(--f2b-canvas-accent-ink);
}

.f2b-widget__pill-input:focus-visible + .f2b-widget__pill-label {
  outline: none;
  box-shadow: 0 0 0 3px
    color-mix(in srgb, var(--f2b-canvas-accent) 25%, transparent);
}

.f2b-widget__pill-overflow {
  gap: 0.4rem;
  margin-top: 0.75rem;
}

/* The `hidden` attribute is the source of truth for the overflow
 * input's visibility. We only opt into the grid layout when the
 * pill is revealed; otherwise the attribute wins and the row
 * collapses entirely. */
.f2b-widget__pill-overflow:not([hidden]) {
  display: grid;
}

.f2b-widget__pill-overflow-input {
  font: inherit;
  width: 7rem;
  min-height: 2.75rem;
  padding: 0.5rem 0.75rem;
  border-radius: var(--brand-radius-md, 6px);
  border: 1px solid
    color-mix(in srgb, var(--f2b-canvas-ink) 15%, transparent);
  background: color-mix(
    in srgb,
    var(--f2b-canvas-surface) 92%,
    transparent
  );
  color: var(--f2b-canvas-ink);
}

/* Consent + terms */

.f2b-widget__consent {
  display: grid;
  gap: 0.5rem;
}

.f2b-widget__consent-label {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.65rem;
  align-items: start;
  font: inherit;
  font-size: 0.9rem;
  line-height: 1.5;
  color: color-mix(in srgb, var(--f2b-canvas-ink) 75%, transparent);
  cursor: pointer;
}

.f2b-widget__consent-input {
  width: 1.1rem;
  height: 1.1rem;
  /* The generic `.f2b-widget :where(input)` 2.75rem tap-target floor
   * would win over `height` (min-height beats height) and sink the
   * glyph ~15px below the first text line. The full-width label is the
   * tap target here, so the box opts out of the floor. */
  min-height: 1.1rem;
  /* Center on the label's first line box (0.9rem font × 1.5 line-height). */
  margin-top: calc((0.9rem * 1.5 - 1.1rem) / 2);
  accent-color: var(--f2b-canvas-accent);
}

.f2b-widget__consent-text a,
.f2b-widget__terms a {
  color: var(--f2b-canvas-accent);
  text-decoration: underline;
}

/* Disclosure block — terms_html renders as a recessed footer note
 * inside the form card (matches the legacy Maru waitlist reference
 * design). Slightly tinted background + hairline border separates it
 * from the form fields above and the submit CTA below. */
.f2b-widget__terms {
  font: inherit;
  font-size: 0.85rem;
  line-height: 1.55;
  color: color-mix(in srgb, currentColor 70%, transparent);
  padding: 0.85rem 1rem;
  background: color-mix(in srgb, currentColor 4%, transparent);
  border: 1px solid color-mix(in srgb, currentColor 10%, transparent);
  border-radius: var(--brand-radius-md, 6px);
}

/* Submit wrap — sticks to the bottom of the scrollable viewport
 * while the form-card is in view, matching the legacy Maru
 * `sticky bottom-0` pattern. On short forms (e.g. email-signup
 * with no party-size pills) the natural bottom IS the viewport
 * bottom, so the sticky has no visual cost. */
.f2b-widget__submit-wrap {
  position: sticky;
  bottom: 0;
  /* Bleed into the form-card's horizontal + bottom padding so the
   * sticky bar reads as part of the card chrome, not floating above
   * the card's bottom edge. */
  margin: 0 calc(-1 * clamp(1.25rem, 3vw, 1.75rem)) calc(-1 * clamp(1.25rem, 3vw, 1.75rem));
  padding: 0.85rem clamp(1.25rem, 3vw, 1.75rem);
  background: color-mix(in srgb, var(--f2b-canvas-surface) 95%, transparent);
  backdrop-filter: blur(8px);
  border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent);
  border-bottom-left-radius: var(--brand-radius-lg, 8px);
  border-bottom-right-radius: var(--brand-radius-lg, 8px);
}

/* Submit button + form status */

.f2b-widget__submit {
  appearance: none;
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;
  padding: 0.65rem 1.25rem;
  font: inherit;
  font-size: 1rem;
  font-weight: 600;
  color: var(--f2b-canvas-accent-ink);
  background: var(--f2b-canvas-accent);
  border: 1px solid var(--f2b-canvas-accent);
  border-radius: var(--brand-radius-md, 6px);
  cursor: pointer;
  transition: background 150ms ease, transform 150ms ease;
}

.f2b-widget__submit:hover:not([disabled]) {
  background: color-mix(in srgb, var(--f2b-canvas-accent) 88%, var(--f2b-canvas-ink));
}

.f2b-widget__submit:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px
    color-mix(in srgb, var(--f2b-canvas-accent) 25%, transparent);
}

.f2b-widget__submit[disabled] {
  opacity: 0.65;
  cursor: not-allowed;
}

.f2b-widget__form-status {
  font: inherit;
  font-size: 0.9rem;
  line-height: 1.5;
  color: color-mix(in srgb, var(--f2b-canvas-ink) 75%, transparent);
  margin: 0;
}

/* The form-status is `hidden` by default; the adapter unhides it only
 * when it has content (a server-emitted message) or when an error
 * variant is set. The :empty + :not([hidden]) selector avoids the
 * empty-block taking vertical space, while the variant pseudo gives
 * a CSS-only visual indicator for network errors that have no server
 * message to display. */
.f2b-widget__form-status[hidden] {
  display: none;
}

.f2b-widget__form-status:not([hidden]):empty {
  min-height: 1.5em;
}

.f2b-widget[data-f2b-state="error"] .f2b-widget__form-status {
  color: var(--f2b-canvas-error);
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.5rem;
  align-items: start;
  padding: 0.75rem 1rem;
  border-radius: var(--brand-radius-md, 6px);
  background: color-mix(
    in srgb,
    var(--f2b-canvas-error) 8%,
    transparent
  );
  border: 1px solid
    color-mix(in srgb, var(--f2b-canvas-error) 30%, transparent);
}

/* Inline alert icon — pure CSS, no fabricated copy. Renders an "!"
 * mark inside a filled circle, anchored at the start of the form
 * status banner so the user sees the failure state even when the
 * server returned no message. */
.f2b-widget[data-f2b-state="error"] .f2b-widget__form-status::before {
  content: "!";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: var(--f2b-canvas-error);
  color: var(--f2b-canvas-accent-ink);
  font-weight: 700;
  font-size: 0.85rem;
  flex-shrink: 0;
}

/* Outer card affordance: a 2px ember-red border + a small red shadow
 * so the failed state is unmissable across both light and dark
 * themes without depending on the inline form-status text. */
.f2b-widget[data-f2b-state="error"] .f2b-widget__form-card {
  border-color: var(--f2b-canvas-error);
  box-shadow:
    inset 0 0 0 1px var(--f2b-canvas-error),
    0 4px 12px color-mix(in srgb, var(--f2b-canvas-error) 20%, transparent);
}

/* Pulse the submit button briefly on error so the click registers
 * visibly even when the network failure happens too fast for the
 * disabled state to be noticed. */
@keyframes f2b-widget-error-pulse {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-4px); }
  40%, 80% { transform: translateX(4px); }
}

@media (prefers-reduced-motion: no-preference) {
  .f2b-widget[data-f2b-state="error"] .f2b-widget__submit {
    animation: f2b-widget-error-pulse 360ms ease-out 1;
  }
}

/* A force-motion tenant (item 9) keeps the error pulse even when the OS
 * requests reduced motion — the no-preference gate above can't match there,
 * so the opt-out carries its own copy. */
html[data-f2b-force-motion] .f2b-widget[data-f2b-state="error"] .f2b-widget__submit {
  animation: f2b-widget-error-pulse 360ms ease-out 1;
}

/* Server-emitted confirmation-pending success block */

.f2b-widget__success {
  display: grid;
  gap: 0.75rem;
  padding: clamp(1.25rem, 3vw, 1.75rem);
  text-align: center;
}

.f2b-widget__success-title {
  font: inherit;
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--f2b-canvas-ink);
  margin: 0;
}

.f2b-widget__success-body {
  font: inherit;
  font-size: 1rem;
  line-height: 1.55;
  color: color-mix(in srgb, var(--f2b-canvas-ink) 75%, transparent);
  margin: 0;
}

/* Audience lifecycle (email confirm / unsubscribe) result CTA — an anchor
 * inside the form card; without this it falls through to UA link blue,
 * which clashes with the canvas ink. */
.f2b-widget__lifecycle-cta {
  font: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--f2b-widget-accent);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  justify-self: center;
}

.f2b-widget__lifecycle-cta:hover {
  color: color-mix(in srgb, var(--f2b-widget-accent) 80%, var(--f2b-canvas-ink));
}

@media (prefers-reduced-motion: reduce) {
  html:not([data-f2b-force-motion]) .f2b-widget__field-input,
  html:not([data-f2b-force-motion]) .f2b-widget__pill-label,
  html:not([data-f2b-force-motion]) .f2b-widget__submit {
    transition: none;
  }
  html:not([data-f2b-force-motion]) .f2b-widget__stat-tile {
    transition: none;
  }
}

/* Scoped scrollability reset — pairs with slot-10's synthesized-route
 * attribute. The tenant homepage may legitimately rely on its own
 * `body { overflow: hidden }` chrome (sticky hero, full-bleed video,
 * etc.); we opt in only the synthesized full-page widget routes so the
 * form-card scrolls naturally on mobile / shorter viewports. */
html:has([data-f2b-route-kind="synthesized-widget"]),
body:has([data-f2b-route-kind="synthesized-widget"]) {
  overflow-y: auto;
  min-height: 100%;
  height: auto;
}

[data-f2b-route-kind="synthesized-widget"] {
  overflow-y: visible;
  min-height: 100dvh;
  height: auto;
  /* Push the synthesized content below any sticky tenant nav so the
   * eyebrow + title aren't clipped. `--f2b-nav-clearance` is set by
   * the runtime after measuring the actual rendered bottom of every
   * fixed/sticky `nav` / `header` / `[role=banner]` touching the
   * viewport top (+16px breathing room); the brand-variable fallback
   * applies when JS is disabled or no sticky nav is detected. */
  padding-top: var(
    --f2b-nav-clearance,
    calc(var(--brand-nav-height, var(--nav-h, 4rem)) + clamp(1rem, 4vw, 4rem))
  );
}

/* The route synthesizer (slot 10 § E) emits a placeholder
 * `<section class="f2b-synthesized-platform-feature">` carrying an
 * `<h1>` + `<p>` that duplicate the canvas-emitted eyebrow + title.
 * The placeholder is the progressive-enhancement fallback for
 * JS-disabled users; once the canvas mounts on the inner marker
 * (the runtime adds `.f2b-widget` to `[data-f2b]`), we hide the
 * placeholder text so the canvas-rendered editorial chrome is the
 * sole heading. `:has()` keeps the fallback visible when JS is off. */
[data-f2b-route-kind="synthesized-widget"]
  > section.f2b-synthesized-platform-feature:has(> [data-f2b].f2b-widget)
  > h1,
[data-f2b-route-kind="synthesized-widget"]
  > section.f2b-synthesized-platform-feature:has(> [data-f2b].f2b-widget)
  > p {
  display: none;
}

/* ===========================================================================
 * Announcements (feature slug `announcements`; admin-features brief 04 slice 2)
 *
 * A published, in-window announcement projects on /_runtime/features.json and
 * `mountAnnouncementsWidget` renders it here. The card is styled ENTIRELY from
 * the tenant's bridged --brand-* tokens (via the --f2b-widget-* contract), so
 * an injected notice wears the tenant's own warm-on-surface card look with no
 * per-tenant CSS authoring. This closes the "composition-plan-* classes have
 * zero CSS → unstyled" gap for structured content.
 *
 * Class family is `notice`, NOT `announcement`: `[class*="announcement"]` is
 * cosmetically hidden by ~40% of real visitors' ad/privacy filter lists
 * (reference_adblock_fragile_class_names). The `data-f2b="announcements"`
 * marker attribute is the F2B-proprietary mount hook and is not filter-fragile.
 *
 * Space is reserved (aspect-ratio image, intrinsic width/height) so a live
 * notice causes no layout shift (CLS); copy renders as textContent (XSS-safe).
 * =========================================================================== */
.f2b-widget--notice {
  display: grid;
  gap: 1.25rem;
  width: 100%;
}

/* The server-owned section heading above the notice cards — rendered ONLY when
 * the server projects one (gated on ≥1 live announcement); the runtime never
 * derives it from the card count. Reads as a section heading in the tenant's own
 * display voice + ink: it consumes the generated page's --sk-* display tokens so
 * it matches the other section headings, degrading to the widget tokens for
 * non-generated tenants. */
.f2b-notice-heading {
  margin: 0;
  color: var(--f2b-widget-text);
  font-family: var(--sk-font-display, var(--f2b-widget-display-font, inherit));
  font-size: var(--sk-display-h2, var(--f2b-widget-title-size, 1.375rem));
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.f2b-notice {
  display: grid;
  grid-template-columns: 1fr;
  overflow: hidden;
  background: var(--f2b-widget-surface);
  color: var(--f2b-widget-text);
  border: 1px solid var(--f2b-widget-border);
  border-radius: var(--f2b-widget-radius);
  box-shadow: var(--f2b-widget-shadow);
}

/* With a hero image: stacked on narrow viewports, image beside copy on wide. */
@media (min-width: 40rem) {
  .f2b-notice--media {
    grid-template-columns: minmax(0, 14rem) 1fr;
    align-items: stretch;
  }
}

.f2b-notice__media {
  position: relative;
  margin: 0;
  /* LQIP hint (dominant colour) while the bytes load — set inline by the
   * renderer; falls back to a faint tint of the surface. */
  background: var(--f2b-notice-lqip, color-mix(in srgb, currentColor 8%, transparent));
}

.f2b-notice__img {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: 3 / 2; /* reserve space → no CLS before the bytes load */
  object-fit: cover;
}

@media (min-width: 40rem) {
  .f2b-notice--media .f2b-notice__img {
    aspect-ratio: auto;
    min-height: 100%;
  }
}

.f2b-notice__content {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-width: 0;
  padding: 1.3rem 1.5rem;
}

.f2b-notice__title {
  margin: 0;
  color: var(--f2b-widget-accent);
  font-family: var(--f2b-widget-display-font, inherit);
  font-size: var(--f2b-widget-title-size, 1.375rem);
  line-height: 1.2;
}

.f2b-notice__body {
  margin: 0;
  /* Operator copy renders as textContent; preserve their line breaks. */
  white-space: pre-line;
  color: var(--f2b-widget-text);
  font-size: var(--f2b-widget-message-size, 1rem);
  line-height: var(--f2b-widget-message-line, 1.5);
}

.f2b-notice__cta {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  min-height: var(--f2b-widget-control-height);
  margin-top: 0.35rem;
  padding: 0.5rem 1.1rem;
  border-radius: var(--f2b-widget-radius);
  background: var(--f2b-widget-accent);
  color: var(--f2b-canvas-accent-ink, var(--cream, #fff));
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  transition-property: filter, transform;
  transition-duration: var(--f2b-widget-transition-duration, 200ms);
  transition-timing-function: var(--f2b-widget-transition-easing, ease);
}

.f2b-notice__cta:hover,
.f2b-notice__cta:focus-visible {
  filter: brightness(1.06);
}

.f2b-notice__cta:focus-visible {
  outline: var(--f2b-widget-focus-ring);
  outline-offset: 2px;
}

.f2b-notice__cta:active {
  transform: translateY(1px);
}

@media (prefers-reduced-motion: reduce) {
  .f2b-notice__cta {
    transition: none;
  }
  .f2b-notice__cta:active {
    transform: none;
  }
}

/* ===========================================================================
 * Menu (structured-content render) — admin-features brief 03 follow-up.
 *
 * The operator's published menu, rendered natively from the tenant's bridged
 * --brand-* tokens (NO per-tenant CSS authoring), mirroring the `.f2b-notice`
 * seam. Class family is `f2b-menu` (the F2B-proprietary, filter-safe prefix —
 * a bare `.price` / `.menu-item` risks ad/privacy filters,
 * reference_adblock_fragile_class_names). Item photos carry intrinsic
 * width/height + a reserved aspect-ratio → no layout shift (CLS); copy renders
 * as textContent (XSS-safe). The price + sold-out label are server-emitted
 * strings rendered verbatim.
 * =========================================================================== */
.f2b-widget--menu {
  display: block;
  width: 100%;
  /* Breathing room below the last section's footnote before whatever follows
   * (the page footer) — the editorial run shouldn't butt against it. */
  padding-block-end: clamp(3.5rem, 7vw, 6rem);
}

/* Editorial menu: an "Our Menu"-style section run — kicker + display title with
 * an accent emphasis word, a 2-column item list (name + secondary-script name +
 * accent price), and a closing footnote band. Contained editorial measure so
 * the run reads as a column, not edge-to-edge. All colour/type come from the
 * tenant's bridged tokens (goban → ember badge, gold rules/price, Playfair
 * titles, Barlow Condensed labels); a single-accent tenant degrades gracefully. */
.f2b-menu {
  display: grid;
  gap: clamp(3rem, 6vw, 5rem);
  max-width: 75rem;
  margin-inline: auto;
}

.f2b-menu__section {
  display: grid;
  gap: clamp(1.5rem, 3vw, 2.25rem);
}

.f2b-menu__section-head {
  display: grid;
  gap: 0.55rem;
  max-width: 62ch;
}

.f2b-menu__section-eyebrow {
  margin: 0;
  font-family: var(--f2b-widget-label-font, inherit);
  font-size: var(--f2b-widget-chip-size, 0.8125rem);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--f2b-widget-accent-alt);
}

.f2b-menu__section-title {
  margin: 0;
  color: var(--f2b-widget-text);
  font-family: var(--f2b-widget-display-font, inherit);
  font-size: clamp(2rem, 1.3rem + 2.6vw, 3rem);
  font-weight: 600;
  line-height: 1.08;
}

/* The server-emitted trailing emphasis word, set off in the secondary accent. */
.f2b-menu__section-title-em {
  color: var(--f2b-widget-accent-alt);
  font-style: italic;
}

.f2b-menu__section-desc {
  margin: 0;
  color: var(--f2b-widget-muted);
  font-size: var(--f2b-widget-message-size, 1rem);
  /* Light type on dark needs a touch more leading. */
  line-height: 1.6;
}

.f2b-menu__items {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Two editorial columns on wider viewports (the cuts/stews/sides treatment). */
@media (min-width: 48rem) {
  .f2b-menu__items {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: clamp(2rem, 4vw, 3.5rem);
  }
}

.f2b-menu__item {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.4rem;
  padding-block: 1.15rem;
  border-bottom: 1px solid var(--f2b-widget-border);
}

/* With a photo: thumbnail beside the copy on wider viewports, stacked narrow. */
@media (min-width: 40rem) {
  .f2b-menu__item--media {
    grid-template-columns: minmax(0, 8rem) 1fr;
    align-items: start;
    column-gap: 1.25rem;
  }
}

/* "cards" layout (server-selected for the combo-sets grid): boxed cards in a
 * 3-column grid. The card body stacks (heading → desc → price) with the price
 * block anchored at the card foot (rule + price + meta), like the source menu.
 * Higher specificity (two classes) overrides the list item's row treatment. */
@media (min-width: 48rem) {
  .f2b-menu__items--cards {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: 1.25rem;
    row-gap: 1.25rem;
  }
}

.f2b-menu__items--cards .f2b-menu__item {
  /* flex so the body fills the (stretched, equal-height) card and the price
   * block can sit at the foot via margin-top:auto. */
  display: flex;
  padding: 1.4rem 1.3rem;
  border: 1px solid var(--f2b-widget-border);
  border-radius: var(--f2b-widget-radius);
  background: color-mix(in srgb, currentColor 6%, transparent);
}

.f2b-menu__items--cards .f2b-menu__item-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
}

.f2b-menu__items--cards .f2b-menu__item-pricing {
  margin-top: auto;
  width: 100%;
  padding-top: 0.85rem;
  border-top: 1px solid var(--f2b-widget-border);
  justify-items: start;
  text-align: left;
}

.f2b-menu__items--cards .f2b-menu__item-price {
  font-size: 1.6rem;
}

.f2b-menu__media {
  margin: 0;
  overflow: hidden;
  border-radius: var(--f2b-widget-radius);
  /* LQIP hint (dominant colour) while the bytes load — set inline by the
   * renderer; falls back to a faint tint of the surface. */
  background: var(--f2b-menu-lqip, color-mix(in srgb, currentColor 8%, transparent));
}

.f2b-menu__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2; /* reserve space → no CLS before the bytes load */
  object-fit: cover;
}

/* The body is the item layout container: a 2-column grid (name | price on one
 * row, description spanning below) for the list; the card layout overrides it to
 * a flex column (see `.f2b-menu__items--cards .f2b-menu__item-body`). */
.f2b-menu__item-body {
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: 1rem;
  row-gap: 0.3rem;
  align-items: baseline;
  min-width: 0;
}

/* Editorial label above the name (e.g. "Best Value"): a quiet tinted accent
 * tag, not a solid fill — in a dense list a filled pill on every row shouts;
 * the outline-tint reads as a label and lets the item name lead. */
.f2b-menu__item-badge {
  grid-column: 1 / -1;
  grid-row: 1;
  justify-self: start;
  margin-bottom: 0.1rem;
  padding: 0.1rem 0.4rem;
  border-radius: 3px;
  border: 1px solid color-mix(in srgb, var(--f2b-widget-accent) 42%, transparent);
  background: color-mix(in srgb, var(--f2b-widget-accent) 12%, transparent);
  color: var(--f2b-widget-accent);
  font-family: var(--f2b-widget-label-font, inherit);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1.5;
}

/* Name (col 1) + price (col 2) share row 2; the description spans row 3 below. */
.f2b-menu__item-heading {
  grid-column: 1;
  grid-row: 2;
  min-width: 0;
}

.f2b-menu__item-name {
  margin: 0;
  color: var(--f2b-widget-text);
  font-family: var(--f2b-widget-display-font, inherit);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.25;
}

/* Secondary-script label (e.g. the Korean name) beneath the name — a quieter
 * tint of the editorial accent so it reads as the same voice, softer. */
.f2b-menu__item-subname {
  margin: 0.15rem 0 0;
  color: color-mix(in srgb, var(--f2b-widget-accent-alt) 62%, var(--f2b-widget-muted));
  font-size: 0.85rem;
  line-height: 1.3;
}

.f2b-menu__item-pricing {
  grid-column: 2;
  grid-row: 2;
  justify-self: end;
  display: grid;
  gap: 0.1rem;
  text-align: right;
}

.f2b-menu__item-price {
  color: var(--f2b-widget-accent-alt);
  font-family: var(--f2b-widget-display-font, inherit);
  font-size: 1.0625rem;
  /* Align the digits across rows. */
  font-variant-numeric: tabular-nums;
}

.f2b-menu__item-meta {
  font-family: var(--f2b-widget-label-font, inherit);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--f2b-widget-muted);
}

.f2b-menu__item-desc {
  grid-column: 1 / -1;
  grid-row: 3;
  margin: 0;
  /* Operator copy renders as textContent; preserve their line breaks. */
  white-space: pre-line;
  max-width: 46ch;
  color: var(--f2b-widget-muted);
  font-size: 0.9rem;
  line-height: 1.55;
}

/* Closing footnote band — a quiet full-width note under the section's items. */
.f2b-menu__section-footnote {
  margin: 0;
  padding: 0.85rem 1.1rem;
  border: 1px solid var(--f2b-widget-border);
  border-radius: var(--f2b-widget-radius);
  background: color-mix(in srgb, currentColor 4%, transparent);
  color: var(--f2b-widget-muted);
  font-size: 0.85rem;
  line-height: 1.5;
}

/* Sold-out: dim the row + show the server-emitted label as a pill. */
.f2b-menu__item[data-available='false'] {
  opacity: 0.62;
}

.f2b-menu__item-soldout {
  grid-column: 1 / -1;
  grid-row: 4;
  justify-self: start;
  display: inline-flex;
  align-items: center;
  margin-top: 0.1rem;
  padding: 0.15rem 0.6rem;
  border: 1px solid var(--f2b-widget-border);
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 6%, transparent);
  color: var(--f2b-widget-muted);
  font-size: var(--f2b-widget-chip-size, 0.875rem);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* ===========================================================================
 * Photo gallery (structured-content render) — admin-features brief 04-1.
 *
 * An operator-placed, ordered set of media-library photos, rendered natively
 * from the tenant's bridged --brand-* tokens (NO per-tenant CSS authoring),
 * mirroring the `.f2b-menu` seam. Class family is `f2b-gallery` (the filter-safe
 * prefix — a bare `.gallery` risks ad/privacy filters,
 * reference_adblock_fragile_class_names). Photos carry intrinsic width/height +
 * a reserved aspect-ratio → no layout shift (CLS); captions render as
 * textContent (XSS-safe). Reveal-on-scroll rides the f2b-scroll-reveal platform
 * primitive (reduced-motion-safe).
 * =========================================================================== */
.f2b-widget--gallery {
  display: block;
  width: 100%;
}

.f2b-gallery {
  max-width: 75rem;
  margin-inline: auto;
}

.f2b-gallery__grid {
  display: grid;
  /* Two columns on mobile (spec 15 2b-iii-D): a ~half-width tile (~180 px at a
   * 412 px viewport) picks the 400 w rendition (~37 KB) over the full-width
   * 800 w (~95 KB), cutting the Fast-3G byte volume that floored the /gallery
   * LCP. The tile `sizes` (render.rs / renderGalleryPhoto) is honest to this
   * grid. A platform-wide gallery design change — D12 makes the ≤ 2.5 s CWV
   * budget binding, not pixel parity. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(0.75rem, 2vw, 1.25rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 64rem) {
  .f2b-gallery__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* masonry (spec 19, G-039): the `--masonry` modifier the ADR-030 bake + the
 * runtime `renderGallery` emit off the marker's `data-f2b-gallery-layout`. CSS
 * multi-column varied-height tiles — 2 columns mobile, 3 at >=64rem (the grid
 * rhythm); the 4/3 crop is lifted below so tiles keep their natural height for
 * the varied emphasis. Byte-parallel to the site-kit `.sk-gallery--masonry`. */
.f2b-gallery__grid--masonry {
  display: block;
  columns: 2;
  column-gap: clamp(0.75rem, 2vw, 1.25rem);
}

@media (min-width: 64rem) {
  .f2b-gallery__grid--masonry {
    columns: 3;
  }
}

.f2b-gallery__grid--masonry .f2b-gallery__item {
  break-inside: avoid;
  margin-bottom: clamp(0.75rem, 2vw, 1.25rem);
}

.f2b-gallery__item {
  margin: 0;
}

.f2b-gallery__figure {
  display: grid;
  gap: 0;
  margin: 0;
}

.f2b-gallery__img {
  display: block;
  width: 100%;
  height: auto;
  /* Reserve a stable crop box before the bytes arrive (CLS-safe); the intrinsic
   * width/height attributes carry the true ratio for the browser's reservation,
   * this frames the grid cell. */
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--f2b-widget-radius, 10px);
  /* LQIP behind the loading image — the runtime/bake sets --f2b-gallery-lqip on
   * the figure; it inherits here so the box is never an empty flash. */
  background: var(--f2b-gallery-lqip, color-mix(in srgb, currentColor 8%, transparent));
}

/* Masonry tiles keep their natural aspect ratio (the varied-height effect); the
 * intrinsic width/height attributes still reserve the box, so no CLS. */
.f2b-gallery__grid--masonry .f2b-gallery__img {
  aspect-ratio: auto;
}

.f2b-gallery__caption {
  margin: 0;
  padding: 0.55rem 0.15rem 0;
  color: var(--f2b-widget-muted);
  font-size: var(--f2b-widget-chip-size, 0.875rem);
  line-height: 1.45;
}
