/*!
 * Marrow Dashboard Primitives
 * ------------------------------------------------------------------
 * Lane 1 foundation, created 2026-07-30.
 *
 * The shared component layer for the three authenticated dashboard
 * surfaces (coach, athlete, gym console). Card, table, metric, empty
 * state, loading state, error state, status pill, and sparkline. Surface
 * lanes consume these classes verbatim and do not fork them.
 *
 * Load order on any dashboard page:
 *   /css/styles.css
 *   /assets/css/dashboard-theme.css
 *   /assets/css/mz-primitives.css      <- this file
 *   the surface's own stylesheet
 *
 * Consumes only --mz-*, --fs-*, --space-*, --radius-* and the base font
 * tokens. Defines ZERO color values of its own. Never loaded by a
 * marketing page, which is what keeps this layer from leaking.
 *
 * Rules honored:
 *   - Vanilla CSS. No build step, no framework, no charting library.
 *   - No new brand color or font tokens. No literal hex anywhere in this
 *     file. The single literal color is the one sanctioned floating
 *     layer shadow, light theme only, defined once.
 *   - Borders OR shadows, never both on one element. Dark mode carries no
 *     box-shadow at all, depth comes from the raised surface ladder.
 *   - No glass, no glow, no gradient fills, no gradient text.
 *   - Exactly two radius values, --radius-sm and --radius-md.
 *   - Nothing animates above 240ms. Only transform and opacity animate.
 *     No spring or overshoot easing.
 *   - prefers-reduced-motion is already handled site wide. This file
 *     honors it and adds the one thing site wide handling cannot know
 *     about: a static fallback for the skeleton shimmer.
 *   - No em or en dashes anywhere in this file.
 */

/* =====================================================================
   0. SHARED UTILITIES
   ===================================================================== */

/* The contract markup uses .sr-only for table captions and icon only
   button labels. styles.css ships .visually-hidden but not this name, and
   the three dashboard surfaces do not all load a stylesheet that defines
   it, so it is defined once here for all three. */
.mz-card .sr-only,
.mz-table .sr-only,
.mz-metric .sr-only,
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Manual substitute for optical sizing. Switzer has no opsz axis, so the
   tracking table in the type spec does the work. This is a no op where
   unsupported. */
.mz-card,
.mz-metric,
.mz-table,
.mz-empty,
.mz-error {
  font-optical-sizing: auto;
}

/* ---------------------------------------------------------------------
   Tabular numerals. ON for every metric number, every numeric table
   column and its header, currency, percentages, timers, and time
   elements. OFF for prose and headings. Never set on body.
   --------------------------------------------------------------------- */
.mz-metric-value,
.mz-table td.num,
.mz-table th.num,
.mz-timer,
.mz-currency,
.mz-pct,
time.mz-t {
  font-variant-numeric: tabular-nums lining-nums;
}
.mz-table td.num,
.mz-table th.num { text-align: right; }

/* =====================================================================
   1. CARD
   Resting card in both themes: card surface, 1px hairline, no shadow.
   The --mz-* layer supplies the correct light and dark surface value.
   ===================================================================== */
.mz-card {
  display: flex;
  flex-direction: column;
  background: var(--mz-bg-card);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  box-shadow: none;
}
@media (max-width: 767px) {
  .mz-card { padding: var(--space-2); }
}

.mz-card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-05) var(--space-2);
  margin: 0 0 var(--space-2);
}
.mz-card-head > .mz-card-actions { margin-left: auto; }

.mz-eyebrow {
  flex-basis: 100%;
  margin: 0;
  font-family: var(--mono);
  font-size: var(--fs-label);
  font-weight: 500;
  line-height: 1.33;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mz-fg-muted);
}

.mz-card-title {
  margin: 0;
  font-family: var(--sans);
  font-size: var(--fs-card);
  font-weight: 550;
  line-height: 1.375;
  letter-spacing: 0;
  color: var(--mz-fg-heading);
}

.mz-card-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.mz-card-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1) var(--space-2);
  min-width: 0;
}
.mz-card-body > p { max-width: 60ch; }

/* styles.css:3415 styles the bare `footer` element for the marketing site
   footer: bone black fill, steel-light ink, 80px of padding. The card
   contract markup uses <footer class="mz-card-foot">, so that rule lands
   on every honesty footnote and paints it as a black slab. Neutralized
   here rather than in styles.css, which is a shared marketing file. */
.mz-card-foot {
  background: none;
  padding: 0;
  margin: var(--space-2) 0 0;
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 400;
  line-height: 1.33;
  color: var(--mz-fg-muted);
  max-width: 45ch;
}
/* dashboard-theme.css:1248 re-pins the same bare `footer` element to the
   page surface under the dark theme, at a higher specificity than a bare
   class, so the dark card footnote needs its own reset. */
:root[data-mz-theme="dark"] .mz-card-foot {
  background: none;
  padding: 0;
}

/* Dominance modifier. Every metric band ships one dominant plus
   supporting cards, never a row of three equals. */
.mz-card--dominant { grid-column: span 2; }
@media (max-width: 767px) {
  .mz-card--dominant { grid-column: span 1; }
}

/* Section header, one per zone. */
.mz-section-title {
  margin: 0 0 var(--space-2);
  font-family: var(--sans);
  font-size: var(--fs-section);
  font-weight: 550;
  line-height: 1.30;
  letter-spacing: -0.01em;
  color: var(--mz-fg-heading);
}

/* Page title, once per page. 700 is reserved for this level alone. */
.mz-page-title {
  margin: 0;
  font-family: var(--display);
  font-size: var(--fs-page);
  font-weight: var(--weight-serif, 400);
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--mz-fg-heading);
}

/* Grid the surfaces drop cards into. */
.mz-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-2);
}

/* =====================================================================
   2. METRIC
   Label (mono, muted), value (display, tabular, heading ink), optional
   note and delta. The delta always names its window or denominator and
   renders neutral unless goodness is semantically defined for that
   metric. Trend direction is not a status.
   ===================================================================== */
/* Centered by Edwin's call 2026-08-05, looking at the Today card: the label,
   the number and the note now share a centre line instead of hanging off a
   ragged left edge. This is the single site-wide handle for that shape, so
   every stacked metric on every surface moves together rather than surface by
   surface. The inline and in-table variant below opts back out, because a
   number centered inside a table row would not line up with its column. */
.mz-metric {
  display: flex;
  flex-direction: column;
  gap: var(--space-05);
  min-width: 0;
  align-items: center;
  text-align: center;
}

.mz-metric-label {
  font-family: var(--mono);
  font-size: var(--fs-label);
  font-weight: 500;
  line-height: 1.33;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mz-fg-muted);
}

.mz-metric-value {
  font-family: var(--display);
  font-size: var(--fs-metric);
  font-weight: var(--weight-serif, 400);
  line-height: 1.10;
  letter-spacing: -0.02em;
  color: var(--mz-fg-heading);
  white-space: nowrap;
}

/* The single dominant number on a surface. Allowed once. */
.mz-metric--xl .mz-metric-value {
  font-size: var(--fs-metric-xl);
  line-height: 1.05;
  letter-spacing: -0.025em;
}

/* Inline or in table metric. Opts OUT of the centering above: inside a table
   row a centered number stops lining up with its column heading. */
.mz-metric--sm {
  align-items: flex-start;
  text-align: left;
}

.mz-metric--sm .mz-metric-value {
  font-family: var(--sans);
  font-size: var(--fs-metric-sm);
  line-height: 1.20;
  letter-spacing: -0.01em;
}

.mz-metric-note {
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 400;
  line-height: 1.33;
  color: var(--mz-fg-muted);
  max-width: 45ch;
  /* The 45ch cap would otherwise pin a short note to the left of a centered
     metric, so the box centers itself under the number. */
  margin-inline: auto;
}

.mz-metric--sm .mz-metric-note { margin-inline: 0; }

.mz-metric-delta {
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 400;
  line-height: 1.33;
  color: var(--mz-status-neutral);
}
.mz-metric-delta[data-state="good"] { color: var(--mz-status-good); }
.mz-metric-delta[data-state="risk"] { color: var(--mz-status-risk); }

/* Loading placeholder inside a value slot. The em dash glyph is
   sanctioned here and nowhere else. */
.mz-metric-value[data-loading="true"] { color: var(--mz-fg-muted); }

/* =====================================================================
   3. TABLE
   Native table, never role="grid". Sticky head on the raised surface,
   hairline row dividers, no zebra striping, numeric columns right
   aligned and tabular, status pill never in the rightmost column.
   ===================================================================== */
.mz-table-wrap {
  width: 100%;
  min-width: 0;
  overflow-x: auto;
}

.mz-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--sans);
  font-size: var(--fs-td);
  line-height: 1.43;
  color: var(--mz-fg-body);
}

.mz-table caption {
  text-align: left;
  color: var(--mz-fg-muted);
}

.mz-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--mz-bg-raised);
  text-align: left;
  font-size: var(--fs-th);
  font-weight: 550;
  line-height: 1.33;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mz-fg-muted);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.mz-table th,
.mz-table td {
  padding: 0 var(--space-2);
  vertical-align: middle;
}
.mz-table th:first-child,
.mz-table td:first-child { padding-left: var(--space-1); }
.mz-table th:last-child,
.mz-table td:last-child  { padding-right: var(--space-1); }

.mz-table tbody tr {
  border-bottom: 1px solid var(--line);
  /* The sticky head must never obscure a focused row (WCAG 2.4.11). */
  scroll-margin-top: 48px;
}
.mz-table tbody tr:last-child { border-bottom: 0; }
.mz-table tbody tr:hover { background: var(--mz-bg-raised); }
.mz-table tbody tr:focus-visible {
  outline: 2px solid var(--mz-accent);
  outline-offset: -2px;
}

/* Row density. Compact 32, default 40, relaxed 48. */
.mz-table[data-density="compact"] tbody td,
.mz-table[data-density="compact"] thead th { height: 32px; }
.mz-table tbody td,
.mz-table thead th { height: 40px; }
.mz-table[data-density="relaxed"] tbody td,
.mz-table[data-density="relaxed"] thead th { height: 48px; }

/* Name cell truncates on one line. The surface supplies a title attribute
   with the full value.
   2026-07-30: this previously used display:-webkit-box with -webkit-line-clamp
   to clamp at two lines. On a <td> that computes to flow-root, which takes the
   cell OUT of table layout and breaks every row on every surface that uses the
   table primitive. A td must keep display:table-cell, so the two line clamp is
   not available here without a wrapper element, and the markup contract puts
   the text directly in the td. Single line ellipsis is the correct trade. */
.mz-table .mz-td-name {
  font-weight: 550;
  color: var(--mz-fg-heading);
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mz-table .mz-td-actions {
  text-align: right;
  white-space: nowrap;
}

/* First column sticky on wide tables. */
.mz-table--freeze-first thead th:first-child,
.mz-table--freeze-first tbody td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--mz-bg-card);
}
.mz-table--freeze-first thead th:first-child {
  z-index: 3;
  background: var(--mz-bg-raised);
}

/* aria-sort belongs on the th[role=columnheader], never on the button inside
   it, where it is invalid ARIA. The button carries data-sort-dir purely as the
   styling hook for the direction glyph below. Results announce into the
   surface's aria-live region, which the surface owns. */
.mz-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--space-05);
  min-height: 24px;
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}
.mz-sort:focus-visible {
  outline: 2px solid var(--mz-accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.mz-sort[data-sort-dir="ascending"]::after  { content: "\2191"; }
.mz-sort[data-sort-dir="descending"]::after { content: "\2193"; }
.mz-sort[data-sort-dir="none"]::after       { content: ""; }

/* Segment chips with counts. Default view is a filtered segment, never
   the raw full list. */
.mz-segments {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-05);
  margin: 0 0 var(--space-2);
}
.mz-segment {
  display: inline-flex;
  align-items: center;
  gap: var(--space-05);
  min-height: 24px;
  padding: var(--space-05) var(--space-1);
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 550;
  color: var(--mz-fg-muted);
  cursor: pointer;
  transition: color var(--mz-dur-instant) var(--mz-ease-standard),
              border-color var(--mz-dur-instant) var(--mz-ease-standard);
}
.mz-segment[aria-pressed="true"] {
  color: var(--mz-fg-heading);
  border-color: var(--mz-accent);
}
.mz-segment:focus-visible {
  outline: 2px solid var(--mz-accent);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------
   Mobile card collapse. Below 768px the table stacks, and because
   display:block strips native table semantics they are restored with
   explicit roles on the markup plus data-label prefixes. No horizontal
   scroll on phones.
   --------------------------------------------------------------------- */
@media (max-width: 767px) {
  .mz-table-wrap { overflow-x: visible; }
  .mz-table,
  .mz-table thead,
  .mz-table tbody,
  .mz-table tr,
  .mz-table th,
  .mz-table td { display: block; }
  .mz-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .mz-table tbody tr {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    padding: var(--space-2);
    margin: 0 0 var(--space-1);
  }
  .mz-table tbody td {
    height: auto;
    padding: var(--space-05) 0;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    text-align: left;
  }
  .mz-table tbody td.num { text-align: right; }
  .mz-table tbody td[data-label]::before {
    content: attr(data-label);
    flex: none;
    font-family: var(--mono);
    font-size: var(--fs-label);
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--mz-fg-muted);
  }
  /* The name cell rule outranks the generic cell rule above, so the first
     cell would keep its desktop treatment while every other cell became a
     labelled row. Put it back in the row, and clear the desktop truncation:
     inside a stacked card there is full width available, so a name should
     wrap rather than ellipsis. */
  .mz-table tbody td.mz-td-name {
    display: flex;
    flex-wrap: wrap;
    max-width: none;
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
  }
  .mz-table .mz-td-actions { justify-content: flex-end; }
  /* Mobile primary targets clear 44px. */
  .mz-table .mz-btn { min-height: 44px; }
}

/* =====================================================================
   4. STATUS PILL
   Always color plus glyph plus word. Soft fill, full strength text, one
   glyph, 1px border at the status hue. Never derived from the brand
   layer, so a green branded gym never reads brand as success.
   ===================================================================== */
.mz-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-05);
  min-height: 24px;
  padding: var(--space-025) var(--space-1);
  border-radius: var(--radius-sm);
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 550;
  line-height: 1.33;
  white-space: nowrap;
  background: var(--mz-status-neutral-soft);
  color: var(--mz-status-neutral);
  border: 1px solid color-mix(in srgb, var(--mz-status-neutral) 24%, transparent);
}
.mz-pill-glyph {
  flex: none;
  font-family: var(--mono);
  font-weight: 500;
  line-height: 1;
}
.mz-pill--good {
  background: var(--mz-status-good-soft);
  color: var(--mz-status-good);
  border-color: color-mix(in srgb, var(--mz-status-good) 24%, transparent);
}
.mz-pill--risk {
  background: var(--mz-status-risk-soft);
  color: var(--mz-status-risk);
  border-color: color-mix(in srgb, var(--mz-status-risk) 24%, transparent);
}
.mz-pill--info {
  background: var(--mz-status-info-soft);
  color: var(--mz-status-info);
  border-color: color-mix(in srgb, var(--mz-status-info) 24%, transparent);
}
.mz-pill--neutral {
  background: var(--mz-status-neutral-soft);
  color: var(--mz-status-neutral);
  border-color: color-mix(in srgb, var(--mz-status-neutral) 24%, transparent);
}
@supports not (background: color-mix(in srgb, red, blue)) {
  .mz-pill { border-color: var(--line); }
}

/* =====================================================================
   5. BUTTONS
   Two levels only: one primary per zone, quiet for everything else.
   Focus ring is the brand layer's job and carries no glow.
   ===================================================================== */
.mz-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-05);
  min-height: 24px;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 550;
  line-height: 1.33;
  cursor: pointer;
  background: none;
  border: 1px solid var(--line);
  color: var(--mz-fg-body);
  transition: border-color var(--mz-dur-instant) var(--mz-ease-standard),
              opacity var(--mz-dur-instant) var(--mz-ease-standard);
}
.mz-btn:hover { border-color: var(--mz-fg-heading); }
.mz-btn:focus-visible {
  outline: 2px solid var(--mz-accent);
  outline-offset: 2px;
}
.mz-btn[disabled] { opacity: 0.55; cursor: not-allowed; }

.mz-btn--primary {
  background: var(--mz-brand-primary);
  border-color: var(--mz-brand-primary);
  color: var(--mz-brand-on-primary);
}
.mz-btn--quiet {
  border-color: transparent;
  color: var(--mz-fg-muted);
  padding-left: var(--space-1);
  padding-right: var(--space-1);
}
.mz-btn--quiet:hover { border-color: var(--line); color: var(--mz-fg-body); }

/* The dark theme focus ring follows this stack's established convention:
   solid marrow red scores 1.62:1 on black, the ember token clears AA. */
:root[data-mz-theme="dark"] .mz-btn:focus-visible,
:root[data-mz-theme="dark"] .mz-sort:focus-visible,
:root[data-mz-theme="dark"] .mz-segment:focus-visible,
:root[data-mz-theme="dark"] .mz-table tbody tr:focus-visible {
  outline-color: var(--marrow-ember-text);
}

/* =====================================================================
   6. EMPTY STATE
   Left aligned, no illustration, no icon, exactly one CTA or none.
   First use states say what will fill the space. No results states are
   visually lighter and offer a clear filters action.

   assets/css/vitality.css loads BEFORE this file on the coach and
   athlete dashboards and paints .mz-empty as a centered card with a
   radial wash, a 20px radius, a box shadow, and a decorative ::before.
   The gym console does not load that file at all, so the same class
   rendered differently per surface. The reset below makes all three
   surfaces render one component. It is a deliberate override, not a
   duplicate.
   ===================================================================== */
.mz-empty {
  position: static;
  overflow: visible;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  text-align: left;
  padding: var(--space-3) 0;
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  box-shadow: none;
}
.mz-empty::before { content: none; }

.mz-empty-head {
  margin: 0;
  font-family: var(--sans);
  font-size: var(--fs-card);
  font-weight: 550;
  line-height: 1.375;
  color: var(--mz-fg-heading);
}
.mz-empty-body {
  margin: 0;
  max-width: 45ch;
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.50;
  color: var(--mz-fg-muted);
}
.mz-empty .mz-btn { margin-top: var(--space-05); }

/* No results variant, lighter than a first use state. */
.mz-empty--filtered .mz-empty-head {
  font-size: var(--fs-body);
  font-weight: 550;
  color: var(--mz-fg-body);
}
.mz-empty--filtered { padding: var(--space-2) 0; }

/* =====================================================================
   7. ERROR STATE
   Card scoped, never page scoped. One failed widget never blanks a
   surface. Messages are short and human, no codes, no tech brand names.
   ===================================================================== */
.mz-error {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  padding: var(--space-2) 0;
}
.mz-error-msg {
  margin: 0;
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 550;
  line-height: 1.50;
  color: var(--mz-fg-heading);
}
.mz-error-help {
  flex-basis: 100%;
  margin: 0;
  max-width: 45ch;
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 400;
  line-height: 1.33;
  color: var(--mz-fg-muted);
}

/* =====================================================================
   8. LOADING STATE
   Skeletons match the final layout exactly, per card and per table,
   never one page skeleton. A card resolves independently the moment its
   own data lands.
   ===================================================================== */
.mz-skeleton {
  display: block;
  border-radius: var(--radius-sm);
  background-color: var(--line);
  background-image: linear-gradient(
    90deg,
    var(--line) 0%,
    var(--mz-bg-raised) 50%,
    var(--line) 100%
  );
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: mz-skeleton-shimmer 1200ms linear infinite;
}
@keyframes mz-skeleton-shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

.mz-skeleton--text   { height: 14px; width: 100%; }
.mz-skeleton--label  { height: 12px; width: 40%; }
.mz-skeleton--metric { height: 32px; width: 60%; }
.mz-skeleton--row    { height: 40px; width: 100%; }
.mz-skeleton--card   { height: 120px; width: 100%; border-radius: var(--radius-md); }

.mz-skeleton-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* Reduced motion: static fill, no shimmer. The site wide reduced motion
   blocks cannot reach a keyframe animation declared in this file, so the
   fallback is declared here and only here. */
@media (prefers-reduced-motion: reduce) {
  .mz-skeleton {
    animation: none;
    background-image: none;
    background-color: var(--line);
  }
  .mz-btn,
  .mz-segment { transition: none; }
}

/* =====================================================================
   9. SPARKLINE
   Hand built inline SVG from MzUI.sparkline. No charting library. 2px
   line, no fill, no gradient, no shadow, axis free, the current numeric
   value always adjacent, and an aria-label stating the trend in words.
   Fewer than 7 points renders no chart at all, value only.

   The line takes --mz-accent-text, the one accent token in this stack
   that is remapped for the dark theme, so the stroke clears the 3:1
   floor against both page surfaces.
   ===================================================================== */
.mz-sparkline {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--mz-accent-text);
}
.mz-sparkline-svg {
  display: block;
  flex: none;
  overflow: visible;
}
.mz-sparkline-svg .mz-sparkline-path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: square;
  stroke-linejoin: round;
}
.mz-sparkline-value {
  font-family: var(--display);
  font-size: var(--fs-metric-sm);
  font-weight: var(--weight-serif, 400);
  line-height: 1.20;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--mz-fg-heading);
}

/* =====================================================================
   10. FLOATING LAYERS
   The one sanctioned literal shadow in this file, light theme only, for
   layers that genuinely float: dropdown, popover, modal, drawer. Dark
   mode uses the raised surface ladder and a hairline instead, because
   shadows are banned outright on black.
   ===================================================================== */
.mz-floating {
  background: var(--mz-bg-overlay);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: 0 12px 40px rgba(10, 9, 8, 0.12);
}
.mz-floating--modal { background: var(--mz-bg-modal); }
:root[data-mz-theme="dark"] .mz-floating,
:root[data-mz-theme="dark"] .mz-floating--modal,
:root[data-mz-theme="dark"] .mz-card,
:root[data-mz-theme="dark"] .mz-empty,
:root[data-mz-theme="dark"] .mz-skeleton { box-shadow: none; }

/* =====================================================================
   11. VIEW SWITCHER
   The "Switch view" control, rendered by assets/js/view-switcher.js into
   any [data-marrow-view-switcher] mount point in a dashboard nav. It is
   in this shared layer, not in a surface stylesheet, because all three
   dashboards mount it and a forked copy would drift.

   It is built from the primitives above: .mz-btn and .mz-btn--quiet for
   the trigger and the retry, .mz-floating for the open menu, .sr-only for
   the current-view text. Only the parts those cannot express are defined
   here. No new brand tokens, no literal colors.
   ===================================================================== */
/* The mount point ships hidden and stays hidden unless the module renders
   something into it. Belt and braces for the same reason: the three navs are
   flex rows with a gap, so an empty but displayed mount would widen the nav
   for the single-view majority who are supposed to see no change at all. */
[data-marrow-view-switcher]:empty { display: none; }

.mz-viewswitch {
  position: relative;
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-05) var(--space-1);
  max-width: 100%;
}

/* The closed control. Reads "Viewing as Coach" on its face, so the whole
   button text is also its accessible name. */
.mz-viewswitch-trigger {
  gap: var(--space-05);
  max-width: 100%;
}
.mz-viewswitch-kicker {
  font-family: var(--mono);
  font-size: var(--fs-help);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--mz-fg-muted);
}
.mz-viewswitch-current {
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 550;
  color: var(--mz-fg-heading);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mz-viewswitch-caret {
  flex: none;
  color: var(--mz-fg-muted);
  transition: transform var(--mz-dur-instant) var(--mz-ease-standard);
}
.mz-viewswitch-trigger[aria-expanded="true"] .mz-viewswitch-caret {
  transform: rotate(180deg);
}

/* THE TWO ROLE TOGGLE, 2026-09-15. Exactly two views render a segmented
   radiogroup instead of a menu, so the current view and the target are both
   on screen. Pill radius is the literal 999px per the design system contract. */
.mz-viewswitch--toggle [role="radiogroup"] {
  display: inline-flex;
  padding: 3px;
  border-radius: 999px;
  background: var(--mz-bg-raised, var(--paper-card));
  border: 1px solid var(--rule-soft);
}
.mz-viewswitch-seg {
  padding: 6px 12px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-secondary);
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
}
.mz-viewswitch-seg[aria-checked="true"] {
  background: var(--ink-primary);
  color: var(--paper-canvas);
  font-weight: 550;
}
.mz-viewswitch-seg:focus-visible {
  outline: 2px solid var(--marrow-accent);
  outline-offset: 2px;
}
.mz-viewswitch-seg[disabled] { opacity: 0.55; cursor: not-allowed; }
.mz-viewswitch--toggle[aria-busy="true"] [role="radiogroup"] { opacity: 0.55; }
.mz-viewswitch[aria-busy="true"] .mz-viewswitch-trigger { opacity: 0.55; }

/* The open menu. .mz-floating already supplies the surface, the hairline
   and the light-theme shadow. */
.mz-viewswitch-list {
  position: absolute;
  top: calc(100% + var(--space-05));
  left: 0;
  z-index: 60;
  min-width: 168px;
  margin: 0;
  padding: var(--space-05);
  list-style: none;
}
.mz-viewswitch-list[hidden] { display: none; }

.mz-viewswitch-item {
  display: block;
  width: 100%;
  padding: var(--space-1) var(--space-2);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 500;
  line-height: 1.50;
  text-align: left;
  color: var(--mz-fg-body);
  cursor: pointer;
  transition: background-color var(--mz-dur-instant) var(--mz-ease-standard);
}
.mz-viewswitch-item:hover { background: var(--mz-bg-raised); }
.mz-viewswitch-item:focus-visible {
  outline: 2px solid var(--mz-accent);
  outline-offset: -2px;
}
:root[data-mz-theme="dark"] .mz-viewswitch-item:focus-visible {
  outline-color: var(--marrow-ember-text);
}
/* The view already on screen. Weight and a hairline carry it, so the state
   is never color alone. aria-checked carries it for assistive tech. */
.mz-viewswitch-item[aria-checked="true"] {
  font-weight: 580;
  color: var(--mz-fg-heading);
  background: var(--mz-bg-raised);
}
.mz-viewswitch-item[disabled] { opacity: 0.55; cursor: not-allowed; }

/* The honest degradation. Muted, quiet, not a status color and not an
   error banner. It says what is true and offers the one useful action. */
.mz-viewswitch--unconfirmed {
  flex-wrap: wrap;
  gap: var(--space-05) var(--space-1);
}
.mz-viewswitch-note {
  margin: 0;
  max-width: 34ch;
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 400;
  line-height: 1.33;
  color: var(--mz-fg-muted);
}

/* The politeness channel. It doubles as the VISIBLE failure text after a
   refused switch, so it is never display:none. An aria-live region that is
   removed from the render tree between announcements is routinely missed,
   which is why the empty state collapses to zero height instead of being
   hidden outright. */
.mz-viewswitch-status {
  margin: 0;
  max-width: 34ch;
  font-family: var(--sans);
  font-size: var(--fs-help);
  font-weight: 400;
  line-height: 1.33;
  color: var(--mz-fg-muted);
}
.mz-viewswitch-status:empty {
  height: 0;
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .mz-viewswitch-seg,
  .mz-viewswitch-caret,
  .mz-viewswitch-item { transition: none; }
}
