/* The design pass: a nautical chart on parchment. Sepia serif type, brass and
   gold on cream, and one fixed backdrop — the owner's own artwork,
   public/background.webp — behind every screen, so the whole app reads as one
   chart rather than six pages. The layout underneath is unchanged: the
   geometry the suite asserts (arm heights, viewport width, where the error
   banner sits) is load-bearing and the theme is paint on top of it. */

/* The two faces of the chart, bundled in public/fonts (SIL OFL, license text
   beside them) rather than fetched from a CDN: Mapwork works with no network,
   so a font that arrives over one would be a theme that vanishes offline.
   IM Fell English is a revival of 17th-century printing type — the letterforms
   actual sea charts were set in. Cinzel is engraved Roman capitals, the
   lettering on a compass card. */
@font-face {
  font-family: 'IM Fell English';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/im-fell-english.woff2') format('woff2');
}

@font-face {
  font-family: 'IM Fell English';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/im-fell-english-italic.woff2') format('woff2');
}

@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/cinzel-600.woff2') format('woff2');
}


:root {
  /* Body text is the antique print; identity and actions are the engraved
     capitals. Two roles, not a face per component. */
  --hand: 'IM Fell English', 'Iowan Old Style', Palatino, Georgia, serif;
  --engraved: 'Cinzel', 'Iowan Old Style', Palatino, Georgia, serif;
  /* N, E, S and W. The app's own body face, and no separate file: the owner
     picked IM Fell English SC off a twenty-face specimen sheet, and its
     capitals are the same drawing as the capitals in the IM Fell English
     already bundled here. Measured rather than assumed — see .point-dir. */
  --point-face: 'IM Fell English', 'Iowan Old Style', Palatino, Georgia, serif;

  /* The anchor a Rigging row wears when it is waiting on somebody else. A mask
     rather than a colored drawing, so the faint and inked states are one
     `color` apart and this is drawn once. */
  --anchor-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='4.2' r='2.2'/%3E%3Cpath d='M12 6.4V21'/%3E%3Cpath d='M7.5 9.4h9'/%3E%3Cpath d='M4.2 14.1c0 4 3.4 6.9 7.8 6.9s7.8-2.9 7.8-6.9'/%3E%3Cpath d='M4.2 14.1h2.6M19.8 14.1h-2.6'/%3E%3C/svg%3E");

  /* The Cave, on a Compass row: the way back for something that turned out not
     to be actionable after all.

     A solid rock with the mouth knocked out of it, rather than the outlined
     arch this was first drawn as. Four outlined versions were rendered at the
     size they are actually used and every one of them read as a croquet hoop
     or a tent: at 20px an outline has no mass, and mass is the whole of what
     says rock. The opening being paper-colored rather than inked is what
     makes it a cave and not a hill — it is the one part of the drawing that
     is a hole. `fill-rule='evenodd'` is what cuts it.

     Drawn as a mask over `currentColor` for the same reason the anchor above
     is: resting and hovered are one color apart, and the artwork exists
     once. */
  --cave-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M1.4 20.4h21.2' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='M2.2 20.4C2.6 15 5 11.2 8 9.4c.9-.5 1.5-1.1 2.2-1.9.9-1 2.4-.9 3.2.2.5.7 1.1 1.3 1.9 1.8 3.2 2 5.2 5.6 5.5 10.9' fill='none' stroke='%23000' stroke-width='2' stroke-linejoin='round' stroke-linecap='round'/%3E%3Cpath d='M6.9 20.4c-.2-3.6.9-6 3.2-6.9 2.6-1 5.1.7 5.7 3.4.2 1.1.3 2.2.1 3.5z' fill='%23000'/%3E%3C/svg%3E");

  /* The page is light parchment, and native controls need telling. The select,
     the checkboxes, the text fields and the scrollbars all pick their
     appearance from this, so setting it once is the fix and restyling each
     control would be four copies of it. */
  color-scheme: light;

  /* Sampled from the quiet interior of background.webp, so the color behind
     the artwork is the artwork's own: no seam where `cover` does not reach on
     a wide window, and no flash of a different cream while it loads. */
  --paper: #f5eee4;       /* the parchment itself */
  --paper-deep: #eadcbd;  /* its darkened edges */
  --card: #faf4e6;        /* a raised card face */
  --ink: #2f2b20;         /* sepia-black text */
  --muted: #7c6e50;       /* faded ink */
  --rule: #d8caa4;        /* a hairline on parchment */
  --rule-strong: #a8925f; /* a drawn border */
  --brass: #8a6c33;       /* the accent: active states, focus, links */
  --brass-fill: #ecd5a1;  /* the gold fill an active control wears */
  --brass-fill-deep: #ddbf7f;
  --rust: #b1502a;        /* the compass star's copper */
  --warn: #9d3a24;        /* deep rust-red, legible on cream */

  /* Warm, and the same warmth the admin console is painted in. Across this
     suite, warm on the app's own ground means "this is not the thing itself" —
     the console that administers Mapwork, and the install that is not the real
     one. Distinct from --warn so an error and an environment never read as the
     same kind of statement. */
  --test: #e0a34a;

  /* Empty reads as absence, not as a cold rating. The other three warm.
     Reported from the owner's phone in night mode: medium and high looked
     almost the same. Measuring the old three said the fault was wider than
     that pair. As #4a7fb5 / #c9962f / #c34f28 they sat at greys 123, 159 and
     116 — medium against high was 1.76:1 and fell to 1.61:1 under a warm
     night filter, and low against high was 1.12:1, which is to say the same
     shade. The whole scale was carried by hue, so anything that flattens hue
     — a night filter, a dim screen, sunlight, a red-green deficiency — took
     the scale with it.

     High comes down to a deep near-ink rust, which puts medium against high
     at 3.53:1 and 3.13:1 under the same filter, and the two ends at 2.74:1.
     Medium is still the lightest of the three and is meant to be: brass is
     gold, and darkening it to force a light-to-dark ramp would cost the
     app's own accent color for an ordering the tick marks below now carry
     without needing color at all. */
  --temp-1: #5b8fbe;
  --temp-2: #c9962f;
  --temp-3: #7e2a19;

  /* The decorations, drawn once each and reused. All ink-on-nothing SVG with
     the opacity baked in, because two background layers cannot be faded
     separately in CSS. */
  --star-rose: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-104 -104 208 208'%3E%3Cg fill='%232f2b20'%3E%3Cpath d='M0 -60 L9 -9 L60 0 L9 9 L0 60 L-9 9 L-60 0 L-9 -9 Z' transform='rotate(45)' fill='%238a6c33'/%3E%3Cpath d='M0 -100 L13 -13 L100 0 L13 13 L0 100 L-13 13 L-100 0 L-13 -13 Z'/%3E%3C/g%3E%3Ccircle r='9' fill='%23f5eedd' stroke='%232f2b20' stroke-width='5'/%3E%3C/svg%3E");
  --star-rust: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-104 -104 208 208'%3E%3Cg fill='%23b1502a'%3E%3Cpath d='M0 -58 L9 -9 L58 0 L9 9 L0 58 L-9 9 L-58 0 L-9 -9 Z' transform='rotate(45)'/%3E%3Cpath d='M0 -100 L13 -13 L100 0 L13 13 L0 100 L-13 13 L-100 0 L-13 -13 Z'/%3E%3C/g%3E%3C/svg%3E");
}

* { box-sizing: border-box; }

/* Every checkbox in the app — the Temperature toggle as much as a row's
   completion box. One rule rather than one per class: accent-color on
   `.row-check` alone was how the toggle stayed browser-blue through a passing
   suite. Round, per the design direction: a compact circular control, drawn
   over the native input so it stays a real checkbox to a screen reader and
   the keyboard. The tick is a background image because a replaced element
   cannot carry a pseudo-element. */
input[type="checkbox"],
input[type="radio"] {
  appearance: none;
  width: 1.375rem;
  height: 1.375rem;
  margin: 0;
  border: 1px solid var(--rule-strong);
  background: var(--card);
  box-shadow: inset 0 1px 2px rgb(60 45 15 / 12%);
  cursor: pointer;
}

/**
 * Square for a checkbox, round for a radio.
 *
 * Both were round for eleven builds and the owner asked for the checkboxes to
 * be square — his call, and an aesthetic one. It happens to be the older
 * convention as well: a square is a thing you turn on and off, a circle is one
 * of a set you pick from, and every platform has drawn them that way for long
 * enough that people read it without being told.
 *
 * So the radios keep their circle rather than following. That distinction was
 * already carrying weight in the capitalization card — see the `:checked`
 * rules below, where a tick means "you turned this on" and a dot means "this
 * is the one" — and squaring both would have taken the shape half of it away
 * and left the marker doing the work alone.
 *
 * 0.25rem rather than a hard corner, which is the radius the row buttons and
 * the sort controls already wear. At 22px it still reads square; a 0 would be
 * the only unrounded corner in the app.
 */
input[type="checkbox"] { border-radius: 0.25rem; }
input[type="radio"] { border-radius: 50%; }

input[type="checkbox"]:checked {
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 10.5 L8.5 14 L15 6.5' fill='none' stroke='%232f2b20' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")
    center / 0.875rem 0.875rem no-repeat,
    linear-gradient(var(--brass-fill), var(--brass-fill-deep));
}

/* A dot rather than the checkbox's tick, and it is the only difference between
   them. The owner asked for checkboxes over radios once already, for the age
   switches, and was right: those are five independent yes-or-nos. Case is one
   choice with three answers, where checkboxes would draw a control that can be
   put into a state — none ticked, or two — that the setting cannot hold. So it
   stays a radio, and the tick is what marks the two apart at a glance: a tick
   is something you turned on, a dot is the one you picked. */
input[type="radio"]:checked {
  background:
    radial-gradient(circle at center, var(--ink) 0 0.1875rem, transparent 0.1875rem),
    linear-gradient(var(--brass-fill), var(--brass-fill-deep));
}

/* The `hidden` attribute loses to any rule that sets `display`, silently and
   from anywhere in the file. It cost one already: #backup-output sets
   `display: block` for its width, which left an empty textarea sitting on the
   page from first load — through a passing suite, because Playwright reads a
   hidden field's value quite happily. Winning globally means the next element
   given `hidden` cannot repeat it. */
[hidden] { display: none !important; }

body {
  margin: 0;
  /* viewport-fit=cover plus these insets keeps content clear of the notch and
     the home indicator once the app is installed to a home screen. The right
     side carries more than the left because the rope hangs there — the text
     column stops at its rail instead of running under it. */
  padding: max(1.5rem, env(safe-area-inset-top)) 1.875rem
           max(2rem, env(safe-area-inset-bottom)) 1.25rem;
  background: var(--paper);
  color: var(--ink);
  /* The antique print throughout the running text. 17.5px because Fell's
     x-height runs small against a modern serif, and inputs must stay at 16px+
     or iOS zooms on focus. */
  font: 17.5px/1.5 var(--hand);
  -webkit-font-smoothing: antialiased;
  /**
   * No double-tap zoom, anywhere in the app.
   *
   * Reported from the phone: cycling a thermometer means tapping the same
   * 14px control repeatedly, and two taps close together are a zoom gesture
   * to the browser. So the page lurched instead of the level changing — the
   * fastest interaction in the product punished for being fast.
   *
   * `manipulation` is the narrow instrument for that. It removes double-tap
   * zoom and nothing else: panning and PINCH zoom both still work, so the
   * page can still be magnified by anybody who needs to. The blunt instrument
   * — `user-scalable=no` on the viewport — would take that away too, and a
   * person who cannot read 17.5px is not a person to take zoom away from.
   *
   * On body rather than on the controls: the value is intersected down the
   * hit-test ancestry, so one declaration here covers every tappable thing on
   * every screen, including whatever gets added next. The rows already carry
   * their own tap targets, and a control added without this line would have
   * quietly gone back to lurching.
   */
  touch-action: manipulation;
}

/* The chart under everything: public/background.webp, the artwork the owner
   supplied — parchment, the rope down the right edge, a compass rose low on
   the left, an anchor and the chart's construction lines. It replaces the
   layers this rule used to draw by hand (a tiled rope, an SVG rose, a curved
   graticule and a vignette), which were a stand-in for exactly this.

   One fixed layer rather than a background on body, because
   background-attachment: fixed is ignored by iOS Safari and this app lives on
   iPhones. Fixed and full-viewport is also what makes every screen share the
   same backdrop — the rope and the rose sit still while the content scrolls
   over them, like ink under tracing paper.

   `cover` and the artwork's own 853x1844 are a phone's proportions almost
   exactly, so on the device this is built for it lands whole. A wide window
   crops the top and bottom rather than stretching it; the parchment either
   side is --paper, which is sampled from the artwork so the join is invisible
   while it loads and wherever it does not reach. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: url('background.webp') center / cover no-repeat;
  /* Held at 60% against --paper behind it. At full strength the artwork was
     distracting on the busier screens — the owner's read, and the screens
     where it showed are the ones with the most to read: Settings, and a
     Compass with a full list scrolling over the anchor. Fading the layer
     rather than the image keeps one file for every screen.

     It was 70% for eleven builds and the owner asked for another 15% off it,
     which is 0.595 exactly; 0.6 is that rounded, and a hundredth of opacity is
     below anything an eye can separate. Written down because the next person
     to see 0.6 should know it is a measurement rather than a round number
     somebody liked. */
  opacity: 0.6;
}

/* The TEST bar. Sticky rather than in the flow, because standard 6 asks for a
   label that is still obvious while the page is being used — a header banner
   scrolls away on the second screenful and the app is a long scroll.

   The negative margins cancel body's own padding so it reaches all three edges,
   and the safe-area inset is added back as padding so an installed PWA does not
   put the text under the notch. z-index because the compass and the sticky bits
   of the list would otherwise pass over it. */
/* One sticky wrapper for both bars, rather than each bar sticky on its own —
   see the comment above .status-bars for why. */
.status-bars {
  position: sticky;
  top: 0;
  z-index: 20;
  margin: calc(-1 * max(1.5rem, env(safe-area-inset-top))) -1.875rem 1.5rem -1.25rem;
  /* The safe-area padding lives here rather than on each bar, so whichever
     bar ends up first — the TEST bar, the offline banner, or neither — clears
     the notch exactly once instead of never or twice. */
  padding-top: env(safe-area-inset-top);
}
/* :has() rather than a JS-toggled class on the wrapper: the wrapper's own
   negative margin and bottom gap must vanish completely when neither bar is
   showing — the common case, production and online — or every screen gets a
   1.5rem gap that used to belong to a bar that is not there. Precedent
   already in this file, on the gate's body:has() selector below. */
.status-bars:not(:has(> :not([hidden]))) {
  display: none;
}

.environment {
  padding: 0.4rem 1.25rem;
  background: var(--test);
  border-bottom: 1px solid var(--rule-strong);
  color: #241706;
  font-family: var(--engraved);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-align: center;
  text-transform: uppercase;
}

/* The network-off banner. dev-standards PR #43 (unmerged as of this writing):
   "a persistent, unmistakable banner ... naming the state and what it means
   ... disappears once the network returns." Shares .status-bars with the
   TEST bar rather than inventing its own sticky/bleed rules, because the two
   are the same kind of statement: something true about this install that the
   screen underneath cannot be trusted to say on its own, and either, both, or
   neither can be showing at once.

   Its own color rather than --test's amber, so a test install that is also
   offline reads as two distinct facts rather than one bar restating itself.
   --muted stays inside the sepia palette instead of importing a foreign hue
   for a single banner. */
.offline-banner {
  padding: 0.4rem 1.25rem;
  background: var(--muted);
  border-bottom: 1px solid var(--rule-strong);
  color: var(--paper);
  font-family: var(--engraved);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-align: center;
}

main { max-width: 34rem; margin: 0 auto; }

/* 0.75rem, halved. The owner marked this gap and the one under the compass on
   a screenshot: 44px between them, holding nothing. Reclaimed rather than
   redesigned — the blocks themselves are unchanged, they simply sit closer. */
.masthead { margin-bottom: 0.75rem; }

/* Engraved capitals for the name — Cinzel's lowercase are small capitals, so
   "Mapwork" comes out as a compass card's lettering. Sized down from the old
   serif because capitals run wide, and the name has to share 390px with the
   menu button. */
h1 {
  margin: 0;
  font-family: var(--engraved);
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  line-height: 1.2;
}

.tagline {
  margin: 0.125rem 0 0;
  color: var(--muted);
  font-size: 0.9375rem;
  font-style: italic;
}

.error {
  margin: 0 0 1rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--warn);
  border-radius: 0.5rem;
  background: rgb(157 58 36 / 7%);
  color: var(--warn);
  font-size: 0.9375rem;
}

/* ------------------------------------------------------------------ compass */

/* The rose is the artwork and the space around it is the control. The square
   below the heading carries rose.svg as its backdrop, and the four buttons
   are wedges that tile that square corner to corner — north the top wedge,
   south the bottom, east and west the sides — so a tap anywhere in a quadrant
   of the square selects that direction. The letters and labels sit outside
   the ring, leaving the drawing clear, which is the owner's ask. */
/* The second gap the owner marked. Top margin gone entirely, bottom halved. */
.compass { margin: 0 0 0.625rem; }

/*
 * The strip that stands in for the rose once the rose is off screen.
 *
 * Fixed, not sticky. Sticky would need a place in the flow, and at the top of
 * the page that place is directly under a compass showing the same four
 * choices — 44px of duplicate, permanently, out of the space this change
 * exists to reclaim. Fixed costs nothing until it is shown.
 *
 * `--sticky-top` is set from the TEST bar's real height at startup, because
 * that bar is sticky at 0 and exists on every environment except production.
 * A hard-coded offset would either tuck the strip under the bar on test or
 * leave a gap on production.
 */
.quadrant-strip {
  position: fixed;
  /*
   * Below the status bar, not underneath it.
   *
   * `--sticky-top` is the TEST bar's height and is 0 on production, so this
   * read `top: 0` there — and a fixed element at 0 sits at the top of the
   * VIEWPORT, which body's `env(safe-area-inset-top)` padding does not move
   * because padding on the body cannot displace fixed positioning. In a
   * browser tab that is harmless: the toolbar already holds the page below the
   * status bar. Installed to a home screen or in the iOS shell there is no
   * toolbar, the page is edge to edge, and the strip renders underneath the
   * Dynamic Island — drawn, and invisible under what the system paints over
   * it. Reported as the quadrant shortcuts having disappeared.
   *
   * max() rather than adding the two, because on test `--sticky-top` is the
   * TEST bar's full height and that bar already spans the inset itself
   * (`.environment` pads by it). Adding would count the notch twice and leave
   * the strip floating a status bar below the bar it should sit against.
   *
   * Chromium reports 0 for every safe-area inset, so this arithmetic is
   * checked in test/drive.mjs against a stand-in variable and the real
   * behaviour is only observable on a device.
   */
  top: max(var(--sticky-top, 0px), env(safe-area-inset-top, 0px));
  left: 0;
  right: 0;
  z-index: 15;
  display: flex;
  gap: 0.75rem;
  align-items: center;
  justify-content: space-between;
  padding: 0.4375rem 1.25rem;
  border-bottom: 1px solid var(--rule);
  background: var(--card);
  box-shadow: 0 1px 4px rgb(60 45 15 / 12%);
}

/* The quadrant you are in, in its own words. The letters say which point; this
   says what the point means, which is the half a person actually chose. */
.strip-name {
  overflow: hidden;
  font-size: 0.9375rem;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.strip-points { display: flex; flex-shrink: 0; gap: 0.125rem; }

.strip-point {
  width: 2.25rem;
  padding: 0.125rem 0;
  border: 1px solid transparent;
  border-radius: 0.25rem;
  background: none;
  color: var(--muted);
  font-family: var(--point-face);
  font-size: 1.0625rem;
  line-height: 1.2;
  cursor: pointer;
}

/* Where you are, in the same brass the compass uses for a chosen point. */
.strip-point[aria-current="true"] { border-color: var(--brass); color: var(--ink); }


/* The quadrant you are standing in, announced but not drawn. It was a heading
   above the rose until the owner asked for the selected word to stay on its
   own point — "mapping stays on the east even when highlighted" — and once the
   label lives at its point the heading is the same word a second time. So it
   keeps its aria-live job for a screen reader, which otherwise has nothing
   saying the selection moved, and takes no space on the screen. */
.compass-hub {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.compass-rose {
  position: relative;
  /* Wider than tall, and deliberately not a square: the wedges tile any
     rectangle, and a square's height was dead space above and below a rose
     sized by the width the side labels leave it. The rose is sized off the
     height (`auto 56%`) so it stays round whatever the width does. */
  height: 18.75rem;
  /* The square is the tap area and the rose is drawn well inside it, because
     the labels ride at the square's edges and at 94% they sat on the ring —
     the owner's screenshot, three of them circled. The margin the smaller
     percentage buys is what the words stand in.

     The three numbers are one arrangement and move together: the square's
     width, the rose's share of it, and the side labels' column below. What
     has to hold is inked-rose + two label columns + a margin each side <= the
     square, and the square is capped by the content column — 340px on a
     390px phone. So larger words are paid for out of the rose: the owner
     asked for both and said the compass could take the cut. */
  max-width: 21.25rem;
  margin: 0 auto;
  background: url('rose.svg') center / auto 56% no-repeat;
}

/* Each wedge's layout box is half the square — north and south the top and
   bottom halves, east and west the left and right — and the clip-path cuts
   the half into the triangle that meets the center. The triangles tile the
   square with no gaps, and clip-path clips hit-testing as well as paint, so
   they are the real tap areas. The suite measures these boxes: north/south
   full-width half-height, east/west half-width full-height, in symmetric
   pairs. */
.point {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  line-height: 1.3;
  text-align: center;
  cursor: pointer;
}

.point-north {
  inset: 0 0 50% 0;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  justify-content: flex-start;
  padding-top: 0.125rem;
}

.point-south {
  inset: 50% 0 0 0;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
  justify-content: flex-end;
  padding-bottom: 0.125rem;
}

/**
 * East and west lay their two lines out as a grid rather than a flex column,
 * so the letter centers under its own word.
 *
 * Flex aligned each child independently against the wedge's outer edge, which
 * was invisible while the letter and the label were both a fixed 5.5rem —
 * `text-align: center` centered the glyph inside a box the same width as the
 * word above it. Capping the width instead of fixing it (see below, and it is
 * what paid for the larger type) made the two boxes different widths, and the
 * narrow letter went flush to the edge: W under the left of "Coding", E under
 * the right of "Mapping". Reported by the owner as the letters "sitting at the
 * end", which is exactly what they were doing.
 *
 * One implicit column is what fixes it. The column takes the width of its
 * widest item — the word — `justify-items: center` centers the letter inside
 * it, and `justify-content` puts the whole column against the outer edge where
 * flex used to put each item separately. So the word does not move a pixel and
 * the measured clearance is unchanged; only the letter does.
 *
 * `align-content` rather than `justify-content` for the vertical centring: the
 * axes swap meaning between flex-column and grid, and reusing the flex line
 * here would have centered the column horizontally and put the words back on
 * the ring.
 */
.point-west {
  inset: 0 50% 0 0;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  display: grid;
  align-content: center;
  justify-items: center;
  justify-content: start;
}

.point-east {
  inset: 0 0 0 50%;
  clip-path: polygon(100% 0, 100% 100%, 0 50%);
  display: grid;
  align-content: center;
  justify-items: center;
  justify-content: end;
}

/* The side labels are capped rather than fixed, and that is what paid for the
   larger type.
     
   A fixed 5.5rem column made every name as wide as the longest one it had to
   hold, so "Coding" reserved 88px and stood 11px off the ring exactly as
   "Programming" did. Sizing to the word and capping the result gives that back
   for free: measured at 390px with the owner's four names, west clears the
   ring by 40px and east by 27px, where both used to clear 11px — and the cap
   means the worst case is still bounded at 12px whatever anybody types.

   Two words still wrap at the space. A single word past the cap breaks
   mid-word, which is ugly and deliberate: the alternative is a word that
   overflows in both directions and lands back across the ring, and the
   quadrant names are the user's own. At this size that starts at about nine
   characters — raising the cap to fit an eleven-character word was measured
   and puts the label 4px INSIDE the ring, so the break is the cheaper of the
   two. */
.point-west .point-dir, .point-west .compass-label,
.point-east .point-dir, .point-east .compass-label {
  width: auto;
  max-width: 5.5rem;
  text-align: center;
  overflow-wrap: anywhere;
}

/* The selected wedge wears the brass wash, clipped to its triangle — a lit
   quarter of the chart rather than a box over it. Colour is not the only
   signal: the letter grows and the label turns from faded ink to full. */
.point.is-active { background: none; }

.point-north.is-active { background: linear-gradient(to top, rgb(214 180 112 / 40%), rgb(214 180 112 / 0%)); }
.point-south.is-active { background: linear-gradient(to bottom, rgb(214 180 112 / 40%), rgb(214 180 112 / 0%)); }
.point-west.is-active { background: linear-gradient(to left, rgb(214 180 112 / 40%), rgb(214 180 112 / 0%)); }
.point-east.is-active { background: linear-gradient(to right, rgb(214 180 112 / 40%), rgb(214 180 112 / 0%)); }

/**
 * The four quadrant labels, in the app's own body face.
 *
 * The owner picked IM Fell English SC off a sheet of twenty, which is the face
 * every other word in the app is already set in, in its small-capitals cut. No
 * separate file was bundled for it, and that is a measurement rather than a
 * shortcut: an SC cut differs from its roman in the LOWERCASE, which becomes
 * small capitals. The capitals themselves are the same drawing, and N, E, S
 * and W are the only characters this ever draws.
 *
 * Checked three ways before dropping the file. The glyph outlines hash almost
 * identically and the advance widths agree to within 3 units in 2048 — S is
 * exact. Rendered at 200px and again at the 30 and 40 these are actually drawn
 * at, the two differ in 2.2% of pixels, which is antialiasing around identical
 * edges rather than a different letter. Overlaid, nothing shows through.
 *
 * So the compass points now share `im-fell-english.woff2` with the body text,
 * and the app ships one font file fewer than it did before the compass had a
 * face of its own. What goes with it is a whole class of bug: there is no
 * four-glyph subset to fall through, nothing that renders blank on a fifth
 * character, and nothing for the service worker's precache list to drift from.
 *
 * Two numbers still move with the face:
 *
 * `font-weight` is 400 and must match the file, which holds that weight and no
 * other. Ask for a weight the file does not have and the browser synthesizes
 * one by smearing the outline sideways, which on a hand this rough is the
 * texture gone. A test pins the CSS weight against the @font-face.
 *
 * `letter-spacing` is 0. Tracking looks like the obvious thing to add to
 * separated Roman capitals — Cinzel's had 0.04em of it here — and it would do
 * nothing: each letter is alone in its own element, so there is no gap between
 * letters for tracking to open. All it adds is a trailing space that pushes
 * the glyph off the center it is supposed to sit on, measured at 0.6px.
 *
 * `font-size` stays at 1.875rem. It was raised from Cinzel's 1.375rem for a
 * script, whose capitals stand well short of their em box — not true here —
 * but the letters have been that size for six builds and shrinking them is a
 * change nobody asked for.
 *
 * Three faces were tried here before this one and the record is worth keeping,
 * because the first two failed the same way. Dancing Script Bold held it for
 * one build, 0.0.42, against a modern-brush-calligraphy reference; the owner
 * called it cartoonish on the phone. Great Vibes followed. Bodoni Moda came
 * from the owner's National Geographic map-type specimen sheet and lasted two
 * builds. What the sheet was really pointing at was a chart's lettering, and
 * the app already had it.
 */
.point-dir {
  color: var(--ink);
  font-family: var(--point-face);
  font-size: 1.875rem;
  font-weight: 400;
  letter-spacing: 0;
  /* Letters and labels sit over the rose’s linework, so both carry the
     paper halo. */
  text-shadow:
    0 0 5px var(--paper), 0 0 5px var(--paper),
    0 0 10px var(--paper), 0 0 10px var(--paper);
}

/* Each label rides above its letter — the owner’s ask: the wording outside
   the compass, above the lettering. DOM order stays letter-then-label for a
   screen reader (“N, Physical Labor”); order swaps only the drawing. */
.point .compass-label {
  order: -1;
  color: var(--muted);
  /* 0.9375rem for two builds, and the owner asked for bigger again. It costs
     the rose nothing this time — the width cap above is what changed, not the
     artwork — and it stays under the 1.375rem letters so N/E/S/W still read as
     the heading and the name as its caption. */
  font-size: 1.1875rem;
  text-shadow:
    0 0 5px var(--paper), 0 0 5px var(--paper),
    0 0 10px var(--paper), 0 0 10px var(--paper);
}

/* The selected point keeps its own word — the owner's ask, and the reason the
   heading above the rose is gone. Mapping stays on the east when east is the
   quadrant you are standing in; it darkens from faded ink to full and takes
   the engraved weight, rather than moving somewhere else on the screen. */
.point.is-active .compass-label {
  color: var(--ink);
  font-weight: 600;
}

/* The selected point's letter grows, and it has to grow from the larger
   resting size the letters have carried since the script did — 1.875 was the
   active size when they were Cinzel and is the resting size now. */
.point.is-active .point-dir { font-size: 2.5rem; }

/* ----------------------------------------------------------------- quadrant */

.add { display: flex; gap: 0.5rem; margin-bottom: 0.625rem; }

.add input {
  flex: 1;
  min-width: 0;
  /*
   * 16px, and not a pixel under, however tight the screen gets.
   *
   * iOS Safari zooms the whole page when a focused input's font is below 16px,
   * and it does not zoom back out — so a 15px capture field would jerk the
   * layout sideways every time somebody added a task. The owner asked for a
   * smaller font here; this is the floor, and the rest of the height came out
   * of the padding instead.
   */
  padding: 0.5625rem 2.25rem 0.5625rem 0.875rem;
  font-size: 1rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  /* A small copper star sits in the field's right end, as it does on the
     chart's own capture line. The padding above keeps long text off it. */
  background: var(--star-rust) right 0.7rem center / 0.875rem 0.875rem no-repeat, var(--card);
  box-shadow: inset 0 1px 3px rgb(60 45 15 / 12%);
  color: var(--ink);
  /* 16px minimum, or iOS zooms the page on focus and never zooms back. */
  font: inherit;
}

.add input::placeholder { color: var(--muted); font-style: italic; }

.add input:focus-visible,
.compass button:focus-visible,
button:focus-visible,
select:focus-visible,
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }

.add button[type="submit"] {
  padding: 0.625rem 1.25rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: linear-gradient(var(--brass-fill), var(--brass-fill-deep));
  box-shadow: inset 0 0 0 1px rgb(250 244 230 / 70%), 0 1px 3px rgb(60 45 15 / 25%);
  color: var(--ink);
  font: 600 0.9375rem/1.4 var(--engraved);
  letter-spacing: 0.05em;
  cursor: pointer;
}

.controls {
  display: flex;
  gap: 0.875rem;
  align-items: center;
  margin-bottom: 0.375rem;
  color: var(--muted);
  font-size: 0.8125rem;
}

.controls select {
  margin-left: 0.25rem;
  padding: 0.1875rem 0.375rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.375rem;
  background: var(--card);
  color: var(--ink);
  font: inherit;
  font-size: 0.875rem;
}

.toggle { display: flex; align-items: center; gap: 0.375rem; cursor: pointer; }

.direction {
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.375rem;
  background: var(--card);
  color: var(--ink);
  font: inherit;
  font-size: 0.8125rem;
  cursor: pointer;
}

.direction:disabled { opacity: 0.4; cursor: default; }

/* --------------------------------------------------------------------- list */

/* The rule between controls and list is an ornament — line, star, line — the
   way a chart rules off its title block. Drawn on the list mount so every
   waypoint gets it without an element of its own. */
#list::before {
  content: '';
  display: block;
  height: 1rem;
  margin: 0.375rem 0 0.75rem;
  background:
    linear-gradient(var(--rule-strong), var(--rule-strong)) left center / calc(50% - 1.25rem) 1px no-repeat,
    linear-gradient(var(--rule-strong), var(--rule-strong)) right center / calc(50% - 1.25rem) 1px no-repeat,
    var(--star-rust) center / 1rem 1rem no-repeat;
  opacity: 0.75;
}

.list { margin: 0; padding: 0; list-style: none; }

/**
 * The line every control in a row sits on.
 *
 * `body` is `17.5px/1.5`, so the first line of an item's words occupies a
 * 26.25px line box starting at the row's padding. A row is `align-items:
 * flex-start` — it has to be, or a three-line item puts its controls halfway
 * down — which means each control aligns its own TOP and lands wherever its
 * own height leaves it. Measured on the phone that put the grip 7px below the
 * words, the anchor 1px above them and the three buttons 3px below: four
 * controls, four different lines, none of them the text's.
 *
 * So each control centers on this instead, either by being exactly this tall
 * or by being pulled up half the difference. The difference is worked out in
 * the control's own rule next to the height it already declares — a formula
 * applied locally rather than a number copied around — and the check in
 * test/drive.mjs pins every center to the same pixel, so a height changed
 * without its offset fails rather than drifting.
 */
.row { --row-line: calc(1.5 * 17.5px); }

.row {
  display: flex;
  /* Top-aligned since the text started wrapping. Centered, a three-line item
     put its checkbox and its × halfway down the row with nothing beside them,
     and the controls stopped lining up with each other from row to row —
     which is what a column of tap targets is for. */
  align-items: flex-start;
  /*
   * Wraps only so an open plan's steps can take a line of their own.
   *
   * The step group is a child of the row it belongs to — that is what keeps it
   * out of `[...list.children]`, which is the array enableDragging maps a drag
   * onto. As a plain flex child it sat BESIDE the words and squeezed the title
   * to one letter per line, which is what the first build of this looked like.
   *
   * Nothing else wraps. `.row-text` is `flex: 1; min-width: 0`, so it absorbs
   * the width and shrinks rather than pushing the controls onto a second line;
   * only `.step-group` below asks for a full line, and it asks explicitly.
   */
  flex-wrap: wrap;
  gap: 0.625rem;
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--rule);
  background: transparent;
}

/* Only while a drag is happening. A permanent transition would animate the
   rebuild after every add and delete too, which reads as lag rather than
   polish. */
.is-reordering .row { transition: transform 120ms ease; }

.is-reordering .row.is-dragging {
  /* The held row follows the finger exactly; easing it would put it behind.
     Opaque while held — the rows it slides over must not show through it. */
  transition: none;
  position: relative;
  z-index: 1;
  border-radius: 0.375rem;
  background: var(--card);
  box-shadow: 0 0.5rem 1.5rem rgb(60 45 15 / 30%);
}

/* ---------------------------------------------------------------------- grip */

.grip {
  display: grid;
  grid-template-columns: repeat(2, 3px);
  grid-auto-rows: 3px;
  gap: 3px;
  flex-shrink: 0;
  /* Size, not padding: the dots are 15px across and this is the target around
     them, which is what a thumb actually hits. It was 0.75rem of vertical
     padding until the controls were lined up — the same 41px, but a number
     that had to be worked out rather than read, and the offset below has to
     read it. */
  height: 2.5625rem;
  align-content: center;
  justify-content: center;
  margin-top: calc((var(--row-line) - 2.5625rem) / 2);
  padding: 0 0.625rem;
  border: 1px solid transparent;
  border-radius: 0.375rem;
  background: none;
  cursor: grab;
  /* Without this the browser claims the gesture as a page scroll and the row
     never moves — the single most common reason a touch drag does nothing. */
  touch-action: none;
}

.grip i { display: block; width: 3px; height: 3px; border-radius: 50%; background: var(--muted); }
.grip:hover i, .grip:focus-visible i { background: var(--ink); }
.grip:active { cursor: grabbing; }

/**
 * The words, whether they are a <span>, the <button> that opens the editor, or
 * the <textarea> that is the editor. One rule for all three, because the whole
 * point of tapping the words is that nothing moves when you do — a button that
 * sat a pixel off, or a field that changed the line breaks, would make every
 * edit start with a flinch.
 */
.row-text {
  flex: 1;
  min-width: 0;
  /* A button and a textarea each bring their own, and neither is the page's. */
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  line-height: inherit;
  text-align: left;
  /* Buttons do not wrap by default in every engine, and this one has to. */
  white-space: pre-wrap;
  /* The words are the affordance, so the pointer says "text" rather than
     "link" — there is no button here to find, which is what was asked for. */
  cursor: text;
  /* Wraps, so a long item can be read.
     
     This truncated to one line with an ellipsis for eight builds, on the
     argument that a reflowing row moves the buttons under a thumb already on
     its way down. The owner reported the cost from the phone — "Recipe
     database with AI captur…" in a quadrant, and the same everywhere from the
     Inbox to the Plank — and it is the larger one: a row whose buttons moved
     is a mis-tap, and a row you cannot read is an item you have to open
     something else to identify.

     `anywhere` rather than plain wrapping, because a pasted URL is one word
     with no break in it and would otherwise push the row wider than the
     screen, taking the × off the edge. */
  overflow-wrap: anywhere;
}

/* Being edited. A ruled underline rather than a box, so the row keeps its
   shape and the change is legible as "these words are live" — a full input
   chrome here would redraw the row around the thing being read. */
.row-edit {
  overflow: hidden;
  border-bottom: 1px solid var(--brass);
  resize: none;
  /* iOS zooms the page on focus for anything under 16px. The rows are 17.5px,
     so this only has to not go backwards. */
  font-size: inherit;
}

.row-edit:focus { outline: none; }

/**
 * The anchor: this item is waiting on somebody else.
 *
 * A mask over `currentColor` rather than two drawings, so the faint and inked
 * states are one `color` apart and the artwork exists once. `-webkit-mask` as
 * well as the standard property, because the phone this is built for is the
 * one that still needs it.
 *
 * Sized and spaced like the thermometer beside it, and centered on the first
 * line for the same reason the rest of the row's controls are — a wrapped item
 * must not leave its controls floating halfway down.
 */
.anchor {
  flex-shrink: 0;
  width: 1.125rem;
  /* The line box itself, so the mask below — which fills this box — centers on
     the first line of the words. It was 1.5rem, which left it a pixel high;
     the owner spotted that on the phone before any of the arithmetic did. */
  height: var(--row-line);
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  /* Faint: the resting state is legible as "this control exists" and no more.
     Inked is the statement. */
  color: var(--rule);
}

.anchor::before {
  content: '';
  display: block;
  width: 100%;
  height: 100%;
  background: currentColor;
  -webkit-mask: var(--anchor-mask) center / 1.125rem 1.125rem no-repeat;
  mask: var(--anchor-mask) center / 1.125rem 1.125rem no-repeat;
}

.anchor[aria-pressed="true"] { color: var(--rust); }

/* An item nobody is waiting on reads normally; one that is waiting is not the
   reader's to move, and the words say so quietly rather than by shouting. The
   anchor is the signal and this is the second half of it, so the distinction
   survives being seen out of the corner of an eye. */
.row:has(.anchor[aria-pressed="true"]) .row-text { color: var(--muted); }

/* Centered, because the completion box stands between the two buttons and is
   smaller than they are. Stretch — the flex default — would pull a 22px circle
   to 32px tall and draw it as an oval.

   The box is the line box, and the 2rem buttons inside it overflow by 3px top
   and bottom. That is deliberate: overflow is visible and a button that sticks
   out is still a button, so the tap targets keep their full height while the
   glyphs line up with the words. Giving the container the buttons' height
   instead would have centered them on nothing. */
.row-actions {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  flex-shrink: 0;
  height: var(--row-line);
}

.row-button {
  width: 2rem;
  height: 2rem;
  border: 1px solid transparent;
  border-radius: 0.375rem;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.row-button:hover:not(:disabled) { border-color: var(--rule-strong); color: var(--ink); }
.row-button:disabled { opacity: 0.25; cursor: default; }

/* The Cave button's face. A .row-button in every other respect — same box, same
   border on hover, same muted resting color as the × it stands beside — so the
   two read as two of a kind rather than as a picture next to a character. */
.row-cave::before {
  content: '';
  display: block;
  width: 100%;
  height: 100%;
  background: currentColor;
  -webkit-mask: var(--cave-mask) center / 1.25rem 1.25rem no-repeat;
  mask: var(--cave-mask) center / 1.25rem 1.25rem no-repeat;
}

/* Taller than the row's, because it stands beside a text field rather than a
   line of text, and a 1.5rem control floating against a 2.6rem input reads as
   though it belongs to something else. Stretching rather than fixing a height
   keeps them level if the field's padding is ever changed. */
.add .temp {
  align-self: stretch;
  width: 1.125rem;
  height: auto;
}

.temp {
  flex-shrink: 0;
  width: 0.875rem;
  /* The tube's drawn height, which is design rather than layout — so this one
     is centered by offset rather than by being stretched to the line. */
  height: 1.5rem;
  margin-top: calc((var(--row-line) - 1.5rem) / 2);
  padding: 0;
  border: 1px solid var(--rule-strong);
  border-radius: 0.4375rem;
  background: var(--card);
  cursor: pointer;
  /* The fill grows from the bottom, like a thermometer. */
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  position: relative;
}

.temp-fill { display: block; width: 100%; }

/**
 * Two hairlines at the third and two-third marks, drawn over the fill.
 *
 * This is the half of the night-mode fix that does not depend on color at
 * all. Without them the only way to tell medium from high is to compare one
 * fill's shade against another's, from memory, on a control 14px wide — and
 * comparing shades is exactly what a night filter, a dim screen or a
 * red-green deficiency takes away. With them the level is read against a
 * scale that is still there when every fill looks the same: below both
 * marks, between them, or above both.
 *
 * Over the fill rather than behind it, which is why this is an overlay and
 * not a background on .temp. Behind it, the fill covers each mark as it
 * passes, so medium and high both show no marks and the one comparison that
 * had to survive is the one that would have been lost.
 *
 * --rule-strong because it has to read against both grounds: 2.5:1 on the
 * empty cream tube and 2.4:1 on the deep rust of a full one. A paper tint
 * would vanish on the empty state and an ink one on a full one.
 */
.temp::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    to top,
    transparent 0,
    transparent calc(33.333% - 0.5px),
    var(--rule-strong) calc(33.333% - 0.5px),
    var(--rule-strong) calc(33.333% + 0.5px),
    transparent calc(33.333% + 0.5px),
    transparent calc(66.667% - 0.5px),
    var(--rule-strong) calc(66.667% - 0.5px),
    var(--rule-strong) calc(66.667% + 0.5px),
    transparent calc(66.667% + 0.5px)
  );
}

.temp[data-level="0"] .temp-fill { height: 0; }
.temp[data-level="1"] .temp-fill { height: 33%; background: var(--temp-1); }
.temp[data-level="2"] .temp-fill { height: 66%; background: var(--temp-2); }
.temp[data-level="3"] .temp-fill { height: 100%; background: var(--temp-3); }

/* Words alone. This drew its own anchor above the message until the backdrop
   became the artwork, which has one — and the two landed on top of each other
   on the Compass screen, a drawing over a drawing. The backdrop's anchor is
   fixed and the message moves with the list, so there is no arrangement where
   both belong; the artwork keeps the anchor and the empty state keeps the
   sentence. Centered, because it is a state of the whole list rather than a
   row of it. */
.empty {
  margin: 1.5rem 0;
  color: var(--muted);
  font-style: italic;
  text-align: center;
}

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

.footer { margin-top: 2.5rem; }

.link {
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.8125rem;
  text-decoration: underline;
  cursor: pointer;
}

#backup-output {
  display: block;
  width: 100%;
  margin-top: 0.75rem;
  padding: 0.625rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: var(--card);
  color: var(--muted);
  font-family: ui-monospace, monospace;
  font-size: 0.75rem;
}

/* The engraved face, not the print one: Fell's old-style figures draw a zero
   as a small o, and "o.o.22" is not a build id anyone can read back over the
   phone. Cinzel's figures are lining. */
.build { margin: 1rem 0 0; color: var(--muted); font-family: var(--engraved); font-size: 0.6875rem; }

/* Completion, standing between the Cave and the ×.
   
   It used to sit second in the row, 10px from the thermometer, and that is the
   mis-tap the owner reported: reaching for a temperature and completing the
   item instead. Moving it here fixes that by distance — the thermometer is now
   the width of the words away — but it lands between two other targets, so the
   distance has to be bought back on both sides.
   
   Equal on both sides, at the owner's instruction. `.row-actions` supplies
   0.25rem of its own, so this is 8px of clear space toward the Cave and 8px
   toward the ×.

   It was deliberately unequal for one build — wider toward the × on the
   argument that deleting something you meant to finish is the mistake that
   costs — and the owner rejected it on sight. Worth writing down, because the
   argument was not so much wrong as beside the point: a row of controls is
   read every time and mis-tapped rarely, and a gap that changes width along
   the row is visible on every one of those readings. Even spacing is what
   makes the three read as one set.

   It came in from 10px and 14px in the same pair of builds, at the owner's
   report of too much space. That much was safe because the neighbors changed:
   the box used to sit against the thermometer, a bare 14px target with no
   padding of its own, and it now sits between two 2rem buttons whose glyphs
   are inset about 11px from their edges — a thumb landing short still has that
   inset to fall into.

   The drawing lives on the shared checkbox rule above; this only keeps the row
   from squeezing it. */
.row-actions > .row-check {
  flex-shrink: 0;
  margin-left: 0.25rem;
  margin-right: 0.25rem;
}

/* ------------------------------------------------------------- the shell */

/* Centered, because the row now holds the Waypoints chip between the name and
   the menu, and three things of three heights read as clutter unless they sit
   on one line. The title claims the spare width; the chip and menu keep to
   their own size. */
.masthead-row { display: flex; align-items: center; gap: 0.75rem; }
.masthead-row h1 { margin-right: auto; }

/* The screen headings — Settings, the gate, How to. The compass hub is also an
   h2 and stays in the print face, because it carries the user's words rather
   than the app's. */
#screen-settings h2,
#screen-gate h2,
#screen-howto h2 {
  margin: 0 0 1rem;
  font-family: var(--engraved);
  font-size: 1.375rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* Three spans rather than the ☰ character, which is a box on a device without
   the glyph and is announced as "trigram for heaven" by some screen readers.
   Padding rather than size: the bars are 18px across, the target around them
   is past the 44px a thumb actually needs. */
.hamburger {
  display: grid;
  gap: 4px;
  flex-shrink: 0;
  padding: 0.75rem;
  margin: -0.5rem -0.5rem 0 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.hamburger i { display: block; width: 18px; height: 2px; background: var(--ink); }

/* Secondary to the four waypoints, and it has to look it. Open above the home
   screen at the same weight, three menu rows and four waypoint rows read as
   one list of seven — which is both wrong about the product, where four is the
   constraint, and wrong about the screen, where only four of them are the
   destinations. Smaller, muted, and ruled off underneath. */
.menu {
  display: grid;
  gap: 0.125rem;
  margin: 0.75rem 0 1.25rem;
  padding-bottom: 0.875rem;
  border-bottom: 1px solid var(--rule);
}

.menu button {
  padding: 0.625rem 0.75rem;
  border: 0;
  border-radius: 0.375rem;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.9375rem;
  text-align: left;
  cursor: pointer;
}

.menu button:hover { background: var(--card); color: var(--ink); }

/* Back to the four waypoints, worn as a small chip in the header row rather
   than a line of its own above every screen — the owner's ask: it was
   spending a row. The rose keeps it "back to where the roses are", and it is
   named rather than a bare arrow, because "‹" on its own is the browser's
   back button in every other app on the phone and this one does not go back —
   it goes up. Sized to share 390px with the title and the menu button. */
.back {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  gap: 0.375rem;
  padding: 0.3125rem 0.625rem 0.3125rem 0.5rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: var(--card);
  color: var(--ink);
  font: 600 0.6875rem/1.4 var(--engraved);
  letter-spacing: 0.04em;
  cursor: pointer;
}

.back::before {
  content: '';
  width: 0.875rem;
  height: 0.875rem;
  background: var(--star-rose) center / contain no-repeat;
}

/* --------------------------------------------------------------- the gate */

/* Mapwork requires an account to start, and this is the whole screen until it
   has one. */
.gate { padding-top: 1.5rem; max-width: 22rem; }

/* Centered down the screen, at the owner's request after seeing it on a phone:
   the form filled the top half and left an empty parchment field below, which
   reads as unfinished rather than spacious.

   It was deliberately top-aligned before and that reason has not gone away —
   "a phone keyboard takes half the screen the moment the email field is
   tapped, and a vertically centered form jumps when it opens." Two things
   answer it. `svh` is the viewport at its smallest possible size and is a
   fixed number for the life of the page, so the box being centered in cannot
   change height while somebody types; `dvh` and `vh` would reintroduce exactly
   that jump and are the two units not to swap this for. And `flex: 1` with
   `justify-content: center` overflows downward rather than clipping, so a
   form grown tall by an error message still scrolls instead of losing its
   ends.

   Scoped with :has() so it applies on the gate and nowhere else — every other
   screen is a list that must start at the top. A browser without :has()
   (Safari before 15.4) simply gets the old top-aligned gate, which is why
   this is written as an addition rather than a change to the rule above. */
body:has(> main > .gate:not([hidden])) {
  min-height: 100svh;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
}
body:has(> main > .gate:not([hidden])) main,
body:has(> main > .gate:not([hidden])) .gate {
  flex: 1;
  display: flex;
  flex-direction: column;
}
body:has(> main > .gate:not([hidden])) .gate { justify-content: center; }
.gate .note { margin-bottom: 1.25rem; }
/* Block, with room under it: as an inline button it sat hard against the
   paragraph below and the two read as one run of text.

   `align-self` because the gate is a flex column now, and a stretched flex
   item is a full-width button whose label centres itself under a left-aligned
   form. Caught by screenshot rather than by reading the diff. */
.gate #forgot { display: block; align-self: flex-start; margin: 0.375rem 0 1.5rem; }

/* ---------------------------------------------------------------- how to */

/* Prose, and the only screen in the app that is. Measure and spacing are what
   this needs rather than components — it is read once or twice and then
   rarely, so it is written to be skimmed by its headings. */
.howto { max-width: 34rem; }
.howto p { margin: 0 0 0.875rem; }
.howto p:last-child { margin-bottom: 0; }
.howto strong { font-weight: 600; color: var(--ink); }

/**
 * What the product is for, before any of the mechanics.
 *
 * The build document's own opening line, and the only sentence on this screen
 * that is an argument rather than an instruction — so it is set as one: larger,
 * inked rather than muted, and held off the left margin by a rule the way a
 * chart's legend is. Everything below it reads as explanation of this.
 *
 * Deliberately not a tinted box. A colored callout would make it look like an
 * alert, and this is the opposite of an alert.
 */
.creed {
  margin: 0 0 1.25rem;
  padding: 0.125rem 0 0.125rem 1rem;
  border-left: 3px solid var(--brass);
  color: var(--ink);
  font-size: 1.125rem;
  line-height: 1.45;
}

/**
 * One question at a time.
 *
 * Shut by default: a dozen answers stacked open is a wall of text on a phone,
 * and somebody arrives here with one question rather than twelve. The marker
 * is the summary's own, not a list bullet, and it turns when the answer opens
 * so a shut question does not read as an unclickable heading.
 */
.faq {
  margin: 0 0 0.5rem;
  border-bottom: 1px solid var(--rule);
}

.faq > summary {
  cursor: pointer;
  padding: 0.5rem 0 0.5rem 1.25rem;
  color: var(--ink);
  font-weight: 600;
  list-style: none;
  position: relative;
}

.faq > summary::-webkit-details-marker { display: none; }

.faq > summary::before {
  content: '';
  position: absolute;
  left: 0.125rem;
  top: 1.05rem;
  width: 0;
  height: 0;
  border-left: 0.375rem solid var(--brass);
  border-top: 0.3125rem solid transparent;
  border-bottom: 0.3125rem solid transparent;
  transition: transform 120ms ease;
  transform-origin: 0.1875rem 50%;
}

.faq[open] > summary::before { transform: rotate(90deg); }
.faq > summary:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
.faq > p { margin: 0 0 0.875rem 1.25rem; color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
  .faq > summary::before { transition: none; }
}

.howto h3 {
  margin: 1.75rem 0 0.625rem;
  font-family: var(--engraved);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.05em;
}

/* The rule under each heading is the chart's own, the same line-star-line the
   list mount carries, so the page belongs to the app rather than reading as a
   document pasted into it. */
.howto h3::after {
  content: '';
  display: block;
  height: 0.75rem;
  margin-top: 0.375rem;
  background:
    linear-gradient(var(--rule-strong), var(--rule-strong)) left center / calc(50% - 0.75rem) 1px no-repeat,
    linear-gradient(var(--rule-strong), var(--rule-strong)) right center / calc(50% - 0.75rem) 1px no-repeat,
    var(--star-rust) center / 0.75rem 0.75rem no-repeat;
  opacity: 0.6;
}

/* ------------------------------------------------------------ home screen */

/* Four items, and four is the constraint everywhere. One per row rather than a
   2×2 grid: these are destinations with counts, not a keypad, and a full-width
   target is the easiest thing on the screen to hit. */
.home { display: grid; gap: 0.625rem; }

.home button {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 3.5rem;
  padding: 0.875rem 1rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.625rem;
  background: var(--card);
  box-shadow: inset 0 0 0 1px rgb(250 244 230 / 70%), 0 1px 3px rgb(60 45 15 / 15%);
  color: var(--ink);
  font: inherit;
  font-size: 1.0625rem;
  cursor: pointer;
}

/* Each waypoint wears the rose, the way a chart marks its stations. */
.home button::before {
  content: '';
  flex-shrink: 0;
  width: 1.25rem;
  height: 1.25rem;
  background: var(--star-rose) center / contain no-repeat;
  opacity: 0.8;
}

.home-name {
  margin-right: auto;
  font-family: var(--engraved);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.05em;
}

.home-count { color: var(--brass); font-size: 0.9375rem; font-weight: 600; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- chambers */

/* Four chambers mirroring the four quadrants, named by the user's own labels.
   Wrapping rather than a fixed four-across: two-word labels at 390px do not
   fit in a quarter of the width, and truncating the words somebody chose is
   the wrong thing to shorten. */
/**
 * Two by two, always, rather than a flex row that wraps where it happens to
 * run out.
 *
 * Reported from the phone: with the owner's own chamber names the row broke
 * three-and-one, and the orphan on the second line read as a mistake. Wrapping
 * itself is not avoidable — a chamber is named by its quadrant and the names
 * are the user's own, so any width can be overrun — but WHERE it wraps is, and
 * an even split looks chosen where an orphan looks broken.
 *
 * A grid rather than flex with a basis, because four is the constraint the
 * whole product is built on: four quadrants, four chambers, four home-screen
 * items. Two columns of two says that, and it cannot fall out of it the way a
 * flex row can when somebody types a short name.
 *
 * `1fr` and not `auto`, so the four are the same width whatever they are
 * called — "Coding" beside "Physical Labor" at their natural widths is the
 * ragged edge this was reported for, one row down.
 */
.chambers {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.375rem;
  margin-bottom: 1rem;
}

/* Wider than a phone, and four fit across with room to spare. The 34rem is
   where two columns start looking like a waste of the width rather than a
   tidy block. */
@media (min-width: 34rem) {
  .chambers { grid-template-columns: repeat(4, 1fr); }
}

.chamber {
  display: flex;
  align-items: center;
  /* Centered in its own cell now that the cells are equal, and the count sits
     with the name rather than drifting to the far edge of a stretched box. */
  justify-content: center;
  gap: 0.375rem;
  /* A long quadrant name wraps inside its chip instead of widening the column
     past its share and pushing the grid out of square. */
  overflow-wrap: anywhere;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: var(--card);
  color: var(--ink);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
}

.chamber.is-active {
  background: linear-gradient(var(--brass-fill), var(--brass-fill-deep));
  color: var(--ink);
  box-shadow: inset 0 0 0 1px rgb(250 244 230 / 70%), 0 1px 3px rgb(60 45 15 / 25%);
}

.chamber-count { color: var(--muted); font-size: 0.8125rem; font-variant-numeric: tabular-nums; }
.chamber.is-active .chamber-count { color: var(--brass); font-weight: 600; }

/* ------------------------------------------------------------- waypoints */

#waypoint-title {
  margin: 0 0 0.25rem;
  font-family: var(--engraved);
  font-size: 1.375rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* What a waypoint is, and what is not built in it yet. Muted and small: it is
   orientation on arrival, not a message to be read twice. */
.note { margin: 0 0 1rem; color: var(--muted); font-size: 0.8125rem; line-height: 1.45; }

/**
 * The auto-generated Inbox row, pinned above #list rather than a row inside
 * it — see index.html for why. Shaped like a row anyway (the same
 * border-bottom and padding as .row) so it reads as the top of the list
 * rather than a second note stacked above it; --card underneath is the one
 * deliberate difference, because what it is saying is "the system put this
 * here", not "you did".
 */
.inbox-nudge {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0.625rem 0;
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: var(--card);
  color: var(--ink);
  font: inherit;
  font-size: 0.875rem;
  text-align: left;
  cursor: pointer;
}

.inbox-nudge:hover, .inbox-nudge:focus-visible { color: var(--brass); }

/* Charting out of the Cave. A word rather than an icon, because there is no
   glyph anybody reads as "this is ready now". */
.row-chart {
  flex-shrink: 0;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.375rem;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}

.row-chart:hover { border-color: var(--brass); color: var(--brass); }

/* -------------------------------------------------------------- settings */

.labels { display: grid; gap: 0.625rem; }

.label-field { display: flex; align-items: center; gap: 0.75rem; }

.label-dir {
  flex-shrink: 0;
  width: 3.25rem;
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
}

.label-input {
  flex: 1;
  min-width: 0;
  padding: 0.625rem 0.75rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: var(--card);
  color: var(--ink);
  /* 16px minimum, or iOS zooms the page on focus and never zooms back. */
  font: inherit;
}

/* --------------------------------------------------- restoring from backup */

/* A settings toggle stands on its own line, unlike the ones packed into the
   controls row above a list, so it gets the row's own spacing. */
.settings-toggle { margin: 0.5rem 0 0; }

/* The Cave's pair belong to the line above them, and the indent is what says
   so — they are two clocks within one waypoint, not two more waypoints. */
.settings-subhead { margin: 1rem 0 0; }
.settings-nested { margin-left: 1.25rem; }

/**
 * A settings card: a titled thing you open, rather than a heading with
 * controls loose underneath it.
 *
 * `.also` already supplies the open/shut behavior — this adds the card, and
 * only the card. The summary takes the engraved capitals every other heading
 * on this screen wears, because it IS the heading when the card is shut, and
 * `.also summary`'s muted 13px would have made it read as a footnote instead.
 */
.card {
  margin: 2rem 0 0.75rem;
  padding: 0.25rem 0.875rem 0.125rem;
  border: 1px solid var(--rule);
  border-radius: 0.5rem;
  background: var(--card);
}

.card > summary {
  cursor: pointer;
  margin: 0 -0.875rem;
  padding: 0.625rem 0.875rem;
  color: var(--ink);
  font-family: var(--engraved);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.05em;
}

/* Only once it is open. A rule under a shut card would draw a line under
   nothing. */
.card[open] > summary {
  margin-bottom: 0.5rem;
  border-bottom: 1px solid var(--rule);
}

.card-note { margin: 0.75rem 0 0.25rem; }
.card .settings-toggle:last-of-type { margin-bottom: 0.75rem; }
/* ...except when the sample belongs to it. That rule exists to hold the last
   toggle off the bottom of its card; on the case card the last option is
   followed by its own example, and the gap pushed the two apart so the third
   sample read as though it belonged to the note underneath. */
.card .settings-toggle:has(+ .case-sample) { margin-bottom: 0; }

/* What each case does to the same line, shown rather than described. The
   sample sits under its own option and takes the case it names, so choosing
   is reading three lines instead of parsing three definitions — which is the
   only honest way to show that Proper Case lowercases an acronym. */
.case-sample {
  margin: 0.125rem 0 0 1.75rem;
  color: var(--muted);
  font-size: 0.8125rem;
  font-style: italic;
}

.settings-heading {
  margin: 2rem 0 0.25rem;
  font-family: var(--engraved);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.05em;
}

#restore-input {
  display: block;
  width: 100%;
  margin-bottom: 0.625rem;
  padding: 0.625rem 0.75rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: var(--card);
  color: var(--muted);
  font-family: ui-monospace, monospace;
  /* 16px minimum, or iOS zooms the page on focus and never zooms back. */
  font-size: 1rem;
}

/* Outlined rather than filled while it is only an offer. Filling it in the
   warning color would make the loudest thing on the Settings screen the one
   control nobody should press by accident. */
.danger {
  width: 100%;
  padding: 0.75rem 1rem;
  border: 1px solid var(--warn);
  border-radius: 0.5rem;
  background: none;
  color: var(--warn);
  font: inherit;
  cursor: pointer;
}

/* Armed. It has said what it will do and is waiting for the second tap, so now
   it is worth being the loudest thing on the screen. */
.danger.is-armed { background: var(--warn); color: var(--paper); font-weight: 600; }

/* How long a deleted item has left. Muted and after the text, because it is
   the row saying when it goes rather than what it is. */
.row-meta {
  flex-shrink: 0;
  color: var(--muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  /* "7d old" is three times the width of "7d", and it shares a 390px row with
     a checkbox, a thermometer, wrapped text and the ×. Aligned to the first
     line so it reads beside the words rather than beside the middle of a
     three-line item, and it never wraps — a label that broke across lines
     would take more from the text than it is worth. */
  align-self: flex-start;
  padding-top: 0.125rem;
  white-space: nowrap;
  /* Stacked when there is more than one, right-aligned so the numbers line up
     with each other rather than with the ragged left edge two words make. */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  line-height: 1.4;
}

/* ------------------------------------------------------------- triage */

/* Ruled off rather than floated over the list, so the panel never covers the
   thing the decision is about.

   It used to sit above the list and the comment here claimed the item was
   "still on screen above it". True on a short list; false the moment one
   scrolls, which is how the owner met it — the ellipsis on a row most of the
   way down opened a menu at the top, beside rows it had nothing to do with.
   dockPanel in app.js moves it into the row instead, and the rule below is
   what makes it sit under the words rather than beside them. */
.chart-panel,
.move-panel {
  margin-bottom: 1rem;
  padding: 0.875rem;
  border: 1px solid var(--brass);
  border-radius: 0.625rem;
  background: var(--card);
  box-shadow: 0 1px 4px rgb(60 45 15 / 15%);
}

/*
 * The ellipsis reads as a menu rather than as a third glyph.
 *
 * Letter-spaced because three full stops set solid look like an ellipsis in
 * running text — a piece of punctuation — rather than a control. Slightly
 * larger than the × for the same reason: at the same size the two read as a
 * pair of characters, and only one of them is.
 */
/*
 * Docked under its row.
 *
 * `.row` is a wrapping flex container, so a panel appended to it lands beside
 * the title and squeezes it to one letter per line unless it claims the whole
 * line — the same trap the step group fell into. flex-basis does that.
 *
 * The bottom margin belongs above the list, not inside a row, where it would
 * open a gap between this item and the next one.
 */
.row > .move-panel,
.row > .chart-panel {
  flex-basis: 100%;
  margin: 0.5rem 0 0;
}

.row-relocate { font-size: 1.125rem; letter-spacing: 0.05em; line-height: 1; }

.chart-what { margin: 0 0 0.75rem; font-size: 0.9375rem; }
.chart-what strong { font-weight: 600; }

/*
 * The movement menu: two rows, and the second one holds all four destinations.
 *
 * It was a column of four full-width buttons for one build. The owner asked for
 * "one or 2 rows with least words possible" — four lines of reading for a
 * decision taken in a second, and the qualifiers on them ("Compass — another
 * quadrant") were saying things the next screen says properly anyway.
 *
 * Words rather than icons, and that was measured rather than assumed. He said
 * icons if the room was needed; at 0.8125rem four one-word labels fit a 390px
 * row with space to spare, and a word nobody has to learn beats a glyph that
 * has to be. The anchor in particular could not have been borrowed: it already
 * means "waiting on somebody" on every row of this same screen, and one mark
 * with two meanings in one view is worse than a slightly wider button.
 *
 * `flex: 1` rather than content width so the four are equal targets. Delete
 * being the same size as the rest is deliberate — a smaller one would be the
 * hardest to hit and the worst to miss.
 */
.move-what { font-size: 0.875rem; margin: 0 0 0.5rem; display: flex; gap: 0.5rem; align-items: baseline; }
.move-what strong { flex: 1; }

/* What travels with it, and the one control that changes what an item is
   rather than where it lives — both above the rule the four destinations sit
   under. */
.move-with { margin: 0 0 0.5rem; color: var(--danger, #8f2c1e); font-size: 0.8125rem; }

.move-convert {
  width: 100%;
  margin-bottom: 0.625rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--brass);
  border-radius: 0.25rem;
  background: var(--card);
  color: inherit;
  font: inherit;
  font-size: 0.8125rem;
  text-align: center;
}

/*
 * A plan's steps, opened under its own row.
 *
 * Indented and ruled down the left, which is the whole of what says these
 * belong to the row above rather than being more of the quadrant. The rule is
 * brass at low weight: strong enough to group, quiet enough that a quadrant
 * with three plans open is still a list rather than a diagram.
 */
.step-group {
  /* A line of its own, under the row it belongs to rather than beside it. */
  flex-basis: 100%;
  margin: 0.25rem 0 0 1.75rem;
  padding-left: 0.75rem;
  border-left: 2px solid var(--rule);
}

.step-group .list { margin: 0; }
.steps-empty { margin: 0.375rem 0; font-size: 0.8125rem; }

/* The capture field belongs to the plan it sits under, not to the quadrant —
   with two plans open there is no single answer to what the main field would
   be adding to. */
.add-step { display: flex; gap: 0.375rem; margin: 0.375rem 0 0.5rem; }

.add-step input {
  flex: 1;
  min-width: 0;
  /* 16px, the same floor the main capture field has and for the same reason:
     iOS zooms a focused input below it and does not zoom back. */
  padding: 0.375rem 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 0.375rem;
  background: var(--card);
  color: inherit;
  font: inherit;
  font-size: 1rem;
}

.add-step button {
  padding: 0.375rem 0.75rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.375rem;
  background: var(--card);
  color: inherit;
  font: inherit;
  font-size: 0.8125rem;
}

/*
 * The marker that says a row is a plan, and opens it.
 *
 * On the right of the words, before the tick, so a quadrant reads: what it is,
 * how deep it goes, whether it is done, where else it could go. Brass because
 * that is this app's word for "chosen or choosable", and small because a
 * quadrant of plans should still read as a list of tasks.
 */
.row-expedition {
  flex-shrink: 0;
  margin-left: 0.5rem;
  padding: 0.125rem 0.4375rem;
  border: 1px solid var(--brass);
  border-radius: 0.75rem;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}

/* The sequence number, standing where a quadrant row keeps its thermometer so
   the two lists line up down the left edge. */
.row-step {
  flex-shrink: 0;
  width: 1.5rem;
  color: var(--muted);
  font-size: 0.875rem;
  text-align: right;
}

.move-targets { display: flex; gap: 0.375rem; }

.move-target {
  flex: 1;
  font: inherit;
  font-size: 0.8125rem;
  text-align: center;
  padding: 0.5rem 0.25rem;
  border: 1px solid var(--rule);
  border-radius: 0.25rem;
  background: var(--card);
  color: inherit;
}

/*
 * Delete looks like what it does, and is last.
 *
 * Not red-on-red: the parchment ground is warm and a saturated red on it reads
 * as an error state rather than a choice. The rule and the word carry it.
 */
.move-delete { color: var(--danger, #8f2c1e); border-color: currentColor; }

/* One per row, full width. These carry the user's own words, which are two
   words often enough that a 2×2 grid truncates them — and truncating the names
   somebody chose is the wrong thing to shorten. */
.chart-directions { display: grid; gap: 0.375rem; margin-bottom: 0.625rem; }

.chart-direction {
  padding: 0.625rem 0.75rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.chart-direction:hover { border-color: var(--brass); color: var(--brass); }

/* ------------------------------------------------------------- snapshots */

.snapshots { display: grid; gap: 0.25rem; margin-bottom: 0.75rem; }

.snapshot {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--rule);
}

/* Reason and age together, because "on opening, 2 hours ago" is one fact about
   a snapshot and reading it as two would need the eye to travel twice. */
.snapshot-what { flex: 1; min-width: 0; font-size: 0.875rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.snapshot-size { flex-shrink: 0; color: var(--muted); font-size: 0.75rem; font-variant-numeric: tabular-nums; }

.snapshot-restore,
.snapshot-save {
  flex-shrink: 0;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.375rem;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}

.snapshot-restore:hover,
.snapshot-save:hover { border-color: var(--brass); color: var(--brass); }

/* Not `.danger`: restoring a snapshot takes one of what it replaces first, so
   the mistake is already undoable and dressing it as destructive would be the
   warning that cries wolf. */
.wide {
  width: 100%;
  margin-bottom: 0.5rem;
  padding: 0.625rem 1rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: var(--card);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}

/* --------------------------------------------------------------- files */

/* The native file input is unstyleable and reads as "No file chosen" next to a
   gray button on every platform. Hidden behind its own label, which is a real
   control for a screen reader and opens the picker on a tap. */
.file-pick { display: block; margin-bottom: 0.75rem; }
.file-pick input { position: absolute; width: 1px; height: 1px; opacity: 0; }

.file-pick span {
  display: block;
  padding: 0.625rem 1rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: var(--card);
  color: var(--ink);
  text-align: center;
  cursor: pointer;
}

.file-pick input:focus-visible + span { outline: 2px solid var(--brass); outline-offset: 2px; }

/* The text route still exists for anyone who wants it, folded away because a
   file is the better answer on the device this is built for. */
.also { margin-bottom: 0.75rem; }
.also summary { color: var(--muted); font-size: 0.8125rem; cursor: pointer; padding: 0.375rem 0; }

/* ----------------------------------------------------------------- account */

/* Stacked rather than a row like `.add`, because two fields and a button side
   by side on a 390px screen leaves each field too narrow to read what is in
   it — and an email address is long. */
.sign-in { display: grid; gap: 0.5rem; margin-bottom: 0.75rem; }

.sign-in input {
  padding: 0.625rem 0.75rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: var(--card);
  color: var(--ink);
  /* 16px minimum, or iOS zooms the page on focus and never zooms back. */
  font: inherit;
}

.sign-in button[type="submit"] {
  padding: 0.625rem 1rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  background: linear-gradient(var(--brass-fill), var(--brass-fill-deep));
  box-shadow: inset 0 0 0 1px rgb(250 244 230 / 70%);
  color: var(--ink);
  font: 600 0.9375rem/1.4 var(--engraved);
  letter-spacing: 0.05em;
  cursor: pointer;
}

/* The signed-in line is a fact rather than a warning, so it reads as body text
   with the address picked out — not as a banner. */
#account-state strong { color: var(--ink); font-weight: 600; }
