feat(admin): complete revision review and rollback workflow

This commit is contained in:
Christian Johannesen 2026-10-18 16:10:01 +02:00
parent c802f83bc0
commit b9a9c85e60
3 changed files with 14 additions and 1 deletions

View file

@ -26,13 +26,24 @@ function AdminWorkspace({ csrf }: { csrf: string }) {
const review = useMutation({ mutationFn: ({ id, action }: { id: string; action: string }) => api.mutate(`/findings/${id}/${action}`, csrf), onSuccess: () => client.invalidateQueries({ queryKey: ['findings'] }) }); const review = useMutation({ mutationFn: ({ id, action }: { id: string; action: string }) => api.mutate(`/findings/${id}/${action}`, csrf), onSuccess: () => client.invalidateQueries({ queryKey: ['findings'] }) });
return <section className="admin-page"><header><div><p className="kicker">Kontrolrum</p><h1>Redaktion</h1><p>Ingen ændringer bliver publiceret uden et menneskeligt review.</p></div><button className="secondary-button" onClick={() => poll.mutate()} disabled={poll.isPending}><RefreshCw size={16}/>Scan officielle kilder</button></header> return <section className="admin-page"><header><div><p className="kicker">Kontrolrum</p><h1>Redaktion</h1><p>Ingen ændringer bliver publiceret uden et menneskeligt review.</p></div><button className="secondary-button" onClick={() => poll.mutate()} disabled={poll.isPending}><RefreshCw size={16}/>Scan officielle kilder</button></header>
<div className="admin-stats"><div><Activity/><span><strong>{findings.data?.filter((item) => item.status === 'pending').length ?? 0}</strong>Afventer review</span></div><div><ShieldCheck/><span><strong>Manuel</strong>Publiceringspolitik</span></div><div><RefreshCw/><span><strong>24 timer</strong>Scanningsinterval</span></div></div> <div className="admin-stats"><div><Activity/><span><strong>{findings.data?.filter((item) => item.status === 'pending').length ?? 0}</strong>Afventer review</span></div><div><ShieldCheck/><span><strong>Manuel</strong>Publiceringspolitik</span></div><div><RefreshCw/><span><strong>24 timer</strong>Scanningsinterval</span></div></div>
<Tabs.Root defaultValue="findings" className="admin-tabs"><Tabs.List><Tabs.Trigger value="findings">Kildefund</Tabs.Trigger><Tabs.Trigger value="editor">Nyt udkast</Tabs.Trigger></Tabs.List> <Tabs.Root defaultValue="findings" className="admin-tabs"><Tabs.List><Tabs.Trigger value="findings">Kildefund</Tabs.Trigger><Tabs.Trigger value="editor">Nyt udkast</Tabs.Trigger><Tabs.Trigger value="revisions">Revisioner</Tabs.Trigger></Tabs.List>
<Tabs.Content value="findings"><div className="admin-list">{findings.isLoading && <p>Henter findings…</p>}{findings.data?.length === 0 && <div className="empty-state"><Check/><h2>Alt er gennemgået</h2><p>Der er ingen kildeændringer i kø.</p></div>}{findings.data?.map((finding) => <div className="finding" key={finding.id}><div><span className={`status ${finding.status}`}>{finding.status}</span><h3>{finding.title}</h3><p>{finding.summary}</p><a href={finding.source_url} target="_blank" rel="noreferrer">Åbn officiel kilde</a></div>{finding.status === 'pending' && <div className="finding-actions"><button onClick={() => review.mutate({ id: finding.id, action: 'reject' })}><X/>Afvis</button><button className="approve" onClick={() => review.mutate({ id: finding.id, action: 'approve' })}><Check/>Godkend fund</button></div>}</div>)}</div></Tabs.Content> <Tabs.Content value="findings"><div className="admin-list">{findings.isLoading && <p>Henter findings…</p>}{findings.data?.length === 0 && <div className="empty-state"><Check/><h2>Alt er gennemgået</h2><p>Der er ingen kildeændringer i kø.</p></div>}{findings.data?.map((finding) => <div className="finding" key={finding.id}><div><span className={`status ${finding.status}`}>{finding.status}</span><h3>{finding.title}</h3><p>{finding.summary}</p><a href={finding.source_url} target="_blank" rel="noreferrer">Åbn officiel kilde</a></div>{finding.status === 'pending' && <div className="finding-actions"><button onClick={() => review.mutate({ id: finding.id, action: 'reject' })}><X/>Afvis</button><button className="approve" onClick={() => review.mutate({ id: finding.id, action: 'approve' })}><Check/>Godkend fund</button></div>}</div>)}</div></Tabs.Content>
<Tabs.Content value="editor"><DraftEditor csrf={csrf}/></Tabs.Content> <Tabs.Content value="editor"><DraftEditor csrf={csrf}/></Tabs.Content>
<Tabs.Content value="revisions"><RevisionManager csrf={csrf}/></Tabs.Content>
</Tabs.Root> </Tabs.Root>
</section>; </section>;
} }
function RevisionManager({ csrf }: { csrf: string }) {
const [input, setInput] = useState('typescript'); const [slug, setSlug] = useState('typescript'); const client = useQueryClient();
const revisions = useQuery({ queryKey: ['revisions', slug], queryFn: () => api.revisions(slug), enabled: Boolean(slug) });
const action = useMutation({ mutationFn: ({ path, body }: { path: string; body?: unknown }) => api.mutate(path, csrf, body), onSuccess: () => { client.invalidateQueries({ queryKey: ['revisions', slug] }); client.invalidateQueries({ queryKey: ['topic', slug] }); } });
return <section className="revision-manager"><form onSubmit={(event) => { event.preventDefault(); setSlug(input.trim()); }}><label>Emnets slug<input value={input} onChange={(event) => setInput(event.target.value)} placeholder="typescript"/></label><button className="secondary-button">Hent revisioner</button></form>
{action.error && <p className="form-error">{action.error.message}</p>}
<div className="admin-list">{revisions.isLoading && <p>Henter revisioner…</p>}{revisions.isError && <p className="form-error">Emnet eller dets revisioner kunne ikke hentes.</p>}{revisions.data?.map((revision) => <div className="finding revision-row" key={revision.id}><div><span className={`status ${revision.status}`}>{revision.active ? 'publiceret' : revision.status}</span><h3>Revision #{revision.number}</h3><p>{revision.changeNote}</p><small>{new Date(revision.createdAt).toLocaleString('da-DK')}</small></div><div className="finding-actions">{revision.status === 'draft' && <><button onClick={() => action.mutate({ path: `/revisions/${revision.id}/reject` })}><X/>Afvis</button><button className="approve" onClick={() => action.mutate({ path: `/revisions/${revision.id}/approve` })}><Check/>Godkend</button></>}{revision.status === 'approved' && !revision.active && <><button className="approve" onClick={() => action.mutate({ path: `/revisions/${revision.id}/publish` })}>Publicér</button><button onClick={() => action.mutate({ path: `/topics/${slug}/rollback`, body: { revisionId: revision.id } })}>Aktivér som rollback</button></>}</div></div>)}</div>
</section>;
}
function DraftEditor({ csrf }: { csrf: string }) { function DraftEditor({ csrf }: { csrf: string }) {
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const create = useMutation({ mutationFn: (input: TopicRevisionInput) => api.createDraft(csrf, input), onSuccess: (result) => setMessage(`Udkast #${String(result.number)} blev oprettet. Revision-ID: ${String(result.id)}`) }); const create = useMutation({ mutationFn: (input: TopicRevisionInput) => api.createDraft(csrf, input), onSuccess: (result) => setMessage(`Udkast #${String(result.number)} blev oprettet. Revision-ID: ${String(result.id)}`) });

View file

@ -9,6 +9,7 @@ const json = async <T>(pending: Response | Promise<Response>): Promise<T> => {
export const api = { export const api = {
topics: (params: URLSearchParams) => json<Paginated<TopicListItem>>(fetch(`/api/v1/topics?${params}`)), topics: (params: URLSearchParams) => json<Paginated<TopicListItem>>(fetch(`/api/v1/topics?${params}`)),
topic: (slug: string) => json<TopicDetail>(fetch(`/api/v1/topics/${slug}`)), topic: (slug: string) => json<TopicDetail>(fetch(`/api/v1/topics/${slug}`)),
revisions: (slug: string) => json<Array<{ id: string; number: number; status: string; changeNote: string; createdAt: string; active: number }>>(fetch(`/api/v1/topics/${slug}/revisions`)),
facets: () => json<{ domains: Array<{ value: string; count: number }>; types: Array<{ value: string; count: number }> }>(fetch('/api/v1/facets')), facets: () => json<{ domains: Array<{ value: string; count: number }>; types: Array<{ value: string; count: number }> }>(fetch('/api/v1/facets')),
login: (username: string, password: string) => json<{ csrfToken: string }>(fetch('/api/v1/admin/login', { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'include', body: JSON.stringify({ username, password }) })), login: (username: string, password: string) => json<{ csrfToken: string }>(fetch('/api/v1/admin/login', { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'include', body: JSON.stringify({ username, password }) })),
session: () => json<{ csrfToken: string }>(fetch('/api/v1/admin/session', { credentials: 'include' })), session: () => json<{ csrfToken: string }>(fetch('/api/v1/admin/session', { credentials: 'include' })),

View file

@ -6,5 +6,6 @@
.library-layout{display:grid;grid-template-columns:230px 1fr;gap:55px;padding:58px clamp(20px,5vw,72px) 100px;max-width:1500px;margin:auto}.filter-title{display:flex;justify-content:space-between;align-items:center;font-weight:700;font-size:14px;padding-bottom:18px;border-bottom:1px solid var(--line)}.filter-title span,.filter-title button{display:flex;align-items:center;gap:7px}.filter-title button{border:0;background:none;font-size:11px;color:var(--muted);cursor:pointer}.filters fieldset{border:0;padding:22px 0 3px;margin:0;border-bottom:1px solid var(--line)}.filters legend{text-transform:uppercase;font:500 10px DM Mono;letter-spacing:.1em;margin-bottom:13px}.filters label{display:grid;grid-template-columns:19px 1fr auto;align-items:center;font-size:13px;padding:7px 0;cursor:pointer}.filters input{appearance:none;width:14px;height:14px;border:1px solid #a6aaa7;border-radius:3px;margin:0}.filters input:checked{border:4px solid var(--ink);background:var(--acid)}.filters small{color:#929790}.catalog-header{display:flex;justify-content:space-between;align-items:end;margin-bottom:28px}.kicker{text-transform:uppercase;letter-spacing:.12em;font:500 10px DM Mono;color:var(--coral);margin:0 0 8px}.catalog-header h2{font-size:32px;letter-spacing:-.035em;margin:0}.catalog-header>span{font:11px DM Mono;color:var(--muted)}.topic-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}.topic-card{background:var(--paper);border:1px solid var(--line);padding:22px;min-height:250px;text-decoration:none;display:flex;flex-direction:column;border-radius:4px;animation:rise .45s both;animation-delay:var(--delay)}.topic-card:hover{transform:translateY(-4px);box-shadow:0 18px 36px #25312916;border-color:#a8aea9}.card-top{display:flex;justify-content:space-between}.type-badge{font:500 10px DM Mono;text-transform:uppercase;color:var(--green);background:#e4eee7;padding:5px 7px;border-radius:3px}.topic-card h3{font-size:21px;letter-spacing:-.025em;margin:23px 0 9px}.topic-card p{font-size:13px;line-height:1.55;color:var(--muted);margin:0}.tag-row{margin-top:auto;display:flex;gap:5px;padding-top:18px}.tag-row span{font:10px DM Mono;background:#ebeae3;padding:4px 6px}.verified{font:9px DM Mono;color:#9b9d98;text-transform:uppercase;margin-top:12px}.state-card,.page-state{padding:55px;text-align:center;background:var(--paper);border:1px solid var(--line)}.state-card.error,.form-error{color:#a02e20}@keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} .library-layout{display:grid;grid-template-columns:230px 1fr;gap:55px;padding:58px clamp(20px,5vw,72px) 100px;max-width:1500px;margin:auto}.filter-title{display:flex;justify-content:space-between;align-items:center;font-weight:700;font-size:14px;padding-bottom:18px;border-bottom:1px solid var(--line)}.filter-title span,.filter-title button{display:flex;align-items:center;gap:7px}.filter-title button{border:0;background:none;font-size:11px;color:var(--muted);cursor:pointer}.filters fieldset{border:0;padding:22px 0 3px;margin:0;border-bottom:1px solid var(--line)}.filters legend{text-transform:uppercase;font:500 10px DM Mono;letter-spacing:.1em;margin-bottom:13px}.filters label{display:grid;grid-template-columns:19px 1fr auto;align-items:center;font-size:13px;padding:7px 0;cursor:pointer}.filters input{appearance:none;width:14px;height:14px;border:1px solid #a6aaa7;border-radius:3px;margin:0}.filters input:checked{border:4px solid var(--ink);background:var(--acid)}.filters small{color:#929790}.catalog-header{display:flex;justify-content:space-between;align-items:end;margin-bottom:28px}.kicker{text-transform:uppercase;letter-spacing:.12em;font:500 10px DM Mono;color:var(--coral);margin:0 0 8px}.catalog-header h2{font-size:32px;letter-spacing:-.035em;margin:0}.catalog-header>span{font:11px DM Mono;color:var(--muted)}.topic-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}.topic-card{background:var(--paper);border:1px solid var(--line);padding:22px;min-height:250px;text-decoration:none;display:flex;flex-direction:column;border-radius:4px;animation:rise .45s both;animation-delay:var(--delay)}.topic-card:hover{transform:translateY(-4px);box-shadow:0 18px 36px #25312916;border-color:#a8aea9}.card-top{display:flex;justify-content:space-between}.type-badge{font:500 10px DM Mono;text-transform:uppercase;color:var(--green);background:#e4eee7;padding:5px 7px;border-radius:3px}.topic-card h3{font-size:21px;letter-spacing:-.025em;margin:23px 0 9px}.topic-card p{font-size:13px;line-height:1.55;color:var(--muted);margin:0}.tag-row{margin-top:auto;display:flex;gap:5px;padding-top:18px}.tag-row span{font:10px DM Mono;background:#ebeae3;padding:4px 6px}.verified{font:9px DM Mono;color:#9b9d98;text-transform:uppercase;margin-top:12px}.state-card,.page-state{padding:55px;text-align:center;background:var(--paper);border:1px solid var(--line)}.state-card.error,.form-error{color:#a02e20}@keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.topic-page{max-width:1180px;margin:auto;padding:50px 30px 100px}.back-link{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-size:13px;text-decoration:none}.topic-hero{display:grid;grid-template-columns:1fr 280px;gap:60px;padding:70px 0 50px;border-bottom:1px solid var(--line)}.topic-hero h1{font-size:70px;margin:18px 0;color:var(--ink)}.topic-hero>div>p{font-size:19px;line-height:1.65;color:var(--muted);max-width:730px}.topic-hero dl{margin:0;background:var(--ink);color:white;padding:25px}.topic-hero dl div{padding:12px 0;border-bottom:1px solid #334039}.topic-hero dt{font:9px DM Mono;text-transform:uppercase;color:#8fa097;margin-bottom:6px}.topic-hero dd{margin:0;font-size:13px}.topic-tabs [role=tablist],.admin-tabs>[role=tablist]{display:flex;border-bottom:1px solid var(--line);margin:0 0 40px}.topic-tabs [role=tab],.admin-tabs [role=tab]{border:0;background:none;padding:20px 22px;cursor:pointer;color:var(--muted);border-bottom:2px solid transparent}.topic-tabs [data-state=active],.admin-tabs [data-state=active]{color:var(--ink);border-color:var(--coral)}.topic-tabs [role=tab] span{background:var(--ink);color:white;border-radius:10px;padding:1px 6px;font-size:10px}.prose-grid{display:grid;grid-template-columns:1fr 320px;gap:70px}.prose{max-width:760px}.prose h2,.sources h2{font-size:25px;letter-spacing:-.025em;margin:42px 0 13px}.prose .lead,.prose p{line-height:1.8;color:#4f5a54}.prose ul,.info-card ul{padding-left:20px;line-height:1.7;color:#4f5a54}.prose li{margin:7px 0}.prose pre{overflow:auto;background:var(--ink);color:#d8ff58;padding:22px;border-radius:4px}.info-card{background:#e8e9df;padding:24px;margin-bottom:16px}.info-title{display:flex;align-items:center;gap:9px}.info-title svg{width:18px}.info-title h3{font-size:15px}.info-card li{font-size:13px;margin:8px 0}.source-intro{display:flex;gap:15px;align-items:flex-start;margin-bottom:25px}.source-intro svg{background:var(--acid);padding:9px;width:42px;height:42px}.source-intro h2{margin:0}.source-intro p{color:var(--muted)}.source-card{display:flex;justify-content:space-between;text-decoration:none;padding:24px;border:1px solid var(--line);background:var(--paper);margin-bottom:10px}.source-card span{font:10px DM Mono;text-transform:uppercase;color:var(--coral)}.source-card h3{margin:8px 0}.source-card p{font-size:12px;color:var(--muted)}.source-card small{display:flex;align-items:center;gap:6px;color:var(--green)} .topic-page{max-width:1180px;margin:auto;padding:50px 30px 100px}.back-link{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-size:13px;text-decoration:none}.topic-hero{display:grid;grid-template-columns:1fr 280px;gap:60px;padding:70px 0 50px;border-bottom:1px solid var(--line)}.topic-hero h1{font-size:70px;margin:18px 0;color:var(--ink)}.topic-hero>div>p{font-size:19px;line-height:1.65;color:var(--muted);max-width:730px}.topic-hero dl{margin:0;background:var(--ink);color:white;padding:25px}.topic-hero dl div{padding:12px 0;border-bottom:1px solid #334039}.topic-hero dt{font:9px DM Mono;text-transform:uppercase;color:#8fa097;margin-bottom:6px}.topic-hero dd{margin:0;font-size:13px}.topic-tabs [role=tablist],.admin-tabs>[role=tablist]{display:flex;border-bottom:1px solid var(--line);margin:0 0 40px}.topic-tabs [role=tab],.admin-tabs [role=tab]{border:0;background:none;padding:20px 22px;cursor:pointer;color:var(--muted);border-bottom:2px solid transparent}.topic-tabs [data-state=active],.admin-tabs [data-state=active]{color:var(--ink);border-color:var(--coral)}.topic-tabs [role=tab] span{background:var(--ink);color:white;border-radius:10px;padding:1px 6px;font-size:10px}.prose-grid{display:grid;grid-template-columns:1fr 320px;gap:70px}.prose{max-width:760px}.prose h2,.sources h2{font-size:25px;letter-spacing:-.025em;margin:42px 0 13px}.prose .lead,.prose p{line-height:1.8;color:#4f5a54}.prose ul,.info-card ul{padding-left:20px;line-height:1.7;color:#4f5a54}.prose li{margin:7px 0}.prose pre{overflow:auto;background:var(--ink);color:#d8ff58;padding:22px;border-radius:4px}.info-card{background:#e8e9df;padding:24px;margin-bottom:16px}.info-title{display:flex;align-items:center;gap:9px}.info-title svg{width:18px}.info-title h3{font-size:15px}.info-card li{font-size:13px;margin:8px 0}.source-intro{display:flex;gap:15px;align-items:flex-start;margin-bottom:25px}.source-intro svg{background:var(--acid);padding:9px;width:42px;height:42px}.source-intro h2{margin:0}.source-intro p{color:var(--muted)}.source-card{display:flex;justify-content:space-between;text-decoration:none;padding:24px;border:1px solid var(--line);background:var(--paper);margin-bottom:10px}.source-card span{font:10px DM Mono;text-transform:uppercase;color:var(--coral)}.source-card h3{margin:8px 0}.source-card p{font-size:12px;color:var(--muted)}.source-card small{display:flex;align-items:center;gap:6px;color:var(--green)}
.admin-login{min-height:calc(100vh - 145px);display:grid;place-items:center;padding:40px}.login-panel{width:min(450px,100%);background:var(--paper);border:1px solid var(--line);padding:40px}.admin-icon{display:grid;place-items:center;background:var(--ink);color:var(--acid);width:50px;height:50px;margin-bottom:28px}.login-panel h1{font-size:35px;letter-spacing:-.04em;margin:5px 0}.login-panel>p{color:var(--muted);line-height:1.6}.login-panel form,.editor-form{display:grid;gap:16px;margin-top:28px}.login-panel label,.editor-form label{display:grid;gap:7px;font-size:12px;font-weight:600}.login-panel input,.editor-form input,.editor-form textarea,.editor-form select{border:1px solid #bec1bb;background:#fff;padding:12px;outline:0;border-radius:3px}.login-panel input:focus,.editor-form input:focus,.editor-form textarea:focus,.editor-form select:focus{border-color:var(--green);box-shadow:0 0 0 3px #264c3b18}.primary-button,.secondary-button,.finding-actions button{border:0;display:inline-flex;justify-content:center;align-items:center;gap:8px;padding:12px 16px;cursor:pointer;border-radius:3px}.primary-button{background:var(--ink);color:white}.primary-button:hover{background:var(--green)}.admin-page{max-width:1250px;margin:auto;padding:60px 30px 100px}.admin-page>header{display:flex;justify-content:space-between;align-items:end}.admin-page h1{font-size:48px;letter-spacing:-.05em;margin:4px 0}.admin-page header p{color:var(--muted)}.secondary-button{background:#e2e3db}.admin-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:40px 0}.admin-stats>div{display:flex;gap:14px;align-items:center;border:1px solid var(--line);background:var(--paper);padding:20px}.admin-stats svg{color:var(--coral)}.admin-stats span{display:grid;font-size:11px;color:var(--muted)}.admin-stats strong{font-size:19px;color:var(--ink)}.finding{display:flex;justify-content:space-between;gap:30px;background:var(--paper);border:1px solid var(--line);padding:24px;margin-bottom:10px}.finding h3{margin:10px 0 5px}.finding p{font-size:13px;color:var(--muted);max-width:700px}.finding a{font-size:12px;color:var(--green)}.status{font:10px DM Mono;text-transform:uppercase;padding:4px 7px;background:#e6e6df}.status.pending{background:#fff0be}.status.approved{background:#dcebd8}.status.rejected{background:#f5d9d2}.finding-actions{display:flex;align-items:center;gap:8px}.finding-actions button{font-size:12px}.finding-actions svg{width:14px}.finding-actions .approve{background:var(--green);color:white}.empty-state{text-align:center;padding:60px}.empty-state>svg{background:var(--acid);padding:12px;width:52px;height:52px}.editor-form{max-width:920px;background:var(--paper);border:1px solid var(--line);padding:30px}.editor-heading{display:flex;gap:14px}.editor-heading h2{margin:0}.editor-heading p{margin:5px 0;color:var(--muted)}.form-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}.form-success{background:#dcebd8;padding:12px;color:#24502f} .admin-login{min-height:calc(100vh - 145px);display:grid;place-items:center;padding:40px}.login-panel{width:min(450px,100%);background:var(--paper);border:1px solid var(--line);padding:40px}.admin-icon{display:grid;place-items:center;background:var(--ink);color:var(--acid);width:50px;height:50px;margin-bottom:28px}.login-panel h1{font-size:35px;letter-spacing:-.04em;margin:5px 0}.login-panel>p{color:var(--muted);line-height:1.6}.login-panel form,.editor-form{display:grid;gap:16px;margin-top:28px}.login-panel label,.editor-form label{display:grid;gap:7px;font-size:12px;font-weight:600}.login-panel input,.editor-form input,.editor-form textarea,.editor-form select{border:1px solid #bec1bb;background:#fff;padding:12px;outline:0;border-radius:3px}.login-panel input:focus,.editor-form input:focus,.editor-form textarea:focus,.editor-form select:focus{border-color:var(--green);box-shadow:0 0 0 3px #264c3b18}.primary-button,.secondary-button,.finding-actions button{border:0;display:inline-flex;justify-content:center;align-items:center;gap:8px;padding:12px 16px;cursor:pointer;border-radius:3px}.primary-button{background:var(--ink);color:white}.primary-button:hover{background:var(--green)}.admin-page{max-width:1250px;margin:auto;padding:60px 30px 100px}.admin-page>header{display:flex;justify-content:space-between;align-items:end}.admin-page h1{font-size:48px;letter-spacing:-.05em;margin:4px 0}.admin-page header p{color:var(--muted)}.secondary-button{background:#e2e3db}.admin-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:40px 0}.admin-stats>div{display:flex;gap:14px;align-items:center;border:1px solid var(--line);background:var(--paper);padding:20px}.admin-stats svg{color:var(--coral)}.admin-stats span{display:grid;font-size:11px;color:var(--muted)}.admin-stats strong{font-size:19px;color:var(--ink)}.finding{display:flex;justify-content:space-between;gap:30px;background:var(--paper);border:1px solid var(--line);padding:24px;margin-bottom:10px}.finding h3{margin:10px 0 5px}.finding p{font-size:13px;color:var(--muted);max-width:700px}.finding a{font-size:12px;color:var(--green)}.status{font:10px DM Mono;text-transform:uppercase;padding:4px 7px;background:#e6e6df}.status.pending{background:#fff0be}.status.approved{background:#dcebd8}.status.rejected{background:#f5d9d2}.finding-actions{display:flex;align-items:center;gap:8px}.finding-actions button{font-size:12px}.finding-actions svg{width:14px}.finding-actions .approve{background:var(--green);color:white}.empty-state{text-align:center;padding:60px}.empty-state>svg{background:var(--acid);padding:12px;width:52px;height:52px}.editor-form{max-width:920px;background:var(--paper);border:1px solid var(--line);padding:30px}.editor-heading{display:flex;gap:14px}.editor-heading h2{margin:0}.editor-heading p{margin:5px 0;color:var(--muted)}.form-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}.form-success{background:#dcebd8;padding:12px;color:#24502f}
.revision-manager>form{display:flex;align-items:end;gap:10px;margin-bottom:24px}.revision-manager>form label{display:grid;gap:6px;font-size:12px;font-weight:600}.revision-manager input{min-width:280px;border:1px solid #bec1bb;background:#fff;padding:11px}.revision-row small{color:var(--muted);font:10px DM Mono}
@media(max-width:1000px){.topic-grid{grid-template-columns:repeat(2,1fr)}.prose-grid{grid-template-columns:1fr}.topic-hero{grid-template-columns:1fr}.topic-hero dl{display:grid;grid-template-columns:1fr 1fr}.admin-stats{grid-template-columns:1fr}} @media(max-width:1000px){.topic-grid{grid-template-columns:repeat(2,1fr)}.prose-grid{grid-template-columns:1fr}.topic-hero{grid-template-columns:1fr}.topic-hero dl{display:grid;grid-template-columns:1fr 1fr}.admin-stats{grid-template-columns:1fr}}
@media(max-width:700px){.topbar{height:62px}.topbar nav a{font-size:0}.topbar nav a svg{width:20px;height:20px}.hero{padding-top:60px}.hero:after{opacity:.3}.library-layout{grid-template-columns:1fr;padding-top:30px}.filters{display:none}.topic-grid{grid-template-columns:1fr}.topic-card{min-height:220px}.topic-page{padding:30px 18px 70px}.topic-hero{padding-top:45px}.topic-hero h1{font-size:48px}.topic-tabs [role=tab]{padding:16px 10px;font-size:12px}.admin-page{padding:35px 18px}.admin-page>header,.finding{align-items:flex-start;flex-direction:column}.finding-actions{width:100%}.form-grid{grid-template-columns:1fr}footer{gap:15px;flex-direction:column}} @media(max-width:700px){.topbar{height:62px}.topbar nav a{font-size:0}.topbar nav a svg{width:20px;height:20px}.hero{padding-top:60px}.hero:after{opacity:.3}.library-layout{grid-template-columns:1fr;padding-top:30px}.filters{display:none}.topic-grid{grid-template-columns:1fr}.topic-card{min-height:220px}.topic-page{padding:30px 18px 70px}.topic-hero{padding-top:45px}.topic-hero h1{font-size:48px}.topic-tabs [role=tab]{padding:16px 10px;font-size:12px}.admin-page{padding:35px 18px}.admin-page>header,.finding{align-items:flex-start;flex-direction:column}.finding-actions{width:100%}.form-grid{grid-template-columns:1fr}footer{gap:15px;flex-direction:column}}