/* RoastCast - the house theme.
 *
 * WHY THIS FILE EXISTS. Every page carried its own <style> block with its own
 * :root, which is fine for one page and became eight copies of a palette that
 * then drifted: `cream` was #f3eadf on six pages and #f6efe6 on two, `dim` and
 * `faint` likewise, `live` was a different green on info.html, and the font
 * stacks disagreed in their fallbacks. Nothing looked broken on any single page
 * and the site felt subtly wrong moving between them, which is exactly the
 * failure mode of a duplicated constant.
 *
 * Worse, the membership colour - the one thing a viewer actually buys - existed
 * only on index.html. pro.html, the page that SELLS the membership, was still
 * painting it gold.
 *
 * So: one palette, one type scale, one set of primitives, linked first on every
 * page. Each page keeps its own <style> for what is genuinely its own, and those
 * rules still win because they come after this file.
 *
 * ADDING A COLOUR: put it here. A hex typed into a page is a token that has
 * started drifting and does not know it yet.
 */

:root{
  /* ---- ground ---------------------------------------------------------- */
  --ink:#14110e;            /* the page. Warm near-black, never pure #000 */
  --panel:#201a13;          /* a card on the page */
  --panel2:#2a2118;         /* a card on a card */
  --line:rgba(243,234,223,.12);

  /* ---- ink ------------------------------------------------------------- */
  /* Three weights and no more. Every "this text is slightly less important"
     decision resolves to one of these, which is what stops a ninth grey being
     invented on page nine. */
  --cream:#f3eadf;          /* body text and headings   - 15.1:1 on --ink */
  --dim:#b9a893;            /* secondary                -  8.1:1 */
  --faint:#7d7060;          /* captions, footnotes      -  4.0:1, LARGE TEXT
                               AND NON-ESSENTIAL ONLY - never a sentence
                               somebody has to read to use the product */

  /* ---- the brand ------------------------------------------------------- */
  --ember:#ff7a1a;          /* RoastCast itself: the logo, section eyebrows */
  --flare:#ffc24d;          /* the warm highlight, and the "make a character" gold */
  --live:#59c97b;           /* something is happening / it worked */
  --warn:#e6b34d;           /* careful, but not broken */

  /* ---- membership -------------------------------------------------------
   *
   * THE GOLDEN RULE OF THIS SITE, WRITTEN DOWN ONCE.
   *
   * Everything here lives in a narrow band of warm hues. Measure them: ink 30
   * degrees, panel 32, cream 33, ember 25, flare 39. That is a fourteen-degree
   * spread - an analogous palette, which is why the pages feel like one place
   * even when they are carrying very different content.
   *
   * Membership used to be violet (265 degrees) and indigo (245). That is 120 to
   * 140 degrees away from ember: not a complement, which would at least be a
   * relationship, but the largest gap you can open without one. On a low-key
   * dark ground a high-chroma cool at that distance does not read as an accent,
   * it reads as a second website. It came from the extension - the Pro badge
   * really is violet - and importing it was the right instinct applied in the
   * wrong direction: the correct question was which of the two surfaces was
   * wrong, not which one to copy.
   *
   * It is also a question of AREA. Violet survives in the panel as an 11px badge
   * and one small button. On the website it became hero words, buttons, cards
   * and a full-width bar - the same hue at fifty times the coverage. Area is
   * what turns an accent into a clash.
   *
   * AND CRIMSON WAS THE SAME MISTAKE, ONE STEP SMALLER. Moving membership from
   * 265 degrees to 342 fixed the arithmetic and not the problem: at 342 it is
   * close enough to belong, but poured into hero words, button fills, icon tiles
   * and a full-width bar it stopped reading as "premium" and started reading as
   * "alert" - which is what a large area of red means to everybody, everywhere.
   * Pink on the maroon wash it created was muddy on top of that.
   *
   * THE PRINCIPLE I BROKE TWICE, written down so it stops happening:
   *
   *     HUE distinguishes at SMALL area. VALUE and MATERIAL distinguish at
   *     LARGE area.
   *
   * A second hue works at 11px - a badge in a chat line, a ring on a locked
   * tile - where there is not enough surface for it to fight anything. At the
   * scale of a page it has to be the page's own colour, made deeper or lighter
   * or given a finish, or it is simply a second brand competing with the first.
   *
   * SO THE WEBSITE IS GOLD. All of it. Membership is not a hue here, it is the
   * top of the warm ramp: a deeper ground, a warmer edge, and a sheen on the
   * highest tier. Crimson survives in exactly one place - the extension's Pro
   * badge and gate tiles - because there it is 10px, and because in the panel
   * gold is already spoken for by the goal bar sitting directly above it.
   *
   * THE WHOLE SYSTEM:
   *   ember -> flare   the brand, and the only accent on this site.
   *   crimson          the Pro badge and the gate tiles, in the panel, at 10px.
   *   --live green     semantic ONLY: it is on, it worked, you own this.
   *   --warn amber     careful, not broken.
   * A fifth means one of these has stopped meaning what it says. */
  --pro:#d92b5e; --pro-hot:#e8467a; --pro-deep:#a81d45; --pro-ink:#ffa8c0;
  --member:#e8467a;                       /* a text link  - 5.01:1 on --ink */
  --member-lit:#f0628f;                   /* its hover    - 6.14:1 */
  /* The filled button, wine into crimson. White clears AA at BOTH ends (7.18:1
     and 4.71:1) rather than only at the dark one, so the label is legible all
     the way across the sweep instead of only under half of it. */
  --member-a:#a81d45; --member-b:#d92b5e;

  /* ---- type ------------------------------------------------------------ */
  --disp:'Bebas Neue',Impact,'Arial Narrow Bold','Haettenschweiler',sans-serif;
  --body:'Montserrat','Segoe UI',system-ui,-apple-system,sans-serif;
  --mono:'Cascadia Code','Consolas',ui-monospace,monospace;

  /* ---- rhythm ---------------------------------------------------------- */
  /* One scale, so vertical space between sections is a choice from a list
     rather than a number somebody picked while looking at one screen. */
  --s1:6px; --s2:10px; --s3:16px; --s4:26px; --s5:42px; --s6:68px;
  --wrap:1080px;            /* the measure everything is centred in */
  --r1:8px; --r2:12px; --r3:16px;    /* corner radii: control, card, panel */
}

/* ---- the reset the whole site assumed but only some pages had ------------- */
*{box-sizing:border-box;margin:0}
/* Author display rules beat the UA [hidden] rule, so without this a modal that
   is display:grid is permanently on screen. Every page needs it and half of them
   were remembering to write it. */
[hidden]{display:none!important}
/* EVERY IN-PAGE JUMP LANDS WITH AIR ABOVE IT, on every page, forever.
   An anchor puts its target's first pixel at the very top of the viewport, which
   is never quite right: something is always flush against the edge.

   TWENTY, NOT EIGHTY-EIGHT. The first version cleared a sticky header's worth of
   space on every page - and only the front page has a sticky header. On the
   membership page, whose header scrolls away like any other content, all that
   did was drag eighty-eight pixels of the PREVIOUS section back into view: you
   clicked a pack and arrived with the hero's headline sliced in half across the
   top of the window, which looks like a rendering fault rather than a landing.
   Twenty is breathing room. Pages that really do have something fixed overhead
   ask for more themselves - see .packsec, which clears its own header. */
html{scroll-behavior:smooth;scroll-padding-top:20px}
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.001ms!important;
    animation-iteration-count:1!important;transition-duration:.001ms!important}
}
body{background:var(--ink);color:var(--cream);font-family:var(--body);
  font-size:17px;line-height:1.55;overflow-x:hidden;
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
img{max-width:100%}

/* ---- headings ------------------------------------------------------------ */
/* Bebas is a display face: tight, tall, and all-caps by design. It gets the
   letter-spacing back that condensed caps need at large sizes, and `balance` so
   a two-line headline breaks somewhere a person would have broken it. */
h1,h2,h3{font-family:var(--disp);font-weight:400;line-height:1.02;
  letter-spacing:.01em;text-wrap:balance}
h1{font-size:clamp(46px,7vw,84px)}
h2{font-size:clamp(34px,5vw,56px)}
h3{font-size:clamp(20px,2.4vw,26px);line-height:1.15}
p{text-wrap:pretty}

/* ---- links --------------------------------------------------------------- */
a{color:var(--flare);text-decoration:none}
a:hover{text-decoration:underline}
/* One focus ring, everywhere. Keyboard users get the same affordance on every
   page, and it is the ember rather than the browser default so it reads as part
   of the site rather than as an error state. */
:focus-visible{outline:2px solid var(--flare);outline-offset:3px;border-radius:4px}

/* ---- the shared furniture ------------------------------------------------ */
.wrap{max-width:var(--wrap);margin:0 auto;padding:0 24px}
.eyebrow{display:inline-block;font-family:var(--mono);font-size:11px;
  letter-spacing:.22em;text-transform:uppercase;color:var(--ember)}
.tag{font-family:var(--mono);font-size:.85em;color:var(--flare)}

/* Buttons. Three kinds and no more: the house action, the membership one, and
   the quiet one. A fourth would be a fourth thing to learn. */
.btn{display:inline-block;border-radius:10px;padding:13px 24px;font-weight:700;
  font-size:16px;cursor:pointer;border:0;font-family:inherit;
  transition:background .14s,filter .14s,border-color .14s,color .14s}
.btn.primary{background:var(--ember);color:var(--ink)}
.btn.primary:hover{background:var(--flare);text-decoration:none}
/* Membership, everywhere it is sold. Has to sit after .primary:hover - they are
   the same specificity, so source order is what stops an ember hover painting
   over the gradient and changing the button's identity under the cursor. */
/* THE MEMBERSHIP BUTTON IS THE HOUSE BUTTON. It was a pink slab beside a cream
   outline - the loudest element on the front page and the one colour on it that
   belonged to nothing else there. Ember into gold with dark ink: 8.6:1, the same
   material as every other primary action, and it stops announcing itself as a
   different product. */
.btn.primary.member{color:#1a1005;position:relative;overflow:hidden;isolation:isolate;
  background:linear-gradient(95deg,var(--ember),var(--flare) 75%)}
.btn.primary.member:hover{filter:brightness(1.07);background:
  linear-gradient(95deg,var(--ember),var(--flare) 75%)}
.btn.primary.member:hover{filter:brightness(1.1)}
/* A GLINT, BORROWED FROM THE PANEL'S FREE-SUMMON BUTTON, where a slow highlight
   crossing the face is what catches an eye that is looking at a stream rather
   than at the button. It works for the same reason here: a page is scanned, and
   a static gold rectangle is a shape while a moving highlight is an event.
   SLOWER THAN THE PANEL'S, deliberately. That one runs every 3.4s because it has
   about two seconds of a viewer's peripheral vision to work with. This is looked
   at directly, and anything repeating every three seconds in a reader's line of
   sight stops being a glint and becomes a spinner. Seven seconds - twice the
   panel's, deliberately - of which the sweep itself is about 1.5, so it is
   mostly still and the movement arrives as a surprise rather than as a rhythm.
   z-index -1 inside an isolated stacking context puts it above the gradient and
   BELOW the label, so the sweep lights the button rather than washing the words.
   The panel does the same thing by lifting its label instead, which is only
   available there because the label is an element. */
.btn.primary.member::after{content:"";position:absolute;inset:0;z-index:-1;
  pointer-events:none;
  background:linear-gradient(105deg,transparent 40%,rgba(255,255,255,.32) 50%,
             transparent 60%);
  transform:translateX(-130%);animation:btnsheen 7s ease-in-out infinite}
@keyframes btnsheen{0%{transform:translateX(-130%)}22%,100%{transform:translateX(130%)}}
/* Decoration, not feedback - unlike the press, which stays. This one goes. */
@media (prefers-reduced-motion: reduce){
  .btn.primary.member::after{animation:none;opacity:0}}
/* `background:transparent` is load-bearing: every .ghost used to be an <a>,
   which has no background of its own, so nothing needed to say so - and the
   first <button> to use the class rendered in the browser's buttonface grey. */
/* ...AND A GHOST STILL HAS TO LOOK LIKE A BUTTON. --line is the hairline this
   site draws card edges with, which is correct for a card edge and far too
   quiet for a control: beside a filled gold CTA it read as a caption in a box.
   That is the same fault the plans grid had, where Basic and Max were 6% washes
   and two of three tiers had no visible call to action at all.
   A WARM OUTLINE AT REAL WEIGHT, and hover WARMS it rather than filling it. The
   fill is what says "this is the recommendation", and a hero with two filled
   buttons has no recommendation in it - "Create a Character" is the second
   option here, which is a different thing from a weaker one. */
.btn.ghost{border:1px solid rgba(255,194,77,.4);color:var(--cream);
  background:rgba(255,194,77,.07);
  box-shadow:inset 0 1px 0 rgba(255,194,77,.16),0 3px 0 rgba(255,194,77,.16)}
.btn.ghost:hover{border-color:var(--flare);color:#ffe0a3;
  background:rgba(255,194,77,.14);text-decoration:none;
  box-shadow:inset 0 1px 0 rgba(255,194,77,.22),0 4px 0 rgba(255,194,77,.22)}
@media (prefers-reduced-motion: reduce){.btn{transition:none}}

/* Cards. The one surface treatment the whole site shares. */
.card{background:var(--panel);border:1px solid var(--line);
  border-radius:var(--r3);padding:var(--s4)}

footer{border-top:1px solid var(--line);padding:26px 0 40px;
  font-size:13px;color:var(--faint)}
footer .wrap{display:flex;gap:18px;flex-wrap:wrap;justify-content:space-between}

/* Skip link: the site is mostly long scrolling pages behind a nav, and a
   keyboard user should not have to tab the whole bar on every one. */
.skip{position:absolute;left:-9999px;top:0;z-index:100;
  background:var(--ember);color:var(--ink);padding:10px 16px;font-weight:700;
  border-radius:0 0 var(--r1) 0}
.skip:focus{left:0;text-decoration:none}

/* ===========================================================================
 * THE POLISH LAYER
 *
 * Everything above is the system: one palette, one scale, one of each
 * primitive. This is the part that makes it feel finished rather than merely
 * consistent - and it is deliberately SMALL and SYSTEMIC. Eight pages cannot be
 * art-directed one at a time and stay a set; what makes a site feel expensive is
 * that the same handful of gestures happen everywhere, exactly the same way.
 *
 * Four gestures, and no fifth:
 *   1. light comes from above
 *   2. surfaces lift toward the cursor
 *   3. underlines grow out of the word
 *   4. the page arrives rather than appearing
 * ======================================================================== */

/* ---- 1. LIGHT COMES FROM ABOVE -------------------------------------------
 * A single very faint warm wash at the top of every page, and a hairline along
 * the top edge of every card. Real objects catch light on the surface facing it,
 * and one consistent light source is the cheapest thing that makes a flat dark
 * palette read as a set of surfaces rather than as boxes of different greys.
 * Fixed rather than scrolling, so it belongs to the SCREEN and does not slide
 * around behind long pages and turn into a gradient somebody notices. */
body::before{content:"";position:fixed;inset:0 0 auto;height:60vh;z-index:0;
  pointer-events:none;
  background:radial-gradient(120% 80% at 50% -20%,rgba(255,122,26,.055),transparent 70%)}
body>*{position:relative;z-index:1}
.card,.tier,.cfgcard,.procard{position:relative}
.card::after,.tier::after{content:"";position:absolute;inset:0;border-radius:inherit;
  pointer-events:none;
  background:linear-gradient(180deg,rgba(255,255,255,.055),transparent 42%)}

/* ---- 2. SURFACES LIFT TOWARD THE CURSOR ---------------------------------- */
/* Two pixels and a warmer edge. Big enough to feel, small enough that a grid of
   them does not jitter as the pointer crosses it - which is the failure mode of
   card hover and the reason most of them are switched off again later. */
.card,.tier{transition:transform .18s cubic-bezier(.2,.9,.25,1),
  border-color .18s,box-shadow .18s}
.card:hover,.tier:hover{transform:translateY(-2px);
  border-color:rgba(255,122,26,.28);
  box-shadow:0 14px 34px -18px rgba(0,0,0,.9)}
/* A button is already the loudest thing in its block, so it lifts LESS than the
   card it sits on. Anything that moves more than the thing containing it reads
   as detached.

   AND IT STANDS ON SOMETHING. One lit top edge and a 3px ledge underneath - not
   a bevel, which is a 2005 artifact and reads as a toy, but the two facts that
   make any real object read as solid: light comes from above, and it is resting
   on a surface. Pressed, the ledge drops to 1px and the button travels 2px, so
   the movement is exactly the ledge it lost and nothing else about it changes.
   THE GHOST STANDS ON SOMETHING TOO NOW - on its own gold rather than on the
   filled button's brown, or the press would drop it onto a shadow belonging to a
   surface it is not on. It kept its own ledge up where its colours are declared,
   because everything about a ghost is one hue at three weights and splitting
   that across two blocks is how the two halves drift apart. All that is left
   here is the travel, and it is the same 2px for both, because the press is the
   one thing every button on this site does identically. */
.btn:not(.ghost){box-shadow:inset 0 1px 0 rgba(255,255,255,.4),
  0 3px 0 rgba(112,48,0,.45),0 7px 16px rgba(0,0,0,.3);
  transition:transform .08s,box-shadow .08s,background .14s,filter .14s}
.btn:not(.ghost):hover{box-shadow:inset 0 1px 0 rgba(255,255,255,.5),
  0 4px 0 rgba(112,48,0,.45),0 11px 22px rgba(0,0,0,.36)}
.btn:not(.ghost):active{transform:translateY(2px);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.3),0 1px 0 rgba(112,48,0,.45),
  0 2px 6px rgba(0,0,0,.3)}
.btn.ghost:active{transform:translateY(2px);
  box-shadow:inset 0 1px 0 rgba(255,194,77,.16),0 1px 0 rgba(255,194,77,.22)}
.btn.ghost{transition:transform .08s,box-shadow .08s,background .14s,
  border-color .14s,color .14s}
@media (prefers-reduced-motion:reduce){.btn{transition:none}}

/* ---- 3. UNDERLINES GROW OUT OF THE WORD ---------------------------------- */
/* `text-decoration: underline` on hover snaps a full-width line into existence
   under the word, which is the single most dated interaction on the web. This
   draws it from the left in 140ms and puts it a couple of pixels lower, where a
   typographer would have set it.

   AN UNCLASSED LINK IN A PARAGRAPH IS PROSE; A CLASSED ONE IS A COMPONENT.
   That is the whole selector, and it replaces `:not(.btn)`, which was not nearly
   narrow enough. This rule paints `background-image`, and background-image is a
   FILL - so every link that had one of its own lost it. The roster's "Unlock all
   26" pill is inside a <p>, so it matched, and its ember gradient was replaced
   by a one-pixel underline: a pill with no fill, dark text on the page's own
   dark ground, invisible. It shipped, and it was reported as "the background
   colour is bugged", which is exactly what it was.

   The rule is self-maintaining, which is the reason to prefer it over a list of
   exclusions: every component link in this project already carries a class
   because it needs one to be styled at all, so the next one is exempt by
   construction rather than by somebody remembering to add it here. */
p a:not([class]),li a:not([class]),.prose a:not([class]){
  text-decoration:none;background-image:linear-gradient(currentColor,currentColor);
  background-size:0% 1px;background-position:0 calc(100% + 2px);
  background-repeat:no-repeat;transition:background-size .14s ease}
p a:not([class]):hover,li a:not([class]):hover,.prose a:not([class]):hover{
  text-decoration:none;background-size:100% 1px}
/* Keyboard gets the ring AND the line: a focus style that is only an outline
   loses the "this is a link" half of the information. */
p a:not([class]):focus-visible,li a:not([class]):focus-visible{background-size:100% 1px}

/* ---- 4. THE PAGE ARRIVES -------------------------------------------------- */
/* Six pixels and 300ms, once, on the sections below the fold. Not a scroll
   animation - those need JavaScript, fire late on a slow connection, and leave a
   blank page if the script fails. This is CSS-only and it is a REVEAL, not a
   dependency: every element is fully visible at the end of its own animation
   whatever else happens.
   The hero is deliberately excluded. The first thing a visitor sees should be
   there when they arrive, not fade in at them. */
@media (prefers-reduced-motion: no-preference){
  main>section:not(:first-child),.card,.tier{
    animation:rise .3s cubic-bezier(.2,.9,.25,1) both}
  @keyframes rise{from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:none}}
  /* The hover transform and the arrival animation both write `transform`, and
     the animation wins while it runs. `both` holds the end state, so once it has
     finished the hover is free again - which is why this is a keyframe that ends
     on `none` rather than one that ends on a value. */
}

/* ---- selection, scrollbar, and the small things ---------------------------- */
::selection{background:rgba(255,122,26,.3);color:var(--cream)}
/* A dark page with a bright OS scrollbar has a white stripe down one side. */
html{scrollbar-color:var(--panel2) var(--ink);scrollbar-width:thin}
/* Numbers that sit in columns - prices, counts, dates - line up. */
.num,.price,td,.tp b{font-variant-numeric:tabular-nums}
/* Anything that can overflow gets its own scroller, so the PAGE never scrolls
   sideways. This is the rule that keeps a long table or a code block from
   breaking the layout on a phone, and it was written per-page before. */
table,pre{display:block;max-width:100%;overflow-x:auto}
