@font-face {
  font-family: "ScaleDegreesTimes-Roman";
  src: url("/fonts/scale-degrees-times-roman.ttf") format("truetype");
  font-style: normal;
  font-weight: 400;
  font-display: block;
}

/*
 * Nunito Sans, by Vernon Adams and Jacques Le Bailly, under the SIL Open Font
 * License - the licence travels in src/fonts/NUNITO-SANS-OFL.txt.
 *
 * This is the face musictheorytree.com is set in, byte-identical to the file
 * the site serves, so the app and the website read as one thing. It is a
 * VARIABLE font: one 31 KB file covers the whole 200-900 axis, which is why
 * every weight below is real rather than a name for the same rendering.
 *
 * Myriad Pro Bold, declared below, stays. It is the face Aaron set the artwork
 * in and the Maps still draw their pitch letters with it; what it no longer
 * does is the interface's work.
 */
@font-face {
  font-family: "Nunito Sans";
  src: url("/fonts/nunito-sans.woff2") format("woff2");
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "MyriadPro-Bold";
  src: url("/fonts/myriad-pro-bold.otf") format("opentype");
  font-style: normal;
  font-weight: 700;
  font-display: block;
}

@font-face {
  font-family: "SourceSansRoman-Bold";
  src: url("/fonts/source-sans-roman-bold.otf") format("opentype");
  font-style: normal;
  font-weight: 700;
  font-display: block;
}

/* Kalam, by the Indian Type Foundry, under the SIL Open Font License. The
   licence travels with it in src/fonts/KALAM-OFL.txt. */
@font-face {
  font-family: "Kalam";
  src: url("/fonts/Kalam-Regular.ttf") format("truetype");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Kalam";
  src: url("/fonts/Kalam-Bold.ttf") format("truetype");
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}

@font-face {
  font-family: "DejaVuSans-Bold";
  src: url("/fonts/dejavu-sans-bold.ttf") format("truetype");
  font-style: normal;
  font-weight: 700;
  font-display: block;
}

:root {
  color-scheme: dark;
  /* Named once. This used to repeat the stack that --font-ui already holds,
     so the face had to be changed in two places to change at all. */
  font-family: var(--font-ui);
  background: #111914;
  color: #f7f4ed;
  /*
   * The interface face, and only the interface's.
   *
   * This asked for "Nunito Sans" once before, with no @font-face and no file,
   * so everything fell back to the system UI font; it was then pointed at
   * Myriad Pro Bold, the face Aaron set the artwork in. That fixed the fallback
   * and created two other problems. Myriad Pro Bold is a SINGLE-WEIGHT file, so
   * measured at 13.6px it renders identically at 400, 500, 600, 700 and 800 -
   * every weight distinction below was written down and none of it reached the
   * screen. And it is Adobe commercial, which is a broader use as an interface
   * font than as a reproduction of the artwork.
   *
   * Nunito Sans is what the website is set in, it is SIL OFL, and its 600 is
   * within 0.1% of Myriad Bold's width - so the width-bound toolbar did not
   * move when this changed. Its x-height is within 2.1%, so nothing needed
   * resizing either; caps run 6% taller, which is a small gain at 13.6px.
   */
  --font-ui: "Nunito Sans", "Segoe UI", Arial, sans-serif;
  --mtt-forest-green: #355e3b;
  --mtt-brown: #5e3935;
  --mtt-lighter-green: #a4dead;
  --mtt-c: #ff6267;
  --mtt-d: #ff925c;
  --mtt-e: #ffe96a;
  --mtt-f: #45c97a;
  --mtt-g: #67c9ef;
  --mtt-a: #6574c8;
  --mtt-b: #c05ab8;
}

* {
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  min-width: 320px;
  min-height: 100%;
  margin: 0;
}

body {
  overflow: hidden;
  background: #111914;
}

/*
 * Form controls do not inherit a font family from their container, so each has
 * to be named. `input` was missing, which left the two number fields in the
 * Guitar panel rendering in Arial - a fallback nobody chose, and the only thing
 * in a menu that was not on the interface face.
 */
button,
select,
input {
  font: inherit;
}

button {
  min-height: 2.45rem;
  padding: 0.62rem 0.85rem;
  border: 1px solid var(--control-border);
  border-radius: 0.72rem;
  background: var(--surface);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: 0.85rem;
  /* 850 was heavier than any label in here needs, and heavier still by the time
     a light glyph has bloomed into a dark ground. See .menu-note. */
  font-weight: 700;
  cursor: pointer;
  transition: background 130ms ease, border-color 130ms ease, transform 130ms ease;
}

button:hover:not(:disabled),
select:hover {
  border-color: var(--mtt-forest-green);
  background: var(--control-hover);
}

button:active:not(:disabled) {
  transform: translateY(1px);
}

button:focus-visible,
select:focus-visible,
[role="button"]:focus-visible {
  outline: 3px solid var(--mtt-lighter-green);
  outline-offset: 2px;
}

button:disabled {
  cursor: wait;
  opacity: 0.64;
}

#mtt-single-orbit-app {
  /* The page, its border area and the trim keep the MTT green; only the
     diagram canvas below goes black. */
  --page-background: #111914;
  --surface: #1b291f;
  --surface-strong: #1b291f;
  --surface-muted: #22382a;
  --text-primary: #f7f4ed;
  --text-muted: #b8c9bb;
  /* Used by the menu headings and the Tonic & Scale labels, and never defined
     until now: an undefined custom property makes the whole declaration
     invalid, so both of them had been inheriting --text-primary by accident
     and looking right by luck. */
  --text-strong: #ffffff;
  --border-subtle: rgba(164, 222, 173, 0.2);
  /* What the Notation Map draws with: the staff's own ink, so the type in the
     readout matches the lines beside it. */
  --notation-ink: #ffffff;
  /* The readout's frame, a shade off the ink. It is furniture rather than
     notation - it says where to look, and it is drawn whether or not anything
     is sounding - so at full strength it competed with the staff it stands
     under. Mixed from the ink rather than named, so it stays a shade off
     whichever ink the theme is using: a grey on the dark ground, and a touch
     lighter than black on the light one. */
  --notation-frame: color-mix(in srgb, var(--notation-ink) 62%, transparent);
  --control-border: rgba(164, 222, 173, 0.28);
  --control-hover: #294332;
  --seam-line: rgba(164, 222, 173, 0.5);
  --pill-inset: #16241b;
  /* There is no bright-green token any more. #45c97a was briefly shared by the
     Map switches and the checkboxes as one "this is live" colour; on the menu
     ground it read as a colour from outside the theme, so both moved to the
     forest green a selected button wears and the bright green stayed on the
     MIDI lamp alone, where it is written literally and pinned by a test. */
  /* How far the reveal icon sits up from the bottom edge in Presentation Mode.
     It used to be page padding as well, holding the whole stage up off the foot
     of the screen - which cost a band of empty ground along the bottom of every
     Presentation Mode screen and every capture taken from one, in both themes.
     The stage runs to the edge now; only the icon is inset. */
  --reveal-gutter: 0.75rem;
  /* The lattice's node convention, for anything that carries a drawn pitch
     class on its own ground rather than on the page's. The chip sits in the
     toolbar, which is chrome, so it holds the dark convention in both themes
     along with everything else in the bar. */
  --chip-ground: #000000;
  --chip-rule: #ffffff;

  /* --- The two grounds ----------------------------------------------------
     Every display area in the application is painted from one of these two
     colours: a Map on the stage, and any sample of a Map housed inside an
     explainer panel. Named here because Eye Comfort View has to reach all of
     them at once, and because a ground written literally somewhere else is a
     ground Eye Comfort cannot find. Nothing that is chrome uses them.

     A Map whose artwork has no authored light layer keeps --ground-dark under
     both themes; see the light block below for which Maps those are. */
  --ground-dark: #000000;
  /* True white, not an off-white. Light mode exists to be captured with Print
     Screen and taken to a physical printer, and anything short of #ffffff
     prints as a panel of flat tone across the page. Eye Comfort View is what
     makes it comfortable to look at; turning it off is what makes it print. */
  --ground-light: #ffffff;

  /* The diagram canvas is pure black so there is no seam against the
     diagram's own black ground. The page keeps its MTT green. */
  --canvas-background: var(--ground-dark);
  --glass: #121c15;
  --legend-heading: #a4dead;
  /* The direction copy inside the interval key used to take a token from
     here. It is inside a .map-housing now - it is type on a Map rather
     than type on a panel - so it takes --housing-ink-muted, which follows
     the ground it is actually drawn on. Only the heading above the housing
     is still panel type, and it is the only one left here. */
  --shadow-canvas: 0 22px 64px rgba(0, 0, 0, 0.38);
  /* How large the control panel is drawn, as a multiple of its authored size.
     The panel, its menus and the border around the stage all answer to it, so
     the frame stays in proportion with the bar it frames - shrinking the bar
     alone inside an unchanged border would look like a mistake. Whatever the
     chrome gives up, the stage takes. */
  --control-scale: 1;
  /* The height of the readout window, named once because two things are sized
     from it: the window itself, and the reveal icon, which sits in the same
     band so that it can never be over a Map. */
  --readout-height: 2.45rem;
  /* Scrollbar thickness, shared by the vertical Piano's viewport and the
     Torus's bar so the two instruments wear the same furniture. 9px is the
     Piano's authored 6px and half again. */
  --scrollbar-thickness: 9px;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100dvh;
  min-height: 0;
  /* Nothing but a gutter under the button row. The frame that used to run
     along the top and the sides was decoration: it took width from the Maps on
     every side and its rounded corners cropped the diagrams it was framing.
     The stage is rectangular and runs to the edges of the screen. */
  padding: 0 0 calc(0.35rem * var(--control-scale));
  overflow: hidden;
  color: var(--text-primary);
  background: var(--page-background);
  transition: color 180ms ease, background 180ms ease;
}

/* --- Light mode -----------------------------------------------------------
 *
 * Light mode is not a second skin over the whole application. It is a second
 * ground for the Maps, and nothing else.
 *
 * The control panel, the menus, the toolbar and the explainer panels wear one
 * set of colours in both modes - the dark set, which is the one they were
 * authored in. What changes is the ground a Map is drawn on, and the ink the
 * Maps drawn on it take. So this block is short by design: every token it used
 * to carry that described chrome rather than a display area has gone back to
 * the single declaration above, and anything added here later should have to
 * argue that it is part of a Map before it is allowed in.
 *
 * Which Maps answer to it, as the artwork stands:
 *
 *   PC Lattice        light layer authored - ink-outlined bands, black rings
 *   Notation          light layer authored - staff and readout take dark ink
 *   Tree, Dual-Orbit  separate light Illustrator layer, on white paper
 *   Piano             draws its own black and white; reads on either ground
 *   Circle            draws its own black and white; reads on either ground
 *   Tree, Single-Orbit   no light layer - keeps its dark artwork on black
 *   Torus of 5ths        no light layer - keeps its dark artwork on black
 *
 * The last two hold --ground-dark under both themes, below. They are not
 * oversights; they are Maps that have not been designated for light mode yet,
 * and a Map is designated by having artwork drawn for it, never by recolouring
 * the dark drawing.
 */
#mtt-single-orbit-app[data-theme="light"] {
  --canvas-background: var(--ground-light);
  /* What the Notation Map draws with. It sits on the canvas rather than on the
     panel, so it is a display-area colour and it follows the ground.
     --notation-frame is mixed from it and follows on its own. */
  --notation-ink: #050706;
}

/* --- Eye Comfort View ------------------------------------------------------
 *
 * On by default, and light mode only.
 *
 * A full-screen #ffffff is punishing to work in for any length of time, and
 * light mode is the one people will sit in longest, because it is the one they
 * build printed material from. Eye Comfort takes the ground down without
 * touching a single authored colour: the ink, the bands, the pitch classes and
 * the rainbow fans are all exactly what they will print as, and only the paper
 * they sit on has moved.
 *
 * #e8eee7 is not a new colour. It is the darker end-stop of the gradient the
 * light canvas used before it went true white - measured at 15.9% below white,
 * and still 17.14:1 against the light ink, well past AAA. So Eye Comfort View
 * is the ground light mode already had, made flat and even; turning it off is
 * what is new.
 *
 * It reaches every display area at once because it moves --ground-light, and
 * --ground-light is the only place a light ground is written down.
 */
#mtt-single-orbit-app[data-theme="light"][data-eye-comfort="on"] {
  --ground-light: #e8eee7;
}

#mtt-single-orbit-app[data-ready="false"] .diagram-transform {
  visibility: hidden;
}


/* ---------------------------------------------------------------------------
   Layout: one stage, one row of buttons

   The diagram gets every pixel the toolbar does not need. Controls live in
   menus that open as overlays above the toolbar, so opening one never changes
   the size or position of anything on the stage. Both the full and
   presentation views use this same shell.
   ------------------------------------------------------------------------ */

.app-workspace {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 0.5rem;
  width: 100%;
  min-height: 0;
  margin: 0 auto;
}

.stage {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}

/* Zoom rather than a font-size cascade: every size in the panel is authored in
   rem, so scaling one root value would move the whole page instead. Zoom scales
   an element's own layout and reflows around it, which is exactly what has to
   happen for the stage to gain the height the bar gives up. */
.toolbar,
.menu {
  zoom: var(--control-scale);
}

.toolbar {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: wrap;
  /* The stage runs to the edges of the screen; the row of controls under it
     does not. Without the frame that used to hold it in, the mark and the
     fullscreen button sat hard against the glass. Stated in rem inside a
     zoomed element, so it answers to the control scale like everything else in
     here. */
  gap: 0.5rem 0.9rem;
  padding-inline: 0.75rem;
  align-items: center;
  justify-content: space-between;
}

.toolbar-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  align-items: center;
  margin-left: auto;
}

.toolbar-divider {
  align-self: center;
  width: 1px;
  height: 1.55rem;
  margin-inline: 0.18rem;
  background: var(--border-subtle);
}

.toolbar-divider-small {
  height: 1rem;
  margin-inline: 0.08rem;
}

.toolbar-button {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  padding: 0.45rem 0.8rem;
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: 0.85rem;
  /* 800 had no effect on the single-weight face this used to be set in.
     On a variable face it is real, and small light-on-dark text at 800
     blooms into the ground - weight comes down here, not up. 700 is the
     top of the interface ladder. */
  font-weight: 700;
  cursor: pointer;
}

.toolbar-button:hover {
  border-color: var(--border-strong, var(--border-subtle));
}

.toolbar-button[aria-expanded="true"] {
  background: var(--mtt-primary, #355e3b);
  border-color: var(--mtt-primary, #355e3b);
  color: #fff;
}

.toolbar-icon-button {
  justify-content: center;
  width: 2.3rem;
  height: 2.3rem;
  padding: 0;
  font-size: 1.2rem;
}

/* Familiar icons may stand alone, but their names remain available on hover
   and keyboard focus. The bubble opens upward because the toolbar sits at the
   bottom of the app. */
.toolbar [data-tooltip] {
  position: relative;
}

.toolbar [data-tooltip]::before,
.toolbar [data-tooltip]::after {
  position: absolute;
  z-index: 20;
  left: 50%;
  visibility: hidden;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, 0.2rem);
  transition: opacity 120ms ease, transform 120ms ease, visibility 0s linear 120ms;
}

.toolbar [data-tooltip]::before {
  bottom: calc(100% + 0.34rem);
  width: 0;
  height: 0;
  border: 0.35rem solid transparent;
  border-top-color: var(--text-strong);
  content: "";
}

.toolbar [data-tooltip]::after {
  bottom: calc(100% + 1rem);
  width: max-content;
  max-width: min(18rem, calc(100vw - 2rem));
  padding: 0.45rem 0.62rem;
  border-radius: 0.55rem;
  background: var(--text-strong);
  color: var(--page-background);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
  content: attr(data-tooltip);
  font-family: var(--font-ui);
  font-size: 0.85rem;
  font-weight: 600;
  line-height: 1.25;
  text-align: center;
  white-space: normal;
}

.toolbar [data-tooltip]:is(:hover, :focus-visible)::before,
.toolbar [data-tooltip]:is(:hover, :focus-visible)::after {
  visibility: visible;
  opacity: 1;
  transform: translate(-50%, 0);
  transition-delay: 240ms;
}

.toolbar [data-tooltip-align="end"]::before {
  right: 0.8rem;
  left: auto;
  transform: translateY(0.2rem);
}

.toolbar [data-tooltip-align="end"]::after {
  right: 0;
  left: auto;
  transform: translateY(0.2rem);
}

.toolbar [data-tooltip-align="end"]:is(:hover, :focus-visible)::before,
.toolbar [data-tooltip-align="end"]:is(:hover, :focus-visible)::after {
  transform: translateY(0);
}

/* The current tonic and the scale it heads, on one ground.
   The tonic is drawn, not typed - the pitch class in its saturated written-letter
   colour, the way the MTT draws its own centre - and the scale is named beside
   it. Ground and rule follow the lattice's node convention: black with a white
   ring under a dark theme, white with a black ring under a light one.
   It replaced a bare circle, which had room for the tonic and nothing else.
   Deliberately taller than the line it sits on, so the current key reads at a
   glance. The negative block margin keeps it from making the button - and with
   it the whole row, and with that the stage - any taller than it already is. */
/*
 * The scale selector is a pill: a title, then the window holding the value.
 *
 * It used to be the chip first and a hamburger after it, on the reasoning that
 * the tonic and scale WERE the button's name and the hamburger said the pair
 * was selectable. Steve reversed that on 9 September: every other control in
 * the row says what it is on the left and shows its state on the right, and
 * this one read as the odd one out.
 *
 * The title is inset 0.8rem from the pill's leading edge and the gap after it
 * is the same 0.8rem, so the word sits evenly in its own half. That gap is
 * doing a job a Map pill gets for free: there, the last thing before the seam
 * is the Map's MARK, and the 12px of air around it is what keeps the name off
 * the seam. A word has no such air - its ink runs to its box - so the space is
 * put back explicitly here. Measured on the Trees pill: 7.2px from the name to
 * the mark and 4.8px from the mark to the seam.
 */
.tonic-scale-button {
  gap: 0.8rem;
  padding-left: 0.8rem;
  padding-right: 0;
}

/* The same weight and the same optical nudge every pill name carries; the
   `pill-name` class alongside is what supplies the 0.03em. */
.tonic-scale-title {
  font-weight: 700;
}

.tonic-scale-chip {
  display: inline-flex;
  gap: 0.36rem;
  align-items: center;
  /* Stretched to the button's own edge, so the title and the window read as
     two parts of one control. The negative block margins cancel the
     button's padding rather than restating its height, so the chip still fits
     if the button's type or padding changes.

     The white rule is gone: it was the lattice's node convention, drawn to
     separate a node from the canvas. Here the ground it separates from is the
     button, the chip is already the only black thing in the bar, and at this
     size the rule read as a highlight round the tonic rather than as an edge. */
  align-self: stretch;
  margin-block: -0.45rem;
  margin-inline-end: 0;
  padding-inline: 0.9rem 0.72rem;
  border: 0;
  border-radius: 999px;
  background: var(--chip-ground);
  /* The same seam the map pills wear where their switch begins - one bright
     line on the LEADING edge, which is what makes a section read as set into
     the button rather than printed on it. It was on the trailing edge while
     the hamburger followed the chip; with the window at the trailing end it
     turns round, so this control is drawn the same way as every pill beside
     it. Drawn as a shadow rather than a border for the same reason it is
     there: a border on one side tapers away where the corners round, leaving
     the arc as a pair of slivers, while a shadow follows the silhouette and
     stays continuous top to bottom. */
  filter: drop-shadow(-1.5px 0 0 var(--seam-line));
}

.pill-menu-icon {
  flex: 0 0 auto;
  font-size: 1.1rem;
  font-weight: 700;
  line-height: 1;
}

.midi-status-light {
  box-sizing: border-box;
  width: 0.54rem;
  height: 0.54rem;
  flex: 0 0 auto;
  border: 1.5px solid currentColor;
  border-radius: 50%;
  background: transparent;
}

.midi-button[data-midi-state="connected"] .midi-status-light {
  border-color: #45c97a;
  background: #45c97a;
  box-shadow: 0 0 0.42rem rgba(69, 201, 122, 0.72);
}

/* Height fixed, width free. Every spelling's lockup is the same height - the
   letter is taller than any accidental - so holding the height holds the letter
   at one size, and the box widens on its own for the accidental beside it. */
.tonic-scale-chip-pitch {
  display: inline-flex;
  align-items: center;
  height: 1.42rem;
}

.tonic-scale-chip-art {
  display: block;
  width: auto;
  height: 100%;
}

.tonic-scale-chip-name {
  color: var(--chip-rule);
  font-family: var(--font-ui);
  font-size: 0.85rem;
  font-weight: 700;
  white-space: nowrap;
}

/*
 * Someone who has asked their system for more contrast gets it.
 *
 * Every secondary colour in here already clears WCAG AA, and most of it clears
 * AAA - but "passes" is not the same as "comfortable", and a person who has set
 * this preference is telling us which of the two they need. The muted inks give
 * up their dimming entirely; nothing about size or weight changes, because
 * those are already set for legibility rather than for effect.
 */
@media (prefers-contrast: more) {
  #mtt-single-orbit-app {
    --text-muted: var(--text-primary);
    --readout-ink-muted: var(--readout-ink);
  }

  .menu-row-stacked button[aria-pressed="true"] em {
    color: inherit;
  }
}

/* --- Presentation Mode ---------------------------------------------------- */

/*
 * Everything that is not a diagram gets out of the way: the bar, its buttons,
 * the title, the border round the stage and the page padding that framed it.
 * What is left is the canvas, edge to edge, in whichever colour the current
 * theme paints it - black under the dark theme, the pale ground under the
 * light one. The Maps re-fit into the space, so they grow rather than sitting
 * in the middle of a larger emptiness.
 *
 * The Maps stay, and with them the readout, which travels inside Notation now
 * rather than along the foot: it is content rather than control, and a stream
 * showing what is being played wants it.
 */
#mtt-single-orbit-app[data-controls="hidden"] {
  /* Nothing. Presentation Mode is the mode a screen is captured and broadcast
     from, so the stage runs to all four edges - a strip of ground along the
     bottom is a strip of ground in every printed page.

     It used to keep a gutter here, for two reasons that did not survive being
     looked at. It was said to hold the stage clear of the taskbar, but a
     browser viewport already ends above the taskbar and never reaches it. And
     it gave the reveal icon a strip of its own so the icon was never over a
     Map - which is real, but it is the icon's requirement and it is paid for
     by every screen that has no icon on it. The icon is inset from the bottom
     edge by --reveal-gutter instead, and lies over the corner of the Map. */
  padding: 0;
  background: var(--canvas-background);
}

#mtt-single-orbit-app[data-controls="hidden"] .app-workspace {
  gap: 0;
}

/* The bar is emptied rather than removed, so the readout keeps its place at the
   foot of the screen and nothing below the stage jumps when the mode changes. */
#mtt-single-orbit-app[data-controls="hidden"] .app-title,
#mtt-single-orbit-app[data-controls="hidden"] .toolbar-buttons {
  display: none;
}

/* No padding of its own, so that with nothing sounding the row collapses to
   nothing at all rather than leaving a band along the foot of the capture. */
#mtt-single-orbit-app[data-controls="hidden"] .toolbar {
  padding: 0;
  background: transparent;
}

/* --- The Notation Map: a staff and a chord readout ------------------------ */

/*
 * One pane, two halves, stacked. The readout sits below by default and above
 * on request; column-reverse is what turns it over, so neither half has to
 * know where it ended up.
 *
 * Both heights are set during layout, in px, from the Map's own scale - see
 * the notation branch of applyLayout. Nothing here is a proportion of the
 * pane, because the staff's box is a fixed number of staff spaces and the
 * readout's frame is another, and they have to stay in that ratio.
 */
.notation-pane {
  display: flex;
  flex-direction: column;
  /* Both halves together fill the pane exactly, so this does nothing until one
     of them is switched off - and then it centres the other where it stands
     rather than letting it fall to an end. The padding is the Map's margin,
     set in px during layout beside the heights. */
  justify-content: center;
  width: 100%;
  height: 100%;
}

.notation-pane[data-readout="above"] {
  flex-direction: column-reverse;
}

.staff-container {
  display: block;
  flex: 0 0 auto;
  width: 100%;
  height: 100%;
}

/*
 * A frame, not a window. It is drawn whether or not anything is sounding, so
 * the space reads as reserved rather than empty - you know to expect something
 * there. Fixed in size for the same reason: an outline that resized itself
 * around each chord would jump about as you played.
 */
.readout-frame {
  display: grid;
  flex: 0 0 auto;
  align-content: center;
  justify-items: center;
  min-width: 0;
  overflow: hidden;
  border: 1px solid var(--notation-frame);
  border-radius: 0.6rem;
}

#mtt-single-orbit-app[data-notation-readout="off"] .readout-frame {
  display: none;
}

#mtt-single-orbit-app[data-notation-staff="off"] .staff-container {
  display: none;
}

.staff-svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* --- The reveal dock ------------------------------------------------------ */

/*
 * Positioned from the top-left in fractions of the viewport, so a dragged
 * position survives a resize in proportion instead of being measured against an
 * edge that has moved. It starts bottom-right, which is where the mode leaves
 * it until someone picks it up.
 */
/*
 * The dock is exactly the icon, cut to the height of the readout band and
 * parked in it. Anywhere higher and it would sit over a Map - over the top
 * octave of the piano, most visibly - and the band is the one strip of the
 * screen that is never a diagram.
 *
 * It scales with the control panel by arithmetic rather than by zoom, because
 * zoom would scale the coordinate system its own fixed position is resolved in
 * and slide it away from the corner it is supposed to hold.
 */
/*
 * The icon fills the readout band and the whole band is lifted off the foot of
 * the screen, rather than the icon giving the gutter up out of its own height.
 * The first attempt did the latter and cost the icon a third of its size for a
 * gutter still too small to read as one; moving the band instead buys the room
 * from the stage, which has it to spare, and costs the icon nothing.
 */
.reveal-dock {
  --reveal-size: calc(var(--readout-height) * var(--control-scale));
  position: fixed;
  left: var(--reveal-x, calc(100vw - var(--reveal-size) - 0.55rem));
  top: var(--reveal-y, calc(100dvh - var(--reveal-size) - var(--reveal-gutter)));
  z-index: 40;
  width: var(--reveal-size);
  height: var(--reveal-size);
  touch-action: none;
}

.reveal-button {
  display: grid;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: grab;
  place-items: center;
  /* A ring of ground behind the icon, so it reads on a black stage and on a
     pale one without carrying a border of its own. */
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.55));
  -webkit-user-select: none;
  user-select: none;
}

.reveal-button:active {
  cursor: grabbing;
}

.reveal-button img {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.reveal-dock[data-dragging="true"] .reveal-button {
  cursor: grabbing;
}

/* One bounce, up and down, on arrival. Long enough to catch the eye and over
   before it becomes something to wait through. */
@keyframes reveal-bounce {
  0%,
  100% {
    transform: translateY(0);
  }
  30% {
    transform: translateY(-0.85rem);
  }
  55% {
    transform: translateY(0.12rem);
  }
  75% {
    transform: translateY(-0.2rem);
  }
}

.reveal-button[data-bounce="true"] {
  animation: reveal-bounce 720ms ease-in-out 1;
}

@media (prefers-reduced-motion: reduce) {
  .reveal-button[data-bounce="true"] {
    animation: none;
  }
}

.reveal-hint {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}

/* Whichever side has room: an icon parked at the left edge would otherwise
   carry its message off the screen. */
.reveal-dock[data-hint-side="left"] .reveal-hint {
  right: calc(100% + 0.55rem);
}

.reveal-dock[data-hint-side="right"] .reveal-hint {
  left: calc(100% + 0.55rem);
}

.reveal-hint,
.escape-hint {
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  background: var(--glass);
  color: var(--text-primary);
  font-family: var(--font-ui);
  /* These carry instructions, and one of them is the only place the right-click
     option is ever mentioned, so they are set well above the floor rather than
     at it - and lighter, for the same reason every small light-on-dark string
     in here is lighter. See .menu-note. */
  font-size: calc(0.95rem * var(--control-scale));
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 320ms ease;
}

.reveal-hint[hidden],
.escape-hint[hidden] {
  display: none;
}

.reveal-hint[data-fading="true"],
.escape-hint[data-fading="true"] {
  opacity: 0;
}

/* Said in the middle of the foot rather than beside the icon, because the icon
   is exactly what is not there when this one is needed. */
.escape-hint {
  position: fixed;
  bottom: 1.6rem;
  left: 50%;
  z-index: 40;
  margin: 0;
  transform: translateX(-50%);
}

.escape-hint kbd {
  font-family: inherit;
}

.reveal-menu {
  position: fixed;
  z-index: 41;
  padding: 0.25rem;
  border: 1px solid var(--border-subtle);
  border-radius: 0.6rem;
  background: var(--surface);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
}

.reveal-menu[hidden] {
  display: none;
}

.reveal-menu button {
  padding: 0.35rem 0.7rem;
  border: 0;
  border-radius: 0.4rem;
  background: transparent;
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}

.reveal-menu button:hover {
  background: var(--control-hover);
}

/* --- Menus ---------------------------------------------------------------- */

.menu-layer {
  /* Spans the whole stage so a menu has real room to grow upward; it is
     click-through, so only the open menu itself receives pointer events. */
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
}

.menu {
  --menu-section-gap: 0.72rem;
  --menu-control-gap: 0.45rem;
  --menu-toggle-gap: 0.35rem;
  position: absolute;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  gap: var(--menu-section-gap);
  width: min(20rem, calc(100vw - 2rem));
  max-height: 100%;
  padding: 0.85rem;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: none;
  border: 1px solid var(--border-subtle);
  border-radius: 1.2rem;
  background: var(--surface);
  box-shadow: 0 18px 46px rgba(0, 0, 0, 0.45);
  pointer-events: auto;
}

.menu[hidden] {
  display: none;
}

.menu-wide {
  width: min(26rem, calc(100vw - 2rem));
}

.menu-heading {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  justify-content: space-between;
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--text-strong);
}

.menu-row {
  display: flex;
  gap: 0.4rem;
}

/* A `hidden` attribute on a menu row did nothing without this. The browser's
   own `[hidden] { display: none }` lives in the user-agent origin, so any
   author rule that sets `display` beats it - and the rule directly above sets
   `display: flex` on every row. Ready for Print Screen was marked hidden in
   dark mode and drew anyway, 39px tall and clickable, which is how a control
   that exists to prepare a page for a printer ended up available on a black
   ground. Every other class in this file that sets `display` already carries
   its own [hidden] pair; this is the one that was missing. */
.menu-row[hidden] {
  display: none;
}

.menu-row button {
  flex: 1 1 auto;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--border-subtle);
  border-radius: 0.7rem;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.85rem;
  /* `font: inherit` above is a SHORTHAND, so it resets weight to the inherited
     400. That was invisible while the interface was set in a single-weight
     face - every weight rendered the same - and became real the moment it moved
     to a variable one. A control's label sits one step above a note and one
     below a section: 600, the same as the labels naming it. */
  font-weight: 600;
  cursor: pointer;
}

.menu-row button[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}

.menu-row button[aria-pressed="true"] {
  background: var(--mtt-primary, #355e3b);
  border-color: var(--mtt-primary, #355e3b);
  color: #fff;
}

.toggle-control {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  font-size: 0.85rem;
}

/*
 * One checkbox, everywhere: a small selected button.
 *
 * The interface had two - the browser's own at 13px in every menu, and Draw's
 * at 20px on a green of its own. Draw's larger proportions won on review; 13px
 * is small for a control hit mid-lesson. 19px is where it settled, once the
 * fill went from a bright green to this softer one and the box could carry a
 * little more size without shouting.
 *
 * It is drawn rather than accented, because `accent-color` sets only the fill
 * and this needs to match a selected button on both counts. Checked is exactly
 * what `Below` wears in the Notation menu: a #355e3b fill with a #355e3b
 * border and white content. Unchecked is exactly what its unselected
 * neighbour wears: no fill, and the same faint --border-subtle edge.
 *
 * So the tick is what carries, the way a selected button's white label carries
 * it - white on the forest fill is 7.46:1, where the fill against the menu
 * ground is only 2.04:1. That is the same bargain every selected button in
 * here already makes, which is the point: this reads as the theme rather than
 * as a control borrowed from outside it.
 *
 * The radius is 0.34rem, which is 19px times the ratio a 39.2px button's
 * 11.2px radius has to its own height - so the box is a button's corner at a
 * button's proportion rather than at a button's measurement. It moves with
 * the size; do not pin one and change the other.
 */
#mtt-single-orbit-app input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 19px;
  height: 19px;
  margin: 0;
  flex: 0 0 auto;
  border: 1px solid var(--border-subtle);
  border-radius: 0.34rem;
  background: transparent;
  background-position: center;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  cursor: pointer;
  transition: background-color 130ms ease, border-color 130ms ease;
}

#mtt-single-orbit-app input[type="checkbox"]:checked {
  border-color: var(--mtt-forest-green);
  background-color: var(--mtt-forest-green);
  /* The tick is drawn rather than typed: a glyph would take whatever face the
     page fell back to, and at 19px the difference between one face's check and
     another's is the difference between a tick and a smudge. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23ffffff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' d='M3.6 8.4l2.9 2.9 5.9-5.9'/%3E%3C/svg%3E");
}

#mtt-single-orbit-app input[type="checkbox"]:disabled {
  cursor: default;
  opacity: 0.64;
}

.menu .toggle-control {
  padding-block: 0.22rem;
}

.toggle-control[hidden] {
  display: none;
}

/* Three settings of one control, so they stack rather than sharing a row: each
   contains the one above it, and reading down is reading outward from the key. */
.menu-row-stacked {
  flex-direction: column;
}

/* The name carries the weight; what it means trails it quietly on the same
   line, so the list can be scanned by its three names alone. */
.menu-row-stacked button em {
  color: var(--text-muted);
  font-size: 0.85rem;
  font-style: normal;
}

/* Dimmed once, by colour. It used to be dimmed twice - an inherited colour and
   an opacity on top of it - which made it the lowest-contrast text in the app
   at 5.05:1, and made the value something you had to multiply out to see. */
.menu-row-stacked button[aria-pressed="true"] em {
  color: #d7e5da;
}

.menu-row-stacked button em::before {
  content: " - ";
}

.piano-controls {
  display: flex;
  flex-direction: column;
  gap: var(--menu-section-gap, 0.72rem);
}

.layout-priority-row {
  flex-wrap: wrap;
}

.layout-priority-row button {
  flex: 1 1 calc(50% - 0.4rem);
}

.instrument-controls {
  display: flex;
  flex-direction: column;
  gap: var(--menu-section-gap, 0.72rem);
}

.instrument-controls[hidden] {
  display: none;
}

.menu::-webkit-scrollbar {
  display: none;
}

.menu select::picker(select)::-webkit-scrollbar {
  display: none;
}

[data-piano-side-controls]:not([hidden]) {
  display: flex;
  flex-direction: column;
  gap: var(--menu-section-gap, 0.72rem);
}

/* There is only one view. `/present` remains a working address so existing
   OBS sources and embeds keep resolving, but it renders identically. */

/* ---------------------------------------------------------------------------
   Interactive MTT piano

   The piano floats in the empty space the diagram already leaves beneath the
   orbit. It has no panel, backdrop, or border of its own, and showing it never
   resizes or moves the diagram.
   ------------------------------------------------------------------------ */

.piano-viewport {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 2;
  display: flex;
  align-items: flex-end;
  /* Horizontal pianos always scale into their assigned pane. Scrolling is
     reserved for long vertical DAW-style keyboards. */
  overflow-x: hidden;
  overflow-y: hidden;
  background: none;
  /* The piano owns its own pointer gestures; the diagram must not pan or zoom
     underneath it. */
  touch-action: pan-x;
  cursor: default;
}

/* Vertical piano: the same authored keyboard turns into a DAW-style rail.
   Pitch rises bottom-to-top; the keys point into the canvas from the chosen
   side. One and two octaves fit the stage, while longer ranges keep that key
   scale and scroll along the vertical pitch axis. */
.piano-viewport[data-orientation="vertical"] {
  top: 0;
  right: auto;
  bottom: 0;
  left: 0;
  width: var(--piano-band);
  flex-direction: column;
  align-items: stretch;
  overflow-x: hidden;
  overflow-y: hidden;
  touch-action: pan-y;
}

.piano-viewport[data-orientation="vertical"][data-side="right"] {
  right: 0;
  left: auto;
}

.piano-viewport[data-orientation="vertical"][data-scroll="true"] {
  overflow-y: auto;
}

/* Restrained overflow: no browser-like scrollbar treatment. Painted through
   the parts rather than `scrollbar-color`, which Chromium honours instead of
   them - and which cannot be given an exact thickness. */
.piano-viewport::-webkit-scrollbar {
  height: var(--scrollbar-thickness);
  width: var(--scrollbar-thickness);
}

@supports not selector(::-webkit-scrollbar) {
  .piano-viewport {
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.28) transparent;
  }
}

.piano-viewport::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.28);
}

.piano-viewport::-webkit-scrollbar-track {
  background: transparent;
}

.piano-container {
  display: block;
  /* The container is the flex item, so the auto margins that centre a keyboard
     narrower than the canvas belong here rather than on the SVG. */
  margin-inline: auto;
}

.piano-viewport[data-orientation="vertical"] .piano-container {
  margin-block: auto;
  margin-inline: 0;
}

.piano-svg {
  display: block;
  height: auto;
  /* No width or height cap: the keyboard is sized by its own zoom and the band
     is sized to match, so clamping here would fight the zoom - it made zooming
     in shrink the piano instead of growing it. The viewport scrolls when the
     keyboard is wider than the space available. */
}

/* Hit regions stay large for touch without changing the visible artwork. */
.piano-svg [data-piano-key] {
  cursor: pointer;
  outline: none;
}

/* CSS draws an outline round the element's bounding box, never round its path,
   so on a white key the focus ring cut straight across the black keys sitting
   over it. The ring is a stroke on the key's own shape instead, defined with
   the roll-over inside the SVG where the keyboard's stroke width is known.
   Turning the outline off has to be explicit: the general focus ring for
   [role="button"] would otherwise paint its rectangle here. */
.piano-svg [data-piano-key]:focus-visible {
  outline: none;
}

@media (max-width: 640px) {
  .app-title {
    display: none;
  }

  /* Tighter padding on a narrow screen, but the type holds its size. Shrinking
     text on the smallest screens is the opposite of what those screens need;
     the row wraps instead, and the control-panel size is there for anyone who
     would rather trade legibility for stage. */
  .toolbar-button {
    padding: 0.4rem 0.6rem;
  }

  .menu {
    width: calc(100vw - 1.6rem);
  }

}


.select-control {
  display: grid;
  gap: var(--menu-control-gap, 0.45rem);
  width: 100%;
}

.select-control select {
  display: block;
  width: 100%;
}

.select-control select {
  min-height: 3.35rem;
  padding: 0.65rem 0.85rem;
  border: 1px solid var(--control-border);
  border-radius: 0.9rem;
  background: var(--surface);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: 0.9rem;
  /* 800 had no effect on the single-weight face this used to be set in. On a
     variable face it is real, and a dropdown's value is a control's label
     rather than a heading - it sits with the buttons at 600, not with the
     panel heading at 700. */
  font-weight: 600;
  cursor: pointer;
}

/* The Circle needs a little more width than the other control menus, but it
   follows the same balanced spacing rhythm. */
.menu[data-menu-panel="circle"] {
  width: min(22rem, calc(100vw - 2rem));
}

.circle-menu-group,
.circle-menu-help {
  display: grid;
  gap: var(--menu-control-gap);
}

.circle-menu-group {
  padding-block: 0.1rem;
}

.circle-menu-group .control-label {
  margin: 0;
}

.circle-menu-group > .menu-note,
.circle-menu-help .menu-note {
  margin: 0;
}

.circle-reset-row {
  padding-block: 0.1rem;
}

@supports (appearance: base-select) {
  .menu select,
  .menu select::picker(select) {
    appearance: base-select;
  }

  .menu select {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .menu select::picker-icon {
    margin-left: auto;
  }

  .menu select::picker(select) {
    position-area: block-end span-inline-end;
    position-try-fallbacks: none;
    inline-size: anchor-size(width);
    max-height: min(18rem, 50vh);
    margin-block-start: 0.4rem;
    padding: 0.35rem;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: none;
    border: 1px solid var(--control-border);
    border-radius: 0.9rem;
    background: var(--surface);
    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.45);
  }

  .menu select option {
    min-height: 2.8rem;
    padding: 0.7rem 0.85rem;
    border-radius: 0.65rem;
  }

  .menu select option:hover,
  .menu select option:focus-visible {
    background: var(--control-hover);
  }

  .menu select option:checked {
    background: var(--mtt-primary, #355e3b);
    color: #ffffff;
  }
}

/*
 * A heading belongs to what follows it, and spacing is what says so.
 *
 * The menu lays its children out with one uniform gap, so a label's own margins
 * are what break the tie. These were 1.6px on top and 7.2px underneath, which
 * with the gap came to 8.8px above and 14.4px below - the heading sat nearer
 * the block it had nothing to do with. They are reversed: roughly 18px of air
 * above it, and about 5px between it and the control it names.
 *
 * The bottom margin is negative because the gap it is cancelling belongs to the
 * parent. Setting it to zero would still leave the full gap underneath.
 */
/*
 * A label owns the gap to the control it names.
 *
 * The menus lay their children out with one uniform 11.52px gap, so a label and
 * the control it belongs to sat exactly as far apart as two unrelated controls
 * did - nothing bound the pair together but reading order. The label now pulls
 * its own control up and pushes the group before it away, which is the same
 * trick the menu headings already use: roughly 17px above, 7px below.
 *
 * The bottom margin is negative because the gap it is cancelling belongs to the
 * parent, not to the label. Measured in the running app at -0.3rem the pair
 * closed to 2.4px, which reads as touching; -0.15rem lands it at 4.8px, the
 * "roughly 5px below" this file already asks for.
 */
.control-label {
  display: block;
  margin: 0.35rem 0 -0.15rem;
  color: var(--text-muted);
  font-size: 0.85rem;
  /* 800 at 12px on this ground filled its own counters in. See .menu-note. */
  font-weight: 600;
  letter-spacing: 0.01em;
}

/* Most menus pair a label and its control as adjacent siblings. Pull the
   second item into the smaller control gap while keeping the larger gap
   between complete settings. */
.menu > .control-label + :is(.menu-row, .zoom-controls, .toggle-control),
.instrument-controls > .control-label + :is(.menu-row, .zoom-controls, .toggle-control),
[data-piano-side-controls] > .control-label + :is(.menu-row, .zoom-controls, .toggle-control),
.menu-section > .menu-subheading + .toggle-control,
.menu > .menu-note,
.instrument-controls > .menu-note {
  margin-top: calc(var(--menu-control-gap) - var(--menu-section-gap));
}

.menu .toggle-control + .toggle-control {
  margin-top: calc(var(--menu-toggle-gap) - var(--menu-section-gap));
}

.zoom-controls {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.6rem;
}

.zoom-controls button {
  min-width: 0;
  min-height: 2.7rem;
  padding: 0.4rem;
  border-color: transparent;
  background: var(--surface-muted);
  font-size: 1.05rem;
}

.zoom-controls .zoom-text-button {
  font-size: 0.85rem;
}

.zoom-controls output {
  min-width: 3.3rem;
  color: var(--text-primary);
  font-size: 0.85rem;
  font-weight: 650;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.button-stack {
  display: grid;
  gap: 0.45rem;
}

.button-stack button {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.55rem;
  width: 100%;
  min-height: 2.75rem;
  text-align: left;
}

.button-stack button span {
  display: inline-grid;
  width: 1.15rem;
  place-items: center;
  font-size: 1.05rem;
}

.diagram-viewport {
  position: relative;
  width: 100%;
  height: 100%;
  min-height: 0;
  overflow: hidden;
  background: var(--canvas-background);
  touch-action: none;
  cursor: grab;
}

.diagram-viewport[data-panning="true"] {
  cursor: grabbing;
}

/* Linked sizing for bottom and side pianos is solved in JavaScript. The grid
   stays stage-sized so every absolute Map rectangle uses one coordinate system
   and no CSS subtraction is accidentally applied a second time. */

/* A full-width keyboard meets the canvas corners, so they square off while the
   piano is showing and round again when it is hidden. */
.diagram-viewport[data-piano="true"] {
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}


.diagram-transform {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  transform-origin: 50% 50%;
  will-change: transform;
}

.piano-viewport[data-orientation="vertical"] .piano-svg {
  width: auto;
}

/* Option A clarity prototype: Tree zoom lives on an internal SVG group.
   Neutralize the old compositor path so the root SVG stays fitted to its
   stage-solved pane. */
#mtt-single-orbit-app[data-tree-variant] .diagram-transform {
  transform: none;
  will-change: auto;
}

.diagram-transform > svg {
  display: block;
  width: 100%;
  height: 100%;
  /* No width cap: the 880px ceiling meant that in fullscreen, where there is
     far more height to use, the diagram stayed width-bound and left a band of
     slack above and below. Uncapped it stays height-bound and fits tight to
     the borders at any viewport size. */
  /* Single-Orbit has no authored light layer, so the Tree keeps its black
     ground under both themes and Eye Comfort never reaches it. */
  background: var(--ground-dark);
}

/* Dual-Orbit carries two separately authored appearance layers.  The dark map
   sits on the canonical black screen ground; the print-oriented light map sits
   on white paper.  Neither is produced by recolouring the other. */
.diagram-transform > svg[data-mtt-variant="dual"][data-theme="light"] {
  background: var(--ground-light);
}

/* The white inner routes are filled slivers that taper to a point where each
   fan converges.  Below WHITE_ROUTE_DETAIL_FLOOR their tips are thinner than a
   pixel, and a shape that narrows to nothing renders as a smooth neutral ramp -
   a grey wedge lying in the fan.  Drop them there and keep them everywhere
   there is room to draw them.  See applyDualOrbitDetail in
   src/music/dual-orbit.js for the measurements behind the floor. */
.diagram-transform > svg[data-mtt-variant="dual"][data-dual-detail="coarse"]
  [data-dual-white-route-layer="true"] {
  display: none;
}

/* Dual-Orbit activation is carried by exact SVG counterparts.  Their coloured
   paint uses the same approved active palette as Single-Orbit; each twin keeps
   the contour and contrast authored specifically for its dark or light layer. */

.diagram-transform .mtt-pitch-hit {
  cursor: pointer;
  outline: none;
}

.diagram-transform .mtt-pitch-hit > circle {
  fill: transparent;
  stroke: transparent;
  stroke-width: 2px;
  pointer-events: all;
}

.diagram-transform .mtt-pitch-hit:focus-visible > circle {
  stroke: #fff;
}

.diagram-transform > svg[data-mtt-variant="dual"][data-theme="light"] .mtt-pitch-hit:focus-visible > circle {
  stroke: #050706;
}

.midi-status {
  margin: 0;
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: 0.78rem;
  font-weight: 700;
  line-height: 1.5;
}

.visually-hidden {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------------------------------------------------------------------------
   Maps on the stage

   The Maps share one stage. JavaScript positions exact, aspect-preserving
   rectangles from a pure row/column/mosaic solver; the piano remains a band
   underneath when linked. Each Map's proportional slot provides the space on
   both sides of it, so two neighbours meet across the sum of those spaces.
   ------------------------------------------------------------------------ */

.map-grid {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
}

.map-pane {
  position: absolute;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

.map-pane[hidden] {
  display: none;
}

.lattice-container,
.lattice-container > svg,
.circle-container,
.circle-container > svg,
.guitar-container,
.guitar-container > svg,
.torus-container,
.torus-container > svg {
  display: block;
  width: 100%;
  height: 100%;
}

.circle-container {
  touch-action: none;
  user-select: none;
  cursor: grab;
}

[data-circle-options][hidden] {
  display: none !important;
}

[data-circle-node] {
  cursor: pointer;
  outline: none;
}

/* The shared role=button focus rule outlines an SVG group's rectangular
   bounds. Circle focus is instead drawn by .circle-focus on the key's exact
   annular path, so Shift-click and keyboard focus retain the key shape. */
.circle-container [data-circle-node]:focus-visible {
  outline: none;
}

.circle-active,
[data-circle-node][data-active="true"] .circle-rest {
  display: none;
}

[data-circle-node][data-active="true"] .circle-active {
  display: inline;
}

.circle-hover,
.circle-focus {
  opacity: 0;
}

[data-circle-style="piano"] [data-circle-node]:hover .circle-hover {
  opacity: 0.12;
}

[data-circle-style="piano"] [data-circle-node]:focus-visible .circle-focus {
  opacity: 1;
}

[data-circle-style="lattice"] [data-circle-node]:hover .circle-hit,
[data-circle-style="lattice"] [data-circle-node]:focus-visible .circle-hit {
  stroke-width: 3;
}

.lattice-container,
.guitar-container,
.torus-container {
  touch-action: none;
  cursor: grab;
}

.torus-container {
  position: relative;
  overflow: hidden;
}

/* The bar sits beside the board, not over it: the natural box reserves a strip
   at the right from two octaves up (see torusReservesScrollGutter) and the
   camera keeps that strip clear, so the bar never covers a cell and never has
   to hide to get out of the way. That is what makes the vertical Piano's bar
   easy to find, and this is that same bar - same thickness, same rounded white
   thumb on a clear track - carried over to the Torus.

   The strip is half a cell wide, which is more than the bar needs at any size
   the board is drawn at; the remainder stays clear between board and bar. */
.torus-scrollbar {
  position: absolute;
  z-index: 4;
  top: 0;
  right: 0;
  bottom: 0;
  width: var(--scrollbar-thickness);
  overflow-x: hidden;
  overflow-y: scroll;
  color-scheme: dark;
  background: transparent;
  cursor: default;
  touch-action: pan-y;
}

.torus-scrollbar::-webkit-scrollbar {
  width: var(--scrollbar-thickness);
}

.torus-scrollbar::-webkit-scrollbar-track {
  background: transparent;
}

.torus-scrollbar::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.28);
}

@supports not selector(::-webkit-scrollbar) {
  .torus-scrollbar {
    scrollbar-color: rgba(255, 255, 255, 0.28) transparent;
    scrollbar-width: thin;
  }
}

.torus-scrollbar > [data-torus-scroll-content] {
  width: 1px;
  min-height: calc(100% + 1px);
}

.lattice-container:active,
.torus-container:active {
  cursor: grabbing;
}

[data-torus-fifths-for][hidden] {
  display: none !important;
}

/* The first Torus pass is the approved dark artwork. Its ground stays black in
   the light app theme until Steve chooses the Illustrator layers for the true
   light version; the SVG itself carries no rounded-card backing. Being on
   --ground-dark is what says so: it is not a Map light mode has been given
   yet, so Eye Comfort has nothing to do here either. */
.torus-container > svg {
  background: var(--ground-dark);
}

.torus-active {
  display: none;
}

[data-torus-occurrence][data-active="true"] .torus-rest {
  display: none;
}

[data-torus-occurrence][data-active="true"] .torus-active {
  display: inline;
}

[data-torus-cell] {
  cursor: pointer;
  outline: none;
}

[data-torus-piano-key] {
  cursor: pointer;
  outline: none;
}

[data-torus-cell]:focus-visible > rect {
  stroke: #a4dead;
  stroke-width: 4px;
}

[data-torus-piano-key]:focus-visible > [data-torus-piano-hit] {
  stroke: #a4dead;
}

/* ---------------------------------------------------------------------------
   Map pills

   One control per Map: the name and its hamburger open the Map's menu, and the
   switch beyond the curved divider turns the Map on and off. The divider is the
   switch section's own left radius bowing into the pill, so the two halves read
   as one button rather than two.
   ------------------------------------------------------------------------ */

.map-pill {
  --pill-height: 2.3rem;
  --seam-overlap: 0.62rem;
  display: inline-flex;
  align-items: stretch;
  height: var(--pill-height);
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--border-subtle);
}

.map-pill .pill-main {
  display: inline-flex;
  gap: 0.45rem;
  align-items: center;
  min-height: 0;
  height: 100%;
  /* Right padding clears the seam's deepest bulge and then leaves a gap. The
     extra was 0.62rem when a hamburger sat in it; the hamburgers are gone and
     the Map's mark stands where they did, which needs less room beside the
     curve than a glyph that had to be read. Reclaiming it across the pills is
     most of what kept the row on one line beside the app mark. */
  padding: 0 calc(var(--seam-overlap) + 0.3rem) 0 0.8rem;
  border: 0;
  border-radius: 999px 0 0 999px;
  background: transparent;
  color: var(--text-primary);
  font-size: 0.85rem;
  /* 800 had no effect on the single-weight face this used to be set in.
     On a variable face it is real, and small light-on-dark text at 800
     blooms into the ground - weight comes down here, not up. 700 is the
     top of the interface ladder. */
  font-weight: 700;
}

/* Centre the visible letters, not just the font's line box.
 *
 * This nudge was measured against Myriad Pro, which is no longer the interface
 * face - it went to Nunito Sans - and the number did not move with it. Myriad
 * needed 0.1em; Nunito needs 0.03em, so every interface label in the toolbar
 * was sitting 0.07em low, which is 0.95px at the 13.6px they are set in.
 *
 * It showed up as Trees looking too HIGH rather than as the rest looking low,
 * because Trees is the one pill name in Georgia and Georgia is excluded here -
 * so the 0.07em error was visible as a step between one label and its six
 * neighbours rather than as a row that had shifted together.
 *
 * Re-measured by painting each face at 100px and scanning for the first and
 * last row carrying ink, rather than from the metrics API, which disagreed:
 *
 *   Nunito Sans 700, "Lattice": ink 0.72em above the baseline to 0.00em below,
 *   so it centres at 0.36em; its font box centres at 0.33em. Difference 0.03em.
 *
 *   Georgia 700, "Trees": ink 0.69em to 0.01em below, centring at 0.34em
 *   against a 0.35em box centre. Difference -0.01em, which is 0.14px here -
 *   below anything that carries meaning, so Georgia stays excluded.
 *
 * Em units preserve the alignment when the control panel scales, without
 * moving icons or buttons. Re-measure this if the interface face changes. */
.pill-name:not(.brand-serif),
.toolbar-label,
/* The scale selector puts two words side by side in one control - the title
   and the value - and the eye lines those up on their shared BASELINE, not on
   each one's own optical centre. Nudging only the title moved it 0.406px below
   the value, which is small and was spotted immediately. Both move together
   now, so each is optically centred in the control AND they sit on one line. */
.tonic-scale-chip-name {
  position: relative;
  top: 0.03em;
}

/*
 * A menu that covers two things keeps them apart, so a switch reads as
 * belonging to the staff or to the readout rather than to the Map at large.
 * The sections lay out as though they were not there: the panel is one grid,
 * and a section that opened a box of its own would break its rhythm.
 */
.menu-section {
  display: contents;
}

/*
 * A section divides with a rule, not by shouting.
 *
 * It used to be uppercase at 0.8rem - the SMALLEST thing in a panel and the
 * loudest, which inverted the hierarchy: the panel heading above it and the
 * toolbar buttons above that are both sentence case, so the one item in the
 * system in caps sat in the middle of the ladder. It was also the only text in
 * the interface below the 13.6px floor.
 *
 * Sentence case at the floor now, carried by weight and ink, with a hairline
 * doing the dividing. Using a LINE for structure leaves the type ladder saying
 * only one thing - importance - which is what lets the whole interface keep a
 * single case.
 */
.menu-subheading {
  margin-top: 1.15rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--control-border);
  color: var(--text-strong);
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}


/* Draw holds a place for a feature that is not built yet.
 *
 * It is dimmed and it never moves, which is the honest state: the row's shape
 * is settled now so that nothing shifts when the Map lands. `cursor: wait` is
 * what the shared disabled rule gives it, and that is a lie here - nothing is
 * coming while you hover - so it takes the plain arrow instead. */
.map-pill[data-not-built="true"] {
  opacity: 0.45;
}

.map-pill[data-not-built="true"] button:disabled {
  cursor: default;
  opacity: 1;
}

.map-pill .pill-main[aria-expanded="true"] {
  background: var(--mtt-forest-green);
  color: #fff;
}

/* The seam is the switch section's own leading edge: both its left corners are
   rounded to exactly half the pill's height, so they meet in the middle and the
   border on that edge draws one continuous arc. Pulling the section back over
   the one before it lets that arc bow into the name rather than butt against
   it, and it repeats the curve of the switch it introduces. */
.map-pill .pill-switch,
.map-pill .pill-menu {
  display: inline-flex;
  align-items: center;
  height: 100%;
  min-height: 0;
  margin-left: calc(-1 * var(--seam-overlap));
  padding: 0 0.72rem 0 0.9rem;
  border: 0;
  /* A stadium, so the leading edge is a true semicircle. Mixing a small radius
     with 999px does not work: the browser scales every corner by one factor
     taken from the largest pair, which flattened this edge to a straight line. */
  border-radius: 999px;
  background: var(--pill-inset);
  color: var(--text-muted);
  /* The seam is drawn as a shadow rather than a border. A border on one side
     only tapers away where the corners round, which left the curve as a pair of
     slivers; a shadow follows the element's actual silhouette, so the arc is
     continuous from top to bottom. */
  filter: drop-shadow(-1.5px 0 0 var(--seam-line));
  transition: background 130ms ease;
}

.map-pill .pill-menu[aria-expanded="true"] {
  background: var(--mtt-forest-green);
  color: #fff;
}

.switch-track {
  position: relative;
  display: inline-block;
  width: 1.95rem;
  height: 1.02rem;
  border-radius: 999px;
  background: var(--control-hover);
  /* Sunk, so the knob above it reads as sitting proud of the track. */
  box-shadow:
    inset 0 2px 3px rgba(0, 0, 0, 0.55),
    inset 0 -1px 0 rgba(255, 255, 255, 0.08);
  transition: background 130ms ease;
}

/* A sphere, not a disc: the highlight sits up and to the left of centre, the
   shading falls away to the lower right, and a contact shadow anchors it to the
   track. */
.switch-knob {
  position: absolute;
  top: 0.12rem;
  left: 0.14rem;
  width: 0.78rem;
  height: 0.78rem;
  border-radius: 999px;
  background: radial-gradient(
    circle at 34% 28%,
    #ffffff 0%,
    #e6ece7 26%,
    #a9b9ab 62%,
    #758577 84%,
    #5c6a5e 100%
  );
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.55),
    inset 0 -1px 1px rgba(0, 0, 0, 0.3),
    inset 0 1px 1px rgba(255, 255, 255, 0.55);
  transition: transform 150ms ease, background 150ms ease;
}

.pill-switch[aria-checked="true"] .switch-track {
  background: var(--mtt-forest-green);
}

.pill-switch[aria-checked="true"] .switch-knob {
  background: radial-gradient(
    circle at 34% 28%,
    #ffffff 0%,
    #e8fbec 22%,
    #a4dead 58%,
    #5f9e6e 84%,
    #40734d 100%
  );
  transform: translateX(0.89rem);
}

/* A switch that is on lights the section it sits in.
 *
 * The knob moving is a small change at the far end of a wide bar, and with
 * seven of these the row could not be read at a glance - which is the whole
 * job of the toolbar while Steve is teaching on camera. So the oval gap around
 * the toggle carries the state instead.
 *
 * It is the green a selected button wears, so "on" is one colour across the
 * whole interface. This was #45c97a, the MIDI lamp's bright green, which read
 * as a colour from outside the theme once the menus used it too. That choice
 * is a deliberate trade and the numbers are worth keeping in view: lit against
 * unlit measured 7.59:1 in the bright green and 2.16:1 in this one, against
 * 1.35:1 when only the small track changed. The knob's travel and the moat
 * around it are doing more of the work again at this contrast.
 *
 * The inset glow stays and matters more here than it did: it lifts the rim so
 * the section still reads as filled rather than merely darker. It is inset
 * because the section runs to the pill's own rounded edge, and an outward halo
 * would spill over the border onto the bar.
 *
 * The seam is a pale green line chosen against the dark inset. Lit, it takes a
 * dark seam instead, so the section keeps the edge that makes it read as set
 * into the pill rather than printed on it. */
.map-pill .pill-switch[aria-checked="true"] {
  background: var(--mtt-forest-green);
  box-shadow: inset 0 0 0.5rem rgba(255, 255, 255, 0.28);
  filter: drop-shadow(-1.5px 0 0 rgba(0, 0, 0, 0.45));
}

/* The mark each button carries: the DIN-5 socket on MIDI, and one
 * drawing per Map on the pills.
 *
 * Sized in em rather than px so it grows with the control panel scale along
 * with the words beside it, and painted in currentColor so it takes the
 * button's own ink - including the dimmed ink while the button is disabled
 * mid-connect, which a hard-coded fill would have ignored.
 *
 * 1.65em against a 0.85rem button puts a mark at about 22.4px - still taller than
 * the 13.6px type beside it, which is the point: a mark drawn to the letters'
 * cap height measures the same as they do and reads as smaller, because a round
 * outline carries less ink than a stack of stems. The three glyph buttons -
 * Standalone toolbar glyphs are type, not marks, and are not sized here.
 *
 * The Map marks are 24x24 like the socket and drawn on the same weights, so
 * the different drawings still read as one set: a 1.9-unit outline, a 1.5-unit
 * inner rule and a 1.6-unit dot. They were compared at 64px, 30px and at this
 * size before any of them was chosen; several drawings that were the truest to
 * their Map lost to ones that survived being small.
 *
 * The DIN-5 pins were drawn at four sizes and compared before these numbers were
 * fixed. Held on a radius of 5.2 with a pin radius of 1.75 they fused into one
 * solid arc - 0.48 units apart, which is a third of a pixel at toolbar size -
 * and the mark read as a filled blob. At 5.6 and 1.6 they clear each other by
 * 1.09 units and stay five separate pins down to 24px. Dropping the keyway
 * cleaned it up further and cost the connector: without it the mark is five
 * dots in a ring rather than a socket. */
.button-icon {
  width: 1.65em;
  height: 1.65em;
  flex-shrink: 0;
  fill: currentColor;
}

/* The DIN-5 runs smaller than the Map marks on purpose. It is the only mark in
 * the row that is a filled disc of detail rather than an outline - a ring, five
 * pins and a keyway - so at the Maps' size it carries far more ink than they do
 * and reads as the heaviest thing in the bar. Two thirds of a step down puts
 * its weight back among them. */
.midi-button .button-icon {
  width: 1.38em;
  height: 1.38em;
}

.toolbar-disclosure-icon {
  font-size: 1.1em;
}

.toolbar-icon {
  padding: 0.45rem 0.62rem;
}

/* ---------------------------------------------------------------------------
   The readout

   A line of type inside the Notation Map, framed rather than windowed. It is
   centred while it fits and falls back to starting at the left when it does
   not, so a long chord loses its tail rather than both ends.
   ------------------------------------------------------------------------ */

.readout {
  /* A grid holding one line, so the line can be centred in the window while
     everything inside it still sits on a shared baseline. Making the window
     itself the flex container could do one or the other, not both, which is
     why the pitches hung towards the top. */
  display: grid;
  align-content: center;
  /* The window runs the width of the row: from the logo to the buttons, with
     the row's own column gap left either side of it. It used to stop at 48rem
     and float in the middle, which left two unequal gaps and made the row look
     unresolved on a wide screen. */
  width: 100%;
  min-width: 0;
  padding: 0 0.85rem;
  overflow: hidden;
  white-space: nowrap;
  font-family: var(--font-ui);
  /* Sized from the Map's own scale, so the chord is the size of the names
     beside the staff. Set on the pane during layout, in px per staff space. */
  font-size: var(--readout-font, 1rem);
  /* It used to be a black window in the toolbar, lit from inside in both
     themes. On the stage it is part of a Map, so it takes the theme like the
     staff beside it: white ink on the dark ground, dark ink on the light one. */
  --readout-ink: var(--notation-ink);
  --readout-ink-muted: color-mix(in srgb, var(--notation-ink) 62%, transparent);
}

/* Centred while it fits, but a readout too long for the space falls back to
   starting at the left edge. Plain centring clips both ends, which loses the
   lowest note as well as the highest. */
.readout {
  justify-content: safe center;
}

.readout-line {
  display: flex;
  gap: 0.55rem;
  /* Everything in the window sits on one baseline: pitches, chord symbol, and
     the words after it. */
  align-items: baseline;
  min-width: 0;
  overflow: hidden;
  /* Centring the line box is not the same as centring what you can see. The
     line sits on a baseline and almost nothing in it has a descender, so the
     box carries empty descent underneath the ink and the pitches read as pushed
     up against the top of the window. The line is nudged down by half the
     difference to put the ink itself in the middle. Re-measure it if the type
     in here is ever resized - the descent scales with the font.

     Measured with the box exactly centred, on a slash chord so the deepest
     descenders are in play: 7.7px above the ink, 11.22px below. It was 14.7px
     below when the chord detail was set at 12.8px; raising that to 14.4px for
     legibility filled more of the space under the baseline, so the nudge came
     down with it - from 0.22rem to 0.11rem.

     A nudge rather than padding on the window: padding shortens the content box
     and the line, being a scroll container, is compressed to match and starts
     clipping its own descenders. Offsetting changes no layout at all. */
  position: relative;
  top: 0.11rem;
}

/*
 * Standing alone, the readout takes two rows instead of one.
 *
 * With the staff gone the frame is a stave's height, which is room enough to
 * set the pitches out on a row of their own and put the chord's name under
 * them - the order a reader takes them in. Side by side they had to share one
 * line's width, and the rule between them was doing the work of the line
 * break. Stacked, the rule has nothing to divide and goes.
 *
 * The type is the same size in both. What the extra room buys is a second row,
 * not bigger letters: a pitch is written at one height everywhere in this app.
 */
#mtt-single-orbit-app[data-notation-staff="off"] .readout-line {
  flex-direction: column;
  gap: var(--readout-row-gap, 0.2em);
  align-items: center;
  /* The nudge under it corrects a single baseline's empty descent. Two rows
     centre on their own block, and the correction would tip it. */
  top: 0;
}

#mtt-single-orbit-app[data-notation-staff="off"] .readout-pitches + .readout-chords {
  padding-left: 0;
  border-left: 0;
}

/* The window itself never hides. It is a permanent instrument panel, so an
   empty one still reads as "nothing sounding" rather than as missing chrome. */

.readout-pitches {
  display: flex;
  gap: 0.4rem;
  align-items: baseline;
  min-width: 0;
  overflow: hidden;
}

.readout-pitch,
.readout-chord,
.readout-alternate {
  display: inline-flex;
  gap: 0.06em;
  align-items: baseline;
}

.inline-pitch {
  display: inline-block;
  /* Sits on the line like a capital rather than hanging from the box top. */
  vertical-align: -0.02em;
}

.readout-pitch {
  /* One em: --readout-font is set from the Map's scale so that a lockup's cap
     height lands on GLYPH_LETTER_HEIGHT - the same height a name beside the
     staff is drawn at, and the same height as a pitch class orbiting the Tree. */
  font-size: 1em;
}

.readout-octave {
  color: var(--readout-ink);
  font-family: var(--font-ui);
  font-size: 0.78em;
  font-variant-numeric: tabular-nums;
}

/*
 * The scale degree, following the whole name.
 *
 * Set at the register's height, so the two things said about a letter are one
 * size and read as a pair of remarks rather than as three sizes in a row - the
 * same pairing the names beside the staff keep. Set off by a gap, because it
 * is a second thing said rather than part of the name; the register runs on
 * because it and the letter are one name between them.
 */
.readout-degree {
  display: inline-flex;
  margin-left: 0.28em;
  align-items: baseline;
}

.inline-degree {
  display: inline-block;
  /* The pitch SVG uses this optical baseline too. degree-inline.js centres the
     smaller degree inside the same cap-height box. */
  vertical-align: -0.02em;
}


.readout-chords {
  display: flex;
  gap: 0.4rem;
  align-items: baseline;
  min-width: 0;
  overflow: hidden;
}

/* The rule divides the pitches from the chord they make. With the staff
   showing there are no pitches here to divide from - it names them itself -
   so the rule would be left standing at the head of the line on its own. */
.readout-pitches + .readout-chords {
  padding-left: 0.55rem;
  border-left: 1px solid color-mix(in srgb, var(--readout-ink) 22%, transparent);
}

/* The chord was set smaller than the pitches beside it and its trailing detail
   smaller again, so the one line held three sizes and the name - the thing
   being read - was the middle one. It is brought up towards the pitches. */
.readout-chord {
  color: var(--readout-ink);
  font-size: 0.927em;
}

.readout-chord-suffix,
.readout-chord-bass {
  color: inherit;
  font-family: var(--font-ui);
  font-size: 0.86em;
}

.readout-chord[data-tentative="true"] {
  color: var(--readout-ink-muted);
}

.readout-chord-detail {
  color: var(--readout-ink-muted);
  font-size: 0.618em;
}

/* Dimmed once, like the description in the menus: the muted ink alone, with no
   opacity multiplied over it. */
.readout-alternate {
  color: var(--readout-ink-muted);
  font-size: 0.618em;
}

/* ---------------------------------------------------------------------------
   Harmonic Center wheel and other menu contents
   ------------------------------------------------------------------------ */

.menu-wheel {
  width: min(19rem, calc(100vw - 2rem));
}

/* Two choices of equal standing, so both get a heading of equal weight.
   Everywhere else in the app a control label is subordinate to the one heading
   at the top of the panel it sits in; this panel has no single subject to be
   subordinate to - the tonic and the scale are the same size of decision. */
.menu-wheel .control-label {
  color: var(--text-strong);
  font-size: 0.9rem;
  font-weight: 700;
}

.wheel-host {
  display: block;
  width: 100%;
}

.harmonic-center-wheel {
  display: block;
  width: 100%;
  height: auto;
}

.harmonic-center-wheel .wheel-centre {
  fill: var(--text-primary);
  font-family: "DejaVuSans-Bold", "DejaVu Sans", sans-serif;
  font-weight: 700;
}

/*
 * Secondary text, on the same floor as everything else that carries meaning.
 *
 * This was 11.5px at weight 700 and was the hardest thing in the app to read -
 * not for want of contrast, which measured 8.75:1, but for want of size. The
 * floor is 0.85rem because that is the size of the Map pill labels, which read
 * comfortably; anything that says something is set at least that large.
 *
 * The weight came DOWN rather than up. Light text on a dark ground halates: the
 * bright strokes bloom optically into the dark field, thickening the stems and
 * closing the counters, so at small sizes a bold weight stops reading as bold
 * and starts reading as a smear. Less weight, a little tracking and a looser
 * line are what open it up.
 */
.menu-note {
  margin: 0;
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: 0.85rem;
  /* A note is prose, and the quietest thing in a panel. 500 was a number with
     no effect on the single-weight face this used to be set in; on a variable
     one it reads as almost-bold beside a 600 label, which is not what a note
     is for. */
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: 0.01em;
}

@media (max-width: 900px) {
  .toolbar-divider {
    display: none;
  }

  .readout {
    display: none;
  }
}

/* ---------------------------------------------------------------------------
   The Guitar Map

   The fretboard draws its own black and white and reads on either ground, the
   way the Piano and the Circle do, so it follows the theme rather than holding
   one colour. The only colour on the board is MTT pitch state.
   ------------------------------------------------------------------------ */

.guitar-viewport {
  position: absolute;
  overflow: hidden;
  min-width: 0;
  min-height: 0;
  background: var(--ground-dark);
}

.guitar-viewport[hidden] { display: none; }

.guitar-container > svg {
  background: var(--ground-dark);
}

.guitar-marker-ground { fill: var(--ground-dark); }
[data-theme="light"] .guitar-marker-ground { fill: var(--ground-light); }

/* Fret numbers and the capo label are annotations rather than pitch content,
   so they are set in the interface face. Everything that names a pitch class
   or a scale degree is drawn from the authored outlines instead. A presentation
   attribute cannot read a custom property, which is why this is a class. */
.guitar-label {
  font-family: var(--font-ui, "Myriad Pro", system-ui, sans-serif);
  font-weight: 600;
}

[data-theme="light"] .guitar-viewport,
[data-theme="light"] .guitar-container > svg {
  background: var(--ground-light);
}

[data-guitar-position] {
  cursor: pointer;
  outline: none;
}

.guitar-hit {
  /* A generous target: the whole cell between two frets and half a string
     either side, whatever size the visible marker inside it has been fitted
     to. A marker at the 24th fret is a third the width of one at the first. */
  cursor: pointer;
}

/* Both states are drawn and one is shown, the way the Piano and the Torus do
   it, so activation never rebuilds the board. */
.guitar-active {
  display: none;
}

[data-guitar-position][data-active="true"] .guitar-rest {
  display: none;
}

[data-guitar-position][data-active="true"] .guitar-active {
  display: inline;
}

/* The focus ring follows the position's own hit rectangle rather than a box
   drawn round the whole group, on the same rule the piano's keys follow. */
[data-guitar-position]:focus-visible .guitar-hit {
  stroke: var(--focus-ring, #67c9ef);
  stroke-width: 6;
  fill: none;
}

/* Per-string tuning rows in the menu. One row per string, highest first, so
   the list reads the way the board is drawn. */
.guitar-string-list {
  display: grid;
  gap: 0.3rem;
}

.guitar-string-row {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 0.55rem;
}

.guitar-string-row select {
  min-width: 0;
}

.number-control {
  display: grid;
  gap: 0.2rem;
}

.number-control input {
  width: 100%;
  min-width: 0;
  /* A typed value is a control's label, like a button's or a dropdown's. */
  font-weight: 600;
}

/* ---------------------------------------------------------------------------
   Brand type

   Anywhere the interface names the MTT it is set in Georgia Bold, matching the
   authored artwork, which calls for the same family. Georgia is not
   redistributable, so this is the same system stack the approved SVG relies on.
   ------------------------------------------------------------------------ */

.brand-serif {
  font-family: Georgia, "Times New Roman", serif;
  font-weight: 700;
  letter-spacing: -0.008em;
}

/* Torus is part of the authored-map name even when it appears beside the
   regular interface face used for Piano. Its slight tightening is optical,
   matching the Georgia Bold treatment elsewhere in the MTT identity. */
.torus-interface-name {
  font-family: Georgia, "Times New Roman", serif;
  font-weight: 700;
  letter-spacing: -0.012em;
}


/* The icon leads the name, sized to the type rather than to a fixed box, so
   the lockup holds together at every control-panel scale. */
/*
 * The mark is measured against the name it leads, not against the row, so the
 * lockup keeps its proportions wherever the title is set - at every panel scale
 * and at every step of the title's own clamp.
 *
 * 1.65em stands a little taller than the wordmark's caps, which is what a mark
 * beside a wordmark should do, and at the tight gap it carries about the same
 * weight as the two words rather than leading them. It grew to 1.8em while the
 * gap was still wide; closing the gap made that much size read as heavy.
 *
 * It also stays well under the row's own height - the readout sets that at
 * 2.45rem - so the mark never becomes the thing that decides how tall the
 * control panel is, which would take the difference out of the stage.
 */
.app-mark {
  display: block;
  width: 1.65em;
  height: 1.65em;
  /* About the width of the space inside "MTT App" itself, so the mark joins the
     name's own rhythm and the three parts read as one lockup rather than as a
     picture set beside a title. */
  margin-right: 0.24em;
  flex: 0 0 auto;
}

/*
 * Two alignments, one inside the other.
 *
 * The two words share a baseline with each other - a serif MTT and a script
 * App, which only read as one name if they sit on one line. The mark does not
 * belong to that baseline: an image baselines on its own bottom edge, so
 * putting it in the same group set the words' baseline against the foot of the
 * mark and floated the whole name three pixels above its middle.
 *
 * So the words are baselined together in a box of their own, and the mark is
 * centred against that box, which is centred in the control panel.
 */
.app-name {
  display: inline-flex;
  align-items: baseline;
  /* The words alone, a little smaller than the em the mark is measured
     against, so the mark carries slightly more of the lockup than the type
     does. The mark is unaffected: it is sized off .app-title, not off this. */
  font-size: 0.94em;
  /* Nudged down against the mark, by eye rather than by measurement.
     Box-centred and ink-centred both put the two within a pixel of each other,
     but neither is what the eye is doing here: the mark is not a shape with a
     centre worth trusting - its stem runs to the top of the square while its
     mass sits low and left - so the alignment that measures right reads high.
     Relative, so it moves the words without moving anything around them. */
  position: relative;
  top: 0.05em;
}

.app-title {
  display: inline-flex;
  align-items: center;
  margin: 0;
  /* The whole lockup is set from here - the mark is 1.5em of it - so this one
     value takes the icon and the name down together. */
  font-size: clamp(0.92rem, 1.45vw, 1.2rem);
  line-height: 1.1;
  color: var(--text-primary);
  white-space: nowrap;
}

/* "MTT" cannot take one tracking value for both of its gaps. M-T can tuck,
   because the M ends on a vertical and the T's arm overhangs to the left; T-T
   cannot, because two arms meet and close up at the top. So the pair after the
   M is set separately from the pair after it. */
.brand-mtt {
  display: inline-block;
  letter-spacing: 0.028em;
}

.brand-mtt::first-letter {
  letter-spacing: -0.022em;
}

/* Optical, not metric: a trailing T already opens a gap under its arm, so the
   space before the script word is set tighter than a word space would be, and
   the script's own left side bearing closes the rest. */
.app-title .brand-mtt {
  margin-right: 0.14em;
}

.app-title .brand-script {
  display: inline-block;
  /* Kalam runs small on the body, so it is set up slightly to match Georgia's
     cap height, and given a shallow slant rather than a synthesised italic. */
  font-family: "Kalam", "Segoe Script", cursive;
  font-size: 1.18em;
  font-weight: 400;
  letter-spacing: 0.004em;
  transform: skewX(-6deg);
  transform-origin: 0 100%;
}

/* ---------------------------------------------------------------------------
   Interval direction key

   Ported with the component from the Pitch-Class Lattice:
     F:\Music Theory Tree\DEV\mtt-pitch-class-lattice\src\components\lattice\IntervalLegend.module.css
   Same rules, same names, no redesign.
   ------------------------------------------------------------------------ */

.menu-large {
  width: min(108rem, calc(100vw - 2rem));
  /* A reference panel this size has to be opaque. At the menus' usual
     translucency the diagram reads straight through the arrows and labels. */
  background: var(--surface-strong);
}

/* --- A Map's ground, inside a panel ----------------------------------------
 *
 * An explainer panel is chrome: it wears the one global colour scheme in both
 * modes. But a panel that shows you a piece of a Map has a problem the rest of
 * the chrome does not - the thing it is showing you is only true on the ground
 * that Map is drawn on. A lattice node is a ring with the canvas showing
 * through it; drawn on a panel it is a ring with a panel showing through it,
 * which is a different object.
 *
 * So the sample gets a housing: a rectangle of the display area itself, set
 * into the panel. In light mode it is the white page the Map prints on; in
 * dark mode it is the black screen the Map sits on. Both exist, and which one
 * shows is which mode you are in.
 *
 * It carries its own ink as well as its own ground, because type inside a
 * housing is type on a Map, not type on a panel - the global scheme's near
 * white would vanish into the light housing. Anything drawn inside a housing
 * should reach for --housing-ink and --housing-ink-muted and never for
 * --text-primary.
 *
 * Because the ground comes from --ground-light, a housing dims with Eye
 * Comfort exactly as the stage does. That is the point: what the explainer
 * shows you is what you will capture.
 */
.map-housing {
  --housing-ground: var(--ground-dark);
  --housing-ink: #ffffff;
  --housing-ink-muted: #b9dac0;
  padding: 1rem 0.75rem 1.25rem;
  border: 1px solid var(--border-subtle);
  border-radius: 0.6rem;
  background: var(--housing-ground);
}

/* The light housing takes the lattice's own light-appearance inks: the
   diagram ink the bands are outlined in, and the muted green the direction
   copy is set in. Both are the values the light theme used to hand the whole
   interface, kept here where they are still describing a display area. */
#mtt-single-orbit-app[data-theme="light"] .map-housing {
  --housing-ground: var(--ground-light);
  --housing-ink: #050706;
  --housing-ink-muted: #465f4a;
}

@media (prefers-contrast: more) {
  .map-housing {
    --housing-ink-muted: var(--housing-ink);
  }
}

.interval-legend {
  display: grid;
  gap: 0.8rem;
  padding-top: 0.25rem;
  color: var(--text-primary);
}

.legend-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 1rem;
  padding: 0 0.2rem;
}

.legend-heading h2 {
  margin: 0;
  color: var(--legend-heading);
  font-family: var(--font-ui);
  font-size: 0.85rem;
  /* The tracking stays - set uppercase needs it - but 900 at this size on a
     dark ground closed the counters entirely. See .menu-note. */
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

.legend-heading p,
.legend-pointer-help {
  margin: 0.3rem 0 0;
  color: var(--text-muted);
  font-size: 0.85rem;
  font-weight: 500;
  line-height: 1.5;
}

.legend-pointer-help {
  flex-shrink: 0;
  text-align: right;
}

.legend-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2.2rem;
  /* Side-facing direction copy extends beyond its SVG so it can retain the
     approved distance from the arrows. This gutter contains that ink without
     moving it inward or allowing the menu edge to clip it. */
  padding-inline: 1.25rem;
}

.legend-item {
  display: block;
  width: 100%;
  max-height: 32rem;
  overflow: visible;
}

.legend-item-title {
  fill: var(--housing-ink);
  font-family: var(--font-ui);
  font-size: 26px;
  font-weight: 700;
}

.legend-direction-label {
  fill: var(--housing-ink-muted);
  font-family: var(--font-ui);
  font-size: 17px;
  /* 900 on a dark ground at this size closed the counters. See .menu-note. */
  font-weight: 700;
  letter-spacing: 0.3px;
}

/* The inside of a lattice node is the ground showing through. The key is not
   the canvas, so it has to paint that colour rather than let it show - and it
   is painted from here rather than from the builder so that it tracks the
   housing, Eye Comfort included, instead of being fixed at build time. */
.legend-node-interior {
  fill: var(--housing-ground);
}

@media (max-width: 620px) {
  .legend-grid {
    grid-template-columns: 1fr;
  }

  .legend-item {
    max-height: 17rem;
  }

  .legend-heading {
    align-items: start;
    flex-direction: column;
  }

  .legend-pointer-help {
    text-align: left;
  }
}
