/* Range Day Pro PWA — minimal utility CSS.
   Basic/functional styling; visual parity effort is a separate future item. */

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

body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.5; }

/* Layout */
.min-h-screen { min-height: 100vh; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
.flex-1 { flex: 1; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-4 { gap: 1rem; }
.text-center { text-align: center; }
.text-left { text-align: left; }
.block { display: block; }
.hidden { display: none; }
.w-full { width: 100%; }
.h-full { height: 100%; }
.overflow-auto { overflow: auto; }
.relative { position: relative; }

/* Spacing */
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-8 { padding: 2rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
.pb-20 { padding-bottom: 5rem; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }

/* Sizing */
.max-w-md { max-width: 28rem; }
.max-w-lg { max-width: 32rem; }
.mx-auto { margin-left: auto; margin-right: auto; }

/* Typography */
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Colors */
.bg-gray-50 { background-color: #f9fafb; }
.bg-gray-100 { background-color: #f3f4f6; }
.bg-white { background-color: #fff; }
.bg-blue-600 { background-color: #2563eb; }
.bg-green-50 { background-color: #f0fdf4; }
.bg-red-50 { background-color: #fef2f2; }
.bg-yellow-50 { background-color: #fffbeb; }
.text-gray-400 { color: #9ca3af; }
.text-gray-500 { color: #6b7280; }
.text-gray-600 { color: #4b5563; }
.text-gray-700 { color: #374151; }
.text-gray-900 { color: #111827; }
.text-white { color: #fff; }
.text-blue-600 { color: #2563eb; }
.text-blue-700 { color: #1d4ed8; }
.text-green-700 { color: #15803d; }
.text-red-600 { color: #dc2626; }
.text-red-700 { color: #b91c1c; }
.text-yellow-800 { color: #92400e; }
.border { border-width: 1px; border-style: solid; }
.border-t { border-top-width: 1px; border-top-style: solid; }
.border-gray-200 { border-color: #e5e7eb; }
.border-gray-300 { border-color: #d1d5db; }
.border-green-200 { border-color: #bbf7d0; }
.border-red-200 { border-color: #fecaca; }
.border-yellow-200 { border-color: #fde68a; }

/* Components */
.rounded { border-radius: 0.25rem; }
.rounded-lg { border-radius: 0.5rem; }
.shadow { box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06); }
.shadow-top { box-shadow: 0 -1px 3px rgba(0,0,0,0.1); }

/* Forms */
input[type="text"], input[type="email"], input[type="password"], input[type="number"], textarea, select {
    border: 1px solid #d1d5db;
    outline: none;
    font-size: 1rem;
}
input:focus, textarea:focus, select:focus {
    box-shadow: 0 0 0 2px #3b82f6;
    border-color: #3b82f6;
}

/* Buttons */
button { cursor: pointer; border: none; font-size: 1rem; }
.hover\:bg-blue-700:hover { background-color: #1d4ed8; }
.hover\:bg-gray-100:hover { background-color: #f3f4f6; }
.hover\:underline:hover { text-decoration: underline; }
.disabled\:opacity-50:disabled { opacity: 0.5; }
.disabled\:cursor-not-allowed:disabled { cursor: not-allowed; }

/* Links */
a { text-decoration: none; color: inherit; }

/* Tab bar — fixed bottom on mobile */
.tab-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    background: #fff;
    border-top: 1px solid #e5e7eb;
    padding: 0.25rem 0;
    padding-bottom: max(0.25rem, env(safe-area-inset-bottom));
    z-index: 50;
}
.tab-bar a, .tab-bar button {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.125rem;
    padding: 0.375rem 0;
    font-size: 0.625rem;
    color: #6b7280;
    background: none;
    border: none;
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
}
.tab-bar a.active, .tab-bar button.active {
    color: #2563eb;
}

/* Offline banner — in normal flow above the scrollable content so it pushes
   the page down instead of overlaying the header. */
.offline-banner {
    flex-shrink: 0;
    background: #fffbeb;
    border-bottom: 1px solid #fde68a;
    color: #92400e;
    text-align: center;
    padding: 0.5rem;
    font-size: 0.875rem;
}

/* Profile list items */
.profile-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid #e5e7eb;
    cursor: pointer;
}
.profile-item:hover {
    background-color: #f9fafb;
}

/* Toggle switch */
.toggle {
    position: relative;
    width: 2.75rem;
    height: 1.5rem;
    background: #d1d5db;
    border-radius: 0.75rem;
    cursor: pointer;
    transition: background 0.2s;
    border: none;
    padding: 0;
}
.toggle.on {
    background: #2563eb;
}
.toggle::after {
    content: '';
    position: absolute;
    top: 0.125rem;
    left: 0.125rem;
    width: 1.25rem;
    height: 1.25rem;
    background: #fff;
    border-radius: 50%;
    transition: transform 0.2s;
}
.toggle.on::after {
    transform: translateX(1.25rem);
}

/* Drill cards */
.drill-card {
    display: flex;
    width: 100%;
    padding: 0.75rem 1rem;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    cursor: pointer;
    text-align: left;
}
.drill-card:hover {
    background: #f9fafb;
}

/* Drill type badges */
.drill-badge {
    display: inline-block;
    padding: 0.125rem 0.5rem;
    font-size: 0.625rem;
    font-weight: 600;
    border-radius: 9999px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.drill-badge-live {
    background: #fef2f2;
    color: #b91c1c;
}
.drill-badge-dry {
    background: #f0fdf4;
    color: #15803d;
}

/* Queue link with badge */
.queue-link {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.875rem;
    color: #2563eb;
    cursor: pointer;
}
.queue-link:hover {
    text-decoration: underline;
}
.queue-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.25rem;
    height: 1.25rem;
    padding: 0 0.25rem;
    font-size: 0.625rem;
    font-weight: 700;
    color: #fff;
    background: #dc2626;
    border-radius: 9999px;
}

/* Zone counter rows */
.zone-counter-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: 2.75rem; /* 44px for tap targets */
    padding: 0.25rem 0.5rem;
    border-bottom: 1px solid #f3f4f6;
}
.zone-counter-row:last-child {
    border-bottom: none;
}
.zone-name {
    font-size: 0.875rem;
    font-weight: 500;
    color: #374151;
}
.zone-controls {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}
.zone-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem; /* 44px */
    height: 2.75rem; /* 44px */
    font-size: 1.25rem;
    font-weight: 600;
    color: #374151;
    background: #f3f4f6;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.zone-btn:hover {
    background: #e5e7eb;
}
.zone-btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}
.zone-count {
    min-width: 2rem;
    text-align: center;
    font-size: 1.125rem;
    font-weight: 600;
    color: #111827;
}

/* Score entry form */
.score-preview {
    padding: 0.75rem;
    background: #f0fdf4;
    border: 1px solid #bbf7d0;
    border-radius: 0.5rem;
}

/* Pass/Fail buttons */
.pass-fail-btn {
    flex: 1;
    padding: 0.5rem 1rem;
    border: 1px solid #d1d5db;
    border-radius: 0.375rem;
    background: #fff;
    font-weight: 500;
    cursor: pointer;
}
.pass-fail-active {
    background: #f0fdf4;
    border-color: #15803d;
    color: #15803d;
}
.pass-fail-fail-active {
    background: #fef2f2;
    border-color: #b91c1c;
    color: #b91c1c;
}

/* Score cards in history */
.score-card {
    display: flex;
    width: 100%;
    padding: 0.75rem 1rem;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    cursor: pointer;
    text-align: left;
}
.score-card:hover {
    background: #f9fafb;
}

/* Score detail rows */
.score-detail-row {
    display: flex;
    justify-content: space-between;
    padding: 0.5rem 0;
    border-bottom: 1px solid #f3f4f6;
}
.score-detail-row:last-child {
    border-bottom: none;
}
.score-detail-label {
    font-size: 0.875rem;
    color: #6b7280;
}
.score-detail-value {
    font-size: 0.875rem;
    font-weight: 500;
    color: #111827;
}

/* Score zone breakdown table */
.score-zone-table {
    border: 1px solid #e5e7eb;
    border-radius: 0.375rem;
    overflow: hidden;
}
.score-zone-header {
    display: flex;
    justify-content: space-between;
    padding: 0.5rem 0.75rem;
    background: #f9fafb;
    font-size: 0.75rem;
    font-weight: 600;
    color: #6b7280;
    text-transform: uppercase;
}
.score-zone-row {
    display: flex;
    justify-content: space-between;
    padding: 0.5rem 0.75rem;
    border-top: 1px solid #f3f4f6;
    font-size: 0.875rem;
    color: #374151;
}

/* Void confirmation overlay */
.void-confirm-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 200;
    padding: 1rem;
}
.void-confirm-dialog {
    background: #fff;
    border-radius: 0.75rem;
    padding: 1.5rem;
    width: 100%;
    max-width: 24rem;
    box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15);
}

/* Queue management */
.queue-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.75rem;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
}
.queue-item-failed {
    border-color: #fecaca;
    background: #fef2f2;
}

/* Event cards */
.event-card {
    display: flex;
    width: 100%;
    padding: 0.75rem 1rem;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    cursor: pointer;
    text-align: left;
}
.event-card:hover {
    background: #f9fafb;
}

/* Event type badges */
.event-badge {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 600;
    padding: 0.125rem 0.5rem;
    border-radius: 9999px;
    background: #f3f4f6;
    color: #6b7280;
}
.event-badge-org {
    background: #dbeafe;
    color: #1d4ed8;
}
.event-badge-rangeday {
    background: #d1fae5;
    color: #047857;
}

/* Event status badges */
.event-status {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 600;
    padding: 0.125rem 0.5rem;
    border-radius: 9999px;
    background: #f3f4f6;
    color: #6b7280;
}
.event-status-draft {
    background: #fef3c7;
    color: #92400e;
}
.event-status-active {
    background: #d1fae5;
    color: #047857;
}
.event-status-completed {
    background: #e0e7ff;
    color: #3730a3;
}

/* Tab buttons (challenges, leaderboard, etc.) */
.tab-btn {
    flex: 1;
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    font-weight: 500;
    color: #6b7280;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    cursor: pointer;
}
.tab-btn:hover {
    color: #374151;
}
.tab-btn-active {
    color: #2563eb;
    border-bottom-color: #2563eb;
}

/* Grid layout */
.grid { display: grid; }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.aspect-square { aspect-ratio: 1 / 1; }
.object-cover { object-fit: cover; }
.overflow-hidden { overflow: hidden; }

/* Group hover utilities */
.group:hover .group-hover\:opacity-100 { opacity: 1; }
.transition-opacity { transition: opacity 0.2s; }

/* Opacity utilities */
.opacity-0 { opacity: 0; }
.bg-opacity-50 { --tw-bg-opacity: 0.5; }
.bg-opacity-60 { --tw-bg-opacity: 0.6; }
.bg-opacity-80 { --tw-bg-opacity: 0.8; }
.bg-black { background-color: rgba(0, 0, 0, var(--tw-bg-opacity, 1)); }

/* Blue tones for leaderboard highlight */
.bg-blue-50 { background-color: #eff6ff; }

/* Hover utilities */
.hover\:bg-red-50:hover { background-color: #fef2f2; }
.hover\:bg-blue-700:hover { background-color: #1d4ed8; }
.hover\:bg-gray-200:hover { background-color: #e5e7eb; }
.bg-gray-800 { background-color: #1f2937; }
.bg-red-600 { background-color: #dc2626; }
.border-b { border-bottom-width: 1px; border-bottom-style: solid; }

/* Additional spacing */
.mt-6 { margin-top: 1.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.gap-3 { gap: 0.75rem; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.py-0\.5 { padding-top: 0.125rem; padding-bottom: 0.125rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.w-6 { width: 1.5rem; }
.h-6 { height: 1.5rem; }
.w-8 { width: 2rem; }
.mx-4 { margin-left: 1rem; margin-right: 1rem; }
.rounded-full { border-radius: 9999px; }
.inline-flex { display: inline-flex; }

/* Absolute positioning */
.absolute { position: absolute; }
.fixed { position: fixed; }
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
.top-1 { top: 0.25rem; }
.right-1 { right: 0.25rem; }
/* Full-screen overlays sit on the modal layer (200, same as the dialog rule
   above). At 50 they tied with .tab-bar and LOST on DOM order — ShellLayout
   renders the tab bar after the Outlet — so the bar drew over the lightbox. */
.z-modal { z-index: 200; }

/* Event roster row */
.event-roster-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.5rem 0.75rem;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
}

/* Drill authoring (shooter-drill-authoring chunk 1) */
.bg-yellow-500 { background-color: #eab308; }
.p-1 { padding: 0.25rem; }
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
.w-20 { width: 5rem; }
.hover\:bg-red-700:hover { background-color: #b91c1c; }
.stage-photo {
    width: 100%;
    max-height: 16rem;
    object-fit: contain;
    background-color: #f9fafb;
    border-radius: 0.25rem;
}

/* Read-only vector stage diagram (step 11) — the inlined core-rendered SVG
   scales to the container width; its viewBox keeps the scene's aspect. */
.stage-diagram {
    width: 100%;
    border-radius: 0.25rem;
    overflow: hidden;
}
.stage-diagram svg {
    display: block;
    width: 100%;
    height: auto;
    max-height: 24rem;
}

/* Stage-diagram editor (step 12) — canvas, toolbar, palette tiles. */
.stage-editor-canvas {
    width: 100%;
    border-radius: 0.25rem;
    overflow: hidden;
    touch-action: none; /* pointer events own the canvas: no scroll-while-drag */
}
.stage-editor-canvas svg {
    display: block;
    width: 100%;
    height: auto;
}
.stage-editor-toolbar {
    display: flex;
    gap: 0.375rem;
    flex-wrap: wrap;
    margin-bottom: 0.75rem;
}
.se-btn {
    padding: 0.375rem 0.625rem;
    font-size: 0.8rem;
    background: #fff;
    border: 1px solid #d1d5db;
    border-radius: 0.25rem;
    color: #374151;
}
.se-btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}
.se-danger {
    color: #dc2626;
    border-color: #fecaca;
}
.stage-editor-palette {
    display: flex;
    gap: 0.5rem;
    overflow-x: auto;
    padding-bottom: 0.25rem;
    -webkit-overflow-scrolling: touch;
}
.se-tile {
    flex: 0 0 auto;
    width: 4.5rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.25rem;
    padding: 0.375rem;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 0.375rem;
}
.se-tile svg {
    width: 2.25rem;
    height: 2.25rem;
}
.se-tile span {
    font-size: 0.55rem;
    color: #6b7280;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

/* Avatar component (avatar.rs) — display + initials-fallback sizing. These
   utilities were referenced by PR-4's avatar.rs but never added here, so the
   fallback background and non-default sizes silently no-op. Additive. */
.h-8 { height: 2rem; }
.w-10 { width: 2.5rem; }
.h-10 { height: 2.5rem; }
.w-16 { width: 4rem; }
.h-16 { height: 4rem; }
.bg-gray-300 { background-color: #d1d5db; }
.shrink-0 { flex-shrink: 0; }

/* Backfill (check-css launch): classes the rsx already used that were
   silently no-oping — standard Tailwind v3 definitions. Found by
   `rangeday check css`, which now gates CI. */
.-right-1 { right: -0.25rem; }
.-top-1 { top: -0.25rem; }
.bg-gray-200 { background-color: #e5e7eb; }
.bg-green-600 { background-color: #16a34a; }
.bg-red-100 { background-color: #fee2e2; }
.bg-yellow-100 { background-color: #fef9c3; }
.border-blue-200 { border-color: #bfdbfe; }
.border-gray-100 { border-color: #f3f4f6; }
.border-t-0 { border-top-width: 0; }
.cursor-pointer { cursor: pointer; }
.flex-shrink-0 { flex-shrink: 0; }
.h-2 { height: 0.5rem; }
.h-3 { height: 0.75rem; }
.inline-block { display: inline-block; }
.items-baseline { align-items: baseline; }
.justify-end { justify-content: flex-end; }
.max-w-sm { max-width: 24rem; }
.max-w-xs { max-width: 20rem; }
.mb-8 { margin-bottom: 2rem; }
.min-w-0 { min-width: 0; }
.ml-auto { margin-left: auto; }
.mt-3 { margin-top: 0.75rem; }
.mt-8 { margin-top: 2rem; }
.p-6 { padding: 1.5rem; }
.pt-3 { padding-top: 0.75rem; }
.py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
.rounded-b { border-bottom-left-radius: 0.25rem; border-bottom-right-radius: 0.25rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.text-amber-600 { color: #d97706; }
.text-amber-700 { color: #b45309; }
.text-gray-800 { color: #1f2937; }
.text-green-600 { color: #16a34a; }
.tracking-wide { letter-spacing: 0.025em; }
.uppercase { text-transform: uppercase; }
.w-2 { width: 0.5rem; }
.w-3 { width: 0.75rem; }
.whitespace-pre-wrap { white-space: pre-wrap; }
.hover\:bg-blue-50:hover { background-color: #eff6ff; }
.hover\:bg-gray-50:hover { background-color: #f9fafb; }
.hover\:bg-green-50:hover { background-color: #f0fdf4; }
.hover\:bg-green-700:hover { background-color: #15803d; }
.hover\:text-blue-800:hover { color: #1e40af; }
.focus\:outline-none:focus { outline: none; }
.focus\:ring-2:focus { outline: none; box-shadow: 0 0 0 2px var(--rdp-ring, rgba(59, 130, 246, 0.5)); border-color: var(--rdp-ring, #3b82f6); }
.focus\:ring-blue-500:focus { --rdp-ring: #3b82f6; }
@media (min-width: 640px) { .sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.space-y-3 > * + * { margin-top: 0.75rem; }
/* Media gallery (PR-4): section stack + square thumbs. */
.media-gallery { display: flex; flex-direction: column; gap: 1rem; }
.media-thumb { aspect-ratio: 1 / 1; border-radius: 0.375rem; }
.bg-blue-100 { background-color: #dbeafe; }
.bg-green-100 { background-color: #dcfce7; }

/* Score-history trend chart (score-history-trends F5) — the core-emitted SVG
   scales to the container width; its viewBox keeps the aspect, same contract
   as .stage-diagram. */
.pb-3 { padding-bottom: 0.75rem; }
.trend-chart {
    width: 100%;
    /* The 360x180 viewBox scales with its container, so an uncapped chart was
       1865x933 on a desktop viewport (measured on the staging drive). Cap the
       CONTAINER's width rather than the svg's height: capping height leaves the
       box full-width with the plot letterboxed in ~676px of dead space either
       side, whereas this keeps the aspect with no wasted width, and phones —
       narrower than the cap — are unaffected. */
    max-width: 32rem;
    border-radius: 0.25rem;
    overflow: hidden;
}
.trend-chart svg {
    display: block;
    width: 100%;
    height: auto;
}

/* ── Admin surfaces (merged from the dashboard's stylesheet) ─────────────
 *
 * Appended verbatim when the admin pages moved into this crate. These apps
 * have NO Tailwind build: an unlisted class silently no-ops rather than
 * failing, so a moved page whose classes stayed behind renders wrong with
 * nothing to notice. `rangeday check css` guards literal classes in CI; it
 * cannot see dynamically-composed class strings, which is what the admin
 * staging drive is for.
 */
.items-end { align-items: flex-end; }
.col-span-2 { grid-column: span 2 / span 2; }
.text-right { text-align: right; }
.ml-2 { margin-left: 0.5rem; }
.py-2\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.pb-2 { padding-bottom: 0.5rem; }
.ml-1 { margin-left: 0.25rem; }
.mr-2 { margin-right: 0.5rem; }
.mr-3 { margin-right: 0.75rem; }
.max-w-3xl { max-width: 48rem; }
.max-w-7xl { max-width: 80rem; }
.tabular-nums { font-variant-numeric: tabular-nums; }
.text-red-500 { color: #ef4444; }
.rounded-t-lg {
    border-top-left-radius: 0.5rem;
    border-top-right-radius: 0.5rem;
}
input[type="text"], input[type="email"], input[type="password"] { border: 1px solid #d1d5db; outline: none; }
input:focus { box-shadow: 0 0 0 2px #3b82f6; border-color: #3b82f6; }
.bg-amber-50 { background-color: #fffbeb; }
.border-amber-200 { border-color: #fde68a; }
.gap-0 { gap: 0; }
.gap-0\.5 { gap: 0.125rem; }
.gap-1\.5 { gap: 0.375rem; }
.gap-6 { gap: 1.5rem; }
.gap-x-6 { column-gap: 1.5rem; }
.gap-y-3 { row-gap: 0.75rem; }
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.h-32 { height: 8rem; }
.italic { font-style: italic; }
.leading-none { line-height: 1; }
.list-disc { list-style-type: disc; }
.max-h-48 { max-height: 12rem; }
.max-w-xl { max-width: 36rem; }
.min-w-\[200px\] { min-width: 200px; }
.ml-4 { margin-left: 1rem; }
.mr-1 { margin-right: 0.25rem; }
.object-contain { object-fit: contain; }
.overflow-x-auto { overflow-x: auto; }
.overflow-y-auto { overflow-y: auto; }
.pl-4 { padding-left: 1rem; }
.pr-3 { padding-right: 0.75rem; }
.pt-2 { padding-top: 0.5rem; }
.pt-4 { padding-top: 1rem; }
.px-1\.5 { padding-left: 0.375rem; padding-right: 0.375rem; }
.text-gray-300 { color: #d1d5db; }
.text-red-400 { color: #f87171; }
.w-64 { width: 16rem; }
.hover\:bg-gray-300:hover { background-color: #d1d5db; }
.hover\:bg-red-200:hover { background-color: #fecaca; }
.hover\:text-gray-600:hover { color: #4b5563; }
.hover\:text-gray-900:hover { color: #111827; }
.hover\:text-red-600:hover { color: #dc2626; }
.hover\:text-red-700:hover { color: #b91c1c; }
.hover\:text-red-800:hover { color: #991b1b; }
.disabled\:opacity-30:disabled { opacity: 0.3; }
.focus\:ring-1:focus {
    outline: none;
    box-shadow: 0 0 0 1px var(--rdp-ring, rgba(59, 130, 246, 0.5));
}
.divide-y > * + * { border-top-width: 1px; }
.space-y-1 > * + * { margin-top: 0.25rem; }
.space-y-2 > * + * { margin-top: 0.5rem; }
.space-y-4 > * + * { margin-top: 1rem; }
.space-y-6 > * + * { margin-top: 1.5rem; }

/* Responsive admin rules. A `^`-anchored sweep of the source sheet drops
 * these entirely — they fail only on narrow viewports, which is a
 * miserable way to find out. */
@media (min-width: 768px) {
    .md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .md\:col-span-3 { grid-column: span 3 / span 3; }
}

/* Archivo, self-hosted. No CDN: the app must boot and render from service-worker
   cache with no network at all, which is the whole point of self-hosting — see
   tmp/design/specs/A1-font-pipeline.md for the measurements behind these ranges.
   Declared ranges MUST match the file's real fvar axes, or the browser
   synthesises the out-of-range values instead of interpolating them. */
@font-face {
  font-family: 'Archivo';
  src: url('/fonts/archivo-var-subset.woff2') format('woff2');
  font-weight: 400 900;
  font-stretch: 62% 100%;
  font-display: swap;
}

/* ==========================================================================
   Design-kit token layer — see tmp/design/specs/00-substrate.md
   ==========================================================================
   Two themes because the app is used in direct sunlight. Quoting the kit's own
   reasoning, because it is a design decision rather than a code comment:

     "volt stays the fill in both, text on volt is always black, and what
      changes is everything around it. Light mode swaps volt for a dark olive
      wherever volt would be TEXT — volt on white is unreadable."

   Hence --rd-volt and --rd-acc are NOT the same token and must not be merged:
   --rd-volt is the accent as a FILL and is identical in both themes;
   --rd-acc is the accent as TEXT or STROKE and goes dark olive in light.
   Name the role, never the colour: a border is a stroke, so it is --rd-acc.

   Each [data-theme] block defines the COMPLETE set, so re-scoping a subtree is
   total. A partial block would inherit half its tokens from the outer theme,
   which is precisely the bug the theme swatch would hit — it renders a
   miniature of the theme it is OFFERING, inside the theme currently active.

   No @media (prefers-color-scheme) here, deliberately. Theme is an explicit
   stored preference:

     "a phone that flips to dark at sunset while someone is still on the line is
      worse than one that stays where they put it."

   A media query would reintroduce exactly that flip. "Match my device" is a
   stored preference VALUE resolved in Rust, not a CSS query.
   ========================================================================== */

:root,
[data-theme="dark"] {
  --rd-bg: #0B0B0B;
  --rd-surf: #151515;
  --rd-surf2: #1E1E1E;
  --rd-line: #2A2A2A;
  /* The boundary of anything TAPPABLE that is not filled with accent.
     --rd-line divides; --rd-ctrl bounds a control. The distinction is not
     cosmetic: a divider may be quiet because the content either side carries
     the meaning, while a control's boundary is often the ONLY thing saying the
     control is there.

     ⚠️ This value is a FLOOR, not a preference. WCAG 1.4.11 asks 3:1 of the
     visual information identifying a component; #6E6E6E clears it on #0B0B0B.
     "If your ground differs from mine the number moves; the 3:1 does not."

     It exists because one box failed three times (designer, 2026-08-04):
     --rd-line (the divider token, 1.4:1), then --rd-chip (WORSE, 1.2:1, on
     circular reasoning — the flanking chips "lend their affordance", but they
     are the same chip and read only because of the glyphs inside them), then
     this. Anywhere --rd-line or --rd-chip is the SOLE indicator that something
     is interactive is the same bug — see docs/roadmap/items/kit-control-boundary. */
  --rd-ctrl: #6E6E6E;
  --rd-fg: #FFFFFF;
  --rd-fg2: #B6B6B6;
  --rd-fg3: #8C8C8C;
  --rd-acc: #CEFF51;
  --rd-volt: #CEFF51;
  --rd-on-volt: #000000;
  --rd-danger: #FF5A3C;
  --rd-warn: #FFB020;
  --rd-chip: #232323;
  --rd-field: #1B1B1B;
  /* Derived: the kit branches on a per-theme `tex` flag in three places. A
     per-theme constant is a per-theme token, so the branch is pre-resolved here
     and no component ever tests it.
     NOTE: --rd-app-bg omits the concrete-texture layer. The asset is 410 KB
     against a current precache payload of ~28 KB, so shipping it is a cost
     decision that has not been taken. Flat ground until it is. */
  --rd-app-bg: #0B0B0B;
  --rd-nav-bg: rgba(8, 8, 8, .94);
  /* OPAQUE, and matched to OUR ground by hand. An outlined control is two
     layers and the inner layer's backdrop is the OUTER one — the stroke — not
     the page. So a translucent fill composites against the stroke and tints
     toward it: the .72 black this used to be lands on dark olive inside a volt
     stroke and on rust inside a danger stroke. (Arithmetic, not a render:
     .72x#0B0B0B over #CEFF51 = ~#414F1F, over #FF5A3C = ~#4F2018.)

     The kit's value is #111111 because that is the composited value of its dark
     scrim over the concrete photograph. We ship a FLAT ground (see --rd-app-bg
     above — the 410 KB asset is not precached), so ours is #0B0B0B. The rule
     travels; the number is per-ground. If the texture ever ships, this becomes
     the composited value, NOT var(--rd-app-bg) — binding it to that token would
     paint the photograph inside the button, which is the thing the two-layer
     construction exists to prevent.

     Light already obeyed this: --rd-hollow-fill == --rd-app-bg at :966. */
  --rd-hollow-fill: #0B0B0B;
  /* Washes and scrims. Values measured from the corpus; the light accent wash
     is contested (#F0F5DC x4, #F4F8E4 x3, #EAF2D2 x1) and takes the most-used
     value provisionally. Panel scrim collapses three alphas (.88/.90/.92). */
  --rd-acc-wash: #1A1D12;
  --rd-warn-wash: #1E1804;
  /* ⚠️ STILL TRANSLUCENT, DELIBERATELY NOT FIXED HERE. This is exposed as
     Fill::PanelScrim, i.e. as an INNER fill, so the rule above applies to it
     identically — but it has zero call sites today, and the 08-04 re-diff found
     that its stated consumer (the Stage Editor) needs alphas this token cannot
     express: .72/.78/.86 as well as the .88/.90/.92 collapsed here, and it stays
     DARK in both themes at half its sites, where ours flips to opaque #FFFFFF in
     light. Flipping the value blind would be guessing at a spec that does not
     exist yet. Blocked on the editor wave; see tmp/design/rediff-2026-08-04.md
     section F. Do not use Fill::PanelScrim on a stroked box before then. */
  --rd-panel-scrim: rgba(11, 11, 11, .90);
  /* Stage-diagram paper is deliberately THEME-INVARIANT — a diagram is drawn on
     cardboard in both themes; it is content, not chrome. The full 11-colour
     stage palette is domain-specific and rides with the Stage Editor migration,
     not Foundations. */
  --rd-paper: #F2EFE6;
  --rd-paper-edge: #C9C6BA;
  --rd-swatch-ground: #0B0B0B;
  /* Designer #17: the filled control needs a silhouette on paper. Carried as
     a TOKEN VALUE, not a structural difference — transparent IS a value, so
     the component is identical in both themes (architect ruling 2). */
  --rd-btn-edge: transparent;
  /* Selected chip — dark is the drawn treatment, unchanged. */
  --rd-chip-on-fill: var(--rd-volt);
  --rd-chip-on-label: var(--rd-on-volt);
}

[data-theme="light"] {
  --rd-bg: #F1F1EC;
  --rd-surf: #FFFFFF;
  --rd-surf2: #E7E7E0;
  --rd-line: #CDCDC2;
  /* Floor on paper, same 3:1 rule — see the dark block. */
  --rd-ctrl: #82827B;
  --rd-fg: #0B0B0B;
  --rd-fg2: #3F4039;
  --rd-fg3: #63645B;
  --rd-acc: #3E4E10;
  --rd-volt: #CEFF51;
  --rd-on-volt: #000000;
  --rd-danger: #B32D0C;
  --rd-warn: #7A4E00;
  --rd-chip: #E2E2D8;
  --rd-field: #FFFFFF;
  --rd-app-bg: #F1F1EC;
  --rd-nav-bg: #FFFFFF;
  --rd-hollow-fill: #F1F1EC;
  --rd-acc-wash: #F0F5DC;
  --rd-warn-wash: #FBF3DC;
  --rd-panel-scrim: #FFFFFF;
  --rd-paper: #F2EFE6;
  --rd-paper-edge: #C9C6BA;
  --rd-swatch-ground: #F1F1EC;
  /* Light only: volt-on-near-white is ~1.3:1 at the edge, so the shape
     dissolves and the chamfer is invisible. */
  --rd-btn-edge: var(--rd-acc);
  /* Inverted on paper: ink fill, page-coloured label (#12). */
  --rd-chip-on-fill: var(--rd-fg);
  --rd-chip-on-label: var(--rd-app-bg);
}

/* --------------------------------------------------------------------------
   Type roles (substrate §2.3). `tdisp` is ALWAYS uppercase — the transform
   lives in the helper, so a source string's casing never reaches the render.
   Width is 62 for display and 75-78 for labels and controls — two values, as a
   rule rather than a list of instances. Body is 100 BY ABSENCE: body sites
   author no font-stretch at all, so 100 arrives as the CSS initial value, which
   is why the shipped subset clamps to 62-100 and not to 78. (66 was drift
   across five corpus sites, collapsed to 62 by the designer on 2026-08-04; the
   allowed set is enforced by ALLOWED_STRETCH in cli/src/css.rs.)
   -------------------------------------------------------------------------- */
.rd-disp {
  font-family: 'Archivo', system-ui, sans-serif;
  text-transform: uppercase;
}
.rd-h1 { font-family: 'Archivo', system-ui, sans-serif; text-transform: uppercase; font-weight: 900; font-stretch: 62%; letter-spacing: 0; font-size: 30px; line-height: .92; color: var(--rd-fg); }
.rd-h1-alt { font-family: 'Archivo', system-ui, sans-serif; text-transform: uppercase; font-weight: 900; font-stretch: 62%; letter-spacing: .01em; font-size: 19px; line-height: .98; color: var(--rd-fg); }
.rd-lab { font-family: 'Archivo', system-ui, sans-serif; text-transform: uppercase; font-weight: 700; font-stretch: 78%; letter-spacing: .2em; font-size: 8.5px; color: var(--rd-fg3); }
.rd-lab-acc { color: var(--rd-acc); }
.rd-btn-label { font-family: 'Archivo', system-ui, sans-serif; text-transform: uppercase; font-weight: 800; font-stretch: 78%; letter-spacing: .14em; font-size: 12.5px; }
.rd-title { font-family: 'Archivo', system-ui, sans-serif; text-transform: uppercase; font-weight: 700; font-stretch: 78%; letter-spacing: .16em; font-size: 10px; color: var(--rd-fg2); }
.rd-p { font-family: 'Archivo', system-ui, sans-serif; font-size: 13px; line-height: 1.5; color: var(--rd-fg2); text-wrap: pretty; margin: 0; }
/* Numeric role, §P2 RESOLVED 2026-08-04 in favour of Foundations: Archivo 800 /
   stretch 75% / tabular. The kit's own `mono` now says the same — "not a system
   mono. Numbers are the loudest thing on a scoring surface, so they speak in the
   display voice."
   ONE qualification Foundations got wrong: NOT "always volt". Colour is --rd-fg
   by default; volt is reserved for the one number that IS the screen. Where the
   accent is right the token is --rd-acc, because a number is text. */
/* ADJUDICATED — designer answer #2. Numbers are the loudest thing on a scoring
   surface and must be in the display voice, not the system's. ONE weight: 800
   (he stripped 29 inline overrides, so a 600 or 700 here is a bug, not a
   variant). Colour is --rd-fg by DEFAULT — "always volt" was wrong and
   collides with #6; volt is for the one number that is the point of the
   screen. ui-monospace survives elsewhere for verbatim technical strings
   (server URLs, WASM error text) — just never for numbers. */
/* The numeric role. Archivo 800 / width 75 / tabular — NOT a system mono
   (substrate §P2, adjudicated 2026-08-04). Colour is --rd-fg by default; volt is
   reserved for the one number that IS the screen.

   NO letter-spacing here. The kit's shared `mono` carries none — the -.01em is
   applied at TNum's call site only (rd-train-kit.jsx:150), and TStat's value
   (:176) is untracked. It used to ride this rule, which silently tracked every
   stat value in the app. */
.rd-num { font-family: 'Archivo', system-ui, sans-serif; font-variant-numeric: tabular-nums; font-weight: 800; font-stretch: 75%; font-size: 17px; color: var(--rd-fg); }

/* --------------------------------------------------------------------------
   /kit review surface. Unlinked route; exists to show tokens and type resolving
   in both themes, and to make a broken token VISIBLE (the falsification rule).
   -------------------------------------------------------------------------- */
.rd-kit { min-height: 100vh; background: var(--rd-app-bg); color: var(--rd-fg); padding: 24px; font-family: 'Archivo', system-ui, sans-serif; }
.rd-kit-head { margin-bottom: 24px; }
.rd-kit-panels { display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-start; }
.rd-kit-panel { flex: 1 1 320px; min-width: 300px; background: var(--rd-app-bg); border: 1px solid var(--rd-line); padding: 18px; }
.rd-kit-sec { margin-top: 20px; }
.rd-kit-sw-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 10px; margin-top: 8px; }
.rd-kit-sw { border: 1px solid var(--rd-line); }
.rd-kit-sw-chip { height: 44px; }
.rd-kit-sw-meta { padding: 6px 7px; background: var(--rd-surf); }
.rd-kit-sw-name { font-size: 10px; color: var(--rd-fg); word-break: break-all; }
.rd-kit-sw-val { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9.5px; color: var(--rd-fg3); }
.rd-kit-spec { border-top: 1px solid var(--rd-line); padding: 10px 0; }
.rd-kit-spec-note { font-size: 9.5px; color: var(--rd-fg3); margin-top: 3px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.rd-kit-row { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; align-items: flex-start; }
/* A container demo's inner box needs a size to be visible; the primitives supply
   their own. Review-surface chrome, not a kit rule. */
.rd-kit-boxdemo { min-height: 44px; padding: 0 14px; display: flex; align-items: center; }
.rd-kit-warn { border-left: 2px solid var(--rd-warn); padding-left: 12px; margin-top: 18px; }

/* ==========================================================================
   DESIGN-KIT PRIMITIVES — merged from tmp/design/kit-css/ (PR B)
   ==========================================================================
   One block per primitive, in spec order. Authored as separate files so
   eleven concurrent authors could not collide in this hand-curated
   stylesheet, then merged through two checkers:
     check-classes.py   used     -> defined
     check-css-dupes.py defined  -> defined ONCE
   The second exists because the first is blind to two authors defining the
   same selector differently, where the cascade silently last-write-wins.
   ========================================================================== */

/* ---- 01-chamfer-box.css ---- */
/* RdChamferBox — the two-layer chamfer container. Spec: docs/design/kit/01-tout.md
   The outer IS the border: a solid box filled with the stroke colour, clipped at
   `r`, padded by the border width. The inner is the surface, clipped at
   max(r - w(2 - √2), 2) — NOT r - 2w, which makes the two cuts collinear and
   the border vanish along the diagonal. Geometry arrives as custom properties (the scale is 8 chamfer
   values x 4 widths — enumerating the product as classes would be noise);
   colour stays here, where the token roles belong. */
.rd-box {
  background: var(--rd-line);
  padding: var(--rd-box-w, 1px);
  box-sizing: border-box;                 /* padding must INSET, not grow: without
                                             this every box renders 2w larger than
                                             its slot and overflows its row */
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--rd-box-r)), calc(100% - var(--rd-box-r)) 100%, 0 100%);
}
.rd-box__inner {
  background: var(--rd-surf);
  height: 100%;                           /* no-op on an auto-height card; load-bearing
                                             when the OUTER gets a definite height from
                                             a stretch row — without it the stroke colour
                                             shows as a solid slab below the content */
  box-sizing: border-box;                 /* lets the inner carry its own padding (84/84
                                             sites do) without fighting height:100% */
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--rd-box-ri)), calc(100% - var(--rd-box-ri)) 100%, 0 100%);
}

/* Stroke roles. A stroke is --rd-acc, NEVER --rd-volt (substrate §1.1). */
.rd-box--stroke-line   { background: var(--rd-line); }
.rd-box--stroke-acc    { background: var(--rd-acc); }
.rd-box--stroke-muted  { background: var(--rd-fg3); }
.rd-box--stroke-danger { background: var(--rd-danger); }
.rd-box--stroke-warn   { background: var(--rd-warn); }
/* Reserves the border's space without painting it, so a state change to a
   visible stroke costs no layout shift. */
.rd-box--stroke-none   { background: transparent; }
.rd-box--stroke-btn-edge { background: var(--rd-btn-edge); }

/* Fill roles. Never `transparent` on an outlined control — the outer layer is
   the border, so a see-through inner lets the border colour flood it. */
.rd-box--fill-surf      { background: var(--rd-surf); }
.rd-box--fill-surf2     { background: var(--rd-surf2); }
.rd-box--fill-field     { background: var(--rd-field); }
.rd-box--fill-hollow    { background: var(--rd-hollow-fill); }
.rd-box--fill-acc-wash  { background: var(--rd-acc-wash); }
.rd-box--fill-warn-wash { background: var(--rd-warn-wash); }
.rd-box--fill-scrim     { background: var(--rd-panel-scrim); }
.rd-box--fill-paper     { background: var(--rd-paper); }
.rd-box--fill-volt      { background: var(--rd-volt); }

/* ---- 02-button.css ---- */
/* RdButton — spec docs/design/kit/02-tbtn.md. Component: apps/web/src/components/kit/button.rs

   Three layers, outside in:

     .rd-btn          the real <button> AND the focus ring (§5.3). Always present.
     .rd-box          the border layer — OUTLINED KINDS ONLY, drawn by RdChamferBox.
     .rd-btn__face    the label box: the .rd-box__inner when outlined, a <span> when filled.

   The filled kinds (primary, and primary while disabled/loading) are ONE layer
   by construction (§1), so they do not go through RdChamferBox at all — there is
   nothing two-layer about them. The two-layer clip and the max(r - w(2 - √2), 2)
   inner chamfer stay in exactly one place: chamfer_box.rs + 01-chamfer-box.css.

   The type role (.rd-btn-label, tdisp(800,78,'.14em') at 12.5px, substrate §2.3)
   is DELIBERATELY NOT REDEFINED HERE — it already ships in the Foundations layer
   at apps/web/style.css:981 and the face element carries both classes. */

/* --------------------------------------------------------------------------
   Layer 3 — the focus ring, which is also the button.

   A ring cannot be an `outline` or a `box-shadow`: clip-path clips both, so
   either one is erased on exactly the element that needs it (§5.3). It has to be
   its own box outside the clip. Making that box the <button> pays for itself
   three times — it is the element that takes focus, the element that takes the
   click, and (because it is always rendered) 2px of permanent hit box on every
   side. That last one is what lifts the dense filled primary from 46 to 50 and
   clears Android's 48dp WITHOUT moving a painted pixel (§3.5; the architect's
   ruling is that platform minimums are met by hit-area extension, not by
   redrawing the designer's geometry). Guarded by hit_target_height_px's tests.
   -------------------------------------------------------------------------- */
.rd-btn {
  /* PROVISIONAL 13 (substrate §P1: Foundations says 14). Mirrored by
     CONTROL_CHAMFER_PX in button.rs, which feeds the outlined kinds' r — both
     move together, and this is the one-line flip if the designer rules 14. */
  --rd-btn-r: 13px;
  /* 13 + 3: the cut stays proportional to the element (substrate §3.2) and 16 is
     already on the kit's tcham scale. NOT 17 — see the_focus_ring_does_not_
     vanish_along_the_cut. Mirrors FOCUS_RING_CHAMFER_PX. */
  --rd-btn-ring-r: 16px;
  display: inline-block;      /* the UA default for <button>; stated because the
                                 `wide: false` case depends on it, below */
  width: 100%;                /* wide is the default: 50 of 52 corpus sites */
  vertical-align: top;        /* an inline-block sits on the baseline, which hangs
                                 a descender's worth of blank under the button */
  padding: 2px;               /* the ring's thickness on the straight edges, and
                                 the whole of the hit-area extension. Mirrors
                                 FOCUS_RING_PAD_PX in button.rs */
  margin: 0;
  box-sizing: border-box;
  background: transparent;    /* the ring, unlit. Always occupying its space, so
                                 focus costs no layout shift */
  border: 0;                  /* style.css:112 already zeroes it; explicit here so
                                 this component does not depend on that rule */
  appearance: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;  /* the UA highlight is a RECTANGLE and
                                                ignores the chamfer, which reads as
                                                the geometry breaking on tap */
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--rd-btn-ring-r)), calc(100% - var(--rd-btn-ring-r)) 100%, 0 100%);
}

/* §3.4 / §9.2. `width: auto` is exactly what the kit does (`width: undefined`).
   Under a stretching flex parent it is a no-op — which is why the corpus's two
   wide={false} sites render full width anyway. Routed to the designer; we do NOT
   add `align-self: start` here, because that is a visible change from the
   preview and needs his sign-off, not our judgement. */
.rd-btn--auto { width: auto; }

.rd-btn:disabled { cursor: default; }

/* 🟡 PROVISIONAL pending Claude Design (§5.3). Focus in this system is an
   --rd-acc stroke — that is the kit's own idiom for inputs (TNum draws focus as
   stroke={focus ? t.acc : t.line}, rd-train-kit.jsx:103). The ring stays
   --rd-acc on the DANGER kind too: focus is a system affordance, not a severity
   signal, and a red ring on a red button is invisible.
   `outline: none` only where we replace it — and note the UA outline was already
   being clipped away, so this documents intent rather than doing work. */
.rd-btn:focus-visible {
  background: var(--rd-acc);
  outline: none;
}

/* --------------------------------------------------------------------------
   The label box — the INNER layer of the two-layer control.

   ADJUDICATED. This previously carried min-height 46 and treated the resulting
   3px asymmetry (46 filled / 49 outlined) as the spec's intent. The designer
   ruled it a bug and gave the rule that explains it:

     "height is a property of the painted box, and on a two-layer control the
      inner layer's min-height is not it."

   The wrapper adds 2w outside this box, so the painted height is
   inner + 2 x 1.5. Every kind is two-layer since #17, so 45 + 3 = 48 painted
   everywhere — and 48 is itself adjudicated: the dense control is the most
   tapped in the system, used outdoors in gloves, so it is 48 PAINTED rather
   than 48 by hit area.
   -------------------------------------------------------------------------- */
.rd-btn__face {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 20px;
  min-height: 45px;           /* + 2 x 1.5 border = 48 painted */
  box-sizing: border-box;
}
.rd-btn--tall .rd-btn__face { min-height: 49px; }   /* + 3 = 52 painted */

/* The single-layer filled construction is GONE — designer answer #17 made every
   kind two-layer, so the fill now arrives through RdChamferBox's Fill role
   (.rd-box--fill-volt live, .rd-box--fill-surf2 disabled) and the clip comes from
   .rd-box. The rules that used to live here (--filled, --volt, --surf2) were
   deleted rather than left: a rule matching nothing is invisible to both
   checkers, and one describing a construction that no longer exists is worse
   than absent.

/* Label colours (§2). A stroke is --rd-acc and never --rd-volt (substrate §1.1);
   volt appears here only as a FILL, above. */
.rd-btn__face--on-volt { color: var(--rd-on-volt); }
.rd-btn__face--acc     { color: var(--rd-acc); }
.rd-btn__face--danger  { color: var(--rd-danger); }
.rd-btn__face--fg2     { color: var(--rd-fg2); }
/* Every kind's inactive label. --rd-fg3 was lifted for sunlight legibility
   (rd-train-kit.jsx:6-10) — it is a quiet grey, not a dim one. */
.rd-btn__face--fg3     { color: var(--rd-fg3); }

/* --------------------------------------------------------------------------
   🟡 PROVISIONAL pending Claude Design — hover (§5.2) and pressed (§5.4).

   The kit specifies neither: the frames are iOS, which has no hover, and the
   corpus has zero `transition`, zero `:active` and zero `focus-visible`. So
   MOTION IS NOT PART OF THIS SYSTEM, and these are fill changes only — no
   transform, no scale, no elevation. A chamfered box that scales makes its
   diagonal crawl against the pixel grid.

   The system has no alpha ramp, and as of 2026-08-04 it has no translucent
   fill either — --rd-hollow-fill went opaque because an inner layer composites
   against its stroke, not the page. color-mix is what keeps the remaining
   compositing inside the token layer: "volt under an 8% --rd-on-volt scrim" is
   expressible without a hex literal, and volt is never re-hued.

   Geometry must not change in either state — a thicker border or a size step
   would shift layout and shimmer the diagonal.
   -------------------------------------------------------------------------- */
@media (hover: hover) and (pointer: fine) {
  .rd-btn:not(:disabled):hover .rd-box--fill-volt {
    background: color-mix(in srgb, var(--rd-on-volt) 8%, var(--rd-volt));
  }
  /* One opaque step up. Border and label untouched, and the fill must never move
     TOWARD the stroke colour — that is the volt-on-volt flood §7.2 rules out. */
  .rd-btn:not(:disabled):hover .rd-box--fill-hollow { background: var(--rd-surf); }
}
.rd-btn:not(:disabled):active .rd-box--fill-volt {
  background: color-mix(in srgb, var(--rd-on-volt) 16%, var(--rd-volt));
}
.rd-btn:not(:disabled):active .rd-box--fill-hollow { background: var(--rd-surf2); }

/* Empty and error states are N/A for this primitive (§5.7): a button has no
   empty state, and errors belong to the field or the banner. Recorded rather
   than omitted. Loading (§5.6) has no styling of its own — it takes the
   per-kind disabled treatment, which is why there is no .rd-btn--loading. */

/* ---- 03-shell.css ---- */
/* RdShell — the app shell. Spec: docs/design/kit/03-tshell.md
   Anatomy, top to bottom, in one flex column: top inset -> banner slot ->
   optional back/title bar -> scrolling content -> tab bar. The order is
   load-bearing (spec §1). Slots 1, 2 and 5 are full-bleed; slots 3 and 4 share
   one gutter, which is why the back label and the first line of content align
   to the same left edge.

   Two elements, not one: the VIEWPORT layer paints the ground, the FRAME is the
   width-capped column. See .rd-shell below for why they cannot be merged. */

/* ---------------------------------------------------------------------------
   1. Viewport layer — paints the ground, centres the capped column.
   --------------------------------------------------------------------------- */
.rd-shell {
  /* PROVISIONAL pending Claude Design (spec §7c): 448px, ONE token, on the
     shell root. 448 because it is the only width cap that already exists in
     our codebase (auth), so it is a decision rather than a new invention.
     Adjudication is a one-line edit HERE. No screen may set its own max width
     — the moment two exist, the flip stops being one line. */
  --rd-shell-max-w: 448px;

  /* PROVISIONAL pending Claude Design (spec §7a): 12px, plus the device
     safe-area inset where one exists. The 12 is the designer's — four framed
     documents pass top={12} to the same screens that get 58 (iOS) / 42
     (Android). The env() term is OURS: it is what makes an installed
     standalone PWA on a notched phone behave. In a normal browser tab env()
     resolves to 0 and the value is plainly 12. */
  --rd-shell-inset-top: calc(12px + env(safe-area-inset-top, 0px));

  /* Belt for a missing modifier class. The Rust `Gutter` enum is a required
     prop, so one of the four modifiers below is always present and always
     wins; this base value only decides what a hypothetical bug looks like,
     and standard is a safer wrong answer than flush. */
  --rd-shell-gutter: 18px;

  /* 100dvh, NEVER 100vh. On mobile browsers 100vh is the URL-bar-collapsed
     height, so a 100vh shell is taller than the visible viewport and its last
     flex child — the tab bar, i.e. the app's PRIMARY NAVIGATION — sits below
     the fold until you scroll (spec §7e). The kit's root is `height: 100%`,
     which needs an ancestor with a height; dvh is that height, and it is
     viewport-relative so it does not care how deeply this element is nested. */
  height: 100dvh;
  width: 100%;
  box-sizing: border-box;
  overflow: hidden;              /* the REGION scrolls, not the document */

  display: flex;
  justify-content: center;       /* centre the capped column */

  /* The ground is painted on the VIEWPORT, not on the capped column (spec
     §7c). Painting it on the column letterboxes the page in flat colour on any
     viewport wider than the cap — which is the whole reason these are two
     elements and not one.

     NOTE: --rd-app-bg is currently a FLAT COLOUR. The photographic texture
     layer is deliberately not shipped — BG.jpeg is 419,630 bytes against a
     precache payload of five small entries (apps/web/sw.js:16-26), and that
     cost has not been decided. Do NOT add a url() here: the token is the one
     place the texture arrives, and it arrives with a Trunk copy-file, a
     PRECACHE_ASSETS entry and a CACHE_NAME bump together or not at all
     (spec §7d). */
  background: var(--rd-app-bg);

  /* Body type role (substrate §2.1: tbody sets family only — no stretch, no
     weight — so descendants inherit the CSS defaults 100% / 400). */
  font-family: 'Archivo', sans-serif;
}

/* The four-step gutter scale (spec §2.2): 47 sites, four values, no others.
   Modelled as four classes rather than an arbitrary integer so a fifth value
   is a deliberate act rather than a typo. Set on the viewport layer; the
   custom property inherits down to the bar and the content region.
   ⚠️ These four class names are pinned to Gutter::px() by a unit test — the
   number in the name IS the number in the padding. */
.rd-shell--gutter-0  { --rd-shell-gutter: 0px; }
.rd-shell--gutter-16 { --rd-shell-gutter: 16px; }
.rd-shell--gutter-18 { --rd-shell-gutter: 18px; }
.rd-shell--gutter-20 { --rd-shell-gutter: 20px; }

/* ---------------------------------------------------------------------------
   2. Frame — the width-capped column. Transparent: the ground is above.
   --------------------------------------------------------------------------- */
.rd-shell__frame {
  flex: 1 1 auto;                /* grow toward the viewport width... */
  max-width: var(--rd-shell-max-w);  /* ...and stop here; the leftover free
                                        space is what justify-content centres */
  min-width: 0;                  /* a flex item defaults to min-width:auto and
                                    refuses to shrink below its content */
  height: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  overflow: hidden;

  /* No chamfer anywhere in this component (spec §4): tcham is not called in
     rd-train-kit.jsx:57-73. The shell is a full-bleed rectangle; the chamfer is
     carried by the controls inside it. */
}

/* ---------------------------------------------------------------------------
   3. Slot 1 — top inset. Air, not a bar.
   --------------------------------------------------------------------------- */
.rd-shell__inset {
  flex: none;
  height: var(--rd-shell-inset-top);
  /* No background of its own — the root's ground shows through, which is what
     makes this air rather than a status bar (spec §4). */
}

/* ---------------------------------------------------------------------------
   4. Slot 2 — banner. Full-bleed, normal flow, ABOVE the bar and the content.
   --------------------------------------------------------------------------- */
.rd-shell__banner {
  /* ⚠️ TRAP 2. `flex: none` is MANDATORY and the kit omits it:
     rd-train-kit.jsx:62 renders {banner} as a BARE flex child, which defaults
     to `flex: 0 1 auto` and is therefore shrinkable. Squeezed against a
     `flex: 1` content region it compresses and clips its own text. The sibling
     shell RDShell:47 gets this right; TShell does not. We port RDShell's
     treatment, not TShell's (spec §7b.2, §11.2). */
  flex: none;

  /* Full-bleed comes from the frame carrying no horizontal padding — the
     gutter lives on the bar and the content region only, so a banner spans
     edge to edge of the capped column regardless of the gutter (spec §1). */

  /* Deliberately NO air below the banner. RDShell:48 has a fixed 14px and
     TShell has none; the kit is internally inconsistent here (spec §10.2) and
     the shell's own rule is "no vertical padding, the screen supplies its own"
     (§4). Routed, not resolved. Our shipped .offline-banner already carries its
     own border-bottom (apps/web/style.css:155-163). */
}

/* ---------------------------------------------------------------------------
   5. Slot 3 — back / title bar.
   --------------------------------------------------------------------------- */
.rd-shell__bar {
  flex: none;
  display: flex;
  align-items: center;
  height: 42px;                  /* fixed — see the overflow treatment below */
  padding: 0 var(--rd-shell-gutter);
  box-sizing: border-box;
}

.rd-shell__back {
  /* One control spanning chevron + gap + label, not a chevron with a caption
     beside it: a 9x15 glyph is far below a usable target outdoors with gloves
     on (spec §6). It takes the full 42px bar height as its hit area. */
  display: inline-flex;
  align-items: center;
  gap: 9px;
  height: 100%;

  /* Shrinkable, and min-width:0 is what lets the shrink actually reach the
     label so the ellipsis below can fire. */
  flex: 0 1 auto;
  min-width: 0;

  /* Leading bleed: the hit area reaches the screen edge so a thumb coming from
     the bezel lands on it, while the negative margin and the equal padding
     cancel out so the chevron does NOT move — the label stays on the same left
     edge as the first line of content. */
  margin-inline-start: calc(-1 * var(--rd-shell-gutter));
  padding-inline-start: var(--rd-shell-gutter);
  padding-inline-end: 0;

  /* It is a real <button>; strip the UA chrome without stripping the
     semantics. */
  background: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  color: inherit;
  text-align: start;
  -webkit-appearance: none;
  appearance: none;
}

/* PROVISIONAL pending Claude Design (spec §5c). The kit is drawn in iOS frames
   and iOS has no hover, so there is no drawn source. Proposal: raise the label
   one step up the grey ladder, fg2 -> fg; the chevron does not change (it is
   already at full accent strength and has nowhere to go). NO background wash —
   the ground is a photograph and a translucent plate on it reads as a
   rendering artefact. Endorsed by the designer's own nav comment
   (rd-train-kit.jsx:36-40): outdoors, white beats lime.
   (hover: hover) so a touch device does not get a stuck hover after a tap. */
@media (hover: hover) {
  .rd-shell__back:hover .rd-shell__label { color: var(--rd-fg); }
}

/* PROVISIONAL pending Claude Design (spec §5d) — the accessibility-critical
   one: the back control is frequently the only focusable element in the shell
   chrome, and an unstyled UA outline on a dark photographic ground is close to
   invisible. Square-cornered because border-radius is banned system-wide
   (substrate §1.5) and the chamfer is a bottom-right cut on CONTROLS, which
   this row is not. :focus-visible only, never :focus, so a pointer press
   leaves no ring.
   An outline is safe HERE specifically because the shell has no clip-path
   anywhere (§4) — on a chamfered element the clip would eat it. */
.rd-shell__back:focus-visible {
  outline: 2px solid var(--rd-acc);
  outline-offset: 2px;
  border-radius: 0;
}

.rd-shell__chevron {
  flex: none;                    /* the chevron never shrinks (spec §5e) */
  display: block;
  width: 9px;
  height: 15px;
  /* ⚠️ --rd-acc, NEVER --rd-volt. This is a STROKE, and per substrate §1.1 a
     stroke takes acc, which goes dark olive (#3E4E10) in light theme. Writing
     volt here yields an unreadable lime chevron on the light ground — the
     exact failure the acc/volt split exists to prevent. `stroke` is an
     inherited SVG property, so this reaches the path. */
  stroke: var(--rd-acc);
}

.rd-shell__label {
  /* Shell-title type role (substrate §2.3): tdisp(700, 78, .16em) at 10px,
     colour fg2. tdisp is always uppercase — the transform is in the helper. */
  font-weight: 700;
  font-stretch: 78%;
  font-size: 10px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--rd-fg2);

  /* A DEFECT FIXED, NOT PORTED (spec §5e): rd-train-kit.jsx:66 declares no
     overflow, no text-overflow and no white-space while :64 fixes the bar at
     42px, so a wrapping label overflows a fixed-height box. The kit never
     exercises it because its longest label is "Modified Bill Drill"; WE do,
     because our labels are user-authored drill and course names of unbounded
     length. Single line, ellipsis, chevron unaffected. */
  min-width: 0;
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   6. Slot 4 — content region. The only thing that scrolls.
   --------------------------------------------------------------------------- */
.rd-shell__content {
  flex: 1;

  /* ⚠️ TRAP 1, and the single most important declaration in this file. A flex
     child defaults to `min-height: auto`, which refuses to shrink below its
     content — so as soon as the content exceeds the fold the column grows and
     the tab bar walks off the bottom of the viewport. It looks correct on
     every short screen, which is exactly what makes it dangerous (spec §4,
     §11.1). This one line is what keeps the tab bar planted. */
  min-height: 0;

  /* Ours to supply, not the kit's (spec §7e). The kit sets overflow:hidden here
     because the device FRAME supplies the scroll (ios-frame.jsx:227). We have
     no frame. Scrolling the REGION rather than the document is what keeps the
     banner and the tab bar still while content moves under them. */
  overflow-y: auto;
  overflow-x: hidden;

  /* Horizontal only — NO top or bottom padding. Screens that want breathing
     room add their own (spec §4); baking it in here takes the choice away from
     all 47 of them. */
  padding: 0 var(--rd-shell-gutter);
  box-sizing: border-box;

  display: flex;
  flex-direction: column;
}

/* ---------------------------------------------------------------------------
   7. Slot 5 — tab bar. The shell reserves the slot; RdTabBar draws it.
   --------------------------------------------------------------------------- */
.rd-shell__nav {
  /* The shell's contract is that it reserves this slot and NEVER lets it
     shrink (spec §1, §11.2). The kit supplies flex:none for the tab bar itself
     (rd-train-kit.jsx:43) and RdTabBar is expected to as well — this is the
     shell honouring its own half of the contract regardless, so the invariant
     does not depend on a sibling component's internals.

     Full-bleed within the capped column: no gutter, by construction.
     Everything inside — glyphs, labels, the active rule, the nav background
     --rd-nav-bg — belongs to RdTabBar and is not styled here. */
  flex: none;
}

/* ---- 04-tab-bar.css ---- */
/* RdTabBar — the bottom tab bar + the NAVI icon set.
   Spec: docs/design/kit/04-tnav.md. Tokens: 00-substrate.md §1.1 (acc/volt),
   §1.3 (the table), §2.3 (the TNav label role).

   NO CHAMFER ANYWHERE IN THIS COMPONENT. rd-train-kit.jsx:43-49 calls tcham
   zero times (§1): the app's one piece of ownable geometry is deliberately
   absent from the chrome, which is what lets it mean something on content.
   That is also why the focus ring below may legally be an `outline` — nothing
   here is clipped, unlike on RdButton where clip-path eats both outline and
   box-shadow.

   Every colour is a token role. Read §1.1 before touching one: the rule and the
   glyph are STROKES, so they are --rd-acc and never --rd-volt. In dark theme
   the two are the same value and the error is invisible; in light theme
   --rd-acc is a dark olive and --rd-volt is lime, and a lime 2px rule on a
   white bar is unreadable. (The Training document's own prose says "a volt top
   rule and a volt glyph" — that is the slip §1.1 pre-empts; the kit code is
   right.) */

/* -- Bar ------------------------------------------------------------------ */

/* A FLOW SIBLING of the scroll region inside the shell's full-height flex
   column (`flex: none`), never `position: fixed` with a magic content padding.
   Today's shipped bar is fixed (style.css:124) against `main { pb-20 }` = 80px
   (style.css:41, shell.rs:62) for a bar that is neither 80px now nor 58px
   after — a constant that has already drifted once. A flow sibling shrinks the
   content region to fit, so there is no constant to maintain (§13 trap 5). */
.rd-tabbar {
  flex: none;
  display: grid;
  /* 1fr columns, NOT flex: equal by construction and equal regardless of label
     length, which is what stops "COMMUNITY" stealing width from "HOME".
     The 5 is the tab count, which is a module constant rather than a prop
     (§12); `tab_bar.rs` pins TABS.len() == 5 against this literal. */
  grid-template-columns: repeat(5, 1fr);
  border-top: 1px solid var(--rd-line);
  background: var(--rd-nav-bg);
  /* The kit's flat `padding-bottom: 6` is an iOS-frame value — the home
     indicator occupies it. A browser supplies no such furniture, so on web this
     must grow to the real inset or the bottom of a 52px touch target sits under
     the iOS Safari gesture bar (§8, trap 1). Our shipped bar already does this
     (style.css:132); dropping back to a flat 6px is a silent regression. */
  padding-bottom: max(6px, env(safe-area-inset-bottom));
}

/* -- Cell ----------------------------------------------------------------- */

/* The whole cell is the link (§8, trap 2). A Link wrapping only the text would
   take a 52 x 80.4 target down to roughly 10 x 46. 52 x 80.4 CSS px clears iOS
   HIG 44, Material 48 and WCAG 2.5.5 on both axes — the 8px label is a
   legibility question, never a reachability one. */
.rd-tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 9px 0 7px;
  min-height: 52px;
  /* 52 is a BORDER-BOX total, not 52-plus-padding: every preview document sets
     `*{box-sizing:border-box}` (Range Day - Training.html:6), so the whole
     content stack is budgeted inside it — 2 border + 9 pad + 19 glyph + 5 gap +
     10 label line + 7 pad = 52 (§4.3). Restated locally rather than leaning on
     the global reset, because changing the glyph size or the gap changes the
     bar height and the arithmetic should be visible where it is spent. */
  box-sizing: border-box;
  /* The 2px is ALWAYS reserved, transparent when unselected, so selecting a tab
     changes only colour and never layout. "No border when unselected" shifts
     the whole bar 2px on every navigation. */
  border-top: 2px solid transparent;
  /* ⚠️ Pulls the cell up by exactly the bar's 1px hairline so the selected 2px
     rule sits ON the hairline rather than below it. Drop this and the selected
     tab gains a 1px grey line above its rule — a 3px double-stripe that reads
     as a rendering fault. No test catches it; it is the single most likely
     silent geometry loss in the component (§4.2, trap 1). */
  margin-top: -1px;
  text-decoration: none;
  /* Pressed is "unchanged": no ripple, no scale. Navigation completes in a
     frame and the arriving screen is the acknowledgement; a highlight flash
     would be a second, slower answer to a question already answered (§7). */
  -webkit-tap-highlight-color: transparent;
}

.rd-tab--on {
  border-top-color: var(--rd-acc);
}

/* Home, until `/home` exists. Renders as a non-link cell rather than pointing
   at some other tab's route — see the `TABS` doc comment in tab_bar.rs. Not a
   disabled state: the design has no disabled tab, this is a hole in our router.
   Deletes itself when the Home screen ships. */
.rd-tab--unrouted {
  cursor: default;
}

/* -- Glyph ---------------------------------------------------------------- */

/* 19x19 rendered from a 20-unit viewBox at stroke-width 1.7 (set as SVG
   attributes in tab_bar.rs, since they are geometry rather than colour).
   Coordinate space and stroke weight travel together: our old icons are
   0 0 24 24 at stroke-width 2, and pasting NAVI paths into that shell yields a
   clipped, visibly heavier bar (§13 trap 4). */
.rd-tab__glyph {
  display: block;
}

/* Colour lives here, not in a presentation attribute: var() is not substituted
   inside an SVG presentation attribute, and a hex literal is banned. */
.rd-tab__glyph path {
  fill: none;
  stroke: var(--rd-fg3);
  transition: stroke 120ms;
}

.rd-tab--on .rd-tab__glyph path {
  stroke: var(--rd-acc);
}

/* ⚠️ ONE GLYPH ONLY. `home` fills when selected (rd-train-kit.jsx:48). Do NOT
   generalise: target, community and profile all contain closed subpaths, so the
   same fill turns the target into a solid disc and the person into a
   silhouette; history is three open strokes and would fill as nothing (§13
   trap 2). The class is applied by NaviIcon::fills_when_selected(), which has a
   test on it, so widening this rule means editing Rust and failing a test
   rather than adding a CSS selector. */
.rd-tab--on .rd-tab__glyph--fill path {
  fill: var(--rd-acc);
}

/* -- Label ---------------------------------------------------------------- */

/* tdisp(700, 78, .08em) at 8.5px (00-substrate §2.3, rd-train-kit.jsx:84).
   ✅ P3 ADJUDICATED: this was 8px and provisional. The kit now draws 8.5 —
   "Labels at 8.5pt with the icon, always" (Range Day - Training.html:150) — and
   substrate §P3 rules that every 7.5px and 8px site goes to 8.5 wherever it
   appears. The declaration below has shipped 8.5 since the token layer landed;
   only this comment lagged. It is still the joint-smallest type bound to any
   primitive AND the app's primary wayfinding text, so the two rules below stand.

   Two rules ride with it: an 8.5px label may never be set in a grey weaker than
   --rd-fg3, and text scaling must not be suppressed (no
   -webkit-text-size-adjust: none, no user-scalable=no) — at this size the
   browser's own scaling is the only remedy a user has. */
.rd-tab__label {
  font-family: 'Archivo', system-ui, sans-serif;
  text-transform: uppercase;
  font-weight: 700;
  font-stretch: 78%;
  letter-spacing: .08em;
  font-size: 8.5px;
  /* Budgeted at ~10px in the 52px stack (§4.3). */
  line-height: 10px;
  color: var(--rd-fg3);
  /* Not in the kit and required here: with no wrapping rule, a two-word label
     wraps at a narrow viewport, the cell exceeds 52px and the grid row grows
     the whole bar. No ellipsis either — a truncated wayfinding label is worse
     than a shorter word. If a label does not fit, the WORD changes, not the
     layout (§4.4). */
  white-space: nowrap;
  transition: color 120ms;
}

/* ⚠️⚠️ THE SELECTED LABEL IS --rd-fg, NOT --rd-acc. This is the single decision
   this component's designer comment exists to prevent, and it is the one that
   looks correct while being wrong (§13 trap 3). Verbatim, rd-train-kit.jsx:38-40:

     "The selected label is WHITE, not volt. Outdoor photos of the shipped app
      show lime losing contrast before white does, so volt carries the rule and
      the glyph — emphasis — while the word that tells you where you are stays
      white."

   Colouring it --rd-acc is INVISIBLE in dark theme, where --rd-acc and --rd-fg
   are both bright. It is caught only in light theme, or outdoors, which is the
   one place this app is actually used. */
.rd-tab--on .rd-tab__label {
  color: var(--rd-fg);
}

/* -- Hover: PROVISIONAL, pending Claude Design ---------------------------- */

/* iOS has no hover, so the kit specifies none (constraints-for-design.md §8.1).
   The step is BORROWED, not invented: the designer states the visible hierarchy
   comes from the step between fg2 and fg3 (rd-train-kit.jsx:6-10), so hover
   reuses the system's existing one-notch move instead of adding a sixth grey.
   Hover must NEVER reach --rd-acc or draw the rule — acc-plus-rule is the
   entire selected vocabulary, and a hover that borrows it makes "where am I"
   ambiguous, the precise failure the mandatory labels exist to prevent. */
@media (hover: hover) {
  .rd-tab:hover .rd-tab__label {
    color: var(--rd-fg2);
  }
  .rd-tab:hover .rd-tab__glyph path {
    stroke: var(--rd-fg2);
  }
  /* The selected tab does not change on hover: it is already at maximum
     emphasis and any further move implies a state the system does not have.
     These two rules are not redundant — `.rd-tab:hover .rd-tab__label` and
     `.rd-tab--on .rd-tab__label` have EQUAL specificity (0,2,0), so the hover
     rule above would win on source order and repaint the selected label grey. */
  .rd-tab--on:hover .rd-tab__label {
    color: var(--rd-fg);
  }
  .rd-tab--on:hover .rd-tab__glyph path {
    stroke: var(--rd-acc);
  }
}

/* -- :focus-visible: PROVISIONAL, pending Claude Design ------------------- */

/* Full perimeter, not a top edge: the selected tab already carries an --rd-acc
   top rule, so a treatment that only thickens the top edge is indistinguishable
   from selection on the one tab a keyboard user is most likely to land on.
   `:focus-visible`, never `:focus` — a tap must not leave a ring. Square
   corners: §1.5 bans border-radius and this component has no chamfer.
   And it is never acceptable to remove the default outline without shipping
   this: an invisible focus ring on the app's primary navigation is a total
   keyboard loss, not a cosmetic one (§7). */
.rd-tab:focus-visible {
  outline: 2px solid var(--rd-acc);
  outline-offset: -2px;
}

/* -- States with no rule, stated rather than omitted ---------------------- */
/* Disabled: DOES NOT EXIST. A tab is always reachable.
   Loading / empty / error: DO NOT EXIST. The bar is a constant and never
   reflects data state.
   No tab selected (§6.4): not a class. It is the absence of .rd-tab--on on all
   five cells, which is already the default above — all rules transparent, all
   glyphs and labels --rd-fg3, no aria-current anywhere. The bar still renders;
   the user must keep their way out. */

/* ---- 05-label.css ---- */
/* RdLabel — the all-caps micro-label. Spec: docs/design/kit/05-tlab.md
   Component: apps/web/src/components/kit/label.rs

   MERGE NOTE — this block LAYERS ON the PR A type roles already in style.css:
   `.rd-lab` (:979) and `.rd-lab-acc` (:980). It deliberately does NOT redefine
   them: the type recipe (Archivo / uppercase / 700 / stretch 78% / .2em /
   --rd-fg3) stays there and stays the single definition of the role. Everything
   below is either an ADDITION the role class does not carry, or — where marked
   OVERRIDE — a deliberate replacement written as a COMPOUND selector, so it
   wins on specificity and merge order cannot decide it. */

/* ⚠️ PROVISIONAL P3 (substrate §0a). 8.5px is below Foundations' own floor —
   "Nothing on a phone screen goes below 13px" (Range Day - Foundations.html:116)
   — which the rendered kit breaks in 295 of 362 size declarations, with 239 in
   the 8-11.5px band. Shipping as drawn under substrate §0 (rendered kit practice
   beats Foundations prose) and routed loudly, not absorbed: the designer
   compensated with CONTRAST rather than size (rd-train-kit.jsx:6-10, "almost all
   secondary copy in this app is 8-11px, so the tertiary grey has to clear
   roughly 5:1"), so the size and the lifted --rd-fg3 are ONE decision. A surface
   that keeps 8.5px and reverts the grey gets the worst of both.

   The size is a token and not a literal for exactly one reason: this role has 40
   call sites, two of them inside the kit (so every numeric field caption and
   every stat key in the app), and there is no partial adoption available. If
   Claude Design rules for the floor, the fix must be one line here. */
:root { --rd-lab-size: 8.5px; }

.rd-label {
  /* The kit leaves line-height UNSET (05-tlab.md §3.2) — in the preview harness
     it inherits from an ancestor, which is fine in a preview and unsafe here,
     where the ancestor is not fixed. Every one of the 40 corpus strings is one
     line at phone width (the longest is 26 characters), so the corpus offers NO
     evidence for what a wrapped label should do. 1.2 keeps a two-line accident
     readable; the VALUE is an assumption, unverified, and wrap-vs-truncate is
     routed to the designer. Leaving it unset is the one option that guarantees a
     surprise later. */
  line-height: 1.2;
  /* Owns no vertical rhythm: 7 of 40 sites wrap it in a spacing container and
     the other 33 sit flush against a neighbour that carries the gap. Explicit
     because the element is the caller's choice (§7) and h2-h6, <p> and <dt> all
     arrive with a UA margin. */
  margin: 0;
}

/* OVERRIDE: swaps .rd-lab's literal 8.5px for the token above. Identical pixel
   today; the point is the flip point. Compound selector so this holds wherever
   the merge lands it. */
.rd-label.rd-lab { font-size: var(--rd-lab-size); }

/* .rd-label must NEVER declare `color`. Emphasis is carried by .rd-lab-acc
   (style.css:980, --rd-acc), which beats .rd-lab's --rd-fg3 by source order in
   the existing file; a `color` here would sit later still and silently win over
   both. The two-value emphasis set is enforced in Rust (`Emphasis`), and it is
   --rd-acc and never --rd-volt: volt is the accent as a FILL, acc is the accent
   as text or stroke (substrate §1.1). Volt here reads fine in dark and is
   unreadable in light (#CEFF51 on #F1F1EC). */

/* States (05-tlab.md §4). The kit draws exactly two — default and accent — and
   both are above. There is no disabled, loading, error or empty state: absence
   IS the empty state and it lives at the call site (`{label && <TLab …>}`,
   rd-train-kit.jsx:102), not in the component.

   hover / :focus-visible — NOT APPLICABLE, stated rather than omitted so a
   reviewer knows it was considered. RdLabel is non-interactive text: no hover,
   no focus ring, no tab stop, and it is never itself the interactive element.
   The corpus cannot rule on this — onClick, cursor:, role=, tabIndex and href=
   are ZERO across all seventeen .jsx files, so nothing there is interactive,
   including its buttons. PROVISIONAL pending Claude Design, for the two cases we
   will hit: when the label names a form control the CONTROL shows focus (the
   ring belongs to the field, see 10-tnum.md §4.2); when the label sits inside a
   tappable card the CARD takes hover and focus as one unit. The label never
   lights up independently and never becomes its own tab stop. If a design ever
   needs a tappable micro-label, that is a button wearing the label voice — a
   different primitive, not a prop added here. No rules are emitted for these
   deliberately: an absent selector is how the next author learns this was a
   decision rather than an omission. */

/* Two hazards worth carrying, neither of which is a rule to add here:

   1. letter-spacing: .2em applies AFTER the last character too, so a
      right-aligned or flex-end label sits ~1.7px short of its box edge. No
      corpus site right-aligns one, so it does not bite yet. Compensate with a
      negative margin at THAT call site; never by trimming the token.
   2. font-stretch: 78% needs the variable font's `wdth` axis. The shipped subset
      is 62-100 (substrate §2.2, PR #468), so 78 is comfortably inside it — but
      if the subset is ever narrowed, this role breaks SILENTLY by snapping to
      the nearest available width rather than erroring. */

/* ---- 06-heading.css ---- */
/* RdHeading — the display heading. Spec: docs/design/kit/06-th1.md
   Component: apps/web/src/components/kit/heading.rs

   MERGE NOTE — this block LAYERS ON `.rd-h1` (style.css:977, PR A), which stays
   the single definition of the type role: Archivo / uppercase / 900 /
   stretch 62% / letter-spacing 0 / line-height .92 / --rd-fg. Everything below
   is an ADDITION, except the one marked OVERRIDE, which is written as a COMPOUND
   selector so it wins on specificity and merge order cannot decide it.

   Two declarations in .rd-h1 are load-bearing and must stay there:

     line-height: .92   Sub-1 on purpose and part of the ROLE, not a tunable
                        (substrate §2.3). At weight 900 / width 62% the caps are
                        tall and flat-topped, so the default `normal` opens a
                        visible gutter between the two lines of a two-line
                        heading; .92 closes it so a stacked heading reads as one
                        mass. The sibling system reached .9 for the same job at a
                        larger default size (rd-auth-screens.jsx:79).
     letter-spacing: 0  Written EXPLICITLY, not omitted. Omitting it lets an
                        ancestor's tracking inherit — which is exactly the bug
                        .rd-lab's .2em would cause on a nested heading. */

.rd-heading {
  /* Owns no vertical rhythm: 4 of 33 sites wrap it in a spacing container (all
     four a marginTop separating the heading from an eyebrow above it) and the
     other 29 sit flush, with the FOLLOWING element carrying the gap. Explicit
     because the element is the caller's choice (05-tlab.md §7) and every
     h1-h6 arrives with a UA margin. */
  margin: 0;
}

/* OVERRIDE, deliberate: .rd-h1 hard-codes font-size: 30px, and that 30 is the
   kit's DEAD default — `size` is passed at 33 of 33 corpus sites
   (grep -ho '<TH1[^>]*>' *.jsx | grep -vc 'size=' -> 0), and even the two sites
   that render at 30 pass it explicitly. A default nobody has taken is a default
   nobody has checked, so the Rust prop is required and every RdHeading sets
   --rd-h1-size inline.

   No var() fallback, deliberately: the only candidate is that same dead 30. If
   the property is ever missing the declaration becomes invalid-at-computed-value
   -time and the size inherits, which is visibly wrong — the failure that gets
   noticed, rather than 30px everywhere, which is the failure that does not.

   Twelve distinct sizes over 33 sites (17-30, all integers) is why this is a
   custom property and not twelve modifier classes. It is a fit control, not a
   hierarchy control: size falls as string length rises, and one screen carries a
   23px title and a 22px empty-state heading a full outline level apart
   (rd-train-shell.jsx:79-86). Never derive the element from it. */
.rd-heading.rd-h1 { font-size: var(--rd-h1-size); }

/* One line per block. 10 of 33 corpus headings are broken by hand, and none of
   the ten breaks where the text would naturally wrap — `Pick your look` at 30px
   fits one line and is broken anyway (rd-train-theme.jsx:35) to make a two-line
   block whose .92 leading reads as one mass. The break is a typographic decision
   made per string; auto-wrapping reproduces none of them.

   The component emits these spans from a sequence of line strings the caller
   passes as DATA. The caller never authors markup: this role renders
   user-authored drill and course names at 8 of 33 sites ({BILL.name},
   rd-train-run.jsx:70), so accepting "<br />" in a string would put a raw-HTML
   sink on the heading — not a trade worth two lines of copy.

   <span>, not <div>: a heading's content model is phrasing content. */
.rd-heading__line { display: block; }

/* ⚠️ NEVER put `overflow: hidden` on .rd-heading, or on any box sized to one.
   At size=30 the .92 line box is 27.6px — SHORTER THAN THE TYPE — so descenders
   and accented capitals overflow it by construction, and clipping eats them.
   This is not theoretical: the corpus's one ellipsised display heading had to
   LEAVE THE ROLE to do it. The site that established this was rd-profile.jsx:95 [gone],
   an ad-hoc tdisp(900, 66, '.01em') relaxed to lineHeight: .98 — the leading was
   relaxed specifically in order to truncate. ⚠️ That line no longer exists: the
   08-04 refresh split rd-profile.jsx (now 68 lines) and collapsed 66 to 62. The
   surviving ellipsised display heading is rd-profile-collapsed.jsx:25, at
   tdisp(900, 62, '.01em') / 18px / lineHeight 1 — so the ESCAPE still exists and
   the reasoning holds, but .rd-h1-alt's own size and leading are now unmoored
   from any live site and are queued for the re-diff batch. That escape lives as
   `.rd-h1-alt` (:978) and is deliberately NOT a prop on this component: if you
   need to ellipsise, you are not in this role. Long user-authored headings are
   an open question routed to the designer (06-th1.md §10 item 1). */

/* States (06-th1.md §4). The kit draws exactly one: --rd-fg at the size passed.
   No disabled, no loading, no error, no empty — and the absence of a state
   colour is DELIBERATE, checked against two screens that had one in hand and
   spent it elsewhere. ScreenGuard (rd-train-run.jsx:314-323) has two variants
   carrying t.warn and t.danger, puts both on a 42px marker block ABOVE the
   heading, and leaves the heading on --rd-fg in both. State never recolours the
   heading; the furniture around it carries the state. There is no colour
   modifier here and no colour prop in Rust — a screen that needs a red heading
   is a finding to route, not a class to add.

   One near-state that is NOT a heading state: rd-authoring-create.jsx:344 dims a
   whole screen with opacity: .3 behind an open sheet. That belongs to whatever
   draws the sheet.

   hover / :focus-visible — NOT APPLICABLE, stated rather than omitted. RdHeading
   is non-interactive text: no hover, no focus ring, no tab stop. The corpus
   cannot rule on it (onClick / cursor: / role= / tabIndex / href= are zero
   across all seventeen .jsx files, so nothing there is interactive, including
   its buttons). PROVISIONAL pending Claude Design: when a heading sits inside a
   control — a navigable list row, a tappable card — the CONTROL takes hover and
   focus as one unit. The heading never lights up independently and never becomes
   its own tab stop. If a heading itself must be actionable, that is a control
   wearing the display voice, which is a different primitive. */

/* font-stretch: 62% is the LOWEST shipped value of the variable font's `wdth`
   axis (subset 62-100, substrate §2.2, PR #468). If the subset is ever narrowed,
   this role breaks first and it breaks SILENTLY — the browser snaps to the
   nearest available width rather than erroring. Worth a look in the first
   release build, and note the standing hazard that Dioxus rsx! differs debug vs
   release, so verify under `trunk --release`. */

/* ---- 07-text.css ---- */
/* RdText — the body paragraph. Spec: docs/design/kit/07-tp.md
   Component: apps/web/src/components/kit/text.rs

   MERGE NOTE — this block LAYERS ON `.rd-p` (style.css:983, PR A), which stays
   the single definition of the type role: Archivo / 13px / line-height 1.5 /
   --rd-fg2 / text-wrap: pretty / margin: 0. Everything below is an ADDITION,
   except the one marked OVERRIDE, which is written as a COMPOUND selector so it
   wins on specificity and merge order cannot decide it.

   Three declarations already in .rd-p are load-bearing:

     margin: 0        The highest-value line in this role. See below.
     line-height: 1.5 Hard-coded and part of the ROLE (substrate §2.3), not a
                      tunable and not exposed as a prop. It is the exact inverse
                      of the heading's .92: the display voice closes up, the
                      reading voice opens out, and that contrast does as much
                      work as the size difference.
     text-wrap: pretty  Progressive enhancement — see the note at the foot. */

/* ⚠️ PROVISIONAL P3 (substrate §0a), and this role is where the floor broke.
   Foundations says "Nothing on a phone screen goes below 13px"
   (Range Day - Foundations.html:116). TP's default of 13 is the ONLY value in
   this whole wave that satisfies that sentence — and 13 of 18 call sites (72%)
   go below it. The role was specified at the floor and then walked under it on
   nearly every real screen.

   Tokenised so a ruling is one line. Worth recording: RdText is the CHEAPEST
   place to honour a partial reinstatement — the whole range is 11.5-13, so
   clamping to 13 costs at most 1.5px of reflow per site, against RdLabel's
   8.5 -> 13. If the floor comes back partially, start here. */
:root { --rd-body-size: 13px; }

.rd-text {
  /* `tbody` is font-family and NOTHING else (rd-train-kit.jsx:18) — no stretch,
     no transform, no tracking, and no weight. In the preview harness the weight
     inherits from <body>; in our stylesheet the ancestor is not fixed, and this
     role's most likely neighbours are weight 700-900 display elements, so
     inheritance will find them. 400 is measured, not assumed: Foundations.html:100
     is the Archivo specimen for reading copy and declares font-weight:400 with
     colour fg2 — this role's colour at a body size. That the value belongs in
     the class rather than being left to inherit is OUR inference, labelled. */
  font-weight: 400;
  /* Restated even though .rd-p already carries it, because it is the single
     highest-value declaration in this component and it must not depend on
     another rule keeping it. ALL 18 corpus sites wrap this paragraph in an
     explicit spacing container — 11 on the same line, the other 7 on the line
     above, checked individually because a same-line grep undercounts multi-line
     JSX. There is no counterexample. The corpus runs under a preview-harness
     reset (Range Day - Training.html:6, `*{…margin:0;padding:0}`) that our
     hand-curated stylesheet does not provide, so a paragraph that inherits the
     UA <p> margin is wrong on all 18 surfaces at once — and wrong in a way that
     looks plausible. */
  margin: 0;
}

/* OVERRIDE: swaps .rd-p's literal 13px for the token, and lets a call site step
   DOWN via --rd-p-size. Two names on purpose: --rd-body-size is the role's own
   size (the P3 flip point, set once at :root), --rd-p-size is the per-instance
   concession (set inline, four values over 18 sites — a custom property rather
   than four modifier classes, and a number, never a colour).

   The component emits --rd-p-size ONLY for a genuine override, and normalises an
   explicit 13 back to "no override", so the default always reaches the render
   through --rd-body-size. Inlining 13px at the five default sites would leave
   the token inert and a P3 ruling silently ineffective. */
.rd-text.rd-p { font-size: var(--rd-p-size, var(--rd-body-size)); }

/* The default is the MAXIMUM, not a midpoint. Four values are passed
   (11.5 x1, 12 x6, 12.5 x6, 13 x5) and every explicit one goes DOWN; there is no
   site above 13. So the size prop is a density concession, not a size in the
   ordinary sense. The five sites that take the default are not scattered: all
   five are the paragraph of a full-screen, vertically-centred outcome or empty
   state, each verified individually inside a justifyContent:'center' container
   (rd-train-shell.jsx:87,154; rd-train-run.jsx:324; rd-train-stress.jsx:99;
   rd-authoring-create.jsx:390). 5 of 5, no exception:

     When the paragraph IS the screen, it is 13. When it competes for space with
     controls, lists or a diagram, it steps down. */

/* text-wrap: pretty is a PROGRESSIVE ENHANCEMENT, and it stays in .rd-p
   unguarded — no @supports, no fallback declaration. The mechanism, not a guess:
   a declaration whose value is not valid for its property is invalid and dropped
   AT PARSE TIME. It does not throw, does not invalidate its neighbours, and does
   not invalidate the rule; the element then takes the initial values (`wrap`,
   `auto`), i.e. ordinary greedy line-breaking. So the failure mode is: the
   paragraph still wraps, at the same width, in the same box, with the same
   leading — some paragraphs just get a shorter last line. Nothing reflows out of
   its container and nothing is hidden. Verified safe for us specifically: no
   corpus site puts this role in a height-constrained box, no ancestor sets
   overflow: hidden, and none has a fixed pixel height.

     Rule: nothing in any layout may depend on it, and a differing line count
     between engines is not a defect.

   NOT claimed here: which engines and versions support it. That matrix moves,
   and stating versions from memory is exactly the unlabelled assumption this
   spec set forbids — check it against our real browser-support floor when it
   matters (07-tp.md §10 item 4). The ruling above holds either way, which is the
   point of specifying it as an enhancement.

   Do NOT substitute `balance`: it is a different value for a different job
   (short blocks — headings, pull quotes) and browsers cap the line count it
   applies to. The designer wrote `pretty` three times across two systems. And if
   a fallback is ever wanted it is not CSS — manual <wbr> or non-breaking spaces
   would put copy decisions in markup, which the element-mapping rule and the
   heading's line-break rule both forbid. The correct fallback is no fallback. */

/* States (07-tp.md §5). The kit draws exactly one: --rd-fg2 at the size passed.
   No disabled, no loading, no error, no empty — when there is nothing to say the
   call site renders no paragraph at all. There is no colour modifier and no
   colour prop: 18 of 18 sites are --rd-fg2, the only one of the three roles in
   this wave with no escape hatch at all. The three-step hierarchy heading ->
   paragraph -> meta maps exactly onto --rd-fg -> --rd-fg2 -> --rd-fg3, and this
   is the middle step that makes "the hierarchy comes from the step between them"
   (rd-train-kit.jsx:6-10) true.

   One near-state that is NOT a state of this role: rd-authoring-create.jsx:344 dims a
   whole screen with opacity: .3 behind an open sheet. That is the backdrop
   treatment on the container.

   hover / :focus-visible — NOT APPLICABLE, stated rather than omitted. RdText is
   non-interactive text: no hover, no focus ring, no tab stop. The case we will
   actually hit is not a tappable paragraph but A LINK INSIDE ONE — body copy is
   where inline links live, and two corpus strings are already reaching for one
   (rd-train-course.jsx:78, rd-authoring-create.jsx:357). PROVISIONAL pending Claude
   Design: this role gains no hover or focus styling ever; an inline link inside
   it is a separate element with its own hover and :focus-visible treatment, and
   it must be distinguishable from surrounding body copy by something other than
   colour alone. The link treatment itself is NOT specified here — the corpus
   contains no inline link (grep -c '<a ' *.jsx -> 0) and Foundations declares
   none. Note the constraint that puts on the API: the Rust prop is a plain
   String and so cannot contain a link. Deliberate, and routed. */

/* No measure cap, and that is an open question rather than a decision. The
   designer's own desktop body style caps at 70ch (Range Day - Training.html:12,
   `.lede`) and this role sets no max-width. At phone width the viewport IS the
   measure, so the corpus cannot tell us whether the omission is intent. It
   becomes real at the first tablet or desktop breakpoint (07-tp.md §10 item 6). */

/* ---- 08-chip.css ---- */
/* RdChipSelector / RdChipCommand / RdChipStatus — spec: docs/design/kit/08-tchip.md
   Three components share exactly ONE thing: the painted leaf below. The roles are
   not shared and are not a modifier — see chip.rs for why. */

/* --------------------------------------------------------------------------
   The painted leaf. Numbers from tmp/design/snapshot/rd-train-kit.jsx:114.

   SINGLE LAYER, deliberately. A chip has no stroke, so the two-layer chamfer
   pattern (substrate §3.3) does NOT apply and the inner-cut rule is not in play. With
   the filled TBtn it is one of only two primitives in the system drawn as a
   solid fill with no border — do not copy RdChamferBox in here.

   Chamfer 7, bottom-right only (substrate §3.1) — the smallest cut bound to
   anything in the kit. Written literally rather than as --rd-box-r because a
   chip's cut is fixed at every one of its 20 corpus call sites; there is no
   scale to parameterise.

   Type role, substrate §2.3: 700 / stretch 78 / 9.5px, uppercase from the
   display helper. The 9.5px sits inside contested provisional §P3 (Foundations:
   "nothing on a phone screen goes below 13px"); kit practice ships under
   substrate §0. Its compensation is CONTRAST, and the contrast MUST travel with
   the size — --rd-fg2 on --rd-chip measures 7.75:1 dark / 8.04:1 light, and a
   later screen that keeps 9.5px but reverts to a dimmer grey loses it. Do not
   soften the label colour here. */
.rd-chip {
  display: inline-block;
  font-family: 'Archivo', system-ui, sans-serif;
  text-transform: uppercase;
  font-weight: 700;
  font-stretch: 78%;
  font-size: 9.5px;
  /* A long label makes a long chip. It does not truncate and it does not wrap;
     the ROW wraps or scrolls instead (§4.3). rd-train-kit.jsx:114. */
  white-space: nowrap;
  /* --rd-chip-bg is the chip's own fill, restated as a variable so the focus
     ring's hole can read it without knowing which state the chip is in. */
  --rd-chip-bg: var(--rd-chip);
  background: var(--rd-chip-bg);
  color: var(--rd-fg2);
  /* The inset focus ring is positioned against this box. */
  position: relative;
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 7px), calc(100% - 7px) 100%, 0 100%);
}

/* Vertical padding is IDENTICAL across both sizes. Billboard is wider and
   looser, never taller: 8→11px horizontal, .05→.1em tracking. */
.rd-chip--compact   { padding: 7px 8px;  letter-spacing: .05em; }
.rd-chip--billboard { padding: 7px 11px; letter-spacing: .1em; }

/* Active — a selector's lit chip, or a status chip's primary attribute.

   --rd-volt, NEVER --rd-acc. The active chip is a FILL, so it takes the fill
   token and is the same lime in both themes; --rd-acc is the accent as text or
   stroke and goes dark olive in light. Writing --rd-acc here is correct in dark
   theme BY COINCIDENCE — the two tokens hold the same #CEFF51 there — and
   renders dark-olive-on-lime in light. It is the single most likely token error
   in this component and no dark-theme screenshot can catch it.

   Label is --rd-on-volt (black in both themes), 18.05:1 on volt. Never white on
   volt (substrate §1.5).

   ⚠ KNOWN DEFECT, NOT COMPENSATED HERE. In light theme --rd-volt is 1.03:1
   against --rd-bg: the fill is invisible as a shape, the 7px chamfer — the
   system's one piece of ownable geometry — cannot be seen at all, and the whole
   state reads as "the label went black". That is designer question 12
   (08-tchip.md §3.2 / §9.4) and it is awaiting a ruling. Implement the tokens as
   measured; do NOT add a border or a shadow to rescue the silhouette. */
/* ADJUDICATED — designer answer #12. The selected state is a FILL FLIP, not a
   label-colour flip, so the chamfer reads and the contrast is structural.

   The defect: in light theme a volt-filled chip measured 1.03:1 against the page
   ground — the shape was invisible and "selected" survived only as a change of
   label colour. His fix, verbatim in intent: "an inverted chip is unmistakable
   at arm's length in daylight, which is the actual use case. Do not solve this
   by fixing the wash value — a wash cannot carry a selected state on paper."

   Carried as token VALUES per architect ruling 2, so the component is
   structurally identical in both themes. Dark is unchanged. */
.rd-chip--on {
  --rd-chip-bg: var(--rd-chip-on-fill);
  background: var(--rd-chip-bg);
  color: var(--rd-chip-on-label);
}

/* --------------------------------------------------------------------------
   The hit area — roles A and B only. Status chips get none; they are not
   tappable.

   ARCHITECT RULING (spec §4.1, reported not asked): interactive chips get a
   44×44 minimum hit area with the PAINTED BOX UNCHANGED. A compact chip
   measures ≈21×26 CSS px — less than a quarter of the 44pt minimum the frame
   itself declares (ios-frame.jsx:63, `height: 44, minWidth: 44`) — and this app
   is used outdoors, in gloves, on a live line. One of the smallest chips in the
   corpus is the control chosen specifically to replace typing.

   Expansion is on the PRESSABLE ELEMENT, not on padding: padding would change
   the drawn design, and the drawn design is what was approved. The chip is
   inset-painted at its natural size inside this box and stays 21×26 to the eye.

   ⚠ One consequence to look at on the kitchen sink: min-width lays the row out
   on a 44px pitch, so two SHORT chips ("1", "Fit") sit further apart than the
   mock draws them, while long labels ("Assessments") are unaffected because the
   min never fires. The alternative — an absolutely-positioned ::before that
   expands the hit area without touching layout — keeps the row rhythm exactly
   but makes adjacent 44px targets OVERLAP in a 27px-pitch row, so the later
   sibling wins the last ~5px of its neighbour's painted glyph. A mis-tap on a
   visible control is worse than a wider gap, hence this choice; it is a
   trade-off, not a free lunch. */
.rd-chip-hit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* PROVISIONAL — pending Claude Design (spec §8.1). The kit specifies no hover;
   the iOS frames have none by construction, so this is a pointer-only concern
   and ScreenShelfDesktop (rd-editor-shelf.jsx:299) is the one corpus screen
   where it applies.

   The LABEL lifts, not the fill: --rd-chip has no neighbouring step in the token
   set and its nearest candidate (--rd-surf2) is 1.16:1 away in dark theme — a
   hover nobody would see. The label already has a one-step ladder the designer
   built (fg2 → fg).

   An already-lit chip does not change: it is volt, there is no brighter state,
   and the volt-scarcity rule forbids inventing one.

   No pressed treatment, deliberately: selection is instantaneous and local, so
   the result IS the feedback (§8.1). */
@media (hover: hover) {
  .rd-chip-hit:hover .rd-chip:not(.rd-chip--on) { color: var(--rd-fg); }
}

/* PROVISIONAL, AND GENUINELY UNSOLVED — spec §8.2, needs a rendered look before
   merge.

   The problem: clip-path clips BOTH `outline` and `box-shadow`, so neither can
   draw a ring that follows the 7px cut, and a chip is a single-layer element so
   the two-layer trick that saves RdChamferBox is not available without changing
   its anatomy. border-radius is banned system-wide (substrate §1.5), so a
   rounded ring is not a fallback.

   ADJUDICATED — the designer's answer: "for single-layer chips, use an inset
   ring: a second clip-path'ed element inside the chip at chamfer `n`, drawn only
   on focus. It costs one node on a primitive that already has one."

   This replaces a `filter: drop-shadow()` candidate. That one DID trace the
   chamfered silhouette — the filter's input is the already-clipped chip, so its
   alpha channel is the cut — but drop-shadow has no spread, so two stacked
   passes read as a GLOW rather than the crisp stroke asked for. The rendered
   evidence decided against it, which is what the ruling asked for.

   Geometry, by the same rule as every other cut (substrate §3.3,
   n = r - w(2 - SQRT2)):
     chip cut          7.0
     ring outer cut    7.0 - 2.0(2-SQRT2) = 5.83   (inset 2px inside the chip)
     ring hole cut     5.83 - 1.5(2-SQRT2) = 4.95   (ring is 1.5px)
   The hole takes the chip's OWN fill via --rd-chip-bg, so the ring reads as a
   stroke on every chip state without knowing which state it is in.

   --rd-acc, not --rd-volt: it is a stroke (substrate §1.1). That is also what
   keeps it legible on a SELECTED chip, where a volt ring on a volt fill would be
   invisible.

   :focus-visible only, never :focus — a plain :focus ring on a chip tapped with
   a thumb leaves a stuck outline on a touch screen. The UA outline is removed
   only inside this rule, and only because the rule replaces it: the UA draws a
   rectangle around the 44px hit box, which is both the wrong shape and the wrong
   size. */
.rd-chip-hit:focus-visible { outline: none; }

/* Present but invisible at rest, so focus costs no layout and no repaint of the
   chip itself — only this node's opacity changes. */
.rd-chip__ring {
  position: absolute;
  inset: 2px;
  background: var(--rd-acc);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 5.83px), calc(100% - 5.83px) 100%, 0 100%);
  opacity: 0;
  pointer-events: none;
}
.rd-chip__ring::after {
  content: "";
  position: absolute;
  inset: 1.5px;
  background: var(--rd-chip-bg);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 4.95px), calc(100% - 4.95px) 100%, 0 100%);
}
.rd-chip-hit:focus-visible .rd-chip__ring { opacity: 1; }
/* The ring is absolutely positioned and its hole is OPAQUE (it carries the
   chip's own fill so the ring reads as a stroke on any state). Without this the
   hole paints straight over the label — caught in the 10x render, not by any
   checker: every class resolved and every test passed with the text invisible.
   `position: relative` alone is NOT enough: positioned siblings paint in DOM
   order and the ring comes second, so the label needs an explicit z-index. */
.rd-chip__label { position: relative; z-index: 1; }

/* ADJUDICATED — designer follow-up ①. The ATTRIBUTE chip: the lead chip of a
   metadata row, a bare category literal. It used to take the selected fill —
   a hairline instead, because "a category literal was never a selection and
   should not have looked like one."

   ⚠️ REBUILT 2026-08-04. This was a single clipped span with
   `box-shadow: inset 0 0 0 1px`, justified by the claim that "an inset shadow is
   clipped WITH the shape, so it traces the chamfer". THAT CLAIM IS FALSE, and
   this stylesheet already said so 70 lines above, in the focus-ring block:
   "clip-path clips BOTH `outline` and `box-shadow`, so neither can draw a ring
   that follows the 7px cut."

   An inset shadow is painted against the BORDER BOX — a rectangle. `clip-path`
   then cuts the painted result, so along the diagonal there is no ring at all:
   the hairline was absent on the one edge it exists to draw, and present on the
   four that a plain rectangle would have given for free.

   Rendered proof, geometry scaled 8x so a 1px stroke reads as an 8px band:
   tmp/design/kit-renders/rediff-01-attr-chip-diagonal-comparison.png.

   It is now the two-layer construction every other stroked box in the kit uses
   (RdChamferBox, stroke Muted, fill Hollow, r=7, w=1) — "a chamfered hairline is
   always two layers, and the inner fill must be opaque." The face below is the
   INNER layer; the stroke is the outer layer's background.

   Process note worth keeping: the old block was labelled "PROVISIONAL, AND
   GENUINELY UNSOLVED ... needs a rendered look before merge", and it merged
   without one (#477). The label was accurate; it just did not stop anything. */
.rd-chip-attr-face {
  font-family: 'Archivo', system-ui, sans-serif;
  text-transform: uppercase;
  font-weight: 700;
  font-stretch: 78%;
  font-size: 9.5px;
  white-space: nowrap;
  color: var(--rd-fg);
}
/* The kit gives the attribute chip its own padding scale rather than reusing
   the plain chip's (rd-train-kit.jsx:167). The 1px stroke sits OUTSIDE this
   face, so the painted box lands 2px shy of a plain chip in each axis. */
.rd-chip-attr-face--compact   { padding: 5px 6px; letter-spacing: .05em; }
.rd-chip-attr-face--billboard { padding: 5px 9px; letter-spacing: .1em; }

/* --------------------------------------------------------------------------
   The selector row. Owned by RdChipSelector — callers do not pass a gap.

   Measured gaps run 5–8px across twelve rows with no single convention (§4.2);
   6 is the median and the value both header rows and the History filter use.
   The 8px command-row gap at rd-train-stage.jsx:249 is screen layout for the
   stage-viewer footer and is NOT shipped here — RdChipCommand does not own a
   row, so that value arrives with the Stage Editor migration.

   Wrap is the default; scroll is opt-in and only for fixed-height containers. */
.rd-chip-row {
  display: flex;
  gap: 6px;
  align-items: center;
}
.rd-chip-row--wrap   { flex-wrap: wrap; }
.rd-chip-row--scroll { flex-wrap: nowrap; overflow-x: auto; }

/* No disabled rule, deliberately. There is no disabled chip anywhere in the
   corpus, and TBtn's treatment (--rd-surf2 fill, --rd-fg3 label,
   rd-train-kit.jsx:81) would collide with --rd-chip, which is already a near
   neighbour of --rd-surf2. A page that needs one routes it rather than inventing
   it here (§5).

   No loading rule: a chip tap never waits on the network (§6).
   No error rule: N/A.
   The empty case is the absence of the whole strip, not a state on the chip
   (§1.2) — it is handled in Rust by rendering nothing. */

/* ---- 09-stat.css ---- */
/* RdStat — the key/value stat pair. Spec: docs/design/kit/09-tstat.md
   Numbers from tmp/design/snapshot/rd-train-kit.jsx:117-123.

   ⚠ ZERO DECORATION, DELIBERATELY. No background, no border, no clip-path, no
   padding, no margin. That is not an oversight and it is not a reset — it is the
   component (spec §3.1, rd-train-kit.jsx:119-122). The chamfered outlined box
   that surrounds stats on screen belongs to the ROW, an equal three-column grid
   that is identical in shape at 8 of 8 corpus groups (§3.2), and the row is the
   caller's. Adding a fill or a stroke here is the top implementation trap, and
   the designer wrote the sentence that rules it out (rd-train-detail.jsx:46-47):

     "Not a form. Values are set as values — no boxes, no field chrome, nothing
      that offers to be typed into. Editing is a button, once, at the bottom."

   So the whole component is two text elements and one gap. */
.rd-stat {
  display: flex;
  flex-direction: column;
  /* The one number RdStat owns. rd-train-kit.jsx:121. */
  gap: 3px;
}

/* The value. Face, weight, tabular figures and the default --rd-fg all come from
   the shared .rd-num role (style.css) — this rule adds ONLY the 16px the
   substrate binds to this primitive (§2.3; .rd-num's own 17px is the numeric
   FIELD's size).

   §P2 (system monospace stack vs Archivo-tabular) lived entirely inside .rd-num
   and RESOLVED 2026-08-04 to Archivo 800 / 75 / tabular. Nothing here ever
   depended on which way it went, which is why this rule needed no change.

   Qualified by the parent rather than written as a bare .rd-stat__value: it must
   beat .rd-num, and both are single-class selectors, so a bare form would depend
   on which file the orchestrator appends last. Two classes (0-2-0) wins
   regardless of merge order.

   No white-space, no text-overflow, no max-width — the kit sets none, so both
   lines wrap. Columns are ≈106px, the longest measured key is `Personal Bests`
   and every measured key fits; whether a two-line key is acceptable is not
   answerable from the corpus (§3.3). Do not invent an ellipsis. */
.rd-stat .rd-stat__value {
  font-size: 16px;
}

/* Accented — the comparative benchmark, 3 of 24 sites.

   --rd-acc, NEVER --rd-volt. A stat value is TEXT, and substrate §1.1 binds text
   and strokes to acc (#CEFF51 dark, #3E4E10 light) while volt is the accent as a
   FILL and is the same lime in both themes. Writing volt here is correct in dark
   theme BY COINCIDENCE — the two tokens hold the same hex — and renders a
   benchmark number at almost no contrast on #F1F1EC in light. No dark-theme
   screenshot catches it (§9.1).

   Colour is the ONLY difference between the two states: size, weight, spacing
   and the key's own --rd-fg3 are all unchanged (§4.2). That is what makes the
   emphasis cheap enough to be scanned rather than read. */
.rd-stat .rd-stat__value--bench {
  color: var(--rd-acc);
}

/* Placeholder — a value that is not a number.

   The kit's `numc` resolves this BEFORE the accent, so these two classes are
   mutually exclusive by construction in stat.rs rather than by specificity
   here: an accented em dash is an accent on nothing. Same --rd-fg3 step the
   numeric field uses for an empty buffer, which is what closes the asymmetry
   between the two primitives (spec §4.3). */
.rd-stat .rd-stat__value--dim {
  color: var(--rd-fg3);
}

/* No :hover, :focus-visible, :active, disabled, loading or error rule — stated
   rather than omitted, per CONVENTIONS.md. A stat pair is read, not operated
   (§5): no cursor, no pointer-events change, no tabindex, no focus ring. Making
   it a focus stop would add 24 dead tab stops. If a page migration later needs a
   tappable stat row, that is the ROW's spec and goes to Claude Design; this
   text must not acquire a hover treatment either way.

   No empty/placeholder rule either. TStat does not dim its em dash the way the
   numeric field dims an empty buffer (rd-train-kit.jsx:121 colours only on acc),
   so the placeholder renders at full --rd-fg. That asymmetry between two
   primitives by the same hand is measured, and it is routed to Claude Design —
   do NOT "fix" it by copying the field's --rd-fg3 branch in here (§4.3, §9.4). */

/* ---- 10-numeric-field.css ---- */
/* RdNumericField — the digit-entry numeric field.
   Spec: docs/design/kit/10-tnum.md. Numbers from
   tmp/design/snapshot/rd-train-kit.jsx:97-111.

   The BOX is not here. It is RdChamferBox at chamfer 9 (01-chamfer-box.css), so
   the two-layer clip and the max(r - w(2 - √2), 2) inner rule are not re-derived — which
   is what makes the inner chamfer move 7 → 6 on focus for free, because the
   border widens 1 → 1.5. Holding it at 7 across both states is this primitive's
   top trap; numeric_field.rs pins it with a test.

   What lives here: the cell around the box, the value row inside it, the caret,
   the sub-caption, and the focus-ring layer.

   §P2 RESOLVED 2026-08-04 — the numeric face and its default colour are the
   shared .rd-num role in style.css (Archivo 800 / 75 / tabular, --rd-fg).
   Nothing below read which way it resolved; these rules contribute only the
   letter-spacing this field alone adds, plus the two sizes the substrate binds to
   this primitive (§2.3) and the measured --rd-fg3 branch, both of which hold
   under either answer. */

/* The cell. flex:1 at 8 of 8 call sites — every drawn instance shares a row with
   a sibling (a hit-factor readout, another field, or a read-only "Was" box), and
   the kit's non-flexed branch is never drawn. Harmless outside a flex row. */
.rd-numf {
  display: flex;
  flex-direction: column;
  flex: 1;
}

/* The kit puts margin-top:5 on the BOX when a label is present and 0 when it is
   not (rd-train-kit.jsx:103). Written as the label's margin-bottom instead: it
   renders identically, the no-label branch is 0 by construction rather than by a
   conditional, and the box's outer layer belongs to RdChamferBox, which exposes
   no class hook of its own. Face/size/colour come from the .rd-lab role. */
.rd-numf__label {
  margin-bottom: 5px;
}

/* --------------------------------------------------------------------------
   The focus-ring layer — 🟡 PROVISIONAL pending Claude Design (spec §4.2, Q4).

   ⚠ A ring on a chamfered control CANNOT be an `outline` or a `box-shadow`:
   clip-path cuts both. It needs its own layer outside the clip, and 02-tbtn.md
   §5.3 specifies an always-present wrapper so focus costs no layout shift. This
   is that wrapper.

   It is drawn with the same trick as the border itself — a padded, clipped box
   whose background IS the ring — so the ring follows the chamfer instead of
   boxing it. Chamfer 12 = 9 + 3, following TBtn §5.3's r+3 idiom (there: 16 =
   13 + 3) so the two provisional rings match each other rather than each being
   derived separately.

   Two consequences, both stated rather than smoothed over:
   - On the diagonal the ring measures |12 - 2*2 - 9| / √2 ≈ 0.7px, not 2px. The
     button's proposal has exactly the same property; collinear would be 13 and
     uniform 2px would be ~15.8. Flagged with Q4, not resolved here.
   - Inside a flex row the wrapper's 2px padding comes out of the field's width,
     so the drawn box is 4px narrower than the design measures. TBtn §5.3 accepts
     the same cost ("4px of extra layout box on every button").

   This is an ADDITIONAL channel, on top of — never instead of — the design-level
   focus treatment, which has already consumed the stroke (§4.1).

   :focus-visible only, never :focus — a plain :focus ring survives a thumb tap
   as a stuck outline. The UA outline is dropped only inside the rule that
   replaces it. The element is a Tab stop only when the caller opts in
   (`focusable`, default off, tabindex="-1"): the corpus binds no input control,
   and whether this field is keyboard-operable is UNDETERMINED (Q3). */
.rd-numf__ring {
  padding: 2px;
  box-sizing: border-box;
  background: transparent;
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%);
}
.rd-numf__ring:focus-visible {
  outline: none;
  background: var(--rd-acc);
}

/* --------------------------------------------------------------------------
   The value row. This is RdChamferBox's inner layer (passed as inner_class), so
   it already carries the fill, the inner clip and box-sizing.

   Centred both axes with gap:1 between value and caret (rd-train-kit.jsx:104).
   min-height 44 is the iOS touch minimum and is met by the field itself.

   ⚠ The caret shifts the digits. Because value and caret are centred AS A GROUP
   with a 1px gap, the 2px caret moves the value's centre 1.5px left on focus.
   That is derived from the flex geometry, not stated by the designer, and
   whether it is intended or should be absorbed by reserving caret space is open
   (spec §3.3, Q6). Implemented as drawn — do not add a phantom caret to
   "fix" it before the ruling. */
.rd-numf__box {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1px;
  min-height: 44px;
}

/* Size, weight, tabular figures and the default --rd-fg are the .rd-num role.
   17px is already .rd-num's own size, so the standard field needs no rule here;
   only Large overrides it.

   letter-spacing is the ONE thing this field adds to the shared role. The kit
   applies -.01em at the TNum call site and nowhere else — TStat's value is
   untracked — so it belongs here, not on .rd-num. It used to ride .rd-num,
   which silently tracked every stat value in the app.

   No white-space, no text-overflow, no max-width: the kit sets no cap on buffer
   length and the box has no fixed width, so what should happen past a maximum —
   ignore the keypress, scroll, shrink the face — is UNDETERMINED (§5.5, Q3).
   Do not invent truncation. */
.rd-numf__value { letter-spacing: -.01em; }
.rd-numf--lg .rd-numf__box  { min-height: 56px; }
.rd-numf--lg .rd-numf__value { font-size: 26px; }
.rd-numf--lg .rd-numf__caret { height: 26px; }

/* Empty buffer, and the provisional disabled state.

   The colour step --rd-fg3 → --rd-fg is the ENTIRE filled/empty signal: no
   placeholder text, no border change, no icon (§4.1). The em dash itself is
   U+2014 and is keyed on buffer EMPTINESS, never on a truthy value — see
   numeric_field.rs, where a legitimately zero score renders as a value.

   Qualified by the parent so it beats .rd-num regardless of which file the
   orchestrator appends last: both are otherwise single-class selectors. */
.rd-numf .rd-numf__value--dim {
  color: var(--rd-fg3);
}

/* The caret bar — 2 × 18px (26 on large), --rd-acc, pinned to the RIGHT of the
   value. It does not travel: the buffer only ever grows at its right-hand end,
   so there is no insertion point for a cursor to sit at. That is the digit-entry
   model showing through the drawing (§5.2).

   --rd-acc, never --rd-volt: this is the focus signal, the same stroke language
   as the border, and in light theme it is the dark olive. */
.rd-numf__caret {
  flex: none;
  width: 2px;
  height: 18px;
  background: var(--rd-acc);
}

/* The sub-caption. Body face at 9.5px — NOT the .rd-p role, which is 13px in
   --rd-fg2. rd-train-kit.jsx:108.

   ⚠ 9.5px sits inside contested provisional P3 ("nothing on a phone screen goes
   below 13px", Foundations:116); kit practice ships under substrate §0, and its
   compensation is CONTRAST — the lifted --rd-fg3 must travel with the size. A
   later screen that keeps 9.5px and reverts to a dimmer grey gets the worst of
   both.

   It carries two different jobs (§5.4): the entry hint on the five time fields
   (`Type 451` — the entry SEQUENCE, not the value, which is what makes an
   unfamiliar shooter's first entry succeed) and a unit or scope caption on the
   three integer ones. Both are copy and both arrive as a prop; no string here. */
.rd-numf__sub {
  margin-top: 4px;
  text-align: center;
  font-family: 'Archivo', system-ui, sans-serif;
  font-size: 9.5px;
  color: var(--rd-fg3);
}

/* Error — 🟡 PROVISIONAL (§4.2, Q4). Proposed by analogy, not invented: TBtn
   kind `d` establishes danger-as-stroke at width 1.5 (rd-train-kit.jsx:88), so
   the error state reuses the FOCUS GEOMETRY with the DANGER colour and one
   stroke language covers both. The message replaces the sub-caption; the caller
   passes it. */
.rd-numf__sub--error {
  color: var(--rd-danger);
}

/* States with no rule here, stated rather than omitted per CONVENTIONS.md:

   hover — 🟡 PROVISIONAL: no stroke change and no fill change, deliberately. The
   design's only stroke change is the focus signal, and an outdoor one-handed app
   has no hover; inventing one here would collide with it. `cursor: text` is
   proposed in the spec but not written, because the box binds no input control
   and the field is not a Tab stop by default — a text cursor over something that
   cannot be clicked into would be a lie. Routed with Q4.

   disabled — the fill (--rd-surf2 via Fill::Recessed), the stroke and the dimmed
   value are all resolved in numeric_field.rs, so there is no extra rule to write.

   loading — N/A. The field holds a locally-typed buffer and waits on nothing. */

/* ---- 11-theme-swatch.css ---- */
/* RdThemeSwatch — one option of the theme radio group.
   Spec: docs/design/kit/11-theme-swatch.md. Substrate: 00-substrate.md §1.4.

   THE STRUCTURAL FACT, and the only thing in this file worth reading twice:
   a swatch renders a miniature of the theme it is OFFERING, not of the theme
   currently in force. Two colour systems are on screen at once.

     - INSIDE the tile  -> the PREVIEWED theme. `.rd-swatch__mini` carries
       data-theme, so every --rd-* below it rebinds. This is why the token
       blocks widened from `:root[data-theme=…]` to `[data-theme=…]` and why
       each block defines the COMPLETE set: a partial block would leak half its
       tokens in from the active theme and the miniature would be a lie.
     - OUTSIDE the tile -> the ACTIVE theme. Radio ring, dot, option label and
       the UNSELECTED tile stroke are chrome; chrome belongs to the theme the
       user is standing in, not the one being offered.

   THE LOAD-BEARING PIXEL is `.rd-swatch__mini-score`. It is --rd-acc, which is
   lime in dark and dark olive in light, while `.rd-swatch__mini-cta` below it
   is --rd-volt, the same #CEFF51 in both. Side by side, the two tiles ARE the
   acc/volt split drawn (substrate §1.1). **If the two tiles' 5.76 render the
   same colour, the build is wrong** — the previewed scope is not being applied
   and the control has stopped explaining the choice it is asking about.

   Not one hex literal here: every colour in the source is routed through a
   token, including the five frozen dark literals the source hard-codes
   (spec §5, R1-R5). They are the visible edge of a missing parameter — the
   source component takes no active-theme argument, so it could not express
   active-theme chrome and froze it at the one configuration ever rendered. */

/* The whole option is one hit target: <label> wrapping the input, the tile and
   the text. The ring is 13px and the label 10px — either alone fails the 44pt
   minimum the frame itself observes (substrate §P3). This element clears it in
   both axes by a wide margin (>=130 x ~200). */
.rd-swatch {
  flex: 1 1 0;
  min-width: 0;
  display: block;
  position: relative;                     /* containing block for the absolutely
                                             positioned native input */
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* Visually hidden but STILL FOCUSABLE and still hit-testable by the label.
   Never `display:none` / `visibility:hidden` — both remove it from the tab
   order and take the native radio-group keyboard semantics with them (single
   tab stop, arrow-key roving, wrap-around, "2 of 2" announcement). */
.rd-swatch__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  opacity: 0;
  pointer-events: none;                   /* the <label> does the hit testing */
  appearance: none;
  -webkit-appearance: none;
}

/* Third, outermost clipped layer — ALWAYS present, transparent at rest, so
   focus costs no layout shift (02-tbtn §5.3 sets this idiom for the kit).
   A focus ring cannot be an `outline` or a `box-shadow` here: clip-path clips
   both, which is the same failure that forced the two-layer border.
   PROVISIONAL, pending Claude Design (spec §3.5, open question 3).

   Geometry: padding p = 2, tile chamfer n = 11, wrapper chamfer 13. The ring's
   thickness along the diagonal is (2p + n - 13)/sqrt(2) = 2/sqrt(2) ~ 1.41px
   against 2px on the straight edges. Validated against the known-positive pair
   in 01-chamfer-box.css: outer 13, w 1.5, inner 10 gives (3 + 10 - 13) = 0,
   i.e. the documented collinear/no-stroke case. Two live consequences:
     - 13 must stay BELOW 2p + n = 15, or the wrapper clips the tile's own
       diagonal instead of ringing it;
     - 12.17 (= 2p + n - p*sqrt(2)) would make the ring uniform 2px all round.
   Both routed with the ring itself rather than chosen here. */
.rd-swatch__focus {
  display: block;
  padding: 2px;
  box-sizing: border-box;                 /* inset, do not grow (see .rd-box) */
  background: transparent;
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 13px), calc(100% - 13px) 100%, 0 100%);
}
.rd-swatch__input:focus-visible ~ .rd-swatch__focus {
  background: var(--rd-acc);              /* focus in this system is an --rd-acc
                                             STROKE (TNum draws it that way) —
                                             acc, never volt: substrate §1.1 */
}

/* The chamfer box's inner. It carries only the fixed height; the previewed
   subtree inside carries the padding and paints the ground edge to edge, so
   the tile's own fill role is structurally invisible. `min-height` rather than
   `height` so it does not fight `.rd-box__inner { height: 100% }` and does not
   depend on which of the two rules the merged stylesheet lists last. */
.rd-swatch__tile {
  display: flex;
  min-height: 148px;
  padding: 0;
}

/* ---- everything below here resolves in the PREVIEWED theme ---------------- */

/* The ground. --rd-swatch-ground, not --rd-app-bg: app-bg carries the
   photographic texture and different scrim alphas, and a background photograph
   inside a 148px miniature is not what the source draws (spec §3.3). The `tex`
   branch is pre-resolved per theme, so nothing here tests a flag
   (substrate §1.2). */
.rd-swatch__mini {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 11px;
  box-sizing: border-box;
  background: var(--rd-swatch-ground);
}

/* The four miniature items are a PICTURE, not a composition. Each is drawn at
   miniature-specific numbers that differ from the primitive it resembles, so
   none of them may be built from RdHeading / RdStat / RdButton: title is TH1's
   axes at 15px (TH1 is 30), score is mono at weight 700 (TNum is 600), CTA is
   7.5px / .1em (RdButton's label is 12.5px / .14em). Using the real primitives
   puts a 30px heading and a 52px button inside a 148px box (spec §7.2).
   They pair with `.rd-disp`, which supplies family + the uppercase transform. */
.rd-swatch__mini-title {
  font-weight: 900;
  font-stretch: 62%;
  letter-spacing: 0;
  font-size: 15px;
  line-height: .95;
  color: var(--rd-fg);
}

/* ⚠ THE LOAD-BEARING PIXEL — see the header. --rd-acc (lime dark / olive light)
   against the CTA's theme-invariant --rd-volt directly below it.

   §P2 RESOLVED 2026-08-04, and this was the one site the conversion missed:
   `.rd-num` went to Archivo 800 / width 75 / tabular while this stayed on the
   system monospace stack at weight 700. The kit draws the swatch score with the
   same shared `mono` role as every other number — "not a system mono. Numbers
   are the loudest thing on a scoring surface, so they speak in the display
   voice." Colour stays --rd-acc: this is text, and text takes acc, not volt. */
.rd-swatch__mini-score {
  font-family: 'Archivo', system-ui, sans-serif;
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  font-stretch: 75%;
  font-size: 21px;
  line-height: 1;
  color: var(--rd-acc);
}

/* Three rules standing in for body copy. Widths are fixed picture geometry, so
   they are modifier classes rather than inline styles — inline style is for
   genuinely dynamic numbers only. */
.rd-swatch__mini-bars {
  display: grid;
  gap: 4px;
  margin-top: 2px;
}
.rd-swatch__mini-bar {
  height: 3px;
  background: var(--rd-line);
}
.rd-swatch__mini-bar--full { width: 100%; }
.rd-swatch__mini-bar--mid  { width: 74%; }
.rd-swatch__mini-bar--short { width: 50%; }

/* Pinned to the bottom of the tile by `margin-top: auto`. --rd-volt as a FILL
   with --rd-on-volt (black in both themes) on it: never white text on volt. */
.rd-swatch__mini-cta {
  margin-top: auto;
  height: 22px;
  background: var(--rd-volt);
  color: var(--rd-on-volt);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-stretch: 78%;
  letter-spacing: .1em;
  font-size: 7.5px;
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%);
}

/* ---- back to the ACTIVE theme -------------------------------------------- */

.rd-swatch__opt {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
}

/* border-radius: 50% is a KNOWN, ROUTED EXCEPTION to Foundations' "no
   border-radius anywhere in this system" (designer question 9). All 13
   border-radius uses in the corpus are circles or pills — avatars, dots, step
   marks, toggles, this radio — and NOT ONE is a rounded rectangle, so the rule
   most likely means "no rounded rects; the chamfer is the only non-square box
   geometry". That reading is ours and is not adopted: implemented as drawn,
   routed for a ruling. The source writes radius 7 on a 13px box and 4 on a 6px
   box — both "at least half", i.e. the intent is circular, so 50% is the
   faithful transcription and 7/4 are not.
   --rd-fg3 here, NOT Foundations' superseded #5E5E5E (spec §5 R2). */
.rd-swatch__radio {
  width: 13px;
  height: 13px;
  box-sizing: border-box;
  border: 1.5px solid var(--rd-fg3);
  border-radius: 50%;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
.rd-swatch__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--rd-acc);              /* a 6px mark reads as a stroke, not an
                                             area -> acc, not volt (spec §5 R1) */
}

/* --rd-fg2, NOT Foundations' superseded #9A9A9A (spec §5 R4). The kit's pairing
   is preserved: the ring takes the tertiary grey, the label the secondary one —
   the hierarchy comes from the step between them. */
.rd-swatch__name {
  font-weight: 800;
  font-stretch: 78%;
  letter-spacing: .1em;
  font-size: 10px;
  color: var(--rd-fg2);
}

/* ---- selected ------------------------------------------------------------ */

/* The tile stroke. Reaching into `.rd-box` from here is deliberate and is the
   one place this component overrides the primitive it composes: RdChamferBox's
   `Stroke` enum has no volt variant BY DESIGN (substrate §1.1 — a stroke is
   acc, never volt), and the selected swatch is the corpus's one volt stroke,
   which the spec is explicit must not be "fixed" to acc: volt is a fill here
   and is #CEFF51 in both themes, so both tiles get a lime selection band
   whichever theme they preview. Confined to this component rather than widening
   the shared enum, which would legalise a volt stroke everywhere. Routed.
   Unselected needs no rule: `Stroke::Line` already resolves --rd-line in the
   ACTIVE scope, which is what the frozen #2A2A2A should have been (§5 R5). */
.rd-swatch--sel .rd-box { background: var(--rd-volt); }
.rd-swatch--sel .rd-swatch__radio { border-color: var(--rd-acc); }
.rd-swatch--sel .rd-swatch__name { color: var(--rd-fg); }   /* not '#fff' (§5 R3) */

/* ---- hover / pressed: PROVISIONAL, pending Claude Design ------------------ */

/* The kit specifies neither — the frames are iOS and have no hover. Proposal:
   on an UNSELECTED option lift the stroke --rd-line -> --rd-fg3 and the label
   --rd-fg2 -> --rd-fg, leaving the border WIDTH at 1 so the tile does not
   shift; selected shows no hover change. Nothing inside the tile reacts: the
   miniature is a picture of another theme and must not answer the pointer —
   which is why no rule below crosses into `.rd-swatch__mini`. */
.rd-swatch:not(.rd-swatch--sel):hover .rd-box { background: var(--rd-fg3); }
.rd-swatch:not(.rd-swatch--sel):hover .rd-swatch__name { color: var(--rd-fg); }
/* Pressed borrows the hover treatment: on touch there is no hover at all, so
   :active is the only pointer feedback this control ever gets. Fill changes
   only — no transform, no scale: a scale on a chamfered box makes the diagonal
   crawl against the pixel grid, and the system has no motion vocabulary (zero
   `transition` in the corpus). */
.rd-swatch:not(.rd-swatch--sel):active .rd-box { background: var(--rd-fg3); }
.rd-swatch:not(.rd-swatch--sel):active .rd-swatch__radio { border-color: var(--rd-acc); }

/* Disabled: N/A by design — both themes are always available, the control has
   no disabled state, and one must not be invented. Loading / error / empty:
   N/A — the choice is local and synchronous, there is nothing to await. */
