/**
 * app.css — the calm client app.
 *
 * DESIGN INTENT
 *
 * A client opens this on a Monday to approve four posts. It should feel like
 * picking up a well-made object, not like operating machinery. Three rules:
 *
 *   1. ONE THING AT A TIME. Anything that isn't the current decision is
 *      hidden, not shrunk. No control panels.
 *   2. WARM, NOT WHITE. A paper-toned canvas rather than clinical white or
 *      corporate navy. Contrast comes from restraint, not from colour.
 *   3. MOTION IS REASSURANCE. Generating a picture takes 10-30 seconds. That
 *      wait is designed, not apologised for.
 *
 * ARCHITECTURE (2026-09-19)
 *
 * Two levels, not four pages.
 *
 *   THE WEEK    a scrolled landing page. The phone is the hero — the client
 *               sees their actual post before they see any chrome. Below it,
 *               one card per network, each arriving on scroll with its title
 *               typed out a letter at a time.
 *
 *   A NETWORK   click a card and it opens as its own landing page, in that
 *               network's colour. The week's navigation gets out of the way
 *               and is replaced by a single way back. Deep-linkable: the URL
 *               carries #facebook, so reload and the browser back button
 *               both behave.
 *
 * The scroll machinery is Sonny's own, lifted from two of his repos rather
 * than invented here:
 *
 *   --sp          per-stage scroll progress, exponentially smoothed.
 *                 (All-is-well-Wellness / scroll-motion.tsx)
 *   .scroll-cue   the outlined mouse with the falling dot.
 *                 (Shreveport-United-Citizens / index.html .scroll-cue)
 *   .scroll-rail  the thin page-progress bar driven by --scroll-progress.
 *                 (All-is-well-Wellness / motion.tsx .scroll-progress)
 *   --mx/--my     magnetic buttons, x*.12 / y*.15 on mouse pointers only.
 *                 (All-is-well-Wellness / motion.tsx)
 *
 * Motion values are from Web-designer-for-stunning-visuals / motion.js —
 * measured, not guessed.
 *
 * BRAND COLOUR
 *
 * The app ships neutral. lib/app.js reads the client's brand.json and sets
 * --brand / --brand-ink at runtime, so every client's app wears their own
 * palette without a rebuild. Inside a network's own page the accent switches
 * again to that network's colour, and back out on the way home.
 *
 * No emoji anywhere. Icons are inline SVG, one weight, one size.
 */

/* ------------------------------------------------------------------ *
 * Tokens
 * ------------------------------------------------------------------ */

:root {
  /* Canvas — warm paper, not white. */
  --bg:          #EDEAE4;
  --bg-raised:   #F6F4F0;
  --bg-sunken:   #E4E0D8;
  --line:        #D6D1C7;
  --line-soft:   #E2DED6;

  /* Ink */
  --ink:         #1A1917;
  --ink-2:       #55524C;
  --ink-3:       #8B867D;

  /* Brand — overwritten per client at runtime. Neutral until then. */
  --brand:       #1A1917;
  --brand-ink:   #FFFFFF;
  --brand-soft:  rgba(26, 25, 23, .07);

  /* Accent — the brand by default; a network's own colour while you are
     inside that network's page. One variable, so nothing else has to know. */
  --accent:      var(--brand);
  --accent-ink:  var(--brand-ink);
  --accent-soft: var(--brand-soft);

  /* Meaning. Deliberately muted — a status should never shout. */
  --ok:          #3F7D5C;
  --warn:        #9A7B3F;
  --stop:        #9A4B42;

  /* Space — a 4px rhythm. */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 72px;

  --r-sm: 10px;
  --r:    16px;
  --r-lg: 24px;
  --r-pill: 999px;

  /* Motion — Mission Metrics house system.
     These are not invented and not borrowed. They are the values from Sonny's
     own motion catalog (Web-designer-for-stunning-visuals / motion.js), which
     was built by measuring live sites rather than guessing. The house curve
     appears six times across that catalog; it is the Mission Metrics feel. */
  --ease:      cubic-bezier(.22, 1, .36, 1);     /* house curve — most motion */
  --ease-wipe: cubic-bezier(.65, 0, .35, 1);     /* clip-path reveals         */
  --ease-sweep:cubic-bezier(.76, 0, .24, 1);     /* full-panel sweeps         */

  --t1: 220ms;   /* micro-interactions  */
  --t2: 400ms;   /* small moves         */
  --t3: 700ms;   /* reveal on enter     */
  --t4: 1200ms;  /* the clip-path wipe  */

  --rise: 24px;  /* "24px reads as intentional; 100px reads as a template" */
  --stagger: 60ms;
  --letter: 26ms; /* per-character delay in a typed title */

  /* kept so nothing below has to change */
  --fast: var(--t1);
  --mid:  var(--t2);
  --slow: var(--t3);
  --ease-soft: var(--ease);
  --ease-in: var(--ease-wipe);

  --tap: 48px;               /* never smaller than a thumb */
  --shadow: 0 1px 2px rgba(26,25,23,.04), 0 8px 24px rgba(26,25,23,.06);
  --shadow-lift: 0 2px 6px rgba(26,25,23,.06), 0 24px 60px rgba(26,25,23,.12);

  /* Scroll state. JS writes these; CSS is the only thing that reads them. */
  --scroll-progress: 0;
  --sp: 0;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:        #14130F;
    --bg-raised: #1D1B17;
    --bg-sunken: #0E0D0B;
    --line:      #302D27;
    --line-soft: #26241F;
    --ink:       #F2EFE9;
    --ink-2:     #A8A39A;
    --ink-3:     #74706A;
    --brand:     #F2EFE9;
    --brand-ink: #14130F;
    --brand-soft: rgba(242,239,233,.10);
    --shadow: 0 1px 2px rgba(0,0,0,.3), 0 8px 28px rgba(0,0,0,.35);
    --shadow-lift: 0 2px 8px rgba(0,0,0,.4), 0 28px 70px rgba(0,0,0,.5);
  }
}

:root[data-theme="dark"] {
  --bg:        #14130F;
  --bg-raised: #1D1B17;
  --bg-sunken: #0E0D0B;
  --line:      #302D27;
  --line-soft: #26241F;
  --ink:       #F2EFE9;
  --ink-2:     #A8A39A;
  --ink-3:     #74706A;
  --brand-soft: rgba(242,239,233,.10);
  --shadow: 0 1px 2px rgba(0,0,0,.3), 0 8px 28px rgba(0,0,0,.35);
  --shadow-lift: 0 2px 8px rgba(0,0,0,.4), 0 28px 70px rgba(0,0,0,.5);
}

/* ------------------------------------------------------------------ *
 * Base
 * ------------------------------------------------------------------ */

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  overflow-x: clip;             /* body alone is not enough — html scrolls */
  /* The drill-in view is its own scroll context; never inherit a stale
     scroll position from the page behind it. */
  scroll-padding-top: 96px;
}

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
}

body {
  font-family: "Helvetica Neue", Helvetica, -apple-system, BlinkMacSystemFont,
               "Segoe UI", Arial, sans-serif;
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  padding-bottom: calc(76px + env(safe-area-inset-bottom));  /* clear the tab bar */
  overflow-x: hidden;                 /* nothing may scroll sideways by accident */
  transition: background var(--mid) var(--ease), color var(--mid) var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* Keyboard users get a visible ring; mouse users never see one. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

/* ------------------------------------------------------------------ *
 * Type
 * ------------------------------------------------------------------ */

/* The small tracked-out capital. Used for every label in the app. */
.eyebrow {
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-3);
}

h1, .h1 {
  font-size: clamp(30px, 8vw, 44px);
  font-weight: 300;                 /* light, large — the Pian move */
  letter-spacing: -.02em;
  line-height: 1.08;
  margin: 0;
}

/* The display title. Bigger and tighter than h1 — used once per view, for
   the thing the client came to look at. */
.display {
  font-size: clamp(32px, 7.4vw, 66px);
  font-weight: 300;
  letter-spacing: -.035em;
  line-height: .96;
  margin: 0;
}

h2, .h2 {
  font-size: 19px;
  font-weight: 500;
  letter-spacing: -.01em;
  margin: 0;
}

.body   { font-size: 15.5px; line-height: 1.62; color: var(--ink-2); }
.quiet  { color: var(--ink-3); }
.num    { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ *
 * Typed title — one letter at a time
 *
 * app.js splits a title into <i class="ch"> spans. Each rises and fades in
 * on its own delay. 26ms per character: fast enough that a long word does
 * not out-stay its welcome, slow enough to read as deliberate.
 *
 * The letters are decorative; the accessible name lives on the wrapper's
 * aria-label, so a screen reader hears the word, not the spelling.
 * ------------------------------------------------------------------ */

.type { display: inline-block; }
.type .ch {
  display: inline-block;
  font-style: inherit;
  opacity: 0;
  transform: translate3d(0, .42em, 0);
  transition: opacity var(--t2) var(--ease), transform var(--t2) var(--ease);
  transition-delay: calc(var(--i, 0) * var(--letter));
  will-change: opacity, transform;
}
/* A space cannot be transformed usefully, and collapses. Keep its width. */
.type .ch.sp { width: .3em; }

.type.lit .ch { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .type .ch { opacity: 1 !important; transform: none !important; transition: none !important; }
}

/* ------------------------------------------------------------------ *
 * Layout
 * ------------------------------------------------------------------ */

.wrap { max-width: 560px; margin: 0 auto; padding: 0 var(--s5); }
@media (min-width: 900px) { .wrap { max-width: 760px; } }

.wrap-wide { max-width: 1180px; margin: 0 auto; padding: 0 var(--s5); }

.stack   > * + * { margin-top: var(--s4); }
.stack-l > * + * { margin-top: var(--s6); }

/* Screens cross-fade. Nothing in this app should ever jump. */
.screen { display: none; }
.screen.on {
  display: block;
  animation: rise var(--slow) var(--ease) both;
}
@keyframes rise {
  from { opacity: 0; transform: translate3d(0, var(--rise), 0); }
  to   { opacity: 1; transform: none; }
}

/* Children arrive in sequence — the calm part. */
.seq > * { animation: rise var(--slow) var(--ease) both; }
.seq > *:nth-child(1) { animation-delay: calc(var(--stagger) * 0); }
.seq > *:nth-child(2) { animation-delay: calc(var(--stagger) * 1); }
.seq > *:nth-child(3) { animation-delay: calc(var(--stagger) * 2); }
.seq > *:nth-child(4) { animation-delay: calc(var(--stagger) * 3); }
.seq > *:nth-child(5) { animation-delay: calc(var(--stagger) * 4); }
.seq > *:nth-child(6) { animation-delay: calc(var(--stagger) * 5); }

/* Reveal on enter. Once, and never undone — the catalog is explicit that
   replaying on scroll-back is distracting.
 *
 * The offset travels through --rev rather than being written straight into
 * `transform`. That matters: a card also lifts on hover, and if both the
 * reveal and the hover wrote `transform` directly, whichever rule came last
 * in the file would silently win and one of the two would stop working.
 * Composing them through a variable lets a card be revealed AND hovered. */
.rv {
  --rev: var(--rise);
  opacity: 0;
  transform: translate3d(0, var(--rev), 0);
  transition: opacity var(--t3) var(--ease), transform var(--t3) var(--ease);
  transition-delay: calc(var(--i, 0) * var(--stagger));
}
.rv.in { --rev: 0px; opacity: 1; }
/* Once revealed, interaction should feel immediate, not 700ms late. */
.rv.in:hover, .rv.in:focus-visible { transition-duration: var(--t1); transition-delay: 0s; }
@media (prefers-reduced-motion: reduce) {
  .rv { --rev: 0px; opacity: 1 !important; transform: none !important; }
}

/* ------------------------------------------------------------------ *
 * Top bar
 * ------------------------------------------------------------------ */

header.top {
  position: sticky; top: 0; z-index: 25;
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s4) var(--s5);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--t2) var(--ease), background var(--t2) var(--ease);
}
/* A hairline appears only once you have scrolled — otherwise the bar floats. */
header.top.sunk { border-bottom-color: var(--line-soft); }

header.top .grow { flex: 1; min-width: 0; }

.top-actions { display: flex; align-items: center; gap: var(--s1); }

.iconbtn {
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  border: 0; background: none; color: var(--ink-2);
  border-radius: var(--r-pill); cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.iconbtn:hover  { background: var(--accent-soft); color: var(--ink); }
.iconbtn:active { transform: scale(.94); }
.iconbtn svg { width: 20px; height: 20px; }

/* ------------------------------------------------------------------ *
 * Page progress — the thin rail across the very top
 * (All-is-well-Wellness / motion.tsx .scroll-progress)
 * ------------------------------------------------------------------ */

.scroll-rail {
  position: fixed; top: 0; left: 0; right: 0; height: 2px;
  z-index: 60; pointer-events: none;
  background: transparent;
}
.scroll-rail::after {
  content: ""; display: block; height: 100%;
  background: var(--accent);
  transform: scaleX(var(--scroll-progress, 0));
  transform-origin: 0 50%;
  transition: background var(--t2) var(--ease);
}

/* ------------------------------------------------------------------ *
 * Scroll cue — the outlined mouse with the falling dot
 * (Shreveport-United-Citizens / index.html .scroll-cue)
 * ------------------------------------------------------------------ */

.scroll-cue {
  display: flex; flex-direction: column; align-items: center; gap: var(--s2);
  border: 0; background: none; cursor: pointer; padding: var(--s2);
  color: var(--ink-3);
  transition: opacity var(--t2) var(--ease), color var(--fast) var(--ease);
}
.scroll-cue:hover { color: var(--ink-2); }
.scroll-cue.gone { opacity: 0; pointer-events: none; }

.scroll-cue .mouse {
  width: 26px; height: 42px;
  border: 2px solid currentColor;
  border-radius: 14px;
  position: relative;
  opacity: .55;
}
.scroll-cue .mouse i {
  position: absolute; left: 50%; top: 8px;
  width: 4px; height: 9px; margin-left: -2px;
  border-radius: 2px; background: var(--accent);
  animation: cueDrop 1.6s cubic-bezier(.4, 0, .4, 1) infinite;
}
@keyframes cueDrop {
  0%   { transform: translateY(0);    opacity: 0; }
  25%  {                              opacity: 1; }
  70%  { transform: translateY(16px); opacity: 0; }
  100% { transform: translateY(16px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .scroll-cue .mouse i { animation: none; opacity: 1; }
}

/* ------------------------------------------------------------------ *
 * The counter — bottom left, says which network you are on
 *
 * A ring with the index over the total and an arrow either side. It is the
 * one piece of chrome that is always true: whatever you are looking at, the
 * counter says where in the week it sits.
 * ------------------------------------------------------------------ */

.counter {
  position: fixed;
  left: var(--s5);
  bottom: calc(92px + env(safe-area-inset-bottom));
  z-index: 24;
  width: 132px; height: 132px;
  display: grid; place-items: center;
  opacity: 0;
  transform: translate3d(0, 12px, 0) scale(.96);
  transition: opacity var(--t3) var(--ease), transform var(--t3) var(--ease);
  pointer-events: none;                /* the ring is decoration… */
}
.counter.in { opacity: 1; transform: none; }

.counter .ring-line {
  position: absolute; inset: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  /* A frosted disc, not an open ring. On a phone the counter floats directly
     over the cards, and an open ring left the numbers sitting on top of a
     caption — unreadable. This gives them their own ground. */
  background: color-mix(in srgb, var(--bg) 84%, transparent);
  backdrop-filter: blur(8px);
}
/* The slash between the two numbers, drawn rather than typed. */
.counter .slash {
  position: absolute; left: 50%; top: 50%;
  width: 1px; height: 84px;
  background: var(--line);
  transform: translate(-50%, -50%) rotate(28deg);
}

.counter .cap {
  position: absolute; left: 0; right: 0;
  text-align: center;
  font-size: 9px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-3);
}
.counter .cap--top { top: 15px; }
.counter .cap--bot { bottom: 15px; }

/* The two numbers sit either side of the drawn slash, as in the reference:
   the one you are on up and to the left, the total down and to the right.
   Absolute, because a grid kept stacking them on top of each other. */
.counter .nums { position: absolute; inset: 0; }
.counter .now, .counter .all {
  position: absolute;
  font-variant-numeric: tabular-nums;
  font-weight: 300; line-height: 1;
  font-size: 30px;
}
.counter .now { color: var(--ink);   left: 31%;  top: 41%;    transform: translate(-50%, -50%); }
.counter .all { color: var(--ink-3); right: 31%; bottom: 24%; transform: translate(50%, 50%); }

/* …but the arrows are real buttons. */
.counter .go {
  position: absolute; top: 50%;
  width: 34px; height: 34px; margin-top: -17px;
  display: grid; place-items: center;
  border: 0; background: none; color: var(--ink-3);
  border-radius: var(--r-pill); cursor: pointer;
  pointer-events: auto;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease),
              opacity var(--fast) var(--ease);
}
.counter .go:hover:not(:disabled) { color: var(--ink); background: var(--accent-soft); }
.counter .go:disabled { opacity: .3; cursor: default; }
.counter .go.prev { left: -6px; }
.counter .go.next { right: -6px; }
.counter .go svg { width: 17px; height: 17px; }

/* On a phone the ring would eat the screen. Shrink it and drop the arrows —
   the client swipes there anyway. */
@media (max-width: 720px) {
  /* On a phone the counter floats over live content, so it stops being an
     open ring and becomes a solid disc that reads as chrome. The network
     name does not fit inside 68px, so the ring carries only the numbers and
     the name is already in the bar at the top of the page. */
  .counter {
    left: var(--s3);
    bottom: calc(84px + env(safe-area-inset-bottom));
    width: 68px; height: 68px;
  }
  .counter .ring-line {
    background: var(--bg-raised);
    box-shadow: var(--shadow);
    backdrop-filter: none;
  }
  .counter .slash { height: 44px; }
  .counter .cap--top { display: none; }
  .counter .cap { font-size: 7px; letter-spacing: .1em; }
  .counter .cap--bot { display: none; }
  .counter .now, .counter .all { font-size: 16px; }
  .counter .now { left: 33%; top: 36%; }
  .counter .all { right: 33%; bottom: 30%; }
  .counter .go { display: none; }
}

/* The vertical word on the right edge. Pure reference-image furniture, and
   the first thing to go when the screen is narrow. */
.edge-note {
  position: fixed; right: var(--s4); top: 50%;
  z-index: 3; pointer-events: none;
  font-size: 10px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--ink-3);
  writing-mode: vertical-rl;
  transform: translateY(-50%);
  opacity: 0; transition: opacity var(--t3) var(--ease);
}
.edge-note.in { opacity: .8; }
@media (max-width: 980px) { .edge-note { display: none; } }

/* ------------------------------------------------------------------ *
 * The hero — the phone first, the words second
 *
 * Sonny's note: "the phone screen prominent on the landing page". So the
 * client's actual post is the largest object on the first screen, and the
 * headline sits under it rather than above.
 * ------------------------------------------------------------------ */

.hero {
  position: relative;
  /* The bloom overhangs on purpose so the colour has no hard edge. Clip it
     here, or that overhang becomes real page width and the document scrolls
     sideways — which is how this page shipped 288px too wide the first time. */
  overflow: clip;
  min-height: min(88svh, 800px);
  display: grid;
  grid-template-rows: 1fr auto;
  padding: var(--s5) 0 var(--s4);
}

.hero-stage {
  display: grid; place-items: center; gap: var(--s4);
  padding: 0 var(--s5);
  text-align: center;
}

/* The soft coloured wash behind the phone. Two radial blooms in the accent,
   drifting with scroll progress. Radial, never a full-screen linear gradient
   — a flat gradient bands badly on a warm canvas. */
.bloom {
  position: absolute; inset: -10% -20% 0;
  z-index: 0; pointer-events: none;
  opacity: .42;
  background:
    radial-gradient(34vw 30vw at 20% 26%,
      color-mix(in srgb, var(--accent) 46%, transparent), transparent 70%),
    radial-gradient(30vw 28vw at 84% 64%,
      color-mix(in srgb, var(--accent) 30%, transparent), transparent 68%);
  transform: translate3d(0, calc(var(--sp, 0) * -60px), 0) scale(calc(1 + var(--sp, 0) * .08));
  transition: opacity var(--t4) var(--ease), background var(--t3) var(--ease);
  filter: blur(6px);
}
@media (prefers-reduced-motion: reduce) { .bloom { transform: none; } }

.hero-stage > * { position: relative; z-index: 2; }

/* The headline, under the phone. */
.hero-words { max-width: 640px; }
.hero-words .body { margin-top: var(--s3); }

.hero-foot {
  display: grid; place-items: center; gap: var(--s4);
  /* clear the fixed tab bar, or the cue is half-eaten by it */
  padding-bottom: calc(84px + env(safe-area-inset-bottom));
}

/* ------------------------------------------------------------------ *
 * The phone
 * ------------------------------------------------------------------ */

.phone {
  width: 100%; max-width: 320px; margin: 0 auto;
  border: 1px solid var(--line);
  border-radius: 28px;
  background: var(--bg-raised);
  padding: var(--s2);
  box-shadow: var(--shadow);
  transition: box-shadow var(--t3) var(--ease), transform var(--t3) var(--ease);
}

/* The hero phone is the biggest thing on the page and lifts off the canvas.
   It also drifts a little as you scroll — the parallax is 16% of travel,
   the same figure All-is-well uses on its hero. */
.phone.hero-phone {
  /* Sized from the viewport HEIGHT, not the width. A 9:16 frame 400px wide
     stands 711px tall, which on a laptop pushed the headline clean off the
     bottom of the screen. 0.5625 is 9/16 — the height cap picks the width. */
  max-width: min(300px, 76vw, calc(42svh * 0.5625));
  box-shadow: var(--shadow-lift);
  transform: translate3d(0, calc(var(--sp, 0) * -34px), 0)
             scale(calc(1 - var(--sp, 0) * .04));
}
@media (min-width: 900px) {
  .phone.hero-phone { max-width: min(330px, calc(46svh * 0.5625)); }
}
@media (prefers-reduced-motion: reduce) { .phone.hero-phone { transform: none; } }

/* The reveal. The frame is a fixed window with overflow hidden; the picture
   moves INSIDE it. Nothing slides across the screen, so nothing feels cheap. */
.phone-frame {
  overflow: hidden;
  border-radius: 20px;
  background: var(--bg-sunken);
  transform: translateZ(0);        /* own layer — keeps it off the main thread */
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--t4) var(--ease-wipe);
}
/* Clip-path wipe reveal, to the catalog's spec: the frame wipes open from the
   bottom while the picture counter-scales 1.15 -> 1 over the same 1.2s.
   The counter-scale is what makes it read as a camera reveal rather than a
   mask. Opacity is never touched — the wipe is the entire effect. */
.phone-media {
  width: 100%; aspect-ratio: 9 / 16;
  object-fit: cover; display: block;
  transform: scale(1.15);
  transition: transform var(--t4) var(--ease-wipe);
  will-change: transform;
}
[data-near="1"] .phone-frame { clip-path: inset(0 0 0% 0); }
[data-near="1"] .phone-media { transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
  .phone-frame { clip-path: none; }
  .phone-media { transform: none; }
}
.phone-media[data-fit="contain"] { object-fit: contain; }

/* ------------------------------------------------------------------ *
 * Network cards — the week, below the hero
 *
 * One card per network. Each arrives on scroll, its title typed out a
 * letter at a time, and the whole card is the way in.
 * ------------------------------------------------------------------ */

.cards {
  display: grid; gap: var(--s5);
  padding: var(--s7) 0 var(--s8);
}
@media (min-width: 900px) {
  .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s6); }
}

.netcard {
  position: relative;
  display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: var(--s5);
  align-items: center;
  width: 100%; text-align: left;
  background: var(--bg-raised);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-lg);
  padding: var(--s5);
  font: inherit; color: inherit; cursor: pointer;
  overflow: hidden;
  transition: transform var(--t2) var(--ease), border-color var(--t2) var(--ease),
              box-shadow var(--t2) var(--ease);
}
@media (min-width: 560px) { .netcard { grid-template-columns: 124px minmax(0, 1fr); } }

/* Hover and the scroll reveal share one transform, through --rev. See .rv. */
.netcard:hover  { --rev: -3px; border-color: var(--line); box-shadow: var(--shadow); }
.netcard:active { --rev: 0px; }

/* Grid children of a <button> are inline by default, which would collapse
   the whole card into one line. */
.netcard > .shot, .netcard > .main { display: block; min-width: 0; }

/* The network's colour bleeds in from the left edge on hover. Its own
   colour, not the brand's — so the four cards are told apart at a glance. */
.netcard::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--net, var(--accent));
  transform: scaleY(0); transform-origin: 50% 0;
  transition: transform var(--t2) var(--ease);
}
.netcard:hover::before, .netcard:focus-visible::before { transform: scaleY(1); }

.netcard .shot {
  border-radius: var(--r); overflow: hidden;
  background: var(--bg-sunken);
  aspect-ratio: 9 / 16;
}
.netcard .shot img, .netcard .shot video {
  width: 100%; height: 100%; object-fit: cover; display: block;
}

.netcard .who { display: flex; align-items: center; gap: var(--s2); color: var(--net, var(--ink)); }
.netcard .who svg { width: 18px; height: 18px; }

.netcard .name {
  display: block;
  font-size: clamp(24px, 5.5vw, 34px);
  font-weight: 300; letter-spacing: -.025em; line-height: 1.02;
  margin: var(--s2) 0 var(--s3);
}

.netcard .line {
  font-size: 14.5px; line-height: 1.55; color: var(--ink-2); margin: 0;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

.netcard .foot {
  display: flex; align-items: center; gap: var(--s2);
  margin-top: var(--s4);
}

/* Status. A dot and a word — never a coloured block. */
.dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--ink-3); }
.dot.ok   { background: var(--ok); }
.dot.warn { background: var(--warn); }
.dot.off  { background: var(--line); }
.dot.bad  { background: var(--stop); }

/* ------------------------------------------------------------------ *
 * A network's own page
 *
 * Opens over the week. The week's navigation is gone; there is exactly one
 * way back, top left, and it is the first thing in the tab order.
 * ------------------------------------------------------------------ */

.net-page { display: none; }
.net-page.on { display: block; animation: netIn var(--t3) var(--ease) both; }

@keyframes netIn {
  from { opacity: 0; transform: translate3d(0, 18px, 0); }
  to   { opacity: 1; transform: none; }
}

.backbar {
  position: sticky; top: 0; z-index: 26;
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s5);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line-soft);
}

.back {
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: var(--tap); padding: 0 var(--s4) 0 var(--s3);
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: none; color: var(--ink); font: inherit; font-size: 14.5px;
  cursor: pointer;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease),
              transform var(--fast) var(--ease);
}
.back:hover { background: var(--accent-soft); border-color: var(--ink-3); }
.back:active { transform: scale(.98); }
.back svg { width: 17px; height: 17px; }

.backbar .here {
  display: flex; align-items: center; gap: var(--s2);
  margin-left: auto; color: var(--ink-2);
}
.backbar .here svg { width: 17px; height: 17px; }

/* The network hero: its name typed out, then the phone, big. */
.net-hero {
  overflow: clip;                      /* same reason as .hero */
  display: grid; place-items: center; gap: var(--s5);
  padding: var(--s7) var(--s5) var(--s6);
  text-align: center;
  position: relative;
}
.net-hero > * { position: relative; z-index: 2; }

.net-hero .display { color: var(--ink); }

.net-body { padding-bottom: var(--s8); }

/* ------------------------------------------------------------------ *
 * Buttons
 * ------------------------------------------------------------------ */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: var(--tap); width: 100%;
  padding: 0 var(--s5);
  border: 1px solid transparent; border-radius: var(--r-pill);
  font: inherit; font-size: 15.5px; font-weight: 500;
  cursor: pointer;
  /* Magnetic offset — mouse pointers only, set by app.js. */
  transform: translate3d(var(--mx, 0), var(--my, 0), 0);
  transition: transform var(--fast) var(--ease), background var(--fast) var(--ease),
              border-color var(--fast) var(--ease), opacity var(--fast) var(--ease);
}
.btn:active:not(:disabled) { transform: translate3d(var(--mx, 0), var(--my, 0), 0) scale(.98); }
.btn:disabled { opacity: .45; cursor: default; }
.btn svg { width: 18px; height: 18px; }

/* The one action that matters on any given screen. */
.btn.primary { background: var(--accent); color: var(--accent-ink); }
.btn.primary:hover:not(:disabled) { filter: brightness(1.08); }

/* Everything else. */
.btn.quiet { background: transparent; color: var(--ink); border-color: var(--line); }
.btn.quiet:hover:not(:disabled) { background: var(--accent-soft); border-color: var(--ink-3); }

.btn.bare { background: none; color: var(--ink-2); border: 0; width: auto; padding: 0 var(--s3); }
.btn.bare:hover:not(:disabled) { color: var(--ink); }

.btn-row { display: flex; gap: var(--s3); }
.btn-row .btn { flex: 1; }
@media (max-width: 420px) { .btn-row { flex-direction: column; } }

/* ------------------------------------------------------------------ *
 * Sheet — where editing happens, so the main screen stays calm
 *
 * NOTE (2026-09-19): position:fixed here used to be undone further down the
 * file by the grain z-index rule, which set position:relative on a group
 * that included .sheet. The sheet then rendered in the document flow, below
 * the fold, and "Change words" looked like it did nothing. That grouped rule
 * no longer touches .sheet / .sheet-back. Do not add them back to it.
 * ------------------------------------------------------------------ */

.sheet-back {
  position: fixed; inset: 0; z-index: 40;
  background: rgba(20,19,15,.34);
  backdrop-filter: blur(3px);
  opacity: 0; pointer-events: none;
  transition: opacity var(--mid) var(--ease);
}
.sheet-back.on { opacity: 1; pointer-events: auto; }

.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 41;
  background: var(--bg-raised);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  border-top: 1px solid var(--line-soft);
  padding: var(--s3) var(--s5) calc(var(--s6) + env(safe-area-inset-bottom));
  max-height: 88vh; overflow-y: auto;
  transform: translateY(101%);
  transition: transform var(--slow) var(--ease);
}
.sheet.on { transform: none; }
@media (min-width: 640px) {
  .sheet {
    left: 50%; right: auto; bottom: 0;
    width: min(560px, 94vw); transform: translate(-50%, 101%);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
  }
  .sheet.on { transform: translate(-50%, 0); }
}

.grabber {
  width: 38px; height: 4px; border-radius: var(--r-pill);
  background: var(--line); margin: var(--s2) auto var(--s5);
}

/* ------------------------------------------------------------------ *
 * Ask the AI — inside the edit sheet
 *
 * A quiet inset panel, not a feature banner. It sits above the words it is
 * going to change, so cause and effect read top to bottom, and it is visibly
 * a different surface from the post itself — the client should never be in
 * doubt about which box holds their actual caption.
 * ------------------------------------------------------------------ */

.ai {
  background: var(--bg);
  border: 1px solid var(--line-soft);
  border-radius: var(--r);
  padding: var(--s4);
}
.ai > * + * { margin-top: var(--s3); }

.ai-head { display: flex; align-items: center; gap: var(--s1); }

.ai .field { background: var(--bg-raised); }

.ai .btn { width: 100%; }

/* The four options. Tall enough to read the whole caption without a scroll —
   a client choosing between four posts needs to see all four, not skim them. */
.opts { display: grid; gap: var(--s2); }
.opts:empty { display: none; }

.opt {
  display: grid; gap: var(--s1);
  text-align: left; width: 100%;
  padding: var(--s3) var(--s4);
  background: var(--bg-raised);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-sm);
  font: inherit; color: inherit; cursor: pointer;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.opt:hover:not(:disabled) { border-color: var(--ink-3); }
.opt.on { border-color: var(--accent); background: var(--accent-soft); }
.opt:disabled { cursor: default; }
.opt.dead { opacity: .5; }

.opt-angle {
  font-size: 10.5px; font-weight: 500; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-3);
}
.opt.on .opt-angle { color: var(--accent); }
.opt-text { font-size: 14.5px; line-height: 1.5; color: var(--ink-2); overflow-wrap: anywhere; }
.opt.loading .opt-text { color: var(--ink-3); }

/* The working state. The word breathes rather than a spinner turning —
   same device as the loading screen, so the app has one idea of "waiting". */
.ai .breathe { animation: breathe 2.2s var(--ease) infinite; }

/* ------------------------------------------------------------------ *
 * Fields
 * ------------------------------------------------------------------ */

.field {
  width: 100%; font: inherit; font-size: 15.5px; line-height: 1.6;
  color: var(--ink); background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r);
  padding: var(--s4);
  transition: border-color var(--fast) var(--ease);
  resize: vertical;
}
.field:focus { outline: none; border-color: var(--ink-3); }
.field::placeholder { color: var(--ink-3); }

/* ------------------------------------------------------------------ *
 * The "?" — same component the engine pages use
 * ------------------------------------------------------------------ */

body .explain-trigger,
body .explain-trigger:link {
  width: 22px; height: 22px; flex: none;
  display: inline-grid; place-items: center;
  margin-left: var(--s2);
  border: 1px solid var(--line); border-radius: 50%;
  background: none; color: var(--ink-3);
  font: inherit; font-size: 12px; font-weight: 600; line-height: 1;
  cursor: pointer; vertical-align: middle;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease),
              background var(--fast) var(--ease);
}
body .explain-trigger:hover { color: var(--ink); border-color: var(--ink-3); background: var(--accent-soft); }

/* ------------------------------------------------------------------ *
 * Waiting — the moment that has to feel calm
 * ------------------------------------------------------------------ */

.waiting { display: grid; place-items: center; gap: var(--s4); padding: var(--s8) 0; }

.ring { width: 84px; height: 84px; display: block; }
.ring circle { fill: none; stroke-width: 1; }
.ring .track { stroke: var(--line); }
.ring .live  {
  stroke: var(--accent);
  stroke-linecap: round;
  transform-origin: 50% 50%;
  animation: sweep 1.9s var(--ease) infinite;
}
@keyframes sweep {
  0%   { stroke-dasharray: 2 200;   stroke-dashoffset: 0;    transform: rotate(-90deg); }
  55%  { stroke-dasharray: 130 200; stroke-dashoffset: -22;  transform: rotate(180deg); }
  100% { stroke-dasharray: 2 200;   stroke-dashoffset: -228; transform: rotate(270deg); }
}

.breathe { animation: breathe 3.4s var(--ease) infinite; }
@keyframes breathe { 0%,100% { opacity: .55; } 50% { opacity: 1; } }

/* ------------------------------------------------------------------ *
 * Tab bar
 * ------------------------------------------------------------------ */

.tabs {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: flex; justify-content: center; gap: var(--s2);
  padding: var(--s2) var(--s4) calc(var(--s2) + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--line-soft);
  transition: transform var(--t2) var(--ease), opacity var(--t2) var(--ease);
}
/* Inside a network's page the week's tab bar gets out of the way — the back
   control is the only navigation that applies there. */
.tabs.away { transform: translateY(110%); opacity: 0; pointer-events: none; }

.tab {
  flex: 1; max-width: 132px; min-height: var(--tap);
  display: grid; place-items: center; gap: 3px;
  border: 0; background: none; cursor: pointer;
  color: var(--ink-3); font: inherit;
  border-radius: var(--r);
  text-decoration: none;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.tab svg { width: 21px; height: 21px; }
.tab span { font-size: 10.5px; letter-spacing: .06em; }
.tab.on { color: var(--ink); }
.tab:active { background: var(--accent-soft); }

/* ------------------------------------------------------------------ *
 * Odds and ends
 * ------------------------------------------------------------------ */

.rule { height: 1px; background: var(--line-soft); border: 0; margin: var(--s6) 0; }

.note {
  font-size: 14px; line-height: 1.55;
  padding: var(--s3) var(--s4);
  border-radius: var(--r); border: 1px solid var(--line-soft);
  background: var(--bg-raised); color: var(--ink-2);
}
.note.bad { border-color: color-mix(in srgb, var(--stop) 40%, var(--line)); color: var(--stop); }
.note.good { border-color: color-mix(in srgb, var(--ok) 40%, var(--line)); color: var(--ok); }

.chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.chip {
  font: inherit; font-size: 13.5px;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: none; color: var(--ink-2); cursor: pointer;
  transition: border-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
.chip:hover { border-color: var(--ink-3); color: var(--ink); }

.center { text-align: center; }
.hide { display: none !important; }

/* Long captions and hashtag strings must never push the page sideways. */
.body, .netcard .line, .caption { overflow-wrap: anywhere; }

/* Screen-reader-only, but still focusable — used for "skip to the post". */
.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ------------------------------------------------------------------ *
 * Grain
 *
 * The reference site has exactly one keyframe animation in its whole
 * stylesheet, and it is film grain. It is what stops a flat warm canvas
 * looking like an unstyled page. Pure decoration, never interactive.
 * ------------------------------------------------------------------ */

.grain {
  position: fixed; inset: -150%;
  z-index: 1; pointer-events: none;
  opacity: .04;          /* catalog: at or below 5%. Felt, not seen. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: noise 500ms steps(8) infinite;
}
@keyframes noise {
  0%   { transform: translate3d(0, 0, 0); }
  25%  { transform: translate3d(-3%, -2%, 0); }
  50%  { transform: translate3d(2%, -4%, 0); }
  75%  { transform: translate3d(-2%, 3%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}
@media (prefers-reduced-motion: reduce) { .grain { animation: none; } }

/* Everything the client touches sits above the grain.
 *
 * THIS RULE SETS z-index ONLY. It must never set `position`.
 *
 * The bug it caused before: the old version of this rule was
 *   .top, .deck, .pager, .tabs, .sheet, .sheet-back, .screen
 *     { position: relative; z-index: 2 }
 * and, being later in the file, it overwrote `position: fixed` on .sheet and
 * .sheet-back. The edit sheet dropped into the document flow below the fold,
 * so "Change words" appeared to do nothing at all. Elements that need a
 * position declare it where they are defined; this rule only stacks them.
 */
.cards, .hero, .net-page, .screen, .netcard { position: relative; }
.cards, .hero, .net-page, .screen { z-index: 2; }


/* ------------------------------------------------------------------ *
 * The week as seven days (2026-09-22)
 *
 * A week with a rhythm (lib/cadence.js) is laid out Sunday to Saturday.
 * Each day spans the full width of .cards and holds its own posts; on a
 * wide screen the posts sit two across inside their day, as the cards
 * always did. A day with no post says why in one quiet line.
 * ------------------------------------------------------------------ */

.cards > .day, .cards > .rhythm { grid-column: 1 / -1; display: grid; gap: var(--s4); min-width: 0; }
@media (min-width: 900px) {
  .cards > .day { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s5); }
  .cards > .day > .dayhead, .cards > .day > .dayrest { grid-column: 1 / -1; }
}

.dayhead {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3);
  margin: 0; padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line-soft);
  font-size: 19px; font-weight: 500; letter-spacing: -.01em;
}
.day.today .dayhead span:first-child { color: var(--accent); }
.day.past { opacity: .55; }

.dayrest {
  display: flex; align-items: center; gap: var(--s2);
  margin: 0; color: var(--ink-3); font-size: 14.5px; text-decoration: none;
}
a.dayrest:hover { color: var(--ink); }
.dayrest svg { width: 16px; height: 16px; flex: none; }

/* A post still being written has no picture yet — hold its place. */
.waitmedia {
  display: grid; place-items: center; width: 100%; height: 100%;
  min-height: 120px; background: var(--bg-sunken, var(--line-soft));
}

.rhythm { padding-top: var(--s6); }
.rhythm-opts { display: grid; gap: var(--s3); }
@media (min-width: 900px) { .rhythm-opts { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.rhythm-opt {
  display: grid; gap: 6px; text-align: left; width: 100%;
  padding: var(--s4); border-radius: var(--r);
  border: 1px solid var(--line); background: none; color: var(--ink);
  font: inherit; cursor: pointer;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.rhythm-opt:hover:not(:disabled) { border-color: var(--ink-3); background: var(--accent-soft); }
.rhythm-opt.on { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.rhythm-opt:disabled { cursor: default; opacity: .6; }
.rhythm-opt .name { font-size: 16px; font-weight: 500; }
.rhythm-opt .line { font-size: 14px; line-height: 1.5; color: var(--ink-2); }

/* Changed your mind — the undo bar after a rhythm change (lib/app.js offerUndo).
   Sits above the tab bar, never over it, and never wider than the page. */
.undo-bar {
  position: fixed; z-index: 60;
  left: 50%; bottom: calc(84px + env(safe-area-inset-bottom, 0px));
  width: min(560px, calc(100% - 32px));
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2) var(--s3);
  padding: var(--s3) var(--s3) var(--s3) var(--s4);
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--bg-raised, var(--bg)); color: var(--ink);
  box-shadow: var(--shadow, 0 10px 30px rgba(0,0,0,.18));
  font-size: 14.5px; line-height: 1.45;
  opacity: 0; transform: translate(-50%, 12px);
  transition: opacity var(--t2, .3s) var(--ease, ease), transform var(--t2, .3s) var(--ease, ease);
}
.undo-bar.on { opacity: 1; transform: translate(-50%, 0); }
.undo-bar > span { flex: 1 1 200px; min-width: 0; }
.undo-bar .btn { width: auto; min-height: 40px; padding: 0 var(--s4); font-size: 14px; }
.undo-bar .iconbtn { width: 36px; height: 36px; font-size: 20px; }

/* ---- full size ------------------------------------------------------
   A phone on a web page is about 300px wide. Before publishing something
   under their own name a client needs to see the actual picture and read
   the actual words — so tapping the phone fills the screen with both, the
   picture at whatever size fits and the post at reading size.

   Above the sheet, because it can be opened from the option page while
   nothing else is open, and below nothing — it is the innermost thing on
   screen whenever it is up.                                             */

.zoom {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: none;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: color-mix(in srgb, var(--bg-sunken) 92%, transparent);
  backdrop-filter: blur(10px);
  padding: var(--s6) var(--s5) calc(var(--s7) + env(safe-area-inset-bottom));
}
.zoom.on { display: block; animation: rise var(--t2) var(--ease) both; }

.zoom-close {
  position: fixed;
  top: calc(var(--s4) + env(safe-area-inset-top));
  right: var(--s4);
  z-index: 2;
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--bg-raised);
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--shadow);
}
.zoom-close:hover { border-color: var(--ink-3); }

.zoom-inner {
  max-width: 760px;
  margin: 0 auto;
  display: grid;
  gap: var(--s5);
  justify-items: center;
}

/* Contain, never cover. A post graphic cropped to fit its container is
   exactly what this view exists to stop the client having to guess at. */
.zoom-media {
  display: block;
  max-width: min(100%, 460px);
  max-height: 68svh;
  width: auto; height: auto;
  object-fit: contain;
  border-radius: var(--r);
  background: var(--bg-sunken);
  box-shadow: var(--shadow-lift);
}
.zoom-none {
  width: min(100%, 460px);
  aspect-ratio: 9/16;
  display: grid; place-items: center;
}

.zoom-words {
  width: 100%;
  background: var(--bg-raised);
  border: 1px solid var(--line-soft);
  border-radius: var(--r);
  padding: var(--s5);
}

/* Reading size, not caption size — 17.5px against the 15px on a card. */
.zoom-text {
  margin: var(--s2) 0 0;
  font-size: 17.5px;
  line-height: 1.7;
  color: var(--ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.zoom-tags {
  margin: var(--s4) 0 0;
  font-size: 15px;
  color: var(--ink-3);
  overflow-wrap: anywhere;
}

/* A phone you can open. */
.zoomable { cursor: zoom-in; }
.zoomable:focus-visible { outline: 2px solid var(--accent); outline-offset: 6px; }

/* ---- make it obvious (2026-09-22) ---------------------------------
   A zoom-in cursor tells nobody on a phone, and nobody who does not
   already hover. So every generated picture or video says so on its face:
   the big phones carry a "Tap to enlarge" pill, and every thumbnail a
   round enlarge button in its corner. Solid, not faded, so it reads in
   both themes over any picture. */
.phone.zoomable { position: relative; }
.zoom-hint {
  position: absolute;
  left: 50%; bottom: calc(var(--s2) + 12px);
  transform: translateX(-50%);
  z-index: 2;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 13px 7px 10px;
  border-radius: var(--r-pill);
  background: rgba(12, 14, 18, .78);
  color: #fff;
  font-size: 13.5px; font-weight: 600; letter-spacing: .01em;
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .25);
  backdrop-filter: blur(6px);
  transition: background var(--t2) var(--ease);
}
.zoom-hint svg { width: 16px; height: 16px; flex: none; }
.zoomable:hover .zoom-hint, .zoomable:focus-visible .zoom-hint { background: var(--accent); }

.shot { position: relative; }
.zoom-chip {
  position: absolute;
  top: 6px; right: 6px;
  z-index: 2;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: rgba(12, 14, 18, .78);
  color: #fff;
  cursor: zoom-in;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .25);
  transition: background var(--t2) var(--ease), transform var(--t2) var(--ease);
}
.zoom-chip svg { width: 17px; height: 17px; }
.zoom-chip:hover, .zoom-chip:focus-visible { background: var(--accent); transform: scale(1.08); }
.zoom-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A post picture that failed to load: show the tinted box behind it, not a
   broken-image icon, while the picture check redraws it (2026-09-24). */
img.img-broken { visibility: hidden; }

/* ------------------------------------------------------------------ *
 * The calendar — a month at a glance; a day opens a bubble of its posts
 * ------------------------------------------------------------------ */

.cal {
  margin: var(--s7) 0 var(--s6);
  background: var(--bg-raised); border: 1px solid var(--line-soft);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
  padding: var(--s5);
}
@media (max-width: 520px) { .cal { padding: var(--s4) var(--s3); } }

.cal-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s4); flex-wrap: wrap; margin-bottom: var(--s5); }
.cal-title { font-size: clamp(24px, 4vw, 32px); font-weight: 300; letter-spacing: -.02em; margin: var(--s1) 0 0; }
.cal-sub { margin: var(--s2) 0 0; font-size: 14px; color: var(--ink-3); }
.cal-nav { display: flex; align-items: center; gap: var(--s1); }
.cal-nav .cal-today { width: auto; min-height: 40px; padding: var(--s2) var(--s4); font-size: 14px; }

.cal-dow, .cal-row { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--s2); }
.cal-dow { margin-bottom: var(--s2); }
.cal-dow span { font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3); text-align: center; }
.cal-grid { display: grid; gap: var(--s2); }

.cal-day {
  all: unset; box-sizing: border-box; cursor: pointer; position: relative;
  min-height: 76px; padding: var(--s2); border-radius: var(--r-sm);
  display: flex; flex-direction: column; justify-content: space-between; gap: var(--s1);
  background: transparent; border: 1px solid var(--line-soft);
  transition: background var(--t1) var(--ease), border-color var(--t1) var(--ease), transform var(--t1) var(--ease);
}
.cal-day .n { font-size: 14px; font-variant-numeric: tabular-nums; color: var(--ink-2); }
.cal-day.out { opacity: .38; }
.cal-day.past .n { color: var(--ink-3); }
.cal-day.has { background: var(--bg); border-color: var(--line); box-shadow: inset 0 -3px 0 var(--accent-soft); }
.cal-day.has .n { color: var(--ink); font-weight: 500; }
.cal-day:hover { border-color: var(--line); transform: translateY(-1px); }
.cal-day:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cal-day.today .n {
  display: inline-grid; place-items: center; width: 26px; height: 26px; margin: -3px 0 0 -3px;
  border-radius: 50%; background: var(--accent); color: var(--accent-ink); font-weight: 500;
}
.cal-day.open { border-color: var(--accent); background: var(--bg-raised); box-shadow: var(--shadow); }
@media (max-width: 520px) {
  .cal-dow, .cal-row { gap: 3px; }
  .cal-day { min-height: 52px; padding: 5px; }
  .cal-day .n { font-size: 12.5px; }
}

.pips { display: flex; flex-wrap: wrap; gap: 4px; }
.pip {
  width: 12px; height: 12px; border-radius: 50%; display: inline-block; flex: none;
  --c: var(--net, var(--ink-3));
}
.pip.posted { background: var(--c); }
.pip.sched  { background: transparent; box-shadow: inset 0 0 0 2px var(--c); }
.pip.ready  { background: color-mix(in srgb, var(--c) 45%, transparent); }
.pip.wait   { background: transparent; box-shadow: inset 0 0 0 1.5px var(--line); }
@media (max-width: 520px) { .pip { width: 7px; height: 7px; } .pips { gap: 2px; } }

.cal-key { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5); margin-top: var(--s4); font-size: 12.5px; color: var(--ink-3); }
.cal-key span { display: inline-flex; align-items: center; gap: var(--s2); }
.cal-key .pip { --net: var(--accent); }

/* The bubble — opens under the week row, its arrow pointing at the day. */
.cal-bubble {
  position: relative; margin: var(--s2) 0 var(--s2);
  background: var(--bg-raised); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lift); padding: var(--s4) var(--s5) var(--s5);
  transform-origin: calc((var(--col) + .5) * 100% / 7) 0;
  animation: cb-in var(--t2) var(--ease);
}
.cal-bubble::before {
  content: ""; position: absolute; top: -8px; left: calc((var(--col) + .5) * 100% / 7 - 8px);
  width: 14px; height: 14px; background: var(--bg-raised);
  border-left: 1px solid var(--line); border-top: 1px solid var(--line);
  transform: rotate(45deg);
}
@keyframes cb-in { from { opacity: 0; transform: translateY(-6px) scale(.97); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .cal-bubble { animation: none; } }
@media (max-width: 520px) { .cal-bubble { padding: var(--s3) var(--s3) var(--s4); } }

.cb-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); margin-bottom: var(--s3); }
.cb-head b { font-weight: 500; font-size: 16px; }
.cb-x { width: 38px; height: 38px; }
.cb-empty { margin: 0; color: var(--ink-3); font-size: 14.5px; }
.cb-empty a { color: var(--ink-2); }

.cb-post {
  display: grid; grid-template-columns: 112px 1fr; gap: var(--s4); align-items: start;
  padding: var(--s4) 0; border-top: 1px solid var(--line-soft);
}
.cb-post:first-of-type { border-top: 0; padding-top: var(--s1); }
@media (max-width: 520px) { .cb-post { grid-template-columns: 76px 1fr; gap: var(--s3); } }
.cb-shot {
  display: block; aspect-ratio: 4 / 5; border-radius: var(--r-sm); overflow: hidden;
  background: var(--bg-sunken); cursor: zoom-in; box-shadow: inset 0 0 0 1px var(--line-soft);
}
.cb-shot img, .cb-shot video { width: 100%; height: 100%; object-fit: cover; display: block; }
.cb-shot .waitmedia { display: grid; place-items: center; height: 100%; padding: var(--s2); text-align: center; }
.cb-main { min-width: 0; }
.cb-top { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2); color: var(--net, var(--ink)); }
.cb-top b { font-weight: 500; color: var(--ink); }
.cb-status { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; font-size: 12.5px; color: var(--ink-3); }
.cb-cap {
  margin: var(--s2) 0 0; font-size: 14.5px; line-height: 1.55; color: var(--ink-2);
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere;
}
.cb-note { margin: var(--s2) 0 0; font-size: 13px; color: var(--ink-3); }
.cb-acts { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s3); }
.cb-acts .btn.sm { width: auto; min-height: 38px; padding: var(--s2) var(--s3); font-size: 13.5px; }
.cb-acts .btn.bare.sm { padding: var(--s2); }

/* ------------------------------------------------------------------ *
 * Back to the main page — top-left of every page that isn't the week
 * ------------------------------------------------------------------ */

.backhome {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  min-height: 40px; padding: 0 var(--s3); margin-right: var(--s2);
  border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--ink-2); font-size: 14px; text-decoration: none;
  transition: background var(--t1) var(--ease), color var(--t1) var(--ease), border-color var(--t1) var(--ease);
}
.backhome:hover { background: var(--accent-soft); color: var(--ink); border-color: var(--ink-3); }
.backhome:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.backhome svg { width: 16px; height: 16px; }
@media (max-width: 420px) { .backhome span { display: none; } .backhome { padding: 0 var(--s2); } }
@media print { .backhome { display: none; } }

/* ------------------------------------------------------------------ *
 * The client's own portal — their logo in the top bar, and Sign out
 * ------------------------------------------------------------------ */

.brandmark {
  flex: none; width: 44px; height: 44px; margin-right: var(--s3);
  display: grid; place-items: center; overflow: hidden;
  border-radius: var(--r-sm); background: #FFFFFF;
  box-shadow: 0 0 0 1px var(--line-soft), var(--shadow);
  transition: transform var(--t1) var(--ease), box-shadow var(--t1) var(--ease);
}
.brandmark img { width: 84%; height: 84%; object-fit: contain; display: block; }
a.brandmark:hover { transform: translateY(-1px); box-shadow: 0 0 0 1px var(--line), var(--shadow-lift); }
a.brandmark:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 420px) { .brandmark { width: 36px; height: 36px; margin-right: var(--s2); } }

.signout {
  display: inline-flex; align-items: center; gap: 6px; margin-left: var(--s2);
  min-height: 40px; padding: 0 var(--s3); cursor: pointer; font: inherit; font-size: 14px;
  color: var(--ink-2); background: transparent;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  transition: background var(--t1) var(--ease), color var(--t1) var(--ease), border-color var(--t1) var(--ease);
}
.signout:hover { background: var(--accent-soft); color: var(--ink); border-color: var(--ink-3); }
.signout:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.signout:disabled { opacity: .5; cursor: default; }
@media (max-width: 480px) { .signout span { display: none; } .signout { padding: 0 var(--s2); } }
@media print { .signout, .brandmark { display: none; } }

/* ------------------------------------------------------------------ *
 * Flow clean-up (2026-09-27): what needs you, approve all, quieter page
 * ------------------------------------------------------------------ */

.hero-sum { margin: var(--s3) 0 0; font-size: 14.5px; color: var(--ink-3); line-height: 1.55; }
.hero-sum:empty { display: none; }
.hero-acts { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--s3); margin-top: var(--s5); }
.hero-acts .btn { width: auto; padding-left: var(--s5); padding-right: var(--s5); }

/* The corner dial and the sideways week label covered content on the week. */
.edge-note { display: none !important; }
.counter.off { display: none !important; }

/* Cards are simply there — no waiting for them to rise in as you scroll. */
.netcard.rv { opacity: 1 !important; transform: none !important; }

.fixlink {
  margin-left: auto; font-size: 12.5px; color: var(--accent); cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px; white-space: nowrap;
}
.fixlink:hover { color: var(--ink); }
.note a { color: inherit; text-underline-offset: 3px; }

/* Days that have passed fold away. */
details.day.past > summary { list-style: none; cursor: pointer; opacity: .75; }
details.day.past > summary::-webkit-details-marker { display: none; }
details.day.past > summary .fold { margin-left: var(--s3); font-size: 12.5px; color: var(--ink-3); text-decoration: underline; text-underline-offset: 3px; }
details.day.past[open] > summary .fold { visibility: hidden; }

/* Planned days: what the rhythm will post, not yet written */
.pip.plan { background: transparent; box-shadow: inset 0 0 0 1.5px var(--c); opacity: .55; border-radius: 3px; }
.cal-day.plan { border-style: dashed; }
.blogtag {
  margin-left: 6px; font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase;
  padding: 1px 5px; border-radius: 4px; background: var(--accent-soft); color: var(--ink-2); vertical-align: 1px;
}
.cb-plan { display: flex; flex-wrap: wrap; gap: var(--s2); margin-bottom: var(--s3); }
.cb-net { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: var(--r-sm);
  border: 1px dashed var(--line); color: var(--net, var(--ink)); font-size: 13.5px; }
@media (max-width: 520px) { .blogtag { display: none; } }
