Files
policy-ui/app/pages/claims/index.vue
Jordan Weingarten 67482f6629 WIP jordan
2026-04-16 11:11:44 -05:00

507 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { slaColor, CARRIER_STATUS_LABELS, WORKFLOW_STATUS_LABELS } from '~/data/mock-claims'
import type { CarrierStatus, BrokerWorkflowStatus } from '~/data/mock-claims'
usePageTitle('Claims')
interface Claim {
id: string
customer: string
agent: string
line: string
type: string
carrier: string
reserved: string
paid: string
daysOpen: number
priority: 'critical' | 'high' | 'medium' | 'low'
status: 'open' | 'under_review' | 'awaiting_docs' | 'approved' | 'denied' | 'closed'
docsPending: number
opened: string
carrierStatus: CarrierStatus
workflowStatus: BrokerWorkflowStatus
slaPercent: number
handler: string
}
const claims = ref<Claim[]>([
{ id: 'CLM-0048', customer: 'Hotel Pacífico', agent: 'Marco V.', line: 'General Risk', type: 'Fire damage', carrier: 'ASSA', reserved: '$128,000', paid: '$0', daysOpen: 3, priority: 'critical', status: 'open', docsPending: 4, opened: 'Apr 2, 2026', carrierStatus: 'investigation', workflowStatus: 'waiting_carrier', slaPercent: 110, handler: 'Ana R.' },
{ id: 'CLM-0047', customer: 'Empresa ABC S.A.', agent: 'Ana R.', line: 'Auto', type: 'Collision — fleet unit #7', carrier: 'Qualitas', reserved: '$14,200', paid: '$0', daysOpen: 5, priority: 'high', status: 'under_review', docsPending: 2, opened: 'Mar 31, 2026', carrierStatus: 'documentation_pending', workflowStatus: 'waiting_insured_docs', slaPercent: 60, handler: 'Ana R.' },
{ id: 'CLM-0046', customer: 'Jorge Herrera', agent: 'Marco V.', line: 'Auto', type: 'Windshield replacement', carrier: 'Qualitas', reserved: '$1,100', paid: '$0', daysOpen: 8, priority: 'low', status: 'awaiting_docs', docsPending: 1, opened: 'Mar 28, 2026', carrierStatus: 'documentation_pending', workflowStatus: 'waiting_insured_docs', slaPercent: 40, handler: 'Marco V.' },
{ id: 'CLM-0045', customer: 'Clínica San José', agent: 'Ana R.', line: 'Life', type: 'Surgery pre-auth', carrier: 'Pan-American Life', reserved: '$23,500', paid: '$0', daysOpen: 12, priority: 'high', status: 'under_review', docsPending: 0, opened: 'Mar 24, 2026', carrierStatus: 'reserved', workflowStatus: 'waiting_carrier', slaPercent: 85, handler: 'Ana R.' },
{ id: 'CLM-0044', customer: 'Carmen Ruiz', agent: 'Ana R.', line: 'Life', type: 'Outpatient claim', carrier: 'Pan-American Life', reserved: '$3,800', paid: '$3,200', daysOpen: 18, priority: 'medium', status: 'approved', docsPending: 0, opened: 'Mar 18, 2026', carrierStatus: 'settlement_offered', workflowStatus: 'ready_to_close', slaPercent: 50, handler: 'Ana R.' },
{ id: 'CLM-0043', customer: 'Supermercado Tico', agent: 'Marco V.', line: 'General Risk', type: 'Customer injury — store premises', carrier: 'Mapfre', reserved: '$45,000', paid: '$0', daysOpen: 22, priority: 'high', status: 'under_review', docsPending: 3, opened: 'Mar 14, 2026', carrierStatus: 'negotiation', workflowStatus: 'client_update_overdue', slaPercent: 100, handler: 'Marco V.' },
{ id: 'CLM-0042', customer: 'Isabel Mora', agent: 'Ana R.', line: 'Auto', type: 'Theft — total loss', carrier: 'ASSA', reserved: '$18,500', paid: '$18,500', daysOpen: 35, priority: 'medium', status: 'closed', docsPending: 0, opened: 'Mar 1, 2026', carrierStatus: 'closed', workflowStatus: 'ready_to_close', slaPercent: 95, handler: 'Ana R.' },
{ id: 'CLM-0041', customer: 'Manuel Torres', agent: 'Marco V.', line: 'Life', type: 'Disability benefit', carrier: 'Pan-American Life', reserved: '$52,000', paid: '$12,000', daysOpen: 41, priority: 'medium', status: 'approved', docsPending: 0, opened: 'Feb 23, 2026', carrierStatus: 'reserved', workflowStatus: 'waiting_carrier', slaPercent: 70, handler: 'Marco V.' },
])
// ── View toggle ─────────────────────────────────────────────────────────────
const viewMode = ref<'my' | 'all'>('all')
type ClaimFilter = 'all' | 'active' | 'resolved'
const activeFilter = ref<ClaimFilter>('all')
// ── Filter dropdowns ────────────────────────────────────────────────────────
const statusFilter = ref('')
const carrierFilter = ref('')
const lobFilter = ref('')
const handlerFilter = ref('')
const agingFilter = ref('')
const priorityFilter = ref('')
const uniqueCarriers = computed(() => [...new Set(claims.value.map(c => c.carrier))].sort())
const uniqueLobs = computed(() => [...new Set(claims.value.map(c => c.line))].sort())
const uniqueHandlers = computed(() => [...new Set(claims.value.map(c => c.handler))].sort())
const filteredClaims = computed(() => {
let result = [...claims.value]
if (activeFilter.value === 'active') result = result.filter(c => !['closed', 'denied'].includes(c.status))
if (activeFilter.value === 'resolved') result = result.filter(c => ['closed', 'denied'].includes(c.status))
if (statusFilter.value) result = result.filter(c => c.status === statusFilter.value)
if (carrierFilter.value) result = result.filter(c => c.carrier === carrierFilter.value)
if (lobFilter.value) result = result.filter(c => c.line === lobFilter.value)
if (handlerFilter.value) result = result.filter(c => c.handler === handlerFilter.value)
if (priorityFilter.value) result = result.filter(c => c.priority === priorityFilter.value)
if (agingFilter.value) {
const ranges: Record<string, [number, number]> = { '0-7': [0, 7], '8-14': [8, 14], '15-30': [15, 30], '30+': [30, 9999] }
const [min, max] = ranges[agingFilter.value] ?? [0, 9999]
result = result.filter(c => c.daysOpen >= min && c.daysOpen <= max)
}
// Sort: breached first
result.sort((a, b) => {
const aBreached = a.slaPercent >= 100 ? 0 : 1
const bBreached = b.slaPercent >= 100 ? 0 : 1
if (aBreached !== bBreached) return aBreached - bBreached
return b.slaPercent - a.slaPercent
})
return result
})
const filterCounts = computed(() => ({
all: claims.value.length,
active: claims.value.filter(c => !['closed', 'denied'].includes(c.status)).length,
resolved: claims.value.filter(c => ['closed', 'denied'].includes(c.status)).length,
}))
const kpis = computed(() => {
const active = claims.value.filter(c => !['closed', 'denied'].includes(c.status))
const underReview = claims.value.filter(c => c.status === 'under_review').length
const avgDays = active.length ? Math.round(active.reduce((s, c) => s + c.daysOpen, 0) / active.length) : 0
const totalReserved = claims.value.filter(c => c.status !== 'closed').reduce((s, c) => s + parseFloat(c.reserved.replace(/[$,]/g, '')), 0)
const breached = claims.value.filter(c => c.slaPercent >= 100 && !['closed', 'denied'].includes(c.status)).length
return { openClaims: active.length, underReview, avgDays, totalReserved, breached }
})
const statusMeta: Record<string, { label: string; class: string }> = {
open: { label: 'Open', class: 'cl-st-open' },
under_review: { label: 'Under Review', class: 'cl-st-review' },
awaiting_docs: { label: 'Awaiting Docs', class: 'cl-st-docs' },
approved: { label: 'Approved', class: 'cl-st-approved' },
denied: { label: 'Denied', class: 'cl-st-denied' },
closed: { label: 'Closed', class: 'cl-st-closed' },
}
const priorityMeta: Record<string, { label: string; class: string }> = {
critical: { label: 'Critical', class: 'cl-pri-critical' },
high: { label: 'High', class: 'cl-pri-high' },
medium: { label: 'Med', class: 'cl-pri-medium' },
low: { label: 'Low', class: 'cl-pri-low' },
}
const carrierPillClass = (s: CarrierStatus) => {
const map: Record<string, string> = {
fnol_submitted: 'cl-csp-fnol', acknowledged: 'cl-csp-ack', investigation: 'cl-csp-inv',
documentation_pending: 'cl-csp-doc', reserved: 'cl-csp-rsv', negotiation: 'cl-csp-neg',
settlement_offered: 'cl-csp-set', closed: 'cl-csp-closed',
}
return map[s] ?? ''
}
function formatCurrency(n: number) {
return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 0 })
}
function clearFilters() {
statusFilter.value = ''
carrierFilter.value = ''
lobFilter.value = ''
handlerFilter.value = ''
agingFilter.value = ''
priorityFilter.value = ''
}
const hasActiveFilters = computed(() => !!(statusFilter.value || carrierFilter.value || lobFilter.value || handlerFilter.value || agingFilter.value || priorityFilter.value))
const toast = useToast()
function handleNewClaim() {
toast.add({ title: 'New claim flow coming soon', description: 'This will open the FNOL intake wizard.', color: 'neutral' })
}
</script>
<template>
<div class="cl-page">
<!-- Header -->
<div class="flex flex-wrap items-end justify-between gap-3">
<div class="max-w-xl">
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Claims</h1>
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
Track claims lifecycle from first notice of loss through resolution and payment.
</p>
</div>
<button type="button" class="cl-action-btn-primary" @click="handleNewClaim">
<UIcon name="i-heroicons-plus" style="width: 14px; height: 14px;" />
New Claim
</button>
</div>
<!-- KPI strip -->
<div class="cl-kpi-strip">
<div class="cl-kpi">
<p class="cl-kpi-label">Open claims</p>
<p class="cl-kpi-value">{{ kpis.openClaims }}</p>
</div>
<div class="cl-kpi">
<p class="cl-kpi-label">Under review</p>
<p class="cl-kpi-value" style="color: #c27b1a;">{{ kpis.underReview }}</p>
</div>
<div class="cl-kpi">
<p class="cl-kpi-label">SLA breached</p>
<p class="cl-kpi-value" :style="kpis.breached > 0 ? 'color: #c13838;' : ''">{{ kpis.breached }}</p>
</div>
<div class="cl-kpi">
<p class="cl-kpi-label">Avg days open</p>
<p class="cl-kpi-value">{{ kpis.avgDays }}d</p>
</div>
<div class="cl-kpi">
<p class="cl-kpi-label">Total reserved</p>
<p class="cl-kpi-value">{{ formatCurrency(kpis.totalReserved) }}</p>
</div>
</div>
<!-- View toggle + Filter tabs -->
<div class="cl-controls-row">
<div class="cl-view-toggle">
<button
type="button"
class="cl-view-btn"
:class="viewMode === 'my' ? 'cl-view-on' : 'cl-view-off'"
@click="viewMode = 'my'"
>My Claims</button>
<button
type="button"
class="cl-view-btn"
:class="viewMode === 'all' ? 'cl-view-on' : 'cl-view-off'"
@click="viewMode = 'all'"
>All Claims</button>
</div>
<div class="cl-filter-tabs">
<button
v-for="f in ([
{ id: 'all', label: 'All' },
{ id: 'active', label: 'Active' },
{ id: 'resolved', label: 'Resolved' },
] as { id: ClaimFilter; label: string }[])"
:key="f.id"
type="button"
class="cl-filter-tab"
:class="activeFilter === f.id ? 'cl-filter-on' : 'cl-filter-off'"
@click="activeFilter = f.id"
>
{{ f.label }}
<span class="cl-filter-count" :class="activeFilter === f.id ? 'cl-filter-count-on' : ''">{{ filterCounts[f.id] }}</span>
</button>
</div>
<span class="text-[11px] text-[var(--text-muted)] ml-auto">{{ filteredClaims.length }} results</span>
</div>
<!-- Filter dropdowns row -->
<div class="cl-dropdown-row">
<select v-model="statusFilter" class="cl-dropdown">
<option value="">Status</option>
<option value="open">Open</option>
<option value="under_review">Under Review</option>
<option value="awaiting_docs">Awaiting Docs</option>
<option value="approved">Approved</option>
<option value="denied">Denied</option>
<option value="closed">Closed</option>
</select>
<select v-model="carrierFilter" class="cl-dropdown">
<option value="">Carrier</option>
<option v-for="c in uniqueCarriers" :key="c" :value="c">{{ c }}</option>
</select>
<select v-model="lobFilter" class="cl-dropdown">
<option value="">LOB</option>
<option v-for="l in uniqueLobs" :key="l" :value="l">{{ l }}</option>
</select>
<select v-model="handlerFilter" class="cl-dropdown">
<option value="">Handler</option>
<option v-for="h in uniqueHandlers" :key="h" :value="h">{{ h }}</option>
</select>
<select v-model="agingFilter" class="cl-dropdown">
<option value="">Aging</option>
<option value="0-7">07 days</option>
<option value="8-14">814 days</option>
<option value="15-30">1530 days</option>
<option value="30+">30+ days</option>
</select>
<select v-model="priorityFilter" class="cl-dropdown">
<option value="">Priority</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<button v-if="hasActiveFilters" class="cl-clear-btn" @click="clearFilters">
<UIcon name="i-heroicons-x-mark" class="w-3 h-3" />
Clear
</button>
</div>
<!-- Claims table -->
<div class="cl-table-wrap">
<table class="cl-table">
<thead>
<tr>
<th style="width: 28px;"></th>
<th>Claim</th>
<th>Customer / Agent</th>
<th>Line / Type</th>
<th>Carrier</th>
<th>Status</th>
<th class="text-right">Reserved</th>
<th class="text-right">Paid</th>
<th class="text-right">Days</th>
<th>Priority</th>
<th class="text-center">Docs</th>
</tr>
</thead>
<tbody>
<tr
v-for="c in filteredClaims"
:key="c.id"
class="cl-row"
:class="{ 'cl-breach-row': c.slaPercent >= 100 && !['closed', 'denied'].includes(c.status) }"
style="cursor: pointer;"
@click="navigateTo(`/claims/${c.id}`)"
>
<td><span class="cl-sla-dot" :class="`cl-sla-${slaColor(c.slaPercent)}`" /></td>
<td>
<NuxtLink :to="`/claims/${c.id}`" class="cl-claim-link" @click.stop>{{ c.id }}</NuxtLink>
</td>
<td>
<p class="text-[13px] font-medium text-[var(--text-primary)]">{{ c.customer || 'Unnamed customer' }}</p>
<p class="text-[11px] text-[var(--text-muted)]">{{ c.agent || '—' }}</p>
</td>
<td>
<p class="text-[13px] text-[var(--text-primary)]">{{ c.line }}</p>
<p class="text-[11px] text-[var(--text-muted)]">{{ c.type }}</p>
</td>
<td class="text-[13px] text-[var(--text-muted)]">{{ c.carrier }}</td>
<td>
<div class="cl-dual-status">
<span class="cl-carrier-status-pill" :class="carrierPillClass(c.carrierStatus)">{{ CARRIER_STATUS_LABELS[c.carrierStatus] }}</span>
<span class="cl-workflow-status-pill">{{ WORKFLOW_STATUS_LABELS[c.workflowStatus] }}</span>
</div>
</td>
<td class="text-right font-semibold text-[13px] text-[var(--text-primary)]">{{ c.reserved }}</td>
<td class="text-right text-[13px]" :class="c.paid !== '$0' ? 'text-[var(--text-primary)] font-medium' : 'text-[var(--text-muted)] opacity-50'">{{ c.paid }}</td>
<td class="text-right">
<span class="text-[13px] font-bold" :class="c.daysOpen > 30 ? 'text-rose-600' : c.daysOpen > 14 ? 'text-amber-600' : 'text-[var(--text-primary)]'">{{ c.daysOpen }}d</span>
</td>
<td>
<span :class="priorityMeta[c.priority].class">{{ priorityMeta[c.priority].label }}</span>
</td>
<td class="text-center">
<span v-if="c.docsPending > 0" class="cl-docs-badge">{{ c.docsPending }}</span>
<span v-else class="text-[11px] text-[var(--text-muted)] opacity-40"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
.cl-page {
max-width: 76rem;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 20px;
padding-bottom: 3rem;
}
.cl-action-btn-primary {
display: inline-flex; align-items: center; gap: 6px;
padding: 8px 16px; border-radius: 8px;
background: #01696f; color: #fff;
font-size: 13px; font-weight: 500; border: none;
cursor: pointer; transition: all 150ms ease; white-space: nowrap;
}
.cl-action-btn-primary:hover { background: #015458; }
/* ── KPI strip ── */
.cl-kpi-strip {
display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px;
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
background: rgba(0,0,0,0.06); box-shadow: 0 1px 3px rgba(0,0,0,0.03);
overflow: hidden;
}
.cl-kpi { padding: 14px 18px; background: #fff; }
.cl-kpi:first-child { border-radius: 12px 0 0 12px; }
.cl-kpi:last-child { border-radius: 0 12px 12px 0; }
.cl-kpi-label {
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.04em; color: #8a8a86;
}
.cl-kpi-value {
margin-top: 4px; font-size: 22px; font-weight: 600;
color: var(--text-primary); font-variant-numeric: tabular-nums;
}
@media (max-width: 640px) { .cl-kpi-strip { grid-template-columns: repeat(2, 1fr); } }
/* ── Controls row ── */
.cl-controls-row {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
/* ── View toggle ── */
.cl-view-toggle {
display: inline-flex; gap: 1px; padding: 2px;
border-radius: 8px; background: rgba(0,0,0,0.04);
}
.cl-view-btn {
padding: 5px 12px; border-radius: 6px; font-size: 12px; font-weight: 600;
border: none; cursor: pointer; transition: all 150ms ease; white-space: nowrap;
}
.cl-view-on { background: #01696f; color: white; }
.cl-view-off { background: transparent; color: #8a8a86; }
.cl-view-off:hover { color: var(--text-primary); }
/* ── Filter tabs ── */
.cl-filter-tabs {
display: inline-flex; gap: 2px; padding: 3px;
border-radius: 10px; background: rgba(0,0,0,0.04);
}
.cl-filter-tab {
display: inline-flex; align-items: center; gap: 5px;
padding: 6px 12px; border-radius: 8px;
font-size: 12px; font-weight: 500; border: none;
cursor: pointer; transition: all 150ms ease; white-space: nowrap;
}
.cl-filter-on { background: #fff; color: var(--text-primary); box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.cl-filter-off { background: transparent; color: var(--text-muted); }
.cl-filter-off:hover { color: var(--text-primary); }
.cl-filter-count {
font-size: 10px; font-weight: 600; padding: 1px 5px;
border-radius: 9999px; background: rgba(0,0,0,0.06); color: var(--text-muted);
}
.cl-filter-count-on { background: rgba(1,105,111,0.1); color: #01696f; }
/* ── Dropdown filters ── */
.cl-dropdown-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.cl-dropdown {
padding: 5px 10px; border-radius: 8px; font-size: 12px; font-weight: 500;
border: 1px solid rgba(0,0,0,0.08); background: #fff; color: var(--text-primary);
cursor: pointer; min-width: 100px;
}
.cl-dropdown:focus { outline: none; border-color: #01696f; }
.cl-clear-btn {
display: inline-flex; align-items: center; gap: 4px; padding: 5px 10px;
border-radius: 8px; font-size: 11px; font-weight: 600;
background: rgba(193, 56, 56, 0.06); color: #c13838;
border: 1px solid rgba(193, 56, 56, 0.15); cursor: pointer;
}
.cl-clear-btn:hover { background: rgba(193, 56, 56, 0.12); }
/* ── Table ── */
.cl-table-wrap {
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.03);
overflow-x: auto;
}
.cl-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.cl-table thead th {
padding: 10px 14px;
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.04em; color: #8a8a86;
border-bottom: 1px solid rgba(0,0,0,0.06);
white-space: nowrap; text-align: left;
}
.cl-table tbody td {
padding: 12px 14px; border-bottom: 1px solid rgba(0,0,0,0.04);
vertical-align: top;
}
.cl-row { transition: background 100ms ease; }
.cl-row:hover { background: rgba(0,0,0,0.015); }
.cl-row:last-child td { border-bottom: none; }
/* ── Breach row ── */
.cl-breach-row { box-shadow: inset 3px 0 0 #c13838; }
/* ── SLA dot ── */
.cl-sla-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.cl-sla-green { background: #059669; }
.cl-sla-amber { background: #c27b1a; }
.cl-sla-red { background: #c13838; }
/* ── Claim link ── */
.cl-claim-link {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 12px; font-weight: 600; color: #01696f;
text-decoration: none;
}
.cl-claim-link:hover { text-decoration: underline; }
/* ── Dual status pills ── */
.cl-dual-status { display: flex; flex-direction: column; gap: 3px; }
.cl-carrier-status-pill {
display: inline-flex; padding: 2px 7px; border-radius: 8px;
font-size: 10px; font-weight: 600; white-space: nowrap;
}
.cl-csp-fnol { background: rgba(59, 130, 246, 0.08); color: #2563eb; }
.cl-csp-ack { background: rgba(16, 185, 129, 0.08); color: #059669; }
.cl-csp-inv { background: rgba(245, 158, 11, 0.08); color: #d97706; }
.cl-csp-doc { background: rgba(147, 51, 234, 0.08); color: #9333ea; }
.cl-csp-rsv { background: rgba(1, 105, 111, 0.08); color: #01696f; }
.cl-csp-neg { background: rgba(194, 123, 26, 0.08); color: #c27b1a; }
.cl-csp-set { background: rgba(16, 185, 129, 0.08); color: #059669; }
.cl-csp-closed { background: rgba(138, 138, 134, 0.08); color: #8a8a86; }
.cl-workflow-status-pill {
display: inline-flex; padding: 0; border-radius: 0;
font-size: 10px; font-weight: 500; white-space: nowrap;
border: none; color: var(--text-muted);
}
/* ── Status badges ── */
.cl-st-open { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(193,56,56,0.08); color: #c13838; white-space: nowrap; }
.cl-st-review { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(194,123,26,0.08); color: #c27b1a; white-space: nowrap; }
.cl-st-docs { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(147,51,234,0.08); color: #9333ea; white-space: nowrap; }
.cl-st-approved { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(1,105,111,0.08); color: #01696f; white-space: nowrap; }
.cl-st-denied { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(0,0,0,0.06); color: #6b6b68; white-space: nowrap; }
.cl-st-closed { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(0,0,0,0.04); color: #8a8a86; white-space: nowrap; }
/* ── Priority badges ── */
.cl-pri-critical { font-size: 10px; font-weight: 700; padding: 1px 7px; border-radius: 9999px; background: rgba(193,56,56,0.12); color: #c13838; white-space: nowrap; }
.cl-pri-high { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px; background: rgba(194,123,26,0.08); color: #c27b1a; white-space: nowrap; }
.cl-pri-medium { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px; background: rgba(0,0,0,0.05); color: #6b6b68; white-space: nowrap; }
.cl-pri-low { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px; background: rgba(0,0,0,0.03); color: #8a8a86; white-space: nowrap; }
/* ── Docs pending badge ── */
.cl-docs-badge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 20px; height: 20px; padding: 0 5px;
border-radius: 9999px; background: rgba(147,51,234,0.08); color: #9333ea;
font-size: 11px; font-weight: 700;
}
</style>