:root {
  --bg: #ffffff;
  --fg: #201f1e;
  --muted: #605e5c;
  --faint: #a8a5a2;       /* rare words: well back from --muted. Darkened a
                             tick from #b8b5b2, his ask 2026-08-14 ("tough to
                             see") -- still clearly recessive next to #605e5c,
                             just no longer at the 2.0:1 floor this file
                             already warns about twice. */
  --rule: #e1dfdd;
  --accent: #2b579a;      /* Word blue */
  --field: #faf9f8;
  --stretch: #7a51c2;     /* stretch rhymes: violet, nothing else uses it */
  /* The sound-options box (treatment H). Its own tokens rather than --field,
     because --field is what the WEBSITE paints the results panel with -- the
     two were the same colour, which is exactly why the box was invisible. */
  --fambox: #f3effb;
  --famedge: rgba(122, 81, 194, .22);
  --famcast: rgba(50, 20, 110, .13);
  --famcast2: rgba(50, 20, 110, .10);
  /* t=d slider. The track runs light -> dark left to right, to say that
     moving right ADDS. WIDENED 2026-08-13 on his ruling "make the gradient
     much more contrasting": it was #ddd4f0 -> #6b45ad, which read as one
     colour slightly deepening. Now it goes from almost-white to near-black
     violet, so the two ends are unmistakably different amounts. The hashmarks
     need to stay readable against BOTH ends of that range, which is why they
     get their own value rather than borrowing --rule. */
  --flaptrack0: #efeaf9;
  --flaptrack1: #35176e;
  --flaptick: #6b6579;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1f1f1f;
    --fg: #f3f2f1;
    --muted: #a19f9d;
    --faint: #6d6b69;   /* the same tick, mirrored for dark */
    --rule: #3b3a39;
    --accent: #8ab4f8;
    --field: #2b2b2b;
    --stretch: #c0a6f5;
    --fambox: #3a3348;
    --famedge: rgba(192, 166, 245, .30);
    --famcast: rgba(0, 0, 0, .55);
    --famcast2: rgba(0, 0, 0, .45);
    /* DIM -> BRIGHT here, not light -> dark. The point of the gradient is
       "more as you go right"; on a #3a3348 box a literally darker right-hand
       end would read as LESS, or vanish outright. Range widened to match the
       light theme's new contrast. */
    --flaptrack0: #423b56;
    --flaptrack1: #e5d8ff;
    --flaptick: #b3abc7;
  }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  /* WORD PANE FLOOR.
     304px was the MEASURED floor, his ruling 2026-08-15: wide enough that the
     count line -- "1078 perfect rhymes · 2313 total rhymes" plus "+ Add
     rhyme" -- never wraps and always keeps daylight between the numbers and
     the button. Worst case measured 291px at these font sizes, so 304 left
     real gap.

     RAISED AND PUT BACK, 2026-08-16. It was tried at 640 (the desktop
     column: "way too wide"), then 480 (the mobile page), then 400. Every one
     of those put the right-hand side of the pane off his screen, and the
     reason is the property itself:

       WORD DOES NOT REFLOW BELOW THIS NUMBER, IT SIDESCROLLS.

     A floor ABOVE the width his pane actually is makes the DOCUMENT wider
     than the pane. The page then hangs off to the right and has to be
     scrolled -- and what lives at that right edge is #rhpillwrap, so the
     symptom reads as "the Perfect/Slant pill is cut off". The pill is not
     cut off and never was: it is a fixed 174px pinned to the right of
     #rhbar, and a sweep from 280px up shows it fully laid out and inside
     the bar at every width. Raising this number is what pushed it out of
     view; lowering it is the only fix.

     So this is 304 again -- not a retreat, the measured answer. It is the
     smallest width that fits the whole search bar AND the pill AND keeps
     the count line on one line (291px worst case), which is precisely what
     "the minimum has to fit the entire thing" asks for. Word will not make
     a task pane narrower than this, so at 304 the sidescroll cannot happen.
     Do not raise it without re-reading this paragraph: the pane's DEFAULT
     width is whatever Word opens or he drags it to, and this floor does not
     set it. Widening the floor cannot widen the pane; it can only push the
     far side of the page out of sight.

     web.css lifts the floor again -- the website's own layouts manage their
     widths and a phone must never sidescroll. */
  min-width: 304px;
  background: var(--bg);
  color: var(--fg);
  font: 14px/1.45 -apple-system, "Segoe UI", system-ui, sans-serif;
}

body {
  display: flex;
  flex-direction: column;
  /* Anchor for #options, which opens upward from the footer button. */
  position: relative;
}

header {
  padding: 10px 12px;
  border-bottom: 1px solid var(--rule);
  flex: 0 0 auto;
}

/* The pane wordmark, centred above the search bar (see the markup). Hidden on
   the website, which builds its own on the scrolling band.

   Sized off the PANE, not off the asset: min() caps it at 168px so it never
   exceeds the art's own 210px width and starts to soften. The percentage is
   what makes it shrink with a dragged-narrow pane instead of crowding the
   gutters. */
#panemark {
  display: flex;
  justify-content: center;
  /* Bottom margin only: the header's own 10px is the space above. */
  margin: 0 0 9px;
}
#panemark img {
  width: min(56%, 168px);
  height: auto;
  display: block;
}
@media (prefers-color-scheme: dark) {
  /*
   * A brightness lift, NOT transparency. The mark is two-tone, "Rhyme" in
   * near-black navy and "Hamster" in green, so on a dark ground the navy
   * half vanishes and the logo reads as half-missing. Drop this only when
   * a properly reversed wordmark asset exists.
   */
  #panemark img { filter: brightness(1.9); }
}

#q {
  width: 100%;
  padding: 7px 9px;
  font: inherit;
  color: var(--fg);
  background: var(--field);
  border: 1px solid var(--rule);
  border-radius: 4px;
}
#q:focus {
  outline: none;
  border-color: var(--accent);
}

main {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 4px 12px 16px;
}

.headword {
  font-size: 17px;
  font-weight: 600;
  padding: 12px 0 2px;
}

.count {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  /* 12px, up from 8 -- the guaranteed daylight between the numbers and
     "+ Add rhyme", part of the same 2026-08-15 ruling as the min-width. */
  gap: 12px;
  color: var(--muted);
  font-size: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 4px;
}

/* Homograph switcher: 'tear' has two unrelated pronunciations, so each is a
   toggle rather than a section you have to scroll to find. */
.senses {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  margin: 8px 0 2px;
}

.sensecap {
  font-size: 10px;
  color: var(--muted);
  margin-right: 1px;
}

/* Add a rhyme. Deliberately quiet until used: a text-weight link rather than
   a button. Normally sits inline at the right of the count line (no margin
   needed there -- .count's own flex gap handles spacing); the no-rhymes case
   has no count line to share, so it needs its own top margin standing alone. */
.addopen {
  font: inherit;
  font-size: 10px;
  padding: 2px 0;
  margin-top: 4px;
  border: 0;
  background: none;
  color: var(--faint);
  cursor: pointer;
  /* A step quieter, his ruling 2026-08-15 -- in the Word pane (and on a
     phone) the count and this button sit nearly shoulder to shoulder, so it
     recedes. web.css restores full strength on the desktop site, where the
     space between them is what keeps it calm -- his own diagnosis. */
  opacity: .7;
}
.count .addopen { margin-top: 0; }

/* THE VOWEL-FAMILY ROWS: "on/yawn   Hide · Show · Merge", one per family in
   play on this word, under the rule and above the buttons they govern (his
   design (b)1, 2026-08-13). The segments themselves are .secseg/.secdot,
   borrowed whole from the perfect/total control so this arrives in a pattern
   he already reads rather than a new one.

   The family NAME is --muted at the count line's size: it is a label for a
   control, not a rhyme, and must not compete with the headword above it. */
/* RIGHT-JUSTIFIED ON THE BUTTON ROW, his ruling 2026-08-13: the controls sit
   on the same line as the homograph buttons they govern, at the far end.

   margin-left:auto is what pushes them, and it works BECAUSE .senses is a
   flex row -- the auto margin eats all the free space to the left. On a
   narrow pane .senses wraps and this box drops to its own line whole, which
   is why putting a control block inside a button row is safe at 320px: it
   never squeezes the buttons, it leaves. */
.fammodes {
  margin-left: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  /* TREATMENT H, his pick 2026-08-13: raised AND tinted -- a violet wash with
     a violet-tinted shadow, so the box reads as an object sitting ON the
     results rather than a region of them.

     WHY THE OLD TREATMENT FAILED, and it is worth writing down because I got
     it wrong first: the box was var(--field), and on the WEBSITE the results
     panel is ALSO var(--field) (web.css, `main`). Identical colours. There was
     no box at all -- only the 1px --rule border drawing a rectangle. The
     add-in differs: there `main` has no background, so the ground is --bg and
     the old fill was off by 1%. Any treatment here has to clear BOTH grounds,
     which is why this one carries a fill AND a shadow instead of relying on
     either alone. Measured on the running page, not reasoned about.

     No caption above it -- his ruling, same day. The shape is the whole
     signal now, so it has to be a shape you can actually see. */
  background: var(--fambox);
  border: 1px solid var(--famedge);
  border-radius: 6px;
  padding: 5px 9px 6px;
  box-shadow: 0 1px 2px var(--famcast),
              0 3px 9px var(--famcast2);
}
/* PROMINENCE, his ruling 2026-08-13: "this is a major feature of the site ...
   don't want them to overwhelm but don't want them to be missed."

   THIS BUMP IS DELIBERATELY HUE-FREE -- size, weight and contrast only. He is
   colour-blind, so a colour chosen by reasoning is a colour chosen blind; the
   candidates are in design/fam-emphasis-ideas.html for him to pick from by
   eye. Everything here works whether or not a colour ever lands on top:
   11px instead of 10, the family name at full --fg instead of --muted, and
   the unlit segments lifted off --muted so all three read as live controls
   rather than as a caption. */
.famrow {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 11px;
  color: var(--fg);
  padding: 0;
}
/* THE LABELS ARE GREY, his ruling 2026-08-13. They name the control; the
   violet now belongs entirely to the thing you click. */
.famname { font-style: italic; color: var(--muted); font-weight: 600; }
/* UNLIT SEGMENTS ARE THE LIT COLOUR, THINNER, his ruling the same day: the
   unlit ones used to be --muted, which made them read as caption text rather
   than as the two choices you have not taken. Same hue now, so all three are
   obviously one control; weight and lightness carry which one is on. That
   split matters more than usual here -- he is colour-blind, so if the ONLY
   difference were hue the control would have no state at all. */
.famrow .secseg { font-size: 11px; color: var(--stretch); font-weight: 400; opacity: .55; }
.famrow .secseg.on { color: var(--stretch); font-weight: 700; opacity: 1; }
.famrow .secseg:hover { opacity: .85; }
.famrow .secseg.on:hover { opacity: 1; }
.famrow .secdot { font-size: 12px; color: var(--stretch); opacity: .4; }
/* THE DESCRIPTOR IS OUT OF THE BOX ENTIRELY, his ruling 2026-08-13, and is
   now a tooltip UNDER the slider: "turn them into hoverboxes/tooltips you see
   below the slider when you hover over it (it shows you what the slider is
   currently on)."

   It went through two shapes before this one. First it sat to the LEFT of the
   t=d label, in the row; then it was faded out at rest and revealed on hover,
   still in the row. Both kept it in the layout, and it was still clutter. Out
   of flow entirely is the version that answers the actual complaint.

   position:absolute inside .flaplane, so it costs the row NO width and the
   t=d / pin-pen alignment is untouched. It hangs below the box on purpose --
   .fammodes does not clip, and there is nothing under the box to collide
   with. .descon keeps it up for a moment after a drag; see flashDesc(). */
.flaptip {
  position: absolute;
  top: calc(100% + 7px);
  right: 0;
  z-index: 5;
  white-space: nowrap;
  font-size: 10px;
  font-style: italic;
  line-height: 1.35;
  color: var(--bg);
  background: var(--fg);
  padding: 3px 7px;
  border-radius: 4px;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity .14s ease-out, visibility 0s linear .14s;
}
.flaplane:hover .flaptip,
.flapslider:focus-visible ~ .flaptip,
.fammodes.descon .flaptip {
  opacity: 1;
  visibility: visible;
  transition: opacity .1s ease-out, visibility 0s;
}
@media (prefers-reduced-motion: reduce) {
  .flaptip { transition: none; }
}

/* HIDE MODE's dead button. Not display:none -- he asked for the box to stay
   and go dim ("dim the box with ~yawn in it and make it unclickable"), which
   keeps the family visible as something that exists and is switched off. The
   cursor matters as much as the colour here: a button that looks clickable
   and does nothing is worse than one that says it is off. */
.sensetab.dimmed {
  opacity: 0.38;
  cursor: default;
  pointer-events: none;
}
/* His ruling 2026-08-13: hidden should "still be there, but much much less".
   Opacity alone left the word at full WEIGHT, so a dimmed tab still carried
   the boldest type in the row and pulled the eye first -- exactly backwards.
   Dropping the weight is what actually makes it recede. */
.sensetab.dimmed .sensetab-word { font-weight: 400; }
.addopen:hover { color: var(--accent); }
.addopen:disabled { opacity: .45; cursor: default; }
.addopen[hidden] { display: none; }

/* Expands in place rather than floating: the pane is narrow and resizable, so
   an anchored popover would spend its life being clipped at an edge. Always
   its own full-width block below the trigger's line, never inline. */
.addpanel {
  margin-top: 6px;
  padding: 8px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--field);
}
.addpanel[hidden] { display: none; }

.addrow {
  display: flex;
  gap: 6px;
  align-items: center;
}

.addword {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  padding: 4px 7px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--bg);
  color: var(--fg);
}
.addword:focus { outline: none; border-color: var(--accent); }

.addsyl {
  flex: 0 0 46px;
  font: inherit;
  font-size: 12px;
  padding: 4px 2px 4px 6px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--bg);
  color: var(--fg);
}

.addsave, .addcancel {
  flex: 0 0 auto;
  font: inherit;
  font-size: 11px;
  padding: 4px 9px;
  border-radius: 4px;
  border: 1px solid var(--rule);
  background: var(--bg);
  color: var(--fg);
  cursor: pointer;
}
.addsave {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}
.addsave:hover { opacity: .9; }
.addcancel:hover { border-color: var(--accent); }

/* The only guard against a typo: nothing checks that what you typed actually
   rhymes, so the family it will join has to be visible before you commit. */
.addpreview {
  font-size: 10px;
  line-height: 1.5;
  color: var(--muted);
  margin-top: 6px;
  min-height: 15px;
}

.sensetab {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  font: inherit;
  font-size: 12px;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--rule);
  background: var(--field);
  color: var(--fg);
  cursor: pointer;
  /* Drag-to-paint (2026-08-18, see taskpane.js): a finger dragging across
     the tabs must paint them, not scroll the page, and on touch that fight
     is decided by the START element's touch-action. The cost is that a
     scroll gesture BEGUN on a sense tab no longer scrolls -- a ~30px row
     that only homographs show, judged worth it. */
  touch-action: none;
}
.sensetab:hover { border-color: var(--accent); }

.sensetab.on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.sensetab-word { font-weight: 600; }

.sensetab-n {
  font-size: 10px;
  opacity: .65;
}

h2 {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 16px 0 6px;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;
  /* Multi-column keeps long -ight style lists scannable without scrolling.
     RESPONSIVE since 2026-08-08: was a hard `columns: 2`, so dragging the
     pane wider bought nothing but longer lines.

     His range, 2026-08-08: FLOOR OF TWO -- "never one column, ever" -- and
     a ceiling of SIX, with the third column arriving early ("I feel like we
     can get to 3 columns sooner").

     COUNT, not width, and that is the whole reason this is a stack of media
     queries instead of one `columns: 170px 5`. A column-width is a MINIMUM
     width, so the moment the pane is narrower than one such column the
     browser drops to a single column -- there is no way to say "at least two
     columns, however narrow they have to be" with column-width. Only an
     explicit column-count can hold a floor. The earlier version used
     `columns: 170px 5` and measured 1 column at 300px, which is exactly the
     thing he ruled out.

     The breakpoints are one number: a 140px column plus the 14px gap, so a
     new column is earned every 154px. Each step is the same size, which is
     what makes "sooner" cascade all the way up instead of just moving one
     line -- the third column now lands at 470px instead of 562, and every
     column after it arrives 92px earlier too.

     140px is a DENSITY CHOICE, and here is its cost. Measuring 2,600 entries
     in the bold face: 79px at the median, 129px at p90, 150px at p95, 188px
     at p99, and 263px for the longest in the dictionary ("transportation
     security administration"). Take off the ~18px the checkbox and its gap
     need and a 140px column leaves ~121px of text, which is a shade under
     p90 -- so roughly one entry in seven wraps to a second line. The old
     170px column left ~152px, about p95, so one in twenty. That is the whole
     trade: columns arrive sooner, and slightly more long phrases wrap. Widen
     the 154px step to undo it; narrow it to push further.

     The ceiling is about SCANNING, not fitting: lists run alphabetically
     DOWN each syllable bucket, so every extra column is more pane the eye
     has to cross to pick the thread back up, and a short bucket (three
     6-syllable phrases) spread wide reads as a broken row of white space.

     Paragraph view is unaffected -- `#out.para ul` sets `columns: auto` at
     id specificity, which outranks every rule here however late it sits. */
  columns: 2;
  column-gap: 14px;
}

/* 24px of pane padding + n columns of 140px + (n-1) gaps of 14px. */
@media (min-width: 470px) { ul { columns: 3; } }
@media (min-width: 624px) { ul { columns: 4; } }
@media (min-width: 778px) { ul { columns: 5; } }
@media (min-width: 932px) { ul { columns: 6; } }

li {
  break-inside: avoid;
  padding: 1px 0;
  display: flex;
  align-items: baseline;
  gap: 6px;
}

input.pick {
  flex: 0 0 auto;
  margin: 0;
  /* Shrink the native control so a column of them reads as a quiet margin
     rather than competing with the words themselves. */
  transform: scale(0.85);
  cursor: pointer;
}

li .w {
  cursor: pointer;
  min-width: 0;
  overflow-wrap: anywhere;
  /* Clicking a word toggles its checkbox, so suppress the text selection a
     click or double-click would otherwise leave behind. */
  -webkit-user-select: none;
  user-select: none;
}
li .w:hover { text-decoration: underline; }

/* The user's core requirement: common words carry visual weight. */
li.common { font-weight: 700; }

/* Rare words are de-emphasised rather than hidden -- a hint, not a filter.
   --faint is considerably lighter than --muted so they recede on a fast scan
   while staying legible when looked at directly. */
/* ITALIC REMOVED 2026-08-13, his ruling, when italic became the mark of a
   SLANT rhyme. A rare slant word is faded and italic; if plain rare words
   kept their italic too, the two would be pixel-identical and the fade would
   be carrying a distinction it cannot carry. The fade stays and is now doing
   the whole job on its own -- a replacement treatment for rare words is on
   the open list. */
li.rare .w {
  color: var(--faint);
}

/* Slant rhymes. They are interleaved alphabetically with everything else and
   wear no colour, no section and no rule -- italic is the only mark, because
   italics are slanted. Deliberately NOT distinguished from the invented-word
   violet italics by shape: his ruling was that violet alone may separate
   them. */
li.slant .w {
  font-style: italic;
}

/* Secondary-stress rows carried a colour here (li.sec / li.rare.sec) from
   2026-08-10 until later the same day. REMOVED on his call -- "let's get rid
   of the teal and go back to black. (I don't need them in color any more.)"
   -- once the count-line segments made per-row marking redundant.

   Worth knowing before anyone reaches for a colour here again: it went clay
   #a04b3c -> teal -> amber -> teal across four rounds, because Jeremy is
   COLOUR-BLIND and the first three were chosen without him ever seeing them
   render. Any colour decision in this pane must be shown to him as an IMAGE,
   never as a page that draws itself with scripts. The rest of that history is
   in data/SECONDARY-STRESS-RULINGS.txt. */

/* The count-line segments: "170 perfect rhymes · 257 total rhymes".
   Buttons, but dressed as the text they replace -- the count line is
   metadata, and a pair of real-looking buttons there would outweigh the
   headword directly above it.

   The INACTIVE half keeps the line's own --muted; only the ACTIVE half moves,
   to --fg and semibold. Dimming the inactive one instead would land on
   --faint, which measures 2.0:1 against white (4.5:1 is the floor) and is
   unreadable at 10px. The unlit sense tab solves it the same way: it keeps
   its text colour and drops its fill. */
/* "287 perfect rhymes" with the promise revoked. 1px explicitly, because the
   count line is 10px and the browser default thickness derives from the font
   size -- at this size it renders as a hairline that is easy to miss, which
   is the one way this control could fail. */
.count .strike {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.seccount { display: inline-flex; align-items: baseline; gap: 5px; white-space: nowrap; }
.secseg {
  font: inherit;
  font-size: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.secseg.on { color: var(--fg); font-weight: 600; }

/* The brackets around the slant segment. Plain text, in the count line's own
   grey, so the clickable part is the words and not the punctuation. */
.secparen { color: var(--muted); }

/* "+ Add rhyme" on its own line under the rule, left-aligned with the words.
   It used to ride the end of the count line; that line now carries a third
   segment and there was no room left on it. */
/* .addrow retired 2026-08-14 -- "+ Add rhyme" went back onto the count line
   when the "(including...)" segment was cut and the room came back. */

/* THE PERFECT-ER ROWS. One layout for all five: checkbox, "Never perfect:",
   family in italics. Flex so the checkbox column is a real column, and the
   prefix gets an explicit width so every <em> starts at the same x even if
   the font metrics ever shift. The t=d row is a <div> (its slider must not
   live inside the label -- see the note in the HTML), so it needs the same
   8px lead-in the labels get from #options .loosebody > label. */
#options .loosebody > label,
#options .loosebody > .fampanelrow {
  display: flex;
  align-items: center;
}
#options .loosebody > .fampanelrow { padding-left: 8px; }
#options .loosebody .npfx { flex: 0 0 auto; width: 92px; margin-left: 6px; }
#options .loosebody label em { font-style: italic; }
.fampanelrow.neverrow > label { display: flex; align-items: center; flex: 0 0 auto; }
.secseg:hover { color: var(--fg); }
.secseg.on:hover { cursor: default; }
/* The separator was --faint and 10px, and Jeremy could not see it at all
   ("I can't see the dot") -- which is the same 2.0:1 contrast failure this
   file warns about two rules up, walked into anyway. A middle dot is a tiny
   glyph to begin with, so it gets BOTH fixes: --muted, the same colour as the
   unlit segment either side of it, and a size of its own, larger than the
   10px line it sits on. If it still reads weak, the next step is a pipe --
   more ink than a dot at any size, and his own first instinct ("primary
   only | all"). */
.secdot {
  color: var(--muted);
  font-size: 13px;
  line-height: 1;
}

/* ---------- paragraph view (2026-08-07) ---------- */

/* The comma-run alternative to the columns, toggled by #viewbtn. PURE CSS
   over the SAME DOM: the checkboxes, labels, block/right-click wiring,
   common/rare styling and the gather machinery all keep working untouched,
   because nothing about the markup changes -- the list is simply reflowed
   as justified running text with generated commas. Scoped to #out.para, so
   it covers every list the pane renders now or later: sections, stretch
   shelves, all of it. Syllable headers stay block, so each count is still
   its own paragraph, exactly his spec. */
#out.para ul {
  columns: auto;
  column-gap: 0;
  text-align: justify;
  /* A justified last line stretches its handful of words wall to wall,
     which reads as a rendering bug; every real paragraph ends ragged. */
  text-align-last: left;
  line-height: 1.65;   /* was 1.75; "EVER SO SLIGHTLY LESS", his call */
}
#out.para li {
  display: inline;
  padding: 0;
  /* nowrap ON THE LI, normal on its ::after: the pair that keeps a word and
     its own comma on one line. Without it the g-dropped and stretch entries
     shed orphan commas ("noon-y" / ", poon-y") -- their hyphens and
     apostrophes hand WebKit break opportunities right at the word's end,
     and overflow-wrap:normal on the label was tried first and did NOT fix
     it. nowrap removes every seam inside the entry; the ::after going back
     to normal restores the one legitimate break, after the comma's space.
     Cost: a long PHRASE can no longer wrap mid-phrase, which at 320px is
     fine for everything in the index and the right reading anyway. */
  white-space: nowrap;
}
/* THE COMMA IS NOWRAP TOO, and the SPACE moved off it (2026-08-11).
   Jeremy: "sometimes the commas between STRETCH WORDS end up not attaching to
   the word on a line, but being orphaned alone, starting the next line...
   look up cutesy and open stretch rhymes, and slowly widen the pane."

   The previous version put ", " on the ::after with white-space: normal,
   reasoning that the li's nowrap held the word together while the ::after
   restored the one legitimate break after the comma's space. It holds in
   Chromium -- swept 320px to 760px in 2px steps, not one orphan -- but Word
   is WebKit, which is exactly the engine this rule's own comment warns about
   for hyphens, and every stretch word is hyphenated (boots-y, chutes-y).

   So the break is no longer left to the engine's judgement. The comma is
   nowrap and glued to the word; the SPACE is a separate ::before on the
   FOLLOWING item, which is where the break is wanted and the only place it
   can now happen. An orphan comma is not merely unlikely -- there is no
   longer a wrap opportunity in front of one. */
#out.para li::after { content: ","; white-space: nowrap; }
#out.para li + li::before { content: " "; white-space: normal; }
#out.para ul li:last-child::after { content: ""; }
/* The checkbox stays in the DOM -- the label's for= still toggles it and
   the change listener still fires -- but its BOX leaves the layout: a grid
   of naked checkboxes threaded through running text reads as noise. The
   tick's job passes to the highlight below. */
#out.para input.pick {
  position: absolute;
  width: 0;
  height: 0;
  opacity: 0;
  pointer-events: none;
}
/* The gathered mark: a solid accent pill, the same statement the lit sense
   tab makes ("this one is active"), so no new colour language. var(--bg)
   for the text rather than white, so dark mode's pale-blue accent gets dark
   text and stays legible. */
#out.para input.pick:checked + .w {
  background: var(--accent);
  color: var(--bg);
  border-radius: 3px;
  padding: 0 3px;
}
/* The pill's own colour must win over the rare-word grey; italic survives,
   which keeps the rare hint readable even while gathered. */
#out.para li.rare input.pick:checked + .w { color: var(--bg); }

/* ---------- stretch rhymes ---------- */

/* The offer line: as quiet as "+ Add rhyme", but in the stretch colour so the
   invitation and what it produces share an identity. The hint carries a few
   examples in small text before you commit (the spec). */
.stretchbar {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-top: 16px;
  min-width: 0;
}
.stretchopen {
  flex: 0 0 auto;
  font: inherit;
  font-size: 10px;
  padding: 2px 0;
  border: 0;
  background: none;
  color: var(--stretch);
  opacity: .75;
  cursor: pointer;
}
.stretchopen:hover { opacity: 1; text-decoration: underline; }
.stretchhint {
  font-size: 10px;
  font-style: italic;
  color: var(--faint);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The stretch, literally. The section grows DOWN from the button (hence
   transform-origin: top), overshoots past its real height so the words pull
   thin and tall, squashes back past the mark, and settles -- taffy, not a
   drawer sliding open.
   Two separate animations rather than one, because they need different
   origins: the block scales from its top edge while each WORD squashes about
   its own middle. Running both makes the type visibly distort during the
   pull instead of just moving, which is the whole point of the gag.
   scaleY on text is cheap (compositor-only) and the whole thing is over in
   ~0.6s, so it reads as a flourish rather than a wait. */
/* THE ORIGINAL curve, restored on request -- these are the values from the
   first version, not the "much more aggressive" one that followed. The two
   were tried in both directions and this is the keeper. */
@keyframes stretchdown {
  0%   { transform: scaleY(0.04); opacity: 0; }
  45%  { transform: scaleY(1.22); opacity: 1; }
  64%  { transform: scaleY(0.90); }
  80%  { transform: scaleY(1.06); }
  92%  { transform: scaleY(0.98); }
  100% { transform: scaleY(1); }
}
/* The word scales MULTIPLY with the block's, which is where the published
   numbers come from: the squish at 64% is 0.90 x 0.78 = 0.70, exactly the
   floor Jeremy asked for.
   scaleX is the addition to the original: the type is squeezed to 0.66 of
   its width at full extension and bulges to 1.08 when it lands, so it thins
   as it lengthens and fattens as it compresses. Without it the words only
   got BIGGER; with it they read as actually stretched. */
@keyframes stretchword {
  0%   { transform: scale(0.60, 2.4)  translateY(-0.18em); opacity: 0; }
  45%  { transform: scale(0.66, 1.35) translateY(-0.04em); opacity: 1; }
  64%  { transform: scale(1.08, 0.78) translateY(0.02em); }
  80%  { transform: scale(0.94, 1.12); }
  92%  { transform: scale(1.02, 0.97); }
  100% { transform: scale(1, 1); }
}
.stretch {
  transform-origin: top center;
  /* linear between keyframes: the wobble lives in the VALUES, and an easing
     curve on top of them smears the snap into a glide. */
  animation: stretchdown 650ms linear both;
}
/* Jeremy's report, 2026-08-09: in WORD the stretch lists open as ONE column
   and snap to two "after a beat" -- the beat being exactly this 650ms
   animation. Not reproducible in the Chromium preview (measured 2 columns
   at t=0/200/500/900ms), which points at Word's WebKit webview: multicol
   inside an ancestor with a running transform animation is a known WebKit
   layout deferral. translateZ(0) gives the list its own compositing layer,
   the standard fix for that class. Harmless everywhere else. CONFIRMED
   FIXED IN WORD by Jeremy the same day -- so this line is load-bearing:
   remove it and the one-column flash comes back in the real host, while
   every preview browser keeps looking fine. */
.stretch ul { transform: translateZ(0); }
.stretch li .w {
  display: inline-block;   /* scaleY needs a block box to act on */
  transform-origin: center;
  animation: stretchword 650ms linear both;
}
/* The headings pull apart hardest -- they have the furthest to travel back,
   so they read as the thing being stretched. */
/* The section headings ride the SAME animation as the words, not one of their
   own. They used to pull their letter-spacing apart, which was a different
   trick on a different property -- and that was exactly the problem: it read
   as a second animation running alongside, breaking the illusion that the
   whole section is one sheet of taffy. Now everything in here scales.
   inline-block is what makes it work: a full-width block would scale about
   the centre of the PANE and slide the left-aligned text inward, so the box
   has to shrink to its own text first, exactly like a word does. */
.stretch h2 {
  display: inline-block;
  transform-origin: center;
  animation: stretchword 650ms linear both;
}

/* Closing: a window shade snapping back up. Deliberately NOT the open
   animation reversed -- the open is taffy and wobbles, and a wobble on the way
   out reads as indecision. This one is short (230ms) and ease-in, so it
   accelerates into the roll and is gone. The words squash FLAT (scaleY down,
   scaleX slightly out) rather than pulling thin, because that is what a sheet
   being rolled up does to what is printed on it. */
@keyframes stretchup {
  0%   { transform: scaleY(1); opacity: 1; }
  22%  { transform: scaleY(1.04); opacity: 1; }
  100% { transform: scaleY(0.02); opacity: 0; }
}
@keyframes stretchwordup {
  0%   { transform: scale(1, 1); opacity: 1; }
  22%  { transform: scale(0.98, 1.05); opacity: 1; }
  100% { transform: scale(1.14, 0.10); opacity: 0; }
}
/* .zip wins over the opening animation on the same elements purely by coming
   later in this file -- same specificity, so source order decides. */
.stretch.zip {
  animation: stretchup 230ms ease-in both;
  pointer-events: none;      /* no clicking a row that is on its way out */
}
.stretch.zip li .w,
.stretch.zip h2 { animation: stretchwordup 230ms ease-in both; }

/* Hovering the trigger previews the joke: the label pulls wide, so the
   control demonstrates what it does before you commit to it. */
.stretchopen { transition: letter-spacing 180ms ease, opacity 180ms ease; }
.stretchopen:hover { letter-spacing: .08em; }

/* Motion is decoration here; anyone who has asked the OS to stop moving
   things gets the list with no theatre at all. */
@media (prefers-reduced-motion: reduce) {
  .stretch,
  .stretch li .w,
  .stretch h2,
  .stretch.zip,
  .stretch.zip li .w,
  .stretch.zip h2 { animation: none; }
  .stretchopen { transition: none; }
  .stretchopen:hover { letter-spacing: normal; }
}

/* The section itself: violet italics throughout, so an invented word can
   never be mistaken for dictionary vocabulary even out of context. */
.stretchcap {
  font-size: 10px;
  font-style: italic;
  color: var(--stretch);
  opacity: .7;
  margin-top: 2px;
}
.stretch h2 { color: var(--stretch); opacity: .75; }
.stretch li .w {
  color: var(--stretch);
  font-style: italic;
}

.status {
  color: var(--muted);
  padding: 18px 0;
}

.status strong { color: var(--fg); }

/* The startup word count -- greyed past --muted, smaller, italic, on his
   ruling 2026-08-06. --faint is the rare-word grey, so it recedes the same
   way obscure words do. */
/* The random greeting, 2026-08-07. Sits at --muted: it is the line that says
   the pane is alive, so it carries more than the count under it, but it is
   still not a heading and must not read as one. */
.status .greeting {
  display: block;
  color: var(--muted);
}

.status .wordcount {
  display: block;
  margin-top: 3px;
  color: var(--faint);
  font-size: 0.85em;
  font-style: italic;
}

/* The startup hamster, 2026-08-07. A WATERMARK, not a splash illustration:
   it sits well back so the pane still reads as empty-and-waiting. Scoped to
   #out.startup, a class taskpane.js adds only after the word-count line and
   strips in both status() and render(), so it can never end up behind a word
   list or an error message.

   Drawn on a pseudo-element rather than as background-image on #out itself,
   because a background-image cannot be faded. The other way to fade one is a
   translucent white gradient stacked over it -- which is wrong here: this
   pane has a real dark mode, and a white wash inverts into a light blob the
   moment it goes dark. Same reasoning as the gear icon's punched-out hub.

   Sized by the BOX with background-size: contain, not by the image, so the
   art keeps its aspect ratio and can neither overflow a narrow pane nor grow
   silly on a tall one. */
#out.startup {
  position: relative;
}

#out.startup::before {
  content: "";
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 15%;
  /* The calc is a fossil of the lockup era: 18% was the wordmark's band and
     44px its height plus a 10px gap. The wordmark is gone (2026-08-18 ruling,
     see below), but the hamster deliberately stays where the lockup put him
     -- dropping him into the vacated band would move the art on a screen
     nobody complained about. If his position ever needs retuning, this can
     become a plain percentage. */
  bottom: calc(18% + 44px);
  /* 237px, not 300: assets/hamster.png is cropped to its alpha bounds now, so
     the box width IS the drawn width. The old 300 was sized against a file
     carrying ~21% transparent margin. Same hamster on screen, honest number. */
  width: min(76%, 237px);
  /* Anchored BOTTOM, not centre: contain leaves slack on whichever axis is
     not limiting, and centring splits that slack top and bottom, so the gap
     under him drifted with pane shape. Pinning the image's bottom edge to the
     box's keeps the tuck constant. */
  background: url("assets/hamster.png") no-repeat center bottom;
  background-size: contain;
  /* FULL STRENGTH IN THE WORD PANE, his ruling 2026-08-15. The watermark
     reading was a website idea -- in the pane the front door is just the
     hamster, and at 46% he looked washed out rather than quiet. web.css
     restores the website's own values, so only Word changes. */
  opacity: 1;
  pointer-events: none;
}

/* THE LOCKUP WORDMARK IS GONE, his ruling 2026-08-18: "drop the startup
   screen word mark underneath the hamster on web, desktop and wordpane."
   From 2026-08-16 the startup screen carried two marks -- the pane wordmark
   above the search bar and a second one under the hamster as half of one
   lockup, drawn here as #out.startup::after. With the pane mark settled,
   two marks on one screen was exactly the thing the 2026-08-11 note warned
   about, and the lower one lost. The front door is the pane mark plus the
   hamster, nothing else. */

/* The absolutely-positioned ::before paints AFTER in-flow content in the same
   stacking context, so without this the watermark would lie on top of the
   word count rather than behind it. Lifting the text is more predictable than
   pushing the pseudo-element to z-index -1, which depends on which ancestor
   happens to establish a stacking context. */
#out.startup .status {
  position: relative;
  z-index: 1;
}

#confirm {
  flex: 0 0 auto;
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 8px 12px;
  border-top: 1px solid var(--rule);
  background: var(--field);
  font-size: 12px;
}
#confirm[hidden] { display: none; }

#confirmtext {
  flex: 1 1 auto;
  min-width: 0;
}

/* Three buttons will not sit beside the question in a 320px pane, so the
   stretch variant wraps: question on its own row, actions underneath. */
#confirm.stacked { flex-wrap: wrap; }
#confirm.stacked #confirmtext { flex: 1 1 100%; }
#confirmpat[hidden] { display: none; }

#confirm button,
#undoblock {
  flex: 0 0 auto;
  font: inherit;
  font-size: 11px;
  padding: 4px 10px;
  border-radius: 4px;
  border: 1px solid var(--rule);
  background: var(--bg);
  color: var(--fg);
  cursor: pointer;
}

#confirmyes {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}
#confirmyes:hover { opacity: .9; }

#undoblock[hidden] { display: none; }
#undoblock:hover { border-color: var(--accent); }

#manual {
  flex: 0 0 auto;
  display: flex;
  gap: 6px;
  align-items: flex-start;
  padding: 8px 12px;
  border-top: 1px solid var(--rule);
  background: var(--field);
}
#manual[hidden] { display: none; }

#manualtext {
  flex: 1 1 auto;
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 4px;
  padding: 5px 7px;
  resize: none;
}

#manualclose {
  flex: 0 0 auto;
  font: inherit;
  font-size: 11px;
  padding: 4px 10px;
  border-radius: 4px;
  border: 1px solid var(--rule);
  background: var(--bg);
  color: var(--fg);
  cursor: pointer;
}

footer {
  flex: 0 0 auto;
  border-top: 1px solid var(--rule);
  padding: 6px 12px;
  font-size: 11px;
  color: var(--muted);
  min-height: 30px;
  display: flex;
  align-items: center;
  /* flex-start + the auto margin on #footmsg below, not space-between --
     his ruling 2026-08-15: the gathered-words tray (count, Copy, clear,
     block) sits at the LOWER RIGHT on every surface. space-between spread
     the six children evenly, which happened to look right on a wide web
     window and parked the tray mid-footer in the Word pane and on phones. */
  justify-content: flex-start;
  gap: 8px;
}
/* the seam: everything before this hugs the left, everything after it -- the
   word count, Undo, and the tray -- rides the right edge */
#footmsg { margin-left: auto; }

/* The one always-visible control. Quiet like the old select it replaces --
   a footer affordance, not a form field. */
/* The two footer icon buttons share one box so they line up exactly: same
   square, same border, and their contents CENTRED by flex rather than left to
   text metrics. Doing it by font-size and padding never worked -- a glyph's
   ink sits wherever its own baseline puts it, so the gear and the block sign
   drifted apart vertically no matter how the numbers were tuned. */
#optbtn,
#blockbtn,
#viewbtn,
#dblbtn {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  line-height: 1;
  color: var(--muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
#optbtn:hover,
#blockbtn:hover,
#viewbtn:hover,
#dblbtn:hover { border-color: var(--rule); color: var(--fg); }
#optbtn[aria-expanded="true"],
#blockbtn[aria-expanded="true"] {
  border-color: var(--rule);
  color: var(--fg);
  background: var(--field);
}

#optbtn {
  /* Pulls the PAIR past the footer's own 12px left padding, tucking them into
     the pane's actual corner instead of sitting indented like a text label. */
  margin-left: -8px;
}
#blockbtn {
  /* Set slightly further off the gear than a tight pair would be: the gear is
     solid and the block sign is an outline, so a little air stops the lighter
     shape from reading as part of the heavier one. */
  margin-left: -1px;
}
#viewbtn {
  margin-left: -1px;
  /* margin-right:auto lived here while these three were the whole left group.
     #dblbtn joined the group on 2026-08-16 and is now last in it, so the seam
     moved onto it -- left here, it would have packed the trio and then thrown
     #dblbtn to the far side of the gap it was being moved out of. */
}
/* The gear is the everyday control and is filled, so it carries the darker
   colour; the block sign is an outline and sits back further still, so the
   pair reads as one primary control and one you only notice when looking for
   it. Both come up to full strength on hover, so neither feels disabled. */
#optbtn { color: var(--fg); opacity: .8; }
#optbtn:hover,
#optbtn[aria-expanded="true"] { opacity: 1; }

#blockbtn { opacity: .55; }
#blockbtn:hover,
#blockbtn[aria-expanded="true"] { opacity: 1; }

#viewbtn { opacity: .55; }
#viewbtn:hover { opacity: 1; }
/* Play/pause logic: show only the icon of the view a click would bring.
   Column view (default) offers the paragraph; .para offers the list. More
   specific than the shared `> svg { display:block }` below, so it wins. */
#viewbtn .ico-list { display: none; }
#viewbtn.para .ico-para { display: none; }
#viewbtn.para .ico-list { display: block; }

/* BOTH icons are drawn SVGs on the same 24-unit grid at the same stroke
   weight, which is the only way they line up exactly and stay that way.
   The gear was a text glyph (U+2699) first, and it could not: its ink is
   ENTIRELY above the baseline (measured -- ascent 14.5, descent 0 at 24px),
   so flex centres the line box while the ink rides up out of the middle,
   and it renders smaller than its point size suggests. No amount of
   line-height or nudging fixes a shape whose metrics lie; drawing it does. */
#optbtn > svg,
#blockbtn > svg,
#viewbtn > svg,
#dblbtn > svg {
  width: 17px;
  height: 17px;
  display: block;          /* no inline baseline gap under the icon */
}
#optcaret { display: inline-block; }
#optbtn[aria-expanded="true"] #optcaret { transform: rotate(180deg); }

/* Same anchoring as #options: upward from the footer, pinned left so it can
   never spill off a 320px pane. Capped in height because a long blocklist
   would otherwise run off the top of the pane. */
#blockpanel {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 34px;
  z-index: 20;
  max-height: 60vh;
  display: flex;
  flex-direction: column;
  padding: 8px 10px;
  font-size: 12px;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 6px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .18);
}
#blockpanel[hidden] { display: none; }
#blockhead {
  font-size: 11px;
  opacity: .45;
  padding-bottom: 5px;
  margin-bottom: 5px;
  border-bottom: 1px solid var(--rule);
}
#blocklist {
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
/* The whole row is the restore control, not just the pill on the right --
   so hovering anywhere on the line lights the button AND underlines the word,
   and a click anywhere does the same thing. Two signifiers for one action,
   which is what makes clicking the word feel like an option rather than a
   guess. */
.blockrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 2px 4px;
  margin: 0 -4px;
  border-radius: 3px;
  cursor: pointer;
}
.blockrow:hover { background: var(--field); }
.blockword { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A hidden PATTERN wears the stretch colour and italics, the same dress its
   rows wore -- so the panel shows at a glance which entries are one word and
   which are a whole class. */
.blockpat { color: var(--stretch); font-style: italic; }
/* The empty state is a hint, not a row: no hover, no pointer, and set back so
   it reads as instruction rather than as a word you could restore. */
.blockempty {
  padding: 2px 0;
  color: var(--muted);
  font-style: italic;
}
.blockrow:hover .blockword { text-decoration: underline; color: var(--accent); }
.blockundo {
  flex: 0 0 auto;
  font: inherit;
  font-size: 11px;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0 6px;
  cursor: pointer;
}
/* Lit from the ROW's hover, so pointing at the word activates the button. */
.blockrow:hover .blockundo {
  border-color: var(--accent);
  color: var(--accent);
}

/* Opens UPWARD from the footer, overlaying the list rather than pushing it.
   Anchored to the pane's bottom-left so it can never spill off a 320px pane. */
#options {
  position: absolute;
  left: 8px;
  bottom: 34px;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 10px;
  font-size: 12px;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 6px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .18);
}
#options[hidden] { display: none; }

/* ---------- the popups' stretch/zip, borrowed from the stretch section ----------

   Same graphic language as the rhyme shelf, deliberately dialled DOWN: this is
   chrome you open twenty times an hour, not a flourish you meet once a lookup.
   So it is 240ms against the shelf's 650, and the overshoot is 1.06 rather
   than 1.22 -- present enough to read as taffy, small enough never to feel
   like a wait. Jeremy: "really quick, but a subtle stretch."

   Both popups hang off the footer and open UPWARD, so the origin is the
   bottom-left corner -- the panel unfurls out of the button that spawned it
   instead of growing from its own middle. */
#options,
#blockpanel { transform-origin: bottom left; }

@keyframes popstretch {
  0%   { transform: scaleY(0.06); opacity: 0; }
  55%  { transform: scaleY(1.06); opacity: 1; }
  78%  { transform: scaleY(0.97); }
  100% { transform: scaleY(1); }
}
/* The rows counter-stretch, and the scales MULTIPLY with the panel's exactly
   as they do on the shelf -- that product is what sells it as one sheet being
   pulled rather than a box that merely grew. scaleY only: a scaleX here would
   distort the checkboxes, which are the one thing in this panel that has to
   look like a system control. */
@keyframes popstretchrow {
  0%   { transform: scaleY(2.2); opacity: 0; }
  55%  { transform: scaleY(1.08); opacity: 1; }
  78%  { transform: scaleY(0.98); }
  100% { transform: scaleY(1); }
}
#options:not([hidden]),
#blockpanel:not([hidden]) { animation: popstretch 240ms linear both; }
/* .popin is worn only for the 240ms of the opening, NOT for as long as the
   panel is up. The blocked-words panel rebuilds its rows in place every time
   you restore something, and a `:not([hidden]) > *` rule would re-run the
   whole stretch on each of those -- the list would twitch every click. */
#options.popin > *,
#blockpanel.popin > * {
  transform-origin: center;
  animation: popstretchrow 240ms linear both;
}

/* Closing: the shade snaps back, same as .zip on the shelf -- short, ease-in,
   no wobble. A wobble on the way out reads as indecision. */
@keyframes popzip {
  0%   { transform: scaleY(1); opacity: 1; }
  20%  { transform: scaleY(1.03); opacity: 1; }
  100% { transform: scaleY(0.02); opacity: 0; }
}
@keyframes popziprow {
  0%   { transform: scaleY(1); opacity: 1; }
  100% { transform: scaleY(0.12); opacity: 0; }
}
/* Wins over the opening rules above purely by coming later -- same
   specificity, so source order decides. Same trick as .stretch.zip. */
#options.popzip,
#blockpanel.popzip {
  animation: popzip 150ms ease-in both;
  pointer-events: none;      /* no clicking a toggle on its way out */
}
#options.popzip > *,
#blockpanel.popzip > * { animation: popziprow 150ms ease-in both; }

@media (prefers-reduced-motion: reduce) {
  #options:not([hidden]),
  #blockpanel:not([hidden]),
  #options.popin > *,
  #blockpanel.popin > *,
  #options.popzip,
  #blockpanel.popzip,
  #options.popzip > *,
  #blockpanel.popzip > * { animation: none; }
}

/* The "Word families" and "Exceptions" subgroups. Collapsed by default: the
   vocabulary switches are set-and-forget and the exceptions are rare, so both
   take a deliberate click to reach.

   The rule SEPARATES the groups, so the first one must not draw it. It used
   to have Hide identities sitting above it, which is what the border was
   dividing it from; that switch moved inside Word families on 2026-08-14 and
   left the line hanging at the top of the panel with nothing above it. */
#familygroup,
#loosegroup {
  margin-top: 11px;
  padding-top: 8px;
  border-top: 1px solid var(--rule);
}
#options > details:first-of-type {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}
/* Gap between the disclosure arrow and the word, so the label sits clear of
   the triangle rather than butting against it. */
#familygroup > summary > span,
#loosegroup > summary > span { padding-left: 6px; }
#familygroup > summary,
#loosegroup > summary {
  cursor: pointer;
  user-select: none;
  opacity: .45;
  font-size: 11px;
  letter-spacing: .02em;
  padding: 1px 0;
}
#familygroup > summary:hover,
#loosegroup > summary:hover { opacity: 1; color: var(--accent); }
#familygroup[open] > summary,
#loosegroup[open] > summary { margin-bottom: 3px; }
/* Indent the members so the group reads as subordinate to the four above.
   Selector goes through #options because that is what sets the padding on
   every label in the popup: once .loosebody stopped these being direct
   children of #loosegroup, the old `#loosegroup > label` lost and the whole
   group un-indented. Matching #options' specificity and then adding a class
   wins cleanly. */
#options .loosebody > label { padding-left: 8px; }

/* The disclosure now wears EXACTLY the panel's taffy, by scaling .loosebody
   the way #options scales itself -- same keyframes, same 240/150ms, so the
   two gestures are literally the same animation at two scales.

   The first attempt animated the labels alone and left the box snapping to
   full height around them. It technically ran (the classes and computed
   styles were all correct, which is how it passed inspection) and still
   looked like nothing was happening, because the eye tracks the BOX edge,
   not the text inside it. Animate the thing that changes size.

   Origin is top: a <details> grows downward out of its summary, the mirror
   of the footer popups' bottom-left. */
#familygroup > .loosebody,
#loosegroup > .loosebody { transform-origin: top center; }
#familygroup.popin > .loosebody,
#loosegroup.popin > .loosebody { animation: popstretch 240ms linear both; }
#familygroup.popin > .loosebody > label,
#loosegroup.popin > .loosebody > label {
  transform-origin: center;
  animation: popstretchrow 240ms linear both;
}
/* Closing gets its OWN keyframe rather than borrowing the panel's popzip.
   popzip is a shade snapping up: 150ms, straight down, no character. On the
   footer panels that is right, because the whole box vanishes and the eye has
   plenty to watch. Here the box is four rows deep inside a panel that stays
   put, so the same curve read as an instant cut -- Jeremy: "when you close it
   there's no stretch animation involved."

   So this one PULLS first (1.12 at 22%) and only then collapses, over 200ms:
   the taffy stretching thin before it goes, which is the same gesture as the
   opening rather than its opposite. */
/* There is deliberately NO closing animation for this group -- see the click
   handler in taskpane.js. Three were tried and all read worse than letting
   <details> just shut. Do not add a fourth without being asked. */

@media (prefers-reduced-motion: reduce) {
  #loosegroup.popin > .loosebody,
  #familygroup.popin > .loosebody > label,
#loosegroup.popin > .loosebody > label { animation: none; }
}

/* The t/d slider row. Kept narrow -- it shares the popup with checkboxes and
   should read as one more line, not a control panel. */
.sliderrow input[type="range"] {
  width: 64px;
  margin: 0 4px;
}
/* Off state: the checkbox says off, so the slider just dims and holds its
   remembered stop rather than vanishing or jumping. */
.sliderrow input[type="range"]:disabled { opacity: .35; }
#flapdesc {
  opacity: .6;
  font-size: 11px;
  white-space: nowrap;
}

/* A "loose" pronunciation button -- one that exists only because a Looser
   rhymes switch is on. Dashed instead of solid, so an offered approximation
   never dresses like a genuine homograph reading; the label's tilde is added
   in taskpane.js. */
.sensetab.loose { border-style: dashed; }
.sensetab.loose.on { border-style: dashed; border-color: var(--rule); }
#options label {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  padding: 2px 0;
}
#options label:hover { color: var(--accent); }
#options input[type="checkbox"] {
  margin: 0;
  width: 13px;
  height: 13px;
  cursor: pointer;
  accent-color: var(--accent);
}

/* The status message is the part that can be sacrificed when the footer is
   tight, so it truncates instead of wrapping the row. */
#footmsg {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#tray {
  display: flex;
  gap: 6px;
  flex: 0 0 auto;
}
#tray[hidden] { display: none; }

#tray button {
  font: inherit;
  font-size: 11px;
  padding: 3px 10px;
  border-radius: 4px;
  border: 1px solid var(--rule);
  background: var(--field);
  color: var(--fg);
  cursor: pointer;
}
#tray button:hover { border-color: var(--accent); }

#copy {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
#copy:hover { opacity: .9; }

/* Icon-only, and SQUARE by width alone -- the height is left to the tray's
   default align-items:stretch, which makes it exactly as tall as Copy no
   matter what the font metrics do. Setting a height here instead was the
   first try and it drifted a pixel off Copy at some zoom levels; letting
   flex equalise them cannot drift.

   Quieter than Copy on purpose: Copy is the thing you came to the tray for
   and keeps the filled accent, while Clear is the one you press by mistake.
   It sits back at --muted on the plain field background and only comes up to
   full ink on hover, the same "notice it when you look for it" treatment the
   block sign gets in the footer proper.

   SELECTOR SPECIFICITY IS LOAD-BEARING HERE. These must be written as
   `#tray #clear`, not `#clear`: the shared `#tray button` rule above is
   id+type (0,1,1) and beats a bare id (0,1,0), so a plain `#clear` loses
   BOTH its `padding: 0` and its colour to it. The symptom is not a colour
   bug -- it is a 4px-wide icon, because the inherited `padding: 3px 10px`
   eats 20px of a 26px box and the svg, as a flex item, shrinks into what
   is left. */
#tray #clear,
#tray #blockpicked {
  width: 26px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
}
/* Same hover as Clear, deliberately NOT a warning colour: colour rulings in
   this project go through Jeremy's eyes side by side (see the teal note at
   the top of this file), never get reasoned in from "red means danger". The
   confirm bar is what carries the weight of this button, not its hue. */
#tray #clear:hover,
#tray #blockpicked:hover { color: var(--fg); border-color: var(--accent); }
#tray #clear > svg,
#tray #blockpicked > svg {
  width: 15px;
  height: 15px;
  display: block;          /* no inline baseline gap under the icon */
}

/* The double-click-lookup toggle. It moved into the footer's LEFT group on
   2026-08-16 (his ruling; see the markup), so the margin-left:auto that used
   to pin it to the far right is gone. Keeping it would have been worse than
   redundant: two auto margins in one flex line split the free space between
   them, so it would have fought #footmsg's seam for the same gap instead of
   letting the tray own the right edge outright.
   State is carried by GREY LEVEL alone, not a slash -- his ruling
   2026-08-06: the slash "just isn't reading clearly", so ON is dark ink and
   OFF recedes to --faint, the same grey the rare words use. The slash line
   is still in the markup but never shown; delete it only if you are sure
   nothing else wants it. */
#dblbtn {
  margin-left: -1px;       /* the same tuck its three neighbours carry */
  margin-right: auto;      /* the quartet packs left; everything else follows */
  color: var(--fg);
  opacity: 1;
}
#dblbtn.off { color: var(--faint); }
#dblbtn .slash { display: none; }

/* Hand-rolled tooltips that open UPWARD. The native title bubbles follow the
   cursor, and at the bottom of a task pane that puts them under the Word
   status bar -- "virtually impossible to see" (Jeremy). These render above
   the element instead, inside the pane. */
[data-tip] { position: relative; }
[data-tip]:hover::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: 230px;
  background: var(--fg);
  color: var(--bg);
  font-size: 11px;
  font-weight: 400;
  font-style: normal;
  line-height: 1.35;
  padding: 4px 8px;
  border-radius: 4px;
  white-space: normal;
  text-align: center;
  pointer-events: none;
  z-index: 30;
}
/* Edge buttons: keep the bubble inside the pane. */
#optbtn[data-tip]:hover::after { left: 0; transform: none; }
/* #dblbtn had a right:0 override here for as long as it lived in the right
   corner. It moved into the left group on 2026-08-16 and now centres like
   its neighbours: fourth in from a 12px gutter puts its midpoint at 127px,
   and the bubble is capped at 230px, so half of it clears the left edge. */
/* Tooltips INSIDE the options popup are left-aligned to their label, not
   centred on it. Centring pushed them off the LEFT EDGE of the pane (his
   report): the popup already sits at left:8px, so half a 230px bubble
   centred on a label reaches past x=0 and gets clipped. Anchored left they
   grow rightward into the pane, where there is room. */
#options [data-tip]:hover::after { left: 0; transform: none; }


/* The settings panel's copy of the Hide/Show/Merge control. Same widget as
   the on-page row (famSegment builds both); this only lays it out as a panel
   row instead of a page row. */
.fampanelrow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 2px 0;
}
.fampanelrow .famname { font-size: 11px; color: var(--fg); font-style: normal; }

/* THE t=d ROW:  (descriptor)  t = d  [====slider====]
   His ruling 2026-08-13: back to a plain slider, five stops, with the
   descriptor moved LEFT of the label and parenthesised.

   NOTHING IS CENTRED AND THE DESCRIPTOR IS FIRST, and that is the whole
   trick: "keep the slider right justified, so when the descriptors change,
   neither the t=d or slider moves at all." The box is right-aligned, so a row
   grows LEFTWARD -- a longer descriptor pushes its own left edge out and
   leaves the label and the slider exactly where they were. A fixed width on
   the descriptor, or a centred row, would both have made them twitch on every
   change. */
.flapwrap { display: inline-flex; align-items: center; gap: 8px; }

/* Width is set in JS from a measurement of a real Hide - Show - Merge, so the
   two controls line up: see segmentWidth(). It cannot live here, because it
   is a TEXT width and moves with the font and the user's text size. */
.flapslider {
  -webkit-appearance: none;
  appearance: none;
  background: none;
  height: 13px;
  padding: 0;
  cursor: pointer;
  vertical-align: middle;
}
/* THE TRACK: A GRADIENT, DARK TO THE RIGHT, and INSET so it does not poke out
   past the end hashmarks. Both his rulings, 2026-08-13.

   The inset is the fiddly half. The input is deliberately built HALF A THUMB
   WIDER than the span the hashmarks cover, with negative margins to pull the
   extra back -- that is what makes the thumb's CENTRE land exactly on the
   first and last mark instead of its edge. The side effect was 6.5px of bare
   track sticking out beyond each end mark, which is the line he spotted.

   So the track paints an INSET BAND rather than the whole element: the
   gradient is positioned at var(--th) and sized to 100% - 2*var(--th), which
   is precisely the thumb's travel. --th is written by flapScale() from the
   same THUMB constant that positions the marks, so the two cannot drift.

   Direction: light at the left, dark at the right, to say that moving right
   ADDS. In dark mode it runs dim-to-bright instead -- literally darkening on
   a #1f1f1f box would have made "more" disappear. */
/* THE INPUT'S OWN TRACK IS INVISIBLE. The visible bar is .flapband, a real
   element -- see flapScale().

   WHY IT MOVED OUT OF THE PSEUDO-ELEMENT: the first version painted an inset
   background on ::-webkit-slider-runnable-track, and I "verified" it by
   computing where I believed the band was rather than measuring the paint. A
   pseudo-element's box cannot be read with getBoundingClientRect, so that
   check could never have failed, and he came back saying the line to the left
   of the OFF mark was still there. A real element can be measured against the
   hashmarks directly, so the next check is a real one. */
.flapslider::-webkit-slider-runnable-track { height: 3px; background: transparent; }
.flapslider::-moz-range-track { height: 3px; background: transparent; }
.flapband {
  position: absolute;
  top: 50%;
  height: 3px;
  margin-top: -1.5px;
  border-radius: 2px;
  pointer-events: none;
  background: linear-gradient(to right, var(--flaptrack0), var(--flaptrack1));
}
.flapslider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 13px;
  height: 13px;
  margin-top: -5px;          /* centres a 13px thumb on a 3px track */
  border: 0;
  border-radius: 50%;
  background: var(--stretch);
}
.flapslider::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
}
/* THE HASHMARKS. In their own lane behind the input, positioned from the
   thumb's travel (half a thumb inset at each end) so every mark sits exactly
   under the stop it names -- see flapScale() for why this is not a datalist. */
.flaplane { position: relative; display: inline-block; vertical-align: middle; line-height: 0; }
.flapticks { position: absolute; left: 0; right: 0; top: 50%; height: 0; pointer-events: none; }
/* PROMINENT AND DARK, his second ruling 2026-08-13 -- the first pass ("a hair
   more prominent") went from --rule #e1dfdd to #a49fb4 and was still too
   faint against the pale left end of the track. Now 2px wide and a genuinely
   dark grey, so they read as marks on a scale rather than as texture. */
.flaptick {
  position: absolute;
  top: -7px;
  width: 2px;
  height: 14px;
  margin-left: -1px;
  border-radius: 1px;
  background: var(--flaptick);
}
.flaplane .flapslider { position: relative; display: block; }

.flapslider:focus { outline: none; }
.flapslider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); }


/* ---------- the t=d row in the settings panel, 2026-08-14 ---------- */

/*
 * One line: "Never allow as perfect rhymes: t = d" with the scale on the
 * right of it. The row is a <label>, so clicking the words toggles the box --
 * which is why the slider has to stop the click from reaching it, or dragging
 * the scale would tick the very checkbox that disables the scale.
 */
.fampanelrow.neverrow {
  display: flex;
  align-items: center;
  gap: 8px;
}
.fampanelrow.neverrow .fampanelseg { margin-left: auto; }
.fampanelrow.neverrow .famname:empty { display: none; }

/* Ticked: the scale has nothing to decide, so it goes quiet and stops
   answering the pointer. Both, not one -- see flapScale(). */
.flapwrap.flapoff { opacity: .38; }
.flapwrap.flapoff .flapslider { cursor: default; }


/* ---------- tooltips on a touchscreen ---------- */

/*
 * :hover sticks after a tap, so every bubble was lingering until the next
 * tap somewhere else -- and #viewbtn's lingered while WRONG, because its
 * action wording ("Paragraph view") describes the NEXT press, which reads
 * exactly backwards when the bubble appears after the press. On touch the
 * stuck-hover bubbles are suppressed wholesale, and web-layout.js shows a
 * transient one (.tipnow) that names the view you are LOOKING AT and clears
 * itself on a timer or on the next tap elsewhere.
 */
@media (hover: none) {
  [data-tip]:hover::after { display: none; }
  .tipnow[data-tip]::after {
    content: attr(data-tip);
    display: block !important;
    opacity: 1 !important;
    visibility: visible !important;
  }
}


/* =========================================================================
   THE PERFECT | SLANT PILL -- shared with the website since 2026-08-15.
   This block moved verbatim from web.css when web-slant.js moved to addin/;
   the website's web-layout.js builds #rhbar, the Word pane's self-mount in
   web-slant.js does the same, and everything below styles both.
   ========================================================================= */

#rhbar { position: relative; }

/* ---------- the Perfect | Slant pill ---------- */

/*
 * Replaces #rhslant, the 24px squiggle. Everything here is measured, not
 * chosen by eye; the numbers and the reasons are in data/SLANT-PILL-SPEC.md.
 *
 * The gradient runs across the WHOLE pill as one background and does not
 * restart at the seam -- the Perfect half shows the left end of the ramp and
 * the Slant half is revealed by lifting a feathered veil painted in the page
 * colour. There is no divider: at every depth below 5 the veil's own soft
 * edge marks the join, and at depth 5 the point is that there is no join.
 */
/*
 * Pinned to the search box's INNER box -- 1px in on three sides, which is the
 * width of #q's own border. Not centred with a fixed height any more: the
 * Slant half is no longer a thing floating in the bar, it IS the right end of
 * the bar, so it has to reach the inner edges exactly however tall the input
 * happens to be. (The web bumps #q to 16px on phones; a magic height would
 * have been wrong there and right here.)
 */
#rhpillwrap { position: absolute; right: 1px; top: 1px; bottom: 1px; }

#rhpill {
  position: relative;
  display: inline-flex;
  align-items: stretch;
  height: 100%;
  border: 0;
  padding: 0;
  /* ROUND ON THE LEFT, THE SEARCH BAR'S CORNER ON THE RIGHT. The left end is
     still a pill because it is a shape sitting on the field; the right end is
     not a shape at all, it is the end of the input, so it takes the input's
     4px radius less its 1px border. Fully filled, the colour now runs from
     where "Slant Rhymes" begins straight into the bar's own corner. */
  border-radius: 999px 3px 3px 999px;
  overflow: hidden;
  cursor: pointer;
  vertical-align: middle;
  background: linear-gradient(100deg, #102f68 0%, #b691f7 100%);
  /* No inset outline. It used to draw the pill's own edge; now that the right
     end IS the bar's end, an outline there would trace a border inside a
     border. #q's own 1px is the only edge. */
}

.rhsl-half {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 0 21px;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
}
.rhsl-half b,
.rhsl-half u {
  display: block;
  text-decoration: none;
  line-height: 1.1;
  font-weight: 700;
  letter-spacing: .005em;
}
.rhsl-half b { font-size: 12.5px; }
.rhsl-half u { font-size: 8.5px; }

.rhsl-pf { z-index: 2; color: #fff; }
/*
 * HOVER ON THE PERFECT HALF. It is its own target -- clicking it resets to
 * depth 0 rather than opening the menu -- so it has to answer the pointer.
 *
 * An underline shipped first and was wrong: underline is TEXT language, and
 * this is a filled control. A filled thing should respond by lifting, not by
 * decorating its label. So the half brightens instead -- a white wash at 14%,
 * which reads on the navy end without touching the gradient underneath, the
 * layout, or the type.
 *
 * Only when there is something to go back to: at depth 0 the reset is a
 * no-op, and an affordance that promises nothing is worse than none.
 */
.rhsl-pf::before {
  content: "";
  position: absolute;
  inset: 0;
  background: #fff;
  opacity: 0;
  transition: opacity .12s ease-out;
  pointer-events: none;
}
#rhpill:not(.atzero) .rhsl-pf:hover::before { opacity: .14; }
#rhpill:not(.atzero) .rhsl-pf:active::before { opacity: .22; }
/* The label has to paint above the wash. Both are positioned, so tree order
   decides and ::before comes first -- these only need to be positioned too. */
.rhsl-pf b, .rhsl-pf u { position: relative; }
@media (prefers-reduced-motion: reduce) { .rhsl-pf::before { transition: none; } }

.rhsl-sl { z-index: 2; flex-direction: row; gap: 6px; }
.rhsl-sl b, .rhsl-sl u { font-style: italic; }

/*
 * THE UNFILLED HALF IS THE SEARCH BAR, not the page. It was var(--bg) (white),
 * which made the Slant half read as a white tablet lying on an off-white bar.
 * --field is what #q is painted with, so at depth 0 the half disappears into
 * the input and all that is left is its label -- which is the point: nothing
 * is selected, so there is nothing to see.
 */
.rhsl-veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--field);
}
.rhsl-sl.full .rhsl-veil { display: none; }

/* The white copy of the label, clipped to the same feather as the fill. It
   MUST be laid out identically to .rhsl-lab -- same display, same direction,
   same padding -- or the two copies disagree and the label visibly doubles. */
.rhsl-lab,
.rhsl-lab2 { display: flex; flex-direction: row; align-items: center; gap: 6px; }
.rhsl-lab  { position: relative; z-index: 3; color: var(--fg); }
.rhsl-lab2 {
  position: absolute;
  inset: 0;
  z-index: 4;
  justify-content: center;
  padding: 0 21px;
  color: #fff;
  pointer-events: none;
  /* White on the pale end of the ramp is 2.51:1 against a 4.5:1 floor. The
     shadow is invisible at the blue end (12.9:1 there) and is what keeps the
     label readable at depth 5. Chosen over recolouring the label mid-ramp. */
  text-shadow: 0 1px 2px rgba(30, 10, 70, .55);
}
.rhsl-sl.full .rhsl-lab2 { -webkit-mask-image: none; mask-image: none; }

.rhsl-stack { display: flex; flex-direction: column; align-items: center; }

/* Its own item at the right of the half, not part of the label -- centring
   "Rhymes" under "Slant" breaks if the caret counts toward the width. */
.rhsl-caret { font-size: 13px; font-weight: 400; font-style: normal; opacity: .85; }

/* Room for it, so a long word never runs underneath. Measured: the pill is
   174.5px, and 178 left only 3.5px of air -- a typed word would have kissed
   it. 188 keeps a comfortable gap without stealing usable field. */
#q { padding-right: 188px; }

/* ---------- the depth menu ---------- */

/*
 * 330px, not 276. At 276 the example column gets 183px and ordinary words
 * overflow it -- "heart ~ aren't · mine ~ timeline" needs 190px. It was not
 * the long-word edge case that broke the single line, it was the middle of
 * the range.
 */
#rhpillmenu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 40;
  /* 330 on the website; the Word pane is 304 at the floor, so the menu
     bends to the viewport there and the example pairs wrap as units --
     the wrap rule below has handled exactly this since it was built. */
  width: min(330px, calc(100vw - 20px));
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--bg);
  box-shadow: 0 3px 12px rgba(30, 10, 70, .16);
  overflow: hidden;
  color: var(--fg);
  text-align: left;
}
.rhsl-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 6px 11px;
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
  font-size: 11.5px;
}
.rhsl-row:last-child { border-bottom: 0; }
/* "new rhymes" must not wrap in the 42px number column */
.rhsl-head .rhsl-n { width: auto; white-space: nowrap; }
.rhsl-head {
  background: var(--field);
  cursor: default;
  font-size: 9.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 5px 11px;
}
.rhsl-d { width: 13px; flex: 0 0 auto; font-weight: 700; font-style: italic; }
/* WRAP AS A UNIT: the row wraps, each pair does not, so a line that will not
   fit moves the WHOLE second pair down rather than breaking after "rose ~". */
.rhsl-ex {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
}
/*
 * A COLUMN, NOT A RUN. Without this the live pair began wherever the
 * permanent one happened to end, so the right-hand halves stair-stepped down
 * the menu and could not be read as a list. 98px = the widest permanent pair
 * ("heart ~ aren't", measured at 73.9px) plus room for the bullet.
 */
.rhsl-permcol { flex: 0 0 98px; display: flex; align-items: baseline; }
.rhsl-perm { flex: 0 0 auto; white-space: nowrap; }
/* The bullet is the flexible part, so it centres in whatever each row leaves
   over -- close to its own pair on a long one, further out on a short one. */
.rhsl-sep { flex: 1 1 auto; text-align: center; }
.rhsl-ex i { font-style: italic; color: var(--fg); }
.rhsl-sep { color: var(--faint); }
.rhsl-live {
  font-weight: 700;
  color: var(--fg);
  white-space: nowrap;
  display: inline-block;
}
.rhsl-live i { font-style: italic; font-weight: 700; }
.rhsl-n {
  flex: 0 0 auto;
  width: 42px;
  text-align: right;
  font-size: 10.5px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.rhsl-n.dash { color: var(--faint); }
/* One highlight for every selected row, and it runs CUMULATIVELY from perfect
   up to the chosen depth -- choosing 3 lights perfect, 1, 2 and 3, because
   that is what you get. A single lit row would say you had picked one tier.

   LIT-NESS MUST NOT DEPEND ON THE WASH ALONE -- his report 2026-08-15:
   walking the depths "skipped" the tiers that add nothing. It did not; every
   row got the class. But on a no-new-rhymes row the wash was the ONLY mark,
   at 11% opacity, while its neighbours also carried a bold live pair -- so
   the empty tiers read unlit next to them. Now the row's TEXT steps up to
   full ink and the permanent pair takes weight when lit, so the lit run
   reads as a solid block of dark text ending exactly at the chosen depth --
   a position-and-weight cue, not a colour one. */
.rhsl-row.on { background: rgba(122, 81, 194, .16); }
.rhsl-row.on .rhsl-d { color: #6a3fb8; }
.rhsl-row.on .rhsl-ex { color: var(--fg); }
.rhsl-row.on .rhsl-perm { font-weight: 600; }
.rhsl-row.on .rhsl-n { color: var(--fg); }
/* a lit dash keeps saying "adds nothing", one step up from faint so it is
   legible INSIDE the lit block instead of looking like a hole in it */
.rhsl-row.on .rhsl-n.dash { color: var(--muted); }
/* Perfect has no digit, its own weight, and can never be switched off. */
.rhsl-perfect .rhsl-ex { font-weight: 600; color: var(--fg); }

/*
 * THE COMMITTED DEPTH, marked while you preview around it.
 *
 * Hovering repaints the fill, so the moment you start exploring you lose
 * sight of what you actually chose -- every row you pass looks selected. This
 * stripe is the answer: the wash says "this is what you would get", the
 * stripe says "this is what you have".
 *
 * A LEFT EDGE, not a dot beside the digit. The dot was anchored to .rhsl-d,
 * which is EMPTY on the perfect row -- no digit, no height, so the dot fell
 * out of the row and sat under it. The stripe needs nothing to hang off.
 * It is also a position cue rather than a colour one, which is the point.
 */
.rhsl-row.committed { box-shadow: inset 3px 0 0 #6a3fb8; }
@media (prefers-color-scheme: dark) {
  .rhsl-row.committed { box-shadow: inset 3px 0 0 #c0a6f5; }
}

/* The row under the pointer gets a touch more than the wash, so the edge of
   the preview is findable when several rows are lit at once. */
.rhsl-row[data-depth]:hover { background: rgba(122, 81, 194, .18); }

/* No hover preview on a touchscreen -- there is no pointer to preview with,
   and :hover on touch sticks after the tap. */
@media (hover: none) {
  .rhsl-row[data-depth]:hover { background: inherit; }
}
.rhsl-foot {
  padding: 6px 11px 7px;
  font-size: 10.5px;
  color: var(--muted);
  background: var(--field);
  border-top: 1px solid var(--rule);
  line-height: 1.45;
}
.rhsl-foot b { color: var(--fg); }

@media (prefers-color-scheme: dark) {
  #out.startup::before {
    opacity: 1;
  }

  /* The light-mode nudge inverted: a step brighter than --muted, still short
     of --fg. Same reasoning, opposite direction. */
  .status .greeting { color: #d2d0ce; }
}

/* ---------- the pill in dark ---------- */

/*
 * The ramp inverts the way the t=d track did: on a dark ground a literally
 * darker start would vanish into the page, so both ends lift. The label on
 * the Slant half goes near-black instead of white, because the pale violet
 * end is the LIGHTEST thing in the header once the theme flips.
 */
@media (prefers-color-scheme: dark) {
  #rhpill {
    background: linear-gradient(100deg, #27529f 0%, #dbc4ff 100%);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14);
  }
  .rhsl-lab2 {
    color: #14102a;
    text-shadow: 0 1px 2px rgba(255, 255, 255, .30);
  }
  .rhsl-row.on { background: rgba(192, 166, 245, .15); }
  .rhsl-row.on .rhsl-d { color: #c0a6f5; }
}



/* ---------- the pill in dark ---------- */

/*
 * The ramp inverts the way the t=d track did: on a dark ground a literally
 * darker start would vanish into the page, so both ends lift. The label on
 * the Slant half goes near-black instead of white, because the pale violet
 * end is the LIGHTEST thing in the header once the theme flips.
 */
@media (prefers-color-scheme: dark) {
  #rhpill {
    background: linear-gradient(100deg, #27529f 0%, #dbc4ff 100%);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14);
  }
  .rhsl-lab2 {
    color: #14102a;
    text-shadow: 0 1px 2px rgba(255, 255, 255, .30);
  }
  .rhsl-row.on { background: rgba(192, 166, 245, .15); }
  .rhsl-row.on .rhsl-d { color: #c0a6f5; }
}


