customer-presentation/admin/admin.css
Arlind Ukshini 2d06f8e513 /fenjaops: add Remove button for non-admin invites
- New endpoint: DELETE /api/fenjaops/invites/:email, behind
  requireAuth + requireAdmin. Guardrails:
    * refuses if the target email is an admin (demote first via
      bin/invite.js admin remove) — preserves the invariant that
      a compromised admin session can't lock everyone out;
    * refuses if the target email equals the caller's own —
      prevents self-inflicted lockouts from the UI;
    * deletes active sessions for the target email so the user
      is kicked out immediately instead of holding their 30-day
      cookie.
- Admin page: Invites table gains an "Action" column. Non-admin,
  non-self rows show a Remove button (quiet ink outline; crimson
  on hover to cue destructive intent). Admin and self rows show
  an em-dash. Click → browser confirm() → DELETE → load() to
  refresh counts + tables.
- admin.js fetches /auth/me alongside the other payloads so
  render can compare each row's email against the viewer's.
- PROJECT.md and CLAUDE.md updated: the "no web deletion"
  invariant is narrowed to "no web deletion of admins or self"
  to reflect the new capability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:23:12 +02:00

240 lines
5.7 KiB
CSS

/* ─────────────────────────────────────────────────────────────
admin/admin.css — utilitarian read-only admin styles. Shares
Fenja's paper/ink palette with the public site but drops the
editorial serif in favour of system-sans for scannability.
───────────────────────────────────────────────────────────── */
:root {
--paper: #faf6ee;
--paper-2: #f6f2e8;
--ink: #2e2e28;
--ink-soft: #5f5e5e;
--ink-dim: #8a887f;
--line: rgba(46, 46, 40, 0.12);
--accent: #b96b58;
--admin: #8a3a2f;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--paper);
color: var(--ink);
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 14px;
line-height: 1.5;
}
.masthead {
padding: 32px 48px 8px;
border-bottom: 1px solid var(--line);
}
.masthead h1 {
margin: 0 0 4px;
font-family: "Newsreader", Georgia, serif;
font-size: 34px;
font-weight: 400;
letter-spacing: -0.015em;
}
.masthead .dim { color: var(--ink-dim); font-style: italic; }
.masthead .meta {
margin: 0;
color: var(--ink-dim);
font-size: 13px;
}
.masthead code {
background: var(--paper-2);
padding: 1px 6px;
border-radius: 3px;
font-size: 12.5px;
}
.panel {
padding: 28px 48px;
border-bottom: 1px solid var(--line);
}
.panel h2 {
margin: 0 0 16px;
font-family: "Newsreader", Georgia, serif;
font-size: 20px;
font-weight: 500;
letter-spacing: -0.005em;
color: var(--ink);
}
.panel h2 .dim { color: var(--ink-dim); font-weight: 400; font-style: italic; }
/* Stats cards */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
max-width: 900px;
}
.stat {
background: var(--paper-2);
padding: 16px 20px;
border-radius: 6px;
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-k {
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--ink-dim);
font-weight: 600;
}
.stat-v {
font-family: "Newsreader", Georgia, serif;
font-size: 32px;
font-weight: 500;
color: var(--ink);
line-height: 1;
}
/* Tables */
.t {
width: 100%;
border-collapse: collapse;
font-variant-numeric: tabular-nums;
}
.t th,
.t td {
text-align: left;
padding: 10px 14px;
border-bottom: 1px solid var(--line);
vertical-align: top;
}
.t thead th {
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--ink-dim);
font-weight: 600;
border-bottom: 1px solid var(--line);
background: var(--paper-2);
}
.t tbody tr:hover { background: var(--paper-2); }
.t .num { text-align: right; font-variant-numeric: tabular-nums; }
.t .mono { font-family: ui-monospace, "SF Mono", Consolas, monospace; font-size: 12px; color: var(--ink-soft); }
.t .when { white-space: nowrap; color: var(--ink-soft); font-size: 13px; }
.t .badge {
display: inline-block;
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--admin);
font-weight: 700;
}
.t .dim { color: var(--ink-dim); }
/* Row action buttons (Remove, …). Quiet by default, crimson on hover
so the destructive intent reads clearly before the click. */
.row-action {
all: unset;
cursor: pointer;
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 5px 10px;
border-radius: 3px;
background: transparent;
color: var(--ink-soft);
box-shadow: inset 0 0 0 1px var(--line);
transition:
background 140ms ease,
color 140ms ease,
box-shadow 140ms ease;
}
.row-action:hover {
color: #fff;
background: var(--admin);
box-shadow: inset 0 0 0 1px var(--admin);
}
.row-action:disabled {
opacity: 0.55;
cursor: progress;
}
.empty {
margin: 12px 2px 0;
color: var(--ink-dim);
font-style: italic;
font-size: 13px;
}
/* Invite form */
.invite-form {
display: grid;
grid-template-columns: minmax(220px, 1fr) minmax(160px, 1fr) auto;
gap: 14px;
align-items: end;
max-width: 900px;
}
.invite-form label {
display: flex;
flex-direction: column;
gap: 6px;
}
.invite-form .lbl {
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--ink-dim);
font-weight: 600;
}
.invite-form .opt {
text-transform: none;
letter-spacing: 0;
font-weight: 400;
font-style: italic;
color: var(--ink-dim);
}
.invite-form input {
background: var(--paper-2);
border: 1px solid var(--line);
border-radius: 4px;
padding: 9px 11px;
font: inherit;
color: var(--ink);
}
.invite-form input:focus {
outline: none;
border-color: var(--accent);
background: var(--paper);
}
.invite-form button {
background: var(--ink);
color: var(--paper);
border: none;
border-radius: 4px;
padding: 10px 20px;
font: inherit;
font-weight: 600;
letter-spacing: 0.02em;
cursor: pointer;
}
.invite-form button:hover:not(:disabled) { background: #000; }
.invite-form button:disabled { opacity: 0.55; cursor: progress; }
.invite-form .form-msg {
grid-column: 1 / -1;
margin: 4px 0 0;
padding: 9px 12px;
border-radius: 4px;
font-size: 13px;
}
.invite-form .form-msg.ok { background: #e4eadf; color: #3a5330; }
.invite-form .form-msg.err { background: #f2dcd6; color: var(--admin); }
@media (max-width: 640px) {
.masthead, .panel { padding-left: 20px; padding-right: 20px; }
.masthead h1 { font-size: 26px; }
.t th, .t td { padding: 8px 10px; }
.t .mono { display: none; }
.invite-form { grid-template-columns: 1fr; }
.invite-form button { width: 100%; }
}