/* ── SELF-HOSTED FONTS ───────────────────────────────────────────────── */
@font-face { font-family: 'Manrope'; font-style: normal; font-weight: 200 800; font-display: swap; src: url('../fonts/manrope-variable.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: normal; font-weight: 700; font-display: swap; src: url('../fonts/fraunces-700-normal.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: normal; font-weight: 900; font-display: swap; src: url('../fonts/fraunces-900-normal.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: italic; font-weight: 300; font-display: swap; src: url('../fonts/fraunces-300-italic.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: italic; font-weight: 700; font-display: swap; src: url('../fonts/fraunces-700-italic.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/jetbrains-mono-400.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 600; font-display: swap; src: url('../fonts/jetbrains-mono-600.woff2') format('woff2'); }
/* ════════════════════════════════════════════════════════════════════════
base.css — Fonts, reset, page shell, and top navigation.
════════════════════════════════════════════════════════════════════════
COLOUR PALETTE (the "warm parchment & brass" theme)
──────────────────────────────────────────────────────────────────────
#f5edd6 parchment page background
#fffcf2 soft cream cards, search box, table
#fdf8ee warm cream toolbars, legend, about block
#f0e4c4 pale honey table heading row
#fef3dc warm hover buttons on hover
#fef8ea row hover table row on hover
#fff8e4 "now" row the currently-active hour
#1e1208 espresso headings + emphasis
#4a3420 warm dark body text in the table
#6b4f2a muted bronze subtle emphasis
#7a5c2a tagline brown italic intro line
#9a7d5a soft brown column toggle text
#b09870 warm grey muted labels
#c0a880 pale dust night-row text, footer
#c8922a BRASS brand accent (links, dial, focus)
#a06e18 deep brass hover state for some links
#c9b08a warm border panel borders
#d4c0a0 soft border toolbar/legend borders
#ede4cc line table row dividers
FONTS
──────────────────────────────────────────────────────────────────────
Fraunces serif display titles, day labels, hour cell
Manrope body sans-serif paragraphs, small labels
JetBrains Mono numeric monospace table data
════════════════════════════════════════════════════════════════════════ */
/* ── 1. FONTS + RESET ───────────────────────────────────────────────── */
/* Font loading is handled in index.html via a non-blocking
to avoid render-blocking. The @import has been intentionally removed. */
*,
*:before,
*:after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* ── 2. PAGE BACKGROUND & SHELL ─────────────────────────────────────── */
body {
background: #f5edd6; /* parchment */
}
.utci-app {
font-family: Manrope, sans-serif;
color: #1e1208;
background: #f5edd6;
min-height: 100vh;
/* The side padding, as a variable: the header and the event note both
full-bleed by cancelling it, and would otherwise each need the number
restating at every breakpoint that changes it. */
--app-pad: 36px;
/* No top padding: .utci-topnav is a real header at the very top of the flow
now, and supplies its own. Was 52px, reserved for the nav when it was
absolutely positioned over this space. */
padding: 0 var(--app-pad) 32px;
position: relative;
/* overflow-x: clip (not hidden) — `clip` clips horizontal overflow
WITHOUT making this a scroll container, so `position: sticky` on
descendants (table
) can still stick relative to the viewport.
`hidden` would create a containing block that breaks sticky. */
overflow-x: clip;
}
/* The centred container that holds everything. */
.utci-shell {
position: relative;
z-index: 1;
margin: 0 auto;
}
/* ── 3. SITE HEADER (wordmark left, nav links right) ───────────────── */
/* Rendered as — the same element the static pages
(about.html et al) use — and it now carries the wordmark and tagline that
used to be repeated inside .utci-header. It sits FIRST in the flow, at the
very top of the page, with the event banner beneath it.
Full-bleed: .utci-app pads its content in by --app-pad a side, so the header
cancels that with a matching negative margin and pads itself instead — the
bar's rule reaches both page edges while its contents still line up with
the content below. */
.utci-topnav {
position: relative; /* keeps z-index working without leaving the flow */
z-index: 100;
margin: 0 calc(var(--app-pad) * -1) 18px;
display: flex;
align-items: center;
justify-content: flex-end; /* .nav-logo's margin-right:auto splits the row */
padding: 18px var(--app-pad);
gap: 28px;
background: #f5edd6;
border-bottom: 1px solid #e0d0a8;
}
.utci-topnav a {
font-family: Manrope, sans-serif;
font-size: 16px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #b09870;
text-decoration: none;
border-bottom: 1px solid transparent;
padding-bottom: 2px;
transition: color 0.2s, border-color 0.2s;
}
.utci-topnav a:hover {
color: #1e1208;
border-bottom-color: #1e1208;
}
/* Desktop: nav-links renders inline as part of the header's flex row */
.utci-topnav .nav-links {
display: contents; /* participates in the header's flex layout transparently */
}
/* ── About dropdown ─────────────────────────────────────────────────── */
.utci-topnav .nav-dropdown {
position: relative;
display: inline-flex;
align-items: center;
}
/* Invisible bridge so hover doesn't break between toggle and menu */
.utci-topnav .nav-dropdown::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 100%;
height: 4px;
}
.utci-topnav .nav-dropdown-toggle .caret {
display: inline-block;
margin-left: 4px;
font-size: 0.7em;
transition: transform 0.2s;
}
.utci-topnav .nav-dropdown:hover .nav-dropdown-toggle .caret,
.utci-topnav .nav-dropdown:focus-within .nav-dropdown-toggle .caret {
transform: rotate(180deg);
}
.utci-topnav .nav-dropdown-menu {
position: absolute;
top: calc(100% - 1px);
left: 50%;
margin-top: 0;
background: #f5edd6;
border: 1px solid #e0d0a8;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(100, 70, 20, 0.12);
display: flex;
flex-direction: column;
min-width: 210px;
padding: 6px;
opacity: 0;
visibility: hidden;
transform: translateX(-50%) translateY(-4px);
transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease;
z-index: 110;
}
.utci-topnav .nav-dropdown:hover .nav-dropdown-menu,
.utci-topnav .nav-dropdown:focus-within .nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
/* The Account dropdown sits at the far right of the nav row, so a menu
centred under it (like About's) would spill past the viewport edge on
most screen widths. Right-align it to the toggle instead, growing left. */
.utci-topnav .nav-dropdown--right .nav-dropdown-menu {
left: auto;
right: 0;
transform: translateY(-4px);
}
.utci-topnav .nav-dropdown--right.nav-dropdown:hover .nav-dropdown-menu,
.utci-topnav .nav-dropdown--right.nav-dropdown:focus-within .nav-dropdown-menu {
transform: translateY(0);
}
/* Labels align to the same right edge the menu itself hangs from. */
.utci-topnav .nav-dropdown--right .nav-dropdown-menu a {
text-align: right;
}
.utci-topnav .nav-dropdown-menu a {
text-transform: none;
letter-spacing: normal;
font-size: 14px;
font-weight: 600;
color: #5a3e1a;
padding: 8px 12px;
border-radius: 6px;
border-bottom: none;
white-space: nowrap;
}
.utci-topnav .nav-dropdown-menu a:hover {
background: #ede4cc;
color: #1e1208;
border-bottom: none;
}
/* ── Event note ─────────────────────────────────────────────────────── */
/* Active cosmic/weather events shown as a strip across the content width,
directly under the site header. It is IN FLOW — it pushes the page down
instead of covering anything, which is the whole point of it not being a
flyout. One event at a time with its full message, cycling with a fade.
It used to render ABOVE the nav, which meant repositioning the nav out of
its absolute placement whenever a note was present. The header owns the top
of the page now, so the note is simply the first thing under it — on desktop
and on mobile (where the header is fixed and .utci-app pads clear of it)
alike, with no override either way. */
.event-note {
display: flex;
align-items: baseline;
gap: 11px;
padding: 10px 15px;
margin-bottom: 14px;
background: #fdf8ee;
border: 1.5px solid #c9b08a;
border-radius: 4px;
position: relative;
overflow: hidden;
animation: event-note-in 0.35s cubic-bezier(0.34, 1.25, 0.64, 1) both;
}
@keyframes event-note-in {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
/* Per-event colour, kept to a hairline so the strip stays parchment —
same device as .almanac-entry-accent. */
.event-note-accent {
position: absolute;
inset: 0;
opacity: 0.75;
pointer-events: none;
/* One element, drawn as an L: a 1px strip down the left that turns and runs
along the bottom. The note's own overflow: hidden rounds the corner off
with the rest of the box. */
clip-path: polygon(
0 0,
1px 0,
1px calc(100% - 1px),
100% calc(100% - 1px),
100% 100%,
0 100%
);
}
/* Safety-relevant weather (heat, frost, storm, wind, wet) — brass emphasis.
This is the fallback: when the event carries a weather tint, app.js
overrides background/border inline with the matching day-tab colour and
adds .is-tinted (below). */
.event-note.is-priority {
background: #f0e4c4;
border-color: #c8922a;
}
/* Tinted warning banner — the hue now carries the "this is a warning" signal,
so the copy drops the brass accents for neutral dark tones that stay
readable on any of the palette's hues (orange, blue, grey). */
.event-note.is-tinted .event-note-msg {
color: #3f3222;
}
.event-note.is-tinted.is-priority .event-note-dates {
color: #56452c;
}
.event-note.is-tinted .event-note-dot {
background: rgba(0, 0, 0, 0.18);
}
.event-note.is-tinted .event-note-dot:hover {
background: rgba(0, 0, 0, 0.34);
}
.event-note.is-tinted .event-note-dot.active {
background: rgba(0, 0, 0, 0.55);
}
/* Slide stack. Every active event is rendered and every slide is placed in the
same single grid cell, so the strip's height is set by the TALLEST slide and
stays put as the rotation cycles — a one-line event followed by a message
that wraps to two lines no longer grows the banner and shunts the page. */
.event-note-stack {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: start;
flex: 1;
min-width: 0;
}
.event-note-slide {
grid-area: 1 / 1; /* stack all slides on top of each other */
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
opacity: 0; /* only .is-active is visible */
pointer-events: none;
user-select: none; /* keeps hidden copy out of a drag-selection */
transition: opacity 0.35s ease;
}
.event-note-header {
display: flex;
align-items: baseline;
gap: 6px;
}
.event-note-body {
display: flex;
align-items: baseline;
gap: 10px;
min-width: 0;
}
.event-note-slide.is-active {
opacity: 1;
pointer-events: auto;
user-select: auto;
}
.event-note-emoji {
font-size: 16px;
line-height: 1;
flex-shrink: 0;
}
.event-note-title {
font-family: Fraunces, serif;
font-style: italic;
font-weight: 700;
font-size: 14px;
color: #2a1a08;
flex-shrink: 0;
}
.event-note-msg {
font-family: Manrope, sans-serif;
font-size: 12.5px;
font-weight: 500;
line-height: 1.45;
color: #6b4f2a;
min-width: 0;
}
.event-note-dates {
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #9a7d5a;
white-space: nowrap;
margin-left: auto; /* push the dates to the right-hand end */
flex-shrink: 0;
}
.event-note.is-priority .event-note-dates {
color: #a06e18;
}
.event-note-dots {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
align-self: center;
}
.event-note-dot {
width: 6px;
height: 6px;
padding: 0;
border: none;
border-radius: 50%;
background: #d9c39b;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.event-note-dot:hover {
background: #c0a880;
}
.event-note-dot.active {
background: #c8922a;
transform: scale(1.3);
}
/* Close control — a small brass "Close ×" pill in the top-right corner. It
sits at the end of the strip's flex row rather than being absolutely
positioned, so the dates line can never run underneath it. */
.event-note-close {
display: inline-flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
align-self: flex-start;
margin: -2px -6px 0 2px; /* pull into the padding so it hugs the corner */
padding: 3px 7px;
border: 1px solid transparent;
border-radius: 999px;
background: transparent;
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #9a7d5a;
line-height: 1;
cursor: pointer;
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.event-note-close:hover {
background: rgba(200, 146, 42, 0.14);
border-color: rgba(200, 146, 42, 0.45);
color: #7a5a1e;
}
.event-note-close:focus-visible {
outline: 2px solid #c8922a;
outline-offset: 1px;
}
.event-note.is-priority .event-note-close {
color: #a06e18;
}
.event-note.is-tinted .event-note-close {
color: #56452c;
}
.event-note.is-tinted .event-note-close:hover {
background: rgba(0, 0, 0, 0.09);
border-color: rgba(0, 0, 0, 0.22);
color: #2a1a08;
}
/* Closing: the slot's height runs down to zero (measured and driven inline by
noteClose in app.js) and the note is anchored to its BOTTOM edge, so the
banner rides up behind the header's seam while everything below it rises by
the same amount at the same rate. One animated property for both movements —
a transform on the note would slide it independently of the page, and a
transform on the content below would break the fixed-position controls
living inside it. */
.event-note-slot {
/* The gap under the banner lives here rather than on the note, and is
deliberately equal to what the header gets back when .has-event-note is
dropped at the end of the close: the header's own margin-bottom (18px,
zeroed while a note is attached) plus, on a phone, the 8px .utci-app
stops padding once the note is gone. Without that, the space swapped in
at unmount showed up as a jump. */
margin-bottom: 18px;
display: grid;
grid-template-rows: 1fr; /* the row fills the slot, whatever its height */
align-items: end; /* ...and the note hangs from its bottom edge */
overflow: hidden;
}
.event-note-slot > .event-note {
min-height: 0; /* grid items floor at min-content without this */
}
/* The entry animation is fill-mode: both, so its final transform would stay
applied — cleared so it cannot offset the note against the slot. */
.event-note.is-closing {
animation: none;
pointer-events: none;
}
/* The note follows the header in the flow now, so it needs no special
positioning — only the gap the header's own margin-bottom would otherwise
have given the content it sits in front of. */
@media (max-width: 640px) {
.event-note {
align-items: flex-start;
gap: 9px;
padding: 10px 12px;
}
.event-note-dates {
margin-left: 0; /* no room to right-align on a phone */
}
/* Phone: the cross alone, pushed to the right-hand end of the row and
padded out to a ~26px touch target. */
.event-note-close {
margin: -4px -6px 0 auto;
padding: 8px;
}
.event-note-close-label {
display: none;
}
}
/* ── Note attached to the header ─────────────────────────────────────
When a note is present it hangs off the header's bottom edge rather than
floating below it as a separate panel: the header drops the gap it would
otherwise leave, and the note keeps a square top with no top border, inset
to the content edge so there is a gap either side. The header's own bottom
rule becomes the seam the strip hangs from.
Placed after the ≤640px block above deliberately — these override the
note's base padding and border at every width, mobile included. */
.has-event-note .utci-topnav {
margin-bottom: 0;
}
.has-event-note .event-note {
/* Inset to the content edge rather than full-bleed, so the strip keeps a
gap either side of it. The gap below is the slot's — see .event-note-slot. */
margin: 0;
padding: 10px 15px;
/* No top border and square top corners: the header's own bottom rule is the
seam it hangs from. A tinted note's inline borderColor still tints what is
left, since that sets the colour only and this sets style and width. */
border: 1.5px solid #c9b08a;
border-top: none;
border-radius: 0 0 4px 4px;
}
@media (max-width: 640px) {
/* Matches the two-class specificity above so the phone's tighter padding
survives the override. */
.has-event-note .event-note {
padding: 10px 12px;
}
/* 18px + the 8px of .utci-app padding-top below — see .event-note-slot. */
.event-note-slot {
margin-bottom: 26px;
}
/* The bar is position:fixed and 72px tall on a phone, and .utci-app pads 80px
clear of it — the spare 8px would show as a gap above the note. Two classes
so it outranks the single-class padding-top in ui.css whatever the order. */
.utci-app.has-event-note {
padding-top: 72px;
}
}
/* The site identity, in the header on every viewport. Was display:none here
and unhidden only on mobile, back when the big .utci-title wordmark carried
the desktop identity instead. */
.nav-logo {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
flex-shrink: 0;
/* Same device about.css uses on .nav-logo: one auto margin pushes everything
after it to the right-hand end, which works with .nav-links staying
display:contents. */
margin-right: auto;
}
.nav-logo-wordmark {
display: flex;
align-items: center;
gap: 0.05em;
/* It is the page's
now, so the browser's own h1 margin and 2em size are
cleared here — the wordmark's size and weight come from .nav-logo-sun and
.nav-logo-scope, and the icon is sized in rem. */
margin: 0;
font-size: 1rem;
font-weight: inherit;
}
.nav-logo-sun,
.nav-logo-scope {
font-family: Manrope, sans-serif;
font-weight: 250;
/* 1.6rem matches .nav-logo-sun in about.css, so the wordmark is the same size on the
forecast and on every static page. The phone bar scales it back up (see the
≤640px block) where it is the centred focal point rather than a corner mark. */
font-size: 1.6rem;
color: #1e1208;
letter-spacing: -0.01em;
}
.nav-logo-icon {
width: 1.26rem; /* same 0.79 ratio to the wordmark as .nav-logo-icon */
height: 1.26rem;
color: #1e1208;
flex-shrink: 0;
}
.nav-logo-tag {
display: block;
font-family: Manrope, sans-serif;
font-style: normal;
font-weight: 600;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #7a5c2a;
margin-top: 3px;
}
/* Desktop: overlay hidden */
.utci-topnav .nav-overlay {
display: none;
}
/* ── Burger button (hidden on desktop) ─────────────────────────────── */
.utci-burger {
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
width: 36px;
height: 36px;
background: none;
border: none;
cursor: pointer;
padding: 4px;
z-index: 110;
flex-shrink: 0;
}
.utci-burger span {
display: block;
width: 22px;
height: 2px;
background: #7a5c2a;
border-radius: 2px;
transition: transform 0.25s ease, opacity 0.2s ease;
transform-origin: center;
}
/* ── Mobile nav drawer ──────────────────────────────────────────────── */
@media (max-width: 640px) {
.utci-app {
padding: 96px 16px 24px; /* 96px = 72px nav height + 24px breathing room */
}
/* Show wordmark in nav bar, hide it from main content */
.nav-logo {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
text-align: center;
pointer-events: none;
}
.nav-logo-wordmark {
display: block;
line-height: 1;
}
.nav-logo-sun,
.nav-logo-scope {
font-size: 2.1rem;
}
.nav-logo-icon {
width: 1.65rem;
height: 1.65rem;
}
/* Phone keeps the bar pinned to the viewport, with the wordmark centred in
it and the links behind the burger. The offsets are set here rather than
inherited: the desktop rule is in-flow now and carries no top/left/right,
and its full-bleed negative margin has nothing to cancel once the bar is
fixed and already edge-to-edge. */
.utci-topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
margin: 0;
padding: 10px 20px;
height: 72px;
background: #f5edd6;
border-bottom: 1px solid #e0d0a8;
}
.utci-burger {
display: flex;
}
/* Hide the links by default on mobile — shown in drawer */
.utci-topnav .nav-links {
position: fixed;
top: 0;
right: -100%;
width: 220px;
height: 100vh;
background: #f5edd6;
border-left: 1px solid #e0d0a8;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding: 80px 32px 32px;
gap: 28px;
transition: right 0.28s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: -8px 0 24px rgba(100,70,20,0.08);
z-index: 105;
}
.utci-topnav .nav-links.is-open {
right: 0;
}
.utci-topnav .nav-links a {
font-size: 15px;
}
/* Dropdown collapses to an indented static list inside the drawer */
.utci-topnav .nav-dropdown {
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 18px;
}
.utci-topnav .nav-dropdown-toggle .caret { display: none; }
.utci-topnav .nav-dropdown-menu {
position: static;
opacity: 1;
visibility: visible;
transform: none;
box-shadow: none;
border: none;
background: none;
padding: 0 0 0 14px;
margin-top: 0;
min-width: 0;
gap: 18px;
}
.utci-topnav .nav-dropdown-menu a { padding: 0; font-size: 14px; }
/* Right-aligned text is a desktop-popup touch (see the --right rule
above) - on mobile everything is one left-indented list, so match
About's list instead of standing out. */
.utci-topnav .nav-dropdown--right .nav-dropdown-menu a { text-align: left; }
/* Burger → X animation */
.utci-topnav.nav-open .utci-burger span:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.utci-topnav.nav-open .utci-burger span:nth-child(2) {
opacity: 0;
transform: scaleX(0);
}
.utci-topnav.nav-open .utci-burger span:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
/* Overlay behind drawer */
.utci-topnav .nav-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(30,18,8,0.35);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
z-index: 104;
}
.utci-topnav.nav-open .nav-overlay {
opacity: 1;
pointer-events: auto;
}
}