/* clipboard-inferred.css - GENERATED by scripts/build-assets.mjs from tokens.json,
   runtime/fonts/faces.json, runtime/src/*.css and runtime/extensions/*.css. Do not edit. */
/* tokens:generated:start */
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/Poppins-Regular.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/Poppins-Medium.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/Poppins-SemiBold.woff2") format("woff2");
}
:root {
  --cbi-colour-border: #F4F4F4;
  --cbi-colour-border-strong: #D0D0D0;
  --cbi-colour-canvas-bg: #FFFFFF;
  --cbi-colour-card-surface: #FFFFFF;
  --cbi-colour-category1: #588C65;
  --cbi-colour-category2: #7DB75E;
  --cbi-colour-category3: #C45994;
  --cbi-colour-category4: #62A2AB;
  --cbi-colour-chart1: #1AA78D;
  --cbi-colour-chart2: #F06501;
  --cbi-colour-chart3: #3846E8;
  --cbi-colour-chart4: #DCAC28;
  --cbi-colour-chart5: #9782F5;
  --cbi-colour-control-border: #767676;
  --cbi-colour-disabled-bg: #EDEDED;
  --cbi-colour-disabled-text: #B3B3B3;
  --cbi-colour-error: #B23846;
  --cbi-colour-error-ink: #8E1F2B;
  --cbi-colour-error-tint: #FEABAB;
  --cbi-colour-field-fill: #F4F4F4;
  --cbi-colour-focus-ring: #3C50F2;
  --cbi-colour-info-accent: #4349B4;
  --cbi-colour-info-banner-bg: #FBFBFB;
  --cbi-colour-link: #4055E6;
  --cbi-colour-neutral-pill: #F4F4F4;
  --cbi-colour-on-primary: #FFFFFF;
  --cbi-colour-primary: #3C50F2;
  --cbi-colour-primary-active: #232FB0;
  --cbi-colour-primary-alt: #6F7BF3;
  --cbi-colour-primary-hover: #2E40D8;
  --cbi-colour-primary-ink: #2A38B8;
  --cbi-colour-primary-tint: #D1D7FC;
  --cbi-colour-scrim: rgba(15, 17, 33, 0.48);
  --cbi-colour-sidebar-active-tint: #F4F4F4;
  --cbi-colour-sidebar-bg: #FFFFFF;
  --cbi-colour-skeleton-base: #EFEFEF;
  --cbi-colour-skeleton-highlight: #F7F7F7;
  --cbi-colour-success: #69946D;
  --cbi-colour-success-chart: #1AA78D;
  --cbi-colour-success-ink: #2F5334;
  --cbi-colour-success-strong: #3F6B45;
  --cbi-colour-success-tint: #C1FBCA;
  --cbi-colour-text-muted: #909090;
  --cbi-colour-text-muted-ink: #6B6B6B;
  --cbi-colour-text-on-dark: #FFFFFF;
  --cbi-colour-text-primary: #1D1D1D;
  --cbi-colour-text-secondary: #A2A2A2;
  --cbi-colour-text-secondary-ink: #5F5F5F;
  --cbi-colour-topbar-legacy-bg: #2F57A5;
  --cbi-colour-warning: #D8A25C;
  --cbi-colour-warning-chart: #F06501;
  --cbi-colour-warning-ink: #7A4E12;
  --cbi-colour-warning-tint: #FFF2DE;
  --cbi-elevation-border-weight: 1px;
  --cbi-elevation-card-shadow: 0 1px 2px rgba(15, 17, 33, 0.04);
  --cbi-elevation-dropdown-shadow: 0 4px 12px rgba(15, 17, 33, 0.12);
  --cbi-elevation-focus-ring-offset: 2px;
  --cbi-elevation-focus-ring-width: 2px;
  --cbi-elevation-modal-shadow: 0 8px 24px rgba(15, 17, 33, 0.16);
  --cbi-elevation-toast-shadow: 0 6px 16px rgba(15, 17, 33, 0.18);
  --cbi-layering-dropdown: 20;
  --cbi-layering-modal: 40;
  --cbi-layering-scrim: 30;
  --cbi-layering-shell: 0;
  --cbi-layering-sticky: 10;
  --cbi-layering-toast: 50;
  --cbi-motion-base: 200ms;
  --cbi-motion-easing: cubic-bezier(0, 0, 0.2, 1);
  --cbi-motion-fast: 120ms;
  --cbi-radii-card: 8px;
  --cbi-radii-choice-card: 12px;
  --cbi-radii-circle: 50%;
  --cbi-radii-control: 8px;
  --cbi-radii-modal: 8px;
  --cbi-radii-pill: 999px;
  --cbi-sizing-avatar-size: 32px;
  --cbi-sizing-board-column-width: 280px;
  --cbi-sizing-button-padding-x: 16px;
  --cbi-sizing-cart-width: 360px;
  --cbi-sizing-content-padding: 24px;
  --cbi-sizing-control-height: 36px;
  --cbi-sizing-control-height-sm: 28px;
  --cbi-sizing-icon-size: 16px;
  --cbi-sizing-icon-size-lg: 24px;
  --cbi-sizing-icon-size-sm: 14px;
  --cbi-sizing-input-padding-x: 12px;
  --cbi-sizing-modal-width: 560px;
  --cbi-sizing-modal-width-lg: 840px;
  --cbi-sizing-modal-width-sm: 400px;
  --cbi-sizing-popover-width: 280px;
  --cbi-sizing-sidebar-logo-size: 56px;
  --cbi-sizing-sidebar-row-height: 34px;
  --cbi-sizing-sidebar-width: clamp(148px, 14vw, 240px);
  --cbi-sizing-stat-donut-size: 128px;
  --cbi-sizing-table-row-height: 42px;
  --cbi-sizing-toast-width: 360px;
  --cbi-sizing-topbar-height: 40px;
  --cbi-spacing-lg: 16px;
  --cbi-spacing-md: 12px;
  --cbi-spacing-sm: 8px;
  --cbi-spacing-xl: 24px;
  --cbi-spacing-xs: 4px;
  --cbi-spacing-xxl: 32px;
  --cbi-spacing-xxxl: 48px;
  --cbi-typography-body-line-height: 1.3;
  --cbi-typography-body-size: 14px;
  --cbi-typography-body-weight: 400;
  --cbi-typography-button-label-line-height: 1.2;
  --cbi-typography-button-label-size: 14px;
  --cbi-typography-button-label-weight: 500;
  --cbi-typography-caption-line-height: 1.4;
  --cbi-typography-caption-size: 12px;
  --cbi-typography-caption-weight: 400;
  --cbi-typography-font-family: 'Poppins', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --cbi-typography-modal-title-line-height: 1.3;
  --cbi-typography-modal-title-size: 18px;
  --cbi-typography-modal-title-weight: 600;
  --cbi-typography-page-title-line-height: 1.3;
  --cbi-typography-page-title-size: 18px;
  --cbi-typography-page-title-weight: 600;
  --cbi-typography-pill-label-line-height: 1.4;
  --cbi-typography-pill-label-size: 12px;
  --cbi-typography-pill-label-weight: 500;
  --cbi-typography-section-title-line-height: 1.3;
  --cbi-typography-section-title-size: 18px;
  --cbi-typography-section-title-weight: 600;
  --cbi-typography-sidebar-label-line-height: 1.3;
  --cbi-typography-sidebar-label-size: 14px;
  --cbi-typography-sidebar-label-weight: 400;
  --cbi-typography-stat-number-line-height: 1.1;
  --cbi-typography-stat-number-size: 28px;
  --cbi-typography-stat-number-weight: 600;
  --cbi-typography-table-cell-line-height: 1.3;
  --cbi-typography-table-cell-size: 14px;
  --cbi-typography-table-cell-weight: 400;
  --cbi-typography-table-header-line-height: 1.2;
  --cbi-typography-table-header-size: 13px;
  --cbi-typography-table-header-weight: 600;
  --cbi-typography-top-bar-title-line-height: 1.2;
  --cbi-typography-top-bar-title-size: 14px;
  --cbi-typography-top-bar-title-weight: 500;
  --cbi-typography-weight-medium: 500;
  --cbi-typography-weight-regular: 400;
  --cbi-typography-weight-semibold: 600;
}
/* tokens:generated:end */
/* source: src/basic.css */
/*
 * basic.css - Clipboard (inferred) design system, category "basic".
 *
 * Owns: buttons, the underlined tab set, cards (including the toggle/choice card variant), the
 * icon sizing helper, the avatar, the emoji-flag sizing helper, and the inline text link. Draws on
 * docs/clipboard-ds/components.md sections 3 (Tabs), 4 (Buttons) and 10 (Cards and stat/donut
 * widgets), and docs/clipboard-ds/visual-system.md sections 1.1, 3, 4 and 5.
 *
 * Values come only from generated custom properties, calc() over them, and the permitted
 * structural literals (0, percentages, fr, auto, none, currentColor, transparent, inherit,
 * unitless numbers, and angle literals in transforms and gradients (deg, turn)).
 *
 * 0.4.0 (owner review, round 3): angle literals were added to that list rather than tokenised. An
 * angle is geometry, not a design decision -- a spinner turns a full circle, a tick mark is drawn
 * on the diagonal, a gradient runs down the box -- and the package already carries rotate(45deg)
 * in forms.css and 90deg gradients in status.css, so the convention was already established.
 */

/* Buttons (components.md section 4). Primary/outline consistently ~36-40px tall with generous
   horizontal padding; ~6-8px corner radius across every source that shows one at readable
   resolution. */
.cbi-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--cbi-spacing-xs);
  height: var(--cbi-sizing-control-height);
  padding: 0 var(--cbi-sizing-button-padding-x);
  border-radius: var(--cbi-radii-control);
  border: var(--cbi-elevation-border-weight) solid transparent;
  background: none;
  font-family: inherit;
  font-size: var(--cbi-typography-button-label-size);
  line-height: var(--cbi-typography-button-label-line-height);
  font-weight: var(--cbi-typography-button-label-weight);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

/* designed: no product evidence for a focus ring on any button (interaction patterns, "no source
   shows a keyboard focus ring anywhere"); the ring itself is the system-wide contract requirement
   (contract section 4, "Focus"). */
.cbi-button:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* Primary (solid): solid indigo fill, white bold label. */
.cbi-button--primary {
  background: var(--cbi-colour-primary);
  color: var(--cbi-colour-on-primary);
}

/* designed: no product evidence for a darker hover/pressed primary (visual-system.md section 1.1,
   open question 2 -- the frosted-looking dashboard-card buttons read as translucency, not a hover
   colour). primaryHover/primaryActive are designed tokens for exactly this case. */
.cbi-button--primary:hover {
  background: var(--cbi-colour-primary-hover);
}

/* designed: no product evidence */
.cbi-button--primary:active {
  background: var(--cbi-colour-primary-active);
}

/* Outline/secondary: white fill, ~1.5px indigo border, indigo text. */
.cbi-button--outline {
  background: var(--cbi-colour-card-surface);
  border-color: var(--cbi-colour-primary);
  color: var(--cbi-colour-primary);
}

/* designed: no product evidence for an outline-button hover treatment. */
.cbi-button--outline:hover {
  background: var(--cbi-colour-primary-tint);
}

/* Text/ghost (link-style): plain coloured text, no button chrome. */
.cbi-button--text {
  height: auto;
  padding: 0;
  border: none;
  color: var(--cbi-colour-link);
  text-decoration: underline;
}

/* Destructive: the trash-can icon is the only destructive-button token with more than one
   example (cart line-item delete, Create Time Period's "Delete"). */
.cbi-button--destructive {
  background: var(--cbi-colour-card-surface);
  border-color: var(--cbi-colour-error);
  color: var(--cbi-colour-error);
}

/* Green-confirm pair: solid green "Yes" seen once, in the Cancel Session confirm dialog
   (components.md section 4, Medium confidence, single 360p frame). The paired outline "No"
   reuses cbi-button--outline; do not assume this pair applies to every destructive action.
   0.4.0 (owner review): the sampled success hue only reads about 3.5:1 for a white label, below
   WCAG 2.2 AA 1.4.3's 4.5:1, so this one solid-fill-with-white-label use takes the stronger
   designed successStrong fill instead; colour.success is unchanged everywhere else (the status
   pill and any other success accent). */
.cbi-button--confirm {
  background: var(--cbi-colour-success-strong);
  color: var(--cbi-colour-on-primary);
}

/* Icon-only: the kebab menu trigger and the modal close X. Neither shows a distinct hover/pressed
   treatment in the evidence.
   0.4.0 (owner review, round 5): the glyph is the whole control here, with no label beside it, so
   it is a non-text indicator needing 3:1 (WCAG 2.2 AA 1.4.11). The sampled textSecondary grey reads
   about 2.55:1 on the white card and canvas surfaces an icon button normally sits on, so this
   variant takes the accessible secondary ink instead -- the same substitution textSecondaryInk
   exists for. The loading ring below inherits it through currentColor, which is why this file no
   longer carries a separate ring colour. Surfaces that paint themselves indigo override both at
   once: the top bar in chrome.css, the mid-drag board chip just below. */
.cbi-button--icon {
  width: var(--cbi-sizing-control-height);
  height: var(--cbi-sizing-control-height);
  padding: 0;
  border-color: transparent;
  color: var(--cbi-colour-text-secondary-ink);
}

/* 0.4.0 (owner review, round 5): the mid-drag board chip (data.css) is the one non-top-bar surface
   in the shipped demo that paints itself --cbi-colour-primary and carries an icon button, where the
   secondary ink above reads about 1.1:1. Same scoping idea as the top bar: on this one background,
   the on-primary ink, glyph and loading ring together. Placed here beside the variant it corrects
   rather than in data.css, so the exception sits with the rule it depends on. */
.cbi-board__card--dragging .cbi-button--icon {
  color: var(--cbi-colour-on-primary);
}

.cbi-button--sm {
  height: var(--cbi-sizing-control-height-sm);
  padding: 0 var(--cbi-spacing-sm);
}

/* designed: no product evidence for a loading treatment on any button. 0.4.0 (owner review, round
   2): cursor: progress alone never painted anything -- pointer-events: none stops the pointer
   reaching the button in the first place, so no cursor ever gets a chance to show, which left a
   loading button pixel-identical to an idle one. A spinner ring (cbi-button__spinner below, drawn
   beside the label rather than over it) gives a visible cue that never touches the label's own
   passing-contrast colour. */
.cbi-button--loading {
  pointer-events: none;
}

/* designed: no product evidence. A three-quarter ring in the button's own text colour
   (currentColor), sized to match the icon slot beside it; spins continuously while loading,
   replaced by a static ring under prefers-reduced-motion below. */
.cbi-button__spinner {
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  flex: none;
  border: var(--cbi-elevation-focus-ring-width) solid currentColor;
  border-right-color: transparent;
  border-radius: var(--cbi-radii-circle);
  animation: cbi-button-spin calc(var(--cbi-motion-base) * 4) linear infinite;
}

@keyframes cbi-button-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .cbi-button__spinner {
    animation: none;
  }
}

/* designed: no product evidence for a disabled button as a system (one Low-confidence data point
   only, the Create Time Period "Delete" button). aria-disabled preferred over native disabled per
   the tool's own accessibility rule; :disabled kept for any native control that still needs it.
   0.4.0 (owner review, round 3): a loading button is excluded. button() emits aria-disabled while
   loading (the control is genuinely not actionable yet), which used to drop this grey paint over
   it -- the variant's fill and ink were lost and the currentColor spinner, drawn in disabledText
   on disabledBg, fell to about 1.79:1, well under the 3:1 that a non-text indicator needs. A
   loading button keeps its own variant fill, so the ring clears 3:1 against it -- and the icon
   variant, which has no fill of its own, clears it through the accessible secondary ink the variant
   itself now paints (round 5), which the ring follows through currentColor. */
.cbi-button[aria-disabled="true"]:not(.cbi-button--loading),
.cbi-button:disabled:not(.cbi-button--loading) {
  background: var(--cbi-colour-disabled-bg);
  border-color: var(--cbi-colour-disabled-bg);
  color: var(--cbi-colour-disabled-text);
  cursor: default;
}

.cbi-button__icon {
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  flex: none;
  fill: currentColor;
}

.cbi-button__label {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Class map addition: the active filter count badge on a filter toggle button ("Show Filters
   (3)"). E: observed, section 7 and the list filtering pattern. Distinct from cbi-topbar__badge,
   which is the top bar's notification dot and is topbar scoped. */
.cbi-button__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  padding: 0 var(--cbi-spacing-xs);
  border-radius: var(--cbi-radii-pill);
  background: var(--cbi-colour-primary-tint);
  color: var(--cbi-colour-primary-ink);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  font-weight: var(--cbi-typography-weight-semibold);
}

/* Tabs (underlined set, components.md section 3). Active = bold indigo text + a solid indigo
   underline (~2-3px) under the full label width; inactive = plain grey text, no underline, no
   pill. A light 1px hairline sits under the whole strip. */
.cbi-tabs {
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-tabs__list {
  display: flex;
  gap: var(--cbi-spacing-lg);
  list-style: none;
  margin: 0;
  padding: 0;
}

.cbi-tabs__tab {
  display: inline-flex;
  align-items: center;
  padding: var(--cbi-spacing-sm) 0;
  background: none;
  border: none;
  border-bottom: calc(var(--cbi-elevation-border-weight) * 3) solid transparent;
  color: var(--cbi-colour-text-secondary-ink);
  font-family: inherit;
  font-size: var(--cbi-typography-body-size);
  font-weight: var(--cbi-typography-weight-medium);
  cursor: pointer;
}

.cbi-tabs__tab--active {
  color: var(--cbi-colour-primary);
  font-weight: var(--cbi-typography-weight-semibold);
  border-bottom-color: var(--cbi-colour-primary);
}

/* 0.4.0 (owner review, round 5): an href item inside a strip that also owns real tabs is the page
   you are on, not the selected tab, and tabs() no longer gives it the active modifier (nothing
   could ever take that modifier off again, so the strip painted two active items at once). It gets
   its own quieter current-page treatment instead: the primary ink and the active weight, no indigo
   underline, so the underline stays the selected tab's alone. The :not() keeps the treatment out of
   strips that own no tabs, where the active item -- anchor or button -- carries both this attribute
   and the modifier and should keep the full active paint. */
.cbi-tabs__tab[aria-current="page"]:not(.cbi-tabs__tab--active) {
  color: var(--cbi-colour-text-primary);
  font-weight: var(--cbi-typography-weight-semibold);
}

/* designed: no product evidence for a hover state on an inactive tab. */
.cbi-tabs__tab:hover {
  color: var(--cbi-colour-primary);
}

/* designed: no product evidence */
.cbi-tabs__tab:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-tabs__panel {
  padding-top: var(--cbi-spacing-lg);
}

/* Cards (components.md section 10). White, rounded corners, internal padding, header row with a
   title left and a control right. Elevation reads nearly flat on real-capture-style surfaces; the
   pronounced shadows in the pool belong to marketing composites (visual-system.md section 4). */
.cbi-card {
  background: var(--cbi-colour-card-surface);
  border-radius: var(--cbi-radii-card);
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  box-shadow: var(--cbi-elevation-card-shadow);
  padding: var(--cbi-sizing-content-padding);
}

.cbi-card--flat {
  border: none;
  box-shadow: none;
}

.cbi-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-sm);
  margin-bottom: var(--cbi-spacing-md);
}

.cbi-card__title {
  margin: 0;
  font-size: var(--cbi-typography-section-title-size);
  line-height: var(--cbi-typography-section-title-line-height);
  font-weight: var(--cbi-typography-section-title-weight);
}

.cbi-card__body {
  color: var(--cbi-colour-text-primary);
}

.cbi-card__footer {
  margin-top: var(--cbi-spacing-md);
  display: flex;
  gap: var(--cbi-spacing-sm);
  justify-content: flex-end;
}

/* Choice card: toggle/choice-card radius, one clear step up from the control radius
   (visual-system.md sections 3 and 4). Used for the roll-call Explained/Unexplained Absence
   cards and similar pick-one-of-N surfaces. 0.4.0 (owner review): card() always renders this as a
   plain <section>, never a button or a focusable element; the radio/checkbox composed into its
   body is the actual interactive part. cursor: pointer and a focus ring on the section itself were
   a false affordance, so both are gone -- the composed control carries its own cursor and ring. */
.cbi-card--choice {
  border-radius: var(--cbi-radii-choice-card);
}

/* Selected-state border/text reads lighter/greyer than the primary indigo in the source because a
   1-2px anti-aliased stroke blends into the white card; the true stroke is recorded as the primary
   indigo (visual-system.md section 1.1). */
.cbi-card--selected {
  border-color: var(--cbi-colour-primary);
  color: var(--cbi-colour-primary);
}

/* Icon sizing helper (components.md section 5, "Iconography"). Solid glyph icons, no thin-stroke
   outline style anywhere in the pool. */
.cbi-icon {
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  flex: none;
  fill: currentColor;
}

.cbi-icon--sm {
  width: var(--cbi-sizing-icon-size-sm);
  height: var(--cbi-sizing-icon-size-sm);
}

.cbi-icon--lg {
  width: var(--cbi-sizing-icon-size-lg);
  height: var(--cbi-sizing-icon-size-lg);
}

/* Avatar: circular initials badge, top bar and elsewhere (components.md section 1). */
.cbi-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-avatar-size);
  height: var(--cbi-sizing-avatar-size);
  border-radius: var(--cbi-radii-circle);
  background: var(--cbi-colour-sidebar-active-tint);
  color: var(--cbi-colour-primary);
  font-size: var(--cbi-typography-caption-size);
  font-weight: var(--cbi-typography-weight-semibold);
  flex: none;
}

/* Inline emoji-flag sizing (visual-system.md section 5, "emoji used interchangeably with
   solid-glyph icons for inline status/attendance flags"). Sizes the emoji glyph, not a drawn icon. */
.cbi-flag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
}

/* Inline text link (components.md section 4, "Text/ghost"). Used both as a standalone button
   variant and inline inside banner/body copy (e.g. the info banner's "Music Term 1 2023" link). */
.cbi-link {
  display: inline;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--cbi-colour-link);
  text-decoration: underline;
  cursor: pointer;
}

/* designed: no product evidence */
.cbi-link:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* Inert-control pulse (0.4.0, owner review, 2026-09-04). A click on a control carrying
   data-cbi-inert (a button, link or sidebar row that is not part of the prototype under test)
   makes controls.js add this class for 1600 ms to every wired control on the page, so a tester
   sees which actions the prototype answers. A moderator affordance, not product chrome: designed,
   no product evidence. Two cycles of a primary outline plus an expanding halo; a static outline
   for the same time under prefers-reduced-motion. */
.cbi-pulse {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-primary);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
  animation: cbi-pulse calc(var(--cbi-motion-base) * 4) var(--cbi-motion-easing) 2;
}

@keyframes cbi-pulse {
  from {
    box-shadow: 0 0 0 0 var(--cbi-colour-primary-tint);
  }
  to {
    box-shadow: 0 0 0 calc(var(--cbi-spacing-sm) + var(--cbi-spacing-xs)) transparent;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cbi-pulse {
    animation: none;
  }
}
/* source: src/chrome.css */
/*
 * chrome.css - Clipboard (inferred) design system, category "chrome".
 *
 * Owns: page shell, top app bar (coloured default era + legacy plain-white era), left sidebar
 * nav, page header. Draws on docs/clipboard-ds/components.md sections 1 (Top app bar) and 2 (Left
 * sidebar nav), and docs/clipboard-ds/visual-system.md sections 1.1-1.3 (colour), 3 (spacing and
 * layout) and 5 (iconography).
 *
 * Every value below is a generated custom property (see tokens.json for the confidence and
 * evidence behind each one) or a permitted structural literal (0, percentages, fr, auto, vh/vw,
 * unitless numbers). Nothing here is a literal colour, size, radius or shadow.
 *
 * This file also carries the one permitted non-.cbi- block: the base reset (contract section 4).
 */

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

body {
  margin: 0;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  color: var(--cbi-colour-text-primary);
  background: var(--cbi-colour-canvas-bg);
  -webkit-font-smoothing: antialiased;
}

/* Shell: persistent sidebar + a flexible main column, no fixed max-width content column observed
   anywhere (visual-system.md section 3, "Container / grid behaviour"). */
.cbi-shell {
  display: grid;
  grid-template-columns: var(--cbi-sizing-sidebar-width) 1fr;
  min-height: 100vh;
  width: 100%;
}

/* 0.4.2 (owner change): the sidebar stays pinned to the viewport while the main column scrolls,
   matching every full-page capture in components.md sections 1 and 2 (the chrome never moves).
   position: sticky needs the grid item's own height to be the page height for there to be
   anything to stick within, so the item is start-aligned (align-self: start) rather than
   stretched to the row's height; min-height: 100vh keeps the sidebar's own background filling
   the viewport on short pages, and it becomes the item's fixed height once sticky, so the nav
   scrolls inside itself (overflow-y: auto) when it is taller than the window rather than pushing
   the page down. */
.cbi-shell__sidebar {
  grid-column: 1;
  position: sticky;
  top: 0;
  align-self: start;
  min-height: 100vh;
  height: 100vh;
  overflow-y: auto;
}

.cbi-shell__main {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* 0.4.2 (owner change): pinned alongside the sidebar, above the scrolling content; the layering
   token is the one already reserved for this (tokens.json layering.sticky, "Sticky headers (top
   bar, table header)"). Anchored menus and popovers are position: fixed and read the page
   viewport directly, so they are unaffected; the scrim and modal likewise stay position: fixed
   and continue to cover the whole viewport regardless of where the bar or sidebar have scrolled
   to. */
.cbi-shell__topbar {
  flex: none;
  position: sticky;
  top: 0;
  z-index: var(--cbi-layering-sticky);
}

.cbi-shell__content {
  flex: 1;
  padding: var(--cbi-sizing-content-padding);
}

/* Top app bar (components.md section 1). Full-width solid indigo bar, centred white page title,
   circular avatar badge at the far right, is the newer/default era per contract rule "build the
   newer indigo coloured-top-bar treatment as the default". */
.cbi-topbar {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--cbi-sizing-topbar-height);
  padding: 0 var(--cbi-sizing-content-padding);
  background: var(--cbi-colour-primary);
  color: var(--cbi-colour-on-primary);
}

/* Legacy plain-white header, one source only (S83K1InwUNw). Reads its own dedicated token and
   never re-scopes --cbi-colour-primary (contract section 4, "variants change which token they
   read, never a token's value"). */
.cbi-topbar--legacy {
  justify-content: flex-start;
  background: var(--cbi-colour-topbar-legacy-bg);
  color: var(--cbi-colour-on-primary);
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

/* 0.4.0 (owner review, 2026-09-04): the top bar carries no brand slot. Earlier releases rendered
   a crest glyph plus a product name at its left edge, which the evidence record never shows:
   components.md section 1 places the school crest "outside the bar itself, in the sidebar". The
   crest now renders through sidebar({ brand }) below. */
.cbi-topbar__title {
  font-size: var(--cbi-typography-top-bar-title-size);
  line-height: var(--cbi-typography-top-bar-title-line-height);
  font-weight: var(--cbi-typography-top-bar-title-weight);
  text-align: center;
}

.cbi-topbar__actions {
  position: absolute;
  right: var(--cbi-sizing-content-padding);
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
}

/* 0.4.0 (owner review, round 3): the wrapper is sized to a full control height, not to an icon.
   It holds a rendered icon button (one control height square), so an icon-sized wrapper let the
   control overflow it and drew the circular hover wash smaller than the thing being hovered. */
.cbi-topbar__action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-control-height);
  height: var(--cbi-sizing-control-height);
  border-radius: var(--cbi-radii-circle);
  color: var(--cbi-colour-on-primary);
  background: none;
  border: none;
  cursor: pointer;
  position: relative;
}

/* designed: no product evidence for a hover/focus treatment on a top-bar action icon (components.md
   section 1, "no click-through, dropdown, or search affordance was ever observed"). */
.cbi-topbar__action:hover {
  background: var(--cbi-colour-primary-hover);
}

/* 0.4.0 (owner review, round 2): the previous .cbi-topbar__action:focus-visible rule here was
   dead code -- .cbi-topbar__action is a plain, non-focusable <span> wrapper around the real button
   (topbar()'s actionsHtml in chrome.js), so :focus-visible on it could never match anything; the
   button inside it is what actually receives focus. Removed; covered by
   .cbi-topbar .cbi-button:focus-visible below. */

/* 0.4.0 (owner review): the bar is painted --cbi-colour-primary and the system-wide focus ring
   reads the same hex (--cbi-colour-focus-ring), so a focused control on this one background was
   invisible. Swaps the ring to the on-primary ink here, scoped to the top bar only. */
.cbi-topbar .cbi-button:focus-visible {
  outline-color: var(--cbi-colour-on-primary);
}

/* 0.4.0 (owner review, round 3): .cbi-topbar__action sets a colour on the wrapper span, but the
   icon button rendered inside it paints its own ink (.cbi-button--icon in basic.css), which wins on
   the element that actually draws the glyph -- so a top-bar icon action read a grey glyph on the
   indigo bar, about 2.27:1, under the 3:1 that a non-text control needs. Same scoping idea as the
   focus ring above: on this one background, the on-primary ink. Round 5: the loading ring follows
   currentColor again, so this one rule now covers the glyph and the ring together and the separate
   spinner override that sat below it is gone. */
.cbi-topbar .cbi-button--icon {
  color: var(--cbi-colour-on-primary);
}

/* Small red notification dot on the avatar's edge (components.md section 1). */
.cbi-topbar__badge {
  position: absolute;
  top: 0;
  right: 0;
  width: var(--cbi-spacing-sm);
  height: var(--cbi-spacing-sm);
  border-radius: var(--cbi-radii-circle);
  background: var(--cbi-colour-error);
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-primary);
}

.cbi-topbar__avatar {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-avatar-size);
  height: var(--cbi-sizing-avatar-size);
  border-radius: var(--cbi-radii-circle);
  background: var(--cbi-colour-canvas-bg);
  color: var(--cbi-colour-primary);
  font-size: var(--cbi-typography-caption-size);
  font-weight: var(--cbi-typography-weight-semibold);
}

/* Left sidebar nav (components.md section 2). White background, icon+label rows, two blocks
   separated by a thin divider, footer credit line. */
.cbi-sidebar {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--cbi-colour-sidebar-bg);
  border-right: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  padding: var(--cbi-spacing-md);
}

/* School crest at the top of the sidebar, above the nav list (components.md section 2: "school-
   crest logo top (own-tenant branding)"; Incidents.png, TUoVXM43U2M-t38). Centred; the image is
   sized by height so a portrait or landscape crest both fit inside the narrowest sidebar. 0.4.0
   (owner review): this replaces the top bar's brand slot. */
.cbi-sidebar__brand {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--cbi-spacing-sm) 0 var(--cbi-spacing-md);
}

.cbi-sidebar__brand-link {
  display: inline-flex;
  border-radius: var(--cbi-radii-control);
}

/* designed: no product evidence */
.cbi-sidebar__brand-link:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-sidebar__logo {
  display: block;
  width: auto;
  height: var(--cbi-sizing-sidebar-logo-size);
  max-width: 100%;
  object-fit: contain;
}

/* The neutral placeholder crest, used when no image is supplied or the image fails to load. */
.cbi-sidebar__crest {
  display: inline-flex;
  width: var(--cbi-sizing-sidebar-logo-size);
  height: var(--cbi-sizing-sidebar-logo-size);
  color: var(--cbi-colour-primary);
}

.cbi-sidebar__crest .cbi-icon {
  width: 100%;
  height: 100%;
}

.cbi-sidebar__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-xs);
}

.cbi-sidebar__section {
  margin-top: var(--cbi-spacing-md);
  padding-top: var(--cbi-spacing-md);
  border-top: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-sidebar__item {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
  height: var(--cbi-sizing-sidebar-row-height);
  padding: 0 var(--cbi-spacing-sm);
  border-radius: var(--cbi-radii-control);
  color: var(--cbi-colour-text-secondary-ink);
  text-decoration: none;
  cursor: pointer;
}

/* Active row: a plain neutral-grey wash, NOT a tinted blue/lavender fill -- a corroborated,
   non-obvious finding (visual-system.md section 1.3). Only the icon and label turn primary
   indigo; the wash itself never changes token. */
.cbi-sidebar__item--active {
  background: var(--cbi-colour-sidebar-active-tint);
}

.cbi-sidebar__item--active .cbi-sidebar__icon,
.cbi-sidebar__item--active .cbi-sidebar__label {
  color: var(--cbi-colour-primary);
}

/* designed: no product evidence for a hover state on an inactive sidebar item (components.md
   section 2, "Hover state has no evidence anywhere"). Reuses the active wash rather than inventing
   a new tint. */
.cbi-sidebar__item:hover {
  background: var(--cbi-colour-sidebar-active-tint);
}

/* designed: no product evidence */
.cbi-sidebar__item:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-sidebar__icon {
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  color: var(--cbi-colour-text-secondary-ink);
  flex: none;
  fill: currentColor;
}

.cbi-sidebar__label {
  font-size: var(--cbi-typography-sidebar-label-size);
  line-height: var(--cbi-typography-sidebar-label-line-height);
  font-weight: var(--cbi-typography-sidebar-label-weight);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cbi-sidebar__footer {
  margin-top: auto;
  padding-top: var(--cbi-spacing-md);
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  color: var(--cbi-colour-text-muted-ink);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
}

/* Page header (composite chrome named in brief Phase 3; not separately numbered in
   components.md, built to the same anatomy as the card/modal title treatment). */
.cbi-page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--cbi-spacing-md);
  margin-bottom: var(--cbi-spacing-lg);
}

.cbi-page-header__title {
  font-size: var(--cbi-typography-page-title-size);
  line-height: var(--cbi-typography-page-title-line-height);
  font-weight: var(--cbi-typography-page-title-weight);
  margin: 0;
}

/* W2.5 review: textSecondary read at 2.55:1 on the white/canvas surface this sits on, below
   WCAG 2.2 AA 1.4.3's 4.5:1 for body text; the subtitle is read as ordinary page copy ("Term 3,
   Week 6"), not a caption, so it takes the accessible ink companion like the sidebar labels do. */
.cbi-page-header__subtitle {
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-secondary-ink);
  margin: var(--cbi-spacing-xs) 0 0;
}

/* W2.5 review: textMuted read at 3.19:1 on white, below 4.5:1; the meta line carries real
   information ("Updated 2 minutes ago"), not decoration, so it takes the ink companion. */
.cbi-page-header__meta {
  color: var(--cbi-colour-text-muted-ink);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
}

.cbi-page-header__actions {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
  flex: none;
}
/* source: src/commerce.css */
/* commerce.css - Clipboard (inferred) runtime, commerce category
   (slice S5 CSS B). Owns: cbi-cart, cbi-activity-card.
   Evidence: components.md section 19 (Checkout cart with countdown hold
   timer) and the Activity Selection card notes in section 13 (Status
   pills/badges); visual-system.md sections 1.9, 1.10, 3, 4. Every value is
   a var(--cbi-*) custom property, or calc()/percentages/keywords over one,
   per contract section 1.1 and section 4. No component rule declares a
   --cbi-* property. Australian English; no em dashes. */

/* ---------------------------------------------------------------------- */
/* cbi-cart - components.md section 19. "Your Items" header with a
   clock-icon countdown chip, line items with a trash-can delete icon and a
   right-aligned item total, a summary distinguishing "Total due now" from
   "Total due separately", a Payment Method card, and a sticky footer with
   the pay amount and a full-width primary button. Only the default state
   (items present, timer counting down) is evidenced; an expired timer, an
   empty cart and a post-payment screen have no evidence. */

.cbi-cart {
  /* components.md section 19: the cart renders as a narrow side panel
     (Activity Selection 2.jpg, ERlsad5zdBc-t130). W2 composed this from
     --cbi-sizing-toast-width, the nearest token to the observed panel
     width, because the canonical sizing key list had no dedicated
     side-panel width token; tokens.json now carries sizing.cartWidth
     (360px, the same rendered value) so this reads that directly instead
     of a borrowed reference to an unrelated toast component. */
  display: flex;
  flex-direction: column;
  width: var(--cbi-sizing-cart-width);
  border-radius: var(--cbi-radii-card);
  background: var(--cbi-colour-card-surface);
  box-shadow: var(--cbi-elevation-modal-shadow);
  overflow: hidden;
}

.cbi-cart__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-sm);
  padding: var(--cbi-spacing-md) var(--cbi-spacing-lg);
  background: var(--cbi-colour-primary);
  color: var(--cbi-colour-on-primary);
}

.cbi-cart__timer {
  display: inline-flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-on-primary);
}

/* designed: no product evidence for an urgent-threshold colour change; the
   three captures in the pool show counting-down values (14:51 / 14:49 /
   9:03), all under 15 minutes, with no distinct colour treatment observed
   at any of them */
.cbi-cart__timer--urgent {
  color: var(--cbi-colour-error);
}

.cbi-cart__items {
  display: flex;
  flex-direction: column;
  padding: var(--cbi-spacing-sm) var(--cbi-spacing-lg);
}

.cbi-cart__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-sm);
  padding: var(--cbi-spacing-sm) 0;
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-cart__item-name {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  font-weight: var(--cbi-typography-body-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-cart__item-meta {
  display: block;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-cart__item-price {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  color: var(--cbi-colour-text-primary);
}

.cbi-cart__remove {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-icon-size-lg);
  height: var(--cbi-sizing-icon-size-lg);
  border-radius: var(--cbi-radii-control);
  color: var(--cbi-colour-text-muted);
  cursor: pointer;
  fill: currentColor;
  transition: background var(--cbi-motion-fast) var(--cbi-motion-easing);
}

/* designed: no product evidence */
.cbi-cart__remove:hover {
  background: var(--cbi-colour-error-tint);
  color: var(--cbi-colour-error);
}

/* designed: no product evidence */
.cbi-cart__remove:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* Class map addition: the Payment Method section wrapping a choice card (a radio-style "New
   Card - Use a new payment card" row). E: observed, section 19. */
.cbi-cart__payment {
  padding: var(--cbi-spacing-md) var(--cbi-spacing-lg);
  border-top: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-cart__summary {
  /* components.md section 19: "Total due now" and "Total due separately"
     are two distinct due-amount concepts on the same screen. */
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-xs);
  padding: var(--cbi-spacing-md) var(--cbi-spacing-lg);
  border-top: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-cart__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--cbi-spacing-sm);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  color: var(--cbi-colour-text-secondary-ink);
}

.cbi-cart__total {
  font-weight: var(--cbi-typography-section-title-weight);
  color: var(--cbi-colour-text-primary);
}

/* Class map addition, W2.5 restructure: components.md section 19's sticky footer bar -
   "To pay now [amount]" plus a full-width primary "Pay Now" button - is structurally distinct
   from __summary (the due-now/due-separately breakdown above it) and from __actions (the button
   row inside it). W2 built the whole bar as __actions alone; __footer is now the outer sticky bar
   and __actions is the inner button row, matching the amount-plus-button(s) anatomy the record
   describes. */
.cbi-cart__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-md);
  padding: var(--cbi-spacing-md) var(--cbi-spacing-lg);
  border-top: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-cart__actions {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
}

@media (prefers-reduced-motion: reduce) {
  .cbi-cart__remove {
    transition: none;
  }
}

/* ---------------------------------------------------------------------- */
/* cbi-activity-card - components.md section 13's Activity Selection card
   family (Piano/Trumpet/Violin/Singing Tuition), coloured by an accent bar
   under the card matching the activity's category. Selection itself is
   shown as the adjoining status pill flipping colour (status.css owns
   cbi-pill), not a documented change on the card container. */

.cbi-activity-card {
  display: flex;
  flex-direction: column;
  border-radius: var(--cbi-radii-card);
  background: var(--cbi-colour-card-surface);
  box-shadow: var(--cbi-elevation-card-shadow);
  overflow: hidden;
  cursor: pointer;
  transition: box-shadow var(--cbi-motion-fast) var(--cbi-motion-easing);
}

/* designed: no product evidence */
.cbi-activity-card:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-activity-card__image {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--cbi-colour-sidebar-active-tint);
}

.cbi-activity-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-xs);
  padding: var(--cbi-spacing-lg);
}

.cbi-activity-card__title {
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-section-title-size);
  line-height: var(--cbi-typography-section-title-line-height);
  font-weight: var(--cbi-typography-section-title-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-activity-card__meta {
  /* components.md section 13 (via product-map.md's Activity Selection
     screens): sport/term line plus "N places remaining" caption. */
  display: block;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-activity-card__price {
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-stat-number-size);
  line-height: var(--cbi-typography-stat-number-line-height);
  font-weight: var(--cbi-typography-stat-number-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-activity-card__category {
  /* components.md section 13: "accent bar under an activity card",
     colour-coded per activity, visual-system.md section 1.10. */
  height: calc(var(--cbi-elevation-border-weight) * 4);
}

.cbi-activity-card__category--category1 {
  background: var(--cbi-colour-category1);
}

.cbi-activity-card__category--category2 {
  background: var(--cbi-colour-category2);
}

.cbi-activity-card__category--category3 {
  background: var(--cbi-colour-category3);
}

.cbi-activity-card__category--category4 {
  background: var(--cbi-colour-category4);
}

/* designed: no product evidence for a selected-card container treatment;
   components.md section 13 documents only the adjoining pill changing
   colour, never the card itself */
.cbi-activity-card--selected {
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-primary);
}

/* designed: no product evidence for a sold-out/full card treatment
   anywhere in the pool */
.cbi-activity-card--full {
  opacity: 0.6;
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  .cbi-activity-card {
    transition: none;
  }
}
/* source: src/data.css */
/* data.css - Clipboard (inferred) runtime, data category (slice S5 CSS B).
   Owns: cbi-table, cbi-stat, cbi-roll, cbi-board.
   Evidence: components.md sections 9 (Tables), 10 (Cards and stat/donut
   widgets), 16 (Roll-marking control), 17 (Drag-and-drop team columns);
   visual-system.md sections 1.3, 1.6, 1.8, 1.9, 1.10, 3, 4.
   Every value is a var(--cbi-*) custom property, or calc()/percentages/
   keywords over one, per contract section 1.1 and section 4. No component
   rule declares a --cbi-* property; the generated :root block is the only
   declaration site. Australian English; no em dashes. */

/* ---------------------------------------------------------------------- */
/* cbi-table - components.md section 9. Comfortable rows, no zebra
   striping anywhere in the evidence pool, thin hairline row dividers, an
   inline-edit convention (grey rounded box) distinct from plain read-only
   text, and a two-line person row wherever a name sits above a year/ID
   caption. Sorting-active, multi-select and loading states have no
   evidence beyond the skeleton pattern owned by status.css. */

/* Finding W3-3: display: block broke the table formatting context, so a thead th and the tbody td
   beneath it no longer shared a column width. Table keeps its UA default display: table; width and
   border-collapse are set explicitly since cbi-table__wrap already supplies the overflow-x: auto
   scroll container this class does not need to duplicate. */
.cbi-table {
  width: 100%;
  border-collapse: collapse;
}

/* Finding W3-3 follow-up: __head and __row were display: flex rows and __cell was flex: 1 1 0, so
   the header row and each body row were independent flex containers, each dividing its own width
   between its own cells. A shared-basis (flex: 1 1 0) column next to a content-sized (flex: 0 0
   auto) column absorbs whatever that row's own fixed column needed, so the shared column's pixel
   width differs row to row even at equal container width - restoring display: table on .cbi-table
   above cannot fix this on its own while the rows and cells opt back out of table layout with
   display: flex. Converted to real table-row/table-cell display so the browser's table column
   algorithm - which sizes one column from every cell in it, header included - is what aligns them,
   the same mechanism the checker measured directly (a thead th and the tbody td beneath it now
   report one shared width). Row-level padding/gap/min-height (invalid box-model properties on a
   table-row) move to the cell: height plus vertical-align: middle reproduces the old
   align-items: center, and inline padding on the cell reproduces the old row padding and
   inter-cell gap. */
.cbi-table--dense .cbi-table__row .cbi-table__cell {
  height: var(--cbi-sizing-sidebar-row-height);
}

.cbi-table__wrap {
  width: 100%;
  overflow-x: auto;
}

.cbi-table__toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-sm);
  padding-block-end: var(--cbi-spacing-sm);
}

.cbi-table__head {
  display: table-row;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-table-header-size);
  line-height: var(--cbi-typography-table-header-line-height);
  font-weight: var(--cbi-typography-table-header-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-table__row {
  display: table-row;
  background: var(--cbi-colour-card-surface);
}

.cbi-table__row--two-line .cbi-table__cell {
  /* components.md section 9: two-line person rows (bold name, grey caption
     line below) read taller than the single-line comfortable row. No
     separate two-line height token exists in the canonical key list, so
     the row grows around its content rather than a fixed second height. */
  height: auto;
  padding-block: var(--cbi-spacing-sm);
}

.cbi-table__row--clickable {
  cursor: pointer;
  transition: background var(--cbi-motion-fast) var(--cbi-motion-easing);
}

/* designed: no product evidence */
.cbi-table__row--clickable:hover {
  background: var(--cbi-colour-sidebar-active-tint);
}

/* designed: no product evidence */
.cbi-table__row--clickable:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* designed: no product evidence - no source shows a row mid-selection;
   checkboxes exist in the Incidents header but no row is shown checked */
.cbi-table__row--selected {
  background: var(--cbi-colour-primary-tint);
}

/* Targeted inert pulse (0.4.1, owner change): controls.js adds this to the single row an inert
   control names through data-cbi-inert-target, for the same 1600 ms the page-wide cbi-pulse runs.
   A row is a <tr>, and neither an outline nor a box-shadow paints reliably on one (the border and
   background model belongs to the cells), so the pulse is a background tint on the row's own
   cells: two cycles over the same duration, a static tint under prefers-reduced-motion.
   designed: no product evidence. */
.cbi-table__row--pulse .cbi-table__cell {
  animation: cbi-table-row-pulse calc(var(--cbi-motion-base) * 4) var(--cbi-motion-easing) 2;
}

@keyframes cbi-table-row-pulse {
  from {
    background: var(--cbi-colour-primary-tint);
  }
  to {
    background: transparent;
  }
}

/* W2.5 review: textSecondary reads 2.55:1 on the row's white/canvas surface, below WCAG 2.2 AA
   1.4.3's 4.5:1; this is the cell's own text (the two-line row's second line is the separate
   cbi-table__caption below), so it is body copy, not a caption, and takes the ink companion. */
.cbi-table__cell {
  display: table-cell;
  height: var(--cbi-sizing-table-row-height);
  vertical-align: middle;
  padding-inline: calc(var(--cbi-spacing-md) / 2);
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  min-width: 0;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-table-cell-size);
  line-height: var(--cbi-typography-table-cell-line-height);
  font-weight: var(--cbi-typography-table-cell-weight);
  color: var(--cbi-colour-text-secondary-ink);
}

.cbi-table__cell:first-child {
  padding-inline-start: var(--cbi-spacing-md);
}

.cbi-table__cell:last-child {
  padding-inline-end: var(--cbi-spacing-md);
}

.cbi-table__cell--numeric {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* 0.4.0 (owner review, 2026-09-04): every column header reads left-aligned, numeric columns
   included, matching the observed header row (components.md section 9, "bold left-aligned
   headers"; Incidents.png, Attendance Summary.png). data.js still gives a numeric column's header
   the --numeric modifier so the header and its cells share one table column; only the header's
   text alignment is overridden here. Body cells keep right-aligned tabular figures. */
.cbi-table__head .cbi-table__cell {
  text-align: left;
}

.cbi-table__cell--actions {
  /* display: flex here would blockify the box away from table-cell and pull this column back out
     of the shared table layout the same way the row/cell rewrite above just fixed, so alignment is
     text-align only; the kebab button (an inline-block by its own UA default) sits flush right on
     its own. */
  text-align: right;
}

/* Map addition (contract section 4.1 allows extending a named part with a
   modifier): components.md section 9's "Total Sessions" column renders as
   a light-grey rounded input box, visually distinct from the plain-text
   computed cells beside it - a deliberate editable-vs-read-only cue. No
   source shows the cell mid-edit, so only the static styling is owned. */
.cbi-table__cell--editable {
  min-width: var(--cbi-sizing-control-height);
  padding: 0 var(--cbi-spacing-sm);
  border-radius: var(--cbi-radii-control);
  background: var(--cbi-colour-sidebar-active-tint);
  text-align: center;
}

.cbi-table__primary {
  display: block;
  font-weight: var(--cbi-typography-table-cell-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-table__caption {
  display: block;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  font-weight: var(--cbi-typography-caption-weight);
  color: var(--cbi-colour-text-muted-ink);
}

/* 0.4.1 (owner change, 2026-09-05): a sortable header is a real <button>, and this rule never
   reset the browser's own button chrome, so a sortable column's header painted as a boxed default
   button beside the plain-text headers next to it. Same reset .cbi-link already carries, plus the
   header cell's own text alignment and line height, so the label reads exactly like every other
   header cell; line-height comes after the font shorthand, which would otherwise have reset it to
   normal. The pointer cursor and the focus ring below are the only chrome kept. */
.cbi-table__sort {
  display: inline-flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  line-height: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  fill: currentColor;
}

/* designed: no product evidence - no header in the pool is shown mid-hover. An unsorted sortable
   header carries no direction chevron, so hover is the one cue that says the header is a control
   at all: the primary ink plus an underline. */
.cbi-table__sort:hover {
  color: var(--cbi-colour-primary);
  text-decoration: underline;
}

/* designed: no product evidence - the direction chevron data.js appends after the label while the
   column is the active sort, and controls.js swaps on a click. Sized by the cbi-icon--sm modifier
   the builder passes, so the glyph reads at --cbi-sizing-icon-size-sm beside the header text. */
.cbi-table__sort-icon {
  display: inline-flex;
  align-items: center;
  fill: currentColor;
}

/* designed: no product evidence */
.cbi-table__sort:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* designed: no product evidence - no header in the pool shows an
   active-sort state; the checklist still needs a callable indicator */
.cbi-table__sort--asc,
.cbi-table__sort--desc {
  color: var(--cbi-colour-text-primary);
  font-weight: var(--cbi-typography-table-header-weight);
}

/* designed: no product evidence for an empty table state anywhere in the
   pool; every table observed carries rows. table-cell (not the base
   cbi-table__cell, since the skeleton's empty <td> stands alone with a
   colspan) so this stays part of the table's row/column model per the
   W3-3 follow-up above; display: flex here would blockify it out of that
   model the same way it did for cbi-table__cell--actions. The emptyState()
   builder centres its own icon/title internally, so text-align is enough. */
.cbi-table__empty {
  display: table-cell;
  text-align: center;
  padding: var(--cbi-spacing-xxl);
  color: var(--cbi-colour-text-muted-ink);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
}

@media (prefers-reduced-motion: reduce) {
  .cbi-table__row--clickable {
    transition: none;
  }

  .cbi-table__row--pulse .cbi-table__cell {
    animation: none;
    background: var(--cbi-colour-primary-tint);
  }
}

/* ---------------------------------------------------------------------- */
/* cbi-stat - components.md section 10. A ring chart with a bold number
   centred in the hole, reused across at least three unrelated metrics
   (Fixtures, Incidents, Fairness Summary), with a vertical colour-swatch
   legend. Default state only is evidenced; the "Last 7 Days" period filter
   implies a switch interaction that no source shows mid-change. */

.cbi-stat {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-lg);
}

.cbi-stat__donut {
  /* components.md section 10: the ring reads roughly 120-160px across the
     Dashboard and Incidents examples. W2 composed this from
     --cbi-sizing-avatar-size * 4 because the canonical sizing key list had
     no dedicated donut-diameter token; tokens.json now carries
     sizing.statDonutSize (128px, the same rendered value) so this reads
     that directly instead of an arithmetic stand-in. */
  /* Findings W3-4/W3-7: neither this class nor cbi-stat__value was positioned, so the value sat
     beside the ring instead of inside its hole. position: relative makes this the containing block
     for the value's absolute centring below; the svg is sized to fill the box so the ring itself
     is not left to its own intrinsic size. */
  position: relative;
  flex: 0 0 auto;
  width: var(--cbi-sizing-stat-donut-size);
  height: var(--cbi-sizing-stat-donut-size);
  border-radius: var(--cbi-radii-circle);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--cbi-colour-canvas-bg);
  /* The coloured ring segments are per-instance data (percentages differ
     every time) and are drawn by whichever builder renders this class, not
     by this static stylesheet. cbi-stat__segment below is the class the
     builder applies to each arc. */
}

.cbi-stat__donut svg {
  width: 100%;
  height: 100%;
}

/* Class map addition: the donut ring's own arcs. cbi-stat__swatch--chartN below sets a legend
   square's background fill, which is a different CSS property from an SVG arc's stroke, so the
   ring had no way to take a chart colour without an inline paint style (composition rule 9
   forbids that). The builder sets each arc's stroke-dasharray/stroke-dashoffset geometry per
   instance (percentages differ every render); this class only supplies the colour and the shared
   stroke styling. */
.cbi-stat__segment {
  fill: none;
  stroke-linecap: butt;
}

.cbi-stat__segment--chart1 {
  stroke: var(--cbi-colour-chart1);
}

.cbi-stat__segment--chart2 {
  stroke: var(--cbi-colour-chart2);
}

.cbi-stat__segment--chart3 {
  stroke: var(--cbi-colour-chart3);
}

.cbi-stat__segment--chart4 {
  stroke: var(--cbi-colour-chart4);
}

.cbi-stat__segment--chart5 {
  stroke: var(--cbi-colour-chart5);
}

.cbi-stat__value {
  /* Findings W3-4/W3-7: centred over the ring against cbi-stat__donut's position: relative above. */
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-stat-number-size);
  line-height: var(--cbi-typography-stat-number-line-height);
  font-weight: var(--cbi-typography-stat-number-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-stat__label {
  display: block;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  font-weight: var(--cbi-typography-caption-weight);
  color: var(--cbi-colour-text-muted-ink);
  text-align: center;
}

.cbi-stat__legend {
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-sm);
}

/* Class map addition: the five-column legend layout (Activity Selection Report Summary's
   "Fairness Summary"), against the default vertical swatch list (Dashboard, Incidents).
   E: observed, section 10, both layouts. */
.cbi-stat__legend--row {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--cbi-spacing-lg);
}

.cbi-stat__legend-item {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  font-weight: var(--cbi-typography-body-weight);
  color: var(--cbi-colour-text-secondary-ink);
}

/* Class map addition: the count-and-percentage figure inside a legend item ("412 - 34%"), which
   needs its own weight and colour and cannot reuse __value (the big centred donut number). */
.cbi-stat__legend-value {
  font-weight: var(--cbi-typography-weight-semibold);
  color: var(--cbi-colour-text-primary);
}

.cbi-stat__swatch {
  flex: 0 0 auto;
  width: var(--cbi-sizing-icon-size-sm);
  height: var(--cbi-sizing-icon-size-sm);
  border-radius: var(--cbi-radii-circle);
}

.cbi-stat__swatch--chart1 {
  background: var(--cbi-colour-chart1);
}

.cbi-stat__swatch--chart2 {
  background: var(--cbi-colour-chart2);
}

.cbi-stat__swatch--chart3 {
  background: var(--cbi-colour-chart3);
}

.cbi-stat__swatch--chart4 {
  background: var(--cbi-colour-chart4);
}

.cbi-stat__swatch--chart5 {
  background: var(--cbi-colour-chart5);
}

/* ---------------------------------------------------------------------- */
/* cbi-roll - components.md section 16. A per-student checkbox at the row's
   right edge (empty = present, solid red with a white cross = marked
   absent), read-only badge icons and emoji flags to its left, a sort and
   tally footer, and a drill-down modal whose toggle-card selection reuses
   the same mark indicator at a larger touch target. */

.cbi-roll {
  display: block;
}

.cbi-roll__toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-md);
  padding-block-end: var(--cbi-spacing-md);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  color: var(--cbi-colour-text-secondary-ink);
}

/* Class map addition: the observed instruction line ("Select the students to mark as absent")
   sitting between the modal header and the first row. E: observed, section 16. */
.cbi-roll__instructions {
  display: block;
  padding-block-end: var(--cbi-spacing-sm);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  color: var(--cbi-colour-text-secondary-ink);
}

/* Class map addition: the footer carrying the sort control, the tally text and the Save button.
   The W2 map had __toolbar for the header only. E: observed, section 16. */
.cbi-roll__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-md);
  padding-block-start: var(--cbi-spacing-md);
  border-top: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

/* Class map addition: the tally text ("8/10 present (80%)") inside the footer. */
.cbi-roll__tally {
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  font-weight: var(--cbi-typography-weight-semibold);
  color: var(--cbi-colour-text-primary);
}

.cbi-roll__row {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-md);
  min-height: var(--cbi-sizing-table-row-height);
  padding: 0 var(--cbi-spacing-sm);
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

/* components.md section 16: web-clipboard-home-attendance.jpg shows one row
   highlighted pale amber as a distinct visual state; the trigger for it is
   unconfirmed, but the state itself is observed (Medium confidence), not
   designed from nothing. */
.cbi-roll__row--flagged {
  background: var(--cbi-colour-warning-tint);
}

.cbi-roll__name {
  font-weight: var(--cbi-typography-table-cell-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-roll__meta {
  display: block;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-roll__flags {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  flex: 0 0 auto;
}

.cbi-roll__marks {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
  flex: 0 0 auto;
}

/* 0.4.0 (owner review): border (#F4F4F4, near-white) left the resting mark's boundary essentially
   invisible against the card surface; controlBorder replaces it here so the mark stays visible
   before it is flipped. The absent state below still switches to the error colour. */
.cbi-roll__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-icon-size-lg);
  height: var(--cbi-sizing-icon-size-lg);
  border-radius: var(--cbi-radii-control);
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-control-border);
  background: var(--cbi-colour-card-surface);
  color: transparent;
  cursor: pointer;
  fill: currentColor;
  transition: background var(--cbi-motion-fast) var(--cbi-motion-easing);
}

/* designed: no product evidence */
.cbi-roll__mark:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-roll__mark--present {
  /* components.md section 16: default = empty box = present. */
  background: var(--cbi-colour-card-surface);
}

.cbi-roll__mark--absent {
  /* components.md section 16: checked = solid red box, white cross icon. */
  border-color: var(--cbi-colour-error);
  background: var(--cbi-colour-error);
  color: var(--cbi-colour-on-primary);
}

/* designed: the drill-down's "Explained/Unexplained Absence" toggle cards
   (components.md section 16) are a bigger touch target than this row
   checkbox; no source shows the un-selected explained state separately
   from the selected one, so the resting appearance here is designed */
.cbi-roll__mark--explained {
  border-color: var(--cbi-colour-primary);
  color: var(--cbi-colour-primary);
}

.cbi-roll__mark--active {
  /* components.md section 16: the selected toggle card gets a blue border,
     blue text and a filled radio dot. */
  border-color: var(--cbi-colour-primary);
  background: var(--cbi-colour-primary);
  color: var(--cbi-colour-on-primary);
}

@media (prefers-reduced-motion: reduce) {
  .cbi-roll__mark {
    transition: none;
  }
}

/* ---------------------------------------------------------------------- */
/* cbi-board - components.md section 17. Column View renders each team as
   a vertical column (header, student rows, a dashed "+ Add Student" ghost
   row), with a View switcher (List/Power/Column) and a filter bar above
   the board. Drop-complete and team-creation have no evidence. */

.cbi-board {
  display: block;
}

.cbi-board__toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--cbi-spacing-sm);
  padding-block-end: var(--cbi-spacing-md);
}

.cbi-board__view-switch {
  position: relative;
  flex: 0 0 auto;
  margin-inline-start: auto;
}

.cbi-board__columns {
  display: flex;
  align-items: flex-start;
  gap: var(--cbi-spacing-lg);
  overflow-x: auto;
}

/* Class map addition: columns are grouped under a collapsible year-level heading
   ("Year 7 - 3 Teams" with a chevron), evidenced in the non-drag Teams table view and carried
   here as the same grouping applied to Column View's columns. E: observed, section 17. */
.cbi-board__group {
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-sm);
}

.cbi-board__group-title {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-section-title-size);
  line-height: var(--cbi-typography-section-title-line-height);
  font-weight: var(--cbi-typography-section-title-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-board__column {
  /* components.md section 17: three columns (7As/7Bs/7Cs) share the available width evenly in
     the evidence image. W2 composed this from --cbi-sizing-popover-width, the nearest token to
     the observed column width, because the canonical sizing key list had no dedicated board
     column-width token; tokens.json now carries sizing.boardColumnWidth (280px, the same rendered
     value) so this reads that directly instead of a borrowed reference. */
  flex: 0 0 auto;
  width: var(--cbi-sizing-board-column-width);
  border-radius: var(--cbi-radii-card);
  background: var(--cbi-colour-canvas-bg);
  padding: var(--cbi-spacing-sm);
}

.cbi-board__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--cbi-spacing-sm);
  padding: var(--cbi-spacing-sm);
}

.cbi-board__title {
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-section-title-size);
  line-height: var(--cbi-typography-section-title-line-height);
  font-weight: var(--cbi-typography-section-title-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-board__meta {
  display: block;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-board__list {
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-xs);
}

.cbi-board__card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-sm);
  min-height: var(--cbi-sizing-sidebar-row-height);
  padding: 0 var(--cbi-spacing-sm);
  border-radius: var(--cbi-radii-control);
  background: var(--cbi-colour-card-surface);
  color: var(--cbi-colour-text-primary);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  cursor: grab;
  transition: background var(--cbi-motion-fast) var(--cbi-motion-easing);
}

/* designed: no product evidence */
.cbi-board__card:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-board__card--dragging {
  /* components.md section 17: the mid-drag chip renders as a solid-purple
     pill lifted above the board under a grab-hand cursor. */
  cursor: grabbing;
  border-radius: var(--cbi-radii-pill);
  background: var(--cbi-colour-primary);
  color: var(--cbi-colour-on-primary);
  box-shadow: var(--cbi-elevation-dropdown-shadow);
}

.cbi-board__drop {
  /* components.md section 17: the row under the dragged chip shows a
     pale-lavender highlight as the implied drop target. */
  background: var(--cbi-colour-primary-tint);
}

.cbi-board__badge {
  display: inline-flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  flex: 0 0 auto;
}

/* Class map addition, W2.5 mapping correction: components.md section 17's "+ Add Student" row is
   a dashed, permanent affordance pinned at the column's bottom, distinct from a real student row.
   W2 built this as a cbi-board__card--ghost modifier; w3-api-draft.md section 6 (D-11) rejects
   that shape deliberately -- the ghost row is a permanent affordance, not a card in a drag list,
   and a card modifier would make it match .cbi-board__card selectors and read as a drop
   candidate. Renamed here to cbi-board__add, a sibling element of __card rather than a variant of
   it, so W3 builds against __add and no page can address the removed --ghost modifier. */
.cbi-board__add {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--cbi-spacing-sm);
  min-height: var(--cbi-sizing-sidebar-row-height);
  padding: 0 var(--cbi-spacing-sm);
  border-radius: var(--cbi-radii-control);
  border: var(--cbi-elevation-border-weight) dashed var(--cbi-colour-border-strong);
  background: transparent;
  color: var(--cbi-colour-text-muted-ink);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  cursor: pointer;
}

/* designed: no product evidence */
.cbi-board__add:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

@media (prefers-reduced-motion: reduce) {
  .cbi-board__card {
    transition: none;
  }
}
/* source: src/forms.css */
/*
 * forms.css - Clipboard (inferred) design system, category "forms".
 *
 * Owns: the field wrapper, text input, textarea, floating-label filter select, search field,
 * checkbox, radio, toggle switch, the inline month date-picker plus the weekly date-strip, and
 * the body-chart injury-picker tabs. Draws on docs/clipboard-ds/components.md sections 5
 * (Floating-label filter selects), 6 (Text inputs and textareas), 7 (Search fields), 8 (Date
 * pickers and the date-strip) and 18 (Body-chart injury picker), plus the "Form validation style"
 * interaction pattern, and docs/clipboard-ds/visual-system.md sections 1.3, 1.6 and 3.
 *
 * W2.5 review: the record describes inputs, selects and search fields as sharing the same
 * "light-grey filled rounded box" (components.md sections 5-7), which visual-system.md section 1.6
 * notes is the same #F4F4F4 value as the neutral/pending pill and the sidebar active wash. The W2
 * gap report flagged that the canonical token list (contract section 1.1) had no dedicated "field
 * fill" colour and used colour.neutralPill as the nearest key; tokens.json now carries
 * colour.fieldFill as that same sampled value under its own field semantic (not a new sample), and
 * every field fill below reads it instead of neutralPill.
 */

/* Field wrapper: label above, control, optional help/error line below (components.md sections 5
   and 6 anatomy). */
.cbi-field {
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-xs);
}

.cbi-field--inline {
  flex-direction: row;
  align-items: center;
  gap: var(--cbi-spacing-sm);
}

.cbi-field__label {
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  font-weight: var(--cbi-typography-weight-semibold);
  color: var(--cbi-colour-text-primary);
}

.cbi-field__control {
  display: block;
}

.cbi-field__help {
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-field__error {
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-error);
}

/* designed: no product evidence for an error/invalid field treatment anywhere in the pool
   ("Form validation style" -- the red required-field asterisk is the only concrete affordance
   observed; no red border, inline error, or error summary was ever shown). */
.cbi-field--error .cbi-input,
.cbi-field--error .cbi-textarea,
.cbi-field--error .cbi-select__control {
  border-color: var(--cbi-colour-error);
}

/* designed: no product evidence for a disabled field as a system. The wrapper modifier is the
   primary path (a builder-set --disabled class on the parent cbi-field, or directly on cbi-input
   for the D-13 wrapper); cbi-textarea stays a bare element and additionally answers to the native
   :disabled attribute since nothing wraps it. cbi-select__control is a <button> (forms.js select()
   never renders the native disabled attribute on it, so keyboard focus and the disabledReason
   title stay reachable), so it answers to aria-disabled="true" instead; :disabled is kept here
   too in case a future caller ever renders a genuinely native select.
   Round 2 fix: the colour leg of this rule only reaches text that actually inherits it. Both the
   input's inner control and the select's value span set their own colour, so each needs a
   companion inherit rule below; without the select one, a disabled select painted the disabled
   background (1.06:1 against the enabled fill, invisible) and kept its full-strength value text,
   so sighted users could not tell it from an enabled select at all. */
.cbi-field--disabled .cbi-input,
.cbi-input--disabled,
.cbi-field--disabled .cbi-textarea,
.cbi-textarea:disabled,
.cbi-field--disabled .cbi-select__control,
.cbi-select__control:disabled,
.cbi-select__control[aria-disabled="true"] {
  background: var(--cbi-colour-disabled-bg);
  color: var(--cbi-colour-disabled-text);
  cursor: default;
}

.cbi-field--disabled .cbi-input .cbi-input__control,
.cbi-input--disabled .cbi-input__control {
  color: inherit;
  cursor: default;
}

/* The select's companion to the input rule above. .cbi-select__value and
   .cbi-select--filled .cbi-select__value both set their own colour, so the control's disabled
   colour is never inherited without this; specificity here is 0,3,0, which beats the --filled
   rule whatever the source order. The chevron deliberately keeps textSecondary rather than taking
   the inherit: it is a decorative indicator (same reasoning as the .cbi-input__icon, which the
   input's disabled rule likewise leaves alone), and inheriting disabledText would drop it below
   the contrast it holds today for no extra signal - the value text carries the dimming. */
.cbi-field--disabled .cbi-select__control .cbi-select__value,
.cbi-select__control[aria-disabled="true"] .cbi-select__value,
.cbi-select__control:disabled .cbi-select__value {
  color: inherit;
}

/* Text input (components.md section 6, restructured per contract D-13, W2.5 review): a wrapper
   box (light-grey filled rounded box, no visible border in the resting state in any source) around
   a transparent, borderless control and an optional trailing icon. A bare <input class="cbi-input">
   cannot hold the calendar-glyph icon evidenced at a date field's right edge (section 8.4, two
   sources), and a second block for the wrapper is forbidden by the class-map rule (one block per
   component), so the input element itself becomes cbi-input__control inside this box. */
.cbi-input {
  width: 100%;
  height: var(--cbi-sizing-control-height);
  padding: 0 var(--cbi-sizing-input-padding-x);
  border-radius: var(--cbi-radii-control);
  border: var(--cbi-elevation-border-weight) solid transparent;
  background: var(--cbi-colour-field-fill);
  display: inline-flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
}

/* designed: no product evidence for a focused/bordered input state (components.md section 6,
   "none show a focused/bordered state"); focus-within so the ring sits on the wrapper regardless
   of which inner control actually holds keyboard focus. */
.cbi-input:focus-within {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
  background: var(--cbi-colour-card-surface);
}

.cbi-input__control {
  flex: 1;
  min-width: 0;
  height: 100%;
  border: none;
  outline: none;
  background: none;
  color: var(--cbi-colour-text-primary);
  font-family: inherit;
  font-size: var(--cbi-typography-body-size);
}

.cbi-input__control::placeholder {
  color: var(--cbi-colour-text-muted-ink);
}

/* Class map addition (D-13): the trailing glyph on a plain date field (components.md section 8.4).
   Decorative alongside the field's own value/placeholder text, so it keeps the sampled grey rather
   than the ink companion -- an icon beside a labelled control, not the control's text itself. */
.cbi-input__icon {
  flex: 0 0 auto;
  width: var(--cbi-sizing-icon-size-sm);
  height: var(--cbi-sizing-icon-size-sm);
  color: var(--cbi-colour-text-muted);
  fill: currentColor;
}

/* Textarea: only one legible example, shown empty with placeholder text only, single-line height
   in that capture; whether it resizes is not evidenced (components.md section 6).
   W2.5 review: moved off the borrowed colour.neutralPill reference to the dedicated
   colour.fieldFill token, the same as the other field fills below -- a textarea is a form field,
   so it belongs in that group. Same rendered value (#F4F4F4), no visual change. */
.cbi-textarea {
  width: 100%;
  min-height: calc(var(--cbi-sizing-control-height) * 2);
  padding: var(--cbi-sizing-input-padding-x);
  border-radius: var(--cbi-radii-control);
  border: var(--cbi-elevation-border-weight) solid transparent;
  background: var(--cbi-colour-field-fill);
  color: var(--cbi-colour-text-primary);
  font-family: inherit;
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  resize: vertical;
}

.cbi-textarea::placeholder {
  color: var(--cbi-colour-text-muted-ink);
}

/* designed: no product evidence */
.cbi-textarea:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
  background: var(--cbi-colour-card-surface);
}

/* Floating-label filter select (components.md section 5). Small bold label above a light-grey
   filled rounded box with a chevron; open state renders a white card option list directly under
   the field, with the highlighted/first option on a light-purple row fill. The filter-bar variant
   uses the same anatomy with a lighter micro-label; both share this block, --filled distinguishes
   a chosen value from the placeholder. */
.cbi-select {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-xs);
}

/* Form-context default (components.md section 5): "small bold label sitting directly above the
   field". W2.5 review: this rule previously carried the filter-bar's lighter micro-label styling
   unconditionally, which left the form-context bold-black label (the anatomy's actual default)
   unbuilt; --micro below now owns the lighter treatment and this base rule owns the bold one. */
.cbi-select__label {
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  font-weight: var(--cbi-typography-weight-semibold);
  color: var(--cbi-colour-text-primary);
}

/* Class map addition: the filter-bar variant (Attendance's Time Period/Activities/Staff selects
   and similar, seen dozens of times) uses a tiny grey micro-label instead of the form-context bold
   black label -- functionally the same component, lighter-weight label styling for a filter-bar
   context. E: observed, section 5. */
.cbi-select--micro .cbi-select__label {
  font-weight: var(--cbi-typography-weight-medium);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-select__control {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-sm);
  height: var(--cbi-sizing-control-height);
  padding: 0 var(--cbi-sizing-input-padding-x);
  border-radius: var(--cbi-radii-control);
  border: var(--cbi-elevation-border-weight) solid transparent;
  background: var(--cbi-colour-field-fill);
  color: var(--cbi-colour-text-primary);
  cursor: pointer;
}

/* designed: no product evidence for a focus-ring on the closed select control. */
.cbi-select__control:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-select--open .cbi-select__control {
  border-color: var(--cbi-colour-primary);
}

.cbi-select__value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-select--filled .cbi-select__value {
  color: var(--cbi-colour-text-primary);
}

/* W2.5 review: textSecondary reads 2.55:1 on white, below WCAG 2.2 AA 1.4.3's 4.5:1 for text --
   but the chevron is a decorative open/close indicator beside the control's own value text (which
   carries its own accessible colour), not a second reading of the value itself, so it stays at the
   sampled hue rather than taking the ink companion (documented departure, tracked here rather than
   in component-requests.md since the departure is deliberate, not a gap). */
.cbi-select__chevron {
  width: var(--cbi-sizing-icon-size-sm);
  height: var(--cbi-sizing-icon-size-sm);
  color: var(--cbi-colour-text-secondary);
  flex: none;
  fill: currentColor;
}

.cbi-select__menu {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  /* 0.4.0 (owner review): the menu is a ul, so the UA list marker and bottom margin are reset;
     without this every option carried a bullet. */
  list-style: none;
  margin: var(--cbi-spacing-xs) 0 0;
  z-index: var(--cbi-layering-dropdown);
  background: var(--cbi-colour-card-surface);
  border-radius: var(--cbi-radii-control);
  box-shadow: var(--cbi-elevation-dropdown-shadow);
  padding: var(--cbi-spacing-xs) 0;
}

.cbi-select__option {
  padding: var(--cbi-spacing-sm) var(--cbi-sizing-input-padding-x);
  color: var(--cbi-colour-text-primary);
  cursor: pointer;
}

/* Highlighted/first option on open: a light-purple row fill (components.md section 5, "Injury"
   and "Head" both shown highlighted in the open-state reference). */
.cbi-select__option--selected {
  background: var(--cbi-colour-primary-tint);
  color: var(--cbi-colour-primary);
}

/* designed: no product evidence */
.cbi-select__option:hover {
  background: var(--cbi-colour-primary-tint);
  color: var(--cbi-colour-primary);
}

/* designed: no product evidence */
.cbi-select__option:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: calc(var(--cbi-elevation-focus-ring-offset) * -1);
}

/* Search field (components.md section 7). Grey-filled rounded pill, left-aligned magnifying-glass
   icon, grey placeholder; shorter/lower-profile than a primary button. */
.cbi-search {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  height: var(--cbi-sizing-control-height-sm);
  padding: 0 var(--cbi-sizing-input-padding-x);
  border-radius: var(--cbi-radii-pill);
  background: var(--cbi-colour-field-fill);
}

/* W2.5 review: textMuted reads 3.19:1 on the field fill, below 4.5:1 for text -- but the
   magnifying-glass is a decorative icon beside the labelled search input (the typed/placeholder
   text carries its own accessible colour), not a second reading of the value, so it stays at the
   sampled hue as a documented departure, same reasoning as cbi-select__chevron above. */
.cbi-search__icon {
  width: var(--cbi-sizing-icon-size-sm);
  height: var(--cbi-sizing-icon-size-sm);
  color: var(--cbi-colour-text-muted);
  flex: none;
  fill: currentColor;
}

.cbi-search__input {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  color: var(--cbi-colour-text-primary);
  font-family: inherit;
  font-size: var(--cbi-typography-body-size);
}

.cbi-search__input::placeholder {
  color: var(--cbi-colour-text-muted-ink);
}

/* designed: no product evidence for a distinct focus treatment on the search field. */
.cbi-search__input:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-search__clear {
  flex: none;
  width: var(--cbi-sizing-icon-size-sm);
  height: var(--cbi-sizing-icon-size-sm);
  padding: 0;
  border: none;
  background: none;
  color: var(--cbi-colour-text-muted);
  cursor: pointer;
}

/* designed: no product evidence */
.cbi-search__clear:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* Checkbox (evidenced in the column-settings dropdown, components.md section 9): rounded-square
   box, indigo fill with a check mark when checked. */
.cbi-checkbox {
  display: inline-flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  cursor: pointer;
}

/* 0.4.0 (owner review): borderStrong (#D0D0D0, designed) reads well under the 3:1 non-text
   contrast minimum on white; controlBorder replaces it here for the unchecked resting state so the
   box's boundary stays visible. The checked state below still switches to the primary fill. */
.cbi-checkbox__box {
  position: relative;
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  flex: none;
  border-radius: calc(var(--cbi-radii-control) / 2);
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-control-border);
  background: var(--cbi-colour-card-surface);
}

/* Class map addition (W2.5, task 4): the native control needed its own class to stay visually
   hidden once the class-map naming rule prefers cbi- owned selectors over a bare "input" element
   descendant. Positioned to cover the box exactly and kept in the tab order (opacity, not
   display: none), so keyboard and screen-reader semantics stay on the real control.
   W2.5 review: kept the W2 bare-descendant selector (.cbi-checkbox input) alongside the new
   .cbi-checkbox__input class selector, both here and on every state variant below, so a control
   built to a skeleton that still emits a class-less native input (no class attribute) is hidden
   the same as one built with the new class. Both selectors are rooted at a cbi- owned class
   (.cbi-checkbox), so this stays contract-compliant either way. */
.cbi-checkbox input,
.cbi-checkbox__input {
  position: absolute;
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.cbi-checkbox input:checked ~ .cbi-checkbox__box,
.cbi-checkbox__input:checked ~ .cbi-checkbox__box {
  background: var(--cbi-colour-primary);
  border-color: var(--cbi-colour-primary);
}

/* The tick itself: a two-sided border box rotated 45deg into an L-turned-checkmark, no url()/glyph
   font per the contract's "no @import, url()" rule. Sized and positioned by percentage of the box
   (percentages are a permitted structural value per contract section 4) rather than a literal px
   offset. Hidden at rest via opacity so the shape never has to be added/removed from the DOM. */
.cbi-checkbox__box::after {
  content: "";
  position: absolute;
  left: 30%;
  top: 12%;
  width: 28%;
  height: 55%;
  border-style: solid;
  border-color: var(--cbi-colour-on-primary);
  border-width: 0 calc(var(--cbi-elevation-border-weight) * 2) calc(var(--cbi-elevation-border-weight) * 2) 0;
  transform: rotate(45deg);
  opacity: 0;
}

.cbi-checkbox input:checked ~ .cbi-checkbox__box::after,
.cbi-checkbox__input:checked ~ .cbi-checkbox__box::after {
  opacity: 1;
}

/* designed: no product evidence */
.cbi-checkbox input:focus-visible ~ .cbi-checkbox__box,
.cbi-checkbox__input:focus-visible ~ .cbi-checkbox__box {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* designed: no product evidence for a disabled checkbox anywhere in the pool. Reuses the same
   disabled tokens as the cbi-field wrapper modifier above so a disabled control reads consistently
   across the runtime (finding W3-10). */
.cbi-checkbox input:disabled ~ .cbi-checkbox__box,
.cbi-checkbox__input:disabled ~ .cbi-checkbox__box {
  background: var(--cbi-colour-disabled-bg);
  border-color: var(--cbi-colour-disabled-bg);
}

.cbi-checkbox__label {
  font-size: var(--cbi-typography-body-size);
  color: var(--cbi-colour-text-primary);
}

/* Radio (evidenced twice: the Create Time Period "Share time period" options, section 16's
   drill-down "Unexplained/Explained Absence" choice; the record calls this a filled radio dot on a
   plain circle when selected. designed: no product evidence for a hover/disabled radio treatment. */
.cbi-radio {
  display: inline-flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  cursor: pointer;
}

/* Class map addition (W2.5, same reasoning as cbi-checkbox__input): a class-owned selector for
   the native control instead of a bare "input" element descendant, kept in the tab order via
   opacity rather than removed from it. W2.5 review: kept the W2 bare-descendant selector
   (.cbi-radio input) alongside the new class selector for the same reason as the checkbox above -
   a skeleton that still emits a class-less native input is hidden either way, and both selectors
   stay rooted at the cbi-radio class. */
.cbi-radio input,
.cbi-radio__input {
  position: absolute;
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

/* Class map addition: the visible ring (components.md sections 16 and 19 both show a plain-circle
   radio, evidenced twice; the W2 map named the cbi-radio block but no part for the ring/dot). */
/* 0.4.0 (owner review): same swap as cbi-checkbox__box above -- controlBorder for the resting
   unselected ring; the checked state below still switches to the primary colour. */
.cbi-radio__dot {
  position: relative;
  display: inline-flex;
  flex: none;
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  border-radius: var(--cbi-radii-circle);
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-control-border);
  background: var(--cbi-colour-card-surface);
}

.cbi-radio input:checked ~ .cbi-radio__dot,
.cbi-radio__input:checked ~ .cbi-radio__dot {
  border-color: var(--cbi-colour-primary);
}

.cbi-radio__dot::after {
  content: "";
  position: absolute;
  inset: calc(var(--cbi-elevation-border-weight) * 3);
  border-radius: var(--cbi-radii-circle);
  background: var(--cbi-colour-primary);
  opacity: 0;
}

.cbi-radio input:checked ~ .cbi-radio__dot::after,
.cbi-radio__input:checked ~ .cbi-radio__dot::after {
  opacity: 1;
}

/* designed: no product evidence */
.cbi-radio input:focus-visible ~ .cbi-radio__dot,
.cbi-radio__input:focus-visible ~ .cbi-radio__dot {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* designed: no product evidence for a disabled radio anywhere in the pool. Reuses the same
   disabled tokens as the cbi-field wrapper modifier above (finding W3-10). */
.cbi-radio input:disabled ~ .cbi-radio__dot,
.cbi-radio__input:disabled ~ .cbi-radio__dot {
  background: var(--cbi-colour-disabled-bg);
  border-color: var(--cbi-colour-disabled-bg);
}

.cbi-radio__label {
  font-size: var(--cbi-typography-body-size);
  color: var(--cbi-colour-text-primary);
}

/* designed: no product evidence for an on/off switch control anywhere in the pool. The record
   shows only toggle-style choice cards (cbi-card--choice), not a track-and-thumb switch; this is a
   pattern an agent would otherwise hand-build, one of the two cases the brief allows as designed. */
.cbi-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

/* 0.4.0 (owner review): controlBorder for the off-state fill (same 3:1 reasoning as the checkbox
   and radio above); the on state below still switches to the primary fill. */
.cbi-toggle__track {
  position: relative;
  width: calc(var(--cbi-sizing-icon-size-lg) * 2);
  height: var(--cbi-sizing-icon-size-lg);
  border-radius: var(--cbi-radii-pill);
  background: var(--cbi-colour-control-border);
}

.cbi-toggle--on .cbi-toggle__track {
  background: var(--cbi-colour-primary);
}

.cbi-toggle__thumb {
  position: absolute;
  top: calc(var(--cbi-elevation-border-weight) * 2);
  left: calc(var(--cbi-elevation-border-weight) * 2);
  width: calc(var(--cbi-sizing-icon-size-lg) - calc(var(--cbi-elevation-border-weight) * 4));
  height: calc(var(--cbi-sizing-icon-size-lg) - calc(var(--cbi-elevation-border-weight) * 4));
  border-radius: var(--cbi-radii-circle);
  background: var(--cbi-colour-card-surface);
  transition: left var(--cbi-motion-fast) var(--cbi-motion-easing);
}

.cbi-toggle--on .cbi-toggle__thumb {
  left: auto;
  right: calc(var(--cbi-elevation-border-weight) * 2);
}

.cbi-toggle__label {
  font-size: var(--cbi-typography-body-size);
  color: var(--cbi-colour-text-primary);
}

/* designed: no product evidence */
.cbi-toggle:focus-visible .cbi-toggle__track {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* designed: no product evidence for a disabled switch anywhere in the pool. The toggle is a
   <button role="switch"> so disabled state is carried by aria-disabled, not the native attribute
   (project WCAG rule); reuses the same disabled tokens as the cbi-field wrapper modifier above
   (finding W3-10). */
.cbi-toggle[aria-disabled="true"] .cbi-toggle__track {
  background: var(--cbi-colour-disabled-bg);
}

.cbi-toggle[aria-disabled="true"] .cbi-toggle__label {
  color: var(--cbi-colour-disabled-text);
}

@media (prefers-reduced-motion: reduce) {
  .cbi-toggle__thumb {
    transition: none;
  }
}

/* Inline month date-range picker (components.md section 8, sub-component 3). Month-nav header,
   7-column Sunday-first grid, weekday header row; a selected range renders as a light-purple band
   with solid circular caps on the start/end dates. */
.cbi-date-picker {
  background: var(--cbi-colour-card-surface);
  border-radius: var(--cbi-radii-card);
  padding: var(--cbi-sizing-content-padding);
}

.cbi-date-picker__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--cbi-spacing-md);
}

.cbi-date-picker__title {
  font-size: var(--cbi-typography-section-title-size);
  font-weight: var(--cbi-typography-section-title-weight);
}

.cbi-date-picker__nav {
  display: flex;
  gap: var(--cbi-spacing-xs);
}

.cbi-date-picker__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--cbi-spacing-xs);
}

.cbi-date-picker__weekday {
  text-align: center;
  font-size: var(--cbi-typography-caption-size);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-date-picker__day {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-avatar-size);
  height: var(--cbi-sizing-avatar-size);
  border-radius: var(--cbi-radii-circle);
  color: var(--cbi-colour-text-primary);
  background: none;
  border: none;
  cursor: pointer;
}

.cbi-date-picker__day--today {
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-primary);
}

.cbi-date-picker__day--selected {
  background: var(--cbi-colour-primary);
  color: var(--cbi-colour-on-primary);
}

/* Range fill: a light-purple band spanning the cells between the selected start and end dates
   (components.md section 8). Not in the section 4.1 map by name; added as a modifier on the same
   block per contract instruction. W2.5 mapping note: w3-api-draft.md section 6 names this
   cbi-date-picker__day--in-range; this file already shipped --range for the same band before that
   draft existed, so --range stands and W3-4 builds against --range, not --in-range. */
.cbi-date-picker__day--range {
  background: var(--cbi-colour-primary-tint);
  border-radius: 0;
}

/* designed: no product evidence for a muted/blackout day treatment; used for adjacent-month
   spillover days. */
.cbi-date-picker__day--muted {
  color: var(--cbi-colour-text-muted-ink);
}

/* designed: no product evidence */
.cbi-date-picker__day:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* Weekly schedule date-strip (components.md section 8, sub-component 1). 7 circular day-chips,
   today solid-indigo filled with a white numeral, other days plain outline/grey. */
.cbi-date-strip {
  display: flex;
  gap: var(--cbi-spacing-sm);
}

.cbi-date-strip__nav {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
}

.cbi-date-strip__day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  background: none;
  border: none;
  cursor: pointer;
}

.cbi-date-strip__weekday {
  font-size: var(--cbi-typography-caption-size);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-date-strip__date {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-avatar-size);
  height: var(--cbi-sizing-avatar-size);
  border-radius: var(--cbi-radii-circle);
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  color: var(--cbi-colour-text-primary);
}

.cbi-date-strip__day--active .cbi-date-strip__date {
  background: var(--cbi-colour-primary);
  border-color: var(--cbi-colour-primary);
  color: var(--cbi-colour-on-primary);
}

/* designed: no product evidence */
.cbi-date-strip__day:focus-visible .cbi-date-strip__date {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* Class map addition: the two stacked count badges below each day chip (a session-count icon plus
   number, and a green fixture-count icon plus number). E: observed, section 8.1, Dashboard. */
.cbi-date-strip__count {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-date-strip__count--fixture {
  color: var(--cbi-colour-success);
}

/* Body-chart injury picker tabs (components.md section 18). A front/back toggle-tab pair above the
   body-silhouette figure; the figure and marker themselves are drawn by the extension that owns
   this component, this block only styles the tab switch and marker sizing/colour. */
.cbi-body-chart {
  display: block;
}

.cbi-body-chart__tabs {
  display: flex;
  gap: var(--cbi-spacing-sm);
}

.cbi-body-chart__tab {
  padding: var(--cbi-spacing-xs) var(--cbi-spacing-md);
  border-radius: var(--cbi-radii-pill);
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  background: none;
  color: var(--cbi-colour-text-secondary-ink);
  font-family: inherit;
  font-size: var(--cbi-typography-body-size);
  cursor: pointer;
}

.cbi-body-chart__tab--active {
  border-color: var(--cbi-colour-primary);
  color: var(--cbi-colour-primary);
}

/* designed: no product evidence */
.cbi-body-chart__tab:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-body-chart__figure {
  position: relative;
  display: flex;
  justify-content: center;
  padding: var(--cbi-spacing-lg) 0;
}

/* Findings W3-1/W3-6/W3-8: display: flex above overrides the UA [hidden] rule, so the inactive
   figure kept painting and the front/back tab produced no visible change. Pairs display: none with
   the hidden attribute the same way overlays.css does for the --open modifiers. */
.cbi-body-chart__figure[hidden] {
  display: none;
}

/* Class map addition: the class on the original inline SVG outline inside __figure (components.md
   section 18, a plain-outline body silhouette; the drawing itself is original, not licensed icon
   art, so it is not part of runtime/icons/index.json). Styles only the stroke/fill the figure's own
   markup does not set inline, per composition rule 9 (no inline paint style). */
.cbi-body-chart__silhouette {
  width: 100%;
  max-width: calc(var(--cbi-sizing-avatar-size) * 4);
  fill: none;
  stroke: var(--cbi-colour-border-strong);
}

/* Injury marker: a red dot placed on the body silhouette (components.md section 18). */
.cbi-body-chart__marker {
  position: absolute;
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  border-radius: var(--cbi-radii-circle);
  background: var(--cbi-colour-error);
}
/* source: src/overlays.css */
/* overlays.css - Clipboard (inferred) runtime, overlays category
   (slice S5 CSS B). Owns: cbi-scrim, cbi-modal, cbi-menu, cbi-popover.
   Evidence: components.md section 11 (Modals and confirm dialogs), and the
   kebab-menu and column-settings/day-list popover notes in section 9
   (Tables) and section 15 (Calendar/scheduler); visual-system.md sections
   1.1, 1.6, 3, 4. Every value is a var(--cbi-*) custom property, or
   calc()/percentages/keywords over one, per contract section 1.1 and
   section 4. No component rule declares a --cbi-* property. Australian
   English; no em dashes. */

/* ---------------------------------------------------------------------- */
/* cbi-scrim + cbi-modal - components.md section 11. White rounded card
   over a dimmed backdrop, evidenced across every modal in the pool. Header:
   bold title left, close-X right, except on the Cancel Session dialog,
   which drops the close-X and relies on the button pair (corrected on
   review 2026-09-02: Export Attendance Summary keeps its close-X at the
   same narrow width -- see components.md section 11's review paragraph;
   the runtime expresses this per instance with modal()'s closeButton
   option, not with CSS keyed to a size variant). Footer: right-aligned
   button pair, or a three-part footer isolating a destructive action to
   the far left. Only the default (open) state is evidenced anywhere in
   the pool. */

/* --open (W2.5, class map addition, contract section 4's state-modifier
   rule): display is governed entirely by this modifier, never by the
   native [hidden] attribute alone. An author rule setting display on
   .cbi-scrim/.cbi-modal would otherwise beat the UA stylesheet's
   [hidden] { display: none } by cascade origin regardless of selector
   specificity or source order, so [hidden] alone cannot close either
   box; openModal/closeModal (controls.js) toggle both the class and the
   attribute together, the attribute carrying the accessibility semantics
   and the class carrying the paint. */
.cbi-scrim {
  display: none;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: var(--cbi-colour-scrim);
  z-index: var(--cbi-layering-scrim);
}

.cbi-scrim--open {
  display: flex;
}

.cbi-modal {
  display: none;
  flex-direction: column;
  width: var(--cbi-sizing-modal-width);
  max-height: calc(100vh - var(--cbi-spacing-xxxl) * 2);
  overflow: hidden;
  border-radius: var(--cbi-radii-modal);
  background: var(--cbi-colour-card-surface);
  box-shadow: var(--cbi-elevation-modal-shadow);
  z-index: var(--cbi-layering-modal);
}

.cbi-modal--open {
  display: flex;
}

.cbi-modal--sm {
  /* components.md section 11: small single-purpose confirm dialogs (Export
     Attendance Summary, Cancel Session) read narrower than the full-content
     modals. W2 composed this from calc(modalWidth * 0.7) = 392px because the
     canonical sizing key list had no dedicated narrow-modal width token;
     tokens.json now carries sizing.modalWidthSm (400px, a plain step close
     to that reading) so this reads that directly instead of an arithmetic
     stand-in. */
  width: var(--cbi-sizing-modal-width-sm);
}

.cbi-modal--lg {
  /* components.md section 11: larger content modals (Roll call, Report
     Incident, invoice detail) keep the full header and hold more content. */
  width: var(--cbi-sizing-modal-width-lg);
}

.cbi-modal--confirm {
  /* Same narrow width as --sm: the confirm template is title, one sentence
     of body copy, Cancel + primary-action pair. */
  width: var(--cbi-sizing-modal-width-sm);
}

/* designed: no product evidence for a modal-level colour treatment on a
   destructive confirm - components.md section 11 only shows the
   destructive action isolated to the far left of the footer, never a
   header or border colour cue */
.cbi-modal--danger .cbi-modal__title {
  color: var(--cbi-colour-error);
}

.cbi-modal--drilldown {
  /* components.md section 9: the stat-cell drill-down (e.g. "Arrived
     Late") is inspection-only - title, a short session list, a single
     Cancel button - and reads at the same narrow width as a confirm
     dialog. */
  width: var(--cbi-sizing-modal-width-sm);
}

/* W2.5 review: components.md section 11 was corrected on review -- whether a dialog carries a
   close-X does not track its width variant (Export Attendance Summary keeps its close-X at the
   same narrow width Cancel Session drops it at), so this is no longer a CSS rule keyed to
   cbi-modal--sm/--confirm. The runtime's modal() builder expresses it per instance with a
   closeButton option instead: when closeButton is false the builder omits the cbi-modal__close
   markup altogether, which is what the Cancel Session demo example below now does. */

.cbi-modal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-md);
  padding: var(--cbi-spacing-lg);
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-modal__title {
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-modal-title-size);
  line-height: var(--cbi-typography-modal-title-line-height);
  font-weight: var(--cbi-typography-modal-title-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-modal__close {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-icon-size-lg);
  height: var(--cbi-sizing-icon-size-lg);
  border-radius: var(--cbi-radii-circle);
  color: var(--cbi-colour-text-muted);
  cursor: pointer;
  fill: currentColor;
  transition: background var(--cbi-motion-fast) var(--cbi-motion-easing);
}

/* designed: no product evidence */
.cbi-modal__close:hover {
  background: var(--cbi-colour-sidebar-active-tint);
}

/* designed: no product evidence */
.cbi-modal__close:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* W2.5 review (task 8 audit): textSecondary reads 2.55:1 on the modal's white card-surface, below
   WCAG 2.2 AA 1.4.3's 4.5:1; a dialog's body is the reason it exists, same reasoning already
   applied to cbi-toast__text. */
.cbi-modal__body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--cbi-spacing-lg);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  color: var(--cbi-colour-text-secondary-ink);
}

.cbi-modal__footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--cbi-spacing-sm);
  padding: var(--cbi-spacing-lg);
  border-top: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

/* Class map addition: components.md section 11's "three-part footer isolating a destructive
   action to the far left" (Create Time Period's Delete / Cancel / Save), called out as a good
   safety pattern -- the destructive control physically separated from Cancel/Save. E: observed. */
.cbi-modal__footer--split {
  justify-content: space-between;
}

.cbi-modal__list {
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-sm);
  /* 0.4.0 (owner review): a ul, so the UA marker, margin and indent are reset. */
  list-style: none;
  margin: 0;
  padding: 0;
}

@media (prefers-reduced-motion: reduce) {
  .cbi-modal__close {
    transition: none;
  }
}

/* ---------------------------------------------------------------------- */
/* cbi-menu - components.md section 9's kebab row menus, confirmed open
   once (session detail page: Delete / Cancel / Email staff / SMS staff).
   No table row's kebab menu is shown open elsewhere in the pool, so most
   menu instances in a build are designed from that one confirmed shape. */

/* --open (W2.5, class map addition): same rationale as cbi-modal/cbi-scrim above -- display is
   governed by this modifier so the box's own display: flex cannot defeat [hidden] by cascade
   origin. cbi-select--open is the precedent the map already carries, but cbi-select__menu has no
   author display of its own to defeat [hidden] in the first place, so it needed no --open pair;
   this block does, because it sets display: flex unconditionally. */
.cbi-menu {
  display: none;
  flex-direction: column;
  min-width: var(--cbi-sizing-popover-width);
  padding: var(--cbi-spacing-xs);
  border-radius: var(--cbi-radii-control);
  background: var(--cbi-colour-card-surface);
  box-shadow: var(--cbi-elevation-dropdown-shadow);
  z-index: var(--cbi-layering-dropdown);
}

.cbi-menu--open {
  display: flex;
}

/* --anchored (0.4.0, owner review): fixed positioning applied only when controls.js opens the menu
   from a trigger click, so the menu no longer sits in normal flow and pushes the rest of the page
   down. Offsets are set inline by controls.js from the trigger's own bounding rect. The demo and
   library's static open: true renders never gain this class, so they stay in flow and inspectable
   inline, unaffected by this modifier. 0.4.0 (owner review, round 2): the base .cbi-menu block's
   z-index (layering.dropdown, 20) sits below the scrim (layering.scrim, 30), so a menu triggered
   from inside an open dialog and rendered at body level painted behind the dim; --anchored now
   overrides to one above the modal layer so it always paints on top regardless of where in the
   document it ends up. */
.cbi-menu--anchored {
  position: fixed;
  z-index: calc(var(--cbi-layering-modal) + 1);
}

.cbi-menu__item {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
  min-height: var(--cbi-sizing-control-height-sm);
  padding: 0 var(--cbi-spacing-sm);
  border-radius: var(--cbi-radii-control);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  color: var(--cbi-colour-text-primary);
  cursor: pointer;
  transition: background var(--cbi-motion-fast) var(--cbi-motion-easing);
}

/* designed: no product evidence */
.cbi-menu__item:hover {
  background: var(--cbi-colour-sidebar-active-tint);
}

/* designed: no product evidence */
.cbi-menu__item:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-menu__item--danger {
  /* components.md section 9: "Delete" sits in the same menu as the other
     actions; coloured to read as destructive. */
  color: var(--cbi-colour-error);
}

/* designed: no product evidence */
.cbi-menu__item--danger:hover {
  background: var(--cbi-colour-error-tint);
}

.cbi-menu__divider {
  height: var(--cbi-elevation-border-weight);
  margin: var(--cbi-spacing-xs) 0;
  background: var(--cbi-colour-border);
}

.cbi-menu__icon {
  flex: 0 0 auto;
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  fill: currentColor;
}

@media (prefers-reduced-motion: reduce) {
  .cbi-menu__item {
    transition: none;
  }
}

/* ---------------------------------------------------------------------- */
/* cbi-popover - components.md section 15's day-list popover (clicking a
   month-grid day number opens a small scrollable list of that day's
   sessions with venue) and section 9's column-settings gear dropdown
   (search box, "Select all results", grouped checkboxes). Both are a
   header, a scrollable list and per-item metadata, so they share one
   block. */

/* --open (W2.5, class map addition): same rationale as cbi-modal/cbi-scrim/cbi-menu above. */
.cbi-popover {
  display: none;
  flex-direction: column;
  width: var(--cbi-sizing-popover-width);
  max-height: calc(var(--cbi-sizing-table-row-height) * 6);
  overflow-y: auto;
  border-radius: var(--cbi-radii-control);
  background: var(--cbi-colour-card-surface);
  box-shadow: var(--cbi-elevation-dropdown-shadow);
  z-index: var(--cbi-layering-dropdown);
}

.cbi-popover--open {
  display: flex;
}

/* --anchored (0.4.0, owner review): same rationale as cbi-menu--anchored above; round 2's
   above-the-modal z-index applies here too. */
.cbi-popover--anchored {
  position: fixed;
  z-index: calc(var(--cbi-layering-modal) + 1);
}

.cbi-popover__header {
  padding: var(--cbi-spacing-sm) var(--cbi-spacing-md);
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-section-title-size);
  line-height: var(--cbi-typography-section-title-line-height);
  font-weight: var(--cbi-typography-section-title-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-popover__list {
  display: flex;
  flex-direction: column;
  padding: var(--cbi-spacing-xs);
  /* 0.4.0 (owner review): a ul, so the UA marker and margin are reset. */
  list-style: none;
  margin: 0;
}

/* Class map addition: the column-settings gear dropdown's divider between the default-on metrics
   and the opt-in "Attendance Flags" sub-group (components.md section 9). cbi-menu already has
   __divider; cbi-popover did not. */
.cbi-popover__divider {
  height: var(--cbi-elevation-border-weight);
  margin: var(--cbi-spacing-xs) 0;
  background: var(--cbi-colour-border);
}

/* Class map addition: the greyed "Attendance Flags" sub-header inside the column-settings gear
   dropdown, between the default-on metrics group and the opt-in flags group (components.md
   section 9). */
.cbi-popover__group-title {
  padding: var(--cbi-spacing-xs) var(--cbi-spacing-md);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  font-weight: var(--cbi-typography-weight-medium);
  color: var(--cbi-colour-text-muted-ink);
}

/* 0.4.0 (owner review): cbiOverlaysPopoverItemHtml renders this as a plain <li>, never a button or
   a focusable element in its own right (the actual interactive part, when there is one, is the
   composed control -- a checkbox() or similar -- sitting inside it). cursor: pointer and a focus
   ring here were a false affordance: nothing about the row itself was clickable or reachable by
   Tab. Both removed; hover/background feedback lives on the composed control instead. */
.cbi-popover__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cbi-spacing-sm);
  padding: var(--cbi-spacing-sm);
  border-radius: var(--cbi-radii-control);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  color: var(--cbi-colour-text-primary);
  transition: background var(--cbi-motion-fast) var(--cbi-motion-easing);
}

/* designed: no product evidence */
.cbi-popover__item:hover {
  background: var(--cbi-colour-sidebar-active-tint);
}

.cbi-popover__meta {
  display: block;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-muted-ink);
}

@media (prefers-reduced-motion: reduce) {
  .cbi-popover__item {
    transition: none;
  }
}
/* source: src/scheduling.css */
/* scheduling.css - Clipboard (inferred) runtime, scheduling category
   (slice S5 CSS B). Owns: cbi-calendar.
   Evidence: components.md section 15 (Calendar/scheduler) and section 12
   (Banners, for the clash-warning colour); visual-system.md sections 1.9,
   1.10, 3, 4. Every value is a var(--cbi-*) custom property, or
   calc()/percentages/keywords over one, per contract section 1.1 and
   section 4. No component rule declares a --cbi-* property. Australian
   English; no em dashes. */

/* cbi-calendar covers both the month grid (High confidence, four or more
   independent sources) and the day column / hourly grid (Medium
   confidence, one owner asset plus one web capture). A genuine week view
   has no evidence anywhere in the pool and is not built. Drag-to-reschedule
   and multi-day selection have no evidence either. */

.cbi-calendar {
  display: block;
}

.cbi-calendar__toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--cbi-spacing-sm);
  padding-block-end: var(--cbi-spacing-md);
}

.cbi-calendar__title {
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-section-title-size);
  line-height: var(--cbi-typography-section-title-line-height);
  font-weight: var(--cbi-typography-section-title-weight);
  color: var(--cbi-colour-text-primary);
}

.cbi-calendar__nav {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
}

.cbi-calendar__view-switch {
  flex: 0 0 auto;
  margin-inline-start: auto;
}

.cbi-calendar__grid {
  display: grid;
  /* Finding W3-2/W3-9 companion, W7 proof-split: an unbounded 1fr track lets its min-content
     (a long nowrap event chip) win over the fraction, so a whole-string label such as "7am Vocal
     Tuition - Melva Klein" pinned every populated column at 250px and crushed Saturday/Sunday to
     78px/68px, overflowing the 1280 viewport by 319px. minmax(0, 1fr) floors each track's
     min-content contribution at 0 so the fraction governs width and the chip truncates instead
     (see .cbi-calendar__event below). */
  grid-template-columns: repeat(7, minmax(0, 1fr));
  border-top: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  border-inline-start: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-calendar__weekday {
  padding: var(--cbi-spacing-sm);
  border-inline-end: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-table-header-size);
  line-height: var(--cbi-typography-table-header-line-height);
  font-weight: var(--cbi-typography-table-header-weight);
  color: var(--cbi-colour-text-secondary-ink);
  text-align: center;
}

.cbi-calendar__cell {
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-xs);
  min-height: calc(var(--cbi-sizing-table-row-height) * 3);
  /* W7 proof-split finding: a flex child's implicit min-width is its content's min-content,
     which propagates the same widening pressure into the grid track above; 0 lets the track's
     minmax(0, 1fr) actually take effect on the cell that sits inside it. */
  min-width: 0;
  padding: var(--cbi-spacing-xs);
  border-inline-end: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-calendar__cell--today .cbi-calendar__day-number {
  /* components.md section 15: today reads as a solid-filled circle on the
     weekly strip (section 8); reused here for the equivalent month-grid
     treatment, since no source shows a distinct today marker for the month
     grid specifically. */
  border-radius: var(--cbi-radii-circle);
  background: var(--cbi-colour-primary);
  color: var(--cbi-colour-on-primary);
}

/* designed: no product evidence for a dimmed leading/trailing week; every
   month-grid capture in the pool is cropped to the current month only */
.cbi-calendar__cell--other-month {
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-calendar__cell--other-month .cbi-calendar__day-number {
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-calendar__day-number {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--cbi-sizing-icon-size-lg);
  height: var(--cbi-sizing-icon-size-lg);
  border-radius: var(--cbi-radii-circle);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  line-height: var(--cbi-typography-body-line-height);
  color: var(--cbi-colour-text-primary);
  cursor: pointer;
}

/* designed: no product evidence */
.cbi-calendar__day-number:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-calendar__event {
  /* components.md section 15: each day cell holds a stacked list of
     coloured-dot session entries (time + activity + staff name). */
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  padding: 0 var(--cbi-spacing-xs);
  border-radius: var(--cbi-radii-control);
  /* W7 proof-split finding (screen-08, Calendar): the builder passed the whole event string as
     one label ("7am Vocal Tuition - Melva Klein"); as a flex-item chip this element's implicit
     min-width is that nowrap text's full min-content, which forced the grid track wider than
     the 1fr share. min-width: 0 lets overflow: hidden clip the chip's own text (the
     text-overflow: ellipsis below is inert while this element is a flex container) instead of
     the column widening. */
  min-width: 0;
  overflow: hidden;
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-secondary-ink);
  white-space: nowrap;
  text-overflow: ellipsis;
}

.cbi-calendar__event::before {
  content: "";
  flex: 0 0 auto;
  width: var(--cbi-spacing-xs);
  height: var(--cbi-spacing-xs);
  border-radius: var(--cbi-radii-circle);
  background: var(--cbi-colour-text-muted);
}

.cbi-calendar__event--category1::before {
  background: var(--cbi-colour-category1);
}

.cbi-calendar__event--category2::before {
  background: var(--cbi-colour-category2);
}

.cbi-calendar__event--category3::before {
  background: var(--cbi-colour-category3);
}

.cbi-calendar__event--category4::before {
  background: var(--cbi-colour-category4);
}

/* designed: no product evidence for a clash-specific chip treatment beyond
   the toolbar-level clash warning banner (components.md section 12); the
   one clash frame available (taqu-jV8VM4-t8) shows two blocks side by side
   under that banner rather than a marked single chip, so the marker here
   borrows the banner's warning colour */
.cbi-calendar__event--clash {
  border: var(--cbi-elevation-border-weight) solid var(--cbi-colour-warning);
}

/* designed: no product evidence - no calendar capture shows a cancelled
   session chip; only an OS push notification announces a cancellation
   (components.md section 14) */
.cbi-calendar__event--cancelled {
  color: var(--cbi-colour-text-muted-ink);
  text-decoration: line-through;
}

.cbi-calendar__more {
  /* components.md section 15: a busy cell truncates to a "+N more"
     overflow link. */
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-link);
  cursor: pointer;
}

/* designed: no product evidence */
.cbi-calendar__more:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

.cbi-calendar__day {
  /* components.md section 15: the day column (hourly grid) is a distinct
     component from the month grid above - a single day rendered as hourly
     rows with duration-spanning session blocks. */
  display: flex;
  flex-direction: column;
  border-inline-start: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
}

.cbi-calendar__hour {
  position: relative;
  display: flex;
  min-height: calc(var(--cbi-sizing-table-row-height) * 1.5);
  padding: var(--cbi-spacing-xs) var(--cbi-spacing-sm);
  border-bottom: var(--cbi-elevation-border-weight) solid var(--cbi-colour-border);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
  color: var(--cbi-colour-text-muted-ink);
}

/* Findings W3-2/W3-9: the builder emits an inline top/height percentage (composition rule 9's one
   allowance for this component) against the cbi-calendar__hour containing block (position: relative
   above), so this must be positioned for that geometry to have any effect - flex alone left it
   inert and every session rendered at the row's natural height instead of spanning its duration. */
.cbi-calendar__slot {
  position: absolute;
  inset-inline: var(--cbi-spacing-sm);
  flex: 1 1 auto;
  display: flex;
  align-items: flex-start;
  gap: var(--cbi-spacing-xs);
}

.cbi-calendar__clash {
  /* components.md section 15: two session blocks spanning the same time
     range, rendered side by side rather than overlapping. */
  position: absolute;
  inset-inline: var(--cbi-spacing-sm);
  display: flex;
  align-items: stretch;
  gap: var(--cbi-spacing-xs);
  flex: 1 1 auto;
}

/* Class map addition: components.md section 15 describes a horizontal red "now" indicator line on
   the day column, a part the section 4.1 map does not name explicitly. W2.5 mapping note: this
   was landed as cbi-calendar__now; w3-api-draft.md section 6 names it cbi-calendar__now-line,
   renamed here to match so W3 builds against one name. */
.cbi-calendar__now-line {
  position: absolute;
  inset-inline: 0;
  height: var(--cbi-elevation-border-weight);
  background: var(--cbi-colour-error);
}

/* Map addition: components.md section 15 (Calendar & Activity.png)
   describes small coloured absence-banner rows stacked above the hourly
   grid on the day view ("Absence" x3, "+1 more" overflow), distinct from
   both the toolbar clash banner (status.css) and the timed session block. */
.cbi-calendar__absence {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  padding: var(--cbi-spacing-xs) var(--cbi-spacing-sm);
  border-radius: var(--cbi-radii-control);
  background: var(--cbi-colour-error-tint);
  color: var(--cbi-colour-error);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
}
/* source: src/status.css */
/*
 * status.css - Clipboard (inferred) design system, category "status".
 *
 * Owns: status pills/badges, banners, the toast region and toast (designed), the empty state, and
 * skeleton loaders. Draws on docs/clipboard-ds/components.md sections 12 (Banners), 13 (Status
 * pills/badges), 14 (Toasts/notifications), 20 (Skeleton loaders) and 21 (Empty states), plus the
 * "Feedback (success/failure)" interaction pattern, and docs/clipboard-ds/visual-system.md
 * section 1.8 (state colours).
 */

/* Status pills/badges (components.md section 13). Fully rounded stadium shape, coloured text on a
   pale matching-hue background, sometimes a small leading icon/glyph. Each pill renders a discrete
   data state; pills carry no hover/focus/disabled states of their own. */
.cbi-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--cbi-spacing-xs);
  padding: var(--cbi-spacing-xs) var(--cbi-spacing-sm);
  border-radius: var(--cbi-radii-pill);
  font-size: var(--cbi-typography-pill-label-size);
  line-height: var(--cbi-typography-pill-label-line-height);
  font-weight: var(--cbi-typography-pill-label-weight);
}

/* Muted sage green, e.g. "Approved by manager" / "Paid". Kept deliberately separate from the
   saturated teal used for chart segments (visual-system.md section 1.8, finding 1). */
.cbi-pill--success {
  background: var(--cbi-colour-success-tint);
  color: var(--cbi-colour-success-ink);
}

/* Neutral/pending, e.g. "Pending manager approval". */
.cbi-pill--neutral {
  background: var(--cbi-colour-neutral-pill);
  color: var(--cbi-colour-text-secondary-ink);
}

/* Amber/orange, e.g. "Potentially N sessions to log". */
.cbi-pill--warning {
  background: var(--cbi-colour-warning-tint);
  color: var(--cbi-colour-warning-ink);
}

/* Red, e.g. "Unpaid". */
.cbi-pill--error {
  background: var(--cbi-colour-error-tint);
  color: var(--cbi-colour-error-ink);
}

/* Light-blue, e.g. an integration status pill (components.md section 13). */
.cbi-pill--info {
  background: var(--cbi-colour-info-banner-bg);
  color: var(--cbi-colour-info-accent);
}

/* Blue/lavender, e.g. "Open for Selection" before it flips to the success pill on selection
   (components.md section 13 and the "Feedback" pattern -- an in-place colour change, not a
   separate message). */
.cbi-pill--primary {
  background: var(--cbi-colour-primary-tint);
  color: var(--cbi-colour-primary-ink);
}

/* Class map addition: the plain coloured text variant with no background fill, covering
   "Approved: Yes" in the Timesheets table and the parenthetical "(concussion)" attendance flags.
   E: observed, section 13, two contexts. */
.cbi-pill--text {
  background: none;
  padding: 0;
}

.cbi-pill__dot {
  width: var(--cbi-spacing-xs);
  height: var(--cbi-spacing-xs);
  flex: none;
  border-radius: var(--cbi-radii-circle);
  background: currentColor;
}

.cbi-pill__icon {
  width: var(--cbi-sizing-icon-size-sm);
  height: var(--cbi-sizing-icon-size-sm);
  flex: none;
  fill: currentColor;
}

/* The dot and icon keep the sampled hue (not the accessible ink above): they are decorative,
   non-text accents, and the pill label's colour is what carries the ink swap for 4.5:1 text
   contrast. designed: the success and warning dots fall below the 3:1 non-text threshold against
   their own tint (2.96:1 and 2.06:1); recorded as a departure rather than sampled-value fidelity
   loss (component-requests.md). */
.cbi-pill--success .cbi-pill__icon {
  color: var(--cbi-colour-success);
}

.cbi-pill--neutral .cbi-pill__dot {
  background: var(--cbi-colour-text-secondary);
}

.cbi-pill--warning .cbi-pill__dot {
  background: var(--cbi-colour-warning);
}

.cbi-pill--error .cbi-pill__dot {
  background: var(--cbi-colour-error);
}

.cbi-pill--primary .cbi-pill__dot {
  background: var(--cbi-colour-primary);
}

/* Banners (components.md section 12). Info banner: a thin solid left accent edge on a near-white
   rounded rectangle plus a circled "i" icon -- understated, not a filled colour block. Clash
   warning banner: a pale amber-tinted rectangle with a warning-triangle icon; whether it also
   carries a left accent stripe is not clearly resolvable at 360p (Medium confidence on that
   specific detail only). No dismiss control is evidenced on either variant. */
.cbi-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--cbi-spacing-sm);
  padding: var(--cbi-spacing-md);
  border-radius: var(--cbi-radii-card);
  border-left: calc(var(--cbi-elevation-border-weight) * 4) solid transparent;
}

.cbi-banner--info {
  background: var(--cbi-colour-info-banner-bg);
  border-left-color: var(--cbi-colour-info-accent);
}

.cbi-banner--warning {
  background: var(--cbi-colour-warning-tint);
  border-left-color: var(--cbi-colour-warning);
}

/* designed: no product evidence for an error banner (only the info and warning banners are
   observed); built as a restrained extension of the same anatomy. */
.cbi-banner--error {
  background: var(--cbi-colour-error-tint);
  border-left-color: var(--cbi-colour-error);
}

/* designed: no product evidence for a success banner. */
.cbi-banner--success {
  background: var(--cbi-colour-success-tint);
  border-left-color: var(--cbi-colour-success);
}

.cbi-banner__icon {
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  flex: none;
  fill: currentColor;
  color: var(--cbi-colour-info-accent);
}

.cbi-banner--warning .cbi-banner__icon {
  color: var(--cbi-colour-warning);
}

.cbi-banner--error .cbi-banner__icon {
  color: var(--cbi-colour-error);
}

.cbi-banner--success .cbi-banner__icon {
  color: var(--cbi-colour-success);
}

.cbi-banner__body {
  flex: 1;
  min-width: 0;
}

.cbi-banner__title {
  margin: 0;
  font-weight: var(--cbi-typography-weight-semibold);
  color: var(--cbi-colour-text-primary);
}

/* textPrimary, not textSecondary/textSecondaryInk: textSecondaryInk still falls short of 4.5:1 on
   the error tint (3.54:1), and textPrimary passes on all four banner backgrounds (WCAG 2.2 AA
   1.4.3). */
.cbi-banner__text {
  margin: 0;
  color: var(--cbi-colour-text-primary);
}

.cbi-banner__actions {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
}

/* designed: no product evidence for a dismiss ("x") control on either banner variant
   (components.md section 12, "no dismiss control is visible on either banner variant"). */
.cbi-banner__close {
  flex: none;
  background: none;
  border: none;
  padding: 0;
  color: var(--cbi-colour-text-muted);
  cursor: pointer;
}

/* designed: no product evidence */
.cbi-banner__close:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* designed: no product evidence for an in-app toast/snackbar anywhere in the pool
   (components.md section 14 -- only an OS push notification and one ambiguous marketing
   illustration exist). Built as a restrained convention consistent with the rest of the system,
   per contract.json's rule not to claim fidelity here. */
.cbi-toast-region {
  position: fixed;
  z-index: var(--cbi-layering-toast);
  display: flex;
  flex-direction: column;
  gap: var(--cbi-spacing-sm);
  width: var(--cbi-sizing-toast-width);
  max-width: calc(100vw - var(--cbi-spacing-lg) * 2);
}

/* 0.4.0 (owner review, 2026-09-04): the corner is a modifier the builder emits from
   toastRegion({ position }). Bottom-right is the default: it keeps the stack clear of the top
   bar, the page-header actions and the sidebar, and it is the position the builder had always
   documented as its default. The former fixed top-right placement sat over the avatar and over
   every page's primary action. Toasts stack downwards in arrival order, so the newest sits
   nearest the corner. */
.cbi-toast-region--bottom-right {
  right: var(--cbi-spacing-lg);
  bottom: var(--cbi-spacing-lg);
}

.cbi-toast-region--bottom-left {
  left: var(--cbi-spacing-lg);
  bottom: var(--cbi-spacing-lg);
}

.cbi-toast-region--bottom-centre {
  left: 50%;
  bottom: var(--cbi-spacing-lg);
  transform: translateX(-50%);
}

.cbi-toast-region--top-right {
  right: var(--cbi-spacing-lg);
  top: var(--cbi-spacing-lg);
}

.cbi-toast {
  display: flex;
  align-items: flex-start;
  gap: var(--cbi-spacing-sm);
  padding: var(--cbi-spacing-md);
  border-radius: var(--cbi-radii-card);
  background: var(--cbi-colour-card-surface);
  box-shadow: var(--cbi-elevation-toast-shadow);
  border-left: calc(var(--cbi-elevation-border-weight) * 4) solid transparent;
}

.cbi-toast--success {
  border-left-color: var(--cbi-colour-success);
}

.cbi-toast--error {
  border-left-color: var(--cbi-colour-error);
}

.cbi-toast--info {
  border-left-color: var(--cbi-colour-info-accent);
}

.cbi-toast__icon {
  width: var(--cbi-sizing-icon-size);
  height: var(--cbi-sizing-icon-size);
  flex: none;
  fill: currentColor;
}

.cbi-toast__body {
  flex: 1;
  min-width: 0;
}

.cbi-toast__title {
  margin: 0;
  font-weight: var(--cbi-typography-weight-semibold);
  color: var(--cbi-colour-text-primary);
}

/* W2.5 review: textSecondary reads 2.55:1 on the toast's white card-surface, below WCAG 2.2 AA
   1.4.3's 4.5:1; the toast's whole purpose is to be read once and acted on, so its message line is
   ordinary body copy, not a caption. */
.cbi-toast__text {
  margin: 0;
  color: var(--cbi-colour-text-secondary-ink);
}

.cbi-toast__close {
  flex: none;
  background: none;
  border: none;
  padding: 0;
  color: var(--cbi-colour-text-muted);
  cursor: pointer;
}

/* designed: no product evidence */
.cbi-toast__close:focus-visible {
  outline: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-focus-ring);
  outline-offset: var(--cbi-elevation-focus-ring-offset);
}

/* Empty state (components.md section 21). One evidenced example: the Dashboard's "Unlogged
   Hours" card -- a small illustration plus a caption, no action button. Everything beyond that one
   example (an action row, other icon/text combinations) is designed. */
/* 0.4.0 (owner review): this colour is inherited by __text below (which sets none of its own), so
   it is real body copy, not decoration; muted-ink keeps that line at 4.5:1. */
.cbi-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--cbi-spacing-sm);
  padding: var(--cbi-sizing-content-padding);
  color: var(--cbi-colour-text-muted-ink);
}

.cbi-empty__icon {
  width: var(--cbi-sizing-icon-size-lg);
  height: var(--cbi-sizing-icon-size-lg);
  color: var(--cbi-colour-text-muted);
  fill: currentColor;
}

/* W2.5 review: textSecondary reads 2.55:1 on the empty state's white/canvas surface, below WCAG
   2.2 AA 1.4.3's 4.5:1; the title is the empty state's primary content, not a caption, so it takes
   the ink companion. */
.cbi-empty__title {
  margin: 0;
  font-weight: var(--cbi-typography-weight-medium);
  color: var(--cbi-colour-text-secondary-ink);
}

.cbi-empty__text {
  margin: 0;
  font-size: var(--cbi-typography-caption-size);
  line-height: var(--cbi-typography-caption-line-height);
}

/* designed: no product evidence for an action button in any empty state; the one evidenced
   example (Unlogged Hours) has none. */
.cbi-empty__actions {
  display: flex;
  gap: var(--cbi-spacing-sm);
  margin-top: var(--cbi-spacing-xs);
}

/* Skeleton loaders (components.md section 20). The credible evidence is a table-row skeleton: one
   real row above many placeholder bars matching its column widths. The sidebar skeleton bars seen
   in marketing composites are flagged Low confidence, most likely a mockup simplification rather
   than a real loading state, but the block is still built (an executable component needs a
   loading state) and is labelled accordingly at each use site, not here. A fully-empty loading
   state and the load/unload transition are designed: no product evidence beyond the one partial
   composition. */
.cbi-skeleton {
  display: block;
  border-radius: var(--cbi-radii-control);
  background: linear-gradient(
    90deg,
    var(--cbi-colour-skeleton-base) 0%,
    var(--cbi-colour-skeleton-highlight) 50%,
    var(--cbi-colour-skeleton-base) 100%
  );
  background-size: 200% 100%;
  animation: cbi-skeleton-shimmer calc(var(--cbi-motion-base) * 8) var(--cbi-motion-easing) infinite;
}

@keyframes cbi-skeleton-shimmer {
  from {
    background-position: 200% 0;
  }
  to {
    background-position: -200% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cbi-skeleton {
    animation: none;
  }
}

.cbi-skeleton--text {
  height: var(--cbi-sizing-icon-size-sm);
  width: 100%;
}

.cbi-skeleton--circle {
  width: var(--cbi-sizing-avatar-size);
  height: var(--cbi-sizing-avatar-size);
  border-radius: var(--cbi-radii-circle);
}

/* Finding W3-5: block display stacked the __line children vertically instead of laying them out as
   one row of column-width bars matching the real row's columns (preview args pass four widths for
   one row). */
.cbi-skeleton--row {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
  height: var(--cbi-sizing-table-row-height);
  width: 100%;
}

.cbi-skeleton--card {
  height: calc(var(--cbi-sizing-table-row-height) * 3);
  width: 100%;
  border-radius: var(--cbi-radii-card);
}

.cbi-skeleton--nav {
  height: var(--cbi-sizing-sidebar-row-height);
  width: 100%;
}

.cbi-skeleton__line {
  display: block;
  height: var(--cbi-sizing-icon-size-sm);
  margin-bottom: var(--cbi-spacing-xs);
  border-radius: var(--cbi-radii-control);
  background: linear-gradient(
    90deg,
    var(--cbi-colour-skeleton-base) 0%,
    var(--cbi-colour-skeleton-highlight) 50%,
    var(--cbi-colour-skeleton-base) 100%
  );
  background-size: 200% 100%;
  animation: cbi-skeleton-shimmer calc(var(--cbi-motion-base) * 8) var(--cbi-motion-easing) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .cbi-skeleton__line {
    animation: none;
  }
}
/* extension: steps.css */
/* extension: steps
   origin: prototype proof-extend, 2026-09-03
   basis: New fee run wizard screen 1 (step 2 of 4 progress indicator); designed anatomy, no
     product evidence for a wizard stepper anywhere in the pool
   evidenceLevel: designed
*/

.cbi-steps {
  display: flex;
  align-items: center;
  width: 100%;
}

.cbi-steps__item {
  display: flex;
  align-items: center;
  gap: var(--cbi-spacing-sm);
  flex: 0 0 auto;
}

.cbi-steps__marker {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: var(--cbi-sizing-control-height-sm);
  height: var(--cbi-sizing-control-height-sm);
  border-radius: var(--cbi-radii-circle);
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-caption-size);
  font-weight: var(--cbi-typography-weight-semibold);
  flex: none;
}

.cbi-steps__number {
  line-height: var(--cbi-typography-caption-line-height);
}

.cbi-steps__label {
  font-family: var(--cbi-typography-font-family);
  font-size: var(--cbi-typography-body-size);
  font-weight: var(--cbi-typography-weight-regular);
  line-height: var(--cbi-typography-body-line-height);
  color: var(--cbi-colour-text-secondary-ink);
  white-space: nowrap;
}

.cbi-steps__connector {
  flex: 1 1 auto;
  min-width: var(--cbi-sizing-icon-size-lg);
  height: var(--cbi-elevation-focus-ring-width);
  margin: 0 var(--cbi-spacing-sm);
  background: var(--cbi-colour-border-strong);
}

.cbi-steps__item--complete .cbi-steps__marker {
  background: var(--cbi-colour-success);
  color: var(--cbi-colour-on-primary);
}

.cbi-steps__item--complete .cbi-steps__label {
  color: var(--cbi-colour-text-primary);
}

.cbi-steps__item--current .cbi-steps__marker {
  background: var(--cbi-colour-primary);
  color: var(--cbi-colour-on-primary);
}

.cbi-steps__item--current .cbi-steps__label {
  color: var(--cbi-colour-text-primary);
  font-weight: var(--cbi-typography-weight-semibold);
}

.cbi-steps__item--upcoming .cbi-steps__marker {
  background: var(--cbi-colour-card-surface);
  color: var(--cbi-colour-text-secondary-ink);
  border: var(--cbi-elevation-focus-ring-width) solid var(--cbi-colour-border-strong);
}

.cbi-steps__connector--complete {
  background: var(--cbi-colour-success);
}

.cbi-steps__connector--current {
  background: var(--cbi-colour-primary);
}
