diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx
deleted file mode 100644
index 9bd5a25..0000000
--- a/web/src/components/ui/card.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
-
-function Card({
- className,
- size = "default",
- ...props
-}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
- return (
-
img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
- className
- )}
- {...props}
- />
- )
-}
-
-function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardAction({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardContent({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardAction,
- CardDescription,
- CardContent,
-}
diff --git a/web/src/lib/class-recipes.test.ts b/web/src/lib/class-recipes.test.ts
new file mode 100644
index 0000000..1035923
--- /dev/null
+++ b/web/src/lib/class-recipes.test.ts
@@ -0,0 +1,22 @@
+import { expect, test } from "vitest";
+
+import { rowStateClass, segmentClass } from "./class-recipes";
+
+test("segmentClass active uses the primary tokens + focus ring", () => {
+ const cls = segmentClass(true, "px-2 py-1");
+ expect(cls).toContain("bg-primary");
+ expect(cls).toContain("text-primary-foreground");
+ expect(cls).toContain("focus-visible:ring-ring/50");
+ expect(cls).toContain("px-2");
+});
+
+test("segmentClass inactive uses border, not the primary fill", () => {
+ const cls = segmentClass(false);
+ expect(cls).toContain("border");
+ expect(cls).not.toContain("bg-primary");
+});
+
+test("rowStateClass toggles selected vs idle-hover", () => {
+ expect(rowStateClass(true)).toBe("bg-primary/10");
+ expect(rowStateClass(false)).toBe("hover:bg-muted");
+});
diff --git a/web/src/lib/class-recipes.ts b/web/src/lib/class-recipes.ts
new file mode 100644
index 0000000..06931a6
--- /dev/null
+++ b/web/src/lib/class-recipes.ts
@@ -0,0 +1,14 @@
+import { cn } from "@/lib/utils";
+
+import { focusRing } from "./focus-ring";
+
+/** Segmented-control / filter-pill item. Unifies the active/inactive token recipe +
+ * focus ring; callers pass their contextual padding/size via `className`. */
+export function segmentClass(active: boolean, className?: string): string {
+ return cn("rounded-md", focusRing, active ? "bg-primary text-primary-foreground" : "border", className);
+}
+
+/** Selected vs idle row background for master-detail / list rows. */
+export function rowStateClass(active: boolean): string {
+ return active ? "bg-primary/10" : "hover:bg-muted";
+}
diff --git a/web/src/lib/use-lang.ts b/web/src/lib/use-lang.ts
new file mode 100644
index 0000000..674cd68
--- /dev/null
+++ b/web/src/lib/use-lang.ts
@@ -0,0 +1,7 @@
+import { useTranslation } from "react-i18next";
+
+/** The instance's active UI language, narrowed to the two supported locales. */
+export function useLang(): "sv" | "en" {
+ const { i18n } = useTranslation();
+ return i18n.language.startsWith("sv") ? "sv" : "en";
+}