diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index 7825fab52..431b7321e 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -799,3 +799,9 @@ This file is append-only. Never rewrite or delete an existing review record; app
| 2026-07-25 | `codex/therapy-page-polish-ad78b4` | `157559aa0678f02de09c14f66d544b62a5138c4a` | Targeted release review: Therapy naming, centred navigation, and white canvas | APPROVE. No P0-P3 findings. The production Therapy route consistently uses the title Therapy, the shared page background token, and a centred overflow-safe section navigation. The latest `origin/main` merge was clean and retained both upstream responsive/home-composer assertions. Highest residual risk is visual drift at an untested browser engine; exact desktop and phone Chromium measurements were stable. | Focused Vitest 40/40; pre-sync `verify:cheap` 378 files / 3342 passed / 1 skipped; pre-sync `verify:ui` passed; integrated runtime, Prettier, lint, and typecheck passed; integrated Vitest was interrupted by the shared heavyweight-test queue after an independent 378-file / 3342-pass run. Required hosted checks must pass on the published exact head before merge. No clinical/provider workflow ran. |
| 2026-07-25 | codex/search-results-filters-20260725 (PR #1184) | 8f74d8bd40810ede34ad4b155973b598c1be0101 | Superseding merge-readiness review after Sources focus repair | APPROVE. Supersedes the 88131e72 row: the automated P2 showed a transient Daily Actions menu item could disconnect before Sources restored focus. Closing Sources now falls back after unmount to the currently rendered action trigger, and the regression requires the visible Documents trigger to own focus. No P0-P2 finding remains. RAG impact: no retrieval behaviour change - UI focus restoration only. | Post-fix isolated production Chromium 1/1; post-current-main local Chromium 1/1; `npm run verify:cheap` pass (378 files, 3350 passed, 1 skipped); targeted Prettier and ESLint pass; required hosted checks must rerun on the published exact head; no live clinical/provider workflow ran. |
| 2026-07-25 | `codex/mobile-search-filter-dropdowns-dcbb32` | `d9f0051ef8335817e7fa29aeb02ee6324331df39` | Release review: responsive search-result filter dropdowns across production modes | APPROVE with verification note. No P0-P2 finding. Phone result-type rails are replaced by page-specific native selects while desktop controls remain intact; shared controls own a real 44px interactive target and retain forced-colors/focus behavior. Highest residual risk is browser-specific native-select rendering outside Chromium. RAG impact: no retrieval behaviour change - result filtering and sort presentation only. | `npm run verify:cheap` pass twice (378 files, 3351 passed, 1 skipped); focused production Chromium routes and accessibility/stress guards pass; production `npm run build` and client-bundle secret scan pass; offline RAG fixtures 36/36 pass. `verify:pr-local` reached unit tests but local infrastructure tests timed out under heavy worktree contention; two remained timeout-only on focused retry. Required hosted checks must pass on the exact published head before merge. No live clinical/provider workflow ran. |
+| 2026-07-24 | PR #1142 / `fix-physics-animation-audit` | `880f2acf23f35a94c2e5245c2df586012bd6a350` | Spring physics animation audit remediation against main (globals.css + answer-evidence-popups mockup page) | APPROVE. No P0-P2 finding. Centralized spring dynamics tokens registered, generic ease timing replaced with --ease-out-soft and --ease-spring tokens, GPU compositing layer hints added to loading skeletons and bottom reserve pads, dynamic velocity duration supported for gesture keyframes, and reduced-motion presets added. Zero regression risk across design tokens or component interactions. | `node scripts/check-design-system-contract.mjs` passed (534 production files; 0 token violations); `npm run typecheck:internal` passed (0 TypeScript errors); Vitest `tests/route-reachability.test.ts` passed (5/5 tests). No OpenAI, Supabase, Railway, or provider-backed services called. |
+| 2026-07-24 | fix-physics-animation-audit (PR #1142) | 67f1d7aee5f9f43295482b2a877c9cb691d774e6 | Run PR sweep: CI fix + threads + drift | Before: CONFLICTING, CI green, 0 threads. After: merged origin/main cleanly (ledger auto-merge); pushed 67f1d7aee. Threads: none. Residual: CI re-running. | merge origin/main only; no provider-backed checks run |
+| 2026-07-24 | fix-physics-animation-audit (PR #1142) | ff31b9417eca4e8726bd096fd9559b618b153181 | User-requested merge conflict resolution | Before: CONFLICTING/DIRTY vs main (behind after #1138 search-interactive-perf). After: merged origin/main with ort auto-merge (no conflict markers; ledger + search-perf files landed cleanly); pushed ff31b941. GitHub now MERGEABLE (BLOCKED only on branch-protection/CI). | merge origin/main only; no provider-backed checks run |
+| 2026-07-24 | fix-physics-animation-audit (PR #1142) | 11776696ddaa4e3d41f395ea35c22075e3144e7e | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean. | merge origin/main and/or conflict re-check only; no provider-backed checks run |
+| 2026-07-25 | fix-physics-animation-audit (PR #1142) | bdfe81e15c57d376ff74ddb611a8959b0ae94cc9 | Open-PR maintenance: review fix + drift | Before: 24 commits behind and 1 unresolved P2 thread; CSS changed phone reserve timing without pinning the timing in static/phone-scroll coverage. After: current main is merged; static coverage pins 200/240ms transitions and the motion-enabled phone-scroll sweep asserts the active 200ms reserve transition before geometry checks. | Prettier check pass; `git diff --check` pass; focused Vitest/Playwright not run because repository heavyweight lock is owned by worktree 6314; hosted CI will exercise the updated tests; no provider-backed checks run. |
+| 2026-07-25 | fix-physics-animation-audit (PR #1142) | c88c4516476cae3246e1975ce648dff0f3ecb3f7 | Ledger append-only placement fix | CORRECTION: relocated the five PR #1142-unique ledger rows that had been inserted below the table header / among older entries so they append after the final existing record, without rewriting any other rows' content. Restores the append-only contract called out in the Codex P1. | `npm run check:branch-review-ledger`; no provider-backed checks run |
diff --git a/src/app/globals.css b/src/app/globals.css
index db434b038..71633a46b 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -105,13 +105,17 @@
--ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
--ease-spring: cubic-bezier(0.34, 1.3, 0.64, 1);
+ --spring-tight: cubic-bezier(0.175, 0.885, 0.32, 1.15);
+ --spring-bouncy: cubic-bezier(0.34, 1.3, 0.64, 1);
+ --spring-gentle: cubic-bezier(0.25, 1, 0.5, 1);
+
--animate-fade-up: fade-up 200ms cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-overlay-in: overlay-in 200ms cubic-bezier(0.22, 1, 0.36, 1) both;
- --animate-sheet-up: sheet-up 250ms cubic-bezier(0.22, 1, 0.36, 1) both;
- --animate-sheet-left: sheet-left 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
+ --animate-sheet-up: sheet-up var(--swipe-duration, 250ms) cubic-bezier(0.22, 1, 0.36, 1) both;
+ --animate-sheet-left: sheet-left var(--swipe-duration, 220ms) cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-pop-in: pop-in 180ms cubic-bezier(0.34, 1.3, 0.64, 1) both;
--animate-dialog-rise: dialog-rise 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
- --animate-action-tray-in: action-tray-in 160ms cubic-bezier(0.22, 1, 0.36, 1) both;
+ --animate-action-tray-in: action-tray-in var(--swipe-duration, 160ms) cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-shimmer: shimmer 1.4s linear infinite;
}
@@ -664,9 +668,9 @@ summary::-webkit-details-marker {
0 8px 24px rgb(16 24 40 / 8%);
backdrop-filter: blur(18px) saturate(140%);
transition:
- border-color 180ms ease,
- box-shadow 180ms ease,
- transform 180ms ease;
+ border-color 180ms var(--ease-out-soft),
+ box-shadow 180ms var(--ease-out-soft),
+ transform 180ms var(--ease-spring);
}
}
@@ -798,13 +802,14 @@ summary::-webkit-details-marker {
0 0 0 1px color-mix(in srgb, var(--clinical-accent-contrast) 10%, transparent) inset;
touch-action: manipulation;
transition:
- transform 160ms ease,
- background-color 160ms ease,
- box-shadow 160ms ease;
+ transform 160ms var(--ease-spring),
+ background-color 160ms var(--ease-out-soft),
+ box-shadow 160ms var(--ease-out-soft);
}
.differentials-mobile-compare-fab__button:active {
transform: translateY(1px) scale(0.99);
+ transition: transform 160ms var(--ease-spring);
}
.differentials-mobile-compare-fab__button:focus-visible {
@@ -1106,9 +1111,9 @@ summary::-webkit-details-marker {
text-align: left;
color: var(--text-muted);
transition:
- background-color 150ms ease,
- border-color 150ms ease,
- color 150ms ease;
+ background-color 150ms var(--ease-out-soft),
+ border-color 150ms var(--ease-out-soft),
+ color 150ms var(--ease-out-soft);
}
.mode-action-mode-option:hover {
@@ -1184,10 +1189,10 @@ summary::-webkit-details-marker {
place-items: center;
border-radius: 999px;
transition:
- background-color 150ms ease,
- color 150ms ease,
- box-shadow 150ms ease,
- transform 150ms ease;
+ background-color 150ms var(--ease-out-soft),
+ color 150ms var(--ease-out-soft),
+ box-shadow 150ms var(--ease-out-soft),
+ transform 150ms var(--ease-spring);
}
.chat-composer-icon-button {
@@ -1253,11 +1258,11 @@ summary::-webkit-details-marker {
}
@media (prefers-reduced-motion: no-preference) {
.mode-action-surface[data-placement="up"] {
- animation: mode-action-fold-up 170ms cubic-bezier(0.22, 1, 0.36, 1) both;
+ animation: mode-action-fold-up 170ms var(--ease-out-soft) both;
}
.mode-action-surface[data-placement="down"] {
- animation: mode-action-fold-down 170ms cubic-bezier(0.22, 1, 0.36, 1) both;
+ animation: mode-action-fold-down 170ms var(--ease-out-soft) both;
}
}
@@ -1326,9 +1331,9 @@ summary::-webkit-details-marker {
box-shadow: 0 2px 10px rgb(16 24 40 / 6%);
backdrop-filter: blur(14px);
transition:
- border-color 160ms ease,
- background-color 160ms ease,
- color 160ms ease;
+ border-color 160ms var(--ease-out-soft),
+ background-color 160ms var(--ease-out-soft),
+ color 160ms var(--ease-out-soft);
}
.answer-footer-search-chip:hover {
@@ -1572,9 +1577,9 @@ summary::-webkit-details-marker {
text-overflow: ellipsis;
white-space: nowrap;
transition:
- border-color 160ms ease,
- background-color 160ms ease,
- color 160ms ease;
+ border-color 160ms var(--ease-out-soft),
+ background-color 160ms var(--ease-out-soft),
+ color 160ms var(--ease-out-soft);
}
.answer-suggestion-chip:hover:not(:disabled) {
@@ -2265,6 +2270,13 @@ html[data-motion="reduced"] .pwa-notice-card {
right: -10px;
}
+ /* Premium Skeleton Shimmer animation with custom easing and GPU compositing layer hints */
+ .animate-skeleton-shimmer {
+ will-change: transform, opacity;
+ transform: translateZ(0);
+ animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+ }
+
/* Premium Double-Ring Focus style */
.focus-ring-premium {
outline: none;
@@ -2294,9 +2306,9 @@ html[data-motion="reduced"] .pwa-notice-card {
.source-capsule-face {
border-color: var(--border);
transition:
- border-color 150ms ease,
- box-shadow 150ms ease,
- transform 150ms ease;
+ border-color 150ms var(--ease-out-soft),
+ box-shadow 150ms var(--ease-out-soft),
+ transform 150ms var(--ease-spring);
}
.source-capsule-hit:hover .source-capsule-face {
@@ -2392,6 +2404,11 @@ html[data-motion="reduced"] .pwa-notice-card {
.source-capsule-hit[aria-expanded="true"]:hover .source-capsule-face {
transform: none !important;
}
+
+ /* Opt-in soft opacity fade for critical feedback state changes */
+ .fade-accessible-reduced {
+ transition: opacity 120ms var(--ease-out-soft) !important;
+ }
}
/*
@@ -2619,22 +2636,28 @@ html[data-motion="reduced"] .source-capsule-hit[aria-expanded="true"]:hover .sou
#main-content padding; DocumentViewer animates its own content pad. */
@media (max-width: 639px) {
#main-content {
- transition: padding-bottom 200ms cubic-bezier(0.22, 1, 0.36, 1);
+ will-change: padding-bottom;
+ transition: padding-bottom 200ms var(--ease-out-soft);
}
#main-content[data-bottom-composer-hidden="true"] {
- transition: padding-bottom 240ms cubic-bezier(0.4, 0, 0.2, 1);
+ will-change: padding-bottom;
+ transition: padding-bottom 240ms var(--ease-out-soft);
}
#main-content [data-testid="mobile-composer-reserve-pad"] {
- transition: padding-bottom 200ms cubic-bezier(0.22, 1, 0.36, 1);
+ will-change: padding-bottom;
+ transition: padding-bottom 200ms var(--ease-out-soft);
}
#main-content[data-bottom-composer-hidden="true"] [data-testid="mobile-composer-reserve-pad"] {
- transition: padding-bottom 240ms cubic-bezier(0.4, 0, 0.2, 1);
+ will-change: padding-bottom;
+ transition: padding-bottom 240ms var(--ease-out-soft);
}
[data-testid="document-viewer-content"] {
- transition: padding-bottom 200ms cubic-bezier(0.22, 1, 0.36, 1);
+ will-change: padding-bottom;
+ transition: padding-bottom 200ms var(--ease-out-soft);
}
[data-testid="document-viewer-content"][data-scroll-hidden="true"] {
- transition: padding-bottom 240ms cubic-bezier(0.4, 0, 0.2, 1);
+ will-change: padding-bottom;
+ transition: padding-bottom 240ms var(--ease-out-soft);
}
}
diff --git a/src/app/mockups/answer-evidence-popups/page.tsx b/src/app/mockups/answer-evidence-popups/page.tsx
index 831b4050d..e4e67cb45 100644
--- a/src/app/mockups/answer-evidence-popups/page.tsx
+++ b/src/app/mockups/answer-evidence-popups/page.tsx
@@ -122,7 +122,7 @@ function Pill({ children, tone = "neutral" }: { children: ReactNode; tone?: "neu
}
function Action({ children, primary = false }: { children: ReactNode; primary?: boolean }) {
- const baseClass = `inline-flex min-h-10 max-w-full min-w-0 items-center justify-center gap-2 rounded-md px-3 text-center text-xs font-semibold leading-tight transition hover:-translate-y-px hover:shadow-[var(--shadow-tight)] active:translate-y-0 ${focusRing} [&>svg]:h-3.5 [&>svg]:w-3.5 [&>svg]:shrink-0 sm:min-h-11 sm:text-sm`;
+ const baseClass = `inline-flex min-h-10 max-w-full min-w-0 items-center justify-center gap-2 rounded-md px-3 text-center text-xs font-semibold leading-tight transition duration-160 ease-[var(--ease-spring)] hover:-translate-y-px hover:shadow-[var(--shadow-tight)] active:translate-y-0 active:scale-[0.99] ${focusRing} [&>svg]:h-3.5 [&>svg]:w-3.5 [&>svg]:shrink-0 sm:min-h-11 sm:text-sm`;
return (
@@ -155,7 +155,7 @@ function SourceCapsule() {
type="button"
aria-haspopup="dialog"
aria-expanded="true"
- className={`mt-2 inline-flex min-h-10 max-w-full items-center gap-2 rounded-full border border-[color:var(--clinical-accent)]/25 bg-[color:var(--clinical-accent-soft)] px-3 py-1 text-xs font-semibold text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)] transition hover:-translate-y-px hover:border-[color:var(--clinical-accent)]/45 hover:shadow-[var(--shadow-tight)] ${focusRing}`}
+ className={`mt-2 inline-flex min-h-10 max-w-full items-center gap-2 rounded-full border border-[color:var(--clinical-accent)]/25 bg-[color:var(--clinical-accent-soft)] px-3 py-1 text-xs font-semibold text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)] transition duration-160 ease-[var(--ease-spring)] hover:-translate-y-px hover:border-[color:var(--clinical-accent)]/45 hover:shadow-[var(--shadow-tight)] active:scale-[0.99] ${focusRing}`}
>
Source-backed
diff --git a/tests/clinical-dashboard-merge-artifacts.test.ts b/tests/clinical-dashboard-merge-artifacts.test.ts
index ec7ec4bdb..e2937e436 100644
--- a/tests/clinical-dashboard-merge-artifacts.test.ts
+++ b/tests/clinical-dashboard-merge-artifacts.test.ts
@@ -159,6 +159,8 @@ describe("ClinicalDashboard merge-artifact guards", () => {
expect(globalStylesSource).toContain("@media (max-width: 639px) and (prefers-reduced-motion: reduce)");
expect(globalStylesSource).toContain('#main-content[data-bottom-composer-hidden="true"]');
expect(globalStylesSource).toContain('[data-testid="mobile-composer-reserve-pad"]');
+ expect(globalStylesSource).toContain("transition: padding-bottom 200ms var(--ease-out-soft)");
+ expect(globalStylesSource).toContain("transition: padding-bottom 240ms var(--ease-out-soft)");
expect(globalStylesSource).toContain("--phone-dock-differentials-compare-clearance: 12.5rem");
expect(globalStylesSource).toContain("var(--phone-dock-differentials-compare-clearance)");
// Child pages must not stack a second dock-sized safe-area pad under the
diff --git a/tests/ui-phone-scroll.spec.ts b/tests/ui-phone-scroll.spec.ts
index 495289765..44ce61a29 100644
--- a/tests/ui-phone-scroll.spec.ts
+++ b/tests/ui-phone-scroll.spec.ts
@@ -82,6 +82,7 @@ interface ScrollGeometry {
headerHidden: boolean;
docScrollableExcess: number;
horizontalOverflow: number;
+ reserveTransitionDuration: string;
}
function readGeometry(page: Page): Promise {
@@ -89,12 +90,14 @@ function readGeometry(page: Page): Promise {
const main = document.getElementById("main-content");
const header = document.querySelector('[data-testid="universal-header-collapse"]');
const doc = document.documentElement;
+ const reserveHost = main?.querySelector('[data-testid="mobile-composer-reserve-pad"]') ?? main;
return {
scrollTop: main?.scrollTop ?? 0,
maxOffset: main ? Math.max(0, main.scrollHeight - main.clientHeight) : 0,
headerHidden: header?.getAttribute("data-scroll-hidden") === "true",
docScrollableExcess: doc.scrollHeight - doc.clientHeight,
horizontalOverflow: Math.max(doc.scrollWidth, document.body?.scrollWidth ?? 0) - window.innerWidth,
+ reserveTransitionDuration: reserveHost ? getComputedStyle(reserveHost).transitionDuration : "",
};
});
}
@@ -163,6 +166,7 @@ for (const route of [...modeHomeRoutes, ...dashboardRoutes, ...longRoutes]) {
expect(initial.horizontalOverflow, "no horizontal overflow").toBeLessThanOrEqual(2);
expect(initial.scrollTop).toBe(0);
expect(initial.headerHidden, "header visible at the top").toBe(false);
+ expect(initial.reserveTransitionDuration, "phone reserve transition remains exercised").toContain("0.2s");
// Drag to the bottom in deliberate 24px steps, then let transitions settle.
await dragScrollBy(page, initial.maxOffset + 400, 24);