:root {
  --bg: #0a0e13;
  --surface: #121923;
  --surface-2: #18212d;
  --surface-3: #1f2a38;
  --border: #253141;
  --border-soft: #1c2532;
  --text: #e7eef8;
  --muted: #8b9aae;
  --muted-2: #61718708;
  --primary: #e8eef7;
  --danger: #ff6b6b;
  --radius: 12px;
  --radius-sm: 8px;
  --shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }

html, body { height: 100%; }

body {
  margin: 0;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
p { margin: 0; }
.muted { color: var(--muted); }

/* ------------------------------------------------------------- buttons */

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  padding: 7px 12px;
  transition: background 0.14s, border-color 0.14s, transform 0.08s;
}
button:hover { background: var(--surface-3); }
button:active { transform: translateY(1px); }
button:focus-visible { outline: 2px solid #5aa9ff; outline-offset: 2px; }
button:disabled { opacity: 0.55; cursor: default; }

button.primary {
  background: var(--primary);
  color: #0a0e13;
  border-color: var(--primary);
  font-weight: 650;
}
button.primary:hover { background: #fff; }

#addBtn { display: inline-flex; align-items: center; gap: 6px; }
/* An SVG rather than a "+" glyph, so it sits dead centre when the button
   becomes a circle on mobile — text glyphs carry uneven line-box padding. */
.add-plus { width: 14px; height: 14px; display: block; flex: none; }

button.ghost { background: transparent; }
button.ghost:hover { background: var(--surface-2); }
button.small { padding: 6px 10px; font-size: 13px; }

button.danger { color: var(--danger); border-color: transparent; background: transparent; }
button.danger:hover { background: rgba(255, 107, 107, 0.12); }

button.danger-solid {
  background: #e0453f;
  border-color: #e0453f;
  color: #fff;
  font-weight: 650;
}
button.danger-solid:hover { background: #f05650; }

.icon-btn {
  border: 0;
  background: transparent;
  padding: 4px 8px;
  color: var(--muted);
  font-size: 15px;
}
.icon-btn:hover { color: var(--text); background: var(--surface-2); }

/* -------------------------------------------------------------- topbar */

.topbar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--border-soft);
  flex-wrap: wrap;
}

.brand { display: flex; align-items: center; gap: 11px; margin-right: auto; }
.brand h1 { font-size: 21px; line-height: 1.15; letter-spacing: -0.02em; white-space: nowrap; }
.brand svg { flex: none; }

/* A grid rather than a flex row, so the columns are equal instead of each
   being as wide as its own word — "Grid" and "Timezones" are five letters
   apart and it showed.

   minmax(0, 1fr) rather than 1fr: 1fr means minmax(auto, 1fr), and that auto
   is the content's own minimum, so "Timezones" refused to go below its width
   and the other two shared what was left. The same inequality wearing a grid. */
.viewtabs {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 3px;
  gap: 3px;
}
.viewtabs button {
  border: 0;
  background: transparent;
  padding: 6px 10px;
  border-radius: 7px;
  color: var(--muted);
  font-weight: 600;
  /* The column sets the width now, so the label lives inside it rather than
     deciding it. Trimmed padding keeps the longest one clear of the edges. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.viewtabs button[aria-pressed="true"] { background: var(--surface-3); color: var(--text); }

/* The longer title squeezes this row, so let the buttons flow onto another
   line when narrow — but never break a label mid-word. */
.topbar-right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 8px;
}
.topbar-right > button { white-space: nowrap; }

/* Who you are and what you can do with the book were two buttons side by side,
   and the second was three dots naming nothing. They are one control now: the
   profile button opens the menu, and switching profile is the first thing in
   it. One fewer control on the row a phone can least afford it. */
.menu-wrap { position: relative; display: inline-flex; }
.menu-caret { color: var(--muted); flex: none; margin-left: 1px; }
.menu-veil { display: none; }
.menu-who {
  padding: 7px 11px 5px;
  font-size: 11.5px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.menu-sep { height: 0; border-top: 1px solid var(--border-soft); margin: 4px 6px; }

/* A guest can read the book and nothing else, so the menu holds one thing:
   who you are. Offering Categories or Import only to refuse them is a menu
   that lies about what it does. Export stays hidden with them — it is the
   whole book in a file, and that is not a guest's to take.
   Forced, because #addBtn carries this attribute too and an id selector would
   otherwise outrank a rule built from a class and an attribute. */
body.is-guest [data-writer-only] { display: none !important; }

/* Import rewrites spots it matches by coordinate, so it is an edit of the
   whole book wearing a different hat — the admin's alone, and the server
   refuses it from anyone else whatever the menu shows. */
body:not(.is-admin) [data-admin-only] { display: none !important; }

/* Add spot is the loudest thing in the header and the profile button sits
   right beside it; without the old overflow button between them they read as
   one control. A rule and a wider gap put them in separate groups.
   Only when there is something on the other side of it, though: a guest has no
   Add spot, and on a phone it floats at the bottom instead, so in both cases
   the rule would be dividing the profile button from nothing. */
.topbar-right > .menu-wrap { position: relative; }

body:not(.is-guest) .topbar-right > .menu-wrap { margin-right: 9px; }
body:not(.is-guest) .topbar-right > .menu-wrap::after {
  content: '';
  position: absolute;
  right: -9px;
  top: 50%;
  width: 1px;
  height: 20px;
  transform: translateY(-50%);
  background: var(--border);
}

.menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 70;
  min-width: 178px;
  padding: 5px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.menu button {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 9px 11px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  text-align: left;
  font-size: 13.5px;
  color: var(--text);
}
.menu button:hover { background: var(--surface-2); }
.menu button svg { color: var(--muted); flex: none; }

/* On a phone it is a bottom sheet, because that is what every other dialog in
   this app already becomes down here — and the header is the far corner from
   a thumb. */
@media (max-width: 760px) {
  .menu-veil {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 69;
    background: rgba(0, 0, 0, 0.45);
  }
  .menu {
    position: fixed;
    inset: auto 0 0 0;
    width: 100%;
    min-width: 0;
    padding: 6px 8px max(30px, calc(env(safe-area-inset-bottom, 0px) + 16px));
    border-width: 1px 0 0;
    border-radius: 16px 16px 0 0;
  }
  /* The grabber every sheet on a phone has. */
  .menu::before {
    content: '';
    display: block;
    width: 34px;
    height: 4px;
    margin: 4px auto 6px;
    border-radius: 2px;
    background: var(--border);
  }
  .menu button { padding: 13px 12px; font-size: 15px; }
  .menu-who { font-size: 12.5px; padding: 4px 12px 6px; }
}

/* ------------------------------------------------------------- profiles */

.profile-dot {
  --who: var(--muted);
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  flex: none;
  border-radius: 50%;
  background: var(--who);
  color: #0b1018;
  font-size: 11px;
  font-weight: 800;
  line-height: 1;
}
.profile-dot.tiny { width: 13px; height: 13px; font-size: 0; }

.profile-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px 5px 5px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  max-width: 190px;
}
/* The name, not the caret: :last-child picked the svg, so the name never
   ellipsised and a long one wrapped the button onto two lines. */
.profile-btn > span:not(.profile-dot) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.profile-btn.unset { border-style: dashed; color: var(--muted); }

.profile-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.profile-list li { display: flex; align-items: center; gap: 4px; }
.profile-empty { color: var(--muted); font-size: 13px; padding: 6px 2px; }

.profile-pick {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 11px;
  text-align: left;
  background: var(--bg);
}
.profile-pick[aria-pressed="true"] { border-color: var(--primary); background: var(--surface-2); }
.profile-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.profile-count { margin-left: auto; font-size: 11.5px; color: var(--muted); white-space: nowrap; }
.profile-you {
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #0b1018;
  background: var(--primary);
  border-radius: 999px;
  padding: 2px 7px;
}

/* The admin adds a name and a password together — too much for one line on a
   phone, so it wraps and the button takes the row under them. */
.profile-new { flex-wrap: wrap; }
.profile-new button { margin-left: auto; }

/* Signing in and setting a password both happen in the row itself, so the
   name you picked stays in front of you. */
/* The message takes the line under the row it belongs to, rather than the top
   of the dialog where it had to name the field it was about. */
.profile-auth { align-items: center; flex-wrap: wrap; }
.profile-auth .err, .profile-new .err, .profile-setup .err { flex-basis: 100%; margin: 0; }

/* These inputs are bare, not wrapped in a .field like the spot form's. */
/* A name that is not yours to change. Still black on white and selectable —
   it is information, not a disabled control. */
/* Take hold here. touch-action matters: without it the browser claims the
   gesture for scrolling and the drag never starts on a phone. A row with no
   grip is one that cannot move — the gap keeps the rest of the row lined up
   with those that can. */
.cat-grip {
  display: inline-grid;
  place-items: center;
  flex: none;
  /* Wider and taller than the dots it draws: a grip is aimed at with a thumb,
     and thirteen pixels of icon is not something a thumb can find. */
  width: 22px;
  min-height: 28px;
  color: var(--muted);
  cursor: grab;
  touch-action: none;
}
.cat-grip:hover { color: var(--text); }
.cat-grip-gap { flex: none; width: 22px; }

/* Lifted, and following the finger. The rows underneath hold their places, so
   the line is the only thing that says where this one will land. */
.cat-row.is-dragging {
  position: relative;
  z-index: 3;
  cursor: grabbing;
  border-color: var(--primary);
  background: var(--surface-2);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.5);
}
.is-reordering .cat-row { transition: none; }
/* Drawn inside the row rather than between rows: a gap opening and closing
   under the pointer moves everything else as you aim at it. */
.cat-row.drop-above, .cat-row.drop-below { position: relative; }
.cat-row.drop-above::before, .cat-row.drop-below::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--primary);
}
.cat-row.drop-above::before { top: -4px; }
.cat-row.drop-below::after { bottom: -4px; }
/* The name is what the row is for, so it takes the slack and, if it must, the
   ellipsis — with the full text on hover. The count keeps its width: "0 sp…"
   is a worse thing to read than a shortened name. */
.cat-row-name { flex: 1 1 auto; min-width: 0; }
.cat-row-count { flex: none; }

input.is-fixed {
  color: var(--muted);
  background: var(--surface-2);
  cursor: default;
}

.profile-auth input.invalid,
.profile-new input.invalid,
.profile-setup input.invalid { border-color: var(--danger); }
.profile-auth .profile-name { flex: none; max-width: 40%; }
.profile-auth .profile-dot { flex: none; }

.profile-setup {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
}
.profile-setup select {
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background-color: var(--bg);
  color: var(--text);
  font: inherit;
}
.profile-setup label {
  display: flex;
  flex-direction: column;
  gap: 5px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
}
.profile-setup .hint.small { margin: 0; font-size: 11.5px; }

/* Switched off: still listed for the admin, plainly not in play. */
.profile-off .profile-pick.is-off {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: 1;
  min-width: 0;
  padding: 9px 11px;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  opacity: 0.55;
}
.profile-off .profile-name { font-weight: 600; }

/* Says which row can do everything, without competing with the "you" pill. */
.profile-admin {
  margin-left: 6px;
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}

.profile-new, .profile-rename, .profile-auth, .profile-setup { display: flex; gap: 7px; width: 100%; }
.profile-new input, .profile-rename input, .profile-auth input, .profile-setup input {
  flex: 1;
  min-width: 0;
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font: inherit;
}
.profile-new input:focus, .profile-rename input:focus,
.profile-auth input:focus, .profile-setup input:focus { outline: none; border-color: #3b82f6; }

/* Who added a spot — on cards and in map popups. */
.author {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11.5px;
  color: var(--muted);
}
.who-dot {
  --who: var(--muted);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--who);
  flex: none;
}

.home-clock {
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--muted);
  padding: 6px 10px;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

/* ------------------------------------------------------------- filters */

.filters {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 16px;
  background: var(--bg);
  border-bottom: 1px solid var(--border-soft);
  flex-wrap: wrap;
}

/* The wrapper exists only so a phone can fold these away. Here it dissolves,
   leaving its children as direct flex items of .filters. My List and Group sit
   outside it in the markup, so `order` puts them back after the chips where
   they read best on a wide screen. */
.filter-body { display: contents; }
#catStrip { order: 1; }
#listToggle { order: 4; }
.filter-search { order: 6; }
/* Last, and the full width of the row. Sat between the categories and Clear
   it was a block wedged mid-row: it shoved Clear and My List along and pushed
   the search box onto a line of its own. Tags refine the categories above
   them, so they read as a row underneath rather than an interruption. */
#subStrip { order: 7; flex-basis: 100%; }

/* A chip row that scrolls sideways instead of wrapping. The bar lives in the
   flow above the book, so anything that wraps in it pushes the spots down: at
   fourteen categories with tags under each, the filters were eleven rows and
   the grid started off the screen. Scrolling fixes the bar's height at one row
   however far the book grows. See strip.js for the wheel, the arrows and the
   fades. */
.chip-strip {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1 1 180px;
  min-width: 0;
  /* Room set aside for the arrows. Nothing on a touchscreen, which has none. */
  --gutter: 0px;
  padding-inline: var(--gutter);
}
/* The arrows get a lane of their own rather than sitting on top of the chips,
   where they covered the one you were about to read. Reserved for both rows
   together, and for as long as either can scroll: tied to each row's own
   overflow it would indent the categories and not the tags, and appear and
   vanish under the cursor as you scrolled. */
@media (hover: hover) and (pointer: fine) {
  .filters:has(.chip-strip.can-scroll) .chip-strip { --gutter: 26px; }
}
.chip-strip > .chips {
  flex: 1;
  min-width: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  /* A chip's focus ring is drawn outside its border and would be clipped by
     the scroll box. */
  padding: 2px;
  margin: -2px;
}
.chip-strip > .chips::-webkit-scrollbar { display: none; }
.chip-strip .chip { flex: none; }

/* What says the row goes on. Painted over the chips rather than beside them:
   an arrow that appears and disappears in the layout would shunt every chip
   sideways as you scroll. */
.chip-strip::before,
.chip-strip::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 38px;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease;
}
/* Inset by the lane, so the fade starts where the chips do. */
.chip-strip::before { left: var(--gutter); background: linear-gradient(to right, var(--bg), transparent); }
.chip-strip::after { right: var(--gutter); background: linear-gradient(to left, var(--bg), transparent); }
.chip-strip.has-start::before,
.chip-strip.has-end::after { opacity: 1; }

.strip-arrow {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  z-index: 2;
  display: none;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--muted);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.strip-arrow[data-strip="prev"] { left: 0; }
.strip-arrow[data-strip="next"] { right: 0; }
.strip-arrow:hover { color: var(--text); border-color: var(--surface-3); }
/* Only a mouse needs them: a trackpad and a touchscreen scroll sideways
   already, and on those the arrows are two more things sitting over the chips. */
@media (hover: hover) and (pointer: fine) {
  .chip-strip.has-start .strip-arrow[data-strip="prev"],
  .chip-strip.has-end .strip-arrow[data-strip="next"] { display: grid; place-items: center; }
}
/* Straight after the chips on a wide row. Clear undoes the categories, the
   tags and the search — not My List, which is the default view and survives it
   — so it belongs against the chips rather than beside a toggle it never
   touches. Trailing the whole row put it on a line of its own. The phone puts
   it at the front instead, against the Filters button. */
.filters-group { order: 3; display: inline-flex; align-items: center; gap: 2px; }

/* Cards across. Hidden by default because it only exists for narrow screens:
   a desktop auto-fits five columns and the question does not arise. */
.viewrow { display: contents; }
.density {
  display: none;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--bg);
}
.density button {
  display: inline-grid;
  place-items: center;
  width: 32px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--muted);
}
.density button[aria-pressed="true"] { background: var(--surface-3); color: var(--text); }

/* Not a chip. A chip is a thing you switch on and it stays lit; this undoes
   them and then disappears. So: no pill, no border, no fill — a quiet bit of
   text that darkens to a warning colour under the cursor. */
.clear-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  padding: 6px 8px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
}
.clear-btn:hover { background: transparent; color: var(--danger); }
.clear-btn svg { opacity: 0.85; }

/* Shown only on a phone; see the media query at the end. Two classes, not one:
   .chip sets display later in this file and a bare .filters-toggle loses the
   tie to it. Descendant rather than child — the button sits inside
   .filters-group now, and a child selector quietly stopped matching. */
.filters .filters-toggle { display: none; }
.filters-count {
  display: inline-grid;
  place-items: center;
  min-width: 16px;
  height: 16px;
  margin-left: 2px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--accent, #5aa9ff);
  color: #07121f;
  font-size: 10.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.chips { display: flex; gap: 7px; }

/* Second row: sub-tags of whichever category is selected. The divider that
   used to sit on its left made sense while this was inline beside the
   categories; on its own row it was just an indent pushing everything right. */
.sub-chips {
  align-items: center;
  gap: 6px;
}
.chip-sub { font-size: 12.5px; padding: 5px 10px; }

/* Tags are grouped under the category they belong to. The label is not a chip
   and must not look like one — it names the group rather than being a thing
   you can switch on.

   Groups run inline, divided by a rule. They used to take a row each: inline
   needed that divider, and a divider turns into a leading indent the moment a
   group wraps to the next line, which made the second category read as though
   it were nested inside the first. Now that the row scrolls rather than wraps
   there is no second line for that to happen on. */
.sub-group {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
}
.sub-group + .sub-group {
  margin-left: 4px;
  padding-left: 12px;
  border-left: 1px solid var(--border);
}
.sub-group-of {
  display: inline-flex;
  align-items: center;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--cat);
  opacity: 0.85;
  white-space: nowrap;
  padding-left: 2px;
}


.popup-cat em {
  font-style: normal;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: 6px;
  padding-left: 7px;
  border-left: 1px solid var(--border);
}

.chip {
  --cat: var(--muted);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
}
.chip .cat-icon { opacity: 0.75; }
.chip:hover { color: var(--text); border-color: var(--surface-3); }
.chip[aria-pressed="true"] {
  color: var(--text);
  border-color: var(--cat);
  background: var(--cat-tint, var(--surface-2));
}
.chip[aria-pressed="true"] .cat-icon { opacity: 1; }
.chip-all[aria-pressed="true"] { border-color: var(--primary); background: var(--surface-3); }

.chip-count {
  font-family: var(--mono);
  font-size: 11px;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
}

.filter-search {
  position: relative;
  display: flex;
  align-items: center;
  /* Does not grow. It shared the leftover width with the category strip while
     the strip could wrap; now the strip is the thing that has to give, so the
     slack belongs to it and the box keeps one width at any window size.

     Flexbox decides whether to wrap on the basis, not the minimum, so a
     generous basis dropped the box onto a line of its own on a narrow desktop
     while there was still room to shrink into. */
  flex: 0 1 300px;
  min-width: 190px;
  margin-left: auto;
}
.filter-search svg { position: absolute; left: 11px; color: var(--muted); pointer-events: none; }
.filter-search input {
  width: 100%;
  padding: 8px 12px 8px 33px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}
.filter-search input::placeholder { color: #5f6e82; }
.filter-search input:focus { outline: none; border-color: #3b82f6; }

/* ---------------------------------------------------------------- main */

main { flex: 1; min-height: 0; position: relative; }

#gridView { height: 100%; overflow-y: auto; padding: 16px; }
/* The pull moves the content with the finger; see the pull-to-refresh block
   in app.js for why the browser's own gesture cannot be used here. */
/* :not() so the indicator keeps its own transform — an ID selector here would
   otherwise outrank .pull-hint and flatten it to this translate, leaving it
   half a width off centre. */
#gridView > *:not(.pull-hint) { transform: translateY(var(--pull, 0px)); }
#gridView.settling > *:not(.pull-hint) { transition: transform 0.22s ease-out; }

.pull-hint {
  position: absolute;
  top: 6px;
  left: 50%;
  z-index: 5;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--muted);
  opacity: 0;
  pointer-events: none;
  /* Its own transform, so the content's translate does not drag it twice. */
  transform: translate(-50%, calc(var(--pull, 0px) - 44px)) rotate(var(--pull-spin, 0deg));
}
#gridView.settling .pull-hint { transition: transform 0.22s ease-out, opacity 0.22s ease-out; }
.pull-hint.ready { color: var(--text); border-color: var(--border-strong, var(--border)); }
.pull-hint.busy { animation: pull-spin 0.8s linear infinite; color: var(--text); }

@keyframes pull-spin {
  to { transform: translate(-50%, calc(var(--pull, 0px) - 44px)) rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .pull-hint.busy { animation: none; }
  #gridView.settling > *, #gridView.settling .pull-hint { transition: none; }
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(255px, 1fr));
  gap: 14px;
  align-content: start;
}

/* --------------------------------------------------------------- clocks */

/* The filters narrow spots. There are no spots on this screen. */
body.view-clocks .filters { display: none; }

#clocksView { height: 100%; overflow-y: auto; padding: 16px; }

.clocks-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 16px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.clocks-target {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--muted);
}
.clocks-target select {
  appearance: none;
  font-family: var(--mono);
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  background: var(--surface-2);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%238b9aae' d='M6.4 8.6 5 10l7 7 7-7-1.4-1.4L12 14.2 6.4 8.6Z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 6px center;
  background-size: 15px;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 24px 6px 9px;
  /* So the list the phone opens is dark too. */
  color-scheme: dark;
}
.clocks-target select:focus-visible { outline: 2px solid #5aa9ff; outline-offset: 1px; }
.clocks-colon { font-family: var(--mono); font-weight: 700; color: var(--muted); margin: 0 -3px; }
#placeAdd { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }

/* Columns, not one long strip. A row needs about 300px to say what it says,
   and on a wide screen the rest of the line was empty while the board ran off
   the bottom — nineteen places fit on one screen across, and fitting is the
   point of a board. The order still reads left to right, then down. */
.clock-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 10px;
}

/* Four is the cap. Auto-fill reaches a fifth column at about 1570px and keeps
   going, and past four the rows are shorter than the gaps between them —
   reading across a line of six is more work than scrolling a little.

   Four wide ones rather than four narrow ones in the middle of the screen:
   centring left a margin down each side that read as a mistake rather than as
   a gutter. The cards take the width instead. */
@media (min-width: 1560px) {
  .clock-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* On the row rather than over a heading. Nineteen places across fourteen
   countries meant a heading bar for almost every row, which said the same
   thing the flag says in the space of a line. Same-country places still land
   together, because the board keeps the order it was written in. */
.clock-flag {
  flex: none;
  width: 22px;
  height: 16px;
  border-radius: 2px;
  object-fit: cover;
  /* Several flags are mostly white, and a white rectangle on a dark card has
     no edge without this. */
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16);
}
/* What is left when there is no file for a code. */
.clock-flag-text {
  display: inline-grid;
  place-items: center;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--muted);
  background: var(--surface-3);
}

.clock-row {
  position: relative;
  display: grid;
  gap: 4px;
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: 12px;
}

.clock-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.clock-head h3 { font-size: 15px; letter-spacing: 0.01em; }
.clock-region { font-size: 12px; color: var(--muted); }

.clock-when { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.clock-date { font-size: 12.5px; color: #a9b7c9; }
.clock-time {
  font-family: var(--mono);
  font-size: 19px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.clock-zone { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-size: 11.5px; }
.clock-offset { color: var(--muted); }
.clock-tz { font-family: var(--mono); color: #6f8097; }

/* The answer the screen exists for, so it is the loudest line on the row. */
.clock-until {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 6px;
  padding: 7px 10px;
  border-radius: 8px;
  font-size: 13px;
  color: #dfeaf9;
  background: rgba(90, 169, 255, 0.11);
  border: 1px solid rgba(90, 169, 255, 0.3);
}
.clock-until strong { font-variant-numeric: tabular-nums; }
.clock-home { color: var(--muted); font-size: 12px; }
/* Within the hour — the point at which it stops being planning. */
/* Within the window, the whole card goes green — the same green a collected
   rally stop uses, because it is the same kind of statement. A gradient rather
   than a flat wash so the card still has a top and a bottom, strongest at the
   corner the eye lands on first and fading back into the surface by the time
   it reaches the clock. */
.clock-row.is-soon {
  border-color: rgba(34, 197, 94, 0.55);
  box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.3);
  background:
    linear-gradient(135deg, rgba(34, 197, 94, 0.22), rgba(34, 197, 94, 0.05) 62%),
    var(--surface);
}
.clock-row.is-soon .clock-region,
.clock-row.is-soon .clock-offset { color: #a7d9bb; }
.clock-row.is-soon .clock-tz { color: #8fc4a6; }

/* The line inside it stops competing: the card is already saying it, so this
   goes back to being the sentence rather than the highlight. */
.clock-until.is-soon {
  color: #dcfce7;
  background: rgba(6, 46, 24, 0.5);
  border-color: rgba(34, 197, 94, 0.45);
}
.clock-until.is-soon .clock-home { color: #86efac; }

.clock-actions { position: absolute; right: 8px; top: 8px; display: flex; gap: 2px; }

@media (max-width: 760px) {
  #clocksView { padding: 12px 12px 100px; }
  .clock-time { font-size: 17px; }
  .clocks-bar { gap: 8px; margin-bottom: 12px; }

  /* The same floating circle Add spot uses in the grid, in the same corner, so
     "add one of these" is one gesture wherever you are. */
  #placeAdd {
    position: fixed;
    right: 16px;
    bottom: max(30px, calc(env(safe-area-inset-bottom, 0px) + 16px));
    z-index: 60;
    width: 56px;
    height: 56px;
    margin-left: 0;
    padding: 0;
    gap: 0;
    border-radius: 50%;
    justify-content: center;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.45);
  }
  #placeAdd .add-label { display: none; }
  #placeAdd .add-plus { width: 22px; height: 22px; }
}

/* ------------------------------------------------------------- grouping */

/* A heading used to be a small line of text over a hairline, and partway down
   a long book the moment one group ended and the next began went by unnoticed.
   A rule across the full width carries the break now, with the heading itself
   heavier above its own underline.

   The space around that rule is deliberately lopsided and deliberately small:
   19px below the last card, 11px above the heading. Even spacing left the line
   floating in a void that read as a hole in the page rather than a divider, and
   sitting the rule nearer the heading is what says which group it introduces.
   Under 12px on either side and it would be indistinguishable from the gap
   between two cards. */
.group + .group {
  margin-top: 19px;
  padding-top: 11px;
  border-top: 1px solid var(--border);
}

.group-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 11px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.group-head h2 {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.01em;
}
/* The name is the one thing here that can give ground, so it is the one thing
   allowed to. Without this the heading wrapped instead, putting Show on map on
   a line of its own and costing a row of screen on every group. */
.group-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.group-count { flex: none; }

/* The whole heading is the fold, not a chevron beside it: a group name is a
   big obvious thing to press and the chevron on its own is a 12px target on a
   phone. It borrows the heading's own type rather than a button's, so the row
   still reads as a heading and not as a control someone bolted on. */
.group-fold {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
  padding: 2px 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-align: left;
  cursor: pointer;
}
.group-fold svg { flex: none; color: var(--muted); transition: color 0.14s; }
.group-fold:hover svg { color: var(--text); }
.group-fold:hover .group-name { text-decoration: underline; text-underline-offset: 3px; }

/* Folded, the heading is all there is, so the rule under it would be dividing
   it from nothing. */
.group.is-shut .group-head { margin-bottom: 0; padding-bottom: 0; border-bottom: 0; }
.group-map { flex: none; display: inline-flex; align-items: center; gap: 6px; }

/* Save all, beside Show on map. Reads as the same kind of thing because it is
   — both act on the group rather than on any one card — and it borrows the
   card bookmark's blue once the whole group is in, so the two say "saved" in
   the same colour wherever you meet them. */
.group-save { flex: none; display: inline-flex; align-items: center; gap: 6px; }
.group-save svg { flex: none; }
.group-save.is-on { color: #5aa9ff; border-color: rgba(90, 169, 255, 0.4); }
.group-save.is-on:hover { color: #7dbcff; background: rgba(90, 169, 255, 0.14); }
.group-none .group-head h2 { color: var(--muted); font-weight: 600; }
.group-count {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  padding: 1px 7px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
}
/* Only the first of them, now that there are two: give both `margin-left: auto`
   and the heading pushes them apart instead of pushing the pair right. */
.group-head > button:first-of-type { margin-left: auto; }
.group-head > button + button { margin-left: 0; }

/* How far through this group's rally you are. Sits with the heading, not the
   cards, because it describes the set rather than any one stop. */
.group-progress {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}
.group-bar {
  display: block;
  width: 72px;
  height: 5px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  overflow: hidden;
}
.group-bar > span {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: #22c55e;
  transition: width 0.22s ease;
}
.group-progress.is-all { color: #4ade80; }

/* The bar has nothing left to say once it is full, and the words say it. */
.group-progress.is-all .group-bar { display: none; }

/* Below this there is not room for the heading, its progress and a worded
   button, and the button is the least of the three: it repeats what tapping a
   card's thumbnail already does. The pin keeps the action without the row. */
@media (max-width: 560px) {
  .group-map-label, .group-save-label { display: none; }
  .group-map, .group-save { padding: 6px 7px; }
}

@media (max-width: 640px) {
  .group-bar { width: 52px; }
}

.card-group {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  align-self: flex-start;
  max-width: 100%;
  font-size: 11.5px;
  font-weight: 600;
  color: #c3d0e1;
  padding: 2px 8px 2px 6px;
  border-radius: 999px;
  background: var(--cat-tint, var(--surface-2));
  border: 1px solid var(--border-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Inside a group section the name is already the heading. */
.is-grouped .group:not(.group-none) .card-group { display: none; }

.chip-toggle .cat-icon, .chip-toggle svg { opacity: 0.7; }
/* A solid colour disc reads as "on" all by itself, so dim it while the toggle
   is off — otherwise deselecting leaves the chip still looking selected. */
.chip-toggle .profile-dot { opacity: 0.35; transition: opacity 0.14s; }
.chip-toggle[aria-pressed="true"] {
  color: var(--text);
  border-color: var(--primary);
  background: var(--surface-3);
}
.chip-toggle[aria-pressed="true"] svg,
.chip-toggle[aria-pressed="true"] .profile-dot { opacity: 1; }

/* Green while it is on, the same green as a collected tick everywhere else:
   this chip and the ticks it hides are one idea. */
.chip-todo[aria-pressed="true"] {
  border-color: #22c55e;
  background: rgba(34, 197, 94, 0.14);
}
.chip-todo[aria-pressed="true"] svg { color: #4ade80; }

/* --------------------------------------------------------------- cards */

.card {
  --cat: var(--muted);
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color 0.15s, transform 0.15s;
}
.card:hover { border-color: var(--border); transform: translateY(-2px); }

.card-thumb {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--surface-2);
  display: grid;
  place-items: center;
  /* It is a button: clicking the map opens this spot in map view. */
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 0;
  cursor: pointer;
  overflow: hidden;
}
.card-thumb:hover { background: var(--surface-2); }
.card-thumb:focus-visible { outline: 2px solid #5aa9ff; outline-offset: -2px; }

/* Marks the exact coordinate at the centre of the satellite thumbnail. */
.thumb-pin {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, calc(-100% + 2px));
  line-height: 0;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.55));
  pointer-events: none;
}

.thumb-hint {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 12.5px;
  font-weight: 650;
  color: #fff;
  background: rgba(8, 12, 18, 0.55);
  opacity: 0;
  transition: opacity 0.15s;
}
.card-thumb:hover .thumb-hint,
.card-thumb:focus-visible .thumb-hint { opacity: 1; }
.thumb-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* 3x3 patch of map tiles, offset so the coordinate is at the centre. */
.thumb-tiles {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 768px;
  height: 768px;
  pointer-events: none;
}
.thumb-tile {
  position: absolute;
  width: 256px;
  height: 256px;
  display: block;
}

.thumb-fallback { display: none; opacity: 0.4; }
.card-thumb.thumb-broken .thumb-photo,
.card-thumb.thumb-broken .thumb-tiles { display: none; }
.card-thumb.thumb-broken .thumb-fallback { display: block; }
.card-thumb.thumb-broken .thumb-tag,
.card-thumb.thumb-broken .thumb-pin { display: none; }

.thumb-tag {
  position: absolute;
  right: 8px;
  bottom: 8px;
  font-size: 10.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 7px;
  border-radius: 5px;
  background: rgba(8, 12, 18, 0.72);
  color: #c4d0e0;
}

.card-cat {
  position: absolute;
  left: 8px;
  top: 8px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 9px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 650;
  color: #fff;
  background: var(--cat);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}

/* Gym sub-tag, sitting under the category badge on the thumbnail. */
.card-sub {
  position: absolute;
  left: 8px;
  top: 34px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  color: #0b1018;
  background: var(--sub);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}

.card-body { padding: 11px 13px 4px; display: flex; flex-direction: column; gap: 6px; }

.card-name {
  font-size: 15px;
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.card-loc { font-size: 12.5px; color: #a9b7c9; }

.card-time { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; font-size: 12.5px; }
.card-time .clock { font-family: var(--mono); font-size: 14px; font-variant-numeric: tabular-nums; }
.card-time .offset { color: var(--muted); }
.card-time .dayshift:not(:empty) {
  color: #ffb020;
  font-size: 11px;
  border: 1px solid rgba(255, 176, 32, 0.35);
  border-radius: 4px;
  padding: 0 4px;
}
.card-time .approx {
  font-size: 10.5px;
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: 4px;
  padding: 0 4px;
}

/* The coordinate is the point of the whole app, so it is the loudest thing
   on the card rather than one more muted line. */
.coord {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: 8px 10px;
  background: rgba(90, 169, 255, 0.11);
  border: 1px solid rgba(90, 169, 255, 0.3);
  color: #dfeaf9;
}
.coord:hover {
  background: rgba(90, 169, 255, 0.2);
  border-color: rgba(90, 169, 255, 0.55);
  color: #fff;
}
.coord svg { flex: none; opacity: 0.65; }
.coord:hover svg { opacity: 1; }

.card-notes {
  font-size: 12.5px;
  color: #9fadc0;
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Author on the left, icon actions on the right — one compact row instead of
   a strip of text buttons. */
.card-actions {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 10px 10px;
  border-top: 1px solid var(--border-soft);
}
.card-actions .author { flex: 1; min-width: 0; margin-right: 4px; }
.author-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.act {
  display: inline-grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex: none;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
}
.act:hover { background: var(--surface-3); color: var(--text); }
.act-danger:hover { background: rgba(255, 107, 107, 0.14); color: var(--danger); }
/* Saved to My List — solid bookmark, in the accent used for your own things. */
.act-mark.is-on { color: #5aa9ff; }
.act-mark.is-on:hover { background: rgba(90, 169, 255, 0.14); color: #7dbcff; }

/* --------------------------------------------------------------- empty */

.empty { display: grid; place-items: center; padding: 70px 20px; text-align: center; }
.empty h2 { font-size: 17px; margin-bottom: 7px; }
.empty p { color: var(--muted); margin-bottom: 16px; }

/* ----------------------------------------------------------------- map */

#mapView { position: absolute; inset: 0; }
#map { position: absolute; inset: 0; background: #0a0e13; }

.map-toolbar {
  position: absolute;
  top: 12px;
  left: 12px;
  right: 12px;
  z-index: 5;
  /* Three tracks so the search sits centred on the map regardless of how
     wide the button group beside it happens to be. */
  display: grid;
  grid-template-columns: 1fr minmax(0, 380px) 1fr;
  gap: 10px;
  align-items: start;
  pointer-events: none;
}
.map-toolbar > * { pointer-events: auto; }

.map-search { position: relative; grid-column: 2; width: 100%; }
.map-search input {
  width: 100%;
  padding: 10px 34px 10px 13px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: rgba(12, 17, 24, 0.94);
  color: var(--text);
  font: inherit;
  box-shadow: var(--shadow);
  backdrop-filter: blur(6px);
}
.map-search input:focus { outline: none; border-color: #38bdf8; }
#mapSearchClear {
  position: absolute;
  right: 5px;
  top: 5px;
  border: 0;
  background: transparent;
  color: var(--muted);
  padding: 5px 8px;
}
#mapSearchClear:hover { color: var(--text); }

.suggestions {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 5px;
  list-style: none;
  background: rgba(12, 17, 24, 0.97);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  max-height: 320px;
  overflow-y: auto;
}
.suggestion {
  display: block;
  width: 100%;
  text-align: left;
  border: 0;
  background: transparent;
  padding: 7px 9px;
  border-radius: 7px;
}
.suggestion:hover { background: var(--surface-3); }
.suggestion-name { display: block; font-weight: 600; font-size: 13.5px; }
.suggestion-meta {
  display: block;
  font-size: 11.5px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.map-buttons { grid-column: 3; justify-self: end; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

.basemaps {
  display: flex;
  background: rgba(12, 17, 24, 0.94);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 3px;
  gap: 2px;
  box-shadow: var(--shadow);
}
.basemaps button {
  border: 0;
  background: transparent;
  padding: 6px 12px;
  border-radius: 7px;
  font-size: 13px;
  color: var(--muted);
  font-weight: 600;
}
.basemaps button[aria-pressed="true"] { background: var(--surface-3); color: var(--text); }

.map-btn {
  background: rgba(12, 17, 24, 0.94);
  box-shadow: var(--shadow);
  font-size: 13px;
  font-weight: 600;
  padding: 9px 13px;
  border-radius: 10px;
}

.map-empty {
  position: absolute;
  z-index: 5;
  left: 50%;
  bottom: 28px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-radius: 10px;
  background: rgba(12, 17, 24, 0.95);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  font-size: 13px;
}

.maplibregl-popup-content {
  background: rgba(13, 18, 26, 0.97);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 11px;
  padding: 12px 13px;
  box-shadow: var(--shadow);
  font-family: var(--font);
}
.maplibregl-popup-tip { border-top-color: rgba(13, 18, 26, 0.97) !important; }
.maplibregl-popup-close-button { color: var(--muted); font-size: 17px; padding: 2px 7px; }
.maplibregl-popup-close-button:hover { background: transparent; color: var(--text); }

.popup { display: flex; flex-direction: column; gap: 7px; }
.popup-cat {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--cat);
}
.popup h4 { font-size: 15px; padding-right: 14px; }
.popup-loc { font-size: 12.5px; color: #a9b7c9; }
.popup-time { display: flex; gap: 7px; align-items: baseline; font-size: 12.5px; }
.popup-time .clock { font-family: var(--mono); font-size: 14px; font-variant-numeric: tabular-nums; }
.popup-time .offset { color: var(--muted); }
.popup-notes { font-size: 12.5px; color: #9fadc0; line-height: 1.45; }
.popup-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 4px;
  padding-top: 8px;
  border-top: 1px solid var(--border-soft);
}
.popup-actions .author { flex: 1; min-width: 0; }
/* Search-result popup: one full-width call to action, no author line. */
.popup-actions.single { border-top: 0; padding-top: 2px; }
.popup-actions.single button { flex: 1; }

/* Several spots at one point: which of them this is, and a way to the rest.
   Sits above the title, since it frames everything below it. The popup has no
   close button — clicking the map closes it — so the row has the full width and
   the arrows sit at the edges, well apart from each other. */
.popup-stack {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  margin: -2px -2px 1px;
  padding: 3px 4px;
  border-radius: 7px;
  background: var(--surface-3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.stack-step {
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  flex: none;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.stack-step:hover { background: var(--surface); color: var(--text); }
/* A thumb needs more than 22px; desktop keeps the smaller pair. */
@media (pointer: coarse) {
  .stack-step { width: 32px; height: 32px; }
}

/* -------------------------------------------------- category manager */

.cat-manage { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-direction: column; gap: 3px; }

.cat-row {
  display: flex;
  align-items: center;
  /* Tighter than it was: the move handles are two more things on a row that
     already carried three. */
  gap: 8px;
  padding: 7px 9px;
  border-radius: var(--radius-sm);
  background: var(--bg);
  border: 1px solid var(--border-soft);
}
/* Tags are indented and quieter: they belong to the row above, not beside it. */
.cat-row.is-tag {
  margin-left: 24px;
  background: transparent;
  border-color: transparent;
  border-left: 1px solid var(--border);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
/* The fold sits before the swatch; the gap keeps tag rows aligned under the
   category above them, since they carry no fold of their own. */
.cat-fold {
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  flex: none;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
}
.cat-fold:hover { background: var(--surface-2); color: var(--text); }
.cat-fold-gap { width: 22px; flex: none; }

.cat-folded { margin: -1px 0 6px 30px; }
.cat-folded button {
  width: 100%;
  text-align: left;
  color: var(--muted);
  font-size: 12px;
  padding: 5px 9px;
  border-style: dashed;
}
.cat-folded button:hover { color: var(--text); }

.cat-swatch {
  display: inline-grid;
  place-items: center;
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 7px;
  background: var(--cat);
}
.cat-row.is-tag .cat-swatch { width: 22px; height: 22px; border-radius: 6px; }
.cat-row-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cat-row-count { margin-left: auto; font-size: 11.5px; color: var(--muted); white-space: nowrap; }

.cat-addtag { margin: 0 0 6px 30px; }
.cat-addtag button { color: var(--muted); font-size: 12.5px; padding: 4px 8px; }
.cat-addtag button:hover { color: var(--text); }
.cat-add-top { width: 100%; }

/* Colour and icon pickers — hand-rolled radio groups. */
.swatches { display: flex; flex-wrap: wrap; gap: 7px; }
.swatch {
  width: 28px;
  height: 28px;
  padding: 0;
  border-radius: 50%;
  background: var(--cat);
  border: 2px solid transparent;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.swatch[aria-checked="true"] { border-color: var(--text); }

.icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
  gap: 5px;
  max-height: 176px;
  overflow-y: auto;
  padding: 3px;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  background: var(--bg);
}
.icon-pick {
  display: grid;
  place-items: center;
  height: 36px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 7px;
  background: transparent;
  color: var(--muted);
}
.icon-pick:hover { background: var(--surface-2); color: var(--text); }
.icon-pick[aria-checked="true"] { background: var(--surface-3); color: var(--text); border-color: var(--border); }

/* The editor carries its own footer inside the scrolling body. */
.cat-edit-foot { margin-top: 16px; padding: 0; border-top: 0; background: transparent; }

.confirm-pick { margin-top: 14px; }
.confirm-pick select {
  width: 100%;
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background-color: var(--bg);
  color: var(--text);
  font: inherit;
}

/* ------------------------------------------------------------- dialogs */

dialog.sheet {
  width: min(560px, calc(100vw - 24px));
  max-height: min(86dvh, 760px);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6);
  overflow: hidden;
}
dialog.narrow { width: min(420px, calc(100vw - 24px)); }
dialog::backdrop { background: rgba(4, 7, 11, 0.66); backdrop-filter: blur(2px); }
/* [open] matters: a bare `display: flex` would override the browser's own
   `dialog:not([open]) { display: none }` and leave every dialog on screen.
   Without the column the body cannot be told how tall it may be, so its
   overflow-y had nothing to scroll and the dialog just clipped it — which is
   why a long category list had no way to reach the button at its foot. */
dialog.sheet[open] { display: flex; flex-direction: column; }
/* The sheet's own form, not every form inside one. Unqualified it also caught
   the little inline forms in the profile list — sign in, rename, set a
   password — and stood each of their fields on its own line. */
dialog.sheet > form { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.sheet-body { flex: 1 1 auto; min-height: 0; }
.sheet-head, .sheet-foot { flex: none; }

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border-soft);
}
.sheet-head h2 { font-size: 16px; }

.sheet-body { padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; }
.sheet-body > h2 { font-size: 16px; }
.sheet-body > p { color: var(--muted); line-height: 1.5; }

.sheet-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--border-soft);
  background: var(--surface);
}

.field { display: flex; flex-direction: column; gap: 6px; }
.field label, .field .label { font-size: 12.5px; font-weight: 650; color: #b6c3d4; }
.req { color: var(--danger); }
.field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.field .row { display: flex; gap: 8px; }
.field .row input { flex: 1; min-width: 0; }

/* Groups are listed like categories: same row, minus the fold and the swatch,
   because a group has no tags under it and no colour of its own. */
/* A stamp collected. Green because it is an achievement, not a warning, and
   filled once it is yours so a glance down the grid reads as a tally. */
.act-done.is-on { color: #4ade80; }
.act-done:hover { color: var(--text); }

/* The card says so too, so a rally can be scanned for what is still missing
   without reading every tick. Said three ways, because one quiet outline was
   easy to miss down a long list: a badge on the photo, a green edge, and a
   tint under the details. The photo itself is left alone — it is the reason
   the card is worth looking at. */
.card-done {
  position: absolute;
  right: 8px;
  top: 8px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px 3px 6px;
  border-radius: 999px;
  background: #22c55e;
  color: #052e16;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.02em;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
}

.card.is-done {
  border-color: #22c55e;
  box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.55);
}
.card.is-done:hover { border-color: #4ade80; }

/* The same green wash the timezone board uses when a place is close, and for
   the same reason: a flat tint says "something about this card", a gradient
   says which end of it. It runs from under the photo down into the footer
   rather than across the whole card, because the photo covers the top and a
   gradient hidden behind it is just a flat tint again. */
.card.is-done .card-body {
  background: linear-gradient(180deg, rgba(34, 197, 94, 0.2), rgba(34, 197, 94, 0.05));
}
.card.is-done .card-actions {
  background: rgba(34, 197, 94, 0.05);
  border-top-color: rgba(34, 197, 94, 0.22);
}

/* The small headings in the import dialog. .label was only ever styled inside
   a .field, and these stand on their own. */
.sheet-body > .label {
  display: block;
  margin: 16px 0 6px;
  font-size: 12.5px;
  font-weight: 650;
  color: #b6c3d4;
}

/* A worked example, shown rather than described. */
.code-sample {
  /* The sheet body is a flex column, so anything that scrolls gets squashed to
     nothing unless it says otherwise: nine lines of example came out 24px tall. */
  flex: none;
  margin: 0 0 4px;
  padding: 11px 13px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg);
  overflow-x: auto;
}
.code-sample code {
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.55;
  color: var(--text);
  white-space: pre;
}
.field-notes {
  margin: 0 0 4px;
  padding-left: 18px;
  font-size: 12.5px;
  color: var(--muted);
  line-height: 1.7;
}
.field-notes b { color: var(--text); font-family: var(--mono); font-size: 12px; }

/* Dropping a file is the quick way in; the button underneath is the sure one. */
.dropzone {
  flex: none;
  margin-top: 14px;
  padding: 22px 14px;
  border: 1.5px dashed var(--border);
  border-radius: var(--radius-sm);
  text-align: center;
  transition: border-color 0.12s, background 0.12s;
}
.dropzone p { margin: 0; }
.dropzone.is-over {
  border-color: var(--primary);
  background: rgba(90, 169, 255, 0.08);
}
/* What is waiting to be imported, and the two things you can do about it. */
.staged {
  flex: none;
  margin-top: 14px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.staged-file {
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--muted);
}
.staged-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 650;
  color: var(--text);
}
.staged .hint { margin: 7px 0 11px; }
/* Offered only when the file actually holds spots the book already has. */
.staged-choice {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0 0 11px;
  font-size: 12.5px;
  color: var(--text);
}
.staged-choice input { margin: 2px 0 0; flex: none; }
/* Something in the file will not make it: not an error, but not nothing. */
.staged .hint.is-warning { color: #ffb020; }
.staged .primary { width: 100%; }

.linky {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--primary);
  font: inherit;
  text-decoration: underline;
}

.group-empty { color: var(--muted); font-size: 13px; padding: 8px 2px; }

/* The group picker sits among the text fields and should not look like a
   visitor. Its arrow is drawn here rather than left to the browser, which
   pins its own hard against the right edge with no say in the matter — this
   one is inset to line up with the padding on the other side. Only the arrow
   is ours: the list it opens is still the platform's, which is what a phone
   wants. */
.field select, .profile-setup select, .confirm-pick select {
  appearance: none;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%238b9aae' d='M6.4 8.6 5 10l7 7 7-7-1.4-1.4L12 14.2 6.4 8.6Z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 15px 15px;
  /* Clear of the arrow, so a long group name never runs under it. */
  padding-right: 36px;
}

.field select {
  width: 100%;
  padding: 9px 11px;
  padding-right: 36px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background-color: var(--bg);
  color: var(--text);
  font: inherit;
}
.field select:focus { outline: none; border-color: #3b82f6; }
.field.invalid select { border-color: var(--danger); }
/* Shown under the picker once "new group" is chosen. */
#f-group { margin-top: 7px; }

.field input[type="text"], .field textarea {
  width: 100%;
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  resize: vertical;
}
.field input::placeholder, .field textarea::placeholder { color: #566476; }
.field input:focus, .field textarea:focus { outline: none; border-color: #3b82f6; }
.field.invalid input, .field.invalid textarea { border-color: var(--danger); }
/* A hidden field is not a field: keep the gap out of the layout with it. */
#f-group[hidden] { display: none; }

.hint { font-size: 11.5px; color: var(--muted); }
.err:empty { display: none; }
.err { font-size: 12px; color: var(--danger); }

.cat-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; }
.cat-option { --cat: var(--muted); position: relative; }
.cat-option input { position: absolute; opacity: 0; pointer-events: none; }
.cat-option span {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
  transition: border-color 0.14s, color 0.14s, background 0.14s;
}
.cat-option span:hover { color: var(--text); }
.cat-option input:checked + span { border-color: var(--cat); color: var(--text); background: color-mix(in srgb, var(--cat) 14%, transparent); }
.cat-option input:focus-visible + span { outline: 2px solid #5aa9ff; outline-offset: 2px; }


/* -------------------------------------------------------------- toasts */

#toasts {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
  pointer-events: none;
}
.toast {
  padding: 10px 16px;
  border-radius: 10px;
  background: rgba(18, 25, 35, 0.97);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  font-size: 13.5px;
  max-width: min(440px, calc(100vw - 32px));
  animation: rise 0.18s ease-out;
}
.toast-ok { border-color: rgba(52, 211, 153, 0.45); }
.toast-warn { border-color: rgba(255, 176, 32, 0.45); }
.toast-error { border-color: rgba(255, 107, 107, 0.5); }
.toast.leaving { opacity: 0; transition: opacity 0.35s; }

@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------- the wait */

/* Import and export both have a wait in them that used to show nothing: a
   button that greyed out, and then eventually a toast. Greying out says "not
   now"; it does not say "working". These say working.

   One ring, used in a button and in a toast, sized from the text around it so
   it sits on the same line as whatever it is waiting for. */
.spinner {
  display: inline-block;
  flex: none;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  opacity: 0.8;
  animation: spin 0.7s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* A button that is working is not a button that is disabled — it looks alive,
   it just cannot be pressed again. */
.is-working {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  cursor: progress;
}
.is-working[disabled] { opacity: 1; }

/* Unlike every other toast this one does not leave on its own: it is there
   until the thing it is reporting on has finished. */
.toast-busy { display: flex; align-items: center; gap: 9px; }

@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2.4s; }
}

/* -------------------------------------------------------------- mobile */

@media (max-width: 760px) {
  /* A grid rather than a wrapping flex row, because wrapping was the bug: a
     flex line that does not fit breaks in two before it shrinks anything, so
     the title kept dropping the clock and the profile onto rows of their own.
     Two named rows instead — who and when on the first, which view on the
     second — and the title column is minmax(0, 1fr), which does shrink. */
  .topbar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "brand right"
      "viewrow viewrow";
    align-items: center;
    column-gap: 10px;
    row-gap: 10px;
    padding: 9px 12px;
  }
  .brand { grid-area: brand; gap: 9px; min-width: 0; margin-right: 0; }
  /* Ellipsis, not a second line: the row has a fixed height now, and a title
     that outgrows it should give up its tail rather than the layout. */
  .brand h1 {
    font-size: 17px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .viewrow { grid-area: viewrow; display: flex; width: auto; align-items: center; gap: 8px; }
  .viewtabs { flex: 1; min-width: 0; }
  /* The cards-across picker takes its share of this row too, so the labels
     give up a little more room here than they do on a wide screen. */
  .viewtabs button { padding: 6px 6px; font-size: 13.5px; }
  .topbar-right { grid-area: right; gap: 6px; flex-wrap: nowrap; }
  .home-clock { order: -1; }

  /* Add becomes a thumb-reachable floating button in the bottom corner — but
     not so far into the corner that it shares space with the home indicator,
     which is what made it awkward to press. Same floor as the sheets. */
  #addBtn {
    position: fixed;
    right: 16px;
    bottom: max(30px, calc(env(safe-area-inset-bottom, 0px) + 16px));
    z-index: 60;
    width: 56px;
    height: 56px;
    padding: 0;
    gap: 0;
    border-radius: 50%;
    justify-content: center;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.45);
  }
  .add-label { display: none; }
  .add-plus { width: 26px; height: 26px; }

  /* The map has its own controls and a full-bleed canvas; a button floating
     over it looked bolted on, so it belongs to the grid only. */
  /* Add on the clock board would add a spot, which is not what that screen is
     for — it has its own Add place. */
  body.view-map #addBtn,
  body.view-clocks #addBtn { display: none; }

  /* Add spot floats at the bottom down here, so the header divider that marks
     it off from the profile button would be dividing nothing. */
  .topbar-right > .menu-wrap { margin-right: 0; }
  .topbar-right > .menu-wrap::after { display: none; }

  #toasts { bottom: max(100px, calc(env(safe-area-inset-bottom, 0px) + 86px)); }

  .filters { padding: 9px 12px; gap: 8px; }
  /* A row to itself inside the fold, rather than the fixed box it keeps on a
     wide screen — and at the top of it. On a wide row the search trails the
     chips; folded, that same order dropped it between the categories and
     their tags, splitting the one thing that has to be read together. */
  .filter-search { order: 0; flex: 1 1 100%; min-width: 0; margin-left: 0; }

  /* Chips wrap rather than scroll sideways. Behind the fold they sit in a
     panel that already scrolls down, so wrapping costs nothing and a phone is
     where seeing every filter at once matters most — scrolling here only cut
     a category name in half, which read as broken rather than as more. */
  .chip-strip { flex: 1 1 100%; }
  .chip-strip > .chips { width: 100%; flex-wrap: wrap; overflow: visible; gap: 6px; }
  .chip-strip::before, .chip-strip::after { content: none; }

  /* Wrapped, the groups need the row each and the divider off: a rule to the
     left of a group that has fallen to the next line reads as an indent, as
     though the second category were nested inside the first. */
  .sub-group { flex-basis: 100%; flex-wrap: wrap; }
  .sub-group + .sub-group { margin-left: 0; padding-left: 0; border-left: 0; }

  /* The label sits above its tags rather than in front of them. Inline, it
     only ever labelled the first line: a group wide enough to wrap sent the
     rest back to the left margin, where they ran into the group below and
     nothing said which category any of them belonged to. Groups are also held
     further apart than the lines inside one, so the block is what reads as a
     group rather than every row looking alike. */
  .sub-group-of { flex-basis: 100%; }
  .sub-group + .sub-group { margin-top: 7px; }

  /* On a phone the filter block was 210px of an 812px screen. It folds behind
     one row — in both views, so switching between them does not move the
     furniture — and the count says when something is still filtering, so a
     hidden filter is never a silent one. My List, Group and Clear stay out of
     the fold: they are the ones worth a single tap.

     Opened, the filters hang over the book rather than pushing it down. Taken
     out of the layout like this, the bar stays one row however many categories
     you have — as part of the layout it and the header owned more than half
     the screen before a single spot showed. It is a transient thing: you open
     it, pick, and it is gone. */
  .filters { padding: 7px 12px; position: relative; }
  .filters .filters-toggle { display: inline-flex; }
  .filter-body { display: none; }
  body.filters-open .filter-body {
    display: flex;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 62;
    flex-wrap: wrap;
    align-content: flex-start;
    gap: 8px;
    max-height: 62dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 10px 12px max(14px, env(safe-area-inset-bottom, 0px));
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    box-shadow: 0 16px 30px rgba(0, 0, 0, 0.45);
  }
  body.filters-open .filter-search { width: 100%; }

  /* Icon only, and against the Filters button rather than adrift at the end of
     the row — a lone cross out there read as a third toggle. */
  /* Tight to Filters, loose to My List — proximity is what says which control
     the cross belongs to. */
  .filters-group { order: 0; margin-right: 5px; }
  .clear-btn { padding: 6px; }
  .clear-label { display: none; }


  /* Clearance at the foot for the floating Add button. This used to be a rule
     of its own further up the block and a later `padding: 12px` quietly reset
     it, which is why the button sat on the last card. */
  #gridView { padding: 12px 12px 100px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; }

  /* Too narrow to centre anything — stack the toolbar instead. */
  .map-toolbar { top: 8px; left: 8px; right: 8px; gap: 8px; grid-template-columns: 1fr; }
  .map-search, .map-buttons { grid-column: 1; width: 100%; justify-self: stretch; }
  .basemaps { flex: 1; }
  .basemaps button { flex: 1; padding: 7px 6px; }

  .field-pair { grid-template-columns: 1fr; }
  .cat-picker { grid-template-columns: repeat(2, 1fr); }
  /* Was flex-wrap: wrap, which is how Show on map ended up on its own line.
     The name ellipsises instead, so the heading stays one row however long the
     group is called. */
  .group-head { flex-wrap: nowrap; }
  dialog.sheet { width: 100vw; max-width: 100vw; max-height: 92dvh; border-radius: 16px 16px 0 0; margin-bottom: 0; }

  /* A sheet sits against the bottom of the screen, which on a phone is where
     the home indicator lives and where the OS is watching for a swipe up. The
     last control in a short dialog landed right on it — awkward to hit, and
     easy to miss into a gesture. So the content stops short of the edge: the
     inset the phone reports, plus enough that a thumb is not aiming at the
     very rim. The sheet itself still reaches the bottom; only what you tap
     moves up.

     max() rather than a sum: a phone reporting no inset still needs the gap,
     and one reporting 34px does not need a full gap on top of it. */
  .sheet-body { padding-bottom: max(40px, calc(env(safe-area-inset-bottom, 0px) + 22px)); }
  .sheet-foot { padding-bottom: max(36px, calc(env(safe-area-inset-bottom, 0px) + 20px)); }
}

/* Below about 460px the three things on the top row — title, clock, profile —
   want more room than the screen has, so they all give a little rather than
   one of them being sent to a row of its own. The title scales with the
   viewport instead of stepping at a breakpoint: that is what makes the same
   phone at 100% zoom look like a wider phone at 67%, which it did not before.
   The slope is set by the narrowest common phone, 360px. It used to be 3.4vw,
   which had to cover a profile chip as wide as whatever name was signed in;
   with the name out of that row the same 360px screen has about forty more
   pixels to give, and holding the title at the old size would just be leaving
   them unused — on a 412px phone it was rendering at 14px inside a column with
   70px to spare. */
@media (max-width: 460px) {
  .topbar { column-gap: 6px; padding: 8px 9px; }
  .brand { gap: 6px; }
  .brand svg { width: 21px; height: 21px; }
  .brand h1 { font-size: clamp(11.5px, 3.35vw, 17px); }
  .topbar-right { gap: 4px; }
  .home-clock { font-size: 10.5px; padding: 4px 5px; }
  /* The name stays. An avatar alone says who you are only if you already know
     which colour you are, and this is the control you press to become someone
     else — it should say who it would be switching away from. It is capped in
     vw so a long name eats its own tail rather than the title's. */
  .profile-btn { font-size: 11.5px; padding: 3px 7px 3px 3px; gap: 4px; max-width: 30vw; }
  .profile-dot { width: 18px; height: 18px; font-size: 9.5px; }
  .menu-caret { width: 11px; height: 11px; }
}

@media (max-width: 420px) {
  .grid { grid-template-columns: 1fr; }
}

/* Below this the three labels and the cards-across picker are sharing about
   210px, and the longest one starts to ellipsis. Smaller rather than clipped:
   "Timezone…" tells you less than a small "Timezones". */
@media (max-width: 355px) {
  .viewtabs button { padding: 6px 3px; font-size: 12px; }
}

/* ------------------------------------------------------------- density */

/* Only once something has actually been chosen: with no data-density the grid
   auto-fits exactly as it always did, so a tablet that was showing two columns
   keeps showing two.

   677px is where the auto layout stops fitting a third 210px column, so below
   it the grid is always on one of the two the picker offers — which is what
   lets the picker light the segment that is really on screen. Above it there
   is room for three and nothing to choose, so it does not appear. */
@media (max-width: 677px) {
  .density { display: inline-flex; }
  /* A map has no cards to count. */
  body.view-map .density,
  body.view-clocks .density { display: none; }

  body[data-density="1"] .grid { grid-template-columns: 1fr; }
  body[data-density="2"] .grid { grid-template-columns: repeat(2, 1fr); }
}

/* What a card gives up is a question about its width, not about how many are
   beside it: two across a tablet is roomier than one across a phone. So the
   thinning starts exactly where the card drops below 210px — the narrowest
   the auto-fit layout was ever willing to go — which is 455px of screen at
   two columns.

   Pinning it to the same number the layout already uses is also what makes
   the picker honest: pressing the segment that is already lit rearranges
   nothing, because at that width the chosen layout is the one on screen. */
@media (max-width: 455px) {
  /* Every millimetre back into the card, because the coordinate below has to
     fit on one line and it is the widest fixed thing the card carries. */
  body[data-density="2"] #gridView { padding-left: 10px; padding-right: 10px; }
  body[data-density="2"] .grid { gap: 10px; }
  body[data-density="2"] .card-body { padding: 8px 7px 3px; gap: 4px; }
  body[data-density="2"] .card-name { font-size: 13.5px; }
  body[data-density="2"] .card-loc { font-size: 11.5px; }

  /* Two columns leave a card about 165px wide, and twenty-odd mono digits do
     not fit beside the copy glyph at any size worth reading. The glyph goes:
     the whole pill is the copy button and always was, so nothing is lost but
     the hint — and a coordinate broken across two lines stopped looking like
     a coordinate. */
  body[data-density="2"] .card .coord {
    /* Fluid for the same reason the title is: a 320px phone has 20px less card
       to spend and would otherwise lose a digit off the end. */
    font-size: clamp(9.6px, 2.9vw, 10.5px);
    letter-spacing: 0;
    padding: 6px 7px;
    gap: 0;
    justify-content: center;
  }
  body[data-density="2"] .card .coord svg { display: none; }
  body[data-density="2"] .card .coord span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  body[data-density="2"] .card-notes { font-size: 11.5px; -webkit-line-clamp: 2; line-clamp: 2; }
  /* Three icon buttons leave about forty pixels, which is not enough for
     "Added by Ginga-" but is plenty for "Ginga-". Dropping the preamble keeps
     the footer on one line: the two words are identical on every card in the
     book, and the name is the only part that answers anything. Who edited it
     goes the same way — that belongs to a card you are reading, not scanning. */
  body[data-density="2"] .card-actions { padding: 6px 7px 8px; }
  body[data-density="2"] .author-lead,
  body[data-density="2"] .author-extra { display: none; }
  body[data-density="2"] .act { width: 28px; height: 28px; }
  /* The clock is the first thing to go: inside a group every card repeats the
     same time, so it is the least each card is saying for the space it takes. */
  body[data-density="2"] .card-time,
  body[data-density="2"] .thumb-tag { display: none; }

  /* Two across, the card is about 170px and the category pill and the
     Collected pill were both asking for the width of a word. The tick keeps
     its corner and drops the word — the green edge and the tint underneath
     are already saying it, and the badge that has to stay legible is the one
     naming the category. The label is a text node with no element of its own,
     so there is nothing to hide but the type itself; the glyph carries its
     own width and height and survives. */
  body[data-density="2"] .card-done {
    gap: 0;
    padding: 4px;
    font-size: 0;
    letter-spacing: 0;
  }
}


/* ------------------------------------------------- place search (dialog) */

/* The dialog asks for one thing and works the rest out, so the search is the
   field with weight and the answer sits under it to be checked. */
.place-search { position: relative; }
.place-hits {
  position: absolute;
  z-index: 5;
  left: 0;
  right: 0;
  top: calc(100% + 4px);
  list-style: none;
  margin: 0;
  padding: 4px;
  max-height: 210px;
  overflow-y: auto;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.5);
}
.place-hits button {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: 2px 9px;
  width: 100%;
  padding: 7px 8px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  text-align: left;
}
.place-hits button:hover { background: var(--surface-3); }
.place-hits .clock-flag { grid-row: span 2; align-self: center; }
.hit-name { font-size: 13.5px; font-weight: 650; color: var(--text); }
.hit-where {
  grid-column: 2;
  font-size: 11.5px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.place-found {
  display: flex;
  align-items: center;
  gap: 9px;
  min-height: 34px;
  padding: 7px 10px;
  border-radius: 8px;
  font-size: 13px;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px dashed var(--border);
}
.place-found.is-set {
  color: var(--text);
  border-style: solid;
  border-color: rgba(90, 169, 255, 0.35);
  background: rgba(90, 169, 255, 0.1);
}
.place-found code { font-family: var(--mono); font-size: 11.5px; color: #9fb3cc; }

/* ------------------------------------------------------- group dropdown */

/* Its own, because a <select> hands the list to the platform and a phone
   draws that full-screen in its own colours. The panel is fixed rather than
   absolute: the dialog body scrolls, and an absolute one would be clipped by
   it or drag the form around. */
.group-select { position: relative; }
.group-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
  font-size: 14px;
  text-align: left;
}
.group-trigger:hover { border-color: var(--border-strong, var(--border)); }
.group-trigger.is-empty { color: var(--muted); }
.group-trigger svg { flex: none; opacity: 0.7; }
.group-trigger[aria-expanded="true"] { border-color: #5aa9ff; }

.group-menu {
  position: fixed;
  z-index: 80;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 5px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.55);
}
.group-items { overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }

.group-filter {
  margin-bottom: 5px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg);
  color: var(--text);
  font-size: 13px;
}
.group-filter:focus-visible { outline: 2px solid #5aa9ff; outline-offset: 1px; }

.group-item {
  padding: 9px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text);
  font-size: 13.5px;
  text-align: left;
}
.group-item:hover { background: var(--surface-3); }
.group-item.is-on { background: rgba(90, 169, 255, 0.16); font-weight: 650; }
/* Making one is a different kind of act from choosing one. */
.group-item-new { color: var(--muted); border-top: 1px solid var(--border-soft); border-radius: 0 0 7px 7px; margin-top: 3px; padding-top: 10px; }
.group-item-new:hover { color: var(--text); }

/* Two templates, side by side: JSON and CSV are the same fields in two shapes,
   so they should not look like a choice with a default. */
.template-row { display: flex; gap: 8px; flex-wrap: wrap; }
.template-row button { flex: 1 1 auto; }
