/* =========================================================
   Cards whose face the app draws: notes, links, stickers, models, embeds.
   Loaded after items.css — per-[data-type] corrections to .item / .card.
   Some class names are built by interpolation (note-* , ntb-ico-*) — do not
   delete selectors just because no grep finds them.
   ========================================================= */

/* Notes: the one item type born on the board, so it is drawn as a sticky
   rather than as a filed card - a square of coloured pad paper, cut corners,
   pressed flat to the board.
   The tint comes from data-tint (1-4, cycled at creation and stored in
   meta.tint), so a wall of notes reads as a pack rather than a monotone. */
.item[data-type="note"] {
  --item-shadow: var(--note-shadow);
  --shape-radius: var(--note-radius);
  --note-paper: var(--note-1);
  /* The sticky's furniture is a fraction of the sticky. Its type is not.
     A note was 240px square and its furniture was written as the fixed numbers
     that suited a note that size - 20px of margin, a 30px adhesive band. Halve
     the square and neither halves with it: the margins take most of the sheet
     and there is nowhere left to write. So the margin and the band are shares
     of the note, and a small note is a small note rather than a big note's
     fittings crammed into a smaller square.

     **The type is a size, not a share.** It was a share too, and that made
     resizing a note a zoom: the same paragraph on a 400px note and on a 160px
     one were not the same words at two lengths, they were two different type
     sizes, and dragging the corner slid between them. Words on a board have a
     size the way words on paper do - the sheet you write them on is not what
     decides it. Pulling a note about now rewraps what it says and moves
     nothing else, which is what a bigger sheet is for. The size is the user's
     to set, on the note, with A- and A+ (--note-scale) - see the note over the
     three kinds below.

     That also closes a loop the old ramp could not get out of. Both halves of
     the fitting contract in canvas/notes.ts - the note grows to its text, the
     resize floor stops at the height its text needs - are a *measurement* of
     the wrapped column, and a ramp keyed to the box changed the wrapping
     whenever the measurement changed the box. Growing a landscape note by a
     line raised min(w, h), which grew the type, which needed another line.
     Every number below is now keyed to the width alone (--half-w, w / 2, from
     canvas/items.ts/placeBox) or to nothing at all, so the height a note is
     given can never alter what fits in it.

     Calibrated on the 240px default, where --half-w is the 120 the old
     --half-min was: a default note resolves to exactly the numbers it was
     drawn with. Notes wider than tall change, and grow on the next pass of
     growNote() if their text now needs the room. */
  --note-half:    var(--half-w, 120px);
  --note-pad:     clamp(9px,  calc(var(--note-half) * 0.167), 20px);
  --note-band:    clamp(12px, calc(var(--note-half) * 0.25),  30px);
  /* Tokens rather than numbers, so the ramp still moves with the whimsy tier
     the way every other size on a board does. --t-title and not --t-display:
     the display step is the largest thing on a board and a heading on a 120px
     sticky is not that. A note is small, and a title that takes a third of the
     sheet before the first word of the body is a title nobody chose - the old
     box-keyed ramp reached --t-display only on notes far bigger than the one
     anybody actually writes on. Both well under what they were (the title
     was the whole --t-title, the body 16px): a 21px title on a sticky read as
     shouting, and the composer, which shows the note magnified, set the
     proportion the board now matches. */
  --note-t-title: calc(var(--t-title) * 0.76);
  --note-t-body:  13px;
  /* The item under the card takes the pad colour too. Two rounded boxes of the
     same radius stacked exactly on top of each other still leave a hairline of
     the lower one showing along the curve, where the antialiasing of the two
     edges does not quite agree - and a near-white hairline around a coloured
     sticky reads as a printing misregistration. */
  background: var(--note-paper);
}
.item[data-type="note"][data-tint="2"] { --note-paper: var(--note-2); }
.item[data-type="note"][data-tint="3"] { --note-paper: var(--note-3); }
.item[data-type="note"][data-tint="4"] { --note-paper: var(--note-4); }

.item[data-type="note"] .card {
  padding: var(--note-pad);
  /* The band at the head is the adhesive strip reading through the sheet. It
     is a fixed share of the sheet rather than a fixed depth, because a strip
     of glue is as wide as the pad it came off. */
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 6%, transparent) 0,
      transparent var(--note-band)),
    var(--note-sheet, var(--note-paper));
  /* No ruled border - pad paper has no printed edge. Just enough of an inset
     line to keep a pale note off a pale board. */
  box-shadow: inset 0 0 0 var(--board-hairline) color-mix(in srgb, var(--ink) 7%, transparent);
}
/* The filed-card inner rule has no business on a sticky. */
.item[data-type="note"] .card::after { content: none; }

/* "No card" on a sticky - the same promise it makes on a cut-out, which is that
   the paper goes and what was on it stays. Here the words stay: same face, same
   size, same alignment, sitting on the board the way something written straight
   onto it would.

   Said here rather than beside the other [data-bare] rules in items.css,
   because what it has to cancel is written here and nowhere else - the pad
   colour on the item, and the band, the paper and the inset line on the card.
   `.item[data-bare]` and `.item[data-type="note"]` have the same weight, so a
   cancel written in the other file would win or lose on which sheet loaded
   last, which is not a thing to leave to the <link> order.

   The band is the part worth naming. It is the adhesive strip reading through
   the sheet, so a note with no sheet must not keep it - a gradient with no
   paper under it is a smear across the top of the text with nothing to stick
   to, and it survives `background: none` on the item alone because it is
   painted on .card.

   The item's own box-shadow is deliberately untouched, exactly as items.css
   leaves the selection ring alone: the drop shadow is --item-shadow and the
   shadow twin is left out of #item-shadows for anything bare (noTwin() in
   canvas/items.ts, reconciled on every edit by syncTwin() beside it), while the
   ring is the app answering a click and an item you have selected has to show
   you what you have got. */
.item[data-type="note"][data-bare] { background: none; }
.item[data-type="note"][data-bare] .card {
  background: none;
  box-shadow: none;
}

/* "Low opacity" on a sticky - the row that took "No card"'s place on notes.
   The paper goes to 60% and the words stay solid, so the board shows through
   the sheet and the note still reads. The pointer, or an edit, brings the
   paper back to full. The shadow twin goes (noTwin() in canvas/items.ts),
   because it would show through the sheet as a grey block.

   The sheet is --note-sheet, the colour the card's paper reads here and at
   Harsh (quality.css), so one declaration fades every tier. A colour and not
   a background on the card, so the adhesive band over it stays and the change
   can fade. The pad colour on the item goes, or the two layers would stack to
   more than 60%. Not on a note with no paper, which the rules above have
   already emptied. */
.item[data-type="note"][data-faded]:not([data-bare]) {
  --note-sheet: color-mix(in srgb, var(--note-paper) 60%, transparent);
  background: none;
}
.item[data-type="note"][data-faded]:not([data-bare]):is(:hover, .is-editing) {
  --note-sheet: var(--note-paper);
}
.item[data-type="note"][data-faded]:not([data-bare]) .card {
  transition: background-color var(--dur-fast) var(--ease);
}
/* Zoomed out past the chrome rung the body is down (item-chrome.css) and the
   item is the whole note, so it takes the pad colour back. With none, a faded
   note that far out was a name floating on the board. */
#world.zoom-far .item[data-type="note"][data-faded]:not([data-bare]) {
  background: var(--note-paper);
}

/* A note is a column of formatted block-lines (renderers.ts/note). The column
   carries the note's font, its size (through --note-scale) and its vertical
   placement (justify-content); each line carries only its own kind and its
   alignment. font-family is written inline by applyNoteStyle from an allowlist,
   so the rule here is just the fallback before that runs.

   The gap between blocks belongs to the type and is written here rather than
   with the sheet's other measures above, for a reason that is easy to trip
   over: --note-scale is set *on this element* by applyNoteStyle, and a custom
   property is substituted where it is declared. A `--note-gap` declared on the
   item would read the scale off the item, where it is not set, and inherit the
   answer down already multiplied by 1. The 8px is the old box-keyed 9px gap at
   the 240px default, put on the spacing rhythm (tests/spacing.test.js). */
.item[data-type="note"] .note-rich {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: calc(8px * var(--note-scale, 1));
  overflow: hidden;
  font-family: var(--font-display);
  color: var(--ink);
  white-space: pre-wrap;
  word-break: break-word;
}
.item[data-type="note"] .note-rich[data-valign="top"] { justify-content: flex-start; }
.item[data-type="note"] .note-rich[data-valign="middle"] { justify-content: center; }
.item[data-type="note"] .note-rich[data-valign="bottom"] { justify-content: flex-end; }

/* Lines and the runs in them. A line is note-h1 when every run in it is a
   title and note-p otherwise, and what that sets is only the line's own strut
   - the smallest line box it can have - so an empty line keeps the height of
   what it will be typed in. The words are sized by their run.

   The two kinds, and --run-scale, default 1, is the only thing that sizes
   them: the run's own setting, off A- and A+ in the editor, applied to the two
   fixed steps of the ramp above. Nothing about the box appears here, which is
   the whole of what makes resizing a note a rewrap rather than a zoom. */
.item[data-type="note"] .note-line { line-height: 1.4; }
.item[data-type="note"] .note-line.note-h1 {
  line-height: 1.2;
  font-size: calc(var(--note-t-title) * var(--note-scale, 1));
}
.item[data-type="note"] .note-line.note-p {
  font-size: calc(var(--note-t-body) * var(--note-scale, 1));
}
.item[data-type="note"] .note-run {
  color: var(--ink-2);
  font-size: calc(var(--note-t-body) * var(--run-scale, 1));
}
.item[data-type="note"] .note-run[data-title] {
  font-weight: calc(var(--display-weight) + 300);
  color: var(--ink);
  font-size: calc(var(--note-t-title) * var(--run-scale, 1));
}
.item[data-type="note"] .note-line.al-left { text-align: left; }
.item[data-type="note"] .note-line.al-center { text-align: center; }
.item[data-type="note"] .note-line.al-right { text-align: right; }

/* ── The marker ──────────────────────────────────────────────────────────────
   Words drawn over with a highlighter, set from the toolbar. `wash` on a run
   (canvas/note-model.ts), which writes the name onto the run as data-wash and
   never anything else - the four names below are the allowlist and the
   attribute is the only way a colour out of a file reaches the page.

   The mapping is deliberately unscoped. Two elements that have nothing else to
   do with each other carry this attribute - the run on the sheet, and the chip
   on the toolbar button that says which colour is set - and the whole reason
   the wash is an attribute rather than a class is that both of them then read
   one name through one rule. Scoping it would mean writing the four colours
   twice and finding out later that only one copy was updated.

   --note-wash, and the name is the fix. This block wrote `--wash`, which
   tokens.css has owned since long before as the whimsy *ornament multiplier* -
   `--wash: 1 / 1.4 / 0` at :root and per tier, consumed by base.css as
   `calc(3% * var(--wash))`. Two meanings, one inherited name, and both of them
   wrong somewhere:

     - the chip's `var(--wash, transparent)` never took its fallback, because
       `--wash: 1` is always inherited from :root. It read empty only because
       `background: 1` is invalid at computed-value time, which is not the same
       thing and is not what the comment beside it claimed.
     - `--wash` is in the look allowlist (ui/look.ts), so a .mbrd carrying
       `{"--wash": "red"}` - an allowed token, and `red` passes the value
       grammar - painted every unmarked marker chip red.

   The newer of the two moves, which is this one: `--wash` is in saved files and
   in the allowlist and the note colour is neither. --note-wash also matches the
   four sheet tokens it is built from. */
[data-wash="amber"]      { --note-wash: var(--note-wash-amber); }
[data-wash="terracotta"] { --note-wash: var(--note-wash-terracotta); }
[data-wash="olive"]      { --note-wash: var(--note-wash-olive); }
[data-wash="graphite"]   { --note-wash: var(--note-wash-graphite); }

/* Around the words and not across the column, which is the difference between a
   marker and a ruled band - an inline run is exactly as wide as its words, and
   box-decoration-break draws each wrapped piece as a stroke of its own.

   The negative margins are exactly the padding, so marking words moves them
   not at all: the stroke overshoots the text a little either side, where a
   hand-drawn one goes, and the line under the caret does not jump sideways as
   the colour is chosen. */
.item[data-type="note"] .note-run[data-wash] {
  background: var(--note-wash);
  padding-inline: 0.16em;
  margin-inline: -0.16em;
  border-radius: 0.18em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* Sprite icons in the note bar (title and text) - a size up from the menu's,
   as the bar's letter glyphs are sized to the button rather than the ramp. */
.note-toolbar .ntb-btn .ico { width: 18px; height: 18px; }

/* The formatting toolbar floats above the note while it is edited. Outside .card
   so the card's clip cannot cut it, and scaled by --iz like the rest of the item
   chrome so it holds its size on screen at any zoom. */
.note-toolbar {
  /* Lives in screen space (#viewport), never in the item: positioned by JS for
     the desktop float (.is-float) and pinned by CSS on mobile (.is-mobile), so
     it is never scaled by zoom nor clipped by the item box. */
  position: fixed;
  display: flex;
  gap: 8px;
  padding: 8px 10px;
  /* --radius, the corner of every panel with controls in it. It read
     var(--radius-md, 14px) for a long time and --radius-md has never existed,
     so every board at every tier got the 14px fallback - a rounded bar floating
     over square cards on the plain tier, and a tight one over 26px cards on the
     soft. The fallback was near enough to the middle tier's panel corner that
     nobody saw it, which is exactly why it lasted. */
  border-radius: var(--radius);
  background: var(--paper-card);
  box-shadow:
    0 3px 10px color-mix(in srgb, var(--ink) 16%, transparent),
    inset 0 0 0 var(--hairline) var(--item-border);
  white-space: nowrap;
  z-index: 6;
  font-family: var(--font-body);
  pointer-events: auto;
  /* Its own compositor layer, so the note's growing shadow underneath cannot
     bleed into the bar's and read as the bar's shadow deepening on each press. */
  isolation: isolate;
}
/* Desktop float: JS sets left/top (top-left the anchor point); centre on it. */
.note-toolbar.is-float { transform: translateX(-50%); transform-origin: 50% 0; }

.note-toolbar .ntb-group { display: flex; gap: 2px; align-items: center; }
.note-toolbar .ntb-group + .ntb-group {
  margin-left: 5px;
  padding-left: 10px;
  border-left: var(--hairline) solid color-mix(in srgb, var(--ink) 12%, transparent);
}
.note-toolbar .ntb-btn {
  min-width: 36px;
  height: 36px;
  padding: 0 8px;
  border: 0;
  /* The bar's corner less the bar's 8px padding, so a button in the bar's
     corner runs parallel to it: 5px at the middle, and a full pill at Softish,
     where the 36px button caps the 18px. --leaf left it rounder than the bar
     around it. */
  border-radius: max(0px, calc(var(--radius) - 8px));
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  /* A glyph sized to its own button rather than to the type scale, and off it on
     purpose: this is a character standing in for an icon in a 36px box, so it is
     drawn against the box the way an icon would be. --t-* steps would make it
     wander inside a control whose size does not move with them. The same holds
     for the select beside it, for the library's two round buttons and for the
     sidebar's icon button. (Sorted deliberately-off-scale by
     research/old/visual-audit-2026-08-12.md item 2.) */
  font-size: 19px;
  line-height: 36px;
  cursor: pointer;
}
.note-toolbar .ntb-icon { display: inline-flex; align-items: center; justify-content: center; }

/* On a phone the bar takes the top of the screen for the length of the edit, in
   place of the menu and edit-name buttons that live in the two top corners (both
   hidden by the :root.note-edit-mobile rule below). It spans the board on the
   same insets those buttons used, and its groups wrap so the controls fall into
   two rows and stay readable rather than shrinking to fit one. */
.note-toolbar.is-mobile {
  box-sizing: border-box;
  z-index: var(--z-chrome-over);
  bottom: auto;
  top: max(12px, env(safe-area-inset-top));
  left: max(12px, env(safe-area-inset-left));
  right: max(12px, env(safe-area-inset-right));
  width: auto;
  transform: none;
  flex-wrap: wrap;
  justify-content: center;
  row-gap: 6px;
  padding: 8px;
}
.note-toolbar.is-mobile .ntb-btn { min-width: 34px; height: 36px; }

/* While a note is edited on the phone, its bar owns the top edge: the menu
   (hamburger) and the edit-name (pen) buttons step aside so the two never stack. */
:root.note-edit-mobile #menu-btn,
:root.note-edit-mobile #mobile-header-edit-btn,
:root.note-edit-mobile #mobile-find-btn,
/* The zoom cluster and the scale stick step aside too: on a phone the note bar
   owns the whole screen for the edit, and a Desktop board carries these two
   where the Mobile board does not - so without this they sit under the bar. */
:root.note-edit-mobile #zoom-ctl,
:root.note-edit-mobile #scale-bar { display: none; }
/* The ink veil rather than --paper-2, and this is the case it is for: the bar
   floats over a note whose paper is one of four tints, so a wash struck from
   the app's paper would be a different colour from the note under it. A veil
   made of ink darkens whatever is behind it.

   Guarded, which it was not: this bar is used with a finger more than with a
   pointer, and it stays on screen after the tap that lit it. */
@media (hover: hover) {
  .note-toolbar .ntb-btn:hover {
    background: color-mix(in srgb, var(--ink) 8%, transparent);
    color: var(--ink);
  }
}
.note-toolbar .ntb-btn.is-active {
  background: var(--accent);
  /* --accent-fg, not --paper-card: this is ink printed *on* the accent, and
     --accent-fg is the one mixed to stay legible when the palette moves. */
  color: var(--accent-fg);
}
/* The face control. A .ntb-btn like the rest of the bar now, wearing one extra
   rule: it is the only control here whose label is a word rather than a glyph,
   so it needs room to say Sheet and Terracotta rather than being held to the
   34px square the icons sit in.

   It was a <select>, and the tinted well below is what is left of the dressing
   that made a platform control look like it belonged - see the note over it in
   canvas/notes.ts for what went with it. */
.note-toolbar .ntb-select {
  width: auto;
  min-width: 62px;
  padding: 0 10px;
  background: color-mix(in srgb, var(--ink) 6%, transparent);
}

/* The marker control: a pen, and under it a bar in the colour at the caret.
   The bar is drawn from --note-wash, which the data-wash block up by the note
   runs sets - one attribute, one mapping, two very different elements reading
   it.

   Unmarked there is no attribute and so no --note-wash, and the fallback is the
   whole of what "none" looks like here: nothing inside a ring - which is now
   what actually happens, rather than what the old `--wash` fallback claimed
   while an inherited `1` shadowed it. The ring is on in both states
   deliberately, so the bar does not change size or weight as a colour is
   chosen - only what is in it. */
.note-toolbar .ntb-marker { flex-direction: column; gap: 2px; }
.note-toolbar .ntb-marker .ico { width: 18px; height: 18px; margin-bottom: -3px; }
.note-toolbar .ntb-chip {
  display: block;
  width: 18px;
  height: 5px;
  /* --radius-xs, which resolves to 4.004px at the middle tier - so this literal
     was the token frozen as the number it happened to be on the day it was
     copied, and the chip stayed rounded when the Corner radius slider went to 0.
     tokens.css describes that fault in the past tense; three sites still had
     it. */
  border-radius: var(--radius-xs);
  background: var(--note-wash, transparent);
  box-shadow: inset 0 0 0 var(--hairline) color-mix(in srgb, var(--ink) 30%, transparent);
}
/* On the accent, where the ring would vanish into it. */
.note-toolbar .ntb-btn.is-active .ntb-chip {
  box-shadow: inset 0 0 0 var(--hairline) color-mix(in srgb, var(--accent-fg) 55%, transparent);
}

/* The alignment and vertical-placement glyphs, drawn as bars rather than trusted
   to a font. currentColor tracks the button, so they invert on the active one.
   Alignment: three bars pushed to one side. Vertical: two bars grouped high,
   centred or low in the box. */
.note-toolbar .ntb-ico { display: inline-block; background-repeat: no-repeat; }
.note-toolbar .ntb-ico-al-left,
.note-toolbar .ntb-ico-al-center,
.note-toolbar .ntb-ico-al-right {
  width: 22px;
  height: 16px;
  background-image:
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor);
  background-size: 22px 2px, 14px 2px, 18px 2px;
}
.note-toolbar .ntb-ico-al-left { background-position: left 2px, left 8px, left 14px; }
.note-toolbar .ntb-ico-al-center { background-position: center 2px, center 8px, center 14px; }
.note-toolbar .ntb-ico-al-right { background-position: right 2px, right 8px, right 14px; }
.note-toolbar .ntb-ico-va-top,
.note-toolbar .ntb-ico-va-middle,
.note-toolbar .ntb-ico-va-bottom {
  width: 16px;
  height: 18px;
  background-image:
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor);
  background-size: 14px 2px, 10px 2px;
}
.note-toolbar .ntb-ico-va-top { background-position: center 1px, center 5px; }
.note-toolbar .ntb-ico-va-middle { background-position: center 6px, center 10px; }
.note-toolbar .ntb-ico-va-bottom { background-position: center 11px, center 15px; }

/* How much room is left, while a note is being written. On the sheet, in the
   bottom-right corner of its padding: the text column ends where the padding
   starts (growNote() keeps it so), so the two never overlap, and absolute
   positioning keeps it out of noteHeight()'s measurement. Sized off the pad
   like the rest of the sheet, so it reads as writing on the note rather than
   chrome over it. */
.item[data-type="note"] .note-count {
  position: absolute;
  right: var(--note-pad);
  bottom: calc(var(--note-pad) * 0.18);
  line-height: 1;
  font-family: var(--font-display);
  /* One step up the ramp from --t-tiny, and one step up the ink from --ink-3.
     It was the smallest size in the ramp set in the faintest ink there is, in
     an italic, tucked under the bottom-right corner of the sheet - four ways of
     being quiet at once, and the note above this rule says the one thing this
     must not be is a limit you find out about by hitting it. Still the quietest
     thing on a note being written, which is right: it is a number you glance at
     twice and read once. The min() keeps it inside the padding on a narrow
     note, where the pad shrinks below the size of the type. */
  font-size: min(var(--t-small), calc(var(--note-pad) * 0.7));
  font-style: var(--display-italic);
  color: var(--ink-2);
  white-space: nowrap;
  pointer-events: none;
}
/* Running out. The colour carries it and the weight backs the colour up, which
   matters on a board wearing a palette where --danger is not far from --ink-2.
   Weight rather than size: this sits under a sheet that grows as it is written,
   and a counter that changed size at twenty characters left would move the sheet
   under the caret. */
.note-count.is-low { color: var(--danger); font-weight: 600; }

/* Links: the other item born on the board, and the only one whose subject is
   somewhere else entirely. Drawn as an ordinary filed card, because that is
   what it is - a name, an address, and no body. The one part that behaves
   differently is the name, which is the anchor. */
.card-link .card-name {
  color: var(--accent-text);
  text-decoration: underline;
  text-decoration-thickness: from-font;
  text-underline-offset: 2px;
  /* .item-body refuses the pointer so that the whole card is one drag target;
     the anchor is the exception, the same bargain .transport makes for the
     audio scrubber. Everything around it - the badge, the address, the card's
     own padding - still drags, selects and resizes as usual. */
  pointer-events: auto;
  cursor: pointer;
  /* An address has no spaces to break at, so a long one is allowed to break
     anywhere rather than shouldering the card's edge out of the way. */
  overflow-wrap: anywhere;
}
.card-link .card-name:hover { text-decoration-thickness: 2px; }
/* The address itself: monospaced, so that a URL reads as a URL and not as
   prose, and upright rather than in the italic the other card metadata wears -
   an address is a literal, and slanting one is a small lie about it. */
.card-link .card-meta {
  font-family: var(--font-mono);
  font-style: normal;
  overflow-wrap: anywhere;
  /* Two lines of address is as much orientation as anyone wants, and the whole
     of it is on the anchor's tooltip for the rare time it is not.

     Prefixed only, deliberately. The unprefixed `line-clamp` was here as
     future-proofing and bought nothing: there is no engine that supports it and
     not the -webkit- form, Firefox aliases the prefixed one, and the unprefixed
     declaration's only observable effect was a dropped-declaration warning in
     every Firefox console that opened this page. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
/* ---- Stickers (canvas/renderers.ts/sticker) -------------------------------
   The one item that is not a card at all: no paper, no border, no caption, no
   cover slot. A sticker is a shape lying on a picture, and a shape in a
   bordered box is a card with a star in it - which is a different object and
   not the one anybody asked for. So everything the .item wrapper normally
   contributes is taken back off here, and what is left is the SVG.

   A sticker is a paper body with an inked outline - the same reading the app's
   own icons have, one size up. That is what makes it belong to this board
   rather than to a phone keyboard, and it is also the only version of the idea
   that stays legible over a dark photograph, which bare line work does not.

   **Nothing here is stroked**, and that is Phosphor's doing rather than a
   choice: it draws every weight as a *filled* path, its outline weights
   included, where the line is a closed path tracing the outline of a stroke.
   So the line weight is upstream's, `stroke: none` is written out rather than
   left to chance, and a sticker resized is a bigger drawing at the same
   proportions - which is what the old, hand-drawn, actually-stroked version had
   to work for.

   Both properties are *inherited*, which is what lets them be set in a
   stylesheet at all: the shape lives in an externally referenced <use>, so no
   selector in this file can reach a path inside it, and only what the <svg>
   passes down arrives. `color` is how the ink gets to the paths that opt out of
   the default paint - a duotone glyph's foreground is `fill="currentColor"` -
   since a presentation attribute beats an inherited property and
   `currentColor` is the one value that still follows the tint from outside. */
/* No card, at all: no paper, no hairline, no shadow. The three lines below
   switch off the ones .item paints itself. The fourth was invisible from here
   and is the one that showed: the geometry twin in #item-shadows is a separate
   element in a separate layer, so `--item-shadow: none` on the card never
   reached it and every sticker sat in a rounded-rectangle halo the size of its
   box. Switched off where it is built - see NO_TWIN in canvas/items.ts - since
   a twin that paints nothing is still an element per sticker, placed and
   mirrored on every move. */
.item[data-type="sticker"] {
  background: transparent;
  --item-shadow: none;
  --item-border: transparent;
  box-shadow: none;
}
/* No caption and no menu handle. The far-zoom headline is refused in JS
   instead (NO_HEAD in canvas/item-dom.ts), because it is never built rather than
   built and hidden. The name still exists - the trash and Find want it - it
   just is not printed across the shape. */
.item[data-type="sticker"] .item-bar { display: none; }
/* The shape may hang past the box's rounded corners: it is a star, not a
   picture that needs cropping to the card it is in. */
.item[data-type="sticker"] .item-body { overflow: visible; }

.sticker-art {
  display: block;
  width: 100%;
  height: 100%;
  color: var(--sticker-tint);
  fill: var(--sticker-body);
  stroke: none;
  /* A drop-shadow rather than a box-shadow, and that is the point of using one
     at all: it follows the outline the filter is given, and the outline is the
     whole reason this thing is not a card. A box-shadow would draw a
     rectangle's shadow under a star.

     Tight and close, like the note's - a sticker is pressed flat, not floating.
     Light, because a paper body with a thin outline has not the mass to carry a
     heavy one; a solid silhouette could, and this was heavier when the shapes
     were solid. */
  filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--ink) 16%, transparent))
          drop-shadow(0 3px 5px color-mix(in srgb, var(--ink) 15%, transparent));
  /* The shape must not eat presses that the item's own box would have taken.
     Hit-testing on a sticker is rectangular like everything else - see the
     item, which keeps its pointer-events - so this is only here to stop the
     SVG from becoming a second, differently shaped target inside it. */
  pointer-events: none;
}

/* The pad, out of tokens.css.
   Eight rules rather than one var(--sticker-{n}), because CSS cannot build a
   custom property name out of an attribute - and the alternative is each
   renderer writing a colour into a style attribute, which would take this file
   out of the question of what a sticker looks like.

   Keyed on the artwork rather than on whatever is around it, which is what
   keeps it to eight lines. A sticker is drawn in five places - on the board, on
   a pad tile, on the ghost following the pointer out of the pad, on a Mobile
   feed tile, and in the bin - and every one of them builds the same
   .sticker-art element, so every one of them can carry the tint on it. Five
   selectors per rule was the first version, and it was five chances to forget
   the sixth place. */
.sticker-art { --sticker-tint: var(--sticker-1); }
.sticker-art[data-tint="2"] { --sticker-tint: var(--sticker-2); }
.sticker-art[data-tint="3"] { --sticker-tint: var(--sticker-3); }
.sticker-art[data-tint="4"] { --sticker-tint: var(--sticker-4); }
.sticker-art[data-tint="5"] { --sticker-tint: var(--sticker-5); }
.sticker-art[data-tint="6"] { --sticker-tint: var(--sticker-6); }
.sticker-art[data-tint="7"] { --sticker-tint: var(--sticker-7); }
.sticker-art[data-tint="8"] { --sticker-tint: var(--sticker-8); }

/* The copy on a Mobile feed tile. Placed in percentages of the tile by
   ui/feed.ts, so it holds its fraction of the host through every relayout of
   the wall underneath it; the translate is what puts the shape's own centre
   over that point rather than its corner. */
.feed-sticker {
  position: absolute;
  translate: -50% -50%;
  pointer-events: none;
  z-index: 2;
}

/* ---- A model on a card (canvas/model.ts) ---------------------------------
   The canvas fills the card above the filename and takes the pointer, because
   dragging it turns the model rather than moving the item. Everything around
   it - the padding, the name - still drags the card, which is the same bargain
   the audio scrubber and the link anchor make. */
/* Tighter than a card of type, and that is the difference between a plate and a
   mount. The 15/16 in items.css is set for a card whose contents are *lines* -
   a filename, a duration, a URL - where the margin is what stops the text
   reading as though it had been printed off the edge. A model is a picture, and
   the picture is already inset twice before this padding is reached: it is
   fitted to its bounding *sphere*, which for anything longer than it is wide
   leaves air on the long axis, and FIT_MARGIN in canvas/model.ts adds more so a
   cube seen corner-on cannot clip. Stacking a plate's margin on top of those
   two was three insets doing one job, and what it drew was a small part in the
   middle of a large card.

   **Struck from the rule rather than typed in as a number.** The inner rule is
   printed at --card-rule-gap from the edge, and that dial moves with the whimsy
   tier - 5px in the middle and 8px at the ornate end. A flat 8 here would have
   been a comfortable margin on most boards and the model sitting exactly *on*
   the line at the top of the dial, which is the kind of thing that is only ever
   found in a screenshot. Four pixels of air inside the rule is the whole rule:
   the drawing clears the line by the same amount whatever the tier says.

   The name keeps the horizontal margin it always had - it is still a line of
   type and still wants to sit inside the rule rather than under its ends. */
.card-model {
  gap: 6px;
  --card-pad-y: calc(var(--card-rule-gap) + 4px);
  --card-pad-x: calc(var(--card-rule-gap) + 4px);
}
.card-model .card-name { padding-inline: 6px; }
.model-stage {
  flex: 1;
  min-height: 0;
  width: 100%;
  pointer-events: auto;
  cursor: grab;
  /* A pinch on a phone must reach the board, and a drag must not scroll the
     page out from under the model. */
  touch-action: none;
}
.model-stage.is-turning { cursor: grabbing; }
/* The photograph the card shows when nobody is turning it - see
   canvas/model.ts. It stands in the stage's place and has to sit in the box the
   same way, and `contain` is what makes that exact: the shot is taken at this
   box's own proportions, measured off it (stageShape() in canvas/model.ts), so
   containing it here is a scale and nothing else. The framing itself fits the
   model's bounding sphere to the narrow axis, which is the same measurement at
   every angle - so a model on a card holds still while you turn it over.

   **Written against .card-model, and the descendant is the whole point.** A
   bare `.model-still` is one class where `.item-body img` above is a class and
   a type, so the generic rule out-specified this one and every model still on
   the board was drawn `cover` - cropped to fill, at a scale the live canvas
   never used. That is a model that changes size and loses its corners the
   moment you stop turning it, which is the fault takeShot() was squared off to
   fix and could not, because the fix was being overruled two files away.
   Two classes beats a class and a type, whatever the source order says.

   No cursor and no pointer: this is a picture, and a drag on it moves the card
   like a drag on any other picture does. Turning it over is a menu away. */
.card-model .model-still {
  flex: 1;
  min-height: 0;
  width: 100%;
  object-fit: contain;
  /* The stage draws on a transparent buffer, so a still of a pale model on a
     pale card is the same picture the canvas would have shown. */
  background: none;
}
/* And it stays visible however far out the board goes - as the hundred-pixel
   twin beside it, which is the same swap every photograph on the board gets.
   The stage below hides at the detail rung because a WebGL canvas is the most
   expensive thing on the card and a turned model is a grey smudge out there.
   The picture is not hidden: taking it away turned every model into an empty
   card at exactly the zoom where the board stops being a set of things to read
   and becomes a composition to look at, which is the one job a picture of the
   model still has out there.

   It used to be kept on the grounds that the still was "an <img> of at most
   450px, cheaper than any photograph beside it". That stopped being true when
   shotEdge() started sizing the shot to the card - a large card is photographed
   at 900px now - so the model takes the twin on the same terms the photographs
   do, and the two rules that swap it are in items.css beside theirs. */
.card-model .card-name {
  flex: none;
  font-size: var(--t-tiny);
  color: var(--ink-3);
  font-style: var(--display-italic);
}
/* Reading a large STL is a real pause, and the card says so rather than
   sitting empty. Replaced by the drawing, or by the reason there is not one. */
.model-note {
  display: grid;
  place-items: center;
  flex: 1;
  min-height: 0;
  font-family: var(--font-display);
  font-size: var(--t-tiny);
  font-style: var(--display-italic);
  color: var(--ink-3);
  text-align: center;
  padding: 0 8px;
}
.model-note.is-error { color: var(--danger); }
/* Far out, a turned model is a grey smudge and the canvas is the most
   expensive thing on the card. Same rung as every other detail (viewport.ts). */
#world.zoom-far .model-stage { visibility: hidden; }

/* And the filename goes with it, on a card that is showing the part. At this
   rung every other caption on the board is already down - the item bar, the
   plate, the label (item-chrome.css) - and this is the same line of type by
   another route, set at --t-tiny inside the card rather than on a strip under
   it. At 40% it is four pixels of smear, and the rung exists to take exactly
   that away. What is left is the picture, edge to edge inside the mount, which
   is what a photograph beside it shows out here and is the whole of why
   item-chrome.css stopped drawing this card a headline.

   Only while there is a still: a model still reading, or being turned, has a
   hidden stage behind this name and the name is all the card has left to say.
   `flex: none` on the rule above means the picture takes the room back. */
#world.zoom-far .card-model:has(.model-still) .card-name { display: none; }

/* "No card" on a model - the third reading of the promise the cut-out and the
   sticky make, which is that the paper goes and what was on it stays. Here the
   part stays: same size, same place, same view, standing on the board.

   Said here rather than beside the [data-bare] rules in items.css for the reason
   the sticky's are said next to the sticky's - what has to be cancelled is
   written here and nowhere else, and `.item[data-bare]` and
   `.item[data-type="model"]` have the same weight, so a cancel in the other file
   would win or lose on which sheet the <link> order loaded last.

   The padding goes with it, and that is not the same decision as the one above.
   On a card the margin is the mount; with no card there is nothing to be
   mounted on, and a part floating a few pixels inside an invisible box is a
   part that does not line up with the box you are dragging. Bare means the
   drawing fills the item, so what you grab is what you see.

   The caption goes too, on the cut-out's terms exactly - see the note beside
   `.item[data-bare] .item-bar` in items.css. "No card" has to mean no card, and
   a filename on a hairline strip is the one edge of a frame that never left.
   The still is the same picture as the stage, so both are covered by one rule
   and neither needs to know which is showing. */
.item[data-type="model"][data-bare] .card {
  background: none;
  box-shadow: none;
  padding: 0;
}
.item[data-type="model"][data-bare] .card::after { content: none; }
.item[data-type="model"][data-bare] .card-name { display: none; }

/* ---- The links that can become players (canvas/embed.ts) -----------------
   The button is a standing offer, not a state: nothing has been requested from
   anyone until it is pressed, and it is styled to say so - the same accent a
   link wears, on the card's own paper, rather than the filled red of a control
   that is already doing something. The chip's own paper and type stay the
   board's - a card that dressed the whole of itself in YouTube red or Spotify
   green would be two more palettes on a board whose whole premise is that you
   chose the pigments. The one brand colour let in is the logo itself, drawn in
   embed.ts: a mark is recognisable by its colour and nothing else is, so the
   green rides on the glyph alone and the chip around it stays ours. */
.embed-go {
  /* Centred both ways by soaking the free space into the margins. A card that
     was ever played keeps that larger size, and the offer returns to it on
     reload because nothing may load without a press - so the offer has to look
     deliberate at any size, not a chip stranded in a corner of a big empty
     card. `margin: auto` in a flex column does exactly that at every size. */
  margin: auto;
  /* Above the faint provider mark behind it (renderers.ts). The card is the
     positioned ancestor, so this only needs a layer, not coordinates. */
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px 8px 13px;
  border: var(--board-hairline) solid var(--rule-2);
  border-radius: var(--leaf);
  background: var(--paper-2);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--t-small);
  /* The display face rides high, and a tight line box lets a button trim its
     ascenders (most visibly in Firefox). Room for the whole glyph, not the
     x-height. */
  line-height: 1.4;
  letter-spacing: calc(0.04em * var(--ls-scale));
  cursor: pointer;
  pointer-events: auto;   /* same bargain the anchor above makes */
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
@media (hover: hover) {
  .embed-go:hover { background: var(--accent); border-color: var(--accent-text); color: var(--accent-fg); }
}
.embed-go svg { width: 17px; height: 17px; flex: none; }
/* A song title can be long; the button caps at the card and the label ellipses
   rather than pushing the chip past the edge. min-width:0 lets the flex label
   shrink below its text; the logo keeps its size (flex:none above). */
.embed-go { max-width: 100%; }
.embed-go span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Far out the button is too small to read or press, and a card zoomed away to a
   tile wants nothing on it but the stretched mark. So the offer stands down and
   the watermark is the whole of the card at that distance. */
#world.zoom-far .embed-go { display: none; }

/* A plain link card wears the same chip, but as an anchor that opens the page
   (renderers.ts linkChip). text-decoration off, because the chip is a button
   shape, not a run of underlined text. Its label is a .card-name so F2 renames
   it in place - but inside the chip the underlined-accent link-name look is
   wrong, so it is reset to the button's own label: ink on the card's paper,
   ellipsed on one line, and following the chip's colour on hover. */
.embed-go { text-decoration: none; }
.card-link .embed-go .card-name {
  color: inherit;
  text-decoration: none;
  overflow-wrap: normal;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* The provider's name, faint, behind the offer: filled to the card width and
   pulled tall. Width from a container-unit font-size (a fraction of the card),
   height from the scaleY on top of it - the stretch the reference asks for. A
   fixed cqw is a different fraction of the word on each whimsy face, so this is
   tuned near the widest (softish) and the card clips any overrun, which is the
   printed-mark look. Not interactive: the press under it drags the card. */
/* The mark fills the card's padding box - the inside margin - so the word
   reaches the inner edges and not the card's bleed. An SVG sized to that box by
   the four insets; its viewBox is the word's ink and preserveAspectRatio="none"
   fills it (renderers.ts fitMark), so the word stretches to the margins on every
   whimsy face with no crop and no guesswork. */
.embed-mark {
  position: absolute;
  inset: var(--card-pad-y) var(--card-pad-x);
  overflow: hidden;
  opacity: 0.11;
  pointer-events: none;
  user-select: none;
}
.embed-mark svg { display: block; width: 100%; height: 100%; }
/* A logo watermark (X): the mark itself stretched to fill, in the board ink. */
.embed-mark path { fill: var(--ink); }
.embed-mark text {
  font-family: var(--font-display);
  font-weight: calc(var(--display-weight) + 300);
  /* Upright, not the display face's usual italic - a stamped mark stands
     straight. Any size: the viewBox normalises it to the box (renderers.ts). */
  font-style: normal;
  font-size: 40px;
  fill: var(--ink);
}

/* The song title lives on the button while the card is an offer (embed.ts), so
   the top-left copy is only wanted once the player is in - where it is the drag
   handle, the one surface still ours over a frame that eats every pointer. So
   it is out of the offer entirely and appears with the frame. */
.embed-title {
  display: none;
  position: relative;
  z-index: 1;
  align-self: flex-start;
  max-width: 100%;
  font-family: var(--font-display);
  font-size: var(--t-small);
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Taken. The title appears as the drag handle. Cursored as the grip it is; the
   frame takes everything under it. */
.card.has-embed { gap: 6px; }
.card-embed.has-embed .embed-title {
  display: block;
  flex: none;
  opacity: 0.7;
  pointer-events: auto;
  cursor: grab;
}
.card-embed.has-embed .embed-title:active { cursor: grabbing; }
.embed-frame {
  flex: 1;
  min-height: 0;
  width: 100%;
  border: 0;
  border-radius: var(--radius-xs);
  pointer-events: auto;
}
/* A video is letterboxed against black because that is what a film is shown
   against, and any other colour reads as a bug in the player. */
.embed-frame[data-provider="youtube"] { background: #000; }

/* Spotify's player, made to sit on this board rather than on Spotify.
   ---------------------------------------------------------------------------
   Only so much of this is ours to decide, and it is worth being exact about
   which: inside the frame is Spotify's document on Spotify's origin, and no
   stylesheet of ours reaches across that boundary - by design, and there is no
   version of the web where it does not. There is no colour lever left to pull:
   the player takes its own from the artwork, and the one query parameter that
   changes it, `theme=0`, only makes it darker. So the card holds the player as
   it comes.

   Everything else here is the ring around it, which is all ours: the card's own
   paper behind the frame so the corners are this app's radius and not the
   twelve-pixel one their generator writes inline while the frame loads, the
   hairline that every other card wears, and a width and height (embed.ts) sized
   so the player's own compact layout lands in the box with no scrollbar across
   it and no dead band above or below. The result is a Spotify card that reads
   as one of ours holding a player, rather than a rectangle of somebody else's
   interface pasted on top. */
.embed-frame[data-provider="spotify"] {
  background: var(--paper-2);
  border-radius: var(--leaf);
  /* The player draws its own rounded corners at a radius it picked. Clipping to
     ours means the two never disagree at the corner, which is the single most
     obviously-foreign thing about an embed at rest. */
  overflow: hidden;
}
/* The card behind a Spotify player closes up: their layout carries its own
   padding, so ours on top of it reads as a mat around a picture that did not
   ask for one. */
.card.has-embed[data-provider="spotify"] { gap: 4px; }

/* Nothing to see at a distance, and a cross-origin frame is the most expensive
   thing on the board to keep composited. */
#world.zoom-far .embed-frame { visibility: hidden; }

/* The anchor stands down at far zoom, for the reason the grips and the caption
plates do: below a third of size a card is a coloured tile, and a click on
   one is a click on the board rather than a considered press on a line of text
   five pixels tall. Opening a tab is not a thing to do by accident. */
#world.zoom-far .card-link .card-name { 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.

   Four of these restate a whole shorthand rather than a colour, for the reason
   base.css gives: a gradient stop, a shadow in a list and a drop-shadow in a
   filter chain are all parts of one declaration and cannot be reached
   separately. Each is otherwise identical to the rule it mirrors - if one of
   them is edited above and not here, the two disagree only on a browser nobody
   testing this is holding, which is what tests/legacy-color.test.js is for. */
@supports not (color: color-mix(in srgb, red, blue)) {
  .item[data-type="note"] .card {
    background:
      linear-gradient(180deg,
        rgb(var(--ink-c) / 6%) 0,
        transparent var(--note-band)),
      var(--note-sheet, var(--note-paper));
    box-shadow: inset 0 0 0 var(--board-hairline) rgb(var(--ink-c) / 7%);
  }
  /* No 60% sheet without color-mix(): a faded note stays solid here, where the
     unparseable mix would have left its paper transparent. */
  .item[data-type="note"][data-faded]:not([data-bare]) { --note-sheet: var(--note-paper); }
  .note-toolbar {
    box-shadow:
      0 3px 10px rgb(var(--ink-c) / 16%),
      inset 0 0 0 var(--hairline) var(--item-border);
  }
  .note-toolbar .ntb-group + .ntb-group {
    border-left: var(--hairline) solid rgb(var(--ink-c) / 12%);
  }
  @media (hover: hover) {
    .note-toolbar .ntb-btn:hover { background: rgb(var(--ink-c) / 8%); }
  }
  .note-toolbar .ntb-select { background: rgb(var(--ink-c) / 6%); }
  .note-toolbar .ntb-chip {
    box-shadow: inset 0 0 0 var(--hairline) rgb(var(--ink-c) / 30%);
  }
  .note-toolbar .ntb-btn.is-active .ntb-chip {
    box-shadow: inset 0 0 0 var(--hairline) rgb(var(--accent-fg-c) / 55%);
  }
  .sticker-art {
    filter: drop-shadow(0 1px 1px rgb(var(--ink-c) / 16%))
            drop-shadow(0 3px 5px rgb(var(--ink-c) / 15%));
  }
}

