diff --git a/backend/internal/web/ui/empty-state.css b/backend/internal/web/ui/empty-state.css
new file mode 100644
index 0000000..b361f0a
--- /dev/null
+++ b/backend/internal/web/ui/empty-state.css
@@ -0,0 +1,40 @@
+.ui-empty-state {
+ align-items: center;
+ border: 1px dashed var(--color-border-subtle);
+ border-radius: 1rem;
+ color: var(--color-text-muted);
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ justify-content: center;
+ padding: 3rem 1.5rem;
+ text-align: center;
+}
+
+.ui-empty-state-title {
+ color: var(--color-text-primary);
+ font-size: 1.125rem;
+ font-weight: 700;
+ margin: 0;
+}
+
+.ui-empty-state-icon {
+ align-items: center;
+ background: var(--color-surface-muted);
+ border-radius: 999px;
+ color: var(--color-text-faint);
+ display: inline-flex;
+ height: 4rem;
+ justify-content: center;
+ width: 4rem;
+}
+
+.ui-empty-state-icon svg {
+ height: 2rem;
+ width: 2rem;
+}
+
+.ui-empty-state-description {
+ margin: 0;
+ max-width: 32rem;
+}
diff --git a/backend/internal/web/ui/empty_state.templ b/backend/internal/web/ui/empty_state.templ
new file mode 100644
index 0000000..22a975d
--- /dev/null
+++ b/backend/internal/web/ui/empty_state.templ
@@ -0,0 +1,27 @@
+package ui
+
+type EmptyStateProps struct {
+ Title string
+ Description string
+ Icon templ.Component
+ Action templ.Component
+}
+
+templ EmptyState(props EmptyStateProps) {
+
+ if props.Icon != nil {
+
+ @props.Icon
+
+ }
+ { props.Title }
+ if props.Description != "" {
+ { props.Description }
+ }
+ if props.Action != nil {
+
+ @props.Action
+
+ }
+
+}
diff --git a/backend/internal/web/ui/modal.css b/backend/internal/web/ui/modal.css
new file mode 100644
index 0000000..854e1cd
--- /dev/null
+++ b/backend/internal/web/ui/modal.css
@@ -0,0 +1,53 @@
+.ui-modal-backdrop {
+ align-items: center;
+ background: var(--overlay-backdrop-default);
+ display: flex;
+ inset: 0;
+ justify-content: center;
+ padding: 1rem;
+ position: fixed;
+ z-index: 40;
+}
+
+.ui-modal-panel {
+ background: var(--color-surface-default);
+ border: 1px solid var(--color-border-default);
+ border-radius: 1rem;
+ box-shadow: var(--shadow-surface-lg);
+ max-width: 32rem;
+ width: min(100%, 32rem);
+}
+
+.ui-modal-header,
+.ui-modal-body,
+.ui-modal-actions {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+}
+
+.ui-modal-header {
+ border-bottom: 1px solid var(--color-border-default);
+ padding-bottom: 1rem;
+ padding-top: 1.25rem;
+}
+
+.ui-modal-header h2 {
+ color: var(--color-text-primary);
+ font-size: 1.125rem;
+ font-weight: 700;
+ margin: 0;
+}
+
+.ui-modal-body {
+ padding-bottom: 1.25rem;
+ padding-top: 1.25rem;
+}
+
+.ui-modal-actions {
+ border-top: 1px solid var(--color-border-default);
+ display: flex;
+ gap: 0.75rem;
+ justify-content: flex-end;
+ padding-bottom: 1rem;
+ padding-top: 1rem;
+}
diff --git a/backend/internal/web/ui/modal.templ b/backend/internal/web/ui/modal.templ
new file mode 100644
index 0000000..f3ac5d4
--- /dev/null
+++ b/backend/internal/web/ui/modal.templ
@@ -0,0 +1,27 @@
+package ui
+
+type ModalProps struct {
+ Title string
+ Body templ.Component
+ Actions templ.Component
+}
+
+templ Modal(props ModalProps) {
+
+
+
+ if props.Body != nil {
+
+ @props.Body
+
+ }
+ if props.Actions != nil {
+
+ @props.Actions
+
+ }
+
+
+}
diff --git a/backend/internal/web/ui/table.css b/backend/internal/web/ui/table.css
new file mode 100644
index 0000000..292f192
--- /dev/null
+++ b/backend/internal/web/ui/table.css
@@ -0,0 +1,10 @@
+.ui-table-shell {
+ overflow-x: auto;
+ width: 100%;
+}
+
+.ui-table {
+ border-collapse: collapse;
+ min-width: 100%;
+ width: 100%;
+}
diff --git a/backend/internal/web/ui/table.templ b/backend/internal/web/ui/table.templ
new file mode 100644
index 0000000..dead420
--- /dev/null
+++ b/backend/internal/web/ui/table.templ
@@ -0,0 +1,23 @@
+package ui
+
+type TableProps struct {
+ Head templ.Component
+ Body templ.Component
+}
+
+templ Table(props TableProps) {
+
+
+
+ if props.Head != nil {
+ @props.Head
+ }
+
+
+ if props.Body != nil {
+ @props.Body
+ }
+
+
+
+}