body {
  font-family: system-ui, sans-serif;
  margin: 1rem;
  color: #222;
}

h1 {
  font-size: 1.4rem;
  font-weight: 600;
}

/* Controls on the left, fabric on the right, so entering a value and seeing
   its effect never involves scrolling. Named areas rather than DOM order,
   so the fabric can come first on a narrow screen. */
.page {
  display: grid;
  grid-template-areas: "controls split fabric";
  /* A definite width, not max-content. max-content means "as wide as the
     content wants without wrapping", so one long sentence in any panel would
     stretch the whole column — and the column would jump about as panels
     appear and disappear with the mode. The splitter overrides the variable. */
  grid-template-columns: var(--controls-width, 28rem) auto minmax(0, 1fr);
  gap: 1rem;
  align-items: start;
}

/* Viewing control, not a sizing one: the fabric's pixel size comes from the
   gauge and stitch count, so this changes how much of it you can see, never
   how big it is. */
.splitter {
  grid-area: split;
  align-self: stretch;
  width: 9px;
  min-height: 6rem;
  border-radius: 5px;
  background: #e3e3e3;
  cursor: col-resize;
  touch-action: none;
}

.splitter:hover,
.splitter.dragging {
  background: #b0b0b0;
}

.fabricSide {
  grid-area: fabric;
  /* The controls column is taller than the fabric, so pin the fabric in
     view while scrolling through them. */
  position: sticky;
  top: 1rem;
}

/* The canvas and its two dimension controls.
   Stitches sits above the canvas because it governs the horizontal
   dimension; rows sits to the right because it governs the vertical. */
.layout {
  display: grid;
  grid-template-areas:
    "top     ."
    "canvas  right";
  grid-template-columns: auto auto;
  gap: 0.75rem;
  justify-content: start;
  align-items: start;
  margin-bottom: 1.25rem;
}

/* Each control is centred on the edge it governs: stitches across the top,
   rows down the right. Both re-centre automatically as the canvas resizes,
   because the grid column and row are sized by the canvas itself. */
.top {
  grid-area: top;
  justify-self: center;
}

.right {
  grid-area: right;
  align-self: center;
}

.dimension label {
  font-size: 0.85rem;
  color: #555;
  margin-right: 0.4rem;
}

/* The area is only as big as the canvas box; the handles hang off its
   edges, so they follow the canvas without any positioning maths. */
.canvasArea {
  grid-area: canvas;
  position: relative;
  width: fit-content;
}

#canvasWrap {
  overflow: hidden;
  border: 1px solid #bbb;
  min-width: 24px;
  min-height: 24px;
}

/* Three grips: right edge changes stitches, bottom edge changes rows,
   corner changes both. touch-action:none stops a touch drag from
   scrolling the page instead of resizing. */
.handle {
  position: absolute;
  background: #c4c4c4;
  border-radius: 2px;
  touch-action: none;
}

.handle:hover {
  background: #8a8a8a;
}

.handleRight {
  top: 0;
  right: -10px;
  width: 8px;
  height: 100%;
  cursor: ew-resize;
}

.handleBottom {
  left: 0;
  bottom: -10px;
  height: 8px;
  width: 100%;
  cursor: ns-resize;
}

/* "Seam" captions, one per edge of the unrolled tube. They sit outside the
   fabric, on the far side of the dashed line from it. Hidden unless the
   fabric is circular — the margin is added then too, so flat knitting does
   not carry empty space either side. */
.seamLabel {
  position: absolute;
  top: 50%;
  display: none;
  writing-mode: vertical-rl;
  transform: translateY(-50%) rotate(180deg);
  padding: 4px 6px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.72);
  font-size: 0.8rem;
  color: #333;
  white-space: nowrap;
}

.canvasArea.hasSeam {
  margin: 0 3.25rem;
}

.canvasArea.hasSeam .seamLabel {
  display: block;
}

.seamLabelLeft {
  right: 100%;
  margin-right: 0.6rem;
}

/* Further out on this side, to clear the resize grip. */
.seamLabelRight {
  left: 100%;
  margin-left: 1.4rem;
}

.handleCorner {
  right: -10px;
  bottom: -10px;
  width: 8px;
  height: 8px;
  background: #8a8a8a;
  cursor: nwse-resize;
}

#fabric {
  display: block;
  width: 100%;
  height: 100%;
}

#readout,
#yarnReadout {
  margin: 0;
  font-size: 0.85rem;
  color: #555;
}

#yarnReadout {
  margin-top: 0.2rem;
}

/* A colour in the middle of a sentence, because the instruction gets followed
   while looking at yarn rather than at a colour picker. */
.inlineSwatch {
  display: inline-block;
  width: 1.6rem;
  height: 0.85rem;
  border: 1px solid #999;
  border-radius: 2px;
  vertical-align: -0.1rem;
}

/* The fabric is clickable now: that is where a join gets reported. */
#fabric {
  cursor: crosshair;
}

.panel {
  grid-area: controls;
  display: grid;
  gap: 1.3rem;
  align-content: start;
  /* Grid items refuse to shrink below their content by default; this lets the
     panels actually honour the column width instead of overflowing it. */
  min-width: 0;
}

.panel input,
.panel select {
  min-width: 0;
}

/* The panel's width is set by the splitter, not the window, so a media query
   cannot see it change — the viewport never moved. A container query responds
   to the element's own size, which is the thing that actually varies here. */
.panel {
  container-type: inline-size;
}

@container (max-width: 27rem) {
  /* Rather than squeezing every column until the track is useless, give the
     track a line of its own. */
  body.fades .colorRow,
  body.fades .colorHeader {
    grid-template-columns: 1.9rem 1.8rem 1fr;
  }

  body.fades .colorRow > .fadeTrack {
    grid-column: 1 / -1;
    grid-row: 2;
    /* Room for the pin and the label, which now sit inside the row rather than
       in the gap above it. */
    margin-top: 3.2rem;
  }

  body.fades .colorRow {
    margin-bottom: 1.1rem;
  }

  body.fades #colorRows {
    margin-top: 0;
  }

  /* The heading belongs to a column that no longer exists on this line. */
  body.fades .colorHeader > .fadeCell {
    display: none;
  }

  /* Four max-content columns of label and box do not fit; pair them down. */
  .fieldsSwatch {
    grid-template-columns: max-content minmax(0, 1fr);
  }

  .typeRow,
  .typeHeader {
    grid-template-columns: minmax(0, 1fr) 2.6rem 4rem max-content;
  }
}

fieldset {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 0.9rem 1rem 1rem;
  /* Browsers give fieldset min-inline-size: min-content in their own
     stylesheet, so it refuses to shrink into its grid column and spills
     sideways over the fabric. This is the standard override. */
  min-inline-size: 0;
  min-width: 0;
}

.panel fieldset > *,
.panel .panelSection > * {
  max-width: 100%;
}

/* --- Sections that open ------------------------------------------------------
   A closed section keeps its values but does not apply them, so opening one is
   how you say "this part of the fabric is worth my attention". A top-level
   section is dressed as a fieldset so a panel converted a piece at a time does
   not look half broken; a sub-section sits inside one. */

.panelSection {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 0.9rem 1rem 1rem;
  min-inline-size: 0;
  min-width: 0;
}

.panelSection > summary,
.subSection > summary {
  cursor: pointer;
  font-size: 0.9rem;
  color: #345;
}

.panelSection > summary {
  /* Reads as the legend it replaces. */
  font-size: 0.85rem;
  color: #555;
}

.panelSection[open] > summary,
.subSection[open] > summary {
  margin-bottom: 0.7rem;
}

.subSection {
  margin-top: 0.7rem;
  border-top: 1px solid #eee;
  padding-top: 0.6rem;
}

/* An open section is doing something to the fabric, and that is worth seeing
   at a glance when several are open at once. */
.subSection[open] > summary,
.panelSection[open] > summary {
  font-weight: 600;
}

/* Grid and flex children refuse to shrink below their content unless told
   otherwise, which is how a row ends up wider than the panel holding it. */
.colorRow > *,
.typeRow > *,
.fields > * {
  min-width: 0;
}

legend {
  font-size: 0.85rem;
  color: #555;
  padding: 0 0.3rem;
}

label {
  font-size: 0.9rem;
}

input[type="number"] {
  width: 6rem;
  padding: 0.2rem 0.3rem;
}

input[type="color"] {
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 1px solid #bbb;
}

.swatch {
  margin-top: 0.7rem;
  border-top: 1px solid #eee;
  padding-top: 0.6rem;
}

.swatch summary {
  cursor: pointer;
  font-size: 0.9rem;
  color: #345;
}

/* Label/input pairs in two columns, so nothing wraps mid-pair.
   max-content sizes each column to its widest item, which keeps every
   input left-aligned with the others. */
.fields {
  display: grid;
  grid-template-columns: max-content max-content;
  gap: 0.6rem 0.9rem;
  align-items: center;
  margin-bottom: 0.9rem;
}

/* Four columns, so each measurement sits beside the count it goes with:
   width with stitches, height with rows. That pairing is the calculation. */
.fieldsSwatch {
  grid-template-columns: repeat(4, max-content);
}

.hint {
  font-size: 0.82rem;
  color: #666;
  margin: 0.4rem 0;
}

.choice {
  display: block;
  margin: 0.35rem 0;
}

/* One figure for the whole fabric, replaced by the table when that is opened.
   Not merely supplemented: two controls claiming the same job, with only one
   of them listened to, is the thing inEffect() exists to prevent. */
/* One stitch to a row means one figure; a pattern means the table. Both follow
   from the row choice, so neither is a decision of its own. */
body.patterned .oneStitchOnly {
  display: none;
}

.patternOnly {
  display: none;
}

body.patterned .patternOnly {
  display: block;
}

/* A taste of the pattern editor while it is unchosen, gone once it is. */
body.patterned .patternTaste {
  display: none;
}

/* The name box for a new stitch, which only means anything while "Custom" is
   the selection. */
.customOnly {
  display: none;
}

body.customStitch .customOnly {
  display: inline-block;
}

/* Radio, label and selector on one line — the sentence reads across. */
.rowChoice {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.rowChoice label {
  margin: 0;
}

.rowChoice input[type="text"] {
  width: 7rem;
}

/* Nothing turns in the round, so the turn figure is not merely inactive there
   — it has nothing to describe. */
body.circular .flatOnly {
  display: none;
}

/* Bare-minimum calibration was never asked to hit a precision, so the box
   that states one — and the column reporting against it — have nothing to
   show. */
body.calMinimal .targetOnly {
  display: none;
}

.typeRow,
.typeHeader {
  display: grid;
  grid-template-columns: 7rem 3rem 5.5rem max-content;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.35rem;
}

/* Strip, then the repeat marks and remove together on the right. The strip
   takes whatever is left, which is why the splitter matters. */
/* Controls, then the scrolling strip, then the row's total. flex-end lines the
   controls and the total up with the cells rather than with the counter strip
   sitting above them. */
.templateRow {
  display: flex;
  align-items: flex-end;
  gap: 0.6rem;
  margin-bottom: 0.5rem;
  padding-bottom: 0.45rem;
  border-bottom: 1px dashed #e8e8e8;
}

/* Outside the scroller, so it stays visible however far the strip is
   scrolled. */
.rowTotal {
  flex: 0 0 auto;
  min-width: 2.6rem;
  padding-bottom: 0.3rem;
  text-align: right;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  color: #445;
}

.rowTotal.bad {
  color: #b8322b;
  font-weight: 700;
}

.templateRow:last-child {
  border-bottom: none;
}

/* One group, so remove sits beside the repeat marks rather than being flung
   to the far edge on its own. */
.templateControls {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Counter strip and cells scroll together so they never drift apart. */
.templateScroll {
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  padding-bottom: 0.2rem;
}

.countStrip,
.cellStrip {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.3rem;
  width: max-content;
}

.countStrip {
  margin-bottom: 0.15rem;
}

/* Right-aligned, because the figure is the total *after* this cell — so it
   belongs against the cell's trailing edge, where the count is reached. */
.countCell {
  width: 3.2rem;
  flex: 0 0 3.2rem;
  text-align: right;
  padding-right: 0.35rem;
  font-size: 0.72rem;
  color: #567;
  font-variant-numeric: tabular-nums;
}

/* Inside a group the figure counts the group, not the fabric — dimmed so it
   is not mistaken for a running total. */
.countCell.inGroup {
  color: #aab;
  font-style: italic;
}

.templateCell {
  width: 3.2rem;
  flex: 0 0 3.2rem;
  padding: 0.2rem 0.3rem;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  text-align: center;
}

/* Marks where the turn falls. Only meaningful once turning is switched on. */
.turnChip {
  display: none;
  width: 3.2rem;
  flex: 0 0 3.2rem;
  padding: 0.2rem 0.3rem;
  border: 1px dashed #c9b7c6;
  border-radius: 3px;
  background: #f7f1f6;
  color: #8a6d85;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.85rem;
  text-align: center;
}

body.turning .turnChip {
  display: block;
}

.repeatMarks {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

.repeatMark {
  font-size: 0.75rem;
  color: #666;
}

/* "end" with no repeat open above it. */
.repeatMark.disabled {
  color: #bbb;
  cursor: not-allowed;
}

.repeatMark input {
  margin-right: 0.15rem;
}

.repeatCountLabel {
  font-size: 0.8rem;
  color: #666;
}

.repeatCount {
  width: 3.2rem;
  margin-left: 0.2rem;
}

/* A template fixes the fabric's size in both directions, so none of the
   grips has anything left to drag. */
body.templated .handle {
  display: none;
}

.templateCell:focus {
  outline: 2px solid #4a7;
  outline-offset: 1px;
}

input[readonly] {
  background: #f2f2f2;
  color: #555;
}

.typeHeader {
  font-size: 0.8rem;
  color: #666;
}

.typeRow input {
  width: 100%;
}

.typeChoice {
  margin-top: 0.8rem;
}

/* The calibration list mirrors the stitch type table above it, so it takes the
   same first column width — the two read as one list seen twice. */
.calTypeRow,
.calHeader {
  display: grid;
  grid-template-columns: 7rem 4rem max-content;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.35rem;
}

.calHeader {
  font-size: 0.8rem;
  color: #666;
}

.calTypeName {
  font-size: 0.9rem;
}

/* Block, so the whole column width is clickable rather than just the box. */
.calCheck {
  display: block;
}

/* Matches .repeatMark: a control that means nothing right now looks like it. */
.calCheck.disabled {
  opacity: 0.45;
}

.calLabel {
  font-size: 0.9rem;
  margin: 0.9rem 0 0.2rem;
}

/* Numbered, because this is a list of things to make and the numbers are what
   gets ticked off against the pile of finished swatches. */
.swatchList {
  margin: 0.8rem 0 0.4rem;
  padding-left: 1.4rem;
  font-size: 0.9rem;
}

.swatchList li {
  margin-bottom: 0.3rem;
}

.swatchCount {
  font-weight: bold;
}

/* Expected against wanted, one unknown per line. A column of figures is what
   makes the one falling short obvious. */
.precisionTable {
  display: grid;
  grid-template-columns: max-content max-content max-content;
  gap: 0.25rem 0.9rem;
  font-size: 0.82rem;
  color: #666;
  margin: 0.6rem 0;
}

.precisionTable .short {
  color: #b3261e;
}

/* --- Problems ---------------------------------------------------------------
   A control that stops the fabric being drawn, and the note that says why.
   The mark says where; the badge over the canvas says that something is
   wrong, which is the half you need when the panel is scrolled elsewhere. */

.problem {
  outline: 2px solid #b3261e;
  outline-offset: 1px;
  background: #fdf2f1;
}

/* Spans whatever grid it lands in, so it reads as a line under the row rather
   than being squeezed into one column of it. */
.problemNote {
  grid-column: 1 / -1;
  margin: 0.2rem 0 0;
  font-size: 0.8rem;
  color: #b3261e;
}

.staleBadge {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  margin: 0;
  padding: 0.3rem 0.6rem;
  font-size: 0.82rem;
  background: #b3261e;
  color: #fff;
  border-radius: 3px;
  /* Never in the way of a click on the fabric underneath. */
  pointer-events: none;
  display: none;
}

/* The last fabric that made sense stays visible, because it is what you were
   comparing against when you started typing — but dimmed, so it cannot be
   mistaken for a picture of the settings now in the boxes. */
body.invalid .staleBadge {
  display: block;
}

body.invalid #fabric {
  opacity: 0.3;
}

/* The size and yarn lines describe the fabric above them, so they go stale
   with it. Leaving them bright would be the same lie in words. */
body.invalid #readout,
body.invalid #yarnReadout {
  opacity: 0.3;
}

/* The measurement panel is not a mode — it is a place to write answers down,
   so it exists only once there are swatches to write them against. */
.measurePanel {
  display: none;
}

body.prescribed .measurePanel {
  display: block;
}

/* The length/weight table only means anything if you are weighing. */
.weightBlock {
  display: none;
}

body.weighing .weightBlock {
  display: block;
}

.weightRow,
.weightHeader {
  display: grid;
  grid-template-columns: 6rem 6rem max-content;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.35rem;
}

.weightHeader {
  font-size: 0.8rem;
  color: #666;
}

.weightRow input {
  width: 100%;
}

.weightRow button {
  padding: 0.1rem 0.4rem;
  font-size: 0.75rem;
}

/* Description above, entry below: in a column this narrow a swatch takes a
   whole line to describe, and putting the box beside it would leave the
   numbers in a ragged column nobody can scan. */
.measureRow {
  margin-bottom: 0.55rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px dashed #e8e8e8;
}

.measureRow:last-child {
  border-bottom: none;
}

.measureWhat {
  font-size: 0.85rem;
  margin-bottom: 0.25rem;
}

.measureEntry {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.measureEntry input {
  width: 5.5rem;
}

.unitTag {
  font-size: 0.82rem;
  color: #666;
}

/* What the solver will be handed, as opposed to what was typed. Quiet, because
   it is a check rather than an instruction. */
.measureNet {
  font-size: 0.8rem;
  color: #777;
}

.measureNet.bad {
  color: #b3261e;
}

/* The answer. Values in a column so they can be read against each other, and
   against the error bars beside them. */
.solutionTable {
  display: grid;
  grid-template-columns: max-content max-content max-content;
  gap: 0.25rem 0.9rem;
  font-size: 0.85rem;
  color: #555;
  margin: 0.8rem 0 0.5rem;
}

.solutionValue {
  font-weight: 600;
  color: #222;
}

.solutionTable .short,
.hint.bad {
  color: #b3261e;
}

/* Applying is the deliberate act, so the button is only there when there is
   something safe to apply. */
#applyCalibration {
  display: none;
}

body.solved #applyCalibration {
  display: inline-block;
}

/* Header and rows share one column template so the headings line up with
   the boxes they name. */
/* The length column is sized by its heading, not by the numbers — band lengths
   are one or two digits, so a wide box is just empty space taken from the
   track, which is the part that benefits from room. */
.colorRow,
.colorHeader {
  display: grid;
  grid-template-columns: 1.9rem 1.8rem max-content;
  gap: 0.9rem;
  align-items: center;
  margin-bottom: 0.55rem;
}

/* Band lengths are one or two digits, so the box is sized for that and scrolls
   if you type more. Spinners are dropped with it: they cost a third of the
   width and nobody nudges a tape measurement by 0.1 at a time. */
.colorRow input[type="number"] {
  height: 1.6rem;
  padding: 0.1rem 0.25rem;
}

.colorRow input[type="number"]::-webkit-outer-spin-button,
.colorRow input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.colorRow input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}

/* Row buttons carry a single short word and do not need the default size. */
.colorRow button,
.typeRow button {
  padding: 0.1rem 0.4rem;
  font-size: 0.75rem;
}

/* Each fade row's floating label lives in the gap above it, so the gap has to
   be tall enough to hold one — and the list needs the same clearance at the
   top, where there is no previous row to borrow from. */
/* The gap above each row has to hold the pin and the label stacked on top of
   it — 22px of pin plus a 1.4rem box — so it is sized by them, not by taste. */
body.fades .colorRow {
  margin-bottom: 3.3rem;
}

body.fades #colorRows {
  margin-top: 3.2rem;
}

/* Fades add two columns rather than hiding cells inside the existing grid —
   a display:none grid item leaves no gap, but the template still has to
   change or the remaining cells land in the wrong columns. */
body.fades .colorRow,
body.fades .colorHeader {
  grid-template-columns: 1.9rem 1.8rem minmax(9rem, 1fr) max-content;
}

/* The colour stretched into a track, with the floating length box above it. */
/* No reserved space here on purpose. Padding to make room for the label would
   make this cell taller than its neighbours, and a centred row would then push
   the track below the boxes beside it. The label floats clear of the layout
   instead, and the row spacing below leaves it somewhere to go. */
.fadeTrack {
  position: relative;
  display: flex;
  align-items: center;
}

/* appearance:none removes the browser's own rendering — including the filled
   portion left of the thumb, which Chrome paints in accent-color and Firefox
   via ::-moz-range-progress. That fill says "a level from zero", which is the
   wrong story: this is a position along the band. */
.fadeSlider {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 1.05rem;
  margin: 0;
  border: 1px solid #b4b4b4;
  border-radius: 3px;
  cursor: ew-resize;
  /* The gradient itself is set per row in paintFadeTrack(). */
  background: #ddd;
}

.fadeSlider::-webkit-slider-runnable-track {
  height: 1.05rem;
  background: transparent;
}

.fadeSlider::-moz-range-track {
  height: 1.05rem;
  background: transparent;
}

.fadeSlider::-moz-range-progress {
  background: transparent;
}

/* A grabbable head with a hairline down its middle: wide enough to catch with
   a mouse or thumb, while the line still says exactly where the fade begins.
   Width must stay in step with FADE_THUMB_PX in controls.js, which uses it to
   place the floating label. */
.fadeSlider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  /* Track height plus a head that sits above it. The negative margin drops the
     thumb so its point lands on the track's bottom edge, leaving the head
     clear of the colour instead of covering it. */
  /* Just the hairline: same height as the track, so there is no positioning
     to get wrong. The pin above is a separate element. */
  height: 1.05rem;
  border: none;
  background:
    linear-gradient(#2a2a2a, #2a2a2a) center / 2px 100% no-repeat,
    linear-gradient(#ffffff, #ffffff) center / 4px 100% no-repeat;
  cursor: grab;
}

.fadeSlider::-webkit-slider-thumb:active {
  cursor: grabbing;
}

.fadeSlider::-moz-range-thumb {
  width: 14px;
  height: 1.05rem;
  border: none;
  background:
    linear-gradient(#2a2a2a, #2a2a2a) center / 2px 100% no-repeat,
    linear-gradient(#ffffff, #ffffff) center / 4px 100% no-repeat;
  cursor: grab;
}

.fadeSlider::-moz-range-thumb:active {
  cursor: grabbing;
}

/* focus-visible, not focus: a mouse drag no longer draws a ring across the
   track and through the pin, while a keyboard user still gets one. */
.fadeSlider:focus {
  outline: none;
}

.fadeSlider:focus-visible {
  outline: 2px solid #4a7;
  outline-offset: 2px;
}

/* Sits wholly above the track, at the exact position of the hairline. Solid
   fill, no outline and no shadow — it reads against the panel behind it, and
   the hairline below already carries the contrast over dark yarn. */
.fadePin {
  position: absolute;
  left: 0;
  bottom: calc(100% + 1px);
  width: 14px;
  height: 20px;
  transform: translateX(-50%);
  /* Grabbable in its own right — see the pointerdown handler in controls.js.
     The track underneath keeps its native dragging as well. */
  touch-action: none;
  cursor: grab;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 20'%3E%3Cpath d='M2 0h10a2 2 0 0 1 2 2v9l-7 9-7-9V2a2 2 0 0 1 2-2z' fill='%233b7dd8'/%3E%3C/svg%3E")
    center / 14px 20px no-repeat;
}

/* The attribute selector in ".colorRow input[type=number] { width: 100% }"
   outranks a plain class, so this has to name the type as well or the label
   stretches to the whole track and stops looking like a marker. */
/* Clear of the pin, which stands 20px above the track. */
.colorRow input[type="number"].fadeLabel {
  position: absolute;
  bottom: calc(100% + 1.6rem);
  left: 0;
  width: 3.4rem;
  height: 1.4rem;
  transform: translateX(-50%);
  padding: 0.05rem 0.15rem;
  font-size: 0.72rem;
  text-align: center;
}

.fadeCell,
.fadeBlock {
  display: none;
}

body.fades .fadeCell {
  display: block;
}

body.fades .fadeBlock {
  display: block;
}

.fadeSlider {
  width: 100%;
  min-width: 0;
}

.colorHeader {
  font-size: 0.78rem;
  color: #666;
}

/* Columns are sized to the controls, not the headings, so the gaps between
   swatch, length, track and button all come out the same. The headings are
   wider than that and are allowed to run into the gap rather than widen the
   column and reintroduce the uneven spacing. */
.colorHeader > span {
  white-space: nowrap;
  overflow: visible;
}

/* A value and its unit are one control, so they share one grid cell. */
.withUnit {
  display: flex;
  gap: 0.4rem;
  align-items: center;
}

.colorRow input[type="number"] {
  width: 100%;
}

button {
  padding: 0.25rem 0.6rem;
  font: inherit;
}

/* Narrow screens: stack everything. The drag handle is mouse-only anyway,
   so on a phone the number boxes are the whole interface. */
@media (max-width: 900px) {
  /* Too narrow for two columns: stack, fabric first so it is what you see
     when the page opens. Sticky is dropped — it would cover the controls. */
  .page {
    grid-template-areas:
      "fabric"
      "controls";
    grid-template-columns: 1fr;
    gap: 1.25rem;
  }

  .fabricSide {
    position: static;
  }

  /* Nothing to split when the columns are stacked. */
  .splitter {
    display: none;
  }
}

@media (max-width: 640px) {
  .layout {
    grid-template-areas:
      "top"
      "canvas"
      "right";
    grid-template-columns: 1fr;
  }

  /* max-content columns can overrun a narrow screen — stack instead. */
  .fields,
  .fieldsSwatch {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }
}
