/* ==========================================================================
   Bubl — blue primary, purple secondary, gradients between the two.
   Dark is the default; light is a full re-theme, not an inversion.
   ========================================================================== */

:root {
  --blue-400:#60A5FA; --blue-500:#3B82F6; --blue-600:#2563EB; --blue-700:#1D4ED8;
  --purple-400:#C084FC; --purple-500:#A855F7; --purple-600:#9333EA;
  --magenta:#E879F9;

  --grad: linear-gradient(135deg, var(--blue-600) 0%, var(--purple-500) 100%);
  /* The bubble. Written once because three things are made of it now -- the
     one the sign-in card grows out of, the one the composer grows out of, and
     the composer itself -- and a bubble that opens into a surface of a
     different colour is two things, not one. */
  /* The colour the composer ends on, and the colour the page ground is set to
     while it is open. One value, used at both ends, so the strip an installed
     iPhone will not let us paint cannot be a different shade from the thing
     above it. Measured off the device: the sheet's own bottom edge. */
  --compose-ground: #383063;
  /* The same idea for the sign-in page, which has its own glow rising from
     below the bottom edge and so its own colour where the strip meets it.
     Worked out from that gradient rather than guessed: at the bottom edge the
     glow is 20% of the way along, and the browser composites that to exactly
     this over #05070F -- checked by replaying the same stops through a
     canvas rather than trusting the arithmetic. */
  --welcome-ground: #1E1E3D;
  /* And for a photo opened full screen. The scrim is rgba(3,6,16,.78) over the
     app's own ground, which composites to this -- the third surface that can
     cover a phone, and the one that was leaving the strip the colour of a map
     nobody could see any more. */
  --lightbox-ground: #050914;
  /* The feed drawer's own colour where it meets the bottom of the screen, and
     so the colour of the strip an installed iPhone will not let us paint. The
     drawer is glass over a moving map, so its rendered colour is not any one
     value -- it settles onto this at its bottom edge, which is what makes the
     two match by construction.
     #31355A, and the drawer is painted with it directly -- the two are the same
     declaration now, so there is nothing left to keep in step.
     Four wrong answers came before this, and every one was the same mistake:
     naming a colour that was not a constant. --panel ignored the hue layer.
     #242142 was the arithmetic for the old mix, but the glow along the bottom
     edge lit it further. #302850 was right over a dark map and sixteen units
     wrong over snow, because a tenth of the basemap showed through and was
     being saturated on the way. #2F2750 fixed that but still lost when the
     drawer was collapsed, because the body was a diagonal gradient and the top
     of it -- the only part on screen then -- was a much lighter blue-slate.
     Sitting between what the phone actually showed at the two ends, #333B57 at
     the top and #2C2851 at the bottom, so neither position moves far. */
  --drawer-ground: #31355A;

  /* --- bubble glass -------------------------------------------------------
     The material the floating chrome is made of, named so it can be asked for
     by name. Four parts: a sheen down the top-left where the light would fall
     on something round, the two brand hues at low strength so it is tinted
     rather than grey, a translucent floor dark enough to keep text legible
     over a bright map, and a lit 1px top edge. Stack them in that order --
     sheen, hue, floor -- since a colour is only valid as the last layer.
     The search bar, the map buttons and the suggestion list all take these, so
     the chrome reads as one set and stays that way when one of them changes. */
  --glass-sheen: linear-gradient(150deg,
    rgba(255,255,255,.12) 0%, rgba(255,255,255,.025) 46%, rgba(255,255,255,0) 60%);
  --glass-hue: linear-gradient(150deg,
    color-mix(in srgb, var(--primary) 16%, transparent),
    color-mix(in srgb, var(--secondary) 16%, transparent));
  --glass-floor: color-mix(in srgb, var(--panel) 84%, transparent);
  --glass-edge: rgba(255, 255, 255, .20);
  --glass-lip: inset 0 1px 0 rgba(255, 255, 255, .28);
  --glass-blur: blur(22px) saturate(165%);
  --bubl-glass:
    linear-gradient(150deg, rgba(255,255,255,.14) 0%, rgba(255,255,255,.03) 46%, rgba(255,255,255,0) 62%),
    linear-gradient(150deg,
      color-mix(in srgb, var(--primary) 34%, transparent),
      color-mix(in srgb, var(--secondary) 30%, transparent)),
    color-mix(in srgb, var(--panel) 86%, transparent);
  --grad-soft: linear-gradient(135deg, rgba(37,99,235,.16) 0%, rgba(168,85,247,.16) 100%);
  --grad-text: linear-gradient(120deg, var(--blue-400) 0%, var(--purple-400) 100%);

  --r-sm:8px; --r-md:12px; --r-lg:18px; --r-pill:999px;
  --nav-h:60px;
  --sidebar-w:392px;
  --ease:cubic-bezier(.22,.7,.28,1);
  /* For anything that fades. --ease leaves at full speed, which reads as a
     snap on a cross-fade; this one starts and finishes gently. */
  --ease-soft:cubic-bezier(.4,.04,.22,1);
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* ---- dark (default) ---- */
:root, :root[data-theme="dark"] {
  --bg:#070A12;
  --panel:#0D1322;
  --panel-2:#141C30;
  --panel-3:#1B2540;
  --ink:#E9EEFA;
  --ink-2:#96A5C4;
  --ink-3:#63718F;
  --line:#1E2740;
  --line-2:#2A3554;
  --primary:#4F8DF7;
  --primary-ink:#FFFFFF;
  --secondary:#A971F8;
  --shadow: 0 1px 2px rgba(0,0,0,.5), 0 18px 44px -20px rgba(0,0,0,.9);
  --glow: 0 0 0 1px rgba(79,141,247,.28), 0 8px 30px -8px rgba(120,90,250,.5);
  --map-ocean:#050810;
  --map-land:#1A2540;
  --map-line:#3A4A78;
  --map-label:#8092BC;
  --scrim: rgba(3,6,14,.66);
}

:root[data-theme="light"] {
  --bg:#EEF3FA;
  --panel:#FFFFFF;
  --panel-2:#F3F7FC;
  --panel-3:#E7EEF8;
  --ink:#0C1322;
  --ink-2:#4C5C77;
  --ink-3:#7A89A4;
  --line:#DDE6F1;
  --line-2:#CBD8E8;
  --primary:#2563EB;
  --primary-ink:#FFFFFF;
  --secondary:#8B3FE8;
  --shadow: 0 1px 2px rgba(16,38,76,.07), 0 16px 38px -22px rgba(16,38,76,.35);
  --glow: 0 0 0 1px rgba(37,99,235,.22), 0 10px 26px -12px rgba(124,58,237,.35);
  --map-ocean:#CFDCEE;
  --map-land:#FAFCFE;
  --map-line:#A8BDD8;
  --map-label:#748AAB;
  --scrim: rgba(20,32,58,.38);
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}
svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7;
      stroke-linecap: round; stroke-linejoin: round; }
button, input, select, textarea { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 4px; }

/* ==========================================================================
   Nav
   ========================================================================== */
.nav {
  height: var(--nav-h);
  display: flex; align-items: center; gap: 16px;
  padding: 0 16px;
  background: color-mix(in srgb, var(--panel) 86%, transparent);
  backdrop-filter: blur(18px) saturate(150%);
  border-bottom: 1px solid var(--line);
  position: relative; z-index: 40;
}
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--ink); flex: none; }
/* The app icon itself, rather than a flat gradient with a dot stuck on it.
   The icon is a lit sphere and the mark was an approximation of one; using the
   artwork means they cannot drift, and whoever redraws the icon redraws this.
   125%: the sphere is 154px inside a 192px canvas, so the picture is scaled up
   until its dark surround falls outside the circle. Measured, not guessed. */
.mark {
  width: 26px; height: 26px; border-radius: 50%;
  background: url('/icons/icon-192.png') center / 125% no-repeat, var(--grad);
  display: grid; place-items: center; position: relative;
  box-shadow: 0 4px 14px -4px rgba(124,58,237,.7);
}
/* The specular is painted into the artwork now. */
.mark i { display: none; }
.mark.small { width: 34px; height: 34px; }
.wordmark {
  font-size: 19px; font-weight: 700; letter-spacing: -.03em;
  background: var(--grad-text); -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* ---- search ---- */
.searchwrap { width: 100%; position: relative; }
.searchbox {
  display: flex; align-items: center; gap: 8px;
  height: 38px; padding: 0 8px 0 12px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  transition: border-color .18s, box-shadow .18s, background .18s;
}
.searchbox:focus-within { border-color: transparent; box-shadow: var(--glow); background: var(--panel); }
.searchbox .mag { color: var(--ink-3); flex: none; width: 16px; height: 16px; }
.searchbox input {
  flex: 1; min-width: 40px; border: 0; background: none; outline: none;
  font-size: 14px; color: var(--ink);
}
.searchbox input::placeholder { color: var(--ink-3); }

.tagchip {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  height: 24px; padding: 0 10px; border-radius: var(--r-pill);
  background: var(--grad); color: #fff;
  font-size: 12.5px; font-weight: 600; white-space: nowrap;
}
.tagchip .em { font-size: 12px; }

.suggest {
  position: absolute; top: calc(100% + 8px); left: 0; right: 0;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--shadow);
  padding: 6px; z-index: 60; max-height: min(58vh, 420px); overflow-y: auto;
}
.suggest-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 10px; border: 0; background: none; border-radius: var(--r-sm);
  cursor: pointer; text-align: left;
}
/* A lift, not a slab. --panel-2 is darker than the glass these sit on, so
   hovering sank the row instead of raising it. Same everywhere a row lives on
   one of the glass surfaces. */
.suggest-item:hover, .suggest-item.active { background: rgba(255, 255, 255, .07); }
.suggest-item .em { font-size: 16px; width: 20px; text-align: center; flex: none; }
.suggest-item .lab { font-weight: 600; }
.suggest-item .via { color: var(--ink-3); font-size: 12.5px; }
.suggest-item .cnt {
  margin-left: auto; color: var(--ink-3); font-size: 12px;
  font-variant-numeric: tabular-nums; font-family: var(--mono);
}
.suggest-empty { padding: 14px; color: var(--ink-3); text-align: center; }

/* ---- nav actions ---- */
.navactions { display: flex; align-items: center; gap: 8px; flex: none; }
.iconbtn {
  display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: var(--r-sm);
  background: transparent; border: 1px solid transparent; color: var(--ink-2);
  cursor: pointer; transition: background .16s, color .16s, border-color .16s;
}
.iconbtn:hover { background: var(--panel-2); color: var(--ink); border-color: var(--line); }
.iconbtn.tiny { width: 28px; height: 28px; }
.iconbtn.tiny svg { width: 15px; height: 15px; }
.iconbtn.on { background: var(--grad-soft); color: var(--primary); border-color: var(--line-2); }

[data-theme="dark"] .ico-sun { display: block; }
[data-theme="dark"] .ico-moon { display: none; }
[data-theme="light"] .ico-sun { display: none; }
[data-theme="light"] .ico-moon { display: block; }

.btn {
  height: 34px; padding: 0 15px; border-radius: var(--r-pill);
  border: 1px solid var(--line-2); background: var(--panel-2); color: var(--ink);
  font-size: 13.5px; font-weight: 600; cursor: pointer;
  transition: transform .12s var(--ease), filter .16s, box-shadow .16s;
}
.btn:hover { filter: brightness(1.08); }
.btn:active { transform: scale(.97); }
.btn-primary {
  background: var(--grad); color: #fff; border-color: transparent;
  box-shadow: 0 6px 18px -8px rgba(124,58,237,.8);
}
.btn.wide { width: 100%; height: 42px; font-size: 15px; }
.linkbtn {
  border: 0; background: none; color: var(--ink-3); cursor: pointer;
  font-size: 12px; padding: 2px 4px; text-decoration: underline; text-underline-offset: 2px;
}
.linkbtn:hover { color: var(--primary); }

.userchip { display: flex; align-items: center; gap: 8px; }
.avatar {
  width: 28px; height: 28px; border-radius: 50%; background: var(--grad);
  display: grid; place-items: center; color: #fff; font-size: 12px; font-weight: 700;
}
.uname { font-weight: 600; font-size: 13.5px; max-width: 110px; overflow: hidden;
         text-overflow: ellipsis; white-space: nowrap; }

/* ==========================================================================
   Stage: map + sidebar
   ========================================================================== */
.stage {
  /* The app fills its containing block and takes no measurement to do it.
     `inset: 0` against the fixed body is the window itself -- no percentage
     resolving against a parent that has to be right first, and no viewport
     unit, which on an installed iPhone measures the safe area and stops short
     of the home indicator. It used to subtract --nav-h for a bar that both
     breakpoints now hide. */
  position: absolute;
  inset: 0;
  display: grid; grid-template-columns: minmax(0,1fr) var(--sidebar-w);
}
.mapwrap { position: relative; min-width: 0; background: var(--map-ocean); }
#map { position: absolute; inset: 0; }
.maplibregl-ctrl-attrib { font-size: 10px; }
.maplibregl-ctrl-bottom-right { z-index: 5; }

.mapchrome {
  position: absolute; left: 16px; bottom: 16px; z-index: 6;
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
  pointer-events: none;
}
.statpill, .legend {
  display: flex; align-items: center; gap: 8px;
  height: 32px; padding: 0 13px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  backdrop-filter: blur(14px);
  border: 1px solid var(--line);
  font-size: 12.5px; color: var(--ink-2); box-shadow: var(--shadow);
  pointer-events: auto; white-space: nowrap;
}
.statpill .dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--grad); flex: none;
  box-shadow: 0 0 10px 1px rgba(124,58,237,.75);
}
.statpill b { color: var(--ink); font-variant-numeric: tabular-nums; }
.precision {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px 8px 13px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--panel) 92%, transparent);
  backdrop-filter: blur(14px);
  border: 1px solid var(--secondary);
  font-size: 12.5px; color: var(--ink-2); box-shadow: var(--shadow);
  pointer-events: auto; max-width: min(92vw, 420px);
}
.precision b { color: var(--ink); }
.precision button {
  flex: none; border: 0; border-radius: var(--r-pill); cursor: pointer;
  padding: 4px 12px; background: var(--grad); color: #fff;
  font-size: 12px; font-weight: 600;
}
.legend-label { font-size: 11px; color: var(--ink-3); }
.legend-ramp {
  width: 84px; height: 7px; border-radius: 4px;
  background: linear-gradient(90deg, var(--blue-600), var(--purple-500) 62%, var(--magenta));
}


/* ==========================================================================
   Sidebar feed
   ========================================================================== */
.sidebar {
  display: flex; flex-direction: column; min-height: 0;
  background: var(--panel);
  border-left: 1px solid var(--line);
  z-index: 20;
}
/* No fill of its own. It sits at the top of the drawer, and the drawer is glass
   now -- an opaque panel across the filters was a flat rectangle laid over the
   one surface that is supposed to be lit. The desktop layout had already had to
   override this to transparent; there is nothing left to override. */
.feedhead {
  padding: 10px 14px; border-bottom: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 10px; flex: none;
  background: transparent;
}
.queryrow { display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
.querychip {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  height: 30px; padding: 0 13px; border-radius: var(--r-pill);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10), rgba(255,255,255,0) 60%),
    var(--grad-soft);
  border: 1px solid var(--line);
  backdrop-filter: blur(10px) saturate(150%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.16);
  font-size: 12.5px; color: var(--ink-2); white-space: nowrap;
}
.querychip b { color: var(--ink); font-variant-numeric: tabular-nums; }
.querychip #queryText { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.querychip.pinned { border-color: var(--primary); }

.mediaseg { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; padding-bottom: 1px; }
.mediaseg::-webkit-scrollbar { display: none; }
.mediaseg button {
  display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto;
  height: 27px; padding: 0 11px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--panel-2); color: var(--ink-3);
  font-size: 12px; font-weight: 600; cursor: pointer; transition: all .15s;
}
.mediaseg button svg { width: 13px; height: 13px; }
.mediaseg button:hover { color: var(--ink); border-color: var(--line-2); }
.mediaseg button.on {
  background: var(--grad); color: #fff; border-color: transparent;
  box-shadow: 0 4px 12px -5px rgba(124,58,237,.8);
}
.mediaseg .mcount { font-family: var(--mono); font-size: 10px; opacity: .7; }
.mediaseg button.on .mcount { opacity: .85; }

.controls { display: flex; gap: 8px; align-items: center; }
.segmented {
  display: flex; flex: 1; min-width: 0; padding: 2px; gap: 2px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm);
}
.segmented button {
  flex: 1; min-width: 0; height: 26px; padding: 0 6px; border: 0; border-radius: 6px;
  background: none; color: var(--ink-3); font-size: 12px; font-weight: 600;
  cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: background .15s, color .15s;
}
.segmented button:hover { color: var(--ink); }
.segmented button.on { background: var(--grad); color: #fff; }
.select {
  height: 30px; border-radius: var(--r-sm); padding: 0 8px; flex: none;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink-2);
  font-size: 12px; cursor: pointer;
}

.feed { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 8px; }
.feed.loading { opacity: .45; }

/* Every post has an edge, all the time.
   The border only existed on hover, so at rest the feed was a column of text
   with nothing holding each post together -- and on a phone, where there is no
   hover at all, it never appeared. It is glass now: a rim you can see, lit
   along the top the way the rest of the chrome is, so a post reads as an object
   sitting on the drawer rather than as a paragraph in a list. */
.card {
  padding: 12px 13px; border-radius: var(--r-md);
  border: 1px solid rgba(255, 255, 255, .11);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07);
  cursor: pointer;
}
/* Lit, not darkened. Hovering used to swap the whole fill for a heavier one,
   which read as the card dimming just as you reached for it. It keeps its
   colour and its edge brightens instead -- the same rim, turned up, with a soft
   bloom in the brand hue behind it. */
.card {
  transition: box-shadow .18s var(--ease), border-color .18s var(--ease),
              transform .18s var(--ease);
}
.card:hover, .card:focus-visible {
  border-color: color-mix(in srgb, var(--primary) 42%, transparent);
  box-shadow:
    var(--glass-lip),
    0 0 0 1px color-mix(in srgb, var(--primary) 16%, transparent),
    0 10px 30px -18px color-mix(in srgb, var(--secondary) 70%, transparent);
}
.card:active { transform: scale(.995); }
/* Posts sit apart, not stacked. Two pixels was right when a card was an
   untitled block of text with no edge -- now that each one has a rim, they were
   close enough to read as one panel with lines ruled across it. */
.card + .card { margin-top: 12px; }
/* Who wrote it, at a size you can actually see.
   22px is smaller than a favicon, and an emoji avatar inside one is a smudge --
   which is most of what an avatar is here, since people pick one. The row it
   sits in was tight to match, so the name, the place and the time ran together
   into a single grey line. */
.card-top { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.card-av {
  width: 32px; height: 32px; border-radius: 50%; flex: none;
  display: grid; place-items: center; font-size: 15px; font-weight: 700; color: #fff;
  /* The same lit rim the rest of the glass has, so a face reads as an object
     rather than a coloured dot. */
  box-shadow: inset 0 1px 0 rgba(255,255,255,.34), 0 2px 8px -3px rgba(2,6,20,.7);
}
.card-auth { font-weight: 650; font-size: 13.5px; }
.card-meta { color: var(--ink-3); font-size: 12px; display: flex; align-items: center; gap: 5px; min-width: 0; }
.card-meta .sep { opacity: .5; }
.card-place { color: var(--primary); font-weight: 600; }
.card-body { font-size: 14px; line-height: 1.48; margin: 0 0 9px; color: var(--ink); }

.card-media {
  position: relative; margin: 0 0 9px; border-radius: var(--r-md);
  overflow: hidden; background: var(--panel-3);
  border: 1px solid var(--line);
}
.card-media img, .card-media video {
  display: block; width: 100%; height: auto; max-height: 260px;
  object-fit: cover; background: var(--panel-3);
}
.card-media video { cursor: pointer; }
.card-media .playbtn {
  position: absolute; inset: 0; display: grid; place-items: center;
  border: 0; background: linear-gradient(180deg, rgba(6,10,20,.05), rgba(6,10,20,.45));
  cursor: pointer; transition: opacity .2s;
}
.card-media .playbtn i {
  width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center;
  background: rgba(12,18,32,.62); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.28);
  box-shadow: 0 8px 24px -8px rgba(0,0,0,.7);
}
.card-media .playbtn i::after {
  content: ''; width: 0; height: 0; margin-left: 3px;
  border-left: 13px solid #fff; border-top: 8px solid transparent; border-bottom: 8px solid transparent;
}
.card-media.playing .playbtn { opacity: 0; pointer-events: none; }
.card-media .badge {
  position: absolute; top: 8px; left: 8px; z-index: 2;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 8px; border-radius: var(--r-pill);
  background: rgba(10,14,26,.6); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.16);
  color: #fff; font-size: 10.5px; font-weight: 700; letter-spacing: .04em;
}
.card-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 8px; }
.minitag {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: var(--r-pill);
  background: var(--panel-3); border: 1px solid var(--line);
  font-size: 11.5px; font-weight: 600; color: var(--ink-2); cursor: pointer;
}
.minitag:hover { color: var(--primary); border-color: var(--primary); }
/* The disc is smaller here than in the composer -- these sit in a line of body
   text rather than in a field of their own. */
.minitag .tagface { width: 14px; height: 14px; font-size: 10px; }
.minitag .tagface.letter { font-size: 8px; }
.card-foot { display: flex; align-items: center; gap: 14px; color: var(--ink-3); font-size: 12px; }
.card-foot button {
  display: inline-flex; align-items: center; gap: 5px;
  border: 0; background: none; color: inherit; cursor: pointer; padding: 2px;
  font-variant-numeric: tabular-nums;
}
.card-foot button:hover { color: var(--primary); }
.card-foot button.liked { color: var(--secondary); }
.card-foot button.liked svg { fill: currentColor; }
.card-foot svg { width: 14px; height: 14px; }

.feedstate { padding: 34px 22px; text-align: center; color: var(--ink-2); }
.feedstate .big {
  width: 46px; height: 46px; margin: 0 auto 12px; border-radius: 50%;
  background: var(--grad-soft); border: 1px solid var(--line);
  display: grid; place-items: center; font-size: 20px;
}
.feedstate h3 { margin: 0 0 6px; font-size: 15px; color: var(--ink); }
.feedstate p { margin: 0 0 14px; font-size: 13px; line-height: 1.5; }
.skeleton { padding: 12px 13px; }
.skeleton span {
  display: block; height: 9px; border-radius: 5px; margin-bottom: 8px;
  background: linear-gradient(90deg, var(--panel-2), var(--panel-3), var(--panel-2));
  background-size: 200% 100%; animation: shimmer 1.3s linear infinite;
}
@keyframes shimmer { to { background-position: -200% 0; } }

/* ==========================================================================
   Mobile: globe is the screen, feed is a bottom sheet
   ========================================================================== */
.mobile-only { display: none !important; }

@media (max-width: 900px) {
  .mobile-only { display: grid !important; }
  .stage { grid-template-columns: 1fr; }
  .wordmark { display: none; }
  .nav { gap: 8px; padding: 0 8px; }
  .searchwrap { max-width: none; }
  .searchbox { height: 34px; padding-left: 10px; }
  #projBtn { display: none; }
  .navactions { gap: 4px; }
  .btn { height: 30px; padding: 0 12px; font-size: 13px; }
  .uname { display: none; }
  /* labels stay readable: scroll the sorts rather than truncating them */
  .segmented { overflow-x: auto; scrollbar-width: none; }
  .segmented::-webkit-scrollbar { display: none; }
  .segmented button { flex: 0 0 auto; padding: 0 11px; }

  .legend { display: none; }
}

/* ==========================================================================
   Modals
   ========================================================================== */
.modal { border: 0; padding: 0; background: none; max-width: 100vw; max-height: 100vh; }
.modal::backdrop { background: var(--scrim); backdrop-filter: blur(6px); }
.modal-card {
  position: relative; width: min(94vw, 420px);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
  padding: 26px 24px 22px; display: flex; flex-direction: column; gap: 16px;
}
.modal-x { position: absolute; top: 12px; right: 12px; }
.modal-head { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.modal-head h2 { margin: 0; font-size: 19px; letter-spacing: -.02em; }
.modal-head p { margin: 0; color: var(--ink-3); font-size: 13px; }
.tabs {
  display: flex; padding: 3px; gap: 3px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm);
}
.tabs button {
  flex: 1; height: 30px; border: 0; border-radius: 6px; background: none;
  color: var(--ink-3); font-size: 13px; font-weight: 600; cursor: pointer;
}
.tabs button.on { background: var(--grad); color: #fff; }
.fields { display: flex; flex-direction: column; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 5px; position: relative; }
.field > span { font-size: 12px; font-weight: 600; color: var(--ink-2); }
.field > span em { font-weight: 400; font-style: normal; color: var(--ink-3); }
.field input, .field textarea {
  width: 100%; padding: 10px 12px; border-radius: var(--r-sm);
  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);
  outline: none; resize: vertical; font-size: 14px;
  transition: border-color .16s, box-shadow .16s;
}
.field input:focus, .field textarea:focus { border-color: transparent; box-shadow: var(--glow); }
.counter { position: absolute; right: 4px; bottom: -16px; font-size: 11px; color: var(--ink-3);
           font-family: var(--mono); }
.formerror {
  margin: 0; padding: 9px 11px; border-radius: var(--r-sm);
  background: color-mix(in srgb, #F43F5E 14%, transparent);
  border: 1px solid color-mix(in srgb, #F43F5E 38%, transparent);
  color: #FB7185; font-size: 12.5px;
}
.hint { margin: 0; text-align: center; font-size: 11.5px; color: var(--ink-3); }
.hint code { font-family: var(--mono); background: var(--panel-2); padding: 1px 5px; border-radius: 4px; }
.hint.privacy { text-align: left; padding-top: 4px; }

/* --- the tag box --------------------------------------------------------- */
/* A field with chips in it, and a list under it. Replaces the wall of every tag
   we had, which could only work while the set was fixed. */

/* The face on a tag: its emoji, or a disc with its first letter for one that
   somebody made up and so has no picture and never will. */
.tagface {
  flex: none; display: grid; place-items: center;
  width: 18px; height: 18px; border-radius: 50%;
  font-size: 12px; line-height: 1;
}
.tagface.emoji { background: none; border-radius: 0; }
.tagface.letter { color: #fff; font-size: 10px; font-weight: 800; }

.tagbox {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: 7px 8px;
  border: 1px solid rgba(255, 255, 255, .30);
  border-radius: var(--r-md);
  background: rgba(12, 16, 32, .32);
  transition: border-color .16s, box-shadow .16s;
  cursor: text;
}
.tagbox:focus-within {
  border-color: color-mix(in srgb, var(--primary) 78%, white 22%);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 34%, transparent);
}
.tagchips { display: contents; }
.tagchip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 4px 4px 8px; border-radius: var(--r-pill);
  background: var(--grad); color: #fff;
  font-size: 12.5px; font-weight: 700; max-width: 100%;
}
/* One somebody made up reads as theirs rather than as one of ours. */
.tagchip.custom {
  background: rgba(255, 255, 255, .12);
  border: 1px dashed rgba(255, 255, 255, .34);
  color: var(--ink);
}
.tagchip .lab { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tagchip .x {
  flex: none; width: 18px; height: 18px; border: 0; border-radius: 50%;
  background: rgba(0, 0, 0, .26); color: inherit;
  font-size: 14px; line-height: 1; cursor: pointer;
  display: grid; place-items: center;
}
.tagchip .x:hover { background: rgba(0, 0, 0, .44); }
#tagInput {
  flex: 1 1 120px; min-width: 100px;
  border: 0; background: none; outline: none;
  color: var(--ink); font: inherit; font-size: 14.5px; padding: 3px 2px;
}
#tagInput::placeholder { color: color-mix(in srgb, var(--ink) 45%, transparent); }
#tagInput:disabled { cursor: not-allowed; }

.tagsuggest {
  margin-top: 6px; padding: 5px;
  max-height: 232px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: color-mix(in srgb, var(--panel) 94%, transparent);
  box-shadow: var(--shadow);
}
.tagopt {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 8px 9px; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--ink); font: inherit; font-size: 13.5px;
  text-align: left; cursor: pointer;
}
.tagopt .lab { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tagopt .cnt { flex: none; font-size: 11.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.tagopt:hover, .tagopt.active { background: var(--panel-2); }
/* The one that makes a new tag is the odd one out and should look it. */
.tagopt.make .lab b { font-weight: 800; }
.tagopt.make .cnt {
  text-transform: uppercase; letter-spacing: .05em; font-weight: 800; font-size: 10px;
  color: var(--primary);
}

.tagpick { display: flex; flex-wrap: wrap; gap: 6px; max-height: 148px; overflow-y: auto; padding: 2px; }
.tagpick button {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--panel-2); color: var(--ink-2);
  font-size: 12.5px; font-weight: 600; cursor: pointer; transition: all .14s;
}
.tagpick button:hover { border-color: var(--primary); color: var(--ink); }
.tagpick button.on { background: var(--grad); color: #fff; border-color: transparent; }

/* Toasts come down from the top, not up from the bottom.
   The bottom centre is the busiest place in this app: the posts pill sits there
   on a phone and the search dock does on a desktop, so a message announcing
   "Satellite imagery" landed on top of one or the other every time. The top
   centre is empty on both -- the wordmark and the account are to the left of it
   and the map controls to the right -- and on a phone it puts the message
   directly under the search bar, which is where you were already looking. */
.toast {
  position: fixed; left: 50%; top: 22px; transform: translate(-50%, -12px);
  max-width: min(88vw, 460px); text-align: center;
  padding: 11px 18px; border-radius: var(--r-pill); z-index: 99;
  background: color-mix(in srgb, var(--panel) 94%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  border: 1px solid var(--line); box-shadow: var(--shadow);
  font-size: 13px; font-weight: 500; opacity: 0;
  transition: opacity .22s, transform .22s var(--ease);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

@media (max-width: 900px) {
  /* Into the slot the search bar used to hold.
     It sat below the bar when the bar was up here; the bar has moved down to
     the button row, so the middle of the top row is empty and a message belongs
     in it -- between the home button and the account chip, on the same line,
     the same height, with the same gaps either side. Those three numbers are
     the dock's own, so the toast lands exactly where the field used to be. */
  .toast {
    top: var(--edge-t);
    left: calc(var(--edge) + var(--tap) + 8px);
    right: calc(var(--edge) + var(--tap) + 8px);
    max-width: none;
    height: var(--tap);
    display: flex; align-items: center; justify-content: center;
    padding: 0 16px;
    transform: translateY(-8px);
    /* The material of the buttons it sits between, rather than a flatter panel
       floating over them -- and --ink rather than the softer body colour, since
       at this size it is the only thing on the line. */
    border-color: var(--glass-edge);
    background: var(--glass-sheen), var(--glass-hue), var(--glass-floor);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    box-shadow: 0 12px 34px -16px rgba(4, 8, 22, .8), var(--glass-lip);
    color: var(--ink);
    font-size: 13px; font-weight: 600;
    /* One line: the row it is in is one line. */
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .toast.show { transform: translateY(0); }
}

@media (min-width: 901px) {
  /* Centred over the map rather than over the window: the feed takes the right
     of the screen, so the middle of the window is not the middle of anything
     you are looking at, and it put the message under the map controls. Same
     expression the search dock uses, for the same reason. */
  .toast { left: calc((100% - var(--sidebar-w)) / 2); }
}

/* map popup */
.maplibregl-popup-content {
  background: var(--panel); color: var(--ink); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--shadow); padding: 10px 12px; font-size: 12.5px;
}
.maplibregl-popup-tip { border-top-color: var(--panel) !important; border-bottom-color: var(--panel) !important; }
.bublpop b { font-size: 14px; }
.bublpop .sub { color: var(--ink-3); font-size: 11.5px; margin-top: 2px; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}


/* ==========================================================================
   MapLibre chrome, themed to match the app
   ========================================================================== */

/* Zoom controls: same translucent panel as the rest of the UI, stroke icons at
   the same weight, and the brand gradient on press. */
.maplibregl-ctrl-group {
  background: color-mix(in srgb, var(--panel) 88%, transparent) !important;
  backdrop-filter: blur(14px) saturate(140%);
  border: 1px solid var(--line);
  border-radius: var(--r-sm) !important;
  box-shadow: var(--shadow) !important;
  overflow: hidden;
}
.maplibregl-ctrl-group button {
  width: 32px; height: 32px;
  background-color: transparent;
  transition: background-color .16s, background-image .16s;
}
.maplibregl-ctrl-group button + button { border-top: 1px solid var(--line); }
.maplibregl-ctrl-group button:not(:disabled):hover {
  background-color: color-mix(in srgb, var(--primary) 20%, transparent);
}
.maplibregl-ctrl-group button:not(:disabled):active { background-image: var(--grad); }
.maplibregl-ctrl-group button:focus,
.maplibregl-ctrl-group button:focus:not(:focus-visible) { box-shadow: none; }

.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon  { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='29'%20height='29'%20viewBox='0%200%2029%2029'%3E%3Cpath%20d='M14.5%209.5v10M9.5%2014.5h10'%20stroke='%234F8DF7'%20stroke-width='2'%20stroke-linecap='round'%20fill='none'/%3E%3C/svg%3E") !important; }
.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='29'%20height='29'%20viewBox='0%200%2029%2029'%3E%3Cpath%20d='M9.5%2014.5h10'%20stroke='%234F8DF7'%20stroke-width='2'%20stroke-linecap='round'%20fill='none'/%3E%3C/svg%3E") !important; }
:root[data-theme="light"] .maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon  { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='29'%20height='29'%20viewBox='0%200%2029%2029'%3E%3Cpath%20d='M14.5%209.5v10M9.5%2014.5h10'%20stroke='%232563EB'%20stroke-width='2'%20stroke-linecap='round'%20fill='none'/%3E%3C/svg%3E") !important; }
:root[data-theme="light"] .maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='29'%20height='29'%20viewBox='0%200%2029%2029'%3E%3Cpath%20d='M9.5%2014.5h10'%20stroke='%232563EB'%20stroke-width='2'%20stroke-linecap='round'%20fill='none'/%3E%3C/svg%3E") !important; }
.maplibregl-ctrl-zoom-in:active .maplibregl-ctrl-icon  { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='29'%20height='29'%20viewBox='0%200%2029%2029'%3E%3Cpath%20d='M14.5%209.5v10M9.5%2014.5h10'%20stroke='%23FFFFFF'%20stroke-width='2'%20stroke-linecap='round'%20fill='none'/%3E%3C/svg%3E") !important; }
.maplibregl-ctrl-zoom-out:active .maplibregl-ctrl-icon { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='29'%20height='29'%20viewBox='0%200%2029%2029'%3E%3Cpath%20d='M9.5%2014.5h10'%20stroke='%23FFFFFF'%20stroke-width='2'%20stroke-linecap='round'%20fill='none'/%3E%3C/svg%3E") !important; }

/* Anything MapLibre still draws with its hard-coded #333 glyph (geolocate and
   its state icons) at least stays legible against a dark panel. */
:root[data-theme="dark"] .maplibregl-ctrl-geolocate .maplibregl-ctrl-icon {
  filter: invert(1) opacity(.62);
}
:root[data-theme="light"] .maplibregl-ctrl-geolocate .maplibregl-ctrl-icon { opacity: .55; }

/* Attribution removed at the client's request; this also covers any control
   MapLibre adds internally. Delete this block and set `attributionControl:
   { compact: false }` in app.js to bring the credit line back. */
.maplibregl-ctrl-attrib,
.maplibregl-ctrl-attrib-button { display: none !important; }


/* ==========================================================================
   Liquid glass: map controls and the docked search
   ========================================================================== */

/* One stack, top-right: zoom, then projection, then imagery. */
.mapctl {
  position: absolute; top: 14px; right: 14px; z-index: 7;
  display: flex; flex-direction: column; gap: 8px;
}
.glassbtn {
  position: relative; width: 38px; height: 38px; display: grid; place-items: center;
  border-radius: 13px; cursor: pointer; overflow: hidden;
  color: var(--ink);
  border: 1px solid rgba(255, 255, 255, .16);
  background:
    linear-gradient(150deg, rgba(255,255,255,.16) 0%, rgba(255,255,255,.03) 46%, rgba(255,255,255,0) 60%),
    linear-gradient(150deg, color-mix(in srgb, var(--primary) 26%, transparent),
                            color-mix(in srgb, var(--secondary) 26%, transparent));
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  box-shadow:
    0 6px 20px -8px rgba(6, 10, 24, .7),
    inset 0 1px 0 rgba(255, 255, 255, .30),
    inset 0 -8px 14px -10px rgba(120, 90, 250, .55);
  transition: transform .15s var(--ease), box-shadow .18s, border-color .18s;
}
.glassbtn::after {           /* specular sweep across the top edge */
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 80% at 24% 0%, rgba(255,255,255,.42), rgba(255,255,255,0) 58%);
}
.glassbtn:hover { transform: translateY(-1px); border-color: rgba(255,255,255,.30); }
.glassbtn:active { transform: scale(.94); }
.glassbtn.on {
  border-color: color-mix(in srgb, var(--primary) 70%, transparent);
  box-shadow:
    0 6px 22px -6px color-mix(in srgb, var(--secondary) 70%, transparent),
    inset 0 1px 0 rgba(255,255,255,.36);
}
.glassbtn svg { width: 17px; height: 17px; position: relative; z-index: 1; }
.glassbtn .glabel {
  position: relative; z-index: 1;
  font-size: 12px; font-weight: 800; letter-spacing: .02em;
  font-family: var(--font);
}
[data-theme="light"] .glassbtn { color: #10203C; border-color: rgba(255,255,255,.55); }

/* Search, docked bottom-centre over the map. */
.searchdock {
  position: absolute; left: 50%; bottom: 22px; transform: translateX(-50%);
  width: min(560px, calc(100% - 120px)); z-index: 8;
}
.searchdock .searchbox {
  height: 48px; padding: 0 10px 0 16px; border-radius: var(--r-pill);
  border: 1px solid rgba(255,255,255,.20);
  /* Same glass recipe as the control buttons, so the chrome reads as one set. */
  /* Dark glass, lightly tinted. The bright version washed the placeholder out
     against whatever the map happened to be showing underneath. */
  background:
    linear-gradient(150deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.02) 46%, rgba(255,255,255,0) 60%),
    linear-gradient(150deg, color-mix(in srgb, var(--primary) 15%, transparent),
                            color-mix(in srgb, var(--secondary) 15%, transparent)),
    color-mix(in srgb, var(--panel) 82%, transparent);
  backdrop-filter: blur(22px) saturate(160%);
  -webkit-backdrop-filter: blur(22px) saturate(160%);
  box-shadow:
    0 12px 34px -16px rgba(4, 8, 22, .8),
    inset 0 1px 0 rgba(255, 255, 255, .28),
    inset 0 -8px 16px -12px rgba(120, 90, 250, .5);
  position: relative;
  /* The sheen animates across the pill and must be clipped by it. */
  overflow: hidden;
}
.searchdock .searchbox::before {   /* the liquid sheen */
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: radial-gradient(120% 80% at 22% -4%, rgba(255,255,255,.22), rgba(255,255,255,0) 58%);
}
.searchdock .searchbox:focus-within {
  border-color: rgba(255,255,255,.40);
  box-shadow:
    0 16px 40px -16px rgba(4, 8, 22, .85),
    0 0 0 2px color-mix(in srgb, var(--primary) 26%, transparent),
    inset 0 1px 0 rgba(255,255,255,.34);
}
.searchdock .searchbox input { font-size: 14.5px; color: var(--ink); position: relative; z-index: 1; }
/* What the map is showing, at the end of the field you would use to change it.
   Only there once something is being searched -- otherwise the placeholder is
   free to carry the number instead, which reads better than a label. */
.searchcount {
  flex: none; z-index: 1;
  font-size: 12px; font-weight: 700; color: var(--ink-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
  padding-left: 2px;
}
.searchdock .searchbox input::placeholder { color: var(--ink-2); opacity: 1; }
.searchdock .searchbox .mag { color: var(--ink-2); width: 17px; height: 17px; z-index: 1; }
.searchdock .searchbox .iconbtn { color: var(--ink-2); z-index: 1; }
.searchdock .searchbox .iconbtn:hover { background: rgba(255,255,255,.16); border-color: transparent; }
.searchdock .tagchip {
  background: rgba(255,255,255,.22); color: #fff; z-index: 1;
  border: 1px solid rgba(255,255,255,.34); backdrop-filter: blur(8px);
}


/* Suggestions open upward from the dock. */
/* Bubble glass, the same as the bar it opens from -- it used to be a flatter,
   greyer panel, which read as a different thing hanging off the chrome rather
   than part of it. */
.searchdock .suggest {
  top: auto; bottom: calc(100% + 10px);
  background: var(--glass-sheen), var(--glass-hue), var(--glass-floor);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-color: var(--glass-edge);
  box-shadow: 0 -18px 48px -20px rgba(4,8,22,.9), var(--shadow), var(--glass-lip);
}

@media (max-width: 900px) {
  /* Where these sit on a phone is decided at the bottom of this file, with
     the rest of the phone layout. This is only how big they are. */
  .searchdock .searchbox { height: var(--tap); }
  .glassbtn { width: var(--tap); height: var(--tap); border-radius: 14px; }
  #projBtn { display: grid; }
}


/* ==========================================================================
   Bubbly glass, applied to the pills and chips
   ========================================================================== */
@keyframes bublpop {
  0%   { transform: scale(.82); opacity: 0; }
  55%  { transform: scale(1.06); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes bublfloat {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-2px); }
}

/* A pill that behaves like a bubble: swells on hover, squashes on press. */
.mediaseg button, .segmented button, .minitag, .btn, .querychip, .statpill, .select {
  transition: transform .18s var(--ease), box-shadow .2s, background .18s, color .18s, border-color .18s;
}
.mediaseg button:hover:not(.on), .minitag:hover { transform: translateY(-1.5px) scale(1.04); }
.mediaseg button:active, .segmented button:active, .minitag:active, .btn:active { transform: scale(.94); }

.mediaseg button, .select {
  border-color: rgba(255,255,255,.12);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10), rgba(255,255,255,.02) 55%, rgba(255,255,255,0)),
    var(--panel-2);
  backdrop-filter: blur(10px) saturate(150%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.16);
}
.mediaseg button.on {
  animation: bublpop .34s var(--ease);
  box-shadow:
    0 6px 16px -6px color-mix(in srgb, var(--secondary) 75%, transparent),
    inset 0 1px 0 rgba(255,255,255,.36);
}
.segmented {
  background:
    linear-gradient(150deg, rgba(255,255,255,.08), rgba(255,255,255,0) 60%),
    var(--panel-2);
  backdrop-filter: blur(10px) saturate(150%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.14);
}
.segmented button.on { animation: bublpop .3s var(--ease); }
.minitag {
  background:
    linear-gradient(150deg, rgba(255,255,255,.12), rgba(255,255,255,0) 60%),
    var(--panel-3);
  border-color: rgba(255,255,255,.12);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.18);
}
.statpill, .querychip, .legend, .precision {
  border-color: rgba(255,255,255,.13);
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.18);
}
.tagchip { box-shadow: inset 0 1px 0 rgba(255,255,255,.3); }
.card-media { box-shadow: inset 0 1px 0 rgba(255,255,255,.10); }

/* ==========================================================================
   User menu
   ========================================================================== */
.navactions { margin-left: auto; }
.usermenu { position: relative; }
.userchip {
  display: flex; align-items: center; gap: 8px;
  height: 36px; padding: 0 10px 0 6px; border-radius: var(--r-pill);
  border: 1px solid rgba(255,255,255,.12); cursor: pointer;
  background:
    linear-gradient(150deg, rgba(255,255,255,.12), rgba(255,255,255,0) 60%),
    var(--panel-2);
  backdrop-filter: blur(12px) saturate(160%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.18);
  transition: transform .16s var(--ease), border-color .16s;
}
.userchip:hover { transform: translateY(-1px); border-color: var(--line-2); }
.userchip .chev { width: 15px; height: 15px; color: var(--ink-3); transition: transform .2s var(--ease); }
.userchip[aria-expanded="true"] .chev { transform: rotate(180deg); }

.dropdown {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 60;
  min-width: 194px; padding: 6px;
  background: color-mix(in srgb, var(--panel) 92%, transparent);
  backdrop-filter: blur(22px) saturate(180%);
  border: 1px solid var(--line); border-radius: var(--r-md);
  box-shadow: var(--shadow);
  animation: bublpop .22s var(--ease);
  transform-origin: top right;
}
/* Buttons and links alike. Two of these entries navigate to a page now, and an
   anchor that is not named here gets none of this -- it lays out inline, which
   is what put Settings and Admin side by side on one line with no styling. */
.dropdown button,
.dropdown a[role="menuitem"] {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 11px; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--ink); cursor: pointer;
  font-size: 13.5px; font-weight: 500; text-align: left;
  text-decoration: none;
}
.dropdown button:hover,
.dropdown a[role="menuitem"]:hover { background: rgba(255, 255, 255, .07); }
.dropdown button svg,
.dropdown a[role="menuitem"] svg { width: 16px; height: 16px; color: var(--ink-3); }
.dropdown-sep { height: 1px; background: var(--line); margin: 5px 8px; }

/* --- notifications ------------------------------------------------------- */

/* Wider than the other dropdowns, because this one carries sentences rather
   than one-word commands. Capped against the window so a phone gets the width
   it has rather than a scrollbar. */
#userDropdown { min-width: min(320px, calc(100vw - 24px)); max-width: calc(100vw - 24px); }

/* The count rides on the avatar rather than sitting beside it, so the thing you
   already look for -- your own face in the corner -- is the thing that changes.
   A pill, not a dot: how many is the whole point, and a dot that means "some"
   sends you to open the panel to find out it was one. */
.avwrap { position: relative; display: inline-grid; place-items: center; flex: none; }
.notifdot {
  position: absolute; top: -5px; right: -7px;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: 9px;
  background: linear-gradient(140deg, #FF5E7A, #E0245E);
  /* Ringed in the surface behind it, so it reads as sitting on top of the
     avatar rather than as part of it. */
  border: 2px solid var(--panel);
  color: #fff; font-size: 10.5px; font-weight: 800; line-height: 13px;
  display: grid; place-items: center;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 2px 8px -2px rgba(224, 36, 94, .8);
}
/* On the map the chip sits on glass, not on a panel. */
.searchdock .notifdot { border-color: color-mix(in srgb, var(--panel) 80%, transparent); }

.dropdown-head {
  padding: 7px 11px 5px;
  font-size: 11.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-3);
}
/* Bounded, and scrolls. A dropdown that grows with the list ends up taller than
   the window and puts Sign out somewhere off the bottom of the screen. */
.notiflist { max-height: min(46vh, 320px); overflow-y: auto; }
.notiflist .notif {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 9px;
  width: 100%; padding: 8px 10px; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--ink); text-align: left; cursor: pointer;
  font: inherit;
}
.notiflist .notif:hover, .notiflist .notif:focus-visible { background: rgba(255, 255, 255, .07); }
/* Unread carries a bar as well as a tint: a background a shade lighter than the
   one next to it is not a difference everyone can see. */
.notiflist .notif.unread {
  background: color-mix(in srgb, var(--primary) 12%, transparent);
  box-shadow: inset 2.5px 0 0 var(--primary);
}
.notiflist .notif .who { font-weight: 700; }
/* One face for the group, with a count when there are more behind it. */
.notiflist .notif-face { position: relative; flex: none; display: block; }
.notiflist .notif-face .more {
  position: absolute; right: -5px; bottom: -3px;
  min-width: 16px; height: 16px; padding: 0 3px;
  border-radius: 8px; border: 2px solid var(--panel);
  background: var(--panel-3); color: var(--ink-2);
  font-size: 9.5px; font-weight: 800; line-height: 12px;
  display: grid; place-items: center; font-variant-numeric: tabular-nums;
}

/* Follow, on someone's page. A toggle, so it says which way it is set in words
   as well as in colour -- and it keeps its width between the two labels so the
   header does not shift when you press it. */
.followbtn {
  justify-self: end; align-self: center;
  min-width: 96px; height: 34px; padding: 0 16px;
  border-radius: var(--r-pill); border: 1px solid transparent;
  background: var(--grad); color: #fff;
  font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  transition: background .16s, border-color .16s, transform .16s var(--ease);
}
.followbtn:hover { transform: translateY(-1px); }
.followbtn:active { transform: scale(.96); }
.followbtn.on {
  background: none; border-color: var(--line); color: var(--ink-2);
}
.followbtn.on:hover { border-color: #FF8A8A; color: #FF8A8A; }
.followbtn:disabled { opacity: .6; cursor: default; }
.notiflist .notif .what { font-size: 12.5px; line-height: 1.4; }
.notiflist .notif .excerpt {
  display: block; margin-top: 2px;
  font-size: 12px; color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.notiflist .notif .when { font-size: 11.5px; color: var(--ink-3); white-space: nowrap; }
.notiflist .card-av { width: 26px; height: 26px; font-size: 11px; }
.notif-empty { padding: 10px 11px 14px; font-size: 12.5px; color: var(--ink-3); }

/* ==========================================================================
   Settings
   ========================================================================== */
.settings-card { width: min(94vw, 470px); max-height: 88vh; overflow-y: auto; }
.sset { display: flex; flex-direction: column; gap: 12px; }
.sset h3 {
  margin: 0; font-size: 12px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-3);
}
.setrow { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
.setrow b { display: block; font-size: 14px; font-weight: 600; }
.setrow span { display: block; font-size: 12px; color: var(--ink-3); }
.segmented.compact { flex: 0 0 auto; width: 148px; }
.avatarrow { display: flex; gap: 14px; align-items: flex-start; }
.avatarfields { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.avatar.big {
  width: 64px; height: 64px; font-size: 26px; flex: none;
  box-shadow: inset 0 2px 0 rgba(255,255,255,.35), 0 8px 20px -8px rgba(0,0,0,.5);
}
.huerow { display: flex; flex-wrap: wrap; gap: 7px; }
.huedot {
  width: 26px; height: 26px; border-radius: 50%; cursor: pointer;
  border: 2px solid transparent; padding: 0;
  box-shadow: inset 0 2px 0 rgba(255,255,255,.4);
  transition: transform .15s var(--ease), border-color .15s;
}
.huedot:hover { transform: scale(1.14); }
.huedot.on { border-color: var(--ink); transform: scale(1.1); }


/* ==========================================================================
   Search: bubbly on hover, on press, and on the way open
   ========================================================================== */
@keyframes searchpop {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.035, 0.955); }   /* squash */
  70%  { transform: scale(0.992, 1.022); }   /* stretch */
  100% { transform: scale(1); }
}
@keyframes sheenSweep {
  0%   { transform: translateX(-120%) skewX(-18deg); opacity: 0; }
  35%  { opacity: .5; }
  100% { transform: translateX(220%) skewX(-18deg); opacity: 0; }
}
@keyframes riseUp {
  0%   { opacity: 0; transform: translateY(16px) scale(.94); }
  62%  { opacity: 1; transform: translateY(-3px) scale(1.012); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes itemFloatIn {
  0%   { opacity: 0; transform: translateY(8px) scale(.96); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

.searchdock .searchbox {
  transition: transform .22s var(--ease), box-shadow .22s, border-color .22s, background .22s;
  will-change: transform;
}
.searchdock .searchbox:hover {
  transform: translateY(-2px) scale(1.012);
  border-color: rgba(255,255,255,.30);
}
.searchdock .searchbox::after {          /* a highlight that sweeps on hover */
  content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 38%;
  border-radius: inherit; pointer-events: none; opacity: 0;
  background: linear-gradient(100deg, rgba(255,255,255,0), rgba(255,255,255,.30), rgba(255,255,255,0));
}
.searchdock .searchbox:hover::after { animation: sheenSweep 1.1s var(--ease); }
.searchdock .searchbox:focus-within { animation: searchpop .42s var(--ease); }
.searchdock .searchbox:active { transform: translateY(-1px) scale(.994); }

.searchdock .suggest {
  animation: riseUp .3s var(--ease);
  transform-origin: bottom center;
  border-radius: var(--r-lg);
  padding: 7px;
}
.searchdock .suggest-item {
  border-radius: var(--r-pill);
  animation: itemFloatIn .26s var(--ease) backwards;
  transition: transform .16s var(--ease), background .16s;
}
.searchdock .suggest-item:nth-child(1) { animation-delay: .01s; }
.searchdock .suggest-item:nth-child(2) { animation-delay: .04s; }
.searchdock .suggest-item:nth-child(3) { animation-delay: .07s; }
.searchdock .suggest-item:nth-child(4) { animation-delay: .10s; }
.searchdock .suggest-item:nth-child(5) { animation-delay: .13s; }
.searchdock .suggest-item:nth-child(n+6) { animation-delay: .16s; }
.searchdock .suggest-item:hover, .searchdock .suggest-item.active {
  transform: translateX(3px) scale(1.015);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10), rgba(255,255,255,0) 60%),
    color-mix(in srgb, var(--primary) 16%, transparent);
}
.searchdock .suggest-item .em {
  transition: transform .18s var(--ease);
}
.searchdock .suggest-item:hover .em { transform: scale(1.22) rotate(-6deg); }

/* Scrollbar inside the suggestions, to match. */
.searchdock .suggest { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--primary) 55%, transparent) transparent; }
.searchdock .suggest::-webkit-scrollbar { width: 9px; }
.searchdock .suggest::-webkit-scrollbar-track { background: transparent; margin: 6px 0; }
.searchdock .suggest::-webkit-scrollbar-thumb {
  border-radius: var(--r-pill);
  border: 2px solid transparent;
  background-clip: content-box;
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--primary) 85%, transparent),
    color-mix(in srgb, var(--secondary) 85%, transparent));
}
.searchdock .suggest::-webkit-scrollbar-thumb:hover {
  background-image: linear-gradient(180deg, var(--primary), var(--secondary));
}

@media (prefers-reduced-motion: reduce) {
  .searchdock .searchbox:hover, .searchdock .searchbox:active { transform: none; }
  .searchdock .suggest, .searchdock .suggest-item { animation: none; }
}


/* The in-view counter lives in the top bar now, beside the account. */
.navactions .querychip { max-width: 320px; }
.navactions .querychip #queryText { overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 1100px) { .navactions .querychip { max-width: 190px; } }
@media (max-width: 820px)  { .navactions .querychip, #pinBtn { display: none; } }


/* ==========================================================================
   Desktop shell: no top bar, a floating sidebar, map running underneath
   ========================================================================== */
@media (min-width: 901px) {
  .nav { display: none; }
  .stage { grid-template-columns: 1fr; }
  .mapwrap { position: absolute; inset: 0; }

  /* The map runs the full width; the sidebar floats over its right edge. */
  .sidebar {
    position: absolute;
    right: 0;
    top: var(--rail-gap);
    bottom: var(--rail-gap);
    width: var(--sidebar-w);
    border: 1px solid var(--line);
    border-right: 0;
    border-radius: var(--r-xl) 0 0 var(--r-xl);
    background: color-mix(in srgb, var(--panel) 88%, transparent);
    backdrop-filter: blur(26px) saturate(170%);
    -webkit-backdrop-filter: blur(26px) saturate(170%);
    box-shadow: -26px 0 60px -30px rgba(2, 6, 18, .85), inset 0 1px 0 rgba(255,255,255,.10);
    overflow: hidden;
    z-index: 20;
  }

  /* Controls stay clear of it. */
  .mapctl { right: calc(var(--sidebar-w) + 18px); }
  .fab { right: calc(var(--sidebar-w) + 24px); }
  .searchdock {
    left: calc((100% - var(--sidebar-w)) / 2);
    width: min(560px, calc(100% - var(--sidebar-w) - 120px));
  }

  .brandfloat {
    position: absolute; top: 16px; left: 18px; z-index: 9;
    display: flex; align-items: center; gap: 10px;
    height: 40px; padding: 0 16px 0 10px; border-radius: var(--r-pill);
    text-decoration: none;
    border: 1px solid rgba(255,255,255,.14);
    background:
      linear-gradient(150deg, rgba(255,255,255,.12) 0%, rgba(255,255,255,.02) 46%, rgba(255,255,255,0) 60%),
      color-mix(in srgb, var(--panel) 78%, transparent);
    backdrop-filter: blur(20px) saturate(170%);
    box-shadow: 0 10px 28px -14px rgba(2,6,18,.85), inset 0 1px 0 rgba(255,255,255,.22);
    transition: transform .18s var(--ease);
  }
  .brandfloat:hover { transform: translateY(-1px); }
}

@media (max-width: 900px) { .brandfloat { display: none; } }

:root { --rail-gap: 14px; --r-xl: 22px; }

/* Account row inside the sidebar. */
.accountrow {
  display: flex; align-items: center; justify-content: flex-end; gap: 8px;
  padding-bottom: 2px;
}
.accountrow .usermenu { margin-left: 0; }
.accountrow .dropdown { right: 0; top: calc(100% + 8px); }
.queryrow .querychip { flex: 1; }


/* ==========================================================================
   Post detail
   ========================================================================== */
.detail { padding: 4px 4px 18px; }
.backbtn {
  display: inline-flex; align-items: center; gap: 6px;
  margin: 4px 6px 8px; padding: 6px 12px 6px 8px;
  border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--panel-2); color: var(--ink-2);
  font-size: 12.5px; font-weight: 600; cursor: pointer;
  transition: transform .15s var(--ease), color .15s;
}
.backbtn:hover { transform: translateX(-2px); color: var(--ink); }
.backbtn svg { width: 14px; height: 14px; }
/* The post you opened is the subject of the screen, so it is lit rather than
   sunk. It filled itself with --panel-2, which was "slightly lighter" back when
   the feed sat on a near-black panel and is darker than the drawer it sits on
   now -- so opening a post dimmed it. It keeps the rim every card has, turned
   up, with a little white over the top: the same way a card looks when you
   point at it, held. */
.card.detail-card {
  cursor: default;
  background: rgba(255, 255, 255, .05);
  border-color: color-mix(in srgb, var(--primary) 38%, transparent);
  box-shadow:
    var(--glass-lip),
    0 0 0 1px color-mix(in srgb, var(--primary) 14%, transparent),
    0 10px 30px -20px color-mix(in srgb, var(--secondary) 60%, transparent);
}
.card.detail-card .card-media img, .card.detail-card .card-media video { max-height: 340px; }
.comment-head {
  padding: 12px 13px 6px; font-size: 11.5px; font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase; color: var(--ink-3);
}
.comments { display: flex; flex-direction: column; gap: 6px; }
.comment {
  display: flex; gap: 11px; padding: 12px 14px; border-radius: var(--r-md);
}
/* Lifted, never dimmed. --panel-2 is lighter than the old dark panel but darker
   than the drawer it now sits on, so hovering a comment sank it -- the same
   mistake the cards had. A little white instead, which is brighter than
   whatever is underneath it whatever that turns out to be. */
.comment { transition: background .15s; border-radius: var(--r-md); }
.comment:hover { background: rgba(255, 255, 255, .06); }
.comment .card-av { width: 30px; height: 30px; font-size: 14px; flex: none; }
.comment-meta {
  font-size: 12px; color: var(--ink-3); margin-bottom: 5px;
  display: flex; align-items: baseline; gap: 7px;
}
.comment-meta b { color: var(--ink); font-weight: 650; font-size: 13.5px; }
.comment p { margin: 0; font-size: 14px; line-height: 1.5; }
/* A reply sits under what it answers, once. One indent, with a line down the
   side so the thread is readable without a second one. */
.comment.is-reply {
  margin-left: 22px;
  padding-left: 12px;
  border-left: 2px solid color-mix(in srgb, var(--primary) 34%, transparent);
}
.replylink {
  margin-top: 4px; padding: 2px 0;
  border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 11.5px; font-weight: 700;
  color: var(--ink-3);
}
.replylink:hover { color: var(--primary); }
/* Who the box is aimed at, and the way back to aiming it at the Bubl. */
.replyto {
  display: inline-flex; align-items: center; gap: 5px; flex: none;
  padding: 0 4px 0 9px; height: 26px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--primary) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--primary) 45%, transparent);
  font-size: 11.5px; font-weight: 700; color: var(--ink);
  white-space: nowrap;
}
.replyto .x {
  width: 17px; height: 17px; border: 0; border-radius: 50%;
  background: rgba(0,0,0,.26); color: inherit;
  font-size: 13px; line-height: 1; cursor: pointer; display: grid; place-items: center;
}
.replyto .x:hover { background: rgba(0,0,0,.45); }

.replybox { display: flex; gap: 8px; padding: 12px 13px 4px; }
.replybox input {
  flex: 1; min-width: 0; height: 36px; padding: 0 13px;
  border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--panel-2); color: var(--ink); outline: none; font-size: 13.5px;
}
.replybox input:focus { border-color: transparent; box-shadow: var(--glow); }
.replybox .btn { height: 36px; }


/* ==========================================================================
   Blow a Bubl
   Same glass as the search bar and the counter, so the three read as one set.
   All the colour lives in the icon.
   ========================================================================== */
.fab {
  position: absolute; z-index: 9;
  right: 24px; bottom: 22px;
  display: flex; align-items: center; gap: 10px;
  height: 48px; padding: 0 20px 0 8px;
  border: 1px solid rgba(255, 255, 255, .20);
  border-radius: var(--r-pill);
  color: var(--ink); font-size: 14.5px; font-weight: 650; cursor: pointer;
  background:
    linear-gradient(150deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.02) 46%, rgba(255,255,255,0) 60%),
    linear-gradient(150deg, color-mix(in srgb, var(--primary) 15%, transparent),
                            color-mix(in srgb, var(--secondary) 15%, transparent)),
    color-mix(in srgb, var(--panel) 82%, transparent);
  backdrop-filter: blur(22px) saturate(160%);
  -webkit-backdrop-filter: blur(22px) saturate(160%);
  box-shadow:
    0 12px 34px -16px rgba(4, 8, 22, .8),
    inset 0 1px 0 rgba(255, 255, 255, .28),
    inset 0 -8px 16px -12px rgba(120, 90, 250, .5);
  animation: fabDrift 5.2s ease-in-out infinite;
  transition: border-color .18s, box-shadow .18s;
  will-change: transform;
}
.fab-label { position: relative; z-index: 2; }

/* the pop of colour: a small bubble with a plus in it */
.fab-icon {
  position: relative; z-index: 2; flex: none;
  width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background:
    radial-gradient(circle at 32% 26%, rgba(255,255,255,.75), rgba(255,255,255,0) 52%),
    linear-gradient(140deg, var(--blue-400) 0%, var(--blue-500) 38%,
                            var(--purple-500) 74%, var(--magenta) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.55),
    0 4px 12px -4px color-mix(in srgb, var(--secondary) 85%, transparent);
  transition: transform .22s var(--ease);
}
.fab-icon svg { width: 16px; height: 16px; stroke: #fff; stroke-width: 2.6; }
.fab:hover .fab-icon { transform: rotate(90deg) scale(1.08); }

.fab:hover {
  animation: fabWobble .62s var(--ease);
  border-color: rgba(255, 255, 255, .34);
  box-shadow:
    0 16px 40px -16px rgba(4, 8, 22, .85),
    inset 0 1px 0 rgba(255, 255, 255, .34);
}
.fab:active { animation: none; transform: scale(.94); }

@media (min-width: 901px) { .fab { right: calc(var(--sidebar-w) + 26px); } }
@media (max-width: 900px) {
  .fab { right: 44px; bottom: 74px; padding: 0 7px; }
  .fab-label { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .fab { animation: none; }
  .fab:hover .fab-icon { transform: none; }
}

/* ==========================================================================
   Topic panel: no sideways scrolling, one radius throughout
   ========================================================================== */
/* overflow-y set with overflow-x left at visible makes the browser promote the
   horizontal axis to auto, which is where the stray scrollbar came from. */
.searchdock .suggest { overflow-x: hidden; overflow-y: auto; scrollbar-gutter: stable; }
.searchdock .suggest * { min-width: 0; }
.srowgroup { padding-left: 4px; padding-right: 4px; }
.searchdock .suggest .catback,
.searchdock .suggest .freerow,
.searchdock .suggest .suggest-item { border-radius: var(--r-lg); }

/* ==========================================================================
   The in-view counter: same height and material as the search bar and the
   compose button, so the three read as one set of controls.
   ========================================================================== */
.mapchrome { left: 24px; bottom: 22px; gap: 10px; }
.statpill {
  height: 48px; padding: 0 20px; gap: 10px;
  border-radius: var(--r-pill);
  border: 1px solid rgba(255,255,255,.20);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.02) 46%, rgba(255,255,255,0) 60%),
    linear-gradient(150deg, color-mix(in srgb, var(--primary) 15%, transparent),
                            color-mix(in srgb, var(--secondary) 15%, transparent)),
    color-mix(in srgb, var(--panel) 82%, transparent);
  backdrop-filter: blur(22px) saturate(160%);
  -webkit-backdrop-filter: blur(22px) saturate(160%);
  box-shadow:
    0 12px 34px -16px rgba(4, 8, 22, .8),
    inset 0 1px 0 rgba(255, 255, 255, .28),
    inset 0 -8px 16px -12px rgba(120, 90, 250, .5);
  font-size: 14px; color: var(--ink);
}
.statpill b { font-weight: 700; color: var(--ink); }
@media (max-width: 900px) {
  .mapchrome { left: 12px; bottom: 12px; }
  .statpill { height: 40px; padding: 0 14px; font-size: 12.5px; }
}

/* A row that moves the map rather than filtering it. Warmer than the topics
   around it, so the one row in this panel with a different kind of consequence
   does not look like the rest of them. */
.placerow .srow-lab b, .phraserow .srow-lab b { font-weight: 800; }
/* The sentence row does two things at once, so it leads and it looks like it. */
.phraserow { border-color: color-mix(in srgb, var(--primary) 45%, transparent) !important; }
.searchdock .suggest .phraserow {
  background: color-mix(in srgb, var(--primary) 15%, transparent);
}
.searchdock .suggest .phraserow:hover,
.searchdock .suggest .phraserow.active {
  background: color-mix(in srgb, var(--primary) 26%, transparent);
}
.searchdock .suggest .placerow:hover,
.searchdock .suggest .placerow.active {
  background: color-mix(in srgb, var(--secondary) 16%, transparent);
  border-color: color-mix(in srgb, var(--secondary) 55%, transparent);
}
.srow-face.place {
  background: color-mix(in srgb, var(--secondary) 20%, transparent);
  border: 1px solid color-mix(in srgb, var(--secondary) 42%, transparent);
}

/* A category is the heavier of the two -- it opens another level. */
.srow-cat .srow-lab { font-size: 14px; font-weight: 700; letter-spacing: -.01em; }
.srow-cat .srow-face { font-size: 18px; }
.catback {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 9px 12px; margin-bottom: 4px;
  border: 1px solid var(--line); background: var(--panel-2); color: var(--ink);
  font-size: 13px; font-weight: 700; cursor: pointer;
  transition: transform .15s var(--ease), background .15s;
}
.catback:hover { transform: translateX(-2px); background: var(--panel-3); }
.catback svg { width: 15px; height: 15px; color: var(--ink-3); }
.catback-n { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--ink-3); font-weight: 500; }


/* ==========================================================================
   One control family
   The search bar, the in-view counter, Blow a Bubl and the map controls are
   the same glass at the same size, so the chrome reads as one set rather than
   four separate widgets. Everything below overrides the earlier per-widget
   recipes; the shared values live here in one place.
   ========================================================================== */
.glassbtn {
  width: 48px; height: 48px;
  border-radius: var(--r-pill);
  border: 1px solid rgba(255, 255, 255, .20);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.02) 46%, rgba(255,255,255,0) 60%),
    linear-gradient(150deg, color-mix(in srgb, var(--primary) 15%, transparent),
                            color-mix(in srgb, var(--secondary) 15%, transparent)),
    color-mix(in srgb, var(--panel) 82%, transparent);
  backdrop-filter: blur(22px) saturate(160%);
  -webkit-backdrop-filter: blur(22px) saturate(160%);
  box-shadow:
    0 12px 34px -16px rgba(4, 8, 22, .8),
    inset 0 1px 0 rgba(255, 255, 255, .28),
    inset 0 -8px 16px -12px rgba(120, 90, 250, .5);
}
/* The old sweep was tuned for a brighter, smaller button and read as a hotspot
   at this size. */
.glassbtn::after {
  background: radial-gradient(120% 80% at 26% 0%, rgba(255,255,255,.26), rgba(255,255,255,0) 60%);
}
.glassbtn:hover {
  transform: none;
  border-color: rgba(255, 255, 255, .34);
  box-shadow:
    0 16px 40px -16px rgba(4, 8, 22, .85),
    inset 0 1px 0 rgba(255, 255, 255, .34);
}
.glassbtn svg { width: 19px; height: 19px; }
.glassbtn .glabel { font-size: 13px; font-weight: 700; }
.mapctl { top: 22px; right: 24px; gap: 10px; }
@media (min-width: 901px) { .mapctl { right: calc(var(--sidebar-w) + 26px); } }
@media (max-width: 900px) {
  .mapctl { top: 12px; right: 12px; gap: 8px; }
  .glassbtn { width: 40px; height: 40px; }
  .glassbtn svg { width: 17px; height: 17px; }
}

/* Blow a Bubl: same type as the counter, and an icon sized to sit inside the
   pill rather than fill it. */
.fab { font-size: 14px; font-weight: 700; gap: 9px; padding: 0 20px 0 10px; }
.fab-icon { width: 28px; height: 28px; }
.fab-icon svg { width: 14px; height: 14px; stroke-width: 2.8; }
@media (max-width: 900px) { .fab { padding: 0 10px; } }


/* ==========================================================================
   Feed: room to breathe at the bottom
   The last card used to sit flush against the sidebar's rounded bottom edge,
   and mid-scroll a card was cut off by a hard line. A mask fades the scroll
   content out over the last stretch, and the extra padding lets the final card
   scroll clear of the fade instead of ending inside it.
   ========================================================================== */
/* No fade at the bottom. It used to soften the last 46px so a card would not
   be cut off by the panel's rounded edge -- but what it actually did was dim
   the last line of whatever you were reading, every time you reached the end.
   The padding does that job on its own: the final card scrolls clear of the
   edge rather than being hidden as it arrives at it.
   (It had already been turned off on phones for the same reason.) */
.feed {
  padding: 8px 8px 34px;
}
@media (max-width: 900px) {
  .feed { padding-bottom: 26px; }
}

/* Switch between "where I am" and "where the map is" in the composer. */
.wherebtn {
  margin-top: 6px; padding: 0; border: 0; background: none;
  font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
  color: var(--primary); text-decoration: underline; text-underline-offset: 3px;
}
.wherebtn:hover { color: var(--secondary); }


/* ==========================================================================
   Search dropdown: a list
   Squares before, which gave the name a tile's width to fit in -- anything past
   about ten characters was cut -- and assumed every topic has an emoji, which
   stopped being true the moment people could make their own. A row gives the
   name the whole width and carries a count and a line of context besides.
   ========================================================================== */
.srowgroup { margin: 2px 0 4px; }
.srowhead {
  padding: 6px 6px 7px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-3);
}
.srow {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center; gap: 11px;
  width: 100%; padding: 9px 11px;
  border: 1px solid transparent; border-radius: var(--r-md);
  background: none; color: var(--ink);
  cursor: pointer; text-align: left; font: inherit;
}
.srow + .srow { margin-top: 2px; }
.srow .tagface, .srow-face {
  flex: none; display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: 50%;
  font-size: 16px; line-height: 1;
}
/* An emoji needs no disc behind it; a letter is the disc. */
.srow .tagface.emoji, .srow-face.cat {
  background: var(--panel-3);
  border: 1px solid var(--line);
}
.srow .tagface.letter { color: #fff; font-size: 13px; font-weight: 800; }
.srow-text { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.srow-lab {
  font-size: 13.5px; font-weight: 650; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.srow-sub {
  font-size: 11px; color: var(--ink-3); line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.srow-n {
  font-size: 11.5px; font-weight: 700; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.srow-chev { width: 15px; height: 15px; fill: none; stroke: var(--ink-3); stroke-width: 2.2; }
/* The free-text row and the back button are rows, not tiles. */
.freerow {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 10px 12px; margin-bottom: 6px;
  border: 1px solid var(--line); background: var(--panel-2);
  color: var(--ink); cursor: pointer; text-align: left;
}
.freerow .lab { font-size: 13px; font-weight: 600; }
.freerow .via { margin-left: auto; font-size: 11px; color: var(--ink-3); }
/* A row has the width to move sideways into, which a tile in a grid cell did
   not -- so this is a nudge along the reading direction rather than a scale. */
.searchdock .suggest .srow:hover, .searchdock .suggest .srow.active {
  transform: translateX(3px);
  background: rgba(255, 255, 255, .08);
  border-color: color-mix(in srgb, var(--primary) 45%, transparent);
}
.searchdock .suggest .srow:hover .srow-chev,
.searchdock .suggest .srow.active .srow-chev { stroke: var(--primary); }


/* ==========================================================================
   Top-left: the wordmark and the account chip, side by side
   Both are the same glass as the search bar, so the corners of the map read
   as one set of controls rather than two unrelated widgets.
   ========================================================================== */
.topleft {
  position: absolute; top: 22px; left: 24px; z-index: 21;
  display: flex; align-items: center; gap: 10px;
}
/* The float is no longer positioned on its own -- .topleft places it. */
@media (min-width: 901px) {
  .brandfloat { position: static; top: auto; left: auto; }
}
@media (max-width: 900px) {
  .topleft { top: 12px; left: 12px; gap: 8px; }
  .brandfloat { display: none; }
}

.topleft .brandfloat,
.topleft .userchip,
.topleft .btn {
  height: 48px; border-radius: var(--r-pill);
  border: 1px solid rgba(255, 255, 255, .20);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.02) 46%, rgba(255,255,255,0) 60%),
    linear-gradient(150deg, color-mix(in srgb, var(--primary) 15%, transparent),
                            color-mix(in srgb, var(--secondary) 15%, transparent)),
    color-mix(in srgb, var(--panel) 82%, transparent);
  backdrop-filter: blur(22px) saturate(160%);
  -webkit-backdrop-filter: blur(22px) saturate(160%);
  box-shadow:
    0 12px 34px -16px rgba(4, 8, 22, .8),
    inset 0 1px 0 rgba(255, 255, 255, .28),
    inset 0 -8px 16px -12px rgba(120, 90, 250, .5);
  color: var(--ink);
}
.topleft .brandfloat { padding: 0 20px 0 12px; gap: 10px; }
.topleft .brandfloat .mark { width: 30px; height: 30px; }
.topleft .brandfloat .mark i { width: 9px; height: 9px; top: 5px; left: 6px; }
.topleft .userchip { padding: 0 14px 0 8px; gap: 9px; }
.topleft .userchip .avatar { width: 32px; height: 32px; font-size: 13px; }
.topleft .userchip .uname { font-size: 14px; font-weight: 700; }
.topleft .btn { padding: 0 22px; font-size: 14px; font-weight: 700; }
.topleft .brandfloat:hover,
.topleft .userchip:hover,
.topleft .btn:hover {
  transform: none;
  border-color: rgba(255, 255, 255, .34);
  box-shadow:
    0 16px 40px -16px rgba(4, 8, 22, .85),
    inset 0 1px 0 rgba(255, 255, 255, .34);
}
/* It hangs on the left edge now, so it opens from the left. */
.topleft .dropdown {
  left: 0; right: auto; top: calc(100% + 10px);
  transform-origin: top left;
}

/* The gap under the back button should read as the same gap as between tile
   rows, and its edges should line up with the grid rather than sit 4px wider. */
.searchdock .suggest .catback {
  margin: 0 4px 8px;
  width: calc(100% - 8px);
}
.searchdock .suggest .catback + .srowgroup { margin-top: 0; }


/* ==========================================================================
   Account dropdown: the same panel as the search suggestions
   Same glass, same radius, same item shape and hover, so opening either one
   feels like the same control.
   ========================================================================== */
.topleft .dropdown {
  min-width: 216px; padding: 7px;
  border-radius: var(--r-lg);
  border: 1px solid rgba(255, 255, 255, .14);
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  box-shadow: 0 18px 48px -20px rgba(4, 8, 22, .9), var(--shadow);
  animation: riseUp .3s var(--ease);
}
.topleft .dropdown button {
  border-radius: var(--r-lg);
  padding: 10px 12px;
  font-size: 13.5px; font-weight: 600;
  animation: itemFloatIn .26s var(--ease) backwards;
  transition: transform .16s var(--ease), background .16s;
}
.topleft .dropdown button:nth-of-type(1) { animation-delay: .01s; }
.topleft .dropdown button:nth-of-type(2) { animation-delay: .05s; }
.topleft .dropdown button:hover {
  transform: translateX(3px) scale(1.015);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10), rgba(255,255,255,0) 60%),
    color-mix(in srgb, var(--primary) 16%, transparent);
}
.topleft .dropdown button svg { transition: transform .18s var(--ease); }
.topleft .dropdown button:hover svg { transform: scale(1.16) rotate(-6deg); }
.topleft .dropdown-sep { margin: 6px 10px; background: rgba(255,255,255,.12); }
@media (prefers-reduced-motion: reduce) {
  .topleft .dropdown, .topleft .dropdown button { animation: none; }
  .topleft .dropdown button:hover { transform: none; }
}


/* ==========================================================================
   Feed controls: three dropdowns
   The media and sort pickers used to be segmented button strips. They grew on
   hover and the growth was clipped by the sidebar, and five sort options never
   fitted the width anyway. Dropdowns hold their size and hold their labels.
   ========================================================================== */
.controls { gap: 7px; }
.controls .select {
  flex: 1 1 0; min-width: 0; height: 32px;
  padding: 0 26px 0 10px;
  font-size: 12.5px; font-weight: 600; color: var(--ink);
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background:
    /* the chevron, drawn rather than shipped */
    linear-gradient(45deg, transparent 50%, var(--ink-3) 50%) calc(100% - 14px) 13px / 5px 5px no-repeat,
    linear-gradient(135deg, var(--ink-3) 50%, transparent 50%) calc(100% - 9px) 13px / 5px 5px no-repeat,
    var(--panel-2);
  -webkit-appearance: none; appearance: none;
  text-overflow: ellipsis;
  transition: border-color .15s, background-color .15s;
}
.controls .select:hover { border-color: color-mix(in srgb, var(--primary) 45%, transparent); }
.controls .select:focus-visible { outline: none; border-color: transparent; box-shadow: var(--glow); }
/* The list itself is painted by the OS, so give it a readable ground. */
.controls .select option { background: var(--panel); color: var(--ink); }


/* ==========================================================================
   Feed pickers
   A native select draws its own list, so these are real elements: the closed
   button matches the chrome, and the open panel matches the search dropdown
   and the account menu. Bubbly on press, and nothing glows behind them.
   ========================================================================== */
.controls { gap: 7px; position: relative; z-index: 30; }
.picker { position: relative; flex: 1 1 0; min-width: 0; }

/* The same material as the pill above them.
   These were flat --panel-2, which is a dark slab against the drawer's glass --
   so the three controls under the pill looked like a different, heavier thing
   bolted on. Same recipe as .querychip now: a sheen down the top-left, the soft
   brand gradient, a lit rim. */
.picker-btn {
  display: flex; align-items: center; gap: 6px; width: 100%;
  height: 34px; padding: 0 9px 0 11px;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10), rgba(255,255,255,0) 60%),
    var(--grad-soft);
  backdrop-filter: blur(10px) saturate(150%);
  -webkit-backdrop-filter: blur(10px) saturate(150%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.16);
  color: var(--ink); font-size: 12.5px; font-weight: 650;
  cursor: pointer; overflow: hidden;
  transition: transform .16s var(--ease), border-color .16s, box-shadow .16s;
}
.picker-val {
  flex: 1; min-width: 0; text-align: left;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.picker-chev {
  flex: none; width: 14px; height: 14px; color: var(--ink-3);
  transition: transform .22s var(--ease);
}
/* Brightened, not replaced. Both of these used to swap the whole background for
   a flat --panel-3, which threw the glass away exactly when the control was
   being used -- and is darker than the drawer besides. A little white over the
   top keeps the material and lifts it. */
.picker-btn:hover {
  border-color: color-mix(in srgb, var(--primary) 55%, transparent);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.22), inset 0 0 0 999px rgba(255,255,255,.05);
  transform: translateY(-1px);
}
.picker-btn:active { transform: scale(.95); }
/* No glow: focus and open state are shown by the border and the chevron. */
.picker-btn:focus-visible {
  outline: none;
  border-color: color-mix(in srgb, var(--primary) 75%, transparent);
}
.picker.open .picker-btn {
  border-color: color-mix(in srgb, var(--primary) 70%, transparent);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.26), inset 0 0 0 999px rgba(255,255,255,.08);
  animation: pickerPop .34s var(--ease);
}
.picker.open .picker-chev { transform: rotate(180deg); }

@keyframes pickerPop {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.05, .93); }
  68%  { transform: scale(.985, 1.03); }
  100% { transform: scale(1); }
}

.picker-menu {
  position: absolute; z-index: 40; top: calc(100% + 7px); left: 0; right: 0;
  padding: 6px;
  max-height: min(46vh, 320px); overflow-y: auto; overflow-x: hidden;
  border-radius: var(--r-lg);
  /* Bubble glass, like the search list -- see the tokens. It was a flatter,
     greyer panel, which is the same complaint as the buttons that open it. */
  border: 1px solid var(--glass-edge);
  background: var(--glass-sheen), var(--glass-hue), var(--glass-floor);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: 0 18px 48px -20px rgba(4, 8, 22, .9), var(--shadow), var(--glass-lip);
  transform-origin: top center;
  animation: riseDown .26s var(--ease);
}
@keyframes riseDown {
  from { opacity: 0; transform: translateY(-8px) scale(.94); }
  to   { opacity: 1; transform: none; }
}

.picker-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 9px 11px; border: 0; border-radius: var(--r-lg);
  background: none; color: var(--ink);
  font-size: 12.5px; font-weight: 600; text-align: left; cursor: pointer;
  animation: itemFloatIn .24s var(--ease) backwards;
  transition: transform .16s var(--ease), background .16s;
}
.picker-item:nth-child(1) { animation-delay: .01s; }
.picker-item:nth-child(2) { animation-delay: .04s; }
.picker-item:nth-child(3) { animation-delay: .07s; }
.picker-item:nth-child(4) { animation-delay: .10s; }
.picker-item:nth-child(n+5) { animation-delay: .13s; }
.picker-lab { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-note { flex: none; font-size: 11px; font-weight: 600; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.picker-item:hover, .picker-item.active {
  transform: translateX(3px) scale(1.015);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10), rgba(255,255,255,0) 60%),
    color-mix(in srgb, var(--primary) 16%, transparent);
}
.picker-item.on .picker-lab { color: var(--ink); }
.picker-item.on::after {
  content: ''; flex: none; width: 6px; height: 6px; border-radius: 50%;
  background: var(--grad);
}
@media (prefers-reduced-motion: reduce) {
  .picker-menu, .picker-item, .picker.open .picker-btn { animation: none; }
  .picker-btn:hover, .picker-item:hover, .picker-item.active { transform: none; }
}

/* ==========================================================================
   Lightbox: a photo or video at the size it deserves
   ========================================================================== */
.lightbox {
  position: fixed; inset: 0; z-index: 120;
  display: grid; place-items: center;
  padding: 40px;
  /* Settling onto --lightbox-ground at the bottom, so the strip below it is
     the same colour by construction rather than by a constant kept in step by
     hand. Same reasoning as the composer and the sign-in page. */
  background:
    linear-gradient(to bottom, rgba(0, 0, 0, 0) 84%, var(--lightbox-ground) 100%),
    rgba(3, 6, 16, .78);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  animation: lbIn .22s var(--ease);
  cursor: zoom-out;
}
@keyframes lbIn { from { opacity: 0; } to { opacity: 1; } }
.lightbox-inner {
  position: relative; max-width: min(1200px, 100%); max-height: 100%;
  display: flex; flex-direction: column; gap: 12px; align-items: center;
  cursor: default;
  animation: lbPop .3s var(--ease);
}
@keyframes lbPop {
  from { opacity: 0; transform: scale(.9); }
  to   { opacity: 1; transform: none; }
}
.lightbox img, .lightbox video {
  max-width: 100%; max-height: calc(100vh - 132px);
  /* The picture handles its own pinch and drag -- the page cannot, since the
     viewport is locked so the map can keep its own. See pinchable(). */
  touch-action: none;
  transform-origin: center center;
  will-change: transform;
  border-radius: var(--r-lg);
  box-shadow: 0 40px 90px -30px rgba(0, 0, 0, .9), inset 0 1px 0 rgba(255,255,255,.14);
  background: #05070F;
}
.lightbox-cap {
  max-width: 720px; text-align: center;
  font-size: 13.5px; line-height: 1.5; color: var(--ink-2);
}
.lightbox-cap b { color: var(--ink); font-weight: 650; }
.lightbox-x {
  position: absolute; top: -14px; right: -14px;
  width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid rgba(255,255,255,.22); cursor: pointer;
  background: color-mix(in srgb, var(--panel) 86%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  color: var(--ink);
  transition: transform .16s var(--ease), border-color .16s;
}
.lightbox-x:hover { transform: scale(1.1); border-color: rgba(255,255,255,.4); }
.lightbox-x:active { transform: scale(.92); }
.lightbox-x svg { width: 17px; height: 17px; }
@media (max-width: 900px) {
  .lightbox { padding: 16px; }
  .lightbox-x { top: 4px; right: 4px; }
}
/* The media in a card is now a control. */
.card-media { cursor: zoom-in; }


/* ==========================================================================
   The way in
   Blank, then bubbles rise, then one of them arrives in the middle carrying
   the word and opens into the form. Four phases, driven by a class on the
   root of the overlay so the whole sequence is declarative and interruptible.
   ========================================================================== */
.welcome {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  /* The keyboard takes the bottom of the screen, so the card centres in what is
     left rather than in the whole window. Without this the card stays centred
     on a window half of which is under the keyboard, the focused field ends up
     behind it, and iOS deals with that by panning the entire visual viewport --
     which slides the card up under the status bar and leaves a gap beneath it.
     Making room ourselves means there is nothing for it to pan away from.
     --kb is published by fitViewport(), which measures it from
     window.visualViewport. */
  padding-bottom: var(--kb, 0px);
  transition: padding-bottom .18s ease;
  overflow: hidden;
  /* Starts blank on purpose: the bubbles are the first thing that happens.
     The last stretch settles onto --welcome-ground, which is also what the page
     ground is set to while this is up -- so the strip an installed iPhone will
     not let us paint is the same colour as the page above it by construction
     rather than by a constant that the next change to the glow would silently
     invalidate. Same reasoning as the composer's bottom edge. */
  background:
    linear-gradient(to bottom, rgba(0, 0, 0, 0) 80%, var(--welcome-ground) 100%),
    radial-gradient(120% 90% at 50% 118%,
      color-mix(in srgb, var(--primary) 26%, transparent) 0%,
      color-mix(in srgb, var(--secondary) 16%, transparent) 34%,
      transparent 68%),
    #05070F;
  animation: welcomeIn .45s var(--ease);
}
@keyframes welcomeIn { from { opacity: 0; } to { opacity: 1; } }
.welcome.leaving { animation: welcomeOut .5s var(--ease) forwards; pointer-events: none; }
@keyframes welcomeOut {
  from { opacity: 1; }
  to   { opacity: 0; transform: scale(1.04); }
}

/* --- the field of bubbles rising past --- */
.welcome-field { position: absolute; inset: 0; pointer-events: none; }
.wb {
  position: absolute; bottom: -22vh; border-radius: 50%;
  opacity: 0;
  background:
    radial-gradient(circle at 30% 24%, rgba(255,255,255,.72), rgba(255,255,255,.10) 44%, rgba(255,255,255,0) 64%),
    linear-gradient(150deg, var(--blue-400), var(--purple-500) 72%, var(--magenta));
  box-shadow:
    inset 0 2px 6px rgba(255,255,255,.4),
    inset 0 -10px 22px -12px color-mix(in srgb, var(--secondary) 80%, transparent),
    0 10px 34px -14px color-mix(in srgb, var(--primary) 70%, transparent);
  animation: wbRise linear infinite;
  will-change: transform, opacity;
}
@keyframes wbRise {
  0%   { transform: translate3d(0, 0, 0) scale(.86); opacity: 0; }
  12%  { opacity: var(--wb-a); }
  50%  { transform: translate3d(var(--wb-dx), -62vh, 0) scale(1); }
  84%  { opacity: var(--wb-a); }
  100% { transform: translate3d(0, -124vh, 0) scale(.9); opacity: 0; }
}

/* --- the one that becomes the form --- */
.welcome-stage { position: relative; z-index: 2; display: grid; place-items: center; }
.welcome-hero {
  position: relative;
  display: grid; place-items: center;
  width: 12px; height: 12px; border-radius: 50%;
  transform: translateY(56vh);
  opacity: 0;
  overflow: hidden;
  background:
    linear-gradient(150deg, rgba(255,255,255,.14) 0%, rgba(255,255,255,.03) 46%, rgba(255,255,255,0) 62%),
    linear-gradient(150deg,
      color-mix(in srgb, var(--primary) 34%, transparent),
      color-mix(in srgb, var(--secondary) 30%, transparent)),
    color-mix(in srgb, var(--panel) 86%, transparent);
  border: 1px solid rgba(255,255,255,.22);
  backdrop-filter: blur(22px) saturate(170%);
  -webkit-backdrop-filter: blur(22px) saturate(170%);
  box-shadow:
    0 40px 90px -34px rgba(2, 6, 20, .9),
    inset 0 2px 0 rgba(255,255,255,.30),
    inset 0 -16px 34px -18px color-mix(in srgb, var(--secondary) 75%, transparent);
  will-change: transform, width, height;
}
/* The specular cap that makes it read as a bubble rather than a disc. */
.welcome-gloss {
  position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background: radial-gradient(58% 42% at 32% 16%, rgba(255,255,255,.42), rgba(255,255,255,0) 62%);
  transition: opacity .5s var(--ease);
}

/* Phase 2: it rises into the middle and settles. */
.welcome.phase-rise .welcome-hero {
  width: 232px; height: 232px;
  opacity: 1;
  transform: translateY(0);
  transition:
    transform 1.15s cubic-bezier(.16, .84, .3, 1),
    opacity .5s var(--ease),
    width .8s cubic-bezier(.34, 1.42, .5, 1),
    height .8s cubic-bezier(.34, 1.42, .5, 1);
}

/* Phase 3: it opens out into the card. */
.welcome.phase-open .welcome-hero {
  width: min(94vw, 430px);
  /* Measured from the form, so the two modes are different heights and the
     card is never taller or shorter than what is inside it. */
  height: var(--hero-h, min(86vh, 552px));
  border-radius: var(--r-xl);
  opacity: 1;
  transform: translateY(0);
  transition:
    width .72s cubic-bezier(.2, .9, .25, 1),
    height .72s cubic-bezier(.2, .9, .25, 1),
    border-radius .72s cubic-bezier(.2, .9, .25, 1);
}
.welcome.phase-open .welcome-gloss { opacity: .35; }

.welcome-word {
  position: absolute;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  opacity: 0; transform: scale(.8);
  transition: opacity .45s var(--ease), transform .45s var(--ease);
}
.welcome-word b {
  font-size: 27px; font-weight: 700; letter-spacing: -.02em; color: #fff;
  text-shadow: 0 2px 18px rgba(10, 14, 40, .6);
}
.welcome-word .mark { width: 42px; height: 42px; }
.welcome-word .mark i { width: 13px; height: 13px; top: 7px; left: 9px; }
.welcome.phase-word .welcome-word { opacity: 1; transform: none; }
.welcome.phase-open .welcome-word {
  opacity: 0; transform: scale(1.5);
  transition: opacity .26s var(--ease), transform .4s var(--ease);
}

/* Phase 4: the form itself, arriving a beat after the shape has settled. */
.welcome .authcard {
  position: relative; z-index: 2;
  width: min(94vw, 430px);
  padding: 26px 26px 22px;
  display: flex; flex-direction: column; gap: 15px;
  opacity: 0; transform: translateY(10px); pointer-events: none;
  transition: opacity .38s var(--ease) .16s, transform .44s var(--ease) .16s;
}
.welcome.phase-form .authcard { opacity: 1; transform: none; pointer-events: auto; }
.welcome .auth-head { display: flex; flex-direction: column; align-items: center; gap: 7px; text-align: center; }
.welcome .auth-head h2 { margin: 0; font-size: 21px; letter-spacing: -.02em; }
.welcome .auth-head p { margin: 0; font-size: 13px; color: var(--ink-2); }
.welcome .auth-head .mark { width: 38px; height: 38px; margin-bottom: 2px; }
.welcome .auth-head .mark i { width: 12px; height: 12px; top: 6px; left: 8px; }
.welcome .fields { display: flex; flex-direction: column; gap: 11px; }
.welcome .hint { text-align: center; }
/* Each row of the form floats in behind the one above it. */
.welcome.phase-form .authcard > * { animation: itemFloatIn .4s var(--ease) backwards; }
.welcome.phase-form .authcard > *:nth-child(1) { animation-delay: .18s; }
.welcome.phase-form .authcard > *:nth-child(2) { animation-delay: .24s; }
.welcome.phase-form .authcard > *:nth-child(3) { animation-delay: .30s; }
.welcome.phase-form .authcard > *:nth-child(4) { animation-delay: .36s; }
.welcome.phase-form .authcard > *:nth-child(n+5) { animation-delay: .42s; }

.welcome-later {
  position: absolute; bottom: 26px; left: 50%; transform: translateX(-50%);
  z-index: 3;
  padding: 9px 18px; border-radius: var(--r-pill);
  border: 1px solid rgba(255,255,255,.16);
  background: rgba(255,255,255,.05);
  color: var(--ink-2); font-size: 13px; font-weight: 600; cursor: pointer;
  opacity: 0; pointer-events: none;
  transition: opacity .4s var(--ease), background .16s, color .16s, transform .16s;
}
.welcome.phase-form .welcome-later { opacity: 1; pointer-events: auto; }
.welcome-later:hover { background: rgba(255,255,255,.1); color: var(--ink); }
.welcome-later:active { transform: translateX(-50%) scale(.95); }

@media (prefers-reduced-motion: reduce) {
  .welcome, .welcome-hero, .welcome-word, .welcome .authcard,
  .welcome.phase-form .authcard > *, .wb { animation: none !important; transition: none !important; }
  .wb { display: none; }
}


/* ==========================================================================
   Leaving the way in
   Three beats, in this order: the panel fades out, the sidebar slides in from
   the right, and the map fades up behind it. They overlap by a little so it
   reads as one move, but the order is what carries it.
   ========================================================================== */
.welcome.closing {
  pointer-events: none;
  /* Held back a beat so the field has time to empty in view. */
  animation: welcomeFade .54s var(--ease-soft) .22s forwards;
}
/* The field drains: every bubble drops its loop and rushes off the top. */
.welcome-field.rush .wb {
  animation: none !important;
  transition: transform .68s cubic-bezier(.36, .1, .18, 1), opacity .68s var(--ease-soft);
}
@keyframes welcomeFade {
  from { opacity: 1; }
  to   { opacity: 0; }
}
/* The card leads: it is what the eye is on, so it goes first and slightly up. */
.welcome.closing .authcard {
  opacity: 0; transform: translateY(-8px) scale(.99);
  transition: opacity .36s var(--ease-soft), transform .46s var(--ease-soft);
}
.welcome.closing .welcome-later { opacity: 0; transition: opacity .28s var(--ease-soft); }

/* Beat two: the sidebar arrives from the right edge it lives on. */
@media (min-width: 901px) {
  body.app-enter .sidebar {
    animation: sidebarSlide .68s cubic-bezier(.22, .82, .24, 1) .14s both;
  }
}
@keyframes sidebarSlide {
  from { transform: translateX(104%); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

/* Beat three: the map, and then the controls on top of it. `both` holds the
   from-state through the delay, so nothing flashes in before its turn. */
body.app-enter .mapwrap { animation: mainFade .66s var(--ease-soft) .34s both; }
@keyframes mainFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
body.app-enter .topleft,
body.app-enter .mapctl,
body.app-enter .mapchrome,
body.app-enter .searchdock,
body.app-enter .fab { animation: chromeIn .52s var(--ease-soft) both; }
@keyframes chromeIn {
  from { opacity: 0; transform: translateY(8px) scale(.99); }
  to   { opacity: 1; transform: none; }
}
body.app-enter .topleft    { animation-delay: .54s; }
body.app-enter .mapctl     { animation-delay: .62s; }
body.app-enter .searchdock { animation-delay: .68s; }
body.app-enter .mapchrome  { animation-delay: .74s; }
body.app-enter .fab        { animation-delay: .80s; }

@media (prefers-reduced-motion: reduce) {
  .welcome.closing, .welcome.closing .authcard,
  body.app-enter .sidebar, body.app-enter .mapwrap, body.app-enter .topleft,
  body.app-enter .mapctl, body.app-enter .mapchrome, body.app-enter .searchdock,
  body.app-enter .fab { animation: none !important; transition: none !important; }
}

/* The lockup: mark and wordmark together, in the bubble and on the card. */
.auth-brand { display: flex; align-items: center; gap: 9px; }
.auth-brand .wordmark { font-size: 22px; }
.welcome-word .auth-brand .wordmark { font-size: 25px; }
.welcome .auth-head .auth-brand { margin-bottom: 3px; }

/* Pop one on hover. The splash is made of separate elements rather than the
   bubble itself, because the bubble is mid-keyframe and cannot also be
   transitioned. Pointer events are re-enabled on the bubbles alone -- the field
   stays transparent to clicks so it never eats one meant for the form. */
.welcome-field .wb { pointer-events: auto; cursor: pointer; }
.wb.popped { animation: none; opacity: 0; pointer-events: none; }
/* The droplets it scatters. */
.wb-drop {
  position: absolute; border-radius: 50%; pointer-events: none;
  background: linear-gradient(150deg, var(--blue-400), var(--purple-500));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.6);
  animation: wbDrop .5s cubic-bezier(.25, .7, .35, 1) forwards;
}
@keyframes wbDrop {
  0%   { transform: translate(0, 0) scale(1); opacity: .9; }
  100% { transform: translate(var(--dx), var(--dy)) scale(.2); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .wb-drop { display: none; }
}

/* ==========================================================================
   Switching between signing in and creating an account
   Only what actually differs moves. The first version faded the whole card out
   and back in, which is why the form appeared to vanish and return: almost
   nothing on it changes between the two modes, so almost nothing should
   animate. What changes is the heading, which fields are present, and the
   button's wording.
   ========================================================================== */
/* While the card is opening, its height is part of the reveal and moves with
   it. Once open, a height change is an adjustment -- a field appearing, an
   error showing -- and should just keep up. */
.welcome.settled .welcome-hero { transition: height .44s var(--ease-soft) !important; }

/* The heading is the only text that changes, so it is the only text that
   cross-fades -- briefly, and in the direction the card is about to grow. */
.welcome .auth-head {
  transition: opacity .30s var(--ease-soft), transform .40s var(--ease-soft);
}
.welcome .authcard.swap-out .auth-head {
  opacity: 0; transform: translateY(calc(var(--swap-dir, 6px) * 0.7));
}

/* A field that is not in this mode collapses rather than disappearing. It
   keeps its place in the flow until it has finished closing, and the negative
   margin cancels the row gap it would otherwise still take up. */
.welcome .field {
  overflow: hidden;
  max-height: 140px;
  transition: max-height .46s var(--ease-soft), opacity .34s var(--ease-soft),
              margin-bottom .46s var(--ease-soft), transform .42s var(--ease-soft);
}
.welcome .field.collapsed {
  max-height: 0;
  opacity: 0;
  margin-bottom: -11px;            /* cancels the .fields gap */
  transform: translateY(-4px);
  pointer-events: none;
}

/* The button does not move; only its wording changes. */
/* The button does not move and barely dims: only its wording changes, and
   blanking it made the whole card look like it was reloading. */
.welcome #authSubmit { transition: opacity .30s var(--ease-soft); }
.welcome .authcard.swap-out #authSubmit { opacity: .72; }
.welcome .hint { transition: opacity .30s var(--ease-soft); }
.welcome .authcard.swap-out .hint { opacity: 0; }

/* The tab pill slides between the two, instead of the highlight teleporting. */
.welcome .tabs { position: relative; isolation: isolate; }
.welcome .tabs::before {
  content: ''; position: absolute; z-index: -1;
  top: 3px; bottom: 3px; left: 3px;
  width: calc(50% - 3px);
  border-radius: calc(var(--r-md) - 2px);
  background: var(--grad);
  box-shadow: 0 4px 14px -6px color-mix(in srgb, var(--secondary) 90%, transparent);
  transition: transform .38s cubic-bezier(.34, 1.3, .5, 1);
}
.welcome .tabs.on-register::before { transform: translateX(100%); }
.welcome .tabs button.on { background: none; box-shadow: none; }
.welcome .tabs button { transition: color .24s var(--ease); position: relative; }

@media (prefers-reduced-motion: reduce) {
  .welcome.settled .welcome-hero, .welcome .auth-head, .welcome .field,
  .welcome #authSubmit, .welcome .hint, .welcome .tabs::before {
    transition: none !important;
  }
}

/* Signing out: the entrance in reverse. */
body.app-leave .topleft,
body.app-leave .mapctl,
body.app-leave .mapchrome,
body.app-leave .searchdock,
body.app-leave .fab { animation: chromeOut .34s var(--ease-soft) both; }
@keyframes chromeOut {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(8px) scale(.99); }
}
@media (min-width: 901px) {
  body.app-leave .sidebar { animation: sidebarOut .56s cubic-bezier(.34, .06, .26, 1) .12s both; }
}
@keyframes sidebarOut {
  from { transform: none; opacity: 1; }
  to   { transform: translateX(104%); opacity: 0; }
}
body.app-leave .mapwrap { animation: mainFadeOut .50s var(--ease-soft) .20s both; }
@keyframes mainFadeOut {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  body.app-leave .topleft, body.app-leave .mapctl, body.app-leave .mapchrome,
  body.app-leave .searchdock, body.app-leave .fab, body.app-leave .sidebar,
  body.app-leave .mapwrap { animation: none !important; }
}


/* ==========================================================================
   The 3D and satellite toggles, switched on
   The active state used to sit in a purple pool of its own light. The border
   and the fill say "on" perfectly well.
   ========================================================================== */
.glassbtn.on {
  border-color: color-mix(in srgb, var(--primary) 70%, transparent);
  box-shadow:
    0 12px 34px -16px rgba(4, 8, 22, .8),
    inset 0 1px 0 rgba(255, 255, 255, .36);
}
/* The same purple bloom, inside the button this time, on every one of them. */
.glassbtn {
  box-shadow:
    0 12px 34px -16px rgba(4, 8, 22, .8),
    inset 0 1px 0 rgba(255, 255, 255, .28);
}

/* ==========================================================================
   Mobile: map first, everything else floating on top of it
   No nav bar. The wordmark and the account chip stack in the top-left corner
   with the in-view counter under them, the search sits at the bottom centre,
   and the feed is a full-width drawer you swipe in from the right.
   ========================================================================== */

/* The counter lives in the top-left stack in the DOM now. On desktop it is
   still pinned to the bottom-left of the viewport -- fixed rather than
   absolute, because .topleft is its offset parent and is up in the corner. */
@media (min-width: 901px) {
  .mapchrome { position: fixed; left: 24px; bottom: 22px; top: auto; }
}

@media (max-width: 900px) {
  /* The nav is gone entirely: its brand and its feed button both have better
     homes now. */
  .nav { display: none; }
  .stage { grid-template-columns: 1fr; }

  .topleft {
    top: 12px; left: 12px; right: 12px;
    flex-direction: column; align-items: flex-start; gap: 8px;
  }
  .brandfloat { display: flex; }
  .topleft .brandfloat,
  .topleft .userchip,
  .topleft .btn { height: 42px; }
  .topleft .brandfloat { padding: 0 16px 0 10px; }
  .topleft .brandfloat .wordmark { font-size: 17px; }
  .topleft .userchip { padding: 0 13px 0 7px; }
  .topleft .userchip .avatar { width: 28px; height: 28px; }

  /* Third in the column, not pinned to a corner. */
  .mapchrome { position: static; }
  .statpill { height: 38px; padding: 0 14px; font-size: 12px; }

  /* Clear of the stack above it. */
  .mapctl { top: 12px; right: 12px; }

  /* Bottom centre, clear of the compose button. */
  .searchdock {
    left: 50%; right: auto; transform: translateX(-50%);
    width: min(560px, calc(100% - 92px));
    bottom: 18px;
  }
  .searchdock .searchbox { height: 46px; }

  .fab { right: 14px; bottom: 18px; }

  /* All the way across: a feed you can actually read. */
  .sidebar {
    width: 100%;
    border-radius: 0;
    border-left: 0;
  }

}

/* Those two used to be squeezed into a nav bar. In the top-left stack there is
   room for the names, and a nameless pill next to a nameless chip tells you
   nothing. */
@media (max-width: 900px) {
  .topleft .wordmark { display: inline; }
  .topleft .uname { display: inline; max-width: 34vw; }
}


/* ==========================================================================
   Add to home screen
   A sheet above the search dock, in the same glass as everything else, shown
   once and then never again unless it is asked for from Settings.
   ========================================================================== */
.installcard {
  position: fixed; z-index: 60;
  left: 12px; right: 12px; bottom: 76px;
  display: flex; align-items: center; gap: 12px;
  padding: 13px 14px;
  border-radius: var(--r-lg);
  border: 1px solid rgba(255, 255, 255, .20);
  background:
    linear-gradient(150deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.02) 46%, rgba(255,255,255,0) 60%),
    linear-gradient(150deg, color-mix(in srgb, var(--primary) 15%, transparent),
                            color-mix(in srgb, var(--secondary) 15%, transparent)),
    color-mix(in srgb, var(--panel) 88%, transparent);
  backdrop-filter: blur(22px) saturate(160%);
  -webkit-backdrop-filter: blur(22px) saturate(160%);
  box-shadow: 0 18px 46px -20px rgba(4, 8, 22, .9), inset 0 1px 0 rgba(255,255,255,.26);
  /* Comes up from under the search bar rather than fading in on top of it. */
  opacity: 0; transform: translateY(14px) scale(.98);
  transition: opacity .32s var(--ease), transform .38s var(--ease);
}
.installcard.in { opacity: 1; transform: none; }

.install-mark {
  position: relative; flex: none;
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--grad);
  box-shadow: inset 0 2px 0 rgba(255,255,255,.4),
              0 6px 18px -6px color-mix(in srgb, var(--secondary) 85%, transparent);
}
.install-mark i {
  position: absolute; top: 8px; left: 9px;
  width: 12px; height: 12px; border-radius: 50%; background: rgba(255,255,255,.62);
}

.install-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.install-body b { font-size: 13.5px; font-weight: 700; color: var(--ink); }
.install-body span { font-size: 12px; line-height: 1.45; color: var(--ink-2); }
.install-body span b { font-weight: 700; color: var(--ink); font-size: 12px; }

/* The iOS share glyph, drawn rather than shipped: a box with an arrow out of
   the top, which is the only way to name the button Apple gives no API for. */
.ios-share {
  display: inline-block; vertical-align: -3px;
  width: 13px; height: 15px; margin: 0 1px;
  border: 1.5px solid var(--ink-2);
  border-top: 0;
  border-radius: 3px;
  position: relative;
}
.ios-share::before {
  content: ''; position: absolute; left: 50%; top: -7px;
  width: 1.5px; height: 9px; background: var(--ink-2); transform: translateX(-50%);
}
.ios-share::after {
  content: ''; position: absolute; left: 50%; top: -7px;
  width: 6px; height: 6px;
  border-left: 1.5px solid var(--ink-2); border-top: 1.5px solid var(--ink-2);
  transform: translateX(-50%) rotate(45deg); transform-origin: top left;
  margin-left: -0.75px;
}

.install-actions { display: flex; flex-direction: column; align-items: stretch; gap: 5px; flex: none; }
.install-actions .btn { height: 34px; padding: 0 16px; font-size: 13px; }
.install-actions .linkbtn { font-size: 12px; color: var(--ink-3); }
.install-actions .linkbtn:hover { color: var(--ink); }

.install-x {
  position: absolute; top: -9px; right: -9px;
  width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  border: 1px solid rgba(255,255,255,.2);
  background: color-mix(in srgb, var(--panel) 94%, transparent);
  backdrop-filter: blur(12px);
  color: var(--ink-2);
  transition: transform .16s var(--ease), color .16s;
}
.install-x:hover { transform: scale(1.1); color: var(--ink); }
.install-x svg { width: 13px; height: 13px; }

/* Wider than a phone, it sits in the corner rather than spanning the screen. */
@media (min-width: 620px) {
  .installcard { left: auto; right: 22px; width: 380px; bottom: 22px; }
}
@media (prefers-reduced-motion: reduce) {
  .installcard { transition: none; }
}


/* ==========================================================================
   Phones: the corrections
   ========================================================================== */

/* Safari's URL bar makes 100vh taller than what you can actually see, so the
   map ran under it. dvh is the viewport as it currently is, and the
   -webkit-fill-available line is the fallback for the iOS versions that
   predate it. */
html { height: 100%; }
/* The document itself must never scroll. Without this, focusing the search
   field lets iOS scroll the whole page up to make room for the keyboard and
   the map goes with it. Pinning the body is enough; the root element is left
   alone, because a fixed <html> is a strange thing to hand a browser. */
html { overflow: hidden; overscroll-behavior: none; }
/* ...except on the pages that are documents rather than a map. The map pins
   the root so focusing the search field cannot scroll the whole app away; a
   settings page has to scroll, and on iOS a root with overflow:hidden means a
   finger drag does nothing at all. The class is on <html> because that is the
   element being undone -- a class on <body> cannot reach it. */
html.sitepage, html.sitepage body {
  position: static;
  height: auto;
  overflow: visible;
  overscroll-behavior: auto;
}
body {
  /* The app is the initial containing block, and nothing else.
     This used to be `height: 100dvh`, and that is the strip of page background
     along the bottom of an installed iPhone: with viewport-fit=cover the
     window covers the whole display, but the viewport units iOS reports in a
     standalone app measure the *safe* area, which stops above the home
     indicator. So the body came up short and its own background showed
     underneath -- which is why the gap moved but never went away when the
     background colour was changed.
     `inset: 0` on a fixed element is the containing block itself, which is the
     window, home indicator and all. No viewport units, so nothing to disagree
     about. Its height is definite either way, which is what .stage needs. */
  position: fixed;
  inset: 0;
  /* height:auto so the insets decide the box. `html, body { height: 100% }` at
     the top of this file would otherwise over-constrain it -- given top, bottom
     and a height, the height wins and the insets are quietly discarded. */
  height: auto;
  overflow: hidden;
  overscroll-behavior: none;
}


@media (max-width: 900px) {
  /* The feed scrolls; the sheet is dragged by its grab bar, which opts out of
     browser panning itself. */
  .sidebar, .feed { touch-action: pan-y; }

  .installcard { bottom: calc(136px + var(--kb, 0px) + env(safe-area-inset-bottom, 0px)); }
  .topleft { top: max(12px, var(--safe-t, 0px), env(safe-area-inset-top, 0px)); }

  /* The list drops from a bar at the top, so its ceiling is the room below the
     bar -- which the keyboard eats into from underneath. --suggest-max is
     measured in JS; the min() is the fallback before the first measurement. */
  .searchdock .suggest {
    max-height: min(var(--suggest-max, 46vh), 62vh);
  }
  /* The width of the phone, not the width of the field -- the list should sit
     on --edge at both ends like every other row. The field is a positioned
     element now (it rides the button row), so the offsets that do that live
     with the rule that positions it, further down. */

  /* One per line now, so there is no column count to fix -- the rows take the
     width the list gained and give it to the name. */
  .searchdock .srow { padding: 10px 11px; }
  .searchdock .srow-lab { font-size: 14px; }
}

/* The wordmark is a desktop luxury: on a phone it takes a row of map to say
   something the icon already says. Only the mark travels, and it sits at the
   head of the control stack rather than alone in the opposite corner. */
.brandmini { display: none; }
@media (max-width: 900px) {
  .brandmini {
    display: grid; place-items: center;
    width: 40px; height: 40px; flex: none;
    text-decoration: none;
  }
  .brandmini .mark { width: 32px; height: 32px; }
  /* Sized and dressed like the account chip opposite it, so the two ends of
     the bar carry the same weight and the row reads as one piece of chrome. */
  .searchdock .brandmini {
    width: var(--tap); height: var(--tap); border-radius: 50%;
    border: 1px solid rgba(255,255,255,.12);
    background:
      linear-gradient(150deg, rgba(255,255,255,.12), rgba(255,255,255,0) 60%),
      var(--panel-2);
    backdrop-filter: blur(12px) saturate(160%);
    box-shadow: inset 0 1px 0 rgba(255,255,255,.18);
  }
  .searchdock .brandmini .mark { width: 36px; height: 36px; }
  .searchdock .brandmini .mark i { width: 11px; height: 11px; top: 6px; left: 8px; }
  .brandmini .mark i { width: 10px; height: 10px; top: 5px; left: 7px; }
  .brandmini:active { transform: scale(.92); }
  /* Which leaves the account chip and the counter alone on the left. */
  .topleft .brandfloat { display: none; }
}


/* ==========================================================================
   iPhone: the status bar, the home indicator, and the notch
   With viewport-fit=cover the page owns the whole screen, which is what we
   want for the map -- and it means everything readable has to keep itself out
   from under the hardware. The feed was running under the clock.
   ========================================================================== */
@media (max-width: 900px) {
  /* The sheet stops short of the status bar by design, so it needs no inset at
     the top -- that was 47 points of nothing between the grip and the first
     Bubl. The bottom inset is on .sidebar, with the sheet layout. */
  .feed { padding-bottom: 26px; }

  /* Post detail and modals sit above the same hardware. */
  .modal-card { padding-top: max(18px, env(safe-area-inset-top, 0px)); }
}

/* What sits behind the page, and deliberately a literal rather than a variable.
   iOS paints two things from it: any strip of screen outside the viewport, and
   the tint of Safari's own bars. In light mode --bg is #EEF3FA and --panel is
   white, which is why an iPhone showed a bright band along the bottom while the
   map above it was night-time satellite -- so this cannot follow the theme.
   The value is the drawer's own navy. Where a strip of it does show on an
   installed iPhone, it sits directly under the drawer and reads as part of it
   rather than as a line ruled across the bottom of the screen. Pure black was
   worse: darker than both the panel above it and the sky. */
/* The page ground is the drawer's colour, because on a phone the drawer is what
   reaches the bottom of the screen -- and the strip an installed iPhone will not
   let us paint takes this. One token, so the two cannot drift. */
html, body { background-color: var(--drawer-ground); }


/* ==========================================================================
   Phones: one column of controls, all of it the same size
   The account chip, the mark and the four map buttons are one stack down the
   right-hand edge, and every one of them is a 40px circle -- a row that is
   nearly the same size reads worse than one that is obviously different.
   ========================================================================== */
@media (max-width: 900px) {
  .mapctl .usermenu { position: relative; flex: none; }

  .mapctl .userchip,
  .mapctl #authBtn,
  .brandmini,
  .mapctl .glassbtn {
    width: 40px;
    height: 40px;
    flex: none;
  }
  .mapctl .userchip {
    padding: 0;
    gap: 0;
    justify-content: center;
    border-radius: 50%;
  }
  /* The avatar is the control on a phone; the name and the chevron go. */
  .mapctl .userchip .uname,
  .mapctl .userchip .chev { display: none; }
  .mapctl .userchip .avatar { width: 32px; height: 32px; font-size: 13px; }

  /* Signed out, the same footprint rather than a wide pill in a column. */
  .mapctl #authBtn {
    padding: 0;
    border-radius: 50%;
    font-size: 11px;
    font-weight: 700;
  }

  /* It hangs off the right edge now, so it opens from the right. */
  .mapctl .dropdown {
    left: auto;
    right: 0;
    top: calc(100% + 10px);
    transform-origin: top right;
  }

  /* Which leaves the counter alone in the opposite corner. */
  .topleft { gap: 0; }
}

/* ==========================================================================
   ?diag=1 -- what the device thinks the viewport is
   Several of these numbers only ever misbehave on a phone, which is the one
   place a console is not to hand. The safe-area probes are read from real
   elements because env() cannot be read back out of a computed style.
   ========================================================================== */
.diag {
  position: fixed; z-index: 300; inset: auto 8px 8px 8px;
  max-height: 62vh; overflow: auto;
  padding: 12px 14px;
  border-radius: var(--r-lg);
  border: 1px solid rgba(255,255,255,.2);
  background: rgba(6, 10, 24, .94);
  backdrop-filter: blur(18px);
  color: #DDE6FA;
  box-shadow: 0 20px 50px -20px rgba(0,0,0,.9);
}
.diag h4 {
  margin: 0 0 8px; font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: #8FA3C8;
}
.diag pre {
  margin: 0; font-family: var(--mono); font-size: 11.5px; line-height: 1.55;
  white-space: pre-wrap; word-break: break-word;
}
.diag-x {
  position: absolute; top: 6px; right: 8px;
  border: 0; background: none; color: #8FA3C8;
  font-size: 20px; line-height: 1; cursor: pointer; padding: 4px 6px;
}
/* The four probes: fixed boxes whose size *is* the inset, since env() values
   cannot be read back from a computed style. */
.safe-probe { position: fixed; visibility: hidden; pointer-events: none; }
#probeTop    { top: 0; left: 0; height: env(safe-area-inset-top, 0px); width: 1px; }
#probeBottom { bottom: 0; left: 0; height: env(safe-area-inset-bottom, 0px); width: 1px; }
#probeLeft   { top: 0; left: 0; width: env(safe-area-inset-left, 0px); height: 1px; }
#probeRight  { top: 0; right: 0; width: env(safe-area-inset-right, 0px); height: 1px; }


/* ==========================================================================
   Phones: two controls, not six
   A map is touched directly -- pinch zooms, drag pans -- so the buttons around
   it should be the ones you cannot do with a finger. That is the account, and
   the compose button. Which basemap, flat or globe, and the way home are
   occasional, so they fold behind one control and the map gets its edge back.
   ========================================================================== */

/* On a desktop the group is not a box at all: its children lay out in .mapctl
   exactly as they did before it existed. */
.mapctl-group { display: contents; }

@media (max-width: 900px) {
  /* Pinch is the zoom control on a touchscreen, and it is always available. */
  #zoomInBtn, #zoomOutBtn2 { display: none; }

  .mapctl-group {
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: flex-end;
    /* Collapsed: laid out, measured, and simply not there. Animating to a
       fixed height would either clip the contents or leave a gap. */
    max-height: 0;
    opacity: 0;
    overflow: hidden;
    pointer-events: none;
    transform: translateY(-6px);
    transition: max-height .3s var(--ease), opacity .22s var(--ease),
                transform .28s var(--ease);
  }
  .mapctl-group.open {
    max-height: 220px;
    opacity: 1;
    pointer-events: auto;
    transform: none;
  }

  /* The toggle turns into a close affordance while the group is out. */
  #mapMoreBtn svg { transition: transform .3s var(--ease); }
  #mapMoreBtn[aria-expanded="true"] svg { transform: rotate(90deg); }
  #mapMoreBtn[aria-expanded="true"] {
    border-color: color-mix(in srgb, var(--primary) 70%, transparent);
  }
}

@media (prefers-reduced-motion: reduce) {
  .mapctl-group { transition: none; }
}


/* ==========================================================================
   Compose, in the column
   On a phone it joins the stack on the right edge at the same 40px as
   everything else. It keeps its gradient while the rest are glass, because it
   is the one thing in that column you press on purpose -- a row of six
   identical circles would hide it.
   ========================================================================== */
@media (max-width: 900px) {
  .mapctl .fab {
    position: static;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    justify-content: center;
    flex: none;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: none;
    /* The drift belongs to a button floating over the map. In a column it just
       makes the row jitter. */
    animation: none;
  }
  .mapctl .fab .fab-label { display: none; }
  .mapctl .fab .fab-icon {
    width: 40px;
    height: 40px;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,.55),
      0 6px 18px -6px color-mix(in srgb, var(--secondary) 85%, transparent);
  }
  .mapctl .fab .fab-icon svg { width: 18px; height: 18px; }
  .mapctl .fab:hover { animation: none; box-shadow: none; }
  .mapctl .fab:active { transform: scale(.92); }
  .mapctl .fab:hover .fab-icon { transform: none; }
}


/* ==========================================================================
   Phones, rebuilt around the thumb
   ==========================================================================
   Three things decided this layout.

   The reachable part of a phone held in one hand is an arc up from the bottom
   corner, so what you press often lives at the bottom: compose, and the sheet.
   The top-right is the worst place on the screen and had the most in it.

   Search is a destination, not a control -- you tap it, a keyboard covers half
   the screen, you leave again. It does not need to be under the thumb, and at
   the top it stops competing with everything else for the bottom edge.

   And the feed was a side drawer, which is a desktop idea in mobile clothes:
   all-or-nothing, and it hides the map it is describing. A bottom sheet with
   detents lets you hold both at once, which is the whole point of a feed that
   follows the map.
   ========================================================================== */
@media (max-width: 900px) {
  /* Every floating control is this big and this far from the edge. They read
     as one set of chrome laid over the map, so a button that is four pixels
     wider than its neighbour is the only thing anybody sees. */
  :root {
    --tap: 44px;      /* also the smallest comfortable touch target */
    --edge: 12px;
    /* How much of the drawer stays on screen when it is down. Not a peek --
       nothing is legible in 18px -- but enough that its rounded top and its
       border are visible, so the page ground below them, which is the same
       colour as the drawer, reads as the drawer carrying on rather than as a
       band.
       34, not 18: iOS keeps the bottom of the screen for its own home-indicator
       swipe, so most of an 18px lip was inside a strip where the first upward
       drag never reaches us. Kept in step with SHEET_LIP in app.js. */
    --sheet-lip: 34px;

    /* The gap at the top and bottom is the same gap as at the sides -- unless
       there is a status bar or a home indicator in the way, in which case it
       is the room the system asked for and not a pixel more.
       max(), not addition: the inset already includes its own clearance, so
       adding the edge gap on top of it is what made the top and bottom read as
       loose next to the sides. */
    /* Both sources, as peers -- never env() as the fallback of --safe-t.
       A fallback only applies while the custom property is *undefined*, and
       publishInsets() defines both on the first frame. So `var(--safe-t,
       env(...))` reads env() exactly once, before the measurement, and never
       again: from then on a probe that measured zero -- because iOS had not
       settled the inset yet, or because it settled after load -- silently
       overrode a perfectly good env() with nothing, and the top chrome sat
       under the clock for the rest of the session with no way to recover.
       max() of the two means neither source can subtract from the other, and a
       late-arriving env() is picked up by the cascade without JS running
       again. max(), still not addition: the inset already carries its own
       clearance, and adding the edge gap on top of it reads as loose. */
    --edge-t: max(var(--edge), var(--safe-t, 0px), env(safe-area-inset-top, 0px));
    --edge-b: max(var(--edge), var(--safe-b, 0px), env(safe-area-inset-bottom, 0px));

    /* The floating row sits where it always sits. Nothing here moves it out of
       the keyboard's way any more: the app takes iOS's default behaviour, which
       is to slide the whole viewport up, and everything goes with it. */
  }

  /* ---- search, at the top ---- */
  .searchdock {
    /* --safe-t is published from JS, because env(safe-area-inset-top) is the
       right answer everywhere except an iOS home-screen app with a translucent
       status bar -- which is exactly where this chrome would end up under the
       clock. See publishInsets(). */
    top: var(--edge-t);
    bottom: auto;
    left: var(--edge);
    right: var(--edge);
    width: auto;
    transform: none;
    /* Above the drawer, which is 26. The dock itself never overlaps it -- the
       search bar rides 12px clear of its top edge the whole way up -- but what
       opens *out* of the dock does: the account menu drops down over the drawer
       when the drawer is at the top, and the suggestion list does the same.
       Below 26 they were being drawn behind it. */
    z-index: 30;
  }
  /* Which way it opens depends on where the bar currently is, and the bar
     moves -- so the direction is set from JS when the list is shown rather than
     fixed here. Default is upward, because that is where the bar spends most of
     its time. */
  .searchdock .suggest {
    top: auto;
    bottom: calc(100% + 10px);
    transform-origin: bottom center;
    animation: riseUp .28s var(--ease-soft);
  }
  .searchdock .suggest.opens-down {
    top: calc(100% + 10px);
    bottom: auto;
    transform-origin: top center;
    animation: riseDown .28s var(--ease-soft);
  }

  /* ---- compose, back under the thumb ---- */
  .mapctl .fab, .fab {
    position: absolute;
    top: auto;
    left: auto;
    right: var(--edge);
    bottom: var(--edge-b);
    /* The lift, on `translate` rather than inside `transform`, and with no
       transition. --sheet-peek is rewritten every frame the sheet moves, so
       those frames *are* the animation; a transition on top is a second
       animation chasing the first, which is why these trailed the sheet all
       the way down. `translate` is its own property, so it composes with the
       press effect's transform instead of replacing it, and it composites
       rather than relaying the button out on every frame the way `bottom` did.
       Only the press is left in the transition below. --btn-lift, not
       --sheet-peek: the row keeps the same gap from the drawer that it keeps
       from the sides, and never less than the home indicator's inset from the
       bottom. drawSheet() works out which of the two applies. */
    translate: 0 calc(-1 * var(--btn-lift, 0px));
    width: var(--tap);
    height: var(--tap);
    border-radius: 50%;
    padding: 0;
    justify-content: center;
    z-index: 22;
    transition: transform .18s var(--ease);
  }
  .fab .fab-icon { width: var(--tap); height: var(--tap); }
  .fab .fab-icon svg { width: 20px; height: 20px; }
  .fab .fab-label { display: none; }

  /* ---- the one remaining map control, opposite it ---- */
  .mapctl {
    top: auto;
    right: auto;
    left: var(--edge);
    bottom: var(--edge-b);
    translate: 0 calc(-1 * var(--btn-lift, 0px));
    flex-direction: column-reverse;        /* the group opens upward */
    align-items: flex-start;
    gap: 10px;
    z-index: 22;
  }
  .mapctl-group { flex-direction: column-reverse; align-items: flex-start; }
  .mapctl .usermenu, .mapctl #authBtn { display: none; }   /* the avatar lives in the search bar */

  /* ---- the feed, as a sheet ---- */
  /* Most of the sheet lives below the fold. Left unclipped it becomes page
     overflow, and then anything that scrolls an element into view -- focusing
     the grab bar, say -- scrolls the entire layout off the top. */
  .stage, .mapwrap { overflow: hidden; }

  .sidebar {
    position: absolute;
    inset: auto 0 0 0;
    top: auto;
    width: 100%;
    /* Exactly the room below where the search bar comes to rest.
       The bar rides the drawer's top edge all the way up and stops in its old
       slot at the top of the screen; the drawer stops with it, one --edge
       below. So the height is not a taste decision -- it is everything under
       the bar's landing place, and stating it that way is what makes the two
       arrive together on any phone. It was 92%, which happens to be the right
       number on a 852pt screen and fifteen pixels wrong on a 667pt one.
       A percentage of .stage, which is the window: vh and dvh both measure the
       safe area on an installed iPhone, so a sheet sized in them comes up short
       of the height it is asking for. */
    height: calc(100% - var(--tap) - var(--edge-t) - var(--edge));
    padding-top: 0;
    /* One flat colour, and that is the whole point.
       A body gradient cannot coexist with a strip that is one flat colour. The
       drawer ran from a light blue-slate at the top to purple at the bottom, so
       the strip could match one end or the other and never both -- and
       collapsed, when only the top few pixels of the drawer are on screen, it
       was the light end sitting directly on the dark one. Measured off the
       phone: drawer #333B57, strip #222140. Open, the seam is at the drawer's
       bottom; collapsed, it is near its top. One colour is the only thing that
       is right in both places, and it is the same declaration the page ground
       reads, so there is nothing left to keep in step.
       Opaque, too. At 90% the last tenth was whatever the map happened to be
       under it, saturated half again by the backdrop filter, so the bottom edge
       ran from #2E274F over empty space to #3E365E over snow. Dropping that
       filter with it is a real saving on a phone -- it was blurring a
       full-screen region every frame the sheet moved, to produce something
       nobody saw.
       What still makes this read as glass is the lit 1px edge along the top and
       the corners it turns, both of which are below. */
    background: var(--drawer-ground);
    border: 1px solid rgba(255, 255, 255, .18);
    border-bottom: 0;
    /* Generous, like the composer -- this is a bubble that stopped rising. */
    border-radius: 28px 28px 0 0;
    /* No glow along the bottom edge. It painted over the background -- box
       shadows do -- so the very bottom of the drawer came out brighter and more
       purple than the flat colour the strip below it was being given, which is
       the mismatch. The lit top edge is what makes this read as glass; the
       bottom of it is against the bottom of the screen where nobody sees a
       glow anyway. */
    box-shadow:
      0 -18px 50px -22px rgba(0, 0, 0, .75),
      inset 0 1px 0 rgba(255, 255, 255, .28);
    overflow: hidden;                      /* so the rounded top actually clips */
    /* Only the bottom needs an inset. The sheet stops short of the status bar
       by design, so anything added at the top is dead space between the grip
       and the first line of the feed -- which is the first thing you see when
       you pull it up. */
    padding-bottom: max(var(--safe-b, 0px), env(safe-area-inset-bottom, 0px));
    transform: translateY(var(--sheet-y, 100%));
    transition: transform .42s cubic-bezier(.22, .82, .24, 1);
    z-index: 26;
  }
  /* The transform is the finger's while dragging and the spring's while
     settling. Either way a CSS transition would be a second opinion. */
  .sidebar.dragging, .sidebar.settling { transition: none; }

  .queryrow { justify-content: flex-start; }
  .queryrow .querychip { flex: 1 1 auto; }

  /* The grip is a handle, not a header. It gets the room it needs to be
     grabbed and not one pixel more -- the feed starts directly under it. */
  .sheet-grab {
    display: flex !important;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 20px;
    position: relative;
    flex: none;
    padding: 0;
    border: 0;
    background: none;
    cursor: grab;
    touch-action: none;                    /* the drag belongs to the sheet */
  }
  .sheet-grab span {
    width: 40px; height: 4px; border-radius: 4px;
    background: var(--ink-3);
    opacity: .5;
  }
  /* Collapsed, the whole lip is the handle rather than a 20px bar inside it.
     There is nothing else down there to press, and the difference between
     hitting the grip and hitting a few pixels under it is not one anybody
     should have to think about. */
  .sidebar:not(.expanded) .sheet-grab { height: var(--sheet-lip); }
  .sheet-grab:active { cursor: grabbing; }
  .sidebar .feedhead { padding-top: 6px; }


  /* ---- one size, stated last ----
     The phone layout grew in layers, and three earlier blocks in this file
     still set 40px and 46px on some of these. Restating it here, at the
     specificity those rules use, is what makes the four floating controls
     actually match rather than nearly match. */
  .searchdock .searchbox,
  .searchdock .brandmini,
  .searchdock .userchip,
  .mapctl .glassbtn,
  .mapctl .fab,
  .fab { height: var(--tap); }

  .searchdock .brandmini,
  .searchdock .userchip,
  .mapctl .glassbtn,
  .mapctl .fab,
  .fab { width: var(--tap); }

  .mapctl .glassbtn svg { width: 19px; height: 19px; }
  .mapctl { gap: 10px; }

  /* The counter had its own corner. The peek row is the counter now. */
  .mapchrome { display: none !important; }

  /* The feed only scrolls once the sheet is open; at peek a drag should move
     the sheet, not the list underneath it. */
  .sidebar:not(.expanded) .feed { overflow: hidden; }

  /* And nothing in the feed can be pressed until the drawer has arrived.
     A swipe that ends over a photo used to open it -- the release lands as a
     click on whatever happened to be under the finger, which is a photo about
     as often as not. The list is inert while the sheet is moving and while it
     is down, and comes back the moment it settles at a detent.
     Taps are unaffected: `dragging` is only set once a drag has passed the
     recognition threshold, so a press that never moved still reaches the card
     underneath it. */
  .sidebar:not(.expanded) .feed,
  .sidebar.dragging .feed,
  .sidebar.settling .feed { pointer-events: none; }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar, .mapctl, .fab { transition: none !important; }
}

/* --- the search bar carries the account, so the corners stay empty --- */
@media (max-width: 900px) {
  .searchdock { display: flex; align-items: center; gap: 8px; }
  /* The field is not in the dock any more; it rides the button row at the
     bottom. What is left up here is the mark and the account chip, which stay
     at the two ends. */
  .searchdock { justify-content: space-between; }
  .searchdock .searchwrap {
    position: fixed;
    /* Exactly the gap the dock leaves either side of it -- so the bar sits in
       the same column of the screen whether it is at the bottom or the top, and
       the whole journey is vertical. */
    left: calc(var(--edge) + var(--tap) + 8px);
    right: calc(var(--edge) + var(--tap) + 8px);
    /* The base rule sets width:100%, which beats `right` on a positioned box
       and let the bar run out past the compose button. */
    width: auto;
    bottom: var(--edge-b);
    /* The same lift as the two buttons beside it. They fade out on the way up
       and this does not: it is going somewhere. */
    translate: 0 calc(-1 * var(--btn-lift, 0px));
    /* With the dock, for the list that opens out of it -- see the note there. */
    z-index: 30;
  }
  /* The list opens against the field, which is now a positioned element, so it
     needs pulling back out to the edges of the screen. */
  .searchdock .suggest {
    left: calc(-1 * (var(--tap) + 8px));
    right: calc(-1 * (var(--tap) + 8px));
  }
  .searchdock .usermenu, .searchdock #authBtn { flex: none; }
  /* The chip is an avatar and nothing else: at this width a name and a
     chevron buy no information and cost the search field its room. */
  .searchdock .userchip {
    width: var(--tap); height: var(--tap); padding: 0; justify-content: center;
    border-radius: 50%;
  }
  .searchdock .userchip .uname, .searchdock .userchip .chev { display: none; }
  /* Exactly the mark's geometry on the other side of the bar: same 36px disc
     in the same 44px chip, so the two ends of the row are mirror images
     rather than two things that are nearly the same. The glow is applied in
     the avatar's own hue, from paintAvatar -- see there for why it is not
     here. */
  .searchdock .avatar {
    width: 36px; height: 36px;
    font-size: 15px;
  }
  .searchdock #authBtn { height: var(--tap); border-radius: var(--r-pill); padding: 0 16px; }
  .searchdock .dropdown { top: calc(100% + 10px); }

  /* Once the sheet is more than half up, the map is no longer the thing you are
     looking at, and floating buttons over a list are just litter. The search bar
     is the exception -- it is not litter, it is on its way back to the top of
     the screen, and it arrives exactly as the drawer stops.
     Fading only. A transform here would replace the one that keeps them locked
     to the sheet, and they would jump as they went. `sheet-high` is set while
     the sheet is still moving, so they are gone by the time it arrives rather
     than blinking off once it has.
     No kb-open case any more. They used to hide when the keyboard came up,
     because nothing down there was reachable -- but the app takes iOS's default
     behaviour now, so the whole viewport slides up and the row goes with it,
     staying exactly where it is relative to everything else. */
  body.sheet-high .fab,
  body.sheet-high .mapctl,
  body.sheet-full .fab,
  body.sheet-full .mapctl {
    opacity: 0; pointer-events: none;
    transition: opacity .22s var(--ease);
  }
}


/* ==========================================================================
   Moderation, and a person's page
   ========================================================================== */
.admin-card {
  width: min(94vw, 620px);
  max-height: min(88vh, 760px);
  gap: 14px;
}
.adminbody {
  min-height: 220px;
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 12px;
}

/* Four numbers, and the two that mean work sit next to the two that are just
   context, so "3 suspended" reads as a queue rather than a statistic. */
.astats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
.astat {
  display: flex; flex-direction: column; gap: 2px;
  padding: 12px 14px; border-radius: var(--r-md);
  background: var(--panel-2); border: 1px solid var(--line);
}
.astat b { font-size: 22px; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.astat span { font-size: 12px; color: var(--ink-2); }
.astat em { font-style: normal; font-size: 11px; color: var(--ink-3); }

/* .segmented.compact is 148px wide, which is right for the two-word theme
   toggle in settings and far too narrow for three section names. The tab strip
   is the panel's primary navigation, so it gets the full width. */
#adminTabs.segmented.compact { width: 100%; flex: none; }
#adminTabs button { flex: 1; height: 30px; padding: 0 12px; font-size: 12.5px; }

.atools { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
#adminFilters.segmented.compact { width: auto; flex: 0 0 auto; }
#adminFilters button { flex: 0 0 auto; padding: 0 12px; }
.asearch {
  flex: 1 1 200px; min-width: 0; height: 34px; padding: 0 12px;
  border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--panel-2); color: var(--ink); font-size: 13.5px; outline: none;
}
.asearch:focus { border-color: transparent; box-shadow: var(--glow); }

.alist { display: flex; flex-direction: column; gap: 6px; }
.arow {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center; gap: 10px;
  padding: 9px 10px; border-radius: var(--r-md);
  border: 1px solid var(--line); background: var(--panel-2);
}
/* A suspended account or a removed Bubl is dimmed and struck through the
   left edge, so a list of forty rows still reads at a glance. */
.arow.is-banned { opacity: .72; border-left: 3px solid #F43F5E; }
.arow-who { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.arow-who b { font-size: 13.5px; }
.arow-who span { font-size: 11.5px; color: var(--ink-3); }
.arow-who em {
  font-style: normal; font-size: 11.5px; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.arow-body { color: var(--ink-2) !important; }
.arow-tags { display: flex; gap: 4px; }
.arow-do { display: flex; gap: 6px; align-items: center; }
.arow-note { font-size: 11.5px; color: var(--ink-3); }

.rolechip {
  display: inline-flex; align-items: center;
  padding: 2px 8px; border-radius: var(--r-pill);
  font-size: 10.5px; font-weight: 700; letter-spacing: .03em; text-transform: uppercase;
  border: 1px solid transparent;
}
.r-admin { background: color-mix(in srgb, var(--primary) 22%, transparent); color: var(--ink); }
.r-moderator { background: color-mix(in srgb, var(--secondary) 20%, transparent); color: var(--ink); }
.r-banned { background: color-mix(in srgb, #F43F5E 18%, transparent); color: #FB7185; }

.btn.tiny, .select.tiny { height: 28px; padding: 0 10px; font-size: 12px; }
/* The two irreversible-feeling actions are the only red on the page. */
.btn.danger {
  border-color: color-mix(in srgb, #F43F5E 42%, transparent);
  color: #FB7185;
}
.btn.danger:hover { background: color-mix(in srgb, #F43F5E 16%, transparent); }

/* --- the author, as a way in ---------------------------------------------- */
.card-who {
  display: inline-flex; align-items: center; gap: 8px;
  border: 0; background: none; padding: 0; cursor: pointer;
  color: inherit; font: inherit; border-radius: var(--r-pill);
  transition: opacity .14s;
}
.card-who:hover { opacity: .72; }
.card-who.inline { gap: 0; }
.card-who:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* The shield sits at the far end of the byline, where it is out of the way
   until you are looking for it. */
.modbtn {
  margin-left: auto; flex: none;
  width: 26px; height: 26px; display: grid; place-items: center;
  border-radius: 8px; border: 1px solid transparent; background: none;
  color: var(--ink-3); cursor: pointer; transition: all .14s;
}
.modbtn svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.7; }
.modbtn:hover { color: var(--ink); border-color: var(--line-2); background: var(--panel-2); }

/* --- a person's page ------------------------------------------------------ */
.profhead {
  display: grid;
  /* Three columns now: the face, who they are, and the follow button. The rows
     under it span all three. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: 10px 12px;
  padding: 14px; margin-bottom: 4px;
  border-radius: var(--r-lg);
  border: 1px solid var(--line); background: var(--panel-2);
}
.card-av.big { width: 44px; height: 44px; font-size: 18px; }
.profwho { min-width: 0; }
.profwho h3 {
  margin: 0; font-size: 16px; letter-spacing: -.01em;
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
}
.profwho p { margin: 2px 0 0; font-size: 12px; color: var(--ink-3); }
.profstats {
  grid-column: 1 / -1;
  display: flex; gap: 16px; font-size: 12.5px; color: var(--ink-2);
}
.profstats b { color: var(--ink); font-variant-numeric: tabular-nums; }
.prof-places {
  grid-column: 1 / -1;
  margin: 0; font-size: 12px; color: var(--ink-3);
}

@media (max-width: 900px) {
  .admin-card { width: 100vw; max-width: 100vw; max-height: 92dvh; border-radius: 0; }
  /* Four columns of controls do not fit a phone, so the actions drop under
     the name rather than being squeezed into a strip too narrow to hit. */
  .arow { grid-template-columns: auto minmax(0, 1fr); }
  .arow-tags, .arow-do { grid-column: 2; }
  .arow-do { justify-content: flex-start; }
}

/* --- the ask dialog and the shield menu ----------------------------------- */
.ask-card { width: min(94vw, 420px); gap: 14px; }
.ask-card .modal-head h2 { font-size: 17px; }
.ask-card .modal-head p { text-align: center; line-height: 1.5; }
.askrow { display: flex; gap: 8px; }
.askrow .btn { flex: 1; height: 38px; justify-content: center; }
.askcheck {
  display: flex; gap: 10px; align-items: flex-start; cursor: pointer;
  padding: 10px 12px; border-radius: var(--r-sm);
  background: var(--panel-2); border: 1px solid var(--line);
}
.askcheck input { margin-top: 2px; accent-color: var(--primary); }
.askcheck span { display: flex; flex-direction: column; gap: 2px; font-size: 13px; }
.askcheck em { font-style: normal; font-size: 11.5px; color: var(--ink-3); }

.modmenu {
  position: fixed; z-index: 80;
  min-width: 190px; padding: 6px;
  background: color-mix(in srgb, var(--panel) 94%, transparent);
  backdrop-filter: blur(22px) saturate(180%);
  border: 1px solid var(--line); border-radius: var(--r-md);
  box-shadow: var(--shadow);
  animation: bublpop .2s var(--ease);
  transform-origin: top right;
  display: flex; flex-direction: column; gap: 2px;
}
.modmenu button {
  text-align: left; padding: 8px 10px; border: 0; border-radius: 7px;
  background: none; color: var(--ink-2); font-size: 13px; font-weight: 600; cursor: pointer;
}
.modmenu button:hover { background: var(--panel-2); color: var(--ink); }
.modmenu button.danger { color: #FB7185; }
.modmenu button.danger:hover { background: color-mix(in srgb, #F43F5E 14%, transparent); }


/* ==========================================================================
   Blowing a Bubl
   ==========================================================================
   The composer is the bubble. A handful burst off the button and one of them
   keeps growing until it is the whole screen -- the same move the sign-in card
   makes, so the two moments in the app that ask you for something arrive the
   same way.

   No heading: the button you pressed said what this is, and a full screen that
   opens with a title telling you what you just chose is furniture.
   ========================================================================== */
.compose-modal { max-width: none; max-height: none; width: 100%; height: 100%; }
.compose-modal::backdrop { background: var(--scrim); backdrop-filter: blur(10px); }

.compose-sheet {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  /* The bubble's own glass, so the sheet is the inside of the thing that just
     rose rather than a panel that replaced it -- with the last stretch of it
     settling onto one flat colour.
     That last part is what makes the iPhone's unpaintable strip disappear. The
     glass is a gradient, so its colour at the bottom edge is not any single
     value you can compute and match: measured on the device it was #383063 at
     the bottom and #494473 two hundred pixels higher. Fading the bottom onto
     --compose-ground, and setting the page ground to the same token, makes the
     two equal by construction rather than by arithmetic that the next change to
     the gradient would quietly invalidate.
     On its own, with no colour appended: --bubl-glass already ends in one, and
     a colour in any layer but the last makes the whole declaration invalid --
     which silently left the sheet with no background at all. */
  background:
    linear-gradient(to bottom, rgba(0, 0, 0, 0) 62%, var(--compose-ground) 100%),
    var(--bubl-glass);
  backdrop-filter: blur(22px) saturate(170%);
  -webkit-backdrop-filter: blur(22px) saturate(170%);
  color: var(--ink);
  /* One movement. There is no separate bubble element any more -- the sheet's
     own clip is the bubble, and it rises and grows at once. */
  animation: bublRise 1.15s cubic-bezier(.46, .03, .32, 1) both;
}
/* The rise and the swell, as one interpolation.
   A bubble used to rise as an element of its own, stop in the middle, and hand
   over to a sheet that swelled from where it had stopped: a pause and a
   handover inside what should be a single gesture. Both ends of this keyframe
   are the same circle -- it starts the size of a bubble with its centre below
   the bottom edge, and arrives centred and larger than the screen -- so
   interpolating moves it up and grows it together, and there is nothing to
   hand over because nothing is ever swapped.

   112%, not more: a circle at 100% already reaches the corners, so every
   percent past that grows off-screen where nobody can see it while still
   costing time on the timeline. */
@keyframes bublRise {
  /* Starting in frame, and small enough to read as a bubble. The centre used
     to be at 124% -- below the bottom edge -- so the only thing ever on screen
     was the top cap of something enormous: a rising dome, not a bubble. It now
     starts as a whole circle near the bottom, which is the thing that is
     supposed to be growing. */
  0%   { clip-path: circle(var(--bubl-r, 64px) at 50% 86%); opacity: 0; }
  8%   { opacity: 1; }
  100% { clip-path: circle(112% at 50% 50%); opacity: 1; }
}
/* The contents are inside the bubble, so they arrive with it rather than being
   there from the first frame at full size behind a small hole. */
.compose-bar, .compose-scroll { animation: composeContent .42s var(--ease) .52s both; }
@keyframes composeContent {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* --- and how it goes ----------------------------------------------------
   It pops. It used to shrink back down into the bubble it grew from, which is
   an un-inflation: bubbles do not do that, they hold their shape until the film
   gives out all at once. So the surface tightens and swells a fraction first,
   then tears -- gone in about a tenth of a second, faster than it swelled, and
   scaling *up* on the way out rather than down. Nothing that pops gets smaller.
   The spray that sells it is drawn in JS, since a ring of droplets needs a ring
   of angles. See popCompose(). */
.compose-modal.closing .compose-sheet {
  animation: bublPop .26s cubic-bezier(.55, 0, .8, 1) both;
}
/* A film holds its shape until it does not.
   The old curve started leaving immediately, so it read as a fade with a bit of
   scale on it. This one spends its first half tightening while still fully
   opaque -- barely moving, which is what makes the second half read as the
   thing letting go -- and then tears: nearly all of the disappearing happens in
   the last 40%, faster than it swelled. */
@keyframes bublPop {
  0%   { transform: scale(1);     opacity: 1; }
  42%  { transform: scale(1.018); opacity: 1; }
  60%  { transform: scale(1.038); opacity: .94; }
  76%  { transform: scale(1.075); opacity: .38; }
  100% { transform: scale(1.14);  opacity: 0; }
}
/* The contents go with it, and slightly ahead of it -- they were inside. */
.compose-modal.closing .compose-bar,
.compose-modal.closing .compose-scroll {
  animation: popContents .17s cubic-bezier(.4, 0, .9, .6) both;
}
@keyframes popContents {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(1.05); }
}
.compose-modal.closing::backdrop { animation: popScrim .34s ease-out both; }
@keyframes popScrim { from { opacity: 1; } to { opacity: 0; } }

/* --- the spray ---
   Droplets off the rim, and one ring going out where the surface used to be.
   Fixed rather than absolute so it sits in the dialog's top layer with the
   sheet; it is a sibling of the sheet, so the sheet's own fade does not take
   the droplets with it. */
.popfield { position: fixed; inset: 0; pointer-events: none; z-index: 3; }
.popfield i {
  position: absolute; border-radius: 50%;
  background:
    radial-gradient(circle at 32% 26%, rgba(255,255,255,.92), rgba(255,255,255,.16) 44%, rgba(255,255,255,0) 68%),
    linear-gradient(150deg, var(--blue-400), var(--purple-500) 72%, var(--magenta));
  box-shadow: inset 0 1px 3px rgba(255,255,255,.6);
  animation: popDrop .38s cubic-bezier(.08, .82, .3, 1) both;
}
/* Thrown, not drifting. Almost all the travel is in the first third -- that is
   what a burst does -- and they are gone before they stop. */
@keyframes popDrop {
  0%   { opacity: 0; transform: translate(0, 0) scale(.42); }
  10%  { opacity: 1; transform: translate(calc(var(--dx) * .3), calc(var(--dy) * .3)) scale(1.12); }
  55%  { opacity: .55; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(.2); }
}
/* The shockwave: a thin ring where the film was, on its way out. */
.popfield b {
  position: absolute; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--primary) 70%, white 30%);
  animation: popRing .34s cubic-bezier(.2, .7, .3, 1) both;
}
@keyframes popRing {
  from { width: calc(var(--r0) * 2); height: calc(var(--r0) * 2);
         margin: calc(var(--r0) * -1) 0 0 calc(var(--r0) * -1);
         opacity: .85; border-width: 3px; }
  to   { width: calc(var(--r1) * 2); height: calc(var(--r1) * 2);
         margin: calc(var(--r1) * -1) 0 0 calc(var(--r1) * -1);
         opacity: 0; border-width: 1px; }
}

/* The bubble's surface while it rises.
   Same recipe as .welcome-hero, because it is meant to read as the same object:
   the two moments in the app that ask you for something should arrive the same
   way. Positioned and sized from JS so it tracks the sheet's clip circle
   exactly -- the circle is what grows, and this is what makes the circle look
   like a bubble rather than a hole. */
.compose-bubble {
  position: fixed; left: 50%; top: 86%;
  width: 0; height: 0;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  pointer-events: none;
  z-index: 3;
  opacity: 0;
  border: 1px solid rgba(255,255,255,.22);
  background:
    linear-gradient(150deg, rgba(255,255,255,.14) 0%, rgba(255,255,255,.03) 46%, rgba(255,255,255,0) 62%),
    linear-gradient(150deg,
      color-mix(in srgb, var(--primary) 22%, transparent),
      color-mix(in srgb, var(--secondary) 18%, transparent));
  box-shadow:
    0 40px 90px -34px rgba(2, 6, 20, .9),
    inset 0 2px 0 rgba(255,255,255,.30),
    inset 0 -16px 34px -18px color-mix(in srgb, var(--secondary) 75%, transparent);
  will-change: width, height, top, opacity;
}
.compose-modal.closing .compose-bubble { display: none; }

/* --- the ones that come up with it ---
   A few small bubbles rising alongside the sheet's own circle, from the same
   place. They are decoration and they are also what makes the growing clip read
   as having come from somewhere.
   (The single large `.riser` that used to lead them is gone: the sheet's clip
   is the bubble now, so there is no second element to keep in step.) */
.burst { position: fixed; inset: 0; pointer-events: none; z-index: 90; }
/* The same bubbles the sign-in page raises -- .wb's recipe exactly, down to the
   specular placement and the three shadows, so the two moments match. */
.burst i {
  position: absolute; border-radius: 50%;
  background:
    radial-gradient(circle at 30% 24%, rgba(255,255,255,.72), rgba(255,255,255,.10) 44%, rgba(255,255,255,0) 64%),
    linear-gradient(150deg, var(--blue-400), var(--purple-500) 72%, var(--magenta));
  box-shadow:
    inset 0 2px 6px rgba(255,255,255,.4),
    inset 0 -10px 22px -12px color-mix(in srgb, var(--secondary) 80%, transparent),
    0 10px 34px -14px color-mix(in srgb, var(--primary) 70%, transparent);
  animation: burstRise 1.15s cubic-bezier(.32, .5, .34, 1) both;
}
/* They rise and go, rather than spraying outward. A splash is what something
   makes on the way in; these are coming up with the big one. */
@keyframes burstRise {
  0%   { opacity: 0; transform: translate(0, 0) scale(.45); }
  14%  { opacity: .92; }
  70%  { opacity: .7; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(1.05); }
}

/* --- the sheet itself ---------------------------------------------------- */
.compose-bar {
  flex: none;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  padding: max(12px, env(safe-area-inset-top, 0px)) 14px 12px;
}
/* On a dark panel --ink-3 is a quiet grey; on this glass it is barely there.
   Everything in here goes up a step. */
.compose-bar #composeWhere {
  margin: 0; min-width: 0; text-align: center;
  font-size: 12.5px; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.compose-bar .btn { height: 38px; padding: 0 20px; border-radius: var(--r-pill); }
/* The confirm button: the same bubble as the close, in the colour that means
   go. Two bubbles in the two top corners, one to leave and one to finish. */
.compose-bar .iconbtn.go {
  border-color: color-mix(in srgb, var(--primary) 62%, white 12%);
  background:
    radial-gradient(circle at 32% 26%, rgba(255,255,255,.42), rgba(255,255,255,.08) 54%),
    var(--grad);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.5),
    inset 0 -7px 14px -8px rgba(30, 10, 70, .8),
    0 6px 18px -8px color-mix(in srgb, var(--primary) 85%, transparent);
  color: #fff;
}
.compose-bar .iconbtn.go:hover {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.62),
    inset 0 -7px 14px -8px rgba(30, 10, 70, .85),
    0 8px 24px -8px color-mix(in srgb, var(--primary) 95%, transparent);
}
/* A tick does not want to spin the way the close cross does. */
.compose-bar .iconbtn.go:hover svg { transform: none; }
.compose-bar .iconbtn svg { width: 19px; height: 19px; fill: none; stroke: currentColor;
  stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.compose-bar .iconbtn .waiting { display: none; }
/* Waiting on a location: the tick steps aside for a ring that turns, rather
   than the label being overwritten with words the icon cannot hold. */
.compose-bar .iconbtn.busy .tick { display: none; }
.compose-bar .iconbtn.busy .waiting {
  display: block;
  stroke-dasharray: 14 40;
  animation: goSpin .9s linear infinite;
}
@keyframes goSpin { to { transform: rotate(360deg); } }
/* The pill in the middle is the location switch. It reads as pressable and
   changes with what it is set to, rather than describing a state some other
   control changes. Disabled when there is nothing to switch to -- while the
   location is still being looked for, or when it was never found. */
.compose-bar #composeWhere {
  border: 1px solid transparent;
  background: none;
  color: var(--ink-2);
  font: inherit;
  cursor: pointer;
  transition: background .16s, border-color .16s, color .16s;
}
.compose-bar #composeWhere:not(:disabled):hover {
  border-color: color-mix(in srgb, var(--primary) 45%, transparent);
  color: var(--ink);
}
.compose-bar #composeWhere:not(:disabled):active { transform: scale(.98); }
/* Set to the reader's own location: the state worth showing, so it is lit. */
.compose-bar #composeWhere[aria-pressed="true"] {
  border-color: color-mix(in srgb, var(--primary) 38%, transparent);
  color: var(--ink);
}
.compose-bar #composeWhere:disabled { cursor: default; }

/* Round, like everything else in a composer made of bubbles -- and this one is
   a bubble, since pressing it pops the whole thing. Glass with a specular cap,
   breathing gently on its own so it reads as something with air in it rather
   than a dismiss control.
   The breathing is on `scale` and the press is on `transform`, which are
   separate properties that compose: put both on `transform` and the running
   animation wins, so the button would not squash when pressed. */
.compose-bar .iconbtn {
  position: relative;
  width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid rgba(255, 255, 255, .26);
  background:
    radial-gradient(circle at 32% 26%, rgba(255,255,255,.30), rgba(255,255,255,.06) 52%),
    linear-gradient(150deg, rgba(255,255,255,.16), rgba(255,255,255,.04));
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.42),
    inset 0 -6px 12px -8px color-mix(in srgb, var(--secondary) 80%, transparent),
    0 4px 14px -8px rgba(2, 6, 20, .9);
  color: var(--ink);
  transition: background .16s, transform .2s var(--ease), border-color .16s, box-shadow .2s;
}
/* The highlight that makes a disc read as a sphere. */
.compose-bar .iconbtn::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(54% 38% at 34% 20%, rgba(255,255,255,.52), rgba(255,255,255,0) 64%);
}
/* The idle pulse is gone. It read as the buttons popping the whole time the
   composer was open, and these two are meant to be the same objects as the home
   button and the account chip on the map -- which sit still. The press, the
   hover and the specular cap are what make them feel like glass; a thing that
   never stops moving is just movement. */
.compose-bar .iconbtn:hover {
  border-color: rgba(255, 255, 255, .46);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.55),
    inset 0 -6px 12px -8px color-mix(in srgb, var(--secondary) 90%, transparent),
    0 6px 20px -8px color-mix(in srgb, var(--primary) 70%, transparent);
}
/* Squashes like something with give in it, then springs back. */
.compose-bar .iconbtn:active { transform: scale(.84); transition-duration: .06s; }
.compose-bar .iconbtn svg { transition: transform .2s var(--ease); }
.compose-bar .iconbtn:hover svg { transform: rotate(90deg); }

/* The way out is where the way home is.
   The home button sits at (--edge, --edge-t) and is one --tap across; this
   takes exactly those three values rather than numbers that happen to match, so
   that corner means one thing -- press here to get back out -- whether the
   composer is open or not, and neither can be moved without the other.
   It has to sit below the composer's own rules rather than up with the rest of
   the phone layout: same specificity, so whichever comes last in the file wins,
   and up there it silently lost. */
@media (max-width: 900px) {
  .compose-bar .iconbtn {
    position: fixed;
    top: var(--edge-t);
    width: var(--tap); height: var(--tap);
    z-index: 4;
  }
  #composeClose { left: var(--edge); }
  /* And the tick where the account chip is, which is the same corner, the same
     size and the same inset -- so both ends of that row are a button wherever
     you are in the app. */
  #composeSubmit { right: var(--edge); }
  /* Both are out of the grid, so what is left is the line between them.
     8px, not 10: the same gap the search dock puts between its own three
     pieces, which is what makes the middle of this row start where the middle
     of that one starts. */
  .compose-bar {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--edge-t) calc(var(--edge) + var(--tap) + 8px) 12px;
    min-height: var(--tap);
  }
  /* The line in the middle is the search bar's twin.
     The map's top row is a button, a pill, a button; this row was a button,
     some loose text, a button -- the same three positions but only two of them
     shaped alike, so the eye had to re-find the middle of it. Same height, same
     radius, same glass, arrived at from the same tokens rather than by copying
     numbers across. */
  .compose-bar #composeWhere {
    /* Block with a line-height, not flex: the base rule ellipsises this when it
       is too long, and text-overflow does not apply to a flex item. */
    height: var(--tap); line-height: var(--tap); padding: 0 16px;
    border-radius: var(--r-pill);
    border: 1px solid rgba(255, 255, 255, .20);
    background:
      linear-gradient(150deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.02) 46%, rgba(255,255,255,0) 60%),
      linear-gradient(150deg, color-mix(in srgb, var(--primary) 15%, transparent),
                              color-mix(in srgb, var(--secondary) 15%, transparent)),
      color-mix(in srgb, var(--panel) 82%, transparent);
    box-shadow:
      0 12px 34px -16px rgba(4, 8, 22, .8),
      inset 0 1px 0 rgba(255, 255, 255, .28);
    /* The same size the search field sets its own text at. */
    font-size: 13px;
  }
}

.compose-scroll {
  flex: 1; min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: flex; flex-direction: column; gap: 16px;
  padding: 16px 16px calc(28px + env(safe-area-inset-bottom, 0px));
}

/* The message is a field, and has to look like one.
   It was borderless and transparent, which on a full-screen sheet made it
   indistinguishable from the sheet -- there was nothing to say where you could
   type or how much room you had. A boundary is not decoration here: WCAG 1.4.11
   asks for 3:1 against the surface behind it for exactly this, the visual
   information needed to identify a control. */
.composer-field { gap: 8px; }
.composer-box {
  border: 1px solid rgba(255, 255, 255, .30);
  border-radius: var(--r-md);
  background: rgba(12, 16, 32, .32);
  padding: 12px 14px;
  transition: border-color .16s, box-shadow .16s, background .16s;
}
/* Focus on the box, since the box is what reads as the control. A ring rather
   than a colour change alone -- 2.4.7 wants focus visible, and a border that
   merely brightens is easy to miss on glass. */
.composer-box:focus-within {
  border-color: color-mix(in srgb, var(--primary) 78%, white 22%);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 34%, transparent);
  background: rgba(12, 16, 32, .44);
}
.compose-scroll .composer-field > label {
  font-size: 13px; font-weight: 700; letter-spacing: -.01em; color: var(--ink);
}

#composeBody {
  width: 100%; min-height: 152px; resize: none;
  padding: 0; border: 0; background: none; color: var(--ink);
  font: inherit; font-size: 17px; line-height: 1.5; outline: none;
}
#composeBody::placeholder { color: color-mix(in srgb, var(--ink) 62%, transparent); }

.compose-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.toolbtn {
  display: inline-flex; align-items: center; gap: 7px;
  height: 36px; padding: 0 13px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--panel-2); color: var(--ink-2);
  font-size: 13px; font-weight: 600; cursor: pointer;
  transition: border-color .15s, color .15s, transform .15s var(--ease);
}
.toolbtn svg { width: 16px; height: 16px; }
.toolbtn:hover { color: var(--ink); border-color: var(--line-2); }
.toolbtn:active { transform: scale(.94); }
.toolbtn[disabled] { opacity: .45; cursor: default; }
.compose-tools .counter {
  position: static; margin-left: auto; font-family: var(--mono); font-size: 11.5px;
}

.compose-media { display: flex; align-items: center; gap: 12px; }
.media-thumb {
  width: 84px; height: 84px; border-radius: var(--r-md); overflow: hidden;
  background: var(--panel-2); border: 1px solid var(--line); flex: none;
}
.media-thumb img, .media-thumb video { width: 100%; height: 100%; object-fit: cover; }

/* A hashtag typed into the body reads as one, so it is obvious the word is
   doing something. */
.compose-scroll .tagpick { max-height: none; }
.compose-scroll .tagbox { background: rgba(12, 16, 32, .28); }
.compose-scroll .tagsuggest {
  background: color-mix(in srgb, #1B1733 88%, transparent);
  backdrop-filter: blur(14px);
}
.compose-scroll .tagopt:hover, .compose-scroll .tagopt.active {
  background: rgba(255, 255, 255, .13);
}
/* The rest of the copy, lifted to read on the glass rather than on a panel. */
.compose-scroll .field > span { color: var(--ink); }
.compose-scroll .field > span em { color: color-mix(in srgb, var(--ink) 68%, transparent); }
.compose-scroll .hint { color: color-mix(in srgb, var(--ink) 66%, transparent); }
.compose-scroll .counter { color: color-mix(in srgb, var(--ink) 70%, transparent); }
/* The count is quiet until it is worth reading. Colour is not the only signal --
   the number itself changes, and it turns to how many are left once that is the
   useful figure, so this does not depend on telling the two hues apart. */
.counter.warn { color: #FFC46B; font-weight: 700; }
.counter.over { color: #FF8A8A; font-weight: 700; }
/* Two separate reasons a field can look wrong -- too long, and rejected on
   submit -- each owning its own class, so neither clears the other's state. */
.composer-box.over-limit,
.composer-box.invalid,
.composer-box.over-limit:focus-within,
.composer-box.invalid:focus-within {
  border-color: #FF8A8A;
  box-shadow: 0 0 0 3px rgba(255, 138, 138, .26);
}
.compose-scroll .toolbtn {
  color: var(--ink);
  border-color: rgba(255, 255, 255, .20);
  background: rgba(255, 255, 255, .10);
}
.compose-scroll .toolbtn:hover { background: rgba(255, 255, 255, .18); }
.compose-scroll .tagpick button {
  color: var(--ink);
  border-color: rgba(255, 255, 255, .18);
  background: rgba(255, 255, 255, .09);
}
.compose-scroll .tagpick button.on { background: var(--grad); border-color: transparent; }

@media (min-width: 901px) {
  /* The dialog fills the viewport so the backdrop does, and then has to place
     the sheet itself -- a <dialog> only centres its own box, and its box is now
     the screen. Scoped to [open] because `display` is what a dialog uses to
     hide itself, and a grid is not display:none. */
  .compose-modal[open] { display: grid; place-items: center; }
  /* On a desktop a full screen for four fields is too much room, so it stops
     at a tall column -- still one surface, still scrolling, still the bubble. */
  .compose-sheet {
    inset: auto; position: relative;
    width: min(92vw, 560px); height: min(88vh, 760px);
    /* Generous, because this is a bubble that stopped growing, not a dialog. */
    --sheet-radius: 34px;
    border-radius: var(--sheet-radius);
    border: 1px solid rgba(255, 255, 255, .18);
    box-shadow:
      0 40px 90px -34px rgba(2, 6, 20, .9),
      inset 0 2px 0 rgba(255,255,255,.28),
      inset 0 -16px 34px -18px color-mix(in srgb, var(--secondary) 65%, transparent);
    overflow: hidden;
  }
  .compose-bar { padding: 14px 16px; }
}

@media (prefers-reduced-motion: reduce) {
  .compose-sheet, .compose-modal.closing .compose-sheet { animation: none !important; }
  .compose-bar, .compose-scroll { animation: none !important; }
  .compose-bar .iconbtn { animation: none; }
  .burst, .popfield { display: none; }
}


/* ==========================================================================
   The feed's width, on a desktop
   ==========================================================================
   A fixed 392px is right for reading a column of posts and wrong for a 34"
   screen, where there is room to see three photographs at once and no reason
   not to. So the edge is draggable, the width is remembered, and the default is
   exactly what it was.

   The whole thing is one custom property, which is why the map padding, the
   search dock and the floating controls all follow it without being told.
   ========================================================================== */
.sidebar-grip { display: none; }

@media (min-width: 901px) {
  .sidebar-grip {
    display: block;
    position: absolute;
    left: -5px; top: 0; bottom: 0;
    width: 11px;
    cursor: col-resize;
    z-index: 22;
    /* No hit area smaller than this is findable, and none larger is
       accidentally grabbed instead of the feed behind it. */
    background: none;
    border: 0;
  }
  /* A hairline that appears under the pointer, so the edge announces itself
     without drawing a permanent seam down the middle of the app. */
  .sidebar-grip::after {
    content: '';
    position: absolute; left: 4px; top: 0; bottom: 0; width: 3px;
    border-radius: 3px;
    background: var(--grad);
    opacity: 0;
    transition: opacity .16s var(--ease);
  }
  .sidebar-grip:hover::after,
  .sidebar-grip:focus-visible::after,
  body.sidebar-resizing .sidebar-grip::after { opacity: .85; }
  .sidebar-grip:focus-visible { outline: none; }

  /* While dragging, nothing else should be selecting text or catching the
     pointer -- least of all the map, which would pan. */
  body.sidebar-resizing { cursor: col-resize; user-select: none; }
  body.sidebar-resizing .mapwrap { pointer-events: none; }
  /* And the feed must not animate its way to each new width, thirty times a
     second. */
  body.sidebar-resizing .sidebar { transition: none; }
}
