+
)}
-
+
diff --git a/apps/web/src/features/ats-checker/report/score-header.tsx b/apps/web/src/features/ats-checker/report/score-header.tsx
index e7e29f0ba..c2ce168e5 100644
--- a/apps/web/src/features/ats-checker/report/score-header.tsx
+++ b/apps/web/src/features/ats-checker/report/score-header.tsx
@@ -32,7 +32,7 @@ export function ScoreHeader({ report }: ScoreHeaderProps) {
+
diff --git a/apps/web/src/features/documents/new-document-dialog.tsx b/apps/web/src/features/documents/new-document-dialog.tsx
index 0e1e08d3e..2a02883cd 100644
--- a/apps/web/src/features/documents/new-document-dialog.tsx
+++ b/apps/web/src/features/documents/new-document-dialog.tsx
@@ -1,3 +1,4 @@
+import type { CSSProperties } from "react";
import type { ImportKind } from "@/features/resume/import/read-file";
import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
@@ -467,8 +468,8 @@ function ImportProgress({ stage, notes }: { stage: number; notes: string[] }) {
className="h-1 overflow-hidden rounded-full bg-sunken"
>
diff --git a/apps/web/src/features/resume/editor/check/job-match.tsx b/apps/web/src/features/resume/editor/check/job-match.tsx
index f5f64aaa3..bc01d7d1f 100644
--- a/apps/web/src/features/resume/editor/check/job-match.tsx
+++ b/apps/web/src/features/resume/editor/check/job-match.tsx
@@ -1,5 +1,6 @@
import type { JdTermMatch } from "@reactive-resume/resume/ats-pdf";
import type { ResumeData, SkillItem } from "@reactive-resume/schema/resume/data";
+import type { CSSProperties } from "react";
import { t } from "@lingui/core/macro";
import { Plural, Trans } from "@lingui/react/macro";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -155,8 +156,8 @@ export function JobMatchTab({ match, data }: JobMatchTabProps) {
className="h-1.5 overflow-hidden rounded-full bg-sunken"
>
diff --git a/apps/web/src/features/resume/editor/page-overlay.tsx b/apps/web/src/features/resume/editor/page-overlay.tsx
index e4225a35e..b6347cf3b 100644
--- a/apps/web/src/features/resume/editor/page-overlay.tsx
+++ b/apps/web/src/features/resume/editor/page-overlay.tsx
@@ -59,8 +59,8 @@ export function PageOverlay({ pageIndex, pageMap, onSelect }: PageOverlayProps)
data-section-id={node.kind === "header" ? undefined : node.sectionId}
data-item-id={node.kind === "item" ? node.itemId : undefined}
className={cn(
- "absolute cursor-pointer rounded-[4px] transition-[background-color,outline-color] duration-quick hover:bg-[oklch(0.5_0.1_150/0.06)]",
- selected && "outline-[1.5px] outline-accent outline-solid",
+ "absolute cursor-pointer rounded-[4px] outline-[1.5px] outline-solid transition-[background-color,outline-color] duration-quick hover:bg-[oklch(0.5_0.1_150/0.06)]",
+ selected ? "outline-accent" : "outline-transparent",
)}
style={{
left: `calc(${(node.x / page.width) * 100}% - 4px)`,
diff --git a/apps/web/src/features/resume/preview/preview.browser.tsx b/apps/web/src/features/resume/preview/preview.browser.tsx
index f5c41aef5..7cd09aac3 100644
--- a/apps/web/src/features/resume/preview/preview.browser.tsx
+++ b/apps/web/src/features/resume/preview/preview.browser.tsx
@@ -10,7 +10,7 @@ import { toast } from "@reactive-resume/ui/components/toast";
import { isRTL } from "@reactive-resume/utils/locale";
import { cn } from "@reactive-resume/utils/style";
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
-import { EASE_OUT_STRONG } from "@/libs/motion";
+import { EASE } from "@/libs/motion";
import { isEditableElementFocused, usePreviewPausedStore, useResumeData } from "../builder/draft";
import { PdfCanvasDocument, PdfCanvasPage } from "./pdf-canvas";
import { ResumePreviewLoader } from "./preview.shared";
@@ -34,7 +34,7 @@ const UPDATE_DEBOUNCE_MS = 100;
const TYPING_DEBOUNCE_MS = 250;
// Incoming layer fades in over the old one; the old layer holds at full opacity until the incoming one is opaque,
// then drops out. Fading both at once dips the page towards the background mid-swap.
-const INCOMING_TRANSITION = { duration: 0.15, ease: EASE_OUT_STRONG };
+const INCOMING_TRANSITION = { duration: 0.15, ease: EASE };
const EXITING_TRANSITION = { duration: 0.1, delay: 0.18 };
// Motion animates in JS, so the CSS reduced-motion rule doesn't reach it: with reduced motion a new render replaces
// the old one at once, and no half-faded page is ever on screen.
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index 6d439dc16..85eda7154 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -1,11 +1,5 @@
@import "@reactive-resume/ui/globals.css";
-@keyframes agent-prompt-marquee {
- to {
- transform: translateX(-33.333%);
- }
-}
-
/* Check's score ring: the arc eases to a new score, so a fix visibly counts. */
@property --ring-value {
syntax: "
";
diff --git a/apps/web/src/libs/motion.ts b/apps/web/src/libs/motion.ts
index fbabf0e4a..a89a280c5 100644
--- a/apps/web/src/libs/motion.ts
+++ b/apps/web/src/libs/motion.ts
@@ -1,3 +1,18 @@
-// Mirrors `--ease-out-strong` in packages/ui/src/styles/globals.css for Motion (JS) animations.
-// Mirror the Desk & Paper motion tokens (--d1/--d2/--d3, --ease) here as Motion animations start using them.
+// Motion (JS) mirrors of the CSS motion tokens in packages/ui/src/styles/globals.css (--d1/--d2/--d3, --ease,
+// --ease-in-out-strong). Durations are in seconds, Motion's unit; multiply by 1000 for APIs that take ms (dnd-kit).
+
+/** --ease / `ease-enter`: everything entering, exiting or changing state. */
+export const EASE = [0.2, 0.8, 0.2, 1] as const;
+/** --ease-in-out-strong / `ease-in-out-strong`: things moving across the screen (indicators, reorder, settling). */
+export const EASE_MOVE = [0.77, 0, 0.175, 1] as const;
+/** --d1 / `duration-quick`: hover, press, toggle, checkbox, focus ring. */
+export const D1 = 0.12;
+/** --d2 / `duration-standard`: menus, popovers, expand/collapse, content swaps, dialogs. */
+export const D2 = 0.2;
+/** --d3 / `duration-emphasized`: side and bottom sheets, toasts, the assistant. */
+export const D3 = 0.32;
+/** Exits run at 70% of the enter duration, e.g. `D2 * EXIT`. */
+export const EXIT = 0.7;
+
+/** --ease-out-strong. Landing page only (features/homepage/site-footer.tsx); app code uses EASE. */
export const EASE_OUT_STRONG = [0.23, 1, 0.32, 1] as const;
diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
index 86969425f..1c7c7270e 100644
--- a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
+++ b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
@@ -17,7 +17,7 @@ import { LevelTypeCombobox } from "@/components/level/combobox";
import { LevelDisplay } from "@/components/level/display";
import { useCurrentResume, useUpdateResumeData } from "@/features/resume/builder/draft";
import { useSyncFormValues } from "@/hooks/use-sync-form-values";
-import { EASE_OUT_STRONG } from "@/libs/motion";
+import { D1, EASE } from "@/libs/motion";
import { useAppForm } from "@/libs/tanstack-form";
import { SectionBase } from "../shared/section-base";
@@ -180,7 +180,7 @@ function QuickColorCircle({ color, active, onSelect, className, ...props }: Quic
onClick={() => onSelect(color)}
className={cn(
"relative flex size-8 items-center justify-center rounded-md bg-transparent",
- "transition-[scale,background-color] duration-150 ease-out-strong hover:bg-sunken/80 active:scale-[0.97]",
+ "transition-[scale,background-color] duration-quick ease-enter hover:bg-sunken/80 active:scale-[0.97]",
className,
)}
{...props}
@@ -193,7 +193,7 @@ function QuickColorCircle({ color, active, onSelect, className, ...props }: Quic
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
- transition={{ duration: 0.16, ease: EASE_OUT_STRONG }}
+ transition={{ duration: D1, ease: EASE }}
className="absolute inset-0 flex size-8 items-center justify-center"
>
diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/layout/pages.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/layout/pages.tsx
index 3763e3d62..47b5298cd 100644
--- a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/layout/pages.tsx
+++ b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/layout/pages.tsx
@@ -33,7 +33,7 @@ import { Icon } from "@reactive-resume/ui/components/icon";
import { Switch } from "@reactive-resume/ui/components/switch";
import { cn } from "@reactive-resume/utils/style";
import { useCurrentResume, useUpdateResumeData } from "@/features/resume/builder/draft";
-import { EASE_OUT_STRONG } from "@/libs/motion";
+import { EASE } from "@/libs/motion";
import { resolveLayoutSectionTitle } from "./title";
import { filterVisibleLayoutSectionIds } from "./visibility";
@@ -263,7 +263,7 @@ export function LayoutPages() {
-
+
{activeId ? : null}
@@ -612,7 +612,7 @@ function LayoutItemContent({
data-overlay={isOverlay ? "true" : undefined}
data-dragging={isDragging ? "true" : undefined}
className={cn(
- "group/item flex cursor-grab touch-none select-none items-center gap-x-2 rounded-md border border-line bg-bg px-2 py-1.5 font-medium text-sm transition-[background-color,border-color] duration-150",
+ "group/item flex cursor-grab touch-none select-none items-center gap-x-2 rounded-md border border-line bg-bg px-2 py-1.5 font-medium text-sm transition-[background-color,border-color] duration-quick",
"hover:bg-sunken/40 active:cursor-grabbing active:border-accent/60 active:bg-sunken/40",
"data-[overlay=true]:cursor-grabbing data-[overlay=true]:border-accent/60 data-[overlay=true]:bg-bg data-[overlay=true]:shadow-md",
"data-[dragging=true]:cursor-grabbing data-[dragging=true]:opacity-40",
diff --git a/apps/web/src/routes/builder/letter/-components/letter-page.tsx b/apps/web/src/routes/builder/letter/-components/letter-page.tsx
index e8bc0f966..25a8e0a8b 100644
--- a/apps/web/src/routes/builder/letter/-components/letter-page.tsx
+++ b/apps/web/src/routes/builder/letter/-components/letter-page.tsx
@@ -215,10 +215,10 @@ function LetterOverlay({ pageIndex, pageMap, itemId, washed }: LetterOverlayProp
key={`${node.key}:${node.y}`}
onClick={() => reveal(node.kind === "header" ? "from" : "body")}
className={cn(
- "absolute cursor-pointer rounded-[4px] transition-[background-color,outline-color] duration-standard",
+ "absolute cursor-pointer rounded-[4px] outline-[1.5px] outline-solid transition-[background-color,outline-color] duration-standard",
washed && node.kind === "item"
- ? "bg-[oklch(0.93_0.05_150/0.55)] mix-blend-multiply outline-[1.5px] outline-[oklch(0.5_0.1_150)] outline-solid"
- : "hover:bg-[oklch(0.5_0.1_150/0.06)]",
+ ? "bg-[oklch(0.93_0.05_150/0.55)] mix-blend-multiply outline-[oklch(0.5_0.1_150)]"
+ : "outline-transparent hover:bg-[oklch(0.5_0.1_150/0.06)]",
)}
style={{
left: `calc(${(node.x / page.width) * 100}% - 4px)`,
diff --git a/apps/web/src/routes/builder/letter/-components/write-panel.tsx b/apps/web/src/routes/builder/letter/-components/write-panel.tsx
index 46df972e1..a5fcdaf7a 100644
--- a/apps/web/src/routes/builder/letter/-components/write-panel.tsx
+++ b/apps/web/src/routes/builder/letter/-components/write-panel.tsx
@@ -1,5 +1,5 @@
import type { CoverLetter } from "@reactive-resume/schema/cover-letter/data";
-import type { ReactElement, ReactNode } from "react";
+import type { CSSProperties, ReactElement, ReactNode } from "react";
import type { Application } from "@/features/applications/types";
import type { LetterDraft } from "@/features/letters/store";
import { t } from "@lingui/core/macro";
@@ -553,8 +553,8 @@ function DraftBox({ draft, letter, application, onKeep }: DraftBoxProps) {
aria-live="polite"
aria-busy={streaming}
className={cn(
- "min-h-[120px] whitespace-pre-wrap rounded-lg bg-accent-soft px-3 py-2.5 text-sm leading-relaxed transition-[outline-color] duration-standard",
- draft.phase === "ready" ? "outline-[1.5px] outline-accent outline-solid" : "outline-transparent",
+ "min-h-[120px] whitespace-pre-wrap rounded-lg bg-accent-soft px-3 py-2.5 text-sm leading-relaxed outline-[1.5px] outline-solid transition-[outline-color] duration-standard",
+ draft.phase === "ready" ? "outline-accent" : "outline-transparent",
)}
>
{text}
@@ -600,8 +600,15 @@ function LengthSection({ content }: { content: string }) {
}}
/>
diff --git a/apps/web/src/routes/dashboard/-components/app-shell.tsx b/apps/web/src/routes/dashboard/-components/app-shell.tsx
index ad47ce79b..2c5a566c9 100644
--- a/apps/web/src/routes/dashboard/-components/app-shell.tsx
+++ b/apps/web/src/routes/dashboard/-components/app-shell.tsx
@@ -214,7 +214,7 @@ function Rail() {
type="button"
aria-label={t`Search or run…`}
onClick={() => openPalette(true)}
- className="grid size-10 place-items-center rounded-lg text-ink-2 hover:bg-hover"
+ className="grid size-10 place-items-center rounded-lg text-ink-2 transition-colors hover:bg-hover"
/>