@import url('https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;500;600&family=Fraunces:opsz,wght,SOFT@9..144,400;500;600,30..100&family=JetBrains+Mono:wght@400;500;600&display=swap');

@font-face {
  font-family: "Neanes";
  src: url("fonts/Neanes.woff2") format("woff2");
  font-display: block;
}

@font-face {
  font-family: "Bravura Text";
  src: url("fonts/BravuraText.woff2") format("woff2");
  font-display: block;
}

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --ink:         #1a1814;
  --ink-soft:    #4a4238;
  --ink-faint:   #8c8275;
  --paper:       #f7f1e3;
  --paper-warm:  #efe6d0;
  --paper-deep:  #e1d5b8;
  --paper-fade:  #faf6ec;
  --rule:        #cdc1a4;
  --rule-soft:   #ded3b8;
  --accent:      #8a2e1a;
  --accent-soft: #b85a3e;
  --focus-glow:  rgba(138, 46, 26, 0.14);

  /* The editor's right-hand column. A note row lays out
     [well] gap [name box] in Generic and [well] gap [well] gap [well] in
     Byzantine; the interval row's cluster is [swatch] gap [label] and takes the
     same total width in each.

     --well-size is also the *height* of every control on those two rows — the
     wells, the swatch, the name box and the label. A text box left to size
     itself from its font would be a different height from the square beside it
     (DM Sans' 1.25em line box puts the name box at 40px against a 34px well)
     and would change height again on a font the reader has and we did not, so
     the boxes are given this height outright and their vertical padding drops
     to 0: the browser centres a single-line input's text in the content box. */
  --well-size: 34px;
  --row-gap: 0.625rem;
  --name-box-width: 9rem;
}

body {
  font-family: 'DM Sans', system-ui, sans-serif;
  font-size: 15px;
  background:
    radial-gradient(ellipse 80% 60% at 18% 0%, rgba(232, 222, 199, 0.55) 0%, transparent 60%),
    radial-gradient(ellipse 65% 50% at 100% 100%, rgba(217, 204, 170, 0.45) 0%, transparent 55%),
    #ece3ce;
  color: var(--ink);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.container {
  display: flex;
  gap: 1.75rem;
  max-width: 1200px;
  margin: 0 auto;
  padding: 2.5rem 2rem;
}

/* --- Panel base --- */

.panel {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 1.5rem 1.6rem;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    0 1px 2px rgba(30, 20, 10, 0.06),
    0 18px 40px -22px rgba(30, 20, 10, 0.32);
}

.panel h2 {
  font-family: 'Fraunces', 'Georgia', serif;
  font-weight: 500;
  font-variation-settings: "opsz" 96, "SOFT" 40;
  font-size: 1.55rem;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin-bottom: 1.25rem;
  padding-bottom: 0.65rem;
  border-bottom: 1px solid var(--rule);
  position: relative;
}

.panel h2::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 28px;
  height: 2px;
  background: var(--accent);
}

/* --- Left Column --- */

/* Fixed, not shrink-wrapped. A note row's right-hand block is a well and a name
   box in Generic but three wells in Byzantine, and in Absolute mode the widest
   line is the note row where in Relative it is the interval row — so a column
   sized to its content changed width on the Notation and the Mode dropdowns,
   moving the Chart panel with it. 35rem clears the widest of the four
   combinations (Generic + Absolute, which needs 553.19px at the default font)
   with a little to spare; the surplus falls into the gap in the middle of a
   row, because every right-hand block is already `margin-left: auto`.

   Both responsive breakpoints below override this with `width: 100%`, so the
   number only applies where there are two columns to balance. */
.left-column {
  flex: 0 0 auto;
  width: 35rem;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

/* --- Settings Panel --- */

.notation-row,
.base-note-row {
  display: flex;
  align-items: center;
  gap: 0.65rem;
}

/* Every settings row after the first is spaced the same way. */
.base-note-row { margin-top: 0.85rem; }

.notation-row label,
.base-note-row label {
  font-weight: 500;
  font-size: 0.72rem;
  flex: 0 0 auto;
  width: 10.5rem;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.09em;
}

.notation-row select,
.base-note-row select {
  flex: 0 0 auto;
  width: 17rem;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--rule);
  border-radius: 3px;
  font-family: inherit;
  font-size: 0.9rem;
  background: var(--paper-fade);
  color: var(--ink);
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.notation-row select:hover,
.base-note-row select:hover {
  border-color: var(--ink-soft);
}

.notation-row select:focus,
.base-note-row select:focus {
  outline: none;
  border-color: var(--ink);
  background: #fff;
  box-shadow: 0 0 0 3px var(--focus-glow);
}

/* === Editor Panel ============================================ */

.editor-panel { position: relative; }

/* One visual family for the editor's own settings: Name, Interval Type, EDO
   Divisions and Mode. They share a label column and a full-width control, and
   Mode carries the rule that separates them from the rows below. */
.scale-name-row,
.interval-type-row,
.edo-settings-row,
.scale-mode-row {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  margin-bottom: 0.7rem;
}

.edo-settings-row { align-items: flex-start; }

.scale-mode-row {
  margin-bottom: 1.1rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--rule);
}

.scale-name-row label,
.interval-type-row label,
.edo-settings-row label,
.scale-mode-row label {
  font-weight: 500;
  font-size: 0.72rem;
  flex: 0 0 auto;
  width: 9.5rem;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.09em;
}

.scale-name-row input,
.interval-type-row select,
.edo-settings-row input,
.scale-mode-row select {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--rule);
  border-radius: 3px;
  font-family: inherit;
  font-size: 0.9rem;
  background: var(--paper-fade);
  color: var(--ink);
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.scale-name-row input,
.edo-settings-row input { cursor: text; }

.scale-name-row input:hover,
.interval-type-row select:hover,
.edo-settings-row input:hover,
.scale-mode-row select:hover { border-color: var(--ink-soft); }

.scale-name-row input:focus,
.interval-type-row select:focus,
.edo-settings-row input:focus,
.scale-mode-row select:focus {
  outline: none;
  border-color: var(--ink);
  background: #fff;
  box-shadow: 0 0 0 3px var(--focus-glow);
}

.edo-input-stack {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
  min-width: 0;
}

#edo-cents-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.76rem;
  color: var(--ink-soft);
  white-space: nowrap;
  padding-top: 0.05rem;
}

/* --- Absolute mode: absolute-interval input on note-row --- */

.note-row .absolute-interval {
  width: 9rem;
  flex: 0 0 auto;
  text-align: left;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.92rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  padding: 0.42rem 0.55rem;
  border: 1px solid var(--rule);
  background: rgba(255, 255, 255, 0.55);
  border-radius: 3px;
  color: var(--ink);
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.note-row .absolute-interval:disabled {
  background: var(--paper-fade);
  color: var(--ink-faint);
  cursor: not-allowed;
}

.note-row .absolute-interval:hover:not(:disabled) {
  border-color: var(--ink-soft);
  background: #fff;
}

.note-row .absolute-interval:focus {
  outline: none;
  border-color: var(--ink);
  background: #fff;
  box-shadow: 0 0 0 3px var(--focus-glow);
}

.note-row .abs-cents-label {
  flex: 0 0 auto;
  min-width: 70px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--accent);
  white-space: nowrap;
}

/* --- Absolute mode: relative-cents display on interval-row --- */

.interval-row .relative-cents-display {
  flex: 0 0 auto;
  width: calc(9rem + 0.55rem + 70px);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

#editor {
  display: flex;
  flex-direction: column;
  counter-reset: degree;
  position: relative;
  padding: 0.15rem 0 0.25rem;
}

/* Vertical rail connecting all degree badges — the "spine" of the scale */
#editor::before {
  content: '';
  position: absolute;
  left: calc(28px + 0.625rem + 14.5px);
  top: 18px;
  bottom: 18px;
  width: 1px;
  background: linear-gradient(
    to bottom,
    transparent 0,
    var(--rule) 8px,
    var(--rule) calc(100% - 8px),
    transparent 100%
  );
  pointer-events: none;
}

.row {
  display: flex;
  align-items: center;
  gap: var(--row-gap);
  position: relative;
}

/* --- Note row --- */

.note-row {
  counter-increment: degree;
  padding: 0.35rem 0;
  border-radius: 3px;
  transition: background 0.18s ease;
}

.note-row:hover {
  background: rgba(138, 46, 26, 0.035);
}

/* The <label> is turned into a circular degree badge.
   The "Note N" text is hidden; the degree number is drawn via a CSS counter,
   so the badge stays correct even when rows are added or removed. */
.note-row label {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  min-width: 30px;
  border-radius: 50%;
  background: var(--ink);
  color: var(--paper);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0;
  position: relative;
  z-index: 2;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 1px 2px rgba(0, 0, 0, 0.18);
}

.note-row label::before {
  content: counter(degree);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: -0.02em;
}

.note-row .note-name {
  width: var(--name-box-width);
  height: var(--well-size);
  font-family: 'DM Sans', sans-serif;
  font-size: 0.93rem;
  padding: 0 0.75rem;
  border: 1px solid var(--rule);
  background: var(--paper-fade);
  border-radius: 3px;
  color: var(--ink);
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.note-row .note-name::placeholder {
  color: var(--ink-faint);
  font-style: italic;
}

.note-row .note-name:hover { border-color: var(--ink-soft); }

.note-row .note-name:focus {
  outline: none;
  border-color: var(--ink);
  background: #fff;
  box-shadow: 0 0 0 3px var(--focus-glow);
}

/* --- Notation switch --- */

/* Both sets of controls live on every note row; CSS decides which are visible,
   so a switch discards nothing. */
.note-row .accidental-well-wrapper,
.note-row .alteration-well-wrapper,
.note-row .fthora-well-wrapper,
.note-row .martyria-well-wrapper { display: none; }

#editor.notation-byzantine .note-row .note-name { display: none; }

/* `flex`, not `block`: a well is an inline-flex button, so a block wrapper puts
   it on a line box, and a line box is as tall as the wrapper's font strut needs
   *plus* whatever the button's own baseline asks for below it. The wrapper came
   out 1.5-3px taller than the 34px button, the button sat at the top of it, and
   `.row`'s `align-items: center` then centred the wrapper rather than the well.
   Worse, each well asked for a different amount: an empty martyria's hint is two
   absolutely-positioned bars, so that button has no in-flow baseline at all and
   dropped a further 0.75px against its neighbours, and a well changed height the
   moment it was filled. A flex container has no line boxes, so the wrapper is
   exactly its button and every well centres on the same line. */
#editor.notation-generic .note-row .accidental-well-wrapper,
#editor.notation-byzantine .note-row .alteration-well-wrapper,
#editor.notation-byzantine .note-row .fthora-well-wrapper,
#editor.notation-byzantine .note-row .martyria-well-wrapper {
  display: flex;
  position: relative;
  flex: 0 0 auto;
}

/* .note-name used to carry the right-alignment; the leftmost *visible* element
   of the row's right-hand block takes it on, which is a different element in
   each notation. */
#editor.notation-generic .note-row .accidental-well-wrapper,
#editor.notation-byzantine .note-row .alteration-well-wrapper { margin-left: auto; }

/* --- Symbol wells --- */

.accidental-well,
.alteration-well,
.fthora-well,
.martyria-well {
  width: var(--well-size);
  height: var(--well-size);
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: var(--paper-fade);
  color: var(--ink);
  cursor: pointer;
  font-family: "Neanes", serif;
  font-size: 22px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.accidental-well:hover,
.alteration-well:hover,
.fthora-well:hover,
.martyria-well:hover { border-color: var(--ink); }

/* The other face, and the one thing an Evo pair cannot do without: the U+0020
   spacer between a sagittal and its apotome is collapsed away by normal
   white-space handling, and the pair goes tight. (`ctx.fillText` needs nothing
   — a canvas paints the glyphs it is handed.) */
.accidental-well {
  font-family: "Bravura Text", serif;
  font-size: 30px;
  white-space: pre;
}

/* A fthora or a sign of alteration carries far less ink than a martyria: the
   widest of those two families spans 0.84em where a martyria spans 1.21, and
   the tallest only 0.68em, so at the martyria's size they float in the middle
   of their box. One factor lifts both families at once — the signs keep their
   sizes relative to one another, and the widest of them now clears its box by
   about 3px a side, the margin a martyria already has. The martyria fills its
   box as it is, so it keeps the size above. */
.alteration-well,
.fthora-well { font-size: 31px; }

/* Flexbox centres the glyph's line box and advance box. Neanes puts a fthora's
   ink a whole em above the baseline and hangs a genus mark below it, so the
   ink is in the middle of neither — `setGlyphBoxText()` measures the difference
   and supplies it here. Both default to 0, so a box is still sane if the script
   has not filled it. Wells, picker rows and the picker's preview all use it. */
.glyph-ink {
  display: block;
  position: relative;
  line-height: 1;
  transform: translate(var(--ink-dx, 0px), var(--ink-dy, 0px));
}

/* A genus row previews a whole martyria but is *about* the mark, so the letter
   is drawn twice: the composition below, then the letter alone on top of it in
   grey, covering its own black copy and leaving the mark black. The mark cannot
   simply be coloured on its own — the font attaches it to the letter's anchor,
   and two elements have no shaping between them — so this is what a two-tone
   composed glyph costs.

   The two layers share `.glyph-ink`'s offset and start at the same corner of the
   same line box, and the mark carries no advance, so the letter lands exactly on
   itself. */
.glyph-layer { display: block; }

.glyph-muted {
  position: absolute;
  top: 0;
  left: 0;
  color: var(--ink-faint);
}

/* An empty well says which sign it takes. The dashed border and the faint ink
   are what keep the hint from reading as a filled well. */
.accidental-well.is-empty,
.alteration-well.is-empty,
.fthora-well.is-empty,
.martyria-well.is-empty {
  border-style: dashed;
  position: relative;
}

/* Two of the three hints are real signs from the well's own vocabulary, drawn
   faint: the two geniki — the general flat and the general sharp — for the
   alteration, and the enharmonic beside the soft chromatic Κε for the fthora.
   The martyria keeps its two bars below, so the three stay tellable apart.

   Placement is the same rule a *filled* well uses. Each pseudo-element stays
   in normal flow, so the well's flex centring lands it exactly where it lands
   `.glyph-ink`, and the transform is then whatever `inkCenteringShiftEm()`
   returns for that glyph string — read out of the app itself, in em, and
   written here because CSS cannot call it. Change a hint's glyphs and you must
   read its shift out again.

   The alteration's two signs need one pseudo-element each. They are
   zero-advance combining marks, so as one string they would sit on top of each
   other, and Neanes' space is 0.007em — nowhere near enough to part them; the
   horizontal nudge is instead half an ink width plus half a 0.12em gap, applied
   outwards. The fthores have ordinary advances, so their pair is one string
   with the font's own spacing between them.

   Unlike the martyria's bars these need the webfont, which `font-display:
   block` withholds for up to three seconds on a cold load. The well is blank
   until then — which is what it would show anyway. */
.accidental-well.is-empty::before,
.alteration-well.is-empty::before,
.alteration-well.is-empty::after,
.fthora-well.is-empty::before {
  display: block;
  line-height: 1;
  color: var(--ink-faint);
}

/* accidentalFlat, a space, then accidentalSharp — the well's own vocabulary,
   drawn faint. Unlike Neanes' signs these carry real advances, so the pair is
   one string rather than a pseudo-element each; but SMuFL sets zero side
   bearings, so with nothing between them the two abut exactly at the ink and
   read as one mark. `\20` is U+0020, Bravura Text's own half staff space —
   the same spacer a composed Sagittal Evo pair uses, and it survives here only
   because `.accidental-well` sets `white-space: pre`. The transform is
   inkCenteringShiftEm()'s answer for that string in Bravura Text, read out of
   the app and written here because CSS cannot call it: a space carries no ink
   and both glyphs' ink spans their whole advance, so it is what the unspaced
   pair measured too. Change the glyphs and you must read it out again. */
.accidental-well.is-empty::before {
  content: "\E260\20\E262";
  font-size: 30px;
  transform: translate(0, 0.0365em);
}

/* yfesisGeniki, then diesisGeniki, each riding on the same carrier a well's own
   sign rides on: WebKit paints nothing for a run of nothing but zero-advance
   marks, and these hints are two of those. See `domGlyphText`. The carrier
   costs each one half its advance — the -0.0035em now inside the nudges. */
.alteration-well.is-empty::before {
  content: "\A0\E204";
  font-size: 31px;
  transform: translate(-0.183em, 0.3886em);
}

.alteration-well.is-empty::after {
  content: "\A0\E1F4";
  font-size: 31px;
  transform: translate(0.176em, 0.5564em);
}

/* fthoraEnharmonic, then fthoraSoftChromaticKe. */
.fthora-well.is-empty::before {
  content: "\E1DC\E1DB";
  font-size: 24px;
  transform: translate(0.0151em, 0.4644em);
}

/* The martyria's hint is two bars rather than a sign, so nothing measures its
   weight for it: at 2px they were twice the stroke of the Neanes glyphs hinting
   the two wells to their left (about 0.8px at 31px), and the well read as one
   holding a martyria rather than as an empty one. A hairline matches them.

   Placed from the centre outwards rather than from the top down, so the pair
   stays centred at whatever `--well-size` is — the phone breakpoint shrinks the
   box to 28px. 2.5px each side of the middle keeps the 5px gap they had. */
.martyria-well.is-empty::before,
.martyria-well.is-empty::after {
  content: "";
  position: absolute;
  height: 1px;
  background: var(--ink-faint);
}

.martyria-well.is-empty::before { width: 14px; bottom: calc(50% + 2.5px); }
.martyria-well.is-empty::after  { width:  8px; top:    calc(50% + 2.5px); }

/* --- Symbol pickers --- */

.accidental-picker,
.alteration-picker,
.fthora-picker,
.martyria-picker {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 10;
  margin-top: 7px;
  padding: 6px;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 4px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    0 14px 38px -10px rgba(30, 20, 10, 0.38);
}

/* The Byzantine panels hang off their own well: those wells sit at the row's
   right edge, and the labels are short. The accidental panel cannot. Its well
   is the leftmost element of the row's right-hand block — a long way into the
   row — and its labels ("Syntonic/Didymus comma (80:81) up (Bosanquet)") push
   it to the 22rem cap, so opening rightward from the well ran it off the side
   of the page: 118px past the viewport at 820px wide, 200px at 375px, with no
   `overflow-x` anywhere in the layout to clip it, so the *page* scrolled
   sideways. On a desktop it stayed on the page but laid itself over the chart.

   So this one panel hangs off the note row instead, whose right edge is on
   screen by definition. `left: 0; right: 0` makes the row the panel's width
   budget, the cap trims it to 22rem when there is room, and `margin-left: auto`
   keeps it by its well rather than stranded at the row's left edge. The panel
   is then never wider than the row and never leaves the page, at any width,
   with no breakpoint of its own. */
/* Same specificity as the rule above that makes every well wrapper a containing
   block, and after it, so this one wrapper gives that role back to the row. */
#editor.notation-generic .note-row .accidental-well-wrapper { position: static; }

.accidental-picker {
  left: 0;
  right: 0;
  max-width: 22rem;
  margin-left: auto;
}

/* The lists are tall, so they scroll inside their panel — which never flips
   up: a scrolling panel cannot escape the editor. */
.accidental-picker.open,
.alteration-picker.open,
.fthora-picker.open { display: block; }

.accidental-picker-body,
.alteration-picker-body,
.fthora-picker-body {
  max-height: 320px;
  overflow-y: auto;
}

#editor .note-row.picker-open { z-index: 100; }

.sym-option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.3rem 0.5rem;
  border: 0;
  border-radius: 3px;
  background: none;
  color: var(--ink);
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
}

.sym-option:hover:not(:disabled) { background: var(--paper-fade); }
.sym-option:disabled { opacity: 0.35; cursor: default; }
.sym-option.is-selected { background: var(--paper-fade); font-weight: 600; }

/* A psaltic sign and a Latin name are hard to scan side by side: the sign is
   small, unfamiliar, and sits nowhere near the name's optical line. Boxing it
   gives the eye a column to run down, and matches the well the sign is going
   into. */
.sym-glyph {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 1px solid var(--rule-soft);
  border-radius: 3px;
  background: var(--paper-fade);
  font-family: "Neanes", serif;
  font-size: 24px;
  line-height: 1;
}

/* The same lift the wells get, for the same reason: a list of fthores or of
   signs of alteration set at the martyria's size is a column of specks. */
.alteration-picker .sym-glyph,
.fthora-picker .sym-glyph { font-size: 34px; }

.sym-option.is-selected .sym-glyph { border-color: var(--ink-faint); }
.sym-option:disabled .sym-glyph { background: none; }

/* The None row of a single-value list holds no sign. A solid empty box reads as
   a glyph that failed to load; a dashed one says "empty", the same way an empty
   well does. (The Genus column's None is not empty: it previews the letter on
   its own, which is exactly what picking it commits.) */
.sym-glyph:empty {
  border-style: dashed;
  background: none;
}

/* SMuFL descriptions run long ("5 comma up, (5C), 1° up [22 27 …] …"), so this
   one list wraps where the Byzantine lists never need to. */
.accidental-picker .sym-option { white-space: normal; }

.accidental-picker .sym-glyph {
  font-family: "Bravura Text", serif;
  font-size: 32px;
  white-space: pre;
}

.martyria-picker.open { display: block; }

.martyria-picker-body {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
}

.martyria-notes-column,
.martyria-genus-column {
  max-height: 300px;
  overflow-y: auto;
  min-width: 9rem;
}

.martyria-genus-column.is-inert { opacity: 0.45; }

.sym-column-title {
  position: sticky;
  top: 0;
  padding: 0.25rem 0.5rem;
  background: var(--paper);
  font-family: 'DM Sans', sans-serif;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.sym-group-title {
  padding: 0.35rem 0.5rem 0.15rem;
  font-family: 'DM Sans', sans-serif;
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.sym-separator {
  height: 1px;
  margin: 0.4rem 0.5rem;
  background: var(--rule);
}

.sym-label {
  font-family: 'DM Sans', sans-serif;
  font-size: 0.82rem;
}

/* The field sits above the scroller rather than inside it, so it holds still
   while the list moves under it and needs no sticky positioning. */
.sym-search {
  display: block;
  width: 100%;
  margin-bottom: 6px;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: var(--paper-fade);
  font-family: 'DM Sans', sans-serif;
  font-size: 0.82rem;
  color: var(--ink);
}

.sym-search::placeholder {
  color: var(--ink-faint);
  font-style: italic;
}

.sym-search:focus {
  outline: none;
  border-color: var(--ink);
  background: #fff;
  box-shadow: 0 0 0 3px var(--focus-glow);
}

.sym-empty {
  padding: 0.5rem;
  font-family: 'DM Sans', sans-serif;
  font-size: 0.82rem;
  font-style: italic;
  color: var(--ink-faint);
}

/* `hidden` is how a filter narrows the list. The rules above give these
   elements a `display`, which would otherwise win over the attribute. */
.sym-option[hidden],
.sym-group-title[hidden],
.sym-separator[hidden],
.sym-empty[hidden] { display: none; }

.note-row .cumulative-cents {
  flex: 0 0 auto;
  width: 9rem;
  padding-left: 0.7rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  white-space: nowrap;
  text-align: left;
}

/* --- Play button --- */

.play-note {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--paper-fade);
  border: 1px solid var(--rule);
  color: var(--ink);
  font-size: 0.65rem;
  line-height: 1;
  padding: 0 0 0 2px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  z-index: 2;
  transition: background 0.18s, border-color 0.18s, color 0.18s, transform 0.18s, box-shadow 0.18s;
  -webkit-user-select: none;
  user-select: none;
}

.play-note:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
  transform: scale(1.08);
  box-shadow: 0 4px 10px -3px rgba(138, 46, 26, 0.42);
}

.play-note:active {
  transform: scale(0.95);
  box-shadow: 0 1px 3px -1px rgba(138, 46, 26, 0.35);
}

/* The sounding note during scale playback, reproducing what a real press looks
   like — the hover fill *and* the active geometry, since a mouse press on a
   desktop is also a hover. Last, so it wins the specificity tie with :hover. */
.play-note.sounding {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
  transform: scale(0.95);
  box-shadow: 0 1px 3px -1px rgba(138, 46, 26, 0.35);
}

/* --- Interval row --- */

.interval-row {
  gap: 0.55rem;
  /* align ratio input with the note-name input above and below */
  padding: 0.15rem 0 0.15rem calc(28px + 0.625rem + 30px + 0.625rem);
}

.interval-row.dropdown-open { z-index: 100; }

.interval-row .interval {
  width: 9rem;
  flex: 0 0 auto;
  text-align: left;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.92rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  padding: 0.42rem 0.55rem;
  border: 1px solid var(--rule);
  background: rgba(255, 255, 255, 0.55);
  border-radius: 3px;
  color: var(--ink);
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.interval-row .interval:hover {
  border-color: var(--ink-soft);
  background: #fff;
}

.interval-row .interval:focus {
  outline: none;
  border-color: var(--ink);
  background: #fff;
  box-shadow: 0 0 0 3px var(--focus-glow);
}

/* A box the app cannot read as an interval. The rules sit after the :hover and
   :focus above so the tint survives both — a user fixing the value is looking
   at the box with the cursor in it, which is exactly when the mark has to stay
   visible. --accent is the same red the message bar and the cents labels use.

   Both selectors are needed: the relative value lives on the interval row, the
   absolute one on the note row. */
.interval-row .interval.is-invalid,
.note-row .absolute-interval.is-invalid {
  border-color: var(--accent);
  background: rgba(138, 46, 26, 0.12);
}

.interval-row .interval.is-invalid:focus,
.note-row .absolute-interval.is-invalid:focus {
  border-color: var(--accent);
  background: rgba(138, 46, 26, 0.12);
  box-shadow: 0 0 0 3px rgba(138, 46, 26, 0.18);
}

.interval-row .cents-label {
  flex: 0 0 auto;
  min-width: 70px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--accent);
  white-space: nowrap;
}

/* The cluster takes the width of the block above it, which is not the same in
   the two notations: Generic's is a well, a gap and the name box; Byzantine's
   is three wells and two gaps. The label is `flex: 1 1 auto`, so it fills
   whatever is left in each — no JavaScript decides this. */
.interval-row .interval-label-cluster {
  /* Shrinkable, though it almost never shrinks: `width` is the size it wants
     and gets whenever the row has the room. Fixed at `0 0 auto` it was the one
     thing in the row that could not give, so on a narrow phone it pushed the
     page sideways while the note-name box above it shrank as asked. */
  flex: 0 1 auto;
  min-width: 0;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--row-gap);
  width: calc(var(--well-size) + var(--row-gap) + var(--name-box-width));
}

#editor.notation-byzantine .interval-row .interval-label-cluster {
  width: calc(var(--well-size) * 3 + var(--row-gap) * 2);
}

.interval-row .interval-label {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--well-size);
  padding: 0 0.6rem;
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: var(--paper-fade);
  font-family: 'DM Sans', sans-serif;
  font-size: 0.85rem;
  font-style: italic;
  color: var(--ink);
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s, font-style 0.15s;
}

.interval-row .interval-label::placeholder {
  color: var(--ink-faint);
}

.interval-row .interval-label:hover { border-color: var(--ink-soft); }

.interval-row .interval-label:focus {
  outline: none;
  border-color: var(--ink);
  background: #fff;
  box-shadow: 0 0 0 3px var(--focus-glow);
  font-style: normal;
}

/* --- Color picker --- */

/* `flex` for the same reason the well wrappers are flex: the swatch is a
   button on a line box otherwise, which left the wrapper 3px taller than it and
   the swatch 1.2-1.5px above the label it sits beside. */
.color-picker-wrapper {
  display: flex;
  position: relative;
  flex: 0 0 auto;
}

.color-swatch {
  width: var(--well-size);
  height: var(--well-size);
  border: 1px solid var(--rule);
  border-radius: 3px;
  cursor: pointer;
  padding: 0;
  position: relative;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.4),
    0 1px 2px rgba(0, 0, 0, 0.08);
  transition: transform 0.2s ease, border-color 0.15s, box-shadow 0.15s;
}

.color-swatch:hover {
  border-color: var(--ink);
  transform: rotate(-8deg) scale(1.12);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.4),
    0 4px 10px -2px rgba(0, 0, 0, 0.2);
}

.color-dropdown {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 10;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 8px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    0 14px 38px -10px rgba(30, 20, 10, 0.38);
  margin-top: 7px;
}

.color-dropdown.open {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 5px;
}

.color-option {
  width: 24px;
  height: 24px;
  border: 1px solid var(--rule);
  border-radius: 3px;
  cursor: pointer;
  padding: 0;
  transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
}

.color-option:hover {
  transform: scale(1.15);
  border-color: var(--ink);
  box-shadow: 0 0 0 1.5px var(--ink);
}

/* === Toolbar ================================================= */

/* The sticky element is the wrapper, not #toolbar itself. The alert bar has to
   stay outside role="toolbar"'s accessible subtree — that subtree is for
   widgets, not a live region — yet it still has to ride on screen, because it
   is the only channel a rejected file reports through and Ctrl/Cmd+O works
   however far down the page the user has scrolled. Sticking the pair together
   also survives the toolbar wrapping onto a second row, which a fixed `top`
   offset on the bar alone would not.

   z-index 200 clears the symbol pickers' 100. Both live in the root stacking
   context — a picker escapes the editor panel into it (see Motion, below) — so
   200 is the number "always on top" actually has to beat. */
#toolbar-bar {
  position: sticky;
  top: 0;
  z-index: 200;
  background: var(--paper-warm);
  border-bottom: 1px solid var(--rule);
  box-shadow: 0 2px 10px -8px rgba(30, 20, 10, 0.55);
}

#toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  padding: 0.4rem 2rem;
}

/* An SVG loaded through <img> renders in an isolated document that no page CSS
   reaches, so `currentColor` never resolves and each file's ink is baked at
   --ink. Every state is therefore opacity plus the button's own background —
   which is also why all seven buttons share one treatment rather than Add note
   keeping its old filled-primary emphasis. */
.toolbar-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.15rem;
  height: 2.15rem;
  padding: 0;
  border: 0;
  border-radius: 3px;
  background: transparent;
  cursor: pointer;
  transition: background 0.15s;
}

.toolbar-btn img {
  width: 1.2rem;
  height: 1.2rem;
  opacity: 0.72;
  pointer-events: none;
  transition: opacity 0.15s;
}

.toolbar-btn:hover:not(:disabled),
.toolbar-btn:focus-visible,
.toolbar-btn:active:not(:disabled) { background: var(--paper-deep); }

.toolbar-btn:hover:not(:disabled) img,
.toolbar-btn:focus-visible img { opacity: 1; }

.toolbar-btn:disabled { background: transparent; cursor: not-allowed; }
.toolbar-btn:disabled img { opacity: 0.3; }

.save-menu-wrapper { position: relative; }

/* The caret is a CSS triangle rather than a sixth .svg, so it follows the
   button's own colour instead of being baked like the icons. */
.toolbar-btn.has-caret::after {
  content: '';
  margin-left: 0.15rem;
  border: 3px solid transparent;
  border-top-color: var(--ink);
  border-bottom: 0;
  opacity: 0.72;
}

.save-menu-panel {
  display: none;
  position: absolute;
  top: calc(100% + 0.3rem);
  left: 0;
  min-width: 15rem;
  padding: 0.3rem;
  background: var(--paper-fade);
  border: 1px solid var(--rule);
  border-radius: 3px;
  box-shadow: 0 10px 24px -12px rgba(30, 20, 10, 0.5);
}

.save-menu-panel.open { display: block; }

.save-menu-panel button {
  display: block;
  width: 100%;
  padding: 0.4rem 0.55rem;
  border: 0;
  border-radius: 3px;
  background: none;
  color: var(--ink);
  font-family: inherit;
  font-size: 0.85rem;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

.save-menu-panel button:hover { background: var(--paper-deep); }

.save-menu-panel hr {
  margin: 0.25rem 0.3rem;
  border: 0;
  border-top: 1px solid var(--rule-soft);
}

.toolbar-separator {
  width: 1px;
  height: 1.4rem;
  margin: 0 0.35rem;
  background: var(--rule);
}

/* A sibling of #toolbar, not a flex child of it (a role="toolbar" accessible
   subtree is meant to hold widgets, not a role="alert" live region) — the
   side margins reproduce the inset the toolbar's own 2rem padding used to
   give it, so it appears in the same place it always did. No top margin:
   #toolbar's own bottom padding is already the gap above it. The bottom margin
   is what holds it off #toolbar-bar's rule when it shows. */
/* `display: flex` below is an id selector, which outranks the UA stylesheet's
   `[hidden] { display: none }` — so without this rule the bar ignores its own
   hidden attribute and sits on the page as an empty strip from load onwards,
   with the × setting an attribute that changes nothing. Anything that gives
   #toolbar-message a display value has to restate this. */
#toolbar-message[hidden] {
  display: none;
}

#toolbar-message {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin: 0 2rem 0.45rem;
  padding: 0.4rem 0.6rem;
  border-radius: 3px;
  background: rgba(138, 46, 26, 0.1);
  color: var(--accent);
  font-size: 0.82rem;
}

#toolbar-message-text {
  flex: 1;
}

/* The × is drawn here rather than written in the markup so the button carries
   no text node: #toolbar-message is the role="alert" live region, and a glyph
   inside it would be read out along with the message. The accessible name comes
   from the button's aria-label instead.

   `line-height: 1` on a 1rem glyph keeps the button's box the height of the
   character, so it sits on the message's first line however many lines the
   message wraps to. */
#toolbar-message-dismiss {
  flex: none;
  padding: 0 0.2rem;
  border: none;
  background: none;
  color: inherit;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0.65;
}

#toolbar-message-dismiss::before {
  content: "\00d7";
}

#toolbar-message-dismiss:hover,
#toolbar-message-dismiss:focus-visible {
  opacity: 1;
}

/* === Chart Panel ============================================ */

/* `min-width: 0` and `overflow-x: auto` together are what keep the *page* from
   scrolling sideways. A flex item's automatic minimum size is its content's,
   so without them the toolbar's intrinsic width propped this panel open and
   the container overflowed the viewport on every screen too narrow for both
   columns — the left one is a fixed width and does not shrink. The phone
   breakpoint already said exactly this for the same reason; it belongs in the
   base rule, where it costs a wide screen nothing. */
.chart-panel {
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  position: sticky;
  top: 2rem;
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
}

.chart-toolbar {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  font-size: 0.85rem;
}

.chart-toolbar-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.chart-toolbar label {
  font-weight: 500;
  font-size: 0.72rem;
  flex: 0 0 auto;
  width: 7rem;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.09em;
}

.chart-toolbar select {
  flex: 0 0 auto;
  width: 10rem;
  padding: 0.42rem 0.65rem;
  border: 1px solid var(--rule);
  border-radius: 3px;
  font-family: inherit;
  font-size: 0.85rem;
  background: var(--paper-fade);
  color: var(--ink);
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.chart-toolbar select:hover { border-color: var(--ink-soft); }

.chart-toolbar select:focus {
  outline: none;
  border-color: var(--ink);
  background: #fff;
  box-shadow: 0 0 0 3px var(--focus-glow);
}

.chart-toolbar #zoom {
  flex: 0 0 auto;
  width: 10rem;
  cursor: pointer;
}

.chart-toolbar #zoom-value {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  min-width: 3em;
  text-align: right;
  color: var(--ink-soft);
}

#chart {
  display: block;
  transform-origin: top left;
  background: var(--paper-fade);
  border: 1px solid var(--rule);
  border-radius: 2px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    0 8px 20px -10px rgba(30, 20, 10, 0.28);
}

/* --- Responsive --- */

@media (max-width: 820px) {
  .container {
    flex-direction: column;
    padding: 1.25rem;
    gap: 1.25rem;
  }
  .left-column {
    flex: none;
    width: 100%;
  }
  .chart-panel {
    position: static;
    width: 100%;
  }
}

/* --- Phone screens: shrink controls to fit narrow viewports --- */

@media (max-width: 540px) {
  .container {
    padding: 0.85rem;
    gap: 1rem;
  }
  .left-column,
  .chart-panel {
    min-width: 0;
    width: 100%;
    max-width: 100%;
  }
  .panel {
    padding: 1rem 1rem;
    min-width: 0;
    max-width: 100%;
  }
  .panel h2 {
    font-size: 1.35rem;
    margin-bottom: 1rem;
  }

  /* Settings rows: stack label above full-width input */
  .notation-row,
  .base-note-row {
    flex-direction: column;
    align-items: stretch;
    gap: 0.35rem;
  }
  .notation-row label,
  .base-note-row label {
    width: auto;
    font-size: 0.68rem;
  }
  .notation-row select,
  .base-note-row select {
    width: 100%;
  }

  /* Editor settings rows: stack label above full-width control, like Settings */
  .scale-name-row,
  .interval-type-row,
  .edo-settings-row,
  .scale-mode-row {
    flex-direction: column;
    align-items: stretch;
    gap: 0.35rem;
  }
  .scale-name-row label,
  .interval-type-row label,
  .edo-settings-row label,
  .scale-mode-row label {
    width: auto;
    font-size: 0.68rem;
  }
  .edo-input-stack { width: 100%; }

  /* Editor rows: shrink note-name and right-cluster, tighten gaps.
     --row-gap moves here too: .row's own gap reads var(--row-gap) already
     (the base rule), so redefining it on #editor is what keeps the cluster's
     width formula (interval-label-cluster, above) consistent with the actual
     note-row layout at this width — a hardcoded .row gap here would let the
     two drift apart again. */
  #editor {
    --name-box-width: 5.5rem;
    --well-size: 28px;
    --row-gap: 0.4rem;
  }

  .note-row .note-name {
    width: 5.5rem;
    font-size: 0.82rem;
    padding: 0 0.5rem;
  }
  .note-row .cumulative-cents {
    width: auto;
    flex: 0 1 auto;
    padding-left: 0.35rem;
    font-size: 0.7rem;
  }

  .interval-row {
    gap: 0.35rem;
    padding-left: calc(28px + 0.4rem + 30px + 0.4rem);
  }
  .interval-row .interval {
    width: 4rem;
    font-size: 0.82rem;
    padding: 0.38rem 0.45rem;
  }
  .interval-row .cents-label {
    min-width: 0;
    font-size: 0.66rem;
  }
  .interval-row .interval-label {
    font-size: 0.76rem;
    padding: 0 0.45rem;
  }

  /* Absolute-mode inputs match the relative interval width */
  .note-row .absolute-interval {
    width: 4rem;
    font-size: 0.82rem;
    padding: 0.38rem 0.45rem;
  }
  .note-row .abs-cents-label {
    min-width: 0;
    font-size: 0.66rem;
  }
  .interval-row .relative-cents-display {
    width: calc(4rem + 0.35rem + 3rem);
    font-size: 0.72rem;
  }

  /* Spine shifts left to match the new interval-row padding */
  #editor::before {
    left: calc(28px + 0.4rem + 14.5px);
  }

  /* Chart toolbar */
  .chart-toolbar-row { gap: 0.45rem; }
  .chart-toolbar label {
    width: 5.25rem;
    font-size: 0.68rem;
  }
  .chart-toolbar select,
  .chart-toolbar #zoom {
    width: auto;
    flex: 1 1 0;
    min-width: 0;
  }
}

/* --- Motion --- */

@media (prefers-reduced-motion: no-preference) {
  /* `backwards`, not `both`: the fill is only wanted *before* the animation
     starts, to hold the panel hidden through its delay. A forwards fill would
     leave `transform: translateY(0)` applied for good — a transform, even an
     identity one, makes the panel a stacking context, and a picker's
     `z-index: 100` cannot then escape the editor panel it is drawn in. The
     chart panel, its equal in the root stacking context and later in the
     document, would paint over it — which is exactly what a phone shows, where
     the chart sits under the editor rather than beside it. */
  .panel {
    animation: panel-rise 0.55s cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
  }
  .editor-panel { animation-delay: 0.06s; }
  .chart-panel  { animation-delay: 0.12s; }

  @keyframes panel-rise {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}
