/* ============================================================
   THE DESIGN SYSTEM

   Load order: juq.css -> patch.css -> system.css -> page css.

   WHY THIS EXISTS

   juq.css is a byte-for-byte copy of the approved mockup and is never edited.
   It is a beautiful home page, but it is not a system: it uses 20 different
   font sizes, a dozen gap values, and retypes the same "small mono uppercase
   label" recipe dozens of times. That is fine for ONE page drawn by hand. It is
   exactly how a site ends up with every page slightly different, which is what
   happened to the previous build.

   So this file does two things:

     1. Names the scale. Every value below ALREADY APPEARS in the mockup - the
        ramp was measured out of it, not invented - so nothing shifts by a
        pixel. What changes is that a new page picks from ten spacing steps and
        eleven type steps instead of picking a number that looked about right.

     2. Provides the components that were being retyped, so "a section label"
        is one class rather than six lines copied between files.

   THE RULE: new pages use these tokens and classes. A raw px font-size or a
   hand-rolled label in a page stylesheet is a bug, and tools/check-drift.mjs
   fails on it.

   juq.css keeps its literal values. Migrating it would mean touching the
   approved copy for no visual gain, and the whole point of keeping it pure is
   that it cannot drift.
   ============================================================ */

:root {
  /* ---------- TYPE ----------
     Measured from the mockup. The dominant sizes there are 11px (27 uses),
     10px/10.5px (35 between them) and 12px/12.5px (26 between them); where the
     mockup uses two neighbouring sizes for the same job, the ramp keeps ONE, so
     new work cannot reintroduce the split. */
  --t-micro: 9.5px;   /* chart axis, day-of-week letters, densest meta */
  --t-label: 10.5px;  /* mono uppercase labels - the most repeated recipe */
  --t-mono: 11px;     /* mono body: tooltips, notes, counters */
  --t-sm: 12.5px;     /* small prose, card descriptions */
  --t-body: 13px;     /* default prose */
  --t-md: 14px;       /* emphasised prose, lead paragraphs */
  --t-lg: 15px;       /* buttons, section headings */
  --t-h3: 18px;       /* card titles */
  --t-h2: 22px;       /* panel headings */
  --t-h1: 26px;       /* page headings */
  /* Display type (hero titles) stays on clamp() per component: it is meant to
     scale with the viewport, and pinning it to a step would flatten that. */

  /* ---------- SPACING ----------
     Also measured: the mockup's gaps cluster hard at 8px and 10px, then 12,
     6, 16, 20. Ten steps covers every real use with room to breathe. */
  --s-1: 4px;
  --s-2: 6px;
  --s-3: 8px;
  --s-4: 10px;
  --s-5: 12px;
  --s-6: 16px;
  --s-7: 20px;
  --s-8: 26px;
  --s-9: 34px;
  --s-10: 46px;
  /* The top of the ramp was measured from `gap` alone, which never exceeds 18px
     in the mockup - so it stopped at 46 and had nothing for page-level padding.
     The real sheets use 56 and 64, and the Links page 60. Without this step a
     page's bottom padding gets clamped to 46 and loses real air. */
  --s-11: 60px;

  /* ---------- PAGE RHYTHM ----------
     The reason pages felt different from each other last time: each one chose
     its own distance between sections. */
  --page-top: var(--s-10);      /* first section to the navbar */
  --section-gap: var(--s-10);   /* between major sections */
  --block-gap: var(--s-7);      /* between blocks inside a section */

  /* ---------- LETTER SPACING ----------
     Three settings, because the mockup only ever really uses three. */
  --ls-label: 0.12em;   /* mono uppercase labels */
  --ls-wide: 0.16em;    /* eyebrows, the widest treatment */
  --ls-tight: -0.03em;  /* display type */
}

/* ============================================================ UTILITIES

   Prefixed u- so they can never collide with a mockup class name. Each one
   exists because it was already being retyped in three or more places.
   ============================================================ */

/* The single most repeated recipe on the site: a small mono uppercase label in
   the muted colour. It appeared 82 times across the stylesheets, in six
   slightly different versions. */
.u-lbl {
  font-family: var(--f-mono);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--muted);
}
.u-lbl-accent { color: var(--accent); }
.u-lbl-ink { color: var(--ink); }
/* The trailing hint some labels carry ("optional", "match these"). */
.u-lbl small {
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-left: var(--s-2);
}

/* A bordered surface. Every page has been drawing this by hand. */
.u-card {
  background: var(--surface);
  border: var(--bw) solid var(--line-strong);
  border-radius: var(--radius);
  padding: var(--s-6) var(--s-7);
}
.u-card-tight { padding: var(--s-5); }
/* The heavier treatment the mockup gives to things it wants you to look at. */
.u-card-loud {
  border-color: var(--ink);
  box-shadow: 8px 8px 0 var(--accent);
}
[data-skin="sweet"] .u-card-loud { border-color: transparent; box-shadow: 0 18px 44px var(--shadow); }

/* Anything standing in for content: loading, empty, signed out, failed. This
   was written five separate times as .njz-note, .lb-empty, .mile-empty,
   .ad-note and .ipk-note. */
.u-note {
  font-family: var(--f-mono);
  font-size: var(--t-mono);
  line-height: 1.6;
  color: var(--muted);
}
.u-note-block { padding: var(--s-9) var(--s-5); text-align: center; }

/* A label-left value-right row with a hairline above. The forecast rows, the
   wallet log and the leaderboard are all this shape. */
.u-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-3) 0;
  border-top: var(--bw) solid var(--line);
  font-size: var(--t-sm);
}
.u-row:first-child { border-top: none; }
.u-row > span:first-child { color: var(--muted); }
.u-row b {
  font-family: var(--f-mono);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Vertical rhythm without every page inventing its own margins. */
.u-stack > * + * { margin-top: var(--block-gap); }
.u-stack-sm > * + * { margin-top: var(--s-4); }

/* The responsive card grid pages keep rebuilding. */
.u-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--u-grid-min, 240px), 1fr));
  gap: var(--s-5);
}

/* Page-level rhythm. A page uses .u-page and stops making the decision. */
.u-page { padding-top: var(--page-top); padding-bottom: var(--s-10); }
.u-section + .u-section { margin-top: var(--section-gap); }

/* Numbers that change in place must not make the layout twitch. */
.u-num { font-variant-numeric: tabular-nums; }

/* ============================================================ ARRIVING COVERED

   The page transition covers the screen, navigates underneath, and sweeps back
   off on the next page. The sweep-off is drawn by JavaScript, and JavaScript
   cannot run before the browser's first paint - so the new page was flashing up
   UNCOVERED for a frame or two before the panel appeared over it and left again.
   That flash is the glitch in the middle of the transition.

   The inline script in each page's <head> sets data-fx-in before anything is
   painted, so this covers the page from the very first frame. revealIn() clears
   the attribute at the exact moment it starts animating the real panel out, so
   the static cover hands over with no gap.

   Above .fx (80) and modals (85), because while it is up it IS the page. The
   inline script also clears it on a timer, so a JavaScript failure can never
   leave someone staring at a solid orange screen.
   ============================================================ */
html[data-fx-in]::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--accent);
  pointer-events: none;
}

/* ============================================================
   PAGE HEAD

   Promoted out of links.css, which is where it was written first. Requests and
   JuqTube both want the identical block, and three copies of a heading scale is
   exactly how the last build ended up with every page slightly different.
   ============================================================ */
.pg-head { padding: var(--s-9) 0 var(--s-2); }
.pg-title { font-size: clamp(34px, 6vw, 58px); margin: var(--s-4) 0 var(--s-3); }
.pg-title em { color: var(--accent); font-style: italic; }
.pg-sub { color: var(--muted); font-size: var(--t-md); max-width: 62ch; margin: 0; }

/* ============================================================
   SEGMENTED CONTROL

   One row of buttons where exactly one is active: sorts, filters, tabs. The
   links page has its own `.lk-seg` doing this; requests and JuqTube need five
   more between them, so it becomes a component rather than a sixth copy.

   `.u-seg-slide` is for options that come and go with context - they collapse
   to nothing rather than disappearing, so the row does not jump and you can see
   WHICH option left.
   ============================================================ */
.u-seg { display: flex; border: var(--bw) solid var(--line-strong); border-radius: var(--radius); overflow: hidden; }
.u-seg button {
  display: flex; align-items: center; gap: var(--s-2);
  font-family: var(--f-mono); font-size: var(--t-label); letter-spacing: 0.1em; text-transform: uppercase;
  padding: var(--s-3) var(--s-5); cursor: pointer; white-space: nowrap;
  background: var(--surface); color: var(--muted);
  border: 0; border-right: var(--bw) solid var(--line-strong);
}
.u-seg button:last-child { border-right: 0; }
.u-seg button.on { background: var(--accent); color: var(--accent-ink); }
.u-seg-slide { max-width: 200px; transition: max-width 0.26s ease, padding 0.26s ease, opacity 0.2s ease; overflow: hidden; }
.u-seg-slide.gone { max-width: 0; padding-left: 0; padding-right: 0; opacity: 0; border-right-width: 0; }

/* ============================================================
   TOASTS

   Confirmations and refusals. The refusals are the reason this exists: running
   out of votes is otherwise invisible, because the button just stops doing
   anything. The container carries aria-live, so a refusal is spoken and not
   only seen.
   ============================================================ */
.u-toasts {
  position: fixed; left: 50%; bottom: var(--s-7); transform: translateX(-50%);
  /* Above the modal layer (85): a refusal raised from inside an open entry
     has to be visible, and at 70 it was behind it. */
  z-index: 92; display: flex; flex-direction: column; gap: var(--s-3); align-items: center;
  pointer-events: none;
}
.u-toast {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-4) var(--s-6);
  border: var(--bw) solid var(--ink); border-radius: var(--radius);
  background: var(--surface); color: var(--ink);
  font-family: var(--f-mono); font-size: var(--t-mono);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.28);
}
.u-toast.ok { border-color: var(--ok); }
.u-toast.err { border-color: var(--accent); }
.u-toast .mark { font-weight: 700; }
.u-toast.ok .mark { color: var(--ok); }
.u-toast.err .mark { color: var(--accent); }

/* ============================================================
   THE FRIED EGG LOADER

   Ported from v1, where it replaced every plain spinner on the site and became
   the one piece of motion the place is recognised by. Four whites wobble in and
   out under a gooey blur while their yolks pop, and a themed message rotates
   underneath.

   NO GSAP. v1 pulled the whole library from a CDN to drive this, with a
   text-only fallback for when it did not load. The redesign has no external
   scripts at all and is not adding one for a loading state, so the same
   animation is CSS keyframes: it starts instantly, costs nothing, and cannot
   fail to arrive.

   The gooey look is a blur plus a hard alpha contrast in one SVG filter, which
   is what makes two overlapping circles read as one blob of egg white rather
   than two circles. The filter itself lives in site.js beside the markup.
   ============================================================ */
/* CENTRED IN WHATEVER IT IS DROPPED INTO. Centring its own contents is not
   enough: most hosts here are grids of cards, and a loader placed in one is a
   grid ITEM, so it lands in the first 258px column on the left however centred
   its insides are. Spanning every column and every flex line is what actually
   puts it in the middle, and both properties are inert in a plain block. */
.juq-egg {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s-4); padding: var(--s-7) var(--s-5);
  width: 100%; grid-column: 1 / -1; flex-basis: 100%;
}
.juq-egg-svg { width: 190px; max-width: 70%; height: auto; display: block; overflow: visible; }

.juq-egg-white {
  fill: var(--ink); transform-box: fill-box; transform-origin: 50% 50%;
  animation: juq-egg-white 2.5s linear infinite;
}
.juq-egg-yolk {
  transform-box: fill-box; transform-origin: 50% 50%;
  animation: juq-egg-yolk 2.5s linear infinite;
}

/* LINEAR on purpose: the easing is already baked into the keyframe stops,
   sampled off GSAP's own elastic.out. Letting CSS ease BETWEEN those stops as
   well would ease an eased curve and flatten the wobble back out. */

/* v1 staggers each egg by half a second and then seeks the timeline 100s in,
   so on arrival they are already mid-cycle at 0.0, 2.0, 1.5 and 1.0 seconds.
   A negative delay is how CSS says "already running". */
.juq-egg-white:nth-of-type(2), .juq-egg-yolk:nth-of-type(2) { animation-delay: -2s; }
.juq-egg-white:nth-of-type(3), .juq-egg-yolk:nth-of-type(3) { animation-delay: -1.5s; }
.juq-egg-white:nth-of-type(4), .juq-egg-yolk:nth-of-type(4) { animation-delay: -1s; }

@keyframes juq-egg-white {
  0% { transform: scale(0); }
  2% { transform: scale(0.3629); }
  4% { transform: scale(0.6883); }
  6% { transform: scale(0.9213); }
  8% { transform: scale(1.0556); }
  10% { transform: scale(1.1102); }
  12% { transform: scale(1.1126); }
  14% { transform: scale(1.0884); }
  16% { transform: scale(1.0563); }
  18% { transform: scale(1.0276); }
  20% { transform: scale(1.007); }
  22% { transform: scale(0.9951); }
  24% { transform: scale(0.9903); }
  26% { transform: scale(0.99); }
  28% { transform: scale(0.9922); }
  30% { transform: scale(0.995); }
  32% { transform: scale(0.9976); }
  34% { transform: scale(0.9994); }
  36% { transform: scale(1.0004); }
  38% { transform: scale(1.0009); }
  40% { transform: scale(1); }
  40% { transform: scale(1); }
  45% { transform: scale(0.9841); }
  50% { transform: scale(0.933); }
  55% { transform: scale(0.8307); }
  60% { transform: scale(0.5); }
  65% { transform: scale(0.1693); }
  70% { transform: scale(0.067); }
  75% { transform: scale(0.0159); }
  80% { transform: scale(0); }
  80%, 100% { transform: scale(0); }
}

@keyframes juq-egg-yolk {
  0% { transform: scale(-1.3, 0); }
  1.75% { transform: scale(-0.1512, 0.4699); }
  2% { transform: scale(-0.0132, 0.537); }
  3.5% { transform: scale(0.8146, 0.8085); }
  4% { transform: scale(0.9528, 0.899); }
  5.25% { transform: scale(1.2984, 1.0249); }
  6% { transform: scale(1.332, 1.1004); }
  7% { transform: scale(1.3767, 1.1406); }
  8% { transform: scale(1.3052, 1.1808); }
  8.75% { transform: scale(1.2516, 1.1822); }
  10% { transform: scale(1.1387, 1.1845); }
  10.5% { transform: scale(1.0935, 1.1757); }
  12% { transform: scale(1.0029, 1.1491); }
  12.25% { transform: scale(0.9878, 1.1432); }
  14% { transform: scale(0.9485, 1.1013); }
  15.75% { transform: scale(0.9536, 1.0625); }
  16% { transform: scale(0.9566, 1.057); }
  17.5% { transform: scale(0.9752, 1.0317); }
  18% { transform: scale(0.9807, 1.0233); }
  19.25% { transform: scale(0.9947, 1.0098); }
  20% { transform: scale(0.9991, 1.0018); }
  21% { transform: scale(1.0051, 0.9962); }
  22% { transform: scale(1.0064, 0.9906); }
  22.75% { transform: scale(1.0074, 0.9893); }
  24% { transform: scale(1.0058, 0.987); }
  24.5% { transform: scale(1.0052, 0.9872); }
  26% { transform: scale(1.0026, 0.9878); }
  26.25% { transform: scale(1.0021, 0.9881); }
  28% { transform: scale(1, 0.9906); }
  30% { transform: scale(1, 0.994); }
  32% { transform: scale(1, 0.9968); }
  34% { transform: scale(1, 0.9989); }
  36% { transform: scale(1, 1.0002); }
  38% { transform: scale(1, 1.0008); }
  40% { transform: scale(1, 1); }
  45% { transform: scale(0.9972); }
  50% { transform: scale(0.9844); }
  55% { transform: scale(0.9116); }
  60% { transform: scale(0.5); }
  65% { transform: scale(0.0884); }
  70% { transform: scale(0.0156); }
  75% { transform: scale(0.0028); }
  80% { transform: scale(0); }
  80%, 100% { transform: scale(0); }
}

/* ---------- the message, word by word ----------

   v1 drove this with GSAP: each word pops in on a back.out(2.5) overshoot,
   staggered 0.08s, then settles into a staggered sine bob so the line reads as
   a wave rather than as static text. The redesign loads no GSAP on public
   pages, so the three curves are SAMPLED FROM GSAP'S OWN FORMULAS and baked
   into the keyframes below. Same numbers, no library. See the generator note
   on each block for the tween it came from. */
.juq-egg-text {
  font-family: var(--f-mono); font-size: var(--t-mono); letter-spacing: 0.04em;
  color: var(--muted); text-align: center; min-height: 1.4em; max-width: 34ch;
}

/* Two nested elements because the entrance and the bob both drive `transform`
   and one element cannot run both. The outer word pops in; the inner one bobs
   forever, starting when the pop has finished. */
.juq-egg-word {
  display: inline-block;
  animation: juq-word-in 0.4s both;
  animation-delay: calc(var(--w) * 0.08s);
}
.juq-egg-word > i {
  display: inline-block; font-style: normal;
  animation: juq-word-bob 1.8s linear infinite both;
  /* --bob is computed per line, because GSAP starts the second tween after the
     WHOLE first one plus 0.1s, which depends on how many words there are. */
  animation-delay: calc(var(--bob) * 1s);
}
.juq-egg-text.swap .juq-egg-word { animation: juq-word-out 0.15s both; }

/* from({ opacity: 0, scale: 0.4, y: 8 }), ease back.out(2.5) */
@keyframes juq-word-in {
  0% { opacity: 0; transform: translateY(8px) scale(0.4); }
  5% { opacity: 0.2554; transform: translateY(5.9565px) scale(0.5533); }
  10% { opacity: 0.4735; transform: translateY(4.212px) scale(0.6841); }
  15% { opacity: 0.6568; transform: translateY(2.7455px) scale(0.7941); }
  20% { opacity: 0.808; transform: translateY(1.536px) scale(0.8848); }
  25% { opacity: 0.9297; transform: translateY(0.5625px) scale(0.9578); }
  30% { opacity: 1; transform: translateY(-0.196px) scale(1.0147); }
  35% { opacity: 1; transform: translateY(-0.7605px) scale(1.057); }
  40% { opacity: 1; transform: translateY(-1.152px) scale(1.0864); }
  45% { opacity: 1; transform: translateY(-1.3915px) scale(1.1044); }
  50% { opacity: 1; transform: translateY(-1.5px) scale(1.1125); }
  55% { opacity: 1; transform: translateY(-1.4985px) scale(1.1124); }
  60% { opacity: 1; transform: translateY(-1.408px) scale(1.1056); }
  65% { opacity: 1; transform: translateY(-1.2495px) scale(1.0937); }
  70% { opacity: 1; transform: translateY(-1.044px) scale(1.0783); }
  75% { opacity: 1; transform: translateY(-0.8125px) scale(1.0609); }
  80% { opacity: 1; transform: translateY(-0.576px) scale(1.0432); }
  85% { opacity: 1; transform: translateY(-0.3555px) scale(1.0267); }
  90% { opacity: 1; transform: translateY(-0.172px) scale(1.0129); }
  95% { opacity: 1; transform: translateY(-0.0465px) scale(1.0035); }
  100% { opacity: 1; transform: translateY(0px) scale(1); }
}

/* to({ y: -3 }, 0.9s, sine.inOut, yoyo, repeat -1).
   Yoyoing a sine.inOut over 0.9s each way is EXACTLY one cosine period of
   1.8s, so one sinusoid replaces the tween and its bounce with no seam at the
   turn: on the way back s = 2 - t/0.9, and cos(pi*s) = cos(2pi*t/1.8). */
@keyframes juq-word-bob {
  0% { transform: translateY(0px); }
  5% { transform: translateY(-0.0734px); }
  10% { transform: translateY(-0.2865px); }
  15% { transform: translateY(-0.6183px); }
  20% { transform: translateY(-1.0365px); }
  25% { transform: translateY(-1.5px); }
  30% { transform: translateY(-1.9635px); }
  35% { transform: translateY(-2.3817px); }
  40% { transform: translateY(-2.7135px); }
  45% { transform: translateY(-2.9266px); }
  50% { transform: translateY(-3px); }
  55% { transform: translateY(-2.9266px); }
  60% { transform: translateY(-2.7135px); }
  65% { transform: translateY(-2.3817px); }
  70% { transform: translateY(-1.9635px); }
  75% { transform: translateY(-1.5px); }
  80% { transform: translateY(-1.0365px); }
  85% { transform: translateY(-0.6183px); }
  90% { transform: translateY(-0.2865px); }
  95% { transform: translateY(-0.0734px); }
  100% { transform: translateY(0px); }
}

/* to({ opacity: 0, scale: 0.8 }, 0.15s, power1.in) */
@keyframes juq-word-out {
  0% { opacity: 1; transform: scale(1); }
  25% { opacity: 0.9375; transform: scale(0.9875); }
  50% { opacity: 0.75; transform: scale(0.95); }
  75% { opacity: 0.4375; transform: scale(0.8875); }
  100% { opacity: 0; transform: scale(0.8); }
}

/* A loader inside a row or a small panel, where the full thing would be
   comically large: same egg, smaller, message beside it rather than under. */
.juq-egg.inline { flex-direction: row; gap: var(--s-4); padding: var(--s-5) 0; justify-content: flex-start; align-items: center; }
.juq-egg.inline .juq-egg-svg { width: 74px; }
.juq-egg.inline .juq-egg-text { text-align: left; }

/* Somebody who asked not to be animated at gets the egg, still, holding its
   cooked frame, and the messages still rotate. */
@media (prefers-reduced-motion: reduce) {
  .juq-egg-white, .juq-egg-yolk { animation: none; }
  .juq-egg-yolk { transform: scale(1); }
  /* The words still arrive and still change; they just stop moving. */
  .juq-egg-word, .juq-egg-word > i, .juq-egg-text.swap .juq-egg-word { animation: none; }
  .juq-egg-text.swap { opacity: 0; transition: opacity 0.15s linear; }
}

/* ============================================================
   EFFECTS

   Something aimed at one person, arriving on whatever page they are on. The
   layer never takes a click and never outlasts about fifteen seconds: it is a
   joke on a friend, not something they have to get rid of.
   ============================================================ */
.fx-layer {
  position: fixed; inset: 0; z-index: 95;
  /* NEVER takes a click. An overlay that eats input is a page somebody has to
     reload to escape, which is a different thing from a prank. */
  pointer-events: none;
  overflow: hidden;
}

.fx-egg, .fx-coin {
  position: absolute; top: -8vh; line-height: 1;
  animation-name: fx-fall; animation-timing-function: linear; animation-fill-mode: both;
}
.fx-coin { font-size: 26px; }

@keyframes fx-fall {
  0% { transform: translateY(-10vh) rotate(0deg); opacity: 0; }
  8% { opacity: 1; }
  90% { opacity: 1; }
  100% { transform: translateY(112vh) rotate(420deg); opacity: 0; }
}

/* The second cursor. Deliberately plain: a recognisable arrow reads as a
   rendering fault, which is funnier than anything decorated would be. */
.fx-ghost {
  position: absolute; top: 0; left: 0; width: 14px; height: 20px;
  background: var(--ink); opacity: 0.55;
  clip-path: polygon(0 0, 0 78%, 28% 60%, 48% 100%, 68% 90%, 48% 52%, 100% 46%);
  transition: transform 0.28s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  /* playEffect refuses to run at all under reduced motion; this is the belt to
     that braces, for anything already on screen when the setting changes. */
  .fx-egg, .fx-coin { animation: none; display: none; }
  .fx-ghost { display: none; }
}
