Files
theoldfitz/styles.css
T
David Dyer 242c5165e1 Refactor admin panel: enhance user management and security features
- Added role-based access control for user management links in admin navigation.
- Improved user account display with a dedicated account link.
- Implemented form submission confirmation for delete actions in the admin interface.
- Introduced a password visibility toggle on the login page.
- Added login throttling to prevent brute-force attacks, recording failed attempts.
- Created a CLI script for creating an admin user, replacing the previous hardcoded admin credentials.
- Updated database schema to support login attempt tracking.
- Enhanced user editing functionality, allowing role changes with restrictions.
- Improved modal handling for user actions, ensuring better focus management.
- Added styles for new account management page and improved modal transitions.
2026-10-25 12:58:19 -04:00

2181 lines
45 KiB
CSS

@charset "UTF-8";
:root {
--ground: #171310;
--ground-2: #0f0c0a;
--surface: #221c16;
--surface-2: #2b2319;
--gold: #c9a35c;
--gold-bright: #e6c789;
--gold-dim: #a68b59;
--green: #33503f;
--green-deep: #203026;
--ink: #f1e8d8;
--ink-muted: #a89a83;
--rule: #42362590;
--font-serif: 'Fraunces', Georgia, 'Times New Roman', serif;
--font-mono: 'IBM Plex Mono', ui-monospace, Menlo, monospace;
--font-sans: 'Karla', ui-sans-serif, system-ui, sans-serif;
--danger-bg: rgba(180, 50, 40, 0.15);
--danger-text: #ff9d9d;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
body {
margin: 0;
background: var(--ground);
color: var(--ink);
font-family: var(--font-sans);
font-size: 1rem;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
}
a {
color: inherit;
}
img,
svg {
display: block;
max-width: 100%;
}
h1,
h2,
h3 {
font-family: var(--font-serif);
font-weight: 600;
line-height: 1.08;
text-wrap: balance;
margin: 0;
}
p {
margin: 0;
}
:focus-visible {
outline: 2px solid var(--gold-bright);
outline-offset: 3px;
}
/* ---------- sections ---------- */
section {
padding-block: 5.5rem;
}
/* ---------- wrap-aware separators (JS-toggled, see main.js) ----------
Shared hook used by the announcement banner and hero tagline row: a
"·" between two pieces of text that can independently wrap onto
separate lines. No CSS selector exists for "am I on the same line as
my sibling," so main.js measures and toggles this class instead of a
fixed breakpoint. Visible by default so a no-JS visitor always sees
the dot (today's behavior), never hidden by default.
display, not visibility: a hidden separator must take no space, or
it still gets counted when justify-content: center balances its flex
line — a lone badge sharing its line with a space-reserving invisible
dot renders off-center. Safe from the "text merges together" bug that
originally motivated reserving space: both wrap-sep contexts space
their siblings with a real flex `gap` (see .announcement-inner,
.tagline-row) rather than the separator's own margin, so removing it
from flow still leaves normal spacing behind, never a collision. */
.wrap-sep-hidden {
display: none;
}
.container {
width: 100%;
max-width: 70rem;
margin: 0 auto;
padding-inline: 1.5rem;
}
.eyebrow {
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--gold);
}
.muted {
color: var(--ink-muted);
}
.skip-link {
position: absolute;
left: 1rem;
top: -3rem;
background: var(--gold);
color: var(--ground-2);
padding: 0.6rem 1rem;
border-radius: 0.125rem;
font-family: var(--font-mono);
font-size: 0.85rem;
transition: top 0.15s ease;
z-index: 100;
}
.skip-link:focus {
top: 1rem;
}
/* scroll reveal */
.reveal {
opacity: 0;
transform: translateY(0.875rem);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.is-visible {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
transform: none;
transition: none;
}
}
.font-mono {
font-family: var(--font-mono);
}
/* ---------- knot mark ---------- */
.knot {
display: block;
width: 1.75rem;
height: 1.75rem;
object-fit: contain;
flex-shrink: 0;
}
.rule-row .knot {
width: 1.375rem;
height: 1.375rem;
opacity: 0.9;
}
.footer-brand .knot {
width: 1.625rem;
height: 1.625rem;
}
/* ---------- brass rule divider ---------- */
.rule-row {
display: flex;
align-items: center;
gap: 1rem;
max-width: 13.75rem;
margin-inline: auto;
}
.rule-row .line {
height: 1px;
flex: 1;
background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
}
.hero-ctas {
margin-top: 2.5rem;
display: flex;
justify-content: center;
gap: 0.9rem;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
/* Centers label text horizontally too — matters once a button is
stretched wider than its content (e.g. action-stack children filling
a fixed-width column) or wraps to multiple lines; a no-op otherwise,
so every button gets this instead of one-off overrides per case. */
justify-content: center;
gap: 0.5rem;
padding: 0.8rem 1.5rem;
font-family: var(--font-mono);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-decoration: none;
border-radius: 0.125rem;
transition: transform 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.btn:hover {
transform: translateY(-1px);
}
.btn:disabled {
opacity: 0.35;
cursor: not-allowed;
transform: none;
}
/* :where() keeps this native-button reset at zero specificity, so
modifier classes like .btn-primary's background can actually win
instead of losing to button.btn's higher (0,1,1) specificity */
:where(button.btn) {
cursor: pointer;
background: transparent;
}
.btn-primary {
background: linear-gradient(180deg, var(--gold-bright), var(--gold));
color: var(--ground-2);
font-weight: 500;
border: none;
}
.btn-primary:hover {
background: linear-gradient(180deg, var(--gold-bright), var(--gold-bright));
}
.btn-outline {
border: 1px solid var(--gold-dim);
color: var(--gold-bright);
}
/* .nav-cta mirrors this hover treatment */
.btn-outline:hover,
.nav-cta:hover {
border-color: var(--gold);
background: var(--surface);
}
.btn-sm {
padding: 0.4rem 0.8rem;
font-size: 0.75rem;
line-height: 1.2;
}
/* Single-glyph reorder buttons (↑ / ↓) — narrower than .btn-sm's text
padding since there's no label to give it room to breathe. */
.btn-icon {
padding-inline: 0.55rem;
}
.btn-danger {
background: var(--danger-bg);
border: 1px solid rgba(220, 80, 60, 0.5);
color: var(--danger-text);
}
.btn-danger:hover {
background: rgba(220, 60, 50, 0.3);
border-color: #ff6e6e;
color: #fff;
}
/* shared surface-panel look: also used by .card, .login-card, .panel below */
.stat-card,
.card,
.login-card,
.panel,
.menu-category {
background: var(--surface);
border: 1px solid var(--rule);
}
.stat-card {
margin-top: 2rem;
padding: 2rem;
border-radius: 0.2rem;
}
.stat-card .eyebrow {
display: block;
margin-bottom: 1.1rem;
}
.stat-big {
font-family: var(--font-serif);
font-weight: 600;
font-style: italic;
font-size: 3rem;
color: var(--gold-bright);
line-height: 1;
}
.stat-card .stat-label {
margin-top: 0.4rem;
font-size: 0.92rem;
color: var(--ink-muted);
}
.stat-source {
margin-top: 1.5rem;
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--gold-dim);
letter-spacing: 0.05em;
}
.card {
border-radius: 0.2rem;
padding: 2.25rem;
}
.card h3 {
font-size: 1.3rem;
color: var(--gold-bright);
margin-bottom: 1.4rem;
}
.card .note {
margin-top: 1.25rem;
font-size: 0.88rem;
color: var(--ink-muted);
}
.card .note strong {
color: var(--ink);
font-weight: 600;
}
.panel {
border-radius: 0.25rem;
padding: 2rem;
margin-bottom: 2rem;
}
.panel h2 {
font-size: 1.35rem;
color: var(--gold-bright);
margin-bottom: 1.25rem;
display: flex;
align-items: center;
gap: 0.6rem;
}
.panel-head {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.status-badge {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 0.4rem;
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.25rem 0.6rem;
border-radius: 0.125rem;
white-space: nowrap;
line-height: 1.2;
}
.status-badge.active {
background: rgba(51, 80, 63, 0.4);
border: 1px solid var(--green);
color: #9fe2bf;
}
.status-badge.inactive {
background: rgba(138, 115, 74, 0.2);
border: 1px solid var(--rule);
color: var(--ink-muted);
}
/* Both the menu item and announcement status badges double as their own
toggle control — a plain <button> would otherwise pick up the
browser's default chrome underneath the badge's own background/border.
Scoped to the element type so it's a no-op wherever a status-badge is
still a plain read-only <span>. Width is fixed per context below
(each page's label set is a different length) so toggling never
changes the badge's — and therefore the Status column's — width and
shoves every other column sideways. */
button.status-badge {
appearance: none;
-webkit-appearance: none;
cursor: pointer;
justify-content: center;
transition: opacity 0.15s ease;
}
button.status-badge:hover {
opacity: 0.8;
}
button.status-badge:disabled {
cursor: not-allowed;
opacity: 0.5;
}
/* "● Available" is the longest menu item label, ~6.7rem measured. */
.item-row button.status-badge {
width: 7rem;
}
/* "⏳ Scheduled" is the longest announcement label, ~7rem measured. */
.ann-row button.status-badge {
width: 7.25rem;
}
/* "○ Disabled" is the longer of the two user labels. Not scoped to
button.status-badge like the two above — your own row's badge is a
plain non-interactive span (see users.php), and it still needs to line
up in the same column. */
.user-row .status-badge {
width: 7rem;
}
/* Admin row actions: the button-group styling shared by every admin
list (menu items/categories, announcements, users). The actual
row/column layout for each list is its own CSS Grid, defined where
that list's markup lives below — there is no <table> anywhere in
this project. */
.row-actions {
display: inline-flex;
gap: 0.5rem;
align-items: center;
flex-wrap: nowrap;
white-space: nowrap;
}
.row-actions .btn {
white-space: nowrap;
}
/* Groups a row's move (↑/↓) and edit/delete controls into two narrow
vertical stacks instead of several buttons side by side — keeps the
Actions column (and the row as a whole) from running wide. */
.action-stack {
display: inline-flex;
flex-direction: column;
gap: 0.35rem;
}
@media (max-width: 47.5em) {
.row-actions {
flex-wrap: wrap;
white-space: normal;
}
}
.empty-state {
text-align: center;
padding: 3rem 1.5rem;
color: var(--ink-muted);
}
.empty-state p {
font-size: 0.95rem;
margin-bottom: 1.25rem;
}
/* Admin list grids: menu items/categories, announcements, and users all
share one CSS Grid pattern instead of a <table> — see the git history
for why. Every list's header row and data rows are grouped under one
selector here for the properties they all share (box model,
typography, the mobile reflow); each list then only has to declare
its own grid-template-columns/areas, i.e. the shape unique to it. */
/* Each list's real column tracks live on the -grid-wrap — the one
shared parent grid. The header row and every data row don't compute
their own auto columns (which is what let them drift out of
alignment: each was sizing "auto" only against its own content); they
subgrid instead, inheriting the exact same tracks from this parent.
.foo-rows is display:contents purely so .foo-row stays a direct grid
child of .foo-grid-wrap for that inheritance — it has no visual
footprint of its own. */
.item-grid-wrap {
grid-template-columns: auto minmax(0, 1fr) auto auto;
}
.ann-grid-wrap {
grid-template-columns: auto minmax(0, 1fr) auto auto auto auto;
}
.user-grid-wrap {
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
}
.item-grid-wrap,
.ann-grid-wrap,
.user-grid-wrap {
display: grid;
column-gap: 0.75rem;
margin-top: 1rem;
}
.item-rows,
.ann-rows,
.user-rows {
display: contents;
}
.item-grid-head, .item-row,
.ann-grid-head, .ann-row,
.user-grid-head, .user-row {
display: grid;
grid-column: 1/-1;
grid-template-columns: subgrid;
align-items: center;
}
.item-grid-head,
.ann-grid-head,
.user-grid-head {
padding: 0 0.75rem 0.85rem;
border-bottom: 1px solid var(--rule);
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gold);
font-weight: 500;
}
.item-row,
.ann-row,
.user-row {
padding: 0.85rem 0.75rem;
border-bottom: 1px solid var(--rule);
}
.item-row:last-child,
.ann-row:last-child,
.user-row:last-child {
border-bottom: none;
}
/* Each list's own shape: which named area each field maps to. "Status"
→ status and "Actions" → actions mean the same thing in all three, so
those two are declared once. */
.item-grid-head, .item-row {
grid-template-areas: "status item price actions";
}
.ann-grid-head, .ann-row {
grid-template-areas: "status headline type schedule placements actions";
}
.user-grid-head, .user-row {
grid-template-areas: "status username role created actions";
}
.col-status,
.item-row [data-label=Status],
.ann-row [data-label=Status],
.user-row [data-label=Status] {
grid-area: status;
}
.col-actions,
.item-row [data-label=Actions],
.ann-row [data-label=Actions],
.user-row [data-label=Actions] {
grid-area: actions;
}
.col-item {
grid-area: item;
}
.col-price {
grid-area: price;
}
.item-row [data-label=Item] {
grid-area: item;
}
.item-row [data-label=Price] {
grid-area: price;
}
.col-headline {
grid-area: headline;
}
.col-type {
grid-area: type;
}
.col-schedule {
grid-area: schedule;
}
.col-placements {
grid-area: placements;
}
.ann-row [data-label=Headline] {
grid-area: headline;
}
.ann-row [data-label=Type] {
grid-area: type;
}
.ann-row [data-label=Schedule] {
grid-area: schedule;
}
.ann-row [data-label=Placements] {
grid-area: placements;
}
.col-username {
grid-area: username;
}
.col-role {
grid-area: role;
}
.col-created {
grid-area: created;
}
.user-row [data-label=Username] {
grid-area: username;
}
.user-row [data-label=Role] {
grid-area: role;
}
.user-row [data-label=Created] {
grid-area: created;
}
/* .item-rows being display:contents makes this a direct child of the
.item-grid-wrap grid when a category is empty — span every column or
it lands squeezed into just the first (Status) track. */
.item-grid-empty {
grid-column: 1/-1;
text-align: center;
padding: 1.5rem 0;
}
.timestamp-cell {
color: var(--ink-muted);
font-size: 0.8rem;
}
/* Each grid's own mobile arrangement, independent of the desktop one
above — this is the whole point of grid-template-areas over a
<table>. Same fields, different map, per list. Placed after the
desktop rules above so it wins the cascade at equal specificity — a
rule's source position still matters once a media query lets both
compete. The reflow mechanics (hide the header row, stack fields with
a printed label, pin Actions to the end) are identical across all
three lists and the category header, so those are shared too; only
each one's grid-template-areas differs. */
@media (max-width: 47.5em) {
/* The wrap collapses to a single column, and every subgridded row
below inherits that automatically — they keep `grid-template-
columns: subgrid` from the desktop rule, so this one change is all
it takes to reflow the whole list (see grid-template-areas per row
further down for how each field then stacks within that column). */
/* Every list's cards get a second column on mobile so Actions can sit
in the top-right corner alongside that list's leading field, instead
of stacking at the very bottom of the card — overrides the shared
single-column rule that would otherwise apply here too.
minmax(0, auto), not plain auto, so this column can still shrink
below its content's natural width under pressure instead of forcing
the whole row to overflow the card. */
.item-grid-wrap,
.ann-grid-wrap,
.user-grid-wrap {
grid-template-columns: 1fr minmax(0, auto);
}
/* Users' actions column gets a fixed cap instead of auto: "Change
Password" is long enough that its max-content size (what auto would
size the column to) is wider than this corner has room for, which
pushed the button past the card edge. A firm cap plus the
wrap/stretch rules below keep it predictable instead. */
.user-grid-wrap {
grid-template-columns: minmax(0, 1fr) 6rem;
}
/* Every row's Actions cell holds one or two .action-stacks (move
up/down, edit/delete, etc. — each stacked vertically, see
.action-stack itself further up) side by side. Keeps those stacks
from wrapping onto their own lines in this narrow corner, which is
what the shared .row-actions wrap rule below would otherwise do. */
.item-row .row-actions,
.ann-row .row-actions,
.user-row .row-actions {
flex-wrap: nowrap;
}
.item-grid-head,
.ann-grid-head,
.user-grid-head {
display: none;
}
.item-row,
.ann-row,
.user-row {
row-gap: 0.4rem;
border: 1px solid var(--rule);
border-radius: 0.25rem;
padding: 0.9rem 1rem;
margin-bottom: 0.85rem;
}
.item-row:last-child,
.ann-row:last-child,
.user-row:last-child {
border-bottom: none;
margin-bottom: 0;
}
.item-row [data-label]::before,
.ann-row [data-label]::before,
.user-row [data-label]::before {
content: attr(data-label);
display: block;
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gold);
margin-bottom: 0.3rem;
}
.item-row {
grid-template-areas: "status actions" "item item" "price price";
/* Overrides the shared align-items: center above — on desktop that
vertically centers Status against the taller Item cell in one wide
row, which is fine, but here it also centers Status against the
taller Actions block above, pushing its badge down instead of
keeping both flush with the card's top edge. */
align-items: start;
}
.ann-row {
grid-template-areas: "status actions" "headline headline" "type type" "schedule schedule" "placements placements";
align-items: start;
}
.user-row {
grid-template-areas: "status actions" "username username" "role role" "created created";
align-items: start;
}
/* Stretches Actions' whole flex chain out to the fixed 6rem column
above (each is shrink-wrapped by default), then lets "Change
Password" — long enough on its own to blow out this corner next to a
username — wrap onto two lines within that width instead of forcing
the column wider. Every other action-stack button here (Edit,
Delete, ↑/↓) is short enough to stay on one line regardless. */
.user-row .row-actions,
.user-row .action-stack,
.user-row .row-actions .btn {
width: 100%;
}
.user-row .btn-change-password {
white-space: normal;
}
}
/* ---------- admin modal dialogs ---------- */
.admin-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(15, 12, 10, 0.8);
backdrop-filter: blur(0.25rem);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1.5rem;
opacity: 0;
pointer-events: none;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0s linear 0.2s;
}
.admin-modal-backdrop.is-open {
opacity: 1;
pointer-events: auto;
visibility: visible;
transition: opacity 0.2s ease, visibility 0s;
}
.admin-modal {
background: var(--surface);
border: 1px solid var(--rule);
border-radius: 0.25rem;
width: 100%;
max-width: 36rem;
max-height: 90vh;
overflow-y: auto;
padding: 2rem;
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.6);
transform: translateY(0.75rem);
transition: transform 0.2s ease;
}
.admin-modal-backdrop.is-open .admin-modal {
transform: translateY(0);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--rule);
}
.modal-header h2 {
font-size: 1.35rem;
color: var(--gold-bright);
margin: 0;
}
.modal-close {
background: transparent;
border: none;
color: var(--ink-muted);
font-size: 1.5rem;
cursor: pointer;
padding: 0.25rem;
line-height: 1;
text-decoration: none;
transition: color 0.15s ease;
}
.modal-close:hover {
color: var(--gold-bright);
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group.full-width {
grid-column: 1/-1;
}
.form-group label {
display: block;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gold);
margin-bottom: 0.45rem;
}
.field-hint {
font-size: 0.78rem;
}
.form-control,
.form-input {
width: 100%;
background: var(--ground-2);
border: 1px solid var(--rule);
color: var(--ink);
font-family: var(--font-sans);
font-size: 0.95rem;
padding: 0.7rem 0.85rem;
border-radius: 0.125rem;
box-sizing: border-box;
outline: none;
transition: border-color 0.15s ease;
}
.form-control:focus,
.form-input:focus {
border-color: var(--gold-bright);
}
textarea.form-control {
resize: vertical;
min-height: 5.5rem;
line-height: 1.5;
}
.error-msg,
.alert-error {
background: var(--danger-bg);
border: 1px solid rgba(220, 80, 60, 0.4);
color: var(--danger-text);
padding: 0.85rem 1.1rem;
font-size: 0.88rem;
border-radius: 0.125rem;
margin-bottom: 1.25rem;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 47.5em) {
.form-grid {
grid-template-columns: 1fr;
}
}
.form-actions {
display: flex;
gap: 0.75rem;
align-items: center;
margin-top: 1.75rem;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 0.5rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 0.92rem;
color: var(--ink);
cursor: pointer;
}
.checkbox-label-lg {
font-size: 1.05rem;
font-weight: 500;
}
.checkbox-label input[type=checkbox] {
width: 1.15rem;
height: 1.15rem;
accent-color: var(--gold);
cursor: pointer;
}
/* password toggle input wrapper */
.password-input-wrap {
position: relative;
display: flex;
align-items: center;
}
.password-input-wrap .form-control,
.password-input-wrap .form-input {
padding-right: 2.75rem;
}
.password-toggle-btn {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: var(--ink-muted);
cursor: pointer;
padding: 0.35rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.125rem;
transition: color 0.15s ease;
}
.password-toggle-btn:hover {
color: var(--gold-bright);
}
.password-toggle-btn svg {
width: 1.15rem;
height: 1.15rem;
}
.alert-success {
background: rgba(51, 128, 90, 0.15);
border: 1px solid var(--green);
color: #9fe2bf;
padding: 0.85rem 1.1rem;
font-size: 0.88rem;
border-radius: 0.125rem;
margin-bottom: 1.25rem;
}
/* ---------- announcement banner ---------- */
.announcement-banner {
background: var(--ground-2);
border-bottom: 1px solid var(--rule);
padding: 0.75rem 0;
font-size: 0.88rem;
position: relative;
z-index: 40;
}
.announcement-inner {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
flex-wrap: wrap;
text-align: center;
}
.announcement-tag {
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 600;
padding: 0.2rem 0.55rem;
border-radius: 0.125rem;
}
.announcement-sep {
color: var(--gold-dim);
user-select: none;
}
.announcement-text {
color: var(--ink);
font-weight: 400;
}
/* banner theme styles */
.banner-holiday {
background: linear-gradient(90deg, rgba(32, 48, 38, 0.9), rgba(15, 12, 10, 0.95), rgba(32, 48, 38, 0.9));
border-bottom-color: var(--rule);
}
.banner-holiday .announcement-tag {
background: rgba(201, 163, 92, 0.18);
border: 1px solid var(--gold-dim);
color: var(--gold-bright);
}
.banner-special {
background: linear-gradient(90deg, rgba(43, 35, 25, 0.9), rgba(15, 12, 10, 0.95), rgba(43, 35, 25, 0.9));
border-bottom-color: var(--gold-dim);
}
.banner-special .announcement-tag {
background: var(--gold);
color: var(--ground-2);
}
.banner-closure {
background: linear-gradient(90deg, rgba(80, 24, 20, 0.85), rgba(15, 12, 10, 0.95), rgba(80, 24, 20, 0.85));
border-bottom-color: rgba(200, 60, 50, 0.4);
}
.banner-closure .announcement-tag {
background: rgba(220, 60, 50, 0.25);
border: 1px solid rgba(220, 80, 70, 0.6);
color: #ffb4b4;
}
.social-row {
margin-top: 1.5rem;
display: flex;
gap: 0.9rem;
}
.social-row a {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.375rem;
height: 2.375rem;
border: 1px solid var(--rule);
border-radius: 0.125rem;
color: var(--ink-muted);
text-decoration: none;
transition: border-color 0.15s ease, color 0.15s ease;
}
.social-row a:hover {
border-color: var(--gold);
color: var(--gold-bright);
}
.social-row svg {
width: 1.0625rem;
height: 1.0625rem;
}
footer .social-row {
margin-top: 1rem;
}
/* ---------- header / nav ---------- */
header.site {
position: sticky;
top: 0;
z-index: 50;
background: rgba(15, 12, 10, 0.86);
backdrop-filter: saturate(140%) blur(0.5rem);
border-bottom: 1px solid var(--rule);
}
.nav-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-block: 0.75rem;
}
.brand {
display: flex;
align-items: center;
gap: 0.65rem;
text-decoration: none;
font-family: var(--font-serif);
font-weight: 600;
letter-spacing: 0.02em;
font-size: 1.05rem;
color: var(--ink);
}
nav.links {
display: flex;
align-items: center;
gap: 1.75rem;
}
nav.links a {
text-decoration: none;
font-size: 0.92rem;
letter-spacing: 0.03em;
color: var(--ink-muted);
padding: 0.4rem 0.2rem;
transition: color 0.15s ease;
}
nav.links a:hover {
color: var(--gold-bright);
}
nav.links a.active {
color: var(--gold-bright);
}
.nav-cta {
display: inline-flex;
align-items: center;
border: 1px solid var(--gold-dim);
color: var(--gold-bright) !important;
padding: 0.45rem 0.9rem;
border-radius: 0.125rem;
font-family: var(--font-mono);
font-size: 0.75rem !important;
letter-spacing: 0.06em;
}
/* mobile nav toggle: hidden on desktop, shown only under the nav.links
breakpoint below. Icon swap is driven by the button's own
aria-expanded state rather than a second JS-toggled class. */
.nav-toggle {
display: none;
background: transparent;
border: none;
padding: 0.5rem;
margin: -0.5rem;
color: var(--ink);
cursor: pointer;
align-items: center;
justify-content: center;
}
.nav-toggle svg {
width: 1.5rem;
height: 1.5rem;
}
.nav-toggle .icon-close {
display: none;
}
.nav-toggle[aria-expanded=true] .icon-menu {
display: none;
}
.nav-toggle[aria-expanded=true] .icon-close {
display: block;
}
@media (max-width: 47.5em) {
.nav-toggle {
display: inline-flex;
}
.nav-cta {
display: none;
}
nav.links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
align-items: stretch;
gap: 0;
background: var(--ground-2);
border-bottom: 1px solid var(--rule);
padding: 0.5rem 0;
}
nav.links.is-open {
display: flex;
}
nav.links a {
padding: 0.9rem 1.5rem;
}
}
/* footer */
footer {
background: var(--ground-2);
border-top: 1px solid var(--rule);
padding-block: 3.5rem 2.5rem;
}
.footer-grid {
display: grid;
grid-template-columns: 1.2fr 1fr 1fr;
gap: 2.5rem;
}
.footer-brand {
display: flex;
align-items: center;
gap: 0.6rem;
text-decoration: none;
}
.footer-brand span {
font-family: var(--font-serif);
font-weight: 600;
color: var(--ink);
font-size: 1rem;
}
footer .col-title {
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--gold-dim);
margin-bottom: 0.9rem;
}
footer address {
margin-top: 1rem;
font-style: normal;
color: var(--ink-muted);
font-size: 0.92rem;
line-height: 1.7;
}
footer .hours-mini {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--ink-muted);
font-variant-numeric: tabular-nums;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
footer a.email {
color: var(--gold-bright);
text-decoration: none;
font-size: 0.92rem;
}
footer a.email:hover {
text-decoration: underline;
}
.footer-bottom {
margin-top: 3rem;
padding-top: 1.75rem;
border-top: 1px solid var(--rule);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
font-size: 0.78rem;
color: var(--ink-muted);
}
@media (max-width: 45em) {
.footer-grid {
grid-template-columns: 1fr;
gap: 2rem;
}
}
.admin-nav {
background: var(--ground-2);
border-bottom: 1px solid var(--rule);
padding: 0.85rem 0;
}
.admin-nav-inner {
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-brand {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
font-family: var(--font-serif);
font-size: 1.15rem;
color: var(--ink);
}
.admin-user {
display: flex;
align-items: center;
gap: 1.25rem;
font-family: var(--font-mono);
font-size: 0.82rem;
color: var(--ink-muted);
}
.admin-user a {
color: var(--gold-bright);
text-decoration: none;
}
.admin-user a:hover {
text-decoration: underline;
}
@media (max-width: 47.5em) {
.admin-nav-inner {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
/* two classes each, so these reliably outrank .admin-nav-links's and
.admin-user's own base-rule gap regardless of source order */
.admin-nav-inner .admin-nav-links,
.admin-nav-inner .admin-user {
flex-wrap: wrap;
gap: 0.5rem 1rem;
}
}
.dashboard-layout {
padding-block: 2.5rem 4rem;
}
.dash-header {
margin-bottom: 2rem;
}
.dash-header h1 {
margin-top: 0.4rem;
font-size: 2rem;
color: var(--ink);
}
/* admin nav links */
.admin-nav-links {
display: flex;
gap: 1.25rem;
align-items: center;
}
.admin-nav-links a {
font-family: var(--font-mono);
font-size: 0.82rem;
letter-spacing: 0.05em;
text-decoration: none;
color: var(--ink-muted);
padding: 0.35rem 0.65rem;
border-radius: 0.125rem;
transition: color 0.15s ease, background 0.15s ease;
}
.admin-nav-links a:hover {
color: var(--gold-bright);
background: var(--surface);
}
.admin-nav-links a.active {
color: var(--gold-bright);
background: var(--surface);
border-bottom: 1px solid var(--gold);
}
.admin-toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
margin-bottom: 2rem;
}
.hero-brand {
margin: 0;
line-height: 0;
}
.hero-logo {
display: block;
width: clamp(17.5rem, 28vw, 31rem);
max-width: 100%;
height: auto;
margin: 0 auto 1.75rem;
}
/* hours card alert callout */
.hours-alert {
margin-bottom: 1.25rem;
padding: 0.85rem 1rem;
border-radius: 0.2rem;
font-size: 0.88rem;
line-height: 1.45;
}
.hours-alert strong {
display: block;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 0.25rem;
}
.hours-alert.alert-holiday {
background: rgba(51, 80, 63, 0.25);
border: 1px solid rgba(201, 163, 92, 0.3);
color: var(--ink);
}
.hours-alert.alert-holiday strong {
color: var(--gold-bright);
}
.hours-alert.alert-special {
background: rgba(43, 35, 25, 0.6);
border: 1px solid var(--gold-dim);
color: var(--ink);
}
.hours-alert.alert-special strong {
color: var(--gold-bright);
}
.hours-alert.alert-closure {
background: rgba(80, 24, 20, 0.3);
border: 1px solid rgba(220, 80, 70, 0.5);
color: #ffdfdf;
}
.hours-alert.alert-closure strong {
color: #ffb4b4;
}
/* ---------- hero ---------- */
.hero {
position: relative;
min-height: calc(100vh - 4.5rem);
display: flex;
align-items: center;
justify-content: center;
padding-block: clamp(4.5rem, 8vh, 7.5rem);
text-align: center;
overflow: hidden;
background: var(--ground);
/* fallback if brick-texture.png fails to load */
}
.hero::before {
content: "";
position: absolute;
inset: 0;
z-index: 1;
opacity: 0.6;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.12), transparent 22%, transparent 78%, rgba(0, 0, 0, 0.18));
pointer-events: none;
}
.hero-media {
position: absolute;
inset: 0;
z-index: 0;
overflow: hidden;
background-image: url("./assets/brick-texture.png");
background-repeat: repeat;
}
.hero-scrim {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(15, 12, 10, 0.35) 0%, rgba(15, 12, 10, 0.2) 45%, rgba(15, 12, 10, 0.6) 100%), radial-gradient(ellipse 60% 48% at 50% 18%, rgba(201, 163, 92, 0.16), transparent 58%);
}
.hero-inner {
position: relative;
z-index: 2;
}
.hero .eyebrow {
margin-bottom: 1rem;
}
.hero h1 {
font-size: clamp(3.1rem, 9vw, 6.2rem);
color: var(--gold-bright);
letter-spacing: -0.01em;
}
.hero .sub {
font-family: var(--font-serif);
font-style: italic;
font-weight: 400;
font-size: clamp(1.15rem, 2.6vw, 1.55rem);
color: var(--ink-muted);
margin-top: 0.35rem;
}
.tagline-row {
margin-top: 2rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
/* column-gap carries the spacing now (was 0 — the dot's own margin
used to be the only thing separating the phrases). Needs to be a
real flex gap, not a margin on the dot, so removing a hidden dot
from flow (see .wrap-sep-hidden) still leaves normal spacing
between the two phrases instead of running them together. */
gap: 0.6rem 0.7rem;
font-family: var(--font-mono);
font-size: 0.82rem;
letter-spacing: 0.04em;
color: var(--ink-muted);
}
/* Real element instead of a ::after pseudo-element — needs to be a
measurable, independently-toggleable DOM node for the wrap-aware
script in main.js to hide it when its neighbors wrap apart. No margin
of its own: the column-gap above already provides 0.7rem on each
side of it (matching the old margin-inline's total spacing exactly),
and still applies directly between the two phrases whenever the dot
itself is removed from flow. */
.tag-sep {
color: var(--gold-dim);
user-select: none;
}
.section-head {
margin-bottom: 2.75rem;
}
.section-head h2 {
font-size: clamp(1.9rem, 3.6vw, 2.6rem);
color: var(--ink);
margin-top: 0.5rem;
}
/* about band */
.about-grid {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 3.5rem;
align-items: start;
}
.about-grid p + p {
margin-top: 1rem;
}
.about-grid h2 {
margin-top: 0.6rem;
}
.about-grid h2 + .lede {
margin-top: 1.5rem;
}
.about-grid .lede {
max-width: 56ch;
color: var(--ink);
font-size: 1.05rem;
}
@media (max-width: 51.25em) {
.about-grid {
grid-template-columns: 1fr;
}
}
/* hours / visit band */
.visit {
background: var(--ground-2);
}
.visit-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.75rem;
}
.hours-list {
margin: 0;
padding: 0;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size: 0.92rem;
}
.hours-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.55rem 0;
border-bottom: 1px solid var(--rule);
}
.hours-row:last-child {
border-bottom: none;
}
.hours-row dt {
margin: 0;
color: var(--ink-muted);
letter-spacing: 0.04em;
font-weight: normal;
}
.hours-row dd {
margin: 0;
text-align: right;
color: var(--ink);
}
.find-us .pin {
width: 2.125rem;
height: 2.125rem;
margin-bottom: 1rem;
}
.find-us address {
font-style: normal;
font-size: 1rem;
color: var(--ink);
}
.find-us .contact-list {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.find-us .contact-list a {
text-decoration: none;
color: var(--gold-bright);
font-size: 0.92rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
width: fit-content;
border-bottom: 1px solid transparent;
}
.find-us .contact-list a:hover {
border-bottom-color: var(--gold-dim);
}
@media (max-width: 45em) {
.visit-grid {
grid-template-columns: 1fr;
}
}
/* live music band */
.music {
background: linear-gradient(180deg, var(--green-deep), var(--green));
color: var(--ink);
text-align: center;
}
.music-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3.5rem;
align-items: center;
text-align: left;
margin-top: 1.5rem;
}
.music-content {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.music-photo-wrap {
position: relative;
border-radius: 0.25rem;
overflow: hidden;
border: 1px solid rgba(201, 163, 92, 0.35);
box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.45);
}
.music-photo {
width: 100%;
height: auto;
display: block;
object-fit: cover;
aspect-ratio: 4/3;
object-position: center top;
}
.music .section-head {
margin-bottom: 1.5rem;
text-align: left;
}
.music .section-head .eyebrow {
color: var(--gold-bright);
}
.music p.body {
max-width: 58ch;
margin: 0;
font-size: 1.02rem;
color: #e4ddc9;
line-height: 1.65;
}
.music .btn-outline {
border-color: rgba(230, 199, 137, 0.45);
}
.music .hero-ctas {
margin-top: 2rem;
justify-content: flex-start;
}
@media (max-width: 50em) {
.music-grid {
grid-template-columns: 1fr;
gap: 2.5rem;
text-align: center;
}
.music-content {
align-items: center;
}
.music .section-head {
text-align: center;
}
.music .hero-ctas {
justify-content: center;
}
}
/* menu band */
.menu-band .container {
max-width: 60rem;
text-align: center;
}
.menu-band p.body {
font-size: 1.02rem;
color: var(--ink-muted);
max-width: 56ch;
margin: 0 auto;
}
.menu-band .hero-ctas {
margin-top: 2.25rem;
justify-content: center;
}
.menu-band .section-head {
text-align: center;
}
.menu-band .rule-row {
margin-bottom: 1.75rem;
}
/* public online menu section */
.menu-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
gap: 2.5rem;
margin-top: 3rem;
text-align: left;
}
/* background/border come from the shared .panel rule above; no
margin-bottom here since these sit in a grid whose own gap spaces them */
.menu-category {
border-radius: 0.25rem;
padding: 2rem;
}
.menu-category-header {
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--rule);
}
.menu-category-header h3 {
font-size: 1.45rem;
color: var(--gold-bright);
}
.menu-category-header p {
margin-top: 0.35rem;
font-size: 0.88rem;
color: var(--ink-muted);
}
.menu-items-list {
display: flex;
flex-direction: column;
gap: 1.4rem;
}
.menu-item-row {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.menu-item-top {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 0.75rem;
}
.menu-item-name {
font-family: var(--font-serif);
font-weight: 600;
font-size: 1.05rem;
color: var(--ink);
display: inline-flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.menu-item-price {
font-family: var(--font-mono);
font-weight: 500;
font-size: 0.95rem;
color: var(--gold-bright);
white-space: nowrap;
}
.menu-item-desc {
font-size: 0.88rem;
color: var(--ink-muted);
line-height: 1.45;
}
.menu-item-badge {
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.15rem 0.45rem;
border-radius: 0.125rem;
background: rgba(201, 163, 92, 0.15);
border: 1px solid var(--gold-dim);
color: var(--gold-bright);
font-weight: normal;
}
/* menu page specific headers & spacing */
.menu-page-header {
padding-block: 4.5rem 2.5rem;
}
.menu-page-content {
padding-bottom: 5.5rem;
}
/* ---------- admin / management styles ---------- */
.admin-login-body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.login-card {
border-radius: 0.25rem;
padding: 2.5rem 2rem;
width: 100%;
max-width: 25rem;
box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.4);
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header img {
width: 2rem;
height: 2rem;
margin: 0 auto 0.75rem;
}
.login-header h1 {
font-size: 1.5rem;
color: var(--gold-bright);
}
.login-header p {
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--ink-muted);
margin-top: 0.35rem;
}
.login-btn {
width: 100%;
justify-content: center;
margin-top: 0.5rem;
}
.back-link {
display: block;
text-align: center;
margin-top: 1.5rem;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--ink-muted);
text-decoration: none;
}
.back-link:hover {
color: var(--gold-bright);
}
/* two classes to reliably outrank .panel h2's margin-bottom above */
.category-panel .cat-name-heading {
margin-bottom: 0.25rem;
grid-area: name;
}
.cat-desc-text {
font-size: 0.88rem;
}
.category-panel .cat-desc-text {
grid-area: desc;
}
/* Category panel-head as a grid: title on its own top row, description
and the move/edit/delete buttons sharing the row below with the
buttons pinned to the far end (matches the desktop arrangement this
replaced — see the mobile override for how that changes below 760px). */
.category-panel .panel-head {
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas: "name actions" "desc actions";
align-items: center;
column-gap: 0.75rem;
}
.category-panel .panel-head .row-actions {
grid-area: actions;
}
/* Wraps the item name and its optional badge (reusing the public menu's
.menu-item-badge pill) so they sit on one inline-flowing line instead
of giving the badge its own column. */
.item-name-row {
display: inline-flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.item-desc-cell {
font-size: 0.82rem;
margin-top: 0.2rem;
}
.item-price-cell {
color: var(--gold-bright);
font-weight: 500;
}
/* mobile reflow for the category header — shares the breakpoint and
reflow approach used by the item/announcement/user list grids in
admin-data-grid, but this one rule is specific to the menu editor's
category panel, so it lives here instead of there. */
@media (max-width: 47.5em) {
.category-panel .panel-head {
grid-template-columns: 1fr;
grid-template-areas: "name" "desc" "actions";
row-gap: 0.5rem;
}
}
/* ---------- admin: pub hours ---------- */
/* Same subgrid technique as the item/ann/user lists above (see the
comment near .item-grid-wrap), but with no grid-template-areas: every
row's five fields sit in the same left-to-right order on both desktop
and mobile, so plain column order is enough — nothing needs to reflow. */
.hours-grid-wrap {
display: grid;
grid-template-columns: auto auto 1fr 1fr 1fr;
column-gap: 0.75rem;
margin-top: 1rem;
}
.hours-day-rows {
display: contents;
}
.hours-grid-head,
.hours-day-row {
display: grid;
grid-column: 1/-1;
grid-template-columns: subgrid;
align-items: center;
}
.hours-grid-head {
padding: 0 0.75rem 0.85rem;
border-bottom: 1px solid var(--rule);
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gold);
font-weight: 500;
}
.hours-day-row {
padding: 0.85rem 0.75rem;
border-bottom: 1px solid var(--rule);
}
.hours-day-row:last-child {
border-bottom: none;
}
.hours-day-row .form-control {
min-width: 7rem;
}
.hours-day-row .day-time-field:disabled {
opacity: 0.4;
}
@media (max-width: 47.5em) {
.hours-grid-wrap {
grid-template-columns: 1fr;
}
.hours-grid-head {
display: none;
}
.hours-day-row {
grid-template-columns: 1fr;
row-gap: 0.5rem;
border: 1px solid var(--rule);
border-radius: 0.25rem;
padding: 0.9rem 1rem;
margin-bottom: 0.85rem;
}
.hours-day-row:last-child {
margin-bottom: 0;
}
.hours-day-row [data-label]::before {
content: attr(data-label);
display: block;
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gold);
margin-bottom: 0.3rem;
}
.hours-day-row .form-control {
width: 100%;
}
}
/* Live preview panel: mimics the public site's Visit card and footer
mini-summary without depending on their .card/footer ancestor scoping
(this lives inside an admin .panel, which already shares that same
surface look — nesting the real classes here would just blend in). */
.hours-preview {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.hours-preview-card,
.hours-preview-footer {
background: var(--ground-2);
border: 1px solid var(--rule);
border-radius: 0.2rem;
padding: 1.5rem;
}
.hours-preview-card .note {
margin-top: 1.25rem;
font-size: 0.88rem;
color: var(--ink-muted);
}
.hours-preview-card .note strong {
color: var(--ink);
font-weight: 600;
}
.hours-preview-footer .col-title {
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--gold-dim);
margin-bottom: 0.9rem;
}
.hours-preview-footer .hours-mini {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--ink-muted);
font-variant-numeric: tabular-nums;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
@media (max-width: 47.5em) {
.hours-preview {
grid-template-columns: 1fr;
}
}
/* ---------- admin: my account ---------- */
/* One panel, fields in the shared two-column .form-grid (which already
stacks below 47.5em). Capped so the fields don't stretch across the
full dashboard width on large screens. */
.account-panel {
max-width: 56rem;
}
/*# sourceMappingURL=styles.css.map */