﻿/* Celestial Aether — website chrome and page furniture.
   ---------------------------------------------------------------------------
   Values here are transcribed from the approved prototype
   ("Aethermoor Prototype.dc.html"), read out of its rendered computed styles
   rather than eyeballed. The prototype drives its layout from JS with two
   breakpoints — `mob = vw < 760` and `mid = vw < 1080` — so those are the two
   media queries used throughout this file. Do not "tidy" these numbers onto the
   Shared.UI token scale: that scale sizes launcher panels, and rounding to it is
   exactly what made the first implementation miss the design.

   Shared.UI still owns the colour/type primitives via tokens.css; the --w-*
   tokens below are website-scoped values with no launcher equivalent.
   --------------------------------------------------------------------------- */

:root {
  /* surfaces + lines (prototype: celestial theme) */
  --w-page-bg: #0e1330;
  --w-nav-bg: rgba(8, 10, 26, .62);
  --w-footer-bg: rgba(8, 10, 26, .4);
  --w-hairline: rgba(180, 200, 255, .09);   /* nav/footer edge */
  --w-divider: rgba(180, 200, 255, .08);    /* row separators inside a list */
  --w-border: rgba(180, 200, 255, .16);     /* default panel border */
  --w-border-strong: rgba(180, 200, 255, .3);
  --w-card-bg: rgba(30, 37, 71, .38);
  --w-card-bg-featured: rgba(30, 37, 71, .42);
  --w-gold-border: rgba(242, 200, 121, .28);
  --w-media-border: rgba(180, 200, 255, .14);  /* framed images inside a post */
  --w-prose: #c9d0ee;                          /* running body copy, lighter than --text-secondary */

  /* radii — the prototype uses two, not the launcher's four */
  --w-radius: 10px;      /* cards, panels, list containers */
  --w-radius-sm: 6px;    /* thumbnails, inputs, small controls */

  /* non-responsive type sizes */
  --w-brand: 19px;
  --w-nav-link: 14px;
  --w-meta: 12px;         /* dates, read time */
  --w-row-title: 15px;    /* compact list row title */
  --w-feat-title: 23px;   /* featured card title — same at both widths */
  --w-eyebrow: 13px;

  /* responsive values — defaults are the >=760px column of the prototype's
     table; the mobile block at the bottom of this file overrides them */
  --w-pad-header: 14px 34px;
  --w-header-left-gap: 34px;
  --w-pad-page: 36px 34px 60px;
  --w-pad-post: 34px 34px 70px;
  --w-pad-center: 52px 34px 80px;
  --w-card-pad: 36px 40px 40px;
  --w-card-pad-2: 32px 40px 36px;
  --w-pad-footer: 20px 34px;
  --w-hero-h: 300px;
  --w-hero-justify: center;
  --w-hero-gap: 15px;
  --w-hero-pad: 0 34px;
  --w-hero-scrim: to right;
  --w-fs-hero: 42px;
  --w-fs-hero-sub: 20px;
  --w-fs-page-title: 34px;
  --w-fs-post-title: 37px;
  --w-lede-h: 380px;
  --w-feat-img-h: 126px;
  --w-landing-cols: minmax(0, 1fr) minmax(280px, 340px);
  --w-featured-cols: minmax(140px, 200px) minmax(0, 1fr);
  --w-blog-cols: minmax(0, 1fr) minmax(240px, 300px);
  --w-blog-row-cols: 88px minmax(0, 1fr);
}

/* Without this, width:100% + padding overflows and every page scrolls sideways on
   mobile. app.css stays minimal because the launcher shares it, so the website owns
   the reset. */
*, *::before, *::after { box-sizing: border-box; }

/* One focus ring for every interactive control. Nothing here ever suppressed the browser's
   own ring, so this is not restoring a missing indicator — it replaces a UA ring that looked
   nothing like the four controls we had already styled by hand. The prototype is no help:
   it is a static mock and defines no :hover, :focus or :active rule anywhere.

   :where() keeps the specificity at zero, so the per-component rules further down (which set
   their own offsets) still win without needing !important. */
:where(a[href], button, input, textarea, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--color-accent-2);
  outline-offset: 2px;
}

/* The mobile menu's checkbox is clipped to a 1px box, so its own ring would be a stray dot.
   The ring belongs on the button it drives — see .site-menu-toggle:focus-visible below. */
.site-menu-toggle:focus-visible { outline: none; }

/* ---- shell ----
   app.css sets the body to --text-md (15px) for the launcher. The website's base is
   14px/1.45, and because almost everything inherits from it, leaving the launcher value
   in place shifted type on every page. */
body {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  background: var(--w-page-bg);
  font-size: 14px;
  line-height: 1.45;
}
.site-main { flex: 1 1 auto; display: flex; flex-direction: column; }

.page { width: 100%; max-width: 1300px; margin: 0 auto; padding: var(--w-pad-page); }
.page-post { max-width: 796px; padding: var(--w-pad-post); }
.page-center { max-width: 1300px; padding: var(--w-pad-center); display: flex; justify-content: center; }
/* Column widths differ per surface: auth 520, subscribe 600, download 900. */
.col-520 { width: 100%; max-width: 520px; }
.col-600 { width: 100%; max-width: 600px; }
.col-900 { width: 100%; max-width: 900px; }

/* ---- nav ---- */
.site-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: var(--w-pad-header);
  border-bottom: 1px solid var(--w-hairline);
  background: var(--w-nav-bg);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

/* The sticky unit is the header, not just the bar — when the menu opens, the panel is in flow
   beneath the bar and pushes the page down with it, exactly as the prototype does. */
.site-header { position: sticky; top: 0; z-index: 30; }

.site-nav-left { display: flex; align-items: center; gap: var(--w-header-left-gap); }
.site-nav-right { display: flex; align-items: center; gap: 14px; }

.site-brand {
  font-family: var(--font-display);
  font-size: var(--w-brand);
  font-weight: 400;
  letter-spacing: .16em;
  color: var(--color-accent);
  white-space: nowrap;
  transition: filter var(--motion-fast);
}
/* The wordmark is a link home and was the one nav element that gave no feedback at all.
   Brightening rather than recolouring, so the gold stays the gold — same move .app-btn.primary
   already makes on its gold plaque. */
.site-brand:hover { filter: brightness(1.08); }

.site-links { display: flex; align-items: center; gap: 24px; }

.site-link {
  font: 400 var(--w-nav-link)/1.45 var(--font-ui);
  color: var(--text-secondary);
  transition: color var(--motion-fast);
}
.site-link:hover { color: var(--text-primary); }
.site-link.is-active { color: var(--text-primary); padding-bottom: 2px; border-bottom: 1px solid var(--text-primary); }
.site-link-admin { color: var(--color-accent); }

.site-user { font: 400 var(--w-nav-link)/1.45 var(--font-ui); color: var(--text-muted); transition: color var(--motion-fast); }
/* It is a link to /account now, so it has to answer the pointer like one. */
.site-user:hover { color: var(--text-secondary); }
.site-signout-form { margin: 0; }
/* Nav CTA is the standard plaque — .app-btn already carries the size the prototype uses. */
.site-cta { flex: 0 0 auto; }

/* Mobile menu. A visually-hidden checkbox drives the panel so it can be a sibling of the bar
   and push content down; <details> would trap it inside the bar's flex row. The checkbox stays
   focusable so the control is reachable by keyboard. */
.site-menu-toggle {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.site-menu-btn {
  display: none;
  width: 44px;
  height: 44px;
  flex: 0 0 auto;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 4px;
  border: 1px solid var(--w-border-strong);
  border-radius: var(--w-radius-sm);
  cursor: pointer;
}
.site-menu-toggle:focus-visible + .site-nav .site-menu-btn { outline: 2px solid var(--text-primary); outline-offset: 3px; }

.site-menu-bar {
  display: block;
  width: 20px;
  height: 2px;
  border-radius: 2px;
  background: #c9d0ee;
  transition: transform var(--motion-fast), opacity var(--motion-fast);
}
/* Open state: outer bars cross, middle bar fades — the prototype's X. */
.site-menu-toggle:checked + .site-nav .site-menu-bar:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.site-menu-toggle:checked + .site-nav .site-menu-bar:nth-child(2) { opacity: 0; }
.site-menu-toggle:checked + .site-nav .site-menu-bar:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

.site-menu-panel {
  display: none;
  flex-direction: column;
  padding: 6px 16px 14px;
  background: rgba(8, 10, 26, .94);
  border-bottom: 1px solid var(--w-hairline);
}
.site-menu-toggle:checked ~ .site-menu-panel { display: flex; }

.site-menu-panel .site-link,
.site-menu-panel button {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 48px;
  padding: 0 6px;
  font: 400 15px/1.45 var(--font-ui);
  letter-spacing: 0;
  text-transform: none;
  color: #c9d0ee;
  border: 0;
  border-bottom: 1px solid var(--w-divider);
  border-radius: 0;
  background: none;
  box-shadow: none;
  text-align: left;
  cursor: pointer;
}
.site-menu-panel > :last-child .site-link,
.site-menu-panel > :last-child button,
.site-menu-panel > .site-link:last-child { border-bottom: 0; }

/* ---- footer ---- */
.site-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 20px;
  padding: var(--w-pad-footer);
  border-top: 1px solid var(--w-hairline);
  background: var(--w-footer-bg);
  font: 400 var(--w-eyebrow)/1.45 var(--font-ui);
  color: var(--text-muted);
}
.site-footer-links { display: flex; flex-wrap: wrap; gap: 18px; }
.site-footer a { color: var(--text-muted); }
.site-footer a:hover { color: var(--text-secondary); }

/* ---- shared furniture ---- */
.card {
  padding: 18px;
  border: 1px solid var(--w-border);
  border-radius: var(--w-radius);
  background: var(--w-card-bg);
}
/* Caution card — the prototype tints the macOS notarization note gold rather than
   giving it the same neutral treatment as the Windows requirements beside it. */
.card.card-warn { background: rgba(242, 200, 121, .04); border-color: rgba(242, 200, 121, .26); }
.eyebrow.eyebrow-warn { color: var(--color-accent); }

/* The prototype's invite/CTA rail card carries a border but no fill. */
.card-hollow { background: transparent; }

/* Centred page panel (subscribe, auth): wider radius, fainter fill, generous padding. */
.card-panel {
  width: 100%;
  padding: var(--w-card-pad);
  border: 1px solid var(--w-border);
  border-radius: 14px;
  background: rgba(30, 37, 71, .34);
}

/* The one genuinely glass panel on the site — the rail's subscribe card. It lifts off the
   page with a long shadow and a blurred backdrop, and carries an inset hairline frame. */
.card-glass {
  padding: 20px;
  border: 1px solid var(--w-border);
  border-radius: 14px;
  background: rgba(23, 29, 62, .46);
  box-shadow: 0 34px 80px -34px rgba(0, 0, 0, .85), inset 0 1px 0 rgba(255, 255, 255, .08);
  backdrop-filter: blur(16px) saturate(1.5);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
}
.card-glass-frame { border: 1px solid var(--w-hairline); border-radius: 9px; padding: 14px; }

/* The gold eyebrow used as a panel heading, distinct from the muted section eyebrow. */
.eyebrow-gold {
  display: block;
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--color-accent);
}

.note { font: 400 11px/1.5 var(--font-ui); color: var(--text-muted); }

/* The prototype carries TWO eyebrow weights and we had been using one for both.
   The default is the quiet one that labels a section — 11px, wide .28em tracking, muted ink.
   .eyebrow-lg is the louder one the landing sidebar card uses. Measured, not guessed. */
.eyebrow {
  display: block;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.eyebrow-lg {
  font-size: var(--w-eyebrow);
  letter-spacing: .14em;
  color: var(--text-secondary);
}

.rule-after { display: flex; align-items: center; gap: 14px; }
.rule-after::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--w-hairline); }

/* Type pills: a soft 8px rectangle, not a full pill, and bold small Manrope. */
.pill {
  display: inline-block;
  padding: 3px 8px;
  border: 1px solid currentColor;
  border-radius: 8px;
  font: 700 11px/1 var(--font-ui);
  letter-spacing: .02em;
  white-space: nowrap;
}
.pill.update { color: var(--color-accent-ink); background: var(--color-accent); border-color: #b8892f; }
.pill.deep-dive { color: #7ee8fa; }
.pill.announcement { color: #ff8fb4; }

/* State pills are NOT post-type pills and had been borrowing their classes, which made a
   published post gold and a draft cyan — the exact inverse of the handoff, where gold means
   "still needs you" and mint means "done and healthy" (the same mint as the online shard dot
   and a healthy complaint rate). Outlines, so a state never reads as the filled type badge. */
.pill.state-draft { color: var(--color-accent); }
.pill.state-published { color: #6ee7b7; }

/* ---- buttons ----
   app.css's .app-btn is the launcher's control: Manrope 15/600, 12px radius, soft glow.
   The website's is a gilded plaque — Cinzel small-caps, wide tracking, a 3px radius and a
   two-tone bevel (bright top edge, heavier bottom border). Overridden here rather than in
   Shared.UI so the launcher keeps its own button. */
.app-btn {
  font: 600 12px/1 var(--font-display);
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: 9px 18px;
  border-radius: 3px;
}

.app-btn.primary {
  color: var(--color-accent-ink);
  background: var(--color-accent);
  border: 1px solid #b8892f;
  border-top-color: #ffe6a8;
  border-bottom-width: 2px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .45), inset 0 -2px 6px rgba(90, 60, 10, .55);
}
.app-btn.primary:hover { transform: none; filter: brightness(1.04); }

.app-btn.ghost {
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--w-hairline);
  font-weight: 600;
  box-shadow: none;
}
.app-btn.ghost:hover { color: var(--text-primary); border-color: var(--w-border); }

/* The in-page call to action (subscribe, sign in to download) is the same plaque, larger. */
/* Three sizes, measured from the prototype: the default plaque (12px / 9px 18px) for nav and
   ghost actions, .lg (14px / 13px 28px) for a sidebar CTA, and .xl (16px / 15px 54px) for the one
   big action on a focused page — an auth submit, the subscribe form, a download gate.
   Tracking is .14em throughout, so it scales with the size on its own. */
.app-btn.lg { font-size: 14px; padding: 13px 28px; }
.app-btn.xl { font-size: 16px; padding: 15px 54px; }

.feed-row, .feed-featured, .entry { color: inherit; }

.status-dot { width: 8px; height: 8px; border-radius: 99px; flex: 0 0 auto; }
.status-dot.online { background: #6ee7b7; box-shadow: 0 0 9px #6ee7b7; }
.status-dot.offline { background: var(--text-muted); }
.feed-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.feed-date { font: 400 var(--w-meta)/1.45 var(--font-ui); color: var(--text-muted); }

/* ---- forms ---- */
.field { display: flex; flex-direction: column; gap: 8px; }
.field-label { font: 400 var(--w-eyebrow)/1.45 var(--font-ui); color: var(--text-secondary); }
.field-input {
  padding: 12px 13px;
  border: 1px solid var(--w-border-strong);
  border-radius: var(--w-radius-sm);
  background: rgba(8, 10, 26, .5);
  color: var(--text-primary);
  font: 400 15px/1.4 var(--font-ui);
  transition: border-color var(--motion-fast);
}
/* A text field is a target you aim at before you commit to it. The edge brightening on
   approach is the same move the focus ring finishes. */
.field-input:hover { border-color: var(--color-accent-2); }
.field-input::placeholder { color: var(--text-muted); }
.field-input:focus-visible { outline: 2px solid var(--color-accent-2); outline-offset: 2px; border-color: var(--w-border-strong); }

/* ---- long-form prose ----
   Body copy reads at 17/29.75 in a lighter ink (#c9d0ee) than the rest of the site's
   secondary text — the prototype deliberately brightens running prose. */
.prose { font: 400 17px/1.75 var(--font-ui); color: var(--w-prose); }
.prose > * + * { margin-top: 22px; }
.prose h2, .prose h3 { font-family: var(--font-display); font-weight: 400; color: var(--text-primary); line-height: 1.25; }
.prose h2 { font-size: 24px; }
.prose h3 { font-size: 19px; }
.prose a { color: var(--color-link); }
.prose a:hover { color: var(--color-link-hover); }
.prose img { max-width: 100%; height: auto; border: 1px solid var(--w-media-border); border-radius: var(--w-radius); }

/* A thematic break (---, ***, ___). Markdig emits a plain <hr /> and the editor preview mirrors it
   with the same tag, so this one rule — under the .prose class both renderers share — is enough for
   both the real post page and the composer's live preview. */
.prose hr { height: 1px; background: rgba(180, 200, 255, .16); margin: 6px 0; border: 0; }

/* A paragraph holding nothing but one image is a figure (see MarkdownRenderer). 290px per the
   handoff. The caption is a real sibling paragraph (.figure-caption), not generated content — CSS
   ::after content is invisible to textContent, copy, and in-page search, and a caption is content,
   not decoration. The same class carries the @video placeholder's caption below. */
/* flex-start, not the default stretch: the image sizes to itself now, and stretch would pull it
   back out to the full column width. */
.prose p.figure { display: flex; flex-direction: column; align-items: flex-start; gap: 9px; }

/* Images render at their own size. The only limit is the column: an image wider than the prose
   measure scales down to fit it, and nothing else is touched.

   This was height:290px with object-fit:cover, which cropped whatever did not fit — a portrait
   screenshot came out as a thin band through its middle. Then it was a 290px cap, which stopped
   the cropping but still scaled a tall image down (a 431x537 rendered at 231x288, 54%). Neither
   showed the picture the writer uploaded.

   A dev log's images are the evidence for what a post claims, so they are shown as they are. The
   trade is real and was chosen deliberately: a very tall image is now very tall in the reading
   column, and nothing reins it in.

   No upscaling either — width/height auto means an image narrower than the column renders at its
   own width rather than being stretched to fill and going soft. */
.prose p.figure img {
  position: relative;
  width: auto;
  max-width: 100%;
  height: auto;
  border-radius: 8px;
  border: 1px solid var(--w-media-border);
}
/* 13px per the handoff's captioned-figure spec — NOT the composer preview's 12px, which
   follows the prototype. The two contexts are governed by different documents. */
.prose p.figure-caption { font: 400 13px/1.5 var(--font-ui); color: var(--text-muted); }
@media (max-width: 759px) {
}

/* @video placeholder (see MarkdownRenderer.PromoteVideoPlaceholders). Real playback is out of
   scope — this is the drop-target the design handoff draws in the meantime: a play glyph and the
   target URL, both painted from a data attribute rather than emitted as a live, clickable link. */
.prose p.video-embed {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  height: 170px;
  border-radius: 8px;
  border: 1px solid rgba(180, 200, 255, .16);
  background: rgba(8, 10, 26, .6);
}
.prose p.video-embed::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 15px solid rgba(242, 200, 121, .85);
  border-top: 9px solid transparent;
  border-bottom: 9px solid transparent;
}
/* The play glyph belongs to the un-embedded state only — the frame that is still a link, because
   JS has not run or the host is one we will not embed. Once the player is in, it draws its own. */
.prose p.video-embed.is-embedded::before { content: none; }

/* Embedded: the frame becomes a 16:9 box and the player fills it, per the handoff. Height is
   dropped so the aspect ratio decides, rather than fighting the 170px placeholder height. */
.prose p.video-embed.is-embedded {
  height: auto;
  aspect-ratio: 16 / 9;
  padding: 0;
  overflow: hidden;
  background: #000;
}
.video-player { display: block; width: 100%; height: 100%; border: 0; }

/* No JS, or a host we will not frame: the paragraph keeps its glyph and holds a plain link. */
.prose p.video-embed a { color: var(--color-link); }
.prose p.video-embed a:hover { color: var(--color-link-hover); }

/* An image slot with nothing uploaded yet. Only reachable on a draft, since publishing a post with
   an empty slot is the author's call to make. */
.prose p.pv-slot {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 120px;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--w-border-strong);
  border-radius: var(--w-radius);
  color: var(--text-muted);
  font: 400 13px/1.5 var(--font-ui);
}
/* The prototype's <image-slot> glyph (same SVG as MEDIA_SLOT_ICON in admin-editor.js), drawn from
   CSS here because MarkdownRenderer keeps .DisableHtml() and never emits raw HTML, so the server
   cannot inject an <svg>. mask-image (not background-image) so the glyph is painted from
   background-color and inherits currentColor — i.e. it always matches this element's `color: var(
   --text-muted)` above, the same way the JS-drawn copies use stroke="currentColor". Supported
   unprefixed by every evergreen browser this app targets (Chrome/Edge/Firefox, Safari 15.4+); the
   -webkit- prefix covers older Safari, so no background-image fallback is needed. */
.prose p.pv-slot::before {
  content: "";
  width: 28px;
  height: 28px;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'/%3E%3Cpath d='m21 15-5-5L5 21'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'/%3E%3Cpath d='m21 15-5-5L5 21'/%3E%3C/svg%3E");
}
.prose blockquote {
  margin: 0;
  padding-left: 18px;
  /* The prototype's rule is the gold at 45% — a solid edge shouts over the quote it marks. */
  border-left: 2px solid rgba(242, 200, 121, .45);
  font: 400 21px/1.55 var(--font-serif);
  font-style: italic;
  color: var(--w-prose);
}
.prose code { font-family: var(--font-mono); font-size: 14px; }

/* Video contract, fixed here so slice 2's editor builds to it rather than inventing one:
   a self-hosted clip renders as <video muted loop playsinline preload="metadata" poster="...">, and
   a poster is REQUIRED on every clip — no mail client supports <video>, so slice 3's email falls
   back to the poster image linking to the post. Long or heavy video uses a YouTube block instead,
   rendered as a click-to-load facade (poster + play affordance), never an eager iframe. */
.prose video { max-width: 100%; height: auto; border-radius: var(--w-radius-sm); }

/* ---- mid breakpoint: sidebars collapse (prototype: vw < 1080) ---- */
@media (max-width: 1079px) {
  :root {
    --w-landing-cols: minmax(0, 1fr);
    --w-blog-cols: minmax(0, 1fr);
  }
}

/* ---- mob breakpoint (prototype: vw < 760) ---- */
@media (max-width: 759px) {
  :root {
    --w-pad-header: 11px 16px;
    --w-header-left-gap: 14px;
    --w-pad-page: 26px 18px 44px;
    --w-pad-post: 22px 18px 56px;
    --w-pad-center: 26px 18px 56px;
    --w-card-pad: 26px 20px 30px;
    --w-card-pad-2: 24px 20px 28px;
    --w-pad-footer: 18px;
    --w-hero-h: 236px;
    --w-hero-justify: flex-end;
    --w-hero-gap: 10px;
    --w-hero-pad: 0 18px 22px;
    --w-hero-scrim: to bottom;
    --w-fs-hero: 28px;
    --w-fs-hero-sub: 17px;
    --w-fs-page-title: 26px;
    --w-fs-post-title: 27px;
    --w-lede-h: 210px;
    --w-feat-img-h: 170px;
    --w-featured-cols: minmax(0, 1fr);
    --w-blog-row-cols: minmax(0, 1fr);
  }

  .site-links-inline { display: none; }
  .site-menu-btn { display: flex; }

  /* The handoff sets a 44px floor for mobile controls, and the drawer already honours it at
     48px. Everything else was thumb-hostile: chips at 27-31px, buttons at 33, the like button
     at 36. min-height with inline-flex centring grows the target without touching the type or
     re-wrapping any label — the padding stays exactly as measured, so desktop is untouched.

     Scoped to things a finger presses. Prose links inside a paragraph are deliberately left
     alone: stretching an inline link to 44px would break the line box around it. */
  .app-btn,
  .filter,
  .sort-chip,
  .archive-year,
  .plat-chip,
  .like-btn,
  .post-back,
  .site-brand,
  .site-footer a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* These read as a row of text, not as centred buttons. */
  .post-back, .site-footer a, .landing-all, .download-alt, .verify-skip { justify-content: flex-start; }
  .landing-all, .download-alt, .verify-skip {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* "Forgot it?" sits inline on the password label's row, so growing the link itself would
     push the label away from its input. Expanding the hit area with a pseudo-element instead
     leaves the layout exactly where it was. */
  .field-aside { position: relative; }
  .field-aside::after { content: ""; position: absolute; inset: -13px -8px; }
}

/* ---- toast ----
   Bottom-centre, gold-bordered, one at a time, 2600ms. Used for the confirmations that would
   otherwise need a whole page region: code generated, invite redeemed, draft saved. */
.toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 60;
  max-width: min(34rem, calc(100vw - 32px));
  padding: 12px 18px;
  border: 1px solid var(--w-gold-border);
  border-radius: 8px;
  background: rgba(8, 10, 26, .96);
  box-shadow: var(--shadow-card);
  font: 400 13px/1.55 var(--font-ui);
  color: var(--text-primary);
  animation: ptoast 220ms var(--ease-glide) both;
}
@keyframes ptoast {
  from { opacity: 0; transform: translate(-50%, 10px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* ---- status ring ----
   The cyan ring the handoff uses on the transactional confirmations (check your inbox, verify). */
/* Measured on the prototype: 40px, a tinted fill behind a soft 1px edge — not a heavy outline. */
.ring {
  width: 40px;
  height: 40px;
  border-radius: 99px;
  border: 1px solid rgba(126, 232, 250, .45);
  background: rgba(126, 232, 250, .12);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-accent-2);
  font: 400 22px/1 var(--font-display);
  box-shadow: 0 0 22px -6px var(--color-accent-2);
}

/* ---- switch ----
   Transcribed from the design system's Switch component (components/core/Switch.jsx), not the
   prototype: 44x24, gold-deep track when on, gold-hi knob. */
.w-switch {
  position: relative;
  width: 44px;
  height: 24px;
  flex: 0 0 auto;
  padding: 0;
  border: 1px solid var(--w-border);
  border-radius: 999px;
  background: rgba(0, 0, 0, .4);
  cursor: pointer;
  transition: background-color var(--motion-fast);
}
.w-switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 999px;
  background: var(--color-accent-hi);
  transition: left var(--motion-fast);
}
.w-switch[aria-checked="true"] { background: #b8892f; }
.w-switch[aria-checked="true"]::after { left: 23px; }
.w-switch:focus-visible { outline: 2px solid var(--color-accent-2); outline-offset: 3px; }

/* ---- preference rows ---- */
.pref-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--w-divider);
}
.pref-row:last-of-type { border-bottom: 0; }
.pref-copy { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.pref-name { font: 400 15px/1.45 var(--font-ui); color: var(--text-primary); }
.pref-note { font: 400 13px/1.5 var(--font-ui); color: var(--text-muted); }

/* The unsubscribe-everything block is deliberately prominent, not a small link under the
   retention options. */
.danger-block {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 18px;
  border: 1px solid color-mix(in srgb, var(--color-danger) 45%, transparent);
  border-radius: var(--w-radius);
  background: color-mix(in srgb, var(--color-danger) 7%, transparent);
}
.danger-title { font: 400 15px/1.45 var(--font-ui); color: var(--text-primary); }
.app-btn.danger {
  color: var(--text-primary);
  background: transparent;
  border: 1px solid var(--color-danger);
  box-shadow: none;
}
.app-btn.danger:hover { background: color-mix(in srgb, var(--color-danger) 16%, transparent); }

.reason-list { display: flex; flex-direction: column; gap: 10px; }
.reason-row { display: flex; align-items: center; gap: 10px; font: 400 14px/1.5 var(--font-ui); color: var(--text-secondary); }
.reason-row input { width: 16px; height: 16px; accent-color: var(--color-accent); }

/* Latest-post payoff card on the confirmed screen. */
.payoff {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 16px;
  border: 1px solid var(--w-gold-border);
  border-radius: var(--w-radius);
  background: var(--w-card-bg-featured);
  color: inherit;
}
.payoff:hover { border-color: var(--color-accent); }
.payoff-title { margin: 0; font: 400 19px/1.3 var(--font-display); color: var(--text-primary); }
.payoff-excerpt { margin: 0; font: 400 14px/1.6 var(--font-ui); color: var(--text-secondary); }

/* Opt-in and consent checkboxes sit apart from any terms line, never bundled with it. */
.optin { display: flex; align-items: flex-start; gap: 10px; }
.optin input { margin-top: 2px; width: 16px; height: 16px; accent-color: var(--color-accent); }
.optin-copy { font: 400 13px/1.55 var(--font-ui); color: var(--text-secondary); }

/* Password strength meter. */
.strength { display: flex; flex-direction: column; gap: 6px; }
.strength-track { height: 3px; border-radius: 99px; background: rgba(180, 200, 255, .14); overflow: hidden; }
.strength-fill { display: block; height: 100%; width: 0; border-radius: 999px; transition: width 200ms var(--ease-glide), background-color 200ms var(--ease-glide); }
.strength-label { font: 400 12px/1.45 var(--font-ui); color: var(--text-muted); }

/* The opt-in block reads as its own decision, not as fine print beside the terms line. */
.optin {
  gap: 11px;
  padding: 14px;
  border: 1px solid rgba(180, 200, 255, .16);
  border-radius: 8px;
  background: rgba(8, 10, 26, .3);
  cursor: pointer;
  transition: border-color var(--motion-fast);
}
/* The whole panel is the hit area — the cursor already says so, and now the edge does too.
   Putting this on the label rather than the checkbox means the feedback covers everywhere
   a click actually lands. */
.optin:hover { border-color: var(--w-border-strong); }
.optin-copy { display: flex; flex-direction: column; gap: 3px; }
.optin-name { font: 400 14px/1.45 var(--font-ui); color: var(--text-primary); }
.optin-note { font: 400 12px/1.5 var(--font-ui); color: var(--text-muted); }

/* Strength colours: pink under 10 characters, gold under 14, mint at 14 and up. Length only —
   see password-strength.js for why. */
.strength-fill.weak { background: var(--color-danger); }
.strength-fill.fine { background: var(--color-accent); }
.strength-fill.strong { background: var(--color-success); }
.strength-label.weak { color: var(--color-danger); }
.strength-label.fine { color: var(--color-accent); }
.strength-label.strong { color: var(--color-success); }

/* Field hint: the sentence under an input that says what the field is for. */
.field-hint { font: 400 12px/1.5 var(--font-ui); color: var(--text-muted); }

/* Invite codes are read off a screen and typed by hand, so they get the mono face and the wide
   tracking that keeps look-alike characters apart. */
.code-input {
  font-family: var(--font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
}
.code-input::placeholder { text-transform: none; letter-spacing: .04em; }

/* ---- post byline: picture, name, title ----
   Shared between the real dev log page and the composer's reader preview, which is why it lives
   here rather than in Post.cshtml's inline block — the admin page loads site.css but not that one.
   Two copies would drift the first time either was touched. */
.post-byline {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  font: 400 14px/1.45 var(--font-ui);
  color: var(--text-secondary);
}
/* The author's picture, their initials, or the plain tint — in that order. */
.byline-avatar {
  width: 40px;
  height: 40px;
  border-radius: 99px;
  background: rgba(126, 232, 250, .16);
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: 500 15px/1 var(--font-ui);
  letter-spacing: .02em;
  color: var(--text-secondary);
  overflow: hidden;
}
/* object-fit so a non-square upload is cropped rather than squashed. */
.byline-avatar.has-image { object-fit: cover; background: none; }

/* Name and title are ONE flex item. As two separate children the row's 10px gap stacked on top of
   the separator's own 6px margins, leaving a visible hole between the name and the middot. */
.post-attrib { display: inline-flex; align-items: baseline; min-width: 0; }
.post-author { color: var(--text-primary); }
.post-role::before { content: "·"; margin: 0 6px; color: var(--text-muted); }
