asxpio/public/style.css
Sergei Poljanski 9bf40690e5
admin: user accounts and session login
The admin was one shared HTTP Basic login: no way to give someone
partial access, revoke one person, or tell who changed what. Add a users
table and a session login form. Passwords are scrypt hashes from Ruby's
OpenSSL, so no native gem is needed. Sessions carry a per-user version
that is bumped on password change, which logs that user out everywhere.

ADMIN_USER/ADMIN_PASSWORD now bootstrap the first superuser on an empty
table and afterwards only work while no active superuser exists, as a
recovery path that re-creates one. Until per-area permissions land,
non-superusers only reach the dashboard and their own password page.

Assisted-by: Claude:opus-5
2026-10-10 08:25:46 +04:00

547 lines
12 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Self-hosted fonts (no CDN). Latin subsets; Georgian falls back to the
vendored Noto, downloaded only when Georgian glyphs are on the page. */
@font-face {
font-family: 'IBM Plex Sans';
src: url('/fonts/ibm-plex-sans-latin-400-normal.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'IBM Plex Sans';
src: url('/fonts/ibm-plex-sans-latin-400-italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'IBM Plex Sans';
src: url('/fonts/ibm-plex-sans-latin-600-normal.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'IBM Plex Sans';
src: url('/fonts/ibm-plex-sans-latin-700-normal.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'IBM Plex Mono';
src: url('/fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Noto Sans Georgian';
src: url('/fonts/NotoSansGeorgian-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+10A0-10FF, U+1C90-1CBF, U+2D00-2D2F;
}
/* Design tokens — shared palette with blog.asxp.io */
:root {
--bg: #121212;
--surface: #1A1A1A;
--surface-alt: #222222;
--surface-hover: #333333;
--border-subtle: #1E1E1E;
--border: #2A2A2A;
--border-strong: #3A3A3A;
--text: #AAAAAA;
--text-bright: #EEEEEE;
--text-strong: #DDDDDD;
--text-soft: #CCCCCC;
--text-link: #BBBBBB;
--text-dim: #999999;
--text-dimmer: #888888;
--text-faint: #777777;
--text-muted: #666666;
--text-ghost: #555555;
--text-hover: #FFFFFF;
--link-underline: #444444;
--danger: #C97070;
--danger-border: #4A1F1F;
--danger-bg: #2A1414;
--pending: #C9A86A;
--pending-border: #4A3E1F;
--pending-bg: #2A2414;
--paid: #7AB97A;
--paid-border: #234A23;
--paid-bg: #142A14;
--font-sans: 'IBM Plex Sans', 'Noto Sans Georgian', sans-serif;
--font-mono: 'IBM Plex Mono', monospace;
}
body {
background-color: var(--bg);
color: var(--text);
font-family: var(--font-sans);
margin: 0;
line-height: 1.5;
}
main {
max-width: 720px;
margin: 0 auto;
padding: 40px 20px 80px;
}
.site-header {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: 1px solid var(--border);
padding-bottom: 12px;
margin-bottom: 36px;
}
.site-title {
font-family: var(--font-mono);
font-size: 1.1rem;
color: var(--text-bright);
border-bottom: none;
}
.site-header nav a {
color: var(--text-faint);
border-bottom: none;
margin-left: 18px;
font-size: 0.9rem;
}
.site-header nav a:hover,
.site-header nav a.active,
.site-title:hover {
color: var(--text-bright);
border-bottom: none;
}
.home-intro {
max-width: 480px;
margin: 0 auto;
text-align: center;
color: var(--text);
}
.header {
text-align: center;
margin-bottom: 40px;
}
.monogram {
display: inline-block;
width: 56px;
height: 56px;
margin-bottom: 14px;
line-height: 0;
}
.header h1 {
font-size: 1.8rem;
font-weight: 600;
color: var(--text-bright);
margin: 0;
letter-spacing: -0.01em;
}
.header .tagline {
font-size: 1rem;
color: var(--text-dim);
margin: 8px 0 0;
}
.header .ka {
display: block;
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 6px;
}
.section {
margin: 36px 0;
}
.section h2 {
font-size: 1rem;
font-weight: 600;
color: var(--text-strong);
text-transform: uppercase;
letter-spacing: 0.08em;
border-bottom: 1px solid var(--border);
padding-bottom: 6px;
margin: 0 0 16px;
}
ul.services {
list-style: none;
padding: 0;
margin: 0;
display: grid;
/* Column count derives from available width — no breakpoint needed,
behaves at any zoom level. */
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 10px 24px;
align-items: start;
}
ul.services li {
padding-left: 14px;
position: relative;
}
ul.services li::before {
content: '›';
position: absolute;
left: 0;
color: var(--text-ghost);
}
.svc-tools {
display: block;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-muted);
margin-top: 2px;
}
dl.kv {
display: grid;
grid-template-columns: max-content 1fr;
gap: 6px 18px;
margin: 0;
}
dl.kv dt {
color: var(--text-faint);
font-size: 0.9rem;
}
dl.kv dd {
margin: 0;
color: var(--text-soft);
}
dl.kv dd.mono,
.mono {
font-family: var(--font-mono);
font-size: 0.92rem;
}
.ka-note {
color: var(--text-muted);
font-size: 0.85rem;
margin-top: 4px;
}
pre {
position: relative;
background: var(--surface-alt);
padding: 10px;
font-size: 14px;
font-family: var(--font-mono);
word-wrap: break-word;
white-space: pre-wrap;
border: 1px solid #020202;
margin: 8px 0;
}
.copy-button {
position: absolute;
top: 5px;
right: 10px;
cursor: pointer;
color: #424242;
background: #111111;
border-radius: 0.5em;
padding: 2px 7px;
}
.copy-button:hover {
color: #000;
}
a {
color: var(--text-link);
text-decoration: none;
border-bottom: 1px dotted var(--link-underline);
}
a:hover {
color: var(--text-hover);
border-bottom-color: var(--text-dimmer);
}
.footer {
text-align: center;
color: var(--text-ghost);
font-size: 0.8rem;
padding: 24px 20px;
border-top: 1px solid var(--border-subtle);
}
.footer-line {
margin-bottom: 12px;
}
.footer-links {
display: flex;
gap: 18px;
justify-content: center;
align-items: center;
}
.footer-links a {
color: var(--text-ghost);
border-bottom: none;
line-height: 0;
}
.footer-links a:hover {
color: var(--text-hover);
}
.footer-links svg {
display: block;
}
/* Form errors (admin invoice form) */
.form-error {
display: block;
color: var(--danger);
font-size: 0.85rem;
margin-top: 4px;
}
.form-error-base {
background: var(--danger-bg);
border: 1px solid var(--danger-border);
padding: 10px;
border-radius: 3px;
margin-bottom: 16px;
}
.contact-intro {
color: var(--text-dim);
margin: 0 0 16px;
}
.contact-direct {
margin-top: 18px;
}
/* Keys */
.key-block {
margin-bottom: 20px;
}
.key-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 4px;
}
.key-label {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-faint);
letter-spacing: 0.08em;
}
.key-file {
font-family: var(--font-mono);
font-size: 0.85rem;
}
/* Demoted Legal section — visible but quiet */
.section.legal {
margin-top: 56px;
color: var(--text-dimmer);
font-size: 0.9rem;
}
.section.legal h2 {
font-size: 0.85rem;
color: var(--text-dimmer);
border-bottom-color: var(--border-subtle);
text-transform: none;
letter-spacing: 0.04em;
}
.section.legal .legal-intro {
margin: 0 0 14px;
color: var(--text-faint);
}
.section.legal dl.kv dt {
color: var(--text-muted);
font-size: 0.85rem;
}
.section.legal dl.kv dd {
color: var(--text-dim);
font-size: 0.9rem;
}
/* Admin nav + dashboard */
.admin-nav {
display: flex;
gap: 16px;
justify-content: center;
margin-bottom: 24px;
font-family: var(--font-mono);
font-size: 0.85rem;
}
.admin-nav a { color: var(--text-dim); text-decoration: none; }
.admin-nav-user { display: flex; gap: 8px; align-items: baseline; margin-left: 16px; padding-left: 16px; border-left: 1px solid var(--border); }
.admin-nav-user form { display: inline; }
.auth-form { max-width: 360px; margin: 0 auto; }
.admin-nav a.active,
.admin-nav a:hover { color: var(--text-bright); }
.dash { display: grid; gap: 24px; }
.dash-tile h2 a { color: inherit; text-decoration: none; }
.stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin: 12px 0 16px;
}
.stats div {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
padding: 12px;
text-align: center;
}
.stats b { display: block; font-size: 1.4rem; color: var(--text-bright); font-variant-numeric: tabular-nums; }
.stats span { font-size: 0.75rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.06em; }
/* Invoices */
.invoice-list {
width: 100%;
border-collapse: collapse;
font-size: 0.92rem;
}
.invoice-list th,
.invoice-list td {
text-align: left;
padding: 8px 10px;
border-bottom: 1px solid var(--border-subtle);
}
.invoice-list th {
color: var(--text-faint);
font-weight: 600;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.invoice-list td.num,
.invoice-list th.num {
text-align: right;
}
.invoice-list td.actions {
text-align: right;
}
.badge {
display: inline-block;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 2px 8px;
border-radius: 3px;
border: 1px solid;
}
.badge-pending {
color: var(--pending);
border-color: var(--pending-border);
background: var(--pending-bg);
}
.badge-paid {
color: var(--paid);
border-color: var(--paid-border);
background: var(--paid-bg);
}
.invoice-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0 16px;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin: 8px 0 12px;
}
.items-table th,
.items-table td {
padding: 4px;
border: none;
vertical-align: top;
}
.items-table th {
color: var(--text-faint);
font-weight: 600;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
text-align: left;
}
.items-table th.num {
text-align: right;
}
.items-table input {
width: 100%;
box-sizing: border-box;
background: var(--surface);
color: var(--text-strong);
border: 1px solid var(--border);
border-radius: 3px;
padding: 6px 8px;
font-family: inherit;
font-size: 0.9rem;
}
.items-table td.num input {
text-align: right;
font-family: var(--font-mono);
}
.items-table td.actions {
width: 32px;
text-align: center;
}
.items-table .remove-row {
background: transparent;
border: 1px solid var(--border);
color: var(--text-faint);
border-radius: 3px;
padding: 4px 8px;
cursor: pointer;
line-height: 1;
}
.items-table .remove-row:hover {
color: var(--danger);
border-color: var(--danger-border);
}
.link-button {
background: transparent;
border: 1px dashed var(--border);
color: var(--text-dimmer);
padding: 6px 12px;
border-radius: 3px;
cursor: pointer;
font-family: inherit;
margin-bottom: 18px;
}
.link-button:hover {
color: var(--text-strong);
border-color: var(--text-ghost);
}
.invoice-form select {
width: 100%;
box-sizing: border-box;
background: var(--surface);
color: var(--text-strong);
border: 1px solid var(--border);
border-radius: 3px;
padding: 8px 10px;
font-family: inherit;
font-size: 0.95rem;
}
.invoice-form input[type="date"] {
width: 100%;
box-sizing: border-box;
background: var(--surface);
color: var(--text-strong);
border: 1px solid var(--border);
border-radius: 3px;
padding: 8px 10px;
font-family: var(--font-mono);
font-size: 0.92rem;
color-scheme: dark;
}
.download-button {
display: inline-block;
background: var(--border);
color: var(--text-strong);
border: 1px solid var(--border-strong);
border-radius: 3px;
padding: 12px 32px;
font-size: 1rem;
text-decoration: none;
}
.download-button:hover {
background: var(--surface-hover);
border-color: var(--text-ghost);
color: var(--text-hover);
}
@media (max-width: 560px) {
dl.kv {
grid-template-columns: 1fr;
gap: 2px 0;
}
dl.kv dd {
margin-bottom: 8px;
}
}