/* === Canvas === */

#viewport {
  position: fixed;
  /* The board ends above the docked Timeline (--dock, ui/timeline-view.ts). */
  inset: 0 0 var(--dock) 0;
  /* All gestures are ours. */
  touch-action: none;
  /* Long-press on iOS starts text selection on the whole surface; contenteditable
     children are exempt and keep their selection. */
  -webkit-user-select: none;
  user-select: none;
  cursor: default;
  /* Stated, not assumed — see contenteditable override below. */
  overflow: hidden;
  background-color: transparent;   /* the paper on <body> shows through */
  background-repeat: repeat;
  contain: layout paint;
  /* Taking the band back, eased, so the board's edge slides up under the
     Timeline as it fades in rather than cutting the cards off in one frame. */
  transition: bottom var(--dur-base) var(--ease);
}
/* Wherever the Timeline fades (chrome.css: idle, a tour, cleaning) the board
   takes its band. Stopping above an invisible bar left an empty strip of paper
   with the grid ending at it and the cards cut off along it - which read as
   the bar still there with only its buttons gone. Canvas/viewport.ts holds the
   board still as the box grows, so nothing on it moves. Giving the band is
   instant: the bar is still opaque at that moment and covers the change. */
:root:is(.is-idle:not(.is-replaying), .is-touring, .is-tour-edit, .is-cleaning) #viewport {
  bottom: 0;
  transition: none;
}
/* Restore selection on editable surfaces. `text`, not `auto` (auto inherits
   `none` back). Both spellings for WebKit < 17. */
#viewport [contenteditable="true"],
#viewport [contenteditable="plaintext-only"] {
  -webkit-user-select: text;
  user-select: text;
}

#viewport.is-panning { cursor: grabbing; }
#viewport.can-pan    { cursor: grab; }
/* #viewport captures the pointer, so the cursor must be set here. */
#viewport.is-moving  { cursor: grabbing; }
/* Over a connection line. After can-pan so it wins the tie. */
#viewport.over-connection { cursor: pointer; }

/* The one transformed layer. (0,0) at viewport centre; viewport.ts writes the
   transform and --iz (= 1/zoom) for constant-screen-size item chrome. */
#world {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  transform-origin: 0 0;
  /* Permanently promoted - but by the translateZ(0) canvas/viewport.ts writes
     into the transform, not by `will-change: transform`, which is what used to
     be here. The promotion is the part that matters and both give it; the hint
     also pins the layer's raster scale to whatever it was first rastered at,
     and a board at 30% was still being rastered at 1:1. That is ten screenfuls
     of texture for one screen, past what Chromium on a phone will hold, and
     past it the compositor stops drawing - the cards blink out and back. See
     the note at the transform write. Toggling promotion on and off is still
     wrong (two full re-rasters per interaction, every hairline flickering),
     which is why this is permanent and lives in the transform itself. */
  --iz: 1;
  /* A hairline in world space scales with zoom, so 1px at 0.5x paints half a
     device pixel and edges vanish unevenly. * --iz makes it one *screen* pixel.
     Own name, not --hairline, because --hairline is a look token a .mbrd may
     set — redeclaring it here would silently override the board's say. */
  --board-hairline: calc(var(--hairline) * var(--iz));
  /* One physical pixel in CSS pixels. Matters on fractional DPR (125%, 150%)
     where 1 CSS px is 1.25 or 1.5 device px — read by the Harsh override below. */
  --device-px: calc(1px / var(--dpr, 1));
}

/* Harsh only: round hairline to whole device pixels. At other tiers the line
   is pale with a drop shadow — rounding down loses a third of its coverage and
   the border disappears. Here the border IS the elevation. */
:root[data-whimsy="2"] #world {
  --board-hairline: calc(max(var(--device-px), round(down, var(--hairline), var(--device-px))) * var(--iz));
}

/* Sub-pixel positioning: the fix is deviceSnap() in canvas/items.ts, which
   nudges cards onto whole device pixels. Requires whole-px width (above) and
   no rotation (Harsh stands cards up — see quality.css). */

/* Shared shadow underlay — twins of every item so shadows never cast over
   the face of a card behind. Complete shadow stack stays below the item stack. */
#item-shadows {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: visible;
  pointer-events: none;
}
.item-shadow {
  position: absolute;
  --shape-radius: var(--radius);
  --own-radius: min(var(--shape-radius), var(--half-min, 9999px));
  border-radius: var(--own-radius);
  box-shadow: var(--item-shadow);
  rotate: calc(var(--item-tilt, 0) * var(--tilt-max));
  /* Mirrors the card's hover/drag lift so the shadow never slides out. */
  translate: 0 calc(var(--sh-lift, 0px) * var(--iz));
  scale: var(--sh-grow, 1);
  /* opacity in this list (not a separate shorthand) for the tag-filter fade. */
  transition: translate var(--dur-base) var(--ease-back),
              scale var(--dur-base) var(--ease-back),
              rotate var(--dur-base) var(--ease-back),
              opacity var(--dur-base) var(--ease);
}
/* In board units, as the card's hover lift is (items.css). */
.item-shadow.is-hover { --sh-lift: calc(var(--lift-hover) / var(--iz)); --sh-grow: var(--grow-hover); }
/* Selected card holds still (grips must not move). */
.item-shadow.is-hover.is-selected { --sh-lift: 0px; --sh-grow: 1; }
.item-shadow.is-dragging { --sh-lift: var(--lift-drag); --sh-grow: var(--grow-hover); }
.item-shadow[data-type="note"] {
  --item-shadow: var(--note-shadow);
  --shape-radius: var(--note-radius);
}
:root[data-snap] .item-shadow,
:root[data-no-tilt] .item-shadow { rotate: 0deg; }

/* Grid lattice. Sized in device pixels by canvas/grid.ts and stretched to CSS
   pixels so marks snap to the device grid. Position/size written per paint. */
#grid-ink {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Paper outline in screen space (canvas/paper.ts) — a border inside the
   transform would scale with zoom and stop reading as a drawn edge. */
#paper {
  position: absolute;
  border: var(--hairline) solid var(--sheet-line);
  background: var(--sheet-fill);
  pointer-events: none;
}

/* Corner grips for dragging the paper scale. pointer-events:auto on these
   alone — the sheet itself must stay transparent to pan and marquee. */
.paper-grip {
  position: absolute;
  width: var(--sheet-grip);
  height: var(--sheet-grip);
  margin: calc(var(--sheet-grip) / -2) 0 0 calc(var(--sheet-grip) / -2);
  box-sizing: border-box;
  border: var(--hairline) solid var(--sheet-line);
  border-radius: var(--radius-xs);
  background: var(--paper-card);
  pointer-events: auto;
  touch-action: none;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              transform var(--dur-base) var(--ease-back);
}
@media (hover: hover) {
  .paper-grip:hover {
    background: var(--accent);
    border-color: var(--accent-deep);
    transform: scale(1.25);
  }
}
/* Centred on each corner by the negative margin above. */
.paper-grip[data-grip="nw"] { left: 0; top: 0; cursor: nwse-resize; }
.paper-grip[data-grip="ne"] { left: 100%; top: 0; cursor: nesw-resize; }
.paper-grip[data-grip="sw"] { left: 0; top: 100%; cursor: nesw-resize; }
.paper-grip[data-grip="se"] { left: 100%; top: 100%; cursor: nwse-resize; }
/* Too small on screen for separate targets — outline stays, handles go. */
#paper.no-grips .paper-grip { display: none; }

/* Held for the drag so the cursor survives leaving the grip. The axis is the
   grabbed corner's, set by canvas/paper.ts — one value for all four pointed
   across the drag on two of them. nwse is the default so a missing attribute
   still resizes rather than showing an arrow. */
:root.is-sizing-paper,
:root.is-sizing-paper * { cursor: nwse-resize !important; }
:root.is-sizing-paper[data-sizing-axis="nesw"],
:root.is-sizing-paper[data-sizing-axis="nesw"] * { cursor: nesw-resize !important; }
/* Above the sheet's top-left corner, out of the enclosed area. */
#paper-label {
  position: absolute;
  bottom: 100%;
  left: 0;
  margin-bottom: 4px;
  font-size: var(--t-tiny);
  font-variant-numeric: tabular-nums;
  letter-spacing: calc(0.04em * var(--ls-scale));
  color: var(--ink-3);
  white-space: nowrap;
}

/* World axes in screen space, device-pixel-snapped. See canvas/grid.ts. */
#axis-ink {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Mobile board strip. The tone difference between sheet and surround marks
   the edge — the lattice stops at the board (inkBox() in canvas/grid.ts). */

/* That difference in tone, as a layer of its own.
   It was a `box-shadow: 0 0 0 100vmax` on the sheet below, and as a picture
   that was right: a shadow is clipped to outside its element's border box,
   radius included, so the tint stopped exactly at the board and followed its
   rounded corners. What it cost was a full-window repaint on every frame the
   sheet moved, which is every frame of every scroll. The sheet is opaque, so a
   wash underneath it is clipped to the same place by the sheet itself - and a
   wash that covers the whole window never has to move. */
#mobile-surround {
  display: none;
  position: absolute;
  inset: 0;
  /* One shade over the whole surround. Six percent is a small step from the
     old four, but enough to keep a near-white sheet legible at every palette. */
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  pointer-events: none;
}
/* Its geometry is written by canvas/mobile-frame.ts, clipped to the window, so
   there is nothing here to position - only what the sheet is made of. */
#mobile-board-frame {
  /* Middle: a white sheet still carrying enough of the palette to belong to
     the board. Softish and Harsh adjust only this share below. */
  --mobile-board-accent: 6%;
  display: none;
  position: absolute;
  box-sizing: border-box;
  border-radius: var(--radius);
  /* Opaque, and the surround above depends on it being so: the wash is painted
     across the whole window and this is what stops it at the board's edge. */
  background: color-mix(in srgb, var(--accent) var(--mobile-board-accent), #fff);
  pointer-events: none;
}
:root[data-whimsy="0"] #mobile-board-frame { --mobile-board-accent: 7%; }
:root[data-whimsy="2"] #mobile-board-frame { --mobile-board-accent: 4.5%; }
/* The 3:2 masthead standing on the board's top edge - canvas/mobile-frame.ts
   derives its height from the board width, so the same board has the same title
   page in short and tall windows. It travels with the board and scrolls away
   for good; the ceiling's wash is already behind it, and this adds no
   background of its own.
   Moved by transform from `top: 0`, not by a top that tracks the board's edge.
   Its size answers to the window and the board's column count and its position
   to every frame of a scroll, and only one of those is worth a layout - so the
   travel is a transform on a layer of its own and the box underneath it holds
   still. Small and bounded (the board width over 1.5), which is what makes the
   promotion safe; the sheet is not, and is cached instead.
   Inert to the pointer: a title page is not a control, and a finger that lands
   on it is a finger scrolling the board. */
#mobile-board-header {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  will-change: transform;
  align-items: center;
  justify-content: center;
  /* No side padding, and that is the wrap rule: the name breaks when it reaches
     the board's own edge and not a dozen pixels short of it. The 12px used to
     be a margin of comfort and read as a mistake instead - a word with room for
     it dropping to a second line while there was still visible board either
     side. Top and bottom keep theirs: that is the band's own breathing room
     above and below the title, not a limit on how wide it may be set. */
  padding: 24px 0;
  overflow: hidden;
  pointer-events: none;
}
:root[data-board-mode="mobile"] #mobile-surround { display: block; }
:root[data-board-mode="mobile"] #mobile-board-frame { display: block; }
:root[data-board-mode="mobile"] #mobile-board-header { display: flex; }
/* mbrd.perf.mobile({ chrome: false }). A dev switch, not a state the app can
   reach on its own - see mobilePerfFlags in canvas/viewport.ts, which is also
   what stops the writes that would otherwise still be happening behind this. */
:root.perf-no-mobile-chrome #mobile-surround,
:root.perf-no-mobile-chrome #mobile-board-frame,
:root.perf-no-mobile-chrome #mobile-board-header { display: none; }
/* Sized against the strip's own width rather than the window's, because the
   strip is what the name is set across - on a wide window in Mobile mode the
   board stays 384 world px and a vw-sized title would run out of it. Four
   lines is where it stops growing downwards; a name longer than that is a
   sentence, and it gets to be small. */
#mobile-board-title {
  margin: 0;
  /* A flex item whose display is -webkit-box does not get an automatic minimum
     size worth the name: left alone it sits at its max-content width and a
     two-word name runs off the side of the board rather than taking a second
     line. These two are what put the wrap back. */
  min-width: 0;
  max-width: 100%;
  /* The one live thing in the band: tapping the name renames the board - see
     editMobileTitle() in main.ts. Auto against the header's `none`, so a drag
     that crosses the name still pans the board. */
  pointer-events: auto;
  cursor: text;
  font-family: var(--font-display);
  /* --mobile-title-fit is the shrink that keeps a name inside the board: to one
     line when the wrap is off, to two lines when it is on. Nothing in CSS can
     ask "is this line wider than its container" or "how many lines did that
     take" - the wrap is the only fitting the language does - so fitTitle() in
     ui/mobile-header.ts measures it and writes the ratio here. Applied to the
     whole clamp rather than inside it, so the fit outranks the 20px floor: a
     name that has to go under twenty pixels to fit is still a name that has to
     fit.

     The ceiling is a fraction of the board, not the 96px it used to be. The size
     dial asks for a fraction (width x scale), and an absolute cap stopped the
     name growing while the board kept going - so the same setting gave one
     name-to-box ratio on a 416-wide phone and another on the 512 the Desktop
     card is normalised against, and one name broke in two different places on
     the two boards. --mobile-title-cap is handed down by ui/mobile-header.ts so
     the card's own arithmetic and this clamp cannot drift; see TITLE_CAP. */
  font-size: calc(clamp(20px,
    calc(var(--mobile-board-width, 0px) * var(--mobile-title-scale, 0.13)),
    calc(var(--mobile-board-width, 0px) * var(--mobile-title-cap, 0.1875))
  ) * var(--mobile-title-fit, 1));
  /* Font height is a vertical-only scale: it changes neither width nor wrapping.
     A hair below centre. Even growth is the middle of the *box*, and 50% is what
     that means - but the box is not the name: a line box reserves descender room
     whether or not the name has a descender in it, so the ink sits a little
     above the middle and a stretch about it reads as drifting down. Two percent
     is the correction, arrived at against a real title after 30%, 70% and 55%
     had each overdone it in their own direction. It is a correction for the line
     box, so it moves if the padding or the leading around it ever does. */
  /* translateY first so it is a plain shift of the whole name, unscaled by the
     Font height dial - the panel's Vertical position, expressed as a percentage
     of the band's own height (--mobile-header-height, published on the masthead
     itself and inherited here - it used to sit on #viewport, which meant every
     card on the board was invalidated to move a title) so the nudge keeps its
     proportion at any font size. 0 leaves
     the name where the band's own centring put it; the band's overflow clips
     whatever a large shift pushes past an edge. */
  transform:
    translateY(calc(var(--mobile-header-height, 0px) * var(--mobile-title-offset, 0) / 100))
    scaleY(var(--mobile-title-stretch, 1));
  transform-origin: 50% 52%;
  /* `normal` follows the active face's own ascent and descent instead of
     assuming every bundled or custom font fits one numeric line box. The
     padding is inside the clamped box, leaving its last line room for g/j/y. */
  line-height: normal;
  /* Room above, and more of it below, because the two edges are not asked the
     same question. `overflow: hidden` is here to serve -webkit-line-clamp and
     it clips at the padding edge, so whatever hangs below the last line box has
     exactly this much to hang into. A descender does hang below it: on a line
     box tightened by the Line height dial it hangs a long way below, and on
     `normal` it is already close. 0.2em was the whole of it and the tails of
     g, j and y were being cut square. In em, so it is the same room at every
     size the size dial reaches. */
  padding-block: 0.2em 0.45em;
  text-align: center;
  text-wrap: balance;
  overflow-wrap: anywhere;
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Under the caret: the same ring the rest of the app puts round the thing it is
   listening to, so a rename in progress looks like one. */
#mobile-board-title:focus { outline: none; }
#mobile-board-title[contenteditable="true"],
#mobile-board-title[contenteditable="plaintext-only"] {
  outline: var(--sel-line) solid var(--select);
  outline-offset: var(--sel-gap);
  border-radius: var(--radius-sm);
  /* And the whitespace stops collapsing while the caret is in here, which is
     what makes a space at the end of the name typeable. Under `normal` a
     trailing space is not rendered at all: the editor answers by inserting a
     non-breaking space so that something moves, the sanitizer turns that back
     into an ordinary one - it has to, a name is not stored with U+00A0 in it -
     and the ordinary one is invisible again. Press space at the end of a word
     and nothing whatever happens, three times over. pre-wrap renders the
     character the editor actually owns; the resting title is left on `normal`,
     because a name that has been committed has no trailing space to show. */
  white-space: pre-wrap;
}
/* Wrapping turned off - ui/mobile-header.ts sets the attribute from
   `settings.mobileHeader.wrap`.

   Three declarations, and none of them is optional. `pre` is the no-wrap that
   also keeps the spaces, and it is written here rather than inline so that it
   outranks the `pre-wrap` above by coming after it at equal specificity -
   inline it would have won even while the name was being renamed, and the
   trailing space would stop being typeable again.

   The other two are what keeps a name that no longer fits centred. Left alone
   the title is an ordinary flex item: `max-width: 100%` and the default
   shrink hold its box to the width of the band, so a line too long to fit
   starts at the left edge and runs off the right - which is what this looked
   like. Free of both it takes its content's own width, and the header's
   `justify-content: center` then centres a box that is wider than its
   container, so the overflow falls equally either side and the band's
   `overflow: hidden` trims the same amount off each end. The name stays on the
   board's middle, whatever the size dial does. */
#mobile-board-title[data-nowrap] {
  white-space: pre;
  flex: none;
  max-width: none;
}
/* A board nobody has named yet still gets its page - the space is reserved
   either way, and an empty band reads as something failing to load. The name
   it is given is just held more quietly than one somebody chose. */
#mobile-board-header[data-untitled] #mobile-board-title { opacity: 0.45; }

/* The Desktop title card: the board name as a movable board item, set in the
   same style as the Mobile masthead. ui/mobile-header.ts writes the identical
   --mobile-title-* custom properties onto .title-name that it writes onto
   #mobile-board-title; the one thing that differs is what the size is measured
   against - the strip's width there, this card's own width here (100cqw), so
   the size dial means the same "percentage of the width it is set across" in
   both. Bare on purpose: no paper, no border, no caption - see renderers.ts. */
/* The masthead's own rules, one for one, so the same style wraps the same on
   both boards - the only thing that differs is what the size is measured
   against: the strip's width there, this 3:2 card's own width (100cqw) here. So
   the name wraps and sits the same *relative to its box* on both, at the card's
   smaller absolute size. Every deviation from #mobile-board-title below was a
   way the two came out different: no side padding (the name breaks at the
   card's edge, not short of it), the same 20-96px clamp, and no overflow-wrap
   that would break where the masthead would not. */
.title-card {
  position: relative;
  /* The item's footprint can be taller than 3:2 once snapping rounds it up to
     whole cells; the card holds its exact 3:2 and the .item-body grid below
     parks it (top by default), so the extra height falls as slack around it. */
  width: 100%;
  aspect-ratio: 3 / 2;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The same paper and hairline outline every card wears - see .card. An inset
     shadow rather than a border so the item's corner clip does not shave it,
     plus the shared drop shadow: on the card itself, not an #item-shadows twin,
     because the twin would trace the taller item box and not this 3:2 card
     (canvas/items.ts skips the twin for the title). */
  border-radius: inherit;
  background: var(--paper-card);
  box-shadow: inset 0 0 0 var(--board-hairline) var(--item-border), var(--item-shadow);
  /* The size query the font-size reads. Content-box, so it is the card's width. */
  container-type: size;
  overflow: hidden;
}
/* The title's box is a grid of one stretched cell, so the 3:2 card can be parked
   against the top, middle or bottom of whatever height the item has. data-valign
   is written by canvas/items.ts from the item's meta and set by dragging the
   card on a snapped board (canvas/input.ts). Top by default: slack below. */
/* Every .item paints a card face (var(--item-bg)) across its whole box, which
   content normally covers edge to edge. The 3:2 title card covers only the top
   of its taller snapped box, so that face would show through the slack as a
   second card - the "undercard". The card carries its own paper, so the item box
   underneath it is transparent. */
.item[data-type="title"] {
  background: transparent;
  /* A size container, so the selection ring below can read the item box in cqw /
     cqh and pull its top and bottom in to the 3:2 card. The card's own
     container-type:size still governs its text (nearest container wins), so this
     only serves the ring. */
  container-type: size;
}
/* The selection ring hugs the 3:2 card, not the taller 4:3 item hitbox. The card
   is full item width, so left/right stay at the item edge; top and bottom pull
   in by the slack the centring leaves - half the difference between the item
   height (100cqh) and the card height (the card is 3:2, so 100cqw * 2/3, i.e.
   200cqw/3). The radius is untouched: card and item share --own-radius. */
.item[data-type="title"].is-selected::before,
.item[data-type="title"].is-stick-target::before {
  top: calc(50cqh - 100cqw / 3 - var(--sel-edge));
  bottom: calc(50cqh - 100cqw / 3 - var(--sel-edge));
  left: calc(-1 * var(--sel-edge));
  right: calc(-1 * var(--sel-edge));
}
/* The hover ring (.item:hover) traces the full item box too, so it would ring
   the slack as well as the card. Suppressed here - the card is what hover
   should read on, and its own paint carries the pointer feedback. */
.item[data-type="title"]:hover { box-shadow: none; }
.item[data-type="title"] .item-body {
  /* A flex column, not a grid: the card keeps its own 3:2 height and stays
     centred in whatever height the item has. A grid item defaults to
     align-self:stretch and would be pulled to the full box height, at which
     point the explicit height wins over aspect-ratio and the card comes out 4:3.
     In a column the card's main-axis size stays its content/aspect height
     (flex-basis auto, no grow), while align-items:stretch gives it the full
     width, and justify-content:center splits any snap slack evenly above and
     below. */
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  /* Not clipped, unlike a normal item-body: the card's drop shadow is a CSS
     box-shadow on .title-card (there is no shadow twin for the title), and the
     body's overflow:hidden would cut it into a hard-edged block pooling in the
     slack - the "undercard" look. The card is smaller than the body and rounds
     its own content, so nothing here needs the clip. */
  overflow: visible;
}
.title-name {
  margin: 0;
  /* min-width:0 puts the wrap back on a flex item that would otherwise sit at
     its max-content width; max-width holds it inside the card. Both mirror
     #mobile-board-title. */
  min-width: 0;
  max-width: 100%;
  text-align: center;
  font-family: var(--font-display);
  color: var(--ink);
  /* Deliberately NOT --display-weight, and this is the one display site that
     opts out. The board's name carries its own weight in board state
     (DEFAULT_MOBILE_HEADER.weight in board-model.ts, the Weight dial in
     ui/mobile-header.ts), written inline by applyTitleStyle() onto both this
     card and the masthead - so an inline 700 wins over anything declared here
     on every board that has ever been styled, which is all of them. The name is
     the same on Softish and on Middle by design: one number, chosen per board,
     that the whimsy axis is not allowed to move.
     700 to match the default the inline style is about to set, so the frame
     before it lands is not a different weight from the frame after. */
  font-weight: 700;
  /* A pure fraction of the card's own width - no screen-pixel clamp here. The
     masthead's 20-96px clamp is folded into --mobile-title-ratio by
     ui/mobile-header.ts (the masthead's effective font over the board width), so
     the card runs its text at exactly the masthead's ratio and wraps the same,
     at any zoom. Falls back to the raw size dial before the ratio is written. */
  font-size: calc(100cqw
    * var(--mobile-title-ratio, var(--mobile-title-scale, 0.13))
    * var(--mobile-title-fit, 1));
  line-height: normal;
  /* Font height is a vertical-only scale; the offset nudges the name up or down
     the card as a fraction of its height (100cqh), as the masthead does against
     its band height. */
  transform:
    translateY(calc(100cqh * var(--mobile-title-offset, 0) / 100))
    scaleY(var(--mobile-title-stretch, 1));
  transform-origin: 50% 52%;
  /* The masthead's wrapping, one for one, so the same name breaks the same on
     both boards. text-wrap:balance is what splits "New board Jul 29" into even
     rows ("New board" / "Jul 29") rather than the greedy "New board Jul" / "29";
     overflow-wrap:anywhere breaks a single word too long to fit rather than
     letting it run off the edge; the -webkit-box line clamp caps it at four rows
     and the padding-block gives the last row's descenders room, exactly as on
     #mobile-board-title. */
  text-wrap: balance;
  overflow-wrap: anywhere;
  padding-block: 0.2em 0.45em;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Not wrapping: one line - the Desktop echo of #mobile-board-title[data-nowrap].
   flex:none + max-width:none let an over-long line take its own width so the
   card's centring keeps it centred (and the card's overflow trims each end
   evenly), the same reasoning the masthead gives; the fit shrink in
   ui/mobile-header.ts then pulls it back inside the card so it does not clip. */
.title-name[data-nowrap] { white-space: pre; flex: none; max-width: none; }
/* A board nobody has named yet, held more quietly - as on the masthead. */
.title-name.is-untitled { opacity: 0.45; }
/* No foot strip: the title card is a name and nothing else. */
.item[data-type="title"] .item-bar { display: none; }

/* The title card's two pop-up buttons, stacked off the RIGHT edge of the card:
   the pen on top opens the shared style panel, the T below it drops into inline
   rename of the board name. canvas/items.ts builds both on the card and leaves
   them there; they show on hover or while the card is selected (the rule below).
   Sized and offset in --iz (= 1/zoom) so they hold a constant on-screen size and
   gap the way the resize grips do. */
.item-pen,
.item-rename {
  position: absolute;
  top: 50%;
  left: 100%;
  margin-left: calc(10px * var(--iz, 1));
  /* --icon-button, like the tour mark on the other corner of the same card and
     like every small icon button in the app. It was a flat 30 and the mark was a
     flat 22, which put two sizes of on-card control on one board with nothing
     choosing between them. */
  width: calc(var(--icon-button) * var(--iz, 1));
  height: calc(var(--icon-button) * var(--iz, 1));
  display: grid;
  place-items: center;
  padding: 0;
  border: var(--board-hairline) solid var(--rule-2);
  /* --leaf, scaled into world units like the rest of this button. It was a flat
     9px, which is within a pixel of --leaf at Middle and nowhere near it at
     either end - the one control on the board that stayed rounded when the
     Corner radius slider went to 0, and stayed tight when it went to 28. */
  border-radius: calc(var(--leaf) * var(--iz, 1));
  background: var(--paper-card);
  box-shadow: var(--shadow-1);
  color: var(--ink-2);
  cursor: pointer;
  z-index: 3;
  /* Hidden until the card is hovered or selected - see the reveal below. */
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease);
}
/* Straddle the card's mid-line: the pen sits half a button-and-gap above it, the
   T the same below, so the pair is centred on the card's right edge. Written as
   the arithmetic it is - half the button plus half the 6px gap - rather than as
   the 18 that arithmetic came to when the button was 30, so the pair stays
   centred and stays 6 apart if the token ever moves again. */
.item-pen { transform: translateY(calc(-50% - (var(--icon-button) + 6px) / 2 * var(--iz, 1))); }
.item-rename {
  transform: translateY(calc(-50% + (var(--icon-button) + 6px) / 2 * var(--iz, 1)));
  font-family: var(--font-display);
  font-weight: 700;
  font-size: calc(16px * var(--iz, 1));
  line-height: 1;
}
.item[data-type="title"]:hover .item-pen,
.item[data-type="title"]:hover .item-rename,
.item[data-type="title"].is-selected .item-pen,
.item[data-type="title"].is-selected .item-rename {
  opacity: 1;
  pointer-events: auto;
}
/* And the card comes to the front for as long as they are up, because the
   z-index the buttons carry cannot reach past the card that holds them. Every
   .item is written an inline z-index by paintStack() in canvas/items.ts - its
   rank in the visual stack - and a numeric z-index makes an element a stacking
   context, so `z-index: 3` on the pen orders it against the card's own children
   and against nothing else. The buttons sit ten pixels *outside* the card's
   right edge, which is exactly the strip a neighbouring card is free to occupy:
   any item ranked above the title card and overlapping that strip painted over
   them, and the fix has to move the card rather than the buttons.

   !important, which is the whole reason this works: the rank is an inline style
   rewritten on every restack, and a stylesheet cannot outrank an inline
   declaration any other way. This is the case the keyword is for.

   The number only has to beat the highest rank, and ranks are positions in a
   list - 0 to one less than the number of items on the board - so anything past
   a plausible board wins. It cannot leak upwards either: #world carries the
   pan/zoom transform and is therefore a stacking context of its own, so every
   item z-index is sealed inside it and none of this competes with the sidebar
   or the corner controls, whose numbers live in a different world entirely.

   One thing it does cost, recorded rather than fixed: visualStackOrder() lifts
   every note above every non-note as a band, so a sticky note stuck to the
   title card normally draws over it. While the card is raised it does not - the
   note goes behind for as long as the pointer is on the card. The card is a
   legal stick target, so this is reachable; it lasts exactly as long as the
   hover, and the alternative is buttons that cannot be clicked.

   Gated on the same zoom rung that reveals the buttons, so the raise exists
   when they do and not otherwise - a card that jumped forward on hover while
   showing nothing would be a stacking change with no visible cause. */
#world:not(.zoom-far) .item[data-type="title"]:hover,
#world:not(.zoom-far) .item[data-type="title"].is-selected { z-index: 100000 !important; }
/* A transparent bridge across the gap to the buttons. Without it the pointer
   crosses dead space between the card's edge and the buttons, .item:hover drops,
   and they vanish before they can be reached. A pseudo-element of the card, so
   hovering it still counts as hovering the card - it spans the card's WHOLE
   height (top:0/bottom:0 on the item box) and the full width out to past the
   buttons, so you can strike out toward them from anywhere down the card's right
   side, not just from the strip level with them. Inert until the buttons are
   revealed, so it never eats a press on a resting card; it sits entirely to the
   right of the card (left:100%), so it never covers the card's own content. */
.item[data-type="title"]::after {
  content: '';
  position: absolute;
  left: 100%;
  top: 0;
  bottom: 0;
  width: calc(56px * var(--iz, 1));
  pointer-events: none;
  z-index: 1;
}
#world:not(.zoom-far) .item[data-type="title"]:hover::after,
#world:not(.zoom-far) .item[data-type="title"].is-selected::after { pointer-events: auto; }
@media (hover: hover) {
  .item-pen:hover,
  .item-rename:hover { background: var(--paper-2); color: var(--accent-deep); border-color: var(--accent-text); }
}
.item-pen:focus-visible,
.item-rename:focus-visible { background: var(--paper-2); color: var(--accent-deep); border-color: var(--accent-text); }
.item-pen svg { width: calc(var(--icon-glyph) * var(--iz, 1)); height: calc(var(--icon-glyph) * var(--iz, 1)); }
/* Gone from the chrome rung (40%) down, with the anchor and pin badges
   (item-chrome.css): that far out they are clutter over a card too small to
   edit. The raise and the bridge above use the same rung. */
#world.zoom-far .item-pen,
#world.zoom-far .item-rename { display: none; }
/* Inline rename in progress: the caret's ring, and pre-wrap so a trailing space
   is typeable - the same treatment #mobile-board-title[contenteditable] gets. */
.title-name[contenteditable="true"],
.title-name[contenteditable="plaintext-only"] {
  outline: var(--sel-line) solid var(--select);
  outline-offset: var(--sel-gap);
  border-radius: var(--radius-sm);
  white-space: pre-wrap;
}

:root[data-board-mode="mobile"] #axis-ink,
:root[data-board-mode="mobile"] #origin-mark,
:root[data-board-mode="mobile"] #web { display: none !important; }
/* These belong to the Desktop board, not to a desktop-sized screen. Keeping
   the rule outside a width query also hides them when Mobile is selected in a
   wide browser, while a narrow screen showing the Desktop board keeps them. */
:root[data-board-mode="mobile"] #zoom-ctl,
:root[data-board-mode="mobile"] #scale-bar { display: none; }

/* The origin, marked like a compass rose rather than just a crossing. Drawn a
   shade stronger than the axes so (0,0) reads as a place, not an accident. */
/* Placed by a transform - viewport.ts writes it, and says there why left/top
   and a negative margin could not do this job. Both stay at zero so the box
   the browser lays out sits at the viewport's own corner, on the device grid,
   with nothing to snap; the transform carries the whole journey, centring
   included. */
#origin-mark {
  position: absolute;
  left: 0;
  top: 0;
  width: 36px;
  height: 36px;
  color: var(--accent-text);
  opacity: 0.7;
  pointer-events: none;
}
#viewport.no-axes #origin-mark { display: none; }

#marquee {
  position: absolute;
  border: 1.5px dashed var(--select);
  background: var(--select-fill);
  border-radius: var(--radius-sm);
  pointer-events: none;
  z-index: 5;
}

/* The lines between items (canvas/web.ts). Drawn by hand with the Join tool
   rather than worked out from the board - the id and the class kept the names
   they had, for the reason that module's header gives.

   Inside #world, so pan and zoom are the layer's transform rather than
   anything this has to redraw.
   It sits below the shared item-shadow underlay; #world paints no background
   of its own, so there is nothing for either backdrop to disappear behind. */
#web {
  position: absolute;
  overflow: visible;
  pointer-events: none;
  z-index: -2;
}
/* Two kinds of element, one appearance. The bulk <path> carries every settled
   line as subpaths of a single `d`, which is what keeps a redraw to one
   attribute write on a board with hundreds of them. A line only gets its own
   <path> for as long as it is fading, because opacity cannot be applied to a
   subpath - and it is handed back to the bulk one the moment it lands.

   stroke-linejoin, which a straight segment never needed: a routed line bends
   around the cards in its way, and a mitre on a right angle at a non-scaling
   stroke width draws a small spur at every corner. */
#web path,
#web path.thread {
  fill: none;
  stroke: var(--web-line);
  stroke-width: var(--web-weight);
  stroke-linecap: round;
  stroke-linejoin: round;
  /* A thread, not a beam: without this the stroke would be multiplied by the
     zoom along with everything else, and reach ten pixels wide at 8x. */
  vector-effect: non-scaling-stroke;
}
/* A pair the router cannot join cleanly draws nothing routed - instead a single
   straight red dashed line runs card-to-card (web.ts, .web-blocked). It replaces
   the old "?" badge: the failure is shown in place, on the board, between the two
   cards that wanted to meet. The dash and width do not scale (non-scaling-stroke
   above), so the marks stay the same size at every zoom. */
#web .web-blocked {
  stroke: var(--danger);
  stroke-dasharray: 6 6;
  stroke-linecap: butt;
}
/* A styled connection - direction, dash or label - drawn in its own layer. It
   inherits the stroke rule above (it is a #web path too); a dash is set per
   line in web.ts, and the arrowhead is a shade firmer than the faint line so a
   direction reads at a glance. */
/* The arrowhead follows its own line's colour where the engine can tell it
   which line that is. One marker is shared by every connection on the board, so
   the only way it can be two colours at once is context-stroke - and the two
   declarations are the fallback: an engine that does not know the keyword drops
   the second and keeps the grey, which leaves a coloured line with a grey head
   rather than no head at all. Safari is the floor here (see
   research/docs/browser-support.md), and this is a difference in shade on an
   arrow, not a feature that fails to work. */
#web .web-arrowhead {
  fill: var(--web-line);
  fill: context-stroke;
  stroke: none;
}

/* ---- what a line can be told to mean ----

   A connection carries a colour and a weight by *name* (see connMeta() in
   board-model.ts), and this is where a name becomes a value. One rule each, no
   arithmetic on a stored string, and nothing here that a file could reach into:
   a name that matches no rule below draws the ordinary line, which is the right
   answer to a board written by a newer version of the app.

   These are on .web-styled alone. A coloured line has meta, and a line with
   meta has already left the bulk path for its own element in the decoration
   layer - the bulk path is one shared stroke and can only be one colour. */
#web .web-c-accent { stroke: var(--accent-text); }
#web .web-c-warm   { stroke: var(--accent-warm); }
/* --leafy, the olive pigment. This read var(--leaf) - the *control corner*, a
   length - so the declaration was invalid and dropped, and a line set to Leaf
   came out whatever the bulk stroke already was. One letter, and the two tokens
   are unrelated: --leaf is 10px, --leafy is #66842b. */
#web .web-c-leaf   { stroke: var(--leafy); }
#web .web-c-danger { stroke: var(--danger); }
/* Weight is relative to the board's own, so a bold line stays bold when the
   whimsy axis or a hand-set token moves --web-weight underneath it. */
#web .web-w-fine { stroke-width: calc(var(--web-weight) * 0.7); }
#web .web-w-bold { stroke-width: calc(var(--web-weight) * 2.2); }
#web .web-label {
  fill: var(--ink);
  /* A paper halo so a label stays legible where it crosses a card or a line.
     Non-scaling so the halo is a hairline at every zoom rather than a slab. */
  stroke: var(--paper);
  stroke-width: 3px;
  paint-order: stroke;
  vector-effect: non-scaling-stroke;
  /* 12px is not a step on the scale (--t-small is 13, --t-tiny 11); it is a
     world-space label that gets scaled by the board transform, so it was sized
     by eye against the lines rather than against the panel type. Left as a
     literal deliberately - the face follows the axis, the size does not. */
  font: 500 12px var(--font-body);
  dominant-baseline: middle;
  -webkit-user-select: none;
  user-select: none;
}
/* The line under the pointer, so a connection reads as a thing you can click and
   edit rather than a drawn-on backdrop.

   A halo under the line, like the mark below and for the same reason - this one
   used to be an opaque accent stroke drawn over the top, which meant pointing
   at a dotted line to click it was the moment it stopped looking dotted. Half
   the mark's strength and a little narrower, so the two are legible together:
   pointing at the line you have already marked is the commonest way to have
   both on screen at once. */
#web .web-hover {
  stroke: var(--accent-text);
  stroke-width: calc(var(--web-weight) + 6px);
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  opacity: 0.18;
  pointer-events: none;
}

/* One card's own threads, lifted out of a board full of them.

   The lift is done by dropping everything else rather than by brightening the
   few: the lines are already at the weight they are meant to read at, and a
   board that got *louder* when you selected something would be the wrong way
   round. canvas/web.ts draws the focused threads a second time into .web-focus
   over the dimmed bulk path, which is why this rule can dim the bulk path
   wholesale without dimming what it is trying to show.

   .web-lit is the styled lines' half of the same idea - those are their own
   elements, so they are lifted by being left alone.

   The fading threads are deliberately not in here. Each carries an inline
   opacity that is the fade itself, so a rule aimed at them would either lose to
   it or fight it; a line arriving or leaving is on screen for a third of a
   second, and it settles into the bulk path this rule does reach. */
#web .web-bulk,
#web .web-styled { transition: opacity var(--dur-fast) var(--ease); }
#web.is-focused .web-bulk,
#web.is-focused .web-styled:not(.web-lit) { opacity: 0.18; }

/* The lifted copy. No stroke of its own: it inherits the same rule the bulk
   path takes, so a focused thread and an unfocused one are the same line at two
   strengths rather than two lines. */
#web .web-focus { opacity: 1; }

/* The world axes go with the rest of the board's lines.

   The lift above drops every line that is not this card's, and the axes were
   the exception - not because anybody decided they should be, but because they
   are a canvas in screen space outside #world and no selector aimed at #web
   could reach them. So a board that dimmed forty threads to show you three kept
   two full-strength rules ruled straight through the picture, which is the lift
   half-applied.

   Reached by a flag on the root instead, written by canvas/web.ts on the same
   line as .is-focused - the arrangement data-snap and .is-connecting already
   use, and the only one available across two stacking contexts.

   Not as far down as the threads. 0.18 on a line that reads as structure rather
   than as content is the axis effectively gone, and the axes are what the whole
   board is positioned against - somebody looking at one card's connections has
   not stopped needing to know where the origin is. Far enough back to stop
   competing, near enough to still be there.

   The same duration and easing as the threads, deliberately: two things fading
   together at two speeds read as two events. */
#axis-ink { transition: opacity var(--dur-fast) var(--ease); }
:root.web-lifted #axis-ink { opacity: 0.4; }

/* The line a press landed on, which stays lit after the pointer has moved on -
   the hover says "this is a thing", this says "this is the thing", and Delete
   means it.

   A halo *under* the line rather than a stroke over it, and that is the whole
   design of it. Drawn on top - which is what this was at first - it is an
   opaque accent-coloured line four pixels wide, and a marked connection stops
   showing whether it is dashed or dotted or what colour it was given at the
   exact moment somebody has selected it to change those things. Underneath, the
   line keeps its own dash, its own colour and its own weight, and reads against
   the halo instead of against the board. web.ts appends it first for this
   reason; the accent is the same one the hover uses, because they are two
   states of one thing and not two kinds of line. */
#web .web-active {
  stroke: var(--accent-text);
  stroke-width: calc(var(--web-weight) + 9px);
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  opacity: 0.34;
  pointer-events: none;
}

/* ---------------------------------------------------------
   Drawing one (ui/toolbar.ts)
   --------------------------------------------------------- */

/* The board while the Join tool is armed. This is the app's only mode, so it
   says so where the hand is rather than only where the button is: the button at
   the top of the screen is pressed, and the whole board under the cursor is the
   tool's own mark. Somebody who armed the tool and then looked away for a minute
   should be able to tell from the board alone.

   It was a crosshair, and a crosshair was not enough - the first tester outside
   this machine asked for "custom cursor na join" while looking straight at one.
   That is the whole argument for the picture below: `crosshair` is the browser's
   generic mode cursor, it means precision rather than joining, and a reader who
   has seen it on a hundred canvases reads it as nothing in particular.

   Two ends and a line between them, drawn twice - a white pass at 5px under a
   dark pass at 2.2px - because a cursor is the one graphic in the app that has
   no control over what it lands on. The halo is what keeps it legible over a
   dark photograph and over bare paper alike, at every whimsy tier, without the
   sheet knowing which it is over. Hotspot at the centre, matching the crosshair
   it replaces, and `crosshair` stays as the fallback for anything that will not
   take a URL cursor.

   A data: URL rather than a file in assets/img: it is 400 bytes, it is part of
   the rule that uses it, and a cursor that lives in the offline shell as a
   separate fetch is a cursor that can be missing at the exact moment the mode
   turns on. img-src carries `data:` already, for the same reason and for the
   glyphs in items.css - see web/_headers.

   The cursor is written on the viewport rather than on the cards, so empty
   canvas carries it too - clicking nothing is a real move in this mode (it
   drops the pick), and a card-only cursor would say the gaps were inert. */
:root.is-connecting #viewport,
:root.is-connecting .item {
  cursor:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg fill='none' stroke='%23fff' stroke-width='5' stroke-linecap='round'%3E%3Cpath d='M7 17 17 7'/%3E%3C/g%3E%3Ccircle cx='5' cy='19' r='4' fill='%23fff'/%3E%3Ccircle cx='19' cy='5' r='4' fill='%23fff'/%3E%3Cpath d='M7 17 17 7' fill='none' stroke='%23111' stroke-width='2.2' stroke-linecap='round'/%3E%3Ccircle cx='5' cy='19' r='2.4' fill='%23111'/%3E%3Ccircle cx='19' cy='5' r='2.4' fill='%23111'/%3E%3C/svg%3E") 12 12,
    crosshair;
}

/* The board while a finger is collecting cards - the second mode, and the one
   that had to say so somewhere a cursor cannot. It is a touch mode by
   construction (the row that turns it on is drawn only on a menu a finger
   opened), so a crosshair would be a sign painted for a pointer that is not
   there and the sign has to be the board itself.

   An inset line at the window's edge rather than anything on the cards: what is
   selected is already drawn on the cards, and a second mark over them would be
   two things saying one thing while hiding the pictures underneath. It is drawn
   *round the whole surface* because that is the scope of what changed - every
   tap on this board now means something else.

   pointer-events stay off it: this is a mark, and a mark that swallowed the
   first tap along the edge of the screen would be a mode that fights the very
   gesture it exists for.

   On #viewport rather than on #world, which is the one thing here that is not a
   preference: #world carries the pan and the zoom, so a frame drawn on it would
   travel with the board and be off screen by the second card. The viewport is
   fixed to the window and `contain: paint` seals both the clip and the z-index
   inside it, so this sits over the cards and under every panel without naming
   any of them. */
:root.is-picking #viewport::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  border: 2px solid var(--select);
}

/* The first end, once one is chosen. canvas/items.ts writes data-pick, and it
   writes it in build() as well as on the live node - a card culled while it is
   picked is thrown away and rebuilt, and a mark applied only to the node would
   go with it.

   A ring rather than a wash, and drawn outside the card rather than over it: it
   has to read on a photograph as clearly as on paper, and half a second later
   this card is going to have a line coming out of it. Outline, not a border or
   a shadow - it does not affect layout, it follows the card's own radius, and
   it survives the overflow clip .item applies to everything inside itself. */
.item[data-pick] {
  outline: 2px solid var(--accent-text);
  outline-offset: 3px;
  border-radius: inherit;
}

/* The card the draft line is currently aimed at. The same ring as the pick and
   deliberately quieter - dashed and one pixel thinner - because the two are on
   screen at the same time and they are not the same claim: one end has been
   chosen, the other is only where the pointer happens to be. Written on the
   live node alone by canvas/items.ts/setConnectAim; unlike the pick it does not
   survive a rebuild, and should not - a card that has been culled is not under
   the pointer. */
.item[data-aim] {
  outline: 1px dashed var(--accent-text);
  outline-offset: 3px;
  border-radius: inherit;
}

/* The line in flight, from the picked card to the pointer.
   Dashed and in the accent so it never reads as a connection that exists: a
   drawn line is grey and solid, and the difference between "this is a line" and
   "this would be a line" has to be visible at a glance without a legend.
   Above everything else in #web and, like the rest of it, untouchable - the
   pointer it is following must never land on it. */
#web .web-draft {
  stroke: var(--accent-text);
  stroke-width: calc(var(--web-weight) + 0.6px);
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  opacity: 0.85;
  pointer-events: none;
}

/* Without color-mix(), for WebKit below 16.2 - section 7 of tokens.css carries
   the argument and declares the channel-form tokens.

   The board frame is the one rule in this file that cannot be written here. It
   is a blend, its share is a variable the whimsy axis moves, and --accent may be
   a pigment ui/pigments.ts pulled out of a photograph a second ago - so it is
   computed in script instead, by legacyBlends() in ui/appearance.ts, which
   reads --mobile-board-accent off the root and so follows the axis for free.

   The literal in the var() fallback is not decoration. This frame is opaque on
   purpose - the surround above is painted across the whole window and the frame
   is what stops it at the board's edge - so it must not resolve to nothing if
   the script has not run yet or has not run at all. White is 94% of the answer
   at every tier, which makes it the right thing to be wrong with. */
@supports not (color: color-mix(in srgb, red, blue)) {
  #mobile-surround { background: rgb(var(--ink-c) / 6%); }
  #mobile-board-frame { background: var(--lc-mobile-board, #fff); }
}

/* The card at the edge of the board - see canvas/farlands.ts, which places it
   and argues why it is not an item.

   Positioned by canvas/farlands.ts the same way items.ts positions a card:
   absolutely, inside #world, at left = x - w/2 and top = -y - h/2. Only the
   four geometry properties are set from script; everything it looks like is
   here, so the whimsy axis and the palette reach it like they reach anything
   else on the paper.

   Inert in every sense the board cares about. It is not selectable, not a drop
   target and not a hit-test participant, so a marquee dragged across it catches
   nothing and a click goes through to the paper underneath - which is what
   keeps it furniture rather than a card somebody has to deal with. */
#far-lands {
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.75em;
  box-sizing: border-box;
  padding: 2.5em;
  background: var(--paper-card);
  border: 1px solid var(--rule);
  border-radius: 3px;
  box-shadow: var(--item-shadow);
  color: var(--ink-2);
  font-size: var(--t-body);
  line-height: 1.5;
  text-wrap: pretty;
  pointer-events: none;
  user-select: none;
}

#far-lands p { margin: 0; }

/* The first line is the one worth reading; the second is the footnote that
   explains it. Stated as a weight and a colour rather than a size, because two
   type sizes inside a card this small reads as a heading and a body, and it is
   neither - it is one remark in two breaths. */
#far-lands p:first-child {
  color: var(--ink);
  font-weight: 600;
}
