The image build failed on master:
ERROR: Ignored the following versions that require a different python
version: 4.19.0 Requires-Python >=3.11
ERROR: Could not find a version that satisfies the requirement
pymongo==4.19.0
The base image was `python:3.10` but the pinned pymongo 4.19 needs Python
>= 3.11. The image is now `python:3.14-slim` — the current stable release,
so it is not carrying the CVEs fixed in 3.11, 3.12 and 3.13.
All the runtime dependencies ship a cp314 wheel (pymongo included), so no
compiler toolchain is needed. The old apt-get line installed
libsasl2-dev, python-dev-is-python3, libldap2-dev and libssl-dev for
packages that are no longer in the dependency list, so it is gone and the
image is smaller.
The build now asserts the interpreter version, so a base image change that
breaks the pinned requirements fails the build with a clear message rather
than a pip resolution error.
Review findings, all from the independent review of this PR
------------------------------------------------------------
- Mongo indexes: the run metadata is always looked up by `beer_run_id` and
the readings by `(beer_run_id, date)`. Both are now indexed.
- Content-Security-Policy: the app serves only its own vendored assets, so
a strict `default-src 'self'` policy is enforceable. Adding it required
removing the inline `style=` attributes from the templates (they became
`.mb-lg` / `.mt-lg` utility classes) and the one inline `<script> (the run
id now travels as a `data-run-id` attribute). A test asserts no template
has either.
- Job timeouts: the `test` and `build` jobs now have `timeout-minutes`, so
a wedged build stops occupying a runner.
- OG range: raised from 1300 to 1500 points, so a strong specialty brew
(a 1.150 wort is real) can be recorded.
Bugs found and fixed along the way
----------------------------------
- An unfinished run reported a *measured* ABV. `abv_percent()` now returns
None for a run that is still in the fermenter — its figure is a projection,
not a measurement. This was showing the two active runs in the "alcohol by
brew" chart as if they were finished.
- The `/api/runs` endpoints the charts fetch from did not exist, so every
chart was silently empty. They are back, with tests.
- The "brews over time" chart left out the months with no brews, which
spaced the busy months evenly and made the chart read as continuous
activity. The quiet months are now included with a zero.
- The run-detail chart was dropped when PR 1 was scoped down, leaving dead
code behind. It is restored.
Tests: 105 (up from 91).
685 lines
20 KiB
CSS
685 lines
20 KiB
CSS
/* ==========================================================================
|
|
Brew Log — design system
|
|
Beer-house palette: warm amber/copper on near-black, with a condensed
|
|
display face (Anton) for headings and Inter for everything else.
|
|
|
|
Palette taken from the brewery-128 template:
|
|
--primary #be7f39 (amber / copper)
|
|
--primary-dark #0e0e0e
|
|
--black-primary #212024
|
|
--black-secondary #161616
|
|
--gray #e9e9e9
|
|
--dark-gray #afafaf
|
|
--paragraph-gray #535353
|
|
========================================================================== */
|
|
|
|
/* ------------------------------------------------------------------ fonts */
|
|
|
|
@font-face {
|
|
font-family: 'Anton';
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
font-display: swap;
|
|
src: url('../fonts/anton-latin.woff2') format('woff2');
|
|
}
|
|
|
|
@font-face {
|
|
font-family: 'Inter';
|
|
font-style: normal;
|
|
font-weight: 100 900;
|
|
font-display: swap;
|
|
src: url('../fonts/inter-latin-400.woff2') format('woff2');
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- tokens */
|
|
|
|
:root {
|
|
/* surfaces */
|
|
--bg: #121110;
|
|
--bg-raised: #1c1a19;
|
|
--bg-raised-2: #242120;
|
|
--bg-input: #161514;
|
|
|
|
/* lines */
|
|
--border: #332e2a;
|
|
--border-strong: #4a4239;
|
|
|
|
/* text */
|
|
--text: #f2ede8;
|
|
--text-muted: #afafaf;
|
|
--text-faint: #7d746c;
|
|
|
|
/* brand */
|
|
--amber: #be7f39;
|
|
--amber-bright: #dda05c;
|
|
--amber-deep: #8f5f28;
|
|
--amber-dim: rgba(190, 127, 57, 0.14);
|
|
--amber-text: #e2b47c;
|
|
|
|
/* foam / highlights */
|
|
--foam: #f7f3ee;
|
|
--cream: #eef4f8;
|
|
|
|
/* status */
|
|
--warn: #e8a33d;
|
|
--warn-dim: rgba(232, 163, 61, 0.14);
|
|
--danger: #d9534f;
|
|
--danger-dim: rgba(217, 83, 79, 0.14);
|
|
--success: #7db902;
|
|
--success-dim: rgba(125, 185, 2, 0.14);
|
|
|
|
--radius: 10px;
|
|
--radius-sm: 6px;
|
|
--radius-lg: 16px;
|
|
|
|
--shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 10px 30px rgba(0, 0, 0, .35);
|
|
|
|
--font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
--display: 'Anton', 'Arial Narrow', Impact, sans-serif;
|
|
--mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--font);
|
|
font-size: 15px;
|
|
line-height: 1.55;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
a { color: var(--amber-text); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
|
|
h1, h2, h3, h4 { margin: 0 0 .5rem; line-height: 1.15; font-weight: 650; }
|
|
h1 { font-size: 1.65rem; }
|
|
h2 { font-size: 1.25rem; }
|
|
h3 { font-size: 1.02rem; }
|
|
|
|
p { margin: 0 0 .75rem; }
|
|
|
|
/* Display type: the condensed face, used sparingly for headings and figures. */
|
|
.display {
|
|
font-family: var(--display);
|
|
font-weight: 400;
|
|
letter-spacing: .01em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- topbar */
|
|
|
|
.topbar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 50;
|
|
background: rgba(18, 17, 16, .94);
|
|
backdrop-filter: blur(8px);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.topbar__inner {
|
|
max-width: 1240px;
|
|
margin: 0 auto;
|
|
padding: .6rem 1.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .55rem;
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
}
|
|
.brand:hover { text-decoration: none; }
|
|
|
|
.brand__mark {
|
|
font-size: 1.4rem;
|
|
line-height: 1;
|
|
filter: drop-shadow(0 0 10px rgba(190, 127, 57, .35));
|
|
}
|
|
|
|
.brand__text {
|
|
font-family: var(--display);
|
|
font-size: 1.22rem;
|
|
letter-spacing: .04em;
|
|
text-transform: uppercase;
|
|
color: var(--amber-bright);
|
|
}
|
|
|
|
.nav { display: flex; gap: .25rem; flex: 1; }
|
|
|
|
.nav__link {
|
|
padding: .4rem .8rem;
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-muted);
|
|
font-size: .92rem;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
transition: background .12s ease, color .12s ease;
|
|
}
|
|
.nav__link:hover { background: var(--bg-raised-2); color: var(--text); text-decoration: none; }
|
|
.nav__link.is-active {
|
|
background: var(--amber-dim);
|
|
color: var(--amber-text);
|
|
box-shadow: inset 0 -2px 0 var(--amber);
|
|
}
|
|
|
|
.topbar__logout { display: flex; align-items: center; gap: .6rem; }
|
|
.topbar__user {
|
|
color: var(--text-muted);
|
|
font-size: .85rem;
|
|
font-family: var(--mono);
|
|
padding: .15rem .5rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ page */
|
|
|
|
.page {
|
|
max-width: 1240px;
|
|
margin: 0 auto;
|
|
padding: 1.5rem 1.25rem 4rem;
|
|
}
|
|
.page--narrow { max-width: 460px; }
|
|
|
|
.page__head {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
.page__title {
|
|
margin: 0;
|
|
font-family: var(--display);
|
|
font-weight: 400;
|
|
font-size: 1.85rem;
|
|
letter-spacing: .02em;
|
|
text-transform: uppercase;
|
|
color: var(--text);
|
|
}
|
|
.page__sub { color: var(--text-muted); font-size: .9rem; margin: .15rem 0 0; }
|
|
|
|
/* ----------------------------------------------------------------- cards */
|
|
|
|
.card {
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
position: relative;
|
|
}
|
|
|
|
/* A thin amber seam along the top of each card — the "label on the barrel" touch. */
|
|
.card::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0 0 auto 0;
|
|
height: 2px;
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
|
background: linear-gradient(90deg, var(--amber-deep), var(--amber) 40%, transparent 85%);
|
|
opacity: .8;
|
|
}
|
|
|
|
.card__head {
|
|
padding: .9rem 1.1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
.card__title {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 650;
|
|
letter-spacing: .01em;
|
|
}
|
|
.card__hint { color: var(--text-faint); font-size: .82rem; }
|
|
.card__body { padding: 1.1rem; }
|
|
.card__body--flush { padding: 0; }
|
|
|
|
.grid { display: grid; gap: 1rem; }
|
|
.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
/* The active-beer cards: fill the row whatever the number of brews. */
|
|
.grid--cards { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
|
|
.grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
.grid--sidebar { grid-template-columns: minmax(0, 380px) minmax(0, 1fr); align-items: start; }
|
|
|
|
@media (max-width: 1000px) {
|
|
.grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
.grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
.grid--sidebar { grid-template-columns: minmax(0, 1fr); }
|
|
}
|
|
@media (max-width: 640px) {
|
|
.grid--2, .grid--3, .grid--4 { grid-template-columns: minmax(0, 1fr); }
|
|
.topbar__inner { flex-wrap: wrap; gap: .75rem; }
|
|
.nav { order: 3; width: 100%; overflow-x: auto; }
|
|
.page__title { font-size: 1.5rem; }
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- stats */
|
|
|
|
.stat {
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: .85rem 1rem;
|
|
}
|
|
.stat__label {
|
|
color: var(--text-faint);
|
|
font-size: .72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: .09em;
|
|
font-weight: 600;
|
|
}
|
|
.stat__value {
|
|
font-family: var(--display);
|
|
font-weight: 400;
|
|
font-size: 1.75rem;
|
|
letter-spacing: .02em;
|
|
line-height: 1.2;
|
|
color: var(--amber-bright);
|
|
}
|
|
.stat__meta { color: var(--text-muted); font-size: .83rem; }
|
|
|
|
/* ----------------------------------------------------------------- forms */
|
|
|
|
.field { margin-bottom: .9rem; }
|
|
.field__label {
|
|
display: block;
|
|
font-size: .74rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: .09em;
|
|
color: var(--text-muted);
|
|
margin-bottom: .3rem;
|
|
}
|
|
.field__hint { color: var(--text-muted); font-size: .8rem; margin-top: .25rem; }
|
|
.field__error { color: var(--danger); font-size: .8rem; margin-top: .25rem; }
|
|
|
|
input[type="text"], input[type="password"], input[type="number"],
|
|
input[type="date"], input[type="email"], textarea, select {
|
|
width: 100%;
|
|
padding: .5rem .65rem;
|
|
background: var(--bg-input);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text);
|
|
font-family: inherit;
|
|
font-size: .92rem;
|
|
transition: border-color .12s ease, box-shadow .12s ease;
|
|
}
|
|
input:focus, textarea:focus, select:focus {
|
|
outline: none;
|
|
border-color: var(--amber);
|
|
box-shadow: 0 0 0 3px var(--amber-dim);
|
|
}
|
|
input::placeholder, textarea::placeholder { color: var(--text-faint); }
|
|
textarea { resize: vertical; min-height: 5rem; }
|
|
|
|
.checkbox {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .55rem;
|
|
font-size: .9rem;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* The native checkbox is a white square on a near-black page — replace it. */
|
|
.checkbox input[type="checkbox"] {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
width: 1.05rem;
|
|
height: 1.05rem;
|
|
margin: 0;
|
|
flex: 0 0 auto;
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: 4px;
|
|
background: var(--bg-input);
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: background .12s ease, border-color .12s ease;
|
|
}
|
|
.checkbox input[type="checkbox"]:hover { border-color: var(--amber); }
|
|
.checkbox input[type="checkbox"]:checked {
|
|
background: var(--amber);
|
|
border-color: var(--amber);
|
|
}
|
|
.checkbox input[type="checkbox"]:checked::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: .32rem;
|
|
top: .07rem;
|
|
width: .28rem;
|
|
height: .55rem;
|
|
border: solid #17110a;
|
|
border-width: 0 2px 2px 0;
|
|
transform: rotate(43deg);
|
|
}
|
|
.checkbox input[type="checkbox"]:focus-visible {
|
|
outline: none;
|
|
box-shadow: 0 0 0 3px var(--amber-dim);
|
|
}
|
|
|
|
/* Hide the native number spinners — the light spinner widgets jar badly
|
|
against the dark theme. */
|
|
input[type="number"]::-webkit-outer-spin-button,
|
|
input[type="number"]::-webkit-inner-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
input[type="number"] { -moz-appearance: textfield; }
|
|
|
|
/* The native date picker's calendar icon is near-black on near-black. */
|
|
input[type="date"]::-webkit-calendar-picker-indicator {
|
|
filter: invert(0.75) sepia(1) saturate(3) hue-rotate(350deg) brightness(1.1);
|
|
cursor: pointer;
|
|
opacity: .7;
|
|
}
|
|
input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
|
|
|
|
/* Dark scrollbars to match the page. */
|
|
* {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--border-strong) var(--bg);
|
|
}
|
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
::-webkit-scrollbar-track { background: var(--bg); }
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--border-strong);
|
|
border-radius: 6px;
|
|
border: 2px solid var(--bg);
|
|
}
|
|
::-webkit-scrollbar-thumb:hover { background: var(--amber-deep); }
|
|
|
|
/* A figure that should read as a number, not as body text. */
|
|
.strong { font-weight: 650; color: var(--text); }
|
|
|
|
.field-row { display: flex; gap: .75rem; }
|
|
.field-row > * { flex: 1; }
|
|
|
|
/* --------------------------------------------------------------- buttons */
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: .4rem;
|
|
padding: .5rem .95rem;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--amber);
|
|
color: #17110a;
|
|
font-family: inherit;
|
|
font-size: .85rem;
|
|
font-weight: 650;
|
|
letter-spacing: .01em;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: filter .12s ease, background .12s ease, border-color .12s ease;
|
|
}
|
|
.btn:hover { filter: brightness(1.12); text-decoration: none; }
|
|
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
|
|
.btn--ghost {
|
|
background: transparent;
|
|
border-color: var(--border-strong);
|
|
color: var(--text-muted);
|
|
}
|
|
.btn--ghost:hover { background: var(--bg-raised-2); color: var(--text); }
|
|
|
|
.btn--danger { background: transparent; border-color: var(--danger); color: var(--danger); }
|
|
.btn--danger:hover { background: var(--danger-dim); }
|
|
|
|
.btn--sm { padding: .3rem .6rem; font-size: .78rem; }
|
|
.btn--block { width: 100%; }
|
|
|
|
.btn-row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
|
|
|
|
/* ---------------------------------------------------------------- tables */
|
|
|
|
.table-wrap { overflow-x: auto; }
|
|
|
|
table.data {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: .88rem;
|
|
}
|
|
table.data th {
|
|
text-align: left;
|
|
padding: .55rem .8rem;
|
|
color: var(--text-faint);
|
|
font-size: .7rem;
|
|
font-weight: 650;
|
|
text-transform: uppercase;
|
|
letter-spacing: .08em;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-raised-2);
|
|
white-space: nowrap;
|
|
}
|
|
table.data td {
|
|
padding: .6rem .8rem;
|
|
border-bottom: 1px solid var(--border);
|
|
vertical-align: top;
|
|
}
|
|
table.data tr:last-child td { border-bottom: none; }
|
|
table.data tbody tr:hover { background: var(--bg-raised-2); }
|
|
table.data .num { font-family: var(--mono); text-align: right; white-space: nowrap; }
|
|
table.data .comment { color: var(--text-muted); font-size: .84rem; }
|
|
|
|
/* ----------------------------------------------------------------- chips */
|
|
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: .3rem;
|
|
padding: .12rem .5rem;
|
|
border-radius: 999px;
|
|
font-size: .74rem;
|
|
font-weight: 600;
|
|
letter-spacing: .02em;
|
|
white-space: nowrap;
|
|
}
|
|
.chip--active { background: var(--amber-dim); color: var(--amber-text); }
|
|
.chip--done { background: rgba(175, 175, 175, .13); color: var(--text-muted); }
|
|
.chip--warn { background: var(--warn-dim); color: var(--warn); }
|
|
.chip--danger { background: var(--danger-dim); color: var(--danger); }
|
|
.chip--good { background: var(--success-dim); color: var(--success); }
|
|
.chip--tap {
|
|
background: var(--bg-raised-2);
|
|
border: 1px solid var(--amber-deep);
|
|
color: var(--amber-text);
|
|
font-family: var(--mono);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- flashes */
|
|
|
|
.flashes { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1rem; }
|
|
|
|
.flash {
|
|
padding: .6rem .9rem;
|
|
border-radius: var(--radius-sm);
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-raised);
|
|
font-size: .9rem;
|
|
}
|
|
.flash--success { border-color: rgba(125, 185, 2, .45); background: var(--success-dim); color: #c3e08a; }
|
|
.flash--error { border-color: rgba(217, 83, 79, .45); background: var(--danger-dim); color: #f0a8a6; }
|
|
.flash--warning { border-color: rgba(232, 163, 61, .45); background: var(--warn-dim); color: #f2c880; }
|
|
.flash--info { border-color: rgba(190, 127, 57, .45); background: var(--amber-dim); color: var(--amber-text); }
|
|
|
|
/* ---------------------------------------------------------------- login */
|
|
|
|
.login {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2rem 1rem;
|
|
background:
|
|
radial-gradient(900px 480px at 50% -10%, rgba(190, 127, 57, .13), transparent 62%),
|
|
var(--bg);
|
|
}
|
|
.login__card {
|
|
width: 100%;
|
|
max-width: 380px;
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: 2rem;
|
|
box-shadow: var(--shadow);
|
|
position: relative;
|
|
}
|
|
.login__card::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0 0 auto 0;
|
|
height: 3px;
|
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
background: linear-gradient(90deg, var(--amber-deep), var(--amber-bright), var(--amber-deep));
|
|
}
|
|
.login__mark { font-size: 2.4rem; line-height: 1; }
|
|
.login__title {
|
|
margin: .6rem 0 .15rem;
|
|
font-family: var(--display);
|
|
font-weight: 400;
|
|
font-size: 1.9rem;
|
|
letter-spacing: .03em;
|
|
text-transform: uppercase;
|
|
color: var(--amber-bright);
|
|
}
|
|
.login__sub { color: var(--text-muted); font-size: .88rem; margin-bottom: 1.4rem; }
|
|
|
|
/* ----------------------------------------------------------- active card */
|
|
|
|
.brew-card {
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 1rem 1.1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: .75rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.brew-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0 0 auto 0;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, var(--amber-deep), var(--amber) 45%, transparent 90%);
|
|
opacity: .75;
|
|
}
|
|
.brew-card__head { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem; }
|
|
.brew-card__name { font-weight: 650; font-size: 1.02rem; margin: 0; }
|
|
.brew-card__meta { color: var(--text-muted); font-size: .82rem; font-family: var(--mono); }
|
|
.brew-card__stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; }
|
|
.brew-card__stat .stat__label { font-size: .68rem; }
|
|
.brew-card__stat .stat__value { font-size: 1.3rem; }
|
|
.brew-card__spark { height: 46px; }
|
|
|
|
/* The "start a new beer" tile — fills the last grid slot and gives the
|
|
page a way to begin a brew without hunting for the form. */
|
|
.brew-card--new {
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: .6rem;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
color: var(--text-muted);
|
|
border-style: dashed;
|
|
border-color: var(--border-strong);
|
|
min-height: 180px;
|
|
}
|
|
.brew-card--new:hover {
|
|
border-color: var(--amber);
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
}
|
|
.brew-card--new::before { display: none; }
|
|
.brew-card__new-mark {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 2.4rem;
|
|
height: 2.4rem;
|
|
border: 1px solid var(--amber);
|
|
border-radius: 50%;
|
|
font-size: 1.35rem;
|
|
line-height: 1;
|
|
color: var(--amber);
|
|
}
|
|
.brew-card__new-text { display: flex; flex-direction: column; gap: .15rem; }
|
|
.brew-card__new-text strong { font-size: .95rem; }
|
|
.brew-card__spark--empty {
|
|
display: flex;
|
|
align-items: center;
|
|
font-style: italic;
|
|
padding: .35rem 0;
|
|
}
|
|
|
|
/* --------------------------------------------------------------- helpers */
|
|
|
|
.muted { color: var(--text-muted); }
|
|
.faint { color: var(--text-faint); }
|
|
.mono { font-family: var(--mono); }
|
|
.nowrap { white-space: nowrap; }
|
|
.small { font-size: .84rem; }
|
|
.tiny { font-size: .76rem; }
|
|
.center { text-align: center; }
|
|
.right { text-align: right; }
|
|
.stack > * + * { margin-top: .75rem; }
|
|
.mt-0 { margin-top: 0; }
|
|
.mb-0 { margin-bottom: 0; }
|
|
.row { display: flex; gap: .75rem; align-items: center; flex-wrap: wrap; }
|
|
.row--between { justify-content: space-between; }
|
|
.hidden { display: none !important; }
|
|
|
|
/* Spacing utilities. These exist so the templates carry no inline
|
|
`style=` attributes, which a strict Content-Security-Policy
|
|
(`style-src 'self'`) would block. */
|
|
.mb-lg { margin-bottom: 1.25rem; }
|
|
.mt-lg { margin-top: 2rem; }
|
|
|
|
.empty {
|
|
padding: 2.5rem 1rem;
|
|
text-align: center;
|
|
color: var(--text-faint);
|
|
}
|
|
.empty__icon { font-size: 2rem; display: block; margin-bottom: .5rem; opacity: .7; }
|
|
|
|
/* --------------------------------------------------------------- reading */
|
|
|
|
.reading-list { list-style: none; margin: 0; padding: 0; }
|
|
.reading-list__item {
|
|
display: flex;
|
|
gap: .75rem;
|
|
align-items: flex-start;
|
|
padding: .6rem 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.reading-list__item:last-child { border-bottom: none; }
|
|
.reading-list__date {
|
|
font-family: var(--mono);
|
|
font-size: .82rem;
|
|
color: var(--text-muted);
|
|
flex: 0 0 5.5rem;
|
|
}
|
|
.reading-list__sg { font-family: var(--mono); font-weight: 650; flex: 0 0 4rem; }
|
|
.reading-list__body { flex: 1; min-width: 0; }
|
|
.reading-list__comment { color: var(--text-muted); font-size: .84rem; }
|