/* ====================================================================
   RANKS AND BADGES: the jar, the ring, the ladder and the collection
   ====================================================================

   Everything this feature LOOKS like is here and nothing about what it is.
   The eight rungs and the forty badges are a shared contract
   (shared/src/ranks.ts, shared/src/badges.ts), the marks are a catalogue of
   lucide imports (admin/src/ranks/badge-art.ts), and this file is the only
   one of the three allowed to name a colour, a size or a duration. Same
   three-way split console-face.css already keeps for the sweets, for the
   same reason: a contract that carried a colour would need a deploy to
   restyle, and a stylesheet that carried an id would drift from the list.

   IT NEVER ANIMATES ON ITS OWN. Not on boot, not on a repaint, not on a
   hover. Two things here move, both exactly once and neither in a loop: the
   jar's fill rising on a rank-up, and the ring closing on the same event.
   Rule 14 is unambiguous about what a thing that moves when nobody asked it
   to reads as, and this is a toy nobody agreed to be measured by: it has no
   business demanding attention.

   THE ACCENT IS A FILL HERE AND NEVER A WORD. Every use of --cbc-accent in
   this file is a bar, a ring, a disc or a mark, which WCAG treats as a
   graphical object at 3:1, and ink-ladder.test.ts already measures exactly
   that colour at exactly that floor on every ground in all three themes
   (it is what --cbc-focus is made of). Where the accent has to be READ it
   is --cbc-accent-text, which is a different step and is measured against
   the 4.5:1 text bar in the same file. Rule 19: a colour has two jobs and
   two bars, and no answer to either is portable between grounds.

   NO RAIL, ANYWHERE. Nothing in this file draws a coloured bar down the
   left edge of anything. The panel is a panel and the blocks are blocks.
   ==================================================================== */

/* ---- The jar ----

   ONE COMPONENT, THREE SIZES, ONE INPUT. The fill is a block behind a lucide
   `pill-bottle`, inset to the bottle's own interior, because an icon can draw
   a jar and cannot draw a LEVEL. Drawing our own bottle instead would put a
   hand-made mark beside 1,995 that share a stroke weight and a grid. */
.cb-jar {
  /* Nought to one, unitless, set by ranks/jar.ts. UNITLESS BECAUSE CALC
     MULTIPLIES BY A NUMBER AND NOT BY A PERCENTAGE: the fill's height is a
     share of the bottle's interior, so the level has to be a multiplier. */
  --cb-jar-level: 0;
  /* THE BOTTLE'S OWN INTERIOR, IN ITS OWN 24-UNIT BOX, AND IT BELONGS TO NO
     SCALE THIS TOKEN SET HAS. lucide draws pill-bottle's body from (6,7) to
     (18,22) with a 2-unit stroke centred on the path, so the glass encloses x
     from 7 to 17 and y from 8 to 21. As shares of 24 that is 29.17 per cent in
     from each side, 33.33 per cent down from the top, and 12.5 per cent up
     from the foot. These are not spacing steps and pushing them onto the
     spacing scale would be rule 13's exact trap, a token from the wrong family
     passing every guard: they are one drawing's geometry, so they are named
     here, next to the drawing, with the arithmetic written out. If the icon is
     ever swapped these three move together and nothing else does. */
  --cb-jar-side: 29.17%;
  --cb-jar-lid: 33.33%;
  --cb-jar-foot: 12.5%;
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--cb-space-6xl);
  block-size: var(--cb-space-6xl);
  /* The outline is ink rather than accent, so the fill is the only thing in
     the mark carrying the brand colour and the level is what the eye lands on.
     A jar drawn in accent with an accent fill is one shape in one colour. */
  color: var(--cbc-ink-muted);
}
.cb-jar[data-size="sm"] {
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
}
.cb-jar[data-size="lg"] {
  inline-size: var(--cb-space-9xl);
  block-size: var(--cb-space-9xl);
}

/* The outline, over the fill. `.cb-icon` sizes itself to a 20px box for a row
   of buttons, which is the right default everywhere except inside a thing
   whose whole job is being a jar. */
.cb-jar .cb-jar-glass {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
}

.cb-jar-fill {
  position: absolute;
  inset-inline: var(--cb-jar-side);
  inset-block-end: var(--cb-jar-foot);
  block-size: calc((100% - var(--cb-jar-lid) - var(--cb-jar-foot)) * var(--cb-jar-level));
  /* Square at the shoulders and rounded at the foot, because that is what
     sugar in a jar does: it takes the shape of the bottom and is flat on top. */
  border-radius: 0 0 var(--cb-radius-xs) var(--cb-radius-xs);
  background: var(--cbc-accent);
  transform-origin: bottom;
}

/* THE ONE MOVEMENT THE JAR IS ALLOWED, and it is scaleY rather than height.
   A keyframe cannot interpolate from `0` to a calc() of two percentages and a
   custom property; a transform can, it runs on the compositor, and it settles
   from the bottom, which is the direction sugar arrives from.

   `both` so the fill is at nought BEFORE the first frame paints. The attribute
   comes off at animationend (ranks/jar.ts), which matters more than it looks:
   a `both`-filled animation left in place keeps applying its last keyframe and
   beats every ordinary declaration for the properties it touches. That is what
   killed three features on the team cluster at once, all three still passing
   their own attribute assertions. */
@keyframes cb-jar-rise {
  from {
    transform: scaleY(0);
  }
  to {
    transform: scaleY(1);
  }
}
.cb-jar[data-rankup] .cb-jar-fill {
  animation: cb-jar-rise var(--cb-motion-slow) var(--cbc-spring) both;
}

/* ---- The ring round the face in the rail ----

   One channel added to a mark that already exists (docs/16 §6.1). A conic
   gradient rather than an SVG arc, so the same declaration holds at the rail's
   32px disc and the account menu's 40px one without a circumference being
   recomputed anywhere.

   REGISTERED, WHICH IS WHAT MAKES IT ANIMATABLE. A percentage inside a
   gradient is a string as far as the cascade is concerned unless the property
   it came from has a syntax; with one, the browser can interpolate it. Without
   this block everything below still works and the ring simply jumps, which is
   the correct failure. */
@property --cb-ring-fill {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

.cb-rank-ring {
  --cb-ring-fill: 0%;
  display: grid;
  place-items: center;
  flex: none;
  /* The ring's own thickness. Two pixels is a hairline by the guard's
     reckoning and the scale's smallest step by the token set's, which is the
     same number twice rather than a coincidence. */
  padding: var(--cb-space-xxs);
  border-radius: var(--cb-radius-full);
  /* Clockwise from twelve, which is where a dial starts and where the eye
     looks first. The hard stop is a second colour with no transition point, so
     there is no gradient smear between the two arcs at any fill. */
  background: conic-gradient(var(--cbc-accent) var(--cb-ring-fill), var(--cbc-line-strong) 0);
  /* ONLY THE RIM IS PAINTED, AND THE MIDDLE IS GENUINELY CUT OUT.
     Measured, not assumed: without this the gradient fills the whole circle
     and the disc in front of it does NOT cover it, because `.cb-avatar` is
     painted in --cbc-accent-wash, which is a colour-mix with `transparent` in
     it. A translucent disc over a pie chart is a pie chart, and that is
     exactly what the rail showed: a solid accent wedge across somebody's face.
     Covering it with an opaque ground of our own would be worse, because the
     button behind changes colour on hover and the patch would seam.
     `black` here is an alpha channel rather than a colour, which is why it is
     the one place in this file a raw keyword would have been tempting; it is
     the token anyway, so nothing has to be argued twice. */
  mask: radial-gradient(
    closest-side,
    transparent calc(100% - var(--cb-space-xxs)),
    var(--cb-color-black) 0
  );
}
/* At the top rung the ring is simply closed. Stated rather than left to a
   fill of 100 per cent, because `full` and "one point off the next rung" are
   different facts that happen to draw the same (ranks/jar.ts argues it), and
   a rounding change should never be able to open this ring by a degree. */
.cb-rank-ring[data-full] {
  background: var(--cbc-accent);
}
/* No transition at rest. Stillness is the resting behaviour here and movement
   is the exception somebody has to ask for, which is the stronger version of
   "under reduced motion the ring simply is where it is". */
.cb-rank-ring[data-rankup] {
  transition: --cb-ring-fill var(--cb-motion-slow) var(--cbc-spring);
}

/* ---- The block at the top of the account menu ----

   THE SLOT IS NOT A BOX, which is what `display: contents` is for. The account
   panel is a column of blocks and the jar is one of them, but the jar is null
   until the tally lands and the panel is built once a sign-in, so something has
   to hold its place. A slot that laid out would be an empty row with the
   panel's own gap on both sides of it, visible as a gap in a menu with nothing
   in it; `contents` means the block's own element is the child the column sees,
   and an empty slot is exactly as absent as no slot at all. */
.cb-rank-slot {
  display: contents;
}

/* ---- The block at the top of the account menu ---- */
.cb-rank-menu {
  display: grid;
  gap: var(--cb-space-lg);
  padding: var(--cb-space-lg) var(--cb-space-md);
  border-bottom: 1px solid var(--cbc-line);
}
.cb-rank-menu-top {
  display: flex;
  align-items: center;
  gap: var(--cb-space-lg);
  min-inline-size: 0;
}
.cb-rank-menu-words {
  display: grid;
  min-inline-size: 0;
}
.cb-rank-menu-name {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
}
/* Wraps rather than truncating. "Sweetmaker, about three weeks away." is the
   whole of what this block says, and an ellipsis through the middle of it
   would leave a menu boasting about a rank and cutting off the sentence that
   makes it mean anything. */
.cb-rank-menu-next {
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
}
.cb-rank-menu-recent {
  display: flex;
  gap: var(--cb-space-sm);
}
.cb-rank-mark-slot {
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-accent-wash);
  color: var(--cbc-accent-text);
}
.cb-rank-mark {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
}

/* ---- The panel on the profile ---- */
.cb-rank-top {
  display: flex;
  align-items: center;
  gap: var(--cb-space-3xl);
  padding: var(--cbc-set-y) var(--cb-space-3xl);
}
.cb-rank-said {
  min-inline-size: 0;
}
.cb-rank-name {
  font-size: var(--cb-type-xl-size);
  line-height: var(--cb-type-xl-line-height);
  letter-spacing: var(--cb-type-xl-letter-spacing);
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
}
.cb-rank-next {
  margin-top: var(--cb-space-xxs);
  max-width: var(--cb-layout-paragraph-max-width);
  font-size: var(--cb-type-sm-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
}
/* The caveat, in the tone the reports coverage note uses: a figure that hit a
   ceiling is a floor and says so, in ordinary ink rather than as a warning,
   because nothing has gone wrong. */
.cb-rank-note {
  margin-top: var(--cb-space-xs);
  max-width: var(--cb-layout-paragraph-max-width);
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-muted);
}
.cb-rank-rungs,
.cb-rank-collection {
  padding: var(--cbc-set-y) var(--cb-space-3xl);
  border-top: 1px solid var(--cbc-line);
}

/* ---- The ladder ---- */
.cb-ladder {
  display: grid;
  gap: var(--cb-space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
}
.cb-rung {
  display: flex;
  align-items: center;
  gap: var(--cb-space-lg);
  padding: var(--cb-space-xs) var(--cb-space-sm);
  border-radius: var(--cb-radius-md);
}
/* HIGHLIGHTING ONE THING HOLDS THE REST BACK rather than shouting the one in
   front (rule 14). The rung you are on gets a ground and full ink; the others
   are simply quieter, and nothing is dimmed with opacity, because opacity over
   an ink token is how text quietly slips under 4.5:1 with no guard watching. */
.cb-rung[data-where="here"] {
  background: var(--cbc-accent-quiet);
}
.cb-rung-mark {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
  color: var(--cbc-ink-faint);
}
.cb-rung[data-where="passed"] .cb-rung-mark {
  color: var(--cbc-accent);
}
.cb-rung[data-where="here"] .cb-rung-mark {
  color: var(--cbc-accent);
}
.cb-rung-words {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-md);
  min-inline-size: 0;
}
.cb-rung-name {
  font-size: var(--cb-type-sm-size);
  color: var(--cbc-ink-muted);
}
.cb-rung[data-where="here"] .cb-rung-name {
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
}
.cb-rung-when {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}

/* ---- The collection ---- */
.cb-badges {
  display: grid;
  gap: var(--cb-space-3xl);
}
.cb-badges-head {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-md);
}
.cb-badges-head h3 {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
}
/* The count is a figure and reads as one: tabular, quiet, and never in the
   accent, because it is not a thing to press and not a thing to worry about. */
.cb-badges-count {
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
}
.cb-badge-block {
  display: grid;
  gap: var(--cb-space-lg);
}
.cb-badge-block-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--cb-space-md);
}
.cb-badge-block-head h4 {
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
  /* Small caps over a block, the same treatment the rail uses over a list, so
     a group heading reads as a label rather than as a smaller heading. The
     tracking comes off the TRACKING scale and not the spacing one: positive
     letter-spacing has its own family in the token set and borrowing a spacing
     step for it is rule 13's named trap. */
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-muted);
}
.cb-badge-block-head p {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-badge-block-head .cb-badges-count {
  margin-inline-start: auto;
}

/* auto-fill rather than a column count, so the same markup is five across on a
   monitor and two on a laptop with a panel open beside it. The minimum is a
   spacing step doing a width's job, which is the house pattern here (the
   dashboard, the reports and the dossier all size a card track the same way)
   and is still a borrow: the width scale starts at 320px and has nothing
   tile-sized in it. Named in the report rather than quietly repeated. */
.cb-badge-set {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--cb-space-11xl), 1fr));
  gap: var(--cb-space-md);
  margin: 0;
  padding: 0;
  list-style: none;
}
/**
 * EVERY TILE THE SAME HEIGHT, which is the whole of what makes forty of them
 * read as a collection rather than as a list that has been jostled.
 *
 * A grid row is as tall as its tallest cell, so one badge whose name wraps
 * takes the whole row with it: before this, two rows in five stood three lines
 * high because of "Asked the assistant" and "Answered the questions", while
 * every other tile in them held one line and a lot of air. The floor is two
 * lines of name plus a date, which is the tallest any of the forty gets, so
 * nothing has to wrap for the grid to stay level and nothing is clipped when it
 * does.
 */
.cb-badge-slot {
  display: flex;
  align-items: center;
  gap: var(--cb-space-md);
  min-inline-size: 0;
  min-block-size: var(--cb-space-7xl);
  padding: var(--cb-space-md);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-xl);
}
/** The name and, under it, the day it was earned. */
.cb-badge-words {
  display: grid;
  gap: var(--cb-space-xxs);
  min-inline-size: 0;
}
/* An unearned slot is an OUTLINE and not a dimmed badge. Dimming would be an
   opacity over ink, which is the one thing ink-ladder.test.ts exists to catch;
   a lighter ink step is the same intent measured. */
.cb-badge-slot[data-state="waiting"],
.cb-badge-slot[data-state="hidden"] {
  background: none;
  border-style: dashed;
}
.cb-badge-slot[data-state="earned"] {
  background: var(--cbc-hover);
}
.cb-badge-mark {
  inline-size: var(--cb-space-2xl);
  block-size: var(--cb-space-2xl);
  color: var(--cbc-ink-faint);
}
/* RARITY IS DRAWN AND NOTHING ELSE. It sets what a badge is worth and how it
   is drawn, and it is not a judgement of the person holding it (badges.ts says
   so), so the three steps are quiet: a mark, a mark on a wash, a mark on the
   accent itself. Only where the badge has actually been earned, because the
   rarity of a thing you have not got is not information you can use. */
.cb-badge-slot[data-state="earned"] .cb-badge-mark {
  color: var(--cbc-accent);
}
.cb-badge-slot[data-state="earned"][data-rarity="rare"] .cb-badge-mark,
.cb-badge-slot[data-state="earned"][data-rarity="singular"] .cb-badge-mark {
  flex: none;
  box-sizing: content-box;
  padding: var(--cb-space-sm);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-accent-wash);
}
.cb-badge-slot[data-state="earned"][data-rarity="singular"] .cb-badge-mark {
  background: var(--cbc-accent);
  color: var(--cbc-on-accent);
}
.cb-badge-name {
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
  min-inline-size: 0;
}
.cb-badge-slot[data-state="earned"] .cb-badge-name {
  font-weight: var(--cb-font-weight-medium);
  color: var(--cbc-ink);
}
.cb-badge-when {
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--cbc-ink-faint);
}

/* ---- Paper (docs/16 §12) ----

   THE JAR IS AN OUTLINE AND THE COLLECTION IS A LIST. A page of solid accent
   discs is a page nobody's printer thanks them for, and forty tiles in a grid
   is most of a sheet spent on the thirty-something somebody has not earned.

   The rail is already hidden on paper (console-theme.css), so the ring never
   prints and needs no rule here.

   These restyle classes THIS FILE owns, which is what makes a bare selector
   honest: the note in the theme's print block is about a second file
   redefining a component somebody else declared, and there is no second file
   here. */
@media print {
  /* The level still prints, as a line rather than as a block of colour. Losing
     it entirely would be the easier rule and it would print a jar that says
     nothing, on the one surface where the words beside it are all somebody
     gets. */
  .cb-jar-fill {
    background: none;
    border: 1px solid var(--cbc-ink-faint);
  }
  .cb-badge-set {
    display: block;
  }
  /* Only what was actually earned. An unearned slot on paper is an
     advertisement for something nobody can act on from a printout. */
  .cb-badge-slot[data-state="waiting"],
  .cb-badge-slot[data-state="hidden"] {
    display: none;
  }
  .cb-badge-slot {
    border: none;
    padding: var(--cb-space-xxs) 0;
    /* The floor above exists to keep a GRID level, and on paper this is a
       list: a two-line minimum on every row would spend half a sheet on air. */
    min-block-size: 0;
  }
  .cb-badge-slot[data-state="earned"] {
    background: none;
  }
  /* Weakest selector first, so the two rules below never have to overturn a
     more specific one that came after them. Reading down a stylesheet and
     finding specificity descending is how a rule ends up dead with nothing
     saying so. */
  .cb-badge-slot[data-state="earned"] .cb-badge-mark {
    color: var(--cbc-ink);
  }
  .cb-badge-slot[data-state="earned"][data-rarity="rare"] .cb-badge-mark,
  .cb-badge-slot[data-state="earned"][data-rarity="singular"] .cb-badge-mark {
    padding: 0;
    background: none;
    color: var(--cbc-ink);
  }
  .cb-rung[data-where="here"] {
    background: none;
    /* The one rung that matters has to survive losing its ground, and on paper
       weight is the channel that costs nothing. */
    font-weight: var(--cb-font-weight-semibold);
  }
}

/* ---- Stillness ----

   BOTH SWITCHES, ALWAYS. The machine's and the console's own, because
   somebody who has turned motion off in this app has said something about this
   app and the operating system knows nothing about it. The scripts already
   refuse to start either sequence under either switch (ranks/jar.ts,
   ranks/ring.ts); this is the belt-and-braces half, for a preference changed
   while an animation is in the air. */
@media (prefers-reduced-motion: reduce) {
  .cb-jar[data-rankup] .cb-jar-fill,
  .cb-rank-ring[data-rankup] {
    animation: none;
    transition: none;
    transform: none;
  }
}
:root[data-motion="reduced"] .cb-jar[data-rankup] .cb-jar-fill,
:root[data-motion="reduced"] .cb-rank-ring[data-rankup] {
  animation: none;
  transition: none;
  transform: none;
}
