/* Roll Evil — the app shell.
   ===========================================================================

   WHY THIS EXISTS

   play.html, create.html and dm.html each carried their own inline stylesheet,
   41k, 22k and 34k characters, with no shared tokens between them. All three
   had independently arrived at the same shape: a dark page, a row of cards,
   each card a dark rectangle with a coloured left border and a small uppercase
   label. That is the layout a language model reaches for by default, which is
   why Chan's Gemini output looked like our own tools.

   THE LOOK

   Chan, 9 Oct, on the first pass: "not bad, but how about something more in
   your face and old school... the brick motif and especially the spikes give
   off an evil feel, add some chains here and there".

   So: heavy masonry with the mortar showing, iron bands, and spikes. The first
   version was a thin brass line, which was tasteful and far too polite for a
   book about playing the monster.

   Explicitly NOT included, on his instruction: title banners, rune strips down
   the sides, corner skulls. Those eat the space a character sheet needs.

   What this also does not do is reproduce Blizzard's actual interface art.
   Roll Evil ships SRD 5.1/5.2.1 CC-BY and folklore only so it is clean to
   sell, and borrowing another publisher's UI furniture would undo that for the
   sake of a border. Every asset here is generated or drawn from scratch: the
   visual language, not the assets.

   HOW IT SCALES

   1. Frames are ONE painted texture used as a 9-slice via border-image. The
      corner stones hold their drawn size at any panel size while the courses
      between them repeat, so a 200px chip and a 1200px panel share one asset
      and the masonry never stretches into smears. The first pass drew the
      frame as SVG rectangles and Chan was right that it read as sterile: a
      vector rectangle has no brush, no chipped edge, no grime in the mortar.
      Ornament that is pure geometry (chains, rules, chamfers) stays drawn,
      because that is geometry and gains nothing from a brush.
   2. Spikes and chains sit in ::before / ::after with negative offsets, so
      they overhang the frame and cost NO layout space. That is what lets an
      in-your-face border survive on a phone.
   3. Type and spacing are clamp(), so nothing steps at a breakpoint.
   4. Ornament DENSITY drops on small screens, not ornament size. Below 720px
      the spikes get shorter and the chains come off entirely; the masonry, the
      iron and the hierarchy stay. Shrinking fine detail is what turns fantasy
      UI into mud on a phone.

   Load this BEFORE the page's own <style> block so the page can still override.
*/

:root {
  /* ---- ground: soot and dark stone, neutral ----
     These were blue (#06080d upward through #243049) to read as Icecrown.
     Chan, 9 Oct: the whole thing came out too frosty. A blue-black ground
     tints every pixel laid on it, so the neutral has to start here, not at
     the accent. Faintly warm rather than dead grey, so it reads as soot. */
  --rv-stone-900: #09090a;
  --rv-stone-800: #0e0e10;
  --rv-stone-700: #16161a;
  --rv-stone-600: #1f1f24;
  --rv-stone-500: #2b2b31;

  /* ---- pale: the light source, bone and ash rather than ice ---- */
  --rv-pale-400: #b5afa4;
  --rv-pale-300: #cfc9bd;
  --rv-pale-200: #e6e1d7;
  --rv-pale-glow: rgba(181, 175, 164, .26);

  /* the ice names stay as aliases so nothing that still points at them
     breaks; they are no longer blue */
  --rv-ice-400: var(--rv-pale-400);
  --rv-ice-300: var(--rv-pale-300);
  --rv-ice-200: var(--rv-pale-200);
  --rv-ice-glow: var(--rv-pale-glow);

  /* ---- iron: the metalwork. Dirty, neutral, never bright ---- */
  --rv-iron-700: #2d2d30;
  --rv-iron-600: #3f3f44;
  --rv-iron-500: #58585e;
  --rv-iron-400: #81818a;

  /* ---- tarnished brass, for the few warm accents ---- */
  --rv-brass-500: #a8854a;
  --rv-brass-400: #c9a45f;

  /* ---- ember: the one genuinely malevolent note. Use it on ONE thing per
     screen, never as a fill ---- */
  --rv-ember-500: #7f3326;
  --rv-ember-400: #a84b35;

  --rv-text: #e3e0d9;
  --rv-text-dim: #9d988f;
  --rv-text-faint: #6b6760;

  --rv-accent: var(--rv-brass-400);

  --rv-rule: rgba(88, 88, 94, .42);
  --rv-rule-strong: rgba(129, 129, 138, .6);

  --rv-gap: clamp(10px, 1.6vw, 18px);
  --rv-pad: clamp(16px, 2.2vw, 28px);
}

/* ===========================================================================
   THE MASONRY FRAME, as a 9-slice.

   One SVG, sliced 3x3. The corner stones are drawn inside a 38px block and
   border-image-slice keeps them unstretched; the courses between repeat with
   `round` so a half-brick never appears at the join.
   =========================================================================== */

.rv-frame {
  position: relative;
  padding: var(--rv-pad);
  background:
    linear-gradient(180deg, rgba(48, 46, 42, .3), rgba(9, 9, 10, .94)),
    var(--rv-stone-800);
  /* The foot is NOT masonry.

     Chan, 9 Oct: close the bottom with a thin line rather than a second heavy
     course. He is right about the weight. The tabs now cap the top and the
     chains run the sides, so a full masonry sill underneath made the panel
     read as walled in on every side with no way out of it.

     So the bottom edge of the border-image is given zero width, which stops
     the sill AND the two bottom corners from drawing, and lets the chains run
     all the way to the foot. A 3px transparent strip is left in the border box
     for ::after to draw the closing bar into. */
  border: 26px solid transparent;
  border-bottom-width: 3px;
  border-image-slice: 72 fill;
  border-image-width: 26px 26px 0;
  border-image-repeat: round;
  /* PAINTED, not drawn. The first pass built this frame out of SVG rectangles
     and it read as sterile, because a vector rectangle has no brush, no chipped
     edge and no grime in the mortar. This is a painted texture sliced 3x3: the
     corners hold at their drawn size, the masonry runs between them repeat.
     Slice measured off the art by make-ui-assets.py, not guessed: the band is
     14.1% of the 512px asset, so 72px. */
  /* the chained frame: masonry lintel and sill, chains down both sides.
     NOTE the slice stays 72 even though a re-measure of this file reports 138.
     The measurement looks for a hollow dark centre, and knocking the sides out
     to transparent moved what it finds. The corner blocks are still the 177px
     masonry they always were, and 177/1252 of 512 is 72. Measuring the
     composite instead of the masonry would cut the corners in half. */
  border-image-source: image-set(
    url("images/ui/ui_frame_chain.webp?v=20261014b") type("image/webp"),
    url("images/ui/ui_frame_chain.png?v=20261014b") type("image/png"));
}

/* ---------------------------------------------------------------------------
   THE INSET FRAME, for a card sitting inside a panel.

   Not a shrunken main frame. A 26px band on a 150px card leaves almost no
   content width, and scaling the main texture down just turns its rivets into
   noise, so the inner card gets its own thinner asset: same granite, no
   spikes, a band measured at 7.8% instead of 14.1%. Use .rv-inset on any card that
   lives inside a .rv-frame, or on anything narrower than about 320px.
   --------------------------------------------------------------------------- */
.rv-frame.rv-inset {
  --rv-pad: clamp(13px, 1.5vw, 19px);
  /* One thin metal bar, nothing else.

     Chan, 9 Oct: the weakness box was "a bit too thick", and pointed at a WoW
     addon window framed by a single thin band. He is right that a second
     painted frame was never going to work at this size. The painted inset
     asset is retired; this is four bevelled pixels.

     The bevel is what sells it as metal: light on the top and left where a
     light above would catch the band, dark on the bottom and right, seated on
     a black hairline inside and out so it does not bleed into the panel. */
  /* All four sides the SAME value.

     Chan, 9 Oct: the top and left looked thicker than the other two. They were
     not, they were lighter, and a lighter band at the same width reads as a
     wider one. A strong bevel is how you fake depth on a 20px chrome button;
     on a 3px band around a dark panel it just makes two of the four sides
     shout. So the bevel is now a hairline on top of a uniform band rather than
     the band itself. */
  border: 3px solid var(--rv-iron-600);
  border-image: none;
  box-shadow:
    inset 0 0 0 1px #000,
    0 0 0 1px #000;
  background: linear-gradient(180deg, var(--rv-stone-700), var(--rv-stone-900));
}
.rv-frame.rv-inset::after { display: none; }
/* the hairline that keeps it from reading as flat tape, at a tenth of the
   contrast the old bevel had */
.rv-frame.rv-inset::before {
  box-shadow: inset 0 1px 0 rgba(230, 225, 215, .05);
}

/* ---------------------------------------------------------------------------
   CORNER PIECES for the thin frame.

   Chan: "lets have some corner graphics see WoW's it doesnt have to be insane,
   just a little is fine". A plain band reads as a div with a border; a band
   with its corners picked out reads as something that was built. Two small
   brackets, drawn with gradients, no asset.

   They are a nested element rather than more pseudo-elements on the frame
   itself, because ::before and ::after are already spoken for. Put
   <i class="rv-corners"></i> as the FIRST child of a .rv-inset.
   --------------------------------------------------------------------------- */
/* `.rv-frame > *` sets position:relative on every direct child and has the
   SAME specificity as a bare .rv-corners, so whichever came later won. It did.
   Qualifying the selector settles it by specificity instead of by file order,
   which is the thing that will not quietly break next time a block moves. */
.rv-frame > .rv-corners,
.rv-corners {
  position: absolute;
  inset: -3px;
  pointer-events: none;
  z-index: 3;
}
.rv-corners::before,
.rv-corners::after {
  content: '';
  position: absolute;
  width: 12px;
  height: 12px;
  /* iron-500, not 400. A bracket brighter than the band it sits on is the same
     mistake as the old bevel: it reads as a highlight rather than as metal. */
  border: 3px solid var(--rv-iron-500);
}
/* top-left and bottom-right: two L brackets, which is enough to read as a
   frame without ornamenting all four and turning it into a picture mount */
.rv-corners::before {
  top: 0;
  left: 0;
  border-right: 0;
  border-bottom: 0;
  box-shadow: -1px -1px 0 0 #000;
}
.rv-corners::after {
  right: 0;
  bottom: 0;
  border-left: 0;
  border-top: 0;
  box-shadow: 1px 1px 0 0 #000;
}

/* ---------------------------------------------------------------------------
   BARS, for a block nested inside a panel.

   Chan, 9 Oct: the nested card was "too much masonry". He is right, and the
   reason is structural rather than decorative. A frame inside a frame reads as
   two walls, and the eye has to work out which one owns the content. Masonry
   is for the OUTER edge of a panel; anything nested gets banded instead.

   Two thin iron bars, top and foot, nothing down the sides. Drawn, not a
   texture: this is a bevel and a gradient, which is pure geometry, and a
   painted bar at 3px would just be three rows of noise.
   --------------------------------------------------------------------------- */
.rv-bars {
  position: relative;
  padding: clamp(12px, 1.5vw, 18px) 0;
  margin: clamp(12px, 1.5vw, 18px) 0;
  background: linear-gradient(180deg, rgba(255, 255, 255, .014), rgba(0, 0, 0, .3));
}
.rv-bars::before,
.rv-bars::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 3px;
  /* light along the top of the bar, body, shadow under it: the three stops are
     what make 3px read as metal instead of as a border */
  background: linear-gradient(180deg,
    var(--rv-iron-400) 0 1px,
    var(--rv-iron-600) 1px 2px,
    #000 2px 3px);
}
.rv-bars::before { top: 0; }
.rv-bars::after { bottom: 0; }

/* ---------------------------------------------------------------------------
   THE HIT POINT BAR.

   Chan, 9 Oct: he wants the bar back, housed like the long plates in the
   reference sheet. A number in a stat plate tells you the value; a bar tells
   you the SHAPE of the value, which is the thing you read mid-fight without
   doing arithmetic. So both: the bar carries the proportion and the number
   rides on top of it.

   Red is deliberate and it is the one place ember earns its keep. Everything
   else on the panel is soot and iron, so the only saturated thing on screen is
   the one that matters when it starts getting short.

   Markup:
     <div class="rv-hp" style="--v:85%">
       <i></i><span>44 <em>/ 52</em></span>
     </div>
   --------------------------------------------------------------------------- */
.rv-hp {
  position: relative;
  height: 30px;
  overflow: hidden;
  background:
    repeating-linear-gradient(90deg, rgba(0,0,0,.45) 0 1px, transparent 1px 20%),
    linear-gradient(180deg, #000, #121214);
  border: 3px solid;
  border-color: var(--rv-iron-500) var(--rv-iron-700) var(--rv-iron-700) var(--rv-iron-500);
  box-shadow: inset 0 0 0 1px #000, 0 0 0 1px #000;
}
/* the fill. Width is the only thing a script has to touch: set --v. */
.rv-hp > i {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: var(--v, 100%);
  background: linear-gradient(180deg, var(--rv-ember-400), var(--rv-ember-500) 60%, #541e16);
  box-shadow:
    inset 0 1px 0 rgba(255, 190, 170, .32),
    inset 0 -6px 10px -8px #000;
  transition: width .25s ease-out;
}
/* the ticks sit ON the fill as well as the groove, so the bar still reads as
   divided when it is nearly full */
.rv-hp > i::after {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(90deg, rgba(0,0,0,.16) 0 1px, transparent 1px 20%);
}
.rv-hp > span {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Cinzel', Georgia, serif;
  font-size: .9rem;
  letter-spacing: .04em;
  color: var(--rv-text);
  text-shadow: 0 1px 3px #000, 0 0 10px #000;
}
/* the total has to stay legible where it crosses the fill, so it is a dimmed
   WHITE rather than the panel's grey, which disappears against red */
.rv-hp > span em { font-style: normal; color: rgba(255, 252, 245, .62); margin-left: .25em; }
/* running out: the fill stops being merely red and starts being bright */
.rv-hp.low > i { background: linear-gradient(180deg, #d9603f, #a3301d 60%, #5e170f); }

/* ---------------------------------------------------------------------------
   ITEM PLATES, the backing for a row in a scrolling list.

   Chan, 9 Oct: the abilities / skills / actions lists need "some sort of
   backing graphic that houses each item". Without one the list is text on a
   panel and every row runs into the next; with one each row is an object you
   can point at, which is what you want when you are reading one off at a
   table.

   Drawn rather than painted, for the same reason the bars are: at 40px tall
   and repeated twenty times down a list, a painted plate is twenty copies of
   the same noise, and it costs a texture fetch per row.
   --------------------------------------------------------------------------- */
.rv-item {
  position: relative;
  display: block;
  width: 100%;
  text-align: left;
  padding: 11px 14px;
  background: linear-gradient(180deg, var(--rv-stone-600), var(--rv-stone-800));
  border: 1px solid #000;
  box-shadow:
    inset 0 1px 0 rgba(230, 225, 215, .07),
    inset 0 -10px 16px -12px #000;
  /* the chamfer is what stops twenty identical rectangles reading as a table */
  clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
  cursor: pointer;
  transition: background .16s, box-shadow .16s;
}
.rv-item + .rv-item { margin-top: 5px; }
.rv-item:hover {
  background: linear-gradient(180deg, var(--rv-stone-500), var(--rv-stone-700));
}
.rv-item.on {
  background: linear-gradient(180deg, #41372a, #19150e);
  box-shadow:
    inset 0 1px 0 rgba(201, 164, 95, .3),
    inset 0 0 18px rgba(168, 133, 74, .22);
}
/* the brass tab down the left edge marks the selected row without moving it */
.rv-item.on::before {
  content: '';
  position: absolute;
  left: 0;
  top: 9px;
  bottom: 0;
  width: 3px;
  background: linear-gradient(180deg, var(--rv-brass-400), var(--rv-brass-500));
}
.rv-item .t {
  font-family: 'Cinzel', Georgia, serif;
  font-size: .98rem;
  color: var(--rv-text);
  margin: 0;
}
.rv-item.on .t { color: var(--rv-brass-400); }
.rv-item .s {
  font-family: 'Inter', sans-serif;
  font-size: .72rem;
  letter-spacing: .04em;
  color: var(--rv-text-faint);
  margin: 2px 0 0;
}
/* a value parked at the right end of the row: a cost, a range, a die */
.rv-item .v {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  font-family: 'Cinzel', Georgia, serif;
  font-size: .95rem;
  color: var(--rv-text-dim);
}
.rv-item.on .v { color: var(--rv-brass-400); }

/* a bar on its own, with no block attached */
.rv-bar {
  height: 3px;
  border: 0;
  margin: clamp(12px, 1.5vw, 18px) 0;
  background: linear-gradient(180deg,
    var(--rv-iron-400) 0 1px,
    var(--rv-iron-600) 1px 2px,
    #000 2px 3px);
}

/* the inner bevel: a lip of light at the top, shadow at the foot, so the panel
   reads as cut INTO the wall rather than floating on it */
.rv-frame::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  box-shadow:
    inset 0 2px 0 rgba(230, 225, 215, .09),
    inset 0 -18px 30px -20px rgba(0, 0, 0, .96);
}
.rv-frame > * { position: relative; z-index: 1; }

/* ---------------------------------------------------------------------------
   SPIKES. Iron teeth along the top edge.

   Sits in ::after at top:-14px, so it OVERHANGS the frame and takes no layout
   space at all. This is what lets the border be aggressive without pushing the
   sheet's own content around.
   --------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   THE CLOSING BAR at the foot of a panel.

   The same three stops as .rv-bars: light, body, black. It runs the full width
   INCLUDING under the chains, so the chains land on something instead of
   fraying out at the bottom of the panel.
   --------------------------------------------------------------------------- */
.rv-frame::after {
  content: '';
  position: absolute;
  left: -26px;
  right: -26px;
  bottom: -3px;
  height: 3px;
  z-index: 2;
  background: linear-gradient(180deg,
    var(--rv-iron-400) 0 1px,
    var(--rv-iron-600) 1px 2px,
    #000 2px 3px);
}
/* a panel that should close with nothing at all */
.rv-frame.rv-plain::after { display: none; }

/* ---------------------------------------------------------------------------
   CHAINS.

   Chan, 9 Oct: the side runs of the frame should be chains, and the little
   dangling ones come off. They were stubs hung from the top corners, and once
   the frame's own left and right edges ARE chain they read as a second, worse
   chain next to the real one. The chain now lives in the frame texture itself,
   composited into the left and right edge runs, so it repeats down the full
   height of any panel for free.
   --------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   A heavier divider: an iron band with a stud at each end.
   --------------------------------------------------------------------------- */
.rv-rule {
  height: 6px;
  border: 0;
  margin: var(--rv-gap) 0;
  background:
    linear-gradient(90deg, transparent, var(--rv-iron-600) 6%, var(--rv-iron-600) 94%, transparent),
    linear-gradient(180deg, rgba(129,129,138,.5), transparent 60%);
  position: relative;
}
.rv-rule::before, .rv-rule::after {
  content: '';
  position: absolute;
  top: -2px;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, var(--rv-iron-400), var(--rv-iron-700));
}
.rv-rule::before { left: 2%; }
.rv-rule::after  { right: 2%; }

/* ===========================================================================
   HEADINGS
   =========================================================================== */

.rv-eyebrow {
  font-family: 'Inter', sans-serif;
  font-size: .62rem;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--rv-accent);
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}
.rv-eyebrow::after {
  content: '';
  flex: 1;
  height: 2px;
  background: linear-gradient(90deg, var(--rv-rule-strong), transparent);
}

.rv-title {
  font-family: 'Cinzel', serif;
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--rv-text);
  margin: 0 0 6px;
  font-size: clamp(1.1rem, 1.1rem + .6vw, 1.7rem);
  text-shadow: 0 2px 14px rgba(8, 8, 9, .85);
}

/* ===========================================================================
   CONTROLS. Iron plates, not pills.
   =========================================================================== */

.rv-btn {
  font-family: 'Inter', sans-serif;
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--rv-text);
  background: linear-gradient(180deg, var(--rv-iron-600), var(--rv-stone-800));
  border: 2px solid var(--rv-iron-700);
  border-top-color: var(--rv-iron-500);
  padding: 9px 16px;
  cursor: pointer;
  position: relative;
  /* chamfered corners: struck metal, not a rounded rect */
  clip-path: polygon(6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%, 0 6px);
  transition: color .18s, box-shadow .18s, background .18s;
}
.rv-btn:hover {
  color: var(--rv-pale-200);
  background: linear-gradient(180deg, #3c3c42, var(--rv-stone-800));
  box-shadow: 0 0 0 1px var(--rv-pale-glow);
}
.rv-btn:active { transform: translateY(1px); }
.rv-btn[aria-pressed="true"], .rv-btn.on {
  /* the selected state is lit by tarnished brass, not by ice: it is the one
     warm thing on the panel, which is what makes it findable */
  color: var(--rv-brass-400);
  background: linear-gradient(180deg, #41372a, #14110c);
  box-shadow: inset 0 0 14px rgba(168, 133, 74, .3);
}

/* ---------------------------------------------------------------------------
   TABS, encased.

   Chan, 9 Oct, with a drawing: each tab arched over its own label and the
   whole row standing on a bar. The old version was a label with a coloured
   underline, which is a web convention, not a built object: nothing encloses
   the word, so the strip reads as six pieces of text that happen to be in a
   row.

   So each tab is now a shape. Arched top, metal on three sides, open at the
   foot, standing on a rail that runs the full width behind them. The selected
   one lifts a pixel, lights in brass, and BREAKS the rail, which is the whole
   trick: the gap is what says this tab owns the panel underneath it.
   --------------------------------------------------------------------------- */
.rv-tabs {
  position: relative;
  display: flex;
  gap: 3px;
  overflow-x: auto;
  padding: 3px 0 0;
  /* the rail. Behind the tabs, so a selected tab can cover it. */
  background: linear-gradient(180deg,
    transparent calc(100% - 3px),
    var(--rv-iron-600) calc(100% - 3px) calc(100% - 1px),
    #000 calc(100% - 1px));
}
/* a scrolling strip should not show a scrollbar sitting under the tabs */
.rv-tabs::-webkit-scrollbar { height: 0; }

.rv-tab {
  position: relative;
  flex: 0 0 auto;
  font-family: 'Inter', sans-serif;
  font-size: .68rem;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--rv-text-dim);
  background: linear-gradient(180deg, var(--rv-stone-600), var(--rv-stone-800));
  border: 2px solid var(--rv-iron-700);
  border-bottom: 0;
  /* square, not arched: once the plates and panels are boxy a rounded tab is
     the only soft thing on the page and it reads as a mistake */
  border-radius: 0;
  padding: 10px 15px 11px;
  cursor: pointer;
  white-space: nowrap;
  transition: color .18s, background .18s, border-color .18s;
}
.rv-tab:hover {
  color: var(--rv-text);
  background: linear-gradient(180deg, var(--rv-stone-500), var(--rv-stone-700));
}
.rv-tab.on {
  color: var(--rv-brass-400);
  background: linear-gradient(180deg, #41372a, #14110c);
  border-color: var(--rv-brass-500);
  /* one pixel taller than its neighbours, and over the rail rather than on it */
  margin-top: -3px;
  padding-bottom: 14px;
  z-index: 2;
}
/* the break in the rail: a sliver of panel colour under the selected tab only */
.rv-tab.on::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 3px;
  background: var(--rv-stone-900);
}

/* ===========================================================================
   STAT PLATE. Most of what a play sheet is: a number readable across a table,
   set in a stone block rather than another card.
   =========================================================================== */

.rv-plate {
  position: relative;
  text-align: center;
  padding: clamp(10px, 1.6vw, 16px) 10px;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(168, 75, 53, .07), transparent 70%),
    linear-gradient(180deg, var(--rv-stone-600), var(--rv-stone-900));
  border: 2px solid var(--rv-iron-700);
  border-top-color: var(--rv-iron-500);
  clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
}
.rv-plate .v {
  font-family: 'Cinzel', serif;
  font-weight: 700;
  font-size: clamp(1.5rem, 1.2rem + 1.6vw, 2.3rem);
  line-height: 1;
  color: var(--rv-text);
}
.rv-plate .l {
  font-family: 'Inter', sans-serif;
  font-size: .56rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--rv-text-faint);
  margin-top: 6px;
}

/* ===========================================================================
   SCALING

   Density drops, size does not. Fine filigree shrunk to phone size stops
   resolving but still costs contrast, which is what makes fantasy UI look
   muddy. The masonry, the iron and the hierarchy carry the identity, so those
   stay at every width; the teeth get shorter and the chains come off.
   =========================================================================== */

@media (max-width: 720px) {
  .rv-frame {
    border-width: 14px;
    border-bottom-width: 3px;
    border-image-width: 14px 14px 0;
    padding: clamp(14px, 3.4vw, 20px);
  }
  .rv-frame::after { left: -14px; right: -14px; }
  /* the inset band is 3px of drawn metal, so it does not step down */
  .rv-eyebrow { letter-spacing: .16em; }
  .rv-plate {
    clip-path: polygon(7px 0, 100% 0, 100% calc(100% - 7px), calc(100% - 7px) 100%, 0 100%, 0 7px);
  }
  .rv-rule::before, .rv-rule::after { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .rv-btn, .rv-tab { transition: none; }
}

/* ===========================================================================
   THE APP SHELL: what play, create and dm all share.

   These three pages were the reason the design system exists, and keeping the
   ground, the square rule and the footer here rather than copying them into
   each page's adapter is the difference between one edit and three.

   Each page then carries only its OWN class names in rollevil-ui-<page>.css.
   =========================================================================== */

/* the flat values everything is built from, so a change is one edit */
:root {
  --p-panel:   #0c0c0e;  /* the ground inside a panel */
  --p-face:    #141418;  /* anything raised off it: a control, a row, a card */
  --p-face-hi: #1d1d22;  /* the same face, hovered */
  --p-line:    #3f3f44;  /* the metal line */
  --p-seat:    #000;     /* what every line is seated on */
}

body {
  /* the splash lives on html so it can sit behind everything; body stays clear */
  background: transparent;
  color: var(--rv-text);
  /* taller, 10 Oct: at 84-168px the candles and the skull were reading as
     ornament rather than as objects on a shelf. The art is 320px tall, so this
     is still a downscale, just a gentler one. */
  --rv-foot-h: clamp(118px, 17vw, 232px);
  padding-bottom: var(--rv-foot-h);
}
html {
  background: var(--rv-stone-900);
  /* the ledge height. Declared on html as well as body: harmless, and it is
     where anything fixed to the viewport can read it. */
  --rv-foot-h: clamp(118px, 17vw, 232px);
}

/* ---------------------------------------------------------------------------
   THE SPLASH, behind the whole page.

   Chan, 10 Oct: "splash art doesnt extend past here, doesnt look nice must be
   extended while the abilities skills etc has their own opaque backing".

   The banner only ever covered .sheet-head, so it stopped dead at the tab bar
   and everything below sat on flat black. It is now a FIXED layer behind the
   document, which also means it holds still while the sheet scrolls over it
   rather than sliding away.

   It is faint on purpose. This is the floor of the room, not a picture: the
   panel above it is opaque so nothing has to be read through her. --banner is
   set on <body> by the sheet's own render, next to where it sets it on the
   header. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: var(--banner, none);
  background-size: cover;
  background-position: center var(--banner-y, 35%);
  opacity: .22;
}

/* ===========================================================================
   BOXY THROUGHOUT.

   The page carries 60+ radius declarations across dozens of selectors, so this
   squares them at the root rather than chasing each one. !important is the
   right tool: these have to beat page rules that are more specific, and being
   one obvious block makes it one obvious thing to delete with the overlay.

   Two exceptions, both because a circle is the MEANING and not decoration: the
   dice button and the proficiency dots. A square proficiency dot reads as a
   checkbox, which is a different promise. The senses counter keeps its circle
   for the same reason, and it gives the square rows something to contrast with.
   =========================================================================== */
body button, body input, body select, body textarea,
body div, body span, body section, body article, body li, body a,
body table, body td, body th, body img, body hr, body label, body p {
  border-radius: 0 !important;
}
.dice-fab, .prof-dot, .sense-pill .sv { border-radius: 50% !important; }

/* ===========================================================================
   THE FOOTER LEDGE.

   Chan, 9 Oct: a footer that scrolls with the page, two candles on the left,
   one on the right, the stone between them stretchable to any device.

   So it is three generated pieces and a repeat, which is the same three-slice
   idea as the frame but on one axis:

     ui_footer_left   521x320  two candles, pilaster, ledge runs off the right
     ui_footer_mid    480x320  plain ledge, tiles left to right (seam 5.1)
     ui_footer_right  482x320  one candle, pilaster, ledge runs off the left

   The ends are pinned to the corners and the middle repeats behind them to
   fill whatever is left, so it fits a phone and an ultrawide from one set.

   The one thing that needed fixing before any of it worked: the generator gave
   each piece a different ledge height (left 40.0% of frame, mid 43.5%, right
   43.3%), which would have stepped at both joins. Each was trimmed from the
   TOP until all three present the same band-to-height ratio, so bottom-aligned
   at a common height their ledges land on one line. Trimming the top rather
   than the bottom is what keeps the candles whole.

   It is on body::after, so it sits after everything and scrolls with the page
   rather than being pinned to the viewport. pointer-events are off: it is
   scenery and must never eat a tap meant for the sheet.
   =========================================================================== */
body::after {
  content: '';
  /* ANCHORED, not in the flow.

     It was a block at the end of the document, which is what "scrolls with the
     page" asked for, but in use that means the candles are only ever visible
     at the very bottom of a long sheet. Chan, 10 Oct: they "do not stay on
     screen and anchored as we scroll down". Fixed to the viewport foot, with
     body padding below the content so nothing can hide behind it. */
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  height: var(--rv-foot-h);
  pointer-events: none;
  background-image:
    image-set(url("images/ui/ui_footer_left.webp?v=20261014b") type("image/webp"),
              url("images/ui/ui_footer_left.png?v=20261014b") type("image/png")),
    image-set(url("images/ui/ui_footer_right.webp?v=20261014b") type("image/webp"),
              url("images/ui/ui_footer_right.png?v=20261014b") type("image/png")),
    image-set(url("images/ui/ui_footer_mid.webp?v=20261014b") type("image/webp"),
              url("images/ui/ui_footer_mid.png?v=20261014b") type("image/png"));
  background-position: left bottom, right bottom, left bottom;
  background-size: auto 100%, auto 100%, auto 100%;
  background-repeat: no-repeat, no-repeat, repeat-x;
}
/* the dice tray covers the bottom of the screen when it is open, and the
   footer behind it is just weight to paint */
body.tray-open::after { display: none; }

/* ===========================================================================
   THE CROWN: a forged rod with a stud at its left end, worn on a box's top edge.

   Two generated pieces:
     ui_bar_rod      512x23  rod with a finished cap each end; its middle was
                             measured uniform before publishing, so it stretches
     ui_stud_diamond  64x64  the stud, hand-finished by Chan

   Chan, 10 Oct: "i would like it to indicate any dropdown menu".

   WHERE IT IS NOW, honestly: the ability boxes and the hit point block, where
   it started life as ornament and he liked it; and the hag cards, which open.
   So it does not yet mean one single thing, and that is worth knowing before
   adding the next one.

   GOING FORWARD it marks something that OPENS. If that is the rule we want it
   to carry, the ability boxes and hit points are the two that would have to
   give it up, because a sign that appears on inert things teaches the reader it
   means nothing. That is Chan's call, not mine to make quietly.

   ONE pseudo-element, two background layers, not two pseudos. The hag cards
   already spend ::before on their art, and a component that needs both is a
   component that cannot be worn by half the things that want it.

   The rod starts 6px in so its own end cap hides inside the stud's body and the
   stud becomes the cap: a diamond only covers its full width along one
   horizontal line, so a cap at x=0 showed above and below it.

   --crown-top moves the whole thing. The default overhangs the edge by half the
   stud; a box that CLIPS its overflow (the hag cards clip their art) sets it to
   0 and wears the crown inside the edge instead.
   =========================================================================== */
.rv-crown,
.ab-card,
.hp-vital,
.cards.rows .card {
  position: relative;
}
.rv-crown::after,
.ab-card::after,
.hp-vital::after,
.cards.rows .card::after {
  content: '';
  position: absolute;
  left: -2px;
  right: 0;
  top: var(--crown-top, -8px);
  height: 17px;
  z-index: 4;
  pointer-events: none;
  background-image:
    image-set(url("images/ui/ui_stud_diamond.webp?v=20261014b") type("image/webp"),
              url("images/ui/ui_stud_diamond.png?v=20261014b") type("image/png")),
    image-set(url("images/ui/ui_bar_rod.webp?v=20261014b") type("image/webp"),
              url("images/ui/ui_bar_rod.png?v=20261014b") type("image/png"));
  background-position: left top, 8px 3px;
  background-size: 17px 17px, calc(100% - 8px) 11px;
  background-repeat: no-repeat, no-repeat;
}

/* ===========================================================================
   SCROLLBARS: a brass nub on a grey rail.

   Chan, 10 Oct. The default bar is the last piece of stock browser furniture
   on the page, and on a dark sheet it reads as a bright grey slab.

   Both syntaxes are here on purpose. scrollbar-color is the standard one and
   is what Firefox honours; the ::-webkit- rules are what Chrome and Safari
   honour, and they allow the border trick that insets the nub from the rail.
   A thumb with a transparent border and background-clip: padding-box is how
   you get a nub NARROWER than the track without a wrapper element.
   =========================================================================== */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--rv-brass-500) var(--p-face);
}
::-webkit-scrollbar { width: 13px; height: 13px; }
::-webkit-scrollbar-track {
  background: var(--p-face);
  box-shadow: inset 1px 0 0 var(--p-seat), inset -1px 0 0 var(--p-seat);
}
::-webkit-scrollbar-thumb {
  background: var(--rv-brass-500);
  border: 3px solid transparent;
  background-clip: padding-box;
  border-radius: 0;
}
::-webkit-scrollbar-thumb:hover { background: var(--rv-brass-400); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: var(--p-face); }

/* The candle glow was removed on 14 Oct. Chan: "the glow is terrible. Just
   remove it." Two attempts, a full-width wash and then six circles measured
   onto the flames, and neither earned its place against static art that already
   reads as candlelight. The ledge stands on its own.

   If it is ever wanted again, the thing that made both versions hard was that
   the light has to track art it cannot see: every position had to be derived
   from --rv-foot-h and re-measured whenever the ledge art changed. An animated
   WebP, where the flicker lives in the art itself, is the honest way to do it. */
