# Align the year-row timelines in the Overview tab ## Context In the Dashboard Overview tab each year is a `
` whose `` is its **own** grid: `grid-template-columns: 3.5rem minmax(0, 1fr) auto var(--icon-sm)` (`frontend/src/styles/components.css:225`). Because every row is an independent grid, the `auto` figures column is sized per row by that row's own text — "· expiring 5", a wider `saved` number, a longer parent name, a 3-digit vs 2-digit PL value. The `minmax(0, 1fr)` track column absorbs the difference, so the timeline bars end at a different x per year (see `timeline-offset.png`: 2029's track is clearly shortest, 2025 the longest). The figures columns are ragged for the same reason. Goal: every year's timeline track starts and ends at the same x, and the figure columns line up down the list — without hardcoding a pixel width that breaks in Swedish (sv strings are longer than en) or when a number gains a digit. ## Approach — one shared grid via `subgrid` Promote the column definition from each summary to the `.year-rows` list and have the rows opt into it with `grid-template-columns: subgrid`. Tracks are then sized once, by the widest content in the whole list, and every row inherits identical column widths. Content still sizes the columns, so it stays bilingual-safe. Split the figures cell into its three real columns (name | PL left | vacation group) at the list level too, so the numbers align vertically across years as well. **CSS only — no TSX changes.** The existing markup already has the right elements (`.year-rows` → `.year-row` (`details`) → `.year-row__summary` / `.year-row__body`, `.year-row__figures` → `.year-row__parent { display: contents }`). ### Files - `frontend/src/styles/components.css` — `.year-rows` (162), `.year-row__summary` (225), `.year-row__summary::after` (238), `.year-row__year` (244), `.year-row__track` (252), `.year-row__figures` (289), `.year-row__body` (322) - `frontend/src/styles/responsive.css` — mobile block at 93–120 (must reset subgrid) ### Shape `.year-rows` becomes the grid owner: ```css .year-rows { display: grid; /* year | track | name | PL left | vacation group | chevron */ grid-template-columns: 3.5rem minmax(0, 1fr) auto auto auto var(--icon-sm); } ``` Each row spans it and re-exposes it: ```css .year-row { /* the
*/ grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; } .year-row__summary { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; /* keep: align-items, column-gap, row-gap, padding, font-weight */ } .year-row__body { grid-column: 1 / -1; } ``` Column placement inside the summary shifts to match the six tracks: year `1`, track `2`, `.year-row__figures` `grid-column: 3 / 6` + `grid-template-columns: subgrid` (keep `display: grid`, `align-items: baseline`, `white-space: nowrap`; drop its own `auto auto auto`), chevron `::after` `grid-column: 6`. `.year-row__parent { display: contents }` stays — each parent's three spans land in the three shared columns. Notes: - `--sp-4` column-gap must be declared on `.year-rows` as well, since a subgrid inherits the parent's gaps unless it sets its own; keep the summary's `column-gap` so the values agree. - `.year-rows > .disclosure:last-child { border-bottom: none }` and the `.disclosure` hairlines in `primitives.css` are unaffected (borders on the `details` still paint). - In the solo case `.year-row__name` isn't rendered; that column collapses to `0` for every row — still uniform. - Left-over `grid-row: 1` declarations on year/track/figures stay valid. ### Fallback Wrap the promotion in a support check so older engines keep today's (merely ragged) layout rather than collapsing: ```css @supports (grid-template-columns: subgrid) { /* the rules above */ } ``` Keep the current `.year-row__summary` rule as the base; the `@supports` block overrides it. ### Mobile `responsive.css` already re-lays the summary as `3rem minmax(0, 1fr) var(--icon-sm)` with the figures wrapped to row 2 as a flex column. Inside that media query, reset the new rules: `.year-rows { display: flex; flex-direction: column }` and `.year-row { display: block }` — so the mobile block's existing declarations apply unchanged. Place these resets after the `@supports` block in source order, or repeat them inside their own `@supports` guard, so specificity/order lets mobile win. ## Verification 1. `cd frontend && npm run dev`, open the Dashboard Overview tab with the demo workspace (`demo@example.com`) — delegate the browser pass to a Sonnet subagent (never drive Chrome from the coordinator). 2. Check: every year's track left and right edge align to the pixel, including 2029 (shortest figures) vs 2025 (no expiring) vs a row with `expiring`; the "PL left" and "Vacation left" labels form straight columns. 3. Repeat in **both** Swedish and English (longer sv strings are the real stress test), and in a solo workspace (no parent names) as well as a couple workspace. 4. Narrow to the mobile breakpoint and confirm figures still wrap under the track, one parent per line (use the same-origin iframe trick — `resize_window` is a no-op in this setup). 5. Gates (delegate to a Haiku subagent, raw output): from `frontend/` `npm run typecheck`, `npm run lint`, `npm run coverage`. CSS-only change — `YearRows.test.tsx` and `Dashboard.test.tsx` should be untouched and green.