customer-presentation/admin/admin.css
Arlind Ukshini cbfb187d16 /fenjaops: admin-only form to invite non-admin users
- POST /api/fenjaops/invites on server.js (requireAuth+requireAdmin).
  Ignores any is_admin field in the body — always stores 0. Records
  the acting admin's email in invited_by so the audit trail shows
  who added whom (CLI adds still record "cli").
- admin/index.html: new "Invite a new user" form panel at the top
  (email + optional first name).
- admin/admin.js: wires the form submit to the POST, shows inline
  success/error, refreshes the tables on success.
- admin/admin.css: form styling matching the existing paper/ink
  palette; mobile stacks.
- Docs: CLAUDE.md, PROJECT.md, OPERATIONS.md, CHECKLIST.md, README.md
  all updated. New non-negotiable property in PROJECT.md: no web
  endpoint can set is_admin=1 or delete an invite — promotion +
  removal stay on bin/invite.js. New CHECKLIST.md section H2 covers
  the page's gating, the invite form, and an escalation-path audit.

Admin promotion and invite deletion remain CLI-only so a compromised
admin session cannot escalate or evict.

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

210 lines
5 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;
}
.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%; }
}