/* =====================================================================
   ETP COMPONENTS
   The reusable UI component library. Load AFTER tokens.css and base.css.
   Brand-book-page exhibit chrome (demos, swatches, code blocks, the LIVE
   panel, etc.) is intentionally NOT included — these are the components a
   product surface actually composes from.
   ===================================================================== */

/* =====================================================================
   BUTTONS — five roles
   primary (filled accent) · secondary (fills on hover) · icon · link · disabled
   ===================================================================== */
.cta {
  height: var(--control-height);
  padding: 0 var(--space-3);
  border: 1px solid var(--color-border);
  background: var(--color-offwhite);
  color: var(--color-black);
  font-size: 12px; letter-spacing: var(--tracking-label); text-transform: uppercase;
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-1);
  transition: background var(--transition-fast),
              color var(--transition-fast),
              border-color var(--transition-fast);
}
/* GUARDED: this hover flips to a full accent FILL, and on touch a tap can leave that fill
   painted until something else is touched -- reading as an active or selected state the user
   did not choose. The control keeps a visible border and background at rest, so gating costs
   no affordance.
   THIS COMMENT USED TO SAY "border-only and colour-only hovers elsewhere in this file are
   deliberately NOT gated: a stuck border is cosmetic." THE SHIPPED WIDGET DISAGREES AND WINS.
   classes.etpbjj.com gates 18 of its 20 hover rules, INCLUDING every border-only one
   (.etp-day, .etp-dropbtn, .etp-locsel). The two it leaves open are bare text links that
   recolour and change nothing else. So the line is the BOX, not the property count: a hover
   that changes a border, a background, a fill or a drawn mark is gated; a hover that only
   recolours text is not. Swept to match 2026-08-16. */
@media (hover: hover) {
  .cta:hover {
    background: var(--color-accent);
    color: var(--color-offwhite);
    border-color: var(--color-accent);
  }
}
/* Primary: one per surface. The single most important action. */
.cta--primary {
  background: var(--color-accent);
  color: var(--color-offwhite);
  border-color: var(--color-accent);
}
@media (hover: hover) {
  .cta--primary:hover {
    background: var(--color-black);
    border-color: var(--color-black);
    color: var(--color-offwhite);
  }
}
/* Disabled: show the label, not a missing button. Disabled is content. */
.cta:disabled, .cta[aria-disabled="true"] {
  background: var(--color-offwhite);
  color: var(--color-muted);
  border-color: var(--color-border);
  cursor: not-allowed;
}
/* Guarded with the hover it neutralises, so the pair cannot drift apart. */
@media (hover: hover) {
  .cta:disabled:hover, .cta[aria-disabled="true"]:hover {
    background: var(--color-offwhite);
    color: var(--color-muted);
    border-color: var(--color-border);
  }
}

.icon-btn {
  width: var(--control-height); height: var(--control-height);
  border: 1px solid var(--color-border);
  background: var(--color-offwhite);
  color: var(--color-accent);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--transition-fast),
              color var(--transition-fast),
              border-color var(--transition-fast);
}
@media (hover: hover) {
  .icon-btn:hover {
    background: var(--color-accent);
    color: var(--color-offwhite);
    border-color: var(--color-accent);
  }
}

.link {
  color: var(--color-muted);
  text-decoration: none;
  font-size: 14px;
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
  transition: color var(--transition-fast);
}
.link:hover { color: var(--color-accent); }

/* =====================================================================
   TOP NAVIGATION
   Wordmark left, jump-links right. Sticky to top. For documentation
   surfaces, longform marketing, and any page that earns a traditional
   nav. Use anchors when it navigates; spans only for inert exhibits.
   ===================================================================== */
.top-nav {
  width: 100%;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
}
.top-nav-brand { display: flex; align-items: center; gap: 12px; }
.top-nav-mark {
  font-family: var(--font-display); font-weight: 900;
  /* NAMED EXCEPTION to --tracking-display, left raw on purpose. This is the WORDMARK, and its
     -0.005em is optical tuning of a brand asset rather than a type-scale choice -- retuning it
     to -0.02em is a brand decision about the mark itself, not part of a tracking cleanup. Every
     other display setting in this file is on the token. Flagged 2026-08-06; change it only with
     Marcelo's sign-off, and log it if you do. */
  text-transform: lowercase; letter-spacing: -0.005em;
  font-size: 18px; color: var(--color-black);
}
.top-nav-divider { color: var(--color-muted); font-size: 14px; }
/* `.top-nav-sub` AND `.top-nav-link` ARE NEARLY IDENTICAL AND MUST NOT BE UNIFIED. The sub sets
   five declarations; the link sets the same five and adds `cursor: pointer` and a colour
   transition, plus its own `:hover`. The sub is a STRICT SUBSET -- it has no declaration of its
   own -- so every difference between them is an interaction affordance.
   THE MISSING ONES ARE CORRECT, NOT ABSENT. The sub is a static label beside the wordmark, and it
   says so three ways: no cursor declaration (computed `auto`, so it reads as text rather than as
   a target), no transition (computed 0s), and no `:hover` rule anywhere in the system. Adding the
   transition "for consistency" would animate a colour change nothing can trigger. Folding the two
   selectors into one would hand a static label a pointer cursor and a hover state, which is the
   real cost: it would look clickable and do nothing.
   THE IDENTITY IS NOT A COINCIDENCE OF THE TYPE SCALE. Of the nine tracked-uppercase rules across
   base.css and components.css, this is the ONLY pair agreeing on all four of size, weight, colour
   and tracking -- every other pair differs on at least one. Both have carried the same five values
   since `9fd0ad0` created them, and `5901b72` moved both onto `--tracking-label` in one sweep. If
   the nav's meta type changes, it changes in two places, deliberately. */
.top-nav-sub {
  font-size: 11px; letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-muted); font-weight: 500;
}
.top-nav-links { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.top-nav-link {
  font-size: 11px; letter-spacing: var(--tracking-label); text-transform: uppercase;
  font-weight: 500; color: var(--color-muted); cursor: pointer;
  transition: color var(--transition-fast);
}
.top-nav-link:hover { color: var(--color-accent); }

/* =====================================================================
   FORM INPUTS
   Motif 2: 1px border at rest, accent on hover/focus. Warning on invalid.
   ===================================================================== */
.field { display: flex; flex-direction: column; gap: var(--space-1); }
.field-label { font-size: 12px; letter-spacing: var(--tracking-label); text-transform: uppercase; font-weight: 400; }
.input {
  height: var(--control-height);
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  background: var(--color-offwhite);
  color: var(--color-black);
  font-size: 16px;
  width: 100%;
  transition: border-color var(--transition-fast);
}
/* GUARDED -- see the box rule in base.css's .has-accent-bar block. */
@media (hover: hover) {
  .input:hover { border-color: var(--color-accent); }
}
/* NO `outline: none` HERE, DELIBERATELY -- do not re-add it. The ring comes from base.css's
   `:focus-visible`, as it does for every other control. This rule is (0,2,0) against that rule's
   (0,1,0), so a suppression here wins silently and leaves only the accent border, which is
   identical to `.input:hover` above. */
.input:focus { border-color: var(--color-accent); }
/* NOT `--color-muted`, WHICH FAILS HERE. Muted on the field's off-white measures 1.49:1 shy of
   the 4.5:1 WCAG 1.4.3 needs for 16px text -- 2.64:1 -- and placeholder copy is text. This mix is
   the first 5% step that clears it, at 5.08:1, and it is mixed against `--color-offwhite`
   specifically because that is this control's own background. Muted itself is unchanged: it is a
   text colour at 30 sites across the system and the widget, and moving it is a palette decision
   of its own rather than a side effect of fixing a placeholder. */
.input::placeholder { color: color-mix(in oklab, var(--color-black) 60%, var(--color-offwhite)); }
/* THE INVALID STATE IS COLOUR-ONLY AT THE FIELD, AND CSS CANNOT FINISH IT. This border swap is
   the entire signal the control itself carries -- `docs/components.md` states the intent, "no
   icons" -- so a user who does not perceive the hue gets nothing from the field, and a screen
   reader user focusing it is told nothing at all. `.field-error` below carries the message and
   clears contrast at 5.06:1, but nothing joins them: the two have to be associated IN THE MARKUP,
   with `aria-invalid="true"` on the input and `aria-describedby` pointing at an `id` on the
   error. The documented example carries neither. NO RULE IS ADDED HERE because the fix is not a
   rule -- a selector cannot create the relationship, and inventing markup this system does not
   ship would be worse than recording where someone hits the constraint. WCAG 1.4.1 and 3.3.1. */
.field.is-invalid .input { border-color: var(--color-warning); }
.field-error { font-size: 12px; letter-spacing: var(--tracking-ui); color: var(--color-warning); }

/* Lockable field: input + inline EDIT button. For a confirmed selection
   that reads as committed but stays editable (pickup window, cart line). */
.field-lockable { position: relative; }
.field-lockable .input { padding-right: 92px; }
.lock {
  position: absolute; right: var(--space-1); top: 50%;
  transform: translateY(-50%);
  height: 32px; padding: 0 var(--space-2);
  background: transparent; color: var(--color-muted);
  font-size: 11px; letter-spacing: var(--tracking-label); text-transform: uppercase;
  border: 1px solid var(--color-border);
  transition: color var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast);
}
/* GUARDED -- border changes, so it is a box hover. */
@media (hover: hover) {
  .lock:hover { color: var(--color-accent); border-color: var(--color-accent); }
}
.field-lockable.is-locked .input {
  color: var(--color-muted);
  pointer-events: none;
  background: color-mix(in oklab, var(--color-black) 3%, transparent);
}

/* =====================================================================
   TOGGLE GROUP
   Segmented control: pick exactly one of two or three. Accent fill marks
   the chosen one. Use aria-pressed on each button.
   ===================================================================== */
.toggle-group { display: inline-flex; }
.toggle-btn {
  height: var(--control-height);
  padding: 0 var(--space-3);
  background: var(--color-offwhite);
  color: var(--color-black);
  font-size: 12px; letter-spacing: var(--tracking-label); text-transform: uppercase;
  border: 1px solid var(--color-border);
  position: relative; /* anchor for the selected button's z-index lift */
  transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}
/* Each button carries a full 1px border and overlaps the next by 1px, so the
   shared edge reads as a single seam. The selected button lifts above the seam
   so its accent border wins both edges and it reads as one solid accent square. */
.toggle-btn + .toggle-btn { margin-left: -1px; }
/* Hover is GUARDED and EXCLUDES the selected segment. On a control where fill means
   SELECTED, a hover tint stuck after a touch-tap implies a choice the user did not make,
   so the tint only applies where hover is real. And the selected segment must keep its
   fill BY INTENT: it previously survived only because [aria-pressed="true"] happened to
   come later at equal specificity (0,3,0), which a reorder would have broken silently.
   The disabled override stays after the tint rule inside the same block -- equal
   specificity, so it still wins on source order, and both are guarded together.
   NOTE: the sweep this comment used to defer HAS BEEN DONE (2026-08-16). Measured after it,
   with comments masked: 14 `:hover` rules in this file, 12 guarded, 2 unguarded -- `.link` and
   `.top-nav-link`, both bare text links that only recolour, matching the two the widget leaves
   open. `base.css` carries 2 more, both now guarded. Do not re-derive the old figure: the
   "other 13 are unguarded" reading was already stale before the sweep, because .cta,
   .cta--primary, .cta:disabled, .icon-btn and .select-option had been guarded since. */
@media (hover: hover) {
  .toggle-btn:not([aria-pressed="true"]):hover { background: var(--color-accent-tint); }
  .toggle-btn[disabled]:hover { background: var(--color-offwhite); }
}
.toggle-btn[aria-pressed="true"] {
  background: var(--color-accent);
  color: var(--color-offwhite);
  border-color: var(--color-accent);
  z-index: 1;
}
/* An accent ring on an accent fill is invisible. Flip it to off-white, exactly as
   .select-option[aria-selected="true"]:focus-visible already does. */
.toggle-btn[aria-pressed="true"]:focus-visible { outline-color: var(--color-offwhite); }
.toggle-btn[disabled] { color: var(--color-muted); cursor: not-allowed; }

/* =====================================================================
   DROPDOWNS
   Motif 1 in its native habitat. Rows hover to 6% tint + 3px accent bar;
   selected rows take the full accent fill. Use for dynamic/long option
   sets (locations, class types, time ranges); match trigger to
   --control-height. For a short, fixed set, use a toggle group instead.
   ===================================================================== */
.select { position: relative; display: inline-block; min-width: 280px; }
.select-trigger {
  height: var(--control-height);
  padding: 0 var(--space-2); width: 100%;
  border: 1px solid var(--color-border);
  background: var(--color-offwhite);
  color: var(--color-black);
  /* 13px + --tracking-ui, NOT 14px untracked. This is the 2026-08-06 type decision finally
     landing here: that row moved `.etp-dropitem` to "13px + --tracking-ui, matching
     .etp-dropbtn exactly", which is this component in the widget. The decision was general
     and only ever landed in the widget -- a transcription gap, not a second design choice. */
  font-size: 13px; letter-spacing: var(--tracking-ui);
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-1);
  transition: border-color var(--transition-fast);
}
/* GUARDED -- matches the widget's .etp-dropbtn:hover, which is gated. */
@media (hover: hover) {
  .select-trigger:hover { border-color: var(--color-accent); }
}
.select-trigger[aria-expanded="true"] { border-color: var(--color-accent); }
.select-trigger .icon-chevron { color: var(--color-accent); }
.select-trigger[aria-expanded="true"] .icon-chevron { transform: scaleY(-1); }

.select-panel {
  position: absolute; top: calc(100% - 1px); left: 0; right: 0;
  background: var(--color-offwhite);
  border: 1px solid var(--color-accent);
  z-index: 10;
  max-height: 320px; overflow-y: auto;
  display: none;
}
.select[data-open="true"] .select-panel { display: block; }

.select-option {
  height: 44px;
  padding: 0 var(--space-2) 0 var(--space-3);
  display: flex; align-items: center; gap: var(--space-1);
  font-size: 14px; cursor: pointer; position: relative;
  border-bottom: 1px solid color-mix(in oklab, var(--color-black) 5%, transparent);
}
.select-option:last-child { border-bottom: 0; }
/* Reuse Motif 1 (accent bar). */
.select-option::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0;
  width: 3px; background: var(--color-accent);
  transform: scaleY(0); transform-origin: top;
  transition: transform var(--transition-fast);
}
/* Guarded: same shape as .etp-dropitem in the widget, which is already gated -- a tap on a
   list row flashes the bar + tint before the action resolves. */
@media (hover: hover) {
  .select-option:hover::before { transform: scaleY(1); }
  .select-option:hover { background: var(--color-accent-tint); }
}
.select-option[aria-selected="true"] { background: var(--color-accent); color: var(--color-offwhite); }
/* Inset focus ring — the panel clips an outset ring at its overflow edge. */
.select-option:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
/* A selected (accent-filled) option needs an offwhite ring to stay visible. */
.select-option[aria-selected="true"]:focus-visible { outline-color: var(--color-offwhite); }

/* Checkbox primitive — used inside multi-select dropdowns. */
.checkbox {
  width: 16px; height: 16px;
  border: 1px solid var(--color-border);
  background: var(--color-offwhite);
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
}
.checkbox.is-checked { background: var(--color-accent); border-color: var(--color-accent); }
.checkbox.is-checked::before {
  content: ""; width: 6px; height: 10px;
  border-right: 1.5px solid var(--color-offwhite);
  border-bottom: 1.5px solid var(--color-offwhite);
  transform: rotate(45deg) translate(-1px, -1px);
}
.select-option[aria-selected="true"] .checkbox { background: var(--color-offwhite); border-color: var(--color-offwhite); }
.select-option[aria-selected="true"] .checkbox::before { border-color: var(--color-accent); }

/* =====================================================================
   DAY CARDS
   A card is a day. Tracked-uppercase day name + tabular display number.
   Press fills with accent. Day strip / any calendar surface.
   ===================================================================== */
/* THE DAY CARD IS LEFT-ALIGNED AND FLUID. DECIDED BY MARK 2026-08-16, as a design decision and
   not a transcription fix -- see the decision-log row. This block previously described a CENTRED
   card at a FIXED 88px, which no shipped surface has ever rendered; classes.etpbjj.com has shipped
   the left-aligned fluid card since the widget existed. Seven declarations moved, and Mark decided
   on the two that are visible to the eye -- alignment, and fluid rather than fixed -- with the
   other five following from them: min-width/min-height replacing the fixed width, align-items and
   justify-content doing the left-and-spread, even 16px padding rather than 16/8, and the 8px gap
   rather than 4px. A card sized by content is the SLOT; 88px was never it. */
.day {
  display: flex; flex-direction: column;
  align-items: flex-start; justify-content: space-between;
  min-width: 0;          /* fluid: never let a long count grow the cell out of its track */
  min-height: 86px;
  border: 1px solid var(--color-border);
  background: var(--color-offwhite);
  color: var(--color-black);
  text-align: left; padding: var(--space-2); gap: var(--space-1);
  transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}
/* GUARDED -- matches the widget's .etp-day:hover, which is gated. */
@media (hover: hover) {
  .day:hover { border-color: var(--color-accent); }
}
.day[aria-pressed="true"] { background: var(--color-accent); color: var(--color-offwhite); border-color: var(--color-accent); }
.day-name { font-size: 11px; letter-spacing: var(--tracking-label); text-transform: uppercase; }
.day-num {
  font-family: var(--font-display); font-weight: 900;
  font-size: 28px; letter-spacing: var(--tracking-display);
  font-variant-numeric: tabular-nums;
}

/* =====================================================================
   TAGS
   1px-bordered pill, 10px tracked-uppercase label, no fill at rest.
   Three registers: neutral, accent (active), warning.
   ===================================================================== */
.tag {
  display: inline-flex; align-items: center;
  height: 28px; padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  font-size: 10px; letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-black); background: transparent;
}
.tag--accent  { background: var(--color-accent); color: var(--color-offwhite); border-color: var(--color-accent); }
.tag--warning { color: var(--color-warning); border-color: var(--color-warning); }

/* =====================================================================
   MODAL
   The modal is the moment. Carries the standard 1px surface border.
   The page behind fades via the sibling .page-fade overlay (see
   base.css), never opacity inherited through the document tree. The
   two-line headline mass-centers as a block via width:min-content +
   per-line nowrap, keeping the hero's asymmetric stagger.

   HTML shape:
     <body>
       <main>...page...</main>
       <div class="page-fade" aria-hidden="true"></div>
       <dialog class="modal" id="...">...</dialog>
     </body>
   ===================================================================== */
.modal {
  border: 1px solid var(--color-border);
  padding: var(--modal-padding-y) var(--modal-padding-x);
  background: var(--color-offwhite);
  color: var(--color-black);
  max-width: var(--modal-max-width);
  width: calc(100% - 64px);
  margin: auto;
  position: fixed; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}
.modal[open] { display: flex; flex-direction: column; gap: var(--space-4); }
.modal::backdrop { background: transparent; }

.modal-head { display: flex; flex-direction: column; gap: var(--space-3); align-items: center; text-align: center; }
.conf-headline {
  font-family: var(--font-display); font-weight: 900;
  text-transform: lowercase; letter-spacing: var(--tracking-display); line-height: 0.95;
  font-size: clamp(36px, 6vw, 56px);
  width: min-content; margin: 0 auto;
}
.conf-headline > span { display: block; }
.conf-headline .line-1 { white-space: nowrap; }
.conf-headline .line-2 { padding-left: 1.1em; white-space: nowrap; }

.modal-body { font-size: 16px; line-height: 1.5; max-width: 48ch; margin: 0 auto; text-align: center; text-wrap: pretty; }
.modal-actions { display: flex; flex-direction: row; gap: var(--space-2); justify-content: center; flex-wrap: wrap; }

/* =====================================================================
   STATES — loading / empty / error
   Loading is a skeleton, not a spinner: opacity pulse only, no shimmer
   sweep. Empty redirects to the next class. Error names what failed and
   offers retry. The voice does not change because something went wrong.
   ===================================================================== */
.skel {
  background: color-mix(in oklab, var(--color-black) 6%, transparent);
  border: 1px solid var(--color-border);
  animation: skel-pulse var(--pulse-skeleton) infinite;
}
@keyframes skel-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.85; }
}
/* THIS RULE IS THE ONLY THING THAT STOPS THE SKELETON PULSING FOR A REDUCED-MOTION USER,
   AND NOTHING TESTS IT. The classes widget deleted its own byte-identical copy when it began
   loading this file (see index.html's skeleton block, which says so) -- correctly, because a
   hand-synced copy drifts. The cost is a live accessibility behaviour with a single source and
   no check between this line and the page: the widget's own remaining prefers-reduced-motion
   block covers .etp-picker transitions only. A Chromium harness cannot set the media query, so
   this is verifiable on a real device and nowhere else. Measured 2026-08-16: .skel's base rule
   supplies all 37 longhands uncontested -- no rule anywhere else in the document declares
   background, border or animation on .skel -- so deleting this leaves an unstoppable pulse.
   Do not remove it, and do not assume a harness pass says anything about it.
   VERIFIED ON A REAL DEVICE 2026-08-16, AND THE DEPENDENCY IS CLOSED. Mark turned Reduce Motion
   on in iOS settings and loaded classes.etpbjj.com: the skeleton blocks sit still, no pulse.
   Verification by OBSERVATION rather than measurement, which is the correct standard for this
   class of item -- the harness already holds every measurement and real-device BEHAVIOUR was the
   only thing missing (same standard as the 2026-08-02 picker-morph row). The paragraph above
   stays as written: it is still unreachable from any harness, and that is precisely why the
   device check was needed. Anyone editing this rule needs a phone, not a preview. */
@media (prefers-reduced-motion: reduce) {
  .skel { animation: none; }
}

/* =====================================================================
   COMPOSITION HELPER — overlap-by-position
   Two bordered controls share a single visual border by overlapping the
   right edge of one with the left of the next. Week navigation, etc.
   ===================================================================== */
.weeknav { display: flex; align-items: center; }
.weeknav > * + * { margin-left: -1px; }
