diff --git a/src/App.css b/src/App.css
index 8fe096b..106307f 100644
--- a/src/App.css
+++ b/src/App.css
@@ -9,62 +9,75 @@
position: sticky;
top: 0;
z-index: 50;
- background: var(--bg-card);
- border-bottom: 1px solid var(--border);
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
-}
-
-.app-header.glass {
- background: rgba(var(--bg-card), 0.8);
+ background: var(--navy);
+ border-bottom: 3px solid var(--crimson);
}
.header-content {
max-width: 1200px;
margin: 0 auto;
- padding: 1rem 1.5rem;
+ padding: 0.8rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
+ gap: 1rem;
}
-.logo-group {
+/* Brand lockup: Jawafdehi wordmark + NGM tag */
+.brand {
display: flex;
align-items: center;
- gap: 1rem;
+ gap: 0.75rem;
+ text-decoration: none;
+ flex-shrink: 0;
}
-.logo-icon {
- width: 36px;
- height: 36px;
- flex-shrink: 0;
+.brand-wordmark {
+ height: 28px;
+ width: auto;
+ display: block;
}
-.logo {
- font-size: 1.25rem;
+.brand-sep {
+ width: 1px;
+ height: 26px;
+ background: rgba(255, 253, 247, 0.25);
+}
+
+.brand-sub {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.15;
+}
+
+.brand-tag {
+ font-size: 0.95rem;
font-weight: 700;
- letter-spacing: -0.025em;
- color: var(--text-primary);
+ letter-spacing: 0.05em;
+ color: var(--warm-white);
}
-.version-pill {
- font-size: 0.75rem;
- font-weight: 600;
- padding: 0.125rem 0.5rem;
- background: var(--border);
- color: var(--text-secondary);
- border-radius: 9999px;
+.brand-tagline {
+ font-size: 0.6rem;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+ color: rgba(255, 253, 247, 0.6);
}
.nav-link {
- margin-left: 1.5rem;
+ margin-left: 1.25rem;
font-size: 0.875rem;
font-weight: 500;
- color: var(--text-secondary);
+ color: rgba(255, 253, 247, 0.78);
+ white-space: nowrap;
}
.nav-link:hover {
- color: var(--text-primary);
+ color: var(--warm-white);
+}
+
+.nav-parent {
+ color: rgba(255, 253, 247, 0.55);
}
/* Main Content */
@@ -84,12 +97,21 @@
padding: 3rem 0;
}
+.hero-eyebrow {
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ font-size: 0.72rem;
+ font-weight: 700;
+ color: var(--crimson);
+ margin-bottom: 0.85rem;
+}
+
.hero h2 {
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -0.025em;
margin-bottom: 1rem;
- background: linear-gradient(135deg, #1d4ed8 0%, #dc2626 100%);
+ background: linear-gradient(135deg, var(--navy) 0%, var(--crimson) 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
@@ -119,7 +141,7 @@
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
- background: #dbeafe;
+ background: var(--bg-tint);
padding: 0 1rem;
}
@@ -128,7 +150,7 @@
background: transparent;
border: none;
border-bottom: 2px solid transparent;
- color: #475569;
+ color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
@@ -136,8 +158,8 @@
}
.tab-btn:hover {
- color: #1e293b;
- background: #bfdbfe;
+ color: var(--text-primary);
+ background: var(--border-cool);
}
.tab-btn.active {
@@ -247,9 +269,9 @@
}
.badge.info {
- background: #dbeafe;
- color: #1e40af;
- border-color: #bfdbfe;
+ background: var(--bg-tint);
+ color: var(--accent);
+ border-color: var(--border-cool);
}
@@ -355,12 +377,38 @@
/* Footer */
.app-footer {
+ margin-top: 2rem;
+ padding: 2.5rem 1.5rem;
+ background: var(--navy);
+ color: rgba(255, 253, 247, 0.7);
+ border-top: 3px solid var(--crimson);
+}
+
+.footer-inner {
+ max-width: 1200px;
+ margin: 0 auto;
text-align: center;
- padding: 2rem;
- color: var(--text-secondary);
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
font-size: 0.875rem;
- border-top: 1px solid var(--border);
- background: var(--bg-main);
+}
+
+.footer-brand {
+ font-weight: 700;
+ font-size: 1rem;
+ color: var(--warm-white);
+ letter-spacing: 0.03em;
+}
+
+.footer-meta {
+ font-size: 0.78rem;
+ color: rgba(255, 253, 247, 0.5);
+}
+
+.app-footer a {
+ color: var(--warm-white);
+ text-decoration: underline;
}
/* Mobile Responsive Styles */
@@ -682,7 +730,7 @@
.filter-select:focus {
outline: none;
border-color: var(--accent);
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
+ box-shadow: 0 0 0 3px var(--accent-soft);
}
.filter-clear {
@@ -740,9 +788,9 @@
}
.file-chip.doc {
- background: #eff6ff;
- color: #1e40af;
- border-color: #bfdbfe;
+ background: var(--bg-tint);
+ color: var(--accent);
+ border-color: var(--border-cool);
}
.file-chip.default {
@@ -768,8 +816,8 @@
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
- background: #e0e7ff;
- border-bottom: 1px solid #bfdbfe;
+ background: var(--bg-tint);
+ border-bottom: 1px solid var(--border-cool);
border-radius: 12px 12px 0 0;
}
@@ -794,8 +842,8 @@
flex: 1;
padding: 0.75rem 2.5rem 0.75rem 2.5rem;
background: #ffffff;
- color: #1e293b;
- border: 1px solid #bfdbfe;
+ color: var(--text-primary);
+ border: 1px solid var(--border-cool);
border-radius: 0.5rem;
font-size: 0.875rem;
transition: all 0.2s;
@@ -804,7 +852,7 @@
.table-search:focus {
outline: none;
border-color: var(--accent);
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
+ box-shadow: 0 0 0 3px var(--accent-soft);
}
.clear-search {
@@ -828,7 +876,7 @@
.table-info {
font-size: 0.875rem;
- color: #1e293b;
+ color: var(--text-primary);
white-space: nowrap;
font-weight: 600;
}
@@ -853,7 +901,7 @@
}
.data-table thead {
- background: #e0e7ff;
+ background: var(--bg-tint);
position: sticky;
top: 0;
z-index: 10;
@@ -864,8 +912,8 @@
text-align: left;
font-size: 0.8rem;
font-weight: 600;
- color: #1e293b;
- border-bottom: 2px solid #bfdbfe;
+ color: var(--text-primary);
+ border-bottom: 2px solid var(--border-cool);
white-space: nowrap;
}
@@ -873,8 +921,8 @@
padding: 0.625rem 0.75rem;
font-size: 0.8rem;
font-weight: 600;
- color: #1e293b;
- border-bottom: 1px solid #cbd5e1;
+ color: var(--text-primary);
+ border-bottom: 1px solid var(--border-cool);
}
.data-table tbody tr {
@@ -883,7 +931,7 @@
}
.data-table tbody tr:hover {
- background: #e0e7ff;
+ background: var(--bg-tint);
}
.data-table tbody tr:last-child td {
@@ -936,7 +984,7 @@
.filter-input:focus {
outline: none;
border-color: var(--accent);
- box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
+ box-shadow: 0 0 0 2px var(--accent-soft);
}
/* Table Pagination - Compact */
@@ -945,8 +993,8 @@
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
- background: #e0e7ff;
- border-top: 1px solid #bfdbfe;
+ background: var(--bg-tint);
+ border-top: 1px solid var(--border-cool);
border-radius: 0 0 12px 12px;
gap: 1rem;
}
@@ -960,8 +1008,8 @@
.table-pagination .pagination-btn {
padding: 0.375rem 0.75rem;
background: #ffffff;
- color: #1e293b;
- border: 1px solid #bfdbfe;
+ color: var(--text-primary);
+ border: 1px solid var(--border-cool);
border-radius: var(--radius-md);
font-size: 0.8rem;
font-weight: 500;
@@ -985,7 +1033,7 @@
.table-pagination .pagination-info {
font-size: 0.8rem;
font-weight: 600;
- color: #1e293b;
+ color: var(--text-primary);
padding: 0 0.375rem;
white-space: nowrap;
}
@@ -1001,7 +1049,7 @@
align-items: center;
gap: 0.375rem;
font-size: 0.8rem;
- color: #1e293b;
+ color: var(--text-primary);
font-weight: 600;
white-space: nowrap;
}
@@ -1009,8 +1057,8 @@
.page-size-select {
padding: 0.25rem 0.375rem;
background: #ffffff;
- color: #1e293b;
- border: 1px solid #bfdbfe;
+ color: var(--text-primary);
+ border: 1px solid var(--border-cool);
border-radius: var(--radius-md);
font-size: 0.8rem;
cursor: pointer;
@@ -1024,7 +1072,7 @@
.page-size-select:focus {
outline: none;
border-color: var(--accent);
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
+ box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Link Styling in Table */
@@ -1171,7 +1219,7 @@
padding: 0.625rem 2.5rem 0.625rem 0.75rem;
background: #ffffff;
color: var(--text-primary);
- border: 1px solid #bfdbfe;
+ border: 1px solid var(--border-cool);
border-radius: var(--radius-md);
font-size: 0.875rem;
transition: all 0.2s;
@@ -1180,7 +1228,7 @@
.advanced-search-input:focus {
outline: none;
border-color: var(--accent);
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
+ box-shadow: 0 0 0 3px var(--accent-soft);
}
.advanced-search-field .clear-search {
@@ -1211,8 +1259,10 @@
/* === Navigation active state === */
.nav-link.active {
- color: var(--accent);
+ color: var(--warm-white);
font-weight: 700;
+ border-bottom: 2px solid var(--crimson);
+ padding-bottom: 3px;
}
/* === Header nav container === */
@@ -1382,9 +1432,9 @@
}
.status-badge.info {
- background: #dbeafe;
- color: #1e40af;
- border: 1px solid #bfdbfe;
+ background: var(--bg-tint);
+ color: var(--accent);
+ border: 1px solid var(--border-cool);
}
.status-card h4 {
@@ -1428,16 +1478,16 @@
font-weight: 600;
padding: 0.15rem 0.5rem;
border-radius: 4px;
- background: #eff6ff;
- color: #1e40af;
- border: 1px solid #bfdbfe;
- font-family: 'Inter', monospace;
+ background: var(--bg-tint);
+ color: var(--accent);
+ border: 1px solid var(--border-cool);
+ font-family: ui-monospace, 'SF Mono', monospace;
}
/* === Status Timestamps Note === */
.status-timestamps-note {
background: #f8fafc;
- border: 1px dashed #cbd5e1;
+ border: 1px dashed var(--border-cool);
border-radius: var(--radius-md);
padding: 1.25rem;
}
diff --git a/src/components/CIAADatasetViewer.tsx b/src/components/CIAADatasetViewer.tsx
index fbcbff9..23673c8 100644
--- a/src/components/CIAADatasetViewer.tsx
+++ b/src/components/CIAADatasetViewer.tsx
@@ -257,7 +257,7 @@ export default function CIAADatasetViewer() {
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
- style={{ color: '#2563eb', textDecoration: 'none', fontWeight: 500 }}
+ style={{ color: 'var(--navy)', textDecoration: 'none', fontWeight: 500 }}
>
{info.getValue() as string}
@@ -420,7 +420,7 @@ export default function CIAADatasetViewer() {
โ๏ธ Plaintiffs ({row.plaintiffs.length})
{row.plaintiffs.map((p, idx) => (
- {p.name}
+ {p.name}
))}
@@ -461,7 +461,7 @@ export default function CIAADatasetViewer() {
โ๏ธ Plaintiffs ({row.appealed_case.plaintiffs.length})
{row.appealed_case.plaintiffs.map((p, idx) => (
- {p.name}
+ {p.name}
))}
diff --git a/src/components/CourtCaseSearch.tsx b/src/components/CourtCaseSearch.tsx
index bd7ab28..60c98db 100644
--- a/src/components/CourtCaseSearch.tsx
+++ b/src/components/CourtCaseSearch.tsx
@@ -226,9 +226,9 @@ export default function CourtCaseSearch() {
background: '#f0f4ff',
borderRadius: '12px',
marginBottom: '1.5rem',
- border: '2px solid #bfdbfe'
+ border: '2px solid var(--border-cool)'
}}>
-
+
๐ Search Court Cases
@@ -239,7 +239,7 @@ export default function CourtCaseSearch() {
htmlFor="court-select"
style={{
display: 'block',
- color: '#1e40af',
+ color: 'var(--navy)',
marginBottom: '0.5rem',
fontSize: '0.9rem',
fontWeight: 600
@@ -262,7 +262,7 @@ export default function CourtCaseSearch() {
width: '100%',
padding: '0.6rem',
borderRadius: '6px',
- border: '2px solid #bfdbfe',
+ border: '2px solid var(--border-cool)',
background: '#ffffff',
fontSize: '0.95rem',
cursor: 'pointer'
@@ -286,7 +286,7 @@ export default function CourtCaseSearch() {
htmlFor="case-number"
style={{
display: 'block',
- color: '#1e40af',
+ color: 'var(--navy)',
marginBottom: '0.5rem',
fontSize: '0.9rem',
fontWeight: 600
@@ -304,7 +304,7 @@ export default function CourtCaseSearch() {
width: '100%',
padding: '0.6rem',
borderRadius: '6px',
- border: '2px solid #bfdbfe',
+ border: '2px solid var(--border-cool)',
background: '#ffffff',
fontSize: '0.95rem'
}}
@@ -319,7 +319,7 @@ export default function CourtCaseSearch() {
padding: '0.6rem 1.5rem',
borderRadius: '6px',
border: 'none',
- background: !loading && caseNumber.trim() ? '#3b82f6' : '#cbd5e1',
+ background: !loading && caseNumber.trim() ? 'var(--navy)' : 'var(--border-cool)',
color: 'white',
fontWeight: 600,
cursor: !loading && caseNumber.trim() ? 'pointer' : 'not-allowed',
@@ -396,7 +396,7 @@ export default function CourtCaseSearch() {
{/* Entities (Parties) */}
{caseData.entities && caseData.entities.length > 0 && (
-
+
๐ฅ Parties Involved
@@ -416,19 +416,19 @@ export default function CourtCaseSearch() {
{/* Hearings */}
{caseData.hearings && caseData.hearings.length > 0 && (
-
+
๐ Hearing History ({caseData.hearings.length} hearings)
{caseData.hearings.map((hearing) => (
-
+
- {hearing.hearing_date_bs}
+ {hearing.hearing_date_bs}
{hearing.hearing_date_ad && ({hearing.hearing_date_ad})}
{hearing.case_status && (
-
+
{hearing.case_status}
)}
@@ -458,7 +458,7 @@ export default function CourtCaseSearch() {
diff --git a/src/components/IndexViewer.tsx b/src/components/IndexViewer.tsx
index 61b00a1..58513bf 100644
--- a/src/components/IndexViewer.tsx
+++ b/src/components/IndexViewer.tsx
@@ -1,8 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import { DataTable } from './DataTable';
-import CIAADatasetViewer from './CIAADatasetViewer';
-import CourtCaseSearch from './CourtCaseSearch';
// TODO: Replace with backend metadata extraction when persons data is available
import { containsPersonName } from '../data/casesData';
@@ -32,10 +30,18 @@ const TABLE_CONFIG = {
} as const;
// NGM Index v2.0 types - Tree-based hierarchical index
+type LinkRole = 'RAW' | 'ALTERNATE' | 'SOURCE_PAGE' | 'MARKDOWN' | 'PERMALINK';
+type SourceLink = { link: string; role: LinkRole };
type Manuscript = {
- url: string;
+ url: string; // back-compat alias for the primary RAW file
file_name: string;
metadata: Record;
+ // NGM Index v2: one logical document per manuscript, carrying roled links.
+ // `links`/`document_id`/`source_type` are absent on legacy index snapshots,
+ // so all readers fall back to `url` (see fileLinks/primaryUrl below).
+ links?: SourceLink[];
+ document_id?: string;
+ source_type?: string;
};
type IndexNodeStub = {
@@ -87,6 +93,31 @@ function getProxiedUrl(url: string): string {
return url;
}
+/** Downloadable file links (RAW first, then ALTERNATE). Falls back to the legacy
+ * single `url` when an older index snapshot has no `links`. */
+function fileLinks(m: Manuscript): SourceLink[] {
+ const roled = (m.links ?? []).filter((l) => l.role === 'RAW' || l.role === 'ALTERNATE');
+ if (roled.length > 0) {
+ return [...roled].sort((a, b) => Number(b.role === 'RAW') - Number(a.role === 'RAW'));
+ }
+ return m.url ? [{ link: m.url, role: 'RAW' }] : [];
+}
+
+/** First link with the given role, if any (e.g. SOURCE_PAGE, MARKDOWN). */
+function linkByRole(m: Manuscript, role: LinkRole): string | undefined {
+ return m.links?.find((l) => l.role === role)?.link;
+}
+
+/** Primary downloadable URL (RAW), with legacy `url` fallback. */
+function primaryUrl(m: Manuscript): string {
+ return fileLinks(m)[0]?.link ?? m.url ?? '';
+}
+
+/** Uppercase file extension parsed from a URL (e.g. "PDF", "DOCX"). */
+function extFromUrl(u: string): string {
+ return u.match(/\.([A-Za-z0-9]+)(?:[?#]|$)/)?.[1]?.toUpperCase() || 'FILE';
+}
+
/** Fetch all manuscripts for a node, following pagination via `next` links. */
async function fetchAllManuscripts(
ref: string,
@@ -279,7 +310,6 @@ export default function IndexViewer() {
const [rootError, setRootError] = useState(null);
const [tabErrors, setTabErrors] = useState>({ kanun: null, ciaa: null, press: null, court: null, dataset: null, courtcases: null });
const [activeTab, setActiveTab] = useState('kanun');
- const [hasVisitedDataset, setHasVisitedDataset] = useState(false);
const loadingRef = useRef>(new Set());
const abortControllersRef = useRef