{q.isError && !d ? (
q.refetch()}>
diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx
index 31baf14b..e7b47d7a 100644
--- a/frontend/src/components/ui/Button.tsx
+++ b/frontend/src/components/ui/Button.tsx
@@ -36,26 +36,28 @@ type ButtonSize =
const VARIANTS: Record = {
// .btn-primary / .btn-danger own the fill + one hover-dim (index.css @layer components).
- primary: "btn-primary",
- danger: "btn-danger",
+ primary: "font-medium btn-primary",
+ danger: "font-medium btn-danger",
// E10 S1b: the neutral variants were RE-AIMED at the paint the app actually uses. Counting the
// un-migrated call sites showed the majority hovering to `accent` (outline) and filling with `card`
// (ghost), while this primitive hovered to `fg` and filled with `surface` — so the 37 migrated sites
// and the ~100 hand-rolled ones had drifted into two different looks. That split IS the
// inconsistency this epic exists to remove; the primitive follows the majority, not the reverse.
- outline: "border border-border text-muted hover:text-accent hover:border-accent",
- "outline-accent": "border border-accent/40 text-accent hover:bg-accent/10",
+ outline: "font-medium border border-border text-muted hover:text-accent hover:border-accent",
+ "outline-accent": "font-medium border border-accent/40 text-accent hover:bg-accent/10",
// Neutral until you reach for it. NOT `danger-soft`, which is already red at rest — the difference
// is whether the button should advertise the destruction before the pointer arrives.
- "outline-danger": "border border-border text-muted hover:text-danger hover:border-danger/40",
- "outline-warn": "border border-warn/40 text-warn hover:bg-warn/10",
- ghost: "text-muted hover:text-fg hover:bg-card",
- glass: "glass-card glass-hover hover:text-accent",
- "danger-soft": "border border-danger/60 text-danger hover:bg-danger/10 hover:border-danger",
+ "outline-danger":
+ "font-medium border border-border text-muted hover:text-danger hover:border-danger/40",
+ "outline-warn": "font-medium border border-warn/40 text-warn hover:bg-warn/10",
+ ghost: "font-medium text-muted hover:text-fg hover:bg-card",
+ glass: "font-medium glass-card glass-hover hover:text-accent",
+ "danger-soft":
+ "font-medium border border-danger/60 text-danger hover:bg-danger/10 hover:border-danger",
// No font-weight here on purpose: BASE already sets font-medium, and a second font-* utility would
// be a coin-flip (both are single-class utilities of equal specificity, and without tailwind-merge
// the winner is decided by the generated stylesheet order, not by the class list order).
- link: "text-accent hover:underline",
+ link: "font-semibold text-accent hover:underline",
};
const SIZES: Record = {
@@ -68,8 +70,11 @@ const SIZES: Record = {
none: "gap-1.5",
};
+// The font-weight is owned by each VARIANT, not by BASE: `link` wants semibold (that is what a text
+// link looks like everywhere in this app) and the action buttons want medium. Two font-* utilities on
+// one element is a coin flip without tailwind-merge, so exactly one has to emit it — see the test.
const BASE =
- "inline-flex items-center justify-center font-medium transition disabled:opacity-50 disabled:cursor-not-allowed";
+ "inline-flex items-center justify-center transition disabled:opacity-50 disabled:cursor-not-allowed";
export interface ButtonProps extends ButtonHTMLAttributes {
variant?: ButtonVariant;