/* ------------------------------------------------------------
   Three faces, and they divide the page between them by weight,
   not by element type -- the same tag can carry any of the three
   depending on what it is doing on screen.

   Marston is the loudest: the site's own name, a page's own
   header, the tab bar that gets you between pages.  There is only
   ever a handful of these on screen at once.

   RDR Lino Regular is the middle tier: a card's own name, a tab
   inside a page (Vendor recipes / Campfire), a dialog's title, a
   section's caption (USED IN, INGREDIENTS), the General/Personalize
   and Skip/Want it switches, and every button's own label.  It is
   not monospaced, unlike the other two, so it never carries a
   column that has to line up.

   FB Remington sets everything else -- prose, descriptions, the
   Settings page's explanations, and the numbers: 3/3, 2x Oregano,
   $14.95.  It is monospaced, so those columns line up on their own.

   All three load one weight and no italic (Marston ships a bold
   and an italic too, left out on purpose), so the browser
   synthesises both where either is asked for.  That is the right
   trade here: the real originals had one weight too, and emphasis
   was struck twice over the same spot.

   FB Remington's character set is a typewriter's, which is to say
   152 of them.  It has no ×, ✓, ·, − or —, so nothing here asks for
   one; see the marks in render.js.

   What each one is used on is decided at the foot of this file,
   and there is a reason it has to be decided there.  swap on all
   of them, because no font should hold up the first paint.
   ------------------------------------------------------------ */

@font-face {
  font-family: "Marston";
  src: url("fonts/marston/Marston.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "FB Remington";
  src: url("fonts/fb_remington/FBRemington-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  /* The face is small for its size: drawn 6% larger, so every type
     size set in it reads a step up without touching the sizes
     themselves, or the display and heading faces beside it. */
  size-adjust: 106%;
}

@font-face {
  font-family: "RDR Lino Regular";
  src: url("fonts/rdr2_lino_regular/RDR Lino Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* The five colours of the game's key art, kept verbatim:
     https://www.color-hex.com/color-palette/72703
     Everything either theme paints is derived from these. */
  --rdr-red:    #bd081a;
  --rdr-amber:  #feac01;
  --rdr-blood:  #b90303;
  --rdr-black:  #020002;
  --rdr-white:  #fffeff;

  --font-body:    "FB Remington", ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-display: "Marston", "Haettenschweiler", "Arial Narrow", sans-serif;
  --font-heading: "RDR Lino Regular", Georgia, "Times New Roman", serif;

  /* For the handful of glyphs a typewriter never had a key for: the tick,
     the cross, the copyright sign.  Named rather than left to the browser
     because each one is in a different set of fonts, and falling back per
     glyph lands them at different weights beside each other. */
  --font-symbol:  "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2",
                  "DejaVu Sans", sans-serif;

  --radius: 10px;
  --tabs-h: 56px;
}

/* ---- parchment ---------------------------------------------
   The palette's white, aged: paper, and the palette's black
   for ink.  Cards sit a shade lighter than the page so they
   lift off it; the recessed things — a buff box, a ledger row —
   sit a shade darker. */

:root {
  color-scheme: light;

  --ink:        #241a12;
  --ink-dim:    #4a3a28;
  --ink-faint:  #6e5c3e;
  --bg:         #e7dbc0;
  --surface:    #f2e9d6;
  --surface-2:  #dccfb0;
  --line:       #c2b191;
  --page-wash:  #f4ecda;

  --accent:      var(--rdr-amber);   /* fills */
  --on-accent:   var(--rdr-black);   /* what sits on a fill */
  --accent-text: #8a5600;            /* ochre: 4.6:1 on paper */
  --ink-name:    #7a2e18;            /* oxblood: the name of a thing */
  --ink-label:   #7a4c00;            /* ochre, dark enough for a ledger row */
  --danger:      var(--rdr-red);
  --danger-text: var(--rdr-blood);
  --ok:          #46621f;
  --warn:        #7d5a12;

  /* Trapper, Pearson, Fence.  Two warm, one cool -- three warm stripes
     at 3px are hard to tell apart, so the Trapper keeps a blue, pulled
     towards slate so it sits in the same world as the other two. */
  --station-blue:   #4e6a82;
  --station-yellow: #9a7519;
  --station-pink:   #8f3f2a;
  /* The campfire: sage rather than the tick's green, so "made at your
     own fire" never reads as "you have enough". */
  --station-green:  #5b6a3e;

  --shadow: 0 8px 24px rgb(64 44 20 / .22);
}

/* ---- leather ------------------------------------------------
   The palette's black warmed towards hide, its white warmed
   towards parchment.  Neither is ever the raw colour: pure
   #020002 under pure #fffeff is a glare to read a long list on. */

:root[data-theme="leather"] {
  color-scheme: dark;

  --ink:        #f1e7d3;
  --ink-dim:    #c0ae92;
  --ink-faint:  #9a8768;
  --bg:         #241a12;
  --surface:    #30231a;
  --surface-2:  #3d2d21;
  --line:       #55402f;
  --page-wash:  #3a2a1c;

  --accent:      var(--rdr-amber);
  --on-accent:   var(--rdr-black);
  --accent-text: var(--rdr-amber);   /* 8.9:1 on leather, so it draws too */
  --ink-name:    #dcb578;            /* gold-tan: warm without reading as
                                        the amber that means "press this" */
  --ink-label:   #c9932f;   /* clears 4.5:1 on a ledger row, the darkest
                                ground any of these labels sits on */
  --danger:      var(--rdr-red);
  --danger-text: #ec6a5a;            /* the raw red is 2.9:1 here */
  --ok:          #9cb86c;
  --warn:        #e0b155;

  --station-blue:   #93a8b8;
  --station-yellow: #ddb455;
  --station-pink:   #c26a45;
  --station-green:  #a9b58a;

  --shadow: 0 8px 24px rgb(0 0 0 / .45);
}

* { box-sizing: border-box; }

/* Every display rule below would otherwise beat the browser's own
   [hidden] { display: none }, which is how an empty toast and an
   empty save bar both stayed on screen. */
[hidden] { display: none !important; }

/* One focus ring for every control, drawn in the ink that marks
   something pressable, rather than each browser's own blue. */
:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

body {
  margin: 0;
  /* A lantern above the masthead, so the page has a top. */
  background:
    radial-gradient(130% 70% at 50% -10%,
                    var(--page-wash) 0%, var(--bg) 62%)
    no-repeat fixed,
    var(--bg);
  color: var(--ink);
  font: 15px/1.5 var(--font-body);
  padding-bottom: var(--tabs-h);
}

/* The footer at the foot of the window, however little the page holds:
   the page is a column at least as tall as the screen, and the content
   takes up whatever the rest leaves over.  Each part centres itself in
   a 1400px column with auto margins, which a flex item would otherwise
   shrink to fit its content -- so each is told to take the full width,
   and its max-width does the rest, as before. */
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}
body > :is(.cover, .first-note, .page-title, main, .footer) { width: 100%; }
body > main { flex: 1 0 auto; }

/* ---------- top bar ---------- */

/* No backdrop-filter here while the tabs are fixed inside it: a
   filtered ancestor becomes the containing block for its fixed
   descendants, which would pin the tab bar under the header
   instead of to the bottom of the screen.  The wide layout, where
   the tabs are static, puts it back. */
.chrome {
  position: sticky;
  top: 0;
  z-index: 10;
}

.topbar {
  position: relative;
  display: flex;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(8px);
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
}

/* A thread of amber under the masthead, brightest in the middle,
   the way a lamp falls on a poster. */
.topbar::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: linear-gradient(to right,
    transparent, color-mix(in srgb, var(--accent-text) 55%, transparent), transparent);
}

/* The masthead: the campfire-emblem logo, then the title on two
   lines — the game above, what this is below.  Two lines because
   the whole name is 36 characters and a phone's top bar is not,
   and because it stacks to the logo's height for free. */
.topbar h1,
.topbar .brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  min-width: 0;
  font-size: 1.05rem;
}

.brand-mark {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 50%;
}

.brand-text { display: flex; flex-direction: column; min-width: 0; }

.brand-game {
  font-size: .62rem;
  line-height: 1.25;
  letter-spacing: .16em;
  color: var(--accent-text);
}

/* Sized so its letters span exactly as wide as the line above:
   the two lines read as one block, squared off at both ends. */
.brand-app {
  font-size: 1.225rem;
  line-height: 1.05;
  letter-spacing: .05em;
  color: var(--ink);
  white-space: nowrap;
}

/* The page's own name, under the menu. */
.page-title {
  margin: 0;
  padding: 18px 16px 0;
  max-width: 1400px;
  margin-inline: auto;
  font-size: 1.5rem;
  color: var(--ink-name);
}
.page-title:empty { display: none; }

/* The masthead switch: the same two-sided control as the recipe
   dialog's, in the accent rather than the green, since it sets what
   the whole app is showing rather than whether one thing is wanted.
   On a narrow phone there is no room for both words, so only the one
   in force stays, set small above the track the way the game's name
   sits above the guide's -- the switch still says which mode is on. */
.mode-switch[aria-checked="true"]  .side.on,
.mode-switch[aria-checked="false"] .side.off { color: var(--ink); }

.mode-switch[aria-checked="true"] .track {
  background: color-mix(in srgb, var(--accent-text) 22%, var(--surface-2));
  border-color: var(--accent-text);
}
.mode-switch[aria-checked="true"] .track i {
  transform: translateX(16px);
  background: var(--accent-text);
}

@media (max-width: 460px) {
  /* Under .topbar, to outrank the switch's own rule further down. */
  .topbar .mode-switch {
    flex-direction: column;
    gap: 3px;
    padding: 2px 0;
  }
  .mode-switch .side {
    order: -1;
    font-size: .6rem;
    line-height: 1.25;
    letter-spacing: .12em;
  }
  .mode-switch[aria-checked="true"]  .side.off,
  .mode-switch[aria-checked="false"] .side.on { display: none; }
}

/* The Back up button and the mode switch, kept together at the right. */
.topbar-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
}

/* A quiet outline at rest, so it never competes with the title; the
   dot in its corner is what speaks up -- amber once a backup is due,
   red once it is overdue.  A phone drops the word and keeps the arrow,
   the same way the mode switch drops its words. */
.backup-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  font: inherit;
  font-size: .76rem;
  color: var(--ink-dim);
  background: none;
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}
.backup-btn:hover { color: var(--ink); border-color: var(--ink-faint); }
.backup-btn[hidden] { display: none; }
.backup-btn svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.backup-btn[data-due]::after {
  content: "";
  position: absolute;
  top: -4px;
  right: -4px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--bg);
}
.backup-btn[data-due="overdue"]::after { background: var(--danger); }
.backup-btn[data-due] { color: var(--ink); }

/* A rule between the two when both are showing: one is an action,
   the other a setting. */
.backup-btn:not([hidden]) + .mode-switch {
  padding-left: 12px;
  border-left: 1px solid var(--line);
}

/* On a phone, the same shape as the mode switch beside it: the word
   small above, and the arrow in an outline the size of the switch's
   track, so the pair reads as two of a kind rather than a box and a
   pill.  The due dot stays at the top right, just past the word. */
@media (max-width: 460px) {
  .backup-btn {
    flex-direction: column;
    gap: 3px;
    padding: 2px 0;
    border: 0;
  }
  .backup-label {
    font-size: .6rem;
    line-height: 1.25;
    letter-spacing: .12em;
    white-space: nowrap;
  }
  .backup-btn svg {
    order: 1;
    box-sizing: content-box;
    width: 14px;
    height: 14px;
    padding: 2px 10px;
    border: 1px solid var(--line);
    border-radius: 999px;
  }
  .backup-btn:hover svg { border-color: var(--ink-faint); }
  .backup-btn[data-due]::after { top: -5px; right: -10px; }
  .backup-btn:not([hidden]) + .mode-switch { padding-left: 10px; }
}

/* The one-time note on where the data lives: a panel with the accent
   down its edge, in the same column as the page under it. */
.first-note {
  max-width: 1400px;
  margin: 16px auto 0;
  padding: 0 16px;
}
.first-note[hidden] { display: none; }
.first-note-box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 10px 16px;
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent-text);
  border-radius: var(--radius);
}
.first-note p {
  flex: 1 1 420px;
  margin: 0;
  font-size: .85rem;
  line-height: 1.5;
  color: var(--ink-dim);
}
.first-note strong { color: var(--ink); font-weight: 600; }

/* ---------- navigation: thumb on narrow, mouse on wide ---------- */

/* No env(safe-area-inset-*) on this bar or anything stacked above it.
   The page is not viewport-fit=cover, so the insets should be 0 -- but
   Firefox for Android reports the system navigation bar's height as the
   bottom inset until the first scroll, and the bar grew a blank band
   that tall under itself. */

.tabs {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 10;
  display: flex;
  background: var(--surface);
  border-top: 1px solid var(--line);
}

.tabs a {
  flex: 1;
  padding: 0 4px;
  height: var(--tabs-h);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  color: var(--ink-dim);
  text-decoration: none;
  font-size: 1rem;
  border-top: 2px solid transparent;
}

.tabs a[aria-current="page"],
.tabs a.here {
  color: var(--ink);
  border-top-color: var(--accent-text);
  background: var(--surface-2);
}

/* The page's own icon with its name: the link's gap spaces it, so the
   margin an icon carries beside running text is dropped here.  On the
   phone's bottom bar the icon sits above the name, the way a thumb bar
   is read -- side by side, four of them do not fit a narrow phone --
   and the unsaved-changes dot moves to the icon's shoulder.  The wide
   layout puts them back in a row. */
.tabs .icon { flex: 0 0 auto; margin-right: 0; }

@media (max-width: 719px) {
  .tabs a {
    position: relative;
    flex-direction: column;
    gap: 3px;
    font-size: .8rem;
  }
  .tabs .icon { width: 1.6em; height: 1.6em; }
  .tabs a[data-pending]::after {
    position: absolute;
    top: 7px;
    left: calc(50% + 10px);
  }
}

@media (min-width: 720px) {
  body { padding-bottom: 0; }

  /* Now that the tabs are in normal flow, the whole bar can blur. */
  .chrome {
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    backdrop-filter: blur(8px);
  }
  .topbar {
    background: transparent;
    backdrop-filter: none;
  }

  .tabs {
    position: static;       /* .chrome is what sticks */
    background: transparent;
    border-top: 0;
    border-bottom: 1px solid var(--line);
    padding: 0 16px;
    justify-content: center;
    gap: 4px;
  }
  .tabs a {
    flex: 0 0 auto;
    height: 48px;
    font-size: 1.15rem;
    padding: 0 16px;
    border-top: 0;
    border-bottom: 2px solid transparent;
  }
  .tabs a[aria-current="page"],
  .tabs a.here {
    border-bottom-color: var(--accent-text);
    background: transparent;
  }
}

/* ---------- cover art ---------- */

/* A letterbox cut from the campfire picture: the cowboy and the fire,
   with the treetops above and the ground below cropped away.  The foot
   fades out into the page, so the art sits on either theme without a
   hard edge.  About 3:1 at full width, close to the phone's shape, so
   the fire clears the fade on a desktop too. */
.cover {
  max-width: 1400px;
  margin: 0 auto;
  padding: 16px 16px 0;
}
.cover img {
  display: block;
  width: 100%;
  height: clamp(120px, 30vw, 420px);
  object-fit: cover;
  object-position: 50% 65%;
  border-radius: var(--radius);
  -webkit-mask-image: linear-gradient(to bottom, #000 72%, transparent);
          mask-image: linear-gradient(to bottom, #000 72%, transparent);
}

/* ---------- layout ---------- */

main { padding: 16px; max-width: 1400px; margin: 0 auto; }

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

/* The search box has the first row to itself, full width, as on
   Inventory; the filters and the sort follow on the rows under it. */
.search-wrap {
  position: relative;
  flex: 1 1 100%;
  min-width: 0;
}
.search {
  width: 100%;
  padding: 9px 34px 9px 12px;
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.search::placeholder { color: var(--ink-faint); }

/* The box's own cancel glyph is dropped in favour of `.search-clear`,
   which every browser draws the same way and which sits in the same
   spot a "search" input already reserved. */
.search::-webkit-search-cancel-button { appearance: none; }

/* The one "×" the app draws, on a toast and on a search box alike:
   same glyph, same box, same hover, wherever something needs closing
   or clearing. */
.close-btn {
  font: inherit;
  line-height: 1;
  color: var(--ink-faint);
  background: none;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
}
.close-btn:hover { color: var(--ink); background: var(--line); }

.search-clear {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  width: 28px;
  height: 28px;
  font-size: 1.2rem;
}


.chips { display: flex; flex-wrap: wrap; gap: 6px; }

.chip {
  padding: 7px 13px;
  font: inherit;
  font-size: .82rem;
  color: var(--ink-dim);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
.chip[aria-pressed="true"] {
  color: var(--bg);
  background: var(--ink);
  border-color: var(--ink);
}

/* A station's or a place's icon, beside its name.  Each is a stamped
   medallion -- a filled disc with the glyph cut from it -- generated
   in two colourways, so the ones drawn here are only ever named, never
   pathed: which image loads is decided below, by theme. */
.icon {
  display: inline-block;
  width: 1.2em;
  height: 1.2em;
  margin-right: .4em;
  vertical-align: -.3em;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

.icon-animals   { background-image: url('images/icons/parchment/animals-inverse.png'); }
.icon-plants    { background-image: url('images/icons/parchment/plants-inverse.png'); }
.icon-supplies  { background-image: url('images/icons/parchment/supplies-inverse.png'); }
.icon-crafting  { background-image: url('images/icons/parchment/crafting-inverse.png'); }
.icon-campfire  { background-image: url('images/icons/parchment/campfire-inverse.png'); }
.icon-satchel   { background-image: url('images/icons/parchment/satchel-inverse.png'); }
.icon-pearson   { background-image: url('images/icons/parchment/pearson-inverse.png'); }
.icon-trapper   { background-image: url('images/icons/parchment/trapper-inverse.png'); }
.icon-fence     { background-image: url('images/icons/parchment/fence-inverse.png'); }
.icon-crafted   { background-image: url('images/icons/parchment/checkbox-checked.png'); }
.icon-uncrafted { background-image: url('images/icons/parchment/checkbox-unchecked.png'); }
/* The four pages, on the tabs. */
.icon-materials { background-image: url('images/icons/parchment/materials-inverse.png'); }
.icon-recipes   { background-image: url('images/icons/parchment/recipes-inverse.png'); }
.icon-inventory { background-image: url('images/icons/parchment/inventory-inverse.png'); }
.icon-settings  { background-image: url('images/icons/parchment/settings-inverse.png'); }

:root[data-theme="leather"] .icon-crafted   { background-image: url('images/icons/leather/checkbox-checked.png'); }
:root[data-theme="leather"] .icon-uncrafted { background-image: url('images/icons/leather/checkbox-unchecked.png'); }
:root[data-theme="leather"] .icon-animals   { background-image: url('images/icons/leather/animals-inverse.png'); }
:root[data-theme="leather"] .icon-plants    { background-image: url('images/icons/leather/plants-inverse.png'); }
:root[data-theme="leather"] .icon-supplies  { background-image: url('images/icons/leather/supplies-inverse.png'); }
:root[data-theme="leather"] .icon-crafting  { background-image: url('images/icons/leather/crafting-inverse.png'); }
:root[data-theme="leather"] .icon-campfire  { background-image: url('images/icons/leather/campfire-inverse.png'); }
:root[data-theme="leather"] .icon-satchel   { background-image: url('images/icons/leather/satchel-inverse.png'); }
:root[data-theme="leather"] .icon-pearson   { background-image: url('images/icons/leather/pearson-inverse.png'); }
:root[data-theme="leather"] .icon-trapper   { background-image: url('images/icons/leather/trapper-inverse.png'); }
:root[data-theme="leather"] .icon-fence     { background-image: url('images/icons/leather/fence-inverse.png'); }
:root[data-theme="leather"] .icon-materials { background-image: url('images/icons/leather/materials-inverse.png'); }
:root[data-theme="leather"] .icon-recipes   { background-image: url('images/icons/leather/recipes-inverse.png'); }
:root[data-theme="leather"] .icon-inventory { background-image: url('images/icons/leather/inventory-inverse.png'); }
:root[data-theme="leather"] .icon-settings  { background-image: url('images/icons/leather/settings-inverse.png'); }

/* The coffee cup sits on the amber Buy me a coffee button, under black
   lettering, in both themes -- so it takes the dark disc in both, which
   is parchment's inverse but leather's plain one. */
.icon-coffee { background-image: url('images/icons/parchment/coffee-inverse.png'); }
:root[data-theme="leather"] .icon-coffee { background-image: url('images/icons/leather/coffee.png'); }

/* A pressed chip or a chosen tab turns its label over -- pale on
   parchment, dark on leather -- and the medallion's disc follows the
   label, so the other colourway takes its place. */
:is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-animals  { background-image: url('images/icons/parchment/animals.png'); }
:is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-plants   { background-image: url('images/icons/parchment/plants.png'); }
:is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-supplies { background-image: url('images/icons/parchment/supplies.png'); }
:is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-crafting { background-image: url('images/icons/parchment/crafting.png'); }
:is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-campfire { background-image: url('images/icons/parchment/campfire.png'); }
:is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-satchel  { background-image: url('images/icons/parchment/satchel.png'); }
:is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-pearson  { background-image: url('images/icons/parchment/pearson.png'); }
:is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-trapper  { background-image: url('images/icons/parchment/trapper.png'); }
:is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-fence    { background-image: url('images/icons/parchment/fence.png'); }

:root[data-theme="leather"] :is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-animals  { background-image: url('images/icons/leather/animals.png'); }
:root[data-theme="leather"] :is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-plants   { background-image: url('images/icons/leather/plants.png'); }
:root[data-theme="leather"] :is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-supplies { background-image: url('images/icons/leather/supplies.png'); }
:root[data-theme="leather"] :is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-crafting { background-image: url('images/icons/leather/crafting.png'); }
:root[data-theme="leather"] :is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-campfire { background-image: url('images/icons/leather/campfire.png'); }
:root[data-theme="leather"] :is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-satchel  { background-image: url('images/icons/leather/satchel.png'); }
:root[data-theme="leather"] :is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-pearson  { background-image: url('images/icons/leather/pearson.png'); }
:root[data-theme="leather"] :is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-trapper  { background-image: url('images/icons/leather/trapper.png'); }
:root[data-theme="leather"] :is(.chip[aria-pressed="true"], .segmented button[aria-selected="true"]) .icon-fence    { background-image: url('images/icons/leather/fence.png'); }

/* A card name's last word and the marks after it, wrapped as one. */
.keep { white-space: nowrap; }

/* A recipe card's station, as its icon alone after the name: it trails
   the name, so the gap goes before it rather than after. */
.station-mark { margin: 0 0 0 6px; }

/* A dropdown filter: category on both galleries. */
.select {
  flex: 0 1 auto;
  min-width: 0;
  padding: 9px 12px;
  font: inherit;
  font-size: .86rem;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* A sort control sits after the filters, left-aligned with them.
   Both galleries use this, so the rule serves Materials and Recipes. */
.sort {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: .8rem;
  color: var(--ink-faint);
}
.sort .select { font-size: .82rem; padding: 7px 10px; }

/* A rule between the filter chips and the sort, so the two groups read
   as two groups rather than one long row of controls.  Drawn rather
   than typed: a 1px box sits at a fixed height and centres itself on
   the row, where a glyph would ride the text baseline and change size
   with the font.

   Below the wide breakpoint the toolbar wraps and the sort starts its
   own line, where a leading divider would separate nothing. */
.sort::before {
  content: "";
  flex: 0 0 auto;
  width: 1px;
  height: 22px;
  background: var(--line);
  margin-right: 4px;
}
@media (max-width: 719px) {
  .sort::before { display: none; }
}

/* The direction says what it does, rather than an arrow you have
   to work out: "Most first", "A-Z", "Legendary first". */
.sort-dir {
  padding: 7px 11px;
  font: inherit;
  font-size: .82rem;
  color: var(--ink-dim);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  white-space: nowrap;
}
.sort-dir:hover { color: var(--ink); border-color: var(--ink-faint); }

/* Filters and sort read from the left; the count alone is pushed right. */
.count { font-size: .8rem; color: var(--ink-faint); margin-left: auto; }

/* ---------- material card ---------- */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 13px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Hover is transient and never means status, so the accent can be spent
   here for free.  Guarded, so a touch screen does not leave one card
   stuck looking live. */
@media (hover: hover) {
  .card:hover { border-color: var(--accent-text); }

  /* the station stripe outranks the hover edge */
  .card.recipe.blue:hover   { border-left-color: var(--station-blue); }
  .card.recipe.yellow:hover { border-left-color: var(--station-yellow); }
  .card.recipe.pink:hover   { border-left-color: var(--station-pink); }
  .card.recipe.green:hover  { border-left-color: var(--station-green); }
}

.card.ghost {
  height: 150px;
  border-style: dashed;
  opacity: .35;
  animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: .15; } }

/* A name is never a status, so it can take colour without spending any
   of the meaning that amber and red carry elsewhere. */
.card h3 { margin: 0; font-size: 1.08rem; line-height: 1.25; color: var(--ink-name); }

/* The verdict in a material's detail view.  Red and green are the
   signals they are everywhere else -- short of it, or holding enough.
   "Done with it" and "spare" are not statuses to act on, so they stay
   in the ink greys rather than spending another colour. */
.hint { color: var(--ink-faint); font-style: italic; }
.hint.enough { color: var(--ok); }
.hint.short  { color: var(--danger-text); }
.hint.spare  { color: var(--ink-dim); }

/* A material card opens its detail view from anywhere on it.  The name
   is the button that does it -- dressed as the heading it replaces --
   so the card keeps one tab stop and a screen reader one control. */
.card.material, .card.recipe { cursor: pointer; }

.card-open {
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  text-align: left;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
@media (hover: hover) {
  .card.material:hover .card-open,
  .card.recipe:hover .card-open {
    text-decoration: underline;
    text-underline-offset: 3px;
  }
}

.badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  font-size: .68rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  border-radius: 999px;
  border: 1px solid currentColor;
  color: var(--ink-dim);
  vertical-align: 1px;
}

/* Quality as the game stamps it: three stars, grey or gold, beside a
   material's name wherever one is listed.  The box is the stars' own
   proportions, a little wider than tall. */
.stars {
  display: inline-block;
  width: 1.5em;
  height: 1.2em;
  margin-left: 6px;
  vertical-align: -.25em;
  background: no-repeat center / contain;
}
.stars-perfect   { background-image: url('images/icons/parchment/stars-perfect.png'); }
.stars-legendary { background-image: url('images/icons/parchment/stars-legendary.png'); }

/* Each theme takes its own pair rather than sharing one: a grey and a
   gold pitched for paper read as the same muddy smudge on leather, and
   the other way round.  Cool grey against warm gold in both, so the
   two are never just two shades of the one colour. */
:root[data-theme="leather"] .stars-perfect {
  background-image: url('images/icons/leather/stars-perfect.png');
}
:root[data-theme="leather"] .stars-legendary {
  background-image: url('images/icons/leather/stars-legendary.png');
}

/* In a dialog's facts the stars lead the word instead of trailing a name. */
.trait .stars { margin: 0 .4em 0 0; }

/* A station as a chip: its stripe colour, so the tag, the card's edge
   and the demand rows all name the Trapper the same way. */
.badge.station        { color: var(--ink-dim); }
.badge.station.blue   { color: var(--station-blue); }
.badge.station.yellow { color: var(--station-yellow); }
.badge.station.pink   { color: var(--station-pink); }
.badge.station.green  { color: var(--station-green); }

/* Crafted and Skipped: settled, so in the quiet ink rather than a colour. */
.badge.state { color: var(--ink-faint); border-style: dashed; }

/* demand rows, one per station that wants this material */

.demands { display: flex; flex-direction: column; gap: 6px; }

/* Settings' crafted-by-category tally: the same demand row, but two
   to a line once there is room, since two dozen categories in one
   narrow column would make the panel the tallest thing on the page. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 6px 16px;
}

.demand {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 8px;
  padding-left: 9px;
  border-left: 3px solid var(--line);
  font-size: .85rem;
}
.demand.blue   { border-left-color: var(--station-blue); }
.demand.yellow { border-left-color: var(--station-yellow); }
.demand.pink   { border-left-color: var(--station-pink); }
.demand.green  { border-left-color: var(--station-green); }

.demand .station { color: var(--ink-dim); }
.demand .qty { font-variant-numeric: tabular-nums; white-space: nowrap; }
.demand .qty .have { color: var(--ok); }
.demand .qty .short { color: var(--danger-text); }

.bar {
  width: 52px;
  height: 4px;
  border-radius: 2px;
  background: var(--surface-2);
  overflow: hidden;
}
.bar > i {
  display: block;
  height: 100%;
  background: var(--ok);
}
.bar.short > i { background: var(--danger-text); }

/* A tick and a cross are icons rather than type: the typewriter face has
   neither, and left to itself the browser picks a different font for each
   one -- ✓ is in far more fonts than ✗ -- which lands them at two
   different weights side by side. */
.mark {
  font-family: var(--font-symbol);
  font-weight: 700;
}

/* The have/short and made/open states use the same checkbox icon the
   card corners do, sized down to sit level with the text beside it
   rather than the .icon default made for a line of its own.  Its row
   is a grid aligned on the text's baseline, which an empty icon has
   none of; centering it against that baseline is what actually lines
   it up with the text, where vertical-align (for inline flow, not a
   grid) does nothing. */
.mark.icon {
  width: 1.3em;
  height: 1.3em;
  margin: -2px 0 0;
  align-self: center;
}

/* ---------- a count riding along on a tab ---------- */

.tab-count {
  margin-left: 6px;
  font-size: .78rem;
  font-variant-numeric: tabular-nums;
  opacity: .6;
}

/* ---------- "used in" and "vendors" on a material card ---------- */

/* The captions on a card -- Used in, Vendors, Ingredients -- tight to
   the list they head and held apart from whatever came before, so a
   card reads as labelled blocks rather than one run-on list.  Sentence
   case rather than the small caps other captions get: sitting this
   close to the plain text under them, all-caps read louder than it
   meant to.  Both margins lean on the card's own `gap` (10px, see
   .card) rather than fighting it: the negative bottom closes most of
   that gap, the positive top adds to it.  `.list-label.card-label`
   rather than `.card-label` alone so this outranks the plain
   `.list-label` rule regardless of which comes later in the file.
   The rule trails the caption on its own line, filling what is left
   of it, rather than sitting above or below as a full border. */
.list-label.card-label {
  margin: 12px 0 -6px;
  text-transform: none;
  display: flex;
  align-items: center;
  gap: 8px;
}
.list-label.card-label::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line);
}

.used-in {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: .8rem;
}

.used-in > li {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: 6px;
  color: var(--ink-dim);
}
.used-in .mark { font-size: .85rem; line-height: 1; }
.used-in li.made          { color: var(--ink-faint); }
.used-in li.retired       { color: var(--ink-faint); }
.used-in li.made .what    { text-decoration: line-through; }
.used-in li.retired .what { text-decoration: line-through; }

.used-in .more {
  color: var(--ink-faint);
  font-style: italic;
  display: block;
}

/* Cross-links: the recipes named on a material's card, the materials
   named on a recipe's.  A card can carry a dozen of them, so they are
   drawn as the surrounding text with the underline turned most of the
   way down -- enough to read as links in a list, not enough to turn a
   card into a page of blue.  Hovered or tabbed to, one comes up to the
   accent and takes its full underline. */
.xlink {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 32%, transparent);
  text-underline-offset: 2px;
}
.xlink:hover,
.xlink:focus-visible {
  color: var(--accent-text);
  text-decoration-color: currentColor;
}

/* Buttons dressed as links: "and 6 more" on a card, "Ledger entries"
   on Settings, Undo in the toast.  Buttons, not anchors: there is no
   address for "this list, opened", and an <a href="#"> would put one
   in the reader's history every time. */
.more-link, .fact-open, .toast .action {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.more-link:hover, .fact-open:hover { color: var(--accent-text); }
.fact-open { color: var(--ink); }
.toast .action { color: var(--accent-text); }

/* ---------- detail dialogs: material, recipe, ledger ---------- */

/* The page behind does not scroll under a modal. */
body:has(dialog.detail[open]) { overflow: hidden; }

/* No padding on the <dialog> itself: .detail-body covers the whole box,
   so a click that lands on the dialog element can only be the backdrop. */
.detail {
  width: min(560px, calc(100vw - 32px));
  max-height: calc(100dvh - 48px);
  padding: 0;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: auto;
  overscroll-behavior: contain;
}
.detail::backdrop {
  background: rgb(20 12 6 / .55);
  backdrop-filter: blur(2px);
}
.detail[open] { animation: detail-in .16s ease-out; }
@keyframes detail-in {
  from { opacity: 0; transform: translateY(8px); }
}
@media (prefers-reduced-motion: reduce) {
  .detail[open] { animation: none; }
}

.detail-body {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 18px 20px 20px;
}

/* The dock: the dialog's one job -- the steppers, the Craft button --
   held at the foot of the box while everything above scrolls under
   it.  Full width, on the dialog's own surface so nothing shows
   through, with a rule and a faint shadow to say there is more above.
   The body drops its foot padding for it, so it sits flush with the
   bottom edge instead of floating 20px over the scrolling text. */
.detail-body:has(> .detail-dock) { padding-bottom: 0; }
.detail-dock {
  position: sticky;
  bottom: 0;
  z-index: 1;
  margin: 0 -20px;
  padding: 14px 20px 16px;
  background: var(--surface);
  border-top: 1px solid var(--line);
  box-shadow: 0 -10px 14px -12px rgb(20 12 6 / .35);
}

.detail-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.detail-head > div { flex: 1; min-width: 0; }
.detail-kicker {
  margin: 0 0 4px;
  font-size: .74rem;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: .08em;
}
.detail-head h2 {
  margin: 0;
  font-size: 1.6rem;
  line-height: 1.15;
  color: var(--ink-name);
  letter-spacing: .05em;
}

.detail-close {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  /* the typewriter face has no ×; see .mark */
  font: 1.4rem/1 var(--font-symbol);
  color: var(--ink-dim);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}
.detail-close:hover { color: var(--ink); border-color: var(--ink-faint); }

/* the facts: a two-column ledger of label over value */
.traits {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 1px;
  margin: 0;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.trait {
  padding: 9px 12px;
  background: var(--page-wash);
}
.trait dt {
  margin-bottom: 3px;
  font-size: .7rem;
  letter-spacing: .08em;
  color: var(--ink-label);
}
.trait dd { margin: 0; font-size: .92rem; }

/* A dialog's captions are written as they are said, "Recipes used in",
   the way a card's are, rather than shouted like the page's own. */
.detail-section .list-label { margin-bottom: 8px; text-transform: none; }
.detail .hint { margin: 0; font-size: .92rem; }

/* Running text in a dialog -- About and How to use: paragraphs and a
   short list in the body face, at a size meant to be read through
   rather than scanned. */
.prose { color: var(--ink-dim); font-size: .9rem; line-height: 1.6; }
.prose p, .prose ul { margin: 0 0 10px; }
.prose > :last-child { margin-bottom: 0; }
.prose ul { padding-left: 20px; }
.prose li + li { margin-top: 6px; }
.prose strong { color: var(--ink); font-weight: 600; }

/* A dialog's boxed list: recipes a material goes into, a recipe's buffs. */
.detail-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.detail-list li {
  display: flex;
  gap: 12px;
  padding: 8px 12px;
  font-size: .88rem;
  border-bottom: 1px solid var(--line);
}
.detail-list li:last-child { border-bottom: 0; }

.detail-recipes li { align-items: center; background: var(--surface); }
.detail-recipes .what { flex: 1; min-width: 0; }
.detail-recipes .what small {
  display: block;
  margin-top: 1px;
  font-size: .72rem;
  color: var(--ink-faint);
}
.detail-recipes .state {
  flex: 0 0 auto;
  padding: 2px 8px;
  font-size: .72rem;
  border: 1px solid currentColor;
  border-radius: 999px;
}
.detail-recipes li.retired .state { color: var(--ink-faint); }
.detail-recipes li.made .what,
.detail-recipes li.retired .what  { color: var(--ink-faint); }

/* The animals a material comes from: the name, and how to take it --
   a weapon, or the bait for a fish -- set to the right. */
.detail-animals li { align-items: baseline; background: var(--surface); }
.detail-animals .what { flex: 1; min-width: 0; }
.detail-animals .how {
  flex: 0 1 auto;
  text-align: right;
  font-size: .78rem;
  color: var(--ink-dim);
}

.detail-stock { display: flex; flex-direction: column; gap: 10px; }

.stock-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  padding: 6px 0 6px 12px;
  font-size: .9rem;
}
.stock-line .stock-text { flex: 1 1 180px; }
.stock-line .station { color: var(--ink); }
.stock-line .have  { color: var(--ok); }
.stock-line .short { color: var(--danger-text); }
.stock-line .from {
  display: block;
  margin-top: 1px;
  font-size: .72rem;
  color: var(--ink-faint);
}
.stock-line.retired .stock-text { color: var(--ink-faint); }

/* "+ Add to Trapper" says where the one goes, so it is wider than a
   plain stepper button and sized to its words. */
.stock-line .stepper button.add {
  width: auto;
  padding: 0 12px;
  font-size: .86rem;
  white-space: nowrap;
}


/* ---------- recipe card ---------- */

/* The station's colour runs down the edge, as on a material card. */
.card.recipe { border-left-width: 3px; }
.card.recipe.blue   { border-left-color: var(--station-blue); }
.card.recipe.yellow { border-left-color: var(--station-yellow); }
.card.recipe.pink   { border-left-color: var(--station-pink); }
.card.recipe.green  { border-left-color: var(--station-green); }
.card.recipe.settled { opacity: .62; }

/* The name gets the whole width, and the price sits under it, flush
   right: beside the name it took the room a long name needed, and
   pushed the station's icon down to a line of its own. */
.recipe-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* The name on the left, the done checkbox pinned to the right on the
   same line -- top-aligned, so a name that wraps to two lines grows
   downward under it rather than pushing it off centre.  Shared by a
   recipe card and a material card alike: whichever is done gets the
   same mark, in the same place. */
.card-title {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}
.card-title h3 { flex: 1; min-width: 0; }

.crafted-mark {
  flex: none;
  width: 1.3em;
  height: 1.3em;
  margin: 0;
  vertical-align: 0;
}

/* A card's own done mark, in its title -- ~20% bigger than the same
   checkbox drawn small beside a line in a dialog's used-in/recipe list. */
.card-title .crafted-mark {
  width: 1.56em;
  height: 1.56em;
}

/* Flush right as before, but with a rule leading into it on the same
   line, filling whatever room the name above did not need.  A free
   recipe has no price text, so the row would otherwise be nothing but
   the rule's own 1px and collapse -- min-height pins it to the same
   line height a priced row gets from its text, so the rule sits the
   same distance under the name either way, and whatever comes after
   the card starts at the same height as its neighbours. */
.price {
  align-self: stretch;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 1.5em;
  font-size: .82rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}
.price::before {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* Buff text is one line for most recipes.  The saddles list five stats,
   which the data stores one per line, and the card marks up as a <ul>. */
.buff {
  margin: 0;
  padding: 8px 10px;
  font-size: .8rem;
  line-height: 1.45;
  white-space: pre-line;
  color: var(--ink-dim);
  background: var(--surface-2);
  border-radius: 7px;
}

/* The marker is a hyphen rather than a disc: it is the one the typewriter
   has, and a disc would be drawn by some other font. */
ul.buff {
  white-space: normal;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
ul.buff li {
  padding-left: 1.1em;
  text-indent: -1.1em;
}
ul.buff li::before {
  content: "- ";
  color: var(--ink-faint);
}

.ingredients {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 5px;
  font-size: .85rem;
}

.ingredients li {
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  align-items: baseline;
  gap: 7px;
}
.ingredients li:not(.have):not(.short) { grid-template-columns: auto 1fr; }

.ingredients .mark { font-size: .9rem; line-height: 1; }
.ingredients li.short .what  { color: var(--ink-dim); }

.ingredients .qty { color: var(--ink-dim); font-variant-numeric: tabular-nums; }

/* A campfire slot that takes any one of several: "any of" as a small
   caption, and how many of each you hold beside its name. */
.ingredients .any-of {
  font-size: .72rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.ingredients .opt-have {
  color: var(--ink-faint);
  font-size: .78rem;
  font-variant-numeric: tabular-nums;
}
.ingredients .tally {
  color: var(--ink-faint);
  font-size: .78rem;
  font-variant-numeric: tabular-nums;
}

/* ---------- recipe detail dialog ---------- */

/* Buffs as a stat and its value, the values in a column of their own. */
.detail-buffs li { align-items: baseline; padding-block: 7px; background: var(--page-wash); }
.detail-buffs .stat  { flex: 1; min-width: 0; color: var(--ink-dim); }
.detail-buffs .value {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.detail-ingredients + .hint { margin-top: 8px; }

.detail-ingredients {
  padding: 10px 12px;
  font-size: .9rem;
  gap: 7px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.craft-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.craft-row .craft-btn { flex: 0 0 auto; padding: 10px 22px; font-size: .95rem; }
.craft-why {
  flex: 1;
  margin: 0;
  font-size: .8rem;
  color: var(--ink-faint);
}
.craft-why.ready { color: var(--ok); }

/* The want switch in the dialog's corner: both words always showing,
   the one that is in force drawn in ink, the other faint. */
.want-switch,
.mode-switch {
  flex: 0 0 auto;
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 4px;
  font: inherit;
  font-size: .76rem;
  color: var(--ink-faint);
  background: none;
  border: 0;
  cursor: pointer;
}
.want-switch .side,
.mode-switch .side { white-space: nowrap; }
.want-switch[aria-checked="true"]  .side.on,
.want-switch[aria-checked="false"] .side.off { color: var(--ink); }

.want-switch .track,
.mode-switch .track {
  position: relative;
  width: 36px;
  height: 20px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  transition: background .15s;
}
.want-switch .track i,
.mode-switch .track i {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--ink-faint);
  transition: transform .15s, background .15s;
}
.want-switch[aria-checked="true"] .track {
  background: color-mix(in srgb, var(--ok) 22%, var(--surface-2));
  border-color: var(--ok);
}
.want-switch[aria-checked="true"] .track i {
  transform: translateX(16px);
  background: var(--ok);
}
.want-switch:hover .track,
.mode-switch:hover .track { border-color: var(--ink-faint); }
@media (prefers-reduced-motion: reduce) {
  .want-switch .track, .want-switch .track i,
  .mode-switch .track, .mode-switch .track i { transition: none; }
}

/* On a phone the switch drops under the title rather than squeezing it. */
@media (max-width: 480px) {
  .detail-head { flex-wrap: wrap; }
  .detail-head > div { flex-basis: calc(100% - 48px); }
  .detail-head .want-switch { order: 3; margin-left: -4px; }
}

.craft-btn {
  padding: 9px 14px;
  font: inherit;
  font-size: .86rem;
  font-weight: 600;
  color: var(--on-accent);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 8px;
  cursor: pointer;
}
.craft-btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 85%, var(--rdr-white));
}
.craft-btn:disabled {
  color: var(--ink-faint);
  background: none;
  border-color: var(--line);
  cursor: not-allowed;
}

.ghost-btn {
  padding: 9px 12px;
  font: inherit;
  font-size: .82rem;
  color: var(--ink-dim);
  background: none;
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}
.ghost-btn:hover { color: var(--ink); border-color: var(--ink-faint); }

/* ---------- inventory: entry, one location at a time ---------- */

.segmented {
  display: flex;
  gap: 2px;
  padding: 2px;
  margin-bottom: 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.segmented button {
  flex: 1;
  padding: 10px 6px;
  font: inherit;
  font-size: .88rem;
  color: var(--ink-dim);
  background: none;
  border: 0;
  border-radius: calc(var(--radius) - 3px);
  cursor: pointer;
}
.segmented button[aria-selected="true"] {
  color: var(--bg);
  background: var(--ink);
}

/* The captions -- USED IN, LOCATIONS, INGREDIENTS, IN THE SATCHEL --
   one family by shape and by colour. */
.list-label {
  margin: 0 0 8px;
  font-size: .76rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-label);
}

/* The way back in, above an empty Inventory: a note and one button,
   set apart the way a Settings panel is. */
.restore-offer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 16px;
  margin-bottom: 20px;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent-text);
  border-radius: var(--radius);
}
.restore-offer .note { flex: 1 1 320px; }

/* A rule between "In the Satchel" and "Recently Touched", with room
   on both sides of it -- the two are different questions ("what am I
   holding" vs. "what have I touched lately") and read as one run-on
   list without a break between them. */
.stock-section + .stock-section {
  margin-top: 28px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

/* A section's name on the left, how many rows it holds on the right --
   the same split as the toolbars on Materials and Recipes. */
.section-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 8px;
}
/* Sentence case rather than the small caps every other list-label
   gets: this one is closer to a sentence than a tag ("In the
   Satchel", "6 Matches") and reads oddly shouted. */
.section-head .list-label { margin: 0; text-transform: none; }

.rows {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 12px;
  padding: 8px 10px 8px 14px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.row:last-child { border-bottom: 0; }

.row .name { flex: 1 1 140px; min-width: 0; font-size: .92rem; }

/* The transfer stepper(s), if any, and the main stepper travel
   together: if the row is too narrow for both beside the name, the
   pair wraps to a line of its own rather than splitting apart. */
.row .controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
  flex: 0 0 auto;
  margin-left: auto;
}

.row .history {
  display: block;
  margin-top: 1px;
  font-size: .74rem;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.stepper {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
}

/* 40px of target: comfortable with a thumb, unobtrusive with a mouse */
.stepper button {
  width: 40px;
  height: 40px;
  font: inherit;
  font-size: 1.2rem;
  line-height: 1;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}
.stepper button:hover:not(:disabled) { border-color: var(--ink-faint); }
.stepper button:active:not(:disabled) { background: var(--line); }
.stepper button:disabled { opacity: .3; cursor: default; }

.stepper output {
  min-width: 2.2ch;
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}
.stepper output.held { color: var(--ok); font-weight: 600; }

/* The row's way into a transfer.  Told apart from the stepper beside
   it by everything but height: an outlined pill against filled
   squares, a word against a sign, with clear air between them -- so
   a thumb aimed at one has nothing that looks like it to land on.
   The same 40px tall, so a row with one sits on the same line as a
   row without. */
.move-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 40px;
  margin-right: 6px;
  padding: 0 12px 0 14px;
  font: inherit;
  font-size: .8rem;
  color: var(--ink-dim);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
.move-btn:hover { color: var(--ink); border-color: var(--ink-faint); }
.move-btn[aria-expanded="true"] { border-color: var(--accent-text); color: var(--ink); }
.move-btn .move-to { display: inline-flex; gap: 2px; }
.move-btn .move-to .icon { margin: 0; width: 1.25em; height: 1.25em; }

/* A rule between the two controls, where a row has both: one moves
   what is already here, the other changes how much there is. */
.move-btn + .stepper {
  padding-left: 12px;
  border-left: 1px solid var(--line);
}

/* Something staged to move: tinted like the staged row it sits on,
   with the count in the button. */
.move-btn.staged {
  color: var(--accent-text);
  border-color: var(--accent-text);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.move-count {
  min-width: 1.5em;
  padding: 1px 5px;
  font-size: .74rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
  color: var(--on-accent);
  background: var(--accent);
  border-radius: 999px;
}

/* On a phone there is no room for both controls beside the name, and
   wrapping them onto a line of their own puts Move right up against
   the stepper.  Instead Move tucks under the name on the left and the
   stepper keeps the right edge: the two are a row's width apart. */
@media (max-width: 559px) {
  .row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 6px 12px;
  }
  .row .controls { display: contents; }
  .row .name    { grid-column: 1; grid-row: 1; }
  .row .stepper { grid-column: 2; grid-row: 1; }
  .row:has(.move-btn) .stepper { grid-row: 1 / span 2; }
  .row .move-btn {
    grid-column: 1;
    grid-row: 2;
    justify-self: start;
    height: 34px;
    margin: 0;
  }
}

/* The Move panel: beside its button on a wide screen, placed by
   inventory.js; a sheet along the bottom on a phone. */
.move-pop {
  position: fixed;
  inset: auto;
  margin: 0;
  width: min(340px, calc(100vw - 16px));
  padding: 16px 16px 14px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.move-pop:popover-open { animation: detail-in .14s ease-out; }
.move-pop::backdrop { background: transparent; }

@media (max-width: 559px) {
  .move-pop {
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-width: none;
    padding: 18px 16px 16px;
    border-width: 1px 0 0;
    border-radius: var(--radius) var(--radius) 0 0;
  }
  .move-pop::backdrop { background: rgb(20 12 6 / .45); }
}
@media (prefers-reduced-motion: reduce) {
  .move-pop:popover-open { animation: none; }
}

.move-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
.move-head > div { flex: 1; min-width: 0; }
.move-kicker {
  margin: 0 0 4px;
  font-size: .74rem;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: .08em;
}
.move-left { margin-left: 6px; color: var(--ink-dim); text-transform: none; letter-spacing: 0; }
.move-left::before { content: "\00b7"; margin-right: 6px; color: var(--ink-faint); }
.move-head h3 {
  margin: 0;
  font-size: 1.15rem;
  line-height: 1.2;
  color: var(--ink-name);
}
.move-head .detail-close { width: 32px; height: 32px; font-size: 1.2rem; }

.move-dests { list-style: none; margin: 0; padding: 0; }
.move-dest {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.move-place { flex: 1; display: inline-flex; align-items: center; gap: 8px; font-size: .92rem; }
.move-place .icon { margin: 0; width: 1.5em; height: 1.5em; }
.move-dest.staged .move-place { color: var(--accent-text); }

.move-note {
  margin: 10px 0 0;
  font-size: .76rem;
  line-height: 1.45;
  color: var(--ink-faint);
}

/* pending, unsaved changes */

.row.staged { background: color-mix(in srgb, var(--accent) 7%, var(--surface)); }

.stepper output.pending { color: var(--accent-text); font-weight: 600; }
.stepper output small {
  margin-left: 3px;
  font-size: .72rem;
  font-weight: 500;
  opacity: .85;
}

/* The commit bar: within a thumb's reach, above the tabs. */
.savebar {
  position: fixed;
  left: 50%;
  bottom: calc(var(--tabs-h) + 12px);
  transform: translateX(-50%);
  z-index: 15;
  display: flex;
  align-items: center;
  gap: 10px;
  width: calc(100% - 32px);
  max-width: 520px;
  padding: 10px 10px 10px 16px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
@media (min-width: 720px) { .savebar { bottom: 20px; } }

.savebar .pending-count { flex: 1; font-size: .86rem; }

.savebar button {
  font: inherit;
  font-size: .86rem;
  padding: 9px 16px;
  border-radius: 8px;
  cursor: pointer;
}
.savebar .discard {
  color: var(--ink-dim);
  background: none;
  border: 1px solid var(--line);
}
.savebar .save {
  color: var(--on-accent);
  background: var(--accent);
  border: 1px solid var(--accent);
}

/* a dot beside the tab label while changes are uncommitted */
.tabs a[data-pending]::after {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
}

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

/* Tiles in a row share one height, as the Materials and Recipes cards
   do, rather than each stopping where its own text runs out. */
.settings {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 14px;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.panel.danger { border-color: color-mix(in srgb, var(--danger) 55%, var(--line)); }

/* The words beside a pair of buttons -- Settings' erase confirmation. */
.state-label {
  flex: 1;
  font-size: .8rem;
  color: var(--ink-faint);
}


/* ---------- ledger entries ---------- */

/* In the dialog, the note sits tight under the heading. */
.detail .note { margin: 0; }
.detail .pager { margin-top: 0; }

.entries {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
}

/* Three columns at any width, so nothing has to scroll sideways:
   the amount, what and where, and why. */
.entry {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: 10px;
  padding: 8px 12px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}
.entry:last-child { border-bottom: 0; }

.entry .delta {
  min-width: 2.6ch;
  font-size: .9rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.entry .delta.up { color: var(--ok); }
.entry .delta.down { color: var(--danger-text); }

.entry-what { min-width: 0; font-size: .88rem; }
.entry-what small {
  display: block;
  margin-top: 1px;
  font-size: .74rem;
  color: var(--ink-faint);
}

.entry .reason {
  font-size: .74rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-label);
  white-space: nowrap;
}

.badge.stale { color: var(--danger-text); }

.panel h3 {
  margin: 0;
  font-size: .78rem;
  letter-spacing: .07em;
  color: var(--ink-label);
}

/* Settings' captions, the tiles' own and the ones inside them, are
   written in sentence case, the way a dialog's are. */
.panel .list-label { margin: 0 0 8px; text-transform: none; }

/* A tile with more to it than a note and a button takes the whole row. */
.panel-wide { grid-column: 1 / -1; }

.note {
  margin: 0;
  font-size: .85rem;
  line-height: 1.5;
  color: var(--ink-dim);
}
.note strong { color: var(--ink); font-weight: 600; }

.panel-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }

/* A tile's controls sit on its bottom edge, so a short tile stretched
   to its row's height has them level with its neighbours' rather than
   floating under its text.  The auto margin goes on the first control,
   which carries everything after it down too: in Bring it back, the
   paste box and the preview stay right under its button. */
.panel > .panel-actions,
.panel > .segmented { margin-top: auto; }

/* The theme switch, cut to a button's height and without the gap
   Inventory leaves under it, so its edges line up with the buttons
   in the tiles beside it. */
.panel > .segmented { margin-bottom: 0; }

/* The import preview is empty until a file is read, and an empty box
   still takes the tile's gap below the buttons -- enough to lift them
   out of line with the next tile's. */
#s-preview:empty { display: none; }
.panel > .segmented button { padding: 7px 6px; font-size: .82rem; }


/* Support the guide: the one tile lit like a lamp -- a wash of the
   accent and the accent down its edge -- and the one filled button on
   the page, the Save button's amber, so it is warm without shouting. */
.panel.support {
  background: color-mix(in srgb, var(--accent) 9%, var(--surface));
  border-left: 3px solid var(--accent-text);
}
.coffee-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 16px 9px 13px;
  font-size: .86rem;
  color: var(--on-accent);
  text-decoration: none;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 8px;
}
.coffee-btn:hover { filter: brightness(1.06); }
.coffee-btn .icon { margin: 0; width: 1.6em; height: 1.6em; }
.coffee-note { font-size: .74rem; color: var(--ink-faint); }

/* label-as-button, so the file input can stay hidden */
.file-btn { display: inline-block; cursor: pointer; }

.facts { margin: 0; display: flex; flex-direction: column; gap: 6px; }
.facts > div {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: .86rem;
}
.facts dt { flex: 1; color: var(--ink-dim); white-space: nowrap; }
.facts dd {
  margin: 0;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* The acknowledgements, in two groups: the sources beside the fonts
   on a wide screen, the one above the other on a narrow one. */
.ack-groups {
  display: grid;
  grid-template-columns: 1fr;
  gap: 18px;
}
@media (min-width: 900px) {
  .ack-groups { grid-template-columns: 2fr 1fr; }
}

/* The sources, credited, as a grid of small tiles: the site's short
   name to find it by, the page's own long title under it in the quiet
   ink.  The whole tile is the link, so a thumb has something to hit. */
.refs {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 8px;
}
.refs a {
  display: block;
  height: 100%;
  padding: 9px 12px;
  line-height: 1.35;
  text-decoration: none;
  background: var(--page-wash);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.refs a:hover { border-color: var(--accent-text); }
.refs a:hover strong { text-decoration: underline; }
.refs strong {
  display: block;
  font-size: .88rem;
  font-weight: 400;
  color: var(--accent-text);
}
.refs small {
  display: block;
  margin-top: 2px;
  font-size: .72rem;
  color: var(--ink-faint);
}

/* A font, credited: its name set in the face itself, and where it came
   from and on what terms under it.  Unlike .refs the row is not the tap
   target -- the links inside it are. */
.credits {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
}
.credits li {
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}
.credits li:first-child { padding-top: 0; }
.credits li:last-child { border-bottom: 0; }
.credits strong {
  display: block;
  font-size: 1rem;
  font-weight: 400;
  color: var(--ink);
}
.credits .face-display { font-family: var(--font-display); letter-spacing: .06em; }
.credits .face-heading { font-family: var(--font-heading); letter-spacing: .04em; }
.credits .face-body    { font-family: var(--font-body); }
.credits small {
  display: block;
  margin-top: 2px;
  font-size: .76rem;
  line-height: 1.45;
  color: var(--ink-faint);
}
.credits a {
  color: var(--accent-text);
  text-decoration: none;
}
.credits a:hover { text-decoration: underline; }

.paste[hidden] { display: none; }
.paste textarea {
  width: 100%;
  margin: 0 0 8px;
  padding: 9px 11px;
  font: inherit;
  font-size: .78rem;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  resize: vertical;
}

.verdict {
  margin-top: 10px;
  padding: 12px;
  font-size: .85rem;
  line-height: 1.5;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.verdict p { margin: 0; }
.verdict.bad { border-color: var(--danger); color: var(--ink); }

.problems {
  margin: 0;
  padding-left: 18px;
  color: var(--warn);
  font-size: .82rem;
}

/* ---------- show more ---------- */

.pager {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

.pager-count {
  flex: 1;
  font-size: .8rem;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.more-btn {
  display: inline-block;
  padding: 9px 16px;
  font: inherit;
  font-size: .86rem;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}
.more-btn:hover { border-color: var(--ink-faint); }

/* The Erase button is a .more-btn wearing this, so this has to come
   after it — the two set the same three properties, and the one that
   means "this cannot be undone" has to be the one that wins. */
.danger-btn {
  color: var(--rdr-white);
  background: var(--danger);
  border-color: var(--danger);
}
.danger-btn:hover { background: var(--rdr-blood); border-color: var(--rdr-blood); }

/* ---------- the search pages ----------

   materials/<slug>/ and recipes/<slug>/, written by
   scripts/build_pages.py: a material's or a recipe's dialog laid out
   as a page of its own, for whoever arrives from a search.  The same
   pieces as the dialog -- the head, the facts, the boxed lists -- in
   a column of reading width. */

.topbar .brand { color: inherit; text-decoration: none; }

.static-page {
  max-width: 760px;
  margin: 0 auto;
  padding: 16px;
}

.crumbs {
  margin-bottom: 14px;
  font-size: .8rem;
  color: var(--ink-faint);
}
.crumbs a { color: var(--ink-dim); }
.crumbs [aria-current] { color: var(--ink); }

.static-detail {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 18px 20px 20px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.static-detail .detail-head h1 {
  margin: 0;
  font-size: 1.6rem;
  line-height: 1.15;
  color: var(--ink-name);
  letter-spacing: .05em;
}
.static-detail .detail-head h1 .stars { margin-left: .35em; }
.static-detail .hint { margin: 0; font-size: .92rem; }

/* The sentence a search result quotes, said first on the page too. */
.static-intro {
  margin: 0;
  font-size: .95rem;
  line-height: 1.6;
  color: var(--ink-dim);
}

.static-cta { margin: 20px 0 0; }
.static-cta .more-btn { text-decoration: none; }

/* The A-Z lists take the app's own full width rather than a reading
   column -- they are scanned, not read -- with as many columns as fit
   and every name on one line with its stars.  Each column is as wide
   as the longest name in its list: a material runs to 33 characters
   and its stars, a recipe to 42. */
.static-page.static-wide { max-width: 1400px; }

.az-list {
  margin: 0;
  padding: 0;
  list-style: none;
  columns: 300px;
  column-gap: 28px;
  font-size: .9rem;
}
.az-list.long { columns: 370px; }
.az-list li { padding: 3px 0; break-inside: avoid; white-space: nowrap; }
/* A phone has one column and less than the longest recipe's width, so
   there a name too long for the screen wraps rather than runs off it. */
@media (max-width: 480px) { .az-list li { white-space: normal; } }
.detail-section .list-label .count { margin-left: 6px; color: var(--ink-faint); }

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

/* Same 1400px column as main, so its rule lines up with the content
   above it rather than running the full width of a desktop screen. */
.footer {
  max-width: 1400px;
  margin: 30px auto 0;
  padding: 14px 16px;
  border-top: 1px solid var(--line);
  text-align: center;
}

.footer p {
  margin: 0;
  font-size: .78rem;
  line-height: 1.6;
  color: var(--ink-faint);
}

/* The face carries no bold, so a name is lifted by colour instead. */
.footer .by { color: var(--ink-dim); }

/* Browse every material / every recipe: drawn like the page's other
   links, in the footer's own quiet ink rather than the browser's blue. */
.footer a { color: var(--ink-dim); text-underline-offset: 2px; }
.footer a:hover { color: var(--ink); }

/* No typewriter had a (c) key -- you typed the three characters.  This one
   borrows the glyph, and drops back a little so it does not outweigh the
   year it sits against. */
.footer .sym {
  font-family: var(--font-symbol);
  font-size: .92em;
}

/* The lines under the credit -- where your data lives, and the
   fan-site disclaimer -- each on a line of its own: set in the body
   face, not the heading one, as small print rather than a signature.
   `.footer p.small-print` so it outranks `.footer p` in the type tiers
   further down.  The privacy line is the one worth reading, so it
   sits a shade darker. */
.footer p.small-print {
  font-family: var(--font-body);
  letter-spacing: normal;
  font-size: .72rem;
}
.footer p.privacy { color: var(--ink-dim); }

/* ---------- misc ---------- */

.loading, .empty {
  color: var(--ink-dim);
  padding: 24px 4px;
  text-align: center;
}

.error {
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  padding: 14px;
  color: var(--ink);
}


.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--tabs-h) + 16px);
  transform: translateX(-50%);
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 14px;
  max-width: calc(100vw - 32px);
  padding: 10px 14px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-size: .86rem;
}
.toast-text { flex: 1; }


.toast .toast-close {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  margin: -4px -4px -4px 0;
  font-size: 1.1rem;
}
@media (min-width: 720px) { .toast { bottom: 20px; } }

/* ============================================================
   The lettering, last.
   Every control in this file sets `font: inherit` so it matches
   the page rather than the operating system, and that shorthand
   resets the family — so the display or heading face has to be
   assigned after them or the buttons quietly fall back to the
   sans.  Applied by hand rather than to every heading, because a
   caps-only face has to earn its place: names, labels and
   buttons yes, prose never.  Neither carries a bold, so anything
   that asked for 600 is reset here and leans on size and
   tracking instead.  The steppers stay in the body font: a
   caps face draws a poor + and -.
   ============================================================ */

/* Marston: the site's own name, a page's own header, and the
   tab bar that moves between pages -- the loudest tier, and the
   rarest on screen. */
h1, h2,
.page-title,
.topbar .brand,
.tabs a {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: .04em;
}

/* RDR Lino Regular: a card's own name, a tab inside a page, a
   dialog's title, a section's caption, the General/Personalize and
   Skip/Want it switches, and every button's own label -- one tier
   down from the above, and everywhere else something reads as a
   header or a control rather than prose. */
h3,
#detail-title,
.chip,
.segmented button,
.craft-btn, .ghost-btn, .more-btn, .danger-btn, .sort-dir, .savebar button,
.coffee-btn,
.badge,
.list-label,
.panel h3,
.trait dt, .detail-kicker,
.entry .reason,
.mode-switch, .want-switch, .backup-btn,
.move-btn .move-word, .move-place,
.footer p {
  font-family: var(--font-heading);
  font-weight: 400;
  letter-spacing: .04em;
}

/* Tracking each face needs at each size it is used. */
.page-title { letter-spacing: .06em; }
.tabs a     { letter-spacing: .07em; }
.card h3    { letter-spacing: .05em; }
.badge, .list-label, .panel h3, .trait dt, .detail-kicker,
.entry .reason { letter-spacing: .09em; }
