/* ─────────────────────────────────────────────────────────────────────────────
   SAVE ME STARS — single source of truth for the site's look.
   THEME: Vintage Almanac / occult print. Aged paper, iron-gall ink, rust-red
   second color; poker carried by vintage-card language (suit ornaments, chip
   medallions, corner-indexed card tiles, miniature card faces).

   Served at /site.css. Linked by BOTH:
     - the React app (index.html)
     - every generated static page (scripts/build-forecasts.js → pageShell)
   Restyling the site = editing this file. Nothing else holds styling.

   Layout:
     1. Palette variables
     2. Base + shared components (app and static pages)
     3. React-app specifics (legacy class names remapped to almanac values)
     4. Static-page layout — scoped under body.forecast
   ──────────────────────────────────────────────────────────────────────────── */

/* 1 ─── PALETTE ─────────────────────────────────────────────────────────── */

:root {
  --paper: #efe6d0;
  --paper-card: rgba(255, 252, 240, 0.55);
  --paper-bright: #fffcf0;
  --ink: #2e2418;
  --rust: #94391f;
  --rust-deep: #6e2712;
  --faded: #6f5f47;
  --rule: #4a3c2a;
  /* Border hierarchy. The 3px double rule used to sit on everything — twelve
     sign cards, the climate panel, the newsletter and the footer, seventeen
     rules on one forecast page — so it stopped meaning anything. It is now
     reserved for the footer's baseline (and the share-card frames, drawn in
     build-og-cards.js). The climate panels wear a parchment card instead, the
     sign cards take this hairline, and plain separators fade out at the ends
     (see .newsletter). Do not promote anything back to the double rule. */
  --rule-soft: rgba(74, 60, 42, 0.34);
}

/* 2 ─── BASE + SHARED COMPONENTS ────────────────────────────────────────── */

body {
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cg fill='%232e2418' fill-opacity='0.026' font-size='17'%3E%3Ctext x='18' y='38'%3E%E2%99%A0%3C/text%3E%3Ctext x='88' y='108'%3E%E2%99%A6%3C/text%3E%3C/g%3E%3C/svg%3E") repeat,
    radial-gradient(ellipse at 50% 0%, rgba(148, 57, 31, 0.05), transparent 55%),
    linear-gradient(180deg, #f0e7d1 0%, #eadfc4 50%, #e4d7b8 100%);
  background-attachment: fixed;
  color: var(--ink);
}

.display { font-family: 'IM Fell English SC', serif; letter-spacing: 0.04em; font-weight: 400; }
.body-italic { font-family: 'EB Garamond', serif; font-style: italic; }
/* Upright sibling, for long-form passages. Italic Garamond suits a one-line
   charm; five sentences of it is the hardest text on the site to read, and
   the climate is the site's most-read paragraph. */
.body-serif { font-family: 'EB Garamond', serif; }
.mono { font-family: 'EB Garamond', serif; letter-spacing: 0.22em; }

/* paper grain (multiply) — replaces the old dark felt + grain */
.felt-bg { background: transparent; }
.grain::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' /%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.4'/%3E%3C/svg%3E");
  opacity: 0.10;
  pointer-events: none;
  mix-blend-mode: multiply;
  z-index: 1;
}

/* Confirmation banner: shown on the home page when Kit sends a newly confirmed
   subscriber to /?confirmed=1. Parchment note in the almanac palette. */
.confirm-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  max-width: 620px;
  margin: 0 auto 2.5rem;
  padding: 0.85rem 1.3rem;
  background: linear-gradient(180deg, #f4ecd8 0%, #ece1c8 100%);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--rust);
  border-radius: 8px;
  box-shadow: 0 6px 22px rgba(46, 36, 24, 0.16);
}
.confirm-banner .cb-star { color: var(--rust); font-size: 1.05rem; line-height: 1; }
.confirm-banner .cb-text {
  font-family: 'EB Garamond', serif;
  font-style: italic;
  color: var(--ink);
  font-size: 1.05rem;
  text-align: center;
}
.confirm-banner .cb-close {
  margin-left: 0.2rem;
  background: none;
  border: none;
  color: var(--faded);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.2rem;
}
.confirm-banner .cb-close:hover { color: var(--rust); }

/* legacy color-helper names, remapped to the almanac palette so existing
   markup keeps working; treat them as semantic accents now */
.gold-text { color: var(--rust); }
.gold-text-soft { color: var(--faded); }
.cream-text { color: var(--ink); }
.crimson-text { color: var(--rust-deep); }
.felt-green { color: var(--faded); }

.gold-border { border-color: var(--rule); }
.gold-border-strong { border-color: var(--rust); }

/* suit ornaments — two-color deck convention */
.ornament {
  color: var(--rust);
  letter-spacing: 0.55em;
  font-size: 0.8rem;
}
.ornament .st-r { color: var(--rust); }
.ornament .st-k { color: var(--ink); opacity: 0.8; }

/* sign tiles: playing cards (corner indices, card rounding, dealt-card hover) */
.sign-card {
  position: relative;
  background: var(--paper-card);
  /* Hairline like the forecast pages' sign cards, and for the same reason:
     twelve of these stack in a grid, and repeated objects take the soft rule
     (see --rule-soft in :root). At full strength this grid outweighed the
     climate panel, which is the page's actual landmark. Hover keeps the loud
     rust border; interaction is allowed to shout. */
  border: 1px solid var(--rule-soft);
  border-radius: 12px;
  transition: transform 160ms ease, border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
  overflow: hidden;
}
.sign-card:hover {
  border-color: var(--rust);
  background: rgba(255, 252, 240, 0.85);
  transform: translateY(-3px) rotate(-0.4deg);
  box-shadow: 2px 3px 0 rgba(46, 36, 24, 0.15);
}
/* Corner glyphs sit two to a card, so at 0.9 they were twenty-four rust
   accents competing with the climate's one headline. */
.cnr { position: absolute; font-size: 1.15rem; color: var(--rust); opacity: 0.55; line-height: 1; }
.cnr.tl { top: 10px; left: 12px; }
.cnr.br { bottom: 10px; right: 12px; transform: rotate(180deg); }
.tile-name { font-family: 'IM Fell English SC', serif; font-size: 1.2rem; color: var(--ink); }
.tile-dates { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.16em; color: var(--faded); margin: 0.15rem 0 0.5rem; }
.tile-arch { font-style: italic; color: var(--rust); font-size: 0.95rem; border-top: 1px dotted var(--faded); padding-top: 0.5rem; }
.tile-tag { font-style: italic; font-size: 0.87rem; color: var(--faded); margin-top: 0.15rem; line-height: 1.4; }

/* One line of instruction above the strip. The strip is twelve bare glyphs,
   and this site's stated reader knows no astrology: knowing you are a Libra
   does not mean recognising its glyph. Hover tooltips carry the names, but
   mobile has no hover, so without this line the strip reads as ornament. */
/* body.forecast-scoped: the note only exists on forecast pages, and the bare
   class would lose its margins to the `body.forecast *` reset at (0,1,1) —
   the same trap that caught the footer, the newsletter select and the sign
   cards. (0,2,0) settles it. */
body.forecast .strip-note {
  text-align: center;
  margin-top: 1.4rem;
  /* The strip's own top margin is dead under the same reset, so this bottom
     margin is the only air between the note and the circles. */
  margin-bottom: 0.9rem;
  font-family: 'EB Garamond', serif;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--faded);
}

/* zodiac chip-button strip (forecast pages, shareable) */
.zodiac-strip {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.55rem;
  margin: 0.6rem 0 0.4rem;
  font-size: 1.02rem;
}
/* Each item is a circled glyph with a three-letter label beneath: the circle
   moved from the anchor onto .zs-glyph so the anchor could become the column
   holding both. Flex gap carries all internal spacing, so the forecast reset
   (which zeroes margins at (0,1,1)) has nothing here to break. */
.zodiac-strip a {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  text-decoration: none;
}
.zodiac-strip .zs-glyph {
  width: 42px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--rust);
  border-radius: 50%;
  background: rgba(255, 252, 240, 0.65);
  color: var(--rust);
  line-height: 1;
  box-shadow: 1px 1px 0 rgba(46, 36, 24, 0.18);
  transition: background 140ms ease, color 140ms ease, transform 140ms ease;
}
.zodiac-strip .zs-name {
  font-family: 'EB Garamond', serif;
  font-size: 0.58rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--faded);
  transition: color 140ms ease;
}
.zodiac-strip a:hover .zs-glyph {
  background: var(--rust);
  color: #f4ead2;
  transform: translateY(-2px);
}
.zodiac-strip a:hover .zs-name { color: var(--rust); }

/* Hand of the Week: miniature vintage card faces */
.hand { font-family: 'IM Fell English SC', serif; font-size: 1.2rem; color: var(--ink); display: flex; align-items: center; gap: 5px; }
/* Built to match the social share cards (scripts/build-og-cards.js): corner
   rank with a small pip beneath it, one large pip in the middle, hairline inner
   rule, and the pair tilted against each other. Same proportions as the card
   faces drawn there (168x236), scaled down. */
.mini-card {
  position: relative;
  display: inline-block;
  width: 46px;
  height: 64px;
  border: 1px solid var(--rule);
  border-radius: 5px;
  background: var(--paper-bright);
  /* EB Garamond, not IM Fell, for the rank: IM Fell's old-style figures hang a
     third of their height below the baseline (a 5 measures 22 up / 11 down at
     48px), which reads as a stray glyph rather than a card rank. Same reason
     scripts/build-og-cards.js gives ranks their own font role. */
  font-family: 'EB Garamond', serif;
  line-height: 1;
  box-shadow: 1.5px 1.5px 0 rgba(46, 36, 24, 0.22);
}
/* the share card's inner hairline rule */
.mini-card::after {
  content: '';
  position: absolute;
  inset: 3px;
  border: 1px solid var(--rule);
  border-radius: 3px;
  opacity: 0.3;
  pointer-events: none;
}
.mini-card .rank { position: absolute; top: 5px; left: 6px; font-size: 0.95rem; }
.mini-card .pip-sm { position: absolute; top: 20px; left: 7px; font-size: 0.46rem; }
.mini-card .pip-lg {
  position: absolute;
  top: 56%;
  left: 52%;
  transform: translate(-50%, -50%);
  font-size: 1.5rem;
}
.mini-card.red { color: var(--rust); }
.mini-card.black { color: var(--ink); }
.mini-card:first-child { transform: rotate(-5deg); }
.mini-card + .mini-card { transform: rotate(5deg) translateY(2px); margin-left: -2px; }

/* panels (climate, methodology, modal) — parchment boxes */
.modal-backdrop {
  background: rgba(46, 36, 24, 0.45);
  backdrop-filter: blur(4px);
}
.modal-panel {
  background: linear-gradient(180deg, #f4ecd8 0%, #ece1c8 100%);
  /* 1px to match the forecast climate card; the parchment and shadow carry
     the weight, the border only defines the edge. */
  border: 1px solid var(--rule);
  border-radius: 16px;
  box-shadow: 0 12px 40px rgba(46, 36, 24, 0.35);
}

.reading-section { border-top: 1px dotted var(--faded); }
.reading-section:first-child { border-top: none; }

@keyframes shimmer {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 0.7; }
}
.shimmer { animation: shimmer 2.5s ease-in-out infinite; }

/* print doesn't glow — kept only so existing markup stays valid */
.glow { text-shadow: none; }

@keyframes fade-up {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}
.fade-up { animation: fade-up 600ms ease-out both; }

.tab-btn {
  background: transparent;
  border: none;
  border-bottom: 1px solid transparent;
  transition: all 250ms;
  position: relative;
  cursor: pointer;
}
.tab-btn.active { color: var(--rust); border-bottom-color: var(--rust); }
.tab-btn:not(.active):hover { color: var(--ink); }

.scroll-area { scrollbar-width: thin; scrollbar-color: var(--rule) transparent; }
.scroll-area::-webkit-scrollbar { width: 6px; }
.scroll-area::-webkit-scrollbar-thumb { background: var(--rule); border-radius: 3px; }

.divider-line { background: none; border-top: 1px dotted var(--faded); height: 0; }

/* dotted row rule for the houses/planets listings */
.almanac-row { border-bottom: 1px dotted var(--faded); }

/* newsletter signup — an almanac subscription notice. Separated by fading
   hairlines rather than borders: it is a divider situation, not a box, and the
   rule dissolving at both ends keeps it lighter than the landmark doubles.
   Pseudo-elements rather than borders on purpose — the forecast pages' `*`
   reset zeroes borders' companions at (0,1,1) but never matches pseudo-elements,
   so these survive both stylesheet contexts unchanged. */
.newsletter {
  text-align: center;
  position: relative;
  padding: 1.7rem 1rem 1.9rem;
  margin: 2.4rem 0;
}
.newsletter::before,
.newsletter::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--rule), transparent);
  opacity: 0.6;
}
.newsletter::before { top: 0; }
.newsletter::after { bottom: 0; }
.newsletter .newsletter-kicker {
  font-family: 'EB Garamond', serif;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.26em;
  color: var(--rust);
  margin-bottom: 0.5rem;
}
.newsletter h2 {
  font-family: 'IM Fell English SC', serif;
  font-weight: 400;
  color: var(--ink);
  font-size: 1.5rem;
  margin: 0 0 0.35rem;
}
.newsletter .newsletter-copy {
  font-family: 'EB Garamond', serif;
  font-style: italic;
  color: var(--faded);
  font-size: 1.02rem;
  margin: 0 auto 1.1rem;
  max-width: 460px;
}
.newsletter form {
  display: flex;
  justify-content: center;
  /* top-align: each field is its own column, so an error growing one column
     downward never nudges the fields beside it. */
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 0.8rem;
}
/* One column per field. The field's error lives inside its column, so showing
   it extends only this column and leaves the neighbouring fields in place. */
.newsletter .nl-field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  width: min(320px, 100%);
}
.newsletter input[type='email'] {
  font-family: 'EB Garamond', serif;
  font-size: 1rem;
  line-height: 1.4;
  color: var(--ink);
  background: var(--paper-bright);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 0.55rem 0.85rem;
  width: 100%;
}
.newsletter input[type='email']::placeholder { color: var(--faded); opacity: 0.7; }
.newsletter input[type='email']:focus { outline: 2px solid var(--rust); outline-offset: 1px; }
/* Sign dropdown mirrors the email field so the two read as a matched pair.
   Two traps here, both real:
   — Selector is .newsletter .nl-field select, not .newsletter select: the
     shorter form ties the forecast pages' `* { padding: 0 }` reset at (0,1,1)
     and loses on source order, which collapsed the control to 25px against the
     input's 41px. The input never had the fight — its [type='email'] attribute
     lifts it to (0,2,1). Third instance of this reset trap; see also
     footer.site-footer and the newsletter button restatement.
   — appearance: none, because with native chrome the select's height is the
     platform's decision, not this stylesheet's, and it does not match the
     input on Windows even with identical font, padding and border. Removing
     the chrome removes the arrow, so a chevron is drawn back on as a
     background SVG. Its stroke is --faded hardcoded (#6f5f47): CSS variables
     do not resolve inside data URIs. */
.newsletter .nl-field select {
  appearance: none;
  -webkit-appearance: none;
  font-family: 'EB Garamond', serif;
  font-size: 1rem;
  line-height: 1.4;
  color: var(--ink);
  background-color: var(--paper-bright);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%236f5f47' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.8rem center;
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 0.55rem 2.1rem 0.55rem 0.85rem;
  width: 100%;
  cursor: pointer;
}
.newsletter .nl-field select:focus { outline: 2px solid var(--rust); outline-offset: 1px; }
/* While the placeholder option is still selected, the control reads as a hint
   (faded) rather than a real answer — same feel as the email placeholder. */
.newsletter select:required:invalid { color: var(--faded); opacity: 0.85; }
/* THE primary-action treatment, shared by the only two things the site asks
   a visitor to do: read this week's forecast (the climate CTA on the home
   page) and subscribe (Deal Me In). One selector list rather than two copied
   rules, so the pair cannot drift apart. Filled rust is reserved for these
   two; everything else that links stays outline or text. */
.newsletter button,
.forecast-cta {
  font-family: 'IM Fell English SC', serif;
  font-size: 1.02rem;
  letter-spacing: 0.06em;
  color: #f4ead2;
  background: var(--rust);
  border: none;
  border-radius: 4px;
  padding: 0.6rem 1.7rem;
  cursor: pointer;
  box-shadow: 1px 1px 0 rgba(46, 36, 24, 0.25);
  transition: background 140ms ease, transform 140ms ease;
}
.newsletter button:hover,
.forecast-cta:hover { background: var(--rust-deep); transform: translateY(-1px); }
/* The error sits inside its field's column, directly beneath that field (so the
   sign error lands under the dropdown). It only exists in the DOM while there is
   an error, and growing its column doesn't move the neighbouring fields. */
.newsletter .newsletter-error {
  margin: 0;
  font-family: 'EB Garamond', serif;
  font-style: italic;
  font-size: 0.92rem;
  color: var(--rust);
  text-align: left;
}
.newsletter .newsletter-fine {
  font-family: 'EB Garamond', serif;
  font-size: 0.8rem;
  color: var(--faded);
  margin-top: 0.7rem;
}

/* 3 ─── REACT-APP SPECIFICS ─────────────────────────────────────────────── */

/* app masthead title takes the IM Fell treatment via .display; size set in JSX */

/* 4 ─── STATIC PAGE LAYOUT (forecast/archive & future evergreen pages) ──── */
/* Everything scoped under body.forecast — inert inside the React app.      */

body.forecast * { margin: 0; padding: 0; box-sizing: border-box; }
body.forecast {
  font-family: 'EB Garamond', Georgia, serif;
  color: var(--ink);
  min-height: 100vh;
  line-height: 1.62;
  font-size: 1.06rem;
  position: relative;
}
body.forecast::before {
  content: '';
  position: fixed;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' /%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.4'/%3E%3C/svg%3E");
  opacity: 0.10;
  pointer-events: none;
  mix-blend-mode: multiply;
  z-index: 0;
}
body.forecast .wrap { max-width: 700px; margin: 0 auto; padding: 3rem 1.5rem 4rem; position: relative; z-index: 1; }

body.forecast a { color: var(--rust); }
body.forecast a:hover { color: var(--rust-deep); }

body.forecast .masthead { text-align: center; margin-bottom: 0.4rem; }
body.forecast .site-title { text-align: center; margin-bottom: 0.1rem; }
body.forecast .site-title a {
  font-family: 'IM Fell English SC', serif;
  font-size: 2.7rem;
  letter-spacing: 0.06em;
  line-height: 1.15;
  color: var(--ink);
  text-decoration: none;
}
body.forecast .subtitle { text-align: center; font-style: italic; color: var(--faded); font-size: 1.05rem; margin-bottom: 1rem; }
body.forecast .ornament { text-align: center; margin: 1.2rem 0; }

body.forecast h1.page-title,
body.forecast h1 {
  font-family: 'IM Fell English SC', serif;
  font-weight: 400;
  color: var(--ink);
  font-size: 2.1rem;
  text-align: center;
  margin: 0.4rem 0 0.3rem;
  line-height: 1.2;
}

body.forecast .kicker {
  text-align: center;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.26em;
  color: var(--rust);
}
/* the dated climate line doubles as the page H1 on week pages */
/* The dated line as a newspaper FOLIO: small ink caps flanked by hairlines
   that fade toward the page edges. Third dress for this line, and the
   reasoning is worth keeping. As a tiny faded label it read as chrome, not a
   title; promoted to 1.3rem display type it became a third IM Fell line
   stacked between the 2.7rem masthead and the 1.75rem rust headline, three
   siblings fighting for one job. The folio resolves it by changing SPECIES
   instead of size: the flanking rules give it the full column's width and
   authority while the type stays small, so it reads as the document's
   dateline, which is what it is. Still the page's H1. */
body.forecast h1.kicker {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  font-family: 'EB Garamond', serif;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-align: center;
  color: var(--ink);
  margin: 2.1rem 0 0;
}
body.forecast h1.kicker::before,
body.forecast h1.kicker::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(to left, var(--rule), transparent);
  opacity: 0.55;
}
body.forecast h1.kicker::after {
  background: linear-gradient(to right, var(--rule), transparent);
}
/* The label owns the gap above; the panel hugs its label. */
body.forecast h1.kicker + .panel { margin-top: 0.55rem; }

/* The climate as a parchment card, the same dress the home page's climate
   wears, instead of two full-width double rules. The belt treatment made the
   same content an object on one page and a fenced-off zone on the other, and
   the fence is what kept reading as heavy. With this, the double rule's only
   remaining jobs are the footer's baseline and the share-card frames. */
body.forecast .panel {
  background: linear-gradient(180deg, #f4ecd8 0%, #ece1c8 100%);
  border: 1px solid var(--rule);
  border-radius: 14px;
  box-shadow: 0 6px 22px rgba(46, 36, 24, 0.13);
  padding: 1.7rem 1.5rem;
  margin: 1.8rem 0;
}
/* "event: poker meaning" headlines wrap at the colon: each half is wrapped in
   an inline-block, so when the line breaks it breaks between call and answer
   rather than at whichever word balance happened to pick. */
.hl-half { display: inline-block; }

body.forecast .panel h2,
.modal-panel h2 {
  /* balance stops the headline stranding its last word on a line of its own;
     browsers without it just wrap as before */
  text-wrap: balance;
}
body.forecast .panel h2 {
  font-family: 'IM Fell English SC', serif;
  font-weight: 400;
  color: var(--rust);
  /* IM Fell is a distressed historic face: it rewards size and punishes
     smallness. At 1.4rem the week's headline needed focusing to read; the
     home page's version reads fine at twice that. */
  font-size: 1.75rem;
  margin-bottom: 0.5rem;
  text-align: center;
}
body.forecast .panel p { text-align: center; }
body.forecast .cosmic-facts {
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--faded);
  margin-top: 1rem;
  text-align: center;
}

body.forecast article.sign {
  /* Hairline, not the landmark double: twelve of these stack on a forecast
     page, and identical heavy frames on all of them flattened the hierarchy.
     See --rule-soft in :root for the system. */
  border: 1px solid var(--rule-soft);
  border-radius: 26px; /* ~5% of card width, true playing-card curvature */
  background: rgba(255, 252, 240, 0.45);
  padding: 2rem 1.9rem;
  margin: 1.8rem 0;
}
body.forecast article.sign header { text-align: center; margin-bottom: 1.1rem; }
body.forecast .glyph {
  width: 76px;
  height: 76px;
  margin: 0 auto 0.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.1rem;
  color: var(--rust);
  line-height: 1;
  border: 1px solid var(--rust);
  border-radius: 50%;
  /* Solid ring, deliberately NOT dashed: the symbols pages define a dashed
     outline as "tap to copy" (their own comments say so), and this medallion
     wore the same dashes decoratively, promising an interaction it did not
     have. Dashes now mean one thing sitewide. The offset ring keeps the
     concentric poker-chip look the dashes were there for. */
  outline: 1px solid rgba(148, 57, 31, 0.35);
  outline-offset: 5px;
  background: rgba(255, 252, 240, 0.55);
}
body.forecast article.sign h2 {
  font-family: 'IM Fell English SC', serif;
  font-weight: 400;
  color: var(--ink);
  font-size: 1.55rem;
  margin: 0.3rem 0 0.15rem;
}
body.forecast .sign-meta { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.2em; color: var(--faded); }
/* text-wrap: pretty asks the browser to avoid leaving one word alone on the
   last line. Newer engines only, so it is a nicety rather than the fix; where
   an orphan actually matters the copy binds its final pair with &nbsp;. */
body.forecast .opening { font-style: italic; font-size: 1.14rem; text-align: center; margin: 0.9rem 0 1.3rem; text-wrap: pretty; }

body.forecast section { border-top: 1px dotted var(--faded); padding-top: 1rem; margin-top: 1rem; }
/* multi-paragraph sections (sign profile pages) — the global reset zeroes
   p margins, so restore breathing room between consecutive paragraphs */
body.forecast section p + p { margin-top: 0.65rem; }
/* the newsletter is a section too — cancel the generic dotted section rule
   (its fading hairlines come from the base .newsletter pseudo-elements) and
   restore the padding the `*` reset zeroes. On forecast pages it sits directly
   above the footer, whose double top-rule serves as the divider, so the bottom
   fade is suppressed to avoid two stacked lines. */
body.forecast section.newsletter {
  border-top: none;
  border-bottom: none;
  padding-top: 1.7rem;
  padding-bottom: 0.4rem;
  margin-top: 2.4rem;
}
body.forecast section.newsletter::after { content: none; }
/* the global `body.forecast *` reset zeroes padding at equal specificity and
   wins by order, so restate the button's cushion with higher specificity */
body.forecast .newsletter button { padding: 0.6rem 1.7rem; }

/* ── COPY/PASTE GLYPH PAGE (/stars/) ─────────────────────────────────────── */
body.forecast .glyph-blurb {
  font-family: 'EB Garamond', serif;
  font-style: italic;
  color: var(--faded);
  margin-bottom: 0.9rem;
}
body.forecast .glyph-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
/* Symbol coverage is patchy in the almanac display faces, so the tiles use the
   system symbol stack: whatever the device actually has for these characters.
   Named .copy-glyph, not .glyph: the sign profile pages already use .glyph for
   the 76px zodiac medallion in their header, and the two would style each
   other. Applies wherever a tile lives, grid or randomiser. */
body.forecast .copy-glyph {
  /* The Japanese faces at the end are load-bearing, not decoration: ｡ and ﾟ are
     halfwidth katakana (U+FF61, U+FF9F) and need a CJK font. Without them
     Chrome showed those two as empty boxes even on a machine that had the
     glyphs, because nothing in a symbol-only stack covers that range. */
  font-family: 'Segoe UI Symbol', 'Apple Symbols', 'Noto Sans Symbols 2',
    'Yu Gothic', 'MS Gothic', 'Hiragino Kaku Gothic ProN', 'Noto Sans JP',
    system-ui, sans-serif;
  font-size: 1.35rem;
  line-height: 1;
  color: var(--ink);
  background: var(--paper-bright);
  border: 1px solid var(--rule);
  border-radius: 5px;
  padding: 0.6rem 0.2rem;
  min-width: 3.1rem;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
}
/* Arrangements are multi-character, so they need room to sit on one line.
   flex:0 0 auto stops the row squeezing a wide arrangement narrower than its
   own text, which spilled the longer dividers outside their tile.
   Selectable text matters: it's the fallback when a clipboard write is refused,
   and it's how the page still works with JS off. */
/* width:max-content is doing real work here. A <button> in a flex container
   does not size to its own text the way a div does, so the wider arrangements
   were being squeezed to a default width with their glyphs spilling outside the
   tile. min-width keeps the single-character tiles an even size. */
body.forecast .glyph-grid .copy-glyph {
  white-space: nowrap;
  user-select: text;
  flex: 0 0 auto;
  width: max-content;
}
body.forecast .glyph-grid .copy-glyph:hover {
  border-color: var(--rust);
  transform: translateY(-1px);
}
body.forecast .glyph-grid .copy-glyph:focus-visible { outline: 2px solid var(--rust); outline-offset: 2px; }
body.forecast .glyph-grid .copy-glyph.copied {
  background: var(--rust);
  border-color: var(--rust);
  color: #f4ead2;
}
/* Link out of a set to a fuller page. Only the zodiac set uses it today. */
body.forecast .glyph-more {
  margin-top: 0.7rem;
  font-family: 'EB Garamond', serif;
  font-style: italic;
  font-size: 0.95rem;
}

/* ── THE ALMANAC (/almanac/) ─────────────────────────────────────────────── */
body.forecast .almanac-list { list-style: none; padding: 0; margin: 0.6rem 0 0; }
body.forecast .almanac-list li {
  padding: 0.9rem 0;
  border-bottom: 1px solid rgba(74, 60, 42, 0.22);
}
body.forecast .almanac-list li:last-child { border-bottom: none; }
body.forecast .almanac-list a {
  font-family: 'IM Fell English SC', serif;
  font-size: 1.15rem;
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
body.forecast .almanac-list a:hover { border-bottom-color: var(--rust); }
body.forecast .almanac-list p {
  margin: 0.25rem 0 0;
  font-family: 'EB Garamond', serif;
  font-size: 0.95rem;
  color: var(--faded);
}
body.forecast .almanac-signs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
  gap: 0.5rem;
  margin-top: 0.8rem;
}
body.forecast .almanac-signs a {
  font-family: 'IM Fell English SC', serif;
  font-size: 0.98rem;
  text-decoration: none;
  padding: 0.5rem 0.7rem;
  border: 1px solid rgba(74, 60, 42, 0.3);
  border-radius: 8px;
  background: rgba(255, 252, 240, 0.4);
}
body.forecast .almanac-signs a:hover { border-color: var(--rust); }

/* ── NODE AXIS PAGE (/north-node/) ───────────────────────────────────────── */
/* Twelve rows of sign / leans-on / grows-toward. A real <table> was tempting
   and wrong: on a phone the three columns would each be about 80px wide. Rows
   stack into a small card per sign instead, and the labels below are visible
   at every width rather than being column headers that scroll away. */
body.forecast .node-table {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 1rem;
}
body.forecast .node-row {
  display: grid;
  grid-template-columns: 8.5rem 1fr 1fr;
  gap: 0.9rem;
  align-items: baseline;
  padding: 0.75rem 0.9rem;
  border: 1px solid rgba(74, 60, 42, 0.3);
  border-radius: 8px;
  background: rgba(255, 252, 240, 0.4);
}
body.forecast .node-sign {
  font-family: 'IM Fell English SC', serif;
  font-size: 1.05rem;
  color: var(--ink);
}
body.forecast .node-sign a { text-decoration: none; border-bottom: 1px solid transparent; }
body.forecast .node-sign a:hover { border-bottom-color: var(--rust); }
/* The two paths crossing. currentColor on the Sun line so it inherits the ink
   colour; the Moon path and markers take the rust accent. */
body.forecast .node-diagram {
  display: block;
  width: 100%;
  max-width: 27rem;
  height: auto;
  margin: 1.4rem auto 0.6rem;
  color: var(--ink);
}
body.forecast .node-diagram .nd-label {
  font-family: 'IM Fell English SC', serif;
  font-size: 13px;
  letter-spacing: 0.1em;
  fill: var(--ink);
}
/* Each path label takes its own line's colour. Both were --faded before, so
   nothing connected a label to the curve it named. */
body.forecast .node-diagram .nd-path {
  font-family: 'EB Garamond', serif;
  font-style: italic;
  font-size: 13px;
}
body.forecast .node-diagram .nd-sun { fill: var(--ink); }
body.forecast .node-diagram .nd-moon { fill: var(--rust); }
body.forecast .node-caption {
  margin: 0.1rem auto 0.4rem;
  max-width: 27rem;
  text-align: center;
  font-family: 'EB Garamond', serif;
  font-style: italic;
  font-size: 0.92rem;
  color: var(--faded);
}
body.forecast .node-cell {
  font-family: 'EB Garamond', serif;
  font-size: 0.95rem;
  line-height: 1.4;
  color: var(--faded);
}
/* The label repeats on every row on purpose. A single header row at the top
   is invisible by the time you have scrolled to Capricorn. */
body.forecast .node-label {
  display: block;
  font-family: 'IM Fell English SC', serif;
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--rust);
  margin-bottom: 0.15rem;
}
@media (max-width: 34rem) {
  body.forecast .node-row { grid-template-columns: 1fr; gap: 0.5rem; }
}

/* ── ZODIAC SYMBOLS PAGE (/zodiac-symbols/) ──────────────────────────────── */
/* Cards rather than the bare tiles of the star page. The symbol is still the
   thing being copied, but here it arrives with a name, a date range and a way
   further into the site, because a visitor who wants ♌ has told us something a
   visitor who wants ★ has not.
   The 10rem floor is measured against the 700px almanac column, not guessed.
   It lands three across on a desktop, where a wider floor gave a lanky 2x6,
   and falls to one on a phone. One is correct there rather than a compromise:
   the column leaves the grid only ~244px, so two would mean 115px cards, which
   is narrower than "Sagittarius" plus its padding. No media query needed. */
body.forecast .zodiac-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  gap: 0.9rem;
}
/* The card is a CONTAINER, not a control. It had a rust hover that lit the
   whole thing up, which promised a click the card does not accept: only the two
   dashed tiles and the link inside it do anything. No hover state here, and a
   lighter border than the tiles it holds, so the things that are interactive
   are the things that look interactive. */
body.forecast .zodiac-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.3rem;
  padding: 1.1rem 0.8rem 1rem;
  border: 1px solid rgba(74, 60, 42, 0.3);
  border-radius: 8px;
  background: rgba(255, 252, 240, 0.4);
}
/* Both copy targets share one language: a dashed outline at rest means "tap to
   copy", solid and rust means "you're on it". The glyph previously had a
   transparent border and only grew chrome on hover, so at rest it read as plain
   text and nobody would know to tap it. It keeps .copy-glyph either way, so the
   delegated handler and the .copied flash need no changes. */
body.forecast .zodiac-card .zodiac-glyph {
  font-size: 2.4rem;
  min-width: 0;
  width: max-content;
  padding: 0.3rem 0.9rem;
  background: transparent;
  border-style: dashed;
  border-color: var(--rule);
  color: var(--rust);
}
body.forecast .zodiac-card .zodiac-glyph:hover {
  border-style: solid;
  border-color: var(--rust);
  background: var(--paper-bright);
}
body.forecast .zodiac-card .zodiac-glyph:focus-visible { outline: 2px solid var(--rust); outline-offset: 2px; }
body.forecast .zodiac-card .zodiac-glyph.copied {
  background: var(--rust);
  border-color: var(--rust);
  color: #f4ead2;
}
body.forecast .zodiac-name {
  font-family: 'IM Fell English SC', serif;
  font-size: 1.15rem;
  color: var(--ink);
}
body.forecast .zodiac-dates {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--faded);
}
body.forecast .zodiac-arch {
  font-family: 'IM Fell English SC', serif;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--rust);
}
/* The ready-made arrangement: a second copy target, deliberately quieter than
   the glyph above it. Someone came for the bare symbol; this is the thing they
   did not know to ask for, so it should read as an offer rather than compete. */
body.forecast .zodiac-card .zodiac-arr {
  font-size: 1.05rem;
  min-width: 0;
  width: max-content;
  max-width: 100%;
  white-space: nowrap;
  user-select: text;
  padding: 0.35rem 0.6rem;
  margin: 0.1rem 0 0.35rem;
  color: var(--faded);
  background: transparent;
  border-style: dashed;
  border-color: var(--rule);
  opacity: 0.85;
}
body.forecast .zodiac-card .zodiac-arr:hover {
  border-style: solid;
  border-color: var(--rust);
  background: var(--paper-bright);
  color: var(--ink);
  opacity: 1;
}
body.forecast .zodiac-card .zodiac-arr:focus-visible { outline: 2px solid var(--rust); outline-offset: 2px; }
body.forecast .zodiac-card .zodiac-arr.copied {
  background: var(--rust);
  border-style: solid;
  border-color: var(--rust);
  color: #f4ead2;
  opacity: 1;
}
/* The third role in the card, and the one that leaves the page. Deliberately
   NOT boxed: an underline and an arrow is the oldest "this goes somewhere"
   signal there is, and keeping it unboxed is what stops it reading as a third
   thing to copy. It only ever underlined on hover before, which left the card
   with no resting cue for navigation at all. */
body.forecast .zodiac-link {
  margin-top: auto;
  padding-top: 0.15rem;
  font-size: 0.85rem;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: rgba(148, 57, 31, 0.45);
}
body.forecast .zodiac-link:hover { text-decoration-color: var(--rust); }
/* Random arrangement roller. Centred and oversized because it's the one thing
   on the page you interact with rather than scan. */
body.forecast .roll { text-align: center; }
body.forecast .roll .glyph-blurb { text-align: center; }
body.forecast .roll-out { display: flex; justify-content: center; margin: 0.2rem 0 1rem; }
/* Dashed, matching the copy targets on the zodiac page: solid filled button
   means "do something", dashed box means "take this". Without it the tile read
   as an output panel next to the loud rust reroll button, and a first-time
   visitor had no way to know the arrangement itself was tappable. The grid
   tiles below stay solid on purpose. A dense grid of identical boxes already
   reads as a palette by convention; a single large box floating alone does
   not, so it needs the stronger cue. */
body.forecast .copy-glyph.glyph-roll,
body.forecast .glyph-roll {
  font-size: 2.1rem;
  padding: 1rem 1.6rem;
  min-width: 11rem;
  border-style: dashed;
  border-color: var(--rule);
}
body.forecast .glyph-roll:hover {
  border-style: solid;
  border-color: var(--rust);
}
body.forecast .glyph-roll.copied {
  border-style: solid;
  background: var(--rust);
  border-color: var(--rust);
  color: #f4ead2;
}
body.forecast .roll-btn {
  font-family: 'IM Fell English SC', serif;
  font-size: 0.92rem;
  letter-spacing: 0.06em;
  color: #f4ead2;
  background: var(--rust);
  border: none;
  border-radius: 4px;
  padding: 0.5rem 1.4rem;
  cursor: pointer;
  box-shadow: 1px 1px 0 rgba(46, 36, 24, 0.25);
  transition: background 140ms ease, transform 140ms ease;
}
body.forecast .roll-btn:hover { background: var(--rust-deep); transform: translateY(-1px); }
body.forecast .roll-btn:focus-visible { outline: 2px solid var(--rust); outline-offset: 2px; }
body.forecast .roll-btn:disabled { opacity: 0.55; cursor: default; transform: none; }

/* Mid-scramble the glyph is faded and inert, so it resolves into full ink when
   the reel lands and a stray tap can't copy a value that's still changing. */
body.forecast .glyph-roll.spinning {
  color: var(--faded);
  pointer-events: none;
}
body.forecast .glyph-roll.landed { animation: roll-land 340ms ease-out; }
@keyframes roll-land {
  0% { transform: scale(1.07); border-color: var(--rust); }
  100% { transform: scale(1); }
}

body.forecast .copy-toast {
  position: fixed;
  left: 50%;
  bottom: 1.6rem;
  transform: translate(-50%, 1rem);
  font-family: 'EB Garamond', serif;
  font-size: 0.95rem;
  color: #f4ead2;
  background: var(--ink);
  border-radius: 6px;
  padding: 0.5rem 1.1rem;
  box-shadow: 0 6px 22px rgba(46, 36, 24, 0.3);
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease, transform 160ms ease;
}
body.forecast .copy-toast.on { opacity: 1; transform: translate(-50%, 0); }

/* Page-type label above the sign name. The evergreen profiles and the weekly
   forecasts share the same masthead and card styling, so without this the two
   are hard to tell apart on arrival. Same small-caps treatment as the masthead
   line on the share cards. Specificity beats the `body.forecast *` reset so the
   margin survives. */
body.forecast .sign header .eyebrow {
  font-family: 'IM Fell English SC', serif;
  font-size: 0.72rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--rust);
  margin-bottom: 0.9rem;
}
body.forecast section h3 {
  font-weight: 600;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.26em;
  color: var(--rust);
  margin-bottom: 0.4rem;
}

body.forecast .days { display: flex; gap: 3rem; }
body.forecast .day-value { font-family: 'IM Fell English SC', serif; font-size: 1.1rem; color: var(--ink); }
body.forecast .day-value.warning { color: var(--rust); }

body.forecast .hand-meaning { font-style: italic; color: var(--faded); font-size: 0.98rem; margin-top: 0.4rem; }

body.forecast ul.houses { list-style: none; }
body.forecast ul.houses li { font-style: italic; font-size: 0.98rem; padding-left: 1.1rem; position: relative; }
body.forecast ul.houses li::before { content: '✦'; position: absolute; left: 0; color: var(--rust); font-size: 0.6rem; top: 0.4rem; }

body.forecast .summary { font-style: italic; color: var(--ink); text-align: center; }

/* "How <Sign> plays" at the END of each sign's weekly reading,
   after the summary: the reader has just finished their week, and this is the
   suggestion of where to go next. It used to sit in the sign header, mirroring
   where the home page's reading modal put it, but the modal is gone and a
   top-of-reading link asked the question before anyone had a reason to ask it.
   Same shape as the other onward links (the profile pages' "Read this week's
   forecast", the almanac's closing line). */
body.forecast .sign .profile-link {
  margin-top: 0.9rem;
  text-align: center;
  font-family: 'IM Fell English SC', serif;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
body.forecast .sign .profile-link a { color: var(--rust); text-decoration: none; }
body.forecast .sign .profile-link a:hover { text-decoration: underline; }

body.forecast nav.weeks {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin: 2rem 0 0;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
}

body.forecast ul.forecast-list { list-style: none; }
body.forecast ul.forecast-list li {
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: rgba(255, 252, 240, 0.45);
  margin: 0.9rem 0;
}
body.forecast ul.forecast-list a { display: block; padding: 1.1rem 1.4rem; text-decoration: none; }
body.forecast ul.forecast-list li:hover { border-color: var(--rust); background: rgba(255, 252, 240, 0.75); }
body.forecast ul.forecast-list .week-range { font-family: 'IM Fell English SC', serif; color: var(--ink); font-size: 1.1rem; }
body.forecast ul.forecast-list .week-headline { font-style: italic; color: var(--faded); font-size: 0.98rem; }

/* One footer for both renderers. These declarations were scoped to
   body.forecast, which the SPA's body is not, so the home page never received
   them and silently fell back to Tailwind defaults: a 1px line where the design
   has a 3px double rule, and 1px letter-spacing where it has 0.22em. Nobody
   chose those; they were just what `border-t` and `tracking-widest` happen to
   be. Scoping to a class instead means the two footers cannot drift on styling
   the way they already had on links, which is what FOOTER_LINKS fixed. */
/* Selectors are `footer.site-footer` rather than a bare class on purpose. The
   static pages carry `body.forecast * { margin: 0; padding: 0 }` at the top of
   this file, which is specificity (0,1,1); a bare `.site-footer` is (0,1,0) and
   loses to it, so the first version of this rule silently dropped the footer's
   margin and padding on every static page while looking correct on the home
   page. Adding the element name ties the specificity and the later position
   wins. The old `body.forecast footer` selector was (0,1,2) and never had the
   fight. Keep this block below that reset. */
footer.site-footer {
  text-align: center;
  margin-top: 3rem;
  padding-top: 1.4rem;
  border-top: 3px double var(--rule);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--faded);
}
/* Rust matches `body.forecast a` on the static pages. Stated here so the home
   page gets it too: Tailwind's preflight sets `a { color: inherit }`, so its
   footer links were quietly picking up the faded body colour instead. */
footer.site-footer a { display: inline-block; color: var(--rust); }
footer.site-footer a:hover { color: var(--rust-deep); }
footer.site-footer .ornament { margin-bottom: 0.7rem; }
footer.site-footer .footer-links { margin-top: 0.5rem; }
