Bläddra i källkod

New design system: AppCard rollout, accessibility and portal polish (#252)

* sidebar

* borders

* card borders

* reusable app card

* app card focus

* app card layout

* customer portal

* lint
Bernt Christian Egeland 1 månad sedan
förälder
incheckning
c66128ae43
87 ändrade filer med 1632 tillägg och 1400 borttagningar
  1. 17 2
      messages/de/portal.json
  2. 17 2
      messages/en/portal.json
  3. 17 2
      messages/es/portal.json
  4. 17 2
      messages/fr/portal.json
  5. 17 2
      messages/it/portal.json
  6. 17 2
      messages/lt/portal.json
  7. 17 2
      messages/nb/portal.json
  8. 17 2
      messages/nl/portal.json
  9. 17 2
      messages/pl/portal.json
  10. 17 2
      messages/pt-BR/portal.json
  11. 17 2
      messages/ru/portal.json
  12. 17 2
      messages/tr/portal.json
  13. 2 1
      src/app/(authenticated)/audit-log/audit-log-client.tsx
  14. 5 4
      src/app/(authenticated)/billing/billing-client.tsx
  15. 2 1
      src/app/(authenticated)/customers/[id]/customer-detail-client.tsx
  16. 2 1
      src/app/(authenticated)/customers/customers-client.tsx
  17. 213 234
      src/app/(authenticated)/dashboard-client.tsx
  18. 2 1
      src/app/(authenticated)/inspections/inspections-client.tsx
  19. 3 2
      src/app/(authenticated)/inventory/inventory-client.tsx
  20. 2 1
      src/app/(authenticated)/labor-presets/labor-presets-client.tsx
  21. 7 0
      src/app/(authenticated)/layout.tsx
  22. 2 1
      src/app/(authenticated)/observations/observations-client.tsx
  23. 2 1
      src/app/(authenticated)/quotes/quotes-client.tsx
  24. 144 217
      src/app/(authenticated)/reports/reports-client.tsx
  25. 7 9
      src/app/(authenticated)/settings/about/page.tsx
  26. 20 26
      src/app/(authenticated)/settings/account/account-settings.tsx
  27. 12 22
      src/app/(authenticated)/settings/alerts/alerts-settings.tsx
  28. 13 17
      src/app/(authenticated)/settings/appearance/appearance-settings.tsx
  29. 12 19
      src/app/(authenticated)/settings/company/company-settings.tsx
  30. 1 1
      src/app/(authenticated)/settings/company/service-type-selector.tsx
  31. 29 37
      src/app/(authenticated)/settings/data/data-settings.tsx
  32. 7 9
      src/app/(authenticated)/settings/install/install-client.tsx
  33. 7 9
      src/app/(authenticated)/settings/invoice/invoice-settings.tsx
  34. 25 33
      src/app/(authenticated)/settings/localization/localization-settings.tsx
  35. 7 9
      src/app/(authenticated)/settings/maintenance/maintenance-settings.tsx
  36. 16 20
      src/app/(authenticated)/settings/payment/payment-settings.tsx
  37. 7 9
      src/app/(authenticated)/settings/support/page.tsx
  38. 7 9
      src/app/(authenticated)/settings/tax/tax-settings.tsx
  39. 36 58
      src/app/(authenticated)/settings/team/team-settings.tsx
  40. 24 40
      src/app/(authenticated)/settings/templates/template-settings.tsx
  41. 7 9
      src/app/(authenticated)/settings/workshop/workshop-settings.tsx
  42. 2 1
      src/app/(authenticated)/vehicles/[id]/notes-table.tsx
  43. 3 2
      src/app/(authenticated)/vehicles/[id]/service-records-table.tsx
  44. 4 3
      src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx
  45. 2 1
      src/app/(authenticated)/vehicles/vehicles-client.tsx
  46. 3 2
      src/app/(authenticated)/work-orders/work-orders-client.tsx
  47. 31 34
      src/app/(public)/portal/[orgId]/dashboard/page.tsx
  48. 8 26
      src/app/(public)/portal/[orgId]/request-service/page.tsx
  49. 32 29
      src/app/(public)/portal/[orgId]/vehicles/[vehicleId]/page.tsx
  50. 77 40
      src/app/(public)/portal/[orgId]/vehicles/page.tsx
  51. 31 1
      src/app/globals.css
  52. 114 0
      src/components/app-card.tsx
  53. 4 1
      src/components/ui/card.tsx
  54. 5 1
      src/components/ui/sidebar.tsx
  55. 8 11
      src/features/admin/Components/admin-organization-details.tsx
  56. 13 19
      src/features/admin/Components/admin-settings.tsx
  57. 2 1
      src/features/admin/Components/admin-users.tsx
  58. 18 28
      src/features/ai/Components/AiSettingsForm.tsx
  59. 2 2
      src/features/billing/Components/RecurringInvoicesClient.tsx
  60. 2 2
      src/features/calendar/Components/CalendarClient.tsx
  61. 8 9
      src/features/custom-fields/Components/CustomFieldsForm.tsx
  62. 2 1
      src/features/custom-fields/Components/CustomFieldsManager.tsx
  63. 31 39
      src/features/dashboard/Components/CustomTableCard.tsx
  64. 7 17
      src/features/email/Components/EmailSettingsForm.tsx
  65. 4 3
      src/features/inventory/Components/InventoryPartForm.tsx
  66. 10 6
      src/features/notifications/hooks/useNotificationWebSocket.ts
  67. 11 4
      src/features/notifications/store/notificationStore.ts
  68. 65 9
      src/features/portal/Actions/portalActions.ts
  69. 25 38
      src/features/portal/Components/CustomerPortalSettings.tsx
  70. 5 0
      src/features/portal/Components/PortalShell.tsx
  71. 104 22
      src/features/portal/Components/ServiceRequestForm.tsx
  72. 10 17
      src/features/quotes/Components/QuoteDocumentsManager.tsx
  73. 10 17
      src/features/quotes/Components/QuoteImagesManager.tsx
  74. 12 18
      src/features/settings/Components/license-settings.tsx
  75. 7 9
      src/features/settings/Components/passkey-settings.tsx
  76. 7 15
      src/features/sms/Components/SmsSettingsForm.tsx
  77. 2 1
      src/features/status-reports/Components/StatusReportList.tsx
  78. 31 49
      src/features/subscription/Components/subscription-settings.tsx
  79. 7 20
      src/features/telegram/Components/TelegramSettingsForm.tsx
  80. 11 13
      src/features/vehicles/Components/MyActiveJobs.tsx
  81. 1 1
      src/features/vehicles/Components/RemindersPageClient.tsx
  82. 2 1
      src/features/vehicles/Components/VehicleForm.tsx
  83. 18 33
      src/features/vehicles/Components/service-documents-manager.tsx
  84. 10 17
      src/features/vehicles/Components/service-images-manager.tsx
  85. 9 12
      src/features/vehicles/Components/service-video-manager.tsx
  86. 2 1
      src/features/workboard/Components/BoardJobCard.tsx
  87. 33 0
      src/lib/interactive-row.ts

+ 17 - 2
messages/de/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} gut",
     "fair": "{count} akzeptabel",
     "poor": "{count} schlecht",
-    "view": "Ansehen"
+    "view": "Ansehen",
+    "serviceHistoryTitle": "Servicehistorie",
+    "inspectionsTitle": "Inspektionen",
+    "columnVehicle": "Fahrzeug",
+    "columnPlate": "Kennzeichen",
+    "columnServices": "Services",
+    "columnInspections": "Inspektionen"
   },
   "requestService": {
     "title": "Service anfordern",
@@ -174,6 +180,15 @@
     "yourRequests": "Ihre Serviceanfragen",
     "failedToLoad": "Daten konnten nicht geladen werden.",
     "staffNotes": "Mitarbeiterhinweise: {notes}",
-    "preferred": "Bevorzugt: {date}"
+    "preferred": "Bevorzugt: {date}",
+    "notListedOption": "Mein Fahrzeug ist nicht aufgeführt",
+    "newVehicleHint": "Beschreiben Sie das Fahrzeug, wir fügen es Ihrem Profil hinzu.",
+    "fillVehicleDetails": "Bitte Marke und Modell des Fahrzeugs angeben",
+    "makeLabel": "Marke",
+    "makePlaceholder": "z. B. Ford",
+    "modelLabel": "Modell",
+    "modelPlaceholder": "z. B. Kuga",
+    "yearLabel": "Baujahr",
+    "plateLabel": "Kennzeichen"
   }
 }

+ 17 - 2
messages/en/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} good",
     "fair": "{count} fair",
     "poor": "{count} poor",
-    "view": "View"
+    "view": "View",
+    "serviceHistoryTitle": "Service history",
+    "inspectionsTitle": "Inspections",
+    "columnVehicle": "Vehicle",
+    "columnPlate": "Plate",
+    "columnServices": "Services",
+    "columnInspections": "Inspections"
   },
   "requestService": {
     "title": "Request Service",
@@ -174,6 +180,15 @@
     "yourRequests": "Your Service Requests",
     "failedToLoad": "Failed to load data.",
     "staffNotes": "Staff notes: {notes}",
-    "preferred": "Preferred: {date}"
+    "preferred": "Preferred: {date}",
+    "notListedOption": "My vehicle isn't listed",
+    "newVehicleHint": "Tell us about the vehicle and we'll add it to your profile.",
+    "fillVehicleDetails": "Please enter the vehicle's make and model",
+    "makeLabel": "Make",
+    "makePlaceholder": "e.g. Ford",
+    "modelLabel": "Model",
+    "modelPlaceholder": "e.g. Kuga",
+    "yearLabel": "Year",
+    "plateLabel": "License plate"
   }
 }

+ 17 - 2
messages/es/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} bueno",
     "fair": "{count} regular",
     "poor": "{count} malo",
-    "view": "Ver"
+    "view": "Ver",
+    "serviceHistoryTitle": "Historial de servicio",
+    "inspectionsTitle": "Inspecciones",
+    "columnVehicle": "Vehículo",
+    "columnPlate": "Matrícula",
+    "columnServices": "Servicios",
+    "columnInspections": "Inspecciones"
   },
   "requestService": {
     "title": "Solicitar servicio",
@@ -174,6 +180,15 @@
     "yourRequests": "Sus solicitudes de servicio",
     "failedToLoad": "Error al cargar los datos.",
     "staffNotes": "Notas del personal: {notes}",
-    "preferred": "Preferido: {date}"
+    "preferred": "Preferido: {date}",
+    "notListedOption": "Mi vehículo no está en la lista",
+    "newVehicleHint": "Cuéntanos sobre el vehículo y lo añadiremos a tu perfil.",
+    "fillVehicleDetails": "Introduce la marca y el modelo del vehículo",
+    "makeLabel": "Marca",
+    "makePlaceholder": "p. ej. Ford",
+    "modelLabel": "Modelo",
+    "modelPlaceholder": "p. ej. Kuga",
+    "yearLabel": "Año",
+    "plateLabel": "Matrícula"
   }
 }

+ 17 - 2
messages/fr/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} bon(s)",
     "fair": "{count} passable(s)",
     "poor": "{count} mauvais",
-    "view": "Voir"
+    "view": "Voir",
+    "serviceHistoryTitle": "Historique d'entretien",
+    "inspectionsTitle": "Inspections",
+    "columnVehicle": "Véhicule",
+    "columnPlate": "Plaque",
+    "columnServices": "Services",
+    "columnInspections": "Inspections"
   },
   "requestService": {
     "title": "Demander un service",
@@ -174,6 +180,15 @@
     "yourRequests": "Vos demandes de service",
     "failedToLoad": "Échec du chargement des données.",
     "staffNotes": "Notes du personnel : {notes}",
-    "preferred": "Souhaité : {date}"
+    "preferred": "Souhaité : {date}",
+    "notListedOption": "Mon véhicule n'est pas dans la liste",
+    "newVehicleHint": "Décrivez le véhicule et nous l'ajouterons à votre profil.",
+    "fillVehicleDetails": "Veuillez saisir la marque et le modèle du véhicule",
+    "makeLabel": "Marque",
+    "makePlaceholder": "p. ex. Ford",
+    "modelLabel": "Modèle",
+    "modelPlaceholder": "p. ex. Kuga",
+    "yearLabel": "Année",
+    "plateLabel": "Plaque d'immatriculation"
   }
 }

+ 17 - 2
messages/it/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} buono",
     "fair": "{count} discreto",
     "poor": "{count} scarso",
-    "view": "Visualizza"
+    "view": "Visualizza",
+    "serviceHistoryTitle": "Storico interventi",
+    "inspectionsTitle": "Ispezioni",
+    "columnVehicle": "Veicolo",
+    "columnPlate": "Targa",
+    "columnServices": "Interventi",
+    "columnInspections": "Ispezioni"
   },
   "requestService": {
     "title": "Richiedi Servizio",
@@ -174,6 +180,15 @@
     "yourRequests": "Le Tue Richieste di Servizio",
     "failedToLoad": "Caricamento dei dati non riuscito.",
     "staffNotes": "Note del personale: {notes}",
-    "preferred": "Preferita: {date}"
+    "preferred": "Preferita: {date}",
+    "notListedOption": "Il mio veicolo non è in elenco",
+    "newVehicleHint": "Descrivi il veicolo e lo aggiungeremo al tuo profilo.",
+    "fillVehicleDetails": "Inserisci marca e modello del veicolo",
+    "makeLabel": "Marca",
+    "makePlaceholder": "es. Ford",
+    "modelLabel": "Modello",
+    "modelPlaceholder": "es. Kuga",
+    "yearLabel": "Anno",
+    "plateLabel": "Targa"
   }
 }

+ 17 - 2
messages/lt/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} gerai",
     "fair": "{count} vidutiniškai",
     "poor": "{count} blogai",
-    "view": "Peržiūrėti"
+    "view": "Peržiūrėti",
+    "serviceHistoryTitle": "Aptarnavimo istorija",
+    "inspectionsTitle": "Apžiūros",
+    "columnVehicle": "Transporto priemonė",
+    "columnPlate": "Valst. nr.",
+    "columnServices": "Aptarnavimai",
+    "columnInspections": "Apžiūros"
   },
   "requestService": {
     "title": "Užsakyti paslaugą",
@@ -174,6 +180,15 @@
     "yourRequests": "Jūsų paslaugų užklausos",
     "failedToLoad": "Nepavyko įkelti duomenų.",
     "staffNotes": "Personalo pastabos: {notes}",
-    "preferred": "Pageidaujama: {date}"
+    "preferred": "Pageidaujama: {date}",
+    "notListedOption": "Mano transporto priemonės sąraše nėra",
+    "newVehicleHint": "Papasakokite apie transporto priemonę – pridėsime ją prie jūsų profilio.",
+    "fillVehicleDetails": "Įveskite transporto priemonės markę ir modelį",
+    "makeLabel": "Markė",
+    "makePlaceholder": "pvz., Ford",
+    "modelLabel": "Modelis",
+    "modelPlaceholder": "pvz., Kuga",
+    "yearLabel": "Metai",
+    "plateLabel": "Valstybinis numeris"
   }
 }

+ 17 - 2
messages/nb/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} bra",
     "fair": "{count} middels",
     "poor": "{count} dårlig",
-    "view": "Se"
+    "view": "Se",
+    "serviceHistoryTitle": "Servicehistorikk",
+    "inspectionsTitle": "Inspeksjoner",
+    "columnVehicle": "Kjøretøy",
+    "columnPlate": "Reg.nr.",
+    "columnServices": "Tjenester",
+    "columnInspections": "Kontroller"
   },
   "requestService": {
     "title": "Be om service",
@@ -174,6 +180,15 @@
     "yourRequests": "Dine serviceforespørsler",
     "failedToLoad": "Kunne ikke laste inn data.",
     "staffNotes": "Personalnotater: {notes}",
-    "preferred": "Ønsket: {date}"
+    "preferred": "Ønsket: {date}",
+    "notListedOption": "Kjøretøyet mitt står ikke i listen",
+    "newVehicleHint": "Fortell oss om kjøretøyet, så legger vi det til i din profil.",
+    "fillVehicleDetails": "Fyll inn kjøretøyets merke og modell",
+    "makeLabel": "Merke",
+    "makePlaceholder": "f.eks. Ford",
+    "modelLabel": "Modell",
+    "modelPlaceholder": "f.eks. Kuga",
+    "yearLabel": "Årsmodell",
+    "plateLabel": "Registreringsnummer"
   }
 }

+ 17 - 2
messages/nl/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} goed",
     "fair": "{count} redelijk",
     "poor": "{count} slecht",
-    "view": "Bekijken"
+    "view": "Bekijken",
+    "serviceHistoryTitle": "Servicegeschiedenis",
+    "inspectionsTitle": "Inspecties",
+    "columnVehicle": "Voertuig",
+    "columnPlate": "Kenteken",
+    "columnServices": "Services",
+    "columnInspections": "Inspecties"
   },
   "requestService": {
     "title": "Service aanvragen",
@@ -174,6 +180,15 @@
     "yourRequests": "Uw serviceaanvragen",
     "failedToLoad": "Gegevens laden mislukt.",
     "staffNotes": "Medewerkernotities: {notes}",
-    "preferred": "Gewenst: {date}"
+    "preferred": "Gewenst: {date}",
+    "notListedOption": "Mijn voertuig staat er niet bij",
+    "newVehicleHint": "Vertel ons over het voertuig, dan voegen we het toe aan je profiel.",
+    "fillVehicleDetails": "Vul het merk en model van het voertuig in",
+    "makeLabel": "Merk",
+    "makePlaceholder": "bijv. Ford",
+    "modelLabel": "Model",
+    "modelPlaceholder": "bijv. Kuga",
+    "yearLabel": "Bouwjaar",
+    "plateLabel": "Kenteken"
   }
 }

+ 17 - 2
messages/pl/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} dobry",
     "fair": "{count} dostateczny",
     "poor": "{count} zły",
-    "view": "Zobacz"
+    "view": "Zobacz",
+    "serviceHistoryTitle": "Historia serwisowa",
+    "inspectionsTitle": "Przeglądy",
+    "columnVehicle": "Pojazd",
+    "columnPlate": "Rejestracja",
+    "columnServices": "Usługi",
+    "columnInspections": "Przeglądy"
   },
   "requestService": {
     "title": "Zamów serwis",
@@ -174,6 +180,15 @@
     "yourRequests": "Twoje zlecenia serwisowe",
     "failedToLoad": "Nie udało się załadować danych.",
     "staffNotes": "Uwagi personelu: {notes}",
-    "preferred": "Preferowana: {date}"
+    "preferred": "Preferowana: {date}",
+    "notListedOption": "Mojego pojazdu nie ma na liście",
+    "newVehicleHint": "Opisz pojazd, a dodamy go do Twojego profilu.",
+    "fillVehicleDetails": "Podaj markę i model pojazdu",
+    "makeLabel": "Marka",
+    "makePlaceholder": "np. Ford",
+    "modelLabel": "Model",
+    "modelPlaceholder": "np. Kuga",
+    "yearLabel": "Rocznik",
+    "plateLabel": "Numer rejestracyjny"
   }
 }

+ 17 - 2
messages/pt-BR/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} bom",
     "fair": "{count} regular",
     "poor": "{count} ruim",
-    "view": "Ver"
+    "view": "Ver",
+    "serviceHistoryTitle": "Histórico de serviços",
+    "inspectionsTitle": "Inspeções",
+    "columnVehicle": "Veículo",
+    "columnPlate": "Placa",
+    "columnServices": "Serviços",
+    "columnInspections": "Inspeções"
   },
   "requestService": {
     "title": "Solicitar Serviço",
@@ -174,6 +180,15 @@
     "yourRequests": "Suas Solicitações de Serviço",
     "failedToLoad": "Falha ao carregar os dados.",
     "staffNotes": "Notas da equipe: {notes}",
-    "preferred": "Preferido: {date}"
+    "preferred": "Preferido: {date}",
+    "notListedOption": "Meu veículo não está na lista",
+    "newVehicleHint": "Conte-nos sobre o veículo e o adicionaremos ao seu perfil.",
+    "fillVehicleDetails": "Informe a marca e o modelo do veículo",
+    "makeLabel": "Marca",
+    "makePlaceholder": "ex.: Ford",
+    "modelLabel": "Modelo",
+    "modelPlaceholder": "ex.: Kuga",
+    "yearLabel": "Ano",
+    "plateLabel": "Placa"
   }
 }

+ 17 - 2
messages/ru/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} норма",
     "fair": "{count} удовл.",
     "poor": "{count} плохо",
-    "view": "Просмотр"
+    "view": "Просмотр",
+    "serviceHistoryTitle": "История обслуживания",
+    "inspectionsTitle": "Осмотры",
+    "columnVehicle": "Автомобиль",
+    "columnPlate": "Госномер",
+    "columnServices": "Обслуживания",
+    "columnInspections": "Осмотры"
   },
   "requestService": {
     "title": "Заявка на обслуживание",
@@ -174,6 +180,15 @@
     "yourRequests": "Ваши заявки на обслуживание",
     "failedToLoad": "Не удалось загрузить данные.",
     "staffNotes": "Заметки персонала: {notes}",
-    "preferred": "Предпочтительно: {date}"
+    "preferred": "Предпочтительно: {date}",
+    "notListedOption": "Моего автомобиля нет в списке",
+    "newVehicleHint": "Расскажите об автомобиле — мы добавим его в ваш профиль.",
+    "fillVehicleDetails": "Укажите марку и модель автомобиля",
+    "makeLabel": "Марка",
+    "makePlaceholder": "напр., Ford",
+    "modelLabel": "Модель",
+    "modelPlaceholder": "напр., Kuga",
+    "yearLabel": "Год выпуска",
+    "plateLabel": "Госномер"
   }
 }

+ 17 - 2
messages/tr/portal.json

@@ -152,7 +152,13 @@
     "good": "{count} iyi",
     "fair": "{count} orta",
     "poor": "{count} kötü",
-    "view": "Görüntüle"
+    "view": "Görüntüle",
+    "serviceHistoryTitle": "Servis geçmişi",
+    "inspectionsTitle": "Muayeneler",
+    "columnVehicle": "Araç",
+    "columnPlate": "Plaka",
+    "columnServices": "Servisler",
+    "columnInspections": "Muayeneler"
   },
   "requestService": {
     "title": "Servis İste",
@@ -174,6 +180,15 @@
     "yourRequests": "Servis Talepleriniz",
     "failedToLoad": "Veriler yüklenemedi.",
     "staffNotes": "Personel notları: {notes}",
-    "preferred": "Tercih edilen: {date}"
+    "preferred": "Tercih edilen: {date}",
+    "notListedOption": "Aracım listede yok",
+    "newVehicleHint": "Aracınızı anlatın, profilinize ekleyelim.",
+    "fillVehicleDetails": "Lütfen aracın markasını ve modelini girin",
+    "makeLabel": "Marka",
+    "makePlaceholder": "örn. Ford",
+    "modelLabel": "Model",
+    "modelPlaceholder": "örn. Kuga",
+    "yearLabel": "Model yılı",
+    "plateLabel": "Plaka"
   }
 }

+ 2 - 1
src/app/(authenticated)/audit-log/audit-log-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useCallback, useState, useTransition } from "react";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { useTranslations } from "next-intl";
@@ -242,7 +243,7 @@ export function AuditLogClient({
               data.logs.map((log) => (
                 <ContextMenu key={log.id} modal={false}>
                 <ContextMenuTrigger asChild>
-                <TableRow className="cursor-pointer" onClick={() => setSelectedLog(log)}>
+                <TableRow className="cursor-pointer" {...interactiveRow(() => setSelectedLog(log))}>
                   <TableCell className="text-xs text-muted-foreground whitespace-nowrap" suppressHydrationWarning>
                     {formatDateTime(log.timestamp)}
                   </TableCell>

+ 5 - 4
src/app/(authenticated)/billing/billing-client.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 import { useCallback, useTransition } from "react";
 import Link from "next/link";
@@ -227,7 +228,7 @@ export default function BillingClient({
       {/* Summary Cards — hidden below md so the invoice list is what a phone
           or portrait tablet opens on. */}
       <div className="hidden gap-3 md:grid md:grid-cols-3">
-        <Card className="border-0 shadow-sm">
+        <Card>
           <CardContent className="flex items-center gap-3 px-4 py-3">
             <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-500/10">
               <DollarSign className="h-4 w-4 text-blue-600" />
@@ -241,7 +242,7 @@ export default function BillingClient({
           </CardContent>
         </Card>
 
-        <Card className="border-0 shadow-sm">
+        <Card>
           <CardContent className="flex items-center gap-3 px-4 py-3">
             <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-emerald-500/10">
               <TrendingUp className="h-4 w-4 text-emerald-600" />
@@ -255,7 +256,7 @@ export default function BillingClient({
           </CardContent>
         </Card>
 
-        <Card className="border-0 shadow-sm">
+        <Card>
           <CardContent className="flex items-center gap-3 px-4 py-3">
             <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-red-500/10">
               <AlertCircle className="h-4 w-4 text-red-600" />
@@ -434,7 +435,7 @@ export default function BillingClient({
                   <TableRow
                     key={record.id}
                     className="cursor-pointer hover:bg-muted/50"
-                    onClick={() => handleRowClick(record)}
+                    {...interactiveRow(() => handleRowClick(record))}
                   >
                     <TableCell className="truncate font-medium">
                       {record.invoiceNumber || "\u2014"}

+ 2 - 1
src/app/(authenticated)/customers/[id]/customer-detail-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useState, useTransition } from "react";
 import Link from "next/link";
 import { useRouter, useSearchParams } from "next/navigation";
@@ -395,7 +396,7 @@ export function CustomerDetailClient({
                       <TableRow
                         key={v.id}
                         className="cursor-pointer"
-                        onClick={() => router.push(`/vehicles/${v.id}?back=${encodeURIComponent(`/customers/${customer.id}`)}`)}
+                        {...interactiveRow(() => router.push(`/vehicles/${v.id}?back=${encodeURIComponent(`/customers/${customer.id}`)}`))}
                       >
                         <TableCell className="font-mono text-sm">
                           {v.licensePlate || "-"}

+ 2 - 1
src/app/(authenticated)/customers/customers-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useState, useCallback, useTransition, useEffect } from "react";
@@ -413,7 +414,7 @@ export function CustomersClient({
                 <ContextMenuTrigger asChild>
                 <TableRow
                   className={`cursor-pointer ${selected.has(c.id) ? "bg-muted/50" : ""}`}
-                  onClick={() => router.push(`/customers/${c.id}`)}
+                  {...interactiveRow(() => router.push(`/customers/${c.id}`))}
                 >
                   <TableCell className="w-[40px]">
                     <Checkbox

+ 213 - 234
src/app/(authenticated)/dashboard-client.tsx

@@ -1,11 +1,12 @@
 "use client";
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useState, useTransition } from "react";
 import { useRouter } from "next/navigation";
 import Link from "next/link";
 import { useTranslations } from "next-intl";
 import { useFormatDate } from "@/lib/use-format-date";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import {
@@ -411,14 +412,14 @@ export function DashboardClient({
     <div className="space-y-4">
       {/* Quick stats row */}
       <div className={`grid grid-cols-2 gap-2 ${stats.isAdmin ? (stats.lowStockParts > 0 ? "sm:grid-cols-5" : "sm:grid-cols-4") : "sm:grid-cols-2"}`}>
-        <Link href="/work-orders?status=active" className="rounded-lg border border-0 shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
+        <Link href="/work-orders?status=active" className="rounded-lg border border-card-edge shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
           <div className="flex items-center justify-between">
             <span className="text-[11px] text-muted-foreground">{t("stats.activeJobs")}</span>
             <ClipboardList className="h-3.5 w-3.5 text-muted-foreground/50" />
           </div>
           <p className="text-lg font-bold">{stats.activeJobs}</p>
         </Link>
-        <Link href="/work-orders?status=pending" className="rounded-lg border border-0 shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
+        <Link href="/work-orders?status=pending" className="rounded-lg border border-card-edge shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
           <div className="flex items-center justify-between">
             <span className="text-[11px] text-muted-foreground">{t("stats.pending")}</span>
             <Clock className="h-3.5 w-3.5 text-muted-foreground/50" />
@@ -426,7 +427,7 @@ export function DashboardClient({
           <p className="text-lg font-bold">{stats.pendingJobs}</p>
         </Link>
         {stats.isAdmin && (
-          <Link href="/inventory" className="rounded-lg border border-0 shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
+          <Link href="/inventory" className="rounded-lg border border-card-edge shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
             <div className="flex items-center justify-between">
               <span className="text-[11px] text-muted-foreground">{t("stats.totalParts")}</span>
               <Settings className="h-3.5 w-3.5 text-muted-foreground/50" />
@@ -437,7 +438,7 @@ export function DashboardClient({
         {stats.isAdmin && stats.lowStockParts > 0 && (
           <Link
             href="/inventory?lowStock=1"
-            className="rounded-lg border-0 bg-amber-50 px-3 py-2 shadow-sm transition-colors hover:bg-amber-100 dark:bg-amber-950/40 dark:hover:bg-amber-950/60"
+            className="rounded-lg border border-amber-500/30 bg-amber-50 px-3 py-2 shadow-sm transition-colors hover:bg-amber-100 dark:bg-amber-950/40 dark:hover:bg-amber-950/60"
           >
             <div className="flex items-center justify-between">
               <span className="text-[11px] text-amber-700 dark:text-amber-500">{t("stats.lowStock")}</span>
@@ -447,7 +448,7 @@ export function DashboardClient({
           </Link>
         )}
         {stats.isAdmin && (
-          <Link href="/customers" className="rounded-lg border border-0 shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
+          <Link href="/customers" className="rounded-lg border border-card-edge shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
             <div className="flex items-center justify-between">
               <span className="text-[11px] text-muted-foreground">{t("stats.totalCustomers")}</span>
               <Users className="h-3.5 w-3.5 text-muted-foreground/50" />
@@ -542,51 +543,48 @@ export function DashboardClient({
         const cardNodes: Partial<Record<string, ReactNode>> = {
         // Vehicles Due for Service
         maintenance: (
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="pb-1">
-              <div className="flex items-center justify-between">
-                <CardTitle className="flex items-center gap-2 text-base">
-                  <Gauge className="h-4 w-4" />
-                  {t("maintenance.title")}
-                </CardTitle>
-                <Tooltip>
-                  <TooltipTrigger asChild>
-                    <Button
-                      variant="ghost"
-                      size="icon"
-                      className="h-7 w-7 text-muted-foreground hover:text-foreground"
-                      onClick={() => router.push("/settings/maintenance")}
-                      aria-label={t("maintenance.settingsAriaLabel")}
-                    >
-                      <Settings className="h-3.5 w-3.5" />
-                    </Button>
-                  </TooltipTrigger>
-                  <TooltipContent>{t("maintenance.settingsAriaLabel")}</TooltipContent>
-                </Tooltip>
-              </div>
-              <p className="text-xs text-muted-foreground">
-                {t("maintenance.description")}
-              </p>
-              <div className="flex gap-1 pt-1">
-                  <Button
-                    variant={maintenanceTab === "active" ? "default" : "ghost"}
-                    size="sm"
-                    className="h-7 text-xs"
-                    onClick={() => setMaintenanceTab("active")}
-                  >
-                    {t("maintenance.active")} ({vehiclesDueForService.length})
-                  </Button>
+          <AppCard
+            icon={Gauge}
+            title={t("maintenance.title")}
+            description={t("maintenance.description")}
+            action={
+              <Tooltip>
+                <TooltipTrigger asChild>
                   <Button
-                    variant={maintenanceTab === "dismissed" ? "default" : "ghost"}
-                    size="sm"
-                    className="h-7 text-xs"
-                    onClick={() => setMaintenanceTab("dismissed")}
+                    variant="ghost"
+                    size="icon"
+                    className="h-7 w-7 text-muted-foreground hover:text-foreground"
+                    onClick={() => router.push("/settings/maintenance")}
+                    aria-label={t("maintenance.settingsAriaLabel")}
                   >
-                    {t("maintenance.dismissedTab")} ({dismissedMaintenanceVehicles.length})
+                    <Settings className="h-3.5 w-3.5" />
                   </Button>
+                </TooltipTrigger>
+                <TooltipContent>{t("maintenance.settingsAriaLabel")}</TooltipContent>
+              </Tooltip>
+            }
+            subheader={
+              <div className="flex gap-1">
+                <Button
+                  variant={maintenanceTab === "active" ? "default" : "ghost"}
+                  size="sm"
+                  className="h-7 text-xs"
+                  onClick={() => setMaintenanceTab("active")}
+                >
+                  {t("maintenance.active")} ({vehiclesDueForService.length})
+                </Button>
+                <Button
+                  variant={maintenanceTab === "dismissed" ? "default" : "ghost"}
+                  size="sm"
+                  className="h-7 text-xs"
+                  onClick={() => setMaintenanceTab("dismissed")}
+                >
+                  {t("maintenance.dismissedTab")} ({dismissedMaintenanceVehicles.length})
+                </Button>
               </div>
-            </CardHeader>
-            <CardContent className="p-0">
+            }
+            contentClassName="p-0"
+          >
               {maintenanceTab === "active" && (
                 <div className="divide-y">
                   {vehiclesDueForService.length === 0 ? (
@@ -595,7 +593,7 @@ export function DashboardClient({
                     <div
                       key={v.vehicleId}
                       className="flex items-center justify-between px-5 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
-                      onClick={() => router.push(`/vehicles/${v.vehicleId}`)}
+                      {...interactiveRow(() => router.push(`/vehicles/${v.vehicleId}`))}
                     >
                       <div className="flex items-center gap-3 min-w-0">
                         <div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${
@@ -664,7 +662,7 @@ export function DashboardClient({
                     <div
                       key={v.vehicleId}
                       className="flex items-center justify-between px-5 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
-                      onClick={() => router.push(`/vehicles/${v.vehicleId}`)}
+                      {...interactiveRow(() => router.push(`/vehicles/${v.vehicleId}`))}
                     >
                       <div className="flex items-center gap-3 min-w-0">
                         <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted">
@@ -701,31 +699,27 @@ export function DashboardClient({
                   ))}
                 </div>
               )}
-            </CardContent>
-          </Card>
+          </AppCard>
         ),
 
         // Upcoming Reminders
         reminders: (
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="pb-3">
-              <div className="flex items-center justify-between">
-                <CardTitle className="flex items-center gap-2 text-base">
-                  <Bell className="h-4 w-4" />
-                  {t("reminders.title")}
-                </CardTitle>
-                <Button
-                  variant="ghost"
-                  size="sm"
-                  className="h-7 text-xs gap-1"
-                  onClick={() => router.push("/reminders")}
-                >
-                  {t("viewAll")}
-                  <ArrowRight className="h-3 w-3" />
-                </Button>
-              </div>
-            </CardHeader>
-            <CardContent className="p-0">
+          <AppCard
+            icon={Bell}
+            title={t("reminders.title")}
+            badge={upcomingReminders.length || undefined}
+            contentClassName="p-0"
+            footer={
+              <button
+                type="button"
+                onClick={() => router.push("/reminders")}
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
+              >
+                {t("viewAll")}
+                <ArrowRight className="h-3 w-3" />
+              </button>
+            }
+          >
               {upcomingReminders.length === 0 ? (
                 <p className="px-5 py-4 text-xs text-muted-foreground">{t("reminders.noData")}</p>
               ) : (
@@ -741,7 +735,7 @@ export function DashboardClient({
                     <div
                       key={r.id}
                       className="flex items-center justify-between px-5 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
-                      onClick={() => router.push(r.vehicle ? `/vehicles/${r.vehicle.id}?tab=reminders` : "/reminders")}
+                      {...interactiveRow(() => router.push(r.vehicle ? `/vehicles/${r.vehicle.id}?tab=reminders` : "/reminders"))}
                     >
                       <div className="flex items-center gap-3 min-w-0">
                         <div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${
@@ -786,31 +780,27 @@ export function DashboardClient({
                 })}
               </div>
               )}
-            </CardContent>
-          </Card>
+          </AppCard>
         ),
 
         // SMS Messages (only offered when SMS is enabled; see availableIds)
         sms: (
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="pb-3">
-              <div className="flex items-center justify-between">
-                <CardTitle className="flex items-center gap-2 text-base">
-                  <MessageSquare className="h-4 w-4" />
-                  {t("messages.title")}
-                </CardTitle>
-                <Button
-                  variant="ghost"
-                  size="sm"
-                  className="h-7 text-xs gap-1"
-                  onClick={() => router.push("/messages")}
-                >
-                  {t("messages.viewAll")}
-                  <ArrowRight className="h-3 w-3" />
-                </Button>
-              </div>
-            </CardHeader>
-            <CardContent className="p-0">
+          <AppCard
+            icon={MessageSquare}
+            title={t("messages.title")}
+            badge={smsThreads.length || undefined}
+            contentClassName="p-0"
+            footer={
+              <button
+                type="button"
+                onClick={() => router.push("/messages")}
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
+              >
+                {t("messages.viewAll")}
+                <ArrowRight className="h-3 w-3" />
+              </button>
+            }
+          >
               {smsThreads.length === 0 ? (
                 <p className="px-5 py-4 text-xs text-muted-foreground">{t("messages.noData")}</p>
               ) : (
@@ -819,7 +809,7 @@ export function DashboardClient({
                   <div
                     key={thread.customerId}
                     className="flex items-center justify-between px-5 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
-                    onClick={() => router.push(`/messages?customerId=${thread.customerId}`)}
+                    {...interactiveRow(() => router.push(`/messages?customerId=${thread.customerId}`))}
                   >
                     <div className="flex items-center gap-3 min-w-0">
                       <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10">
@@ -840,22 +830,17 @@ export function DashboardClient({
                 ))}
               </div>
               )}
-            </CardContent>
-          </Card>
+          </AppCard>
         ),
 
         // Recent Notifications (only offered when SMS is not configured)
         notifications: (
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="pb-3">
-              <div className="flex items-center justify-between">
-                <CardTitle className="flex items-center gap-2 text-base">
-                  <BellRing className="h-4 w-4" />
-                  {t("notifications.title")}
-                </CardTitle>
-              </div>
-            </CardHeader>
-            <CardContent className="p-0">
+          <AppCard
+            icon={BellRing}
+            title={t("notifications.title")}
+            badge={notifications.length || undefined}
+            contentClassName="p-0"
+          >
               {notifications.length === 0 ? (
                 <p className="px-5 py-4 text-xs text-muted-foreground">{t("notifications.noData")}</p>
               ) : (
@@ -864,7 +849,7 @@ export function DashboardClient({
                   <div
                     key={n.id}
                     className="flex items-center justify-between px-5 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
-                    onClick={() => router.push(n.entityUrl)}
+                    {...interactiveRow(() => router.push(n.entityUrl))}
                   >
                     <div className="flex items-center gap-3 min-w-0">
                       <div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${n.read ? "bg-muted" : "bg-primary/10"}`}>
@@ -882,31 +867,26 @@ export function DashboardClient({
                 ))}
               </div>
               )}
-            </CardContent>
-          </Card>
+          </AppCard>
         ),
 
         // Inspections
         inspections: (
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="pb-3">
-              <div className="flex items-center justify-between">
-                <CardTitle className="flex items-center gap-2 text-base">
-                  <ClipboardCheck className="h-4 w-4" />
-                  {t("inspections.title")}
-                </CardTitle>
-                <Button
-                  variant="ghost"
-                  size="sm"
-                  className="h-7 text-xs gap-1"
-                  onClick={() => router.push("/inspections")}
-                >
-                  {t("viewAll")}
-                  <ArrowRight className="h-3 w-3" />
-                </Button>
-              </div>
-            </CardHeader>
-            <CardContent className="p-0">
+          <AppCard
+            icon={ClipboardCheck}
+            title={t("inspections.title")}
+            contentClassName="p-0"
+            footer={
+              <button
+                type="button"
+                onClick={() => router.push("/inspections")}
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
+              >
+                {t("viewAll")}
+                <ArrowRight className="h-3 w-3" />
+              </button>
+            }
+          >
               {inProgressInspections.length === 0 && completedInspections.length === 0 ? (
                 <p className="px-5 py-4 text-xs text-muted-foreground">{t("inspections.noData")}</p>
               ) : (
@@ -922,7 +902,7 @@ export function DashboardClient({
                     <div
                       key={insp.id}
                       className="flex items-center justify-between px-5 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
-                      onClick={() => router.push(`/inspections/${insp.id}`)}
+                      {...interactiveRow(() => router.push(`/inspections/${insp.id}`))}
                     >
                       <div className="flex items-center gap-3 min-w-0">
                         <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-500/10">
@@ -963,7 +943,7 @@ export function DashboardClient({
                     <div
                       key={insp.id}
                       className="flex items-center justify-between px-5 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
-                      onClick={() => router.push(`/inspections/${insp.id}`)}
+                      {...interactiveRow(() => router.push(`/inspections/${insp.id}`))}
                     >
                       <div className="flex items-center gap-3 min-w-0">
                         <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-500/10">
@@ -996,34 +976,28 @@ export function DashboardClient({
                 })}
               </div>
               )}
-            </CardContent>
-          </Card>
+          </AppCard>
         ),
 
         // Quote Requests
         quoteRequests: (
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="pb-3">
-              <div className="flex items-center justify-between">
-                <CardTitle className="flex items-center gap-2 text-base">
-                  <FileText className="h-4 w-4" />
-                  {t("quoteRequests.title")}
-                </CardTitle>
-                <Button
-                  variant="ghost"
-                  size="sm"
-                  className="h-7 text-xs gap-1"
-                  onClick={() => router.push("/inspections")}
-                >
-                  {t("viewAll")}
-                  <ArrowRight className="h-3 w-3" />
-                </Button>
-              </div>
-              <p className="text-xs text-muted-foreground">
-                {t("quoteRequests.description")}
-              </p>
-            </CardHeader>
-            <CardContent className="p-0">
+          <AppCard
+            icon={FileText}
+            title={t("quoteRequests.title")}
+            badge={quoteRequests.length || undefined}
+            description={t("quoteRequests.description")}
+            contentClassName="p-0"
+            footer={
+              <button
+                type="button"
+                onClick={() => router.push("/inspections")}
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
+              >
+                {t("viewAll")}
+                <ArrowRight className="h-3 w-3" />
+              </button>
+            }
+          >
               {quoteRequests.length === 0 ? (
                 <p className="px-5 py-4 text-xs text-muted-foreground">{t("quoteRequests.noData")}</p>
               ) : (
@@ -1039,7 +1013,7 @@ export function DashboardClient({
                     >
                       <div
                         className="flex items-center gap-3 min-w-0 cursor-pointer hover:opacity-80"
-                        onClick={() => router.push(`/inspections/${req.inspection.id}`)}
+                        {...interactiveRow(() => router.push(`/inspections/${req.inspection.id}`))}
                       >
                         <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10">
                           <FileText className="h-4 w-4 text-primary" />
@@ -1122,34 +1096,28 @@ export function DashboardClient({
                 })}
               </div>
               )}
-            </CardContent>
-          </Card>
+          </AppCard>
         ),
 
         // Customer Quote Responses
         quoteResponses: (
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="pb-3">
-              <div className="flex items-center justify-between">
-                <CardTitle className="flex items-center gap-2 text-base">
-                  <FileText className="h-4 w-4" />
-                  {t("quoteResponses.title")}
-                </CardTitle>
-                <Button
-                  variant="ghost"
-                  size="sm"
-                  className="h-7 text-xs gap-1"
-                  onClick={() => router.push("/quotes")}
-                >
-                  {t("viewAll")}
-                  <ArrowRight className="h-3 w-3" />
-                </Button>
-              </div>
-              <p className="text-xs text-muted-foreground">
-                {t("quoteResponses.description")}
-              </p>
-            </CardHeader>
-            <CardContent className="p-0">
+          <AppCard
+            icon={FileText}
+            title={t("quoteResponses.title")}
+            badge={quoteResponses.length || undefined}
+            description={t("quoteResponses.description")}
+            contentClassName="p-0"
+            footer={
+              <button
+                type="button"
+                onClick={() => router.push("/quotes")}
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
+              >
+                {t("viewAll")}
+                <ArrowRight className="h-3 w-3" />
+              </button>
+            }
+          >
               {quoteResponses.length === 0 ? (
                 <p className="px-5 py-4 text-xs text-muted-foreground">{t("quoteResponses.noData")}</p>
               ) : (
@@ -1161,7 +1129,7 @@ export function DashboardClient({
                   >
                     <div
                       className="flex items-center gap-3 min-w-0 cursor-pointer hover:opacity-80"
-                      onClick={() => router.push(`/quotes/${resp.id}`)}
+                      {...interactiveRow(() => router.push(`/quotes/${resp.id}`))}
                     >
                       <div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${
                         resp.status === "accepted" ? "bg-emerald-500/10" : "bg-orange-500/10"
@@ -1245,17 +1213,27 @@ export function DashboardClient({
                 ))}
               </div>
               )}
-            </CardContent>
-          </Card>
+          </AppCard>
         ),
 
         // Recent Completed table
         recentCompleted: (
-          <Card className="border-0 shadow-sm lg:col-span-2">
-            <CardHeader className="pb-3">
-              <CardTitle className="text-base">{t("recentCompleted.title")}</CardTitle>
-            </CardHeader>
-            <CardContent className="p-0">
+          <AppCard
+            icon={Check}
+            title={t("recentCompleted.title")}
+            className="lg:col-span-2"
+            contentClassName="p-0"
+            footer={
+              <button
+                type="button"
+                onClick={() => router.push("/work-orders?status=completed")}
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
+              >
+                {t("viewAll")}
+                <ArrowRight className="h-3 w-3" />
+              </button>
+            }
+          >
               {/* Card list (phones + small tablets) */}
               <div className="space-y-2 px-3 pb-3 md:hidden">
                 {stats.recentServices.length === 0 ? (
@@ -1335,10 +1313,10 @@ export function DashboardClient({
                         <ContextMenuTrigger asChild>
                         <TableRow
                           className={`cursor-pointer transition-opacity ${navigatingId === s.id ? "opacity-50" : ""}`}
-                          onClick={() => {
+                          {...interactiveRow(() => {
                             setNavigatingId(s.id);
                             router.push(recordHref);
-                          }}
+                          })}
                         >
                           <TableCell className="font-mono text-xs">
                             {formatDate(new Date(s.startDateTime ?? s.serviceDate))}
@@ -1409,17 +1387,28 @@ export function DashboardClient({
                 </TableBody>
               </Table>
               </div>
-            </CardContent>
-          </Card>
+          </AppCard>
         ),
 
         // Active Jobs table
         activeJobs: (
-          <Card className="border-0 shadow-sm lg:col-span-2">
-            <CardHeader className="pb-3">
-              <CardTitle className="text-base">{t("activeJobsTable.title")}</CardTitle>
-            </CardHeader>
-            <CardContent className="p-0">
+          <AppCard
+            icon={Clock}
+            title={t("activeJobsTable.title")}
+            badge={stats.todaysServices.length || undefined}
+            className="lg:col-span-2"
+            contentClassName="p-0"
+            footer={
+              <button
+                type="button"
+                onClick={() => router.push("/work-orders?status=active")}
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
+              >
+                {t("viewAll")}
+                <ArrowRight className="h-3 w-3" />
+              </button>
+            }
+          >
               {/* Card list (phones + small tablets) */}
               <div className="space-y-2 px-3 pb-3 md:hidden">
                 {stats.todaysServices.length === 0 ? (
@@ -1496,10 +1485,10 @@ export function DashboardClient({
                       <ContextMenuTrigger asChild>
                       <TableRow
                         className={`cursor-pointer transition-opacity ${navigatingId === s.id ? "opacity-50" : ""}`}
-                        onClick={() => {
+                        {...interactiveRow(() => {
                           setNavigatingId(s.id);
                           router.push(recordHref);
-                        }}
+                        })}
                       >
                         <TableCell>
                           {s.vehicle ? (
@@ -1570,30 +1559,25 @@ export function DashboardClient({
                 </TableBody>
               </Table>
               </div>
-            </CardContent>
-          </Card>
+          </AppCard>
         ),
         // Recent Activity (Audit Logs)
         recentActivity: (
-        <Card className="border-0 shadow-sm">
-          <CardHeader className="pb-1">
-            <div className="flex items-center justify-between">
-              <CardTitle className="flex items-center gap-2 text-base">
-                <ClipboardList className="h-4 w-4" />
-                {t("recentActivity")}
-              </CardTitle>
-              <Button
-                variant="ghost"
-                size="sm"
-                className="h-7 text-xs gap-1"
+        <AppCard
+          icon={ClipboardList}
+          title={t("recentActivity")}
+          contentClassName="p-0"
+          footer={
+              <button
+                type="button"
                 onClick={() => router.push("/audit-log")}
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
               >
                 {t("viewAll")}
                 <ArrowRight className="h-3 w-3" />
-              </Button>
-            </div>
-          </CardHeader>
-          <CardContent className="p-0">
+              </button>
+            }
+        >
             {recentAuditLogs.length === 0 ? (
               <p className="px-5 py-4 text-xs text-muted-foreground">{t("noRecentActivity")}</p>
             ) : (
@@ -1633,30 +1617,26 @@ export function DashboardClient({
                 })}
               </div>
             )}
-          </CardContent>
-        </Card>
+        </AppCard>
         ),
         // Recent Observations
         recentObservations: (
-        <Card className="border-0 shadow-sm">
-          <CardHeader className="pb-1">
-            <div className="flex items-center justify-between">
-              <CardTitle className="flex items-center gap-2 text-base">
-                <Eye className="h-4 w-4" />
-                {t("recentObservations")}
-              </CardTitle>
-              <Button
-                variant="ghost"
-                size="sm"
-                className="h-7 text-xs gap-1"
+        <AppCard
+          icon={Eye}
+          title={t("recentObservations")}
+          badge={recentObservations.length || undefined}
+          contentClassName="p-0"
+          footer={
+              <button
+                type="button"
                 onClick={() => router.push("/observations")}
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
               >
                 {t("viewAll")}
                 <ArrowRight className="h-3 w-3" />
-              </Button>
-            </div>
-          </CardHeader>
-          <CardContent className="p-0">
+              </button>
+            }
+        >
             {recentObservations.length === 0 ? (
               <p className="px-5 py-4 text-xs text-muted-foreground">{t("noRecentObservations")}</p>
             ) : (
@@ -1708,7 +1688,7 @@ export function DashboardClient({
                       <ContextMenuTrigger asChild>
                       <TableRow
                         className="cursor-pointer"
-                        onClick={() => router.push(`/vehicles/${obs.vehicle.id}?tab=findings`)}
+                        {...interactiveRow(() => router.push(`/vehicles/${obs.vehicle.id}?tab=findings`))}
                       >
                         <TableCell className="py-1.5">
                           <Badge
@@ -1751,8 +1731,7 @@ export function DashboardClient({
               </div>
               </>
             )}
-          </CardContent>
-        </Card>
+        </AppCard>
         ),
         };
         for (const w of widgets) {

+ 2 - 1
src/app/(authenticated)/inspections/inspections-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useState, useCallback, useTransition } from "react";
@@ -344,7 +345,7 @@ export function InspectionsClient({
                 <ContextMenuTrigger asChild>
                 <TableRow
                   className="cursor-pointer"
-                  onClick={() => router.push(`/inspections/${insp.id}`)}
+                  {...interactiveRow(() => router.push(`/inspections/${insp.id}`))}
                 >
                   <TableCell>
                     <TableCellLink href={`/vehicles/${insp.vehicle.id}`} block>

+ 3 - 2
src/app/(authenticated)/inventory/inventory-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 import { isLow as isLowStock } from '@/features/inventory/Lib/lowStockAlerts';
 
@@ -626,10 +627,10 @@ export function InventoryClient({
                   <ContextMenuTrigger asChild>
                   <TableRow
                     className="cursor-pointer"
-                    onClick={() => {
+                    {...interactiveRow(() => {
                       setEditPart(part);
                       setShowForm(true);
-                    }}
+                    })}
                   >
                     <TableCell className="w-[40px]" onClick={(e) => e.stopPropagation()}>
                       <Checkbox

+ 2 - 1
src/app/(authenticated)/labor-presets/labor-presets-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useState, useCallback, useTransition } from "react";
@@ -286,7 +287,7 @@ export function LaborPresetsClient({
                 <ContextMenuTrigger asChild>
                 <TableRow
                   className="cursor-pointer"
-                  onClick={() => handleEdit(preset.id)}
+                  {...interactiveRow(() => handleEdit(preset.id))}
                 >
                   <TableCell className="font-medium">{preset.name}</TableCell>
                   <TableCell className="hidden sm:table-cell text-muted-foreground">

+ 7 - 0
src/app/(authenticated)/layout.tsx

@@ -109,6 +109,13 @@ export default async function DashboardLayout({
     <ServiceTypeProvider serviceType={data.serviceType}>
     <LicenseExpiryProvider daysUntilExpiry={daysUntilExpiry} dismissed={licenseExpiryDismissed}>
     <WhiteLabelCtaProvider show={showWhiteLabelCta}>
+    {/* Accent line along the very top of the viewport — the card hairline at
+        page scale: primary on the left, gone by the far edge. Marks where the
+        app begins against the browser chrome. */}
+    <div
+      aria-hidden
+      className="pointer-events-none fixed inset-x-0 top-0 z-50 h-px bg-linear-to-r from-primary via-primary/35 to-transparent"
+    />
     {/* The demo banner already occupies the header, and demo image tags
         (demo-abc1234) are not versions a visitor should be notified about. */}
     {!isDemoMode && (

+ 2 - 1
src/app/(authenticated)/observations/observations-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useCallback, useTransition } from "react";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { useTranslations } from "next-intl";
@@ -225,7 +226,7 @@ export function ObservationsClient({
                   <ContextMenuTrigger asChild>
                   <TableRow
                     className="cursor-pointer"
-                    onClick={() => router.push(`/vehicles/${obs.vehicle.id}?tab=findings`)}
+                    {...interactiveRow(() => router.push(`/vehicles/${obs.vehicle.id}?tab=findings`))}
                   >
                     <TableCell>
                       <Badge variant="outline" className={`text-xs ${severityColors[obs.severity] || ""}`}>

+ 2 - 1
src/app/(authenticated)/quotes/quotes-client.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useState, useCallback, useTransition, useEffect } from 'react'
@@ -311,7 +312,7 @@ export function QuotesClient({
                 <ContextMenuTrigger asChild>
                 <TableRow
                   className="cursor-pointer"
-                  onClick={() => router.push(`/quotes/${q.id}`)}
+                  {...interactiveRow(() => router.push(`/quotes/${q.id}`))}
                 >
                   <TableCell className="font-mono text-xs text-muted-foreground">
                     {q.quoteNumber || '-'}

+ 144 - 217
src/app/(authenticated)/reports/reports-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useState, useEffect, useCallback } from "react";
 import { useSearchParams, useRouter, usePathname } from "next/navigation";
 import { useTranslations } from "next-intl";
@@ -13,12 +14,8 @@ import {
 import { cn } from "@/lib/utils";
 import { format } from "date-fns";
 import type { DateRange } from "react-day-picker";
-import {
-  Card,
-  CardContent,
-  CardHeader,
-  CardTitle,
-} from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
+import { Card, CardHeader, CardContent, CardTitle } from "@/components/ui/card";
 import {
   Table,
   TableBody,
@@ -820,19 +817,19 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
               <div className="space-y-4">
                 {/* Top-line metrics */}
                 <div className="grid gap-2 grid-cols-3">
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="px-3 py-1.5">
                       <p className="text-[11px] text-muted-foreground">{t("revenue.revenue")}</p>
                       <p className="text-base font-semibold truncate">{fmtCurrency(revenueData.summary.totalRevenue)}</p>
                     </CardContent>
                   </Card>
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="px-3 py-1.5">
                       <p className="text-[11px] text-muted-foreground">{t("revenue.collected")}</p>
                       <p className="text-base font-semibold truncate">{fmtCurrency(revenueData.summary.totalCollected)}</p>
                     </CardContent>
                   </Card>
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="px-3 py-1.5">
                       <p className="text-[11px] text-muted-foreground">{t("revenue.outstanding")}</p>
                       <p className="text-base font-semibold truncate">{fmtCurrency(revenueData.summary.outstanding)}</p>
@@ -841,11 +838,9 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                 </div>
 
                 {/* Net Profit breakdown */}
-                <Card className="border-0 shadow-sm">
-                  <CardHeader className="pb-2">
-                    <CardTitle className="text-sm font-medium">{t("revenue.netProfit")}</CardTitle>
-                  </CardHeader>
-                  <CardContent>
+                <AppCard
+                  title={t("revenue.netProfit")}
+                >
                     <div className="space-y-3">
                       <div className="flex items-center justify-between">
                         <div className="flex items-center gap-2">
@@ -866,38 +861,31 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                         <span className="text-base font-semibold tabular-nums">{fmtCurrency(revenueData.summary.netProfit)}</span>
                       </div>
                     </div>
-                  </CardContent>
-                </Card>
+                  </AppCard>
 
                 <div className="grid gap-4 lg:grid-cols-5">
-                  <Card className="border-0 shadow-sm lg:col-span-3">
-                    <CardHeader className="pb-2">
-                      <CardTitle className="text-sm font-medium">{t("revenue.monthlyRevenue")}</CardTitle>
-                    </CardHeader>
-                    <CardContent>
+                  <AppCard
+                    title={t("revenue.monthlyRevenue")}
+                    className="lg:col-span-3"
+                  >
                       <RevenueBarChart
                         data={revenueData.monthly}
                         formatCurrency={fmtCurrency}
                         labels={{ revenue: t("charts.revenue"), collected: t("charts.collected"), netProfit: t("revenue.netProfit") }}
                       />
-                    </CardContent>
-                  </Card>
-                  <Card className="border-0 shadow-sm lg:col-span-2">
-                    <CardHeader className="pb-2">
-                      <CardTitle className="text-sm font-medium">{t("revenue.revenueByType")}</CardTitle>
-                    </CardHeader>
-                    <CardContent>
+                    </AppCard>
+                  <AppCard
+                    title={t("revenue.revenueByType")}
+                    className="lg:col-span-2"
+                  >
                       <RevenueTypeDonut data={revenueData.byType} formatCurrency={fmtCurrency} />
-                    </CardContent>
-                  </Card>
+                    </AppCard>
                 </div>
 
                 {revenueData.monthly.length > 0 && (
-                  <Card className="border-0 shadow-sm">
-                    <CardHeader className="pb-2">
-                      <CardTitle className="text-sm font-medium">{t("revenue.monthlyBreakdown")}</CardTitle>
-                    </CardHeader>
-                    <CardContent>
+                  <AppCard
+                    title={t("revenue.monthlyBreakdown")}
+                  >
                       <Table>
                         <TableHeader>
                           <TableRow>
@@ -938,8 +926,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                           ))}
                         </TableBody>
                       </Table>
-                    </CardContent>
-                  </Card>
+                    </AppCard>
                 )}
               </div>
             )}
@@ -952,7 +939,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
               <div className="space-y-4">
                 {/* Summary cards */}
                 <div className="grid gap-3 grid-cols-2 lg:grid-cols-3">
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="flex items-center gap-3 p-4">
                       <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-red-500/10">
                         <AlertTriangle className="h-4 w-4 text-red-500" />
@@ -964,7 +951,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       </div>
                     </CardContent>
                   </Card>
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="flex items-center gap-3 p-4">
                       <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
                         <DollarSign className="h-4 w-4 text-amber-500" />
@@ -976,7 +963,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       </div>
                     </CardContent>
                   </Card>
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="flex items-center gap-3 p-4">
                       <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-orange-500/10">
                         <Clock className="h-4 w-4 text-orange-500" />
@@ -987,7 +974,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       </div>
                     </CardContent>
                   </Card>
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="flex items-center gap-3 p-4">
                       <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-red-500/10">
                         <Clock className="h-4 w-4 text-red-500" />
@@ -998,7 +985,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       </div>
                     </CardContent>
                   </Card>
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="flex items-center gap-3 p-4">
                       <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-red-600/10">
                         <Clock className="h-4 w-4 text-red-600" />
@@ -1012,7 +999,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                 </div>
 
                 {/* Filter and table */}
-                <Card className="border-0 shadow-sm">
+                <Card>
                   <CardHeader className="pb-2">
                     <div className="flex items-center justify-between">
                       <CardTitle className="text-sm font-medium">{t("pastDueInvoices.pastDueInvoicesList")}</CardTitle>
@@ -1114,7 +1101,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
             {!loading && taxData && (
               <div className="space-y-4">
                 <div className="grid gap-3 grid-cols-1 sm:grid-cols-3">
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="flex items-center gap-3 p-4">
                       <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
                         <Receipt className="h-4 w-4 text-amber-500" />
@@ -1127,7 +1114,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       </div>
                     </CardContent>
                   </Card>
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="flex items-center gap-3 p-4">
                       <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
                         <DollarSign className="h-4 w-4 text-blue-500" />
@@ -1140,7 +1127,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       </div>
                     </CardContent>
                   </Card>
-                  <Card className="border-0 shadow-sm">
+                  <Card>
                     <CardContent className="flex items-center gap-3 p-4">
                       <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
                         <BarChart3 className="h-4 w-4 text-violet-500" />
@@ -1154,26 +1141,21 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                 </div>
 
                 {taxData.monthly.length > 0 && (
-                  <Card className="border-0 shadow-sm">
-                    <CardHeader className="pb-2">
-                      <CardTitle className="text-sm font-medium">{t("tax.monthlyTaxCollected")}</CardTitle>
-                    </CardHeader>
-                    <CardContent>
+                  <AppCard
+                    title={t("tax.monthlyTaxCollected")}
+                  >
                       <TaxBarChart
                         data={taxData.monthly}
                         formatCurrency={fmtCurrency}
                         labels={{ taxCollected: t("charts.taxCollected") }}
                       />
-                    </CardContent>
-                  </Card>
+                    </AppCard>
                 )}
 
                 {taxData.byRate.length > 0 && (
-                  <Card className="border-0 shadow-sm">
-                    <CardHeader className="pb-2">
-                      <CardTitle className="text-sm font-medium">{t("tax.taxByRate")}</CardTitle>
-                    </CardHeader>
-                    <CardContent>
+                  <AppCard
+                    title={t("tax.taxByRate")}
+                  >
                       <Table>
                         <TableHeader>
                           <TableRow>
@@ -1194,16 +1176,13 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                           ))}
                         </TableBody>
                       </Table>
-                    </CardContent>
-                  </Card>
+                    </AppCard>
                 )}
 
                 {taxData.monthly.length > 0 && (
-                  <Card className="border-0 shadow-sm">
-                    <CardHeader className="pb-2">
-                      <CardTitle className="text-sm font-medium">{t("tax.monthlyBreakdown")}</CardTitle>
-                    </CardHeader>
-                    <CardContent>
+                  <AppCard
+                    title={t("tax.monthlyBreakdown")}
+                  >
                       <Table>
                         <TableHeader>
                           <TableRow>
@@ -1228,8 +1207,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                           ))}
                         </TableBody>
                       </Table>
-                    </CardContent>
-                  </Card>
+                    </AppCard>
                 )}
               </div>
             )}
@@ -1243,7 +1221,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
         {!loading && serviceData && (
           <div className="space-y-4">
             <div className="grid gap-3 grid-cols-1 max-w-xs">
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
                     <BarChart3 className="h-4 w-4 text-blue-500" />
@@ -1257,24 +1235,18 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
             </div>
             <div className="grid gap-4 lg:grid-cols-2">
               {serviceData.byStatus.length > 0 && (
-                <Card className="border-0 shadow-sm">
-                  <CardHeader className="pb-2">
-                    <CardTitle className="text-sm font-medium">{t("services.byStatus")}</CardTitle>
-                  </CardHeader>
-                  <CardContent>
+                <AppCard
+                  title={t("services.byStatus")}
+                >
                     <ServiceStatusChart data={serviceData.byStatus} labels={{ count: t("charts.count") }} />
-                  </CardContent>
-                </Card>
+                  </AppCard>
               )}
               {serviceData.byType.length > 0 && (
-                <Card className="border-0 shadow-sm">
-                  <CardHeader className="pb-2">
-                    <CardTitle className="text-sm font-medium">{t("services.byType")}</CardTitle>
-                  </CardHeader>
-                  <CardContent>
+                <AppCard
+                  title={t("services.byType")}
+                >
                     <ServiceTypeDonut data={serviceData.byType} />
-                  </CardContent>
-                </Card>
+                  </AppCard>
               )}
             </div>
           </div>
@@ -1287,7 +1259,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
         {!loading && customerData && (
           <div className="space-y-4">
             <div className="grid gap-3 grid-cols-2 max-w-lg">
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
                     <Users className="h-4 w-4 text-blue-500" />
@@ -1298,7 +1270,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
                     <Users className="h-4 w-4 text-emerald-500" />
@@ -1311,25 +1283,20 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
               </Card>
             </div>
             {customerData.topCustomers.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("customers.topCustomersBySpend")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("customers.topCustomersBySpend")}
+              >
                   <TopCustomersChart
                     data={customerData.topCustomers}
                     formatCurrency={fmtCurrency}
                     labels={{ totalSpent: t("charts.totalSpent") }}
                   />
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
             {customerData.topCustomers.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("customers.topCustomers")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("customers.topCustomers")}
+              >
                   <Table>
                     <TableHeader>
                       <TableRow>
@@ -1352,8 +1319,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       ))}
                     </TableBody>
                   </Table>
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
           </div>
         )}
@@ -1365,7 +1331,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
         {!loading && inventoryData && (
           <div className="space-y-4">
             <div className="grid gap-3 grid-cols-2 lg:grid-cols-5">
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
                     <Package className="h-4 w-4 text-blue-500" />
@@ -1376,7 +1342,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
                     <Package className="h-4 w-4 text-emerald-500" />
@@ -1387,7 +1353,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
                     <DollarSign className="h-4 w-4 text-amber-500" />
@@ -1400,7 +1366,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
                     <DollarSign className="h-4 w-4 text-violet-500" />
@@ -1413,7 +1379,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
                     <TrendingUp className="h-4 w-4 text-emerald-500" />
@@ -1428,11 +1394,9 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
               </Card>
             </div>
             {inventoryData.lowStock.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("inventory.lowStock")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("inventory.lowStock")}
+              >
                   <Table>
                     <TableHeader>
                       <TableRow>
@@ -1465,8 +1429,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       ))}
                     </TableBody>
                   </Table>
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
           </div>
         )}
@@ -1478,7 +1441,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
         {!loading && technicianData && (
           <div className="space-y-4">
             <div className="grid gap-3 grid-cols-2 max-w-lg">
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
                     <Wrench className="h-4 w-4 text-violet-500" />
@@ -1489,7 +1452,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
                     <DollarSign className="h-4 w-4 text-emerald-500" />
@@ -1504,25 +1467,20 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
               </Card>
             </div>
             {technicianData.technicians.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("technicians.revenueByTechnician")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("technicians.revenueByTechnician")}
+              >
                   <TechnicianBarChart
                     data={technicianData.technicians}
                     formatCurrency={fmtCurrency}
                     labels={{ revenue: t("charts.revenue") }}
                   />
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
             {technicianData.technicians.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("technicians.technicianBreakdown")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("technicians.technicianBreakdown")}
+              >
                   <Table>
                     <TableHeader>
                       <TableRow>
@@ -1547,8 +1505,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       ))}
                     </TableBody>
                   </Table>
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
           </div>
         )}
@@ -1560,7 +1517,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
         {!loading && partsData && (
           <div className="space-y-4">
             <div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
                     <Cog className="h-4 w-4 text-blue-500" />
@@ -1571,7 +1528,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
                     <DollarSign className="h-4 w-4 text-emerald-500" />
@@ -1584,7 +1541,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-orange-500/10">
                     <Package className="h-4 w-4 text-orange-500" />
@@ -1597,7 +1554,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-green-500/10">
                     <TrendingUp className="h-4 w-4 text-green-500" />
@@ -1612,21 +1569,16 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
               </Card>
             </div>
             {partsData.parts.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("parts.topPartsByUsage")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("parts.topPartsByUsage")}
+              >
                   <PartsDonut data={partsData.parts} />
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
             {partsData.parts.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("parts.partsUsage")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("parts.partsUsage")}
+              >
                   <Table>
                     <TableHeader>
                       <TableRow>
@@ -1653,8 +1605,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       ))}
                     </TableBody>
                   </Table>
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
           </div>
         )}
@@ -1666,7 +1617,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
         {!loading && jobAnalyticsData && (
           <div className="space-y-4">
             <div className="grid gap-3 grid-cols-2 max-w-lg">
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
                     <BarChart3 className="h-4 w-4 text-blue-500" />
@@ -1677,7 +1628,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
                     <DollarSign className="h-4 w-4 text-emerald-500" />
@@ -1692,45 +1643,34 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
               </Card>
             </div>
             <div className="grid gap-4 lg:grid-cols-2">
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("jobAnalytics.jobsByDayOfWeek")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("jobAnalytics.jobsByDayOfWeek")}
+              >
                   <DayOfWeekChart data={jobAnalyticsData.dayOfWeek} labels={{ jobs: t("charts.jobs") }} />
-                </CardContent>
-              </Card>
+                </AppCard>
               {jobAnalyticsData.topServiceTypes.length > 0 && (
-                <Card className="border-0 shadow-sm">
-                  <CardHeader className="pb-2">
-                    <CardTitle className="text-sm font-medium">{t("jobAnalytics.serviceTypes")}</CardTitle>
-                  </CardHeader>
-                  <CardContent>
+                <AppCard
+                  title={t("jobAnalytics.serviceTypes")}
+                >
                     <ServiceTypeAnalyticsDonut data={jobAnalyticsData.topServiceTypes} />
-                  </CardContent>
-                </Card>
+                  </AppCard>
               )}
             </div>
             {jobAnalyticsData.monthlyTrend.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("jobAnalytics.monthlyTrend")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("jobAnalytics.monthlyTrend")}
+              >
                   <MonthlyTrendChart
                     data={jobAnalyticsData.monthlyTrend}
                     formatCurrency={fmtCurrency}
                     labels={{ jobs: t("charts.jobs"), revenue: t("charts.revenue") }}
                   />
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
             {jobAnalyticsData.topServiceTypes.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("jobAnalytics.serviceTypeBreakdown")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("jobAnalytics.serviceTypeBreakdown")}
+              >
                   <Table>
                     <TableHeader>
                       <TableRow>
@@ -1751,8 +1691,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       ))}
                     </TableBody>
                   </Table>
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
           </div>
         )}
@@ -1764,7 +1703,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
         {!loading && retentionData && (
           <div className="space-y-4">
             <div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
                     <UserCheck className="h-4 w-4 text-emerald-500" />
@@ -1775,7 +1714,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
                     <Users className="h-4 w-4 text-blue-500" />
@@ -1786,7 +1725,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
                     <Users className="h-4 w-4 text-violet-500" />
@@ -1797,7 +1736,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                   </div>
                 </CardContent>
               </Card>
-              <Card className="border-0 shadow-sm">
+              <Card>
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
                     <CalendarDays className="h-4 w-4 text-amber-500" />
@@ -1812,25 +1751,20 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
               </Card>
             </div>
             {retentionData.topReturning.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("retention.topReturningCustomers")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("retention.topReturningCustomers")}
+              >
                   <RetentionBarChart
                     data={retentionData.topReturning}
                     formatCurrency={fmtCurrency}
                     labels={{ visits: t("charts.visits"), totalSpent: t("charts.totalSpent") }}
                   />
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
             {retentionData.topReturning.length > 0 && (
-              <Card className="border-0 shadow-sm">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("retention.returningCustomers")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("retention.returningCustomers")}
+              >
                   <Table>
                     <TableHeader>
                       <TableRow>
@@ -1853,8 +1787,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       ))}
                     </TableBody>
                   </Table>
-                </CardContent>
-              </Card>
+                </AppCard>
             )}
           </div>
         )}
@@ -1895,7 +1828,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
         {!loading && vehicleData && (
           <div className="space-y-4">
             {/* Vehicle header with inline stats */}
-            <Card className="border-0 shadow-sm">
+            <Card>
               <CardContent className="p-4">
                 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                   <div className="flex items-center gap-3 min-w-0">
@@ -1923,23 +1856,20 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
 
             {/* Charts row */}
             <div className="grid gap-4 lg:grid-cols-5">
-              <Card className="border-0 shadow-sm lg:col-span-3">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("vehicles.monthlyCosts")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+              <AppCard
+                title={t("vehicles.monthlyCosts")}
+                className="lg:col-span-3"
+              >
                   <VehicleCostBarChart
                     data={vehicleData.monthlyCosts}
                     formatCurrency={fmtCurrency}
                     labels={{ partsCost: t("charts.partsCost"), laborCost: t("charts.laborCost") }}
                   />
-                </CardContent>
-              </Card>
-              <Card className="border-0 shadow-sm lg:col-span-2">
-                <CardHeader className="pb-2">
-                  <CardTitle className="text-sm font-medium">{t("vehicles.serviceTypeBreakdown")}</CardTitle>
-                </CardHeader>
-                <CardContent>
+                </AppCard>
+              <AppCard
+                title={t("vehicles.serviceTypeBreakdown")}
+                className="lg:col-span-2"
+              >
                   <div className="space-y-3">
                     {vehicleData.serviceTypeBreakdown.map((item) => {
                       const pct = vehicleData.summary.totalCost > 0
@@ -1972,18 +1902,16 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                       </div>
                     )}
                   </div>
-                </CardContent>
-              </Card>
+                </AppCard>
             </div>
 
             {/* Top Parts + Service History side by side on large screens */}
             <div className="grid gap-4 lg:grid-cols-5">
               {vehicleData.topParts.length > 0 && (
-                <Card className="border-0 shadow-sm lg:col-span-2">
-                  <CardHeader className="pb-2">
-                    <CardTitle className="text-sm font-medium">{t("vehicles.topParts")}</CardTitle>
-                  </CardHeader>
-                  <CardContent>
+                <AppCard
+                  title={t("vehicles.topParts")}
+                  className="lg:col-span-2"
+                >
                     <Table>
                       <TableHeader>
                         <TableRow>
@@ -2005,8 +1933,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                         ))}
                       </TableBody>
                     </Table>
-                  </CardContent>
-                </Card>
+                  </AppCard>
               )}
 
               {vehicleData.serviceHistory.length > 0 && (() => {
@@ -2015,7 +1942,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                 const page = Math.min(historyPage, totalPages - 1);
                 const paged = vehicleData.serviceHistory.slice(page * pageSize, (page + 1) * pageSize);
                 return (
-                  <Card className={cn("border-0 shadow-sm", vehicleData.topParts.length > 0 ? "lg:col-span-3" : "lg:col-span-5")}>
+                  <Card className={cn("", vehicleData.topParts.length > 0 ? "lg:col-span-3" : "lg:col-span-5")}>
                     <CardHeader className="pb-2">
                       <div className="flex items-center justify-between">
                         <CardTitle className="text-sm font-medium">{t("vehicles.serviceHistory")}</CardTitle>
@@ -2049,7 +1976,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
                             <TableRow
                               key={row.id}
                               className="cursor-pointer hover:bg-muted/50"
-                              onClick={() => router.push(`/vehicles/${vehicleData.vehicleInfo.id}/service/${row.id}`)}
+                              {...interactiveRow(() => router.push(`/vehicles/${vehicleData.vehicleInfo.id}/service/${row.id}`))}
                             >
                               <TableCell className="text-sm text-muted-foreground">{row.date}</TableCell>
                               <TableCell className="text-sm font-medium">{row.title}</TableCell>
@@ -2083,7 +2010,7 @@ export default function ReportsClient({ currencyCode, primaryColor, organization
 
 function EmptyState({ message }: { message: string }) {
   return (
-    <Card className="border-0 shadow-sm">
+    <Card>
       <CardContent className="flex flex-col items-center justify-center py-12">
         <BarChart3 className="h-12 w-12 text-muted-foreground mb-4" />
         <p className="text-muted-foreground">

+ 7 - 9
src/app/(authenticated)/settings/about/page.tsx

@@ -1,6 +1,6 @@
+import { AppCard } from '@/components/app-card'
 import Link from 'next/link'
 import { getTranslations } from 'next-intl/server'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Separator } from '@/components/ui/separator'
 import { ExternalLink, Info } from 'lucide-react'
 
@@ -18,12 +18,11 @@ export default async function AboutSettingsPage() {
 
   return (
     <div className="space-y-6">
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Info className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('about.title')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Info}
+        title={t('about.title')}
+        contentClassName="space-y-4"
+      >
           <p className="text-sm text-muted-foreground">
             {t('about.description')}
           </p>
@@ -48,8 +47,7 @@ export default async function AboutSettingsPage() {
               </div>
             ))}
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
     </div>
   )
 }

+ 20 - 26
src/app/(authenticated)/settings/account/account-settings.tsx

@@ -1,10 +1,10 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import { useState, useEffect } from 'react'
 import { useSession } from '@/lib/auth-client'
 import { authClient } from '@/lib/auth-client'
 import { toast } from 'sonner'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import {
   Dialog,
   DialogContent,
@@ -247,7 +247,7 @@ export function AccountSettings({
   return (
     <div className="space-y-6">
       {/* Profile Overview */}
-      {/* <Card className="border-0 shadow-sm">
+      {/* <Card>
         <CardContent className="pt-6">
           <div className="flex items-center gap-4">
             <Avatar className="h-16 w-16">
@@ -264,12 +264,11 @@ export function AccountSettings({
       </Card> */}
 
       {/* Update Name */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <User className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('account.profileTitle')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={User}
+        title={t('account.profileTitle')}
+        contentClassName="space-y-4"
+      >
           <p className="text-sm text-muted-foreground">
             {t('account.profileDescription')}
           </p>
@@ -344,16 +343,14 @@ export function AccountSettings({
               {t('account.saveProfile')}
             </Button>
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Change Password */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <KeyRound className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('account.changePasswordTitle')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={KeyRound}
+        title={t('account.changePasswordTitle')}
+        contentClassName="space-y-4"
+      >
           <p className="text-sm text-muted-foreground">
             {t('account.changePasswordDescription')}
           </p>
@@ -397,15 +394,13 @@ export function AccountSettings({
               {t('account.changePassword')}
             </Button>
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
       {/* Two-Factor Authentication */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Shield className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('account.twoFactorTitle')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Shield}
+        title={t('account.twoFactorTitle')}
+        contentClassName="space-y-4"
+      >
           {twoFactorEnabled ? (
             <>
               <div className="flex items-center gap-2 rounded-lg bg-emerald-500/10 px-4 py-3">
@@ -453,8 +448,7 @@ export function AccountSettings({
               </Button>
             </>
           )}
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* 2FA Setup Dialog */}
       <Dialog open={dialogOpen} onOpenChange={(open) => {

+ 12 - 22
src/app/(authenticated)/settings/alerts/alerts-settings.tsx

@@ -4,7 +4,7 @@ import { useState } from "react";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Button } from "@/components/ui/button";
@@ -77,14 +77,7 @@ function ServiceRequestAlertCard({ settings }: { settings: Record<string, string
   };
 
   return (
-    <Card className="border-0 shadow-sm">
-      <CardHeader className="flex flex-row items-center justify-between pb-4">
-        <div className="flex items-center gap-3">
-          <Wrench className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t("alerts.serviceRequest.title")}</CardTitle>
-        </div>
-      </CardHeader>
-      <CardContent className="space-y-6">
+    <AppCard icon={Wrench} title={t("alerts.serviceRequest.title")} contentClassName="space-y-6">
         <p className="text-sm text-muted-foreground">
           {t("alerts.serviceRequest.description")}
         </p>
@@ -147,8 +140,7 @@ function ServiceRequestAlertCard({ settings }: { settings: Record<string, string
             {t("alerts.serviceRequest.save")}
           </Button>
         </SaveButton>
-      </CardContent>
-    </Card>
+    </AppCard>
   );
 }
 
@@ -217,22 +209,21 @@ function LowStockAlertCard({ settings }: { settings: Record<string, string> }) {
   };
 
   return (
-    <Card className="border-0 shadow-sm">
-      <CardHeader className="flex flex-row items-center justify-between pb-4">
-        <div className="flex items-center gap-3">
-          <PackageSearch className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t("alerts.lowStock.title")}</CardTitle>
-        </div>
+    <AppCard
+      icon={PackageSearch}
+      title={t("alerts.lowStock.title")}
+      action={
         <a
           href="https://torqvoice.com/docs/features/low-stock-alerts"
           target="_blank"
           rel="noopener noreferrer"
-          className="text-[11px] text-muted-foreground hover:text-foreground transition-colors"
+          className="text-[11px] text-muted-foreground transition-colors hover:text-foreground"
         >
           {t("alerts.lowStock.readMore")} →
         </a>
-      </CardHeader>
-      <CardContent className="space-y-6">
+      }
+      contentClassName="space-y-6"
+    >
         <p className="text-sm text-muted-foreground">
           {t("alerts.lowStock.description")}
         </p>
@@ -344,7 +335,6 @@ function LowStockAlertCard({ settings }: { settings: Record<string, string> }) {
           )}
           </div>
         </SaveButton>
-      </CardContent>
-    </Card>
+    </AppCard>
   );
 }

+ 13 - 17
src/app/(authenticated)/settings/appearance/appearance-settings.tsx

@@ -1,9 +1,9 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import { useState } from 'react'
 import { useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Label } from '@/components/ui/label'
 import { Button } from '@/components/ui/button'
 import { ThemePicker } from '@/components/theme-picker'
@@ -102,12 +102,11 @@ export function AppearanceSettings({ settings }: { settings: Record<string, stri
 
   return (
     <div className="space-y-6">
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Palette className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('appearance.title')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Palette}
+        title={t('appearance.title')}
+        contentClassName="space-y-4"
+      >
           <p className="text-sm text-muted-foreground">{t('appearance.description')}</p>
 
           <div>
@@ -115,15 +114,13 @@ export function AppearanceSettings({ settings }: { settings: Record<string, stri
             <p className="text-xs text-muted-foreground">{t('appearance.themeHint')}</p>
           </div>
           <ThemePicker />
-        </CardContent>
-      </Card>
+        </AppCard>
 
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Calendar className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('appearance.dateTimeTitle')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-6">
+      <AppCard
+        icon={Calendar}
+        title={t('appearance.dateTimeTitle')}
+        contentClassName="space-y-6"
+      >
           <p className="text-sm text-muted-foreground">
             {t('appearance.dateTimeDescription')}
           </p>
@@ -196,8 +193,7 @@ export function AppearanceSettings({ settings }: { settings: Record<string, stri
               {t('appearance.saveSettings')}
             </Button>
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
     </div>
   )
 }

+ 12 - 19
src/app/(authenticated)/settings/company/company-settings.tsx

@@ -4,7 +4,7 @@ import { useState, useRef } from "react";
 import { useRouter } from "next/navigation";
 import Image from "next/image";
 import { useTranslations } from "next-intl";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Button } from "@/components/ui/button";
@@ -98,13 +98,10 @@ export function CompanySettings({ settings, organizationName }: { settings: Reco
     <div className="space-y-4">
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="pb-2">
-          <CardTitle className="flex items-center gap-2 text-sm text-muted-foreground">
-            <ImageIcon className="h-4 w-4" /> {t('company.logoTitle')}
-          </CardTitle>
-        </CardHeader>
-        <CardContent>
+      <AppCard
+        icon={ImageIcon}
+        title={t('company.logoTitle')}
+      >
           <div className="flex items-center gap-4">
             <div className="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-lg border bg-muted/50">
               {logoUrl ? (
@@ -128,15 +125,12 @@ export function CompanySettings({ settings, organizationName }: { settings: Reco
             </div>
             <input ref={logoInputRef} type="file" accept=".jpg,.jpeg,.png,.webp,.svg" className="hidden" onChange={(e) => { const file = e.target.files?.[0]; if (file) handleLogoUpload(file); e.target.value = ""; }} />
           </div>
-        </CardContent>
-      </Card>
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="pb-2">
-          <CardTitle className="flex items-center gap-2 text-sm text-muted-foreground">
-            <Building2 className="h-4 w-4" /> {t('company.detailsTitle')}
-          </CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+        </AppCard>
+      <AppCard
+        icon={Building2}
+        title={t('company.detailsTitle')}
+        contentClassName="space-y-4"
+      >
           <div className="grid gap-3 sm:grid-cols-2">
             <div className="space-y-1">
               <Label htmlFor="workshopName" className="text-xs">{t('company.shopName')}</Label>
@@ -159,8 +153,7 @@ export function CompanySettings({ settings, organizationName }: { settings: Reco
             <Label htmlFor="workshopAddress" className="text-xs">{t('company.address')}</Label>
             <Textarea id="workshopAddress" placeholder={t('company.addressPlaceholder')} rows={2} value={workshopAddress} onChange={(e) => setWorkshopAddress(e.target.value)} />
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
       <SaveButton>
         <div className="flex items-center justify-between">
           <Button size="sm" onClick={handleSave} disabled={saving}>

+ 1 - 1
src/app/(authenticated)/settings/company/service-type-selector.tsx

@@ -20,7 +20,7 @@ export function ServiceTypeSelector({ serviceType, onServiceTypeChange }: Servic
   const t = useTranslations("settings");
 
   return (
-    <Card className="border-0 shadow-sm">
+    <Card>
       <CardHeader className="pb-2">
         <CardTitle className="text-sm text-muted-foreground">
           {t("company.serviceType")}

+ 29 - 37
src/app/(authenticated)/settings/data/data-settings.tsx

@@ -1,10 +1,11 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import { useRef, useState } from 'react'
 import { useFormatter, useNow, useTranslations } from 'next-intl'
 import Image from 'next/image'
 import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Card, CardHeader, CardContent, CardTitle } from '@/components/ui/card'
 import { Checkbox } from '@/components/ui/checkbox'
 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
 import { Input } from '@/components/ui/input'
@@ -390,13 +391,12 @@ export function DataSettings({
         // missed, red when a whole day has passed.
         const dot = ageHours < 3 ? 'bg-emerald-500' : ageHours < 26 ? 'bg-amber-500' : 'bg-red-500'
         return (
-          <Card className="gap-2 border border-primary/30 shadow-sm">
-            <CardHeader>
-              <CardTitle className="flex items-center gap-2 text-base">
-                <ShieldCheck className="h-4 w-4" /> {t('data.backupStatus.title')}
-              </CardTitle>
-            </CardHeader>
-            <CardContent className="space-y-2">
+          <AppCard
+            icon={ShieldCheck}
+            title={t('data.backupStatus.title')}
+            className="gap-2 border border-primary/30 shadow-sm"
+            contentClassName="space-y-2"
+          >
               <p className="text-sm text-muted-foreground">
                 {t('data.backupStatus.description')}
               </p>
@@ -424,21 +424,19 @@ export function DataSettings({
                   {t('data.backupStatus.supportAction')}
                 </button>
               </p>
-            </CardContent>
-          </Card>
+            </AppCard>
         )
       })()}
 
       <ReadOnlyWrapper>
         <div className="grid gap-6 lg:grid-cols-12">
           {/* Export Card */}
-          <Card className="border-0 shadow-sm lg:col-span-7">
-            <CardHeader>
-              <CardTitle className="flex items-center gap-2 text-base">
-                <Download className="h-4 w-4" /> {t('data.exportTitle')}
-              </CardTitle>
-            </CardHeader>
-            <CardContent className="space-y-4">
+          <AppCard
+            icon={Download}
+            title={t('data.exportTitle')}
+            className="lg:col-span-7"
+            contentClassName="space-y-4"
+          >
               <p className="text-sm text-muted-foreground">
                 {t('data.exportDescription')}
               </p>
@@ -486,17 +484,15 @@ export function DataSettings({
                 {exporting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
                 {t('data.downloadBackup')}
               </Button>
-            </CardContent>
-          </Card>
+            </AppCard>
 
           {/* Import Card */}
-          <Card className="border-0 shadow-sm lg:col-span-5">
-            <CardHeader>
-              <CardTitle className="flex items-center gap-2 text-base">
-                <Upload className="h-4 w-4" /> {t('data.importTitle')}
-              </CardTitle>
-            </CardHeader>
-            <CardContent className="space-y-4">
+          <AppCard
+            icon={Upload}
+            title={t('data.importTitle')}
+            className="lg:col-span-5"
+            contentClassName="space-y-4"
+          >
               <div className="flex items-start gap-2 rounded-md bg-amber-500/10 p-3 text-sm text-amber-700 dark:text-amber-400">
                 <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
                 <span>
@@ -523,18 +519,15 @@ export function DataSettings({
                   {t('data.uploadRestore')}
                 </Button>
               </div>
-            </CardContent>
-          </Card>
+            </AppCard>
         </div>
 
         {/* Import from Other Services */}
-        <Card className="border-0 shadow-sm">
-          <CardHeader>
-            <CardTitle className="flex items-center gap-2 text-base">
-              <ArrowRight className="h-4 w-4" /> {t('data.importFromOther')}
-            </CardTitle>
-          </CardHeader>
-          <CardContent className="space-y-4">
+        <AppCard
+          icon={ArrowRight}
+          title={t('data.importFromOther')}
+          contentClassName="space-y-4"
+        >
             <p className="text-sm text-muted-foreground">
               {t('data.importFromOtherDescription')}
             </p>
@@ -571,8 +564,7 @@ export function DataSettings({
                 />
               </button>
             </div>
-          </CardContent>
-        </Card>
+          </AppCard>
       </ReadOnlyWrapper>
 
       {/* LubeLog Import Dialog */}

+ 7 - 9
src/app/(authenticated)/settings/install/install-client.tsx

@@ -1,10 +1,10 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { CheckCircle2, Download, Info } from 'lucide-react'
 import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Separator } from '@/components/ui/separator'
 import { useInstallPrompt } from '@/components/pwa-install-prompt'
 
@@ -15,12 +15,11 @@ export function InstallSettingsClient() {
 
   return (
     <div className="space-y-6">
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Download className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('title')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Download}
+        title={t('title')}
+        contentClassName="space-y-4"
+      >
           <div className="flex items-start gap-3">
             <Image
               src="/icons/icon-192.png"
@@ -68,8 +67,7 @@ export function InstallSettingsClient() {
               </div>
             </>
           )}
-        </CardContent>
-      </Card>
+        </AppCard>
     </div>
   )
 }

+ 7 - 9
src/app/(authenticated)/settings/invoice/invoice-settings.tsx

@@ -1,9 +1,9 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import { useState, useCallback, useMemo } from 'react'
 import { useRouter, useSearchParams } from 'next/navigation'
 import { useTranslations } from 'next-intl'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Button } from '@/components/ui/button'
@@ -241,12 +241,11 @@ export function InvoiceSettings({
 
       {tab === 'general' ? (
         <ReadOnlyWrapper>
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="flex flex-row items-center gap-3 pb-4">
-              <FileText className="h-5 w-5 text-muted-foreground" />
-              <CardTitle className="text-lg">{t('invoice.tabs.general')}</CardTitle>
-            </CardHeader>
-            <CardContent className="space-y-6">
+          <AppCard
+            icon={FileText}
+            title={t('invoice.tabs.general')}
+            contentClassName="space-y-6"
+          >
               <div className="space-y-3">
                 <h3 className="text-sm font-semibold">{t('invoice.sectionInvoices')}</h3>
                 <div className="grid gap-4 sm:grid-cols-2">
@@ -438,8 +437,7 @@ export function InvoiceSettings({
                   </Button>
                 </div>
               </SaveButton>
-            </CardContent>
-          </Card>
+            </AppCard>
         </ReadOnlyWrapper>
       ) : tab === 'layout' ? (
         <>

+ 25 - 33
src/app/(authenticated)/settings/localization/localization-settings.tsx

@@ -1,9 +1,9 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import { useEffect, useState } from 'react'
 import { useRouter } from 'next/navigation'
 import { useLocale, useTranslations } from 'next-intl'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Label } from '@/components/ui/label'
 import { Button } from '@/components/ui/button'
 import { Separator } from '@/components/ui/separator'
@@ -267,12 +267,11 @@ export function LocalizationSettings({ settings }: { settings: Record<string, st
       <ReadOnlyBanner />
 
       {/* Language */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Globe className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('localization.languageTitle')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Globe}
+        title={t('localization.languageTitle')}
+        contentClassName="space-y-4"
+      >
           <p className="text-sm text-muted-foreground">{t('localization.languageDescription')}</p>
           <div className="space-y-2">
             <Label>{t('localization.language')}</Label>
@@ -334,16 +333,14 @@ export function LocalizationSettings({ settings }: { settings: Record<string, st
               )}
             </div>
           </ReadOnlyWrapper>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Currency */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Coins className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('currency.title')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-6">
+      <AppCard
+        icon={Coins}
+        title={t('currency.title')}
+        contentClassName="space-y-6"
+      >
           <p className="text-sm text-muted-foreground">{t('currency.description')}</p>
 
           <ReadOnlyWrapper>
@@ -429,16 +426,14 @@ export function LocalizationSettings({ settings }: { settings: Record<string, st
               </div>
             </div>
           </ReadOnlyWrapper>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Date & Time */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Calendar className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('appearance.dateTimeTitle')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-6">
+      <AppCard
+        icon={Calendar}
+        title={t('appearance.dateTimeTitle')}
+        contentClassName="space-y-6"
+      >
           <p className="text-sm text-muted-foreground">{t('appearance.dateTimeDescription')}</p>
 
           <ReadOnlyWrapper>
@@ -578,23 +573,20 @@ export function LocalizationSettings({ settings }: { settings: Record<string, st
               </div>
             </div>
           </ReadOnlyWrapper>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Theme */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Palette className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('appearance.title')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Palette}
+        title={t('appearance.title')}
+        contentClassName="space-y-4"
+      >
           <div>
             <Label className="text-sm font-medium">{t('appearance.themeLabel')}</Label>
             <p className="text-xs text-muted-foreground">{t('appearance.themeHint')}</p>
           </div>
           <ThemePicker />
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Save */}
       <SaveButton>

+ 7 - 9
src/app/(authenticated)/settings/maintenance/maintenance-settings.tsx

@@ -3,7 +3,7 @@
 import { useState } from "react";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Button } from "@/components/ui/button";
@@ -49,12 +49,11 @@ export function MaintenanceSettings({ settings }: { settings: Record<string, str
     <div className="space-y-6">
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
-        <Card className="border-0 shadow-sm">
-          <CardHeader className="flex flex-row items-center gap-3 pb-4">
-            <Gauge className="h-5 w-5 text-muted-foreground" />
-            <CardTitle className="text-lg">{t('maintenance.title')}</CardTitle>
-          </CardHeader>
-          <CardContent className="space-y-6">
+        <AppCard
+          icon={Gauge}
+          title={t('maintenance.title')}
+          contentClassName="space-y-6"
+        >
             <p className="text-sm text-muted-foreground">
               {t('maintenance.description')}
             </p>
@@ -121,8 +120,7 @@ export function MaintenanceSettings({ settings }: { settings: Record<string, str
                 </Button>
               </div>
             </SaveButton>
-          </CardContent>
-        </Card>
+          </AppCard>
       </ReadOnlyWrapper>
     </div>
   );

+ 16 - 20
src/app/(authenticated)/settings/payment/payment-settings.tsx

@@ -3,7 +3,7 @@
 import { useState, useEffect } from "react";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
@@ -101,22 +101,21 @@ export function PaymentSettings({ settings, orgId }: { settings: Record<string,
     <div className="space-y-6">
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center justify-between pb-4">
-          <div className="flex items-center gap-3">
-            <Banknote className="h-5 w-5 text-muted-foreground" />
-            <CardTitle className="text-lg">{t('payment.detailsTitle')}</CardTitle>
-          </div>
+      <AppCard
+        icon={Banknote}
+        title={t('payment.detailsTitle')}
+        action={
           <a
             href="https://torqvoice.com/docs/configuration/payment-providers"
             target="_blank"
             rel="noopener noreferrer"
-            className="text-[11px] text-muted-foreground hover:text-foreground transition-colors"
+            className="text-[11px] text-muted-foreground transition-colors hover:text-foreground"
           >
             {t('payment.readMore')} →
           </a>
-        </CardHeader>
-        <CardContent className="space-y-6">
+        }
+        contentClassName="space-y-6"
+      >
           <p className="text-sm text-muted-foreground">
             {t('payment.detailsDescription')}
           </p>
@@ -147,15 +146,13 @@ export function PaymentSettings({ settings, orgId }: { settings: Record<string,
               {t('payment.paymentTermsHint')}
             </p>
           </div>
-        </CardContent>
-      </Card>
+      </AppCard>
 
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <CreditCard className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('payment.onlinePayments')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-6">
+      <AppCard
+        icon={CreditCard}
+        title={t('payment.onlinePayments')}
+        contentClassName="space-y-6"
+      >
           <p className="text-sm text-muted-foreground">
             {t('payment.onlinePaymentsDescription')}
           </p>
@@ -436,8 +433,7 @@ export function PaymentSettings({ settings, orgId }: { settings: Record<string,
               </Button>
             </div>
           </SaveButton>
-        </CardContent>
-      </Card>
+        </AppCard>
       </ReadOnlyWrapper>
     </div>
   );

+ 7 - 9
src/app/(authenticated)/settings/support/page.tsx

@@ -1,6 +1,6 @@
+import { AppCard } from '@/components/app-card'
 import { notFound } from 'next/navigation'
 import { getTranslations } from 'next-intl/server'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Headset } from 'lucide-react'
 import { isSupportEnabled } from '@/lib/support'
 import { SupportSettingsControls } from '@/features/support/Components/SupportSettingsControls'
@@ -19,16 +19,14 @@ export default async function SupportSettingsPage() {
 
   return (
     <div className="space-y-6">
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Headset className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('support.title')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Headset}
+        title={t('support.title')}
+        contentClassName="space-y-4"
+      >
           <p className="text-sm text-muted-foreground">{t('support.description')}</p>
           <SupportSettingsControls />
-        </CardContent>
-      </Card>
+        </AppCard>
     </div>
   )
 }

+ 7 - 9
src/app/(authenticated)/settings/tax/tax-settings.tsx

@@ -3,7 +3,7 @@
 import { useState } from "react";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Button } from "@/components/ui/button";
@@ -96,12 +96,11 @@ export function TaxSettings({
     <div className="space-y-6">
       <ReadOnlyBanner />
 
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Percent className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t("tax.title")}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-6">
+      <AppCard
+        icon={Percent}
+        title={t("tax.title")}
+        contentClassName="space-y-6"
+      >
           <p className="text-sm text-muted-foreground">{t("tax.description")}</p>
 
           <ReadOnlyWrapper>
@@ -260,8 +259,7 @@ export function TaxSettings({
               </Button>
             </div>
           </SaveButton>
-        </CardContent>
-      </Card>
+        </AppCard>
     </div>
   );
 }

+ 36 - 58
src/app/(authenticated)/settings/team/team-settings.tsx

@@ -7,7 +7,7 @@ import { toast } from "sonner";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Badge } from "@/components/ui/badge";
 import { Checkbox } from "@/components/ui/checkbox";
 import {
@@ -318,13 +318,11 @@ export function TeamSettings({
           </p>
         </div>
 
-        <Card className="border-0 shadow-sm">
-          <CardHeader>
-            <CardTitle className="flex items-center gap-2 text-base">
-              <Users className="h-4 w-4" /> {t('team.createOrg')}
-            </CardTitle>
-          </CardHeader>
-          <CardContent className="space-y-4">
+        <AppCard
+          icon={Users}
+          title={t('team.createOrg')}
+          contentClassName="space-y-4"
+        >
             <p className="text-sm text-muted-foreground">
               {t('team.createOrgDescription')}
             </p>
@@ -342,8 +340,7 @@ export function TeamSettings({
                 {t('team.create')}
               </Button>
             </div>
-          </CardContent>
-        </Card>
+          </AppCard>
       </div>
     );
   }
@@ -358,16 +355,11 @@ export function TeamSettings({
       </div>
 
       {/* Members Card */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader>
-          <CardTitle className="flex items-center gap-2 text-base">
-            <Users className="h-4 w-4" /> {organization.name}
-            <Badge variant="outline" className="ml-2 text-xs">
-              {organization.members.length !== 1 ? t('team.memberCountPlural', { count: organization.members.length }) : t('team.memberCount', { count: organization.members.length })}
-            </Badge>
-          </CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Users}
+        title={<>{organization.name} <Badge variant="outline" className="ml-2 text-xs"> {organization.members.length !== 1 ? t('team.memberCountPlural', { count: organization.members.length }) : t('team.memberCount', { count: organization.members.length })} </Badge></>}
+        contentClassName="space-y-4"
+      >
           <div className="space-y-2">
             {organization.members.map((member) => (
               <div
@@ -476,21 +468,14 @@ export function TeamSettings({
               </Button>
             </form>
           )}
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Pending Invitations Card */}
       {isAdmin && pendingInvitations.length > 0 && (
-        <Card className="border-0 shadow-sm">
-          <CardHeader>
-            <CardTitle className="flex items-center gap-2 text-base">
-              <Mail className="h-4 w-4" /> {t('team.pendingInvitations')}
-              <Badge variant="outline" className="ml-2 text-xs">
-                {pendingInvitations.length}
-              </Badge>
-            </CardTitle>
-          </CardHeader>
-          <CardContent>
+        <AppCard
+          icon={Mail}
+          title={<>{t('team.pendingInvitations')} <Badge variant="outline" className="ml-2 text-xs"> {pendingInvitations.length} </Badge></>}
+        >
             <div className="space-y-2">
               {pendingInvitations.map((invitation) => (
                 <div
@@ -537,27 +522,24 @@ export function TeamSettings({
                 </div>
               ))}
             </div>
-          </CardContent>
-        </Card>
+          </AppCard>
       )}
 
       {/* Custom Roles Card */}
       {isAdmin && (
-        <Card className="border-0 shadow-sm">
-          <CardHeader>
-            <div className="flex items-center justify-between">
-              <CardTitle className="flex items-center gap-2 text-base">
-                <ShieldCheck className="h-4 w-4" /> {t('team.customRoles')}
-              </CardTitle>
-              {!showRoleForm && (
-                <Button size="sm" variant="outline" onClick={() => openRoleForm()}>
-                  <Plus className="mr-1 h-4 w-4" />
-                  {t('team.newRole')}
-                </Button>
-              )}
-            </div>
-          </CardHeader>
-          <CardContent className="space-y-4">
+        <AppCard
+          icon={ShieldCheck}
+          title={t('team.customRoles')}
+          action={
+            !showRoleForm && (
+              <Button size="sm" variant="outline" onClick={() => openRoleForm()}>
+                <Plus className="mr-1 h-4 w-4" />
+                {t('team.newRole')}
+              </Button>
+            )
+          }
+          contentClassName="space-y-4"
+        >
             {showRoleForm && (
               <div className="space-y-4 rounded-lg border p-4">
                 <div className="space-y-2">
@@ -741,16 +723,13 @@ export function TeamSettings({
                 ))}
               </div>
             )}
-          </CardContent>
-        </Card>
+        </AppCard>
       )}
 
       {/* Role Descriptions */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader>
-          <CardTitle className="text-base">{t('team.builtInRoles')}</CardTitle>
-        </CardHeader>
-        <CardContent>
+      <AppCard
+        title={t('team.builtInRoles')}
+      >
           <div className="space-y-3 text-sm">
             <div className="flex items-center gap-3">
               <Badge variant="outline" className={`${roleColors.owner}`}>
@@ -771,8 +750,7 @@ export function TeamSettings({
               <span className="text-muted-foreground">{t('team.memberDescription')}</span>
             </div>
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
     </div>
   );
 }

+ 24 - 40
src/app/(authenticated)/settings/templates/template-settings.tsx

@@ -6,7 +6,7 @@ import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import {
   Select,
   SelectContent,
@@ -90,11 +90,9 @@ function TemplateTab({
   return (
     <>
       {/* Template Gallery */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader>
-          <CardTitle className="text-base">{t('templates.presets')}</CardTitle>
-        </CardHeader>
-        <CardContent>
+      <AppCard
+        title={t('templates.presets')}
+      >
           <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
             {templatePresets.map((preset) => {
               const isSelected = currentPresetId === preset.id;
@@ -170,18 +168,15 @@ function TemplateTab({
               );
             })}
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       <div className="grid gap-6 lg:grid-cols-2">
         {/* Color Settings */}
-        <Card className="border-0 shadow-sm">
-          <CardHeader>
-            <CardTitle className="flex items-center gap-2 text-base">
-              <Palette className="h-4 w-4" /> {t('templates.primaryColor')}
-            </CardTitle>
-          </CardHeader>
-          <CardContent className="space-y-4">
+        <AppCard
+          icon={Palette}
+          title={t('templates.primaryColor')}
+          contentClassName="space-y-4"
+        >
             <div className="flex items-center gap-3">
               <Input
                 type="color"
@@ -212,15 +207,12 @@ function TemplateTab({
                 </button>
               ))}
             </div>
-          </CardContent>
-        </Card>
+          </AppCard>
 
         {/* Font & Layout Settings */}
-        <Card className="border-0 shadow-sm">
-          <CardHeader>
-            <CardTitle className="text-base">{t('templates.fontAndLayout')}</CardTitle>
-          </CardHeader>
-          <CardContent>
+        <AppCard
+          title={t('templates.fontAndLayout')}
+        >
             <div className="grid gap-4 sm:grid-cols-2">
               <div className="space-y-4">
                 <div className="space-y-2">
@@ -270,24 +262,20 @@ function TemplateTab({
                 </div>
               </div>
             </div>
-          </CardContent>
-        </Card>
+          </AppCard>
       </div>
 
       {/* Preview */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader>
-          <CardTitle className="text-base">{t('templates.preview', { name: documentLabel })}</CardTitle>
-        </CardHeader>
-        <CardContent>
+      <AppCard
+        title={t('templates.preview', { name: documentLabel })}
+      >
           <InvoiceLayoutPreview
             config={invoiceLayoutConfig ?? getDefaultInvoiceLayout()}
             documentType={documentType}
             template={values}
             logoUrl={logoUrl}
           />
-        </CardContent>
-      </Card>
+        </AppCard>
     </>
   );
 }
@@ -367,13 +355,10 @@ function SmsTemplateTab({
 
   return (
     <div className="space-y-4">
-      <Card className="border-0 shadow-sm">
-        <CardHeader>
-          <CardTitle className="flex items-center gap-2 text-base">
-            <MessageSquare className="h-4 w-4" /> {t('templates.smsTemplates')}
-          </CardTitle>
-        </CardHeader>
-        <CardContent>
+      <AppCard
+        icon={MessageSquare}
+        title={t('templates.smsTemplates')}
+      >
           <p className="mb-4 text-sm text-muted-foreground">
             {t.rich('templates.smsTemplatesDescription', {
               code: (chunks) => <code className="rounded bg-muted px-1 py-0.5 text-xs">{chunks}</code>,
@@ -421,8 +406,7 @@ function SmsTemplateTab({
               </div>
             ))}
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
     </div>
   );
 }

+ 7 - 9
src/app/(authenticated)/settings/workshop/workshop-settings.tsx

@@ -3,7 +3,7 @@
 import { useState } from "react";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Button } from "@/components/ui/button";
@@ -149,12 +149,11 @@ export function WorkshopSettings({ settings, technicians: initialTechnicians = [
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
       <ServiceTypeSelector serviceType={serviceType} onServiceTypeChange={setServiceType} />
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Wrench className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('workshop.title')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-6">
+      <AppCard
+        icon={Wrench}
+        title={t('workshop.title')}
+        contentClassName="space-y-6"
+      >
           <p className="text-sm text-muted-foreground">
             {t('workshop.description')}
           </p>
@@ -361,8 +360,7 @@ export function WorkshopSettings({ settings, technicians: initialTechnicians = [
               </Button>
             </div>
           </SaveButton>
-        </CardContent>
-      </Card>
+        </AppCard>
       </ReadOnlyWrapper>
 
       <AlertDialog open={showAssignDialog} onOpenChange={(open) => {

+ 2 - 1
src/app/(authenticated)/vehicles/[id]/notes-table.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { useCallback, useTransition } from "react";
 import { useFormatDate } from "@/lib/use-format-date";
@@ -207,7 +208,7 @@ export function NotesTable({
               records.map((n) => (
                 <ContextMenu key={n.id} modal={false}>
                 <ContextMenuTrigger asChild>
-                <TableRow className="cursor-pointer" onClick={() => onSelectNote(n)}>
+                <TableRow className="cursor-pointer" {...interactiveRow(() => onSelectNote(n))}>
                   <TableCell className="w-[30px] px-2">
                     {n.isPinned && <Pin className="h-3.5 w-3.5 text-primary" />}
                   </TableCell>

+ 3 - 2
src/app/(authenticated)/vehicles/[id]/service-records-table.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
@@ -371,10 +372,10 @@ export function ServiceRecordsTable({
                   <ContextMenuTrigger asChild>
                   <TableRow
                     className={`cursor-pointer transition-opacity ${navigatingId === record.id ? "opacity-50" : ""}`}
-                    onClick={() => {
+                    {...interactiveRow(() => {
                       setNavigatingId(record.id);
                       router.push(`/vehicles/${vehicleId}/service/${record.id}`);
-                    }}
+                    })}
                   >
                     <TableCell className="font-mono text-xs">
                       {formatDate(effectiveInvoiceDate(record))}

+ 4 - 3
src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { interactiveRow } from '@/lib/interactive-row'
 import { useState, useTransition, useCallback } from 'react'
 import { useRouter, useSearchParams } from 'next/navigation'
 import Link from 'next/link'
@@ -1280,7 +1281,7 @@ export function VehicleDetailClient({
                     <TableRow
                       key={q.id}
                       className="cursor-pointer"
-                      onClick={() => router.push(`/quotes/${q.id}`)}
+                      {...interactiveRow(() => router.push(`/quotes/${q.id}`))}
                     >
                       <TableCell className="font-mono text-xs text-muted-foreground">
                         {q.quoteNumber || '-'}
@@ -1384,7 +1385,7 @@ export function VehicleDetailClient({
                     <TableRow
                       key={insp.id}
                       className="cursor-pointer"
-                      onClick={() => router.push(`/inspections/${insp.id}`)}
+                      {...interactiveRow(() => router.push(`/inspections/${insp.id}`))}
                     >
                       <TableCell className="font-medium">{insp.template.name}</TableCell>
                       <TableCell>
@@ -1510,7 +1511,7 @@ export function VehicleDetailClient({
                 return (
                   <Card
                     key={r.id}
-                    className={`border-0 shadow-sm ${
+                    className={`${
                       urgency === 'overdue'
                         ? 'ring-1 ring-red-500/30'
                         : urgency === 'due-soon'

+ 2 - 1
src/app/(authenticated)/vehicles/vehicles-client.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { interactiveRow } from '@/lib/interactive-row'
 import { useState, useCallback, useTransition, useRef, useEffect } from 'react'
 import { useRouter, usePathname, useSearchParams } from 'next/navigation'
 import Link from 'next/link'
@@ -444,7 +445,7 @@ export function VehiclesClient({
                 <ContextMenuTrigger asChild>
                 <TableRow
                   className="cursor-pointer"
-                  onClick={() => router.push(`/vehicles/${v.id}`)}
+                  {...interactiveRow(() => router.push(`/vehicles/${v.id}`))}
                 >
                   <TableCell className="font-mono text-sm">{v.licensePlate || '-'}</TableCell>
                   <TableCell className="truncate">

+ 3 - 2
src/app/(authenticated)/work-orders/work-orders-client.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useState, useCallback, useTransition } from "react";
@@ -417,10 +418,10 @@ export function WorkOrdersClient({
                   <ContextMenuTrigger asChild>
                   <TableRow
                     className={`cursor-pointer transition-opacity ${navigatingId === r.id ? "opacity-50" : ""}`}
-                    onClick={() => {
+                    {...interactiveRow(() => {
                       setNavigatingId(r.id);
                       router.push(recordHref);
-                    }}
+                    })}
                   >
                     <TableCell className="hidden sm:table-cell font-mono text-xs text-muted-foreground">
                       {r.invoiceNumber || "-"}

+ 31 - 34
src/app/(public)/portal/[orgId]/dashboard/page.tsx

@@ -1,6 +1,6 @@
 import { PortalShell } from '@/features/portal/Components/PortalShell'
 import { getPortalDashboard } from '@/features/portal/Actions/portalActions'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { AppCard } from '@/components/app-card'
 import { Badge } from '@/components/ui/badge'
 import { Button } from '@/components/ui/button'
 import {
@@ -101,18 +101,20 @@ export default async function PortalDashboardPage({
         {/* Two-column main grid */}
         <div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
           {/* Recent invoices — wider */}
-          <Card className="lg:col-span-2">
-            <CardHeader className="flex flex-row items-center justify-between gap-2 pb-3">
-              <CardTitle className="text-base">{t('recentInvoices')}</CardTitle>
+          <AppCard
+            title={t('recentInvoices')}
+            className="lg:col-span-2"
+            contentClassName="pt-4"
+            footer={
               <Link
                 href={`/portal/${orgId}/invoices`}
-                className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
               >
                 {t('viewAll')}
                 <ArrowRight className="h-3 w-3" />
               </Link>
-            </CardHeader>
-            <CardContent className="pt-0">
+            }
+          >
               {d.recentInvoices.length === 0 ? (
                 <p className="py-6 text-center text-sm text-muted-foreground">
                   {t('noInvoicesYet')}
@@ -158,22 +160,22 @@ export default async function PortalDashboardPage({
                   ))}
                 </ul>
               )}
-            </CardContent>
-          </Card>
+          </AppCard>
 
           {/* Recent quotes — narrower */}
-          <Card>
-            <CardHeader className="flex flex-row items-center justify-between gap-2 pb-3">
-              <CardTitle className="text-base">{t('recentQuotes')}</CardTitle>
+          <AppCard
+            title={t('recentQuotes')}
+            contentClassName="pt-4"
+            footer={
               <Link
                 href={`/portal/${orgId}/quotes`}
-                className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
+                className="flex w-full items-center justify-between font-medium transition-colors hover:text-foreground"
               >
                 {t('viewAll')}
                 <ArrowRight className="h-3 w-3" />
               </Link>
-            </CardHeader>
-            <CardContent className="pt-0">
+            }
+          >
               {d.recentQuotes.length === 0 ? (
                 <p className="py-6 text-center text-sm text-muted-foreground">{t('noQuotesYet')}</p>
               ) : (
@@ -208,20 +210,16 @@ export default async function PortalDashboardPage({
                   ))}
                 </ul>
               )}
-            </CardContent>
-          </Card>
+          </AppCard>
         </div>
 
         {/* Active jobs (only if any) */}
         {d.activeJobs.length > 0 && (
-          <Card>
-            <CardHeader className="flex flex-row items-center justify-between gap-2 pb-3">
-              <CardTitle className="flex items-center gap-2 text-base">
-                <Activity className="h-4 w-4 text-primary" />
-                {t('activeJobs')}
-              </CardTitle>
-            </CardHeader>
-            <CardContent className="pt-0">
+          <AppCard
+            icon={Activity}
+            title={t('activeJobs')}
+            contentClassName="pt-0"
+          >
               <ul className="divide-y">
                 {d.activeJobs.map((job) => (
                   <li
@@ -241,23 +239,23 @@ export default async function PortalDashboardPage({
                   </li>
                 ))}
               </ul>
-            </CardContent>
-          </Card>
+            </AppCard>
         )}
 
         {/* Pending requests (only if any) */}
         {d.pendingRequests.length > 0 && (
-          <Card>
-            <CardHeader className="flex flex-row items-center justify-between gap-2 pb-3">
-              <CardTitle className="text-base">{t('pendingServiceRequests')}</CardTitle>
+          <AppCard
+            title={t('pendingServiceRequests')}
+            action={
               <Button asChild size="sm" variant="outline">
                 <Link href={`/portal/${orgId}/request-service`}>
                   <Wrench className="mr-1.5 h-3.5 w-3.5" />
                   {t('newRequest')}
                 </Link>
               </Button>
-            </CardHeader>
-            <CardContent className="pt-0">
+            }
+            contentClassName="pt-4"
+          >
               <ul className="divide-y">
                 {d.pendingRequests.map((req) => (
                   <li
@@ -277,8 +275,7 @@ export default async function PortalDashboardPage({
                   </li>
                 ))}
               </ul>
-            </CardContent>
-          </Card>
+          </AppCard>
         )}
       </div>
     </PortalShell>

+ 8 - 26
src/app/(public)/portal/[orgId]/request-service/page.tsx

@@ -5,12 +5,7 @@ import {
   getPortalServiceRequests,
 } from "@/features/portal/Actions/portalActions";
 import { Badge } from "@/components/ui/badge";
-import {
-  Card,
-  CardContent,
-  CardHeader,
-  CardTitle,
-} from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { getTranslations } from "next-intl/server";
 
 export default async function PortalRequestServicePage({
@@ -64,26 +59,14 @@ export default async function PortalRequestServicePage({
           </p>
         </div>
 
-        {vehicles.length === 0 ? (
-          <Card>
-            <CardContent className="py-8 text-center">
-              <p className="text-muted-foreground">
-                {t('noVehicles')}
-              </p>
-            </CardContent>
-          </Card>
-        ) : (
-          <ServiceRequestForm orgId={orgId} vehicles={vehicles} />
-        )}
+        {/* The form handles the no-vehicles case itself: it opens in
+            "describe your vehicle" mode, so new customers can still ask. */}
+        <ServiceRequestForm orgId={orgId} vehicles={vehicles} />
 
         {requests.length > 0 && (
-          <Card>
-            <CardHeader>
-              <CardTitle className="text-base">
-                {t('yourRequests')}
-              </CardTitle>
-            </CardHeader>
-            <CardContent>
+          <AppCard
+            title={t('yourRequests')}
+          >
               <div className="space-y-3">
                 {requests.map((req) => (
                   <div
@@ -114,8 +97,7 @@ export default async function PortalRequestServicePage({
                   </div>
                 ))}
               </div>
-            </CardContent>
-          </Card>
+            </AppCard>
         )}
       </div>
     </PortalShell>

+ 32 - 29
src/app/(public)/portal/[orgId]/vehicles/[vehicleId]/page.tsx

@@ -1,9 +1,9 @@
 import { PortalShell } from '@/features/portal/Components/PortalShell'
 import { getPortalVehicleDetail } from '@/features/portal/Actions/portalActions'
-import { Card, CardContent } from '@/components/ui/card'
+import { AppCard } from '@/components/app-card'
 import { Badge } from '@/components/ui/badge'
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
-import { Download } from 'lucide-react'
+import { ClipboardCheck, Download, Wrench } from 'lucide-react'
 import Link from 'next/link'
 import { getTranslations } from 'next-intl/server'
 import { db } from '@/lib/db'
@@ -49,13 +49,6 @@ export default async function PortalVehicleDetailPage({
       <div className="space-y-6">
         {/* Vehicle info header */}
         <div className="flex items-start gap-4">
-          {v.imageUrl ? (
-            <img
-              src={v.imageUrl}
-              alt={`${v.make} ${v.model}`}
-              className="h-20 w-20 rounded object-cover"
-            />
-          ) : null}
           <div>
             <h1 className="text-2xl font-bold">
               {v.year} {v.make} {v.model}
@@ -94,15 +87,20 @@ export default async function PortalVehicleDetailPage({
           </TabsList>
 
           <TabsContent value="service-history" className="mt-4">
-            {v.serviceRecords.length === 0 ? (
-              <p className="py-8 text-center text-sm text-muted-foreground">
-                {t('noServiceRecords')}
-              </p>
-            ) : (
-              <div className="space-y-3">
+            <AppCard
+              icon={Wrench}
+              title={t('serviceHistoryTitle')}
+              badge={v.serviceRecords.length || undefined}
+              contentClassName="p-0"
+            >
+              {v.serviceRecords.length === 0 ? (
+                <p className="py-8 text-center text-sm text-muted-foreground">
+                  {t('noServiceRecords')}
+                </p>
+              ) : (
+              <div className="divide-y">
                 {v.serviceRecords.map((sr) => (
-                  <Card key={sr.id}>
-                    <CardContent className="flex items-center justify-between py-4">
+                    <div key={sr.id} className="flex flex-col gap-2 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
                       <div className="min-w-0">
                         <p className="truncate text-sm font-medium">
                           {sr.invoiceNumber ? `#${sr.invoiceNumber} - ` : ''}
@@ -148,18 +146,24 @@ export default async function PortalVehicleDetailPage({
                           {tInvoices('download')}
                         </a>
                       </div>
-                    </CardContent>
-                  </Card>
+                    </div>
                 ))}
               </div>
-            )}
+              )}
+            </AppCard>
           </TabsContent>
 
           <TabsContent value="inspections" className="mt-4">
-            {v.inspections.length === 0 ? (
-              <p className="py-8 text-center text-sm text-muted-foreground">{t('noInspections')}</p>
-            ) : (
-              <div className="space-y-3">
+            <AppCard
+              icon={ClipboardCheck}
+              title={t('inspectionsTitle')}
+              badge={v.inspections.length || undefined}
+              contentClassName="p-0"
+            >
+              {v.inspections.length === 0 ? (
+                <p className="py-8 text-center text-sm text-muted-foreground">{t('noInspections')}</p>
+              ) : (
+              <div className="divide-y">
                 {v.inspections.map((insp) => {
                   const conditions = insp.items.reduce(
                     (acc, item) => {
@@ -170,8 +174,7 @@ export default async function PortalVehicleDetailPage({
                   )
 
                   return (
-                    <Card key={insp.id}>
-                      <CardContent className="flex items-center justify-between py-4">
+                    <div key={insp.id} className="flex flex-col gap-2 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
                         <div className="min-w-0">
                           <p className="truncate text-sm font-medium">{insp.template.name}</p>
                           <p className="text-xs text-muted-foreground">
@@ -208,12 +211,12 @@ export default async function PortalVehicleDetailPage({
                             </Link>
                           )}
                         </div>
-                      </CardContent>
-                    </Card>
+                      </div>
                   )
                 })}
               </div>
-            )}
+              )}
+            </AppCard>
           </TabsContent>
         </Tabs>
       </div>

+ 77 - 40
src/app/(public)/portal/[orgId]/vehicles/page.tsx

@@ -1,7 +1,14 @@
 import { PortalShell } from "@/features/portal/Components/PortalShell";
 import { getPortalVehicles } from "@/features/portal/Actions/portalActions";
-import { Card, CardContent } from "@/components/ui/card";
-import { Car } from "lucide-react";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from "@/components/ui/table";
+import { Car, ChevronRight } from "lucide-react";
 import Link from "next/link";
 import { getTranslations } from "next-intl/server";
 
@@ -40,48 +47,78 @@ export default async function PortalVehiclesPage({
             </p>
           </div>
         ) : (
-          <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
+          <>
+          {/* Card list (phones + small tablets) */}
+          <div className="space-y-2 md:hidden">
             {vehicles.map((v) => (
-              <Link key={v.id} href={`/portal/${orgId}/vehicles/${v.id}`}>
-                <Card className="transition-colors hover:bg-muted/50">
-                  <CardContent className="pt-6">
-                    <div className="flex items-start gap-4">
-                      {v.imageUrl ? (
-                        <img
-                          src={v.imageUrl}
-                          alt={`${v.make} ${v.model}`}
-                          className="h-16 w-16 rounded object-cover"
-                        />
-                      ) : (
-                        <div className="flex h-16 w-16 items-center justify-center rounded bg-muted">
-                          <Car className="h-8 w-8 text-muted-foreground/50" />
-                        </div>
-                      )}
-                      <div className="min-w-0 flex-1">
-                        <p className="truncate font-semibold">
-                          {v.year} {v.make} {v.model}
-                        </p>
-                        {v.licensePlate && (
-                          <p className="text-sm text-muted-foreground">
-                            {v.licensePlate}
-                          </p>
-                        )}
-                        {v.color && (
-                          <p className="text-xs text-muted-foreground">
-                            {v.color}
-                          </p>
-                        )}
-                        <p className="mt-1 text-xs text-muted-foreground">
-                          {t('serviceCount', { count: v._count.serviceRecords })} &middot;{" "}
-                          {t('inspectionCount', { count: v._count.inspections })}
-                        </p>
-                      </div>
-                    </div>
-                  </CardContent>
-                </Card>
+              <Link
+                key={v.id}
+                href={`/portal/${orgId}/vehicles/${v.id}`}
+                className="flex items-center justify-between gap-3 rounded-lg border bg-card p-3 active:bg-muted/50"
+              >
+                <div className="min-w-0 flex-1">
+                  <div className="flex items-start justify-between gap-3">
+                    <span className="min-w-0 flex-1 truncate font-medium">
+                      {v.year} {v.make} {v.model}
+                    </span>
+                    {v.licensePlate && (
+                      <span className="shrink-0 font-mono text-sm">{v.licensePlate}</span>
+                    )}
+                  </div>
+                  <p className="mt-1 text-xs text-muted-foreground">
+                    {t('serviceCount', { count: v._count.serviceRecords })} &middot;{" "}
+                    {t('inspectionCount', { count: v._count.inspections })}
+                  </p>
+                </div>
+                <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
               </Link>
             ))}
           </div>
+
+          {/* Table (md and up). Server-rendered, so the row's link — a real
+              anchor on the vehicle name — carries the navigation. */}
+          <div className="hidden rounded-lg border md:block">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>{t('columnVehicle')}</TableHead>
+                  <TableHead className="w-[140px]">{t('columnPlate')}</TableHead>
+                  <TableHead className="w-[130px]">{t('columnServices')}</TableHead>
+                  <TableHead className="w-[130px]">{t('columnInspections')}</TableHead>
+                  <TableHead className="w-10" />
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {vehicles.map((v) => (
+                  <TableRow key={v.id} className="group relative">
+                    <TableCell className="font-medium">
+                      {/* after:inset-0 stretches the click target across the
+                          whole row while staying one real, focusable link. */}
+                      <Link
+                        href={`/portal/${orgId}/vehicles/${v.id}`}
+                        className="after:absolute after:inset-0 focus-visible:outline-none group-hover:underline"
+                      >
+                        {v.year} {v.make} {v.model}
+                      </Link>
+                    </TableCell>
+                    <TableCell className="font-mono text-sm text-muted-foreground">
+                      {v.licensePlate || "-"}
+                    </TableCell>
+                    <TableCell className="text-sm text-muted-foreground">
+                      {t('serviceCount', { count: v._count.serviceRecords })}
+                    </TableCell>
+                    <TableCell className="text-sm text-muted-foreground">
+                      {t('inspectionCount', { count: v._count.inspections })}
+                    </TableCell>
+                    <TableCell>
+                      <ChevronRight className="h-4 w-4 text-muted-foreground" />
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+          </>
         )}
       </div>
     </PortalShell>

+ 31 - 1
src/app/globals.css

@@ -21,6 +21,7 @@
   --color-destructive: var(--destructive);
   --color-destructive-foreground: var(--destructive-foreground);
   --color-border: var(--border);
+  --color-card-edge: var(--card-edge);
   --color-input: var(--input);
   --color-ring: var(--ring);
   --color-chart-1: var(--chart-1);
@@ -303,6 +304,16 @@
   --sidebar-ring: oklch(0.72 0.15 165);
 }
 
+/* Card edges. --border sits about one shade off the card fill, which reads as
+   no edge at all once a shadow is over it. This mixes the theme's own
+   foreground into the edge instead, so it stays visible in every theme —
+   dark ones included — without hardcoding a colour per theme. Declared on the
+   same element the theme classes land on (documentElement), so `var(--foreground)`
+   resolves to whichever theme is active. */
+:root {
+  --card-edge: color-mix(in oklab, var(--foreground) 20%, transparent);
+}
+
 @layer base {
   * {
     @apply border-border outline-ring/50;
@@ -317,6 +328,16 @@
   }
 }
 
+/* Keyboard-focusable rows (see src/lib/interactive-row.ts). Inset outline so
+   the ring survives overflow-hidden card and table corners. */
+[data-row-interactive] {
+  cursor: pointer;
+}
+[data-row-interactive]:focus-visible {
+  outline: 2px solid var(--ring);
+  outline-offset: -2px;
+}
+
 /* Hide scrollbar utility */
 .scrollbar-none {
   scrollbar-width: none;
@@ -586,7 +607,16 @@
   flex-direction: column;
   overflow: hidden;
 }
-.dashboard-card-wrap > *:first-child > *:last-child {
+/* Legacy Card markup: whatever comes last is the content, so it scrolls.
+   Guarded so an AppCard footer (which is also a last child) never becomes
+   the scroll area. */
+.dashboard-card-wrap > *:first-child > *:last-child:not([data-slot="app-card-footer"]) {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+}
+/* AppCard names its content region, so target it directly. */
+.dashboard-card-wrap > *:first-child > [data-slot="app-card-content"] {
   flex: 1;
   min-height: 0;
   overflow-y: auto;

+ 114 - 0
src/components/app-card.tsx

@@ -0,0 +1,114 @@
+import type { ComponentType, ReactNode } from 'react'
+import { cn } from '@/lib/utils'
+
+/**
+ * The house card. One look for every panel in the app: an icon chip in the
+ * theme colour, a title block, an optional action on the right, and a hairline
+ * under the header that starts in primary and fades out. Compose the inside
+ * freely — lists pass `contentClassName="p-0"` and draw their own rows.
+ *
+ * The design conceit is that the chip is the card's light source: the sheen
+ * is a radial glow centred on it, the chip face carries a top-lit gradient,
+ * and the hairline is brightest directly beneath it. Hovering the card feeds
+ * the light — glow, hairline and border all lift a step, smoothly. None of it
+ * is load-bearing; every effect degrades to a plain bordered card.
+ *
+ * The content region carries data-slot="app-card-content": the dashboard
+ * grid's CSS targets it as the scroll area, so the footer stays pinned below
+ * a scrolling list instead of becoming the scroll area itself.
+ */
+export function AppCard({
+  icon: Icon,
+  title,
+  badge,
+  description,
+  action,
+  subheader,
+  footer,
+  children,
+  className,
+  contentClassName,
+}: {
+  icon?: ComponentType<{ className?: string }>
+  title: ReactNode
+  /** Small count pill after the title. Pass `list.length || undefined` to hide zeros. */
+  badge?: ReactNode
+  /** One quiet line under the title. */
+  description?: ReactNode
+  /** Right-aligned header slot: an icon button, a "view all" link. */
+  action?: ReactNode
+  /** Full-width row between the title block and the hairline (tabs, filters). */
+  subheader?: ReactNode
+  /** Pinned strip under the content: a "view all" link, a total, a caption. */
+  footer?: ReactNode
+  children: ReactNode
+  className?: string
+  /** Overrides the content padding; lists that draw their own rows pass "p-0". */
+  contentClassName?: string
+}) {
+  return (
+    <div
+      data-slot="card"
+      className={cn(
+        'group/card relative flex flex-col overflow-hidden rounded-xl border border-card-edge bg-card text-card-foreground',
+        // Two shadows instead of one: a crisp 1px seat and a soft, low drop.
+        // Reads as depth rather than the flat grey smudge of shadow-sm.
+        'shadow-[0_1px_2px_rgb(0_0_0/0.05),0_12px_32px_-16px_rgb(0_0_0/0.18)]',
+        // Hover: the whole card wakes one step — deeper drop, edge leaning
+        // primary. Deliberately smaller than the tiles' hover so containers
+        // never read as buttons.
+        'transition-[border-color,box-shadow] duration-300 hover:border-primary/30 hover:shadow-[0_1px_2px_rgb(0_0_0/0.05),0_16px_40px_-16px_rgb(0_0_0/0.22)]',
+        // The sheen: a radial glow centred on the icon chip rather than a flat
+        // band, so the header reads as lit by the chip. A second, brighter
+        // pass fades in on hover — gradients can't transition, opacity can.
+        // inset-0, not a fixed-height strip: the ellipse must finish its own
+        // fade-out inside the layer, or its clip edge draws a hard line.
+        'before:pointer-events-none before:absolute before:inset-0 before:bg-[radial-gradient(8rem_5rem_at_2.75rem_2.5rem,color-mix(in_oklab,var(--primary)_9%,transparent),transparent_70%)]',
+        'after:pointer-events-none after:absolute after:inset-0 after:opacity-0 after:transition-opacity after:duration-300 after:bg-[radial-gradient(9.5rem_6rem_at_2.75rem_2.5rem,color-mix(in_oklab,var(--primary)_8%,transparent),transparent_70%)] hover:after:opacity-100',
+        className
+      )}
+    >
+      <div className="relative shrink-0">
+        <div className="flex items-start gap-3 px-5 pb-3 pt-4">
+          {Icon && (
+            <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-linear-to-b from-primary/18 to-primary/6 text-primary ring-1 ring-inset ring-primary/25 shadow-[inset_0_1px_0_rgb(255_255_255/0.15)]">
+              <Icon className="h-4 w-4" />
+            </div>
+          )}
+          <div className="min-w-0 flex-1 self-center">
+            <h3 className="flex items-center gap-2 text-sm font-semibold tracking-tight">
+              <span className="truncate">{title}</span>
+              {badge != null && (
+                <span className="shrink-0 rounded-full bg-primary/10 px-1.5 py-px text-[11px] font-semibold tabular-nums text-primary ring-1 ring-inset ring-primary/15">
+                  {badge}
+                </span>
+              )}
+            </h3>
+            {description && (
+              <p className="mt-0.5 truncate text-xs text-muted-foreground">{description}</p>
+            )}
+          </div>
+          {action && <div className="shrink-0 self-center">{action}</div>}
+        </div>
+        {subheader && <div className="px-5 pb-3">{subheader}</div>}
+        {/* Signature hairline: primary under the chip, gone by the far edge.
+            The brighter copy fades in with the hover glow. */}
+        <div className="relative h-px">
+          <div className="absolute inset-0 bg-linear-to-r from-primary/40 via-card-edge to-transparent" />
+          <div className="absolute inset-0 bg-linear-to-r from-primary/70 via-primary/15 to-transparent opacity-0 transition-opacity duration-300 group-hover/card:opacity-100" />
+        </div>
+      </div>
+      <div data-slot="app-card-content" className={cn('min-h-0 flex-1 p-5 pt-4', contentClassName)}>
+        {children}
+      </div>
+      {footer && (
+        <div
+          data-slot="app-card-footer"
+          className="shrink-0 border-t border-card-edge/60 bg-muted/30 px-5 py-2.5 text-xs text-muted-foreground"
+        >
+          {footer}
+        </div>
+      )}
+    </div>
+  )
+}

+ 4 - 1
src/components/ui/card.tsx

@@ -7,7 +7,10 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
     <div
       data-slot="card"
       className={cn(
-        "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
+        // Matches the AppCard shell (edge + layered shadow) so untitled
+        // utility cards — empty states, wrappers — sit in the same family.
+        // Headers, chips and hairlines come from AppCard, not from here.
+        "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-card-edge py-6 shadow-[0_1px_2px_rgb(0_0_0/0.05),0_12px_32px_-16px_rgb(0_0_0/0.18)]",
         className
       )}
       {...props}

+ 5 - 1
src/components/ui/sidebar.tsx

@@ -474,7 +474,11 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
 }
 
 const sidebarMenuButtonVariants = cva(
-  "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+  // The active item is marked three ways: the accent fill, a primary bar down
+  // its leading edge, and a primary icon. The label keeps the high-contrast
+  // accent foreground — several themes set --sidebar-primary to a light amber
+  // that is unreadable as body text on a light sidebar.
+  "peer/menu-button relative flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding,background-color,color] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:text-sidebar-primary data-[active=true]:before:absolute data-[active=true]:before:left-0 data-[active=true]:before:top-1/2 data-[active=true]:before:h-[60%] data-[active=true]:before:w-[3px] data-[active=true]:before:-translate-y-1/2 data-[active=true]:before:rounded-r-full data-[active=true]:before:bg-sidebar-primary group-data-[collapsible=icon]:before:hidden data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
   {
     variants: {
       variant: {

+ 8 - 11
src/features/admin/Components/admin-organization-details.tsx

@@ -1,8 +1,9 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import Link from 'next/link'
 import { useTranslations } from 'next-intl'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Card, CardHeader, CardContent, CardTitle } from '@/components/ui/card'
 import { Badge } from '@/components/ui/badge'
 import {
   ArrowLeft,
@@ -104,14 +105,11 @@ export function AdminOrganizationDetails({ org }: { org: OrgDetails }) {
         {t('orgDetails.back')}
       </Link>
 
-      <Card>
-        <CardHeader>
-          <CardTitle className="flex items-center gap-2">
-            <Building2 className="h-5 w-5 text-muted-foreground" />
-            {org.name}
-          </CardTitle>
-        </CardHeader>
-        <CardContent className="grid gap-3 text-sm sm:grid-cols-2">
+      <AppCard
+        icon={Building2}
+        title={org.name}
+        contentClassName="grid gap-3 text-sm sm:grid-cols-2"
+      >
           <div>
             <p className="text-muted-foreground">{t('users.created')}</p>
             <p className="font-medium">{formatDate(org.createdAt)}</p>
@@ -150,8 +148,7 @@ export function AdminOrganizationDetails({ org }: { org: OrgDetails }) {
               </p>
             )}
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       <div className="grid gap-3 grid-cols-2 sm:grid-cols-3 lg:grid-cols-6">
         {statCards.map((s) => (

+ 13 - 19
src/features/admin/Components/admin-settings.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import { useState, useTransition } from 'react'
 import { useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
@@ -8,7 +9,6 @@ import { Input } from '@/components/ui/input'
 import { Button } from '@/components/ui/button'
 import { Label } from '@/components/ui/label'
 import { Switch } from '@/components/ui/switch'
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
 import { Loader2, Send } from 'lucide-react'
 import { SYSTEM_SETTING_KEYS } from '../Schema/systemSettingsSchema'
 import type { SystemSettingsMap } from '../Schema/systemSettingsSchema'
@@ -205,12 +205,11 @@ export function AdminSettings({
   return (
     <div className="space-y-6">
       {/* Platform Settings */}
-      <Card>
-        <CardHeader>
-          <CardTitle>{t('adminSettings.platformTitle')}</CardTitle>
-          <CardDescription>{t('adminSettings.platformDescription')}</CardDescription>
-        </CardHeader>
-        <CardContent className="space-y-6">
+      <AppCard
+        title={t('adminSettings.platformTitle')}
+        description={t('adminSettings.platformDescription')}
+        contentClassName="space-y-6"
+      >
           <div className="flex items-center justify-between">
             <div className="space-y-0.5">
               <Label htmlFor="registration-toggle">{t('adminSettings.disableRegistration')}</Label>
@@ -237,18 +236,14 @@ export function AdminSettings({
               onCheckedChange={setEmailVerificationRequired}
             />
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Email Settings */}
-      <Card>
-        <CardHeader>
-          <CardTitle>{t('adminSettings.emailTitle')}</CardTitle>
-          <CardDescription>
-            {t('adminSettings.emailDescription')}
-          </CardDescription>
-        </CardHeader>
-        <CardContent className="space-y-6">
+      <AppCard
+        title={t('adminSettings.emailTitle')}
+        description={t('adminSettings.emailDescription')}
+        contentClassName="space-y-6"
+      >
           <div className="flex flex-wrap gap-2">
             <Button
               type="button"
@@ -678,8 +673,7 @@ export function AdminSettings({
               {t('adminSettings.testEmailHint')}
             </p>
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Save Button */}
       <div className="flex justify-end">

+ 2 - 1
src/features/admin/Components/admin-users.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { interactiveRow } from '@/lib/interactive-row';
 import { useDebouncedSearch } from '@/hooks/use-debounced-search';
 
 import { useCallback, useTransition } from 'react'
@@ -365,7 +366,7 @@ export function AdminUsers({
                 <TableRow
                   key={user.id}
                   className="cursor-pointer"
-                  onClick={() => router.push(`/admin/users/${user.id}`)}
+                  {...interactiveRow(() => router.push(`/admin/users/${user.id}`))}
                 >
                   <TableCell className="font-medium">{user.name}</TableCell>
                   <TableCell>

+ 18 - 28
src/features/ai/Components/AiSettingsForm.tsx

@@ -9,13 +9,7 @@ import { Button } from "@/components/ui/button";
 import { Label } from "@/components/ui/label";
 import { Switch } from "@/components/ui/switch";
 import { Badge } from "@/components/ui/badge";
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardHeader,
-  CardTitle,
-} from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import {
   Select,
   SelectContent,
@@ -156,25 +150,22 @@ export function AiSettingsForm({
     <div className="space-y-6">
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
-        <Card>
-          <CardHeader>
-            <div className="flex items-center justify-between">
-              <CardTitle className="flex items-center gap-2">
-                <Sparkles className="h-5 w-5" />
-                {t("ai.title")}
-              </CardTitle>
-              <a
-                href="https://torqvoice.com/docs/integrations/ai"
-                target="_blank"
-                rel="noopener noreferrer"
-                className="text-[11px] text-muted-foreground hover:text-foreground transition-colors"
-              >
-                {t("ai.readMore")} →
-              </a>
-            </div>
-            <CardDescription>{t("ai.description")}</CardDescription>
-          </CardHeader>
-          <CardContent className="space-y-6">
+        <AppCard
+          icon={Sparkles}
+          title={t("ai.title")}
+          description={t("ai.description")}
+          action={
+            <a
+              href="https://torqvoice.com/docs/integrations/ai"
+              target="_blank"
+              rel="noopener noreferrer"
+              className="text-[11px] text-muted-foreground transition-colors hover:text-foreground"
+            >
+              {t("ai.readMore")} →
+            </a>
+          }
+          contentClassName="space-y-6"
+        >
             <div className="flex items-center justify-between">
               <div className="space-y-0.5">
                 <Label htmlFor="enable-ai">{t("ai.enableLabel")}</Label>
@@ -299,8 +290,7 @@ export function AiSettingsForm({
                 </div>
               </>
             )}
-          </CardContent>
-        </Card>
+        </AppCard>
 
         <SaveButton>
           <div className="flex justify-end">

+ 2 - 2
src/features/billing/Components/RecurringInvoicesClient.tsx

@@ -339,13 +339,13 @@ export default function RecurringInvoicesClient({
 
       {/* Table */}
       {invoices.length === 0 ? (
-        <Card className="border-0 shadow-sm">
+        <Card>
           <CardContent className="flex flex-col items-center justify-center py-12">
             <p className="text-muted-foreground">{t("recurring.noInvoices")}</p>
           </CardContent>
         </Card>
       ) : (
-        <Card className="border-0 shadow-sm">
+        <Card>
           <CardContent className="p-0">
             {/* Card list (phones + small tablets) */}
             <div className="space-y-2 p-3 md:hidden">

+ 2 - 2
src/features/calendar/Components/CalendarClient.tsx

@@ -265,7 +265,7 @@ export default function CalendarClient({
       <div className="grid gap-4 lg:grid-cols-3">
         {/* Calendar grid */}
         <div className="lg:col-span-2">
-          <Card className="border-0 shadow-sm">
+          <Card>
             <CardContent className="p-4">
               {/* Header */}
               <div className="flex items-center justify-between mb-4">
@@ -379,7 +379,7 @@ export default function CalendarClient({
 
         {/* Event list sidebar */}
         <div>
-          <Card className="border-0 shadow-sm">
+          <Card>
             <CardContent className="p-4">
               <CalendarEventList
                 events={filteredEvents}

+ 8 - 9
src/features/custom-fields/Components/CustomFieldsForm.tsx

@@ -13,7 +13,8 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
+import { Card, CardContent } from "@/components/ui/card";
 import { getCustomFieldValues, saveCustomFieldValues, getFieldDefinitions } from "@/features/custom-fields/Actions/customFieldActions";
 import { Loader2 } from "lucide-react";
 import { cn } from "@/lib/utils";
@@ -111,7 +112,7 @@ export function CustomFieldsForm({
 
   if (loading) {
     return (
-      <Card className="border-0 shadow-sm">
+      <Card>
         <CardContent className="flex items-center justify-center py-6">
           <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
         </CardContent>
@@ -122,11 +123,10 @@ export function CustomFieldsForm({
   if (fields.length === 0) return null;
 
   return (
-    <Card className="border-0 shadow-sm">
-      <CardHeader>
-        <CardTitle className="text-base">Custom Fields</CardTitle>
-      </CardHeader>
-      <CardContent className="space-y-4">
+    <AppCard
+      title="Custom Fields"
+      contentClassName="space-y-4"
+    >
         {fields.map((field) => {
           const val = values[field.id] ?? field.value ?? "";
           const hasError = !!errors[field.id];
@@ -209,8 +209,7 @@ export function CustomFieldsForm({
             </div>
           );
         })}
-      </CardContent>
-    </Card>
+      </AppCard>
   );
 }
 

+ 2 - 1
src/features/custom-fields/Components/CustomFieldsManager.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { interactiveRow } from '@/lib/interactive-row'
 import { useState } from 'react'
 import { useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
@@ -221,7 +222,7 @@ export function CustomFieldsManager({
               <div
                 key={field.id}
                 className="flex items-center gap-3 px-3 py-2 hover:bg-muted/50 transition-colors cursor-pointer"
-                onClick={() => openEdit(field)}
+                {...interactiveRow(() => openEdit(field))}
               >
                 <div className="min-w-0 flex-1">
                   <div className="flex items-center gap-1.5">

+ 31 - 39
src/features/dashboard/Components/CustomTableCard.tsx

@@ -1,10 +1,11 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useEffect, useState } from "react";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
+import { AppCard } from "@/components/app-card";
 import {
   Table,
   TableBody,
@@ -72,51 +73,43 @@ export function CustomTableCard({
   };
 
   return (
-    <Card className="border-0 shadow-sm">
-      <CardHeader className="pb-3">
-        <div className="flex items-center justify-between">
-          <CardTitle className="flex items-center gap-2 text-base">
-            <Table2 className="h-4 w-4" />
-            {widget.name}
-          </CardTitle>
-          {editing && (
-            <div className="dashboard-no-drag relative z-20 flex items-center gap-1">
-              <Button
-                variant="ghost"
-                size="icon"
-                className="h-7 w-7"
-                onClick={onEdit}
-              >
-                <Pencil className="h-3.5 w-3.5" />
-                <span className="sr-only">{t("editCard")}</span>
-              </Button>
-              <Button
-                variant="ghost"
-                size="icon"
-                className="h-7 w-7 text-muted-foreground hover:text-destructive"
-                onClick={onDelete}
-              >
-                <Trash2 className="h-3.5 w-3.5" />
-                <span className="sr-only">{t("deleteCard")}</span>
-              </Button>
-            </div>
-          )}
-        </div>
-      </CardHeader>
-      <CardContent className="pt-0">
+    <AppCard
+      icon={Table2}
+      title={widget.name}
+      action={
+        editing ? (
+          <div className="dashboard-no-drag relative z-20 flex items-center gap-1">
+            <Button variant="ghost" size="icon" className="h-7 w-7" onClick={onEdit}>
+              <Pencil className="h-3.5 w-3.5" />
+              <span className="sr-only">{t("editCard")}</span>
+            </Button>
+            <Button
+              variant="ghost"
+              size="icon"
+              className="h-7 w-7 text-muted-foreground hover:text-destructive"
+              onClick={onDelete}
+            >
+              <Trash2 className="h-3.5 w-3.5" />
+              <span className="sr-only">{t("deleteCard")}</span>
+            </Button>
+          </div>
+        ) : undefined
+      }
+      contentClassName="p-0"
+    >
         {error ? (
-          <p className="py-6 text-center text-sm text-muted-foreground">{error}</p>
+          <p className="px-5 py-6 text-center text-sm text-muted-foreground">{error}</p>
         ) : rows === null ? (
           <div className="flex justify-center py-6">
             <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
           </div>
         ) : rows.length === 0 ? (
-          <p className="py-6 text-center text-sm text-muted-foreground">{t("noResults")}</p>
+          <p className="px-5 py-6 text-center text-sm text-muted-foreground">{t("noResults")}</p>
         ) : (
           <>
           {/* Card list (phones + small tablets): the first column heads each
               row, the rest become label/value pairs. */}
-          <div className="space-y-2 md:hidden">
+          <div className="space-y-2 p-3 md:hidden">
             {rows.map((row) => (
               <button
                 key={row.id}
@@ -160,7 +153,7 @@ export function CustomTableCard({
                 <TableRow
                   key={row.id}
                   className="cursor-pointer"
-                  onClick={() => router.push(row.href)}
+                  {...interactiveRow(() => router.push(row.href))}
                 >
                   {columns.map((col) => (
                     <TableCell key={col} className="truncate text-sm">
@@ -174,7 +167,6 @@ export function CustomTableCard({
           </div>
           </>
         )}
-      </CardContent>
-    </Card>
+    </AppCard>
   );
 }

+ 7 - 17
src/features/email/Components/EmailSettingsForm.tsx

@@ -8,13 +8,7 @@ import { Input } from "@/components/ui/input";
 import { Button } from "@/components/ui/button";
 import { Label } from "@/components/ui/label";
 import { Switch } from "@/components/ui/switch";
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardHeader,
-  CardTitle,
-} from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Loader2, Send, Info } from "lucide-react";
 import { ORG_EMAIL_KEYS } from "../Schema/emailSettingsSchema";
 import {
@@ -239,14 +233,11 @@ export function EmailSettingsForm({
     <div className="space-y-6">
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
-      <Card>
-        <CardHeader>
-          <CardTitle>{t("email.title")}</CardTitle>
-          <CardDescription>
-            {t("email.description")}
-          </CardDescription>
-        </CardHeader>
-        <CardContent className="space-y-6">
+      <AppCard
+        title={t("email.title")}
+        description={t("email.description")}
+        contentClassName="space-y-6"
+      >
           <div className="flex items-center justify-between">
             <div className="space-y-0.5">
               <Label htmlFor="use-custom-email">
@@ -763,8 +754,7 @@ export function EmailSettingsForm({
               </div>
             </>
           )}
-        </CardContent>
-      </Card>
+        </AppCard>
 
       <SaveButton>
         <div className="flex justify-end">

+ 4 - 3
src/features/inventory/Components/InventoryPartForm.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useState, useRef, useCallback, useEffect } from "react";
 import { useTranslations } from "next-intl";
 import Link from "next/link";
@@ -405,7 +406,7 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier,
                     <div
                       key={g.url}
                       className="relative aspect-square overflow-hidden rounded-lg border bg-muted cursor-pointer"
-                      onClick={() => { onOpenChange(false); onViewImages?.(gallery.map(img => img.url), i); }}
+                      {...interactiveRow(() => { onOpenChange(false); onViewImages?.(gallery.map(img => img.url), i); })}
                     >
                       <img src={g.url} alt={`Part ${i + 1}`} className="h-full w-full object-cover" />
                       <button
@@ -425,7 +426,7 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier,
                     onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
                     onDragLeave={() => setDragOver(false)}
                     onDrop={handleDrop}
-                    onClick={() => fileInputRef.current?.click()}
+                    {...interactiveRow(() => fileInputRef.current?.click())}
                   >
                     {uploading ? (
                       <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
@@ -442,7 +443,7 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier,
                   onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
                   onDragLeave={() => setDragOver(false)}
                   onDrop={handleDrop}
-                  onClick={() => fileInputRef.current?.click()}
+                  {...interactiveRow(() => fileInputRef.current?.click())}
                 >
                   {uploading ? (
                     <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />

+ 10 - 6
src/features/notifications/hooks/useNotificationWebSocket.ts

@@ -9,14 +9,18 @@ import { getActiveSmsCustomerId } from "@/features/sms/activeSmsView";
 export function useNotificationWebSocket() {
   const wsRef = useRef<WebSocket | null>(null);
   const reconnectTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
-  const mountedRef = useRef(true);
 
   useEffect(() => {
-    mountedRef.current = true;
+    // Liveness is a per-effect-run closure, not a shared ref: with a ref, a
+    // remount (StrictMode does this on every mount in dev) let the *old*
+    // socket's async onclose observe the *new* run's "mounted" state and
+    // schedule a reconnect — leaving two live sockets delivering every
+    // notification twice.
+    let alive = true;
 
     // Fetch initial notifications
     getNotifications().then((result) => {
-      if (!mountedRef.current) return;
+      if (!alive) return;
       if (result.success && result.data) {
         useNotificationStore.getState().setNotifications(
           result.data.notifications,
@@ -26,7 +30,7 @@ export function useNotificationWebSocket() {
     });
 
     function connect() {
-      if (!mountedRef.current) return;
+      if (!alive) return;
 
       const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
       const url = `${protocol}//${window.location.host}/api/protected/ws`;
@@ -82,7 +86,7 @@ export function useNotificationWebSocket() {
       ws.onclose = () => {
         useNotificationStore.getState().setConnected(false);
         wsRef.current = null;
-        if (mountedRef.current) {
+        if (alive) {
           reconnectTimer.current = setTimeout(connect, 3000);
         }
       };
@@ -95,7 +99,7 @@ export function useNotificationWebSocket() {
     connect();
 
     return () => {
-      mountedRef.current = false;
+      alive = false;
       clearTimeout(reconnectTimer.current);
       wsRef.current?.close();
     };

+ 11 - 4
src/features/notifications/store/notificationStore.ts

@@ -34,10 +34,17 @@ export const useNotificationStore = create<NotificationState>((set) => ({
   isPanelOpen: false,
   setNotifications: (notifications, unreadCount) => set({ notifications, unreadCount }),
   addNotification: (notification) =>
-    set((state) => ({
-      notifications: [notification, ...state.notifications].slice(0, 50),
-      unreadCount: state.unreadCount + 1,
-    })),
+    set((state) => {
+      // Dedupe by id: a reconnect race or a double-delivered broadcast must
+      // never show (or count) the same notification twice.
+      if (state.notifications.some((n) => n.id === notification.id)) {
+        return state;
+      }
+      return {
+        notifications: [notification, ...state.notifications].slice(0, 50),
+        unreadCount: state.unreadCount + 1,
+      };
+    }),
   markRead: (id) =>
     set((state) => ({
       notifications: state.notifications.map((n) =>

+ 65 - 9
src/features/portal/Actions/portalActions.ts

@@ -164,7 +164,7 @@ export async function getPortalVehicles() {
         year: true,
         licensePlate: true,
         color: true,
-        imageUrl: true,
+        // The workshop's vehicle photo is internal — never sent to customers.
         _count: {
           select: {
             serviceRecords: true,
@@ -193,7 +193,6 @@ export async function getPortalVehicleDetail(vehicleId: string) {
         mileage: true,
         fuelType: true,
         transmission: true,
-        imageUrl: true,
         serviceRecords: {
           select: {
             id: true,
@@ -317,16 +316,73 @@ export async function getPortalInspections() {
 // ─── Service Requests ─────────────────────────────────────────────────────
 
 export async function createServiceRequest(input: {
-  vehicleId: string
+  vehicleId?: string
+  /**
+   * A vehicle the workshop doesn't know yet. It is created on the customer's
+   * profile so the request — and everything downstream (work orders, history)
+   * — hangs off a real vehicle record from day one.
+   */
+  newVehicle?: {
+    make: string
+    model: string
+    year?: number
+    licensePlate?: string
+  }
   description: string
   preferredDate?: string
 }) {
   return withPortalAuth(async ({ customerId, organizationId }) => {
-    // Validate vehicle belongs to customer
-    const vehicle = await db.vehicle.findFirst({
-      where: { id: input.vehicleId, customerId, organizationId },
-      select: { id: true, make: true, model: true, licensePlate: true },
-    })
+    let vehicle: { id: string; make: string; model: string; licensePlate: string | null } | null =
+      null
+
+    if (input.vehicleId) {
+      // Validate vehicle belongs to customer
+      vehicle = await db.vehicle.findFirst({
+        where: { id: input.vehicleId, customerId, organizationId },
+        select: { id: true, make: true, model: true, licensePlate: true },
+      })
+    } else if (input.newVehicle) {
+      const make = input.newVehicle.make?.trim().slice(0, 60)
+      const model = input.newVehicle.model?.trim().slice(0, 60)
+      const licensePlate = input.newVehicle.licensePlate?.trim().slice(0, 20) || null
+      const currentYear = new Date().getFullYear()
+      const year =
+        Number.isInteger(input.newVehicle.year) &&
+        (input.newVehicle.year as number) >= 1900 &&
+        (input.newVehicle.year as number) <= currentYear + 1
+          ? (input.newVehicle.year as number)
+          : currentYear
+      if (!make || !model) {
+        throw new Error('Vehicle make and model are required')
+      }
+      // Vehicles carry an owning app user; portal customers are not users, so
+      // the record is filed under the organization's owner.
+      const ownerMember = await db.organizationMember.findFirst({
+        where: { organizationId, role: 'owner' },
+        select: { userId: true },
+      })
+      const anyMember =
+        ownerMember ??
+        (await db.organizationMember.findFirst({
+          where: { organizationId },
+          select: { userId: true },
+        }))
+      if (!anyMember) {
+        throw new Error('Organization has no members')
+      }
+      vehicle = await db.vehicle.create({
+        data: {
+          make,
+          model,
+          year,
+          licensePlate,
+          userId: anyMember.userId,
+          organizationId,
+          customerId,
+        },
+        select: { id: true, make: true, model: true, licensePlate: true },
+      })
+    }
 
     if (!vehicle) {
       throw new Error('Vehicle not found')
@@ -342,7 +398,7 @@ export async function createServiceRequest(input: {
         description: input.description,
         preferredDate: toSafeDate(input.preferredDate) ?? null,
         customerId,
-        vehicleId: input.vehicleId,
+        vehicleId: vehicle.id,
         organizationId,
       },
     })

+ 25 - 38
src/features/portal/Components/CustomerPortalSettings.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import { useRef, useState, useTransition } from 'react'
 import { useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
@@ -9,14 +10,6 @@ import { Label } from '@/components/ui/label'
 import { Switch } from '@/components/ui/switch'
 import { Input } from '@/components/ui/input'
 import { Textarea } from '@/components/ui/textarea'
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardFooter,
-  CardHeader,
-  CardTitle,
-} from '@/components/ui/card'
 import Link from 'next/link'
 import { Bell, Copy, Check, Globe, Loader2, Image as ImageIcon, Trash2, Upload } from 'lucide-react'
 import { ReadOnlyBanner, ReadOnlyWrapper } from '@/app/(authenticated)/settings/read-only-guard'
@@ -196,15 +189,22 @@ export function CustomerPortalSettings({
     <div className="space-y-6">
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
-        <Card>
-          <CardHeader>
-            <CardTitle className="flex items-center gap-2">
-              <Globe className="h-5 w-5" />
-              {t('portal.title')}
-            </CardTitle>
-            <CardDescription>{t('portal.description')}</CardDescription>
-          </CardHeader>
-          <CardContent className="space-y-6">
+        <AppCard
+          icon={Globe}
+          title={t('portal.title')}
+          description={t('portal.description')}
+          contentClassName="space-y-6"
+          footer={
+            enabled ? (
+              <div className="flex justify-end">
+                <Button type="button" size="sm" onClick={handleSave} disabled={saving || !dirty}>
+                  {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
+                  {t('portal.save')}
+                </Button>
+              </div>
+            ) : undefined
+          }
+        >
             <div className="flex items-center justify-between">
               <div className="space-y-0.5">
                 <Label htmlFor="portal-enabled">{t('portal.enablePortal')}</Label>
@@ -281,27 +281,15 @@ export function CustomerPortalSettings({
                 </div>
               </>
             )}
-          </CardContent>
-          {enabled && (
-            <CardFooter className="justify-end border-t pt-6">
-              <Button type="button" onClick={handleSave} disabled={saving || !dirty}>
-                {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
-                {t('portal.save')}
-              </Button>
-            </CardFooter>
-          )}
-        </Card>
+        </AppCard>
 
         {enabled && (
-          <Card>
-            <CardHeader>
-              <CardTitle className="flex items-center gap-2">
-                <ImageIcon className="h-5 w-5" />
-                {t('portal.background.title')}
-              </CardTitle>
-              <CardDescription>{t('portal.background.description')}</CardDescription>
-            </CardHeader>
-            <CardContent className="space-y-6">
+          <AppCard
+            icon={ImageIcon}
+            title={t('portal.background.title')}
+            description={t('portal.background.description')}
+            contentClassName="space-y-6"
+          >
               {/* Type selector */}
               <div className="grid grid-cols-3 gap-2">
                 {(
@@ -429,8 +417,7 @@ export function CustomerPortalSettings({
                   </p>
                 </div>
               )}
-            </CardContent>
-          </Card>
+            </AppCard>
         )}
       </ReadOnlyWrapper>
     </div>

+ 5 - 0
src/features/portal/Components/PortalShell.tsx

@@ -81,6 +81,11 @@ export async function PortalShell({
 
   return (
     <div className="min-h-svh bg-muted/20">
+      {/* Same top hairline as the app: primary fading out to the right. */}
+      <div
+        aria-hidden
+        className="pointer-events-none fixed inset-x-0 top-0 z-50 h-px bg-linear-to-r from-primary via-primary/35 to-transparent"
+      />
       <PortalHeader
         orgId={orgParam}
         orgName={org.name}

+ 104 - 22
src/features/portal/Components/ServiceRequestForm.tsx

@@ -8,21 +8,16 @@ import { Button } from "@/components/ui/button";
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
 import { Input } from "@/components/ui/input";
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardHeader,
-  CardTitle,
-} from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import {
   Select,
   SelectContent,
   SelectItem,
+  SelectSeparator,
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
-import { Loader2 } from "lucide-react";
+import { Loader2, Plus, Wrench } from "lucide-react";
 import { createServiceRequest } from "@/features/portal/Actions/portalActions";
 
 type Vehicle = {
@@ -33,6 +28,9 @@ type Vehicle = {
   licensePlate: string | null;
 };
 
+/** Sentinel for "my vehicle isn't listed" — never collides with a cuid. */
+const NEW_VEHICLE = "__new__";
+
 export function ServiceRequestForm({
   orgId,
   vehicles,
@@ -43,10 +41,18 @@ export function ServiceRequestForm({
   const t = useTranslations("portal.requestService");
   const router = useRouter();
   const [isPending, startTransition] = useTransition();
-  const [vehicleId, setVehicleId] = useState("");
+  // A customer with no registered vehicles starts straight in "new vehicle"
+  // mode — before this form allowed that, they were told to phone the shop.
+  const [vehicleId, setVehicleId] = useState(vehicles.length === 0 ? NEW_VEHICLE : "");
+  const [make, setMake] = useState("");
+  const [model, setModel] = useState("");
+  const [year, setYear] = useState("");
+  const [plate, setPlate] = useState("");
   const [description, setDescription] = useState("");
   const [preferredDate, setPreferredDate] = useState("");
 
+  const isNewVehicle = vehicleId === NEW_VEHICLE;
+
   const handleSubmit = (e: React.FormEvent) => {
     e.preventDefault();
 
@@ -54,7 +60,10 @@ export function ServiceRequestForm({
       toast.error(t("selectVehicle"));
       return;
     }
-
+    if (isNewVehicle && (!make.trim() || !model.trim())) {
+      toast.error(t("fillVehicleDetails"));
+      return;
+    }
     if (!description.trim()) {
       toast.error(t("describeIssue"));
       return;
@@ -62,7 +71,15 @@ export function ServiceRequestForm({
 
     startTransition(async () => {
       const result = await createServiceRequest({
-        vehicleId,
+        vehicleId: isNewVehicle ? undefined : vehicleId,
+        newVehicle: isNewVehicle
+          ? {
+              make: make.trim(),
+              model: model.trim(),
+              year: year ? Number(year) : undefined,
+              licensePlate: plate.trim() || undefined,
+            }
+          : undefined,
         description: description.trim(),
         preferredDate: preferredDate || undefined,
       });
@@ -71,7 +88,11 @@ export function ServiceRequestForm({
         toast.success(t("submitSuccess"));
         setDescription("");
         setPreferredDate("");
-        setVehicleId("");
+        setVehicleId(vehicles.length === 0 ? NEW_VEHICLE : "");
+        setMake("");
+        setModel("");
+        setYear("");
+        setPlate("");
         router.refresh();
       } else {
         toast.error(result.error ?? t("submitError"));
@@ -80,14 +101,11 @@ export function ServiceRequestForm({
   };
 
   return (
-    <Card>
-      <CardHeader>
-        <CardTitle>{t("newServiceRequest")}</CardTitle>
-        <CardDescription>
-          {t("formDescription")}
-        </CardDescription>
-      </CardHeader>
-      <CardContent>
+    <AppCard
+      icon={Wrench}
+      title={t("newServiceRequest")}
+      description={t("formDescription")}
+    >
         <form onSubmit={handleSubmit} className="space-y-4">
           <div className="space-y-2">
             <Label htmlFor="vehicle">{t("vehicleLabel")}</Label>
@@ -102,10 +120,75 @@ export function ServiceRequestForm({
                     {v.licensePlate ? ` (${v.licensePlate})` : ""}
                   </SelectItem>
                 ))}
+                {vehicles.length > 0 && <SelectSeparator />}
+                <SelectItem value={NEW_VEHICLE}>
+                  <span className="flex items-center gap-1.5">
+                    <Plus className="h-3.5 w-3.5" />
+                    {t("notListedOption")}
+                  </span>
+                </SelectItem>
               </SelectContent>
             </Select>
           </div>
 
+          {isNewVehicle && (
+            <div className="space-y-4 rounded-lg border border-card-edge bg-muted/30 p-4">
+              <p className="text-xs text-muted-foreground">{t("newVehicleHint")}</p>
+              <div className="grid gap-4 sm:grid-cols-2">
+                <div className="space-y-2">
+                  <Label htmlFor="nv-make">{t("makeLabel")}</Label>
+                  <Input
+                    id="nv-make"
+                    value={make}
+                    onChange={(e) => setMake(e.target.value)}
+                    placeholder={t("makePlaceholder")}
+                    maxLength={60}
+                    required
+                  />
+                </div>
+                <div className="space-y-2">
+                  <Label htmlFor="nv-model">{t("modelLabel")}</Label>
+                  <Input
+                    id="nv-model"
+                    value={model}
+                    onChange={(e) => setModel(e.target.value)}
+                    placeholder={t("modelPlaceholder")}
+                    maxLength={60}
+                    required
+                  />
+                </div>
+                <div className="space-y-2">
+                  <Label htmlFor="nv-year">
+                    {t("yearLabel")}{" "}
+                    <span className="text-muted-foreground">{t("preferredDateOptional")}</span>
+                  </Label>
+                  <Input
+                    id="nv-year"
+                    type="number"
+                    inputMode="numeric"
+                    min={1900}
+                    max={new Date().getFullYear() + 1}
+                    value={year}
+                    onChange={(e) => setYear(e.target.value)}
+                  />
+                </div>
+                <div className="space-y-2">
+                  <Label htmlFor="nv-plate">
+                    {t("plateLabel")}{" "}
+                    <span className="text-muted-foreground">{t("preferredDateOptional")}</span>
+                  </Label>
+                  <Input
+                    id="nv-plate"
+                    value={plate}
+                    onChange={(e) => setPlate(e.target.value)}
+                    maxLength={20}
+                    className="font-mono uppercase"
+                  />
+                </div>
+              </div>
+            </div>
+          )}
+
           <div className="space-y-2">
             <Label htmlFor="description">{t("descriptionLabel")}</Label>
             <Textarea
@@ -141,7 +224,6 @@ export function ServiceRequestForm({
             </Button>
           </div>
         </form>
-      </CardContent>
-    </Card>
+      </AppCard>
   );
 }

+ 10 - 17
src/features/quotes/Components/QuoteDocumentsManager.tsx

@@ -1,10 +1,11 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useState, useCallback, useRef } from "react";
 import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Switch } from "@/components/ui/switch";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { toast } from "sonner";
 import {
   FileText,
@@ -168,19 +169,12 @@ export function QuoteDocumentsManager({
   );
 
   return (
-    <Card className="border-0 shadow-sm">
-      <CardHeader>
-        <CardTitle className="flex items-center gap-2 text-base">
-          <Paperclip className="h-4 w-4" />
-          {t("documents.title")}
-          {maxDocuments !== undefined && (
-            <span className="ml-auto text-xs font-normal text-muted-foreground">
-              {files.length} / {maxDocuments}
-            </span>
-          )}
-        </CardTitle>
-      </CardHeader>
-      <CardContent className="space-y-4">
+    <AppCard
+      icon={Paperclip}
+      title={t("documents.title")}
+      badge={maxDocuments !== undefined ? `${files.length} / ${maxDocuments}` : undefined}
+      contentClassName="space-y-4"
+    >
         {atLimit ? (
           <div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6">
             <p className="text-sm font-medium text-muted-foreground">
@@ -198,7 +192,7 @@ export function QuoteDocumentsManager({
                 handleUpload(e.dataTransfer.files);
             }}
             onDragOver={(e) => e.preventDefault()}
-            onClick={() => inputRef.current?.click()}
+            {...interactiveRow(() => inputRef.current?.click())}
             className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
           >
             <Upload className="mb-2 h-8 w-8 text-muted-foreground/50" />
@@ -271,7 +265,6 @@ export function QuoteDocumentsManager({
             ))}
           </div>
         )}
-      </CardContent>
-    </Card>
+      </AppCard>
   );
 }

+ 10 - 17
src/features/quotes/Components/QuoteImagesManager.tsx

@@ -1,11 +1,12 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useState, useRef, useCallback } from "react";
 import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Switch } from "@/components/ui/switch";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { toast } from "sonner";
 import { Camera, Image as ImageIcon, Loader2, X } from "lucide-react";
 import { compressImage } from "@/lib/compress-image";
@@ -189,19 +190,12 @@ export function QuoteImagesManager({
   );
 
   return (
-    <Card className="border-0 shadow-sm">
-      <CardHeader>
-        <CardTitle className="flex items-center gap-2 text-base">
-          <Camera className="h-4 w-4" />
-          {t("images.title")}
-          {maxImages !== undefined && (
-            <span className="ml-auto text-xs font-normal text-muted-foreground">
-              {images.length} / {maxImages}
-            </span>
-          )}
-        </CardTitle>
-      </CardHeader>
-      <CardContent className="space-y-4">
+    <AppCard
+      icon={Camera}
+      title={t("images.title")}
+      badge={maxImages !== undefined ? `${images.length} / ${maxImages}` : undefined}
+      contentClassName="space-y-4"
+    >
         {atLimit ? (
           <div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6">
             <p className="text-sm font-medium text-muted-foreground">
@@ -215,7 +209,7 @@ export function QuoteImagesManager({
           <div
             onDrop={handleDrop}
             onDragOver={(e) => e.preventDefault()}
-            onClick={() => inputRef.current?.click()}
+            {...interactiveRow(() => inputRef.current?.click())}
             className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
           >
             <ImageIcon className="mb-2 h-8 w-8 text-muted-foreground/50" />
@@ -296,7 +290,6 @@ export function QuoteImagesManager({
             ))}
           </div>
         )}
-      </CardContent>
-    </Card>
+      </AppCard>
   );
 }

+ 12 - 18
src/features/settings/Components/license-settings.tsx

@@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'
 import { Button } from '@/components/ui/button'
 import { Label } from '@/components/ui/label'
 import { Badge } from '@/components/ui/badge'
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { AppCard } from '@/components/app-card'
 import { ExternalLink, Key, Loader2 } from 'lucide-react'
 import { validateLicense } from '../Actions/validateLicense'
 
@@ -51,18 +51,12 @@ export function LicenseSettings({
 
   return (
     <div className="space-y-6">
-      <Card>
-        <CardHeader className="flex flex-row items-start justify-between gap-4">
-          <div className="space-y-1.5">
-            <CardTitle className="flex items-center gap-2">
-              <Key className="h-5 w-5" />
-              {t('license.title')}
-            </CardTitle>
-            <CardDescription>
-              {t('license.description')}
-            </CardDescription>
-          </div>
-          {!licenseValid && !demoMode && (
+      <AppCard
+        icon={Key}
+        title={t('license.title')}
+        description={t('license.description')}
+        action={
+          !licenseValid && !demoMode ? (
             <Button asChild variant="default" size="sm" className="shrink-0">
               <a
                 href={`${process.env.NEXT_PUBLIC_TORQVOICE_COM_URL || 'https://torqvoice.com'}/subscriptions/white-label`}
@@ -73,9 +67,10 @@ export function LicenseSettings({
                 <ExternalLink className="ml-2 h-3 w-3" />
               </a>
             </Button>
-          )}
-        </CardHeader>
-        <CardContent className="space-y-4">
+          ) : undefined
+        }
+        contentClassName="space-y-4"
+      >
           <div className="flex items-center gap-2">
             <Label>{t('license.status')}</Label>
             {licenseValid ? (
@@ -112,8 +107,7 @@ export function LicenseSettings({
           <p className="text-xs text-muted-foreground">
             {t('license.keyHint')}
           </p>
-        </CardContent>
-      </Card>
+      </AppCard>
     </div>
   )
 }

+ 7 - 9
src/features/settings/Components/passkey-settings.tsx

@@ -1,9 +1,9 @@
 'use client'
 
+import { AppCard } from '@/components/app-card'
 import { useState, useEffect } from 'react'
 import { authClient } from '@/lib/auth-client'
 import { toast } from 'sonner'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import {
   Dialog,
   DialogContent,
@@ -119,12 +119,11 @@ export function PasskeySettings() {
 
   return (
     <>
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <Fingerprint className="h-5 w-5 text-muted-foreground" />
-          <CardTitle className="text-lg">{t('account.passkey.title')}</CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Fingerprint}
+        title={t('account.passkey.title')}
+        contentClassName="space-y-4"
+      >
           <p className="text-sm text-muted-foreground">
             {t('account.passkey.description')}
           </p>
@@ -201,8 +200,7 @@ export function PasskeySettings() {
             )}
             {registering ? t('account.passkey.registering') : t('account.passkey.register')}
           </Button>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Rename Dialog */}
       <Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>

+ 7 - 15
src/features/sms/Components/SmsSettingsForm.tsx

@@ -8,13 +8,7 @@ import { Input } from "@/components/ui/input";
 import { Button } from "@/components/ui/button";
 import { Label } from "@/components/ui/label";
 import { Switch } from "@/components/ui/switch";
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardHeader,
-  CardTitle,
-} from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Loader2, Send, Info, Copy, Check } from "lucide-react";
 import { ORG_SMS_KEYS } from "../Schema/smsSettingsSchema";
 import {
@@ -158,12 +152,11 @@ export function SmsSettingsForm({
     <div className="space-y-6">
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
-        <Card>
-          <CardHeader>
-            <CardTitle>{t("sms.title")}</CardTitle>
-            <CardDescription>{t("sms.description")}</CardDescription>
-          </CardHeader>
-          <CardContent className="space-y-6">
+        <AppCard
+          title={t("sms.title")}
+          description={t("sms.description")}
+          contentClassName="space-y-6"
+        >
             <div className="flex items-center justify-between">
               <div className="space-y-0.5">
                 <Label htmlFor="enable-sms">{t("sms.enableLabel")}</Label>
@@ -370,8 +363,7 @@ export function SmsSettingsForm({
                 </div>
               </>
             )}
-          </CardContent>
-        </Card>
+          </AppCard>
 
         <SaveButton>
           <div className="flex justify-end">

+ 2 - 1
src/features/status-reports/Components/StatusReportList.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useState } from "react";
 import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
@@ -273,7 +274,7 @@ export function StatusReportList({
                   <ContextMenuTrigger asChild>
                   <TableRow
                     className="cursor-pointer"
-                    onClick={() => setDetailReport(report)}
+                    {...interactiveRow(() => setDetailReport(report))}
                   >
                     <TableCell className="py-2">
                       <div className="flex items-center gap-1.5 min-w-0">

+ 31 - 49
src/features/subscription/Components/subscription-settings.tsx

@@ -7,13 +7,7 @@ import { useRouter } from "next/navigation";
 import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
 import { Label } from "@/components/ui/label";
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardHeader,
-  CardTitle,
-} from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import {
   AlertDialog,
   AlertDialogAction,
@@ -272,15 +266,15 @@ export function SubscriptionSettings({
   return (
     <div className="space-y-6">
       {/* Card 1: Current Plan */}
-      <Card>
-        <CardHeader>
-          <CardTitle className="flex items-center gap-2">
-            {planIcon}
-            {t("subscription.title")}
-          </CardTitle>
-          <CardDescription>{t("subscription.description")}</CardDescription>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        title={
+          <span className="flex items-center gap-2">
+            {planIcon} {t("subscription.title")}
+          </span>
+        }
+        description={t("subscription.description")}
+        contentClassName="space-y-4"
+      >
           <div className="flex items-center gap-3">
             <Label>{t("subscription.currentPlan")}:</Label>
             {planBadge}
@@ -345,16 +339,13 @@ export function SubscriptionSettings({
               </p>
             </div>
           )}
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Card 2: Plan Features */}
-      <Card>
-        <CardHeader>
-          <CardTitle>{t("subscription.featuresTitle")}</CardTitle>
-          <CardDescription>{t("subscription.featuresDescription")}</CardDescription>
-        </CardHeader>
-        <CardContent>
+      <AppCard
+        title={t("subscription.featuresTitle")}
+        description={t("subscription.featuresDescription")}
+      >
           <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
             {featureRows.map((row) => (
               <div
@@ -378,16 +369,14 @@ export function SubscriptionSettings({
               </div>
             ))}
           </div>
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Card 3: Manage Subscription — demos have no Stripe billing to manage */}
       {isPaid && !isDemo && (
-        <Card>
-          <CardHeader>
-            <CardTitle>{t("subscription.manageTitle")}</CardTitle>
-          </CardHeader>
-          <CardContent className="flex flex-wrap gap-3">
+        <AppCard
+          title={t("subscription.manageTitle")}
+          contentClassName="flex flex-wrap gap-3"
+        >
             {hasStripeCustomer && (
               <Button
                 variant="outline"
@@ -444,18 +433,15 @@ export function SubscriptionSettings({
                 </AlertDialog>
               )
             )}
-          </CardContent>
-        </Card>
+          </AppCard>
       )}
 
       {/* Upgrade Section — free users and demo users subscribe via Stripe Checkout */}
       {(plan === "free" || isDemo) && (
-        <Card>
-          <CardHeader>
-            <CardTitle>{t("subscription.upgradeTitle")}</CardTitle>
-            <CardDescription>{t("subscription.upgradeToProDescription")}</CardDescription>
-          </CardHeader>
-          <CardContent>
+        <AppCard
+          title={t("subscription.upgradeTitle")}
+          description={t("subscription.upgradeToProDescription")}
+        >
             <div className="flex gap-2">
               <Button
                 variant="default"
@@ -484,17 +470,14 @@ export function SubscriptionSettings({
                 {t("subscription.upgradeToEnterprise")}
               </Button>
             </div>
-          </CardContent>
-        </Card>
+          </AppCard>
       )}
 
       {plan === "pro" && !isDemo && (
-        <Card>
-          <CardHeader>
-            <CardTitle>{t("subscription.upgradeTitle")}</CardTitle>
-            <CardDescription>{t("subscription.upgradeToEnterpriseDescription")}</CardDescription>
-          </CardHeader>
-          <CardContent>
+        <AppCard
+          title={t("subscription.upgradeTitle")}
+          description={t("subscription.upgradeToEnterpriseDescription")}
+        >
             <AlertDialog onOpenChange={handleUpgradeDialogOpen}>
               <AlertDialogTrigger asChild>
                 <Button
@@ -545,8 +528,7 @@ export function SubscriptionSettings({
                 </AlertDialogFooter>
               </AlertDialogContent>
             </AlertDialog>
-          </CardContent>
-        </Card>
+          </AppCard>
       )}
     </div>
   );

+ 7 - 20
src/features/telegram/Components/TelegramSettingsForm.tsx

@@ -7,7 +7,7 @@ import { toast } from "sonner";
 import { Input } from "@/components/ui/input";
 import { Button } from "@/components/ui/button";
 import { Label } from "@/components/ui/label";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { Switch } from "@/components/ui/switch";
 import { ExternalLink, Info, Loader2, Copy, Check, Eye, EyeOff } from "lucide-react";
 import { setTelegramSettings } from "../Actions/telegramSettingsActions";
@@ -65,23 +65,11 @@ export function TelegramSettingsForm({
     <div className="space-y-6">
       <ReadOnlyBanner />
       <ReadOnlyWrapper>
-        <Card>
-          <CardHeader>
-            <CardTitle>{t("title")}</CardTitle>
-            <CardDescription>
-              {t("description")}{" "}
-              <a
-                href="https://torqvoice.com/docs/integrations/telegram"
-                target="_blank"
-                rel="noopener noreferrer"
-                className="inline-flex items-center gap-1 font-medium text-primary hover:underline"
-              >
-                {t("helpLink")}
-                <ExternalLink className="h-3 w-3" />
-              </a>
-            </CardDescription>
-          </CardHeader>
-          <CardContent className="space-y-6">
+        <AppCard
+          title={t("title")}
+          description={<>{t("description")}{" "} <a href="https://torqvoice.com/docs/integrations/telegram" target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 font-medium text-primary hover:underline" > {t("helpLink")} <ExternalLink className="h-3 w-3" /> </a></>}
+          contentClassName="space-y-6"
+        >
             <div className="flex items-center justify-between">
               <div className="space-y-0.5">
                 <Label htmlFor="enable-telegram">{t("enable.label")}</Label>
@@ -142,8 +130,7 @@ export function TelegramSettingsForm({
 
               </>
             )}
-          </CardContent>
-        </Card>
+          </AppCard>
 
         {enabled && (
           <SaveButton>

+ 11 - 13
src/features/vehicles/Components/MyActiveJobs.tsx

@@ -1,5 +1,6 @@
 'use client'
 
+import { interactiveRow } from '@/lib/interactive-row'
 import { useRef, useState, useCallback } from 'react'
 import { useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
@@ -18,7 +19,7 @@ import {
   Package,
 } from 'lucide-react'
 import { FindingForm } from '@/features/vehicles/Components/FindingForm'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { AppCard } from '@/components/app-card'
 import { Button } from '@/components/ui/button'
 import { ButtonGroup } from '@/components/ui/button-group'
 import { Badge } from '@/components/ui/badge'
@@ -301,15 +302,13 @@ export function MyActiveJobs({
 
   return (
     <TooltipProvider>
-      <Card className="border-0 shadow-sm">
-        <CardHeader className="px-4 pb-1 pt-3">
-          <CardTitle className="flex items-center gap-2 text-base">
-            <Wrench className="h-4 w-4" />
-            {t('title')}
-          </CardTitle>
-          <p className="text-xs text-muted-foreground">{t('description')}</p>
-        </CardHeader>
-        <CardContent className="p-0">
+      <AppCard
+        icon={Wrench}
+        title={t('title')}
+        description={t('description')}
+        badge={jobs.length || undefined}
+        contentClassName="p-0"
+      >
           <div className="divide-y">
             {jobs.map((job) => {
               const StatusIcon = STATUS_ICON[job.status] || Wrench
@@ -326,7 +325,7 @@ export function MyActiveJobs({
                   <div className="flex items-center justify-between">
                     <div
                       className="flex items-center gap-3 min-w-0 cursor-pointer hover:opacity-80"
-                      onClick={() => router.push(job.vehicleId ? `/vehicles/${job.vehicleId}/service/${job.id}` : `/sales/${job.id}`)}
+                      {...interactiveRow(() => router.push(job.vehicleId ? `/vehicles/${job.vehicleId}/service/${job.id}` : `/sales/${job.id}`))}
                     >
                       <div
                         className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${statusColor}`}
@@ -567,8 +566,7 @@ export function MyActiveJobs({
               )
             })}
           </div>
-        </CardContent>
-      </Card>
+      </AppCard>
 
       <BarcodeScannerDialog
         open={scannerOpen}

+ 1 - 1
src/features/vehicles/Components/RemindersPageClient.tsx

@@ -185,7 +185,7 @@ export function RemindersPageClient({ reminders, vehicles, unitSystem }: Reminde
           </CardContent>
         </Card>
       ) : (
-        <Card className="border-0 shadow-sm">
+        <Card>
           <CardContent className="p-0">
             <TableContextMenuHint />
             <div className="divide-y">

+ 2 - 1
src/features/vehicles/Components/VehicleForm.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useState, useRef, useMemo, useEffect } from "react";
 import { useRouter } from "next/navigation";
 import Image from "next/image";
@@ -205,7 +206,7 @@ export function VehicleForm({ open, onOpenChange, vehicle, customers, defaultCus
           <div className="space-y-2">
             <Label>{t("photo")}</Label>
             <div
-              onClick={() => fileRef.current?.click()}
+              {...interactiveRow(() => fileRef.current?.click())}
               className="group relative flex h-44 cursor-pointer items-center justify-center overflow-hidden rounded-xl border-2 border-dashed border-border bg-muted/30 transition-colors hover:border-primary/50 hover:bg-muted/50"
             >
               {preview ? (

+ 18 - 33
src/features/vehicles/Components/service-documents-manager.tsx

@@ -1,10 +1,11 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useState, useCallback, useRef } from "react";
 import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Switch } from "@/components/ui/switch";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { toast } from "sonner";
 import {
   FileText,
@@ -228,19 +229,12 @@ export function ServiceDocumentsManager({
   return (
     <div className="space-y-6">
       {/* Diagnostic Reports */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader>
-          <CardTitle className="flex items-center gap-2 text-base">
-            <FileText className="h-4 w-4" />
-            {t("documents.diagnosticTitle")}
-            {maxDiagnostics !== undefined && (
-              <span className="ml-auto text-xs font-normal text-muted-foreground">
-                {diagnosticReports.length} / {maxDiagnostics}
-              </span>
-            )}
-          </CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={FileText}
+        title={t("documents.diagnosticTitle")}
+        badge={maxDiagnostics !== undefined ? `${diagnosticReports.length} / ${maxDiagnostics}` : undefined}
+        contentClassName="space-y-4"
+      >
           {diagnosticsAtLimit ? (
             <div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6">
               <p className="text-sm font-medium text-muted-foreground">
@@ -258,7 +252,7 @@ export function ServiceDocumentsManager({
                   handleUpload(e.dataTransfer.files, "diagnostic");
               }}
               onDragOver={(e) => e.preventDefault()}
-              onClick={() => reportInputRef.current?.click()}
+              {...interactiveRow(() => reportInputRef.current?.click())}
               className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
             >
               <Upload className="mb-2 h-8 w-8 text-muted-foreground/50" />
@@ -292,23 +286,15 @@ export function ServiceDocumentsManager({
           )}
 
           {renderFileList(diagnosticReports)}
-        </CardContent>
-      </Card>
+        </AppCard>
 
       {/* Documents */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader>
-          <CardTitle className="flex items-center gap-2 text-base">
-            <Paperclip className="h-4 w-4" />
-            {t("documents.documentsTitle")}
-            {maxDocuments !== undefined && (
-              <span className="ml-auto text-xs font-normal text-muted-foreground">
-                {documents.length} / {maxDocuments}
-              </span>
-            )}
-          </CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Paperclip}
+        title={t("documents.documentsTitle")}
+        badge={maxDocuments !== undefined ? `${documents.length} / ${maxDocuments}` : undefined}
+        contentClassName="space-y-4"
+      >
           {documentsAtLimit ? (
             <div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6">
               <p className="text-sm font-medium text-muted-foreground">
@@ -326,7 +312,7 @@ export function ServiceDocumentsManager({
                   handleUpload(e.dataTransfer.files, "document");
               }}
               onDragOver={(e) => e.preventDefault()}
-              onClick={() => documentInputRef.current?.click()}
+              {...interactiveRow(() => documentInputRef.current?.click())}
               className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
             >
               <Upload className="mb-2 h-8 w-8 text-muted-foreground/50" />
@@ -360,8 +346,7 @@ export function ServiceDocumentsManager({
           )}
 
           {renderFileList(documents)}
-        </CardContent>
-      </Card>
+        </AppCard>
     </div>
   );
 }

+ 10 - 17
src/features/vehicles/Components/service-images-manager.tsx

@@ -1,11 +1,12 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useState, useRef, useCallback } from "react";
 import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Switch } from "@/components/ui/switch";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { toast } from "sonner";
 import { Camera, Image as ImageIcon, Loader2, X } from "lucide-react";
 import { compressImage } from "@/lib/compress-image";
@@ -186,19 +187,12 @@ export function ServiceImagesManager({
   );
 
   return (
-    <Card className="border-0 shadow-sm">
-      <CardHeader>
-        <CardTitle className="flex items-center gap-2 text-base">
-          <Camera className="h-4 w-4" />
-          {t("images.title")}
-          {maxImages !== undefined && (
-            <span className="ml-auto text-xs font-normal text-muted-foreground">
-              {images.length} / {maxImages}
-            </span>
-          )}
-        </CardTitle>
-      </CardHeader>
-      <CardContent className="space-y-4">
+    <AppCard
+      icon={Camera}
+      title={t("images.title")}
+      badge={maxImages !== undefined ? `${images.length} / ${maxImages}` : undefined}
+      contentClassName="space-y-4"
+    >
         {atLimit ? (
           <div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6">
             <p className="text-sm font-medium text-muted-foreground">
@@ -212,7 +206,7 @@ export function ServiceImagesManager({
           <div
             onDrop={handleDrop}
             onDragOver={(e) => e.preventDefault()}
-            onClick={() => inputRef.current?.click()}
+            {...interactiveRow(() => inputRef.current?.click())}
             className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
           >
             <ImageIcon className="mb-2 h-8 w-8 text-muted-foreground/50" />
@@ -294,7 +288,6 @@ export function ServiceImagesManager({
             ))}
           </div>
         )}
-      </CardContent>
 
       <ImageCarousel
         images={images.map((img) => ({
@@ -305,6 +298,6 @@ export function ServiceImagesManager({
         onClose={() => setCarouselIndex(null)}
         onChangeIndex={setCarouselIndex}
       />
-    </Card>
+    </AppCard>
   );
 }

+ 9 - 12
src/features/vehicles/Components/service-video-manager.tsx

@@ -1,10 +1,11 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useState, useCallback, useRef } from "react";
 import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Switch } from "@/components/ui/switch";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { AppCard } from "@/components/app-card";
 import { toast } from "sonner";
 import { Film, Loader2, Upload, X } from "lucide-react";
 import { addServiceAttachment } from "@/features/vehicles/Actions/addServiceAttachment";
@@ -135,14 +136,11 @@ export function ServiceVideoManager({
 
   return (
     <div className="space-y-6">
-      <Card className="border-0 shadow-sm">
-        <CardHeader>
-          <CardTitle className="flex items-center gap-2 text-base">
-            <Film className="h-4 w-4" />
-            {t("videos.title")}
-          </CardTitle>
-        </CardHeader>
-        <CardContent className="space-y-4">
+      <AppCard
+        icon={Film}
+        title={t("videos.title")}
+        contentClassName="space-y-4"
+      >
           <div
             onDrop={(e) => {
               e.preventDefault();
@@ -150,7 +148,7 @@ export function ServiceVideoManager({
                 handleUpload(e.dataTransfer.files);
             }}
             onDragOver={(e) => e.preventDefault()}
-            onClick={() => inputRef.current?.click()}
+            {...interactiveRow(() => inputRef.current?.click())}
             className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
           >
             <Upload className="mb-2 h-8 w-8 text-muted-foreground/50" />
@@ -231,8 +229,7 @@ export function ServiceVideoManager({
               ))}
             </div>
           )}
-        </CardContent>
-      </Card>
+        </AppCard>
     </div>
   );
 }

+ 2 - 1
src/features/workboard/Components/BoardJobCard.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { interactiveRow } from "@/lib/interactive-row";
 import { useDraggable } from "@dnd-kit/core";
 import { CSS } from "@dnd-kit/utilities";
 import { Clock, GripVertical, Wrench, ClipboardCheck } from "lucide-react";
@@ -38,7 +39,7 @@ export function BoardJobCard({
       ref={setNodeRef}
       style={style}
       className="group flex cursor-pointer items-start gap-1 rounded-md border bg-card p-1.5 text-xs shadow-sm transition-shadow hover:shadow-md"
-      onClick={onClick}
+      {...(onClick ? interactiveRow(onClick) : {})}
     >
       <button
         {...listeners}

+ 33 - 0
src/lib/interactive-row.ts

@@ -0,0 +1,33 @@
+import type { KeyboardEvent } from "react";
+
+/**
+ * Spread onto a clickable row — a `<TableRow>` or a list-row `<div>` — to make
+ * it keyboard-operable: Tab reaches it, Enter/Space activate it, and the
+ * `data-row-interactive` attribute gives it a visible focus ring plus a
+ * pointer cursor (styled in globals.css).
+ *
+ *   <TableRow {...interactiveRow(() => router.push(href))}>
+ *
+ * Rows keep their own semantics on purpose. They can't be `<button>`s —
+ * nesting the row's dismiss/menu buttons inside one is invalid HTML — and
+ * `role="button"` on a container with interactive children is an ARIA
+ * violation. A focusable element that announces its content and activates on
+ * Enter is the pragmatic, axe-clean middle ground.
+ *
+ * The `target === currentTarget` guard keeps Enter/Space pressed on inner
+ * controls (checkboxes, kebab menus) from also triggering the row.
+ */
+export function interactiveRow(activate: () => void) {
+  return {
+    "data-row-interactive": true,
+    tabIndex: 0,
+    onClick: activate,
+    onKeyDown: (e: KeyboardEvent<HTMLElement>) => {
+      if (e.target !== e.currentTarget) return;
+      if (e.key === "Enter" || e.key === " ") {
+        e.preventDefault();
+        activate();
+      }
+    },
+  };
+}