/* css/app.css — the application theme.
 *
 * Every color, radius and type face is a token declared in theme/theme.json and rendered
 * into :root by views/fragments/deps/theme-vars.ejs. Nothing here states a literal value,
 * so re-skinning the application is a data change.
 *
 * Two jobs, in order:
 *   1. Bind Bootstrap's own custom properties to those tokens, so every control the pages
 *      already use — buttons, list groups, inputs, dropdowns — is themed at the source
 *      rather than re-styled page by page.
 *   2. Define the shared components the pages compose from: the shell, the tab strip, the
 *      panel, the toolbar, chips and cards. One definition per element kind; a page that
 *      wants a tab strip uses .loc-tabs and gets the same tabs as every other page.
 */

/* The hidden attribute must win over every author rule below. Any `display` set by a
   class beats the UA's `[hidden] { display: none }` on specificity alone, which silently
   turns `element.hidden = true` into a no-op. */
[hidden] { display: none !important; }

/* --- Bootstrap bound to the tokens ---------------------------------------- */
:root {
  --bs-body-bg: var(--loc-canvas);
  --bs-body-color: var(--loc-ink);
  --bs-body-font-family: var(--font-ui);
  --bs-body-font-size: 0.95rem;
  --bs-secondary-color: var(--loc-muted);
  --bs-emphasis-color: var(--loc-ink);
  --bs-border-color: var(--loc-line);
  --bs-border-radius: var(--loc-radius);
  --bs-border-radius-sm: var(--loc-radius-sm);
  --bs-link-color: var(--loc-accent);
  --bs-link-hover-color: var(--loc-accent-hover);
  --bs-focus-ring-color: var(--loc-accent-soft);
  --bs-heading-color: var(--loc-ink);
  /* **A control's height is not something two rules agree about.** A button standing beside
     a field is the same height as the field because both are built from these, and there is
     no second place either could be set. */
  --loc-control-face: 0.9rem;
  --loc-control-line: 1.5;
  --loc-control-pad-block: 0.35rem;
  --loc-control-pad: var(--loc-control-pad-block) 0.6rem;
  /* One tab's height, stated once because two rules depend on agreeing about it: the strip a
     phone collapses to, and the box the menu it opens stands inside without moving the page.
     It is the tab's own padding and line box, so the two cannot drift apart. */
  --loc-tab-row: calc(1.15rem * 1.4 + 0.8rem);
  /* **A label is nearer its own field than the one above it.** Three gaps, in one place, so
     every form in the application agrees about what belongs to what: tight under a label,
     wider between fields, wider again between groups. */
  --loc-label-gap: 0.3rem;
  --loc-field-gap: 1.15rem;
  --loc-group-gap: 1.5rem;
}

/* Every checkbox in the application, stated once. A tick is a tick wherever it appears —
   a search criterion, a print choice, whatever comes next — so nothing restates this and
   nothing styles a checkbox of its own. */
.form-check-input {
  --bs-form-check-bg: var(--loc-surface);
  width: 1rem;
  height: 1rem;
  margin-block-start: 0;
  border: 1px solid var(--loc-line-strong);
  border-radius: var(--loc-radius-sm);
  cursor: pointer;
}
.form-check-input:checked {
  background-color: var(--loc-accent);
  border-color: var(--loc-accent);
}
.form-check-input:focus {
  border-color: var(--loc-accent);
  box-shadow: 0 0 0 0.2rem var(--loc-accent-soft);
}
.form-check-input:hover:not(:checked) { border-color: var(--loc-accent); }

/* Where an address stands with the authority. Agreed reads as settled; anything else reads
   as outstanding, because waiting and expired are both "not yet" to whoever is looking. */
.loc-access-state { font-size: 0.85rem; white-space: nowrap; }
.loc-access-on { color: var(--loc-ok); }
.loc-access-off { color: var(--loc-muted); }

/* What is wrong with what was just typed, said under the field it was typed in. The answer
   is wrong where it was entered, so that is where it is corrected. */
.loc-field-error {
  margin: 0.35rem 0 0;
  font-size: 0.85rem;
  color: var(--loc-danger);
}

/* A checkbox and the words it answers for, as one target: the whole row ticks, because a
   1rem box is not a thing anyone should have to aim at. */
.loc-check {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0.15rem 0.25rem;
  border-radius: var(--loc-radius-sm);
  cursor: pointer;
  user-select: none;
}
.loc-check:hover { background: var(--loc-accent-soft); }
.loc-check-label { min-width: 0; }

.btn {
  --bs-btn-font-family: var(--font-ui);
  --bs-btn-border-radius: var(--loc-radius-sm);
  --bs-btn-focus-shadow-rgb: 138 90 46;
}
.list-group {
  --bs-list-group-bg: var(--loc-surface);
  --bs-list-group-color: var(--loc-ink);
  --bs-list-group-border-color: var(--loc-line);
  --bs-list-group-border-radius: var(--loc-radius);
  --bs-list-group-action-hover-bg: var(--loc-accent-soft);
  --bs-list-group-action-hover-color: var(--loc-ink);
  --bs-list-group-active-bg: var(--loc-accent);
  --bs-list-group-active-border-color: var(--loc-accent);
}

.form-control, .form-select {
  background-color: var(--loc-surface);
  border-color: var(--loc-line-strong);
  color: var(--loc-ink);
  border-radius: var(--loc-radius-sm);
}
.form-control:focus, .form-select:focus {
  border-color: var(--loc-accent);
  box-shadow: 0 0 0 0.2rem var(--loc-accent-soft);
}
.form-control::placeholder { color: var(--loc-line-strong); }

/* **Every field the application asks a value in.** A field is a field wherever it stands —
   a name on a form, a word in a toolbar, a line in a dialog — so the size and shape are
   said once here and worn by all of them. Nothing states a size of its own: a page that
   reached for a smaller control would be deciding, one form at a time, that its fields are
   a different kind of thing from everybody else's, and the drift that follows is exactly
   what a reader sees as an unfinished application. A class beside this one may narrow a
   field — how wide it grows, how its text sits — but never how it is themed. */
.loc-input {
  font-size: var(--loc-control-face);
  padding: var(--loc-control-pad);
  line-height: var(--loc-control-line);
}
textarea.loc-input { padding-block: 0.5rem; }

/* The prompt inside a field: the words that stand in it until it is filled. The recipe's own
   name and description are typed straight onto the card rather than into a form control, so
   they wear the prompt class alone. */
.loc-input::placeholder,
.loc-prompt::placeholder { color: var(--loc-line-strong); opacity: 1; }

/* Every menu in the application arrives the same way: growing down from what opened it, and
   fading off quickly when it is dismissed. */
.dropdown-menu.show { animation: loc-ink-in 0.12s ease-out; transform-origin: top left; }
@media (prefers-reduced-motion: reduce) {
  .dropdown-menu.show { animation: none; }
}
.dropdown-menu {
  --bs-dropdown-bg: var(--loc-surface);
  --bs-dropdown-border-color: var(--loc-line);
  --bs-dropdown-border-radius: var(--loc-radius);
  --bs-dropdown-color: var(--loc-ink);
  --bs-dropdown-link-color: var(--loc-ink);
  --bs-dropdown-link-hover-bg: var(--loc-accent-soft);
  --bs-dropdown-link-hover-color: var(--loc-ink);
  --bs-dropdown-link-active-bg: var(--loc-accent);
  box-shadow: var(--loc-shadow-pop);
}

body { margin: 0; font-family: var(--font-ui); color: var(--loc-ink); background: var(--loc-canvas); }

/* Headings carry the label face — the same one the recipe template and the sheet headers
   use, which is what makes a page read as part of the same application. */
h1, h2, h3, .loc-title { font-family: var(--font-label); font-weight: 400; letter-spacing: 0.01em; }
h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }

/* --- the shell ------------------------------------------------------------ */
/* One gutter for the whole application: the header, the page and the context row all line
   up on it, and nothing ever reaches the window edge.
 *
 * The window is the frame. The header is fixed height and the page takes the rest, which
 * is what lets a page hand its own height to something inside it — a pane that fills the
 * window and scrolls its own contents — instead of growing the document and scrolling the
 * whole application. A page that declares no such thing simply scrolls here as usual. */
.loc-shell { max-width: var(--loc-measure); margin-inline: auto; padding-inline: var(--loc-gutter); }
html, body { height: 100%; }
body { display: flex; flex-direction: column; }
.loc-header { flex: 0 0 auto; }
/* `width: 100%` is load-bearing, not decoration. The shell carries `margin-inline: auto`,
   and an auto margin in a flex item's cross axis cancels `stretch` — so without an explicit
   width the page is sized by its own content and centered, and a page whose content happens
   to be narrow silently shrinks the whole layout with it. */
.loc-main {
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
  overflow-y: auto;
  padding-block: 1rem 1.5rem;
}
/* A page that keeps something above its content — a toolbar acting on what is below it —
   says so by wrapping that content. The shell then stops scrolling and the wrapper scrolls
   in its place, so the row above stays put by being outside the scroll rather than by being
   taken out of the flow and floated over it. A page that wraps nothing scrolls as before. */
.loc-main:has(> .loc-page-scroll) {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding-block: 0;
}
.loc-page-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-block-end: 1.5rem;
}
/* **On a phone the recipe starts at the top and the controls sit under the thumb.** A bar
   between the nav and the content spends the most valuable band on the screen — the one
   directly under the header — on chrome, and puts the controls at the far end of the reach of
   whoever is holding the phone. Ordered last, the content begins immediately below the nav and
   the bar stands at the bottom edge, still outside the scroll: it holds its place while the
   recipe moves under it, which is what it did at the top.

   Only the order changes. The bar is the same bar, the scroll is the same scroll, and on a
   wide screen neither has moved. */
@media (max-width: 47.99rem) {
  .loc-main:has(> .loc-page-scroll) > .loc-toolrow { order: 1; }
}
/* **A bar standing on the edge of the screen stands on the whole of it.** The gutter is the
   page's, and a row of controls held to the foot is not the page — it is the frame around it,
   so it gives the gutter back and its rule runs the full width the way the header's does.
 *
 * Said here rather than on the row, which is inside a scroll that clips: the shell holds no
 * overflow of its own, so this is the nearest box that can widen without being cut off. What
 * the page gives up at its edge the content takes back at its own, so only the frame moves. */
@media (max-width: 47.99rem) {
  .loc-main:has(> .loc-page-scroll) {
    margin-inline: calc(var(--loc-gutter) * -1);
    /* The shell states a width, and a stated width does not grow when the margins around it
       go negative — the box keeps its measure and slides, giving back one edge and stranding
       the other. Letting the width be worked out instead is what makes both edges move. */
    width: auto;
  }
  .loc-main:has(> .loc-page-scroll) > .loc-page-scroll {
    padding-inline: var(--loc-gutter);
  }
}

.loc-header { background: var(--loc-surface); border-bottom: 1px solid var(--loc-line); }
/* **On a phone the content starts under the nav, not against it.** With the toolbar moved to
   the foot of the screen the recipe is what meets the header, and a page butted straight up
   against the bar above it reads as one surface rather than two. The gap is the theme's, so it
   is the same small clearance wherever one is wanted. */
@media (max-width: 47.99rem) {
  .loc-header { margin-block-end: var(--loc-gap); }
}
/* Three parts, one row that never wraps: the brand is anchored left, the account glyph
   anchored right, and the page tabs take whatever is between them. Only the tabs flow.
   The two anchors sit centered in the bar's full height, so they stay put — and stay
   balanced — however many rows the tabs wrap onto. */
/* The line under the tabs belongs to the BAR, not to the tabs: the strip is only the middle
   of the row, so a border drawn on it stops short of the brand at one end and the account at
   the other and reads as a broken rule. Drawn here it runs the whole gutter, and the tabs
   still sit on it — which is what makes a tab read as a tab. */
.loc-bar {
  display: flex;
  flex-wrap: nowrap;
  align-items: stretch;
  gap: 1rem;
  min-height: 3rem;
  border-bottom: 2px solid var(--loc-line-strong);
}
.loc-brand, .loc-user { display: flex; align-items: center; }
.loc-brand {
  flex: 0 0 auto;
  font-family: var(--font-label);
  font-size: 1.6rem;
  line-height: 1;
  color: var(--loc-accent);
  text-decoration: none;
  white-space: nowrap;
}
.loc-context-row { border-top: 1px solid var(--loc-line); background: var(--loc-surface-sunk); }
.loc-context {
  display: flex;
  align-items: baseline;
  /* The separator carries the spacing between crumbs, so the row's own gap only has to keep
     adjacent elements from touching. Setting both is what doubles it. */
  gap: 0.2rem;
  min-height: 2rem;
  padding-block: 0.3rem;
  font-size: 0.9rem;
}
/* The trail reads left to right, coarsest first: the book, the page, then whatever the
   page is working on. The separator is decoration, not content.
   A crumb that opens is still a crumb: the kitchen it names and the book inside it are both
   the trail, and the menu hanging off them is how you reach another one. So the control keeps
   the trail's own face and gives up every part of a button's — the box, the fill and the
   padding — and the rule says that of any crumb menu rather than of one of them, because a
   crumb wearing a button's chrome beside one that does not reads as two different things
   where there is one. */
.loc-crumb { color: var(--loc-muted); }
.loc-crumb.loc-current { font-weight: 600; color: var(--loc-accent); }
.loc-crumb-sep { color: var(--loc-line-strong); margin-inline: 0.2rem; }
.loc-crumb-menu .loc-crumb {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
}
.loc-crumb-menu .loc-crumb:hover { color: var(--loc-accent-hover); }

/* Navigation is for going places, never for selecting text: a drag across the bar is a
   mis-aimed click, and highlighting the labels makes it look like something happened. */
.loc-nav, .loc-tabs, .loc-context, .loc-brand { user-select: none; }

/* --- tabs: one definition, used by the page nav and by every in-page strip -- */
.loc-tabs {
  display: flex;
  align-items: flex-end;
  gap: 0.15rem;
  margin: 0;
  padding: 0;
  list-style: none;
  border-bottom: 2px solid var(--loc-line-strong);
}
.loc-tab {
  appearance: none;
  border: 1px solid transparent;
  border-bottom: 0;
  border-radius: var(--loc-radius) var(--loc-radius) 0 0;
  background: transparent;
  padding: 0.4rem 1rem;
  margin-bottom: -2px;
  font-family: var(--font-label);
  font-size: 1.15rem;
  line-height: 1.4;
  color: var(--loc-muted);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}
.loc-tab:hover { color: var(--loc-accent-hover); background: var(--loc-accent-soft); }
.loc-tab.active {
  color: var(--loc-ink);
  background: var(--page-bg);
  border-color: var(--loc-line-strong);
  border-bottom: 2px solid var(--page-bg);
}
/* The header's strip draws no line of its own — it stands on the bar's, which is the only
   one that reaches both edges of the gutter. */
.loc-bar .loc-tabs { border-bottom: 0; }
.loc-tabs-page { border-bottom-color: var(--loc-line); }
.loc-tabs-page .loc-tab.active { border-bottom-color: var(--page-bg); }

/* --- page furniture ------------------------------------------------------- */
.loc-page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-block-end: 1rem;
}
/* The page name is the active tab in the shared nav, so most heads carry only a toolbar.
   A lone toolbar sits against the right edge of the gutter, where every page puts it. */
.loc-page-head > :only-child { margin-inline-start: auto; }
.loc-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-block-end: .5rem;
}
/* The standard content surface: anything that is a body of work sits on one of these. */
.loc-panel {
  background: var(--loc-surface);
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  box-shadow: var(--loc-shadow);
  padding: 1rem 1.15rem;
}
.loc-panel + .loc-panel { margin-block-start: 1rem; }
.loc-panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-block-end: 0.75rem;
  padding-block-end: 0.5rem;
  border-bottom: 2px solid var(--page-rule);
  font-family: var(--font-label);
  font-size: 1.35rem;
}
.loc-hint { color: var(--loc-muted); font-size: 0.9rem; }
/* A criteria list with nothing in it. It stands where the ticks would stand and reads as one
   of them, so it takes the tag face with the rest and differs only in being muted. */
.loc-facet-empty { color: var(--loc-muted); padding: 0.15rem 0.25rem; }

.loc-chip {
  border: 1px solid var(--loc-line-strong);
  background: var(--loc-surface);
  border-radius: 1rem;
  padding: 0.15rem 0.7rem;
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--loc-ink);
}
.loc-chip:hover { border-color: var(--loc-accent); }
.loc-chip.active { background: var(--loc-accent); color: #fff; border-color: var(--loc-accent); }
.loc-chip-group { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; margin-inline-end: 0.5rem; }
/* Two rows of chips answer two different questions, so each says which it is answering. The
   label sits at the head of its row rather than above it: a row of chips is one statement,
   and a caption on its own line reads as a heading over a section. */
.loc-chip-row { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.loc-chip-label {
  font-family: var(--font-label);
  font-size: 0.8rem;
  color: var(--loc-muted);
  min-width: 3.5rem;
}
.loc-chip:disabled { opacity: 0.45; cursor: not-allowed; }
.loc-chip:disabled:hover { border-color: var(--loc-line-strong); }

/* Where this friend is working, and what they hold there. The kitchen names the scope and the
   roles sit under it, so the two read as one statement rather than as two facts about the
   person — a role means nothing without the kitchen it is held in. */
.loc-user-kitchen {
  margin-block-start: 0.35rem;
  font-size: 0.85rem;
  color: var(--loc-muted);
}
.loc-user-roles { margin-block-start: 0.35rem; margin-inline-end: 0; }
.loc-user-role { cursor: default; font-size: 0.75rem; padding: 0.05rem 0.55rem; }
.loc-facet-sort { width: auto; }
.loc-facet-search { width: 14rem; margin-inline-start: auto; }

.loc-ahead { position: relative; display: block; }
.loc-ahead-list {
  position: absolute;
  inset-inline: 0;
  z-index: 10;
  margin: 0.15rem 0 0;
  padding: 0.25rem;
  list-style: none;
  background: var(--loc-surface);
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius-sm);
  box-shadow: var(--loc-shadow-pop);
}
.loc-ahead-list button { width: 100%; padding: 0.35rem 0.55rem; border: 0; background: none; text-align: start; cursor: pointer; }
.loc-ahead-list button:hover { background: var(--loc-accent-soft); }

/* The rows are spaced for the seal, not for the cards: the badge hangs below its card, and
   the gap has to clear it or a course lands on the recipe underneath. */
/* The column minimum holds four seals side by side with room either side, so a card never
   has to shrink or wrap its courses however many the recipe wears. */
.loc-cards {
  display: grid;
  /* A card is 13rem wide unless the screen is narrower than a card, in which case it is the
     screen: a fixed minimum on a phone is what makes a wall of cards wider than the wall. */
  grid-template-columns: repeat(auto-fill, minmax(min(13rem, 100%), 1fr));
  column-gap: 0.9rem;
  row-gap: 2.25rem;
  /* **Every card is the same card.** A wall of plates is a wall only if the plates match: a
     recipe that has been given a picture and one that has not are the same thing in the book,
     and a grid whose rows each take their own height says otherwise. One height for every row
     settles it, and the cards stretch into it. */
  grid-auto-rows: 1fr;
}
/* The whole card is the link — a card is a thing you pick up, not a label with a hotspot in
   it. One real anchor still carries it, stretched over the card, so it stays keyboard
   reachable and openable in a new tab. */
.loc-card {
  position: relative;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  background: var(--loc-surface);
  /* A card is lifted off the page whether or not a pointer is anywhere near it — the lift is
     what makes it a card rather than a panel drawn on the surface, and it does not depend on
     being pointed at. */
  box-shadow: var(--loc-shadow-pop);
  padding: 0.8rem 0.8rem 1.2rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  cursor: pointer;
  user-select: none;
}
/* A card with no picture has the band of space the picture would have filled, so what it does
   carry stands in the middle of the card rather than at the top of it with the room left
   underneath. The seal is unaffected: it is placed against the card's own bottom edge, not
   against the content, so it stays where it is on every card. */
.loc-card:not(:has(.loc-card-shot)) { justify-content: center; }
/* Hovering deepens the same shadow rather than spreading it: the card is not lifted further,
   it is answered. Same offset and same blur, more of it — a lift that grew fuzzier would read
   as the card moving, and nothing has moved. */
.loc-card:hover { border-color: var(--loc-line-strong); box-shadow: var(--loc-shadow-lift); }
.loc-card:focus-visible { outline: 2px solid var(--loc-accent); outline-offset: 2px; }
/* A pinned card says so on its own edge rather than by where it happens to sit: the leading
   position is a consequence of the pin, and a reader scrolling past the front of the list has
   to be able to tell a pinned card from one that merely sorted first. */
.loc-card-pinned { border-color: var(--loc-accent); }
/* The row of marks is inset over the top of the card, centered, and takes no space of its own —
   a card that grew a row when it was pointed at would push its neighbours around the grid. */
.loc-card-tools {
  position: absolute;
  inset-block-start: 0.4rem;
  inset-inline: 0;
  z-index: 2;
  display: flex;
  justify-content: center;
  gap: 0.25rem;
  opacity: 0;
  transition: opacity 0.12s ease-in-out;
  pointer-events: none;
}
.loc-card-tool {
  border: 1px solid var(--loc-line);
  border-radius: 999px;
  background: var(--loc-surface);
  color: var(--loc-muted);
  width: 1.9rem;
  height: 1.9rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  box-shadow: var(--loc-shadow-pop);
}
.loc-card-tool:hover { color: var(--loc-accent); border-color: var(--loc-accent); }
/* **Pinned is a state of the recipe, so its mark is not a tool that comes and goes.** The rest
   of the row is what somebody can do to a card and is offered when they reach for it; this one
   says what is already true, and a fact about a card that only appears under the pointer is a
   fact nobody scrolling a list can read.
   Filled rather than merely tinted: a pin that is in reads as a pin that is in from across a
   room, and the glyph turns to the surface color so it sits on the accent rather than against
   it. */
.loc-card-tool-lit {
  color: var(--loc-surface);
  background: var(--loc-accent);
  border-color: var(--loc-accent);
  opacity: 1;
  pointer-events: auto;
}
.loc-card-tool-lit:hover { color: var(--loc-surface); border-color: var(--loc-accent); }
/* Revealed by the pointer where there is one, and by the tap that stands in for it where there
   is not. Both end in the same class of the row being reachable.
   The row is what fades; a lit mark inside it stands whatever the row is doing, which is why
   the row rather than each mark carries the transition. */
.loc-card:hover .loc-card-tools,
.loc-card:focus-within .loc-card-tools,
.loc-card-shown .loc-card-tools { opacity: 1; pointer-events: auto; }
/* A row holding a lit mark is a row that is showing something, so it is reachable — and the
   marks that are not lit are hidden rather than merely transparent, or a card would carry
   invisible buttons that still answer a press. */
.loc-card-tools:has(.loc-card-tool-lit) { opacity: 1; pointer-events: auto; }
.loc-card-tools:has(.loc-card-tool-lit) .loc-card-tool:not(.loc-card-tool-lit) {
  opacity: 0;
  pointer-events: none;
}
.loc-card:hover .loc-card-tools .loc-card-tool,
.loc-card:focus-within .loc-card-tools .loc-card-tool,
.loc-card-shown .loc-card-tools .loc-card-tool {
  opacity: 1;
  pointer-events: auto;
}
.loc-card-tool { transition: opacity 0.12s ease-in-out; }
@media print {
  .loc-card-tools { display: none; }
}
.loc-wizard .loc-step { display: block; }
/* **The paper is what a finished recipe looks like, not what writing one looks like**, so the
   working surface is not the card: no banner, no stock it is printed on, no border around it,
   because a form dressed as the thing it produces is a page pretending to be its own output.
   What it keeps is the template's field vocabulary — a line written on, ruled underneath —
   which is a property of a field rather than of the card, and is declared as such beside it.
   What says which recipe is open is the dialog's own title, once.
   The zone spacing is the template's measure, and a zone break is a paragraph break here as
   much as on the card. */
/* **The measure is set by the widest thing the form has to hold, which is the toolstrip.** Every
   other zone reads comfortably narrower, but the strip has a width below which it must break, and
   a card narrower than that would put the block controls on a second row. So the card is exactly
   as wide as one row of controls needs.
   That width is declared rather than measured because every part of it is: fifteen controls at a
   stated size, a menu at a stated size, four rules and the gaps between them. Nothing in the strip
   is sized by its content, which is what makes the sum a number rather than a guess. */
.loc-wizard {
  --zone-gap: 1rem;
  --loc-rich-row: 48rem;
  font-family: var(--font-content);
  max-width: var(--loc-rich-row);
  margin-inline: auto;
}
/* Below the breakpoint the page is the measure: there is no width to be had, so the card takes
   what there is and the strip wraps within it. */
@media (max-width: 47.99rem) {
  .loc-wizard { max-width: 100%; }
}
/* The sheet stands in the middle of whatever room the dialog has: a recipe is a card however
   much screen there is to draw it on. */
.loc-editor .modal-body { overflow-y: auto; }
.loc-editor .loc-step { align-content: start; }
/* **The course is the question every recipe answers, so it takes the width.** It is a set of
   ticks read across, and squeezed into a column beside the others it wraps a word at a time.
   The three open dimensions are words typed in — one short field apiece — so they stand beside
   each other under it. On a phone three of those are too narrow to read what is in them, so
   they fall to two and the third takes the row below. */
.loc-wizard .loc-taxonomy-ruled { display: block; margin-block-end: var(--zone-gap); }
.loc-wizard .loc-taxonomy-open {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.75rem 1.5rem;
}
@media (max-width: 47.99rem) {
  .loc-wizard .loc-taxonomy-open { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* **The ingredients are written in one column and read in two.** On the page a list of short
   lines in two columns is a page of writing; in the editor it is a caret that jumps sideways at
   the fold and a line whose length depends on where in the list it sits. So writing them is one
   column at the full measure, and what does not fit scrolls inside the wizard rather than
   growing the dialog under the cook. */
.loc-wizard .zone-ingredients,
.loc-wizard .zone-directions {
  columns: auto;
  column-gap: normal;
}
/* **A step of the wizard is one question, so the box for it is the size of the answer.** On the
   card the surface is as tall as what is written in it, standing among the other zones; here it
   has a whole step to itself, and a two-line box in the middle of it invites two lines. Four
   times the card's floor is a list somebody can see the shape of while they write it — and it is
   a floor, so a long recipe still grows and scrolls inside the dialog rather than pushing the
   controls off the bottom of it. */
.loc-wizard .pm-host {
  max-width: none;
  min-height: 14.4em;
  display: flex;
}
/* **The floor belongs to the surface, not to the box around it.** A height on the host alone
   leaves the editable region as short as what is written in it, so the box looks tall and the
   bottom two thirds of it are not a place a caret can be put — which reads as a control that
   ignored being clicked. */
.loc-wizard .pm-host .ProseMirror {
  flex: 1 1 auto;
  min-height: inherit;
}
/* The description is written into as much as the lists are, so it gets the same room. */
.loc-wizard .loc-rich-surface { min-block-size: 14.4em; }
/* **A phase takes the weight of a heading as it is typed.** The colon is the designation, so
   the line answering back is what tells the cook it took — and what tells them it did not. */
.loc-wizard .pm-phase {
  font-weight: 700;
  border-bottom: 0;
  margin-block-start: 0.6rem;
}
.loc-wizard .pm-phase:first-child { margin-block-start: 0; }
/* **The name is a field like the others, and is asked for like them.** The card's own header
   lays the word and the name out as one line of a title, set large — that is a reading of the
   name rather than a way of asking for it, and a form whose first box is twice the size of
   every other box says the first question is a different kind of question. So the zone stacks
   and the box takes the template's ordinary field face, which it gets by being a field on this
   surface rather than by being told twice. */
.loc-wizard .zone-header {
  display: block;
  margin-bottom: 0;
}
.loc-wizard .zone-header input { width: 100%; }
/* Source is asked the same way for the same reason: on the card it is a line of small print
   naming where the recipe came from, and on the form it is one more question with one more box
   under it. The label already carries the heading face; what it needs is the line to itself. */
.loc-wizard .zone-source { font-size: inherit; }
.loc-wizard .zone-source .label {
  display: block;
  margin-right: 0;
  margin-block-end: 0.35rem;
}
.loc-wizard .zone-source input { width: 100%; }
/* **Three answers of the same kind, so three columns of the same width.** What a recipe makes,
   how long it takes to get ready and how long it cooks are one question asked three ways, and
   a row that sized each to its own answer would say they were different questions. A phone is
   too narrow for three of them, so they fall to two and the last takes the row below. */
.loc-wizard .zone-meta {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.75rem 1.5rem;
  /* The measurements are a second question on the same step, so they stand off the filing by
     the same measure a zone break takes anywhere else on the card. */
  margin-block-start: var(--zone-gap);
}
@media (max-width: 47.99rem) {
  .loc-wizard .zone-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.loc-editor .recipe-page { margin-inline: auto; }
/* **One zone at a time, in a dialog that does not resize under the cook.** The zones differ
   wildly in height — a name is one line, the directions are a page — and a dialog sized to
   whichever one is showing moves its own footer every time the walk advances, so the controls
   somebody is reaching for are never twice in the same place.
   So the surface holds a height and the walk changes what is inside it. The height is the room
   there is rather than a figure written down: a stated one large enough for the longest zone
   would be taller than a laptop on a laptop, and clip on anything smaller. Filling what is
   available is that same floor on every screen that can hold it, and the body scrolls where
   one cannot — either way the footer stands still.
   Above the breakpoint that is almost the whole screen, with the mask showing at its edges and
   its corners rounded, so it reads as something raised over the book rather than as a page
   that replaced it. Below it the phone gets the whole screen, where a margin would cost the
   writing the only room it has. */
.loc-editor .modal-content { height: 100%; }
/* **The walk leads the row and holds its place.** Back and Next are one pair of controls doing
   one job, so they are grouped as one and stand at the leading edge — the same arrangement the
   cook's dialog uses, and for the same reason: a control that moves between steps is a control
   somebody has to find again each time. What acts on the document rather than on the writing
   is a second group, and the two stand in that order across the row. */
.loc-editor-footer {
  justify-content: flex-start;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.loc-editor-footer .btn { white-space: nowrap; }
.loc-editor-footer .loc-editor-acts { margin-inline-start: auto; }
@media (min-width: 48rem) {
  .loc-editor .modal-dialog { height: calc(100vh - 3.5rem); }
}
/* **The editor takes the whole screen at this application's boundary, not the dialog kit's.**
   The kit gives a dialog the screen below 576px and floats it above that, which leaves a band
   between the two where the page has already changed to its phone shape and the dialog has not —
   a writing surface inset by its margin, its border and its body padding, all at once, on the
   narrowest screens that get none of them back.
   One boundary is the rule (R40.1), so the collapse is stated here at the width this
   application uses rather than taken from the class the kit spells its own width into. */
@media (max-width: 47.99rem) {
  .loc-editor .modal-dialog {
    width: 100vw;
    max-width: none;
    height: 100%;
    margin: 0;
  }
  .loc-editor .modal-content { border: 0; border-radius: 0; }
}
/* The seal sits half on the card and half below it, centered on the bottom edge, and stays
   above the stretched link so its own tooltip still answers the pointer. */
.loc-card-course {
  position: absolute;
  z-index: 2;
  bottom: 0;
  inset-inline-start: 50%;
  transform: translate(-50%, 50%);
  display: flex;
  gap: 0.25rem;
}
/* The name is the card's title: centered, and ruled either side, so a wall of cards reads as
   a set of plates rather than a column of links. The rules shrink before the name does. */
.loc-card-name {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--font-label);
  font-size: 1.25rem;
  color: var(--loc-ink);
  text-decoration: none;
  text-align: center;
}
.loc-card-name:hover { color: var(--loc-accent); }
/* **A dialog's heading stands alone, so it stands in the middle.** A card's name sits in a wall
   of them and is centered on its own card; a dialog has one heading and the whole row to give
   it. The rules either side are flex items of their own, so a name that wraps to two lines
   keeps them centered against the whole of it rather than pinned to its first line. */
.modal-header:has(> .loc-card-name) { justify-content: center; }
.modal-header > .loc-card-name { flex: 1 1 auto; }
.loc-card-rule { color: var(--loc-line-strong); flex: 0 1 auto; }
.loc-card-title { min-width: 0; }
.loc-card-chips { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.loc-card-chip { font-size: 0.7rem; padding: 0.05rem 0.5rem; border-radius: 1rem; background: var(--loc-surface-sunk); }
.loc-card-chip.diet { background: var(--loc-accent-soft); }
.loc-card-chip.ingredient { background: var(--loc-surface-sunk); }
.loc-card-chip.method { background: var(--loc-surface-sunk); font-style: italic; }
.loc-card-category { position: absolute; top: 0; right: 0; }
/* A row in the key is one tag: what it is called, the mark it reads as, its color, and the
   way to retire it. Only the name has anything to grow for — the other three are fixed
   shapes, and a two-letter box that widens with the window says nothing more than it did.
   So the name takes every spare inch and the rest keep their own measure, which also lines
   the columns up down the list rather than letting each row set its own. */
.loc-type-name { flex: 1 1 6rem; min-width: 0; }
.loc-key-abbr { flex: 0 0 4.5rem; text-transform: uppercase; text-align: center; }
.loc-key-group .form-control-color,
.loc-key-group .btn { flex: 0 0 auto; }
.loc-key-group { margin-block-end: 1.25rem; }
/* The face a tag is read in, wherever a tag is read: the two letters in a mark, the word
   in a badge, the word in a chip. One class carries it and every such surface wears that
   class, so a tag never reads as two different things on two parts of one card. */
.loc-tag-face { font-family: var(--font-ui); }

/* The mark the key is read by: a filled disc with two letters in it, the same shape
   wherever it appears — the legend, a card, the recipe itself. */
.loc-key-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 50%;
  font-size: 0.7rem;
  letter-spacing: 0.02em;
  color: var(--loc-on-tint);
  text-shadow: 0 0 2px rgb(0 0 0 / 0.45);
}

/* An open dimension: its name, the field a word is typed into, and what has been chosen so
   far. The three sit in a column so several of them read as a form rather than a sentence. */
.loc-token-field { display: flex; flex-direction: column; gap: 0.3rem; min-width: 12rem; }
/* A ruled dimension wears the same field as an open one — the same label, the same box, the
   same rule under it — so a block of them lines up row for row whether its answers are typed
   or picked. What differs is only what the box holds: a caret in one, and in this one what
   has been picked, said as words.
 *
 * The panel is taken out of flow and hung under the box. In flow it would push everything
   below it down as it opened and pull it back as it shut, which on a form is the whole page
   moving under somebody choosing a course. */
.loc-choice { position: relative; }
.loc-choice-face {
  appearance: none;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: start;
  align-items: center;
  justify-content: space-between;
  flex-wrap: nowrap;
  cursor: pointer;
}
.loc-choice-said {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.loc-choice-mark { flex: 0 0 auto; font-size: 0.7em; opacity: 0.7; }
.loc-choice-open .loc-choice-mark { rotate: 180deg; }
.loc-choice-menu {
  display: none;
  position: absolute;
  inset-inline: 0;
  inset-block-start: 100%;
  z-index: 5;
  /* Tall enough to read down, short enough that the list scrolls rather than the page: a menu
     longer than the window would put its last choice somewhere a thumb has to leave the menu
     to reach. */
  max-height: 14rem;
  overflow-y: auto;
  margin-block-start: 0.15rem;
  padding: 0.3rem;
  background: var(--loc-surface);
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius-sm);
  box-shadow: var(--loc-shadow-pop);
}
.loc-choice-open .loc-choice-menu { display: block; }
/* **The same control, standing open, where there is room for it.** A wide screen has the width
   to keep every criterion in view, and a panel of ticks a reader can see at once is quicker to
   narrow with than a row of menus that each have to be opened. So on a desktop the search
   panel drops the face and lets the list stand in the flow — the same field, the same state,
   the same ticks, wearing the shape the room allows.
 *
 * Only the search panel. A form is a column of fields and a list hanging out of one would make
   that field taller than its neighbours, which is the shape the field exists to avoid. */
@media (min-width: 48rem) {
  .loc-facet-panel .loc-choice-face { display: none; }
  .loc-facet-panel .loc-choice-menu {
    display: block;
    position: static;
    max-height: none;
    overflow: visible;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }
  /* **A standing list is read down, not aimed at.** Two paddings stack here — the row's and
     the tick's — which is the room a finger needs and a pointer does not. Closed up, the whole
     of a dimension is taken in at once, which is the reason the list stands open on a screen
     this wide in the first place. The hover band still covers the line, so what is about to be
     ticked is still the whole row. */
  .loc-facet-panel .loc-facet-entry { padding-block: 0; }
  .loc-facet-panel .loc-check { padding-block: 0.05rem; line-height: 1.35; }
}
.loc-token {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.75rem;
  padding: 0.1rem 0.2rem 0.1rem 0.5rem;
  border: 1px solid var(--loc-line-strong);
  border-radius: 1rem;
  background: var(--loc-surface-sunk);
}
.loc-token-drop { border: 0; background: none; color: var(--loc-muted); line-height: 1; padding: 0 0.25rem; }
.loc-token-drop:hover { color: var(--loc-danger); }
.loc-card-category .loc-card-chip {
  display: block;
  border-radius: 0 var(--loc-radius) 0 var(--loc-radius);
  border-inline-start: 1px solid var(--loc-line);
  border-block-end: 1px solid var(--loc-line);
  padding: 0.1rem 0.6rem;
}
/* A sentence about the recipe on its card, clamped: a card is a thumbnail, and one recipe
   with a long description must not make its card three times the height of its neighbours. */
.loc-card-description {
  margin: 0.15rem 0 0.35rem;
  font-size: 0.82rem;
  line-height: 1.35;
  color: var(--loc-muted);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The description is written text, so it arrives as blocks. Their spacing between each other is
   the writing's own; the space they would open above the first and below the last is the card's
   business, and the card has already said what it is. */
.loc-card-description > :first-child { margin-block-start: 0; }
.loc-card-description > :last-child { margin-block-end: 0; }
.loc-card-used { font-size: 0.75rem; color: var(--loc-muted); }
.loc-card-actions { margin-block-start: auto; display: flex; gap: 0.5rem; }

/* **A listing is worth the height of the screen.** A dialog sized to its contents is a
   different size in every kitchen and a different size after every save; one sized to the
   window is the same place every time, which is what makes a file list somewhere a reader
   learns rather than somewhere they re-read. The margin at each end keeps the page visible
   behind it, so the dialog reads as being over the work rather than as having replaced it. */
.loc-file-picker .modal-dialog {
  height: calc(100vh - 4rem);
  margin-block: 2rem;
  max-width: min(56rem, calc(100vw - 2rem));
}
/* The height is handed down the whole chain, because a link that does not pass it on stops it:
   the dialog is the window's height, the content is the dialog's, the body is what the header
   and the footer leave, and the pane is all of that. Then the listing inside the pane is what
   scrolls — one scrollbar, in the thing that is too long, with the search box and the file count
   standing still above it. */
.loc-file-picker .modal-content { height: 100%; }
.loc-file-picker .modal-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-block-size: 0;
  overflow: hidden;
}
/* A picker of pictures is given a height to sit at, because a gallery has no natural one. This
   one is filling a dialog that already has one, so it takes it rather than bringing its own —
   without this the pane keeps `height: min(60vh, 34rem)` and the dialog stands taller than the
   listing inside it. */
.loc-file-picker .loc-picker-pane {
  flex: 1 1 auto;
  height: auto;
  min-block-size: 0;
}

/* The name is asked for on the row where the dialog is answered, and it takes the width that
   row is not using — a file name is longer than the two words beside it, and a field sized to
   the words would show a fraction of what somebody typed. */
.loc-picker-foot { flex-wrap: nowrap; gap: 0.5rem; }
.loc-picker-foot .loc-picker-name { flex: 1 1 auto; min-width: 8rem; margin: 0; }

/* **A row per file, the height of a file.** The details view sizes its lead for a picture,
   because the pane it was built for is browsing pictures and the lead is the picture. A saved
   document has none — the lead is a glyph standing in a well four rems tall — so here it is the
   size of the line it sits on, and a listing of twenty labels is twenty lines rather than a
   column of empty frames. */
.loc-file-picker .pane-list.details .pane-lead {
  width: 1.6rem;
  height: 1.6rem;
  font-size: 0.9rem;
  background: none;
}

/* On a phone there is no room to spend saying "this is a dialog", so it takes the screen. */
@media (max-width: 34rem) {
  .loc-file-picker .modal-dialog {
    height: 100vh;
    margin: 0;
    max-width: 100vw;
  }
}

/* The pane's own ordering control, standing where it acts: after the search that narrows the
   listing and before the verbs that act on a row of it. Sized to its words rather than to the
   bar, so a pane offering two keys does not give up the room a pane offering six would need. */
.pane-sort {
  width: auto;
  flex: 0 0 auto;
}

/* --- the shared image viewer ---------------------------------------------- */
/* A blown-up image is a look, not a mode: the whole surface dismisses, and the image is
   sized to the window rather than the window to the image. */
.loc-viewer {
  position: fixed;
  inset: 0;
  z-index: 60;
  padding: 1.5rem;
  background: rgb(28 20 12 / 0.82);
  cursor: zoom-out;
}
/* Stepping through a list, wherever a list is walked one at a time. The glyph is the whole
   control, so it is a disc rather than a word, and it is the same disc on every surface
   that pages — only the palette is the surface's, because a control lifted over a darkened
   image and one standing beside a sheet of paper are not the same color. Dimmed at the
   ends, where there is nothing further to step to. */
.loc-stepper-control {
  /* The glyph is the whole control, so it is centered as one. It is an icon rather than a
     typed character: a chevron set as text is punctuation, drawn wherever its own side
     bearings and baseline put it inside the em box, which lands it high and off to one side
     of a circle however the box is aligned. An icon is drawn centered in its box, so aligning
     the box aligns the mark. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 50%;
  width: 3rem;
  height: 3rem;
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
}
.loc-stepper-control:disabled { opacity: 0.25; cursor: default; }
/* On the page's own stock unless the consumer says otherwise. */
.loc-stepper-control {
  border: 1px solid var(--page-rule);
  background: var(--loc-surface);
  color: var(--page-underline);
  box-shadow: var(--loc-shadow);
}
.loc-stepper-control:hover:not(:disabled) {
  background: var(--loc-accent);
  border-color: var(--loc-accent);
  color: var(--loc-on-tint);
}

/* The stepper: content with the way to the next of it at either edge. One layout wherever a
   list is walked — the controls take what they need, the content takes the rest — so a
   surface that pages says where it pages and nothing about how it looks doing it. */
.loc-stepper {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 1rem;
}
/* **A consumer says how wide its subject reads, never where the controls stand.** The measure
   is the content's own business — a recipe is set to a column somebody can read across, a
   picture takes what it is given — but the controls belong at the edges of the surface on
   every one of them. So the subject is capped and centered inside the middle column and the
   columns themselves are not up for negotiation: a surface that narrowed them would pull the
   controls in beside its content, which is the one thing they must never do. */
.loc-stepper-content {
  min-width: 0;
  max-width: var(--stepper-measure, none);
  margin-inline: auto;
}
/* **On a phone the gesture is the control.** A screen this narrow cannot spend its margins on
   two discs when a finger already has a way of saying onward, so the controls go and the
   subject takes the whole width. The swipe that replaces them is registered by the Stepper
   itself, which is what keeps this from being a look with no way through it. */
@media (max-width: 47.99rem) {
  .loc-stepper { grid-template-columns: minmax(0, 1fr); }
  .loc-stepper-control { display: none; }
}
/* **A button says its word where there is room for it, and wears its glyph where there is
   not.** Below the breakpoint a row of worded buttons is what pushes a toolbar off the side of
   the screen, and the glyph already carries the meaning — so the word is set aside rather than
   the button being dropped. It is hidden from the eye and not from a screen reader: a control
   whose only remaining mark is a picture is exactly the one that still has to be named. */
/* A glyph standing in for a word takes the whole button: the gap that separated it from the
   word it no longer has would push it off its own centre. */
@media (max-width: 47.99rem) {
  .loc-toolrow .btn > .me-1 { margin-inline-end: 0 !important; }
  .loc-btn i.me-1 { margin-inline-end: 0; }
}

/* The ground the controls stand on, which a consumer names when it builds one rather than
   the kit naming the consumer. `over` is a control lifted out of a darkened surface; the
   default is one drawn on the page's own stock. A third surface picks a tone; it does not
   get a rule of its own written here. */
.loc-stepper-over .loc-stepper-control {
  border: 1px solid rgb(245 224 195 / 0.35);
  background: rgb(28 20 12 / 0.45);
  color: var(--page-bg);
}
.loc-stepper-over .loc-stepper-control:hover:not(:disabled) {
  background: var(--loc-accent);
  border-color: var(--loc-accent);
}
.loc-viewer-frame {
  position: relative;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  max-height: 100%;
  min-width: 0;
}

/* **Loud, and where the change is expected.** Every other wait in this application is said
   quietly in the standby strip, because the person is working elsewhere while it happens.
   This one they are staring at: a frame that sits empty while a large photograph is fetched
   reads as broken, so the wait is drawn on the canvas the picture will fill. */
.loc-viewer-wait {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  color: var(--page-bg);
  pointer-events: none;
}
.loc-viewer-wait[hidden] { display: none; }
.loc-viewer-wait-said { font-family: var(--font-label); font-size: 1.1rem; }
.loc-viewer-image {
  max-width: 100%;
  max-height: calc(100vh - 7rem);
  object-fit: contain;
  border-radius: var(--loc-radius);
  box-shadow: var(--loc-shadow-pop);
  background: var(--loc-surface-sunk);
}
/* **The name may wrap; the measurement may not.** A size is one component — the figures, the
   sign between them and the unit are a single statement, and a line break anywhere inside it
   turns it into two half-statements. So the row wraps and the size does not: on a narrow frame
   the size drops under the name whole, which is the one place a break belongs. */
.loc-viewer-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.25rem 0.75rem;
  font-family: var(--font-label);
  font-size: 1.25rem;
  /* **The whole caption is white over the scrim**, name and measurement alike: it sits on a
     darkened photograph, and the page's own cream is a paper color that reads as dimmed
     there. Said once on the row, so the two parts cannot come to disagree about it — what
     separates them is size, not brightness. */
  color: var(--loc-on-tint);
}
.loc-viewer-title { min-width: 0; overflow-wrap: anywhere; }
.loc-viewer-detail {
  flex: 0 0 auto;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
}

/* --- the pane: a browsable list of folders and files ---------------------- */
/* Shared component (scripts/pane.js). Any page that constructs a Pane gets this; the page
   decides only how many panes there are and where they sit. */
/* The canonical pane geometry, in one place. Three independent dials, and they are
   independent on purpose — each sits in a different box and behaves differently:
     --pane-edge   the field's inset: the lane at the pane's edges. Outside the scroll,
                   so it is always where the eye expects it.
     --pane-gap    between rows: listing background, unambiguously free space.
     --pane-inset  inside a row: part of the row, so it draws as selection.
     --pane-label  between a row's lead and its name — neither lane nor highlight, just
                   the gap that keeps a caption off its picture.
     --pane-label-beside  the same distance where the name sits *beside* a picture rather
                   than beneath it. A caption under a thumbnail wants less air than a name
                   alongside a block: the axis changes what reads as close.
     --pane-tile   the width a picture wants to be read at, and the floor a wall of them
                   tiles from — the smallest a column may be before the wall drops one.
     --pane-tile-grow  how far past that a picture may be stretched to close a gap, as a
                   multiple. A wall with room to spare shares it out rather than leaving it
                   all at the right edge, and this is what stops a nearly empty pane from
                   answering with two enormous pictures.
   Space between items belongs in the gap; padding is space inside an item that should be
   highlighted with it. Getting that backwards swells the selection and eats the lane. */
.loc-pane {
  --pane-edge: 5px;
  --pane-gap: 10px;
  --pane-inset: 5px;
  --pane-label: 3px;
  --pane-label-beside: 9px;
  --pane-tile: 8rem;
  --pane-tile-grow: 1.4;
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  box-shadow: var(--loc-shadow);
  background: var(--loc-surface);
  overflow: hidden;
}


/* --- drag and drop, the standard ------------------------------------------ */
/* Every drop surface in the application speaks the same two colors, and it is one
   vocabulary rather than a per-component decision: **green is where this can land, red is
   where it came from**. A pane, a gallery, a picker — all wear these two classes, so a
   reader learns the language once.
   The marquee is a dashed edge, still: it says provisional without asking to be watched,
   and a drag already has the pointer's attention. It is drawn as a gradient on the four
   edges rather than as a border, because an outline cannot round its corners with the box
   it surrounds. */
.loc-drop-target, .loc-drop-source {
  --loc-march: 16px;
  --loc-drop-ink: var(--loc-ok);
  background-image:
    repeating-linear-gradient(90deg, var(--loc-drop-ink) 0 8px, transparent 8px 16px),
    repeating-linear-gradient(90deg, var(--loc-drop-ink) 0 8px, transparent 8px 16px),
    repeating-linear-gradient(0deg, var(--loc-drop-ink) 0 8px, transparent 8px 16px),
    repeating-linear-gradient(0deg, var(--loc-drop-ink) 0 8px, transparent 8px 16px);
  background-size: 100% 2px, 100% 2px, 2px 100%, 2px 100%;
  background-position: 0 0, 100% 100%, 0 100%, 100% 0;
  background-repeat: no-repeat;
  border-radius: var(--loc-radius);
}
.loc-drop-source { --loc-drop-ink: var(--loc-danger); }

/* A drop zone that refuses the gesture says so by staying quiet: no marquee, no invitation. */
.loc-drop-off { background-image: none; }

/* The bar and the breadcrumb stay put; only the listing scrolls. That also keeps the
   rubber band's geometry honest — the surface it is drawn on does not move under it. */
.pane-drop {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  padding: 0.6rem 0.35rem 0.35rem;
}
.pane-bar, .pane-crumb { flex: 0 0 auto; margin-inline: 0.35rem; }
.pane-drop.dragging { background-color: var(--loc-accent-soft); }
.pane-drop-hint {
  position: absolute;
  inset-block-end: 0.75rem;
  inset-inline: 0;
  text-align: center;
  font-family: var(--font-label);
  font-size: 1.2rem;
  color: var(--loc-accent-hover);
  pointer-events: none;
  opacity: 0;
}
.pane-drop.dragging .pane-drop-hint { opacity: 1; }
/* The invitation is for an empty pane taking freight from elsewhere. With rows already in
   it the marquee says where the drop lands, and the sentence is noise. */
.pane-drop.drop-hint-off .pane-drop-hint { opacity: 0; }
/* Rows must not swallow the drag events the pane counts to hold its highlight steady.
   The rows themselves are excused, not the list: a list that stops receiving pointer
   events stops being a drop target, and the drop lands nowhere. */
.pane-drop.dragging .pane-row { pointer-events: none; }

.pane-bar { display: flex; gap: 0.35rem; align-items: center; flex-wrap: wrap; margin-block-end: 0.5rem; }
.pane-steps { display: inline-flex; }
.pane-step {
  border: 1px solid var(--loc-line-strong);
  background: var(--loc-surface);
  color: var(--loc-ink);
  width: 2rem;
  height: 1.85rem;
  font-size: 0.85rem;
  cursor: pointer;
}
.pane-steps .pane-step:first-child { border-radius: var(--loc-radius-sm) 0 0 var(--loc-radius-sm); }
.pane-steps .pane-step:last-child { border-radius: 0 var(--loc-radius-sm) var(--loc-radius-sm) 0; }
.pane-steps .pane-step + .pane-step { border-inline-start: 0; }
.pane-action {
  border: 1px solid var(--loc-line-strong);
  border-radius: var(--loc-radius-sm);
  background: var(--loc-surface);
  color: var(--loc-ink);
  width: 2rem;
  height: 1.85rem;
  font-size: 0.85rem;
  cursor: pointer;
}
.pane-step:hover:not(:disabled), .pane-action:hover { background: var(--loc-accent-soft); border-color: var(--loc-accent); }
.pane-action-danger:hover { background: var(--loc-danger); border-color: var(--loc-danger); color: #fff; }
.pane-step:disabled, .pane-action:disabled { opacity: 0.35; cursor: default; }
/* The search control: a field and the button that empties it, one shape wherever a list
   filters, a batch count and the control that returns it to what the recipe says. The two
   are one control, so the rule and the focus ring go around the pair — a ring that stopped
   at the field would make the button look like a separate thing that happens to sit
   against it. Same face and same focus treatment as every other text field; smaller here
   because the bars it sits in are. */
.loc-field {
  display: flex;
  flex: 1 1 4rem;
  min-width: 0;
  height: 1.85rem;
  border: 1px solid var(--loc-line-strong);
  border-radius: var(--loc-radius-sm);
  background: var(--loc-surface);
  overflow: hidden;
}
.loc-field:focus-within {
  border-color: var(--loc-accent);
  box-shadow: 0 0 0 0.2rem var(--loc-accent-soft);
}
.loc-field-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  outline: none;
  background: none;
  color: var(--loc-ink);
  font-family: var(--font-ui);
  font-size: 0.85rem;
  padding: 0.1rem 0.55rem;
}
.loc-field-input::placeholder { color: var(--loc-line-strong); }
/* The control supplies its own clear button, so the browser's is redundant. */
.loc-field-input::-webkit-search-cancel-button { appearance: none; display: none; }
.loc-field-clear {
  flex: 0 0 auto;
  width: 1.75rem;
  border: 0;
  border-inline-start: 1px solid var(--loc-line);
  background: none;
  color: var(--loc-muted);
  font-size: 0.8rem;
  cursor: pointer;
}
.loc-field-clear:hover:not(:disabled) { background: var(--loc-accent-soft); color: var(--loc-accent-hover); }
.loc-field-clear:disabled { opacity: 0.3; cursor: default; }

/* The one name editor, used by a click on a name, by Rename, and by a new folder. */
.pane-name.editing {
  outline: 2px solid var(--loc-accent);
  outline-offset: 1px;
  border-radius: 2px;
  background: var(--loc-surface);
  cursor: text;
  white-space: pre-wrap;
}

/* Where the pane is standing, and how much is in it — the count against the far edge, so
   two panes side by side can be compared down a straight line. */
.pane-crumb {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  font-family: var(--font-label);
  font-size: 1.2rem;
  color: var(--loc-accent);
  padding-block-end: 0.3rem;
  border-bottom: 2px solid var(--page-rule);
  margin-block-end: 0.6rem;
}
.pane-crumb-count { flex: 0 0 auto; font-variant-numeric: tabular-nums; font-size: 0.8rem; color: var(--loc-muted); }

/* The adjustment layer. It draws nothing; it only holds the inset that gives the listing
   its edge lanes, in one place and out of the scroll. */
.pane-field {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  padding: var(--pane-edge);
}

.pane-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Reserve the scrollbar's width whether or not one is showing, so a list that grows past
     its pane does not shove its own contents sideways. */
  scrollbar-gutter: stable;
  /* A strip past the last row, so the end of a full list is somewhere a band and a drop
     can still land. The side lanes are the field's, not this element's — padding here
     would scroll away with the content. */
  padding-block-end: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-content: flex-start;
}
/* An empty listing fills its own space rather than sitting at the top of it: the glyph and
   its caption center on the whole field, faded back, so emptiness reads as a state of the
   pane instead of as the pane's first row. */
.pane-blank {
  flex: 1 1 auto;
  grid-column: 1 / -1;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 8rem;
  padding: 1.5rem 0.5rem;
  color: var(--loc-muted);
  opacity: 0.55;
  text-align: center;
  pointer-events: none;
}
.pane-blank i { font-size: 3.25rem; line-height: 1; }
.pane-blank-caption {
  font-family: var(--font-label);
  font-size: 1rem;
  letter-spacing: 0.02em;
}

/* The rubber band. Drawn inside the pane, never intercepting the pointer that draws it. */
.pane-band {
  position: absolute;
  z-index: 2;
  border: 1px solid var(--loc-accent);
  background: rgb(138 90 46 / 0.12);
  border-radius: 2px;
  pointer-events: none;
}
/* A sweep must not turn into a text drag across the names it passes over. */
.pane-drop.banding { user-select: none; }
/* Which pane the keyboard is talking to. Focus lands on the pane as soon as it is clicked
   into, and the ring follows it between panes. */
.pane-drop:focus { outline: none; }
.loc-pane:focus-within {
  border-color: var(--loc-accent);
  box-shadow: 0 0 0 2px var(--loc-accent-soft), var(--loc-shadow);
}

/* Keyboard walking scrolls the least it can to show the row, and this is what keeps it a
   little clear of the edge it arrives at rather than flush against it. */
.pane-row {
  display: flex;
  align-items: center;
  gap: var(--pane-label);
  border: 1px solid transparent;
  border-radius: var(--loc-radius-sm);
  padding: 0.35rem;
  cursor: default;
  scroll-margin: 0.6rem;
}
.pane-row:hover { border-color: var(--loc-line-strong); }
.pane-row.active { border-color: var(--loc-accent); background: var(--loc-accent-soft); }

/* One row shape, three densities. The lead is a glyph or a picture; the meta column holds
   the editable name and, in details, the figures beside it. */
.pane-meta { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
/* The lead is one box, whatever it holds: a folder glyph, a file glyph, or a thumbnail.
   The view sizes the box and the contents fill it, so every row in a pane has an identical
   lead however it is rendered — the alignment is structural, not two rules agreeing. */
.pane-lead {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-radius: var(--loc-radius-sm);
}
.pane-lead img { width: 100%; height: 100%; object-fit: cover; }
.pane-icon { color: var(--loc-accent); }
.pane-folder .pane-icon { color: var(--page-underline); }

/* **A wall of pictures, read two ways.** Both lay the rows out identically — the lane between
   them is **gap, not padding**, because padding sits inside the row's box and draws as part of
   the selection: the highlight swells past the picture and the band appears to have nowhere to
   start. The row hugs its picture; the space between rows belongs to the listing.

   What the two differ in is one question, and it is the reader's to answer: whether a picture
   is shown whole or shown to a shape.

   **The wall counts its columns from the width it is given, and the pictures take up the
   slack.** A row of fixed-width pictures wrapped by hand leaves whatever did not divide
   evenly as one dead band down the right, which is worst exactly where space is scarce. So
   the wall is a grid of equal columns: as many as fit at `--pane-tile`, each one sharing what
   is left over, so a pane resized by a hair adds a column or widens the ones it has rather
   than growing a gap. `--pane-tile-grow` caps how far a picture is stretched — past that the
   remainder is spread as lanes between them, which reads as spacing rather than as a picture
   blown up beyond the size it was worth showing at. */
.pane-list.thumb,
.pane-list.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--pane-tile), 100%), 1fr));
  gap: var(--pane-gap);
  align-content: start;
}
.pane-list.thumb .pane-row,
.pane-list.tiles .pane-row {
  flex-direction: column;
  align-items: stretch;
  width: 100%;
  max-width: calc(var(--pane-tile) * var(--pane-tile-grow));
  justify-self: center;
  padding: var(--pane-inset);
}
.pane-list.thumb .pane-lead,
.pane-list.tiles .pane-lead {
  width: 100%;
  font-size: 2.75rem;
  background: var(--loc-surface-sunk);
}

/* **Tiles: the picture whole.** The tile is as tall as the image needs, so nothing is lost and
   no two are the same height. Blocks in a line share the tallest one's height and the picture
   sits on the bottom of its block, so the row of names reads as a line instead of stepping up
   and down with whatever aspect each image happens to have — the slack goes above the picture,
   where nothing is looking. */
.pane-list.tiles .pane-row { justify-content: flex-end; }
.pane-list.tiles .pane-lead {
  height: auto;
  min-height: 3rem;
  margin-block-start: auto;
}
.pane-list.tiles .pane-lead img { height: auto; object-fit: contain; }

/* **Thumbnails: the picture to a shape.** Every lead is the same box, and the image fills it
   from the middle — a tall picture loses its top and bottom, a wide one its sides. The wall
   then reads as a grid rather than as a row of different heights, which is the whole of what
   is bought and what is paid for. */
.pane-list.thumb .pane-lead {
  height: auto;
  aspect-ratio: 4 / 3;
}
.pane-list.thumb .pane-lead img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* List: one tight row each, a small glyph and a name. */
.pane-list.list { flex-direction: column; flex-wrap: nowrap; gap: 0; }
.pane-list.list .pane-row { width: 100%; padding: 0.1rem var(--pane-inset); }
.pane-list.list .pane-lead { width: 1.3rem; height: 1.3rem; font-size: 0.95rem; }

/* Details: one lead width for every row, so names and figures line up down the column. */
.pane-list.details { flex-direction: column; flex-wrap: nowrap; gap: var(--pane-gap); }
.pane-list.details .pane-row { width: 100%; padding: var(--pane-inset); gap: var(--pane-label-beside); }
.pane-list.details .pane-lead { width: 5.5rem; height: 4rem; font-size: 2rem; background: var(--loc-surface-sunk); }

.pane-name { font-size: 0.82rem; overflow-wrap: anywhere; }
.pane-detail { font-size: 0.72rem; color: var(--loc-muted); }

/* --- identity and menus --------------------------------------------------- */
/* The tabs wrap to as many rows as the width needs, and the strip keeps its rule: the
   active tab reads as sitting on the line, which is what makes it look like a tab.
   Never a scroll container — `overflow-x` makes the cross axis scrollable too, and the
   two pixels the tabs are pulled down over the rule then become a stub scrollbar. */
.loc-nav {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.2rem;
}
.loc-user { position: relative; flex: 0 0 auto; margin-inline-start: auto; }
/* **A circle is a circle at every width.** It stands in a flex row, and a width is only a
   starting size there — a neighbour asking for room shrinks it, and a round thing shrunk on one
   axis is an oval. So it is told not to give any up, and its width is stated as a floor as well
   as a size. */
.btn.loc-glyph {
  position: relative;
  flex: 0 0 auto;
  width: 2rem;
  height: 2rem;
  min-inline-size: 2rem;
  padding: 0;
  border-radius: 50%;
  border: 0;
  background: var(--loc-accent);
  color: #fff;
  font-weight: 600;
  cursor: pointer;
}
.loc-glyph[aria-expanded="true"] { outline: 2px solid var(--loc-ink); }
/* The picture covers the chip rather than sitting inside it, so the initial underneath
   shows through the moment the picture is absent or fails to load. */
.loc-user-face {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}
.loc-menu-label { display: block; font-size: 0.75rem; color: var(--loc-muted); }
.loc-book label { display: block; margin-block: 0.5rem; }
.loc-book input, .loc-book select { display: block; width: 100%; padding: 0.4rem; margin-block-start: 0.2rem; }

/* --- transient surfaces --------------------------------------------------- */
.loc-toast {
  position: fixed;
  inset-block-end: 1rem;
  inset-inline-start: 50%;
  translate: -50% 0;
  padding: 0.6rem 1rem;
  border-radius: var(--loc-radius-sm);
  background: var(--loc-ink);
  color: #fff;
  box-shadow: var(--loc-shadow-pop);
}
.loc-toast-success { background: var(--loc-ok); }
.loc-toast-warn { background: var(--loc-warn); }
.loc-toast-danger { background: var(--loc-danger); }
/* **A notice goes where the controls are not.** With the bar standing at the foot of a phone
   screen, a notice raised at the same edge lands on top of the button that raised it — and on
   the thumb about to press another. It is said at the top instead, in the band the content
   gives up most cheaply, and it spans the width because there is no width to spare for it to
   sit beside anything. Two classes, so it answers over the utilities in the markup. */
@media (max-width: 47.99rem) {
  .toast-container.position-fixed {
    inset-block: 0 auto;
    inset-inline: 0;
    align-items: center;
  }
  .loc-toast { inset-block: 1rem auto; }
}

/* The blocking surfaces the Auth contract requires. One overlay style; the modifier class
   only changes accent, never layout. */
.loc-overlay { position: fixed; inset: 0; display: grid; place-items: center; background: rgb(59 42 24 / 0.55); }
.loc-overlay-body {
  max-width: 28rem;
  padding: 1.25rem 1.5rem;
  background: var(--loc-surface);
  border-radius: var(--loc-radius);
  box-shadow: var(--loc-shadow-pop);
}
.loc-overlay-body h2 { margin-block-start: 0; }
.loc-detail { color: var(--loc-muted); }
.loc-button {
  padding: 0.45rem 0.9rem;
  border: 0;
  border-radius: var(--loc-radius-sm);
  background: var(--loc-accent);
  color: #fff;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
.loc-button.loc-quiet { background: var(--loc-muted); }
/* **A dialog in the middle, over a mask across the whole screen.** The mask is the working
   part: nothing behind it is trustworthy while the lane is down, so the page is covered
   rather than merely overlaid, and it is heavy enough to read as covered rather than as
   dimmed. */
.loc-down + .modal-backdrop,
.modal-backdrop.show { --bs-backdrop-opacity: 0.72; }

/* **Every button in the application.** A button is a button wherever it stands — in a
   toolbar, beside a field, at the foot of a dialog — so its size and shape are said once
   here and worn by all of them. The only thing a caller chooses is what the button *means*,
   and that is carried by one tint variable rather than by a different color class in each
   fragment: quiet for a way out or a thing merely available, accent for the act the surface
   is about, danger for an act that destroys something. Where a button stands may change its
   measure — a dialog's answer is given a minimum so two of them read as a pair — but never
   how it is themed. */
.btn.loc-btn {
  --loc-btn-tint: var(--loc-accent);
  padding: var(--loc-control-pad-block) 1rem;
  font-size: var(--loc-control-face);
  line-height: var(--loc-control-line);
  border: 1px solid var(--loc-btn-tint);
  border-radius: var(--loc-radius-sm);
  background: var(--loc-btn-tint);
  color: var(--loc-on-tint);
}
.btn.loc-btn:hover:not(:disabled) {
  color: var(--loc-on-tint);
  filter: brightness(0.92);
}
.btn.loc-btn:focus-visible { box-shadow: 0 0 0 0.2rem var(--loc-accent-soft); }
.btn.loc-btn:disabled { opacity: 0.5; }

/* A way out, or a thing merely available: it says what can be done without competing with
   what the surface is about. */
.btn.loc-btn-quiet {
  --loc-btn-tint: var(--loc-line-strong);
  background: var(--loc-surface);
  color: var(--loc-ink);
}
.btn.loc-btn-quiet:hover:not(:disabled) {
  color: var(--loc-ink);
  background: var(--loc-accent-soft);
  border-color: var(--loc-accent);
}

/* Destroying something is offered, never urged: the button says what it would do and waits
   to be chosen, so it carries the danger ink on the page's own surface and only fills once
   the pointer is on it. */
.btn.loc-btn-danger {
  --loc-btn-tint: var(--loc-danger);
  background: var(--loc-surface);
  color: var(--loc-danger);
}
.btn.loc-btn-danger:hover:not(:disabled) {
  background: var(--loc-danger);
  color: var(--loc-on-tint);
}

/* The act that finishes the thing, rather than the one that carries it on. */
.btn.loc-btn-ok { --loc-btn-tint: var(--loc-ok); }

/* A button that is a mark rather than a word: square, so it takes a column no wider than the
   thing it draws, and the same height as every other control on the row. What it does is
   said in its label for anyone who cannot see the mark. */
.btn.loc-btn-icon {
  min-width: 0;
  padding-inline: 0.55rem;
}

/* A dialog's answer is given at the foot of it, and the buttons there read as a pair: one
   measure between them, whatever their words. */
.modal-footer {
  gap: 0.5rem;
  border-top: 1px solid var(--loc-line);
}
.modal-footer > * { margin: 0; }
.modal-footer .loc-btn { min-width: 7rem; }
/* **On a phone the footer is one row across the full width, edge to edge.** The labels are
   already down to their glyphs there, so a control costs a glyph and two stacked rows would
   spend twice the height of a short screen saying the same thing.
   Stated here, after every rule it undoes rather than beside the ones it belongs with: the
   gutter, the margins and the 7rem floor above are each written at the weight this is written
   at, so what settles them is being written last. The same arrangement as the toolbar's, for
   the same reason.
   The sets are dissolved so every control is a share of the row itself. Left standing they are
   two boxes dividing the row, so it is divided twice — once between the sets and again inside
   each — and the halves disagree. The floor goes with them: a width that keeps worded buttons
   even is the wrong width once the words are gone, and seven of it is wider than the screen. */
@media (max-width: 47.99rem) {
  .loc-editor-footer { flex-wrap: nowrap; column-gap: 0; padding: 0; }
  .loc-editor-footer > *,
  .loc-editor-footer > * > * { margin: 0; }
  .loc-editor-footer > .loc-toolrow-group { display: contents; gap: 0; }
  .loc-editor-footer .btn { flex: 1 1 0; min-width: 0; padding-inline: 0; border-radius: 0; }
}
.loc-down .loc-down-content {
  border: 0;
  border-top: 3px solid var(--loc-warn);
  border-radius: var(--loc-radius);
  box-shadow: var(--loc-shadow);
}
.loc-down .loc-down-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding: 2rem 2.25rem 2.25rem;
  text-align: center;
}
/* It waits visibly: something is being attempted on the reader's behalf, and a still dialog
   reads as a dead end they are expected to act on. */
.loc-down .loc-down-wait { margin-block-end: 0.4rem; }
.loc-down .loc-down-lead { margin: 0; }
.loc-down .loc-down-title { font-family: var(--font-label); font-size: 1.6rem; margin: 0; }
.loc-unauthorized .loc-overlay-body { border-top: 3px solid var(--loc-danger); }

/* **The frame a document is printed from.** It holds the sheet while the browser's own viewer
   prints it, and is never looked at — so it takes no room and shows nothing. Removed from the page
   it would take the print preview with it, which is why it stays. */
.loc-print-frame {
  position: fixed;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: 0;
  block-size: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

/* **Waiting on something being made elsewhere.** The same shape the lane-down dialog wears, and
   for the same reason: a mask over the whole page, no way out, and a visible sign of work, because
   the page behind it is not a place to act while what it shows is being rebuilt. It is tinted with
   the accent rather than the warning color — nothing is wrong, something is simply taking a
   moment. */
.loc-wait .loc-wait-content {
  border: 0;
  border-top: 3px solid var(--loc-accent);
  border-radius: var(--loc-radius);
  box-shadow: var(--loc-shadow);
  overflow: hidden;
}
.loc-wait .loc-wait-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding: 2rem 2.25rem 1.75rem;
  text-align: center;
}
.loc-wait .loc-wait-mark { margin-block-end: 0.4rem; color: var(--loc-accent); }
.loc-wait .loc-wait-lead { margin: 0; color: var(--loc-muted); }
.loc-wait .loc-wait-title { font-family: var(--font-label); font-size: 1.6rem; margin: 0; }
/* **A measure of the whole dialog rather than a control inside it**, so it sits at the foot at the
   full width with nothing indenting it — the shape a page uses to say *this far along*. */
.loc-wait .loc-wait-foot {
  border: 0;
  padding: 0 1.25rem 1.1rem;
  gap: 0.4rem;
}
.loc-wait .loc-wait-bar {
  block-size: 0.4rem;
  inline-size: 100%;
  background: var(--loc-line);
}
/* It moves to where it has got to rather than jumping there: the stages are few and far apart, and
   a bar that stepped between them would read as four separate waits rather than one. */
.loc-wait .loc-wait-fill {
  display: block;
  block-size: 100%;
  inline-size: 0;
  background: var(--loc-accent);
  transition: inline-size 0.25s ease-out;
}

/* Connection state: a footer strip, shown only while something is pending. */
.loc-status {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  padding: 0.35rem 1rem;
  background: var(--loc-ink);
  color: #fff;
  font-size: 0.85rem;
}

/* The waiting surface. Sits above every other overlay: it blocks whatever raised it. */

/* Print is a document, not a window: the frame that keeps the application on one screen
   would otherwise clip everything past the first page. */
@media print {
  html, body { height: auto; }
  body { display: block; }
  .loc-main { overflow: visible; }
}

/* --- signed out ----------------------------------------------------------- */
/* Its own page, standing without the application chrome: centered, quiet, and offering
   the one thing there is to do from here. */
.loc-away { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.loc-away-card { text-align: center; padding: 2rem; }
/* The arc carries the name. Its type is the display face and its fill is the accent, so
   the page needs no color of its own — it is the palette, at rest. */
/* The type is sized in user units, not rem: the box scales with the viewport and the
   glyphs must scale with it, or the arc that fits at one width crops at another. */
.loc-away-arc { display: block; width: min(34rem, 88vw); height: auto; margin-inline: auto; overflow: visible; }
.loc-away-arc text {
  font-family: var(--font-label);
  font-size: 62px;
  fill: var(--loc-accent);
}
.loc-away-detail { color: var(--loc-muted); margin-block-end: 1.5rem; }

/* --- the contact card ----------------------------------------------------- */
/* One grid serves every contact: a customer in the rolodex and the business itself are
   the same card, so neither page brings a layout of its own. */
.loc-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.75rem 1rem;
  margin-block-start: 0.5rem;
}
.loc-card-cell.loc-card-wide { grid-column: 1 / -1; }
.loc-brand-logo-row { display: flex; gap: 1rem; align-items: flex-start; margin-block-end: 1.25rem; }
.loc-brand-preview {
  width: 6rem;
  height: 6rem;
  object-fit: contain;
  flex: 0 0 auto;
  background: var(--loc-surface-sunk);
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  padding: 0.4rem;
}
.loc-brand-summary { color: var(--loc-muted); }

/* --- the picture picker --------------------------------------------------- */
/* The images page's own browser, in a dialog. It is given a height because a modal body
   has none of its own, and the pane scrolls inside it exactly as it does on the page. */
.loc-picker-pane { height: min(60vh, 34rem); }
/* The mark is offered in every folder and is not one of the pictures around it, so it is
   drawn as pinned rather than merely first — a row that is sorted nowhere needs to look
   like it was placed. */
.loc-pane-pinned {
  outline: 2px solid var(--loc-accent);
  outline-offset: -2px;
  border-radius: var(--loc-radius-sm);
  background: var(--loc-accent-soft);
}

/* --- a tab's own tool row ------------------------------------------------- */
/* Under the tab strip, never in it: the strip selects, the row acts. What acts on the
   thing sits left, what describes or scopes it sits right, and a narrow window wraps the
   two onto separate lines rather than shrinking either. */
/* The page's own band, directly under the nav and ruled off from the content the same way
   the nav is ruled off from it: what can be done sits left, what is being shown sits right,
   and the line under it says where the chrome stops and the page begins. */
.loc-toolrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 0.75rem;
  /* The band stands above the scroll the way the nav stands above the page — by being
     outside it, not by being lifted over it. It is an ordinary row in ordinary flow; what
     scrolls is the region beneath it.
   *
   * Its vertical rhythm is its own and it is even: the shell holds no padding above a page
   * that fixes its head, so the space over the controls is this row's and matches the space
   * under them, with the rule centered between. */
  flex: 0 0 auto;
  margin-block: 0 0.45rem;
  padding-block: 0.5rem;
  border-block-end: 1px solid var(--loc-line);
}
/* **The rule goes where the bar meets the page.** Standing at the foot of the screen the bar
   is closed from above, not below: a line under the buttons separates them from the edge of
   the window, which is nothing, while the content they act on runs right up to them unmarked.
   So the same rule and the same spacing turn over — the one line the bar draws stays one line,
   drawn on the side the page is actually on. */
@media (max-width: 47.99rem) {
  .loc-toolrow {
    margin-block: 0.45rem 0;
    border-block-end: 0;
    border-block-start: 1px solid var(--loc-line);
  }
  /* The bar takes the whole width only when it is using the whole width. A set that fills the
     row is a band across the foot of the screen and stands on the edges; the book's own two
     controls are objects on a page and keep the margins the page keeps. Margin rather than
     padding, so the rule above them draws to the same measure they do. */
  .loc-toolrow:not(:has(.loc-toolrow-fill:not([hidden]))) {
    margin-inline: var(--loc-gutter);
  }
}
.loc-toolrow-count { color: var(--loc-muted); font-size: 0.85rem; }
/* **On a phone the count gives up its place to the controls.** It is a sentence's worth of
   width spent restating what the list below it already shows, and it is the one thing on the
   bar that can go without taking a capability with it — the alternative is the buttons wrapping
   to a second line. Kept for a screen reader, which has no list in view to count. */
@media (max-width: 47.99rem) {
}
/* A listing with nothing in it says so in the space the listing would have filled: centered,
   at reading size, in the muted ink — an absence stated plainly rather than a footnote under
   an empty area. */
.loc-empty {
  margin: 0;
  padding-block: 3rem;
  text-align: center;
  font-family: var(--font-label);
  font-size: 1.75rem;
  color: var(--loc-muted);
}

/* Held back without being taken away: the element keeps the space it occupies and is simply
   not drawn, on screen or on paper. For anything whose absence would move what surrounds it.  */
.loc-held { visibility: hidden; }

/* The parts of a document a reader may hold back from the paper, as one row of like choices
   beside the control that prints. They are siblings and read as siblings: same shape, same
   size, same voice, one after the next — a checkbox loose in each section would make one
   decision look like several. */
.loc-print-options { gap: 0.25rem 0.9rem; color: var(--loc-muted); font-size: 0.8rem; }
.loc-print-lead { font-family: var(--font-label); font-size: 1.05rem; }
.loc-print-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0;
  cursor: pointer;
  user-select: none;
}
/* **On a phone the reader's controls are one row across the full width, edge to edge.** The same
   division the editor's footer makes, for the same reason: the labels are down to their glyphs,
   so a control costs a glyph, and a set of them each as wide as its own word leaves the row
   uneven and short of the edges.
   The set is dissolved so every control is a share of the row itself rather than a share of its
   own group, and the 7rem floor comes off — a width that keeps worded buttons even is the wrong
   width once the words are gone.
   The display choices are named out of that and keep a row of their own. They are words with
   boxes beside them rather than controls to be spread, and squeezed onto the end of a row of
   buttons they wrap a word at a time into whatever gap is left. They keep the page's gutter,
   since a line of text against the edge of the screen is not a control reaching it.
   Stated after every rule it undoes, which is where a narrow-screen rule goes (R40.8). */
@media (max-width: 47.99rem) {
  .loc-sheet-footer { flex-wrap: wrap; column-gap: 0; padding: 0; }
  .loc-sheet-footer > *,
  .loc-sheet-footer > * > * { margin: 0; }
  .loc-sheet-footer > .loc-toolrow-group:not(.loc-print-options) { display: contents; gap: 0; }
  .loc-sheet-footer .btn { flex: 1 1 0; min-width: 0; padding-inline: 0; border-radius: 0; }
  .loc-sheet-footer .loc-print-options {
    order: -1;
    flex: 1 0 100%;
    padding: 0.4rem var(--loc-gutter);
  }
}

/* A label and its control read as one thing on a toolbar, not as a form. */
.loc-sort-host { gap: 0.4rem; }
.loc-sort-host .loc-hint { margin: 0; white-space: nowrap; }
.loc-toolrow-lead, .loc-toolrow-trail {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.loc-toolrow-trail { margin-inline-start: auto; }
/* A set of controls that belongs to one state of the page, shown or not as a set. It lays
   out like the row it sits in, so the row reads the same however many sets are on it. */
.loc-toolrow-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.loc-toolrow-group .form-select { width: auto; }
/* Offered on a phone and absent everywhere else: a wide screen has both the bandwidth and the
   room to spare, so the choice is not drawn there rather than drawn and pointless. Width
   decides, not the page, so there is one answer to where it applies — and `hidden` still
   governs whether the set is on the bar at all, which is the page's business and stays so. */
.loc-toolrow-group.loc-mobile-only,
.loc-toolrow .btn.loc-mobile-only { display: none; }
@media (max-width: 47.99rem) {
  .loc-toolrow .loc-toolrow-group.loc-mobile-only:not([hidden]) { display: flex; }
  .loc-toolrow .btn.loc-mobile-only:not([hidden]) { display: inline-block; }
}
/* **On a phone the buttons share one row and the choices take the next.** The choices are a
   set of words with boxes beside them, and squeezed onto the end of a row of buttons they wrap
   a word at a time into whatever gap is left — so below the breakpoint they go under the whole
   bar rather than under half of it.

   The leading group dissolves to make that possible: the choices sit inside it, so on their
   own they could only ever drop below the buttons standing beside them and would leave the
   trailing set stranded on a third row. Dissolved, every button is an item of the bar itself
   — the page's controls and the recipe's on one line, the way the bar reads on a wide screen
   — and the choices are ordered first, taking the upper line so the buttons fall to the lower
   one, nearest the thumb of whoever is holding the phone. That ordering is the only thing that
   has to be said about where anything goes. Stated after the groups it undoes, since they
   carry the same weight. */
/* **The choices take the upper line and the acts take the lower one.** What is shown and
   what order it is in are both statements about the list rather than things done to it, so
   they read together — the one against the left edge, the other against the right. The acts
   keep a line of their own beneath, whole and evenly divided, where a thumb reaches them. */
@media (max-width: 47.99rem) {
  .loc-toolrow-split > .loc-print-options { order: -2; flex: 0 1 auto; display: flex; }
  .loc-toolrow-split > .loc-toolrow-trail { order: -1; margin-inline-start: auto; display: flex; flex: 0 1 auto; min-width: 0; }
  .loc-toolrow-split .loc-sort-host { display: flex; flex: 0 1 auto; min-width: 0; }
  .loc-toolrow-split .loc-facet-sort { min-width: 0; max-width: 8.5rem; }
  /* **The menu names itself.** "Order by" in front of a list that reads Name, Course, Diet is
     a word spent saying what the next word already says, and it is the width that decides
     whether the choices share a line with the covers. Set aside for the eye and kept for a
     screen reader, which has no menu in view to read from. */
  .loc-toolrow-split > .loc-toolrow-lead { order: 0; display: flex; flex: 1 0 100%; gap: 0; }
  .loc-toolrow-split > .loc-toolrow-lead > .btn { flex: 1 1 0; }
}
/* Every control on the bar is the same height, and every button on it is at least the same
   width: room for a glyph and five characters, so Edit, Cook, Print, Clone and the rest come
   out one size instead of each being as wide as its own word. A longer label still takes what
   it needs — the floor is what makes the short ones agree, not a cap on the long ones.
   Measured in `ch` because the thing being fitted is characters: change the face and the
   floor follows it. Scoped to the bar, so nothing else in the kit is resized by it. */
.loc-toolrow .btn, .loc-toolrow .form-select { height: 2rem; }
.loc-toolrow .btn { min-width: var(--loc-toolbar-button); }
/* A width set to keep worded buttons even with each other is the wrong width once the words
   are gone: it holds a glyph in the middle of a button sized for a sentence. Stated after the
   width it undoes, because the two carry the same weight and the last one written wins. */
@media (max-width: 47.99rem) {
  .loc-toolrow .btn { min-width: 0; }
}
/* Wordless buttons need no room between them to be told apart — the glyphs already are, and
   on a phone the row has more controls than it has width. So the space between them closes
   and the space between the rows does not: the column gap goes, the row gap stays, and the
   choices keep the line above to themselves.
 *
 * The checkbox sets wear the same group class as the buttons, and their gap is holding a tick
 * apart from the word it belongs to rather than one control apart from the next. They are
 * named out, because closing that space would run the label into the box. */
/* Dividing the row is what a full row wants and an empty one does not. A set that fills the
   bar — everything that can be done to the recipe that is open — is worth spreading across it;
   the book's own two controls spread across the same width read as a row of gaps with buttons
   in them. So the sets that fill say so, and only they are divided. */
@media (max-width: 47.99rem) {
  .loc-toolrow:has(.loc-toolrow-fill:not([hidden])) { column-gap: 0; }
  .loc-toolrow:has(.loc-toolrow-fill:not([hidden])) .loc-toolrow-trail,
  .loc-toolrow:has(.loc-toolrow-fill:not([hidden])) .loc-toolrow-group:not(.loc-print-options) { gap: 0; }
}
/* **On a phone the row is one measure, divided.** The buttons stop being a series of objects
   each as wide as it needs and become equal shares of the width available, which is the only
   division that stays even however many of them a mode puts up. The sets they are grouped
   into are dissolved so every button is a share of the row rather than a share of its own
   group — otherwise a mode showing two sets divides the row twice and the halves disagree.
 *
 * A set that is not showing is left alone. `hidden` says so by not drawing at all, and a
 * display given here would answer over it and put every mode's controls up at once. */
@media (max-width: 47.99rem) {
  .loc-toolrow:has(.loc-toolrow-fill:not([hidden])) .loc-toolrow-trail:not([hidden]),
  .loc-toolrow:has(.loc-toolrow-fill:not([hidden]))
    .loc-toolrow-group:not(.loc-print-options):not([hidden]) { display: contents; }
  .loc-toolrow:has(.loc-toolrow-fill:not([hidden])) .btn { flex: 1 1 0; }
}

/* --- the list bar --------------------------------------------------------- */
/* Search and order share a row and read left to right: the label, its control, then the
   next. The chips wrap onto their own line beneath, because there can be many of them and
   they must not push the two controls a reader uses constantly out of reach. */
/* The row owns its whole line inside the bar, so the two controls have the width to sit
   side by side. Without it the row is one flex item among several and sizes to its
   content, which puts the order control on a line of its own while the space it needed
   sits unused beside it. */
/* The criteria stand beside the results. The panel is a column that scrolls on its own —
   four dimensions of tags is longer than a screen — and the results scroll with the page,
   so narrowing the list never moves the criteria out from under the pointer. On a narrow
   window the two stack and the panel goes back to being a band above the cards. */
/* **A column may be told it is a share of the width; it must also be told it may be less.**
   `1fr` is `minmax(auto, 1fr)`, so a track will not shrink below what is in it — one long
   recipe name, or a card at its own minimum, and the grid grows wider than the screen and
   takes the page with it. `minmax(0, 1fr)` is the same share with permission to be narrow. */
.loc-browse {
  display: grid;
  grid-template-columns: minmax(13rem, 17rem) minmax(0, 1fr);
  gap: 1.25rem;
  align-items: start;
}
.loc-browse-results { min-width: 0; }
.loc-facet-panel:empty { display: none; }
.loc-facet-panel {
  position: sticky;
  top: 0.5rem;
  max-height: calc(100vh - 8rem);
  overflow-y: auto;
  padding: 0.75rem;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  background: var(--loc-surface);
  /* The panel stands off the page exactly as a card does. It is the same kind of object —
     something laid on the surface rather than printed into it — so it takes the same token
     rather than a second value that would drift from it. */
  box-shadow: var(--loc-shadow-pop);
}
.loc-facet-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.5rem; }
/* The search heading takes a row of its own, exactly as a group's heading does. `display:
   block` says nothing inside a flex container — an item is sized by its own content — so
   the rule under it would stop at the width of the word and read as a different kind of
   thing from the headings it sits above. */
.loc-facet-head .loc-facet-heading { flex: 1 1 100%; }
.loc-facet-head .loc-field { flex: 1 1 100%; }
.loc-facet-reset { flex: 0 0 auto; }
/* **On a phone the two share a line.** The field takes a row of its own where there is width
   to spare beneath it, but on a screen this narrow every row the panel spends is a row of the
   book it is not showing — and the control that empties the search belongs beside the box it
   empties anyway. The field takes what the button leaves. */
@media (max-width: 47.99rem) {
  /* A small basis, not `auto`. Wrapping is settled from what each item asks for before any of
     them is shrunk, so a field asking for the width of its own input takes the button onto a
     second line rather than giving way to it. Asked small, both fit on the one line and the
     field then grows into whatever the button leaves. */
  .loc-facet-head .loc-field { flex: 1 1 4rem; min-width: 0; }
}
.loc-facet-group { margin-block-start: 1rem; }
/* The panel's headings: the dimension a group narrows, and the search that crosses them all.
   They are the panel's titles, so they carry the heading face at heading size. */
.loc-facet-heading {
  display: block;
  font-family: var(--font-label);
  font-size: 1.35rem;
  margin: 0 0 0.3rem;
  padding-block-end: 0.2rem;
  border-bottom: 1px solid var(--loc-line);
}
.loc-facet-list { list-style: none; margin: 0; padding: 0; }
/* A criterion is a whole line, so the count sits at the far edge and the column of numbers
   reads down the panel as a column. */
.loc-facet-entry {
  display: flex;
  width: 100%;
  gap: 0.5rem;
  align-items: baseline;
  border: 0;
  border-radius: var(--loc-radius-sm);
  background: none;
  color: var(--loc-ink);
  /* The body size, not a size of its own. A facet entry is ordinary reading text in a list
     — nothing about sitting in a panel makes it a different kind of thing from the rest of
     the page, and a size picked here drifts from the page the moment the page's changes. */
  font-size: var(--bs-body-font-size);
  text-align: start;
  padding: 0.12rem 0.35rem;
}
.loc-facet-entry:hover { background: var(--loc-accent-soft); }
.loc-facet-entry.active { background: var(--loc-accent); color: #fff; }
.loc-facet-count { margin-inline-start: auto; color: var(--loc-muted); font-size: 0.75rem; }
.loc-facet-entry.active .loc-facet-count { color: #fff; }

/* --- forms: one shape, wherever something is filled in ---------------------
 *
 * **A label belongs to the field under it, and the spacing has to say so** (§19.97). Read down
 * a column of fields, the only thing telling somebody which label goes with which control is
 * how close they sit — so the gap above a label is always wider than the gap below it. Equal
 * gaps read as a list of unrelated words and boxes, and every form in this application had
 * them: a label carrying its own bottom margin and nothing separating it from the field above.
 *
 * It is written as spacing between siblings rather than as a wrapper each field must be put
 * in, so a form gets it by being a form — one class on the surface, and any markup inside it
 * is spaced correctly whether or not somebody remembered a container.
 */
.loc-form label,
.loc-form .form-label {
  display: block;
  margin: 0 0 var(--loc-label-gap);
  font-family: var(--font-label);
  color: var(--loc-ink);
}
/* Anything followed by a label starts a new field, and that is where the air goes. */
.loc-form > * + label,
.loc-form > * + .form-label,
.loc-form > * + .loc-field-row {
  margin-block-start: var(--loc-field-gap);
}
/* A note explains the control it follows, so it stays with it. */
.loc-form .loc-note,
.loc-form .form-text {
  margin: var(--loc-label-gap) 0 0;
  color: var(--loc-muted);
  font-size: 0.85rem;
}
/* **A caution reads as a caution.** It stands where the note it replaces would, in the warning
   color and against a wash of it, because a paragraph about what somebody is giving up has to
   be told apart from a paragraph about what a control does. */
.loc-form .loc-note-warn {
  padding: var(--loc-label-gap) 0.6rem;
  border-inline-start: 3px solid var(--loc-warn);
  border-radius: var(--loc-radius-sm);
  background: var(--loc-warn-soft, transparent);
  color: var(--loc-ink);
}
/* A rule divides one group of fields from the next, and carries the air for both. */
.loc-form hr {
  margin-block: var(--loc-group-gap);
  border-color: var(--loc-line);
}
/* A tick reads the other way round: the label is beside it, not above it, and the pair is
   one thing. */
.loc-form .form-check { margin-block-start: var(--loc-field-gap); }
.loc-form .form-check label,
.loc-form .form-check .form-label { display: inline; margin: 0; }
/* Fields that sit side by side are still fields: the row is what the spacing sees. */
.loc-field-row {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 0.75rem;
}
.loc-field-row > * { flex: 1 1 12rem; }

/* **One underline per field, drawn by whatever owns the box.** Wherever a control sits inside
   something already carrying the line — a token field on the recipe sheet, a search field in
   its own box — the inner control draws no edge of its own. Two of them a pixel apart is the
   tell that both were styled as if each were the outer one. */
.loc-field .loc-field-input,
.zone-field input,
.zone-field select {
  border: 0;
  box-shadow: none;
  outline: 0;
}

/* --- the person card, and the picker that browses them -------------------
 *
 * One shape wherever somebody is listed — kitchens, feeds, friends, the chat roster, the
 * invite browser on the admin page — because they are the same thing looked at through
 * different questions, and two card layouts would drift the first time one was fixed. It
 * lives here rather than with any one page: the picker is chrome, included in the layout and
 * openable from anywhere. */

/* The full-screen surface a browser opens onto. It is its own shell rather than a Bootstrap
   modal because what it holds is a page's worth of cards and a search field, not a question
   with two answers — and it is stated once here, so every browser that opens this way is the
   same surface. */
.loc-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1050;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgb(28 20 12 / 0.55);
}
.loc-modal-backdrop[hidden] { display: none; }
.loc-modal {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  inline-size: min(48rem, 100%);
  max-block-size: 100%;
  padding: 1rem 1.15rem;
  background: var(--loc-surface);
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  box-shadow: var(--loc-shadow);
}
/* A browser is given the height as well as the width: it is a list somebody scrolls, and one
   sized to its contents would jump as a search narrowed it. */
.loc-modal-full { block-size: min(44rem, 100%); }
.loc-modal-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding-block-end: 0.5rem;
  border-bottom: 2px solid var(--page-rule);
}
.loc-modal-title { margin: 0; font-family: var(--font-label); font-size: 1.35rem; }
.loc-modal-body {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  overflow-y: auto;
  flex: 1;
}
.loc-empty { color: var(--loc-muted); margin: 0; }

.picker-cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap-hairline);
}

.picker-card {
  display: flex;
  align-items: center;
  gap: var(--pane-gap, 0.75rem);
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--loc-rule);
  border-radius: 0.4rem;
  background: var(--loc-page);
}

.picker-card.is-current {
  border-color: var(--loc-underline);
}

/* Somebody who ended the friendship is shown, not hidden: nobody's list quietly gets one
   shorter and leaves them to work out who is missing. */
.picker-card.is-severed {
  opacity: 0.7;
  border-style: dashed;
}

.picker-body {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  flex: 1;
  min-width: 0;
}

.picker-name {
  font-family: var(--font-label);
  color: var(--loc-ink);
  overflow-wrap: anywhere;
}

.picker-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.picker-acts {
  display: flex;
  gap: var(--gap-hairline);
  flex-shrink: 0;
}

/* A finished state is a flat badge rather than a disabled button: a control that cannot be
   pressed still invites the press. */
.picker-badge {
  font-size: var(--loc-control-face);
  color: var(--loc-ink-quiet, var(--loc-ink));
  padding: var(--loc-control-pad);
  opacity: 0.8;
}

.picker-face {
  inline-size: 2.25rem;
  block-size: 2.25rem;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

.picker-face-blank {
  display: grid;
  place-items: center;
  background: var(--loc-rule);
  color: var(--loc-ink);
  font-family: var(--font-label);
}

/* --- the mobile breakpoint ------------------------------------------------
 *
 * **One breakpoint, and it divides phones from everything else** (§19.94). A tablet runs the
 * desktop interface whole — an iPad in portrait is 768px and an iPad Pro is wider, so the
 * boundary sits just under the narrower of the two and both stay on the same side of it.
 * Anything narrower is a phone, and a phone gets the adaptations rather than a scaled-down
 * copy of a layout built for a pointer.
 *
 * Every mobile rule in this application is written as `max-width: 47.99rem`. A second spelling
 * of the same idea is a second breakpoint the moment one of them is tuned.
 */
/* **The tab strip turns into a menu before anything else turns into a phone**, and that is its
   own breakpoint rather than the interface's. A row of tabs is chrome standing across the top of
   every page, so what it costs is charged to whatever is beneath it — and a page whose work needs
   the width has none to spare for a strip of alternatives to itself. The other things that change
   on a small screen change because a finger is not a pointer; this changes because a strip is
   expensive, which is true well before a screen is small.

   Stated once, here, because the banner is on every page: a strip that collapsed on one page and
   not another would be the same control behaving two ways. */
@media (max-width: 55.62rem) {
  .loc-tabs { flex-wrap: nowrap; height: var(--loc-tab-row); align-items: stretch; }
  .loc-tab { display: none; }
  /* **The face is as wide as what it says.** A menu face stretched across whatever the bar has
     left claims room from the crumbs beside it and reads as a field rather than as a control,
     and the caret ends up an inch from the word it belongs to. It takes its own measure, and
     what it does not take stays with the trail. */
  .loc-nav { flex: 0 1 auto; }
  .loc-bar .loc-tabs:not(.loc-tabs-open) { height: auto; }
  .loc-tabs:not(.loc-tabs-open) .loc-tab.active {
    display: flex;
    align-items: center;
    gap: 0.15rem;
    width: auto;
    block-size: calc(100% + 2px);
    margin-bottom: -2px;
    text-align: start;
  }
  /* **The same mark the kitchen and book menus wear**, because it is the same gesture: a face
     that opens a list. It is drawn as the caret Bootstrap's `.dropdown-toggle` draws — the
     crumb menus are Bootstrap dropdowns and take it from there, and a strip that opened a
     menu behind a different glyph would read as a different kind of control. */
  .loc-tab.active::after {
    content: "";
    display: inline-block;
    margin-inline-start: 0.255em;
    vertical-align: 0.255em;
    border-top: 0.3em solid;
    border-inline: 0.3em solid transparent;
    border-bottom: 0;
  }

  /* **Open, it is a menu standing over the page**, like the kitchen and book menus it borrows
     its face from — not a strip that unfolds and pushes the page down. The list is laid out
     in rows inside a box that keeps the height of one tab, so what is behind it neither moves
     nor is chosen from by accident. */
  .loc-tabs-open {
    display: grid;
    grid-auto-rows: max-content;
    height: var(--loc-tab-row);
    align-items: stretch;
    position: relative;
    z-index: 1020;
    overflow: visible;
  }
  .loc-tabs-open .loc-tab {
    display: block;
    width: 100%;
    text-align: start;
    background: var(--loc-surface);
    border: 1px solid var(--loc-line-strong);
    border-radius: 0;
    margin: 0;
    box-shadow: var(--loc-shadow);
  }
  .loc-tabs-open .loc-tab.active { color: var(--loc-ink); background: var(--loc-accent-soft); }
  .loc-tabs-open .loc-tab.active::after { content: ''; }

}

@media (max-width: 47.99rem) {
  /* **The trail says only what a thumb can act on.** The kitchen and the book are menus and are
     how another one is reached; the page names itself in the strip above, and what lies deeper
     is read on the page it belongs to. */
  .loc-crumb-deep { display: none; }
  .loc-browse { grid-template-columns: minmax(0, 1fr); }
  .loc-facet-panel { position: static; max-height: none; }

  /* **The criteria are behind a door here, so they are not also in the room.** Laid beside
     the cards on a narrow screen the panel is most of the screen; raised, it is a thing asked
     for. The panel standing in the grid is the same element the door opens onto — it is moved
     rather than copied, so what is chosen is chosen once. */
  #browse-grid > .loc-facet-panel { display: none; }

  .loc-facet-modal .loc-facet-panel {
    border: 0;
    box-shadow: none;
    padding: 0;
  }

  /* A phone reads a wall two or three pictures across, so the size a picture is worth showing
     at is smaller here: held at the desktop figure, the same wall is one column of pictures
     too big to compare and a scroll to see any of them. The wall itself needs no other rule —
     it counts its columns from the width it has, and this is the only thing it counts from. */
  .loc-pane { --pane-tile: 5.25rem; }

  /* **The strip is the tab you are on.** The rest are still in the markup, laid out under it
     and revealed together, so what opens is the same row in the same order rather than a
     second control built to stand in for it.

     Collapsed, the strip IS that one tab: it fills the row rather than standing inside it, so
     there is no alignment left to get wrong. The overlap a desktop tab wears — the negative
     margin that seats it on the strip's rule — has nothing to overlap here, and leaving it on
     is what pushes the face off the bottom edge. The caret takes the far end, where a menu
     face puts it. */
  /* **A phone's controls are the platform's size, not a shrunken desktop's.** Two numbers,
     both Apple's, and each fixes something on its own:
     
       17px — the iOS body size. Safari scales the whole document to bring an undersized
              field up to 16px when it is focused, and does not scale back when the keyboard
              goes away, so the interface is left magnified after a keystroke anywhere. The
              usual answer — pinning the viewport against scaling — buys it by taking zoom
              away from everybody who needs it. Meeting the size asks for nothing.
       44px — the minimum touch target. A finger is not a pointer, so a control is sized to
              be hit rather than to be pointed at.

     Both are set on the tokens every control is built from, so a field and the button beside
     it are still the same height as each other. */
  :root {
    --loc-control-face: 1.0625rem;
    --loc-control-pad-block: 0.55rem;
    --loc-touch: 2.75rem;
    --loc-tab-row: var(--loc-touch);
  }
  /* **A tick is not a field.** A checkbox carries no text to be zoomed for and is sized as a
     mark rather than as something typed into, so it is excluded here rather than being given
     back its size afterwards — a rule that broke it and a rule that repaired it would be two
     places to keep in step. Its own target is the label beside it, which is a line of text and
     already big enough to hit. */
  input:not([type="checkbox"]):not([type="radio"]),
  select,
  textarea,
  .form-control,
  .form-select,
  .loc-input,
  /* A field wearing its own class beats a bare element selector, so every one of them is
     named. Safari does not care which rule won — it measures the text it is given. */
  .loc-field .loc-field-input {
    font-size: var(--loc-control-face);
    min-block-size: var(--loc-touch);
  }
  /* **The sheet grows, not the fields in it.** A recipe is a document typed into in place:
     its fields are ruled lines of text laid out on the sheet's own measure, so a field made
     bigger than the words around it sits low on its line and, given a control's height,
     pushes what is written in it out of the box that shows it. What Safari needs is 16px of
     type in anything it can focus — so the page is set at the size the controls use and every
     field simply inherits it. Nothing on the sheet is then a different size from anything
     else on it, which is the whole of how it reads on paper. */
  /* Sixteen is the threshold Safari zooms below, so the sheet is set at exactly that and
     grows by a pixel rather than by three — enough that nothing on it can be focused into a
     zoom, little enough that it is not a scaled-up copy of the page it is on paper. */
  .recipe-page, .loc-wizard { font-size: 16px; }
  .recipe-page input,
  .recipe-page textarea,
  .recipe-page select,
  .loc-wizard input,
  .loc-wizard textarea,
  .loc-wizard select {
    font-size: inherit;
    min-block-size: 0;
  }
  /* **The search field is a component, so the component is what grows.** Its box is a fixed
     height with the text clipped inside it — sized for 13px when nothing else could focus a
     phone into zooming — so raising the text alone would have made a bigger word inside a box
     too short to hold it. The wrapper takes the touch height and the inner field fills it. */
  .loc-field {
    height: auto;
    min-block-size: var(--loc-touch);
    align-items: stretch;
  }
  .loc-field .loc-field-input { min-block-size: 0; }
  .loc-field .loc-field-clear { width: var(--loc-touch); font-size: 1rem; }

  .btn,
  .loc-btn,
  .loc-tab,
  .dropdown-item { min-block-size: var(--loc-touch); }
  /* **A round control meets the touch size by growing, not by stretching.** The rule above sets a
     floor under every control's height; a circle given a taller floor than its width becomes an
     oval, so this one takes the touch size on both axes and stays the shape it is. */
  .btn.loc-glyph {
    inline-size: var(--loc-touch);
    block-size: var(--loc-touch);
    min-inline-size: var(--loc-touch);
  }
  /* A chip is a word to press, sized to the word. It stays as it is: a row of them at the
     height of a field would be a wall rather than a set of choices. */
  .loc-chip { min-block-size: 2rem; }

  /* A page's furniture stacks rather than competing for a width that is not there. */
  .loc-page-head { align-items: stretch; }
  .loc-tools { width: 100%; }
  .loc-chip-label { min-width: 0; }
}

/* The recipe's own picture on its card: the first image of its gallery, which is the one
   the person put first. It leads the card and is cropped to a band rather than shown
   whole — a card is a name with a picture, not a picture with a name. */
.loc-card-shot {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--loc-radius-sm) var(--loc-radius-sm) 0 0;
  margin: -0.75rem -0.75rem 0.5rem;
  width: calc(100% + 1.5rem);
}

/* **Two strips share the bottom edge and the standby one wins.** A message somebody cannot
   open because the lane is down is not news they can act on, so the chat bar is suppressed
   for as long as standby is showing — and nothing is lost, because the bar is a view of
   unread state on the server rather than a queue the browser keeps. */
.loc-chatbar {
  z-index: 1030;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  background: var(--loc-underline);
  color: var(--loc-page);
  cursor: pointer;
  font-size: var(--loc-control-face);
}

.loc-chatbar-from {
  font-family: var(--font-label);
  flex-shrink: 0;
}

.loc-chatbar-opening {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.loc-chatbar-when,
.loc-chatbar-more {
  flex-shrink: 0;
  opacity: 0.85;
}

/* --- written text, wherever a description is written ----------------------- */
/* **The controls stand directly above the box they act on**, because there is only ever the
   one: the composer stands its palette down the side since a zone must be selected first, and
   here the box is the selection, always. Controls of a kind stand together and the kinds are
   ruled apart, so what a block is, how a run of words looks, and what face and color it is
   written in read as three decisions rather than one row of buttons to hunt through. */
/* **The strip and the canvas are one field, so they are ruled once, around both.** The border
   and the focus ring belong to the component rather than to the box inside it — the same shape
   the search field is built in, where the wrapper is what lights and the input inside it carries
   no rule of its own. A ring drawn around the canvas alone would say the toolbar was some
   separate control that happened to be sitting above it. */
.loc-rich {
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  background: var(--loc-surface);
  overflow: hidden;
}
.loc-rich:focus-within {
  border-color: var(--loc-accent);
  box-shadow: 0 0 0 0.2rem var(--loc-accent-soft);
}
/* **Two rows, because there are two kinds of decision here.** The first carries values — the
   face, the size, the four colors — and the acts that undo them; the second carries the
   switches, beginning at bold. Left as one wrapping row the break landed in a different place at
   every width, which is a toolbar whose shape a reader has to relearn each time the window
   moves. */
.loc-rich-bar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.15rem;
  padding: 0.3rem 0.4rem;
  border-block-end: 1px solid var(--loc-line);
  background: var(--loc-surface-sunk);
}
.loc-rich-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.15rem;
  min-inline-size: 0;
}
/* **The row that carries the values is one set of buttons and stays one set.** A face, a size and
   four colors describe one run of words together; broken across two lines they read as two
   groups of controls with no stated relationship, and which controls fell to the second line
   would change with every width. So the row does not break — and where the strip is narrower than
   the row needs, the row moves under the hand instead, which keeps every control reachable
   without any of them changing place.

   Narrow enough for that to matter, the strip is standing over a small label: the alternative is
   a workspace column widened by its toolbar, which is the thing that column was narrowed to stop. */
.loc-rich-row-lead {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: thin;
}
.loc-rich-row-lead .loc-rich-group { flex-wrap: nowrap; }
/* A group breaks internally before the strip overflows: what must never happen is a control
   leaving the box, and a group that has to split across two rows is a smaller price than one
   that is unreachable. */
.loc-rich-group { display: flex; flex-wrap: wrap; align-items: center; gap: 0.15rem; min-inline-size: 0; }
/* A rule that has wrapped to the start of a row divides nothing — there is nothing to its left
   — so it is drawn only where it stands between two groups. Which rules those are is a fact
   about the current width, so the strip settles them itself. */
.loc-rich-part[hidden] { display: none; }
/* **On a narrow screen undoing leads.** The strip wraps into several rows there, and the far end
   of the last row is the hardest place on the control to reach and the least likely to be looked
   at — so the two acts that rescue a mistake take the first position instead, where a thumb
   already is. The rule travels with them and keeps dividing them from what follows. */
/* Where there is room the overflow set is simply the second row, standing in the strip as itself
   — it becomes a panel only where there is no room for a second row at all. */
/* Named with `.btn`, because the class that gives every control on the strip its `inline-flex`
   stands later in this sheet at the same weight and would otherwise put this one back. */
.btn.loc-rich-more-act { display: none; }
/* **On a phone the strip is one row, and the rest is one press away.** Wrapped, these controls
   take three rows of a screen that has writing to show — so the two that interrupt a sentence
   stay out (the face and the color), undoing leads, and everything decided once for a passage
   goes behind a single control at the far end. */
@media (max-width: 47.99rem) {
  .loc-rich-history { order: -2; }
  .loc-rich-history-part { order: -1; }
  .loc-rich-bar { position: relative; }
  .loc-rich-row { flex-wrap: nowrap; }
  .btn.loc-rich-more-act { display: inline-flex; margin-inline-start: auto; }
  /* The rule that divided the set from the face and color has nothing to divide once the set is
     behind a control. */
  .loc-rich-more-part { display: none; }
  .loc-rich-more {
    display: none;
    position: absolute;
    inset-block-start: calc(100% + 0.3rem);
    inset-inline: 0;
    z-index: 29;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.15rem;
    padding: 0.35rem;
    border: 1px solid var(--loc-line);
    border-radius: var(--loc-radius);
    background: var(--loc-surface);
    box-shadow: var(--loc-shadow-pop);
  }
  .loc-rich-more-open .loc-rich-more { display: flex; }
}
/* **The canvas takes what height is left and scrolls inside itself.** A surface that grows with
   what is written pushes the strip off the top of the dialog and hands the scrolling to the
   dialog body, so the controls leave the screen exactly as somebody starts using them. Given the
   leftover height instead, the strip stays put and the writing moves under it — which is what a
   writing surface is, at every width rather than only on a phone.
   Height has to be handed down every step from the dialog to the canvas — a flex child fills its
   parent only if that parent was given something to fill — and each link carries
   `min-block-size: 0`, without which a flex item refuses to shrink below its content and the
   scrolling moves back up to the dialog. */
.loc-editor .modal-body { display: flex; flex-direction: column; }
/* **On a phone the dialog keeps the page's gutter, not the dialog kit's.** A modal is padded for
   a dialog that says a sentence and offers two answers; this one is a full-screen writing surface,
   and 1rem a side is a tenth of the width spent on nothing while the canvas already holds its own
   inset inside its border. The page states what an edge is worth here, so the dialog uses that.
   Stated after the rules above it undoes, which is where a narrow-screen rule goes (R40.8). */
@media (max-width: 47.99rem) {
  .loc-editor .modal-body { padding-inline: var(--loc-gutter); }
}
/* **Each link states its width as well as its height.** A flex item stretches across its parent
   only while nothing else decides its inline size — and an `auto` inline margin decides it: the
   card centers itself, so as a flex item it stops stretching and shrink-wraps to its content
   instead, leaving a gutter down both sides of everything in the dialog while the header and the
   footer, which are not in this chain, still span the whole width.
   Saying `100%` gives the stretch back without taking the centring away: the card is as wide as
   the room it is given, its own measure still caps it, and the auto margins share out whatever
   the cap leaves over. */
.loc-editor .loc-wizard,
.loc-editor .loc-wizard .loc-step,
.loc-editor .zone-description,
.loc-editor .loc-rich-host,
.loc-editor .loc-rich-host .loc-rich {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  inline-size: 100%;
  min-block-size: 0;
}
.loc-editor .loc-wizard .loc-rich-surface {
  flex: 1 1 auto;
  min-block-size: 0;
  overflow-y: auto;
}
.loc-rich-part {
  inline-size: 1px;
  block-size: 1.4rem;
  margin-inline: 0.35rem;
  background: var(--loc-line-strong);
}
.loc-rich-act {
  min-width: 0;
  inline-size: 1.9rem;
  block-size: 1.9rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
/* The heading marks are the one glyph at three sizes, which is what says they are three of
   the same thing rather than three different controls. */
.loc-rich-h1 { font-size: 1em; }
.loc-rich-h2 { font-size: 0.85em; }
.loc-rich-h3 { font-size: 0.7em; }
/* **A fixed measure, because the menu's content is not the strip's business.** Sized to itself
   the menu is as wide as whichever family is chosen, so picking a font would change the width of
   the toolbar and move every control to its right. A stated width also makes the one-row width
   of the whole strip a number that can be declared rather than measured. */
/* The menu carries its own clearance before the swatch: a control with a border and a caret
   sitting hard against a button reads as one compound control rather than two. */
.loc-rich-face { inline-size: 9rem; flex: 0 1 9rem; min-inline-size: 0; margin-inline-end: 0.2rem; }
/* A size is the same control holding a number of two or three figures, so it needs less room
   and takes the difference off the basis rather than restating the rest. */
.loc-rich-size { inline-size: 4.5rem; flex: 0 0 4.5rem; }
/* **A measurement is a small number and takes the room a small number takes.** A face carries a
   family name and a size carries two digits and a word; these carry two digits and a unit mark, and
   at the width of either they push what they stand beside onto a second row of a strip with room to
   spare. Sized to the content rather than to the widest control on the bar. */
/* Dimmed rather than hidden while its effect is off: the control still says what it is for, and a
   strip that gained and lost menus as switches were pressed would rearrange under somebody's hand. */
.loc-rich-measure:disabled { opacity: 0.45; }
.loc-rich-measure {
  inline-size: 4.25rem;
  flex: 0 0 4.25rem;
  min-inline-size: 0;
  padding-inline: 0.4rem 1.4rem;
}
.loc-rich-ink {
  inline-size: 1.9rem;
  block-size: 1.9rem;
  padding: 0.15rem;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  background: var(--loc-surface);
}
/* The surface is a field like any other on the form: it is written on, so it is ruled the way
   everything written on is ruled — and it grows with what is in it rather than scrolling a box
   the size of two lines. */
.loc-rich-surface {
  min-block-size: 4.5rem;
  padding: 0.5rem 0.6rem;
  font-family: var(--font-content);
  overflow-wrap: anywhere;
}
.loc-rich-surface:focus { outline: none; }
.loc-rich-surface > :first-child { margin-block-start: 0; }
.loc-rich-surface > :last-child { margin-block-end: 0; }
/* A control wearing what is true where the caret is. The block group is a mutex by nature
   rather than by rule — a block is one tag and cannot be two — so exactly one of the four is
   ever lit, and one always is: text is what writing is unless somebody says otherwise. */
.loc-rich-act.loc-rich-on {
  background: var(--loc-accent-soft);
  border-color: var(--loc-accent);
  color: var(--loc-accent);
}
/* **A block control sets size and intent, never a face.** The application draws headings in the
   label hand, which is right for a page and wrong inside a piece of writing: pressing Heading
   would silently change what font the words were in, and pressing Text would silently change it
   back. Inside the canvas every block wears whatever face the run carries — which is the one
   thing the font control is for — and the block says only how large and how heavy it is. */
.loc-rich-surface p,
.loc-rich-surface h1,
.loc-rich-surface h2,
.loc-rich-surface h3 {
  font-family: inherit;
  margin-block: 0.35rem;
}
.loc-rich-surface p { font-size: 1rem; font-weight: 400; }
.loc-rich-surface h3 { font-size: 1.15rem; font-weight: 600; }
.loc-rich-surface h2 { font-size: 1.35rem; font-weight: 700; }
.loc-rich-surface h1 { font-size: 1.6rem; font-weight: 700; }
/* --- the shared color chooser -------------------------------------------- */

/* The ink well: a swatch that opens the page's own palette, and shuts it on the second press.
   A native color input's panel cannot be closed by the page, which is why the control is a
   button and the colors are ours.

   One set of rules, wherever a color is chosen — the writing strip and the label designer's
   properties panel draw the same control, so it looks and behaves the same in both. */
.loc-rich-well { position: relative; display: inline-flex; }
/* **A well holding no color says so by being crossed out**, rather than by showing whichever
   color was last chosen — which would read as a color that is set and simply not applied. */
/* A drawn mark stands where a glyph from the set would, at the size the set draws at — so a
   control wearing one is the same size and weight as the controls either side of it, and it
   answers to the button's own color the way an icon does. */
.loc-rich-glyph {
  inline-size: 1em;
  block-size: 1em;
  display: block;
  overflow: visible;
}
.loc-rich-act.loc-rich-none { color: var(--loc-muted); }
.loc-rich-act.loc-rich-none::after {
  content: '';
  position: absolute;
  inset-inline: 20%;
  inset-block-start: 50%;
  block-size: 1px;
  background: var(--loc-muted);
  transform: rotate(-45deg);
}
.loc-rich-swatch i { color: inherit; }
/* **A panel that opens over a workspace has to clear it.** The strip stands above a drawing
   area whose own elements are stacked, so a value chosen from the strip's neighbourhood puts the
   panel behind the artwork it was opened to change. It is chrome over a document and is numbered
   as such. */
.loc-rich-inks {
  display: none;
  position: absolute;
  inset-block-start: calc(100% + 0.3rem);
  inset-inline-start: 0;
  z-index: 30;
  /* Eight swatches across: their own width, the seven spaces between them, the panel's
     padding and its border. The sixteen then read as two even rows whose edge is the edge
     of the wheel and of the row of verbs beneath, and nothing on the panel stands short. */
  width: calc((1.5rem * 8) + (0.15rem * 7) + 0.7rem + 2px);
  padding: 0.35rem;
  gap: 0.25rem;
  flex-wrap: wrap;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  background: var(--loc-surface);
  box-shadow: var(--loc-shadow-pop);
}
/* **Keyed on the panel, not on an ancestor.** While it is open the panel is a child of the
   body, fixed to the window where its well stands, so that nothing between the two can clip it
   — which means no rule about it may name a parent it no longer has. */
/* A picker acting as a menu arrives the way a menu does: growing down from the control that
   opened it, and fading off quickly when it is put away. */
.loc-rich-inks.loc-rich-inks-shown {
  display: flex;
  transform-origin: top left;
  animation: loc-ink-in 0.12s ease-out;
}
.loc-rich-inks.loc-rich-inks-going { animation: loc-ink-out 0.09s ease-in forwards; }
@keyframes loc-ink-in {
  from { opacity: 0; transform: scaleY(0.85) translateY(-4px); }
  to { opacity: 1; transform: scaleY(1) translateY(0); }
}
@keyframes loc-ink-out {
  from { opacity: 1; }
  to { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .loc-rich-inks.loc-rich-inks-shown,
  .loc-rich-inks.loc-rich-inks-going { animation: none; }
}
.loc-rich-chip {
  inline-size: 1.5rem;
  block-size: 1.5rem;
  padding: 0;
  border: 1px solid var(--loc-line);
  border-radius: 0.2rem;
}

.loc-rich-chip:hover { border-color: var(--loc-accent); }
/* **The panel is two things side by side**: what is being made on the left, and what it can be made
   from on the right. The set of sixteen keeps the width it had, so a panel that gains a wash grows
   leftward rather than rearranging the swatches somebody already knows the position of. */
/* Wide enough for both halves where there is a wash to make, and exactly as wide as the sixteen
   where there is not — a panel that reserved the room either way would be mostly empty on every
   well that only chooses a color. */
.loc-rich-inks { align-items: flex-start; }
.loc-rich-inks:has(.loc-rich-wash) { width: 18.6rem; }
.loc-rich-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem;
  inline-size: calc((1.5rem * 4) + (0.15rem * 3));
}
/* Each set of swatches says what it is: the sixteen the document is drawn from, and beneath
   them the colors this control has been given lately, newest first. */
.loc-rich-set {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  inline-size: 100%;
}
.loc-rich-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--loc-muted);
}
.loc-rich-recent { flex-wrap: nowrap; }
/* A slot with nothing in it yet still stands, so the row reads as somewhere colors will
   collect rather than as something missing. */
.loc-rich-chip-empty {
  background: repeating-linear-gradient(45deg, var(--loc-surface) 0 3px, var(--loc-surface-sunk) 3px 6px);
  opacity: 0.5;
  cursor: default;
}
/* With no wash beside them the swatches have the panel to themselves and take all of it,
   so the set reads as one block the width of the row of verbs beneath it. */
.loc-rich-inks:not(:has(.loc-rich-wash)) .loc-rich-chips { inline-size: 100%; }
.loc-rich-wash {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  inline-size: 9rem;
  padding-inline-end: 0.45rem;
  margin-inline-end: 0.15rem;
  border-inline-end: 1px solid var(--loc-line);
}
/* What the wash actually looks like, at a size worth judging — the one thing on the panel that is
   the answer rather than a control for reaching it, which is also why it is where the kind of wash
   is chosen. */
.loc-rich-shown {
  display: block;
  inline-size: 100%;
  block-size: 2.25rem;
  padding: 0;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius-sm);
  cursor: pointer;
}
.loc-rich-shown:hover { border-color: var(--loc-accent); }
.loc-rich-ends { display: flex; gap: 0.3rem; }
/* **Blocks, not chips.** A swatch is one of a kitchen's colors; these two are the ends of the wash
   being made, so they are wider, they sit apart from the set, and one of them is always the one a
   press on the set is about. */
.loc-rich-end {
  flex: 1 1 0;
  block-size: 1.75rem;
  min-inline-size: 0;
  padding: 0;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius-sm);
}
.loc-rich-end:hover { border-color: var(--loc-accent); }
/* The focus, said the same way the selection mark is said on the label workspace: a ring outside
   the block, so being the subject does not narrow the color it is showing. */
.loc-rich-end.loc-rich-end-on {
  border-color: var(--loc-ink);
  outline: 2px solid var(--loc-ink);
  outline-offset: 1px;
}
/* An end holding nothing is not a color and must not look like one — the hatch says *nothing here*
   the same way it does on a well. */
.loc-rich-end.loc-rich-none { background: repeating-linear-gradient(45deg, var(--loc-surface) 0 4px, var(--loc-surface-sunk) 4px 8px); }
.loc-rich-turn { display: block; }
.loc-rich-turn .loc-slider { inline-size: 100%; }
/* The two answers stand at the end of the row the set's own verbs are in: everything on that row is
   pressed once, at the end of the choosing, and a panel this small spends a whole row on two
   buttons. Pushed apart rather than ordered, so the verbs stay where they were and the answers are
   always in the same corner whether or not the row has a wash's controls in it. */
.loc-rich-inks-answers { display: flex; gap: 0.15rem; margin-inline-start: auto; }
/* **The wheel, drawn in the panel.** A browser's own color window belongs to the operating system
   and cannot be placed, styled or reached — so this is built out of the gradients CSS already has:
   every hue along a strip, and at the hue chosen, a square running white to that hue across and
   black up from the bottom. Between them they name every color, which is all a wheel is.

   It always stands here, at a fixed size, so the panel keeps its shape and no swatch moves under a
   hand that is reaching for one. */
.loc-rich-wheel {
  flex: 0 0 100%;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-block-start: 0.15rem;
}
.loc-rich-field {
  position: relative;
  display: block;
  block-size: 5rem;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius-sm);
  cursor: crosshair;
  background:
    linear-gradient(to top, #000, rgb(0 0 0 / 0)),
    linear-gradient(to right, #fff, rgb(255 255 255 / 0)),
    hsl(var(--loc-hue, 0) 100% 50%);
}
.loc-rich-hue {
  position: relative;
  display: block;
  block-size: 0.85rem;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius-sm);
  cursor: ew-resize;
  background: linear-gradient(to right,
    #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
/* What the color is, in whichever of the three spellings this person reads in — and the control
   for changing that, because there is no room on this panel for a menu of three and the readout is
   already the thing the question is about. */
.loc-rich-said { display: flex; align-items: center; gap: 0.15rem; }
.loc-rich-space {
  flex: 1 1 auto;
  min-inline-size: 0;
  padding: 0.1rem 0.25rem;
  border: 1px solid transparent;
  border-radius: var(--loc-radius-sm);
  background: none;
  color: var(--loc-muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  text-align: start;
  white-space: nowrap;
  overflow: hidden;
}
.loc-rich-space:hover { border-color: var(--loc-line); color: var(--loc-ink); }

/* Where the hand left off, on each. A ring rather than a fill, so the mark never covers the color
   it is standing on — which is the one thing being read. */
.loc-rich-dot,
.loc-rich-pin {
  position: absolute;
  pointer-events: none;
  border: 2px solid #fff;
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgb(0 0 0 / 0.45);
}
.loc-rich-dot {
  inline-size: 0.7rem;
  block-size: 0.7rem;
  transform: translate(-50%, -50%);
}
.loc-rich-pin {
  inset-block: -1px;
  inline-size: 0.55rem;
  border-radius: 0.3rem;
  transform: translateX(-50%);
}

/* **The swatch that is already the answer.** A ring drawn outside the chip rather than a border on
   it, so the color it is showing is not narrowed by being the one in force — the mark is about the
   chip and must not take a bite out of the thing it is marking. */
.loc-rich-chip.loc-rich-chip-on {
  border-color: var(--loc-ink);
  outline: 2px solid var(--loc-ink);
  outline-offset: 1px;
}
/* The palette's second verb, and the mode that tells the two apart. A chip applies its ink
   until the pencil is lit, when the same press offers the chip for a new color instead — so
   the panel wears the mode, and no click is ever ambiguous about which one it meant. */
.loc-rich-inks-tools {
  flex: 0 0 100%;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.15rem;
  margin-block-start: 0.15rem;
  padding-block-start: 0.3rem;
  border-block-start: 1px solid var(--loc-line);
}
.loc-rich-inks.loc-rich-inks-editing { outline: 2px solid var(--loc-accent); outline-offset: -1px; }
.loc-rich-inks.loc-rich-inks-editing .loc-rich-chip { cursor: cell; border-style: dashed; }


/* Dropped into a properties panel it fills the row it was given; on a strip it sits inline
   among the other controls and takes only what it needs. */
.led-ink .loc-rich-well,
.led-ink .loc-rich-swatch {
  inline-size: 100%;
  flex: 1 1 auto;
}

.led-font .loc-rich-face {
  inline-size: 100%;
  flex: 1 1 auto;
  margin-inline-end: 0;
}

/* **A strip that carries no canvas of its own.** The writing surface draws its controls above
   the box they act on; the label designer draws one strip above a workspace holding several
   text elements, and rewires it to whichever is selected. Same controls, same arrangement —
   what differs is that this one has no box under it to be joined to. */
/* **A strip with nothing under it cannot clip what opens below it.** The writing surface hides
   its overflow so the bar and the canvas share one rounded box, and every panel the bar opens
   drops into the canvas, which is inside that box. This strip has no canvas — what it opens
   drops past its own bottom edge, where the same rule cuts it off, which reads as a panel that
   will not appear rather than one that appeared and was trimmed. */
.loc-rich-strip {
  border: 0;
  padding: 0;
  background: none;
  overflow: visible;
}

.loc-rich-strip .loc-rich-bar {
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  /* The same ground every strip stands on. A surface color here would make this one read as a
     panel rather than as the toolbar it is. */
  background: var(--loc-surface-sunk);
}

/* Nothing selected is nothing to act on, so the controls say so rather than inviting a press
   that would land on whatever was selected last. */
.loc-rich-idle {
  opacity: 0.45;
  pointer-events: none;
}

/* --- the shared slider ---------------------------------------------------- */

/* **A handle that reads as a handle.** Rectangular and slightly taller than it is wide, on a
   runner shorter than the handle — so the thing that moves is plainly the thing you take hold
   of, and the track is a groove it sits in rather than a bar of its own. Everything takes a
   theme token, so a palette change moves the control with the rest of the sheet. */
.loc-slider {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 5rem auto;
  grid-template-areas: 'run num unit';
  align-items: center;
  gap: 0.25rem 0.4rem;
  inline-size: 100%;
}

/* The run over the number it reports, for a cell too narrow to read them side by side. */
.loc-slider-stacked {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'run run'
    'num unit';
}

/* **The two halves of the run say different things, so they are drawn in different colors.** What
   is behind the handle is the part of the range in use and what is ahead of it is the part that is
   not — the split is where the value is, which is the same thing the handle says and is readable
   without finding the handle first. Two shades a step apart say nothing at a glance; the accent
   against the sunk surface is a difference somebody can see across the panel.

   Stated once and spent twice, because a track is reached only through a vendor pseudo-element and
   each engine names its own — the declaration is shared so the two cannot come to differ. */
.loc-slider-run {
  grid-area: run;
  min-inline-size: 0;
  appearance: none;
  background: none;
  block-size: 1.1rem;
  --loc-slider-track: linear-gradient(to right,
    var(--loc-accent) 0 var(--loc-slider-at, 0%),
    var(--loc-surface-sunk) var(--loc-slider-at, 0%) 100%);
}

.loc-slider-run::-webkit-slider-runnable-track {
  block-size: 0.35rem;
  border-radius: 0.18rem;
  background: var(--loc-slider-track);
  border: 1px solid var(--loc-line);
}

.loc-slider-run::-moz-range-track {
  block-size: 0.35rem;
  border-radius: 0.18rem;
  background: var(--loc-slider-track);
  border: 1px solid var(--loc-line);
}

.loc-slider-run::-webkit-slider-thumb {
  appearance: none;
  inline-size: 0.55rem;
  block-size: 1.1rem;
  border-radius: 0.12rem;
  background: var(--loc-accent);
  border: 1px solid var(--loc-line-strong);
  /* Centered on a track thinner than the handle, which is what puts the groove behind it. */
  margin-block-start: -0.4rem;
}

.loc-slider-run::-moz-range-thumb {
  inline-size: 0.55rem;
  block-size: 1.1rem;
  border-radius: 0.12rem;
  background: var(--loc-accent);
  border: 1px solid var(--loc-line-strong);
}

.loc-slider-run:focus-visible {
  outline: 2px solid var(--loc-accent);
  outline-offset: 2px;
}

.loc-slider-num {
  grid-area: num;
  inline-size: 100%;
  min-inline-size: 0;
  padding: 0.1rem 0.3rem;
  border: 1px solid var(--loc-line);
  border-radius: var(--loc-radius);
  background: var(--loc-surface);
  color: inherit;
  text-align: end;
  /* **The stepper is the run beside it, so the field carries no second one.** A number input
     reserves room at its trailing edge for the browser's own up and down arrows whether or not
     they are worth having — and with the value set against that edge, the number is pushed clear
     of a band it can never enter. Here it is never worth having: a value nudged one step at a
     time is what the handle is for, and typing is what the field is for. */
  appearance: textfield;
  -moz-appearance: textfield;
}
.loc-slider-num::-webkit-outer-spin-button,
.loc-slider-num::-webkit-inner-spin-button {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}

.loc-slider-unit {
  grid-area: unit;
  color: var(--loc-muted);
  font-size: 0.8rem;
}

/* **Nothing in a listing is text to be selected.** A file browser is a set of things to be pressed,
   dragged and double-pressed, and every one of those gestures drags a highlight across the names
   instead — a blue smear over the listing that has to be clicked away, and a double press that
   selects a word rather than opening the file it is part of. What is here is a control wearing a
   name, not a passage. */
.loc-file-picker .modal-body,
.loc-file-picker .modal-header { user-select: none; }

@media (max-width: 47.99rem) {
  /* **A phone shows the book two across.** One card to a row is a card the size of the screen
     and two recipes to a scroll, which is a list read through a keyhole. Two columns and a
     shallower band put six recipes in view at once, which is enough of the book to choose
     from rather than enough to look at. */
  .loc-cards {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 0.6rem;
    row-gap: 1rem;
  }

  .loc-card { padding: 0.55rem 0.55rem 0.75rem; }

  .loc-card-shot {
    aspect-ratio: 4 / 3;
    margin: -0.5rem -0.5rem 0.4rem;
    width: calc(100% + 1rem);
  }

  /* **A card on a phone is a name and a picture.** The description is what a card offers when
     there is room to read it; at this size it is what pushes the next row off the screen, and
     the recipe itself is one tap away. The name sets a shade smaller and closer so a long one
     still takes two lines rather than four. */
  .loc-card-description { display: none; }

  .loc-card-name {
    font-size: 1.05rem;
    line-height: 1.15;
    gap: 0.3rem;
  }

  .loc-card-meta { line-height: 1.2; }
}

/* **A word set aside for the eye is kept for the ear.** On a narrow bar the glyph carries the
   button, the menu names its own choices, the checkbox wears its word, and the list below
   states its own length — so each of these labels is spending width to repeat what is already
   in view. None of that holds for a screen reader, which has no bar in view to read from, so
   the words stay in the document and leave only the layout. */
@media (max-width: 47.99rem) {
  .loc-btn-said,
  .loc-toolrow-count,
  .loc-toolrow-split .loc-sort-host .loc-hint,
  .loc-toolrow-split .loc-print-lead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* **Done is a checkmark, wherever it is said.** A button that ends a thing — a sheet of
   criteria, a run of pages, a color chosen — is answering the same question every time, and
   a phrase invented for each occasion makes four buttons out of one. The glyph carries it, the
   name stays for a screen reader, and any surface that needs "finished" reaches for this. */
.loc-done {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.loc-done > .loc-done-said {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* **Turned sideways, a phone gets one message instead of a sliver of the page.** The height is
   what says a phone is on its side — a landscape window on a desk is still tall enough to work
   in — so the rule reads the height rather than guessing at the device. It takes the shape a
   phone already uses to say something is not available: the page blacked out behind, a rounded
   card centred over it, the caution mark above the words. */
.loc-upright { display: none; }

@media (orientation: landscape) and (max-height: 30rem) {
  .loc-upright {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.25rem;
    background: #000;
  }

  .loc-upright-sheet {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    width: min(20rem, 100%);
    padding: 1.5rem 1.75rem;
    border-radius: 0.9rem;
    background: #1c1c1e;
    color: #fff;
    text-align: center;
    box-shadow: 0 1.25rem 3rem rgba(0, 0, 0, 0.6);
  }

  .loc-upright-mark {
    font-size: 2.25rem;
    color: #ff9f0a;
    margin-block-end: 0.35rem;
  }

  .loc-upright-said {
    margin: 0;
    font-size: 1.0625rem;
    font-weight: 600;
    letter-spacing: -0.01em;
  }

  .loc-upright-more {
    margin: 0;
    font-size: 0.8125rem;
    line-height: 1.35;
    color: rgba(235, 235, 245, 0.6);
  }
}
