{
  "initial_pr": {
    "number": 496,
    "title": "Task: desktop-field-overlap",
    "body": "## Spec\nSpec-ID: desktop-field-overlap\n[specs/tasks/desktop-field-overlap.md](specs/tasks/desktop-field-overlap.md)\n\n## Summary\nTask: desktop-field-overlap\n\n## Review\nReview-Owner: local\n\n## Acceptance Criteria\n- [ ] No checklist items found in spec.\n\n## Known Issues\nNone.\n",
    "created_at": "2026-03-30T21:31:12Z",
    "merged_at": "2026-03-30T21:59:30Z",
    "merge_commit": "ae8c24ee788c202c4d7cd7a03246b74241ac39e7",
    "additions": 185,
    "deletions": 0,
    "changed_files": 3,
    "files": [
      {
        "path": "frontend/e2e/ui-contract/desktop-field-overlap.spec.ts",
        "additions": 94,
        "deletions": 0,
        "changeType": "ADDED"
      },
      {
        "path": "frontend/src/styles.css",
        "additions": 15,
        "deletions": 0,
        "changeType": "MODIFIED"
      },
      {
        "path": "specs/tasks/desktop-field-overlap.md",
        "additions": 76,
        "deletions": 0,
        "changeType": "ADDED"
      }
    ],
    "repository_visibility": "private"
  },
  "extended_pr": {
    "number": 748,
    "title": "Task: age-row-input-overflow-fix",
    "body": "## Spec\nSpec-ID: age-row-input-overflow-fix\n[specs/tasks/age-row-input-overflow-fix.md](specs/tasks/age-row-input-overflow-fix.md)\n\n## Summary\nTask: age-row-input-overflow-fix\n\n## Review\nReview-Owner: local\n\n## Acceptance Criteria\n- [ ] No checklist items found in spec.\n\n## Known Issues\nNone.\n",
    "created_at": "2026-05-05T05:58:45Z",
    "merged_at": "2026-05-05T06:39:39Z",
    "merge_commit": "78df25cb51e53a69548139c66c79b7e0130f7795",
    "additions": 128,
    "deletions": 7,
    "changed_files": 4,
    "files": [
      {
        "path": "frontend/e2e/ui-contract/desktop-field-overlap.spec.ts",
        "additions": 58,
        "deletions": 5,
        "changeType": "MODIFIED"
      },
      {
        "path": "frontend/e2e/ui-contract/relocation-editor.spec.ts",
        "additions": 10,
        "deletions": 2,
        "changeType": "MODIFIED"
      },
      {
        "path": "frontend/src/styles.css",
        "additions": 1,
        "deletions": 0,
        "changeType": "MODIFIED"
      },
      {
        "path": "specs/tasks/age-row-input-overflow-fix.md",
        "additions": 59,
        "deletions": 0,
        "changeType": "ADDED"
      }
    ],
    "repository_visibility": "private"
  },
  "spec": {
    "path": "specs/tasks/desktop-field-overlap.md",
    "commit": "ae8c24ee788c202c4d7cd7a03246b74241ac39e7",
    "text": "---\nid: desktop-field-overlap\nstatus: task\ndepends_on: []\ndescription: Fix age/life-expectancy label overlap on desktop and add general field overlap/overflow e2e test\n---\n\n## Goal\n\nThe age and life-expectancy fields overlap inside the slide-over panel on desktop\nviewports. The mobile fix (`flex-direction: column` at `max-width: 768px`) doesn't\nhelp because the viewport is wide \u2014 only the container is narrow (\u2264560px). Fix the\nCSS so these fields never overlap regardless of viewport width, and add a general\ne2e test that catches field overlap and overflow on desktop so this class of bug\ndoesn't recur.\n\n## CSS Fix\n\nThe `.age-life-expectancy-row` lives exclusively inside `.slide-over-body`\n(confirmed \u2014 no drawer or other container uses it). The slide-over is capped at\n`min(560px, calc(100vw - 2rem))`.\n\nPreferred approach \u2014 **container query**:\n\n1. Add `container-type: inline-size` to `.slide-over-body` (or `.slide-over`).\n2. Add a `@container` rule that switches `.age-life-expectancy-row` to\n   `flex-direction: column` when the container is narrow enough that the labels\n   would overlap (experiment to find the right threshold \u2014 likely \u2264500px).\n3. Keep the existing `@media (max-width: 768px)` rule as a fallback for browsers\n   without container query support, but the container query should be the primary\n   mechanism.\n\nIf container queries introduce unexpected side effects in the slide-over layout,\nfall back to scoping: `.slide-over-body .age-life-expectancy-row { flex-direction: column; }`\n\u2014 always stacking is acceptable since the panel is never wider than 560px.\n\nThe partner age row (second `.age-life-expectancy-row` in `ScenarioForm.tsx`) must\nalso be covered by whichever CSS rule is chosen.\n\n## General Desktop Overlap / Overflow E2E Test\n\nAdd a new desktop-viewport e2e test in `frontend/e2e/ui-contract/` that opens the\nscenario editor (slide-over) and asserts:\n\n1. **No sibling-field overlap** \u2014 for every pair of horizontally-adjacent field\n   wrappers in the form (not just age/life-expectancy), their bounding boxes must\n   not intersect. Use the `boundingBox()` pattern from the existing mobile overlap\n   test.\n2. **No field overflow** \u2014 every field wrapper's bounding box must fit within its\n   scroll container (the `.slide-over-body`). Reuse or extend\n   `expectHorizontalOverflowWithin()` from `e2e/support/helpers.ts`.\n\nThe test should run at the default desktop viewport (1280\u00d7720 or whatever Playwright\ndefaults to) \u2014 not a mobile viewport. It should catch the exact bug reported here:\nfields that look fine on mobile (because they stack) but overlap on desktop.\n\nFollow the patterns in `mobile-layout.mobile.spec.ts` and use\n`createScenario()`/`waitForSlideIn()` helpers from `e2e/support/helpers.ts`.\n\n## Affected Files\n\n- `frontend/src/styles.css` \u2014 container query + `.age-life-expectancy-row` rules\n- `frontend/e2e/ui-contract/` \u2014 new desktop overlap/overflow spec file\n\n## Verification\n\n- `make lint` passes\n- `make e2e` passes, including the new desktop overlap test\n- Visually confirm in the slide-over at default desktop viewport: age and\n  life-expectancy labels do not overlap (both primary and partner rows)\n\n## Danger Zones\n\n- Adding `container-type: inline-size` can change how percentage-based widths and\n  certain layout properties resolve inside the container. Test that the slide-over\n  body's scrolling and padding still behave correctly after the change.\n"
  },
  "initial_test": {
    "path": "frontend/e2e/ui-contract/desktop-field-overlap.spec.ts",
    "commit": "ae8c24ee788c202c4d7cd7a03246b74241ac39e7",
    "text": "import { test, expect } from '@playwright/test';\n\nimport {\n  createScenario,\n  expectHorizontalOverflowWithin,\n  waitForSlideIn,\n} from '../support/helpers';\n\ntest('fields in the slide-over do not overlap or overflow on desktop', async ({ page, request }) => {\n  test.setTimeout(60_000);\n  await createScenario(request, { labelPrefix: 'Desktop Overlap' });\n  await page.goto('/');\n  await expect(page.locator('[data-testid=\"sidebar\"]')).toBeVisible({ timeout: 15_000 });\n\n  await page.locator('[data-testid=\"new-scenario-btn\"]').click();\n  const form = page.locator('[data-testid=\"scenario-form\"]');\n  await expect(form).toBeVisible();\n  await waitForSlideIn(page);\n\n  // Add a partner so the second age/life-expectancy row is rendered\n  const addPartnerBtn = form.locator('[data-testid=\"add-partner-btn\"]');\n  if (await addPartnerBtn.isVisible()) {\n    await addPartnerBtn.click();\n  }\n\n  const slideOverBody = form.locator('.slide-over-body');\n  await expect(slideOverBody).toBeVisible();\n\n  // \u2500\u2500 No sibling-field overlap \u2500\u2500\n  // Check all multi-field row containers in the slide-over, not just\n  // age/life-expectancy rows. Each selector targets a known row-like container\n  // that lays out children horizontally.\n  const rowSelectors = [\n    '.age-life-expectancy-row',\n    '.person-labels-row',\n    '.benefit-card-fields-grid',\n    '.expense-row-bottom',\n  ];\n  const allRows = form.locator(rowSelectors.join(', '));\n  const rowCount = await allRows.count();\n  for (let r = 0; r < rowCount; r++) {\n    const row = allRows.nth(r);\n    await row.scrollIntoViewIfNeeded();\n    const children = row.locator(':scope > *');\n    const childCount = await children.count();\n    for (let i = 0; i < childCount - 1; i++) {\n      const a = await children.nth(i).boundingBox();\n      const b = await children.nth(i + 1).boundingBox();\n      if (!a || !b) continue; // hidden children\n      // Either stacked vertically (bottom of a <= top of b) or separated\n      // horizontally (right of a <= left of b). They must not intersect.\n      const horizontalOverlap = a.x < b.x + b.width && b.x < a.x + a.width;\n      const verticalOverlap = a.y < b.y + b.height && b.y < a.y + a.height;\n      expect(\n        horizontalOverlap && verticalOverlap,\n        `row ${r}: child ${i} and child ${i + 1} bounding boxes must not intersect`,\n      ).toBe(false);\n    }\n  }\n\n  // Verify at least both primary and partner age/life rows were checked\n  const ageLifeRows = form.locator('.age-life-expectancy-row');\n  expect(await ageLifeRows.count()).toBeGreaterThanOrEqual(2);\n\n  // \u2500\u2500 No field overflow \u2500\u2500\n  // Every field wrapper inside the slide-over-body must fit within it.\n  const bodyBox = await slideOverBody.boundingBox();\n  expect(bodyBox).toBeTruthy();\n\n  // Check all field sections \u2014 not just age rows \u2014 for overflow\n  const fieldSelectors = [\n    '.age-life-expectancy-row',\n    '.person-labels-row',\n    '.field-group',\n    '.benefit-card-fields-grid',\n    '.expense-row-bottom',\n  ];\n  const allFields = slideOverBody.locator(fieldSelectors.join(', '));\n  const fieldCount = await allFields.count();\n  for (let i = 0; i < fieldCount; i++) {\n    const wrapper = allFields.nth(i);\n    await wrapper.scrollIntoViewIfNeeded();\n    const wBox = await wrapper.boundingBox();\n    if (!wBox) continue; // hidden wrappers\n    // Right edge of the field must not exceed the right edge of the container\n    expect(\n      wBox.x + wBox.width,\n      `field wrapper ${i} right edge within slide-over-body`,\n    ).toBeLessThanOrEqual(bodyBox!.x + bodyBox!.width + 1); // 1px tolerance\n  }\n\n  // Slide-over body itself must not have horizontal overflow\n  await expectHorizontalOverflowWithin(slideOverBody);\n});\n"
  },
  "extended_test": {
    "path": "frontend/e2e/ui-contract/desktop-field-overlap.spec.ts",
    "commit": "78df25cb51e53a69548139c66c79b7e0130f7795",
    "text": "import { test, expect } from '@playwright/test';\n\nimport {\n  createScenario,\n  expectHorizontalOverflowWithin,\n  waitForSlideIn,\n} from '../support/helpers';\n\ntest('fields in the slide-over do not overlap or overflow on desktop', async ({ page, request }) => {\n  test.setTimeout(60_000);\n  await createScenario(request, { labelPrefix: 'Desktop Overlap' });\n  await page.goto('/');\n  await expect(page.locator('[data-testid=\"sidebar\"]')).toBeVisible({ timeout: 15_000 });\n\n  await page.locator('[data-testid=\"new-scenario-btn\"]').click();\n  const form = page.locator('[data-testid=\"scenario-form\"]');\n  await expect(form).toBeVisible();\n  await waitForSlideIn(page);\n\n  // Add a partner so the second age/life-expectancy row is rendered\n  const addPartnerBtn = form.locator('[data-testid=\"add-partner-btn\"]');\n  if (await addPartnerBtn.isVisible()) {\n    await addPartnerBtn.click();\n  }\n\n  const slideOverBody = form.locator('.slide-over-body');\n  await expect(slideOverBody).toBeVisible();\n\n  // Expand the Advanced tier up-front so the dividend-yield / inflation\n  // .field-pair-row is rendered before the row scan runs. Otherwise the\n  // descendant-control overflow assertion below never exercises any\n  // .field-pair-row controls (the new-scenario defaults render no retirement\n  // field-pair-row either).\n  const advancedToggle = form.locator('[data-testid=\"tier-advanced-toggle\"]');\n  await advancedToggle.scrollIntoViewIfNeeded();\n  await advancedToggle.click();\n  await expect(form.locator('.field-pair-row').first()).toBeVisible();\n\n  // \u2500\u2500 No sibling-field overlap \u2500\u2500\n  // Check all multi-field row containers in the slide-over, not just\n  // age/life-expectancy rows. Each selector targets a known row-like container\n  // that lays out children horizontally.\n  const rowSelectors = [\n    '.age-life-expectancy-row',\n    '.person-labels-row',\n    '.benefit-card-fields-grid',\n    '.expense-row-bottom',\n    '.field-pair-row',\n  ];\n  const allRows = form.locator(rowSelectors.join(', '));\n  const rowCount = await allRows.count();\n  // Track which row selectors actually had their descendant-control overflow\n  // assertion exercised, so we can prove .field-pair-row coverage at the end.\n  const checkedControlRows = new Map<string, number>();\n  for (let r = 0; r < rowCount; r++) {\n    const row = allRows.nth(r);\n    await row.scrollIntoViewIfNeeded();\n    const children = row.locator(':scope > *');\n    const childCount = await children.count();\n    for (let i = 0; i < childCount - 1; i++) {\n      const a = await children.nth(i).boundingBox();\n      const b = await children.nth(i + 1).boundingBox();\n      if (!a || !b) continue; // hidden children\n      // Either stacked vertically (bottom of a <= top of b) or separated\n      // horizontally (right of a <= left of b). They must not intersect.\n      const horizontalOverlap = a.x < b.x + b.width && b.x < a.x + a.width;\n      const verticalOverlap = a.y < b.y + b.height && b.y < a.y + a.height;\n      expect(\n        horizontalOverlap && verticalOverlap,\n        `row ${r}: child ${i} and child ${i + 1} bounding boxes must not intersect`,\n      ).toBe(false);\n    }\n\n    // Label-level overlap check: adjacent .field-label elements inside the row\n    // must not have intersecting bounding boxes. This catches the case where\n    // min-width: 0 lets containers shrink below content width, causing label\n    // text to visually bleed across boundaries while container boxes stay separate.\n    const labels = row.locator('.field-label');\n    const labelCount = await labels.count();\n    for (let i = 0; i < labelCount - 1; i++) {\n      const la = await labels.nth(i).boundingBox();\n      const lb = await labels.nth(i + 1).boundingBox();\n      if (!la || !lb) continue;\n      const hOverlap = la.x < lb.x + lb.width && lb.x < la.x + la.width;\n      const vOverlap = la.y < lb.y + lb.height && lb.y < la.y + la.height;\n      expect(\n        hOverlap && vOverlap,\n        `row ${r}: label ${i} and label ${i + 1} text bounding boxes must not intersect`,\n      ).toBe(false);\n    }\n\n    // Descendant control overflow check: every <input>/<select>/<textarea>\n    // bounding box must fit horizontally within its containing cell (the\n    // direct child of the row). Catches missing `box-sizing: border-box`\n    // where width: 100% + padding + border push a control past its cell.\n    const matchedSelector = await row.evaluate((el, selectors) => {\n      return selectors.find((s) => (el as Element).matches(s)) ?? '';\n    }, rowSelectors);\n    for (let c = 0; c < childCount; c++) {\n      const cell = children.nth(c);\n      const cellBox = await cell.boundingBox();\n      if (!cellBox) continue;\n      const controls = cell.locator('input, select, textarea');\n      const ctrlCount = await controls.count();\n      for (let k = 0; k < ctrlCount; k++) {\n        const ctrlBox = await controls.nth(k).boundingBox();\n        if (!ctrlBox) continue;\n        expect(\n          ctrlBox.x + ctrlBox.width,\n          `row ${r}, cell ${c}, control ${k}: right edge must fit within cell right edge`,\n        ).toBeLessThanOrEqual(cellBox.x + cellBox.width + 1);\n        expect(\n          ctrlBox.x,\n          `row ${r}, cell ${c}, control ${k}: left edge must fit within cell left edge`,\n        ).toBeGreaterThanOrEqual(cellBox.x - 1);\n        if (matchedSelector) {\n          checkedControlRows.set(\n            matchedSelector,\n            (checkedControlRows.get(matchedSelector) ?? 0) + 1,\n          );\n        }\n      }\n    }\n  }\n\n  // Guard against the bug we just fixed: confirm both the row containers\n  // named in the spec actually had their descendant-control overflow\n  // assertion exercised. If a selector silently never matches a visible\n  // control, this loop would otherwise pass while skipping its target.\n  for (const required of ['.age-life-expectancy-row', '.field-pair-row']) {\n    expect(\n      checkedControlRows.get(required) ?? 0,\n      `descendant-control overflow check must exercise at least one control inside ${required}`,\n    ).toBeGreaterThan(0);\n  }\n\n  // \u2500\u2500 Field pair rows side-by-side on desktop \u2500\u2500\n  // Advanced tier was already expanded above so .field-pair-row is rendered.\n  const pairRows = form.locator('.field-pair-row');\n  const pairRowCount = await pairRows.count();\n  expect(pairRowCount, 'at least one .field-pair-row should be visible').toBeGreaterThanOrEqual(1);\n  for (let r = 0; r < pairRowCount; r++) {\n    const row = pairRows.nth(r);\n    await row.scrollIntoViewIfNeeded();\n    const kids = row.locator(':scope > *');\n    const kidCount = await kids.count();\n    if (kidCount < 2) continue;\n    const first = await kids.first().boundingBox();\n    const second = await kids.nth(1).boundingBox();\n    if (!first || !second) continue;\n    expect(\n      Math.abs(first.y - second.y),\n      `.field-pair-row ${r}: children must be on the same horizontal line`,\n    ).toBeLessThanOrEqual(2);\n  }\n\n  // \u2500\u2500 Age/life-expectancy side-by-side on desktop \u2500\u2500\n  // The children of each .age-life-expectancy-row must share the same vertical\n  // position (same Y), proving they are laid out horizontally, not stacked.\n  const ageLifeRows = form.locator('.age-life-expectancy-row');\n  expect(await ageLifeRows.count()).toBeGreaterThanOrEqual(2);\n  const ageLifeRowCount = await ageLifeRows.count();\n  for (let r = 0; r < ageLifeRowCount; r++) {\n    const row = ageLifeRows.nth(r);\n    await row.scrollIntoViewIfNeeded();\n    const ageField = row.locator('.age-field');\n    const lifeField = row.locator('.life-expectancy-field');\n    const ageBox = await ageField.boundingBox();\n    const lifeBox = await lifeField.boundingBox();\n    expect(ageBox, `row ${r}: age-field bounding box`).toBeTruthy();\n    expect(lifeBox, `row ${r}: life-expectancy-field bounding box`).toBeTruthy();\n    // Same Y within 2px tolerance = horizontally aligned (side-by-side)\n    expect(\n      Math.abs(ageBox!.y - lifeBox!.y),\n      `row ${r}: age and life-expectancy fields must be on the same horizontal line`,\n    ).toBeLessThanOrEqual(2);\n  }\n\n  // \u2500\u2500 No field overflow \u2500\u2500\n  // Every field wrapper inside the slide-over-body must fit within it.\n  const bodyBox = await slideOverBody.boundingBox();\n  expect(bodyBox).toBeTruthy();\n\n  // Check all field sections \u2014 not just age rows \u2014 for overflow\n  const fieldSelectors = [\n    '.age-life-expectancy-row',\n    '.person-labels-row',\n    '.field-group',\n    '.benefit-card-fields-grid',\n    '.expense-row-bottom',\n  ];\n  const allFields = slideOverBody.locator(fieldSelectors.join(', '));\n  const fieldCount = await allFields.count();\n  for (let i = 0; i < fieldCount; i++) {\n    const wrapper = allFields.nth(i);\n    await wrapper.scrollIntoViewIfNeeded();\n    const wBox = await wrapper.boundingBox();\n    if (!wBox) continue; // hidden wrappers\n    // Right edge of the field must not exceed the right edge of the container\n    expect(\n      wBox.x + wBox.width,\n      `field wrapper ${i} right edge within slide-over-body`,\n    ).toBeLessThanOrEqual(bodyBox!.x + bodyBox!.width + 1); // 1px tolerance\n  }\n\n  // Slide-over body itself must not have horizontal overflow\n  await expectHorizontalOverflowWithin(slideOverBody);\n});\n"
  },
  "verification": "Historical committed source inspected; tests not rerun for publication."
}
