diff --git a/web/static/app.js b/web/static/app.js
deleted file mode 100644
index a1832be..0000000
--- a/web/static/app.js
+++ /dev/null
@@ -1,343 +0,0 @@
-// Adobe Sign → DocuSign Migrator — frontend app
-// Vanilla JS, no build step.
-
-const $ = id => document.getElementById(id);
-
-let statusTemplates = []; // [{adobe_id, name, status, docusign_id, ...}]
-let dsTemplates = []; // [{id, name, lastModified}]
-let authState = { adobe: false, docusign: false };
-
-// ── Init ────────────────────────────────────────────────────────────────────
-
-document.addEventListener('DOMContentLoaded', async () => {
- await refreshAuth();
- await refreshTemplates();
- await refreshHistory();
-
- $('btn-migrate').addEventListener('click', onMigrate);
- $('btn-refresh').addEventListener('click', async () => {
- await refreshTemplates();
- await refreshHistory();
- });
-});
-
-// ── Auth ─────────────────────────────────────────────────────────────────────
-
-async function refreshAuth() {
- const resp = await fetch('/api/auth/status');
- authState = await resp.json();
- renderAuthBar();
-}
-
-function renderAuthBar() {
- // Adobe: use .env credentials (primary), OAuth dialog (secondary)
- const adobeEl = $('badge-adobe');
- adobeEl.textContent = authState.adobe ? '✓ Adobe Sign' : 'Connect Adobe Sign';
- adobeEl.className = 'auth-badge' + (authState.adobe ? ' connected' : '');
- adobeEl.onclick = authState.adobe
- ? () => disconnectPlatform('adobe')
- : () => connectAdobeEnv();
-
- // DocuSign: JWT grant from .env — no browser sign-in needed
- const dsEl = $('badge-docusign');
- dsEl.textContent = authState.docusign ? '✓ DocuSign' : 'Connect DocuSign';
- dsEl.className = 'auth-badge' + (authState.docusign ? ' connected' : '');
- dsEl.onclick = authState.docusign
- ? () => disconnectPlatform('docusign')
- : () => connectDocusign();
-}
-
-async function disconnectPlatform(platform) {
- await fetch(`/api/auth/${platform}/disconnect`);
- authState[platform] = false;
- renderAuthBar();
- await refreshTemplates();
-}
-
-async function connectAdobeEnv() {
- const el = $('badge-adobe');
- el.textContent = 'Connecting…';
- const resp = await fetch('/api/auth/adobe/connect');
- const data = await resp.json();
- if (data.connected) {
- authState.adobe = true;
- renderAuthBar();
- await refreshTemplates();
- } else {
- el.textContent = 'Connect Adobe Sign';
- // If .env has no credentials, fall back to the OAuth dialog
- if (data.error && data.error.includes('No Adobe Sign credentials')) {
- startAdobeAuth();
- } else {
- setStatus('Adobe Sign error: ' + (data.error || 'unknown'));
- }
- }
-}
-
-async function connectDocusign() {
- const dsEl = $('badge-docusign');
- dsEl.textContent = 'Connecting…';
- const resp = await fetch('/api/auth/docusign/connect');
- const data = await resp.json();
- if (data.connected) {
- authState.docusign = true;
- renderAuthBar();
- await refreshTemplates();
- } else {
- dsEl.textContent = 'Connect DocuSign';
- setStatus('DocuSign error: ' + (data.error || 'unknown'));
- }
-}
-
-// Adobe Sign uses the same manual-paste flow as the CLI:
-// 1. Open auth URL in new tab
-// 2. User authorizes → lands on failed https://localhost:8080/callback page
-// 3. User copies that URL, pastes it into the dialog here
-// 4. We POST it to /api/auth/adobe/exchange
-
-async function startAdobeAuth() {
- const resp = await fetch('/api/auth/adobe/url');
- const { url } = await resp.json();
-
- showAdobeDialog(url);
-}
-
-function showAdobeDialog(authUrl) {
- // Remove any existing dialog
- const existing = $('adobe-auth-dialog');
- if (existing) existing.remove();
-
- const dialog = document.createElement('div');
- dialog.id = 'adobe-auth-dialog';
- dialog.innerHTML = `
-
-
-
Connect Adobe Sign
-
- - Click here to authorize in Adobe Sign
- - After authorizing, your browser will show a page that fails to load — that's expected.
- - Copy the full URL from the address bar and paste it below.
-
-
-
-
-
-
-
-
- `;
- document.body.appendChild(dialog);
-
- $('btn-cancel-dialog').onclick = () => dialog.remove();
- $('btn-submit-code').onclick = () => submitAdobeCode(dialog);
-
- // Also handle Enter key
- $('adobe-redirect-input').addEventListener('keydown', e => {
- if (e.key === 'Enter') submitAdobeCode(dialog);
- });
-}
-
-async function submitAdobeCode(dialog) {
- const url = $('adobe-redirect-input').value.trim();
- if (!url) return;
-
- $('btn-submit-code').disabled = true;
- $('btn-submit-code').textContent = 'Connecting…';
- $('dialog-error').textContent = '';
-
- try {
- const resp = await fetch('/api/auth/adobe/exchange', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ redirect_url: url }),
- });
- const data = await resp.json();
-
- if (!resp.ok || data.error) {
- $('dialog-error').textContent = data.error || 'Connection failed.';
- $('btn-submit-code').disabled = false;
- $('btn-submit-code').textContent = 'Connect';
- return;
- }
-
- dialog.remove();
- authState.adobe = true;
- renderAuthBar();
- await refreshTemplates();
- } catch (e) {
- $('dialog-error').textContent = 'Error: ' + e.message;
- $('btn-submit-code').disabled = false;
- $('btn-submit-code').textContent = 'Connect';
- }
-}
-
-// ── Templates ────────────────────────────────────────────────────────────────
-
-async function refreshTemplates() {
- renderAdobeList([]);
- renderDsList([]);
-
- if (!authState.adobe || !authState.docusign) {
- setStatus(authState.adobe || authState.docusign
- ? 'Connect both platforms to see migration status.'
- : 'Connect Adobe Sign and DocuSign to get started.');
- $('btn-migrate').disabled = true;
- return;
- }
-
- setStatus('Loading templates…');
-
- try {
- const [statusResp, dsResp] = await Promise.all([
- fetch('/api/templates/status'),
- fetch('/api/templates/docusign'),
- ]);
- statusTemplates = (await statusResp.json()).templates || [];
- dsTemplates = (await dsResp.json()).templates || [];
- renderAdobeList(statusTemplates);
- renderDsList(dsTemplates);
- setStatus(`${statusTemplates.length} Adobe template(s) loaded.`);
- } catch (e) {
- setStatus('Error loading templates: ' + e.message);
- }
-}
-
-function renderAdobeList(items) {
- const ul = $('adobe-list');
- if (!items.length) {
- ul.innerHTML = 'No templates found.';
- return;
- }
- ul.innerHTML = items.map(t => `
-
-
- ${escHtml(t.name)}
- ${statusLabel(t.status)}
-
-
- `).join('');
-
- ul.querySelectorAll('.template-item').forEach(li => {
- li.addEventListener('click', e => {
- if (e.target.type === 'checkbox') return;
- const cb = li.querySelector('input[type=checkbox]');
- cb.checked = !cb.checked;
- li.classList.toggle('selected', cb.checked);
- updateMigrateButton();
- });
- li.querySelector('input').addEventListener('change', () => {
- li.classList.toggle('selected', li.querySelector('input').checked);
- updateMigrateButton();
- });
- });
-}
-
-function renderDsList(items) {
- const ul = $('ds-list');
- if (!items.length) {
- ul.innerHTML = 'No templates found.';
- return;
- }
- ul.innerHTML = items.map(t => `
-
- ${escHtml(t.name)}
- ${(t.lastModified || '').slice(0, 10)}
-
- `).join('');
-}
-
-function updateMigrateButton() {
- const checked = document.querySelectorAll('#adobe-list input[type=checkbox]:checked');
- $('btn-migrate').disabled = checked.length === 0;
-}
-
-// ── Migration ─────────────────────────────────────────────────────────────────
-
-async function onMigrate() {
- const checked = [...document.querySelectorAll('#adobe-list input[type=checkbox]:checked')];
- const ids = checked.map(cb => cb.dataset.id);
- if (!ids.length) return;
-
- $('btn-migrate').disabled = true;
- setStatus(`Migrating ${ids.length} template(s)…`);
-
- ids.forEach(id => {
- const spin = $('spin-' + id);
- if (spin) spin.textContent = '⏳';
- });
-
- try {
- const resp = await fetch('/api/migrate', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ adobe_template_ids: ids }),
- });
- const data = await resp.json();
-
- let successCount = 0;
- (data.results || []).forEach(r => {
- const spin = $('spin-' + r.adobe_template_id);
- if (r.status === 'success') {
- successCount++;
- if (spin) spin.textContent = r.action === 'updated' ? '✏️' : '✅';
- } else {
- if (spin) spin.textContent = '❌';
- }
- });
-
- setStatus(`Done: ${successCount}/${ids.length} succeeded.`);
- await refreshTemplates();
- await refreshHistory();
- } catch (e) {
- setStatus('Migration error: ' + e.message);
- }
-}
-
-// ── History ───────────────────────────────────────────────────────────────────
-
-async function refreshHistory() {
- try {
- const resp = await fetch('/api/migrate/history');
- const { history } = await resp.json();
- renderHistory(history || []);
- } catch {
- renderHistory([]);
- }
-}
-
-function renderHistory(records) {
- const tbody = $('history-tbody');
- if (!records.length) {
- tbody.innerHTML = '| No migrations yet. |
';
- return;
- }
- tbody.innerHTML = [...records].reverse().slice(0, 50).map(r => `
-
- | ${(r.timestamp || '').replace('T', ' ').slice(0, 19)} |
- ${escHtml(r.adobe_template_name || r.adobe_template_id || '')} |
- ${escHtml(r.docusign_template_id || '—')} |
- ${escHtml(r.action || '—')} |
-
-
- ${r.status}
-
- |
-
- `).join('');
-}
-
-// ── Utilities ─────────────────────────────────────────────────────────────────
-
-function setStatus(msg) { $('status-msg').textContent = msg; }
-
-function statusLabel(s) {
- return { not_migrated: 'Not Migrated', migrated: 'Migrated', needs_update: 'Needs Update' }[s] || s;
-}
-
-function escHtml(str) {
- return String(str)
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"');
-}
diff --git a/web/static/css/base.css b/web/static/css/base.css
new file mode 100644
index 0000000..af38890
--- /dev/null
+++ b/web/static/css/base.css
@@ -0,0 +1,279 @@
+/* Base reset, typography, and utility classes */
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+html, body {
+ height: 100%;
+ font-family: var(--font);
+ font-size: var(--font-size-base);
+ color: var(--text);
+ background: var(--page-bg);
+ -webkit-font-smoothing: antialiased;
+}
+
+body {
+ display: flex;
+ height: 100vh;
+ overflow: hidden;
+}
+
+/* ── Scrollbar ── */
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
+
+/* ── Buttons ── */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ border-radius: var(--radius-sm);
+ font-size: var(--font-size-base);
+ font-family: var(--font);
+ font-weight: 600;
+ cursor: pointer;
+ border: none;
+ transition: all 0.15s;
+ user-select: none;
+ white-space: nowrap;
+ line-height: 1;
+}
+.btn:disabled { opacity: 0.5; cursor: not-allowed; }
+.btn-primary { background: var(--cobalt); color: #fff; }
+.btn-primary:not(:disabled):hover { background: var(--cobalt-hover); }
+.btn-secondary { background: var(--card-bg); color: var(--text); border: 1px solid var(--border); }
+.btn-secondary:hover { background: var(--ecru); }
+.btn-ghost { background: transparent; color: var(--cobalt); padding: 6px 10px; }
+.btn-ghost:hover { background: var(--cobalt-light); }
+.btn-danger { background: var(--poppy); color: #fff; }
+.btn-danger:hover { background: #e04040; }
+.btn-sm { padding: 5px 10px; font-size: var(--font-size-sm); }
+.btn-xs { padding: 3px 8px; font-size: var(--font-size-xs); }
+.btn-icon { padding: 6px; border-radius: var(--radius-sm); }
+
+/* ── Badges ── */
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+ white-space: nowrap;
+}
+.badge-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
+.badge-green { background: var(--success-bg); color: var(--success); }
+.badge-amber { background: var(--warning-bg); color: var(--warning); }
+.badge-red { background: var(--error-bg); color: var(--error); }
+.badge-blue { background: var(--cobalt-light); color: var(--cobalt); }
+.badge-gray { background: #EDF0F4; color: var(--slate); }
+
+/* ── Cards ── */
+.card {
+ background: var(--card-bg);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border);
+ box-shadow: var(--shadow-sm);
+ margin-bottom: var(--space-md);
+}
+.card-header {
+ padding: 14px 20px;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+.card-title { font-size: var(--font-size-md); font-weight: 700; }
+.card-body { padding: var(--space-md) 20px; }
+
+/* ── Tables ── */
+.table-wrap { overflow-x: auto; }
+table { width: 100%; border-collapse: collapse; }
+th {
+ text-align: left;
+ padding: 10px 14px;
+ font-size: var(--font-size-xs);
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--text-muted);
+ border-bottom: 1px solid var(--border);
+ background: #FAFBFC;
+ white-space: nowrap;
+}
+td {
+ padding: 11px 14px;
+ border-bottom: 1px solid var(--border);
+ font-size: var(--font-size-base);
+ vertical-align: middle;
+}
+tr:last-child td { border-bottom: none; }
+tr:hover td { background: #FAFBFC; }
+
+/* ── Page layout ── */
+.page-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin-bottom: 20px;
+}
+.page-title { font-size: var(--font-size-xl); font-weight: 700; color: var(--text); }
+.page-subtitle { font-size: var(--font-size-base); color: var(--text-muted); margin-top: 2px; }
+.page-actions { display: flex; gap: var(--space-sm); align-items: center; }
+
+/* ── Callouts ── */
+.callout {
+ padding: 12px 16px;
+ border-radius: var(--radius-sm);
+ font-size: var(--font-size-base);
+ margin-bottom: var(--space-md);
+ display: flex;
+ gap: 10px;
+ align-items: flex-start;
+}
+.callout-icon { font-size: 16px; flex-shrink: 0; }
+.callout.info { background: var(--cobalt-light); border: 1px solid #B3D4FF; color: #0052A3; }
+.callout.warn { background: var(--warning-bg); border: 1px solid #FFD280; color: #7A3E00; }
+.callout.success { background: var(--success-bg); border: 1px solid #B3E8D5; color: #006644; }
+.callout.error { background: var(--error-bg); border: 1px solid #FFB3B3; color: #8B0000; }
+
+/* ── Tabs ── */
+.tabs {
+ display: flex;
+ border-bottom: 2px solid var(--border);
+ margin-bottom: 20px;
+ gap: 0;
+}
+.tab {
+ padding: 10px 18px;
+ font-size: var(--font-size-base);
+ font-weight: 600;
+ cursor: pointer;
+ color: var(--text-muted);
+ border-bottom: 2px solid transparent;
+ margin-bottom: -2px;
+ transition: all 0.1s;
+ user-select: none;
+}
+.tab:hover { color: var(--text); }
+.tab.active { color: var(--cobalt); border-bottom-color: var(--cobalt); }
+
+/* ── Divider ── */
+.divider { height: 1px; background: var(--border); margin: var(--space-md) 0; }
+
+/* ── Misc utilities ── */
+.mono { font-family: var(--font-mono); font-size: var(--font-size-sm); background: var(--ecru); padding: 1px 6px; border-radius: 3px; }
+.tag { display: inline-block; padding: 1px 7px; border-radius: var(--radius-sm); font-size: var(--font-size-xs); font-weight: 600; background: var(--ecru); color: var(--text-muted); margin-right: 4px; }
+.cb { width: 15px; height: 15px; accent-color: var(--cobalt); cursor: pointer; flex-shrink: 0; }
+.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
+
+/* ── Empty state ── */
+.empty-state {
+ text-align: center;
+ padding: 48px 24px;
+ color: var(--text-muted);
+}
+.empty-state-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.5; }
+.empty-state-title { font-size: var(--font-size-lg); font-weight: 700; margin-bottom: 6px; color: var(--text); }
+.empty-state-sub { font-size: var(--font-size-base); }
+
+/* ── Spinner ── */
+@keyframes spin { to { transform: rotate(360deg); } }
+.spinner {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--border);
+ border-top-color: var(--cobalt);
+ border-radius: 50%;
+ animation: spin 0.7s linear infinite;
+ flex-shrink: 0;
+}
+.spinner-sm { width: 12px; height: 12px; border-width: 1.5px; }
+
+/* ── Progress bar ── */
+.progress-wrap { margin-bottom: var(--space-lg); }
+.progress-label { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: var(--font-size-sm); color: var(--text-muted); }
+.progress-bar { height: 8px; border-radius: 4px; background: var(--border); overflow: hidden; }
+.progress-fill { height: 100%; background: var(--cobalt); border-radius: 4px; transition: width 0.4s ease; }
+.progress-fill.green { background: var(--success); }
+.progress-fill.amber { background: var(--warning-amber); }
+
+/* ── Toggle switch ── */
+.toggle {
+ width: 36px;
+ height: 20px;
+ background: var(--border);
+ border-radius: 10px;
+ cursor: pointer;
+ position: relative;
+ flex-shrink: 0;
+ transition: background 0.2s;
+ border: none;
+}
+.toggle.on { background: var(--cobalt); }
+.toggle::after {
+ content: '';
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ background: #fff;
+ border-radius: 50%;
+ top: 3px;
+ left: 3px;
+ transition: left 0.2s;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+}
+.toggle.on::after { left: 19px; }
+
+/* ── Stat cards grid ── */
+.stat-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin-bottom: 24px; }
+.stat-card {
+ background: var(--card-bg);
+ border-radius: var(--radius-md);
+ padding: 16px 18px;
+ border: 1px solid var(--border);
+ box-shadow: var(--shadow-sm);
+ cursor: pointer;
+ transition: box-shadow 0.15s;
+}
+.stat-card:hover { box-shadow: var(--shadow-md); }
+.stat-label { font-size: var(--font-size-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 8px; }
+.stat-value { font-size: 28px; font-weight: 800; line-height: 1; margin-bottom: 4px; }
+.stat-sub { font-size: var(--font-size-xs); color: var(--text-muted); }
+.stat-card.blue .stat-value { color: var(--cobalt); }
+.stat-card.green .stat-value { color: var(--success); }
+.stat-card.amber .stat-value { color: var(--warning); }
+.stat-card.red .stat-value { color: var(--error); }
+.stat-card.gray .stat-value { color: var(--slate); }
+
+/* ── Two/three-col layouts ── */
+.two-col { display: grid; grid-template-columns: 1fr 320px; gap: var(--space-md); align-items: start; }
+.three-col { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: var(--space-md); }
+
+/* ── Avatar ── */
+.avatar {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ background: var(--cobalt);
+ color: #fff;
+ font-size: var(--font-size-sm);
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+/* ── Responsive ── */
+@media (max-width: 900px) {
+ .stat-grid { grid-template-columns: repeat(3, 1fr); }
+ .two-col { grid-template-columns: 1fr; }
+}
+@media (max-width: 600px) {
+ .stat-grid { grid-template-columns: repeat(2, 1fr); }
+}
diff --git a/web/static/css/cards.css b/web/static/css/cards.css
new file mode 100644
index 0000000..4ed1998
--- /dev/null
+++ b/web/static/css/cards.css
@@ -0,0 +1,228 @@
+/* Template cards, readiness badges, filter bar, bulk toolbar */
+
+/* ── Readiness badges (extend base .badge) ── */
+.badge-ready { background: var(--success-bg); color: var(--success); }
+.badge-caveats { background: var(--warning-bg); color: var(--warning); }
+.badge-blocked { background: var(--error-bg); color: var(--error); }
+.badge-migrated { background: var(--cobalt-light); color: var(--cobalt); }
+.badge-needs-update { background: var(--warning-bg); color: var(--warning); }
+.badge-verified { background: var(--success-bg); color: var(--success); }
+.badge-not-migrated { background: #EDF0F4; color: var(--slate); }
+.badge-dry-run { background: #EDF0F4; color: var(--slate); }
+.badge-skipped { background: #EDF0F4; color: var(--slate); }
+.badge-error { background: var(--error-bg); color: var(--error); }
+
+/* ── Table name cell ── */
+.table-name {
+ font-weight: 600;
+ color: var(--text);
+ cursor: pointer;
+}
+.table-name:hover { color: var(--cobalt); }
+.table-sub {
+ font-size: var(--font-size-xs);
+ color: var(--text-muted);
+ margin-top: 2px;
+}
+
+/* ── Issue count cell ── */
+.issue-count {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+}
+.issue-count.has-issues { color: var(--warning); cursor: pointer; }
+.issue-count.no-issues { color: var(--success); }
+.issue-count.blocked { color: var(--error); }
+
+/* ── Filter bar ── */
+.filter-bar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: var(--space-md);
+ flex-wrap: wrap;
+}
+.search-input {
+ flex: 1;
+ min-width: 200px;
+ max-width: 320px;
+ padding: 7px 12px 7px 32px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ font-size: var(--font-size-base);
+ font-family: var(--font);
+ background: var(--card-bg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236B5F8A' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E") no-repeat 10px center;
+ outline: none;
+ color: var(--text);
+}
+.search-input:focus { border-color: var(--cobalt); }
+.search-input::placeholder { color: var(--text-muted); }
+
+/* ── Filter tabs ── */
+.filter-tabs {
+ display: flex;
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+}
+.filter-tab {
+ padding: 7px 14px;
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ cursor: pointer;
+ color: var(--text-muted);
+ border-right: 1px solid var(--border);
+ white-space: nowrap;
+ transition: all 0.1s;
+ user-select: none;
+ background: transparent;
+ border-top: none;
+ border-bottom: none;
+}
+.filter-tab:last-child { border-right: none; }
+.filter-tab:hover { background: var(--ecru); }
+.filter-tab.active { background: var(--cobalt); color: #fff; border-color: var(--cobalt); }
+.tab-count { font-size: 10px; margin-left: 4px; opacity: 0.8; }
+
+/* ── Bulk action toolbar ── */
+.bulk-bar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 16px;
+ background: var(--cobalt-light);
+ border: 1px solid var(--cobalt);
+ border-radius: var(--radius-sm);
+ margin-bottom: 12px;
+}
+.bulk-bar-text {
+ font-size: var(--font-size-base);
+ font-weight: 600;
+ color: var(--cobalt);
+ flex: 1;
+}
+.bulk-bar.hidden { display: none; }
+
+/* ── Template row action buttons ── */
+.row-actions { display: flex; gap: 6px; align-items: center; }
+
+/* ── Stat progress bar (dashboard) ── */
+.migration-progress-bar {
+ height: 6px;
+ border-radius: 3px;
+ background: var(--border);
+ overflow: hidden;
+ margin-top: 6px;
+}
+.migration-progress-fill {
+ height: 100%;
+ background: var(--cobalt);
+ border-radius: 3px;
+ transition: width 0.4s;
+}
+
+/* ── Attention items (issues view) ── */
+.attention-list { display: flex; flex-direction: column; gap: 8px; }
+.attention-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 12px 16px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+}
+.attention-item.blocker { border-left: 3px solid var(--error); background: var(--error-bg); }
+.attention-item.warning { border-left: 3px solid var(--warning-amber); background: var(--warning-bg); }
+.attention-icon { font-size: 16px; flex-shrink: 0; }
+.attention-name { font-weight: 600; font-size: var(--font-size-base); }
+.attention-detail { font-size: var(--font-size-sm); color: var(--text-muted); margin-top: 2px; }
+.attention-action { margin-left: auto; flex-shrink: 0; }
+
+/* ── Issue rows (template detail) ── */
+.issue-row {
+ display: flex;
+ gap: 14px;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--border);
+ align-items: flex-start;
+}
+.issue-row:last-child { border-bottom: none; }
+.issue-severity {
+ font-size: var(--font-size-xs);
+ font-weight: 700;
+ padding: 2px 8px;
+ border-radius: 12px;
+ flex-shrink: 0;
+ margin-top: 1px;
+}
+.issue-severity.blocker { background: var(--error-bg); color: var(--error); }
+.issue-severity.warn { background: var(--warning-bg); color: var(--warning); }
+.issue-severity.info { background: var(--cobalt-light); color: var(--cobalt); }
+.issue-body { flex: 1; }
+.issue-title { font-weight: 600; font-size: var(--font-size-base); }
+.issue-desc { font-size: var(--font-size-sm); color: var(--text-muted); margin-top: 3px; line-height: 1.5; }
+.issue-fix { font-size: var(--font-size-xs); margin-top: 6px; padding: 4px 10px; background: var(--ecru); border-radius: var(--radius-sm); color: var(--text); display: inline-block; }
+
+/* ── Result rows (migration results view) ── */
+.result-row {
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ margin-bottom: 8px;
+ overflow: hidden;
+}
+.result-header {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 12px 16px;
+ cursor: pointer;
+ background: var(--card-bg);
+ transition: background 0.1s;
+}
+.result-header:hover { background: #FAFBFC; }
+.result-icon { font-size: 16px; flex-shrink: 0; }
+.result-name { font-weight: 600; flex: 1; }
+.result-meta { font-size: var(--font-size-xs); color: var(--text-muted); }
+.result-body { padding: 12px 16px; border-top: 1px solid var(--border); background: #FAFBFC; display: none; }
+.result-row.open .result-body { display: block; }
+
+/* ── DS template link pill ── */
+.ds-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ background: #EAF2FF;
+ border: 1px solid #B3D4FF;
+ border-radius: var(--radius-pill);
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+ color: var(--cobalt);
+}
+
+/* ── Activity list (dashboard) ── */
+.activity-item {
+ display: flex;
+ gap: 12px;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--border);
+ align-items: flex-start;
+}
+.activity-item:last-child { border-bottom: none; }
+.activity-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-top: 5px;
+ flex-shrink: 0;
+}
+.activity-dot.green { background: var(--success); }
+.activity-dot.amber { background: var(--warning-amber); }
+.activity-dot.red { background: var(--error); }
+.activity-dot.blue { background: var(--cobalt); }
+.activity-text { font-size: var(--font-size-base); flex: 1; }
+.activity-time { font-size: var(--font-size-xs); color: var(--text-muted); flex-shrink: 0; }
diff --git a/web/static/css/forms.css b/web/static/css/forms.css
new file mode 100644
index 0000000..3586581
--- /dev/null
+++ b/web/static/css/forms.css
@@ -0,0 +1,107 @@
+/* Form input styles — used in settings and modals */
+
+.form-group {
+ margin-bottom: var(--space-md);
+}
+.form-label {
+ display: block;
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 6px;
+}
+.form-label-sub {
+ font-size: var(--font-size-xs);
+ color: var(--text-muted);
+ font-weight: 400;
+ margin-left: 4px;
+}
+.form-input {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ font-size: var(--font-size-base);
+ font-family: var(--font);
+ color: var(--text);
+ background: var(--card-bg);
+ outline: none;
+ transition: border-color 0.15s;
+}
+.form-input:focus { border-color: var(--cobalt); box-shadow: 0 0 0 3px rgba(76,0,255,0.08); }
+.form-input:disabled { background: var(--ecru); color: var(--text-muted); cursor: not-allowed; }
+.form-input.error { border-color: var(--error); }
+.form-input-mono { font-family: var(--font-mono); font-size: var(--font-size-sm); }
+
+.form-hint {
+ font-size: var(--font-size-xs);
+ color: var(--text-muted);
+ margin-top: 4px;
+}
+.form-error {
+ font-size: var(--font-size-xs);
+ color: var(--error);
+ margin-top: 4px;
+ min-height: 16px;
+}
+
+/* ── Toggle setting row ── */
+.setting-row {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 14px 0;
+ border-bottom: 1px solid var(--border);
+}
+.setting-row:last-child { border-bottom: none; }
+.setting-body { flex: 1; }
+.setting-label { font-weight: 600; font-size: var(--font-size-base); }
+.setting-desc { font-size: var(--font-size-sm); color: var(--text-muted); margin-top: 2px; line-height: 1.5; }
+.setting-control { flex-shrink: 0; }
+
+/* ── Settings section ── */
+.settings-section {
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ margin-bottom: var(--space-md);
+ overflow: hidden;
+}
+.settings-section-header {
+ padding: 14px 20px;
+ border-bottom: 1px solid var(--border);
+ background: #FAFBFC;
+}
+.settings-section-title {
+ font-size: var(--font-size-md);
+ font-weight: 700;
+}
+.settings-section-sub {
+ font-size: var(--font-size-sm);
+ color: var(--text-muted);
+ margin-top: 2px;
+}
+.settings-section-body { padding: 6px 20px; }
+
+/* ── Connection info card ── */
+.conn-info-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--border);
+ font-size: var(--font-size-base);
+}
+.conn-info-row:last-child { border-bottom: none; }
+.conn-info-label { width: 160px; color: var(--text-muted); font-size: var(--font-size-sm); flex-shrink: 0; }
+.conn-info-value { flex: 1; font-family: var(--font-mono); font-size: var(--font-size-sm); }
+.conn-info-status { flex-shrink: 0; }
+
+/* ── Number input ── */
+input[type="number"].form-input {
+ -moz-appearance: textfield;
+}
+input[type="number"].form-input::-webkit-outer-spin-button,
+input[type="number"].form-input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+}
diff --git a/web/static/css/modals.css b/web/static/css/modals.css
new file mode 100644
index 0000000..9e52c20
--- /dev/null
+++ b/web/static/css/modals.css
@@ -0,0 +1,181 @@
+/* Modal and dialog styles */
+
+/* ── Overlay ── */
+.modal-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(19, 0, 50, 0.5);
+ z-index: 200;
+ animation: backdropIn 0.15s ease;
+}
+@keyframes backdropIn { from { opacity: 0; } to { opacity: 1; } }
+
+/* ── Modal box ── */
+.modal-box {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: var(--card-bg);
+ border-radius: var(--radius-md);
+ width: min(520px, 94vw);
+ max-height: 90vh;
+ display: flex;
+ flex-direction: column;
+ z-index: 201;
+ box-shadow: var(--shadow-md);
+ animation: modalIn 0.18s ease;
+}
+@keyframes modalIn {
+ from { opacity: 0; transform: translate(-50%, -52%); }
+ to { opacity: 1; transform: translate(-50%, -50%); }
+}
+
+.modal-box.modal-lg { width: min(720px, 94vw); }
+.modal-box.modal-sm { width: min(380px, 94vw); }
+
+/* ── Modal sections ── */
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+.modal-title {
+ font-size: var(--font-size-md);
+ font-weight: 700;
+ color: var(--text);
+}
+.modal-body {
+ padding: 20px;
+ overflow-y: auto;
+ flex: 1;
+}
+.modal-footer {
+ padding: 14px 20px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--space-sm);
+ flex-shrink: 0;
+ background: var(--ecru);
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
+}
+
+/* ── Close button ── */
+.modal-close {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ color: var(--text-muted);
+ font-size: 18px;
+ padding: 2px 6px;
+ border-radius: var(--radius-sm);
+ line-height: 1;
+ transition: background 0.1s;
+}
+.modal-close:hover { background: var(--ecru); color: var(--text); }
+
+/* ── Options panel inside modal ── */
+.options-panel {
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 18px;
+ margin-bottom: var(--space-md);
+}
+.options-title {
+ font-weight: 700;
+ font-size: var(--font-size-md);
+ margin-bottom: 14px;
+}
+.option-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--border);
+}
+.option-row:last-child { border-bottom: none; }
+.option-label { font-weight: 600; font-size: var(--font-size-base); }
+.option-desc { font-size: var(--font-size-sm); color: var(--text-muted); margin-top: 2px; }
+.option-body { flex: 1; }
+
+/* ── Progress inside modal ── */
+.migration-progress {
+ padding: 8px 0;
+}
+.progress-template-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ max-height: 280px;
+ overflow-y: auto;
+ margin-top: 12px;
+}
+.progress-template-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ background: var(--ecru);
+ border-radius: var(--radius-sm);
+ font-size: var(--font-size-base);
+}
+.progress-template-name { flex: 1; font-weight: 500; }
+.progress-template-status { font-size: 16px; flex-shrink: 0; }
+
+/* ── Project switcher modal ── */
+.project-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 16px;
+ max-height: 280px;
+ overflow-y: auto;
+}
+.project-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 14px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: background 0.1s;
+}
+.project-row:hover { background: var(--ecru); }
+.project-row.active {
+ border-color: var(--cobalt);
+ background: var(--cobalt-light);
+}
+.project-row-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-sm);
+ background: var(--cobalt);
+ color: #fff;
+ font-size: var(--font-size-sm);
+ font-weight: 800;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+.project-row-name { font-weight: 600; font-size: var(--font-size-base); flex: 1; }
+.project-row-sub { font-size: var(--font-size-xs); color: var(--text-muted); }
+.project-row-active-badge { font-size: var(--font-size-xs); color: var(--cobalt); font-weight: 700; }
+
+/* ── New project form inside modal ── */
+.new-project-form {
+ border-top: 1px solid var(--border);
+ padding-top: 16px;
+ margin-top: 8px;
+}
+.new-project-form h4 {
+ font-size: var(--font-size-base);
+ font-weight: 700;
+ margin-bottom: 10px;
+}
diff --git a/web/static/css/nav.css b/web/static/css/nav.css
new file mode 100644
index 0000000..1657695
--- /dev/null
+++ b/web/static/css/nav.css
@@ -0,0 +1,236 @@
+/* Left sidebar navigation and top bar */
+
+/* ── App layout shell ── */
+#app-nav {
+ width: var(--nav-width);
+ background: var(--nav-bg);
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ height: 100vh;
+ position: fixed;
+ left: 0;
+ top: 0;
+ z-index: 50;
+}
+
+#app-body {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ margin-left: var(--nav-width);
+ height: 100vh;
+}
+
+/* ── Logo area ── */
+#nav-logo {
+ padding: 14px 20px 12px;
+ border-bottom: 1px solid rgba(255,255,255,0.08);
+}
+#nav-logo svg { display: block; }
+.nav-logo-sub {
+ font-size: var(--font-size-xs);
+ color: var(--nav-text);
+ font-weight: 500;
+ letter-spacing: 0.02em;
+ margin-top: 6px;
+}
+
+/* ── Project switcher ── */
+#nav-project-switcher {
+ background: rgba(255,255,255,0.06);
+ border: 1px solid rgba(255,255,255,0.12);
+ border-radius: 6px;
+ padding: 8px 10px;
+ cursor: pointer;
+ transition: background 0.15s;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 8px;
+ width: 100%;
+}
+#nav-project-switcher:hover { background: rgba(255,255,255,0.10); }
+
+.project-icon {
+ width: 24px;
+ height: 24px;
+ border-radius: 4px;
+ background: var(--cobalt);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--font-size-xs);
+ font-weight: 800;
+ color: #fff;
+ flex-shrink: 0;
+}
+.project-name {
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ color: #fff;
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.project-arrow {
+ font-size: 10px;
+ color: var(--nav-text);
+ flex-shrink: 0;
+}
+.project-name.no-project { color: var(--warning-amber); }
+
+/* ── Nav sections ── */
+.nav-section-label {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: rgba(189,201,217,0.5);
+ padding: 16px 20px 6px;
+}
+
+#nav-links { list-style: none; flex: 1; overflow-y: auto; }
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 9px 20px;
+ color: var(--nav-text);
+ cursor: pointer;
+ border-left: 3px solid transparent;
+ transition: all 0.15s;
+ font-size: var(--font-size-base);
+ user-select: none;
+ text-decoration: none;
+}
+.nav-item:hover {
+ background: var(--nav-hover);
+ color: var(--nav-text-active);
+}
+.nav-item.active {
+ background: var(--nav-active-bg);
+ color: var(--nav-text-active);
+ border-left-color: var(--nav-active-border);
+}
+.nav-icon {
+ font-size: 16px;
+ width: 20px;
+ text-align: center;
+ flex-shrink: 0;
+}
+.nav-label { flex: 1; }
+.nav-badge {
+ margin-left: auto;
+ background: var(--poppy);
+ color: #fff;
+ border-radius: 10px;
+ font-size: 10px;
+ font-weight: 700;
+ padding: 1px 6px;
+ min-width: 18px;
+ text-align: center;
+}
+.nav-badge.amber { background: var(--warning-amber); }
+.nav-badge[data-count="0"] { display: none; }
+
+/* ── Nav bottom (customer context) ── */
+#nav-bottom {
+ margin-top: auto;
+ padding: 12px 0;
+ border-top: 1px solid rgba(255,255,255,0.08);
+}
+.nav-customer { padding: 10px 20px; }
+.nav-customer-label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: rgba(189,201,217,0.5);
+ margin-bottom: 4px;
+}
+.nav-customer-name { font-size: var(--font-size-sm); font-weight: 600; color: var(--nav-text-active); }
+.nav-customer-sub { font-size: var(--font-size-xs); color: var(--nav-text); }
+
+/* ── Top bar ── */
+#top-bar {
+ height: var(--topbar-h);
+ background: var(--card-bg);
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ padding: 0 var(--space-lg);
+ gap: var(--space-md);
+ flex-shrink: 0;
+ box-shadow: var(--shadow-sm);
+ z-index: 10;
+}
+
+.breadcrumb {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: var(--font-size-base);
+ color: var(--text-muted);
+}
+.breadcrumb .sep { color: var(--border); }
+.breadcrumb .current { color: var(--text); font-weight: 600; }
+
+#topbar-right {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.conn-pill {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ border-radius: var(--radius-pill);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ border: 1px solid var(--border);
+ cursor: pointer;
+ transition: background 0.15s;
+ background: var(--card-bg);
+}
+.conn-pill:hover { background: var(--ecru); }
+.conn-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+.conn-pill.connected .conn-dot { background: var(--success); }
+.conn-pill.disconnected .conn-dot { background: var(--error); }
+.conn-pill.connecting .conn-dot { background: var(--warning-amber); }
+
+/* ── Router outlet ── */
+#router-outlet {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-lg);
+}
+
+/* ── View transitions ── */
+.view-enter {
+ animation: fadeIn 0.18s ease;
+}
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ── Mobile nav toggle ── */
+@media (max-width: 768px) {
+ #app-nav {
+ transform: translateX(-100%);
+ transition: transform 0.2s;
+ }
+ #app-nav.open { transform: translateX(0); }
+ #app-body { margin-left: 0; }
+}
diff --git a/web/static/css/tables.css b/web/static/css/tables.css
new file mode 100644
index 0000000..2b9a765
--- /dev/null
+++ b/web/static/css/tables.css
@@ -0,0 +1,82 @@
+/* History and audit table styles */
+
+/* ── Sortable column headers ── */
+th.sortable {
+ cursor: pointer;
+ user-select: none;
+}
+th.sortable:hover { background: #F0F1F5; }
+th.sortable::after {
+ content: ' ⇅';
+ font-size: 10px;
+ opacity: 0.5;
+}
+th.sort-asc::after { content: ' ↑'; opacity: 1; color: var(--cobalt); }
+th.sort-desc::after { content: ' ↓'; opacity: 1; color: var(--cobalt); }
+
+/* ── Expandable row ── */
+.row-expandable { cursor: pointer; }
+.row-expanded-content {
+ background: #FAFBFC;
+}
+.row-expand-body {
+ padding: 12px 14px 14px;
+ font-size: var(--font-size-sm);
+ color: var(--text-muted);
+ border-top: 1px solid var(--border);
+}
+.expand-icon { font-size: 10px; color: var(--text-muted); transition: transform 0.15s; }
+tr.open .expand-icon { transform: rotate(90deg); }
+
+/* ── Checksum display ── */
+.checksum {
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ background: var(--ecru);
+ padding: 2px 6px;
+ border-radius: 3px;
+ cursor: help;
+}
+
+/* ── Date range filter ── */
+.date-filter {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: var(--font-size-sm);
+}
+.date-input {
+ padding: 6px 10px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ font-size: var(--font-size-sm);
+ font-family: var(--font);
+ color: var(--text);
+ background: var(--card-bg);
+ outline: none;
+}
+.date-input:focus { border-color: var(--cobalt); }
+
+/* ── Pagination ── */
+.pagination {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 14px;
+ border-top: 1px solid var(--border);
+ font-size: var(--font-size-sm);
+ color: var(--text-muted);
+}
+.pagination-pages { display: flex; gap: 4px; }
+.page-btn {
+ padding: 4px 10px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+ background: var(--card-bg);
+ cursor: pointer;
+ font-size: var(--font-size-sm);
+ transition: background 0.1s;
+}
+.page-btn:hover { background: var(--ecru); }
+.page-btn.active { background: var(--cobalt); color: #fff; border-color: var(--cobalt); }
+.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
diff --git a/web/static/css/tokens.css b/web/static/css/tokens.css
new file mode 100644
index 0000000..48cb66b
--- /dev/null
+++ b/web/static/css/tokens.css
@@ -0,0 +1,70 @@
+/* Docusign 2024 brand design tokens
+ Source: brand.docusign.com (April 2024 rebrand)
+ Inkwell #130032 replaces old navy; Cobalt #4C00FF is new primary.
+*/
+:root {
+ /* ── Brand palette ── */
+ --cobalt: #4C00FF;
+ --cobalt-hover: #3A00CC;
+ --cobalt-light: #EDE8FF;
+ --inkwell: #130032;
+ --deep-violet: #26065D;
+ --mist: #CBC2FF;
+ --ecru: #F8F3F0;
+ --poppy: #FF5252;
+ --poppy-bg: #FFF0F0;
+ --slate: #6B5F8A;
+
+ /* ── Semantic colours ── */
+ --success: #027A48;
+ --success-bg: #ECFDF3;
+ --warning: #92400E;
+ --warning-bg: #FFFBEB;
+ --warning-amber:#FFAB00;
+ --error: var(--poppy);
+ --error-bg: var(--poppy-bg);
+
+ /* ── Nav ── */
+ --nav-bg: var(--inkwell);
+ --nav-hover: var(--deep-violet);
+ --nav-active-bg: rgba(76,0,255,0.22);
+ --nav-active-border:var(--cobalt);
+ --nav-text: #A899CC;
+ --nav-text-active: #FFFFFF;
+ --nav-width: 228px;
+
+ /* ── Layout ── */
+ --topbar-h: 56px;
+ --page-bg: var(--ecru);
+ --card-bg: #FFFFFF;
+ --border: #E2DDF0;
+ --text: var(--inkwell);
+ --text-muted: var(--slate);
+
+ /* ── Shadows ── */
+ --shadow-sm: 0 1px 3px rgba(19,0,50,0.08), 0 1px 2px rgba(19,0,50,0.04);
+ --shadow-md: 0 4px 16px rgba(19,0,50,0.14);
+
+ /* ── Spacing ── */
+ --space-xs: 4px;
+ --space-sm: 8px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+
+ /* ── Border radius ── */
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-pill: 20px;
+
+ /* ── Typography ── */
+ --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
+ --font-size-xs: 11px;
+ --font-size-sm: 12px;
+ --font-size-base:13px;
+ --font-size-md: 14px;
+ --font-size-lg: 16px;
+ --font-size-xl: 20px;
+}
diff --git a/web/static/index.html b/web/static/index.html
index dbcb4c1..3538791 100644
--- a/web/static/index.html
+++ b/web/static/index.html
@@ -3,77 +3,164 @@
- Adobe Sign → DocuSign Migrator
-
+ docusign — Template Migration Console
+
+
+
+
+
+
+
-
+
+