/* Hueclid
   App chrome follows the browser's light/dark preference. The palette itself is
   always shown against its own background rule, never against the app chrome —
   the grid is the measurement, so it cannot inherit the theme. */

:root {
  --ink: #141414;
  --sub: #6a6a6a; /* 4.54:1 on the frame grey #ebebeb (was #6b6b6b, 4.47) */
  --bg: #ffffff;
  /* Greys stepped down from white, so white things — fields, the grid — stand out:
     panels ΔE 2, dropdowns and buttons ΔE 4, the frame ΔE 6. */
  --panel: #f8f8f8;
  --chipbg: #f2f2f2;
  --chrome: #ebebeb;
  --line: #e2e2e0;
  --line2: #c6c6c2;
  /* Field borders: the lightest grey at 3:1 against the cards (#909090 on #f8f8f8, 3.01). */
  --field-line: #909090;
  /* Filled controls' borders (dropdowns, Max chroma, the notation menu): 3:1 against
     the card and against their own --chipbg fill (#8a8a8a: 3.30 on #f8f8f8, 3.07 on
     #f2f2f2). */
  --fill-line: #8a8a8a;
  /* The header: dark grey in both themes, white text on it. */
  --header: #3b3b3b;
  --mark: #949494; /* 3.03:1 on white (was #959595, 2.99) */ /* key and pin marks in the hue column; mid-grey reads on either theme */
  --accent: #2a78d6;
  --warn: #c8102e;
  --scrim: rgba(20, 20, 20, .34);
  --mono: Helvetica, Arial, sans-serif;
  --sans: Helvetica, Arial, sans-serif;
  --cell: 96px;
  --band: 24px;
  --rail: 384px;
  --rowlabel: 128px;
  /* The sidebar's padding, and the gap between its sections. */
  --rail-pad: 24px;
  /* From the top of the sidebar to the top of the Key Color card: the padding, the
     16px title line and the 8px gap under it. The grid starts level with it. */
  --card-top: calc(var(--rail-pad) + 16px + 8px);
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #f2f2f0;
    --sub: #979792;
    --bg: #0b0b0b;
    --chrome: #161616;
    --panel: #0b0b0b;
    --chipbg: #1e1e1e;
    --line: #262626;
    --line2: #3b3b3b;
    --field-line: #5e5e5e; /* 3.04 on #0b0b0b */
    --fill-line: #696969; /* 3.04 on the #1e1e1e fill */
    --accent: #6ba6ec;
    --warn: #ff6b7f;
    --scrim: rgba(0, 0, 0, .66);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/20px var(--sans);
  /* Helvetica's figures at even widths, so values line up as the mono font's did. */
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

button, input { font: inherit; color: inherit; }
input { background: transparent; border: none; outline: none; }
input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* An input inside a bordered field shows focus on the field's border instead, not as
   a second ring inside it. */
.key-input-wrap input:focus-visible, .lch-field input:focus-visible, .rule-field input:focus-visible { outline: none; }

/* Checkboxes: the native input is kept for keyboard and screen readers but hidden;
   the square / square-check icon after it shows the state (js/ui/icon.js). */
.check { position: relative; display: inline-flex; align-items: center; flex: none; cursor: pointer; }
.check-input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.check-input:focus-visible { outline: none; }
.check-input:focus-visible + i { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* ---------- header ---------- */

.chrome {
  display: flex; align-items: center; gap: 10px;
  height: 48px; padding: 0 20px;
  /* A dark band over the grey frame, in both themes: the product's name at the left,
     the tool's version at the right. */
  background: var(--header);
  position: sticky; top: 0; z-index: 40;
}
.chrome-version { font: 400 12px/16px var(--mono); color: var(--sub); }
/* The version beside the name is a menu too: the versions of this system. Underlined,
   with a caret, in --sub, which clears 4.5:1 on the frame grey (#6a6a6a on #ebebeb,
   4.54). */
.version-btn {
  display: inline-flex; align-items: center; gap: 4px; border: none; background: none;
  padding: 0; color: var(--sub); font: 400 12px/16px var(--mono); cursor: pointer;
}
.version-btn span { text-decoration: underline; text-underline-offset: 3px; }
.version-btn:hover { color: var(--ink); }
/* The grid's column: a bar on the grey above the grid, the height of the space
   between the header and the Key Color card, so the grid's top stays level with it.
   Its contents start at the grid panel's left edge. */
.main-col { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.main-col > .grid-wrap { flex: none; }
/* Undo and redo: two icon buttons sharing a side, a button group. */
/* At the bar's right end, as far from the edge as from the bar's top and bottom:
   (48 − 26) / 2 = 11px. */
.history { display: flex; margin-left: auto; }
.history .btn { width: 32px; height: 26px; padding: 0; justify-content: center; border-color: var(--field-line); border-bottom-width: 2px; } /* 2px base, like the filled controls */
.history .btn:first-child { border-radius: 4px 0 0 4px; }
/* Undo keeps its word, which says what the arrow beside it does too. */
.history #undo { width: auto; padding: 0 10px; }
.history .btn:last-child { border-left: none; border-radius: 0 4px 4px 0; }
/* Alone in its group, a button keeps all four sides and corners. */
.history .btn:only-child { border-left: 1px solid var(--field-line); border-radius: 4px; }
.sys-bar { height: var(--card-top); display: flex; align-items: center; gap: 10px; padding: 0 11px 0 0; }
.chrome-spacer { flex: 1; }
.app-version { font: 400 12px/16px var(--mono); color: #c6c6c6; } /* 6.6:1 on the header */
/* The header's links — Help, and the version — light grey underlined on the dark
   header: #c6c6c6 on #3b3b3b, 6.6:1. */
.chrome-link {
  border: none; background: none; padding: 0; cursor: pointer;
  font: 400 12px/16px var(--sans); color: #c6c6c6;
  text-decoration: underline; text-underline-offset: 3px;
}
.chrome-link.app-version { font-family: var(--mono); }
/* Help leads with circle-question, 1em of its text; only the word is underlined. */
#help-link { display: inline-flex; align-items: center; gap: 4px; text-decoration: none; }
#help-link span { text-decoration: underline; text-underline-offset: 3px; }
.chrome-link:hover { color: #ffffff; }
/* What changed: a 320px popover under the version, its right edge on the header's. */
.versions-pop {
  position: fixed; top: 52px; right: 20px; z-index: 60; width: 320px; max-height: 420px;
  overflow-y: auto; border: 1px solid var(--line2); border-radius: 8px;
  background: var(--bg); color: var(--ink); box-shadow: 0 12px 30px rgba(0, 0, 0, .2);
  padding: 12px 16px;
}
.versions-head { display: flex; align-items: center; justify-content: space-between; font: 600 16px/20px var(--sans); padding-bottom: 8px; }
.versions-list { margin: 0; padding: 0; list-style: none; }
.versions-list li { display: grid; grid-template-columns: 56px 1fr; column-gap: 8px; padding: 8px 0; border-top: 1px solid var(--line); }
.versions-num { font: 500 12px/16px var(--mono); }
.versions-title { font: 400 12px/16px var(--sans); }
.versions-date { grid-column: 2; font: 400 12px/16px var(--mono); color: var(--sub); }
.pop-close {
  width: 24px; height: 24px; border: none; background: none; padding: 0; cursor: pointer;
  color: var(--sub); font-size: 12px; line-height: 1;
}
.pop-close:hover { color: var(--ink); }
/* How it works: a modal over the whole tool. */
.help-modal { width: min(640px, 92vw); max-height: 86vh; display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line2); border-radius: 12px; box-shadow: 0 24px 60px rgba(0, 0, 0, .28); }
.help-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--line); font: 600 20px/24px var(--sans); }
.help-body { overflow-y: auto; padding: 16px 24px 24px; font: 400 16px/20px var(--sans); }
.help-body h3 { margin: 20px 0 8px; font: 600 16px/20px var(--sans); }
.help-body h3:first-child { margin-top: 0; }
.help-body p { margin: 0 0 12px; }
.help-body a { color: inherit; text-decoration: underline; }
.help-body ol { margin: 0 0 12px; padding-left: 20px; }
.help-body li { margin-bottom: 8px; }
/* The search row: a field like the Rules panel's — 32px, the 3:1 border, 4px corners,
   16px type — and, beside it, previous and next match as a button group. Focus
   darkens the border to the field's own ink. It mustn't shrink in the modal's
   column. */
.help-find { flex: none; display: flex; gap: 8px; margin: 12px 24px 0; }
.help-search-field {
  flex: 1; min-width: 0; height: 32px; display: flex; align-items: center; gap: 4px;
  border: 1px solid var(--field-line); border-radius: 4px; background: var(--bg); padding: 0 4px 0 7px;
  color: var(--ink);
}
.help-search-field:focus-within { border-color: var(--ink); }
.help-search {
  flex: 1; min-width: 0; height: 30px; padding: 0; border: none; background: none;
  font: 500 16px/20px var(--sans); color: inherit;
}
.help-search:focus-visible { outline: none; }
.help-search::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
.help-clear {
  display: none; flex: none; width: 24px; height: 24px; padding: 0; border: none; background: none;
  color: inherit; font-size: 12px; line-height: 1; cursor: pointer;
}
.help-search-field.has-text .help-clear { display: block; }
/* Two buttons sharing a side, like undo/redo: white, the 3:1 border, a 2px base. */
.btn-pair { display: flex; flex: none; }
.btn-pair .btn {
  width: 32px; height: 32px; padding: 0; justify-content: center;
  border-color: var(--field-line); border-bottom-width: 2px; font-size: 12px;
}
.btn-pair .btn:first-child { border-radius: 4px 0 0 4px; }
.btn-pair .btn:last-child { border-left: none; border-radius: 0 4px 4px 0; }
.help-body h4 { margin: 16px 0 4px; font: 600 16px/20px var(--sans); }
.help-rule > p { margin: 0 0 8px; }
.help-settings, .help-options { margin: 0 0 8px; padding-left: 20px; }
.help-options { margin-top: 4px; }
.help-settings li, .help-options li { margin-bottom: 6px; }
.help-body mark { background: #ffe58a; color: #141414; }
/* The match previous and next have moved to. */
.help-body mark.current { background: #f5a623; }
.help-none { color: var(--sub); }
/* The product's name, at the header's left, white on its dark grey (11.2:1), then its
   tagline a step smaller, regular and quieter: #a6a6a6 on #3b3b3b, 4.6:1, clear of
   4.5:1. Their baselines line up. */
.product-name { font: 700 20px/24px var(--sans); color: #ffffff; white-space: nowrap; }
.product-tagline { font: 400 16px/20px var(--sans); color: #a6a6a6; white-space: nowrap; }
.product { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.caret { opacity: .6; }

/* ---------- buttons ---------- */

.btn {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line2); background: var(--bg);
  border-radius: 4px; padding: 4px 10px;
  font-size: 12px; line-height: 16px; cursor: pointer; white-space: nowrap;
}
.btn:hover:not(:disabled) { border-color: var(--ink); }
.btn:disabled { opacity: .4; cursor: default; }
.btn-quiet { border-color: transparent; background: none; color: var(--sub); }
.btn-quiet:hover:not(:disabled) { color: var(--ink); border-color: var(--line2); }
.btn-primary { background: var(--ink); color: var(--bg); border-color: var(--ink); padding: 7px 20px; font-weight: 500; }
/* The system's name is the palette's title, and quietly the File menu: plain bold
   text, 20px on a 40px line, with a 16px caret after it as the only hint it opens. */
.file-btn {
  display: inline-flex; align-items: center; gap: 8px; min-width: 0;
  border: none; background: none; padding: 0; color: var(--ink); cursor: pointer;
  font: 700 20px/40px var(--sans); letter-spacing: -.01em; white-space: nowrap;
}
.file-btn .caret { font-size: 16px; } /* after the name, so smaller (D143) */

.linkish {
  border: none; background: none; cursor: pointer;
  font: 400 12px/16px var(--mono); color: var(--sub); text-decoration: underline;
}
.linkish:hover { color: var(--ink); }

/* ---------- layout ---------- */

.layout {
  display: flex; align-items: flex-start;
  /* The app's own grey under everything (D105): where the grid ends, the UI shows,
     not the palette's ground. */
  min-height: calc(100vh - 48px); background: var(--chrome);
}

.rail {
  width: var(--rail); flex: none;
  padding: var(--rail-pad);
  position: sticky; top: 48px;
  /* Full height, so it reads as part of the frame even when its content is short. */
  height: calc(100vh - 48px); overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--rail-pad);
  background: var(--chrome);
}
/* Collapsed: a slim strip holding only the toggle, which stays where it was (D79). */
.rail.collapsed { width: 40px; padding: 0; overflow: hidden; }
.rail.collapsed > :not(.panel-toggle) { display: none; }
/* Centred between the header and the Key Color card, its right edge on the card's. */
.panel-toggle {
  position: absolute; top: calc((var(--card-top) - 24px) / 2); right: var(--rail-pad);
  width: 24px; height: 24px; border: none; border-radius: 4px; background: none;
  color: var(--sub); cursor: pointer; font-size: 12px; line-height: 1;
}
.panel-toggle:hover { color: var(--ink); background: var(--chipbg); }
.rail.collapsed .panel-toggle { right: 8px; }

.rail-block { display: flex; flex-direction: column; gap: 8px; }
.rail-head { display: flex; align-items: center; gap: 8px; }
.rail-title {
  flex: 1; font: 500 12px/16px var(--mono);
  letter-spacing: .13em; text-transform: uppercase; color: var(--sub);
}
/* Inside the key field, at its right end, in the field's own colours inverted —
   so it reads on any key and never moves the layout (D78). */
.gamut-warn {
  flex: none; font: 500 9px/12px var(--mono); letter-spacing: .04em;
  background: var(--key-ink); color: var(--key-fill);
  padding: 1px 5px; border-radius: 4px; white-space: nowrap;
}
.gamut-warn[hidden] { display: none; }

/* ---------- hue wheel ---------- */

/* The key colour field spans the block; below it, a narrow column of L/C/H and Max
   chroma, and the wheel in the rest of the width (D76). */
/* The key colour's controls in a white card like the rule and role boxes (D80). */
.key-card {
  display: flex; flex-direction: column; gap: 8px; padding: 8px;
  border: 1px solid var(--line2); border-radius: 12px; background: var(--panel);
}
.key-body { display: flex; align-items: center; gap: 16px; }
.key-col { flex: none; width: 132px; display: flex; flex-direction: column; gap: 6px; }
.wheel-wrap { flex: 1; min-width: 0; display: flex; justify-content: center; }
/* The size is also in js/ui/wheel.js (SIZE), which places the dots. */
.wheel { width: 128px; height: 128px; border-radius: 50%; position: relative; }
.wheel-hole { position: absolute; inset: 17px; border-radius: 50%; background: var(--panel); }
.wheel-dot {
  position: absolute; width: 16px; height: 16px; border-radius: 50%;
  box-shadow: 0 0 0 1.5px var(--panel);
}
.wheel-dot.key { box-shadow: 0 0 0 2.5px var(--panel); }
/* A secondary hue: filled like any dot, but its outline is dotted (D66, D67). An
   outline sits outside the dot, as the solid ring's box-shadow does. */
.wheel-dot.secondary { box-shadow: none; outline: 2px dotted var(--panel); }
/* Manual: where a click on the ring would add a hue (D63). */
.wheel.adding { cursor: crosshair; }
.wheel-dot.ghost { background: none; box-shadow: 0 0 0 1.5px var(--panel), inset 0 0 0 1.5px var(--ink); pointer-events: none; }

/* ---------- key colour ---------- */

/* Two controls, flush: the colour field, bordered in a shade of its own fill, and the
   notation menu, bordered like any field, with no left edge — the field's border is
   the line they share. */
.key-row { display: flex; align-items: stretch; }
.key-input-wrap {
  /* The key colour field: 40px tall. */
  flex: 1; min-width: 0; height: 40px; padding: 0 10px; display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--key-edge, var(--field-line)); border-radius: 4px 0 0 4px;
}
.key-input-wrap:focus-within { border-color: var(--accent); }
.key-input-wrap input { flex: 1; width: 100%; min-width: 0; padding: 0; font: 500 16px/20px var(--mono); letter-spacing: .01em; color: inherit; }
/* Selection inverts the field: the ink becomes the highlight, the key colour the text. */
.key-input-wrap input::selection { background: var(--key-ink); color: var(--key-fill); }
.key-notation {
  border: 1px solid var(--fill-line); border-left: none; border-bottom-width: 2px; border-radius: 0 4px 4px 0;
  background: var(--chipbg); padding: 0 10px; font: 400 12px/16px var(--mono); color: var(--sub); cursor: pointer;
}
.key-notation:hover { color: var(--ink); }
/* The caret at full strength, the text's colour: both clear 4.5:1 on the fill
   (#6a6a6a on #f2f2f2, 4.9; dark 5.68). */
.key-notation .caret { opacity: 1; }

/* Lightness, Chroma and Hue each on their own line, the full width of the column
   (D75), their names spelled out (D153). */
.key-lch { display: flex; flex-direction: column; gap: 6px; }
.lch-field {
  display: flex; align-items: center; gap: 6px;
  /* Every control in the Key Color, Rules and Roles panels is 32px tall. */
  height: 32px; border: 1px solid var(--field-line); border-radius: 4px; padding: 0 7px;
  background: var(--bg);
}
.lch-field:focus-within { border-color: var(--accent); }
.lch-label { flex: none; font: 400 12px/16px var(--mono); color: var(--sub); }
.lch-field input { width: 100%; min-width: 0; padding: 0; font: 500 16px/20px var(--mono); text-align: right; }
.key-tools { display: flex; }
/* Filled controls — Max chroma and the dropdowns — sit on a 2px bottom edge; the
   padding below gives back the extra pixel, so they stay the height of a field. */
.key-max { width: 100%; height: 32px; justify-content: center; padding-top: 0; padding-bottom: 0; background: var(--chipbg); border-color: var(--fill-line); border-bottom-width: 2px; }

/* ---------- rules and roles ---------- */

/* White cards on the sidebar's grey, as the File button is on the header (D79). */
/* Its rows inset their fields 8px: the box's 12px corner less the fields' 4px, so the curves nest.
   It clips the full-bleed action row to its corners. */
.rule-box { border: 1px solid var(--line2); border-radius: 12px; background: var(--panel); overflow: hidden; }
.rule { border-bottom: 1px solid var(--line); }
.rule.off { opacity: .45; }
/* Rule names start 14px in, like the role names; fields keep 8px on the right so
   their 4px corners nest in the panel's 12px. */
.rule-main, .rule-sub { display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 14px; }
/* Labels at the left, fields at the right (D41). */
.rule-sub { padding-top: 0; }
/* A rule's name never truncates. When a long dropdown choice leaves no room beside
   it, the row wraps and the field drops to its own line, right-aligned. */
.rule-main { flex-wrap: wrap; row-gap: 6px; }
/* Names take their own width; the controls group pushes itself right (margin-left:
   auto), so anything placed beside a name — the Min deltaE link — stays beside it. */
.rule-name { flex: 0 0 auto; white-space: nowrap; }
.rule-end { display: flex; align-items: center; gap: 8px; margin-left: auto; }
/* An input's own label sits right beside it, like the inline "steps" (D45). */
/* An input's own label, level with its 32px field. */
.rule-sub .rule-name { flex: 1; text-align: right; font: 400 16px/32px var(--mono); color: var(--sub); }

.rule-field {
  width: 118px; height: 32px; flex: none; display: flex; align-items: center; gap: 5px;
  border: 1px solid var(--field-line); border-radius: 4px; padding: 0 7px;
  background: var(--bg); text-align: left;
}
.rule-field:focus-within { border-color: var(--accent); }
.rule-field input { flex: 1; min-width: 0; padding: 0; font: 500 16px/20px var(--mono); text-align: right; }
.rule-pre, .rule-unit { font: 400 16px/20px var(--mono); color: var(--sub); flex: none; }
/* Dropdowns grow to fit their choice (so "Split complementary" is never cut off),
   from the same 118px minimum as the number fields, so short ones still line up. */
.rule-select { cursor: pointer; background: var(--chipbg); border-color: var(--fill-line); font: 400 12px/16px var(--sans); width: auto; min-width: 118px; border-bottom-width: 2px; }
.rule-select > .fa-caret-down { color: var(--sub); flex: none; }
.rule-select:hover { border-color: var(--ink); }
.rule-select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rule-select.unset { border-style: dashed; color: var(--sub); }
.rule-chip-swatch { width: 15px; height: 15px; flex: none; border-radius: 4px; box-shadow: 0 0 0 1px var(--line2); }

/* The lock beside a rule's name is 12px, smaller than the 16px name it follows. */
.rule-link { border: none; background: none; cursor: pointer; font-size: 12px; line-height: 1; padding: 2px; color: var(--ink); }
.rule-link.unlinked { opacity: .35; }
.rule-x { border: none; background: none; color: var(--sub); cursor: pointer; padding: 2px 3px; line-height: 1; flex: none; }
.rule-x:hover { color: var(--warn); }
/* An input's own label on a rule's main row ("steps"), set like the sub-lines'
   ("hues", "step") so the column of labels matches. */
.rule-inline-label { flex: none; font: 400 16px/32px var(--mono); color: var(--sub); }

/* An action row: the whole last row of a panel is the button (D49). */
.action-row {
  display: flex; align-items: center; gap: 8px; width: 100%;
  height: 48px; padding: 0 14px; border: none; border-radius: 0;
  background: none; color: var(--sub); text-align: left; cursor: pointer;
}
.action-row:hover { background: var(--chipbg); color: var(--ink); }
.action-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.action-row-caret { margin-left: auto; }
/* Action rows are 48px, the height of a rule row and of a role. */

.role-row { display: flex; align-items: center; gap: 8px; padding: 0; border-bottom: 1px solid var(--line); }
/* Renaming and reassigning look alike: the part being changed — the name, or the
   swatch address — gets a dashed outline in the bar's own ink, light on a dark
   swatch, dark on a light one, as thick as the 16px text's strokes (~1.5px). */
.role-name-wrap.editing, .role-chip.editing, .role-name-wrap:focus-within { outline: 1.5px dashed currentColor; outline-offset: 3px; border-radius: 1px; }
/* An outline's corner is the element's radius plus its offset: 1px + 3px draws the
   4px corner of the rule fields. */
.role-name:focus-visible { outline: none; }
/* The name's field: the input, and while it's being edited a clear button at its
   right end that empties it, so typing replaces the name at once. */
.role-name-wrap { flex: 1 1 0; min-width: 0; display: flex; align-items: center; gap: 4px; }
.role-name-wrap .role-name { flex: 1 1 0; }
.role-clear {
  flex: none; width: 24px; height: 24px; margin: -2px -4px -2px 0; border: none; background: none;
  padding: 0; color: inherit; font-size: 12px; line-height: 1; cursor: pointer;
}
/* The name and the address split the bar's width evenly; the ✕ takes what it needs. */
.role-name { flex: 1 1 0; min-width: 0; padding: 0; font: 400 16px/20px var(--sans); color: inherit; }
/* A role is a bar in its swatch's colour: its name at the left, the swatch it sits on
   at the right, ink black or white as the swatch's own is. Unplaced, it's white. */
.role-bar {
  /* 48px, the height of a rule row and of the action rows. */
  flex: 1; min-width: 0; height: 48px; display: flex; align-items: center; gap: 8px;
  /* Edge to edge in its row: no padding round it, no corners. */
  /* 14px in on the left, matching the ✕'s 9px glyph centred in its 38px square
     (about 14.5px from the right edge). */
  padding: 0 0 0 14px; border-radius: 0;
}
.role-chip {
  flex: 1 1 0; min-width: 0; border: none; background: none; padding: 0; color: inherit;
  font: 400 16px/20px var(--mono); text-align: right; cursor: pointer; white-space: nowrap;
}
.role-bar.unset .role-chip { color: var(--sub); }
.role-chip:hover { text-decoration: underline; }
/* The clear button shows only while the name is being edited. */
.role-clear { display: none; }
.role-name-wrap:focus-within .role-clear, .role-name-wrap.editing .role-clear { display: block; }
/* The remove ✕ sits centred in a 38px-wide target the bar's full height at its right
   end — the width keeps the glyph about 14.5px from the edge, matching the name's
   14px — 12px, after the address, so smaller (D143). Its square meets the
   address's field with no gap (the bar's 8px gap is taken back). */
.role-bar .rule-x { width: 38px; height: 48px; margin-left: -8px; padding: 0; display: flex; align-items: center; justify-content: center; color: inherit; font-size: 12px; }

/* ---------- the grid ---------- */

.grid-wrap {
  /* 40px under the last row, about the 42px above the step numbers (the corner's
     20px padding, the Background name and the 2px gap). */
  flex: 1; min-width: 0; padding: 0 var(--pad-end) var(--pad-bottom) 0; overflow: auto;
  --pad-end: 20px; --pad-bottom: 40px;
  /* Its top lines up with the Key Color card's. */
  max-height: calc(100vh - 48px - var(--card-top));
  /* A panel set into the grey frame (D105): lined where it meets it. Its right edge
     is the window's. */
  border: 1px solid var(--line2); border-right: none;
  /* Its right edge is the window's, so only the left corners round. */
  border-radius: 12px 0 0 12px;
  /* Lines and hovers on the grid's ground take its ink (black or white), faint. */
  --gridline: color-mix(in srgb, currentColor 18%, transparent);
  --gridhover: color-mix(in srgb, currentColor 7%, transparent);
  /* The frozen headers (D150): the ground, opaque to their middle, fading to half at
     the edge facing the swatches, over a blur of what passes under. */
  --frost-x: linear-gradient(to right, var(--gridbg, var(--bg)) 50%, color-mix(in srgb, var(--gridbg, var(--bg)) 50%, transparent));
  --frost-y: linear-gradient(var(--gridbg, var(--bg)) 50%, color-mix(in srgb, var(--gridbg, var(--bg)) 50%, transparent));
  --frost-blur: blur(12px);
  /* Their edge once the grid has scrolled under them, on a light ground: a 65% white
     stroke, then a shadow. A dark ground draws a 1px stroke instead, in --gridedge:
     the neutral at 3:1 against it, which main.js sets (D104). The hue column's
     shadow is a gradient, so its pieces meet without seams; its stops trace a
     3px 0 10px box-shadow at 16%. */
  --frost-edge: rgba(255, 255, 255, .65);
  --col-shadow: linear-gradient(to right, var(--frost-edge) 0 1px, rgba(0, 0, 0, .116) 1px, rgba(0, 0, 0, .08) 4px, rgba(0, 0, 0, .044) 7px, rgba(0, 0, 0, .018) 10px, rgba(0, 0, 0, .006) 13px, transparent 15px);
  /* The space between hue rows. */
  --row-gap: 12px;
}
/* The frozen headers stack (D150): the hue column on top, the step numbers under
   it, the swatches under both. The step numbers stay put as the grid scrolls down,
   and move with their columns as it scrolls across — they're in the same scroller
   (D103). The top padding is here, not on the wrapper, so swatches scrolling up
   pass under it, not over it. The corner isn't in this row: it heads the hue column,
   so it scrolls up and away with the hues, over this row's spacer. It sits just
   before this row, pulled back by its own height (--corner-h, set by main.js),
   and this row is at least that tall, so the step numbers stay level with its
   last line. */
.grid-top {
  display: flex; min-width: max-content; align-items: stretch; min-height: var(--corner-h, 0px);
  /* On over the grid's end padding, so it runs to the panel's edge, not short of it. */
  margin-right: calc(-1 * var(--pad-end)); padding-right: var(--pad-end);
  position: sticky; top: 0; z-index: 7;
  background: var(--frost-y); backdrop-filter: var(--frost-blur); -webkit-backdrop-filter: var(--frost-blur);
  transition: box-shadow .15s;
}
.grid-top-spacer { width: var(--rowlabel); flex: none; }
.grid-wrap.scrolled-y .grid-top { box-shadow: inset 0 -1px 0 var(--frost-edge), 0 2px 6px rgba(0, 0, 0, .14); }
/* The hue column — corner, names and the gap between the groups — is frosted
   across, and frosted through the gaps between rows and the space under the last,
   so it reads as one surface down to the panel's edge. */
.grid-top-label, .row-label, .hue-gap, .row-label::before, .hue-gap::before {
  background: var(--frost-x); backdrop-filter: var(--frost-blur); -webkit-backdrop-filter: var(--frost-blur);
}
/* Where the hue column crosses the step numbers' row, the two frosts compound. The
   column's blur samples the swatches but not the step row (one sticky layer's
   backdrop doesn't take in another), so the step row's frost is painted into the
   column too: fixed to the viewport, where the step row sits (--band-top and
   --band-h, set by main.js). */
.row-label, .hue-gap, .row-label::before, .hue-gap::before {
  background: var(--frost-x), var(--band-edge, none),
    var(--frost-y) 0 var(--band-top, -9999px) / 100% var(--band-h, 0px) no-repeat fixed;
}
/* And the step row's edge once the grid has scrolled down: its white stroke and a
   gradient tracing its 0 2px 6px shadow; on a dark ground, its 1px stroke. */
.grid-wrap.scrolled-y {
  --band-edge: linear-gradient(var(--frost-edge) 0 1px, rgba(0, 0, 0, .1) 1px, rgba(0, 0, 0, .06) 3px, rgba(0, 0, 0, .025) 5px, rgba(0, 0, 0, .008) 7px, transparent 9px)
    0 calc(var(--band-top, -9999px) + var(--band-h, 0px) - 1px) / 100% 10px no-repeat fixed;
}
.grid-wrap.ground-dark.scrolled-y {
  --band-edge: linear-gradient(var(--gridedge), var(--gridedge))
    0 calc(var(--band-top, -9999px) + var(--band-h, 0px)) / 100% 1px no-repeat fixed;
}
.row-label::before, .hue-gap::before {
  content: ''; position: absolute; left: 0; right: 0; top: 100%; height: var(--row-gap); pointer-events: none;
}
#grid > :last-child > .row-label::before, #grid > .hue-gap:last-child::before { height: var(--pad-bottom); }
/* Its edge: invisible at rest, there once the grid has scrolled under it. Gradients
   on pseudo-elements, not box-shadows, so the pieces meet without seams; each runs
   on through the gap under it, so the edge reads as one down the column. */
.grid-top-label::after, .row-label::after, .hue-gap::after {
  content: ''; position: absolute; pointer-events: none;
  top: 0; bottom: 0; left: calc(100% - 1px); width: 15px; background: var(--col-shadow);
  opacity: 0; transition: opacity .15s;
}
#grid > :not(:last-child) > .row-label::after,
#grid > .hue-gap:not(:last-child)::after { bottom: calc(-1 * var(--row-gap)); }
#grid > :last-child > .row-label::after,
#grid > .hue-gap:last-child::after { bottom: calc(-1 * var(--pad-bottom)); }
/* On a dark ground a shadow can't show, so the edge is a 1px stroke (D104). */
.grid-wrap.ground-dark.scrolled-y .grid-top { box-shadow: 0 1px 0 var(--gridedge); }
.grid-wrap.ground-dark .grid-top-label::after,
.grid-wrap.ground-dark .row-label::after,
.grid-wrap.ground-dark .hue-gap::after { left: 100%; width: 1px; background: var(--gridedge); }
.grid-wrap.scrolled-x .grid-top-label::after,
.grid-wrap.scrolled-x .row-label::after,
.grid-wrap.scrolled-x .hue-gap::after { opacity: 1; }
/* The corner is labelled like a hue: the name, then its value on the line below (D101). */
.grid-top-label {
  width: var(--rowlabel); flex: none; padding: 20px 24px 8px 24px;
  position: sticky; left: 0; z-index: 8; margin-bottom: calc(-1 * var(--corner-h, 0px));
  display: flex; flex-direction: column; justify-content: flex-end; align-items: flex-start; gap: 2px;
}
.ground-name { font: 600 16px/20px var(--sans); letter-spacing: -.01em; }
/* The Background's value, in the swatches' notation; long notations wrap, as on a swatch. */
.ground-value {
  font: 400 12px/16px var(--mono); opacity: .6;
  color: inherit; background: none; border: none; padding: 0; margin: 0;
  text-align: left; word-break: break-all; cursor: pointer;
}
.ground-value:hover, .ground-value:focus-visible { opacity: 1; }
/* The simulation the grid is drawn in names itself at the step row's right, 12px,
   centred between the row's top and the step numbers' (D151, D152). The slot
   stretches to the row's height; the numbers take its bottom 24px (16px line, 8px
   padding), so the tag's 16px line sits at (height − 24 − 16) / 2. The slot takes no width, so the
   row is no wider than the grid, and the tag hangs left from it. It sticks 24px in
   from the panel's right edge, as the Background's name sits 24px in from its left;
   sticky offsets count from inside the panel's own end padding, hence the sum.
   Its colours are the grid's ink on its ground, not the app theme's. */
.sim-tag-slot { position: sticky; right: calc(24px - var(--pad-end)); width: 0; flex: none; margin-left: auto; }
.sim-tag {
  position: absolute; right: 0; top: calc((100% - 40px) / 2);
  font: 600 12px/16px var(--sans); letter-spacing: .06em; text-transform: uppercase;
  background: var(--gridink, var(--ink)); color: var(--gridbg, var(--bg));
  padding: 0 5px; border-radius: 4px; white-space: nowrap;
}
.sim-tag[hidden] { display: none; }

.col-heads { display: flex; align-items: flex-end; padding: 20px 0 8px; }
.col-head {
  width: var(--cell); flex: none; text-align: center;
  font: 500 12px/16px var(--mono); letter-spacing: .04em; opacity: .6;
}

/* 12px between hue rows; the gaps show the grid's ground. */
#grid { display: flex; flex-direction: column; gap: var(--row-gap); }
/* Each ramp reads as one rounded bar: 12px on the outer corners of its first and
   last steps; the Prime, standing apart, is rounded all round. */
.grid-row > .row-label + .cell { border-radius: 12px; }
.grid-row > .prime-gap + .cell { border-radius: 12px 0 0 12px; }
.grid-row > .cell:last-child { border-radius: 0 12px 12px 0; }
.grid-row { display: flex; align-items: stretch; min-width: max-content; }
/* Between the Prime column and the ramp (D106): the ground, so the Prime doesn't
   read as the ramp's first step. */
.prime-gap { width: 12px; flex: none; }
.cell-oog-fix {
  font: inherit; font-size: 9px; line-height: 12px; color: inherit; background: none; border: none; padding: 0;
  opacity: .7; text-decoration: underline; cursor: pointer;
  display: inline-flex; align-items: center; gap: 3px; white-space: nowrap;
}
.cell-oog-fix:hover { opacity: 1; }
.row-label {
  width: var(--rowlabel); flex: none;
  padding-left: 24px; padding-right: 24px;
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  position: sticky; left: 0; z-index: 8;
}
.row-label-top { display: flex; align-items: center; gap: 6px; font: 600 16px/20px var(--sans); border-bottom: 2px solid currentColor; } /* the grid's ink: black on a light Background, white on a dark one */
.row-name {
  border: none; background: none; padding: 0; cursor: pointer; color: inherit;
  font: inherit; letter-spacing: -.01em;
}
.row-name:hover { text-decoration: underline; }
/* The neutral at 3:1 against the Background (#949494 on white: 3.03), solid. */
.row-caret { margin-left: auto; color: var(--gridedge); opacity: 1; font-size: 12px; } /* after its 16px name, so smaller (D143) */
.row-name + .hue-mark { margin-left: -4px; }
/* The key or pin mark after a hue's name, shown only when it applies, set like a
   superscript: .6em text raised to the cap line — the icon is 1em of that text. */
.hue-mark { font-size: .6em; line-height: 1; align-self: flex-start; margin-top: .1em; color: var(--mark); cursor: help; }
.hue-warn { line-height: 1; cursor: help; }
.rule-warn { flex: none; line-height: 1; cursor: help; color: var(--ink); }
.row-meta { font: 400 12px/16px var(--mono); opacity: .6; }
.row-angle { text-decoration: underline; }
/* Manual: the hue's angle is a field, bordered like the rules panel's so it reads
   as editable. The text keeps row-meta's size; the border sits outside it. */
.hue-field {
  display: flex; align-items: center; gap: 4px; width: 72px;
  margin-left: -4px; padding: 1px 3px; border: 1px solid var(--gridedge); border-radius: 4px;
  cursor: text;
}
.hue-field:hover { border-color: var(--ink); }
.hue-field:focus-within { border-color: var(--accent); opacity: 1; }
.hue-field input { flex: 1; min-width: 0; font: inherit; color: inherit; background: none; border: none; padding: 0; outline: none; }

.cell {
  width: var(--cell); height: var(--cell); flex: none;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: 4px 8px 0; cursor: pointer; overflow: hidden;
  font: 400 12px/16px var(--mono);
}
.cell { position: relative; }
/* A primary hue's Prime in Manual: its editor's icon (sliders), lower right, on hover (D130, D137).
   Above the simulation band when there is one. */
.prime-edit {
  position: absolute; right: 3px; bottom: 3px;
  /* A 24×24 target, WCAG 2.2's minimum (2.5.8), the icon centred in it (D138). The
     icon is 13px: it pairs with no text, and the swatch's 11px would make it smaller
     still — the one icon sized on its own. */
  width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
  border: none; background: none; color: inherit; padding: 0; cursor: pointer;
  font-size: 12px; line-height: 1; opacity: 0;
}
.cell.has-band .prime-edit { bottom: calc(var(--band) + 2px); }
.cell:hover .prime-edit, .prime-edit:focus-visible { opacity: .75; }
.prime-edit:hover { opacity: 1; }
/* The Prime editor: the Key Color card's field and L/C/H, in a popover (D130). */
.prime-pop {
  position: fixed; z-index: 55; width: 300px;
  border: 1px solid var(--line2); border-radius: 8px;
  background: var(--bg); color: var(--ink); box-shadow: 0 12px 30px rgba(0, 0, 0, .2);
  padding: 4px; display: flex; flex-direction: column; gap: 8px;
}
.prime-pop-head { display: flex; align-items: center; justify-content: space-between; font: 500 16px/20px var(--sans); }
.prime-pop-close { border: none; background: none; color: var(--sub); padding: 2px; cursor: pointer; font: inherit; line-height: 1; }
.prime-pop-close:hover { color: var(--ink); }
/* The colour in three models, a row each, the model named at the left (D134). */
/* Rows as far apart as the popover's own sections (8px), columns closer, so it
   reads as a row per model (D135, D136). */
.prime-pop-grid { display: grid; grid-template-columns: auto repeat(3, 1fr); gap: 8px 6px; align-items: center; }
.prime-pop-model { font: 400 9px/12px var(--mono); letter-spacing: .06em; color: var(--sub); padding-right: 2px; }
.prime-pop-grid .lch-field { padding: 3px 6px; gap: 4px; }
.prime-pop-grid .lch-field input { font-size: 12px; line-height: 16px; }
/* The tip pointing at the swatch being edited: a square turned 45°, half hidden. */
.prime-pop-tip {
  position: absolute; width: 14px; height: 14px; background: var(--bg);
  border: 1px solid var(--line2); transform: rotate(45deg);
}
.prime-pop-tip.right { right: -8px; border-left: none; border-bottom: none; }
.prime-pop-tip.left { left: -8px; border-right: none; border-top: none; }
/* One line, now the popover is wide enough (D135). */
.prime-pop-tools { display: flex; gap: 6px; }
.prime-pop-tools .btn { flex: 1 1 0; justify-content: center; white-space: nowrap; }
.cell-text { word-break: break-all; }
.cell-hex { font-weight: 700; letter-spacing: -.02em; }
.cell-contrast { opacity: .72; }
.cell-role { font-weight: 500; letter-spacing: .07em; text-transform: uppercase; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cell-oog { font-size: 9px; line-height: 12px; opacity: .7; }
/* A step outside the Gamut rule's space: a mark after its value (D140). */
.cell-oog-mark { margin-left: 4px; font-weight: 400; cursor: help; }

.grid-row.picking .cell {
  cursor: crosshair;
  box-shadow: inset 0 0 0 6px rgba(0, 0, 0, .22), inset 0 0 0 1px rgba(0, 0, 0, .25);
}
.grid-row.picking .band-sq { pointer-events: none; }

.band { display: flex; margin: 0 -8px; width: calc(100% + 16px); }
/* Simulation set to one kind (D68): the count of collapses is marked on the swatch
   itself, at the foot where the band would be, set like the band's counts (D71). */
.cell-sim-warn { align-self: flex-start; padding-bottom: 5px; font: 700 12px/16px var(--mono); cursor: help; }
.band-sq {
  width: var(--band); height: var(--band); flex: none;
  border: none; padding: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font: 700 12px/16px var(--mono);
}
.band-sq.active { outline: 2px solid currentColor; outline-offset: -2px; z-index: 1; }

/* The gap between the declared hues and the secondary ones (D66), inside the hue
   column's hairline. In Manual it holds "Add a hue", an action row attached under
   the last declared hue (D62); otherwise it's empty, at the same height. Colours
   follow the grid's ink, not the app theme, since it sits on the Background
   rule's ground. */
.hue-gap {
  position: sticky; left: 0; z-index: 8; width: var(--rowlabel); height: 38px;
}
.add-hue-wrap .action-row {
  /* In from the left like the hue names above it; on one line. */
  height: 100%; padding: 0 0 0 24px; white-space: nowrap; border-top: 1px solid var(--gridline); border-radius: 0;
  color: inherit; opacity: .6;
}
.add-hue-wrap .action-row:hover { opacity: 1; background: var(--gridhover); }

/* ---------- menus ---------- */

.menu {
  position: fixed; z-index: 60; min-width: 220px; max-width: 320px;
  overflow-y: auto;
  border: 1px solid var(--line2); border-radius: 8px;
  background: var(--bg); box-shadow: 0 12px 30px rgba(0, 0, 0, .2);
  padding: 4px 0;
}
.menu-search { position: sticky; top: -4px; z-index: 2; background: var(--bg); padding: 0 4px 4px; box-shadow: 0 1px 0 var(--line); }
.menu-search input { width: 100%; border: 1px solid var(--field-line); border-radius: 4px; padding: 4px 8px; font: 400 12px/16px var(--mono); }
.menu-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  border: none; background: none; text-align: left; color: inherit;
  padding: 4px 10px 4px 26px; font-size: 16px; line-height: 20px; cursor: pointer; position: relative;
}
.menu-item:hover:not(:disabled) { background: var(--ink); color: var(--bg); }
.menu-item:disabled { cursor: default; opacity: .55; }
.menu-item.nested { padding-left: 42px; }
.menu-item.flush { padding-left: 10px; }
.menu-item.nested .menu-check { left: 22px; }
.menu-check { position: absolute; left: 6px; }
.menu-label { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.menu-sub { margin-left: auto; font: 400 12px/16px var(--mono); opacity: .55; padding-left: 12px; }
.menu-head { padding: 7px 10px 3px; font: 500 9px/12px var(--mono); letter-spacing: .13em; text-transform: uppercase; color: var(--sub); }
.menu-sep { height: 1px; background: var(--line); margin: 4px 0; }
/* An option's info icon at its right end, and the panel it opens beside the menu (D120). */
.menu-info { flex: none; opacity: .45; cursor: help; padding: 2px 0 2px 4px; }
.menu-item:hover .menu-info { opacity: .7; }
.menu-info:hover { opacity: 1; }
.menu-infopanel {
  position: fixed; z-index: 61; width: 260px;
  border: 1px solid var(--line2); border-radius: 8px;
  background: var(--bg); color: var(--ink); box-shadow: 0 12px 30px rgba(0, 0, 0, .2);
  padding: 10px 12px; font: 400 12px/16px var(--sans);
  pointer-events: none;
}
.menu-swatch { width: 15px; height: 15px; flex: none; border-radius: 4px; box-shadow: 0 0 0 1px var(--line2); }

/* ---------- toast / picking banner ---------- */

.toast {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  background: var(--ink); color: var(--bg);
  padding: 8px 20px; border-radius: 4px;
  font: 400 12px/16px var(--mono); z-index: 70;
}
.toast.picking { font: 400 16px/20px var(--sans); }
.toast[hidden] { display: none; }

/* ---------- export modal ---------- */

.scrim { position: fixed; inset: 0; background: var(--scrim); display: flex; align-items: center; justify-content: center; z-index: 80; }
.modal {
  width: min(820px, 92vw); height: min(560px, 86vh);
  background: var(--bg); border: 1px solid var(--line2); border-radius: 12px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .28);
  display: flex; flex-direction: column; overflow: hidden;
}
.modal-head { height: 48px; flex: none; display: flex; align-items: center; padding: 0 20px; border-bottom: 1px solid var(--line); }
.modal-title { flex: 1; font-weight: 600; }
.modal-body { flex: 1; min-height: 0; display: flex; }
.modal-side { flex: 0 0 240px; border-right: 1px solid var(--line); padding: 8px 0; overflow-y: auto; }
.fmt { display: block; width: 100%; border: none; background: none; text-align: left; padding: 5px 20px; font-size: 16px; line-height: 20px; cursor: pointer; color: inherit; }
.fmt:hover { background: var(--chipbg); }
.fmt.on { background: var(--ink); color: var(--bg); }
.modal-check { display: flex; align-items: center; gap: 8px; padding: 5px 20px; cursor: pointer; font-size: 16px; line-height: 20px; }
.modal-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.modal-pre { flex: 1; min-height: 0; overflow: auto; margin: 0; padding: 20px; font: 400 12px/16px var(--mono); white-space: pre; }
.modal-foot { height: 68px; flex: none; border-top: 1px solid var(--line); display: flex; align-items: center; gap: 10px; padding: 0 20px; }
.modal-meta { flex: 1; font: 400 12px/16px var(--mono); color: var(--sub); }

/* ---------- file dialogs ---------- */

.modal.dialog { width: min(400px, 92vw); height: auto; }
.dialog-body { padding: 20px 20px 8px; }
.dialog-input {
  width: 100%; border: 1px solid var(--field-line); border-radius: 4px;
  padding: 7px 10px; font: 400 16px/20px var(--sans); color: inherit; background: var(--bg);
}
.dialog-input:focus { border-color: var(--accent); outline: none; }
.dialog-note { min-height: 18px; padding-top: 4px; font: 400 12px/16px var(--mono); color: var(--warn); }
.dialog-text { margin: 0 0 12px; font-size: 16px; line-height: 20px; }
.dialog-foot { display: flex; justify-content: flex-end; gap: 8px; padding: 8px 20px 20px; }
.btn-danger { background: var(--warn); color: #fff; border-color: var(--warn); padding: 7px 20px; font-weight: 500; }
.btn-primary:disabled { opacity: .4; cursor: default; }
.modal-foot .btn:not(.btn-primary) { padding: 7px 16px; }
