Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/sync-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,29 @@ docs:
target: docs/SETUP_CHECKLIST.md
description: "Consumer repo setup checklist. Includes the default_workflow_permissions=write step in section 3.3.1 that prevents a Gate startup_failure on fresh consumers (see #2157). Synced so it no longer drifts across the fleet."

# ---- Shared design system (fleet UX consistency) ----
# CSS tokens/components + a Streamlit adapter kit + the presentation-patterns
# standard, synced into each consumer's design-system/. Apps consume a PATTERN
# (theme / empty-state / error / dev-notice / availability-badge / no-raw-ids)
# rather than inventing bespoke fixes. Per-app overrides go in an app stylesheet
# loaded AFTER tokens.css; never fork components.css. Grouped here with the other
# synced reference assets (cf. the run-contract JSON schemas above).
- source: design-system/tokens.css
target: design-system/tokens.css
description: "Design-system theme tokens (theme-air/theme-paper + density axis). Override tokens in an app stylesheet loaded after this; do not fork."
- source: design-system/components.css
target: design-system/components.css
description: "Design-system component layer (.ds): panels/appbar/kpis/tables/fields + presentation-state patterns (.notice, .empty-state, .badge, .skeleton)."
- source: design-system/ds_streamlit.py
target: design-system/ds_streamlit.py
Comment on lines +822 to +823

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the Streamlit adapter importable from its synced path

This syncs the adapter into design-system/ds_streamlit.py, but the new usage examples tell Streamlit apps to run from ds_streamlit import .... From a consumer repo root, Python will not find that module unless every app first mutates sys.path, and design-system cannot be used in a normal dotted import because of the hyphen. Any per-app adoption that follows the shipped docs will fail with ModuleNotFoundError before reaching the helpers; sync it to an importable package/path or include a root shim/loader.

Useful? React with 👍 / 👎.

description: "Streamlit adapter for the design system (inject_theme/empty_state/notice/error/translate_error/dev_note/availability_badge/humanize_id) so Streamlit apps consume the same presentation patterns."
- source: design-system/PRESENTATION_PATTERNS.md
target: design-system/PRESENTATION_PATTERNS.md
description: "The presentation-patterns standard (P1-P6): the rule behind each pattern + per-app-type application + the finding->pattern map from the 2026-06 UX-Review fleet baseline."
- source: design-system/README.md
target: design-system/README.md
description: "Design-system usage guide: how to apply the theme + components/kit per app type."

# Issue templates synced to consumer repos
issue_templates:
- source: .github/ISSUE_TEMPLATE/agent_task.yml
Expand Down
99 changes: 99 additions & 0 deletions templates/consumer-repo/design-system/PRESENTATION_PATTERNS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Presentation Patterns — the fleet design-system rollout standard

The cross-repo UX-Review baseline (2026-06-22, 8 apps) found that **every app's engine works; the
failures are presentation + deployment.** Deployment is a separate program. This doc covers the
**presentation** class: it collapses the ~12 recurring presentation findings into **6 reusable
patterns**, each with a rule, a design-system component (`components.css` / `tokens.css`), a
per-app-type application (Streamlit / web-SPA / static-PWA), and the fleet findings it closes.

**Principle:** apps should consume a *pattern*, not invent a bespoke fix. One pattern, applied
everywhere, beats N one-off PRs — and it's why this is a design-system rollout, not a bug list.

---

## The 6 patterns

### P1 — Light, understated theme by default

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add blank lines around headings to comply with Markdown style rules (MD022).

All section headings require at least one blank line before and after to improve readability and comply with markdownlint standards. Apply the following structure before each ### Heading:

---

### P1 — Light, understated theme by default

This affects 8 headings: lines 16, 22, 30, 39, 46, 54, 77, and 88.

Also applies to: 22-22, 30-30, 39-39, 46-46, 54-54, 77-77, 88-88

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 16-16: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@templates/consumer-repo/design-system/PRESENTATION_PATTERNS.md` at line 16,
The Markdown file violates the MD022 style rule which requires blank lines
before and after section headings for improved readability. Add one blank line
before each of the eight section headings (at lines 16, 22, 30, 39, 46, 54, 77,
and 88) and one blank line after each heading to ensure proper formatting
compliance. This applies to all ### level headings throughout the document
including "P1 — Light, understated theme by default" and subsequent section
headings.

Source: Linters/SAST tools

**Rule:** never ship a default dark theme; default to `theme-air` (Ink & Air). Dark is opt-in only.
- **web:** load `tokens.css` + `components.css`; root element `class="ds theme-air"` (`theme-paper` for friendlier apps).
- **Streamlit:** `.streamlit/config.toml` `[theme] base="light"` + the shared `ds_streamlit.inject_theme()` (maps the `theme-air` tokens to Streamlit's theme). *Already prototyped for TMP.*
- **Fixes:** default-dark on Trend_Model_Project / Portable-Alpha / Manager-Database / Inv-Man-Intake.

### P2 — Empty state = title + reason + next action
**Rule:** a "no data yet" surface ALWAYS shows a title, a one-line reason, and a **next-action CTA**.
NEVER a bare prompt sitting above already-rendered content, and NEVER an internal filename/path.
- **web:** `.ds .empty-state` (`.es-icon/.es-title/.es-desc/.es-cta`).
- **Streamlit:** `ds_streamlit.empty_state(title, desc, cta_label, on_click)`.
- **Fixes:** TMP #5628 (Run-analysis CTA above results), PA #2021 (Results "Outputs.xlsx not found"),
Manager-Database #1214 (empty default date / "Recent Activity"), LMS #351 (root `/` JSON 404).

### P3 — Errors are human messages + remediation
**Rule:** NEVER surface a raw exception, pydantic/validation dump, internal field name, or stack to a
user. Translate at the boundary to a plain message + a recovery action.
- **web:** `.ds .notice--error` (`.ic` + `.body strong` headline + `.body .act` remediation).
- **Streamlit:** `ds_streamlit.error(message, remediation)` backed by `translate_error(exc)` (maps
known fields, e.g. `financing_mode` → "Financing mode is required").
- **Fixes:** PA #2021 (raw `ModelConfig financing_mode` / margin pydantic errors), Inv-Man-Intake (item_id),
the broad "raw error" class across the fleet.

### P4 — Dev/diagnostic notices go to logs, not the UI
**Rule:** auth-bypass / trace-sink / observability / "dev mode" notices NEVER render in the main
content. Use logging, or at most a collapsed "Diagnostics" expander.
- **web:** n/a in the main flow; use a `<details>`/debug panel.
- **Streamlit:** `ds_streamlit.dev_note(msg)` → `logging` (not `st.warning`/`st.write`); diagnostics behind `st.expander("Diagnostics")`.
- **Fixes:** Manager-Database #1215 (auth-bypass `st.warning`), Inv-Man-Intake #630 (trace-sink banner).

### P5 — Feature-availability markers (no silent dead-ends)
**Rule:** a tab/control that isn't applicable in the current mode states so up front (a badge/label),
rather than opening into a silent empty/disabled surface.
- **web:** `.ds .badge` on the tab/control (e.g. "multi-period only", "needs setup").
- **Streamlit:** `ds_streamlit.availability_badge(label)` in the tab title / disabled control caption.
- **Fixes:** TMP #5629 (4/6 Results tabs empty — fixed by labelling, the canonical example), PA #2026
(upload-only pages with no sample path → mark/offer the sample).

### P6 — No raw internal identifiers in user surfaces
**Rule:** decode internal IDs / fixture filenames / record keys into human-readable labels before display.
- **web/Streamlit:** a display-name mapping; keep the raw id as secondary/`title=` metadata if useful.
- **Fixes:** Inv-Man-Intake #629 (opaque `item_id`), #630 (raw fixture filename in selector), PA Results (`Outputs.xlsx`).

---

## Finding → pattern map (presentation class)

| Finding | Pattern(s) |
|---|---|
| TMP #5628 empty-state CTA above results | P2 |
| TMP #5629 unmarked empty tabs *(fixed)* | P5 |
| Manager-Database #1214 empty default views | P2 |
| Manager-Database #1215 auth notice in UI | P4 |
| Inv-Man-Intake #629 non-actionable raw-JSON queue | P6 (+ a real action, app-specific) |
| Inv-Man-Intake #630 trace-sink + raw filenames | P4, P6 |
| Portable-Alpha #2021 raw errors / empty states | P2, P3 |
| Portable-Alpha #2026 upload-only, no sample | P5 |
| Pension-Data #594 *(deployment program — offline)* | — |
| LMS #351 root JSON 404 / empty surfaces | P2 (+ deployment) |
| Default dark theme (TMP/PA/MD/IMI) | P1 |

## Streamlit design kit (most of the fleet is Streamlit)
The CSS components above cover the web apps (Pension-Data, trip-planner, LMS). The four Streamlit
Tier-A apps need a Streamlit-native equivalent — ship a shared `ds_streamlit.py` alongside the CSS:
- `inject_theme()` — applies the `theme-air` palette (P1); pairs with `.streamlit/config.toml`.
- `empty_state(title, desc, cta_label=None, on_click=None)` (P2)
- `notice(kind, title, body, action=None)` and `error(message, remediation=None)` + `translate_error(exc)` (P3)
- `dev_note(msg)` → logging; `diagnostics_expander()` (P4)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clarify or implement diagnostics_expander().

Line 83 lists diagnostics_expander() as part of the Streamlit design kit (P4), but this function does not exist in ds_streamlit.py. The file only includes dev_note(msg) for P4.

Either:

  1. Add diagnostics_expander() to ds_streamlit.py with an implementation that wraps dev messages in a Streamlit expander, or
  2. Remove the reference and clarify that P4 is handled via dev_note() + app-level st.expander("Diagnostics").

The pattern line in the doc (line 43) notes the approach correctly, so the inventory list on line 83 may simply be aspirational/outdated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@templates/consumer-repo/design-system/PRESENTATION_PATTERNS.md` at line 83,
The documentation references a `diagnostics_expander()` function as part of the
P4 pattern in the Streamlit design kit inventory, but this function does not
exist in the actual implementation file. Either add the `diagnostics_expander()`
function to the `ds_streamlit.py` module with an implementation that wraps
diagnostic messages in a Streamlit expander component, or remove the reference
to `diagnostics_expander()` from the pattern inventory and clarify that P4
diagnostics are handled exclusively through the existing `dev_note()` function
combined with application-level `st.expander("Diagnostics")` calls as described
in the pattern section.

- `availability_badge(label)` (P5)
- `humanize_id(raw, mapping)` (P6)
Graduate this kit + the CSS into `Workflows/templates/consumer-repo/design-system/` so maint-68 syncs it fleet-wide.

## Rollout sequence
1. **Graduate** `tokens.css` + `components.css` (with the new patterns) + `ds_streamlit.py` into the Workflows consumer-repo design-system; let the existing sync (maint-68) distribute it.
2. **Apply per app, highest-ROI first** — close each open finding by adopting its pattern (not a bespoke fix), one small PR per app with the pattern's named test gate:
- TMP #5628 → P2 (closest to a clean pass)
- Manager-Database #1214 → P2, #1215 → P4
- Inv-Man-Intake #630 → P4/P6 (#629 also needs a real queue action)
- Portable-Alpha #2021 → P2/P3, #2026 → P5
- Pension-Data / LMS web surfaces → P2/P3 via the CSS components
3. **Theme pass (P1)** across the Streamlit apps once the kit is synced.

_Authored 2026-06-22 from the UX-Review fleet baseline. Components live in `components.css`/`tokens.css`
(this dir); see each repo's `docs/ux-review/REVIEW_LOG.md` for its findings + scores._
51 changes: 51 additions & 0 deletions templates/consumer-repo/design-system/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Shared Design System (staging)

Two themes on one token foundation, plus an orthogonal density axis.

- **`theme-air`** (Ink & Air) — default; work / outward-facing tools.
- **`theme-paper`** (Warm Paper) — friendlier apps (Reader, LMS).
- **`density-compact`** — tightens spacing for data-dense screens (e.g. Trend tables) **without** changing the theme.

## Use

```html
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="components.css">

<body class="theme-air"> <!-- or theme-paper; add density-compact for dense screens -->
<div class="ds">
...components (.panel, .kpi, .appbar, table, .btns, .callout, ...)...
</div>
</body>
```

- Components are scoped under `.ds` so they never leak into a host app's styles.
- `components.css` is theme-agnostic — it only reads tokens. Don't fork it.

## Per-app customization

Override any token in an app stylesheet loaded **after** `tokens.css`:

```css
.theme-air { --accent: #0f6f6a; } /* this app wants a teal accent */
```

That's the "default with per-app customization" model: the base is canonical; an app changes only the tokens it needs.

## Files

| File | Role |
|---|---|
| `tokens.css` | Variables — themes + density. **Source of truth for the look.** |
| `components.css` | Component styles, token-driven and theme-agnostic. |
| `index.html` | Link-based reference page. Open locally, or import into Claude Design. |
| `preview.html` | Generated self-contained snapshot for sharing (regenerate with `build_preview.py`). |

## Theme → app mapping (proposed)

- **Ink & Air:** Trend_Model_Project, Counter_Risk, Manager-Database, Inv-Man-Intake, Pension-Data, Portable-Alpha-Extension-Model, trip-planner, Travel-Plan-Permission. Use `density-compact` on Trend/Counter_Risk data-dense screens.
- **Warm Paper:** Reader, learning-management-system.

## Status & next step

**STAGED** here in the tracker. Graduates to `Workflows/templates/consumer-repo/design-system/` with a `sync-manifest.yml` entry (`is_directory: true`; base tokens `template_sync: exact`) as a deliberate Workflows PR — see [`../PLAN.md`](../PLAN.md) §3.3. Claude Design imports from the Workflows path once graduated, enabling the prototype-in-Design → land-in-repo round-trip.
95 changes: 95 additions & 0 deletions templates/consumer-repo/design-system/components.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* components.css — component layer. Token-driven and theme-AGNOSTIC.
* Requires tokens.css + a theme class (.theme-air | .theme-paper) on an ancestor.
* Everything is scoped under .ds so it never leaks into a host app's own styles.
*/
.ds { color:var(--text); font-family:var(--font-body); font-size:var(--fs-base); line-height:1.5; background:var(--bg); }
.ds *, .ds *::before, .ds *::after { box-sizing:border-box; }
.ds h3 { font-family:var(--font-heading); font-weight:var(--heading-weight); margin:0 0 var(--space-3); font-size:15px; letter-spacing:-.005em; }
.ds .sub { color:var(--muted); font-size:var(--fs-sm); margin:calc(-1 * var(--space-2)) 0 var(--space-3); }

.ds .panel { background:var(--surface); border:var(--card-border); border-radius:var(--radius); box-shadow:var(--shadow); padding:var(--card-pad); margin-bottom:var(--space-4); }

.ds .appbar { display:flex; align-items:center; gap:var(--space-4); padding:var(--control-pad-y) var(--card-pad); background:var(--surface); border:var(--card-border); border-radius:var(--radius); box-shadow:var(--shadow); margin-bottom:var(--space-4); }
.ds .appbar .brand { font-family:var(--font-heading); font-weight:700; font-size:15px; }
.ds .appbar nav { display:flex; gap:var(--space-1); flex-wrap:wrap; }
.ds .appbar nav a { font-size:var(--fs-sm); text-decoration:none; color:var(--muted); padding:5px 10px; border-radius:var(--radius-sm); cursor:pointer; }
.ds .appbar nav a.active { color:var(--accent); background:var(--accent-weak); font-weight:600; }
.ds .appbar .spacer { flex:1; }

.ds .kpis { display:flex; flex-wrap:wrap; gap:var(--space-3); margin-bottom:var(--space-4); }
.ds .kpi { flex:1; min-width:150px; background:var(--surface); border:var(--card-border); border-radius:var(--radius); box-shadow:var(--shadow); padding:var(--space-3) var(--space-4); }
.ds .kpi .label { font-size:var(--fs-sm); color:var(--muted); }
.ds .kpi .val { font-size:var(--kpi-size); font-weight:700; letter-spacing:-.02em; margin-top:4px; font-family:var(--font-heading); }
.ds .kpi .delta { font-size:var(--fs-sm); font-weight:600; margin-top:2px; }
.ds .up { color:var(--pos); } .ds .down { color:var(--neg); }

.ds .grid2 { display:grid; grid-template-columns:1.3fr 1fr; gap:var(--space-4); }
@media (max-width:720px){ .ds .grid2 { grid-template-columns:1fr; } }

.ds table { width:100%; border-collapse:collapse; font-size:var(--fs-sm); }
.ds thead th { text-align:left; color:var(--muted); font-weight:600; font-size:var(--fs-xs); text-transform:uppercase; letter-spacing:.04em; padding:var(--row-pad-y) var(--row-pad-x); border-bottom:1px solid var(--border); }
.ds tbody td { padding:var(--row-pad-y) var(--row-pad-x); border-bottom:1px solid var(--row-border); }
.ds tbody tr:nth-child(even) { background:var(--zebra); }
.ds td.num, .ds th.num { text-align:right; font-variant-numeric:tabular-nums; }

.ds .field { margin-bottom:var(--space-3); }
.ds label { display:block; font-size:var(--fs-sm); font-weight:600; margin-bottom:5px; }
.ds label .help { font-weight:400; color:var(--muted); }
.ds .q { display:inline-flex; width:15px; height:15px; border-radius:50%; border:1px solid var(--border); color:var(--muted); font-size:10px; align-items:center; justify-content:center; vertical-align:1px; margin-left:5px; cursor:help; }
.ds input[type=text], .ds select { width:100%; font:inherit; font-size:var(--fs-base); padding:var(--control-pad-y) var(--control-pad-x); color:var(--text); background:var(--input-bg); border:1px solid var(--border); border-radius:var(--input-radius); }
.ds input[type=range] { width:100%; accent-color:var(--accent); }

.ds .btns { display:flex; flex-wrap:wrap; gap:var(--space-2); margin-top:var(--space-1); }
.ds button { font:inherit; font-size:var(--fs-sm); font-weight:600; padding:var(--control-pad-y) 15px; border-radius:var(--btn-radius); cursor:pointer; border:1px solid transparent; }
.ds .b-primary { background:var(--btn-primary-bg); color:var(--btn-primary-contrast); border-color:var(--btn-primary-bg); }
.ds .b-secondary { background:var(--surface); color:var(--text); border:1px solid var(--border); }
.ds .b-ghost { background:transparent; color:var(--accent); }
.ds .b-danger { background:transparent; color:var(--neg); border:1px solid var(--neg-border); }
.ds button[disabled] { opacity:.45; cursor:not-allowed; }

.ds .callout { display:flex; gap:10px; padding:11px 13px; border-radius:var(--radius); background:var(--accent-weak); border:1px solid var(--accent-line); font-size:var(--fs-sm); }
.ds .callout .ic { color:var(--accent); font-weight:700; }
.ds .empty { text-align:center; color:var(--muted); border:1px dashed var(--border); border-radius:var(--radius); padding:20px; font-size:var(--fs-sm); }
.ds .chart { color:var(--accent); display:block; }

/* ============================================================================
* PRESENTATION-STATE PATTERNS (added 2026-06-22 from the cross-fleet UX-Review
* baseline). These standardize the recurring failure classes: empty/dead-end
* views, raw errors, dev/diagnostic leaks, and unavailable features.
* See PRESENTATION_PATTERNS.md for the rule behind each + per-app-type usage.
* ========================================================================== */

/* notice — the ONE container for user-facing messages. Never render a raw
* exception / internal field name / filename / CLI flag; translate to a human
* message + remediation and put it here. Variants by semantics. */
.ds .notice { display:flex; gap:10px; align-items:flex-start; padding:11px 13px; border-radius:var(--radius); font-size:var(--fs-sm); border:1px solid var(--border); background:var(--panel); }
.ds .notice .ic { font-weight:700; flex:none; line-height:1.4; }
.ds .notice .body { flex:1; }
.ds .notice .body strong { display:block; margin-bottom:2px; }
.ds .notice .body .act { margin-top:6px; } /* remediation link/button slot */
.ds .notice--info { background:var(--info-weak); border-color:var(--accent-line); }
.ds .notice--info .ic { color:var(--info); }
.ds .notice--warn { background:var(--warn-weak); border-color:var(--warn); }
.ds .notice--warn .ic { color:var(--warn); }
.ds .notice--error { background:var(--neg-weak); border-color:var(--neg-border); }
.ds .notice--error .ic { color:var(--neg); }
.ds .notice--ok { background:var(--pos-weak); border-color:var(--pos); }
.ds .notice--ok .ic { color:var(--pos); }

/* empty-state — for "no data yet" surfaces. ALWAYS a title + one-line reason +
* a next-action; NEVER a bare prompt sitting above already-rendered results,
* and NEVER an internal filename/path. */
.ds .empty-state { text-align:center; border:1px dashed var(--border); border-radius:var(--radius); padding:28px 20px; }
.ds .empty-state .es-icon { font-size:22px; opacity:.6; }
.ds .empty-state .es-title { font-family:var(--font-heading); font-weight:var(--heading-weight); font-size:15px; margin:8px 0 4px; }
.ds .empty-state .es-desc { color:var(--muted); font-size:var(--fs-sm); max-width:42ch; margin:0 auto 12px; }
.ds .empty-state .es-cta { display:inline-flex; gap:8px; }

/* badge — small availability/status marker (e.g. "multi-period only",
* "needs setup") so a tab/control states its applicability up front. */
.ds .badge { display:inline-block; font-size:var(--fs-xs); font-weight:600; padding:1px 7px; border-radius:999px; background:var(--panel); color:var(--muted); border:1px solid var(--border); vertical-align:middle; }
.ds .badge--muted { opacity:.8; }

/* skeleton — loading placeholder so a pending surface never looks empty/broken. */
.ds .skeleton { background:linear-gradient(90deg,var(--panel) 25%,var(--border) 37%,var(--panel) 63%); background-size:400% 100%; border-radius:var(--radius-sm); animation:ds-shimmer 1.3s ease infinite; min-height:14px; }
@keyframes ds-shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
Comment on lines +94 to +95

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add reduced-motion handling for shimmer skeletons.

Line 94–95 animates indefinitely with no prefers-reduced-motion fallback. Add a media query to disable shimmer for users who request reduced motion.

Proposed patch
 .ds .skeleton { background:linear-gradient(90deg,var(--panel) 25%,var(--border) 37%,var(--panel) 63%); background-size:400% 100%; border-radius:var(--radius-sm); animation:ds-shimmer 1.3s ease infinite; min-height:14px; }
 `@keyframes` ds-shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
+@media (prefers-reduced-motion: reduce) {
+  .ds .skeleton { animation: none; background-position: 0 0; }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.ds .skeleton { background:linear-gradient(90deg,var(--panel) 25%,var(--border) 37%,var(--panel) 63%); background-size:400% 100%; border-radius:var(--radius-sm); animation:ds-shimmer 1.3s ease infinite; min-height:14px; }
@keyframes ds-shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
.ds .skeleton { background:linear-gradient(90deg,var(--panel) 25%,var(--border) 37%,var(--panel) 63%); background-size:400% 100%; border-radius:var(--radius-sm); animation:ds-shimmer 1.3s ease infinite; min-height:14px; }
`@keyframes` ds-shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
`@media` (prefers-reduced-motion: reduce) {
.ds .skeleton { animation: none; background-position: 0 0; }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@templates/consumer-repo/design-system/components.css` around lines 94 - 95,
The .ds .skeleton class currently applies the ds-shimmer animation indefinitely
without respecting user accessibility preferences. Add a `@media`
(prefers-reduced-motion: reduce) query after the existing animation rules that
targets the .ds .skeleton selector and disables the animation by setting it to
none for users who have requested reduced motion in their system settings.

Loading
Loading