/* ===========================================================================
   spell.css - the front page: building a world spell out of hexagons.

   The shape language is not invented here. A shard is the same lit hexagon
   every button on this site already is (hex.css), and it wears one of the
   site's own colours: --hx-edge is written out in full, per category, because
   a custom property declared once on :root has its var()s filled in right
   there and cannot be reached back into further down the tree. That is the
   same trick app.js pulls for a world's genre colour.

   What this file adds on top:
     - the sigil: rings, runes, a glass face under the reading, and the slots
       drawn around it,
     - one glow behaviour per category, so a mood shard feels soft, an
       atmosphere shard drift a little, a genre shard cut sharper - the
       category must be readable without relying on colour alone,
     - the tray of shelves, and the two halves of the cast button.

   Every animation here is decoration. The page is fully usable with the
   motion turned off (see the last block).
   =========================================================================== */

:root {
  /* The sigil keeps a floor of its own: on a short window it stops shrinking
     rather than squeezing the names inside the shards down to nothing. */
  --sig: clamp(320px, 50vh, 470px);

  /* sqrt(3)/2 - the height of a regular hexagon against its own width. Every
     hexagon on this page is cut with it: a quarter of the width in from either
     side, and the slanted sides meeting at half the height, which is the only
     way all six sides come out the same length in a box whose shape the cut
     does not know. hex.css writes the same number out for the one button it
     cuts that way by hand (see the note by #ctl-sig there). */
  --hx-ratio: .8660254;

  /* How wide one character of a shard's name is, against its own font size.
     Measured on the real thing - the names are uppercase, 600 weight, with a
     little letter-spacing - and deliberately a shade generous, so the letters
     land inside the hexagon with room to spare rather than exactly touching
     its slanted sides. It is what lets a long name stay whole: see the font
     rule on .slot .nm, which reads it together with how long the name is. */
  --cw: .72;
  /* 1 by default, 0 for anyone who asked the system for less movement: the
     hold's own little swell is multiplied by this, so it simply does not
     happen while the light still comes up. */
  --motion: 1;

  /* the two things spell.js answers for every size of spell: how wide the
     middle of the sigil is, and how much of the decorative ring stays lit */
  --core: 54%;
  --ringfade: 1;

  --spell-ink: #f2f0ff;

  /* one colour per category, in the site's own voice: a bright end, the main
     hue, and a deep end, plus the lit edge written out in full */
  --c-purpose: #efc561;  --c-activity: #61efa4;  --c-setting: #61a4ef;
  --c-mood: #c46bff;     --c-atmosphere: #61efd8; --c-audience: #ef6193;
  --c-features: #9fb0ef; --c-structure: #ef9361;  --c-progression: #b4ef61;
  --c-session: #8fa3b8;  --c-style: #9f8cef;
}

/* ---------------------------------------------------------------- the page */

.spell {
  padding: calc(var(--bar-h) + 16px) 18px 30px;
  /* Wide enough that the two racks have room for a word: a pill is a word row,
     and a word row needs about a hundred and thirty pixels before a long name
     starts running out of it. The circle keeps its own size and its own middle
     whatever this is - only the room around it changes. */
  max-width: 1440px; margin: 0 auto;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  /* the sealed cards hang in the page's own top corner, so the page has to be
     the box they are measured against (see the sealed circles block below) */
  position: relative;
  /* How tall the purpose grid stands under the circle: its head, and four rows
     of the same twenty seven pixel pill a column holds. The stage needs the
     number, because there the two rows of the stage are counted in fixed
     heights rather than in content (see the racks-and-purpose block below). */
  --band-h: 147px;
  --cast-h: 156px;
  --cast-lab-h: 32px;
}

.spell-head { text-align: center; max-width: 640px; }
.spell-head h1 {
  margin: 0 0 5px; font-size: 24px; font-weight: 700; letter-spacing: .01em;
  color: #fff;
}
.spell-sub { margin: 0; color: var(--ink-soft); font-size: 13.5px; }

/* Pick -> Drag -> Connect -> Cast. Four small steps, and the first one is lit
   because that is the one you are on. */
.steps {
  list-style: none; margin: 11px 0 0; padding: 0;
  display: flex; justify-content: center; align-items: center; gap: 4px;
  font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-faint);
}
.steps li { display: flex; align-items: center; gap: 4px; }
.steps li + li::before {
  content: "\203A"; margin: 0 6px; color: var(--hair);
  font-size: 13px; letter-spacing: 0;
}
.steps li:first-child { color: var(--hx-star); }
body.has-spell .steps li:first-child { color: var(--ink-faint); }
body.has-spell .steps li:nth-child(2),
body.has-spell .steps li:nth-child(3),
body.has-spell .steps li:nth-child(4) { color: var(--ink-soft); }

/* --------------------------------------------------------------- the stage */

.stage {
  position: relative; width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}

/* the sigil is a square, but the ring inside it only reaches about a third of
   the way out - so the line under it is pulled back up over the empty half
   instead of standing a hand's width below the last shard. */
.stage-hint {
  margin: calc(var(--sig) * -.05) 0 0; text-align: center; max-width: 620px;
  font-size: 11.5px; color: var(--ink-faint);
}

/* the light the sigil stands in. Deliberately a plain radial gradient in a
   square that is a little wider than the sigil - no blur filter, so a slow
   machine pays nothing for it. */
.halo {
  position: absolute; left: 50%; top: 50%;
  width: calc(var(--sig) * 1.75); height: calc(var(--sig) * 1.75);
  margin: calc(var(--sig) * -.875) 0 0 calc(var(--sig) * -.875);
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 50%,
      rgba(196, 107, 255, .16), rgba(122, 52, 255, .08) 42%,
      rgba(4, 4, 12, 0) 68%);
  pointer-events: none; transition: opacity .5s ease; opacity: .75;
}
.sigil[data-n="0"] ~ .stage-hint { opacity: .9; }
.sigil[data-n="0"] { opacity: .72; }

/* the sigil: a square, and every piece inside it is placed in % of that
   square - so the whole thing scales as one thing, at any window size */
.sigil {
  position: relative; width: var(--sig); height: var(--sig);
  transition: opacity .5s ease;
}

/* the rings - three thin circles, the outer two turning very slowly in
   opposite directions. They are what makes an empty sigil look dormant
   instead of broken. */
.rings {
  position: absolute; inset: 0; pointer-events: none;
  opacity: var(--ringfade, 1); transition: opacity .6s ease;
}
.rings .r {
  position: absolute; border-radius: 50%; left: 50%; top: 50%;
  border: 1px solid rgba(169, 156, 255, .18);
  transform: translate(-50%, -50%);
}
.rings .r1 { width: 62%; height: 62%; border-style: dashed;
             border-color: rgba(196, 107, 255, .30);
             animation: turn 96s linear infinite; }
.rings .r2 { width: 78%; height: 78%;
             border-color: rgba(143, 123, 255, .16);
             animation: turn 150s linear infinite reverse; }
.rings .r3 { width: 94%; height: 94%;
             border-style: dotted; border-color: rgba(169, 156, 255, .12); }
.sigil[data-n="3"] .r2, .sigil[data-n="4"] .r2, .sigil[data-n="5"] .r2,
.sigil[data-n="6"] .r2 { border-color: rgba(143, 123, 255, .34); }
.sigil[data-n="3"] .r1, .sigil[data-n="4"] .r1, .sigil[data-n="5"] .r1,
.sigil[data-n="6"] .r1 { border-color: rgba(255, 184, 244, .42); }

@keyframes turn { to { transform: translate(-50%, -50%) rotate(360deg); } }

/* twelve small marks around the rim, spelling nothing */
.runes { position: absolute; inset: 0; pointer-events: none; }
.runes i {
  position: absolute; left: 50%; top: 50%;
  font-size: 11px; color: rgba(216, 156, 255, .40);
  transform: translate(-50%, -50%) translate(var(--rx), var(--ry));
  transition: color .5s ease;
}
.sigil[data-n="4"] .runes i,
.sigil[data-n="5"] .runes i,
.sigil[data-n="6"] .runes i { color: rgba(255, 224, 252, .72); }

/* the glass the reading stands on: a wide, flat hexagon, cut like every other
   shape on the site. The words do NOT live inside it - a clip-path cuts its
   children too, and a phrase like "rainy night forest retreat" would lose its
   ends. So the glass is its own layer, and the reading rides over it. */
.core-glass {
  position: absolute; left: 50%; top: 50%;
  width: var(--core, 54%); height: calc(var(--core, 54%) * var(--hx-ratio));
  transform: translate(-50%, -50%);
  clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%);
  background:
    radial-gradient(circle at 50% 50%, rgba(56, 30, 94, .92), rgba(13, 10, 30, .88));
  box-shadow: 0 0 0 1px rgba(169, 156, 255, .22);
  transition: box-shadow .5s ease, background .5s ease,
              width .5s ease, height .5s ease;
}
.sigil[data-n="0"] .core-glass { box-shadow: 0 0 0 1px rgba(169, 156, 255, .14); }
.sigil[data-n="3"] .core-glass, .sigil[data-n="4"] .core-glass,
.sigil[data-n="5"] .core-glass, .sigil[data-n="6"] .core-glass {
  box-shadow: 0 0 0 1px rgba(255, 184, 244, .40), 0 0 34px rgba(196, 107, 255, .30);
}

.core {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: var(--core-t, 46%); text-align: center; pointer-events: none;
  display: flex; flex-direction: column; gap: 3px; align-items: center;
  transition: width .5s ease;
}
.core-k {
  font-size: 9px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--ink-faint);
}
.core-say {
  font-size: 15px; line-height: 1.25; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: #fff;
  text-shadow: 0 0 14px rgba(196, 107, 255, .55);
  transition: color .3s ease;
}
.sigil[data-n="0"] .core-say { font-size: 13px; color: var(--ink-soft); text-shadow: none; }
.core-sub {
  font-size: 10px; line-height: 1.35; color: var(--ink-soft);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}

/* With a single shard bound that shard IS what stands in the middle of the
   sigil - so the reading steps aside for it and comes back the moment there
   is a second shard, and with it something to read between. */
.sigil[data-n="1"] .core, .sigil[data-n="1"] .core-glass { opacity: 0; }

/* the lines between bound shards. viewBox is 0 0 100 100 with
   preserveAspectRatio="none", so a coordinate IS a percentage of the sigil -
   and vector-effect keeps the stroke a hairline whatever that works out to. */
.web { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.web line {
  stroke: var(--link); stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  transition: opacity .3s ease;
  /* --o is this line's resting weight (handed out below), --lit is how awake
     the hold has it: together they take the line the rest of the way to full,
     so the spell brightens in the order it was built without any line having
     to know which kind of line it is. */
  stroke-opacity: calc(var(--o, 1) + var(--lit, 0) * (1 - var(--o, 1)));
}
/* The lines are read as a brightness ladder rather than as four kinds of line:
   the graph's own numbers for this vocabulary live between about 0.15 and 0.9,
   so the bottom rung has to be visible at 0.2 or the web of a spell would look
   empty when it is not. */
.web line.w1 { --o: .42; stroke-width: .9px; stroke-dasharray: .8 1.6; }
.web line.w2 { --o: .58; stroke-width: 1.1px; }
.web line.w3 { --o: .74; stroke-width: 1.5px; }
.web line.w4 { --o: .92; stroke-width: 2.1px;
               stroke: #ffb8f4;
               filter: drop-shadow(0 0 4px rgba(196, 107, 255, .75)); }
.web line.flow { animation: flow 5.5s linear infinite; }
@keyframes flow { to { stroke-dashoffset: -11.2; } }
.web line.bind { animation: bind .7s ease-out 1; }
@keyframes bind {
  0%   { stroke-opacity: 1; stroke-width: 3px; }
  100% { }
}

/* The shape the spell itself makes, under the meaning. Three parts:
     - the circle the shards stand on, drawn faint and broken so it reads as
       chalk rather than as a border,
     - the rim: one line from each shard to the next, closing that circle,
     - the star: one line from each shard to the one two along. At five shards
       that is a pentagram, at six the two triangles of a hexagram - and it is
       what turns a ring of hexagons into a figure.
   These three are always drawn, at every size; the brightness ladder above is
   the meaning, and it is drawn over the top of them. */
.web .ring-line {
  fill: none; stroke: rgba(169, 156, 255, .22);
  stroke-width: 1px; stroke-dasharray: 2 3;
  vector-effect: non-scaling-stroke;
}
.web .rim-line {
  stroke: rgba(196, 107, 255, .28); stroke-width: 1px;
  vector-effect: non-scaling-stroke;
}
.web .star-line {
  stroke: rgba(255, 184, 244, .32); stroke-width: 1.1px;
  stroke-dasharray: 1.6 2.4;
  vector-effect: non-scaling-stroke;
  animation: starflow 13s linear infinite;
}
@keyframes starflow { to { stroke-dashoffset: -12; } }

/* -------------------------------------------------------------- the shards */

/* a bound shard sits in a slot. The slot is the thing that is positioned; the
   shard inside it is the hexagon. The !important is not decoration: hex.css
   cuts every button on the site with one heavy rule of its own, and that rule
   says position: relative - so a slot that only said "absolute" would quietly
   stay in the flow and stack down the page instead of standing on its spoke.
   The site's own stylesheet says the same thing about itself: whatever has to
   beat that rule, says so. */
.slots { position: absolute; inset: 0; }
.slot {
  position: absolute !important;
  left: calc(50% + var(--x)); top: calc(50% + var(--y));
  /* --shn is how wide this shard is, as a % of the sigil square. spell.js sets
     it per shard, because the circle gets more crowded the more is bound -
     and a shard is cut in the sigil's own measure, not in pixels, so the whole
     figure keeps its proportions at any window size. */
  --sw: calc(var(--sig) * var(--shn, 18) / 100);
  --sh: calc(var(--sw) * var(--hx-ratio));
  width: var(--sw); height: var(--sh);
  margin: calc(var(--sh) / -2) 0 0 calc(var(--sw) / -2);
  /* a quarter of the width in, and the slanted sides meeting at half the
     height: all six sides the same length (see --hx-ratio) */
  --ch: 25%; --rim: 1.6px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px; padding: 0 calc(var(--sw) * .07); cursor: grab; touch-action: none;
  /* Pointer input is divided into the centered circular grab target and the
     separate reach ring; the button remains keyboard-focusable. */
  pointer-events: none;
  /* a shard that joins does not simply appear where it will stand: the circle
     re-forms around it, so every shard walks to its new place */
  transition: left .55s cubic-bezier(.2, .8, .3, 1), top .55s cubic-bezier(.2, .8, .3, 1),
              width .55s ease, height .55s ease, margin .55s ease,
              /* the hold: a shard wakes up under its own --lit (spell.js), so
                 transform and filter have to answer as quickly as the light */
              transform .18s ease, filter .18s ease;
}
.slot.land { animation: land .34s cubic-bezier(.2, 1.5, .5, 1) 1; }
.slot.dragging { opacity: .3; }
/* The one place still empty - where the next shard would stand. It is drawn
   faintly, and only while something is being carried, which is what "possible
   attachment points illuminate" means here. It is not a real place, so it is
   kept out of the tab order and it answers to nothing. */
.sigil .slot.pending { opacity: 0; pointer-events: none; cursor: default; }
.sigil.hot .slot.pending { opacity: .5; }
.sigil .slot.pending {
  background: linear-gradient(90deg, rgba(216, 156, 255, .55), rgba(143, 123, 255, .30));
}
.sigil .slot.pending::before { background: rgba(22, 15, 44, .82); }
.slot.pending .ico { color: rgba(216, 156, 255, .55); }
.slot.pending .dt { display: none; }
@keyframes land {
  0%   { transform: scale(.4) rotate(-14deg); opacity: 0; }
  100% { transform: none; opacity: 1; }
}
.reach-control {
  position: absolute; z-index: 6; left: 0; top: 0;
  overflow: visible; pointer-events: none; touch-action: none;
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--cat) 42%, transparent));
  transition: left .55s cubic-bezier(.2, .8, .3, 1), top .55s cubic-bezier(.2, .8, .3, 1),
              width .55s ease, height .55s ease;
}
.reach-grab { fill: transparent; stroke: none; pointer-events: fill; cursor: grab; }
.reach-grab:active { cursor: grabbing; }
.reach-track { fill: none; stroke: color-mix(in srgb, var(--cat) 58%, #fff 10%); stroke-width: 2.3; pointer-events: stroke; }
.reach-progress { fill: none; stroke: var(--cat); stroke-width: 2.8; stroke-linecap: round; transform: rotate(-90deg); transform-origin: 50% 50%; pointer-events: none; }
.reach-thumb { fill: var(--cat); stroke: #fff; stroke-width: 1.5; pointer-events: none; }
.reach-control:focus-visible .reach-track { stroke: #fff; stroke-width: 4; }
.slot .ico { font-size: min(30px, var(--sw) * .18); color: var(--hx-star); opacity: .85; }
.slot .nm {
  /* A name has to fit the shape, and a hexagon is at its narrowest above and
     below the middle - so the font is capped three ways: a comfortable ceiling,
     a share of the shard, and - the one that matters for a long name - the room
     that name's longest unbreakable run actually needs. "--len" is written by
     spell.js from the name itself; with no --len the six-character default
     stands and the rule is exactly what it was. --nmw is the name's box: 78%
     of the shard's content box, which is the shard less its own side padding
     (.07 of it on either side). */
  --nmw: calc(var(--sw) * .6708);
  /* the floor at 6px is the point past which a name stops being read at all:
     below it the box would rather show fewer letters, clearly, than all of them
     as grey mush - which is what the ellipsis in the rule below does. */
  font-size: max(6px, min(15px, calc(var(--sw) * .15),
                          calc(var(--nmw) / (var(--len, 6) * var(--cw)))));
  font-weight: 600; letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--spell-ink); text-align: center; line-height: 1.1;
  /* a hexagon is at its narrowest above and below the middle, and the name sits
     below the icon - so it is kept to the part of the shape that is wide enough
     to hold it, and ellipsised rather than cut off by the clip-path. The font
     rule above is what keeps that from happening to a name that would fit. */
  max-width: 78%; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.slot .dt {
  width: 4px; height: 4px; border-radius: 50%; background: var(--cat);
  box-shadow: 0 0 5px var(--cat);
}

/* the slot a dragged shard would land in, while the pointer is over the sigil */
.slot.here {
  filter: drop-shadow(0 0 12px rgba(255, 224, 252, .8));
  animation: pulse .9s ease-in-out infinite;
}
@keyframes pulse { 50% { filter: drop-shadow(0 0 20px rgba(255, 224, 252, .95)); } }
.sigil.hot .core-glass { box-shadow: 0 0 0 1.5px rgba(255, 224, 252, .7), 0 0 44px rgba(196, 107, 255, .5); }

/* ------------------------------------------------------------------ the tray */

.tray {
  width: 100%; max-width: 1000px; margin-top: 4px;
  border: 1px solid var(--rail-card-line); border-radius: 12px;
  background: var(--rail-card);
  padding: 12px 14px 10px;
}

.shelves { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.shelves button {
  --ch: 8px; --rim: 1px;
  padding: 5px 11px; font-size: 11px; color: var(--ink-faint);
  display: inline-flex; align-items: baseline; gap: 6px; cursor: pointer;
}
.shelves button b { font-size: 9.5px; opacity: .65; font-weight: 600; }
.shelves button:hover { color: var(--ink); }
.shelves button.on { color: #fff; background: var(--hx-edge-lit); }
.shelves button.on::before { background: var(--hx-face-lit); }
/* a shelf holding something that sits close to a bound shard keeps a small
   light on its corner - the way the sigil says "there is something like that
   over here" without dragging anybody anywhere */
.shelves button.near::after { content: "✧"; }
.shelves button { position: relative; }
.shelves button.near > .nb {
  position: absolute; right: 3px; top: 1px; font-size: 8px; color: var(--hx-star);
}

.frags { display: flex; flex-wrap: wrap; gap: 7px 5px; min-height: 78px;
         max-height: 190px; overflow: auto; padding: 2px 2px 4px; }

/* a shard: the same hexagon as everything else, tinted by its category */
.frag {
  width: 88px; height: calc(88px * var(--hx-ratio)); --ch: 25%; --rim: 1.4px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; padding: 0 8px; cursor: grab; touch-action: none;
  position: relative;
}
.frag .ico { font-size: 13px; color: var(--hx-star); opacity: .8; }
.frag .nm {
  /* the tray shard is a fixed 88px, so its name's box is fixed too: the shard
     less its 8px of side padding. Same three-way cap as the shards on the
     circle, from the same --len (see .slot .nm). */
  --nmw: 72px;
  font-size: min(9.5px, calc(var(--nmw) / (var(--len, 6) * var(--cw))));
  font-weight: 600; letter-spacing: .02em; text-transform: uppercase;
  color: var(--ink); text-align: center; line-height: 1.15;
  max-width: 100%; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.frag .cat {
  position: absolute; left: 50%; bottom: 9px; transform: translateX(-50%);
  width: 14px; height: 2px; border-radius: 2px; background: var(--cat);
  opacity: .75;
}
.frag:hover { filter: drop-shadow(0 0 10px var(--hx-halo)); }
.frag:hover .ico { opacity: 1; }
.frag[aria-pressed="true"] { opacity: .35; cursor: default; }
.frag.dragging { opacity: .25; }

/* The ghost that follows the finger while a shard is being carried. It has to be
   fixed in the window, and hex.css's button rule would make it relative - hence
   the !important, exactly as the site's own stylesheet does it.

   It is also the one shard on this page that is NOT a button, and the hexagon is
   cut on buttons - so its shape is written out here instead: the same cut, and
   the same dark inside inset by the rim. This is the moment the long hexagon of
   a column becomes the real hexagon the shard will wear in the circle.

   It is called .drag-ghost, not .ghost, because a bare .ghost is a name the
   rest of the site already owns: the profile panel's buttons are
   "prof-act ghost" / "prof-act ghost wide" (see profile.css). Every stylesheet
   here is loaded on one page, but every page carries the profile panel - so a
   bare .ghost in this sheet reached into that panel and dressed "Log out" and
   "Done" as 88x76 fixed hexagons, mid-drag, turned 4deg and scaled, with
   pointer-events: none on them. Namespacing the class is the fix. */
.drag-ghost {
  position: fixed !important; z-index: 60; pointer-events: none;
  width: 88px; height: calc(88px * var(--hx-ratio)); --ch: 25%; --rim: 1.4px;
  clip-path: polygon(
    var(--ch) 0, calc(100% - var(--ch)) 0, 100% 50%,
    calc(100% - var(--ch)) 100%, var(--ch) 100%, 0 50%);
  background: var(--hx-edge);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; padding: 0 8px;
  transform: translate(-50%, -50%) scale(1.12) rotate(4deg);
  filter: drop-shadow(0 0 18px rgba(255, 224, 252, .55));
  opacity: .96;
}
.drag-ghost::before {
  content: ""; position: absolute; inset: var(--rim); z-index: -1;
  pointer-events: none;
  clip-path: polygon(
    calc(var(--ch) - var(--rim)) 0, calc(100% - var(--ch) + var(--rim)) 0, 100% 50%,
    calc(100% - var(--ch) + var(--rim)) 100%, calc(var(--ch) - var(--rim)) 100%, 0 50%);
  background: var(--hx-face);
}
.drag-ghost .ico { font-size: 13px; color: var(--hx-star); }
.drag-ghost .nm {
  /* the carried shard is the tray shard, so it takes the tray's measure too */
  --nmw: 72px;
  font-size: min(9.5px, calc(var(--nmw) / (var(--len, 6) * var(--cw))));
  font-weight: 600; text-transform: uppercase; color: #fff;
  text-align: center; line-height: 1.15;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* a shard whose meaning sits close to something already bound: it leans in a
   little, and keeps a small light. It is a hint, never a button that moves. */
.frag.near { filter: drop-shadow(0 0 9px rgba(255, 224, 252, .45)); }
.frag.near .ico { opacity: 1; color: #ffe4fb; }
.frag.near .cat { opacity: 1; box-shadow: 0 0 6px var(--cat); }

.spell-hint { margin: 8px 2px 0; font-size: 10.5px; color: var(--ink-faint); }

/* ------------------------------------------------------------------- the cast */

/* The cast stands in the middle, under the sigil, one column: the name of the
   thing, the two ways to read it, what to do with it, and the small keys. */
.castbar {
  width: 100%; max-width: 640px; margin: 0 auto;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  text-align: center;
}
.cast-lab {
  font-size: 14px; letter-spacing: .14em;
  color: var(--ink-faint); transition: color .3s ease, text-shadow .3s ease;
}
.cast-halves { display: flex; gap: 3px; }
.half {
  --ch: 14px; --rim: 1.6px;
  position: relative;                    /* the hold's fill line rides on it */
  min-width: 168px; padding: 12px 18px;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font-size: 12.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-soft); cursor: pointer;
  /* the hold is a pointer gesture: the browser must not scroll or select the
     page out from under it, and the pointer must not leave the button */
  touch-action: none; user-select: none; -webkit-user-select: none;
  transition: color .2s ease, filter .3s ease, transform .2s ease;
}
.half .h-ico { font-size: 14px; opacity: .7; }
/* Readiness is visible in the button chrome as well as exposed through the
   native disabled state and aria-disabled value. */
#castbar[data-ready="false"] .half {
  color: #f4b7c5; --hx-light: #dd829a; --hx-main: #a64963;
  --hx-edge: #602a3b; --hx-halo: rgba(221, 92, 127, .25); opacity: .58;
}
#castbar[data-ready="true"] .half {
  color: #f0fff5; --hx-light: #7ef3b0; --hx-main: #36ba79;
  --hx-edge: #236c4d; --hx-halo: rgba(75, 232, 147, .42); opacity: 1;
}
.half:disabled { cursor: default; opacity: .5; }
.half:not(:disabled):hover { color: #fff; transform: translateY(-1px);
                             filter: drop-shadow(0 0 12px var(--hx-halo)); }
/* The held button shows how much of the hold is spent: a lit line filling
   along its own flat bottom edge, left to right. It is the same --charge the
   whole figure reads, so the line and the spell are the same length.
   Scoped by the id on purpose: the site's button chrome owns ::after as well
   (a star-ink wash at inset 0), and only an id outranks that rule. */
#castbar .half::after {
  content: ""; position: absolute; top: auto; left: var(--ch); right: var(--ch); bottom: 4px;
  height: 2px; border-radius: 2px; pointer-events: none; z-index: 1;
  background: linear-gradient(90deg, var(--hx-light), var(--hx-main) 55%, var(--hx-deep));
  transform: scaleX(var(--charge, 0)); transform-origin: left center;
  opacity: 0; transition: opacity .18s ease;
}
body.charging #castbar .half:not(:disabled)::after { opacity: .95; }
#castbar[data-n="0"] .half::after { display: none; }
.cast-hold {
  margin: 0; font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-faint); transition: color .3s ease, opacity .3s ease;
}
.castbar[data-n="0"] .cast-hold { opacity: .4; }
/* pressed and let go too soon: the one thing to say is how it works */
.cast-hold.nudge { color: var(--hx-star); opacity: 1; }
/* the cast grows as the sigil fills: one shard just lights the words, four or
   more light the whole thing */
.castbar[data-n="1"] .cast-lab { color: var(--ink-soft); }
.castbar[data-n="2"] .half, .castbar[data-n="3"] .half { color: var(--ink); }
.castbar[data-n="4"] .half, .castbar[data-n="5"] .half, .castbar[data-n="6"] .half,
.castbar[data-n="7"] .half {
  color: #fff; background: var(--hx-edge); --rim: 2.2px;
}
.castbar[data-n="4"] .cast-lab, .castbar[data-n="5"] .cast-lab,
.castbar[data-n="6"] .cast-lab, .castbar[data-n="7"] .cast-lab {
  color: var(--hx-star); text-shadow: 0 0 16px var(--hx-halo);
}
.h-half { flex: 0 0 auto; }

.cast-note { margin: 0; max-width: 560px; font-size: 11px; color: var(--ink-soft); }
.cast-keys { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; }
.mini {
  --ch: 7px; --rim: 1px;
  padding: 5px 11px; font-size: 10.5px; color: var(--ink-faint); cursor: pointer;
}
.mini:hover:not(:disabled) { color: #fff; }
.mini:disabled { opacity: .4; cursor: default; }

/* --------------------------------------------------------- waking the circle

   A cast is one press, and the circle wakes itself. All of it is one number,
   --charge, 0 to 1, written on :root once a frame by spell.js while the wake
   runs; everything the figure does reads that number, plus one --lit per shard
   and per line, so the light runs in the order the spell was built - the first
   shard bound lights first, the last one lights exactly as the circle blooms.
   Nothing here is held and nothing is let go of. */

/* the shard the hold has reached swells and glows. --motion is 0 for anyone
   who asked the system for less movement, and takes the swell with it. */
.sigil .slot:not(.pending) {
  transform: scale(calc(1 + var(--lit, 0) * .12 * var(--motion)));
  filter: brightness(calc(1 + var(--lit, 0) * 1.35))
          drop-shadow(0 0 calc(14px * var(--lit, 0)) var(--hx-halo));
}
/* the two decorative circles and the halo come up with the hold as a whole */
.rings { filter: brightness(calc(1 + var(--charge, 0) * 1.7)); }
.halo { opacity: calc(.75 + var(--charge, 0) * .25); }
/* the glow on the lines is only ever paid for while the circle is waking */
body.charging .web line {
  filter: drop-shadow(0 0 calc(6px * var(--lit, 0)) rgba(255, 184, 244, .9));
}

/* The wake does not sweep: it breathes out. One nebula per shard bound, each
   one the colour of the shard it came from, opening out of the very middle of
   the star and drifting a little further as it grows. The first shard bound is
   the first cloud out, so the order the spell was built in is the order the
   sky fills in. spell.js builds them on the press and throws them away if the
   wake is dropped (see nebula()), and they are a layer of their own rather
   than a filter on the sigil, because a filter on the whole figure would cost
   every shard a repaint on every frame. */
.neb { position: absolute; inset: 0; pointer-events: none; }

.neb-cloud {
  position: absolute; left: 50%; top: 50%;
  width: var(--nw, 300px); height: var(--nw, 300px);
  margin: calc(var(--nw, 300px) / -2) 0 0 calc(var(--nw, 300px) / -2);
  border-radius: 50%; opacity: 0;
  transform: translate3d(0, 0, 0) scale(.05) rotate(0deg);
  animation: neb-rise var(--nlife, 1200ms) cubic-bezier(.22, .62, .3, 1)
             var(--nd, 0ms) 1 both;
  will-change: transform, opacity;
}

/* The gas of the cloud: four cores, none of them dead centre, so it has a shape
   to turn rather than a plain glow - the body in the shard's own colour, a
   second lobe of it, a paler heart of the same colour, and the shard's star at
   the middle of it. It is a pseudo element of its own because the blur has to
   sit on the gas and not on the stars inside it: a filter on the cloud would
   blur its own stars away. */
.neb-cloud::before {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background:
    radial-gradient(circle at 42% 45%, var(--nc, #c46bff) 0%, transparent 54%),
    radial-gradient(circle at 59% 56%, var(--nc, #c46bff) 0%, transparent 63%),
    radial-gradient(circle at 50% 50%, var(--nl, #ffd0f6) 0%, transparent 34%),
    radial-gradient(circle at 50% 50%, var(--ns, #f3d8ff) 0%, transparent 13%);
  filter: blur(var(--nblur, 16px)) saturate(1.5) brightness(1.15);
  /* three soft circles added together, so the silhouette is a cloud with a
     heavy side to it rather than a disc */
  -webkit-mask-image:
    radial-gradient(circle at 44% 46%, #000 34%, transparent 86%),
    radial-gradient(circle at 62% 57%, #000 24%, transparent 78%),
    radial-gradient(circle at 52% 34%, #000 20%, transparent 72%);
          mask-image:
    radial-gradient(circle at 44% 46%, #000 34%, transparent 86%),
    radial-gradient(circle at 62% 57%, #000 24%, transparent 78%),
    radial-gradient(circle at 52% 34%, #000 20%, transparent 72%);
  animation: neb-turn var(--nlife, 1200ms) linear var(--nd, 0ms) 1 both;
}

/* The stars inside the cloud: five dots tiled over it and masked to its round,
   so they turn and grow with the cloud instead of sitting on the screen. They
   are the one part of the cloud that is not blurred, which is what makes the
   wake sparkle rather than glow. */
.neb-cloud::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background-image:
    radial-gradient(circle at 30% 34%, #fff 0 1.6px, transparent 3px),
    radial-gradient(circle at 72% 22%, rgba(255, 255, 255, .9) 0 1.1px, transparent 2.4px),
    radial-gradient(circle at 54% 78%, var(--ns, #fff) 0 1.9px, transparent 3.4px),
    radial-gradient(circle at 18% 66%, rgba(255, 255, 255, .8) 0 .9px, transparent 2px),
    radial-gradient(circle at 84% 62%, var(--nl, #fff) 0 1.3px, transparent 2.8px);
  background-size: 30px 30px, 41px 41px, 53px 53px, 23px 23px, 67px 67px;
  -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 20%, rgba(0, 0, 0, .6) 50%, transparent 76%);
          mask-image: radial-gradient(circle at 50% 50%, #000 20%, rgba(0, 0, 0, .6) 50%, transparent 76%);
  animation: neb-twinkle var(--nlife, 1200ms) ease-in-out var(--nd, 0ms) 1 both,
             neb-spin var(--nlife, 1200ms) linear var(--nd, 0ms) 1 both;
}

/* Out of the middle, out a little further, and still growing while the figure
   blooms and the page turns over: the last thing the spell does is leave, and
   it leaves toward the half of the cast bar it was sent to. */
@keyframes neb-rise {
  0%   { opacity: 0; transform: translate3d(0, 0, 0) scale(.05) rotate(0deg); }
  9%   { opacity: var(--no, .6); }
  62%  { opacity: calc(var(--no, .6) * .92);
         transform: translate3d(calc(var(--nx, 0px) + var(--ndx, 0px) * .35),
                                calc(var(--ny, 0px) + var(--ndy, 0px) * .35), 0)
                    scale(1) rotate(var(--nr, 60deg)); }
  100% { opacity: calc(var(--no, .6) * .78);
         transform: translate3d(calc(var(--nx, 0px) * 1.5 + var(--ndx, 0px)),
                                calc(var(--ny, 0px) * 1.5 + var(--ndy, 0px)), 0)
                    scale(1.55) rotate(calc(var(--nr, 60deg) * 1.7)); }
}
/* the gas turns one way while it opens, the stars over it the other way */
@keyframes neb-turn {
  0%   { transform: rotate(0deg) scale(.92); }
  100% { transform: rotate(var(--nr, 60deg)) scale(1.08); }
}
@keyframes neb-spin {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(calc(var(--nr, 60deg) * -.5)); }
}
/* twice over, so the cloud flashes as it opens and again as it leaves */
@keyframes neb-twinkle {
  0%   { opacity: 0; }
  20%  { opacity: .95; }
  44%  { opacity: .35; }
  68%  { opacity: .88; }
  100% { opacity: .34; }
}
/* the two rings that always turn very slowly wind up for the wake, and the
   dotted one and the runes turn for the first time - all of them decoration,
   all of them over the moment the page goes */
body.charging .rings .r1 { animation-duration: 1.6s; }
body.charging .rings .r2 { animation-duration: 2.4s; }
body.charging .rings .r3 { animation: spellspin 1.1s linear infinite; }
body.charging .runes { animation: spellspin 2s linear infinite reverse; }
body.charging .runes i { color: rgba(255, 224, 252, .9); }
@keyframes spellspin { to { transform: rotate(360deg); } }

/* the beat between the hold completing and the page turning over: the whole
   figure blooms once, already lit from the first shard to the last */
body.casting .sigil { animation: charge .38s ease-in forwards; }
body.casting .halo { opacity: 1; }
@keyframes charge {
  0%   { transform: scale(1); }
  55%  { transform: scale(1.05); }
  100% { transform: scale(1.28); opacity: 0; }
}
body.casting .web line { stroke-opacity: .95 !important; }

/* ------------------------------------------------------------------ colours */

/* The lit edge has to be written out, not referenced: --hx-edge is declared
   once on :root and its var()s are resolved right there. Same colours the
   taxonomy hands the genres - this is just a second place to name them. */
[data-cat="purpose"] {
  --hx-light: #ffe6ac; --hx-main: var(--c-purpose); --hx-deep: #b8862a;
  --hx-edge: linear-gradient(90deg, #ffe6ac, var(--c-purpose) 48%, #b8862a);
  --hx-edge-lit: linear-gradient(90deg, #fff6dd, #ffe6ac 26%, var(--c-purpose) 70%, #b8862a);
  --hx-halo: rgba(239, 197, 97, .45); --hx-star: #fff0c8; --cat: var(--c-purpose);
}
[data-cat="activity"] {
  --hx-light: #c6ffd9; --hx-main: var(--c-activity); --hx-deep: #2a9c63;
  --hx-edge: linear-gradient(90deg, #c6ffd9, var(--c-activity) 48%, #2a9c63);
  --hx-edge-lit: linear-gradient(90deg, #eafff2, #c6ffd9 26%, var(--c-activity) 70%, #2a9c63);
  --hx-halo: rgba(97, 239, 164, .45); --hx-star: #d9ffe9; --cat: var(--c-activity);
}
[data-cat="setting"] {
  --hx-light: #c2dcff; --hx-main: var(--c-setting); --hx-deep: #2a5fb0;
  --hx-edge: linear-gradient(90deg, #c2dcff, var(--c-setting) 48%, #2a5fb0);
  --hx-edge-lit: linear-gradient(90deg, #e8f2ff, #c2dcff 26%, var(--c-setting) 70%, #2a5fb0);
  --hx-halo: rgba(97, 164, 239, .45); --hx-star: #d8e8ff; --cat: var(--c-setting);
}
[data-cat="mood"] {
  --hx-light: #ffd0f6; --hx-main: var(--c-mood); --hx-deep: #7a34ff;
  --hx-edge: linear-gradient(90deg, #ffd0f6, var(--c-mood) 48%, #7a34ff);
  --hx-edge-lit: linear-gradient(90deg, #ffe4fb, #ffd0f6 26%, var(--c-mood) 70%, #7a34ff);
  --hx-halo: rgba(196, 107, 255, .5); --hx-star: #f3d8ff; --cat: var(--c-mood);
}
[data-cat="atmosphere"] {
  --hx-light: #d6fbff; --hx-main: var(--c-atmosphere); --hx-deep: #2aa79a;
  --hx-edge: linear-gradient(90deg, #d6fbff, var(--c-atmosphere) 48%, #2aa79a);
  --hx-edge-lit: linear-gradient(90deg, #f0ffff, #d6fbff 26%, var(--c-atmosphere) 70%, #2aa79a);
  --hx-halo: rgba(97, 239, 216, .45); --hx-star: #dcfffa; --cat: var(--c-atmosphere);
}
[data-cat="audience"] {
  --hx-light: #ffd6e6; --hx-main: var(--c-audience); --hx-deep: #b02a5c;
  --hx-edge: linear-gradient(90deg, #ffd6e6, var(--c-audience) 48%, #b02a5c);
  --hx-edge-lit: linear-gradient(90deg, #ffeef4, #ffd6e6 26%, var(--c-audience) 70%, #b02a5c);
  --hx-halo: rgba(239, 97, 147, .45); --hx-star: #ffe0eb; --cat: var(--c-audience);
}
[data-cat="features"] {
  --hx-light: #e2e8ff; --hx-main: var(--c-features); --hx-deep: #4f5f9c;
  --hx-edge: linear-gradient(90deg, #e2e8ff, var(--c-features) 48%, #4f5f9c);
  --hx-edge-lit: linear-gradient(90deg, #f4f6ff, #e2e8ff 26%, var(--c-features) 70%, #4f5f9c);
  --hx-halo: rgba(159, 176, 239, .45); --hx-star: #e8edff; --cat: var(--c-features);
}
[data-cat="structure"] {
  --hx-light: #ffdec2; --hx-main: var(--c-structure); --hx-deep: #b0562a;
  --hx-edge: linear-gradient(90deg, #ffdec2, var(--c-structure) 48%, #b0562a);
  --hx-edge-lit: linear-gradient(90deg, #fff0e2, #ffdec2 26%, var(--c-structure) 70%, #b0562a);
  --hx-halo: rgba(239, 147, 97, .45); --hx-star: #ffe6d2; --cat: var(--c-structure);
}
[data-cat="progression"] {
  --hx-light: #f2ffc2; --hx-main: var(--c-progression); --hx-deep: #6ba32a;
  --hx-edge: linear-gradient(90deg, #f2ffc2, var(--c-progression) 48%, #6ba32a);
  --hx-edge-lit: linear-gradient(90deg, #faffea, #f2ffc2 26%, var(--c-progression) 70%, #6ba32a);
  --hx-halo: rgba(180, 239, 97, .45); --hx-star: #f0ffd0; --cat: var(--c-progression);
}
[data-cat="session"] {
  --hx-light: #dbe4ee; --hx-main: var(--c-session); --hx-deep: #4a5a6b;
  --hx-edge: linear-gradient(90deg, #dbe4ee, var(--c-session) 48%, #4a5a6b);
  --hx-edge-lit: linear-gradient(90deg, #f0f5fa, #dbe4ee 26%, var(--c-session) 70%, #4a5a6b);
  --hx-halo: rgba(143, 163, 184, .45); --hx-star: #e6eef7; --cat: var(--c-session);
}
[data-cat="style"] {
  --hx-light: #d8ccff; --hx-main: var(--c-style); --hx-deep: #5b45b0;
  --hx-edge: linear-gradient(90deg, #d8ccff, var(--c-style) 48%, #5b45b0);
  --hx-edge-lit: linear-gradient(90deg, #f0eaff, #d8ccff 26%, var(--c-style) 70%, #5b45b0);
  --hx-halo: rgba(159, 140, 239, .45); --hx-star: #e2daff; --cat: var(--c-style);
}

/* --------------------------------------------------- what a category feels like */

/* Mood shards glow soft and wide; atmosphere shards carry a few slow motes;
   genre shards are cut a little sharper and lean in; setting shards stand on a
   faint ground line. Nothing here is the only sign of its category - the name,
   the glyph and the colour bar all say it too. */
[data-cat="mood"] { --rim: .9px; }
[data-cat="mood"] .ico { opacity: .95; }
[data-cat="purpose"] { --rim: 2.1px; }
[data-cat="purpose"] .nm { letter-spacing: .06em; }
[data-cat="activity"] .ico { transform: translateX(1px); }
[data-cat="setting"] .cat { height: 3px; width: 18px; }
[data-cat="atmosphere"] .ico { animation: drift 6.5s ease-in-out infinite; }
@keyframes drift { 50% { transform: translateY(-2px); opacity: .6; } }

/* ------------------------------------------------------------------ narrow */

@media (max-width: 900px) {
  :root { --sig: min(48vh, 400px); }
  .frag { width: 76px; height: calc(76px * var(--hx-ratio)); }
  .half { min-width: 0; padding: 10px 14px; font-size: 11.5px; }
  .castbar { gap: 10px; }
  .frags { max-height: 190px; }
}

/* --------------------------------------------------------- turning it off */

@media (prefers-reduced-motion: reduce) {
  /* the swell goes; the light still comes up, because the light is the point */
  :root { --motion: 0; }
  .rings .r, .frag .ico, .slot, .web line.flow, .slot.here { animation: none !important; }
  .slot, .reach-control, .core, .core-glass { transition: none !important; }
  .web { transition: none !important; transform: none !important; }
  body.casting .sigil { animation: none !important; }
  .neb { display: none; }
  body.charging .rings .r1 { animation-duration: 96s; }
  body.charging .rings .r2 { animation-duration: 150s; }
  body.charging .rings .r3, body.charging .runes { animation: none !important; }
  .drag-ghost { transform: translate(-50%, -50%); }
}
/* ============================================================ the columns ====

   The vocabulary stands around the circle - three columns on its left, three on
   its right, and the genres in a grid below it. Every shard of a facet stands in
   its own column, and each column scrolls on its own. Nothing out here waits
   behind a click any more.

   An item in a column is the same cut as every other button on the site, in a
   box wider than it is tall: a narrow hexagon lying down, a word row the width
   of its column, wearing its category on its rim like any other shard. That is
   the shape the Atlas already gives a word, and it is what lets a column hold
   eight or nine of them where a standing hexagon held two. Picked up, a shard
   is a real hexagon again (see .drag-ghost) - the shape it wears in the circle - and
   that one change of shape is the whole of what the gesture says about where a
   shard is going.

   The circle is not touched by any of this. It keeps the size --sig gives it and
   the middle of the row; the racks stand on either side of it at their own
   height, so a long column never stretches the circle and a short one never
   shrinks it. */

.spell .stage {
  position: relative; width: 100%; max-width: 1440px; margin: 0 auto;
  min-height: 380px; padding: 0; height: auto; overflow: visible;
  display: flex; flex-direction: row; align-items: center;
  justify-content: center; gap: 16px;
}

/* the circle itself: a fixed square in the middle of the row, in the flow, so
   the two racks are laid out around it instead of over it */
.spell .board {
  position: relative; flex: 0 0 auto; margin: 0;
  width: var(--sig); height: var(--sig);
}
.spell .board > .sigil { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
.spell .board > .halo { left: 50%; top: 50%; }

/* While the circle is empty the middle belongs to the invitation, so the
   standing reading and the glass under it step aside - and come back with the
   first shard. (The very same thing happens with exactly one shard, further up:
   a single shard stands in the middle, so the reading has nowhere to stand.) */
.sigil[data-n="0"] .core, .sigil[data-n="0"] .core-glass { opacity: 0; }
/* ... and while a shard is being carried in, the invitation gets out of its way:
   the thing under the hand owns the middle of an empty circle then */
.sigil.hot ~ .roll { opacity: 0; visibility: hidden; pointer-events: none; }

/* The invitation that stands in the middle while the circle is empty: press it
   and the spell is drawn instead of picked. It is a button, but it is NOT cut
   like the other buttons on this site - a clip-path cuts a button's words with
   it, and a sentence like "click here to build a random search spell" would lose
   its ends to the slanted sides. So the shape is a layer of its own underneath,
   exactly the way the reading stands on its own glass, and the words ride over
   it uncut. */
.spell .board > .roll {
  --rw: calc(var(--sig) * .29);        /* wide enough for the sentence, no wider */
  position: absolute; left: 50%; top: 50%; z-index: 2;
  width: var(--rw); height: calc(var(--rw) * var(--hx-ratio));
  transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: calc(var(--rw) / 34);
  padding: 0; border: 0; border-radius: 0;
  background: none; box-shadow: none; clip-path: none;
  color: var(--ink-soft); cursor: pointer; text-align: center;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .45s ease, visibility 0s linear .45s;
}
/* nothing is bound: the middle of the circle is the way in */
.sigil[data-n="0"] ~ .roll {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity .45s ease;
}
/* the button's own chrome comes off here: this one wears the invitation's shape
   instead of a sigil's, and the words are its own */
.spell .board > .roll::before,
.spell .board > .roll::after { content: none; }

/* the shape under the words: a hexagon of night, the same faint cut the reading
   stands on, so the empty middle looks like it is offering something */
.spell .roll .roll-hex {
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%);
  background: radial-gradient(circle at 50% 46%,
    rgba(58, 34, 98, .42), rgba(9, 8, 22, .30) 72%);
  transition: background .3s ease, filter .3s ease;
}

.spell .roll .roll-star {
  font-size: calc(var(--rw) / 8); line-height: 1;
  color: var(--hx-star); filter: drop-shadow(0 0 7px rgba(196, 107, 255, .60));
  transition: color .3s ease;
}
.spell .roll .roll-nm {
  font-size: calc(var(--rw) / 9.1); line-height: 1.1; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; white-space: nowrap;
  color: #fff; text-shadow: 0 0 14px rgba(196, 107, 255, .50);
  transition: color .3s ease;
}
.spell .roll .roll-sub {
  font-size: calc(var(--rw) / 9.3); line-height: 1.25;
  color: var(--ink-soft); transition: color .3s ease;
}

/* under the hand: the hexagon lights up and the words come forward */
.spell .board > .roll:hover .roll-hex {
  background: radial-gradient(circle at 50% 46%,
    rgba(104, 62, 164, .58), rgba(14, 11, 32, .42) 74%);
  filter: drop-shadow(0 0 16px rgba(196, 107, 255, .45));
}
.spell .board > .roll:hover .roll-sub { color: #fff; }
.spell .board > .roll:active .roll-hex { filter: drop-shadow(0 0 22px rgba(255, 184, 244, .55)); }
.spell .board > .roll:focus-visible { outline: 0; }
.spell .board > .roll:focus-visible .roll-hex {
  background: radial-gradient(circle at 50% 46%,
    rgba(104, 62, 164, .58), rgba(14, 11, 32, .42) 74%);
  filter: drop-shadow(0 0 18px rgba(255, 184, 244, .55));
}

/* the lines of help stand together at the very foot of the page, under
   everything the page has to offer, stacked so that a long one can never grow
   into the other */
.spell > .foot {
  width: min(94%, 680px); display: flex; flex-direction: column;
  align-items: center; gap: 3px; text-align: center; pointer-events: none;
  padding: 0 16px;
}
.spell-result-count { margin: 0; color: var(--ink-soft); font-size: 11px; }
.spell-reach { width: min(100%, 480px); margin: 12px auto 6px; pointer-events: auto; }
.spell .foot .stage-hint, .spell .foot .spell-hint {
  position: static; left: auto; bottom: auto; width: auto; max-width: 100%;
  margin: 0; text-align: center;
}
.spell .foot .stage-hint { opacity: .92; }
.spell .foot .spell-hint:empty { display: none; }

/* --------------------------------------------------------- sealed circles

   A spell can be sealed: the circle is written down as a card, and the cards
   stand in the top corner of the page - above the right rack, beside the
   heading, the one place on this page that nothing else wants. Press one, or
   carry one into the middle of the circle, and the spell is spelled again
   exactly as it was kept: the same shards in the same order, so it is the same
   figure and the same search.

   The corner is taken out of the flow on purpose. A card must never move the
   circle, and the circle is the one thing on this page that keeps its place
   whatever else is on the page - so the panel gets a height of its own, scrolls
   inside it, and the air under it is the gap between the heading and the cast
   bar. */
.spell > .seals {
  position: absolute; z-index: 3;
  /* the page's own top padding is where the heading starts, so that is where
     the corner starts - and it stops short of the racks, which begin lower */
  top: calc(var(--bar-h) + 16px); right: 0;
  width: 268px; max-height: var(--seal-h, 134px);
  display: flex; flex-direction: column; gap: 4px;
}
.spell > .seals[hidden] { display: none; }
.seals-head {
  display: flex; align-items: baseline; justify-content: flex-end; gap: 6px;
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-faint);
}
.seals-head b { font-size: 9px; font-weight: 600; color: var(--hx-star); }
.seals-list {
  display: flex; flex-direction: column; gap: 4px; min-height: 0;
  overflow-y: auto; padding: 1px;
}

/* One card: the reading it was given over the shards it is made of, and the key
   that forgets it beside them. Two buttons rather than one, because the forget
   key has to be a press of its own - and because a card wide enough for a
   reading has no room to have its ends eaten by a cut that leans in. */
.seal { display: flex; align-items: stretch; gap: 3px; }
.seal-go {
  --ch: 8px; --rim: 1px;
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 5px 9px; text-align: left; cursor: grab;
  touch-action: none; user-select: none; -webkit-user-select: none;
}
.seal-nm {
  max-width: 100%; font-size: 10px; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; color: var(--spell-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.seal-row { display: flex; align-items: center; gap: 6px; width: 100%; }
.seal-dots { display: flex; gap: 3px; }
/* one small hexagon per shard, in that shard's own category colour */
.seal-dots i {
  display: block; width: 7px; height: 6px;
  clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%);
}
.seal-n { margin-left: auto; font-size: 9px; font-weight: 600; color: var(--ink-faint); }
.seal-x {
  /* a narrow key: the cut is small because the button is, and it keeps its own
     width so the reading beside it can never squeeze the press out of reach */
  --ch: 4px; --rim: 1px; flex: 0 0 auto; width: 22px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; line-height: 1; color: var(--ink-faint); cursor: pointer;
}
.seal-x:hover { color: #fff; }
.seal.dragging { opacity: .35; }

/* the card under the hand, out of the flow like the shard's own ghost, and cut
   like a card: a lit edge with a dark inside inset into it */
.seal.drag-ghost {
  position: fixed !important; z-index: 60; pointer-events: none;
  width: 210px; padding: 5px 9px; --ch: 8px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  clip-path: polygon(
    var(--ch) 0, calc(100% - var(--ch)) 0, 100% 50%,
    calc(100% - var(--ch)) 100%, var(--ch) 100%, 0 50%);
  background: var(--hx-edge);
  transform: translate(-50%, -50%) rotate(-3deg) scale(1.06);
  filter: drop-shadow(0 0 18px rgba(255, 224, 252, .5));
}
.seal.drag-ghost::before {
  content: ""; position: absolute; inset: 1px; z-index: -1; pointer-events: none;
  clip-path: polygon(
    calc(var(--ch) - 1px) 0, calc(100% - var(--ch) + 1px) 0, 100% 50%,
    calc(100% - var(--ch) + 1px) 100%, calc(var(--ch) - 1px) 100%, 0 50%);
  background: var(--hx-face);
}

/* ------------------------------------------------- the racks either side */

/* a rack is one side of the circle: three columns, sharing whatever width the
   page has left over once the circle has taken its own */
.spell .rack {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: row; align-items: flex-start; gap: 5px;
}

/* --------------------------------------------------------- the genre band */

/* The purposes stand under the circle as wide as the circle is, and no wider:
   two deep, four long, so every purpose name stands whole in its cell and the
   block reads as a square of eight under the wheel. Wider cells would only
   stretch the names apart; narrower ones would ellipsise them, which is why
   this grid is two across while a rack is three. There is no reason for this
   one to scroll - it is short by nature - so the grid may grow as tall as it
   likes, and .spell counts that height in --band-h. */
.spell .genres {
  width: 100%; max-width: 1440px; margin: 0 auto;
  display: flex; flex-direction: column; gap: 5px;
}
.spell .genres .col-head { max-width: 340px; margin: 0 auto; }
.spell .genres .col-list {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: 27px; align-items: stretch; justify-items: stretch;
  gap: 4px 12px; padding: 0;
  max-height: none; overflow: visible;
}

.spell .col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 5px; }

/* a column's head: the facet's own glyph, its name, and how much stands in it -
   the chip the group labels used to be, now standing still under the circle */
.spell .col-head {
  --ch: 9px; --rim: 1px;
  width: 100%; padding: 5px 7px;
  display: inline-flex; align-items: baseline; justify-content: center; gap: 5px;
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-soft);
}
.spell .col-head .gi { font-size: 10px; color: var(--cat); filter: drop-shadow(0 0 6px var(--cat)); }
.spell .col-head b { font-size: 9.5px; font-weight: 600; color: var(--ink-faint); }
.spell .col-head:hover { color: #fff; }
.spell .col-head.near { color: var(--hx-star); }
.spell .col-head.near::after { content: "\2727"; }
.spell .col-head.on { color: #fff; background: var(--hx-edge-lit); }
.spell .col-head.on::before { background: var(--hx-face-lit); }

/* the column itself: a stack of narrow pills, and the one thing on this page
   that scrolls by itself - so a facet of sixty shards still takes one column */
.spell .col-list {
  display: flex; flex-direction: column; align-items: stretch; gap: 3px;
  max-height: 236px; overflow-y: auto; overflow-x: hidden;
  padding: 2px 1px 4px;
}
/* The word row the Atlas wears, cut as this site cuts every button: a narrow
   hexagon lying down, as wide as its column, the glyph first and the name after
   it. Laid down instead of stood up, a column holds eight or nine of them where
   a standing hexagon held two - and the nine pixel cut is what keeps a box that
   short reading as a hexagon rather than as a rounded pill. */
.spell .col-list .frag {
  flex: 0 0 auto;
  width: 100%; max-width: none; height: 27px;
  --ch: 9px; --rim: 1px;
  display: flex; flex-direction: row; align-items: center;
  justify-content: flex-start; gap: 5px; padding: 0 8px;
  text-align: left;
}
/* the name is one line now: what does not fit is ellipsised rather than
   wrapped, and the tooltip carries the whole of it (see paintFrags in
   spell.js), so a column stays a column however long a word is */
.spell .col-list .frag .nm {
  flex: 1 1 auto; min-width: 0; max-width: 100%;
  display: block; -webkit-line-clamp: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: 11px; letter-spacing: 0; text-align: left;
}
/* the little colour bar under a tray shard is not needed here: the long hexagon
   already wears its category on its own rim */
.spell .col-list .frag .cat { display: none; }
.spell .col-list .frag .ico { font-size: 10px; flex: 0 0 auto; }

/* --------------------------------------- the circle beside two tall racks */

/* On a window with room for it the whole page is one grid, and the racks run
   the height of everything but the first and the last line of it.

   Five rows: the heading, the cast bar, the circle, the purposes, and the
   closing lines. The racks stand in the outer tracks and span the middle
   three - so a column starts level with the Starmap and Atlas buttons, which is
   the top of the cast bar, and runs down beside the circle and beside the
   purposes, stopping at the top of the closing lines. The stage is not a box
   of its own any more: it carries the four things and then steps out of the
   way (display: contents), so the racks and the circle are placed in the page
   grid instead of in a box inside it. Only the middle track is as wide as the
   circle; the cast bar is a little wider than that and stands across the two
   gaps either side, which is empty air - nothing else is drawn in the gaps, so
   nothing ever lands on a column.

   The two rows that make the circle's and the purposes' depth are counted in
   fixed heights (--sig, the circle's diameter, and --band-h) rather than sized
   by their content: a column holds sixty shards, and a row that measured its
   content would let one of them stretch the whole page to the length of its
   own list. */
@media (min-width: 901px) {
  .spell {
    display: grid; align-items: stretch;
    grid-template-columns: minmax(0, 1fr) var(--sig) minmax(0, 1fr);
    /* the cast bar's row is counted too: with a row that measured its own
       content the racks, which span it, would hand it the height of their
       longest list and the circle would be pushed a page down */
    grid-template-rows: auto var(--cast-h) var(--sig) var(--band-h) auto;
    column-gap: 16px; row-gap: 12px;
  }
  .spell .stage { display: contents; }
  .spell > .spell-head { grid-area: 1 / 1 / 2 / 4; justify-self: center; }
  /* wide enough for the two buttons side by side and no wider: every pixel
     past that would reach into a rack */
  .spell > .castbar { grid-area: 2 / 2 / 3 / 3; align-self: center;
    justify-self: center; width: 100%; max-width: var(--sig); }
  /* the racks span three rows, one of them sized by its own content: without
     this the sixty-shard min-content of a column is spread over those rows
     and stretches the cast bar's row to the length of the longest list */
  .spell .stage > .rack { align-items: stretch; min-height: 0; }
  /* the first line of the cast bar is only its small Cast Spell label; the
     shards line up with the Starmap and Atlas buttons under it, so the columns
     start one label lower than the bar's own top */
  .spell .stage > #rail-left  { grid-area: 2 / 1 / 5 / 2; margin-top: var(--cast-lab-h); }
  .spell .stage > #rail-right { grid-area: 2 / 3 / 5 / 4; margin-top: var(--cast-lab-h); }
  .spell .stage > .board { grid-area: 3 / 2 / 4 / 3; align-self: stretch; }
  .spell .stage > .genres {
    grid-area: 4 / 2 / 5 / 3; justify-self: center;
    width: 100%; max-width: var(--sig);
  }
  .spell > .foot { grid-area: 5 / 1 / 6 / 4; justify-self: center; }
  /* a column now fills the height of the racks rather than stopping at a third
     of it: the only thing that still scrolls is the list inside it */
  .spell .col { height: 100%; min-height: 0; }
  .spell .col-list { flex: 1 1 auto; min-height: 0; max-height: none; }
}

@media (max-width: 900px) {
  /* a narrow window has no free corner to hang the cards in, so they fold in
     under the heading instead - still their own short box, still their own
     scroll, and still nothing the circle has to make room for */
  .spell > .seals { position: static; top: auto; width: min(94%, 420px); max-height: 118px; }
  .seals-head { justify-content: center; }
  /* six columns beside one circle will not fit a narrow window: the two racks
     fold up above and below it, each one a row that scrolls sideways while
     every column keeps its own scroll */
  .spell .stage { flex-wrap: wrap; gap: 10px; }
  .spell .rack { flex: 1 1 100%; overflow-x: auto; padding-bottom: 4px; }
  .spell .genres .col-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .spell .col { flex: 0 0 132px; }
  .spell .col-list { max-height: 176px; }
  /* a narrower window means a narrower pill, and a pill is only as tall as its
     own line, so there is nothing for the smaller shard to shrink into but the
     name - which ellipsises on its own */
}