/*
 * Grainworks — panel styling.
 *
 * UXP supports no pseudo-elements, so every visual part is a real element; layout
 * is flexbox only, and sizes are in px because UXP does not resolve `rem`.
 */

:root {
  /*
   * A panel grey, not black.
   *
   * Pure black read as a hole cut in Photoshop's own chrome, which is grey all
   * around it — the plugin looked like it was missing rather than docked. This
   * sits near Adobe's dark UI theme, so the panel belongs to the application
   * it is inside; the app gets the same value and looks like a tool rather
   * than a terminal.
   *
   * Everything below is relative to it: a recessed control is darker, a raised
   * one lighter, and the values were moved together rather than one at a time.
   */
  --gw-bg: #262626;
  --gw-bg-raised: #303030;
  --gw-hairline: #383838;
  --gw-row: #2f2f2f;
  --gw-row-disabled: #242424;
  --gw-pill: #3b3b3b;
  --gw-pill-disabled: #262626;
  --gw-field: #1c1c1c;
  --gw-field-border: #4d4d4d;
  --gw-track: #535353;
  --gw-fill: #b2bdca;
  --gw-outline: #4a4a4a;
  --gw-text: #d5d5d6;
  --gw-text-dim: #a19ca7;
  --gw-text-off: #5f5f63;
  --gw-tool: #333333;
  --gw-tool-on: var(--gw-accent);
  /*
   * The accent, and it is the icon's orange rather than a new colour.
   *
   * `#e2842f` is the mid ink in mac/bake-icon.mjs, so the app, the Dock icon
   * and the wordmark behind the words are all the same orange — which is the
   * point of having one. The two dimmer variants are for a fill on a dark
   * track and for a hairline, neither of which can take the full chroma
   * without shouting over the image the panel exists to show.
   */
  --gw-accent: #e2842f;
  --gw-accent-dim: #a45f22;
  --gw-accent-quiet: #3a2a18;
  /*
   * Two stacks, because the panel has two jobs for type.
   *
   * Everything that is language — labels, section titles, buttons — is the
   * system UI face. It was all Courier New, which is a typewriter: wide,
   * low-contrast at 10 px, and the single loudest reason the panel read as a
   * hobby project rather than a tool sitting next to Photoshop's own panels.
   *
   * Numbers keep a monospace, and only numbers. A value that changes width as
   * it counts 9 -> 10 -> 100 makes a column of twenty jitter while you drag,
   * so the figures need to be tabular. SF Mono is the macOS system monospace
   * and is what both hosts land on here.
   */
  --gw-ui: -apple-system, "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --gw-num: "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  padding: 0;
  background: var(--gw-bg);
  color: var(--gw-text);
  /* "Adobe Clean" first: inside Photoshop that is the face its own panels
     use, so the plugin sits in the application rather than beside it. The app
     falls through to the system UI face, which is the same stack as --gw-ui. */
  font-family: "Adobe Clean", var(--gw-ui);
  font-size: 11px;
  overflow-x: hidden;
}

.gw-root {
  padding: 8px 10px 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/*
 * Scrolling, for the pages where the panel *is* the whole document.
 *
 * The standalone frame puts #gw-root inside #gw-panel, which is a flex child
 * with `overflow-y: auto` — so that page scrolls because the frame gives it
 * somewhere to scroll. The plugin page has no frame: #gw-root sits directly in
 * <body>, and a UXP panel does not scroll the document implicitly the way a
 * browser does. With every section open the panel is around 1200px of content,
 * so the RENDER footer was simply unreachable in Photoshop.
 *
 * Fixed with the same shape the standalone already proves: a real element with
 * a definite height and its own overflow. `min-height: 0` is what lets a flex
 * child actually shrink below its content and scroll rather than stretching
 * its parent.
 *
 * Scoped to `body.gw-plugin` so the standalone frame is untouched — its body
 * keeps `overflow: hidden` and #gw-panel keeps doing the scrolling.
 */
html { height: 100%; }

body.gw-plugin {
  height: 100%;
  display: flex;
  flex-direction: column;
}

body.gw-plugin #gw-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

/* ------------------------------------------------------------- wordmark */

/*
 * The wordmark, with the icon's halftone sphere behind it.
 *
 * `position: relative` here and `absolute` on the art, rather than a CSS
 * `background-image` with a data URI — whether a UXP panel resolves one of
 * those is not known, and an absolutely positioned child of a relative box is
 * ordinary layout it certainly handles. (`sticky` is the one that is unusable
 * in UXP: it drops out of flow and collapses to content width.)
 *
 * This replaced a dithered tonal ramp sitting under the type. The ramp was
 * honest about what the tool does, but it was a second, unrelated picture
 * stacked under the first — and now the same sphere is the app icon, so the
 * header, the Dock and the accent are one mark instead of three.
 */
/*
 * Tall enough to contain the 76 px sphere outright.
 *
 * With 11 px of padding the circle was clipped top and bottom and spilled
 * over the Open image / Export row below, so it read as a stain bleeding out
 * of the header rather than as a mark inside it. 30 px leaves the whole disc
 * inside with a few pixels to spare, and `overflow: hidden` is then a
 * backstop rather than the thing doing the cropping.
 */
.gw-header {
  padding: 4px 2px 2px;
}

/*
 * The sphere is centred on *this*, which holds only the type — not on the
 * header, which also holds the shell's Open image / Export row.
 *
 * Tall enough for the 96 px disc, so `overflow: hidden` is a backstop rather
 * than the thing doing the cropping.
 */
.gw-wordmark-lockup {
  position: relative;
  /* Holds the 132px disc. */
  height: 140px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/*
 * The sphere sits behind the type and has to lose to it.
 *
 * No `opacity` here, deliberately. It was 0.34, and seen side by side the
 * plugin's sphere was far brighter than the app's — UXP does not honour
 * element opacity the way WKWebView does, so the panel painted the full-chroma
 * inks while the app dimmed them. The dimming is in the fill colours now (see
 * `spherePaths`), which renders the same in all three engines.
 *
 * `z-index: 0` is not decorative. Without a stacking context of its own the
 * order is down to document order alone, and the art can end up painting over
 * the wordmark — which is what it did.
 */
.gw-header-art {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 0;
  transform: translate(-50%, -50%);
  pointer-events: none;
}

/*
 * Light type over the dot field.
 *
 * Two other arrangements were tried and both are recorded because the reasons
 * outlast them. Painting the word in the *background* colour reads as a hole
 * without being one — the dots are still there, covered — and cutting the
 * letters out of the dots for real (js/wordmark-mask.js, still used by
 * `spherePaths`) loses its last letter at the limb and stops looking like
 * grain at small sizes.
 *
 * So the word is ink again, over a dimmed screen. 19px at 600 with the
 * original tracking: heavier and wider was needed when the letters were a
 * hole and had no contrast of their own, and reads as shouting now that they
 * do.
 */
.gw-wordmark {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1px;
  font-family: var(--gw-ui);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.2px;
  color: #e9e9ea;
  user-select: none;
}

/*
 * The gleam: the mark raised off the end of the word, not set into it.
 *
 * `align-self: flex-start` rather than a negative margin, because the
 * wordmark is a centred flex row — a margin would move the whole line's
 * baseline instead of just this glyph. The small left pull tucks it into the
 * y's shoulder; without it the gap reads as a space and the mark looks like a
 * separate object sitting next to the word.
 *
 * No plate behind it. The filled square it once had read as a logo container.
 */
/*
 * The gleam stays *ink*, while the word became a hole.
 *
 * It is the one part of the mark that is not a knockout: a glint knocked out
 * of the dots would be a gap in a gap, which reads as nothing. Its own fill is
 * set where it is drawn, so it does not inherit the word's background colour.
 */
.gw-wordmark-gleam {
  display: flex;
  align-self: flex-start;
  width: 13px;
  height: 13px;
  margin-left: -1px;
  margin-top: -1px;
}

/* -------------------------------------------------------------- section */

.gw-section { display: block; }

/*
 * A left-aligned disclosure row with a hairline under it. The centred rounded
 * pill it replaces drew a border and a fill around every one of ten sections,
 * which is more chrome than the controls inside them.
 */
.gw-section-head {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 6px;
  height: 24px;
  margin: 12px 0 7px;
  padding: 0 2px;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--gw-hairline);
  border-radius: 0;
  cursor: pointer;
  user-select: none;
}
.gw-section-head:hover .gw-section-title { color: var(--gw-text); }

/* The triangle itself is an SVG — see `_drawChevron`. This only positions and
   colours it, since UXP cannot draw the CSS border version. */
.gw-section-chevron {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  color: var(--gw-text-off);
}
.gw-section-head:hover .gw-section-chevron { color: var(--gw-text); }

.gw-section-title {
  font-family: var(--gw-ui);
  font-size: 10px;
  font-weight: 600;
  /* 0.8, not 2.4. Wide tracking on a 10 px uppercase label is a poster
     device; at this size it just makes the word hard to read. */
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--gw-text-dim);
}

.gw-section-glyph {
  font-size: 14px;
  color: var(--gw-text);
  line-height: 1;
}

.gw-section.is-collapsed .gw-section-title { color: var(--gw-text-dim); }

/* A section's own reset, pushed to the far end of its header. Quiet until the
   header is hovered, and quieter still when there is nothing to reset. */
.gw-section-reset {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin-left: auto;
  color: var(--gw-text-off);
  cursor: pointer;
}
.gw-section-reset svg { transition: transform 0.45s ease; }
.gw-section-head:hover .gw-section-reset { color: var(--gw-text-dim); }
.gw-section-reset:hover { color: var(--gw-text); }
.gw-section-reset.is-off,
.gw-section-head:hover .gw-section-reset.is-off { color: var(--gw-hairline); cursor: default; }

.gw-section-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

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

/*
 * A develop-panel row: label, then the track, then the number.
 *
 * The rows used to be cards — a filled pill for the label inside a filled
 * rounded row — which at nine sections and 320-odd controls reads as a wall of
 * boxes. A dense develop panel gets there by removing almost all of it:
 * the row is not drawn at all, the label is plain text on the left, the track
 * runs through the middle, and the value sits right-aligned at the end with no
 * box until you touch it. The chrome that is left is the track and the handle,
 * which are the parts that carry information.
 */
/*
 * 28px tall with a 12px gap, up from 26 and 10.
 *
 * The panel is a column of forty of these, so the two numbers are the whole
 * feel of it — and at 26/10 the slider's handle at full travel sat against
 * the value box with about four pixels between them, which reads as one
 * jammed control rather than two.
 */
.gw-row {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 28px;
  padding: 0 5px 0 3px;
  background: transparent;
  border-radius: 0;
  overflow: hidden;
}
.gw-row + .gw-row { margin-top: 3px; }
/* The whole row lights up on hover, which is the only row background left. */
.gw-row:hover { background: var(--gw-bg-raised); border-radius: 4px; }

/* Dimmed, not filled. With the rows no longer drawn, a disabled background
   reads as a card that survived the restyle rather than as "not available". */
.gw-row.is-disabled { background: transparent; }
.gw-row.is-disabled:hover { background: transparent; }

.gw-label {
  flex: 0 0 31%;
  min-width: 66px;
  max-width: 120px;
  /* Follows .gw-row's height; a shorter label stops centring against it. */
  height: 28px;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  background: transparent;
  border-radius: 0;
  color: var(--gw-text-dim);
  font-size: 10.5px;
  white-space: nowrap;
  overflow: hidden;
  /* Double-click resets the control, so the label is a target, not prose. */
  cursor: default;
  user-select: none;
}
.gw-row:hover .gw-label { color: var(--gw-text); }

.gw-label-wide { flex-basis: 38%; }

.gw-row.is-disabled .gw-label {
  background: transparent;
  color: var(--gw-text-off);
}

.gw-row-controls {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ---------------------------------------------------------- number field */

.gw-field {
  /*
   * 52px, not 40.
   *
   * At 40 the plugin clipped three digits: Spread at 100 rendered as "10C",
   * with the last glyph cut in half. The app was fine at the same width,
   * which is what made it a plugin bug rather than an arithmetic one — UXP
   * does not render `<input>` as an HTML input, it maps it to its own text
   * field with its own internal chrome, so the usable width inside the same
   * 40px box is smaller there than here.
   *
   * 52 is not a guess. The plugin's panel is 332px; after the scrollbar, the
   * section padding and the row padding, a row has 292px to spend. A 31%
   * label caps at 91px, so 52px of field and two 12px gaps still leave 125px
   * of slider — three times the 40px floor `.gw-slider` sets. Four digits and
   * a minus sign fit, which Hue at -180 needs.
   */
  flex: 0 0 52px;
  height: 18px;
  /*
   * The value keeps a quiet box, in both surfaces, because in one of them it
   * has no choice.
   *
   * A borderless number at rest was the intent and it works in the app. It
   * cannot work in the plugin: UXP does not render `<input>` as an HTML input,
   * it maps it to its own text field with its own chrome, and neither
   * `border: none` on the input nor a transparent border on the container
   * removes it. Both `transparent` and `rgba(0,0,0,0)` were tried, and the
   * installed stylesheet was confirmed byte-identical to this file each time,
   * which is what narrowed it to UXP's own widget rather than a CSS mistake.
   *
   * So the choice was a nicer app or two surfaces that look the same, and this
   * panel's whole discipline is that they look the same. The box is dark and
   * thin enough to stay quiet in a column of twenty.
   */
  background: var(--gw-field);
  border: 1px solid var(--gw-field-border);
  border-radius: 3px;
  display: flex;
  align-items: center;
  cursor: ew-resize;
}
/*
 * The box appears only when the number is in play. At rest it is just a
 * number, which is what makes a column of them scannable.
 *
 * Two separate rules, and `is-editing` set from JS rather than
 * `:focus-within`. Grouped as one selector list with `:focus-within` in it,
 * UXP matched the whole rule unconditionally and every value in the Photoshop
 * panel drew its box permanently while the app drew none — the two surfaces
 * looked different, which is the one thing they may not do.
 */
/* Brighter while it is being typed into. UXP draws its own focus ring on the
   widget, so both surfaces indicate focus even though they draw it
   differently. */
.gw-field.is-editing { border-color: #7d7d7d; }

/* A value typed past the slider's range: legal, and worth saying so. */
.gw-field.is-extended {
  border-color: #b8893a;
}

.gw-field.is-disabled {
  border-color: #333;
  background: #141414;
}

.gw-field-input {
  font-family: var(--gw-num);
  width: 100%;
  height: 100%;
  padding: 0 5px;
  border: none;
  background: transparent;
  color: var(--gw-text);
  font-size: 10.5px;
  text-align: right;
  cursor: text;
}

.gw-field.is-disabled .gw-field-input { color: var(--gw-text-off); }

/* --------------------------------------------------------------- slider */

.gw-slider {
  position: relative;
  flex: 1 1 auto;
  min-width: 40px;
  height: 18px;
  display: flex;
  align-items: center;
  cursor: pointer;
}

/*
 * Inset by the handle's radius at both ends, which is what stops the handle
 * leaving its own box.
 *
 * The handle is 9px across and positioned by a percentage of this element, so
 * at 0 and 100 its centre sits on this element's ends. With the track running
 * the full width of the slider, those ends were the slider's ends, and 4.5px
 * of ring hung outside — over the value box on the right, which is what made
 * the plugin's rows look jammed together. `left`/`right` of 5px pulls the
 * travel in by just over that radius, so the handle's outer edge lands flush
 * with the slider instead of past it.
 *
 * `Slider._fromPointer` measures *this* element, so the pointer mapping
 * follows the inset for free: it was always the track that defined the range.
 */
.gw-track {
  position: absolute;
  left: 5px;
  right: 5px;
  height: 2px;
  background: var(--gw-track);
  border-radius: 1px;
}

.gw-fill {
  position: absolute;
  left: 0;
  top: 0;
  height: 2px;
  background: var(--gw-fill);
  border-radius: 1px;
}

/*
 * Open ring handle, centred on its position — and now a child of the 2px
 * track, so it needs its own vertical centring. As a sibling of the track it
 * inherited the slider's flex centring; inside a 2px box it would otherwise
 * sit against the top.
 */
.gw-handle {
  position: absolute;
  top: 50%;
  width: 9px;
  height: 9px;
  margin-left: -4.5px;
  margin-top: -4.5px;
  border: 1px solid #e9e9ea;
  border-radius: 50%;
  background: #e9e9ea;
}
.gw-slider.is-disabled .gw-handle { background: transparent; }

.gw-slider.is-active .gw-handle,
.gw-slider:hover .gw-handle { background: rgba(233, 233, 234, 0.22); }

.gw-slider.is-disabled { cursor: default; }
.gw-slider.is-disabled .gw-track { background: #333; }
.gw-slider.is-disabled .gw-fill { background: transparent; }
.gw-slider.is-disabled .gw-handle { border-color: #4a4a4a; background: transparent; }

/* ----------------------------------------------------------- levels row */

.gw-row-levels { height: 30px; }

.gw-levels {
  position: relative;
  flex: 1 1 auto;
  height: 20px;
  display: flex;
  align-items: center;
}

.gw-levels .gw-track { background: var(--gw-track); }
.gw-levels .gw-fill { background: var(--gw-fill); }

.gw-levels .gw-handle { cursor: ew-resize; }
.gw-handle-gamma {
  width: 9px;
  height: 9px;
  margin-left: -4.5px;
  border-radius: 2px;
  transform: rotate(45deg);
}

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

.gw-picker {
  flex: 1 1 auto;
  min-width: 0;
  --spectrum-picker-min-width: 60px;
}

sp-picker { width: 100%; }

/* --------------------------------------------------------------- swatch */

.gw-swatch {
  flex: 0 0 44px;
  height: 18px;
  border-radius: 3px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  cursor: pointer;
}

.gw-swatch-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  padding: 3px 1px 5px;
}

.gw-swatch-grid-item {
  flex: 0 0 22px;
  height: 16px;
  border-radius: 3px;
}

/* The palette's fold (`renderSwatchGrid`): the tops of the next row, cut by
   the bar's own height and faded by mixing each chip's colour with the
   ground (`peekHex`) — UXP honours neither a transparent colour nor
   `opacity` here. Open, it is a thin bar to fold the bank again. */
.gw-swatch-peek {
  flex: 1 0 100%;
  display: flex;
  gap: 3px;
  height: 6px;
  margin-top: -1px;
  overflow: hidden;
  cursor: pointer;
}
/* Outlined, opaquely, so a dark ink still shows where it is: faded toward a
   dark ground, a near-black one was the ground (a gradient's first twenty
   inks all read about (31, 33, 36) on (38, 38, 38)). */
.gw-swatch-peek-chip {
  flex: 0 0 22px;
  height: 16px;
  box-sizing: border-box;
  border-radius: 3px;
  border: 1px solid #5a5a5a;
}
/* Nothing to fold: the peek's own height, blank, so the folded strip is one
   height whatever it holds (§79). */
.gw-swatch-peek.is-spacer { cursor: default; }
.gw-swatch-peek.is-open {
  height: 4px;
  margin-top: 2px;
  border-radius: 2px;
  background: var(--gw-outline);
}

.gw-swatch-add {
  flex: 0 0 22px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed #5a5a5a;
  background: transparent;
  color: var(--gw-text-dim);
  font-size: 12px;
  line-height: 1;
}

/* ------------------------------------------------------------- toolbar */

.gw-toolbar {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 3px 0 1px;
  flex-wrap: wrap;
}

.gw-tool {
  flex: 0 0 auto;
  width: 26px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gw-tool);
  border-radius: 4px;
  color: var(--gw-text);
  cursor: pointer;
}

.gw-tool:hover { background: #363636; }

/* A tool with nothing to act on — undo at the bottom of the stack. */
.gw-tool.is-off { color: var(--gw-text-off); cursor: default; }
.gw-tool.is-off:hover { background: var(--gw-tool); }

/* An engaged toggle stays legible without dominating the row, a quiet strip
   of near-identical dark buttons. */
.gw-tool.is-on { background: #5c6470; color: #f2f4f7; }

/* BETA (§85): a word rather than a glyph, so as wide as it needs; in the
   accent while beta features are hidden and something uses one. */
.gw-tool[data-tool="beta"] { width: auto; padding: 0 6px; }
.gw-tool-beta { font-family: var(--gw-ui); font-size: 8.5px; font-weight: 700; letter-spacing: 0.8px; }
.gw-tool[data-tool="beta"].is-set { color: var(--gw-accent); box-shadow: inset 0 0 0 1px var(--gw-accent); }

.gw-tool-group {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  background: var(--gw-tool);
  border-radius: 4px;
}

.gw-tool-group .gw-tool { background: #cfd3d8; color: #111; width: 20px; height: 18px; }
.gw-tool-group .gw-tool.is-on { background: #f2a01e; }

.gw-tool-swatch {
  flex: 0 0 20px;
  width: 20px;
  height: 18px;
  border-radius: 2px;
  border: 1px solid #555;
  cursor: pointer;
}

/* Transparent background: show a checker so the state is unmistakable. */
.gw-tool-swatch.is-transparent {
  background-color: #8c8c8c;
  background-image: linear-gradient(45deg, #5a5a5a 25%, transparent 25%, transparent 75%, #5a5a5a 75%);
  background-size: 8px 8px;
}

.gw-tool-a {
  font-family: var(--gw-ui);
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
}

.gw-tool-tile {
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #cfd3d8;
  border-radius: 2px;
}

.gw-tool-m {
  font-family: var(--gw-ui);
  font-size: 10px;
  font-weight: 700;
  color: #111;
  line-height: 1;
}

.gw-tool.is-on .gw-tool-tile { background: #111; }
.gw-tool.is-on .gw-tool-m { color: #cfd3d8; }

/* -------------------------------------------------------- palette tools */

.gw-row-palette { padding-left: 4px; }
.gw-palette-tools { display: flex; gap: 3px; flex: 0 0 auto; }
.gw-palette-tools .gw-tool { width: 22px; height: 18px; }
.gw-picker-palette { margin-left: 6px; }

/* --------------------------------------------------------------- footer */

/*
 * The commit bar: first thing under the logo.
 *
 * It used to sit last, under nine sections, which in the plugin's 332x835
 * window is a long scroll from the top — and RENDER is the control reached
 * first and most often.
 *
 * In flow, deliberately. `position: sticky` was tried first and UXP renders it
 * wrongly: on a flex column child it drops out of flow and collapses to
 * content width, so RENDER came out as a small box overlapping the wordmark.
 * Pinning it properly would mean mounting it outside the scrolling element,
 * and the two host pages use different ones — `#gw-scroll` in the plugin,
 * `#gw-panel` in the app — so that would buy a pinned bar at the cost of the
 * two surfaces no longer having the same structure, which is the property
 * `dev/surface-parity.js` exists to hold.
 */
.gw-footer { padding: 2px 0 4px; }

.gw-btn {
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #1e1e1e;
  border: 1px solid var(--gw-outline);
  border-radius: 6px;
  color: var(--gw-text);
  font-family: var(--gw-ui);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.9px;
  cursor: pointer;
  user-select: none;
}

.gw-btn:hover { background: #161616; }
.gw-btn.is-busy { color: var(--gw-text-dim); cursor: progress; }

.gw-commit { display: flex; gap: 8px; }

.gw-commit .gw-btn { flex: 1 1 50%; }

/* -------------------------------------------------------------- popover */

.gw-popover {
  position: fixed;
  z-index: 40;
  width: 210px;
  padding: 8px;
  background: #1c1c1c;
  border: 1px solid #4a4a4a;
  border-radius: 6px;
}

.gw-popover-top { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }

.gw-popover-x {
  flex: 0 0 18px;
  width: 18px;
  height: 22px;
  box-sizing: border-box;
  line-height: 20px;
  text-align: center;
  font-size: 15px;
  color: var(--gw-text-dim);
  border: 1px solid var(--gw-field-border);
  border-radius: 3px;
  cursor: pointer;
}

.gw-popover-x:hover { color: var(--gw-text); border-color: #6a6a6a; }

/* The eyedropper: the close button's box, lit while a pick is waiting. */
.gw-popover-pick {
  flex: 0 0 22px;
  width: 22px;
  height: 22px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--gw-text-dim);
  border: 1px solid var(--gw-field-border);
  border-radius: 3px;
  cursor: pointer;
}

.gw-popover-pick:hover { color: var(--gw-text); border-color: #6a6a6a; }
.gw-popover-pick.is-on { color: #161616; background: var(--gw-accent); border-color: var(--gw-accent); }

.gw-popover-preview {
  flex: 0 0 34px;
  height: 22px;
  border-radius: 3px;
  border: 1px solid #555;
}

/*
 * A fixed width, not `flex: 1 1 auto`.
 *
 * UXP maps `<input>` to Adobe's own text field, chrome and all, and a flex
 * item's `min-width` defaults to `auto` — so the field refused to shrink below
 * its intrinsic width and ran off the right-hand edge of a 210px popover.
 * `flex-basis: auto` made it worse by starting the calculation from that same
 * intrinsic width.
 *
 * 128px, measured rather than derived: 210 - 16 padding - 34 preview - 18
 * close - 12 of two gaps comes to 130, and at 130 the close button's right
 * edge landed 1px outside the content box. 128 leaves the row 2px of slack.
 */
.gw-popover-hex {
  flex: 0 0 128px;
  width: 128px;
  min-width: 0;
  box-sizing: border-box;
  height: 22px;
  padding: 0 6px;
  background: var(--gw-field);
  border: 1px solid var(--gw-field-border);
  border-radius: 3px;
  color: var(--gw-text);
  font-family: var(--gw-ui);
  font-size: 11px;
}

/* With the eyedropper in the row the field gives up its box and a gap, 28px,
   by the same arithmetic: "#rrggbb" at 11px wants about 50. */
.gw-popover.has-pick .gw-popover-hex { flex-basis: 100px; width: 100px; }

/*
 * The two gradient stops. Whichever is selected takes the sliders, the hex
 * field and the chip strip below, so the tiles need to say which one that is
 * loudly enough to be read at 22 px: the selected one gets a light border and
 * the other a dim one.
 *
 * The empty stop is drawn as diagonal stripes rather than as a transparent
 * tile, because UXP does not honour `transparent` as a border or background
 * colour — the borderless value field and the CSS chevron were both lost to
 * that, and a stop that renders as a plain dark square would read as black
 * ink rather than as no ink.
 */
.gw-popover-stops {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 7px;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--gw-hairline);
}

/*
 * The gradient strip, and its stops.
 *
 * The strip is tall enough to be a pointer target on its own — clicking it
 * adds a stop — and the markers hang *below* the ink so a marker never covers
 * the colour it is pointing at. `overflow: visible` on the strip is therefore
 * load-bearing.
 *
 * Every colour here is an opaque hex or a CSS gradient of them. No
 * `transparent`, for the reason the spectrum field's comment gives at length:
 * UXP does not honour it, and a marker that rendered as a black square would
 * be indistinguishable from a stop holding black.
 */
.gw-grad-strip {
  position: relative;
  height: 18px;
  margin-bottom: 9px;
  cursor: copy;
}

.gw-grad-ink {
  height: 18px;
  border: 1px solid var(--gw-field-border);
  border-radius: 3px;
}

/*
 * A marker is a square rotated to a diamond, which reads as a pointer rather
 * than as a swatch and cannot be confused with the chips in the strip below.
 * Its own colour is inside it, so the list can be read at a glance.
 */
.gw-grad-stop {
  position: absolute;
  top: 17px;
  width: 9px;
  height: 9px;
  margin-left: -4.5px;
  border: 1px solid #14171b;
  transform: rotate(45deg);
  cursor: ew-resize;
}

.gw-grad-stop.is-on { border-color: #f2f2f2; }

/* The ink's own stop cannot move or be removed — it is the swatch. A round
   marker says so without a tooltip. */
.gw-grad-stop.is-fixed {
  border-radius: 50%;
  transform: none;
  cursor: default;
}

.gw-grad-actions { display: flex; gap: 5px; }

/* Its own row, below the chips, so a destructive button is nowhere near the
   colours you click to choose one. */
.gw-popover-delrow { display: flex; margin-top: 6px; }

.gw-popover-del {
  flex: 1 1 auto;
  height: 22px;
  line-height: 22px;
  box-sizing: border-box;
  text-align: center;
  font-size: 9px;
  letter-spacing: 0.08em;
  color: var(--gw-text-dim);
  background: #241c1c;
  border: 1px solid #5a3a3a;
  border-radius: 3px;
  cursor: pointer;
}

.gw-popover-del:hover { color: #f2d0d0; border-color: #8a4a4a; background: #3a2222; }

.gw-popover-stop {
  flex: 0 0 30px;
  height: 22px;
  border-radius: 3px;
  border: 1px solid #4a4a4a;
  cursor: pointer;
}

.gw-popover-stop.is-on { border-color: #cfcfcf; }

.gw-popover-stop.is-empty {
  background-color: #2a2a2a;
  background-image: repeating-linear-gradient(
    45deg, #3c3c3c 0, #3c3c3c 2px, #242424 2px, #242424 4px);
}

.gw-popover-fade {
  flex: 1 1 auto;
  height: 22px;
  line-height: 22px;
  text-align: center;
  border-radius: 3px;
  border: 1px solid var(--gw-field-border);
  background: var(--gw-field);
  color: var(--gw-text-dim);
  font-family: var(--gw-ui);
  font-size: 9px;
  letter-spacing: 0.08em;
  cursor: pointer;
}

.gw-popover-fade.is-on { color: var(--gw-text); border-color: #6a6a6a; }

/*
 * The saturation/value field and the hue strip, which replaced three H/S/V
 * sliders.
 *
 * Every colour here is an opaque hex. There is no `transparent`, no `rgba()`
 * and no canvas, and all three are constraints rather than preferences — see
 * the long comment on `ColorPopover` in js/ui.js. `dev/host-parity.mjs` reads
 * all nine of the rules below back as text and fails the build if a zero-alpha
 * colour appears in any of them, or if one of them goes missing.
 *
 * The border sits on the container and the geometry on an inner element, so
 * the 1 px frame cannot shift the mapping from pointer position to colour:
 * `ColorPopover._frac` measures `.gw-sv-rows` and `.gw-hue-ramp`, and the
 * markers' percentages resolve against the same padding box.
 *
 * Square, not rounded. A radius needs `overflow: hidden` to clip the rows,
 * and that would eat the half of the marker ring that hangs outside the field
 * whenever saturation or value is at an end — which is exactly where it is
 * most useful to see where you are.
 */
.gw-popover-sv {
  position: relative;
  margin-bottom: 7px;
  border: 1px solid var(--gw-field-border);
  cursor: crosshair;
}

/* 120 rows of 1 px. The count is `SV_ROWS` in js/ui.js and the two must
   agree: a row taller than a pixel bands visibly down the grey edge. */
.gw-sv-rows { height: 120px; }
.gw-sv-row { height: 1px; }

/*
 * A two-tone ring: a dark 1 px frame around a light 2 px one. A single-tone
 * marker vanishes at one corner of this field or another — white at the white
 * corner, dark along the black edge — and the live colour sits inside it so
 * the ring reads as a sample rather than as a cursor.
 */
.gw-sv-ring {
  position: absolute;
  width: 14px;
  height: 14px;
  margin-left: -7px;
  margin-top: -7px;
  border: 1px solid #14171b;
  border-radius: 50%;
  background-color: #14171b;
}

.gw-sv-dot {
  position: absolute;
  left: 1px;
  top: 1px;
  width: 8px;
  height: 8px;
  border: 2px solid #f2f2f2;
  border-radius: 50%;
  /* A placeholder `_placeMarkers` overwrites on every move. It has to be some
     colour rather than none: UXP would not honour `transparent` here either. */
  background-color: #808080;
}

.gw-popover-hue {
  position: relative;
  margin-bottom: 8px;
  border: 1px solid var(--gw-field-border);
  cursor: ew-resize;
}

/* Six two-stop gradients rather than one seven-stop one; hue is piecewise
   linear in RGB, so the six segments are exact. */
.gw-hue-ramp { display: flex; height: 14px; }
.gw-hue-seg { flex: 1 1 0; }

.gw-hue-ring {
  position: absolute;
  top: -2px;
  width: 7px;
  height: 18px;
  margin-left: -3.5px;
  border: 1px solid #14171b;
  border-radius: 2px;
  background-color: #f2f2f2;
}

.gw-popover-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  margin-top: 8px;
}

/* A colour still to be chosen: the empty stop's stripes. */
.gw-popover-preview.is-empty {
  background-color: #2a2a2a;
  background-image: repeating-linear-gradient(
    45deg, #3c3c3c 0, #3c3c3c 2px, #242424 2px, #242424 4px);
}

/* H, S and L under the chips. */
.gw-popover-hsl { margin-top: 8px; }
.gw-hsl-row {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 18px;
}
.gw-hsl-name {
  flex: 0 0 10px;
  font-size: 9px;
  color: var(--gw-text-dim);
}
.gw-hsl-row .gw-slider { flex: 1 1 auto; }
.gw-hsl-value {
  flex: 0 0 24px;
  font-size: 9px;
  color: var(--gw-text);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.gw-popover-chip {
  width: 21px;
  height: 14px;
  border-radius: 2px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  cursor: pointer;
}

/* ---------------------------------------------------------------- toast */

.gw-toast {
  position: fixed;
  left: 8px;
  right: 8px;
  bottom: 8px;
  z-index: 50;
  padding: 7px 9px;
  background: #1f1f1f;
  border: 1px solid #4a4a4a;
  border-radius: 5px;
  color: var(--gw-text);
  font-size: 10.5px;
  display: none;
}

.gw-toast.is-visible { display: block; }
.gw-toast.is-error { border-color: #a8342b; color: #ffb9b3; }

/* ----------------------------------------------------------------- help */

/*
 * The help panel, and the chrome About and Unfreeze's question share with it.
 *
 * Help itself is two panes wherever it has 560 px (§90): the rules for that
 * hang off `.gw-help-book`, the class `buildHelp` adds, so the two dialogs
 * that also carry `.gw-help` stay the one column they are. In Photoshop a
 * modal dialog is a window of its own rather than a box in the panel, which
 * is why Help can be wider there than the panel that opens it; see
 * `showHelp` in js/main.js. The examples are the reason to want the width —
 * a 336 px figure shows the dot structure, and the dot structure is the thing
 * the tool is about.
 */
/*
 * No `vw`/`vh` in this file. UXP resolves a viewport unit to ~0 rather than
 * dropping it, and the difference matters: a dropped `max-width: 94vw` would
 * have left the 368px below, but a `max-width` of 0 collapsed this dialog to
 * its min-content width — about 50px, one character per line, down the left
 * edge of the panel. The size is set in px from a measured `clientWidth`
 * instead; see `showHelp` in js/main.js. `dev/host-parity.mjs` keeps them out.
 */
.gw-help {
  display: block;
  width: 368px;
  box-sizing: border-box;
  overflow-y: auto;
  padding: 14px;
  background: #1a1a1a;
  border: 1px solid #4a4a4a;
  border-radius: 8px;
  color: var(--gw-text);
  font-size: 11px;
  line-height: 1.5;
}

/*
 * Nothing in here may shrink.
 *
 * The panel lays this dialog out as a flex column whatever `display` says, and
 * a flex child's default `flex-shrink: 1` means a definite height gets shared
 * out rather than scrolled. The arithmetic is the symptom: the children add up
 * to 1,607px inside a 744px box, a 54% squeeze, and what the panel drew was
 * every paragraph overlapping the next by about half its lines. Each block
 * keeps its content height now, and the definite height plus `overflow-y`
 * above scrolls it — the same arrangement as `#gw-scroll`, which is the one
 * scroll container known to work here.
 *
 * Both declarations are inert in a browser: the dialog is already a block
 * there, so `flex-shrink` on its children means nothing, and the app measures
 * 0 overlapping pairs before and after.
 */
.gw-help > * { flex-shrink: 0; }

.gw-help h3 {
  margin: 0 0 8px;
  font-family: var(--gw-ui);
  font-weight: 600;
  letter-spacing: 0.4px;
  font-size: 13px;
}

.gw-help p { margin: 0 0 8px; }
.gw-help code { font-family: var(--gw-num); color: var(--gw-fill); }

/* The update line. `word-break` because the release URL is 78 characters and
   the docked panel is 320 wide — without it the panel grows a horizontal
   scrollbar and the sentence runs off the edge of it. */
.gw-help-update {
  margin-top: 6px;
  min-height: 15px;
  word-break: break-word;
}

/* ------------------------------------------------------------- tone ramp */

.gw-tone-host {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.gw-note {
  padding: 7px 10px;
  background: var(--gw-row);
  border-radius: 6px;
  color: var(--gw-text-dim);
  font-size: 10.5px;
  line-height: 1.4;
}

/* ------------------------------------------------------------ palette menu */

.gw-palette-btn {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  height: 19px;
  padding: 0 5px;
  background: var(--gw-field);
  border: 1px solid var(--gw-field-border);
  border-radius: 3px;
  color: var(--gw-text);
  font-size: 10.5px;
  cursor: pointer;
  overflow: hidden;
}

.gw-palette-btn:hover { border-color: #6a6a6a; }
.gw-palette-btn.is-off { color: var(--gw-text-off); border-color: #333; background: #141414; cursor: default; }
.gw-palette-btn-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gw-palette-btn-caret { flex: 0 0 auto; color: var(--gw-text-dim); }

.gw-palette-menu {
  position: fixed;
  z-index: 70;
  width: 244px;
  /* Height set in px from a measured innerHeight in `show()`, for the reason
     given above `.gw-help`. */
  overflow-y: auto;
  background: #2a2a2a;
  border: 1px solid #555;
  border-radius: 4px;
  padding: 3px 0;
}

.gw-palette-group {
  padding: 6px 10px 2px;
  color: #9a9a9a;
  font-size: 9px;
  letter-spacing: .7px;
  text-transform: uppercase;
}

.gw-palette-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  color: #e4e4e4;
  font-size: 10.5px;
  cursor: pointer;
  white-space: nowrap;
}

.gw-palette-item:hover { background: #3d3d3d; }
.gw-palette-item.is-sel { background: #444a52; }
.gw-palette-item-label { flex: 0 0 auto; max-width: 104px; overflow: hidden; text-overflow: ellipsis; }
.gw-palette-item-note { flex: 0 0 auto; color: var(--gw-text-off); font-size: 9.5px; }

/* The chips sit right-aligned so the eye can run down them like a strip. */
.gw-palette-item-chips { flex: 1 1 auto; display: flex; justify-content: flex-end; gap: 1px; }

.gw-palette-chip {
  width: 8px;
  height: 12px;
  border-radius: 1px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .35);
}

/* ---------------------------------------------------------- inline action */

.gw-row-action {
  padding: 0;
  background: transparent;
}

.gw-btn-inline {
  flex: 1 1 auto;
  height: 24px;
  font-size: 11px;
  letter-spacing: 1.2px;
}

/* A button with nothing to act on: visibly inert, still laid out. */
.gw-btn.is-off {
  color: var(--gw-text-off);
  border-color: #333;
  cursor: default;
}

.gw-btn.is-off:hover { background: transparent; }

.gw-preset-host { display: block; }

/* Several buttons sharing one action row. *//* Several buttons sharing one action row. */
.gw-row-action.gw-row-split {
  display: flex;
  gap: 4px;
}

/* A free-text field fills the row instead of sitting at a number's width. */
.gw-field-text {
  flex: 1 1 auto;
  cursor: text;
}

.gw-field-input-text {
  text-align: left;
  padding: 0 6px;
}

.gw-field-input-text::placeholder { color: var(--gw-text-off); }.gw-btn-inline {
  flex: 1 1 auto;
  height: 24px;
  font-size: 11px;
  letter-spacing: 1.2px;
}

/* ==================================================================== curves */

.gw-curve {
  display: flex;
  flex-direction: column;
  gap: 5px;
  background: var(--gw-row);
  border-radius: 9px;
  padding: 7px 8px 8px;
}

.gw-curve.is-disabled { background: var(--gw-row-disabled); }
.gw-curve.is-disabled .gw-curve-plot { opacity: 0.4; }

.gw-curve-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Matched to `.gw-section-title` — it is the same kind of label and was the
   last thing still set in poster tracking. */
.gw-curve-title {
  color: var(--gw-text-dim);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.8px;
  text-transform: uppercase;
}

.gw-curve-chans { display: flex; gap: 3px; }

.gw-curve-chan {
  min-width: 26px;
  padding: 2px 5px;
  border-radius: 5px;
  background: var(--gw-pill);
  color: var(--gw-text-off);
  font-size: 9px;
  letter-spacing: 0.6px;
  text-align: center;
  user-select: none;
}

.gw-curve-chan.is-on { background: var(--gw-fill); color: #14171b; }

/* A channel that has been shaped, so a curve hiding on another tab is visible
   from the one you are looking at. */
.gw-curve-chan.is-set { color: var(--gw-text); }
.gw-curve-chan-r.is-set { color: #ff6b6b; }
.gw-curve-chan-g.is-set { color: #63d471; }
.gw-curve-chan-b.is-set { color: #6aa9ff; }
.gw-curve-chan.is-on.is-set { color: #14171b; }

/* The width/height here are a browser fallback only — UXP is sized by the
   attributes CurveRow sets, because it resolves a CSS height on a viewBox'd
   <svg> as Infinity. See DESIGN.md §34. */
.gw-curve-plot {
  display: block;
  max-width: 100%;
  background: var(--gw-field);
  border: 1px solid var(--gw-field-border);
  border-radius: 6px;
}

/*
 * The ghost histogram. Filled in the quiet accent — a warm #3a2a18 against the
 * plot's #1c1c1c ground — which is what `--gw-accent-quiet` exists for: it
 * reads as a shape without approaching the grid's #4a4a4a, so the grid lines
 * and the curve both stay on top of it rather than fighting it. No stroke, for
 * the same reason: an outlined silhouette is a chart, and this is a backdrop.
 */
.gw-curve-hist { fill: var(--gw-accent-quiet); stroke: none; }

.gw-curve-grid { stroke: var(--gw-outline); stroke-width: 1; fill: none; }
.gw-curve-diag { stroke: var(--gw-outline); stroke-width: 1; fill: none; opacity: 0.55; }

.gw-curve-line { fill: none; stroke: var(--gw-fill); stroke-width: 3; }
.gw-curve-line[data-channel="r"] { stroke: #ff6b6b; }
.gw-curve-line[data-channel="g"] { stroke: #63d471; }
.gw-curve-line[data-channel="b"] { stroke: #6aa9ff; }

.gw-curve-dot {
  fill: var(--gw-bg);
  stroke: var(--gw-fill);
  stroke-width: 3;
}


/* ================================================================== help */

.gw-help-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}

.gw-help-head h3 { margin: 0; font-size: 13px; letter-spacing: 0.02em; }

.gw-help-x {
  width: 20px;
  height: 20px;
  line-height: 18px;
  text-align: center;
  border-radius: 3px;
  border: 1px solid var(--gw-field-border);
  color: var(--gw-text-dim);
  font-size: 14px;
  cursor: pointer;
}

.gw-help-title {
  margin: 12px 0 6px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--gw-hairline);
  color: var(--gw-text-dim);
  font-size: 9px;
  letter-spacing: 0.14em;
}

.gw-help-title:first-of-type { margin-top: 4px; }

/* The figure sits on the mid grey rather than on the panel, so a preset whose
   own background is near-black still reads as a picture with edges. */
.gw-help-figwrap {
  display: flex;
  justify-content: center;
  padding: 8px;
  background: var(--gw-bg-raised);
  border: 1px solid var(--gw-hairline);
  border-radius: 4px;
}

/* Nearest-neighbour, because the example *is* a dither and smoothing it would
   show something the renderer does not produce. `image-rendering` is ignored
   for SVG geometry, which is the other reason the examples are paths: a run of
   1x1 rects cannot be resampled at all. */
.gw-help-fig { display: block; }

.gw-help-caption {
  margin: 7px 0 8px;
  color: var(--gw-text-dim);
  font-size: 10px;
  line-height: 1.45;
  min-height: 28px;
}

.gw-help-apply-row { display: flex; margin-bottom: 10px; }
.gw-help-apply-row .gw-btn { flex: 1 1 auto; }

.gw-help-group { margin-bottom: 8px; }

.gw-help-group-name {
  margin-bottom: 4px;
  color: var(--gw-text-dim);
  font-size: 8px;
  letter-spacing: 0.12em;
}

.gw-help-chips { display: flex; flex-wrap: wrap; gap: 4px; }

.gw-help-chip {
  padding: 3px 7px;
  border-radius: 3px;
  border: 1px solid var(--gw-field-border);
  background: var(--gw-field);
  color: var(--gw-text-dim);
  font-family: var(--gw-ui);
  font-size: 10px;
  cursor: pointer;
}

.gw-help-chip.is-on {
  border-color: #7d7d7d;
  color: var(--gw-text);
}

.gw-help-p { margin: 0 0 7px; }

/* ------------------------------------------------- help: two panes (§90) */

/*
 * Help as a list of topics and one topic, in two layouts of the same boxes.
 *
 * `is-panes` puts the list down the left and the page beside it, each
 * scrolling on its own; `is-stacked` puts the list above the page and
 * scrolls the pair as one. `buildHelp`'s `layout` chooses between them from
 * the dialog's width, and gives every box that scrolls a height in px —
 * which is also why none of the sizes a scroll depends on are written here.
 *
 * **No box given a size in px has a padding or a border of its own.** The
 * padding is on the page inside it and the list's hairline is a box of its
 * own, so the arithmetic is the same whether or not an engine honours
 * `box-sizing`, which Adobe's list of the CSS UXP supports does not name.
 * The head is the one px box with a border, and it is 39 px and its 1 px
 * hairline under either model: `layout` sets 39 and this leaves the head at
 * `content-box`, the default.
 *
 * Flex and nothing else: UXP's `display` is none, inline, block,
 * inline-block, flex and inline-flex, and no grid.
 */
.gw-help.gw-help-book { padding: 0; }

.gw-help-book .gw-help-head {
  box-sizing: content-box;
  margin: 0;
  padding: 0 12px 0 16px;
  border-bottom: 1px solid var(--gw-hairline);
}

/* The header's wordmark, smaller and to the left. */
.gw-help-wordmark {
  justify-content: flex-start;
  font-size: 15px;
  letter-spacing: -0.1px;
}
.gw-help-wordmark .gw-wordmark-gleam { width: 11px; height: 11px; }

.gw-help-body { display: flex; }
.gw-help-body > * { flex-shrink: 0; }
.gw-help-book.is-panes .gw-help-body { flex-direction: row; }
/* The hairline between the list and the page, 1 px of the 156 `layout`
   gives the list. */
.gw-help-rule { width: 1px; background: var(--gw-hairline); }
.gw-help-book.is-stacked .gw-help-rule { display: none; }
/* The one column scrolls as the whole dialog did before, which is the
   arrangement UXP is known to scroll: a definite height, and children that
   keep their own. */
.gw-help-book.is-stacked .gw-help-body { flex-direction: column; overflow-y: auto; }
/* Chromium counts a box that scrolls as focusable, and a modal dialog opens
   with focus on the first focusable thing in it — here the page, since
   nothing else in Help is a control the browser knows — so every open drew
   a focus ring round the whole pane. */
.gw-help-book .gw-help-body:focus,
.gw-help-book .gw-help-toc:focus,
.gw-help-book .gw-help-page:focus { outline: none; }

/* The list. A 2 px bar on every row, in the list's own background until the
   row is the one shown: UXP draws no `transparent`, so a bar that is not
   there has to be one the colour of what is behind it. */
.gw-help-toc > * { flex-shrink: 0; }
.gw-help-topic {
  color: var(--gw-text-dim);
  font-family: var(--gw-ui);
  font-size: 11px;
  line-height: 1.35;
  cursor: pointer;
}
.gw-help-topic:hover { color: var(--gw-text); }

.gw-help-book.is-panes .gw-help-toc {
  overflow-y: auto;
  background: #161616;
}
.gw-help-book.is-panes .gw-help-topic {
  padding: 4px 12px 4px 14px;
  border-left: 2px solid #161616;
}
.gw-help-book.is-panes .gw-help-topic:first-child { margin-top: 8px; }
.gw-help-book.is-panes .gw-help-topic:last-child { margin-bottom: 8px; }
.gw-help-book.is-panes .gw-help-topic.is-on {
  color: var(--gw-text);
  background: var(--gw-row);
  border-left-color: var(--gw-accent);
}

/* Above the page, the list is chips, as the examples' presets are. Margins
   rather than `gap`, so the spacing does not depend on one more property. */
.gw-help-book.is-stacked .gw-help-toc {
  display: flex;
  flex-wrap: wrap;
  padding: 10px 10px 6px 14px;
  border-bottom: 1px solid var(--gw-hairline);
}
.gw-help-book.is-stacked .gw-help-topic {
  margin: 0 4px 4px 0;
  padding: 3px 7px;
  border: 1px solid var(--gw-field-border);
  border-radius: 3px;
  background: var(--gw-field);
}
.gw-help-book.is-stacked .gw-help-topic.is-on {
  border-color: var(--gw-accent);
  color: var(--gw-text);
}

/* The page, and inside it the topic, which carries the padding: `layout`'s
   PAD_WIDE and PAD_NARROW. */
.gw-help-page > * { flex-shrink: 0; }
.gw-help-book.is-panes .gw-help-page { overflow-y: auto; }
.gw-help-book.is-panes .gw-help-topic-page { padding: 14px 16px 18px; }
.gw-help-book.is-stacked .gw-help-topic-page { padding: 12px 14px 16px; }

.gw-help-topic-page > * { flex-shrink: 0; }
.gw-help-topic-page > .gw-help-title { margin-top: 0; }

/* What the thing is for, a size up from what follows it. */
.gw-help-lead {
  margin: 0 0 10px;
  color: var(--gw-text);
  font-size: 12px;
  line-height: 1.45;
}

/*
 * A point: its bullet and its text as two boxes in a row.
 *
 * Not `<li>`: UXP has no `::before` to draw a bullet in, and a list marker is
 * nothing this code can see drawn. The dot sits on the first line's centre —
 * 16.5 px of line at 11 px and 1.5, so 6 px down for a 4 px dot — and the
 * text wraps under itself.
 */
.gw-help-list { margin: 0 0 10px; }
.gw-help-li { display: flex; align-items: flex-start; margin: 0 0 4px; }
.gw-help-dot {
  flex: 0 0 4px;
  width: 4px;
  height: 4px;
  margin: 6px 9px 0 2px;
  border-radius: 2px;
  background: var(--gw-text-dim);
}
.gw-help-li-text { flex: 1 1 0px; min-width: 0; }

/* ============================================================ status bar */

/*
 * The bar the icon row became. In the plugin it is the foot of the panel; in
 * the app the shell moves it under the image. Same element either way, so it
 * has to look right against both a panel edge and a window edge — hence a top
 * hairline rather than a box.
 */
.gw-statusbar {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 6px 8px;
  border-top: 1px solid var(--gw-hairline);
}

/* Inside the app's bottom bar it is one item among several, so it drops the
   rule it needs when it is the foot of a panel — and takes the rest of the
   bar, so its load bar reaches the bar's right-hand end. */
#gw-viewtools .gw-statusbar {
  flex: 1 1 auto;
  padding: 0;
  border-top: 0;
}

/*
 * The load bar (§84): the accent, sliding along a dim track at the bar's
 * right-hand end while something is loading. `display` rather than
 * `opacity` to show it, which a UXP panel does not honour everywhere, and a
 * transform to move it, which the compositor runs while a render has the
 * main thread.
 */
.gw-loadbar {
  display: none;
  flex: 0 0 auto;
  width: 48px;
  height: 3px;
  margin-left: auto;
  margin-right: 2px;
  overflow: hidden;
  border-radius: 2px;
  background: #2c2c2c;
}
.gw-loadbar.is-on { display: block; }
/* The whole track, and the animation makes it the moving segment. A UXP
   panel runs no keyframes: in Photoshop the segment sat still at the
   track's left like a bar a third full, and a still bar should say
   "working", not "a third done". So what shows with no animation is the
   whole bar in the accent. */
.gw-loadbar-ink {
  width: 48px;
  height: 3px;
  border-radius: 2px;
  background: var(--gw-accent);
  transform-origin: 0 0;
  animation: gw-load 1.1s ease-in-out infinite;
}
@keyframes gw-load {
  from { transform: translateX(-18px) scaleX(0.375); }
  to { transform: translateX(48px) scaleX(0.375); }
}

/* ================================================================== menu */

.gw-popmenu {
  position: fixed;
  z-index: 60;
  min-width: 190px;
  max-width: 280px;
  padding: 4px;
  background: #1c1c1c;
  border: 1px solid #4a4a4a;
  border-radius: 6px;
}

.gw-popmenu-item {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 5px 7px;
  border-radius: 3px;
  color: var(--gw-text);
  font-family: var(--gw-ui);
  font-size: 12px;
  cursor: pointer;
}

.gw-popmenu-item:hover { background: var(--gw-accent); color: #16130f; }
.gw-popmenu-item:hover .gw-popmenu-note { color: #3a2a18; }

/* Not `pointer-events: none`, which would let a click fall through to the item
   behind it in the list. */
.gw-popmenu-item.is-off {
  opacity: 0.45;
  cursor: default;
}
.gw-popmenu-item.is-off:hover { background: none; color: var(--gw-text); }

/*
 * The tick keeps its column whether or not it is showing, so a label does not
 * shift sideways when the thing it names is switched on. A pseudo-element
 * would have done it in one rule and cannot be relied on in UXP.
 */
.gw-popmenu-tick {
  flex: 0 0 11px;
  font-size: 11px;
  line-height: 1;
}

.gw-popmenu-label { flex: 1 1 auto; }

.gw-popmenu-note {
  flex: 0 0 auto;
  color: var(--gw-text-off);
  font-size: 10px;
}

.gw-popmenu-div {
  height: 1px;
  margin: 4px 2px;
  background: var(--gw-hairline);
}

/* A quiet full-width button, for the one under RENDER. Not `.gw-btn-inline`,
   which is half-width and lives in a split row. */
.gw-btn-quiet {
  margin-top: 5px;
  background: none;
  border: 1px solid var(--gw-hairline);
  color: var(--gw-text-dim);
  font-size: 9px;
}

.gw-btn-quiet:hover { color: var(--gw-text); border-color: var(--gw-field-border); }

/*
 * The update row at the foot of the panel.
 *
 * Its own rules rather than the help dialog's, because it is no longer in the
 * help dialog: the button sits with RESET ALL SETTINGS and the answer is one
 * line of small text under it, which can run to a couple of sentences with an
 * address in it and must wrap rather than widen the panel.
 */
.gw-update-row {
  display: flex;
  gap: 6px;
  margin-top: 6px;
}
.gw-update-row .gw-btn { flex: 1 1 auto; }
.gw-update-msg {
  margin: 4px 0 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--gw-dim, #8a8a8a);
  /* An address does not break at a space, and the panel is 368px wide. */
  overflow-wrap: anywhere;
}
.gw-update-msg:empty { display: none; }

/* About is a heading and a button; Help is a manual. Same chrome, no filling. */
.gw-about { display: block; }
.gw-about .gw-help-head { margin-bottom: 8px; }

/* A question and its two answers (`askUser`), the one that goes ahead on the
   right and in the accent. */
.gw-confirm { display: block; }
.gw-confirm-row { display: flex; justify-content: flex-end; gap: 6px; margin-top: 10px; }
.gw-confirm-row .gw-btn { padding: 0 12px; }
.gw-confirm-row .gw-btn.is-go { border-color: var(--gw-accent); color: var(--gw-accent); }

/* A frozen mask's marker on its row (§83): the accent, as a mask's own
   state is elsewhere on the row. */
.gw-mask-frozen {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  color: var(--gw-accent);
  cursor: pointer;
}

/* ================================================================== masks */

/* The mask frame is placed in the panel's root; see `layoutMaskScope`. */
.gw-root { position: relative; }

/* While a mask is edited: its header is the frame's top, and three strips
   are its sides and foot, in the accent, around every control below. */
.gw-mask-edge {
  position: absolute;
  display: none;
  z-index: 5;
  width: 1px;
  height: 1px;
  background: var(--gw-accent);
}

/* A row with rows folded under it (`nestRows`): its name opens them. */
.gw-label.has-subrows { cursor: pointer; }
/* A name too long for the column wraps onto a second line rather than
   losing its end: "Color Resolution" beside its chip is about 107 px in the
   app's 98 px column (its text renders some 14 % wider than Chromium's), and
   an ellipsis cut it to "Color Resolut…" (§80). Two lines of 12 px fit the
   28 px row. */
.gw-label-text {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  line-height: 12px;
  /* A word at a time (`nestRows`), so a wrapped name is as wide as its
     words and the chip stays beside the last of them (§82). */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  align-content: center;
}
.gw-label-word { flex: 0 0 auto; }
.gw-label-tail { display: flex; align-items: center; flex: 0 0 auto; }
/* The space between words is this margin, and the space itself a hidden
   span that keeps the label's text whole: a UXP panel drops a trailing
   space inside a flex item, and drew "RenderMode" (§84). */
.gw-label-space { display: none; }
.gw-label-word.is-after, .gw-label-tail.is-after { margin-left: 3px; }
/* A raised chip, so it reads as a button: the 8 px glyph in the dimmest grey
   it replaced was reported as "not noticeable enough". Opaque colours only,
   for UXP. */
.gw-sub-caret {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  /* 14 px and a 4 px gap: "Color Resolution" is 78 px of text in the 97 px
     label column, and at 16 + 6 it came to 100 and was cut (§80). */
  width: 14px;
  height: 14px;
  margin-left: 4px;
  border-radius: 4px;
  border: 1px solid var(--gw-field-border);
  background: var(--gw-pill);
  color: var(--gw-text);
}
.gw-label.has-subrows:hover .gw-sub-caret { background: #474747; color: #ffffff; }
.gw-sub-caret.is-open { background: #474747; }
/* Something in there is not at its default. */
.gw-sub-caret.is-set { color: var(--gw-accent); border-color: var(--gw-accent); }
.gw-subrows {
  margin: 0 0 2px 10px;
  padding-left: 6px;
  border-left: 1px solid var(--gw-hairline);
}

.gw-mask-toolbar {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 2px 0 4px;
}

.gw-mask-new { flex: 1 1 auto; }

/* Small: a view switch beside the button that makes masks, not its equal. */
.gw-mask-chip {
  flex: 0 0 auto;
  height: 16px;
  line-height: 14px;
  padding: 0 5px;
  font-size: 8px;
  letter-spacing: 0.8px;
  color: var(--gw-text-dim);
  border: 1px solid var(--gw-outline);
  border-radius: 3px;
  cursor: pointer;
}

.gw-mask-chip.is-on { color: #ff6b6b; border-color: #a63a3a; }

.gw-mask-list { position: relative; }

.gw-mask-empty {
  padding: 6px 4px;
  font-size: 10px;
  color: var(--gw-text-off);
}

/* One line per mask; the selected one carries the accent and opens below. */
.gw-mask-row {
  display: flex;
  align-items: center;
  gap: 4px;
  height: 24px;
  margin-top: 3px;
  padding: 0 4px 0 2px;
  background: var(--gw-row);
  border: 1px solid var(--gw-hairline);
  border-radius: 4px;
  cursor: pointer;
}

.gw-mask-row:hover { background: var(--gw-bg-raised); }
/* The header of the mask being edited: the frame's top, and pinned over the
   rows beneath it as the panel scrolls, so it needs to be above them and
   solid. */
.gw-mask-row.is-target {
  position: relative;
  z-index: 6;
  border-color: var(--gw-accent);
  background: var(--gw-row);
}
.gw-mask-row.is-target.is-pinned { box-shadow: 0 3px 6px rgba(0, 0, 0, 0.45); }

.gw-mask-done {
  flex: 0 0 auto;
  height: 18px;
  line-height: 16px;
  padding: 0 8px;
  font-size: 9px;
  letter-spacing: 1.1px;
}
.gw-mask-row.is-off .gw-mask-name,
.gw-mask-row.is-off .gw-mask-sum { color: var(--gw-text-off); }

.gw-mask-grip {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  color: var(--gw-text-off);
  cursor: grab;
}

.gw-mask-eye,
.gw-mask-more {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  color: var(--gw-text-dim);
  border-radius: 3px;
}

.gw-mask-eye:hover,
.gw-mask-more:hover { color: var(--gw-text); background: var(--gw-pill); }
.gw-mask-more { font-size: 13px; }

.gw-mask-name {
  flex: 0 1 auto;
  font-size: 11px;
  color: var(--gw-text);
  white-space: nowrap;
  overflow: hidden;
}

.gw-mask-sum {
  flex: 1 1 auto;
  font-size: 9px;
  color: var(--gw-text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-align: right;
}

.gw-mask-rename {
  flex: 1 1 auto;
  height: 18px;
  font-size: 11px;
  color: var(--gw-text);
  background: var(--gw-field);
  border: 1px solid var(--gw-field-border);
  border-radius: 3px;
}

.gw-mask-dropline {
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--gw-accent);
}

/* Inside the frame, which is the only gold box now. */
.gw-mask-detail {
  margin: 0 0 4px;
  padding: 4px 4px 2px;
  background: var(--gw-bg);
}

.gw-part-list { margin-bottom: 4px; }

.gw-part-row {
  display: flex;
  align-items: center;
  gap: 4px;
  height: 20px;
  padding: 0 4px;
  border-radius: 3px;
  cursor: pointer;
}

.gw-part-row:hover { background: var(--gw-bg-raised); }
.gw-part-row.is-sel { background: var(--gw-pill); }

.gw-part-op {
  flex: 0 0 auto;
  width: 12px;
  font-size: 12px;
  color: var(--gw-accent);
  text-align: center;
}

.gw-part-name {
  flex: 1 1 auto;
  font-size: 10px;
  color: var(--gw-text);
}

/* A shape still to be drawn on the picture. */
.gw-part-wait {
  flex: 0 0 auto;
  margin-right: 4px;
  font-size: 9px;
  color: var(--gw-accent);
}

.gw-part-btn {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  line-height: 16px;
  font-size: 10px;
  text-align: center;
  color: var(--gw-text-dim);
  border-radius: 3px;
}

.gw-part-btn:hover { color: var(--gw-text); background: var(--gw-bg-raised); }
.gw-part-btn.is-on { color: var(--gw-accent); }

.gw-part-adds {
  display: flex;
  gap: 4px;
  margin-bottom: 4px;
}

.gw-part-add {
  flex: 1 1 0;
  padding: 0 2px;
  font-size: 9px;
  letter-spacing: 0.6px;
}

.gw-mask-rule {
  height: 1px;
  margin: 4px 0;
  background: var(--gw-hairline);
}

/* The foot of the mask's own settings, and the head of the look it prints:
   a bar in the frame's gold, and what the rows under it are. */
.gw-mask-divide {
  margin: 8px 0 2px;
  padding: 5px 0 1px;
  border-top: 3px solid var(--gw-accent);
  text-align: center;
}

.gw-mask-divide-text {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 1.2px;
  color: var(--gw-accent);
  white-space: nowrap;
}

/* A Luminance or Saturation band: one bar, over the scale it selects from
   and the picture's own distribution of it. See `BandRow`. */
.gw-band {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin: 2px 0 4px;
  padding: 5px 8px 6px;
  background: var(--gw-row);
  border-radius: 6px;
}

.gw-band.is-disabled .gw-band-plot { opacity: 0.4; }

.gw-band-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.gw-band-title {
  color: var(--gw-text-dim);
  font-size: 10px;
}

.gw-band-readout {
  color: var(--gw-text);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}

/* Sized by the attributes `BandRow` sets; this is the look only. */
.gw-band-plot {
  display: block;
  max-width: 100%;
  background: var(--gw-field);
  border: 1px solid var(--gw-field-border);
  border-radius: 4px;
  cursor: ew-resize;
}

/* Brighter than the curve's ghost: here the spread is what the band is
   placed against, not a backdrop to read something else over. */
.gw-band-hist { fill: var(--gw-text-off); opacity: 0.6; stroke: none; }
/* The viewBox is stretched, so a stroke is in its units: 4 is about a pixel. */
.gw-band-shape { fill: rgba(255, 107, 107, 0.22); stroke: #ff6b6b; stroke-width: 4; }
.gw-band-end { fill: var(--gw-fill); }

.gw-layer-name {
  flex: 1 1 auto;
  font-size: 10px;
  color: var(--gw-text);
  white-space: nowrap;
  overflow: hidden;
}

/* Infidelity standalone — the frame around the panel. */

html, body {
  margin: 0;
  height: 100%;
  background: #0a0a0a;
  color: #d5d5d6;
  font-family: "Adobe Clean", -apple-system, "Segoe UI", sans-serif;
  overflow: hidden;
}

#gw-app {
  display: flex;
  height: 100%;
}

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

#gw-stage {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: #111;
  border-right: 1px solid #262626;
}

#gw-stage.is-over { background: #16202b; }

/* `#gw-bar` and its centred `#gw-info` were here. The bar is gone and the
   filename is in the bottom bar; see the rule further down. */

.gw-btn2 {
  flex: 0 0 auto;
  padding: 5px 11px;
  background: #242424;
  border: 1px solid #3f3f3f;
  border-radius: 4px;
  color: #e4e4e4;
  font-size: 11px;
  cursor: pointer;
  user-select: none;
}

.gw-btn2:hover { background: #2f2f2f; }
/*
 * The accent, so Export is the same orange as the icon and the wordmark.
 *
 * A near-black label rather than a white one, and the reason is measured
 * rather than felt: against #e2842f, white is 2.77:1, the panel's own text
 * colour is 1.89:1, and #16130f is 6.68:1 (7.74:1 on the hover shade). Only
 * the dark one clears 4.5:1, so the button that says Export is the one place
 * in the panel with dark type on light.
 */
.gw-btn2.is-primary {
  background: var(--gw-accent);
  border-color: var(--gw-accent);
  color: #16130f;
}
.gw-btn2.is-primary:hover { background: #ef9139; border-color: #ef9139; }

.gw-field2 {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 4px;
  color: #9a9a9a;
}

.gw-field2 input,
.gw-field2 select {
  width: 62px;
  padding: 4px 5px;
  background: #1d1d1d;
  border: 1px solid #3f3f3f;
  border-radius: 3px;
  color: #e4e4e4;
  font-size: 11px;
}

.gw-field2 select { width: 78px; }
.gw-field2 input[type="color"] { width: 30px; padding: 2px; }

#gw-view {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  /* Positioned so the mask handles, absolutely placed inside it, are placed
     in its scrolled content and move with the canvas when it scrolls. */
  position: relative;
  display: flex;
  /* Centred by the canvas's own `margin: auto`, not by align/justify. Flex
     centring makes the overflow past the top and left edges unreachable, so a
     zoomed-in image could not be panned all the way back to its corner. */
  align-items: flex-start;
  justify-content: flex-start;
  padding: 12px;
}

#gw-canvas {
  display: none;
  /* Sized in pixels by applyZoom; centred here, and scrollable to every edge. */
  margin: auto;
  /* Checkerboard behind the image so a transparent background reads clearly. */
  background-color: #1a1a1a;
  background-image:
    linear-gradient(45deg, #232323 25%, transparent 25%, transparent 75%, #232323 75%),
    linear-gradient(45deg, #232323 25%, transparent 25%, transparent 75%, #232323 75%);
  background-size: 16px 16px;
  background-position: 0 0, 8px 8px;
}

#gw-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /*
   * `margin: auto` is what centres it, exactly as the canvas does.
   *
   * `#gw-view` sets `align-items: flex-start; justify-content: flex-start` on
   * purpose — flex centring makes the overflow past the top and left edges
   * unreachable, so a zoomed-in image cannot be panned back to its corner —
   * and the canvas works around that with its own `margin: auto`. The drop
   * zone never got the same treatment, so it sat in the top-left of the
   * viewer while the thing it is standing in for sits in the middle.
   */
  margin: auto;
  gap: 10px;
  padding: 40px;
  border: 1px dashed #3a3a3a;
  border-radius: 10px;
  color: #6f6f6f;
  font-size: 13px;
  text-align: center;
}

#gw-drop b { color: #b8b8b8; font-weight: 600; }
#gw-drop small { color: #5a5a5a; }

/* ------------------------------------------------------------ mask handles */

/*
 * The overlay js/maskhandles.js lays over the canvas: a box the canvas's
 * size, holding the red tint and the SVG. Nothing in it takes a pointer
 * event except the invisible `.gw-mh-hit` shapes, so the picture still pans
 * everywhere a press does not mean something to the mask.
 */
.gw-mh {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
}

.gw-mh-tint {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.gw-mh-svg {
  position: absolute;
  left: 0;
  top: 0;
  /* Handles of a shape larger than the picture are drawn, and grabbed, in
     the margin round it. */
  overflow: visible;
  pointer-events: none;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
}

.gw-mh-vis * { pointer-events: none; }

.gw-mh-hit .is-fill {
  fill: transparent;
  stroke: none;
  pointer-events: fill;
}

.gw-mh-hit .is-stroke {
  fill: none;
  stroke: transparent;
  stroke-linecap: round;
  stroke-linejoin: round;
  pointer-events: stroke;
}

/* Every line twice: a dark band under a light one, so it reads on a white
   sky and in a black shadow alike. */
.gw-mh-under {
  fill: none;
  stroke: rgba(0, 0, 0, 0.55);
  stroke-width: 3;
  stroke-linejoin: round;
}

.gw-mh-over {
  fill: none;
  stroke: #fff;
  stroke-width: 1.25;
  stroke-linejoin: round;
}

.gw-mh-over.is-dash { stroke-dasharray: 5 4; }
.gw-mh-under.is-dash { stroke: rgba(0, 0, 0, 0.35); }
.gw-mh-under.is-box { stroke: rgba(0, 0, 0, 0.3); stroke-width: 2; }
.gw-mh-over.is-box { stroke: rgba(255, 255, 255, 0.7); stroke-width: 1; }
.gw-mh-under.is-ghost { stroke: rgba(0, 0, 0, 0.25); }
.gw-mh-over.is-ghost { stroke: rgba(255, 255, 255, 0.55); stroke-dasharray: 3 4; }

.gw-mh-handle {
  fill: #fff;
  stroke: #161616;
  stroke-width: 1;
}

.gw-mh-vertex {
  fill: #fff;
  stroke: #161616;
  stroke-width: 1;
}

.gw-mh-knob {
  fill: var(--gw-accent);
  stroke: #161616;
  stroke-width: 1;
}

.gw-mh-knob.is-hollow { fill: #161616; stroke: var(--gw-accent); stroke-width: 1.5; }
.gw-mh-dot { fill: #161616; }

/* While a handle is held the cursor is the handle's, wherever the pointer
   goes — over the canvas, whose own cursor the shell sets for panning, or
   into the margin. */
#gw-view.gw-mh-busy,
#gw-view.gw-mh-busy * { cursor: var(--gw-mh-cursor) !important; }

/* The eyedropper. A class rather than an inline cursor, because the shell
   rewrites the canvas's inline cursor for panning on every zoom. */
#gw-canvas.gw-mh-picking { cursor: crosshair !important; }

/* A brush stroke while it is painted, before the panel has it: the brush's
   width, red as the overlay is, lighter while it erases. */
.gw-mh-paint {
  fill: none;
  stroke: rgba(255, 60, 60, 0.45);
  stroke-linecap: round;
  stroke-linejoin: round;
}
.gw-mh-paint.is-erase { stroke: rgba(255, 255, 255, 0.45); }
/* An Object's area painted so far, fainter than the stroke being laid. */
.gw-mh-paint.is-ghost { stroke: rgba(255, 60, 60, 0.2); }
.gw-mh-paint.is-ghost.is-erase { stroke: rgba(255, 255, 255, 0.2); }

/* Its loupe: the colour a click would take, beside the pointer. */
.gw-mh-loupe { stroke: #161616; stroke-width: 3; }
.gw-mh-loupe-ring { fill: none; stroke: #fff; stroke-width: 1.25; }

/* ------------------------------------------------------------------ panel */

#gw-panel {
  flex: 0 0 340px;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
  /* Follows the panel's own grey rather than pinning black here — this rule
     was why the app stayed black after --gw-bg moved. */
  background: var(--gw-bg);
}

/*
 * Native shell only. Infidelity.app draws its content right up into the title
 * bar so the toolbar and the window chrome read as one surface, so the toolbar
 * has to keep clear of the traffic lights on the left.
 */
/*
 * The native shell now keeps a real titlebar above the content, so the toolbar
 * no longer has to dodge the traffic lights — and the window is draggable by
 * its titlebar, which `-webkit-app-region` never delivered under WKWebView.
 */
/* Nothing to dodge any more: the bar it applied to is gone. */

/* --------------------------------------------------------------- zoom bar */

/* Under the image, left-aligned. The stage is a column: view, then viewbar. */
#gw-viewbar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  padding: 6px 10px;
  background: #151515;
  border-top: 1px solid #262626;
  font-size: 11px;
}

/* The app's own row at the top of the sidebar: Open, Export, and the colour
   well the palette eyedropper draws from. */
/* Inside the panel's header slot now, so the wordmark is the first thing at
   the top. No top padding of its own: the header already has some. */
#gw-sidebar-top {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 2px 8px 0;
}
#gw-sidebar-top .gw-btn2 {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
}

/* The caret that says Export asks before it acts. */
.gw-btn2-caret { display: flex; opacity: 0.75; }
.gw-btn2.is-open .gw-btn2-caret { opacity: 1; }

/*
 * The Export format menu.
 *
 * `position: fixed` and positioned from the button's rect, not absolutely
 * inside it: the sidebar scrolls, and an absolutely positioned child would
 * scroll away from the pointer between opening and clicking.
 */
.gw-menu {
  position: fixed;
  z-index: 60;
  min-width: 108px;
  padding: 4px;
  background: #1c1c1c;
  border: 1px solid #4a4a4a;
  border-radius: 6px;
}

.gw-menu-item {
  padding: 5px 9px;
  border-radius: 3px;
  color: var(--gw-text);
  font-family: var(--gw-ui);
  font-size: 12px;
  cursor: pointer;
}

.gw-menu-item:hover { background: var(--gw-accent); color: #16130f; }

/* An entry the current mode cannot honour. Greyed rather than dropped, so the
   menu keeps its shape and the tooltip can say why — the same rule the panel's
   own rows follow. */
.gw-menu-item.is-off { color: var(--gw-text-off); cursor: default; }
.gw-menu-item.is-off:hover { background: none; color: var(--gw-text-off); }

/*
 * Looking-at-the-picture controls, under the image on the right of the zoom.
 * Square, because they are icons and a label would double the width of the
 * strip for three things that are recognisable without one.
 */
/* The shared status bar is moved in here; it brings its own spacing. */
#gw-viewtools {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  margin-left: 12px;
}

/*
 * The filename, where the top bar used to be a whole strip for it.
 *
 * First in the bar and allowed to shrink, so a long path gives way to the
 * zoom and the buttons rather than pushing them off the end — the controls
 * are worth more than the last twenty characters of a name.
 */
#gw-info {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  margin-right: 14px;
  color: var(--gw-text-dim);
  font-family: var(--gw-ui);
  font-size: 11px;
}

.gw-icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  padding: 0;
  color: var(--gw-text-dim);
}

.gw-icon-btn:hover { color: var(--gw-text); }
.gw-icon-btn.is-on { color: var(--gw-accent); border-color: var(--gw-accent-dim); }
.gw-icon-btn.is-off { opacity: 0.35; pointer-events: none; }

#gw-zoomwrap {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 6px;
  color: #9a9a9a;
}

#gw-zoomrange {
  width: 104px;
  height: 14px;
  margin: 0;
  accent-color: #6f9bb5;
}

#gw-zoomval {
  min-width: 46px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: #c4c4c4;
  cursor: ew-resize;
  user-select: none;
}



/* --------------------------------------------- the export menu's JPEG pane */

.gw-menu-head {
  padding: 4px 9px 6px;
  color: var(--gw-text-dim);
  font-family: var(--gw-ui);
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.gw-menu-slider {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 9px 6px;
}

.gw-menu-slider input[type="range"] { flex: 1 1 auto; width: 150px; }

.gw-menu-readout {
  flex: 0 0 24px;
  text-align: right;
  color: var(--gw-text);
  font-family: var(--gw-num);
  font-size: 11px;
}

/* The warning that used to be this row's tooltip. A tooltip on a menu item is
   not reachable — you are already holding the pointer still to read the
   menu — so it is body text here. */
.gw-menu-note {
  max-width: 230px;
  padding: 0 9px 7px;
  color: var(--gw-text-off);
  font-family: var(--gw-ui);
  font-size: 10px;
  line-height: 1.4;
}

.gw-menu-item.is-go {
  margin-top: 2px;
  background: var(--gw-accent);
  color: #16130f;
  text-align: center;
  font-size: 11px;
}
.gw-menu-item.is-go:hover { background: #ef9139; }

/* ================================================================== splash */

/*
 * The mark assembling itself, then glowing.
 *
 * Every dot is drawn by the panel's own `spherePaths` — the same function that
 * draws the wordmark and the same geometry as the Dock icon — so the splash is
 * the product's picture rather than a spinner borrowed from somewhere. The
 * animation is entirely CSS: boot is synchronous JavaScript for most of its
 * length, so anything driven from a timer or a frame callback would sit frozen
 * for exactly the moment it is needed. A transform and an opacity are the
 * compositor's job and keep moving while the main thread is busy — the same
 * reasoning as the render progress bar.
 */
#gw-splash {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  background: var(--gw-bg);
  /* Not `pointer-events: none`: while it is up, a click on the drop zone
     underneath would be a click on something that is not ready. */
}

#gw-splash.is-done {
  opacity: 0;
  pointer-events: none;
  /* Long enough to read as a dissolve rather than a cut, short enough not to
     delay someone who is already reaching for Open image. */
  transition: opacity 260ms ease-out;
}

#gw-splash-mark {
  display: flex;
  width: 132px;
  height: 132px;
}

/*
 * Two animations on one element would fight over `opacity`, so the assembly is
 * a scale-and-fade on the dots and the glow is a drop-shadow that starts after
 * it. `steps()` on the assembly is deliberate: a dither arrives in passes, and
 * a smooth ramp looked like a photograph fading up rather than a screen being
 * laid down.
 */
#gw-splash-mark svg {
  animation: gw-splash-in 520ms steps(6, end) both,
             gw-splash-glow 1400ms 520ms ease-in-out infinite;
}

@keyframes gw-splash-in {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes gw-splash-glow {
  0%, 100% { filter: drop-shadow(0 0 0 rgba(226, 132, 47, 0)); }
  50%      { filter: drop-shadow(0 0 14px rgba(226, 132, 47, 0.55)); }
}

#gw-splash-word {
  /* A row, so the star sits high against the last letter as the header's
     does (`.gw-wordmark-gleam`). */
  display: flex;
  align-items: flex-start;
  color: var(--gw-text);
  font-family: var(--gw-ui);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 0.01em;
  opacity: 0;
  animation: gw-splash-word 420ms 300ms ease-out both;
}

/*
 * The word's star (§84). It turns in as the sphere's last pass lands (the
 * sphere assembles over 520 ms), then glints — a swell, a quarter turn and
 * the accent's glow — timed to peak inside the 1,400 ms the splash is held,
 * and again every 1.2 s while boot is still running. A transform and a
 * filter, the compositor's work, for the reason the rest of the splash is
 * CSS: boot has the main thread.
 */
.gw-splash-gleam {
  display: flex;
  width: 14px;
  height: 14px;
  /* Tight against the "e" and level with its top, as in the header; the
     glyph's own box has a cell of air on each side. */
  margin-top: -1px;
  margin-left: -3px;
  animation: gw-splash-gleam-in 400ms 520ms cubic-bezier(0.2, 1.4, 0.4, 1) both,
             gw-splash-glint 1200ms 920ms ease-in-out infinite;
}

@keyframes gw-splash-gleam-in {
  from { opacity: 0; transform: scale(0.2) rotate(-120deg); }
  to   { opacity: 1; transform: none; }
}

@keyframes gw-splash-glint {
  0%, 60%, 100% { transform: none; filter: drop-shadow(0 0 0 rgba(226, 132, 47, 0)); }
  30%           { transform: scale(1.25) rotate(45deg); filter: drop-shadow(0 0 5px rgba(226, 132, 47, 0.9)); }
}

@keyframes gw-splash-word {
  from { opacity: 0; transform: translateY(3px); }
  to   { opacity: 0.85; transform: none; }
}
